~jstys-z/helioviewer.org/client5

« back to all changes in this revision

Viewing changes to api/lib/helioviewer/JP2Image.Manual.php

  • Committer: V. Keith Hughitt
  • Date: 2009-04-01 21:08:05 UTC
  • Revision ID: hughitt1@kore-20090401210805-372f7dgih07vxk42
nightly build 04-01-2009

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
/**
 
3
 * @package JP2Image - JPEG 2000 Image Class
 
4
 * @author Keith Hughitt <Vincent.K.Hughitt@nasa.gov>
 
5
 */
 
6
abstract class JP2Image {
 
7
        protected $kdu_expand   = CONFIG::KDU_EXPAND;
 
8
        protected $kdu_lib_path = CONFIG::KDU_LIBS_DIR;
 
9
        protected $cacheDir     = CONFIG::CACHE_DIR;
 
10
        protected $jp2Dir       = CONFIG::JP2_DIR;
 
11
        protected $noImage      = CONFIG::EMPTY_TILE;
 
12
        protected $baseScale    = 2.63; //Scale of an EIT image at the base zoom-level: 2.63 arcseconds/px
 
13
        protected $baseZoom     = 10;   //Zoom-level at which (EIT) images are of this scale.
 
14
        
 
15
        protected $db;
 
16
        protected $imageId;
 
17
        protected $xRange;
 
18
        protected $yRange;
 
19
        protected $zoomLevel;
 
20
        protected $tileSize;
 
21
        protected $desiredScale;
 
22
        protected $desiredToActual;
 
23
        protected $scaleFactor;
 
24
        
 
25
        protected $jp2;
 
26
        protected $jp2Width;
 
27
        protected $jp2Height;
 
28
        protected $jp2Scale;
 
29
        protected $detector;
 
30
        protected $measurement;
 
31
        protected $opacityGrp;
 
32
        protected $timestamp;
 
33
        
 
34
        protected $image;
 
35
                
 
36
        /**
 
37
         * @param int The image identifier
 
38
         * @param int The zoomlevel to work with
 
39
         * @param array An associative array reprenting the desired image width
 
40
         *              in terms of tile x-coordinates.
 
41
     * @param array An associative array reprenting the desired image height
 
42
     *              in terms of tile y-coordinates.
 
43
         * @param int The size of the tile to work with.
 
44
         * 
 
45
         * @TODO: Move away from working in terms of tiles in the "JP2Image" class
 
46
         * and instead use top-left corner, width, and height. The Tile class can
 
47
         * make any neccessary conversions from tile x&y.
 
48
         * 
 
49
         * Also need to determine how functions that use "tilesize" can be handled.
 
50
         */
 
51
        protected function __construct($id, $zoomLevel, $xRange, $yRange, $tileSize) {
 
52
                require_once('DbConnection.php');
 
53
                date_default_timezone_set('UTC');
 
54
                $this->db        = new DbConnection();
 
55
                $this->imageId   = $id;
 
56
                $this->zoomLevel = $zoomLevel;
 
57
                $this->tileSize  = $tileSize;
 
58
                $this->xRange    = $xRange;
 
59
                $this->yRange    = $yRange;
 
60
 
 
61
                // Get image meta information
 
62
                $this->getMetaInfo();
 
63
 
 
64
                // Determine desired image scale
 
65
                $this->zoomOffset   = $zoomLevel - $this->baseZoom;
 
66
                $this->desiredScale = $this->baseScale * (pow(2, $this->zoomOffset));
 
67
                
 
68
                // Ratio of the desired scale to the actual JP2 image scale
 
69
                $this->desiredToActual = $this->desiredScale / $this->jp2Scale;
 
70
                
 
71
                // Scale Factor
 
72
                $this->scaleFactor = log($this->desiredToActual, 2);
 
73
                
 
74
                // Relative Tilesize
 
75
                $this->relativeTilesize = $this->tileSize * $this->desiredToActual;             
 
76
        }
 
77
        
 
78
        /**
 
79
         * buildImage
 
80
         * @return
 
81
         */
 
82
        protected function buildImage($filename) {
 
83
                // extract region from JP2
 
84
                $pgm = $this->extractRegion($filename);
 
85
                
 
86
                // Use PNG as intermediate format so that GD can read it in
 
87
                $png = substr($filename, 0, -3) . "png";
 
88
                exec("convert $pgm -depth 8 -quality 10 $png");
 
89
                
 
90
                // Apply color-lookup table
 
91
                if (($this->detector == "EIT") || ($this->measurement == "0WL")) {
 
92
                        $clut = $this->getColorTable($this->detector, $this->measurement);
 
93
                        $this->setColorPalette($png, $clut, $png);
 
94
                }
 
95
 
 
96
                // IM command for transparency, padding, rescaling, etc.
 
97
                $cmd = "convert $png -background black ";
 
98
                
 
99
                // Apply alpha mask for images with transparent components
 
100
                if ($this->hasAlphaMask()) {
 
101
                        $mask = substr($filename, 0, -4) . "-mask.tif";
 
102
                        $cmd .= "$mask ";
 
103
                }
 
104
                
 
105
                // Determine relative size of image at this scale
 
106
                $jp2RelWidth  = $this->jp2Width  /  $this->desiredToActual;
 
107
                $jp2RelHeight = $this->jp2Height /  $this->desiredToActual;
 
108
                
 
109
                // Get dimensions of extracted region
 
110
                $extracted = $this->getImageDimensions($pgm);
 
111
 
 
112
                // Pad up the the relative tilesize (in cases where region extracted for outer tiles is smaller than for inner tiles)
 
113
                $relTs = $this->relativeTilesize;
 
114
                if (($relTs < $this->tileSize) && (($extracted['width'] < $relTs) || ($extracted['height'] < $relTs))) {
 
115
                        $pad = "convert $png -background black " . $this->padImage($jp2RelWidth, $jp2RelHeight, $extracted['width'], $extracted['height'], $relTs, $this->xRange["start"], $this->yRange["start"]) . " $png";
 
116
                        exec($pad);
 
117
                }               
 
118
                
 
119
                // Resize if necessary (Case 3)
 
120
                if ($relTs < $this->tileSize)
 
121
                        $cmd .= "-geometry " . $this->tileSize . "x" . $this->tileSize . "! ";
 
122
 
 
123
                // Refetch dimensions of extracted region
 
124
                $tile = $this->getImageDimensions($png);
 
125
                
 
126
                // Pad if tile is smaller than it should be (Case 2)
 
127
                if ((($tile['width'] < $this->tileSize) || ($tile['height'] < $this->tileSize)) && ($relTs >= $this->tileSize)) {
 
128
                        $cmd .= $this->padImage($jp2RelWidth, $jp2RelHeight, $tile['width'], $tile['height'], $this->tileSize, $this->xRange["start"], $this->yRange["start"]);
 
129
                }
 
130
                
 
131
                if ($this->hasAlphaMask()) {
 
132
                        $cmd .= "-compose copy_opacity -composite ";
 
133
                }
 
134
                
 
135
                // Compression settings & Interlacing
 
136
                $cmd .= $this->setImageParams();
 
137
                
 
138
                //echo ("$cmd $filename");
 
139
                //exit();
 
140
                
 
141
                //WORKING:
 
142
                //convert /var/www/hv/cache/512/2003/10/08/1135_13_+00_+00.png -background black  
 
143
                //        /var/www/hv/cache/512/2003/10/08/1135_13_+00_+00-mask.tif -gravity NorthWest -extent 512x512 
 
144
                //        -compose copy_opacity -composite -quality 20 -interlace plane -depth 8 -colors 256 test.png
 
145
 
 
146
                // Execute command
 
147
                exec("$cmd $filename");
 
148
 
 
149
                // Remove intermediate file (note: remove mask)
 
150
                //unlink($pgm);         
 
151
        
 
152
                return $filename;
 
153
        }
 
154
        
 
155
        /**
 
156
         * Set Image Parameters
 
157
         * @return String Image compression and quality related flags.
 
158
         */
 
159
        private function setImageParams() {
 
160
                $args = " -quality ";
 
161
                if ($this->getImageFormat() == "png") {
 
162
                        $args .= Config::PNG_COMPRESSION_QUALITY . " -interlace plane";
 
163
                } else {
 
164
                        $args .= Config::JPEG_COMPRESSION_QUALITY . " -interlace line";
 
165
                }
 
166
                $args .= " -depth " . Config::BIT_DEPTH . " -colors " . Config::NUM_COLORS . " ";
 
167
                
 
168
                return $args;
 
169
        }
 
170
        
 
171
        /**
 
172
         * Call's the identify command in order to determine an image's dimensions
 
173
         * @return Object the width and height of the given image
 
174
         * @param $filename String - The image filepath
 
175
         */
 
176
        private function getImageDimensions($filename) {
 
177
                $dimensions = split("x", trim(exec("identify $filename | grep -o \" [0-9]*x[0-9]* \"")));
 
178
                return array (
 
179
                        'width'  => $dimensions[0],
 
180
                        'height' => $dimensions[1]
 
181
                );
 
182
        }
 
183
        
 
184
        /**
 
185
         * Extract a region using kdu_expand
 
186
         * @return String - Filename of the expanded region 
 
187
         * @param $filename String - JP2 filename
 
188
         */
 
189
        private function extractRegion($filename) {
 
190
                // Intermediate image file
 
191
                $pgm = substr($filename, 0, -3) . "pgm";
 
192
                
 
193
                // For images with transparent parts, extract a mask as well
 
194
                if ($this->hasAlphaMask()) {
 
195
                        $mask = substr($filename, 0, -4) . "-mask.tif";
 
196
                        $cmd = "$this->kdu_expand -i $this->jp2 -raw_components -o $pgm,$mask ";
 
197
                }
 
198
                else {
 
199
                        $cmd = "$this->kdu_expand -i $this->jp2 -o $pgm ";
 
200
                }
 
201
                
 
202
                // Case 1: JP2 image resolution = desired resolution
 
203
                // Nothing special to do...
 
204
 
 
205
                // Case 2: JP2 image resolution > desired resolution (use -reduce)              
 
206
                if ($this->jp2Scale < $this->desiredScale) {
 
207
                        $cmd .= "-reduce " . $this->scaleFactor . " ";
 
208
                }
 
209
 
 
210
                // Case 3: JP2 image resolution < desired resolution (get smaller tile and then enlarge)
 
211
                // Don't do anything yet...
 
212
 
 
213
                // Add desired region
 
214
                $cmd .= $this->getRegionString($this->jp2Width, $this->jp2Height, $this->relativeTilesize);
 
215
                
 
216
                //echo $cmd;
 
217
                
 
218
                // Execute the command
 
219
                try {
 
220
                        exec('export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:' . "$this->kdu_lib_path; " . $cmd, $out, $ret);
 
221
                        
 
222
                        if ($ret != 0)
 
223
                                throw new Exception("Failed to expand requested sub-region!<br><br> <b>Command:</b> '$cmd'");
 
224
                                
 
225
                } catch(Exception $e) {
 
226
                        echo '<span style="color:red;">Error:</span> ' .$e->getMessage();
 
227
                        exit();
 
228
                }
 
229
                
 
230
                return $pgm;
 
231
        }
 
232
        
 
233
        /**
 
234
         * getRegionString
 
235
         * Build a region string to be used by kdu_expand. e.g. "-region {0.0,0.0},{0.5,0.5}"
 
236
         * 
 
237
         * NOTE: Because kakadu's internal precision for region strings is less than PHP,
 
238
         * the numbers used are cut off to prevent erronious rounding.
 
239
         */
 
240
        private function getRegionString() {
 
241
                $jp2Width  = $this->jp2Width;
 
242
                $jp2Height = $this->jp2Height;
 
243
                $ts = $this->relativeTilesize;
 
244
                
 
245
                // Rounding
 
246
                $precision = 6;
 
247
                
 
248
                // Parameters
 
249
                $top = $left = $width = $height = null;
 
250
                
 
251
                // Number of tiles for the entire image
 
252
                $imgNumTilesX = max(2, ceil($jp2Width  / $ts));
 
253
                $imgNumTilesY = max(2, ceil($jp2Height / $ts));
 
254
                
 
255
                // Tile placement architecture expects an even number of tiles along each dimension
 
256
                if ($imgNumTilesX % 2 != 0)
 
257
                        $imgNumTilesX += 1;
 
258
 
 
259
                if ($imgNumTilesY % 2 != 0)
 
260
                        $imgNumTilesY += 1;
 
261
                        
 
262
                // Shift so that 0,0 now corresponds to the top-left tile
 
263
                $relX = (0.5 * $imgNumTilesX) + $this->xRange["start"];
 
264
                $relY = (0.5 * $imgNumTilesY) + $this->yRange["start"];
 
265
 
 
266
                // number of tiles (may be greater than one for movies, etc)
 
267
                $numTilesX = min($imgNumTilesX - $relX, $this->xRange["end"] - $this->xRange["start"] + 1);
 
268
                $numTilesY = min($imgNumTilesY - $relY, $this->yRange["end"] - $this->yRange["start"] + 1);
 
269
 
 
270
                // Number of "inner" tiles
 
271
                $numTilesInsideX = $imgNumTilesX - 2;
 
272
                $numTilesInsideY = $imgNumTilesY - 2;
 
273
                
 
274
                // Dimensions for inner and outer tiles
 
275
                $innerTS = $ts;
 
276
                $outerTS = ($jp2Width - ($numTilesInsideX * $innerTS)) / 2;
 
277
                
 
278
                // <top>
 
279
                $top  = substr((($relY == 0) ? 0 :  $outerTS + ($relY - 1) * $innerTS) / $jp2Height, 0, $precision);
 
280
                
 
281
                // <left>
 
282
                $left = substr((($relX == 0) ? 0 :  $outerTS + ($relX - 1) * $innerTS) / $jp2Width, 0, $precision);
 
283
 
 
284
                // <height>
 
285
                $height = substr(((($relY == 0) || ($relY == ($imgNumTilesY -1))) ? $outerTS : $innerTS) / $jp2Height, 0, $precision);
 
286
                
 
287
                // <width>
 
288
                $width  = substr(((($relX == 0) || ($relX == ($imgNumTilesX -1))) ? $outerTS : $innerTS) / $jp2Width, 0, $precision);
 
289
 
 
290
                // {<top>,<left>},{<height>,<width>}
 
291
                $region = "-region \{$top,$left\},\{$height,$width\}";
 
292
 
 
293
                return $region;
 
294
        }
 
295
        
 
296
        /**
 
297
         * padImage
 
298
         */
 
299
        //function padImage($im, $ts, $x, $y) {
 
300
        /**
 
301
        function padImage($tif, $ts, $x, $y, $relTs) {
 
302
                $padx = $ts - $relTs;
 
303
                $pady = $ts - $relTs;
 
304
 
 
305
                // top-left
 
306
                if (($x == -1) && ($y == -1))
 
307
                        return "-background transparent -gravity SouthEast -extent $ts" . "x" . "$ts ";
 
308
 
 
309
                // top-right
 
310
                if (($x == 0) && ($y == -1))
 
311
                        return "-background transparent -gravity SouthWest -extent $ts" . "x" . "$ts ";
 
312
 
 
313
                // bottom-right
 
314
                if (($x == 0) && ($y == 0))
 
315
                        return "-background transparent -gravity NorthWest -extent $ts" . "x" . "$ts ";
 
316
 
 
317
                // bottom-left
 
318
                if (($x == -1) && ($y == 0))
 
319
                        return "-background transparent -gravity NorthEast -extent $ts" . "x" . "$ts ";
 
320
 
 
321
        }**/
 
322
        
 
323
        private function padImage ($jp2Width, $jp2Height, $tileWidth, $tileHeight, $ts, $x, $y) {
 
324
                // Determine min and max tile numbers
 
325
                $imgNumTilesX = max(2, ceil($jp2Width  / $this->tileSize));
 
326
                $imgNumTilesY = max(2, ceil($jp2Height / $this->tileSize));
 
327
                
 
328
                // Tile placement architecture expects an even number of tiles along each dimension
 
329
                if ($imgNumTilesX % 2 != 0)
 
330
                        $imgNumTilesX += 1;
 
331
 
 
332
                if ($imgNumTilesY % 2 != 0)
 
333
                        $imgNumTilesY += 1;
 
334
                
 
335
                $tileMinX = - ($imgNumTilesX / 2);
 
336
                $tileMaxX =   ($imgNumTilesX / 2) - 1;
 
337
                $tileMinY = - ($imgNumTilesY / 2);
 
338
                $tileMaxY =   ($imgNumTilesY / 2) - 1; 
 
339
                                
 
340
                // Determine where the tile is located (where tile should lie in the padding)
 
341
                $gravity = null;
 
342
                if ($x == $tileMinX) {
 
343
                        if ($y == $tileMinY) {
 
344
                                $gravity = "SouthEast";
 
345
                        }
 
346
                        else if ($y == $tileMaxY) {
 
347
                                $gravity = "NorthEast";
 
348
                        }
 
349
                        else {
 
350
                                $gravity = "East";
 
351
                        }
 
352
                }
 
353
                else if ($x == $tileMaxX) {
 
354
                        if ($y == $tileMinY) {
 
355
                                $gravity = "SouthWest";
 
356
                        }
 
357
                        else if ($y == $tileMaxY) {
 
358
                                $gravity = "NorthWest";
 
359
                        }
 
360
                        else {
 
361
                                $gravity = "West";
 
362
                        }
 
363
                }
 
364
                
 
365
                else {
 
366
                        if ($y == $tileMinY) {
 
367
                                $gravity = "South";
 
368
                        }
 
369
                        else {
 
370
                                $gravity = "North";
 
371
                        }
 
372
                }
 
373
                
 
374
                // Construct padding command
 
375
                // TEST: use black instead of transparent for background?
 
376
                return "-gravity $gravity -extent $ts" . "x" . "$ts ";
 
377
        }
 
378
        
 
379
        private function getColorTable($detector, $measurement) {
 
380
                if ($detector == "EIT") {
 
381
                        return Config::WEB_ROOT_DIR . "/images/color-tables/ctable_EIT_$measurement.png";
 
382
                }
 
383
                else if ($detector == "0C2") {
 
384
                        return Config::WEB_ROOT_DIR .  "/images/color-tables/ctable_idl_3.png";
 
385
                }
 
386
                else if ($detector == "0C3") {
 
387
                        return Config::WEB_ROOT_DIR . "/images/color-tables/ctable_idl_1.png";
 
388
                }               
 
389
        }
 
390
        
 
391
        public function display($filepath=null) {
 
392
                // Cache-Lifetime (in minutes)
 
393
                $lifetime = 60;
 
394
                $exp_gmt = gmdate("D, d M Y H:i:s", time() + $lifetime * 60) ." GMT";
 
395
                header("Expires: " . $exp_gmt);
 
396
                header("Cache-Control: public, max-age=" . $lifetime * 60);
 
397
 
 
398
                // Special header for MSIE 5
 
399
                header("Cache-Control: pre-check=" . $lifetime * 60, FALSE);
 
400
 
 
401
                // Filename & Content-length
 
402
                if (isset($filepath)) {
 
403
                        $exploded = explode("/", $filepath);
 
404
                        $filename = end($exploded);
 
405
                                                
 
406
                        header("Content-Length: " . filesize($filepath));
 
407
                        header("Content-Disposition: inline; filename=\"$filename\"");  
 
408
                }
 
409
 
 
410
                // Specify format
 
411
                $format = $this->getImageFormat();
 
412
 
 
413
                if ($format == "png")
 
414
                        header("Content-Type: image/png");
 
415
                else
 
416
                        header("Content-Type: image/jpeg");
 
417
                
 
418
                readfile($filepath);
 
419
        }
 
420
        
 
421
        /**
 
422
         * hasAlphaMask
 
423
         * @return string
 
424
         */
 
425
        private function hasAlphaMask() {
 
426
                return $this->measurement === "0WL" ? true : false;
 
427
        }
 
428
        
 
429
        /**
 
430
         * getMetaInfo
 
431
         * @param $imageId Object
 
432
         */
 
433
        protected function getMetaInfo() {
 
434
                $query  = sprintf("SELECT timestamp, uri, opacityGrp, width, height, imgScaleX, imgScaleY, measurement.abbreviation as measurement, detector.abbreviation as detector FROM image 
 
435
                                                        LEFT JOIN measurement on image.measurementId = measurement.id  
 
436
                                                        LEFT JOIN detector on measurement.detectorId = detector.id 
 
437
                                                        WHERE image.id=%d", $this->imageId);
 
438
 
 
439
                $result = $this->db->query($query);
 
440
 
 
441
                if (!$result) {
 
442
                        echo "$query - failed\n";
 
443
                        die (mysqli_error($this->db->link));
 
444
                }
 
445
                else if (mysqli_num_rows($result) > 0) {
 
446
                        $meta = mysqli_fetch_array($result, MYSQL_ASSOC);
 
447
                        
 
448
                        $this->jp2         = $meta['uri'];
 
449
                        $this->jp2Width    = $meta['width'];
 
450
                        $this->jp2Height   = $meta['height'];
 
451
                        $this->jp2Scale    = $meta['imgScaleX'];
 
452
                        $this->detector    = $meta['detector'];
 
453
                        $this->measurement = $meta['measurement'];
 
454
                        $this->opacityGrp  = $meta['opacityGrp'];
 
455
                        $this->timestamp   = $meta['timestamp'];
 
456
                }
 
457
                else
 
458
                        return false;
 
459
        }
 
460
        
 
461
        /**
 
462
         * getImageFormat
 
463
         * @return 
 
464
         */
 
465
        protected function getImageFormat() {
 
466
                return ($this->opacityGrp == 1) ? "jpg" : "png";
 
467
        }
 
468
        
 
469
        /**
 
470
         * setColorPalette
 
471
         */
 
472
        private function setColorPalette ($input, $clut, $output) {
 
473
                $gd     = imagecreatefrompng($input);
 
474
                $ctable = imagecreatefrompng($clut);
 
475
                
 
476
                //echo "$input<br> $clut<br> $output";
 
477
                //exit();
 
478
                
 
479
                for ($i = 0; $i <= 255; $i++) {
 
480
                        $rgba = imagecolorsforindex($ctable, $i);
 
481
                        imagecolorset($gd, $i, $rgba["red"], $rgba["green"], $rgba["blue"]);
 
482
                }
 
483
 
 
484
                // Enable interlacing
 
485
                imageinterlace($gd, true);
 
486
                
 
487
                //$this->getImageFormat() == "jpg" ? imagejpeg($gd, $output, Config::JPEG_COMPRESSION_QUALITY) : imagepng($gd, $output); 
 
488
                //if ($this->getImageFormat() == "jpg")
 
489
                //      imagejpeg($gd, $output, Config::JPEG_COMPRESSION_QUALITY);
 
490
                //else
 
491
                imagepng($gd, $output);
 
492
 
 
493
                // Cleanup
 
494
                if ($input != $output)
 
495
                        unlink($input);
 
496
                imagedestroy($gd);
 
497
                imagedestroy($ctable);
 
498
        }
 
499
}
 
500
?>