2
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
* Image_SubFieldImage class definition
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
17
* Represents a JPEG 2000 sub-field image.
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.
23
* @TODO Switch to using a single "optional" array passed to initialize for color table, padding, etc?
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
32
class Image_SubFieldImage
36
protected $outputFile;
38
protected $imageSubRegion;
39
protected $desiredScale;
40
protected $desiredToActual;
41
protected $scaleFactor;
42
protected $subfieldWidth;
43
protected $subfieldHeight;
44
protected $subfieldRelWidth;
45
protected $subfieldRelHeight;
48
protected $jp2RelWidth;
49
protected $jp2RelHeight;
55
* Creates an Image_SubFieldImage instance
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
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.
67
* @TODO: Rename "jp2scale" syntax to "nativeImageScale" to get away from JP2-specific terminology
68
* ("desiredScale" -> "desiredImageScale" or "requestedImageScale")
70
public function __construct($jp2, $roi, $outputFile, $offsetX, $offsetY, $options)
72
$this->outputFile = $outputFile;
82
"rescale" => IMagick::FILTER_TRIANGLE
85
$this->imageOptions = array_replace($defaults, $options);
87
// Source image dimensions
88
$jp2Width = $jp2->getWidth();
89
$jp2Height = $jp2->getHeight();
90
$jp2Scale = $jp2->getScale();
92
// Convert region of interest from arc-seconds to pixels
93
$this->imageSubRegion = $roi->getImageSubRegion($jp2Width, $jp2Height, $jp2Scale, $offsetX, $offsetY);
95
// Desired image scale (normalized to 1au)
96
$this->desiredScale = $roi->imageScale();
98
$this->desiredToActual = $this->desiredScale / $jp2->getScale();
99
$this->scaleFactor = log($this->desiredToActual, 2);
100
$this->reduce = max(0, floor($this->scaleFactor));
102
$this->subfieldWidth = $this->imageSubRegion["right"] - $this->imageSubRegion["left"];
103
$this->subfieldHeight = $this->imageSubRegion["bottom"] - $this->imageSubRegion["top"];
105
$this->subfieldRelWidth = $this->subfieldWidth / $this->desiredToActual;
106
$this->subfieldRelHeight = $this->subfieldHeight / $this->desiredToActual;
108
$this->jp2RelWidth = $jp2Width / $this->desiredToActual;
109
$this->jp2RelHeight = $jp2Height / $this->desiredToActual;
111
$this->offsetX = $offsetX;
112
$this->offsetY = $offsetY;
116
* Sets parameters (gravity and size) for any padding which should be applied to extracted subfield image
118
* @param array $padding An associative array containing the width,height, and gravity values to use during padding.
122
public function setPadding($padding)
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);
132
* Saves the new filepath
134
* @param string $filepath The new file path to the image
138
public function setNewFilePath($filepath)
140
$this->outputFile = $filepath;
144
* Gets the SubfieldImage's output file
146
* @return string outputFile
148
public function outputFile()
150
return $this->outputFile;
154
* Getters that are needed for determining padding, as they must be accessed from Tile or ImageLayer classes.
156
* @return int jp2RelWidth
158
public function jp2RelWidth()
160
return $this->jp2RelWidth;
164
* Gets the jp2 image's relative height
166
* @return int jp2RelHeight
168
public function jp2RelHeight()
170
return $this->jp2RelHeight;
174
* Gets the extracted image's relative width
176
* @return int subfieldRelWidth
178
public function subfieldRelWidth()
180
return $this->subfieldRelWidth;
184
* Gets the extracted image's relative height
186
* @return int subfieldRelHeight
188
public function subfieldRelHeight()
190
return $this->subfieldRelHeight;
194
* Builds the requested subfield image.
196
* Normalizing request & native image scales:
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.
202
* There are three possible cases which may occur:
204
* 1) desiredToActual = 1
206
* In this case the subfield requested is at the natural image scale. No resizing is necessary.
208
* 2) desiredToActual < 1
210
* The subfield requested is at a lower image scale (HIGHER quality) than the source JP2.
212
* 3) desiredToActual > 1
214
* The subfield requested is at a higher image scale (LOWER quality) than the source JP2.
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
222
protected function build()
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.
234
// Choose extension to convert source image to
235
if ($this->options['palettedJP2']) {
240
$input = substr($this->outputFile, 0, -4) . rand() . $extension;
242
// Extract region (PGM)
243
$this->jp2->extractRegion($input, $this->imageSubRegion, $this->reduce);
245
// Apply colormap if needed
246
if (!$this->options['palettedJP2']) {
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
255
$grayscaleString = $grayscale->getimageblob();
257
// Assume that no color table is needed
258
$coloredImage = $grayscale;
260
// Apply color table if one exists
261
if ($this->colorTable) {
262
$grayscale->destroy();
263
$coloredImageString = $this->setColorPalette($grayscaleString);
265
$coloredImage = new IMagick();
266
$coloredImage->readimageblob($coloredImageString);
269
$coloredImage = new IMagick($input);
272
// Set alpha channel for images with transparent components
273
$this->setAlphaChannel($coloredImage);
275
// Apply compression and interlacing
276
$this->compressImage($coloredImage);
278
// Resize extracted image to correct size before padding.
279
$rescaleBlurFactor = 0.6;
281
$coloredImage->resizeImage(
282
round($this->subfieldRelWidth), round($this->subfieldRelHeight),
283
$this->imageOptions['rescale'], $rescaleBlurFactor
285
$coloredImage->setImageBackgroundColor('transparent');
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();
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']
300
// Imagick 3.0 and lower
301
$coloredImage->extentImage(
302
$this->padding['width'], $this->padding['height'],
303
-$this->padding['offsetX'], -$this->padding['offsetY']
307
$this->image = $coloredImage;
309
// Check for PGM before deleting just in case another process already removed it
310
if (file_exists($input)) {
314
} catch(Exception $e) {
315
// Clean-up intermediate files
316
$this->_abort($this->outputFile);
322
* Returns the IMagick instance assocated with the image
324
public function getIMagickImage()
330
* Saves the file using the specified output filename
332
public function save()
334
if (!file_exists($this->outputFile) && !is_null($this->image)) {
335
$this->image->writeImage($this->outputFile);
340
* Sets compression for images that are not ImageLayers
342
* @param Object &$imagickImage An initialized Imagick object
346
protected function compressImage(&$imagickImage)
349
$parts = explode(".", $this->outputFile);
350
$extension = end($parts);
352
// Apply compression based on image type for those formats that support it
353
if ($extension === "png") {
355
$imagickImage->setImageCompression(IMagick::COMPRESSION_LZW);
357
// Compression quality
358
$quality = $this->imageOptions['compress'] ? PNG_HIGH_COMPRESSION : PNG_LOW_COMPRESSION;
359
$imagickImage->setImageCompressionQuality($quality);
362
if ($this->imageOptions['interlace']) {
363
$imagickImage->setInterlaceScheme(IMagick::INTERLACE_PLANE);
365
} elseif ($extension === "jpg") {
367
$imagickImage->setImageCompression(IMagick::COMPRESSION_JPEG);
369
// Compression quality
370
$quality = $this->imageOptions['compress'] ? JPG_HIGH_COMPRESSION : JPG_LOW_COMPRESSION;
371
$imagickImage->setImageCompressionQuality($quality);
374
if ($this->imageOptions['interlace']) {
375
$imagickImage->setInterlaceScheme(IMagick::INTERLACE_LINE);
379
$imagickImage->setImageDepth($this->imageOptions['bitdepth']);
383
* Figures out where the extracted image lies inside the final image
384
* if the final image is larger.
386
* @param Array $roi The region of interest in arcseconds of the final image.
388
* @return array with padding
390
public function computePadding()
392
$centerX = $this->jp2->getWidth() / 2 + $this->offsetX;
393
$centerY = $this->jp2->getHeight() / 2 + $this->offsetY;
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;
401
$left = ($this->roi->left() / $this->desiredScale) - $relLeftToCenter;
402
$top = ($this->roi->top() / $this->desiredScale) - $relTopToCenter;
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
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)
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
420
* @param Object &$imagickImage IMagick Object
424
protected function setAlphaChannel(&$imagickImage)
426
$imagickImage->setImageOpacity($this->imageOptions['opacity'] / 100);
430
* Sets the subfield image color lookup table (CLUT)
432
* @param string $clut Location of the lookup table to use
436
protected function setColorTable($clut)
438
$this->colorTable = $clut;
442
* Handles clean-up in case something goes wrong to avoid mal-formed tiles from being displayed
444
* @param string $filename Filename for aborted subfield image
446
* @TODO: Close any open IM/GD file handlers
450
private function _abort($filename)
452
foreach(glob(substr($filename, 0, -3) . "*") as $file) {
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)
462
* Note: input and output are usually the same file.
464
* @param string &$input Location of input image
466
* @return String binary string representation of image after processing
468
protected function setColorPalette(&$input)
470
$clut = $this->colorTable;
472
// Read in image string
473
$gd = imagecreatefromstring($input);
476
throw new Exception("Unable to apply color-table: $input is not a valid image.", 32);
479
$ctable = imagecreatefrompng($clut);
482
for ($i = 0; $i <= 255; $i++) {
483
$rgb = imagecolorat($ctable, 0, $i);
484
$r = ($rgb >> 16) & 0xFF;
485
$g = ($rgb >> 8) & 0xFF;
487
imagecolorset($gd, $i, $r, $g, $b);
490
// Write new image string
494
$blob = ob_get_contents();
500
imagedestroy($ctable);
506
* Displays the image on the page
510
public function display()
512
//header("Cache-Control: public, max-age=" . $lifetime * 60);
513
$headers = apache_request_headers();
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';
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);
523
// Image not in cache or out of date (200)
524
header($lastModified, true, 200);
526
header('Content-Length: '.filesize($this->outputFile));
529
$fileinfo = new finfo(FILEINFO_MIME);
530
$mimetype = $fileinfo->file($this->outputFile);
531
header("Content-type: " . $mimetype);
533
// Filename & Content-length
534
$filename = basename($this->outputFile);
536
header("Content-Disposition: inline; filename=\"$filename\"");
538
// Attempt to read in from cache and display
541
while ($attempts < 3) {
542
// If read is successful, we are finished
543
if (readfile($this->outputFile)) {
547
usleep(500000); // wait 0.5s
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);
560
public function __destruct()
562
// Destroy IMagick object
563
if (isset($this->image)) {
564
$this->image->destroy();
b'\\ No newline at end of file'