~jstys-z/helioviewer.org/client5

« back to all changes in this revision

Viewing changes to api/src/Image/SubFieldImage.php

  • Committer: Keith Hughitt
  • Date: 2012-07-26 21:26:16 UTC
  • Revision ID: keith.hughitt@nasa.gov-20120726212616-hos4e9yyun3ebvlq
Added conky script to display Helioviewer.org status information

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
 
3
/**
 
4
 * Image_SubFieldImage class definition
 
5
 *
 
6
 * PHP version 5
 
7
 *
 
8
 * @category Image
 
9
 * @package  Helioviewer
 
10
 * @author   Keith Hughitt <keith.hughitt@nasa.gov>
 
11
 * @author   Jaclyn Beck <jaclyn.r.beck@gmail.com>
 
12
 * @license  http://www.mozilla.org/MPL/MPL-1.1.html Mozilla Public License 1.1
 
13
 * @link     http://launchpad.net/helioviewer.org
 
14
 */
 
15
 
 
16
/**
 
17
 * Represents a JPEG 2000 sub-field image.
 
18
 *
 
19
 * The SubFieldImage class provides functionality for outputting a sub-section of a JPEG 2000
 
20
 * image (possibly the entire image) in a common format such as JPEG or PNG. Color tables and alpha
 
21
 * masks can also be applied at this level.
 
22
 * 
 
23
 * @TODO Switch to using a single "optional" array passed to initialize for color table, padding, etc?
 
24
 *
 
25
 * @category Image
 
26
 * @package  Helioviewer
 
27
 * @author   Keith Hughitt <keith.hughitt@nasa.gov>
 
28
 * @author   Jaclyn Beck <jaclyn.r.beck@gmail.com>
 
29
 * @license  http://www.mozilla.org/MPL/MPL-1.1.html Mozilla Public License 1.1
 
30
 * @link     http://launchpad.net/helioviewer.org
 
31
 */
 
32
class Image_SubFieldImage
 
33
{
 
34
    protected $jp2;
 
35
    protected $image;
 
36
    protected $outputFile;
 
37
    protected $roi;
 
38
    protected $imageSubRegion;
 
39
    protected $desiredScale;
 
40
    protected $desiredToActual;
 
41
    protected $scaleFactor;
 
42
    protected $subfieldWidth;
 
43
    protected $subfieldHeight;
 
44
    protected $subfieldRelWidth;
 
45
    protected $subfieldRelHeight;
 
46
    protected $jp2Width; 
 
47
    protected $jp2Height;
 
48
    protected $jp2RelWidth;
 
49
    protected $jp2RelHeight;
 
50
    protected $offsetX;
 
51
    protected $offsetY;
 
52
    protected $options;
 
53
 
 
54
    /**
 
55
     * Creates an Image_SubFieldImage instance
 
56
     *
 
57
     * @param string $jp2          Original JP2 image from which the subfield should be derrived
 
58
     * @param array      $roi          Subfield region of interest
 
59
     * @param string $outputFile   Location to output the subfield image to
 
60
     * @param float  $offsetX      Offset of the center of the sun from the center of the image on the x-axis
 
61
     * @param float  $offsetY      Offset of the center of the sun from the center of the image on the y-axis
 
62
     *
 
63
     * @TODO: Add optional parameter "noResize" or something similar to allow return images
 
64
     * which represent the same region, but may be at a different scale (e.g. tiles). The normal
 
65
     * case (for movies, etc) would be to resize to the requested scale on the server-side.
 
66
     *
 
67
     * @TODO: Rename "jp2scale" syntax to "nativeImageScale" to get away from JP2-specific terminology
 
68
     *        ("desiredScale" -> "desiredImageScale" or "requestedImageScale")
 
69
      */
 
70
    public function __construct($jp2, $roi, $outputFile, $offsetX, $offsetY, $options)
 
71
    {
 
72
        $this->outputFile  = $outputFile;
 
73
        $this->jp2         = $jp2;
 
74
        $this->roi         = $roi;
 
75
        
 
76
        // Default settings
 
77
        $defaults = array(
 
78
            "bitdepth"    => 8,
 
79
            "compress"    => true,
 
80
            "interlace"   => true,
 
81
            "opacity"     => 100,
 
82
            "rescale"     => IMagick::FILTER_TRIANGLE
 
83
        );
 
84
        
 
85
        $this->imageOptions = array_replace($defaults, $options);
 
86
        
 
87
        // Source image dimensions
 
88
        $jp2Width  = $jp2->getWidth();
 
89
        $jp2Height = $jp2->getHeight();
 
90
        $jp2Scale  = $jp2->getScale(); 
 
91
 
 
92
        // Convert region of interest from arc-seconds to pixels
 
93
        $this->imageSubRegion = $roi->getImageSubRegion($jp2Width, $jp2Height, $jp2Scale, $offsetX, $offsetY);
 
94
        
 
95
        // Desired image scale (normalized to 1au)
 
96
        $this->desiredScale    = $roi->imageScale();
 
97
        
 
98
        $this->desiredToActual = $this->desiredScale / $jp2->getScale();
 
99
        $this->scaleFactor     = log($this->desiredToActual, 2);
 
100
        $this->reduce          = max(0, floor($this->scaleFactor));
 
101
        
 
102
        $this->subfieldWidth  = $this->imageSubRegion["right"]  - $this->imageSubRegion["left"];
 
103
        $this->subfieldHeight = $this->imageSubRegion["bottom"] - $this->imageSubRegion["top"];
 
104
 
 
105
        $this->subfieldRelWidth  = $this->subfieldWidth  / $this->desiredToActual;
 
106
        $this->subfieldRelHeight = $this->subfieldHeight / $this->desiredToActual;
 
107
        
 
108
        $this->jp2RelWidth  = $jp2Width  / $this->desiredToActual;
 
109
        $this->jp2RelHeight = $jp2Height / $this->desiredToActual;
 
110
        
 
111
        $this->offsetX = $offsetX;
 
112
        $this->offsetY = $offsetY;
 
113
    }
 
114
 
 
115
    /**
 
116
     * Sets parameters (gravity and size) for any padding which should be applied to extracted subfield image
 
117
     * 
 
118
     * @param array $padding An associative array containing the width,height, and gravity values to use during padding.
 
119
     * 
 
120
     * @return void
 
121
     */
 
122
    public function setPadding($padding) 
 
123
    { 
 
124
        $this->padding = $padding;
 
125
        //Allow browser to rescale tiles which are not larger than the requested size
 
126
        /*if (!($padding && ($padding['width'] > $this->width))) {
 
127
            $this->setSkipResize(true);
 
128
        }*/
 
129
    }
 
130
    
 
131
    /**
 
132
     * Saves the new filepath
 
133
     * 
 
134
     * @param string $filepath The new file path to the image
 
135
     * 
 
136
     * @return void
 
137
     */
 
138
    public function setNewFilePath($filepath)
 
139
    {
 
140
        $this->outputFile = $filepath;
 
141
    }
 
142
    
 
143
    /**
 
144
     * Gets the SubfieldImage's output file
 
145
     * 
 
146
     * @return string outputFile
 
147
     */
 
148
    public function outputFile() 
 
149
    {
 
150
        return $this->outputFile;
 
151
    }
 
152
 
 
153
    /**
 
154
     * Getters that are needed for determining padding, as they must be accessed from Tile or ImageLayer classes.
 
155
     * 
 
156
     * @return int jp2RelWidth
 
157
     */
 
158
    public function jp2RelWidth() 
 
159
    {
 
160
        return $this->jp2RelWidth;
 
161
    }
 
162
    
 
163
    /**
 
164
     * Gets the jp2 image's relative height
 
165
     * 
 
166
     * @return int jp2RelHeight
 
167
     */
 
168
    public function jp2RelHeight() 
 
169
    {
 
170
        return $this->jp2RelHeight;
 
171
    }
 
172
    
 
173
    /**
 
174
     * Gets the extracted image's relative width
 
175
     * 
 
176
     * @return int subfieldRelWidth
 
177
     */
 
178
    public function subfieldRelWidth()
 
179
    {
 
180
        return $this->subfieldRelWidth;
 
181
    }
 
182
    
 
183
    /**
 
184
     * Gets the extracted image's relative height
 
185
     * 
 
186
     * @return int subfieldRelHeight
 
187
     */    
 
188
    public function subfieldRelHeight()
 
189
    {
 
190
        return $this->subfieldRelHeight;
 
191
    }
 
192
    
 
193
    /**
 
194
     * Builds the requested subfield image.
 
195
     *
 
196
     * Normalizing request & native image scales:
 
197
     * 
 
198
     * When comparing the requested or "desired" image scale for the subfield image to the native or "actual" image 
 
199
     * scale of the source image, it is convenient to create a variable called "desiredToActual" which represents
 
200
     * the ratio of the desired scale to the actual scale.
 
201
     * 
 
202
     * There are three possible cases which may occur:
 
203
     * 
 
204
     *     1) desiredToActual = 1
 
205
     *        
 
206
     *          In this case the subfield requested is at the natural image scale. No resizing is necessary.
 
207
     *     
 
208
     *     2) desiredToActual < 1
 
209
     * 
 
210
     *          The subfield requested is at a lower image scale (HIGHER quality) than the source JP2.
 
211
     *          
 
212
     *     3) desiredToActual > 1
 
213
     *     
 
214
     *          The subfield requested is at a higher image scale (LOWER quality) than the source JP2.
 
215
     *         
 
216
     * @TODO: Normalize quality scale. 
 
217
     * @TODO: Create a cleanup array with names of files to be wiped after processing is complete?
 
218
     * @TODO: Move generation of intermediate file to separate method
 
219
     *
 
220
     * @return void
 
221
     */
 
222
    protected function build()
 
223
    {
 
224
        /* 
 
225
         * Need to extend the time limit that writeImage() can use so it 
 
226
         * doesn't throw fatal errors when movie frames are being made. 
 
227
         * It seems that even if this particular instance of writeImage 
 
228
         * doesn't take the  full time frame, if several instances of it are 
 
229
         * running PHP will complain.
 
230
         */
 
231
        set_time_limit(600);
 
232
        
 
233
        try {
 
234
            // Choose extension to convert source image to
 
235
            if ($this->options['palettedJP2']) {
 
236
                $extension = ".bmp";     
 
237
            } else {
 
238
                $extension = ".pgm";
 
239
            }
 
240
            $input = substr($this->outputFile, 0, -4) . rand() . $extension;
 
241
            
 
242
            // Extract region (PGM)
 
243
            $this->jp2->extractRegion($input, $this->imageSubRegion, $this->reduce);
 
244
 
 
245
            // Apply colormap if needed
 
246
            if (!$this->options['palettedJP2']) {
 
247
                
 
248
                // Convert to GD-readable format
 
249
                $grayscale = new IMagick($input);
 
250
                $grayscale->setImageFormat('PNG');
 
251
                $grayscale->setImageType(IMagick::IMGTYPE_GRAYSCALE); 
 
252
                $grayscale->setImageDepth(8);
 
253
                $grayscale->setImageCompressionQuality(10); // Fastest PNG compression setting
 
254
                
 
255
                $grayscaleString = $grayscale->getimageblob();
 
256
                
 
257
                // Assume that no color table is needed
 
258
                $coloredImage = $grayscale;
 
259
                
 
260
                // Apply color table if one exists
 
261
                if ($this->colorTable) {
 
262
                    $grayscale->destroy();
 
263
                    $coloredImageString = $this->setColorPalette($grayscaleString);
 
264
                
 
265
                    $coloredImage = new IMagick();        
 
266
                    $coloredImage->readimageblob($coloredImageString);
 
267
                }                
 
268
            } else {
 
269
                $coloredImage = new IMagick($input);
 
270
            }
 
271
            
 
272
            // Set alpha channel for images with transparent components
 
273
            $this->setAlphaChannel($coloredImage);
 
274
            
 
275
            // Apply compression and interlacing
 
276
            $this->compressImage($coloredImage);
 
277
                        
 
278
            // Resize extracted image to correct size before padding.
 
279
            $rescaleBlurFactor =  0.6;
 
280
            
 
281
            $coloredImage->resizeImage(
 
282
                round($this->subfieldRelWidth), round($this->subfieldRelHeight), 
 
283
                $this->imageOptions['rescale'], $rescaleBlurFactor
 
284
            );
 
285
            $coloredImage->setImageBackgroundColor('transparent');
 
286
 
 
287
            // Places the current image on a larger field of black if the final 
 
288
            // image is larger than this one
 
289
                        $imagickVersion = $coloredImage->getVersion();
 
290
 
 
291
                        if ($imagickVersion['versionNumber'] > IMAGE_MAGICK_662_VERSION_NUM) {
 
292
                            // ImageMagick 6.6.2-6 and higher 
 
293
                            // Problematic change occurred in revision 6.6.4-2
 
294
                            // See: http://www.imagemagick.org/script/changelog.php
 
295
                $coloredImage->extentImage(
 
296
                    $this->padding['width'], $this->padding['height'],
 
297
                    $this->padding['offsetX'], $this->padding['offsetY']
 
298
                );                          
 
299
                        } else {
 
300
                            // Imagick 3.0 and lower
 
301
                $coloredImage->extentImage(
 
302
                    $this->padding['width'], $this->padding['height'],
 
303
                    -$this->padding['offsetX'], -$this->padding['offsetY']
 
304
                );
 
305
                        }
 
306
            
 
307
            $this->image = $coloredImage;
 
308
            
 
309
            // Check for PGM before deleting just in case another process already removed it 
 
310
            if (file_exists($input)) {
 
311
                unlink($input);
 
312
            }
 
313
 
 
314
        } catch(Exception $e) {
 
315
            // Clean-up intermediate files
 
316
            $this->_abort($this->outputFile);
 
317
            throw $e;
 
318
        }
 
319
    }
 
320
 
 
321
    /**
 
322
     * Returns the IMagick instance assocated with the image
 
323
     */
 
324
    public function getIMagickImage()
 
325
    {
 
326
        return $this->image;
 
327
    }
 
328
 
 
329
    /**
 
330
     * Saves the file using the specified output filename
 
331
     */
 
332
    public function save()
 
333
    {
 
334
        if (!file_exists($this->outputFile) && !is_null($this->image)) {
 
335
            $this->image->writeImage($this->outputFile);
 
336
        }
 
337
    }
 
338
    
 
339
    /**
 
340
     * Sets compression for images that are not ImageLayers
 
341
     * 
 
342
     * @param Object &$imagickImage An initialized Imagick object
 
343
     * 
 
344
     * @return void
 
345
     */
 
346
    protected function compressImage(&$imagickImage)
 
347
    {
 
348
        // Get extension
 
349
        $parts = explode(".", $this->outputFile);
 
350
        $extension = end($parts);
 
351
 
 
352
        // Apply compression based on image type for those formats that support it
 
353
        if ($extension === "png") {
 
354
            // Compression type
 
355
            $imagickImage->setImageCompression(IMagick::COMPRESSION_LZW);
 
356
            
 
357
            // Compression quality
 
358
            $quality = $this->imageOptions['compress'] ? PNG_HIGH_COMPRESSION : PNG_LOW_COMPRESSION;
 
359
            $imagickImage->setImageCompressionQuality($quality);
 
360
            
 
361
            // Interlacing
 
362
            if ($this->imageOptions['interlace']) {
 
363
                $imagickImage->setInterlaceScheme(IMagick::INTERLACE_PLANE);
 
364
            }
 
365
        } elseif ($extension === "jpg") {
 
366
            // Compression type
 
367
            $imagickImage->setImageCompression(IMagick::COMPRESSION_JPEG);
 
368
 
 
369
            // Compression quality
 
370
            $quality = $this->imageOptions['compress'] ? JPG_HIGH_COMPRESSION : JPG_LOW_COMPRESSION;             
 
371
            $imagickImage->setImageCompressionQuality($quality);
 
372
            
 
373
            // Interlacing
 
374
            if ($this->imageOptions['interlace']) {
 
375
                $imagickImage->setInterlaceScheme(IMagick::INTERLACE_LINE);
 
376
            }
 
377
        }
 
378
 
 
379
        $imagickImage->setImageDepth($this->imageOptions['bitdepth']);
 
380
    }
 
381
    
 
382
    /**
 
383
     * Figures out where the extracted image lies inside the final image
 
384
     * if the final image is larger.
 
385
     * 
 
386
     * @param Array $roi   The region of interest in arcseconds of the final image.
 
387
     * 
 
388
     * @return array with padding
 
389
     */
 
390
    public function computePadding()
 
391
    {
 
392
        $centerX = $this->jp2->getWidth()  / 2 + $this->offsetX;
 
393
        $centerY = $this->jp2->getHeight() / 2 + $this->offsetY;
 
394
        
 
395
        $leftToCenter = ($this->imageSubRegion['left'] - $centerX);
 
396
        $topToCenter  = ($this->imageSubRegion['top']  - $centerY);
 
397
        $scaleFactor  = $this->jp2->getScale() / $this->desiredScale;
 
398
        $relLeftToCenter = $leftToCenter * $scaleFactor;
 
399
        $relTopToCenter  = $topToCenter  * $scaleFactor;
 
400
 
 
401
        $left = ($this->roi->left() / $this->desiredScale) - $relLeftToCenter;
 
402
        $top  = ($this->roi->top()  / $this->desiredScale) - $relTopToCenter;
 
403
 
 
404
        // Rounding to prevent inprecision during later implicit integer casting (Imagick->extentImage)
 
405
        // http://www.php.net/manual/en/language.types.float.php#warn.float-precision
 
406
        return array(
 
407
           "gravity" => "northwest",
 
408
           "width"   => round($this->roi->getPixelWidth()),
 
409
           "height"  => round($this->roi->getPixelHeight()),
 
410
           "offsetX" => ($left < 0.001 && $left > -0.001)? 0 : round($left),
 
411
           "offsetY" => ($top  < 0.001 && $top  > -0.001)? 0 : round($top)
 
412
        );
 
413
    }
 
414
    
 
415
    /**
 
416
     * Default behavior for images is to just set their opacity.
 
417
     * LASCOImage.php and CORImage.php have an applyAlphaMaskCmd that overrides this one and applies
 
418
     * an alpha mask and does some special commands for opacity
 
419
     * 
 
420
     * @param Object &$imagickImage IMagick Object
 
421
     * 
 
422
     * @return string
 
423
     */
 
424
    protected function setAlphaChannel(&$imagickImage)
 
425
    {
 
426
        $imagickImage->setImageOpacity($this->imageOptions['opacity'] / 100);
 
427
    }
 
428
 
 
429
    /**
 
430
     * Sets the subfield image color lookup table (CLUT)
 
431
     *
 
432
     * @param string $clut Location of the lookup table to use
 
433
     *
 
434
     * @return void
 
435
     */
 
436
    protected function setColorTable($clut)
 
437
    {
 
438
        $this->colorTable = $clut;
 
439
    }
 
440
 
 
441
    /**
 
442
     * Handles clean-up in case something goes wrong to avoid mal-formed tiles from being displayed
 
443
     *
 
444
     * @param string $filename Filename for aborted subfield image
 
445
     *
 
446
     * @TODO: Close any open IM/GD file handlers
 
447
     *
 
448
     * @return void
 
449
     */
 
450
    private function _abort($filename)
 
451
    {
 
452
        foreach(glob(substr($filename, 0, -3) . "*") as $file) {
 
453
            unlink($file);
 
454
        }
 
455
    }
 
456
 
 
457
    /**
 
458
     * Applies the specified color lookup table to the image using GD
 
459
     * Override this in any ImageType class that doesn't have a color
 
460
     * table, i.e. MDI and AIA (for now)
 
461
     *
 
462
     * Note: input and output are usually the same file.
 
463
     *
 
464
     * @param string &$input  Location of input image
 
465
     *
 
466
     * @return String binary string representation of image after processing
 
467
     */    
 
468
    protected function setColorPalette(&$input)
 
469
    {   
 
470
        $clut = $this->colorTable;
 
471
 
 
472
        // Read in image string
 
473
        $gd = imagecreatefromstring($input);
 
474
 
 
475
        if (!$gd) {
 
476
            throw new Exception("Unable to apply color-table: $input is not a valid image.", 32);
 
477
        }
 
478
 
 
479
        $ctable = imagecreatefrompng($clut);
 
480
 
 
481
        // Apply color table
 
482
        for ($i = 0; $i <= 255; $i++) {
 
483
            $rgb = imagecolorat($ctable, 0, $i);
 
484
            $r = ($rgb >> 16) & 0xFF;
 
485
            $g = ($rgb >> 8) & 0xFF;
 
486
            $b = $rgb & 0xFF;
 
487
            imagecolorset($gd, $i, $r, $g, $b);
 
488
        }
 
489
 
 
490
        // Write new image string
 
491
        ob_start();
 
492
 
 
493
        imagepng($gd, NULL);
 
494
        $blob = ob_get_contents();
 
495
        
 
496
        ob_end_clean();
 
497
 
 
498
        // Clean up
 
499
        imagedestroy($gd);
 
500
        imagedestroy($ctable);
 
501
            
 
502
        return $blob;
 
503
    }
 
504
 
 
505
    /**
 
506
     * Displays the image on the page
 
507
     *
 
508
     * @return void
 
509
     */
 
510
    public function display()
 
511
    {
 
512
        //header("Cache-Control: public, max-age=" . $lifetime * 60);
 
513
        $headers = apache_request_headers();
 
514
        
 
515
        // Enable caching of images served by PHP
 
516
        // http://us.php.net/manual/en/function.header.php#61903
 
517
        $lastModified = 'Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($this->outputFile)) . ' GMT';
 
518
        
 
519
        if (isset($headers['If-Modified-Since']) && (strtotime($headers['If-Modified-Since']) == filemtime($this->outputFile))) {
 
520
            // Cache is current (304)
 
521
            header($lastModified, true, 304);    
 
522
        } else {
 
523
            // Image not in cache or out of date (200)
 
524
            header($lastModified, true, 200);
 
525
            
 
526
            header('Content-Length: '.filesize($this->outputFile));
 
527
 
 
528
            // Set content-type
 
529
            $fileinfo = new finfo(FILEINFO_MIME);
 
530
            $mimetype = $fileinfo->file($this->outputFile);
 
531
            header("Content-type: " . $mimetype);
 
532
 
 
533
            // Filename & Content-length
 
534
            $filename = basename($this->outputFile);
 
535
            
 
536
            header("Content-Disposition: inline; filename=\"$filename\"");
 
537
 
 
538
            // Attempt to read in from cache and display
 
539
            $attempts = 0;
 
540
            
 
541
            while ($attempts < 3) {
 
542
                // If read is successful, we are finished
 
543
                if (readfile($this->outputFile)) {
 
544
                    return;
 
545
                }
 
546
                $attempts += 1;
 
547
                usleep(500000); // wait 0.5s
 
548
            }
 
549
            
 
550
            // If the image fails to load after 3 tries, display an error message
 
551
            throw new Exception("Unable to read image from cache: $filename", 33);                
 
552
        }
 
553
    }
 
554
 
 
555
    /**
 
556
     * Destructor
 
557
     * 
 
558
     * @return void
 
559
     */
 
560
    public function __destruct()
 
561
    {
 
562
        // Destroy IMagick object
 
563
        if (isset($this->image)) {
 
564
            $this->image->destroy();    
 
565
        }
 
566
    }
 
567
}
 
568
?>
 
 
b'\\ No newline at end of file'