3
* @package JP2Image - JPEG 2000 Image Class
4
* @author Keith Hughitt <Vincent.K.Hughitt@nasa.gov>
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.
21
protected $desiredScale;
22
protected $desiredToActual;
23
protected $scaleFactor;
30
protected $measurement;
31
protected $opacityGrp;
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.
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.
49
* Also need to determine how functions that use "tilesize" can be handled.
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();
56
$this->zoomLevel = $zoomLevel;
57
$this->tileSize = $tileSize;
58
$this->xRange = $xRange;
59
$this->yRange = $yRange;
61
// Get image meta information
64
// Determine desired image scale
65
$this->zoomOffset = $zoomLevel - $this->baseZoom;
66
$this->desiredScale = $this->baseScale * (pow(2, $this->zoomOffset));
68
// Ratio of the desired scale to the actual JP2 image scale
69
$this->desiredToActual = $this->desiredScale / $this->jp2Scale;
72
$this->scaleFactor = log($this->desiredToActual, 2);
75
$this->relativeTilesize = $this->tileSize * $this->desiredToActual;
82
protected function buildImage($filename) {
83
// extract region from JP2
84
$pgm = $this->extractRegion($filename);
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");
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);
96
// IM command for transparency, padding, rescaling, etc.
97
$cmd = "convert $png -background black ";
99
// Apply alpha mask for images with transparent components
100
if ($this->hasAlphaMask()) {
101
$mask = substr($filename, 0, -4) . "-mask.tif";
105
// Determine relative size of image at this scale
106
$jp2RelWidth = $this->jp2Width / $this->desiredToActual;
107
$jp2RelHeight = $this->jp2Height / $this->desiredToActual;
109
// Get dimensions of extracted region
110
$extracted = $this->getImageDimensions($pgm);
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";
119
// Resize if necessary (Case 3)
120
if ($relTs < $this->tileSize)
121
$cmd .= "-geometry " . $this->tileSize . "x" . $this->tileSize . "! ";
123
// Refetch dimensions of extracted region
124
$tile = $this->getImageDimensions($png);
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"]);
131
if ($this->hasAlphaMask()) {
132
$cmd .= "-compose copy_opacity -composite ";
135
// Compression settings & Interlacing
136
$cmd .= $this->setImageParams();
138
//echo ("$cmd $filename");
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
147
exec("$cmd $filename");
149
// Remove intermediate file (note: remove mask)
156
* Set Image Parameters
157
* @return String Image compression and quality related flags.
159
private function setImageParams() {
160
$args = " -quality ";
161
if ($this->getImageFormat() == "png") {
162
$args .= Config::PNG_COMPRESSION_QUALITY . " -interlace plane";
164
$args .= Config::JPEG_COMPRESSION_QUALITY . " -interlace line";
166
$args .= " -depth " . Config::BIT_DEPTH . " -colors " . Config::NUM_COLORS . " ";
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
176
private function getImageDimensions($filename) {
177
$dimensions = split("x", trim(exec("identify $filename | grep -o \" [0-9]*x[0-9]* \"")));
179
'width' => $dimensions[0],
180
'height' => $dimensions[1]
185
* Extract a region using kdu_expand
186
* @return String - Filename of the expanded region
187
* @param $filename String - JP2 filename
189
private function extractRegion($filename) {
190
// Intermediate image file
191
$pgm = substr($filename, 0, -3) . "pgm";
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 ";
199
$cmd = "$this->kdu_expand -i $this->jp2 -o $pgm ";
202
// Case 1: JP2 image resolution = desired resolution
203
// Nothing special to do...
205
// Case 2: JP2 image resolution > desired resolution (use -reduce)
206
if ($this->jp2Scale < $this->desiredScale) {
207
$cmd .= "-reduce " . $this->scaleFactor . " ";
210
// Case 3: JP2 image resolution < desired resolution (get smaller tile and then enlarge)
211
// Don't do anything yet...
213
// Add desired region
214
$cmd .= $this->getRegionString($this->jp2Width, $this->jp2Height, $this->relativeTilesize);
218
// Execute the command
220
exec('export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:' . "$this->kdu_lib_path; " . $cmd, $out, $ret);
223
throw new Exception("Failed to expand requested sub-region!<br><br> <b>Command:</b> '$cmd'");
225
} catch(Exception $e) {
226
echo '<span style="color:red;">Error:</span> ' .$e->getMessage();
235
* Build a region string to be used by kdu_expand. e.g. "-region {0.0,0.0},{0.5,0.5}"
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.
240
private function getRegionString() {
241
$jp2Width = $this->jp2Width;
242
$jp2Height = $this->jp2Height;
243
$ts = $this->relativeTilesize;
249
$top = $left = $width = $height = null;
251
// Number of tiles for the entire image
252
$imgNumTilesX = max(2, ceil($jp2Width / $ts));
253
$imgNumTilesY = max(2, ceil($jp2Height / $ts));
255
// Tile placement architecture expects an even number of tiles along each dimension
256
if ($imgNumTilesX % 2 != 0)
259
if ($imgNumTilesY % 2 != 0)
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"];
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);
270
// Number of "inner" tiles
271
$numTilesInsideX = $imgNumTilesX - 2;
272
$numTilesInsideY = $imgNumTilesY - 2;
274
// Dimensions for inner and outer tiles
276
$outerTS = ($jp2Width - ($numTilesInsideX * $innerTS)) / 2;
279
$top = substr((($relY == 0) ? 0 : $outerTS + ($relY - 1) * $innerTS) / $jp2Height, 0, $precision);
282
$left = substr((($relX == 0) ? 0 : $outerTS + ($relX - 1) * $innerTS) / $jp2Width, 0, $precision);
285
$height = substr(((($relY == 0) || ($relY == ($imgNumTilesY -1))) ? $outerTS : $innerTS) / $jp2Height, 0, $precision);
288
$width = substr(((($relX == 0) || ($relX == ($imgNumTilesX -1))) ? $outerTS : $innerTS) / $jp2Width, 0, $precision);
290
// {<top>,<left>},{<height>,<width>}
291
$region = "-region \{$top,$left\},\{$height,$width\}";
299
//function padImage($im, $ts, $x, $y) {
301
function padImage($tif, $ts, $x, $y, $relTs) {
302
$padx = $ts - $relTs;
303
$pady = $ts - $relTs;
306
if (($x == -1) && ($y == -1))
307
return "-background transparent -gravity SouthEast -extent $ts" . "x" . "$ts ";
310
if (($x == 0) && ($y == -1))
311
return "-background transparent -gravity SouthWest -extent $ts" . "x" . "$ts ";
314
if (($x == 0) && ($y == 0))
315
return "-background transparent -gravity NorthWest -extent $ts" . "x" . "$ts ";
318
if (($x == -1) && ($y == 0))
319
return "-background transparent -gravity NorthEast -extent $ts" . "x" . "$ts ";
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));
328
// Tile placement architecture expects an even number of tiles along each dimension
329
if ($imgNumTilesX % 2 != 0)
332
if ($imgNumTilesY % 2 != 0)
335
$tileMinX = - ($imgNumTilesX / 2);
336
$tileMaxX = ($imgNumTilesX / 2) - 1;
337
$tileMinY = - ($imgNumTilesY / 2);
338
$tileMaxY = ($imgNumTilesY / 2) - 1;
340
// Determine where the tile is located (where tile should lie in the padding)
342
if ($x == $tileMinX) {
343
if ($y == $tileMinY) {
344
$gravity = "SouthEast";
346
else if ($y == $tileMaxY) {
347
$gravity = "NorthEast";
353
else if ($x == $tileMaxX) {
354
if ($y == $tileMinY) {
355
$gravity = "SouthWest";
357
else if ($y == $tileMaxY) {
358
$gravity = "NorthWest";
366
if ($y == $tileMinY) {
374
// Construct padding command
375
// TEST: use black instead of transparent for background?
376
return "-gravity $gravity -extent $ts" . "x" . "$ts ";
379
private function getColorTable($detector, $measurement) {
380
if ($detector == "EIT") {
381
return Config::WEB_ROOT_DIR . "/images/color-tables/ctable_EIT_$measurement.png";
383
else if ($detector == "0C2") {
384
return Config::WEB_ROOT_DIR . "/images/color-tables/ctable_idl_3.png";
386
else if ($detector == "0C3") {
387
return Config::WEB_ROOT_DIR . "/images/color-tables/ctable_idl_1.png";
391
public function display($filepath=null) {
392
// Cache-Lifetime (in minutes)
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);
398
// Special header for MSIE 5
399
header("Cache-Control: pre-check=" . $lifetime * 60, FALSE);
401
// Filename & Content-length
402
if (isset($filepath)) {
403
$exploded = explode("/", $filepath);
404
$filename = end($exploded);
406
header("Content-Length: " . filesize($filepath));
407
header("Content-Disposition: inline; filename=\"$filename\"");
411
$format = $this->getImageFormat();
413
if ($format == "png")
414
header("Content-Type: image/png");
416
header("Content-Type: image/jpeg");
425
private function hasAlphaMask() {
426
return $this->measurement === "0WL" ? true : false;
431
* @param $imageId Object
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);
439
$result = $this->db->query($query);
442
echo "$query - failed\n";
443
die (mysqli_error($this->db->link));
445
else if (mysqli_num_rows($result) > 0) {
446
$meta = mysqli_fetch_array($result, MYSQL_ASSOC);
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'];
465
protected function getImageFormat() {
466
return ($this->opacityGrp == 1) ? "jpg" : "png";
472
private function setColorPalette ($input, $clut, $output) {
473
$gd = imagecreatefrompng($input);
474
$ctable = imagecreatefrompng($clut);
476
//echo "$input<br> $clut<br> $output";
479
for ($i = 0; $i <= 255; $i++) {
480
$rgba = imagecolorsforindex($ctable, $i);
481
imagecolorset($gd, $i, $rgba["red"], $rgba["green"], $rgba["blue"]);
484
// Enable interlacing
485
imageinterlace($gd, true);
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);
491
imagepng($gd, $output);
494
if ($input != $output)
497
imagedestroy($ctable);