296 lines
9.4 KiB
PHP
296 lines
9.4 KiB
PHP
<?php
|
|
/* This file is released under the GPL, any version you like
|
|
*
|
|
* PHP PSD reader class, v1.3
|
|
*
|
|
* By Tim de Koning
|
|
*
|
|
* Kingsquare Information Services, 22 jan 2007
|
|
*
|
|
* example use:
|
|
* ------------
|
|
* <?php
|
|
* include_once('classPhpPsdReader.php')
|
|
* header("Content-type: image/jpeg");
|
|
* print imagejpeg(imagecreatefrompsd('test.psd'));
|
|
* ?>
|
|
*
|
|
* More info, bugs or requests, contact info@kingsquare.nl
|
|
*
|
|
* Latest version and demo: http://www.kingsquare.nl/phppsdreader
|
|
*
|
|
* TODO
|
|
* ----
|
|
* - read color values for "multichannel data" PSD files
|
|
* - find and implement (hunter)lab to RGB algorithm
|
|
* - fix 32 bit colors... has something to do with gamma and exposure available since CS2, but dunno how to read them...
|
|
*/
|
|
|
|
|
|
class PhpPsdReader {
|
|
var $infoArray;
|
|
var $fp;
|
|
var $fileName;
|
|
var $tempFileName;
|
|
var $colorBytesLength;
|
|
|
|
function PhpPsdReader($fileName) {
|
|
set_time_limit(0);
|
|
$this->infoArray = array();
|
|
$this->fileName = $fileName;
|
|
$this->fp = fopen($this->fileName,'r');
|
|
|
|
if (fread($this->fp,4)=='8BPS') {
|
|
$this->infoArray['version id'] = $this->_getInteger(2);
|
|
fseek($this->fp,6,SEEK_CUR); // 6 bytes of 0's
|
|
$this->infoArray['channels'] = $this->_getInteger(2);
|
|
$this->infoArray['rows'] = $this->_getInteger(4);
|
|
$this->infoArray['columns'] = $this->_getInteger(4);
|
|
$this->infoArray['colorDepth'] = $this->_getInteger(2);
|
|
$this->infoArray['colorMode'] = $this->_getInteger(2);
|
|
|
|
|
|
/* COLOR MODE DATA SECTION */ //4bytes Length The length of the following color data.
|
|
$this->infoArray['colorModeDataSectionLength'] = $this->_getInteger(4);
|
|
fseek($this->fp,$this->infoArray['colorModeDataSectionLength'],SEEK_CUR); // ignore this snizzle
|
|
|
|
/* IMAGE RESOURCES */
|
|
$this->infoArray['imageResourcesSectionLength'] = $this->_getInteger(4);
|
|
fseek($this->fp,$this->infoArray['imageResourcesSectionLength'],SEEK_CUR); // ignore this snizzle
|
|
|
|
/* LAYER AND MASK */
|
|
$this->infoArray['layerMaskDataSectionLength'] = $this->_getInteger(4);
|
|
fseek($this->fp,$this->infoArray['layerMaskDataSectionLength'],SEEK_CUR); // ignore this snizzle
|
|
|
|
|
|
/* IMAGE DATA */
|
|
$this->infoArray['compressionType'] = $this->_getInteger(2);
|
|
$this->infoArray['oneColorChannelPixelBytes'] = $this->infoArray['colorDepth']/8;
|
|
$this->colorBytesLength = $this->infoArray['rows']*$this->infoArray['columns']*$this->infoArray['oneColorChannelPixelBytes'];
|
|
|
|
if ($this->infoArray['colorMode']==2) {
|
|
$this->infoArray['error'] = 'images with indexed colours are not supported yet';
|
|
return false;
|
|
}
|
|
} else {
|
|
$this->infoArray['error'] = 'invalid or unsupported psd';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
function getImage() {
|
|
// decompress image data if required
|
|
switch($this->infoArray['compressionType']) {
|
|
// case 2:, case 3: zip not supported yet..
|
|
case 1:
|
|
// packed bits
|
|
$this->infoArray['scanLinesByteCounts'] = array();
|
|
for ($i=0; $i<($this->infoArray['rows']*$this->infoArray['channels']); $i++) $this->infoArray['scanLinesByteCounts'][] = $this->_getInteger(2);
|
|
$this->tempFileName = tempnam(realpath('/tmp'),'decompressedImageData');
|
|
$tfp = fopen($this->tempFileName,'wb');
|
|
foreach ($this->infoArray['scanLinesByteCounts'] as $scanLinesByteCount) {
|
|
fwrite($tfp,$this->_getPackedBitsDecoded(fread($this->fp,$scanLinesByteCount)));
|
|
}
|
|
fclose($tfp);
|
|
fclose($this->fp);
|
|
$this->fp = fopen($this->tempFileName,'r');
|
|
default:
|
|
// continue with current file handle;
|
|
break;
|
|
}
|
|
|
|
// let's write pixel by pixel....
|
|
$image = imagecreatetruecolor($this->infoArray['columns'],$this->infoArray['rows']);
|
|
|
|
for ($rowPointer = 0; ($rowPointer < $this->infoArray['rows']); $rowPointer++) {
|
|
for ($columnPointer = 0; ($columnPointer < $this->infoArray['columns']); $columnPointer++) {
|
|
/* The color mode of the file. Supported values are: Bitmap=0;
|
|
Grayscale=1; Indexed=2; RGB=3; CMYK=4; Multichannel=7;
|
|
Duotone=8; Lab=9.
|
|
*/
|
|
switch ($this->infoArray['colorMode']) {
|
|
case 2: // indexed... info should be able to extract from color mode data section. not implemented yet, so is grayscale
|
|
exit;
|
|
break;
|
|
case 0:
|
|
// bit by bit
|
|
if ($columnPointer == 0) $bitPointer = 0;
|
|
if ($bitPointer==0) $currentByteBits = str_pad(base_convert(bin2hex(fread($this->fp,1)), 16, 2),8,'0',STR_PAD_LEFT);
|
|
$r = $g = $b = (($currentByteBits[$bitPointer]=='1')?0:255);
|
|
$bitPointer++;
|
|
if ($bitPointer==8) $bitPointer = 0;
|
|
break;
|
|
|
|
case 1:
|
|
case 8: // 8 is indexed with 1 color..., so grayscale
|
|
$r = $g = $b = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
break;
|
|
|
|
case 4: // CMYK
|
|
$c = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
$currentPointerPos = ftell($this->fp);
|
|
fseek($this->fp,$this->colorBytesLength-1,SEEK_CUR);
|
|
$m = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
fseek($this->fp,$this->colorBytesLength-1,SEEK_CUR);
|
|
$y = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
fseek($this->fp,$this->colorBytesLength-1,SEEK_CUR);
|
|
$k = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
fseek($this->fp,$currentPointerPos);
|
|
$r = round(($c * $k) / (pow(2,$this->infoArray['colorDepth'])-1));
|
|
$g = round(($m * $k) / (pow(2,$this->infoArray['colorDepth'])-1));
|
|
$b = round(($y * $k) / (pow(2,$this->infoArray['colorDepth'])-1));
|
|
|
|
break;
|
|
|
|
case 9: // hunter Lab
|
|
// i still need an understandable lab2rgb convert algorithm... if you have one, please let me know!
|
|
$l = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
$currentPointerPos = ftell($this->fp);
|
|
fseek($this->fp,$this->colorBytesLength-1,SEEK_CUR);
|
|
$a = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
fseek($this->fp,$this->colorBytesLength-1,SEEK_CUR);
|
|
$b = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
fseek($this->fp,$currentPointerPos);
|
|
|
|
$r = $l;
|
|
$g = $a;
|
|
$b = $b;
|
|
|
|
break;
|
|
default:
|
|
$r = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
$currentPointerPos = ftell($this->fp);
|
|
fseek($this->fp,$this->colorBytesLength-1,SEEK_CUR);
|
|
$g = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
fseek($this->fp,$this->colorBytesLength-1,SEEK_CUR);
|
|
$b = $this->_getInteger($this->infoArray['oneColorChannelPixelBytes']);
|
|
fseek($this->fp,$currentPointerPos);
|
|
break;
|
|
|
|
}
|
|
|
|
if (($this->infoArray['oneColorChannelPixelBytes']==2)) {
|
|
$r = $r >> 8;
|
|
$g = $g >> 8;
|
|
$b = $b >> 8;
|
|
} elseif (($this->infoArray['oneColorChannelPixelBytes']==4)) {
|
|
$r = $r >> 24;
|
|
$g = $g >> 24;
|
|
$b = $b >> 24;
|
|
}
|
|
|
|
$pixelColor = imagecolorallocate($image,$r,$g,$b);
|
|
imagesetpixel($image,$columnPointer,$rowPointer,$pixelColor);
|
|
}
|
|
}
|
|
fclose($this->fp);
|
|
if (isset($this->tempFileName)) unlink($this->tempFileName);
|
|
return $image;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* PRIVATE FUNCTIONS
|
|
*
|
|
*/
|
|
|
|
function _getPackedBitsDecoded($string) {
|
|
/*
|
|
The PackBits algorithm will precede a block of data with a one byte header n, where n is interpreted as follows:
|
|
n Meaning
|
|
0 to 127 Copy the next n + 1 symbols verbatim
|
|
-127 to -1 Repeat the next symbol 1 - n times
|
|
-128 Do nothing
|
|
|
|
Decoding:
|
|
Step 1. Read the block header (n).
|
|
Step 2. If the header is an EOF exit.
|
|
Step 3. If n is non-negative, copy the next n + 1 symbols to the output stream and go to step 1.
|
|
Step 4. If n is negative, write 1 - n copies of the next symbol to the output stream and go to step 1.
|
|
|
|
*/
|
|
|
|
$stringPointer = 0;
|
|
$returnString = '';
|
|
|
|
while (1) {
|
|
if (isset($string[$stringPointer])) $headerByteValue = $this->_unsignedToSigned(hexdec(bin2hex($string[$stringPointer])),1);
|
|
else return $returnString;
|
|
$stringPointer++;
|
|
|
|
if ($headerByteValue >= 0) {
|
|
for ($i=0; $i <= $headerByteValue; $i++) {
|
|
$returnString .= $string[$stringPointer];
|
|
$stringPointer++;
|
|
}
|
|
} else {
|
|
if ($headerByteValue != -128) {
|
|
$copyByte = $string[$stringPointer];
|
|
$stringPointer++;
|
|
|
|
for ($i=0; $i < (1-$headerByteValue); $i++) {
|
|
$returnString .= $copyByte;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function _unsignedToSigned($int,$byteSize=1) {
|
|
switch($byteSize) {
|
|
case 1:
|
|
if ($int<128) return $int;
|
|
else return -256+$int;
|
|
break;
|
|
|
|
case 2:
|
|
if ($int<32768) return $int;
|
|
else return -65536+$int;
|
|
|
|
case 4:
|
|
if ($int<2147483648) return $int;
|
|
else return -4294967296+$int;
|
|
|
|
default:
|
|
return $int;
|
|
}
|
|
}
|
|
|
|
function _hexReverse($hex) {
|
|
$output = '';
|
|
if (strlen($hex)%2) return false;
|
|
for ($pointer = strlen($hex);$pointer>=0;$pointer-=2) $output .= substr($hex,$pointer,2);
|
|
return $output;
|
|
}
|
|
|
|
function _getInteger($byteCount=1) {
|
|
switch ($byteCount) {
|
|
case 4:
|
|
// for some strange reason this is still broken...
|
|
return @reset(unpack('N',fread($this->fp,4)));
|
|
break;
|
|
|
|
case 2:
|
|
return @reset(unpack('n',fread($this->fp,2)));
|
|
break;
|
|
|
|
default:
|
|
return hexdec($this->_hexReverse(bin2hex(fread($this->fp,$byteCount))));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an image identifier representing the image obtained from the given filename, using only GD, returns an empty string on failure
|
|
*
|
|
* @param string $fileName
|
|
* @return image identifier
|
|
*/
|
|
|
|
function imagecreatefrompsd($fileName) {
|
|
$psdReader = new PhpPsdReader($fileName);
|
|
if (isset($psdReader->infoArray['error'])) return '';
|
|
else return $psdReader->getImage();
|
|
}
|