├── .gitignore ├── tests ├── bootstrap.php └── HelperTest.php ├── src ├── FileStructure │ ├── Resources │ │ ├── Resource │ │ │ ├── ResourceInterface.php │ │ │ ├── EmptyResource │ │ │ │ └── EmptyResource.php │ │ │ ├── Guides │ │ │ │ ├── GuidesData.php │ │ │ │ └── Guides.php │ │ │ ├── LayerComps │ │ │ │ ├── LayerComps.php │ │ │ │ └── LayerCompsData.php │ │ │ ├── ResolutionInfo │ │ │ │ ├── ResolutionInfo.php │ │ │ │ └── ResolutionInfoData.php │ │ │ ├── Resource.php │ │ │ └── ResourceBase.php │ │ └── Resources.php │ ├── LayerMask │ │ ├── Layer │ │ │ ├── BlendingRanges │ │ │ │ ├── BlendingRangesInterface.php │ │ │ │ └── BlendingRanges.php │ │ │ ├── LegacyLayerName │ │ │ │ ├── LegacyLayerNameInterface.php │ │ │ │ └── LegacyLayerName.php │ │ │ ├── Info │ │ │ │ ├── LayerInfo │ │ │ │ │ ├── VectorMask │ │ │ │ │ │ ├── PathRecord │ │ │ │ │ │ │ ├── PathRecordInterface.php │ │ │ │ │ │ │ └── PathRecord.php │ │ │ │ │ │ └── VectorMask.php │ │ │ │ │ ├── EmptyLayerInfo │ │ │ │ │ │ └── EmptyLayerInfo.php │ │ │ │ │ ├── LayerId │ │ │ │ │ │ └── LayerId.php │ │ │ │ │ ├── LayerNameSource │ │ │ │ │ │ └── LayerNameSource.php │ │ │ │ │ ├── VectorStroke │ │ │ │ │ │ └── VectorStroke.php │ │ │ │ │ ├── ObjectEffects │ │ │ │ │ │ └── ObjectEffects.php │ │ │ │ │ ├── GradientFill │ │ │ │ │ │ └── GradientFill.php │ │ │ │ │ ├── VectorOrigination │ │ │ │ │ │ └── VectorOrigination.php │ │ │ │ │ ├── FillOpacity │ │ │ │ │ │ └── FillOpacity.php │ │ │ │ │ ├── BlendInteriorElements │ │ │ │ │ │ └── BlendInteriorElements.php │ │ │ │ │ ├── VectorStrokeContent │ │ │ │ │ │ └── VectorStrokeContent.php │ │ │ │ │ ├── UnicodeName │ │ │ │ │ │ └── UnicodeName.php │ │ │ │ │ ├── BlendClippingElements │ │ │ │ │ │ └── BlendClippingElements.php │ │ │ │ │ ├── NestedSectionDivider │ │ │ │ │ │ └── NestedSectionDivider.php │ │ │ │ │ ├── LayerInfo.php │ │ │ │ │ ├── Locked │ │ │ │ │ │ └── Locked.php │ │ │ │ │ ├── SolidColor │ │ │ │ │ │ └── SolidColor.php │ │ │ │ │ ├── Metadata │ │ │ │ │ │ └── Metadata.php │ │ │ │ │ ├── Artboard │ │ │ │ │ │ └── Artboard.php │ │ │ │ │ ├── SectionDivider │ │ │ │ │ │ └── SectionDivider.php │ │ │ │ │ ├── LayerInfoBase.php │ │ │ │ │ ├── LayerInfoBuilderInterface.php │ │ │ │ │ └── LayerInfoBuilder.php │ │ │ │ ├── InfoInterface.php │ │ │ │ └── Info.php │ │ │ ├── PositionAndChannels │ │ │ │ ├── PositionAndChannelsInterface.php │ │ │ │ └── PositionAndChannels.php │ │ │ ├── LayerInterface.php │ │ │ ├── Mask │ │ │ │ ├── MaskInterface.php │ │ │ │ └── Mask.php │ │ │ ├── BlendMode │ │ │ │ ├── BlendModeInterface.php │ │ │ │ └── BlendMode.php │ │ │ ├── ChannelImage │ │ │ │ └── ChannelImage.php │ │ │ └── Layer.php │ │ ├── Data │ │ │ └── GlobalMask.php │ │ └── LayerMask.php │ ├── Header │ │ ├── HeaderInterface.php │ │ └── Header.php │ └── Image │ │ └── Image.php ├── Image │ ├── ImageFormat │ │ ├── ImageData │ │ │ ├── ImageDataBuilderInterface.php │ │ │ ├── Raw.php │ │ │ ├── ImageDataBase.php │ │ │ ├── Rle.php │ │ │ └── ImageDataBuilder.php │ │ ├── ImageFormatInterface.php │ │ ├── LayerImageData │ │ │ ├── LayerImageDataBuilderInterface.php │ │ │ ├── LayerRaw.php │ │ │ ├── LayerImageDataBase.php │ │ │ ├── LayerRle.php │ │ │ └── LayerImageDataBuilder.php │ │ └── BaseData │ │ │ └── DecodeRLEChannel │ │ │ ├── DecodeRLEChannelInterface.php │ │ │ └── DecodeRLEChannel.php │ ├── ImageExport │ │ ├── Exports │ │ │ ├── ImageExportInterface.php │ │ │ └── Png.php │ │ └── ImageExport.php │ ├── ImageMode │ │ ├── ImageModeInterface.php │ │ ├── Modes │ │ │ ├── Greyscale.php │ │ │ ├── ImageModeBase.php │ │ │ ├── Rgb.php │ │ │ └── Cmyk.php │ │ └── ImageMode.php │ └── ImageChannels │ │ ├── ImageChannels.php │ │ └── RgbaJson.php ├── LazyExecuteProxy │ ├── Interfaces │ │ ├── LazyExecuteInterface.php │ │ ├── LayerInfoInterface.php │ │ ├── LayerMaskInterface.php │ │ ├── ResourcesInterface.php │ │ ├── ImageInterface.php │ │ └── ChannelImageInterface.php │ ├── Proxies │ │ ├── LayerInfoProxy.php │ │ ├── LayerMaskProxy.php │ │ ├── ImageProxy.php │ │ ├── ChannelImageProxy.php │ │ └── ResourcesProxy.php │ └── LazyExecuteProxy.php ├── Descriptor │ ├── DescriptorInterface.php │ ├── Data │ │ ├── RectKey.php │ │ ├── ReferenceData.php │ │ ├── ClassData.php │ │ ├── FilePathData.php │ │ ├── EnumData.php │ │ ├── PropertyData.php │ │ ├── DescriptorData.php │ │ ├── EnumReferenceData.php │ │ └── FloatPointNumberData.php │ ├── Parsers │ │ ├── ReferenceParser │ │ │ ├── ReferenceParserInterface.php │ │ │ └── ReferenceParser.php │ │ └── ItemParser │ │ │ ├── ItemParserInterface.php │ │ │ └── ItemParser.php │ ├── DataMapper │ │ ├── DataMapperInterface.php │ │ └── DataMapper.php │ └── Descriptor.php ├── Shortcuts │ ├── ShortcutsInterface.php │ └── Shortcuts.php ├── Node │ ├── NodeInterface.php │ ├── Group │ │ └── Group.php │ ├── Layer │ │ └── Layer.php │ └── Node.php ├── File │ ├── FileInterface.php │ └── File.php ├── Helper.php └── Psd.php ├── phpunit.xml ├── composer.json ├── README.md └── assets └── logo.svg /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | files 3 | .DS_Store -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src 6 | 7 | 8 | 9 | tests 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/LazyExecuteProxy/Interfaces/ImageInterface.php: -------------------------------------------------------------------------------- 1 | data = $this->file->readInt(); 12 | } 13 | 14 | public function export() 15 | { 16 | return $this->getData(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Image/ImageFormat/ImageData/Raw.php: -------------------------------------------------------------------------------- 1 | file->readBytes($this->header->getFileLength(), function ($val) { 13 | return str_pad($val, 3, "0", STR_PAD_LEFT); 14 | }); 15 | 16 | $this->channelData->setChannelsData(implode($bytes)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Descriptor/Parsers/ReferenceParser/ReferenceParserInterface.php: -------------------------------------------------------------------------------- 1 | data = $this->file->readString(4); 12 | } 13 | 14 | public function export() 15 | { 16 | return $this->getData(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/LazyExecuteProxy/Proxies/LayerInfoProxy.php: -------------------------------------------------------------------------------- 1 | parse(); 13 | return $this->obj->export(); 14 | } 15 | 16 | public function getData() 17 | { 18 | $this->parse(); 19 | return $this->obj->getData(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/VectorStroke/VectorStroke.php: -------------------------------------------------------------------------------- 1 | file->ffseek(4, true); 12 | $this->data = $this->descriptor->parse(); 13 | } 14 | 15 | public function export() 16 | { 17 | return $this->getData(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/ObjectEffects/ObjectEffects.php: -------------------------------------------------------------------------------- 1 | file->ffseek(8, true); 12 | $this->data = $this->descriptor->parse(); 13 | } 14 | 15 | public function export() 16 | { 17 | return $this->getData(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/GradientFill/GradientFill.php: -------------------------------------------------------------------------------- 1 | file->ffseek(4, true); // Skip sig 12 | $this->data = $this->descriptor->parse(); 13 | } 14 | 15 | public function export() 16 | { 17 | return $this->getData(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/VectorOrigination/VectorOrigination.php: -------------------------------------------------------------------------------- 1 | file->ffseek(8, true); 12 | $this->data = $this->descriptor->parse(); 13 | } 14 | 15 | public function export() 16 | { 17 | return $this->getData(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixelfactory/psd-php", 3 | "license": "MIT", 4 | "description": "Library for reading psd file", 5 | "keywords": ["psd", "photoshop", "image"], 6 | "authors": [ 7 | { 8 | "name": "LoginovIlya", 9 | "email": "LoginovIlya@users.noreply.github.com" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "autoload": { 14 | "psr-4":{ 15 | "Psd\\": "src/" 16 | } 17 | }, 18 | "require": { 19 | "ext-imagick": "*", 20 | "ext-iconv": "*" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^9.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/FillOpacity/FillOpacity.php: -------------------------------------------------------------------------------- 1 | data = $this->file->readByte(); 13 | } 14 | 15 | /** 16 | * @throws Exception 17 | */ 18 | public function export() 19 | { 20 | return $this->getData(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/BlendInteriorElements/BlendInteriorElements.php: -------------------------------------------------------------------------------- 1 | data = $this->file->readBoolean(); 12 | $this->file->ffseek(3, true); 13 | } 14 | 15 | public function export() 16 | { 17 | return $this->getData(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/VectorStrokeContent/VectorStrokeContent.php: -------------------------------------------------------------------------------- 1 | file->ffseek(8, true); 12 | $this->data = $this->descriptor->parse(); 13 | } 14 | 15 | 16 | public function export() 17 | { 18 | return $this->getData(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/UnicodeName/UnicodeName.php: -------------------------------------------------------------------------------- 1 | file->tell(); 12 | $this->data = $this->file->readUnicodeString(); 13 | 14 | $this->file->ffseek($pos + $length); 15 | } 16 | 17 | public function export(): string 18 | { 19 | return $this->getData(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/LazyExecuteProxy/Proxies/LayerMaskProxy.php: -------------------------------------------------------------------------------- 1 | parse(); 14 | return $this->obj->getLayers(); 15 | } 16 | 17 | public function getGlobalMask(): GlobalMask 18 | { 19 | $this->parse(); 20 | return $this->obj->getGlobalMask(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/BlendClippingElements/BlendClippingElements.php: -------------------------------------------------------------------------------- 1 | data = $this->file->readBoolean(); 13 | $this->file->ffseek(3, true); 14 | } 15 | 16 | /** 17 | * @throws Exception 18 | */ 19 | public function export() 20 | { 21 | return $this->getData(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Node/Group/Group.php: -------------------------------------------------------------------------------- 1 | name = $name; 14 | } 15 | 16 | public function addData($data): int 17 | { 18 | $this->data[] = $data; 19 | return count($this->data); 20 | } 21 | 22 | public function getData() 23 | { 24 | return $this->data; 25 | } 26 | 27 | public function getName(): string 28 | { 29 | return $this->name; 30 | } 31 | 32 | public function isFolder(): bool 33 | { 34 | return true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/HelperTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($pad2Result, $result); 16 | } 17 | 18 | public function pad2Data(): array 19 | { 20 | return [ 21 | [-4, -4], 22 | [-3, -2], 23 | [-2, -2], 24 | [-1, 0], 25 | [0, 0], 26 | [1, 2], 27 | [2, 2], 28 | [3, 4], 29 | [4, 4] 30 | ]; 31 | } 32 | } -------------------------------------------------------------------------------- /src/LazyExecuteProxy/Proxies/ImageProxy.php: -------------------------------------------------------------------------------- 1 | parse(); 15 | return $this->obj->getExporter($type); 16 | } 17 | 18 | public function getPixelData(): RgbaJson 19 | { 20 | $this->parse(); 21 | return $this->obj->getPixelData(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/PositionAndChannels/PositionAndChannelsInterface.php: -------------------------------------------------------------------------------- 1 | channelData[$i] = $this->file->readByte(); 14 | // } 15 | $bytes = $this->file->readBytes($chanLength - 2, function ($val) { 16 | return str_pad($val, 3, "0", STR_PAD_LEFT); 17 | }); 18 | 19 | $this->channelData->addChannelsData(implode($bytes)); 20 | 21 | return ($chanPos + $chanLength - 2); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Mask/MaskInterface.php: -------------------------------------------------------------------------------- 1 | parse(); 15 | return $this->obj->getExporter($type); 16 | } 17 | 18 | public function getLayerData(): array 19 | { 20 | $this->parse(); 21 | return $this->obj->getLayerData(); 22 | } 23 | 24 | public function getPixelData(): RgbaJson 25 | { 26 | $this->parse(); 27 | return $this->obj->getPixelData(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Descriptor/Data/ReferenceData.php: -------------------------------------------------------------------------------- 1 | type = $type; 17 | return $this; 18 | } 19 | 20 | /** 21 | * @param $value 22 | * @return $this 23 | */ 24 | public function setValue($value): self 25 | { 26 | $this->value = $value; 27 | return $this; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getType(): string 34 | { 35 | return $this->type; 36 | } 37 | 38 | /** 39 | * @return mixed 40 | */ 41 | public function getValue() 42 | { 43 | return $this->value; 44 | } 45 | } -------------------------------------------------------------------------------- /src/LazyExecuteProxy/Proxies/ResourcesProxy.php: -------------------------------------------------------------------------------- 1 | parse(); 13 | return $this->obj->getResources(); 14 | } 15 | 16 | public function getResource($search) 17 | { 18 | $this->parse(); 19 | return $this->obj->getResource($search); 20 | } 21 | 22 | public function getResourceByName(string $name) 23 | { 24 | $this->parse(); 25 | return $this->obj->getResourceByName($name); 26 | } 27 | 28 | public function getResourceById($id) 29 | { 30 | $this->parse(); 31 | return $this->obj->getResourceById($id); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Descriptor/Data/ClassData.php: -------------------------------------------------------------------------------- 1 | id; 18 | } 19 | 20 | /** 21 | * @return string 22 | */ 23 | public function getName(): string 24 | { 25 | return $this->name; 26 | } 27 | 28 | /** 29 | * @param string $id 30 | * @return $this 31 | */ 32 | public function setId(string $id): self 33 | { 34 | $this->id = $id; 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * @param string $name 41 | * @return $this 42 | */ 43 | public function setName(string $name): self 44 | { 45 | $this->name = $name; 46 | 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Descriptor/Data/FilePathData.php: -------------------------------------------------------------------------------- 1 | sig = $sig; 19 | return $this; 20 | } 21 | 22 | /** 23 | * @param string $path 24 | * @return $this 25 | */ 26 | public function setPath(string $path): self 27 | { 28 | $this->path = $path; 29 | return $this; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getSig(): string 36 | { 37 | return $this->sig; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getPath(): string 44 | { 45 | return $this->path; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Descriptor/Data/EnumData.php: -------------------------------------------------------------------------------- 1 | type = $type; 19 | 20 | return $this; 21 | } 22 | 23 | /** 24 | * @param string $value 25 | * @return $this 26 | */ 27 | public function setValue(string $value): self 28 | { 29 | $this->value = $value; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getType(): string 38 | { 39 | return $this->type; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getValue(): string 46 | { 47 | return $this->value; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/LegacyLayerName/LegacyLayerName.php: -------------------------------------------------------------------------------- 1 | file = $file; 19 | } 20 | 21 | public function parse(): void 22 | { 23 | $len = Helper::pad4($this->file->readByte()); 24 | $this->legacyName = $this->file->readString($len); 25 | } 26 | 27 | public function getLegacyName(): string 28 | { 29 | if (!isset($this->legacyName)) { 30 | throw new Exception('LegacyLayerName not parsed. LegacyName is undefined.'); 31 | } 32 | 33 | return $this->legacyName; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Image/ImageFormat/LayerImageData/LayerImageDataBase.php: -------------------------------------------------------------------------------- 1 | file = $file; 20 | $this->header = $header; 21 | $this->channelData = $channelDataa; 22 | } 23 | 24 | abstract protected function parseData(int $chanPos, int $chanLength, int $height): int; 25 | 26 | public function parse(int $chanPos, int $chanLength, int $height): int 27 | { 28 | return $this->parseData($chanPos, $chanLength, $height); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Descriptor/Data/PropertyData.php: -------------------------------------------------------------------------------- 1 | classData = $classData; 19 | return $this; 20 | } 21 | 22 | /** 23 | * @param string $id 24 | * @return $this 25 | */ 26 | public function setId(string $id): self 27 | { 28 | $this->id = $id; 29 | return $this; 30 | } 31 | 32 | /** 33 | * @return ClassData 34 | */ 35 | public function getClassData(): ClassData 36 | { 37 | return $this->classData; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getId(): string 44 | { 45 | return $this->id; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Image/ImageFormat/ImageData/ImageDataBase.php: -------------------------------------------------------------------------------- 1 | file = $file; 20 | $this->header = $header; 21 | //$this->channelData = []; //array_fill(0, $this->header->getFileLength(), 0); 22 | 23 | $this->channelData = new ImageChannels($this->header->getChannelLength()); 24 | } 25 | 26 | abstract protected function parseData(): void; 27 | 28 | public function parse(): ImageChannels 29 | { 30 | $this->parseData(); 31 | 32 | return $this->channelData; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/FileStructure/Resources/Resource/Guides/GuidesData.php: -------------------------------------------------------------------------------- 1 | location = $location; 17 | return $this; 18 | } 19 | 20 | /** 21 | * @param string $direction 22 | * @return $this 23 | */ 24 | public function setDirection(string $direction): self 25 | { 26 | $this->direction = $direction; 27 | return $this; 28 | } 29 | 30 | /** 31 | * @return int 32 | */ 33 | public function getLocation(): int 34 | { 35 | return $this->location; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getDirection(): string 42 | { 43 | return $this->direction; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/NestedSectionDivider/NestedSectionDivider.php: -------------------------------------------------------------------------------- 1 | file->readInt(); 13 | $data = [ 14 | 'isFolder' => false, 15 | 'isHidden' => false, 16 | ]; 17 | 18 | if ($code === 1 || $code === 2) { 19 | $data['isFolder'] = true; 20 | } else if ($code === 3) { 21 | $data['isHidden'] = true; 22 | } else { 23 | throw new Exception(sprintf('NestedSectionDivider error. Not supported code: %s', $code)); 24 | } 25 | 26 | $this->data = $data; 27 | } 28 | 29 | public function export() 30 | { 31 | return $this->getData(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/FileStructure/Resources/Resource/LayerComps/LayerComps.php: -------------------------------------------------------------------------------- 1 | file->ffseek(4, true); 22 | $descriptorData = (new Descriptor($this->file))->parse(); 23 | 24 | foreach ($descriptorData as $comp) { 25 | $this->data[] = (new LayerCompsData()) 26 | ->setId($comp->getData()[static::KEY_COMP_ID]) 27 | ->setName($comp->getData()[static::KEY_NAME]) 28 | ->setCapturedInfo($comp->getData()[static::KEY_CAPTURED_INFO]); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/LayerInfo.php: -------------------------------------------------------------------------------- 1 | layerInfo = $layerInfo; 17 | return $this; 18 | } 19 | 20 | /** 21 | * @param string $name 22 | * @return $this 23 | */ 24 | public function setName(string $name): self 25 | { 26 | $this->name = $name; 27 | return $this; 28 | } 29 | 30 | /** 31 | * @return LayerInfoBase 32 | */ 33 | public function getLayerInfo(): LayerInfoBase 34 | { 35 | return $this->layerInfo; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getName(): string 42 | { 43 | return $this->name; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/FileStructure/Resources/Resource/ResolutionInfo/ResolutionInfo.php: -------------------------------------------------------------------------------- 1 | file->readUInt() / 65536; 13 | $hResUnit = $this->file->readUShort(); 14 | $widthUnit = $this->file->readUShort(); 15 | 16 | // 32-bit fixed-point number (16.16) 17 | $vRes = $this->file->readUInt() / 65536; 18 | $vResUnit = $this->file->readUShort(); 19 | $heightUnit = $this->file->readUShort(); 20 | 21 | $this->data = (new ResolutionInfoData()) 22 | ->setHRes($hRes) 23 | ->setHResUnit($hResUnit) 24 | ->setWidthUnit($widthUnit) 25 | ->setVRes($vRes) 26 | ->setVResUnit($vResUnit) 27 | ->setHeightUnit($heightUnit); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/File/FileInterface.php: -------------------------------------------------------------------------------- 1 | file->ffseek(4, true); 17 | 18 | // Future implementation of document-specific grids 19 | $this->file->ffseek(8, true); 20 | 21 | $numGuides = $this->file->readInt(); 22 | $this->data = []; 23 | 24 | for ($i = 0; $i < $numGuides; $i += 1) { 25 | $location = Helper::fixed($this->file->readInt() / 32, 1); 26 | $direction = $this->file->readByte() ? static::DIRECTION_HORIZONTAL : static::DIRECTION_VERTICAL; 27 | 28 | $this->data[] = (new GuidesData())->setLocation($location)->setDirection($direction); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Descriptor/Data/DescriptorData.php: -------------------------------------------------------------------------------- 1 | classData = $classData; 20 | return $this; 21 | } 22 | 23 | /** 24 | * @param array $data 25 | * @return $this 26 | */ 27 | public function setData(array $data): self 28 | { 29 | $this->data = $data; 30 | return $this; 31 | } 32 | 33 | public function addData(string $key, $value) 34 | { 35 | $this->data[$key] = $value; 36 | } 37 | 38 | /** 39 | * @return mixed 40 | */ 41 | public function getClassData() 42 | { 43 | return $this->classData; 44 | } 45 | 46 | /** 47 | * @return array 48 | */ 49 | public function getData(): array 50 | { 51 | return $this->data; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/Locked/Locked.php: -------------------------------------------------------------------------------- 1 | file->readInt(); 13 | 14 | $transparencyLocked = (($locked & (0x01 << 0)) > 0) || ($locked === -2147483648); 15 | $compositeLocked = (($locked & (0x01 << 1)) > 0) || ($locked === -2147483648); 16 | $positionLocked = (($locked & (0x01 << 2)) > 0) || ($locked === -2147483648); 17 | 18 | $this->data = [ 19 | 'transparencyLocked' => $transparencyLocked, 20 | 'compositeLocked' => $compositeLocked, 21 | 'positionLocked' => $positionLocked, 22 | 'allLocked' => ($transparencyLocked && $compositeLocked && $positionLocked), 23 | ]; 24 | } 25 | 26 | public function export() 27 | { 28 | return $this->getData(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Shortcuts/Shortcuts.php: -------------------------------------------------------------------------------- 1 | psd = $psd; 16 | } 17 | 18 | public function getWidth(): int 19 | { 20 | return $this->psd->getHeader()->getWidth(); 21 | } 22 | 23 | public function getHeight(): int 24 | { 25 | return $this->psd->getHeader()->getHeight(); 26 | } 27 | 28 | public function savePreview(string $fileName): bool 29 | { 30 | return $this->psd->getImage()->getExporter(\Psd\Image\ImageExport\ImageExport::EXPORT_FORMAT_PNG)->save($fileName); 31 | } 32 | 33 | public function getTree(): NodeInterface 34 | { 35 | if (!isset($this->node)) { 36 | $this->node = $this->buildNode($this->psd->getLayers()); 37 | } 38 | 39 | return $this->node; 40 | } 41 | 42 | protected function buildNode(array $layers): NodeInterface 43 | { 44 | return Node::build($layers); 45 | } 46 | } -------------------------------------------------------------------------------- /src/Image/ImageMode/Modes/Greyscale.php: -------------------------------------------------------------------------------- 1 | 0]]; 14 | 15 | if ($channels === 2) { 16 | $channelsInfo[] = ['id' => -1]; 17 | } 18 | 19 | $this->channelsInfo = $channelsInfo; 20 | } 21 | 22 | public function combineChannel(): RgbaJson 23 | { 24 | for ($i = 0; $i < $this->numPixels; $i += 1) { 25 | $grey = (int)$this->channelData->getChanelData($i); 26 | 27 | $a = ($this->channels === 2) 28 | ? $this->channelData->getChanelData($i + 1) 29 | : 255; 30 | 31 | [, $r, $g, $b] = array_map(function (string $color): string { 32 | return str_pad($color, 3, "0", STR_PAD_LEFT); 33 | }, Helper::colorToArgb($grey * 0x00010101)); 34 | 35 | $this->pixelData->addRgba($r, $g, $b, $a); 36 | } 37 | 38 | return $this->pixelData; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/LazyExecuteProxy/LazyExecuteProxy.php: -------------------------------------------------------------------------------- 1 | obj = $obj; 21 | $this->file = $file; 22 | 23 | // Save start position 24 | $this->startPos = $this->file->tell(); 25 | // Skip parsing 26 | $this->obj->skip(); 27 | $this->parsed = false; 28 | } 29 | 30 | public function parse(): void 31 | { 32 | if ($this->parsed) { 33 | return; 34 | } 35 | 36 | $origPos = $this->file->tell(); 37 | $this->file->ffseek($this->startPos); 38 | 39 | $this->obj->parse(); 40 | 41 | $this->file->ffseek($origPos); 42 | $this->parsed = true; 43 | } 44 | 45 | public function skip(): void 46 | { 47 | $this->obj->skip(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/SolidColor/SolidColor.php: -------------------------------------------------------------------------------- 1 | file->ffseek(4, true); 22 | $this->data = $this->descriptor->parse(); 23 | } 24 | 25 | public function export(): array 26 | { 27 | return [ 28 | 'r' => round($this->getColorObject()['data'][self::DATA_KEY_RED]), 29 | 'g' => round($this->getColorObject()['data'][self::DATA_KEY_GREEN]), 30 | 'b' => round($this->getColorObject()['data'][self::DATA_KEY_BLUE]), 31 | ]; 32 | } 33 | 34 | protected function getColorObject(): array 35 | { 36 | return $this->getData()['data'][self::DATA_KEY_CLR]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Image/ImageMode/Modes/ImageModeBase.php: -------------------------------------------------------------------------------- 1 | header = $header; 34 | $this->pixelData = new RgbaJson(); 35 | $this->channelData = $channelData; 36 | $this->numPixels = $numPixels; 37 | $this->channelLength = $channelLength; 38 | $this->channels = $channels; 39 | 40 | $this->initChannelsInfo($channels); 41 | } 42 | 43 | abstract public function initChannelsInfo(int $channels): void; 44 | 45 | abstract public function combineChannel(): RgbaJson; 46 | } 47 | -------------------------------------------------------------------------------- /src/FileStructure/Resources/Resource/LayerComps/LayerCompsData.php: -------------------------------------------------------------------------------- 1 | id = $id; 18 | return $this; 19 | } 20 | 21 | /** 22 | * @param $name 23 | * @return $this 24 | */ 25 | public function setName($name): self 26 | { 27 | $this->name = $name; 28 | return $this; 29 | } 30 | 31 | /** 32 | * @param $capturedInfo 33 | * @return $this 34 | */ 35 | public function setCapturedInfo($capturedInfo): self 36 | { 37 | $this->capturedInfo = $capturedInfo; 38 | return $this; 39 | } 40 | 41 | /** 42 | * @return mixed 43 | */ 44 | public function getId() 45 | { 46 | return $this->id; 47 | } 48 | 49 | /** 50 | * @return mixed 51 | */ 52 | public function getName() 53 | { 54 | return $this->name; 55 | } 56 | 57 | /** 58 | * @return mixed 59 | */ 60 | public function getCapturedInfo() 61 | { 62 | return $this->capturedInfo; 63 | } 64 | } -------------------------------------------------------------------------------- /src/Helper.php: -------------------------------------------------------------------------------- 1 | > 24, 32 | (($color) & 0x00FF0000) >> 16, 33 | (($color) & 0x0000FF00) >> 8, 34 | ($color) & 0x000000FF, 35 | ]; 36 | } 37 | 38 | public static function clamp(int $num, int $min = 0, int $max = 255): int 39 | { 40 | return min(max($num, $min), $max); 41 | } 42 | 43 | public static function fixed(float $num, int $fractionDigits = 0): int 44 | { 45 | return intval($num * $fractionDigits) * $fractionDigits; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Image/ImageMode/ImageMode.php: -------------------------------------------------------------------------------- 1 | header = $header; 19 | } 20 | 21 | public function build($channelData, int $channels, int $numPixels, int $channelLength): ImageModeBase 22 | { 23 | $mode = $this->header->getMode(); 24 | 25 | if ($mode === HeaderInterface::HEADER_MODE_KEY_CMYK_COLOR) { 26 | new Cmyk($this->header, $channelData, $channels, $numPixels, $channelLength); 27 | } 28 | if ($mode === HeaderInterface::HEADER_MODE_KEY_GRAY_SCALE) { 29 | return new Greyscale($this->header, $channelData, $channels, $numPixels, $channelLength); 30 | } 31 | if ($mode === HeaderInterface::HEADER_MODE_KEY_RGB_COLOR) { 32 | return new Rgb($this->header, $channelData, $channels, $numPixels, $channelLength); 33 | } 34 | 35 | throw new Exception(sprintf('Error mode: %s', $mode)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/Metadata/Metadata.php: -------------------------------------------------------------------------------- 1 | file->readInt(); 17 | 18 | for ($i = 0; $i < $count; $i++) { 19 | $this->file->ffseek(4, true); 20 | 21 | $key = $this->file->readString(4); 22 | $this->file->readByte(); 23 | 24 | $this->file->ffseek(3, true); // padding 25 | 26 | $len = $this->file->readInt(); 27 | $end = $this->file->tell() + $len; 28 | 29 | if ($key === static::LAYER_COMPS) { 30 | $this->file->ffseek(4, true); 31 | $this->buildDescriptor($this->file)->parse(); 32 | } 33 | 34 | $this->file->ffseek($end); 35 | } 36 | } 37 | 38 | public function export(): void 39 | { 40 | } 41 | 42 | protected function buildDescriptor(FileInterface $file): DescriptorInterface 43 | { 44 | return new Descriptor($file); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Descriptor/Data/EnumReferenceData.php: -------------------------------------------------------------------------------- 1 | classData = $classData; 18 | return $this; 19 | } 20 | 21 | /** 22 | * @param string $type 23 | * @return $this 24 | */ 25 | public function setType(string $type): self 26 | { 27 | $this->type = $type; 28 | return $this; 29 | } 30 | 31 | /** 32 | * @param string $value 33 | * @return $this 34 | */ 35 | public function setValue(string $value): self 36 | { 37 | $this->value = $value; 38 | return $this; 39 | } 40 | 41 | /** 42 | * @return ClassData 43 | */ 44 | public function getClassData(): ClassData 45 | { 46 | return $this->classData; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getType(): string 53 | { 54 | return $this->type; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getValue(): string 61 | { 62 | return $this->value; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/Artboard/Artboard.php: -------------------------------------------------------------------------------- 1 | file->ffseek(4, true); 26 | $this->data = $this->descriptor->parse(); 27 | } 28 | 29 | public function export() 30 | { 31 | return [ 32 | 'coords' => [ 33 | 'left' => $this->getArtboardRectData()['data'][static::RECT_KEY_LEFT], 34 | 'top' => $this->getArtboardRectData()['data'][static::RECT_KEY_TOP], 35 | 'right' => $this->getArtboardRectData()['data'][static::RECT_KEY_RIGHT], 36 | 'bottom' => $this->getArtboardRectData()['data'][static::RECT_KEY_BOTTOM], 37 | ] 38 | ]; 39 | } 40 | 41 | protected function getArtboardRectData(): array 42 | { 43 | return $this->getData()['data']['artboardRect']; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Descriptor/DataMapper/DataMapperInterface.php: -------------------------------------------------------------------------------- 1 | file = $file; 21 | } 22 | 23 | /** 24 | * @throws Exception 25 | */ 26 | public function parse(): ResourceBase 27 | { 28 | $type = $this->file->readString(4); 29 | 30 | if ($type !== static::SIGNATURE) { 31 | throw new Exception('Wrong resource data.'); 32 | } 33 | 34 | $id = $this->file->readShort(); 35 | 36 | $resource = new EmptyResource($this->file, $id); 37 | 38 | if ($id === ResourceBase::RESOURCE_ID_GUIDES) { 39 | $resource = new Guides($this->file, $id); 40 | } else if ($id === ResourceBase::RESOURCE_ID_LAYER_COMPS) { 41 | $resource = new LayerComps($this->file, $id); 42 | } else if ($id === ResourceBase::RESOURCE_ID_RESOLUTION_INFO) { 43 | $resource = new ResolutionInfo($this->file, $id); 44 | } 45 | 46 | $resource->parseResource(); 47 | 48 | return $resource; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Descriptor/Data/FloatPointNumberData.php: -------------------------------------------------------------------------------- 1 | '#Ang', 10 | 'Density' => '#Rsl', 11 | 'Distance' => '#Rlt', 12 | 'None' => '#Nne', 13 | 'Percent' => '#Prc', 14 | 'Pixels' => '#Pxl', 15 | 'Millimeters' => '#Mlm', 16 | 'Points' => '#Pnt', 17 | ]; 18 | 19 | protected string $id; 20 | protected string $unit; 21 | protected float $value; 22 | 23 | /** 24 | * @param string $id 25 | * @return $this 26 | */ 27 | public function setId(string $id): self 28 | { 29 | $this->id = $id; 30 | return $this; 31 | } 32 | 33 | /** 34 | * @param string $unit 35 | * @return $this 36 | */ 37 | public function setUnit(string $unit): self 38 | { 39 | $this->unit = $unit; 40 | return $this; 41 | } 42 | 43 | /** 44 | * @param float $value 45 | * @return $this 46 | */ 47 | public function setValue(float $value): self 48 | { 49 | $this->value = $value; 50 | return $this; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getId(): string 57 | { 58 | return $this->id; 59 | } 60 | 61 | /** 62 | * @return string 63 | */ 64 | public function getUnit(): string 65 | { 66 | return $this->unit; 67 | } 68 | 69 | /** 70 | * @return float 71 | */ 72 | public function getValue(): float 73 | { 74 | return $this->value; 75 | } 76 | } -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/BlendMode/BlendModeInterface.php: -------------------------------------------------------------------------------- 1 | 'normal', 9 | 'dark' => 'darken', 10 | 'lite' => 'lighten', 11 | 'hue' => 'hue', 12 | 'sat' => 'saturation', 13 | 'colr' => 'color', 14 | 'lum' => 'luminosity', 15 | 'mul' => 'multiply', 16 | 'scrn' => 'screen', 17 | 'diss' => 'dissolve', 18 | 'over' => 'overlay', 19 | 'hLit' => 'hard_light', 20 | 'sLit' => 'soft_light', 21 | 'diff' => 'difference', 22 | 'smud' => 'exclusion', 23 | 'div' => 'color_dodge', 24 | 'idiv' => 'color_burn', 25 | 'lbrn' => 'linear_burn', 26 | 'lddg' => 'linear_dodge', 27 | 'vLit' => 'vivid_light', 28 | 'lLit' => 'linear_light', 29 | 'pLit' => 'pin_light', 30 | 'hMix' => 'hard_mix', 31 | 'pass' => 'passthru', 32 | 'dkCl' => 'darker_color', 33 | 'lgCl' => 'lighter_color', 34 | 'fsub' => 'subtract', 35 | 'fdiv' => 'divide', 36 | ]; 37 | 38 | public function parse(): void; 39 | 40 | public function getBlendKey(): string; 41 | 42 | public function getOpacity(): float; 43 | 44 | public function getClipping(): float; 45 | 46 | public function getFlags(): float; 47 | 48 | public function getMode(): string; 49 | 50 | public function getClipped(): bool; 51 | 52 | public function getVisible(): bool; 53 | 54 | public function opacityPercentage(): float; 55 | 56 | public function parseBlendKey(): string; 57 | } 58 | -------------------------------------------------------------------------------- /src/Image/ImageFormat/LayerImageData/LayerRle.php: -------------------------------------------------------------------------------- 1 | decodeRLEChannel = $this->buildDecodeRLEChannel($file); 19 | } 20 | 21 | protected function parseData(int $chanPos, int $chanLength, int $height): int 22 | { 23 | $byteCounts = $this->parseByteCounts($height); 24 | return $this->parseChannelData($byteCounts, $chanPos, $height); 25 | } 26 | 27 | protected function parseByteCounts(int $height): array 28 | { 29 | $data = []; 30 | 31 | for ($i = 0; $i < $height; $i++) { 32 | $data[] = $this->file->readShort(); 33 | } 34 | 35 | return $data; 36 | } 37 | 38 | protected function parseChannelData(array $byteCounts, int $chanPos, int $height): int 39 | { 40 | return $this->decodeRLEChannel->decode( 41 | $this->channelData, 42 | $chanPos, 43 | $height, 44 | 0, 45 | $byteCounts 46 | ); 47 | } 48 | 49 | protected function buildDecodeRLEChannel(FileInterface $file): DecodeRLEChannelInterface 50 | { 51 | return new DecodeRLEChannel($file); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/SectionDivider/SectionDivider.php: -------------------------------------------------------------------------------- 1 | file->readInt(); 24 | 25 | if (!isset(static::SECTION_DIVIDER_TYPES[$code])) { 26 | throw new Exception(sprintf('SectionDivider error. Wrong code: %s', $code)); 27 | } 28 | 29 | $layerType = static::SECTION_DIVIDER_TYPES[$code]; 30 | 31 | $isFolder = ($code === 1 || $code === 2); 32 | $isHidden = ($code === 3); 33 | $blendMode = ''; 34 | $subType = ''; 35 | 36 | if ($length >= 12) { 37 | $this->file->ffseek(4, true); 38 | $blendMode = $this->file->readString(4); 39 | 40 | if ($length >= 16) { 41 | $subType = ($this->file->readInt() === 0) ? static::SUB_TYPE_NORMAL : static::SUB_TYPE_SCENE_GROUP; 42 | } 43 | } 44 | 45 | $this->data = [ 46 | 'isFolder' => $isFolder, 47 | 'isHidden' => $isHidden, 48 | 'layerType' => $layerType, 49 | 'blendMode' => $blendMode, 50 | 'subType' => $subType 51 | ]; 52 | } 53 | 54 | public function export() 55 | { 56 | return $this->getData(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Image/ImageFormat/BaseData/DecodeRLEChannel/DecodeRLEChannel.php: -------------------------------------------------------------------------------- 1 | file = $file; 15 | } 16 | 17 | /** 18 | * @throws \Exception 19 | */ 20 | public function decode(ImageChannels $channelData, int $chanPos, int $height, int $lineIndex, array $byteCounts): int 21 | { 22 | $chanPosSum = $chanPos; 23 | 24 | for ($j = 0; $j < $height; $j++) { 25 | 26 | $byte_count = $byteCounts[$lineIndex + $j]; 27 | $finish = $this->file->ftell() + $byte_count; 28 | 29 | while ($this->file->ftell() < $finish) { 30 | $len = $this->file->readByte(); 31 | $array = []; 32 | 33 | if ($len < 128) { 34 | $len += 1; 35 | 36 | //Read many bytes 37 | $array = $this->file->readBytes($len, function ($val) { 38 | return str_pad($val, 3, "0", STR_PAD_LEFT); 39 | }); 40 | } elseif ($len > 128) { 41 | $len ^= 0xff; 42 | $len += 2; 43 | 44 | $val = $this->file->readByte(); 45 | $val = str_pad($val, 3, "0", STR_PAD_LEFT); 46 | $array = array_fill(0, $len, $val); 47 | } 48 | 49 | $channelData->addChannelsData(implode($array)); 50 | 51 | $chanPosSum += $len; 52 | } 53 | } 54 | return $chanPosSum; 55 | } 56 | } -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/LayerInfoBase.php: -------------------------------------------------------------------------------- 1 | file = $file; 23 | $this->descriptor = $this->buildDescriptor($file); 24 | } 25 | 26 | public function parse(): void 27 | { 28 | $length = $this->readLength($this->file); 29 | $layerInfoEnd = $this->file->tell() + $length; 30 | 31 | $this->parseData($length); 32 | 33 | $this->file->ffseek($layerInfoEnd); 34 | } 35 | 36 | public function skip(): void 37 | { 38 | $this->file->ffseek($this->readLength($this->file), true); 39 | } 40 | 41 | abstract protected function parseData(int $length): void; 42 | 43 | abstract public function export(); 44 | 45 | /** 46 | * @throws Exception 47 | */ 48 | public function getData() 49 | { 50 | if (!isset($this->data)) { 51 | throw new Exception(sprintf('Data is undefined. Class: %s', get_class($this))); 52 | } 53 | 54 | return $this->data; 55 | } 56 | 57 | protected function readLength(FileInterface $file): int 58 | { 59 | return Helper::pad2($file->readInt()); 60 | } 61 | 62 | protected function buildDescriptor(FileInterface $file): DescriptorInterface 63 | { 64 | return new Descriptor($file); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Image/ImageMode/Modes/Rgb.php: -------------------------------------------------------------------------------- 1 | 0], 14 | ['id' => 1], 15 | ['id' => 2], 16 | ]; 17 | 18 | if ($channels === 4) { 19 | $channelsInfo[] = ['id' => -1]; 20 | } 21 | 22 | $this->channelsInfo = $channelsInfo; 23 | } 24 | 25 | public function combineChannel(): RgbaJson 26 | { 27 | $rgbChannels = array_filter( 28 | array_map(static function ($ch) { 29 | return $ch['id']; 30 | }, $this->channelsInfo), static function ($ch) { 31 | return $ch >= -1; 32 | }); 33 | 34 | for ($i = 0; $i < $this->numPixels; $i += 1) { 35 | $r = $g = $b = 0; 36 | $a = 255; 37 | 38 | foreach ($rgbChannels as $index => $chan) { 39 | $pos = $i + ($this->channelData->getChannelLength() * $index); 40 | 41 | $val = $this->channelData->getChanelData($pos); 42 | 43 | switch ($chan) { 44 | case -1: 45 | $a = $val; 46 | break; 47 | case 0: 48 | $r = $val; 49 | break; 50 | case 1: 51 | $g = $val; 52 | break; 53 | case 2: 54 | $b = $val; 55 | break; 56 | } 57 | } 58 | 59 | $this->pixelData->addRgba($r, $g, $b, $a); 60 | } 61 | 62 | return $this->pixelData; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/BlendingRanges/BlendingRanges.php: -------------------------------------------------------------------------------- 1 | file = $file; 16 | } 17 | 18 | public function parse(): void 19 | { 20 | $length = $this->file->readInt(); 21 | if ($length === 0) { 22 | return; 23 | } 24 | 25 | $grey = [ 26 | 'source' => [ 27 | 'black' => [$this->file->readByte(), $this->file->readByte()], 28 | 'white' => [$this->file->readByte(), $this->file->readByte()], 29 | ], 30 | 'dest' => [ 31 | 'black' => [$this->file->readByte(), $this->file->readByte()], 32 | 'white' => [$this->file->readByte(), $this->file->readByte()], 33 | ], 34 | ]; 35 | 36 | $numChannels = ($length - 8) / 8; 37 | 38 | $channels = []; 39 | 40 | for ($i = 0; $i < $numChannels; $i++) { 41 | $channels[] = [ 42 | 'source' => [ 43 | 'black' => [$this->file->readByte(), $this->file->readByte()], 44 | 'white' => [$this->file->readByte(), $this->file->readByte()], 45 | ], 46 | 'dest' => [ 47 | 'black' => [$this->file->readByte(), $this->file->readByte()], 48 | 'white' => [$this->file->readByte(), $this->file->readByte()], 49 | ], 50 | ]; 51 | } 52 | 53 | $this->blendingRanges = [ 54 | 'grey' => $grey, 55 | 'channels' => $channels, 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Image/ImageFormat/ImageData/Rle.php: -------------------------------------------------------------------------------- 1 | decodeRLEChannel = $this->buildDecodeRLEChannel($file); 19 | } 20 | 21 | protected function parseData(): void 22 | { 23 | $byteCounts = $this->parseByteCounts(); 24 | $this->parseChannelData($byteCounts); 25 | } 26 | 27 | protected function parseByteCounts(): array 28 | { 29 | $byteCounts = []; 30 | 31 | for ($i = 0; $i < ($this->header->getChannels() * $this->header->getHeight()); $i += 1) { 32 | $byteCounts[] = $this->file->readShort(); 33 | } 34 | 35 | return $byteCounts; 36 | } 37 | 38 | protected function parseChannelData(array $byteCounts): void 39 | { 40 | $lineIndex = 0; 41 | $chanPos = 0; 42 | 43 | for ($i = 0; $i < $this->header->getChannels(); $i++) { 44 | $chanPos = $this->decodeRLEChannel->decode( 45 | $this->channelData, 46 | $chanPos, 47 | $this->header->getHeight(), 48 | $lineIndex, 49 | $byteCounts 50 | ); 51 | 52 | $lineIndex += $this->header->getHeight(); 53 | } 54 | } 55 | 56 | protected function buildDecodeRLEChannel(FileInterface $file): DecodeRLEChannelInterface 57 | { 58 | return new DecodeRLEChannel($file); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Data/GlobalMask.php: -------------------------------------------------------------------------------- 1 | overlayColorSpace = $overlayColorSpace; 19 | return $this; 20 | } 21 | 22 | /** 23 | * @param array $colorComponents 24 | * @return $this 25 | */ 26 | public function setColorComponents(array $colorComponents): self 27 | { 28 | $this->colorComponents = $colorComponents; 29 | return $this; 30 | } 31 | 32 | /** 33 | * @param float $opacity 34 | * @return $this 35 | */ 36 | public function setOpacity(float $opacity): self 37 | { 38 | $this->opacity = $opacity; 39 | return $this; 40 | } 41 | 42 | /** 43 | * @param int $kind 44 | * @return $this 45 | */ 46 | public function setKind(int $kind): self 47 | { 48 | $this->kind = $kind; 49 | return $this; 50 | } 51 | 52 | /** 53 | * @return int 54 | */ 55 | public function getOverlayColorSpace(): int 56 | { 57 | return $this->overlayColorSpace; 58 | } 59 | 60 | /** 61 | * @return array 62 | */ 63 | public function getColorComponents(): array 64 | { 65 | return $this->colorComponents; 66 | } 67 | 68 | /** 69 | * @return float 70 | */ 71 | public function getOpacity(): float 72 | { 73 | return $this->opacity; 74 | } 75 | 76 | /** 77 | * @return int 78 | */ 79 | public function getKind(): int 80 | { 81 | return $this->kind; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Node/Layer/Layer.php: -------------------------------------------------------------------------------- 1 | name = $name; 30 | $this->top = $top; 31 | $this->right = $right; 32 | $this->bottom = $bottom; 33 | $this->left = $left; 34 | $this->width = $width; 35 | $this->height = $height; 36 | $this->layerData = $layerData; 37 | } 38 | 39 | public function isFolder(): bool 40 | { 41 | return false; 42 | } 43 | 44 | 45 | public function getName(): string 46 | { 47 | return $this->name; 48 | } 49 | 50 | public function getTop(): int 51 | { 52 | return $this->top; 53 | } 54 | 55 | public function getRight(): int 56 | { 57 | return $this->right; 58 | } 59 | 60 | public function getBottom(): int 61 | { 62 | return $this->bottom; 63 | } 64 | 65 | public function getLeft(): int 66 | { 67 | return $this->left; 68 | } 69 | 70 | public function getWidth(): int 71 | { 72 | return $this->width; 73 | } 74 | 75 | public function getHeight(): int 76 | { 77 | return $this->height; 78 | } 79 | 80 | public function getLayerData(): LayerInterface 81 | { 82 | return $this->layerData; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Descriptor/Parsers/ReferenceParser/ReferenceParser.php: -------------------------------------------------------------------------------- 1 | dataMapper = $dataMapper; 16 | } 17 | 18 | /** 19 | * @throws Exception 20 | */ 21 | public function parse(): array 22 | { 23 | $numItems = $this->dataMapper->parseInteger(); 24 | $items = []; 25 | 26 | for ($i = 0; $i < $numItems; $i++) { 27 | $type = $this->dataMapper->parseType(); 28 | 29 | if ($type === static::REFERENCE_TYPE_PROP) { 30 | $value = $this->dataMapper->parseProperty(); 31 | } else if ($type === static::REFERENCE_TYPE_CLSS) { 32 | $value = $this->dataMapper->parseClass(); 33 | } else if ($type === static::REFERENCE_TYPE_ENMR) { 34 | $value = $this->dataMapper->parseEnumReference(); 35 | } else if ($type === static::REFERENCE_TYPE_IDNT) { 36 | $value = $this->dataMapper->parseIdentifier(); 37 | } else if ($type === static::REFERENCE_TYPE_INDX) { 38 | $value = $this->dataMapper->parseIndex(); 39 | } else if ($type === static::REFERENCE_TYPE_NAME) { 40 | $value = $this->dataMapper->parseText(); 41 | } else if ($type === static::REFERENCE_TYPE_RELE) { 42 | $value = $this->dataMapper->parseOffset(); 43 | } else { 44 | throw new Exception('Wrong reference type.'); 45 | } 46 | 47 | $items[] = (new ReferenceData())->setType($type)->setValue($value); 48 | } 49 | 50 | return $items; 51 | } 52 | } -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/LayerInfoBuilderInterface.php: -------------------------------------------------------------------------------- 1 | channelLength = $channelLength; 20 | } 21 | 22 | /** 23 | * @return int 24 | */ 25 | public function getChannelLength(): int 26 | { 27 | return $this->channelLength; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getChannelsData(): string 34 | { 35 | return $this->channelsData; 36 | } 37 | 38 | /** 39 | * @param int $position 40 | * @return string 41 | */ 42 | public function getChanelData(int $position): string 43 | { 44 | return substr($this->channelsData, $position * self::CHANNEL_DATA_LENGTH, self::CHANNEL_DATA_LENGTH); 45 | } 46 | 47 | /** 48 | * @param string $channelsData 49 | * @return $this 50 | * @throws Exception 51 | */ 52 | public function setChannelsData(string $channelsData): self 53 | { 54 | $this->channelsData = $this->validateChannelsData($channelsData); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @param string $channelsData 61 | * @return $this 62 | * @throws Exception 63 | */ 64 | public function addChannelsData(string $channelsData): self 65 | { 66 | $this->channelsData .= $this->validateChannelsData($channelsData); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * @param string $channelsData 73 | * @return string 74 | * @throws Exception 75 | */ 76 | protected function validateChannelsData(string $channelsData): string 77 | { 78 | if (strlen($channelsData) % self::CHANNEL_DATA_LENGTH === 0) { 79 | return $channelsData; 80 | } 81 | 82 | throw new Exception('Wrong channels format.'); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Image/ImageExport/Exports/Png.php: -------------------------------------------------------------------------------- 1 | width = $width; 24 | $this->height = $height; 25 | $this->pixelData = $pixelData; 26 | } 27 | 28 | /** 29 | * @throws ImagickException 30 | * @throws ImagickPixelIteratorException 31 | * @throws ImagickPixelException 32 | */ 33 | public function export(): Imagick 34 | { 35 | $pixelDataJson = $this->pixelData->getPixelData(); 36 | $png = new Imagick(); 37 | 38 | $png->newImage($this->width, $this->height, new ImagickPixel("transparent")); 39 | $imageIterator = new ImagickPixelIterator($png); 40 | 41 | $i = 0; 42 | 43 | foreach ($imageIterator as $pixels) { 44 | foreach ($pixels as $column => $pixel) { 45 | $rgba = substr($pixelDataJson, ($i * 42) + 1, 41); 46 | 47 | $r = substr($rgba, 6, 3); 48 | $g = substr($rgba, 16, 3); 49 | $b = substr($rgba, 26, 3); 50 | $a = substr($rgba, 36, 3); 51 | 52 | /** @var $pixel ImagickPixel */ 53 | $pixel->setColor('rgba(' . $r . ',' . $g . ',' . $b . ',' . $a . ')'); 54 | $i++; 55 | } 56 | 57 | $imageIterator->syncIterator(); 58 | } 59 | 60 | $png->setImageFormat("png"); 61 | 62 | return $png; 63 | } 64 | 65 | /** 66 | * @throws ImagickException 67 | * @throws ImagickPixelIteratorException 68 | * @throws ImagickPixelException 69 | */ 70 | public function save(string $fileName): bool 71 | { 72 | return file_put_contents($fileName, $this->export()) !== false; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Node/Node.php: -------------------------------------------------------------------------------- 1 | data = new Group('root'); 17 | } 18 | 19 | static public function build(array $data): NodeInterface 20 | { 21 | $nodeData = new self(); 22 | 23 | for ($i = 0; $i < count($data); $i++) { 24 | $layer = $data[$i]; 25 | 26 | if ($layer->isFolder()) { 27 | $nodeData->addNode($layer->getName()); 28 | } else if ($layer->isFolderEnd()) { 29 | $nodeData->parentNode(); 30 | } else { 31 | $nodeData->addValue(new Layer( 32 | $layer->getName(), 33 | $layer->getPosition()['top'], 34 | $layer->getPosition()['right'], 35 | $layer->getPosition()['bottom'], 36 | $layer->getPosition()['left'], 37 | $layer->getPosition()['width'], 38 | $layer->getPosition()['height'], 39 | $layer, 40 | )); 41 | } 42 | } 43 | 44 | return $nodeData; 45 | } 46 | 47 | public function getNode(): Group 48 | { 49 | return $this->data; 50 | } 51 | 52 | public function addNode(string $name): void 53 | { 54 | $node = $this->getNodeByPath(); 55 | 56 | $keyData = $node->addData(new Group($name)); 57 | 58 | $this->path[] = ($keyData - 1); 59 | } 60 | 61 | public function parentNode(): void 62 | { 63 | array_pop($this->path); 64 | } 65 | 66 | public function addValue($value): void 67 | { 68 | $node = $this->getNodeByPath(); 69 | 70 | $node->addData($value); 71 | } 72 | 73 | protected function &getNodeByPath(): Group 74 | { 75 | $temp = &$this->data; 76 | 77 | foreach ($this->path as $key) { 78 | $temp = &$temp->getData()[$key]; 79 | } 80 | 81 | return $temp; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Image/ImageFormat/ImageData/ImageDataBuilder.php: -------------------------------------------------------------------------------- 1 | file = $file; 19 | $this->header = $header; 20 | } 21 | 22 | /** 23 | * @throws Exception 24 | */ 25 | public function build(int $type): ImageDataBase 26 | { 27 | if ($type === ImageFormatInterface::IMAGE_FORMAT_RAW) { 28 | return $this->buildRaw($this->file, $this->header); 29 | } 30 | 31 | if ($type === ImageFormatInterface::IMAGE_FORMAT_RLE) { 32 | return $this->buildRle($this->file, $this->header); 33 | } 34 | 35 | if ($type === ImageFormatInterface::IMAGE_FORMAT_ZIP) { 36 | return $this->buildZip($this->file, $this->header); 37 | } 38 | 39 | if ($type === ImageFormatInterface::IMAGE_FORMAT_ZIP_PREDICTION) { 40 | return $this->buildZipPrediction($this->file, $this->header); 41 | } 42 | 43 | throw new Exception(sprintf('Error type: %s', $type)); 44 | } 45 | 46 | protected function buildRaw(FileInterface $file, HeaderInterface $header): ImageDataBase 47 | { 48 | return new Raw($file, $header); 49 | } 50 | 51 | protected function buildRle(FileInterface $file, HeaderInterface $header): ImageDataBase 52 | { 53 | return new Rle($file, $header); 54 | } 55 | 56 | /** 57 | * @throws Exception 58 | */ 59 | protected function buildZip(FileInterface $file, HeaderInterface $header): ImageDataBase 60 | { 61 | throw new Exception('ZIP image compression not supported yet.'); 62 | } 63 | 64 | /** 65 | * @throws Exception 66 | */ 67 | protected function buildZipPrediction(FileInterface $file, HeaderInterface $header): ImageDataBase 68 | { 69 | throw new Exception('ZipPrediction image compression not supported yet.'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Image/ImageChannels/RgbaJson.php: -------------------------------------------------------------------------------- 1 | pixelData, 0, -1) . ']'; 23 | } 24 | 25 | /** 26 | * @param string $r 27 | * @param string $g 28 | * @param string $b 29 | * @param string $a 30 | * @return $this 31 | * @throws Exception 32 | */ 33 | public function addRgba(string $r, string $g, string $b, string $a): self 34 | { 35 | $pixelData = '{"r":"' . $r . '","g":"' . $g . '","b":"' . $b . '","a":"' . $a . '"},'; 36 | 37 | if (strlen($pixelData) !== self::JSON_PIXEL_DATA_LENGTH) { 38 | throw new Exception(sprintf( 39 | 'Wrong rgba format. %s', 40 | $this->getInfoAboutColor(compact('r', 'g', 'b', 'a')) 41 | )); 42 | } 43 | 44 | $this->pixelData .= $pixelData; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * @param array $rgbaColors 51 | * @return string 52 | */ 53 | protected function getInfoAboutColor(array $rgbaColors): string 54 | { 55 | foreach ($rgbaColors as $colorName => $colorValue) { 56 | if (strlen($colorValue) !== self::JSON_COLOR_LENGTH) { 57 | return $this->getColorMessage($colorName, $colorValue); 58 | } 59 | } 60 | 61 | return 'Color not found. Something went wrong.'; 62 | } 63 | 64 | /** 65 | * @param $colorName 66 | * @param $colorValue 67 | * @return string 68 | */ 69 | protected function getColorMessage($colorName, $colorValue): string 70 | { 71 | return sprintf( 72 | 'Color "%s" too short. Length: "%s" !== "%s". Value: "%s"', 73 | $colorName, 74 | strlen($colorValue), 75 | self::JSON_COLOR_LENGTH, 76 | $colorValue 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/Info.php: -------------------------------------------------------------------------------- 1 | file = $file; 26 | $this->layerInfoBuilder = $this->buildLayerInfo(); 27 | } 28 | 29 | public function parse(int $layerEnd): void 30 | { 31 | $this->data = []; 32 | 33 | while ($this->file->tell() < $layerEnd) { 34 | $sig = $this->file->readString(4); 35 | 36 | if ($sig !== static::FILE_SIGNATURE) { 37 | throw new Exception(sprintf('Invalid file signature detected. Got: %s. Expected 8BIM.', $sig)); 38 | } 39 | 40 | $key = $this->file->readString(4); 41 | $layerInfoData = $this->layerInfoBuilder->build($this->file, $key); 42 | 43 | $this->data[$layerInfoData->getName()] = new LayerInfoProxy($layerInfoData->getLayerInfo(), $this->file); 44 | 45 | // For debugging purposes, we store every key that we can parse. 46 | $this->infoKeys[] = $key; 47 | } 48 | } 49 | 50 | public function getDataInfo(string $name) 51 | { 52 | if (!isset($this->getData()[$name])) { 53 | throw new Exception('Info not found.'); 54 | } 55 | 56 | return $this->getData()[$name]; 57 | } 58 | 59 | public function getData() 60 | { 61 | if (!isset($this->data)) { 62 | throw new Exception('Info not parsed. Data is undefined.'); 63 | } 64 | 65 | return $this->data; 66 | } 67 | 68 | public function getInfoKeys(): array 69 | { 70 | return $this->infoKeys; 71 | } 72 | 73 | protected function buildLayerInfo(): LayerInfoBuilderInterface 74 | { 75 | return new LayerInfoBuilder(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/FileStructure/Resources/Resource/ResourceBase.php: -------------------------------------------------------------------------------- 1 | file = $file; 36 | $this->id = $id; 37 | } 38 | 39 | public function parseResource(): void 40 | { 41 | $this->name = $this->parseName(); 42 | $this->length = Helper::pad2($this->file->readInt()); 43 | $resourceEnd = $this->file->tell() + $this->length; 44 | 45 | $this->parseResourceData(); 46 | $this->file->ffseek($resourceEnd); 47 | } 48 | 49 | /** 50 | * @throws Exception 51 | */ 52 | public function getData() 53 | { 54 | if (!isset($this->data)) { 55 | throw new Exception('Resource not parsed. Data is undefined.'); 56 | } 57 | 58 | return $this->data; 59 | } 60 | 61 | /** 62 | * @throws Exception 63 | */ 64 | public function getName(): string 65 | { 66 | if (!isset($this->name)) { 67 | throw new Exception('Resource not parsed. Name is undefined.'); 68 | } 69 | 70 | return $this->name; 71 | } 72 | 73 | public function getId(): int 74 | { 75 | return $this->id; 76 | } 77 | 78 | abstract public function parseResourceData(): void; 79 | 80 | protected function parseName(): string 81 | { 82 | $nameLength = Helper::pad2($this->file->readByte() + 1) - 1; 83 | $name = $this->file->readString($nameLength); 84 | 85 | if ($name === '') { 86 | return static::RESOURCE_NAME_UNDEFINED_NAME; 87 | } 88 | 89 | return $name; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/VectorMask/VectorMask.php: -------------------------------------------------------------------------------- 1 | file->ffseek(4, true); // version 18 | $this->tag = $this->file->readInt(); 19 | 20 | // I haven't figured out yet why this is 10 and not 8. 21 | $numRecords = ($length - 10) / 26; 22 | $this->data = []; 23 | 24 | for ($i = 0; $i < $numRecords; $i++) { 25 | $pathRecord = $this->buildPathRecord($this->file); 26 | $pathRecord->parse(); 27 | 28 | $this->data[] = $pathRecord; 29 | } 30 | } 31 | 32 | public function export(): array 33 | { 34 | $invert = $this->getInvert(); 35 | $notLink = $this->getNotLink(); 36 | $disable = $this->getDisable(); 37 | $paths = []; 38 | foreach ($this->getData() as $pathRecord) { 39 | $paths[] = $pathRecord . export(); 40 | } 41 | 42 | return [ 43 | 'invert' => $invert, 44 | 'notLink' => $notLink, 45 | 'disable' => $disable, 46 | 'paths' => $paths, 47 | ]; 48 | } 49 | 50 | protected function getInvert(): bool 51 | { 52 | return ($this->getTag() & 0x01) > 0; 53 | } 54 | 55 | protected function getNotLink(): bool 56 | { 57 | return ($this->getTag() & (0x01 << 1)) > 0; 58 | } 59 | 60 | protected function getDisable(): bool 61 | { 62 | return ($this->getTag() & (0x01 << 2)) > 0; 63 | } 64 | 65 | protected function getTag(): int 66 | { 67 | if (!isset($this->tag)) { 68 | throw new Exception('VectorMask not parsed. Tag is undefined.'); 69 | } 70 | 71 | return $this->tag; 72 | } 73 | 74 | protected function buildPathRecord(FileInterface $file): PathRecordInterface 75 | { 76 | return new PathRecord($file); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/FileStructure/Header/HeaderInterface.php: -------------------------------------------------------------------------------- 1 | 'Bitmap', 26 | self::HEADER_MODE_KEY_GRAY_SCALE => 'GrayScale', 27 | self::HEADER_MODE_KEY_INDEXED_COLOR => 'IndexedColor', 28 | self::HEADER_MODE_KEY_RGB_COLOR => 'RGBColor', 29 | self::HEADER_MODE_KEY_CMYK_COLOR => 'CMYKColor', 30 | self::HEADER_MODE_KEY_HSL_COLOR => 'HSLColor', 31 | self::HEADER_MODE_KEY_HSB_COLOR => 'HSBColor', 32 | self::HEADER_MODE_KEY_MULTICHANNEL => 'Multichannel', 33 | self::HEADER_MODE_KEY_DUOTONE => 'Duotone', 34 | self::HEADER_MODE_KEY_LAB_COLOR => 'LabColor', 35 | self::HEADER_MODE_KEY_GRAY16 => 'Gray16', 36 | self::HEADER_MODE_KEY_RGB48 => 'RGB48', 37 | self::HEADER_MODE_KEY_LAB48 => 'Lab48', 38 | self::HEADER_MODE_KEY_CMYK64 => 'CMYK64', 39 | self::HEADER_MODE_KEY_DEEP_MULTICHANNEL => 'DeepMultichannel', 40 | self::HEADER_MODE_KEY_DUOTONE16 => 'Duotone16', 41 | ]; 42 | 43 | public function parse(): void; 44 | 45 | public function modeName(): string; 46 | 47 | public function getVersion(): int; 48 | 49 | public function getChannels(): int; 50 | 51 | public function getDepth(): int; 52 | 53 | public function getMode(): int; 54 | 55 | public function getRows(): int; 56 | 57 | public function getCols(): int; 58 | 59 | public function getHeight(): int; 60 | 61 | public function getWidth(): int; 62 | 63 | public function getNumPixels(): int; 64 | 65 | public function getChannelLength(int $width = null, int $height = null): int; 66 | 67 | public function getFileLength(): int; 68 | } 69 | -------------------------------------------------------------------------------- /src/Image/ImageMode/Modes/Cmyk.php: -------------------------------------------------------------------------------- 1 | 0], 16 | ['id' => 1], 17 | ['id' => 2], 18 | ['id' => 3], 19 | ]; 20 | 21 | if ($channels === 5) { 22 | $channelsInfo[] = ['id' => -1]; 23 | } 24 | 25 | $this->channelsInfo = $channelsInfo; 26 | } 27 | 28 | public function combineChannel(): RgbaJson 29 | { 30 | $cmykChannels = array_filter( 31 | array_map(static function ($ch) { 32 | return $ch['id']; 33 | }, $this->channelsInfo), static function ($ch) { 34 | return $ch >= -1; 35 | }); 36 | 37 | for ($i = 0; $i < $this->numPixels; $i++) { 38 | $c = 0; 39 | $m = 0; 40 | $y = 0; 41 | $k = 0; 42 | $a = 255; 43 | 44 | for ($index = 0; $index < count($cmykChannels); $index++) { 45 | $chan = $cmykChannels[$index]; 46 | $val = $this->channelData[$i + ($this->channelLength * $index)]; 47 | 48 | switch ($chan) { 49 | case -1: 50 | $a = $val; 51 | break; 52 | case 0: 53 | $c = $val; 54 | break; 55 | case 1: 56 | $m = $val; 57 | break; 58 | case 2: 59 | $y = $val; 60 | break; 61 | case 3: 62 | $k = $val; 63 | break; 64 | default: 65 | throw new Exception('Error cmyk channels'); 66 | } 67 | } 68 | 69 | $rgb = Helper::cmykToRgb(255 - $c, 255 - $m, 255 - $y, 255 - $k); 70 | $this->pixelData->addRgba( 71 | str_pad($rgb['r'], 3, "0", STR_PAD_LEFT), 72 | str_pad($rgb['g'], 3, "0", STR_PAD_LEFT), 73 | str_pad($rgb['b'], 3, "0", STR_PAD_LEFT), 74 | str_pad($a, 3, "0", STR_PAD_LEFT) 75 | ); 76 | } 77 | 78 | return $this->pixelData; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Image/ImageFormat/LayerImageData/LayerImageDataBuilder.php: -------------------------------------------------------------------------------- 1 | file = $file; 20 | $this->header = $header; 21 | } 22 | 23 | public function build(int $type, ImageChannels $channelData): LayerImageDataBase 24 | { 25 | if ($type === ImageFormatInterface::IMAGE_FORMAT_RAW) { 26 | return $this->buildRaw($this->file, $this->header, $channelData); 27 | } 28 | 29 | if ($type === ImageFormatInterface::IMAGE_FORMAT_RLE) { 30 | return $this->buildRle($this->file, $this->header, $channelData); 31 | } 32 | 33 | if ($type === ImageFormatInterface::IMAGE_FORMAT_ZIP) { 34 | return $this->buildZip($this->file, $this->header, $channelData); 35 | } 36 | 37 | if ($type === ImageFormatInterface::IMAGE_FORMAT_ZIP_PREDICTION) { 38 | return $this->buildZipPrediction($this->file, $this->header, $channelData); 39 | } 40 | 41 | throw new Exception(sprintf('Error type: %s', $type)); 42 | } 43 | 44 | protected function buildRaw(FileInterface $file, HeaderInterface $header, ImageChannels $channelData): LayerImageDataBase 45 | { 46 | return new LayerRaw($file, $header, $channelData); 47 | } 48 | 49 | protected function buildRle(FileInterface $file, HeaderInterface $header, ImageChannels $channelData): LayerImageDataBase 50 | { 51 | return new LayerRle($file, $header, $channelData); 52 | } 53 | 54 | /** 55 | * @throws Exception 56 | */ 57 | protected function buildZip(FileInterface $file, HeaderInterface $header, ImageChannels $channelData): LayerImageDataBase 58 | { 59 | throw new Exception('ZIP layer image compression not supported yet.'); 60 | } 61 | 62 | /** 63 | * @throws Exception 64 | */ 65 | protected function buildZipPrediction(FileInterface $file, HeaderInterface $header, ImageChannels $channelData): LayerImageDataBase 66 | { 67 | throw new Exception('ZipPrediction layer image compression not supported yet.'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Descriptor/Parsers/ItemParser/ItemParser.php: -------------------------------------------------------------------------------- 1 | dataMapper = $dataMapper; 19 | $this->referenceParser = $referenceParser; 20 | } 21 | 22 | public function parse(string $parseType) 23 | { 24 | if ($parseType === static::ITEM_TYPE_BOOL) { 25 | return $this->dataMapper->parseBoolean(); 26 | } 27 | if ($parseType === static::ITEM_TYPE_DOUB) { 28 | return $this->dataMapper->parseDouble(); 29 | } 30 | if ($parseType === static::ITEM_TYPE_ENUM) { 31 | return $this->dataMapper->parseEnum(); 32 | } 33 | if ($parseType === static::ITEM_TYPE_ALIS) { 34 | return $this->dataMapper->parseAlias(); 35 | } 36 | if ($parseType === static::ITEM_TYPE_PTH) { 37 | return $this->dataMapper->parseFilePath(); 38 | } 39 | if ($parseType === static::ITEM_TYPE_LONG) { 40 | return $this->dataMapper->parseInteger(); 41 | } 42 | if ($parseType === static::ITEM_TYPE_COMP) { 43 | return $this->dataMapper->parseLargeInteger(); 44 | } 45 | if ($parseType === static::ITEM_TYPE_OBAR) { 46 | return $this->dataMapper->parseObjectArray(); 47 | } 48 | if ($parseType === static::ITEM_TYPE_TDTA) { 49 | return $this->dataMapper->parseRawData(); 50 | } 51 | if ($parseType === static::ITEM_TYPE_OBJ) { 52 | return $this->referenceParser->parse(); 53 | } 54 | if ($parseType === static::ITEM_TYPE_TEXT) { 55 | return $this->dataMapper->parseText(); 56 | } 57 | if ($parseType === static::ITEM_TYPE_UNTF) { 58 | return $this->dataMapper->parseUnitDouble(); 59 | } 60 | if ($parseType === static::ITEM_TYPE_UNFL) { 61 | return $this->dataMapper->parseUnitFloat(); 62 | } 63 | if ($parseType === static::ITEM_TYPE_TYPE || $parseType === static::ITEM_TYPE_GLBC) { 64 | return $this->dataMapper->parseClass(); 65 | } 66 | 67 | throw new Exception(sprintf('Error parseType: %s', $parseType)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/FileStructure/Resources/Resource/ResolutionInfo/ResolutionInfoData.php: -------------------------------------------------------------------------------- 1 | hRes = $hRes; 21 | return $this; 22 | } 23 | 24 | /** 25 | * @param int $hResUnit 26 | * @return $this 27 | */ 28 | public function setHResUnit(int $hResUnit): self 29 | { 30 | $this->hResUnit = $hResUnit; 31 | return $this; 32 | } 33 | 34 | /** 35 | * @param int $widthUnit 36 | * @return $this 37 | */ 38 | public function setWidthUnit(int $widthUnit): self 39 | { 40 | $this->widthUnit = $widthUnit; 41 | return $this; 42 | } 43 | 44 | /** 45 | * @param int $vRes 46 | * @return $this 47 | */ 48 | public function setVRes(int $vRes): self 49 | { 50 | $this->vRes = $vRes; 51 | return $this; 52 | } 53 | 54 | /** 55 | * @param int $vResUnit 56 | * @return $this 57 | */ 58 | public function setVResUnit(int $vResUnit): self 59 | { 60 | $this->vResUnit = $vResUnit; 61 | return $this; 62 | } 63 | 64 | /** 65 | * @param int $heightUnit 66 | * @return $this 67 | */ 68 | public function setHeightUnit(int $heightUnit): self 69 | { 70 | $this->heightUnit = $heightUnit; 71 | return $this; 72 | } 73 | 74 | /** 75 | * @return int 76 | */ 77 | public function getHRes(): int 78 | { 79 | return $this->hRes; 80 | } 81 | 82 | /** 83 | * @return int 84 | */ 85 | public function getHResUnit(): int 86 | { 87 | return $this->hResUnit; 88 | } 89 | 90 | /** 91 | * @return int 92 | */ 93 | public function getWidthUnit(): int 94 | { 95 | return $this->widthUnit; 96 | } 97 | 98 | /** 99 | * @return int 100 | */ 101 | public function getVRes(): int 102 | { 103 | return $this->vRes; 104 | } 105 | 106 | /** 107 | * @return int 108 | */ 109 | public function getVResUnit(): int 110 | { 111 | return $this->vResUnit; 112 | } 113 | 114 | /** 115 | * @return int 116 | */ 117 | public function getHeightUnit(): int 118 | { 119 | return $this->heightUnit; 120 | } 121 | } -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/BlendMode/BlendMode.php: -------------------------------------------------------------------------------- 1 | file = $file; 23 | } 24 | 25 | public function parse(): void 26 | { 27 | $this->file->ffseek(4, true); 28 | 29 | $this->blendKey = $this->parseBlendKey(); 30 | $this->opacity = $this->file->readByte(); 31 | $this->clipping = $this->file->readByte(); 32 | $this->flags = $this->file->readByte(); 33 | 34 | $this->file->ffseek(1, true); 35 | } 36 | 37 | public function getBlendKey(): string 38 | { 39 | if (!isset($this->blendKey)) { 40 | throw new Exception('BlendMode not parsed. BlendKey is undefined.'); 41 | } 42 | 43 | return $this->blendKey; 44 | } 45 | 46 | public function getOpacity(): float 47 | { 48 | if (!isset($this->opacity)) { 49 | throw new Exception('BlendMode not parsed. Opacity is undefined.'); 50 | } 51 | 52 | return $this->opacity; 53 | } 54 | 55 | public function getClipping(): float 56 | { 57 | if (!isset($this->clipping)) { 58 | throw new Exception('BlendMode not parsed. Clipping is undefined.'); 59 | } 60 | 61 | return $this->clipping; 62 | } 63 | 64 | public function getFlags(): float 65 | { 66 | if (!isset($this->flags)) { 67 | throw new Exception('BlendMode not parsed. Flags is undefined.'); 68 | } 69 | 70 | return $this->flags; 71 | } 72 | 73 | public function getMode(): string 74 | { 75 | return static::BLEND_MODE_KEY[$this->getBlendKey()]; 76 | } 77 | 78 | public function getClipped(): bool 79 | { 80 | return $this->getClipping() === 1; 81 | } 82 | 83 | public function getVisible(): bool 84 | { 85 | return !(($this->getFlags() & (0x01 << 1)) > 0); 86 | } 87 | 88 | public function opacityPercentage(): float 89 | { 90 | return ($this->getOpacity() * 100) / 255; 91 | } 92 | 93 | public function parseBlendKey(): string 94 | { 95 | $blendKey = trim($this->file->readString(4)); 96 | 97 | if (!isset(static::BLEND_MODE_KEY[$blendKey])) { 98 | throw new Exception(sprintf('BlendKey not found. Key: %s', $blendKey)); 99 | } 100 | 101 | return $blendKey; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Descriptor/Descriptor.php: -------------------------------------------------------------------------------- 1 | file = $file; 25 | 26 | $this->dataMapper = new DataMapper($this->file); 27 | $this->itemParser = new ItemParser( 28 | $this->dataMapper, 29 | new ReferenceParser($this->dataMapper), 30 | ); 31 | } 32 | 33 | /** 34 | * Parses the Descriptor at the current location in the file. 35 | * @throws Exception 36 | */ 37 | public function parse(): DescriptorData 38 | { 39 | $descriptor = (new DescriptorData()) 40 | ->setClassData($this->dataMapper->parseClass()) 41 | ->setData([]); 42 | 43 | $numItems = $this->file->readInt(); 44 | 45 | /** 46 | * Each item consists of a key/value combination, which is why our 47 | * descriptor is stored as an object instead of an array at the root. 48 | */ 49 | for ($i = 0; $i < $numItems; $i += 1) { 50 | $id = $this->dataMapper->parseId(); 51 | $type = $this->dataMapper->parseItemType(); 52 | 53 | if ($type === ItemParserInterface::ITEM_TYPE_VLLS) { 54 | $descriptor->addData($id, $this->parseItems()); 55 | } else if ($type === ItemParserInterface::ITEM_TYPE_OBJC || $type === ItemParserInterface::ITEM_TYPE_GLBO) { 56 | $descriptor->addData($id, (new Descriptor($this->file))->parse()); 57 | } else { 58 | $descriptor->addData($id, $this->itemParser->parse($type)); 59 | } 60 | } 61 | 62 | return $descriptor; 63 | } 64 | 65 | /** 66 | * @throws Exception 67 | */ 68 | protected function parseItems(): array 69 | { 70 | $count = $this->dataMapper->parseInteger(); 71 | $items = []; 72 | 73 | for ($i = 0; $i < $count; $i++) { 74 | $type = $this->dataMapper->parseItemType(); 75 | 76 | if ($type === ItemParserInterface::ITEM_TYPE_VLLS) { 77 | throw new Exception('Recursive data'); 78 | } 79 | 80 | if ($type === ItemParserInterface::ITEM_TYPE_OBJC || $type === ItemParserInterface::ITEM_TYPE_GLBO) { 81 | $items[] = (new Descriptor($this->file))->parse(); 82 | } else { 83 | $items[] = $this->itemParser->parse($type); 84 | } 85 | } 86 | 87 | return $items; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/FileStructure/Image/Image.php: -------------------------------------------------------------------------------- 1 | file = $file; 32 | $this->header = $header; 33 | 34 | $this->imageFormat = $this->buildImageFormat($this->file, $this->header); 35 | $this->imageMode = $this->buildImageMode($this->header); 36 | } 37 | 38 | public function skip(): void 39 | { 40 | $this->file->ffseek($this->getEndPos()); 41 | } 42 | 43 | 44 | /** 45 | * @throws Exception 46 | */ 47 | public function getExporter(string $type): ImageExportInterface 48 | { 49 | return ImageExport::buildImageExport($type, $this->header->getWidth(), $this->header->getHeight(), $this->getPixelData()); 50 | } 51 | 52 | /** 53 | * @throws Exception 54 | */ 55 | public function getPixelData(): RgbaJson 56 | { 57 | if (!isset($this->pixelData)) { 58 | throw new Exception('PixelData is undefined.'); 59 | } 60 | 61 | return $this->pixelData; 62 | } 63 | 64 | /** 65 | * Parses the image and formats the image data. 66 | */ 67 | public function parse(): void 68 | { 69 | $compression = $this->file->readShort(); 70 | $this->parseImageData($compression); 71 | } 72 | 73 | /** 74 | * Parses the image data based on the compression mode. 75 | */ 76 | protected function parseImageData(int $compression): void 77 | { 78 | $channelData = $this->imageFormat->build($compression)->parse(); 79 | 80 | $this->pixelData = $this->imageMode->build( 81 | $channelData, 82 | $this->header->getChannels(), 83 | $this->header->getNumPixels(), 84 | $this->header->getChannelLength(), 85 | )->combineChannel(); 86 | } 87 | 88 | protected function getEndPos(): int 89 | { 90 | return $this->file->tell() + $this->header->getFileLength(); 91 | } 92 | 93 | protected function buildImageMode(HeaderInterface $header): ImageModeInterface 94 | { 95 | return new ImageMode($header); 96 | } 97 | 98 | protected function buildImageFormat(FileInterface $file, HeaderInterface $header): ImageDataBuilderInterface 99 | { 100 | return new ImageDataBuilder($file, $header); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/FileStructure/Resources/Resources.php: -------------------------------------------------------------------------------- 1 | file = $file; 25 | $this->resource = $this->buildResource($this->file); 26 | } 27 | 28 | public function skip(): void 29 | { 30 | $this->file->ffseek($this->file->readInt(), true); 31 | } 32 | 33 | /** 34 | * @throws Exception 35 | */ 36 | public function parse(): void 37 | { 38 | $finish = $this->file->readInt() + $this->file->tell(); 39 | 40 | while ($this->file->tell() < $finish) { 41 | $this->parseResource(); 42 | } 43 | 44 | $this->file->ffseek($finish); 45 | } 46 | 47 | public function getResources(): array 48 | { 49 | return $this->resources; 50 | } 51 | 52 | /** 53 | * @throws Exception 54 | */ 55 | public function getResource($search) 56 | { 57 | if (is_string($search)) { 58 | return $this->getResourceByName($search); 59 | } 60 | 61 | return $this->getResourceById($search); 62 | } 63 | 64 | /** 65 | * @throws Exception 66 | */ 67 | public function getResourceByName(string $name) 68 | { 69 | if (!isset($this->resourcesByName[$name])) { 70 | throw new Exception(sprintf('Resource not found. Name: %s', $name)); 71 | } 72 | 73 | return $this->getResourceById($this->resourcesByName[$name]); 74 | } 75 | 76 | /** 77 | * @throws Exception 78 | */ 79 | public function getResourceById($id) 80 | { 81 | if (!isset($this->resources[$id])) { 82 | throw new Exception(sprintf('Resource not found. Id: %s', $id)); 83 | } 84 | 85 | return $this->resources[$id]; 86 | } 87 | 88 | /** 89 | * @throws Exception 90 | */ 91 | protected function parseResource(): void 92 | { 93 | $resource = $this->resource->parse(); 94 | 95 | $this->resources[$resource->getId()] = $resource; 96 | 97 | if ( 98 | isset(ResourceBase::RESOURCE_IDS[$resource->getId()]) 99 | && $resource->getName() !== ResourceBase::RESOURCE_NAME_UNDEFINED_NAME 100 | ) { 101 | if (isset($this->resourcesByName[$resource->getName()])) { 102 | throw new Exception('Resource name already exists.'); 103 | } 104 | 105 | $this->resourcesByName[$resource->getName()] = $resource->getId(); 106 | } 107 | } 108 | 109 | protected function buildResource(FileInterface $file): ResourceInterface 110 | { 111 | return new Resource($file); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/PositionAndChannels/PositionAndChannels.php: -------------------------------------------------------------------------------- 1 | file = $file; 27 | } 28 | 29 | public function parse(): void 30 | { 31 | $this->top = $this->file->readInt(); 32 | $this->left = $this->file->readInt(); 33 | $this->bottom = $this->file->readInt(); 34 | $this->right = $this->file->readInt(); 35 | $this->channels = $this->file->readShort(); 36 | 37 | // Every color channel has both an ID and a length. The ID correlates to 38 | // the color channel, e.g. 0 = R, 1 = G, 2 = B, -1 = A, and the length is 39 | // the size of the data. 40 | for ($i = 0; $i < $this->channels; $i++) { 41 | $this->channelsInfo[] = [ 42 | 'id' => $this->file->readShort(), 43 | 'dataLength' => $this->file->readInt(), 44 | ]; 45 | } 46 | } 47 | 48 | public function getTop(): int 49 | { 50 | if (!isset($this->top)) { 51 | throw new Exception('PositionAndChannels not parsed. Top is undefined.'); 52 | } 53 | 54 | return $this->top; 55 | } 56 | 57 | public function getLeft(): int 58 | { 59 | if (!isset($this->left)) { 60 | throw new Exception('PositionAndChannels not parsed. Left is undefined.'); 61 | } 62 | 63 | return $this->left; 64 | } 65 | 66 | public function getBottom(): int 67 | { 68 | if (!isset($this->bottom)) { 69 | throw new Exception('PositionAndChannels not parsed. Bottom is undefined.'); 70 | } 71 | 72 | return $this->bottom; 73 | } 74 | 75 | public function getRight(): int 76 | { 77 | if (!isset($this->right)) { 78 | throw new Exception('PositionAndChannels not parsed. Right is undefined.'); 79 | } 80 | 81 | return $this->right; 82 | } 83 | 84 | public function getChannels(): int 85 | { 86 | if (!isset($this->channels)) { 87 | throw new Exception('PositionAndChannels not parsed. Channels is undefined.'); 88 | } 89 | 90 | return $this->channels; 91 | } 92 | 93 | public function getChannelsInfo(): array 94 | { 95 | if (count($this->channelsInfo) === 0) { 96 | throw new Exception('PositionAndChannels not parsed. ChannelsInfo is empty.'); 97 | } 98 | 99 | return $this->channelsInfo; 100 | } 101 | 102 | public function getRows(): int 103 | { 104 | return $this->getBottom() - $this->getTop(); 105 | } 106 | 107 | public function getHeight(): int 108 | { 109 | return $this->getRows(); 110 | } 111 | 112 | public function getCols(): int 113 | { 114 | return $this->getRight() - $this->getLeft(); 115 | } 116 | 117 | public function getWidth(): int 118 | { 119 | return $this->getCols(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Mask/Mask.php: -------------------------------------------------------------------------------- 1 | file = $file; 29 | } 30 | 31 | public function parse(): void 32 | { 33 | $this->size = $this->file->readInt(); 34 | if ($this->size === 0) { 35 | return; 36 | } 37 | 38 | $maskEnd = $this->file->tell() + $this->size; 39 | 40 | // First, we parse the coordinates of the mask. 41 | $this->top = $this->file->readInt(); 42 | $this->left = $this->file->readInt(); 43 | $this->bottom = $this->file->readInt(); 44 | $this->right = $this->file->readInt(); 45 | 46 | $this->defaultColor = $this->file->readByte(); 47 | $this->flags = $this->file->readByte(); 48 | 49 | $this->file->ffseek($maskEnd); 50 | } 51 | 52 | public function getSize(): int 53 | { 54 | if (!isset($this->size)) { 55 | throw new Exception('Mask not parsed. Size is undefined.'); 56 | } 57 | 58 | return $this->size; 59 | } 60 | 61 | public function getDefaultColor(): float 62 | { 63 | if (!isset($this->defaultColor)) { 64 | throw new Exception('Mask not parsed. DefaultColor is undefined.'); 65 | } 66 | 67 | return $this->defaultColor; 68 | } 69 | 70 | public function getFlags(): float 71 | { 72 | if (!isset($this->flags)) { 73 | throw new Exception('Mask not parsed. Flags is undefined.'); 74 | } 75 | 76 | return $this->flags; 77 | } 78 | 79 | public function getTop(): int 80 | { 81 | return $this->top; 82 | } 83 | 84 | public function getLeft(): int 85 | { 86 | return $this->left; 87 | } 88 | 89 | public function getBottom(): int 90 | { 91 | return $this->bottom; 92 | } 93 | 94 | public function getRight(): int 95 | { 96 | return $this->right; 97 | } 98 | 99 | public function getWidth(): int 100 | { 101 | return $this->getRight() - $this->getLeft(); 102 | } 103 | 104 | public function getHeight(): int 105 | { 106 | return $this->getBottom() - $this->getTop(); 107 | } 108 | 109 | public function getRelative(): bool 110 | { 111 | return ($this->getFlags() & 0x01) > 0; 112 | } 113 | 114 | public function getDisabled(): bool 115 | { 116 | return ($this->getFlags() & (0x01 << 1)) > 0; 117 | } 118 | 119 | public function getInvert(): bool 120 | { 121 | return ($this->getFlags() & (0x01 << 2)) > 0; 122 | } 123 | 124 | public function getFromOtherData(): bool 125 | { 126 | return ($this->getFlags() & (0x01 << 3)) > 0; 127 | } 128 | 129 | public function export(): array 130 | { 131 | if ($this->getSize() === 0) { 132 | return []; 133 | } 134 | 135 | return [ 136 | 'top' => $this->getTop(), 137 | 'left' => $this->getLeft(), 138 | 'bottom' => $this->getBottom(), 139 | 'right' => $this->getRight(), 140 | 'width' => $this->getWidth(), 141 | 'height' => $this->getHeight(), 142 | 'defaultColor' => $this->getDefaultColor(), 143 | 'relative' => $this->getRelative(), 144 | 'disabled' => $this->getDisabled(), 145 | 'invert' => $this->getInvert(), 146 | 'fromOtherData' => $this->getFromOtherData(), 147 | ]; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/LayerMask.php: -------------------------------------------------------------------------------- 1 | file = $file; 29 | $this->header = $header; 30 | $this->mergedAlpha = false; 31 | } 32 | 33 | /** 34 | * @throws Exception 35 | */ 36 | public function getLayers(): array 37 | { 38 | if (!isset($this->layers)) { 39 | throw new Exception('LayerMask not parsed. Layers is undefined.'); 40 | } 41 | 42 | return $this->layers; 43 | } 44 | 45 | /** 46 | * @throws Exception 47 | */ 48 | public function getGlobalMask(): GlobalMask 49 | { 50 | if (!isset($this->globalMask)) { 51 | throw new Exception('LayerMask not parsed. GlobalMask is undefined.'); 52 | } 53 | 54 | return $this->globalMask; 55 | } 56 | 57 | public function skip(): void 58 | { 59 | $this->file->ffseek($this->file->readInt(), true); 60 | } 61 | 62 | public function parse(): void 63 | { 64 | $maskSize = $this->file->readInt(); 65 | $finish = $maskSize + $this->file->tell(); 66 | 67 | if ($maskSize <= 0) { 68 | return; 69 | } 70 | 71 | $this->layers = array_reverse($this->parseLayers()); 72 | $this->parseGlobalMask(); 73 | 74 | $this->file->ffseek($finish); 75 | } 76 | 77 | protected function parseGlobalMask(): void 78 | { 79 | $length = $this->file->readInt(); 80 | if ($length <= 0) { 81 | return; 82 | } 83 | 84 | $maskEnd = $this->file->tell() + $length; 85 | 86 | $overlayColorSpace = $this->file->readShort(); 87 | $colorComponents = [ 88 | $this->file->readShort() >> 8, 89 | $this->file->readShort() >> 8, 90 | $this->file->readShort() >> 8, 91 | $this->file->readShort() >> 8, 92 | ]; 93 | 94 | $opacity = $this->file->readShort() / 16.0; 95 | $kind = $this->file->readByte(); 96 | 97 | $this->globalMask = (new GlobalMask()) 98 | ->setOverlayColorSpace($overlayColorSpace) 99 | ->setColorComponents($colorComponents) 100 | ->setOpacity($opacity) 101 | ->setKind($kind); 102 | 103 | $this->file->ffseek($maskEnd); 104 | } 105 | 106 | protected function parseLayers(): array 107 | { 108 | $layers = []; 109 | $layerInfoSize = Helper::pad2($this->file->readInt()); 110 | 111 | if ($layerInfoSize > 0) { 112 | $layerCount = $this->file->readShort(); 113 | 114 | if ($layerCount < 0) { 115 | $layerCount = abs($layerCount); 116 | $this->mergedAlpha = true; 117 | } 118 | 119 | for ($i = 0; $i < $layerCount; $i += 1) { 120 | $layer = $this->buildLayer($this->file, $this->header); 121 | $layer->parse(); 122 | 123 | $layers[] = $layer; 124 | } 125 | 126 | foreach ($layers as $layer) { 127 | $layer->parseChannelImage(); 128 | } 129 | } 130 | 131 | return $layers; 132 | } 133 | 134 | protected function buildLayer(FileInterface $file, HeaderInterface $header): LayerInterface 135 | { 136 | return new Layer($file, $header); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Psd.php: -------------------------------------------------------------------------------- 1 | file = $this->buildFile($fileName); 43 | $this->header = $this->buildHeader($this->file); 44 | $this->resources = $this->buildResources($this->file); 45 | $this->layerMask = $this->buildLayerMask($this->file, $this->header); 46 | $this->image = $this->buildImage($this->file, $this->header); 47 | $this->shortcuts = $this->buildShortcuts(); 48 | } 49 | 50 | public function parse(): bool 51 | { 52 | if ($this->parsed) { 53 | return false; 54 | } 55 | 56 | $this->parseHeader(); 57 | $this->parseResources(); 58 | $this->parseLayerMask(); 59 | $this->parseImage(); 60 | 61 | $this->parsed = true; 62 | return $this->parsed; 63 | } 64 | 65 | protected function parseHeader(): void 66 | { 67 | $this->header->parse(); 68 | } 69 | 70 | protected function parseResources(): void 71 | { 72 | $this->resources = new ResourcesProxy($this->resources, $this->file); 73 | } 74 | 75 | protected function parseLayerMask(): void 76 | { 77 | $this->layerMask = new LayerMaskProxy($this->layerMask, $this->file); 78 | } 79 | 80 | public function getHeader(): HeaderInterface 81 | { 82 | if (!$this->parsed) { 83 | $this->parse(); 84 | } 85 | 86 | return $this->header; 87 | } 88 | 89 | public function getResources(): ResourcesInterface 90 | { 91 | if (!$this->parsed) { 92 | $this->parse(); 93 | } 94 | 95 | return $this->resources; 96 | } 97 | 98 | public function getImage(): ImageInterface 99 | { 100 | if (!$this->parsed) { 101 | $this->parse(); 102 | } 103 | 104 | return $this->image; 105 | } 106 | 107 | public function getLayers(): array 108 | { 109 | if (!$this->parsed) { 110 | $this->parse(); 111 | } 112 | 113 | return $this->layerMask->getLayers(); 114 | } 115 | 116 | public function getShortcuts(): ShortcutsInterface 117 | { 118 | return $this->shortcuts; 119 | } 120 | 121 | protected function buildFile(string $fileName): FileInterface 122 | { 123 | return new File($fileName); 124 | } 125 | 126 | protected function buildHeader(FileInterface $file): HeaderInterface 127 | { 128 | return new Header($file); 129 | } 130 | 131 | protected function buildResources(FileInterface $file): ResourcesInterface 132 | { 133 | return new Resources($file); 134 | } 135 | 136 | protected function buildLayerMask(FileInterface $file, HeaderInterface $header): LayerMaskInterface 137 | { 138 | return new LayerMask($file, $header); 139 | } 140 | 141 | protected function buildImage(FileInterface $file, HeaderInterface $header): ImageInterface 142 | { 143 | return new Image($file, $header); 144 | } 145 | 146 | protected function buildShortcuts(): ShortcutsInterface 147 | { 148 | return new Shortcuts($this); 149 | } 150 | 151 | protected function parseImage(): void 152 | { 153 | $this->image = new ImageProxy($this->image, $this->file); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/ChannelImage/ChannelImage.php: -------------------------------------------------------------------------------- 1 | file = $file; 43 | $this->header = $header; 44 | $this->layerData = $layerData; 45 | $this->imageDataBuilder = $this->buildImageDataBuilder($this->file, $this->header); 46 | $this->imageMode = $this->buildImageMode($this->header); 47 | 48 | $minChanId = min(array_map(static function ($chan) { 49 | return $chan['id']; 50 | }, $this->layerData['layerChannelsInfo'])); 51 | 52 | $this->maxWidth = ($minChanId < -1) ? $this->layerData['layerMaskWidth'] : $this->layerData['layerWidth']; 53 | $this->maxHeight = ($minChanId < -1) ? $this->layerData['layerMaskHeight'] : $this->layerData['layerHeight']; 54 | 55 | 56 | $this->channelData = new ImageChannels($this->maxWidth * $this->maxHeight * count($this->layerData['layerChannelsInfo'])); 57 | 58 | $this->numPixels = $this->layerData['layerWidth'] * $this->layerData['layerHeight']; 59 | } 60 | 61 | public function getExporter(string $type): ImageExportInterface 62 | { 63 | return ImageExport::buildImageExport($type, $this->maxWidth, $this->maxHeight, $this->getPixelData()); 64 | } 65 | 66 | public function skip(): void 67 | { 68 | for ($i = 0; $i < count($this->layerData['layerChannelsInfo']); $i += 1) { 69 | $this->file->ffseek($this->layerData['layerChannelsInfo'][$i]['dataLength'], true); 70 | } 71 | } 72 | 73 | public function getLayerData(): array 74 | { 75 | return $this->layerData; 76 | } 77 | 78 | public function parse(): void 79 | { 80 | $chanPos = 0; 81 | 82 | for ($i = 0; $i < count($this->layerData['layerChannelsInfo']); $i++) { 83 | $chan = $this->layerData['layerChannelsInfo'][$i]; 84 | 85 | if ($chan['dataLength'] <= 0) { 86 | $this->parseData($chanPos, $chan['dataLength'], $this->maxHeight); 87 | continue; 88 | } 89 | 90 | $start = $this->file->tell(); 91 | 92 | $chanPos = $this->parseData($chanPos, $chan['dataLength'], $this->maxHeight); 93 | 94 | $finish = $this->file->tell(); 95 | 96 | if ($finish !== $start + $chan['dataLength']) { 97 | $this->file->ffseek($start + $chan['dataLength']); 98 | } 99 | } 100 | 101 | $this->pixelData = $this->imageMode->build( 102 | $this->channelData, 103 | $this->layerData['layerChannels'], 104 | $this->numPixels, 105 | $this->header->getChannelLength($this->layerData['layerWidth'], $this->layerData['layerHeight']) 106 | )->combineChannel(); 107 | } 108 | 109 | public function getPixelData(): RgbaJson 110 | { 111 | if (!isset($this->pixelData)) { 112 | throw new Exception('PixelData is undefined.'); 113 | } 114 | 115 | return $this->pixelData; 116 | } 117 | 118 | protected function parseData(int $chanPos, int $dataLength, int $height): int 119 | { 120 | $compression = $this->file->readShort(); 121 | 122 | return $this->imageDataBuilder->build($compression, $this->channelData)->parse($chanPos, $dataLength, $height); 123 | } 124 | 125 | protected function buildImageMode(HeaderInterface $header): ImageModeInterface 126 | { 127 | return new ImageMode($header); 128 | } 129 | 130 | protected function buildImageDataBuilder(FileInterface $file, HeaderInterface $header): LayerImageDataBuilderInterface 131 | { 132 | return new LayerImageDataBuilder($file, $header); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/LayerInfoBuilder.php: -------------------------------------------------------------------------------- 1 | setLayerInfo($layerInfo) 91 | ->setName($name); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Info/LayerInfo/VectorMask/PathRecord/PathRecord.php: -------------------------------------------------------------------------------- 1 | file = $file; 19 | } 20 | 21 | public function parse(): void 22 | { 23 | $this->recordType = $this->file->readShort(); 24 | 25 | if ($this->isPathRecord($this->recordType)) { 26 | $this->data = $this->parsePathRecord(); 27 | return; 28 | } 29 | 30 | if ($this->isBezierPoint($this->recordType)) { 31 | $this->data = $this->parseBezierPoint( 32 | $this->isLinked($this->recordType), 33 | ); 34 | 35 | return; 36 | } 37 | 38 | if ($this->isClipboardRecord($this->recordType)) { 39 | $this->data = $this->parseClipboardRecord(); 40 | return; 41 | } 42 | 43 | if ($this->isInitialFill($this->recordType)) { 44 | $this->data = $this->parseInitialFill(); 45 | return; 46 | } 47 | 48 | $this->file->ffseek(24, true); 49 | } 50 | 51 | public function export(): array 52 | { 53 | return [ 54 | 'recordType' => $this->getRecordType(), 55 | 'data' => $this->getData(), 56 | ]; 57 | } 58 | 59 | protected function getRecordType(): int 60 | { 61 | if (!isset($this->recordType)) { 62 | throw new Exception('PathRecord not parsed. RecordType is undefined.'); 63 | } 64 | 65 | return $this->recordType; 66 | } 67 | 68 | protected function getData() 69 | { 70 | if (!isset($this->data)) { 71 | throw new Exception('PathRecord not parsed. Data is undefined.'); 72 | } 73 | 74 | return $this->data; 75 | } 76 | 77 | protected function isPathRecord(int $recordType): bool 78 | { 79 | return $recordType === 0 || $recordType === 3; 80 | } 81 | 82 | protected function isBezierPoint(int $recordType): bool 83 | { 84 | return $recordType === 1 || $recordType === 2 || $recordType === 4 || $recordType === 5; 85 | } 86 | 87 | protected function isClipboardRecord(int $recordType): bool 88 | { 89 | return $recordType === 7; 90 | } 91 | 92 | protected function isInitialFill(int $recordType): bool 93 | { 94 | return $recordType === 8; 95 | } 96 | 97 | protected function isLinked(int $recordType): bool 98 | { 99 | return $recordType === 1 || $recordType === 4; 100 | } 101 | 102 | protected function parsePathRecord(): array 103 | { 104 | $numPoints = $this->file->readShort(); 105 | $this->file->ffseek(22, true); 106 | 107 | return [ 108 | 'numPoints' => $numPoints, 109 | ]; 110 | } 111 | 112 | protected function parseBezierPoint(bool $linked): array 113 | { 114 | $precedingVert = $this->file->readPathNumber(); 115 | $precedingHoriz = $this->file->readPathNumber(); 116 | 117 | $anchorVert = $this->file->readPathNumber(); 118 | $anchorHoriz = $this->file->readPathNumber(); 119 | 120 | $leavingVert = $this->file->readPathNumber(); 121 | $leavingHoriz = $this->file->readPathNumber(); 122 | 123 | return [ 124 | 'linked' => $linked, 125 | 'precedingVert' => $precedingVert, 126 | 'precedingHoriz' => $precedingHoriz, 127 | 'anchorVert' => $anchorVert, 128 | 'anchorHoriz' => $anchorHoriz, 129 | 'leavingVert' => $leavingVert, 130 | 'leavingHoriz' => $leavingHoriz, 131 | ]; 132 | } 133 | 134 | protected function parseClipboardRecord(): array 135 | { 136 | $clipboardTop = $this->file->readPathNumber(); 137 | $clipboardLeft = $this->file->readPathNumber(); 138 | $clipboardBottom = $this->file->readPathNumber(); 139 | $clipboardRight = $this->file->readPathNumber(); 140 | $clipboardResolution = $this->file->readPathNumber(); 141 | 142 | $this->file->ffseek(4, true); 143 | 144 | return [ 145 | 'clipboardTop' => $clipboardTop, 146 | 'clipboardLeft' => $clipboardLeft, 147 | 'clipboardBottom' => $clipboardBottom, 148 | 'clipboardRight' => $clipboardRight, 149 | 'clipboardResolution' => $clipboardResolution, 150 | ]; 151 | } 152 | 153 | protected function parseInitialFill(): array 154 | { 155 | $initialFill = $this->file->readShort(); 156 | $this->file->ffseek(22, true); 157 | 158 | return [ 159 | 'initialFill' => $initialFill, 160 | ]; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/FileStructure/Header/Header.php: -------------------------------------------------------------------------------- 1 | file = $file; 29 | } 30 | 31 | /** 32 | * @throws Exception 33 | */ 34 | public function parse(): void 35 | { 36 | if ($this->file->tell() !== 0) { 37 | throw new Exception('Wrong file position'); 38 | } 39 | 40 | $this->readSignature(); 41 | $this->version = $this->file->readUShort(); 42 | 43 | $this->file->ffseek(6, true); 44 | 45 | $this->channels = $this->file->readUShort(); 46 | $this->rows = $this->file->readUInt(); 47 | $this->cols = $this->file->readUInt(); 48 | $this->depth = $this->file->readUShort(); 49 | $this->mode = $this->file->readUShort(); 50 | 51 | $colorDataLen = $this->file->readUInt(); 52 | $this->file->ffseek($colorDataLen, true); 53 | } 54 | 55 | /** 56 | * @throws Exception 57 | */ 58 | public function modeName(): string 59 | { 60 | if (!isset($this->mode)) { 61 | throw new Exception('Header not parsed. Mode is undefined.'); 62 | } 63 | 64 | return static::HEADER_MODE[$this->mode]; 65 | } 66 | 67 | /** 68 | * @throws Exception 69 | */ 70 | public function getVersion(): int 71 | { 72 | if (!isset($this->version)) { 73 | throw new Exception('Header not parsed. Version is undefined.'); 74 | } 75 | 76 | return $this->version; 77 | } 78 | 79 | /** 80 | * @throws Exception 81 | */ 82 | public function getChannels(): int 83 | { 84 | if (!isset($this->channels)) { 85 | throw new Exception('Header not parsed. Channels is undefined.'); 86 | } 87 | 88 | return $this->channels; 89 | } 90 | 91 | /** 92 | * @throws Exception 93 | */ 94 | public function getDepth(): int 95 | { 96 | if (!isset($this->depth)) { 97 | throw new Exception('Header not parsed. Depth is undefined.'); 98 | } 99 | 100 | return $this->depth; 101 | } 102 | 103 | /** 104 | * @throws Exception 105 | */ 106 | public function getMode(): int 107 | { 108 | if (!isset($this->mode)) { 109 | throw new Exception('Header not parsed. Mode is undefined.'); 110 | } 111 | 112 | return $this->mode; 113 | } 114 | 115 | /** 116 | * @throws Exception 117 | */ 118 | public function getRows(): int 119 | { 120 | if (!isset($this->rows)) { 121 | throw new Exception('Header not parsed. Rows is undefined.'); 122 | } 123 | 124 | return $this->rows; 125 | } 126 | 127 | /** 128 | * @throws Exception 129 | */ 130 | public function getCols(): int 131 | { 132 | if (!isset($this->cols)) { 133 | throw new Exception('Header not parsed. Cols is undefined.'); 134 | } 135 | 136 | return $this->cols; 137 | } 138 | 139 | /** 140 | * @throws Exception 141 | */ 142 | public function getHeight(): int 143 | { 144 | return $this->getRows(); 145 | } 146 | 147 | /** 148 | * @throws Exception 149 | */ 150 | public function getWidth(): int 151 | { 152 | return $this->getCols(); 153 | } 154 | 155 | /** 156 | * @throws Exception 157 | */ 158 | public function getNumPixels(): int 159 | { 160 | $pixels = $this->getWidth() * $this->getHeight(); 161 | 162 | if ($this->getDepth() === 16) { 163 | $pixels *= 2; 164 | } 165 | 166 | return $pixels; 167 | } 168 | 169 | /** 170 | * @throws Exception 171 | */ 172 | public function getChannelLength(int $width = null, int $height = null): int 173 | { 174 | $widthData = $width ?? $this->getWidth(); 175 | $heightData = $height ?? $this->getHeight(); 176 | 177 | switch ($this->getDepth()) { 178 | case 1: 179 | return (($widthData + 7) / 8) * $heightData; 180 | case 16: 181 | return $widthData * $heightData * 2; 182 | default: 183 | return $widthData * $heightData; 184 | } 185 | } 186 | 187 | /** 188 | * @throws Exception 189 | */ 190 | public function getFileLength(): int 191 | { 192 | return $this->getChannelLength() * $this->getChannels(); 193 | } 194 | 195 | /** 196 | * @throws Exception 197 | */ 198 | protected function readSignature(): void 199 | { 200 | $sig = $this->file->readString(4); 201 | 202 | if ($sig !== static::FILE_SIGNATURE) { 203 | throw new Exception(sprintf('Invalid file signature detected. Got: %s. Expected 8BPS.', $sig)); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Descriptor/DataMapper/DataMapper.php: -------------------------------------------------------------------------------- 1 | file = $file; 22 | } 23 | 24 | public function parseAlias(): string 25 | { 26 | $len = $this->file->readInt(); 27 | return $this->file->readString($len); 28 | } 29 | 30 | public function parseBoolean(): bool 31 | { 32 | return $this->file->readBoolean(); 33 | } 34 | 35 | public function parseDouble(): float 36 | { 37 | return $this->file->readDouble(); 38 | } 39 | 40 | public function parseFilePath(): FilePathData 41 | { 42 | $finish = $this->file->readInt() + $this->file->tell(); 43 | $sig = $this->file->readString(4); 44 | 45 | // Little endian. Who knows. 46 | $this->file->readIntLE(); // Path size 47 | $numChars = $this->file->readIntLE(); 48 | 49 | $path = $this->file->readUnicodeString($numChars); 50 | 51 | if ($this->file->tell() !== $finish) { 52 | throw new Exception('Fail read data.'); 53 | } 54 | 55 | return (new FilePathData())->setSig($sig)->setPath($path); 56 | } 57 | 58 | public function parseUnitDouble(): FloatPointNumberData 59 | { 60 | $unitId = $this->file->readString(4); 61 | 62 | $unit = array_search($unitId, FloatPointNumberData::FLOAT_POINT_NUMBER_FORMAT); 63 | 64 | if ($unit === false) { 65 | throw new Exception('Wrong double point number format. UnitId: \'%s\'', $unitId); 66 | } 67 | 68 | $value = $this->file->readDouble(); 69 | 70 | return (new FloatPointNumberData())->setId($unitId)->setUnit($unit)->setValue($value); 71 | } 72 | 73 | public function parseUnitFloat(): FloatPointNumberData 74 | { 75 | $unitId = $this->file->readString(4); 76 | 77 | $unit = array_search($unitId, FloatPointNumberData::FLOAT_POINT_NUMBER_FORMAT); 78 | 79 | if ($unit === false) { 80 | throw new Exception('Wrong double point number format. UnitId: \'%s\'', $unitId); 81 | } 82 | 83 | $value = $this->file->readFloat(); 84 | 85 | return (new FloatPointNumberData())->setId($unitId)->setUnit($unit)->setValue($value); 86 | } 87 | 88 | public function parseId(): string 89 | { 90 | $len = $this->file->readInt(); 91 | 92 | if ($len === 0) { 93 | return $this->file->readString(4); 94 | } 95 | 96 | return $this->file->readString($len); 97 | } 98 | 99 | public function parseIndex(): int 100 | { 101 | return $this->parseInteger(); 102 | } 103 | 104 | public function parseOffset(): int 105 | { 106 | return $this->parseInteger(); 107 | } 108 | 109 | public function parseIdentifier(): int 110 | { 111 | return $this->parseInteger(); 112 | } 113 | 114 | public function parseInteger(): int 115 | { 116 | return $this->file->readInt(); 117 | } 118 | 119 | public function parseLargeInteger(): int 120 | { 121 | return $this->file->readLongLong(); 122 | } 123 | 124 | /** 125 | * @throws Exception 126 | */ 127 | public function parseObjectArray(): void 128 | { 129 | throw new Exception(sprintf('Descriptor object array not implemented yet. %s', $this->file->tell())); 130 | } 131 | 132 | public function parseRawData(): string 133 | { 134 | $len = $this->file->readInt(); 135 | return $this->file->read($len); 136 | } 137 | 138 | public function parseClass(): ClassData 139 | { 140 | $name = $this->file->readUnicodeString(); 141 | $id = $this->parseId(); 142 | 143 | return (new ClassData())->setName($name)->setId($id); 144 | } 145 | 146 | public function parseEnum(): EnumData 147 | { 148 | $type = $this->parseId(); 149 | $value = $this->parseId(); 150 | 151 | return (new EnumData())->setType($type)->setValue($value); 152 | } 153 | 154 | public function parseEnumReference(): EnumReferenceData 155 | { 156 | $classData = $this->parseClass(); 157 | $type = $this->parseId(); 158 | $value = $this->parseId(); 159 | 160 | return (new EnumReferenceData())->setClassData($classData)->setType($type)->setValue($value); 161 | } 162 | 163 | public function parseProperty(): PropertyData 164 | { 165 | $classData = $this->parseClass(); 166 | $id = $this->parseId(); 167 | 168 | return (new PropertyData())->setClassData($classData)->setId($id); 169 | } 170 | 171 | public function parseText(): string 172 | { 173 | return $this->file->readUnicodeString(); 174 | } 175 | 176 | /** 177 | * @throws Exception 178 | */ 179 | public function parseItemType(): string 180 | { 181 | $type = $this->file->readString(4); 182 | 183 | if (!isset(ItemParserInterface::ITEM_TYPES[$type])) { 184 | throw new Exception(sprintf('Type format not supported. Type: %s', $type)); 185 | } 186 | 187 | return $type; 188 | } 189 | 190 | public function parseType(): string 191 | { 192 | return $this->file->readString(4); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

PSD-PHP

3 | Logo 4 |

Library for reading psd file

5 |
6 | 7 | --- 8 | 9 | ### Installation 10 | 11 | ``` 12 | composer require pixelfactory/psd-php 13 | ``` 14 | 15 | ### Usage 16 | Create an instance of the 'Psd' class by passing the file path. 17 | ```php 18 | require_once '../vendor/autoload.php'; 19 | 20 | $psd = new \Psd\Psd('./image.psd'); 21 | ``` 22 | 23 | Then you have two ways to use the library, 'simple' and 'professional'\ 24 | **Simple** - way is suitable for those who are not familiar with the structure of the psd file and just want to get the necessary information\ 25 | **Professional** - way can be used by more experienced developers to get access to a specific part of the file 26 | 27 | 28 | ### Simple 29 | 30 | #### Getting file sizes 31 | ```php 32 | $psd = new \Psd\Psd('./image.psd'); 33 | $psdSimpleMethods = $psd->getShortcuts(); 34 | 35 | echo $psdSimpleMethods->getWidth(); // Print file width 36 | echo $psdSimpleMethods->getHeight(); // Print file height 37 | ``` 38 | 39 | #### Saving an image 40 | 41 | ```php 42 | $psd = new \Psd\Psd('./image.psd'); 43 | $psdSimpleMethods = $psd->getShortcuts(); 44 | 45 | var_dump($psdSimpleMethods->savePreview('./out.png')); // Print 'true' if file be saved 46 | ``` 47 | 48 | #### Working with the layers tree 49 | ```php 50 | // TODO 51 | ``` 52 | 53 | ##### \[Layers tree\] Moving from directories 54 | ```php 55 | // TODO 56 | ``` 57 | 58 | ##### \[Layers tree\] Getting information about a layer 59 | ```php 60 | // TODO 61 | ``` 62 | 63 | ##### \[Layers tree\] Saving a layer image 64 | ```php 65 | // TODO 66 | ``` 67 | 68 | ### Professional 69 | 70 | The psd class has the same structure as the psd file. 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 86 | 87 | 88 | 89 | 90 | 95 | 96 | 97 | 98 | 103 | 104 | 105 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | 121 | 122 | 123 | 124 | 125 |
NameMethodExamples
82 | 83 | File header 84 | 85 | getHeaderLink
91 | 92 | Color mode data 93 | 94 |
99 | 100 | Image resources 101 | 102 | getResourcesLink
108 | 109 | Layer and mask information 110 | 111 | getLayersLink
117 | 118 | Image data 119 | 120 | getImageLink
126 | 127 | 1 - 'Color mode data' has no method because it is skipped and not processed by the library. This should not affect the work with most images because they have the "rgb" or "cmyk" color mode. This section is used only in the "Indexed" or "Duotone" color mode. 128 | 129 | #### Header data 130 | 131 | You can call the 'getHeader' method to get class implements [HeaderInterface](https://github.com/PixelFactory/psd-php/blob/master/src/FileStructure/Header/HeaderInterface.php) what contains methods for all fields image header section. 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 |
File header sectionHeaderInterface methods
Signature
VersiongetVersion
Reserved-
ChannelsgetChannels
heightgetRows (Alias: getHeight)
widthgetCols (Alias: getWidth)
DepthgetDepth
Color modegetMode (Convert mode number to text: modeName)
-parse
-getNumPixels
-getChannelLength
-getFileLength
191 | 192 | Example: 193 | ```php 194 | echo $psd->getHeader()->getMode(); // Return file mode (int) 195 | echo $psd->getHeader()->modeName(); // Return file mode name 196 | echo $psd->getHeader()->getChannels(); // Return file count channels 197 | ``` 198 | 199 | #### Image resources 200 | 201 | Image resources section store additional information. Such as guides, etc.\ 202 | The library is working with resources: 203 | - Guides(1032) 204 | - Layer Comps(1065) 205 | - Resolution Info(1005) 206 | 207 | The full list of resources you can be found in the [documentation](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_38034) 208 | 209 | To find the necessary resource, you need to call the method getResources (this method return class what extends from [ResourcesInterface](https://github.com/PixelFactory/psd-php/blob/master/src/LazyExecuteProxy/Interfaces/ResourcesInterface.php)). \ 210 | Next, you can use the search by the resource name or resource id. 211 | 212 | Example. Get guides: 213 | 214 | ```php 215 | /** @var \Psd\FileStructure\Resources\Resource\Guides\GuidesData[] $guides */ 216 | $guides = $psd 217 | ->getResources() 218 | ->getResourceById(\Psd\FileStructure\Resources\Resource\ResourceBase::RESOURCE_ID_GUIDES) 219 | ->getData(); 220 | 221 | foreach ($guides as $guide) { 222 | printf("%s - %s\n", $guide->getDirection(), $guide->getLocation()); // Result: 'vertical - 100' 223 | } 224 | ``` 225 | #### Layer and mask information 226 | ```php 227 | // TODO 228 | ``` 229 | 230 | #### Image data 231 | This section stores the image. You can get a class for exporting an image using the method [getExporter](https://github.com/PixelFactory/psd-php/blob/master/src/FileStructure/Image/Image.php#L47). \ 232 | Now is available only [png](https://github.com/PixelFactory/psd-php/blob/master/src/Image/ImageExport/Exports/Png.php) class for export image: 233 | ```php 234 | /* @var Psd\Image\ImageExport\Exports\Png $exporter */ 235 | $exporter = $psd->getImage()->getExporter(\Psd\Image\ImageExport\ImageExport::EXPORT_FORMAT_PNG); 236 | ``` 237 | All exporters classes implements interface: [ImageExportInterface](https://github.com/PixelFactory/psd-php/blob/master/src/Image/ImageExport/Exports/ImageExportInterface.php) \ 238 | You can export the image to the [Imagick](https://www.php.net/manual/en/class.imagick.php) class or save it. 239 | ```php 240 | /** @var Imagick $image */ 241 | $image = $exporter->export(); 242 | /** @var bool $status */ 243 | $status = $exporter->save('./out.png'); 244 | ``` 245 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 15 | 17 | 20 | 21 | 22 | 25 | 28 | 29 | 31 | 32 | 34 | 36 | 38 | 45 | 46 | 48 | 51 | 54 | 57 | 59 | 62 | 63 | 64 | 66 | 69 | 71 | 72 | 73 | 75 | 77 | 79 | 86 | 87 | 89 | 92 | 95 | 98 | 101 | 102 | 104 | 107 | 110 | 111 | -------------------------------------------------------------------------------- /src/File/File.php: -------------------------------------------------------------------------------- 1 | [ 22 | // 'length' => 8, 23 | // 'code' => 'J', 24 | // 'convert_little2big' => false, 25 | // ], 26 | 'longlong' => [ 27 | 'length' => 8, 28 | 'code' => 'q', 29 | 'convert_little2big' => true, 30 | ], 31 | 'double' => [ 32 | 'length' => 8, 33 | 'code' => 'E', 34 | 'convert_little2big' => false, 35 | ], 36 | 'float' => [ 37 | 'length' => 4, 38 | 'code' => 'g', 39 | 'convert_little2big' => false, 40 | ], 41 | 'uint' => [ 42 | 'length' => 4, 43 | 'code' => 'N', 44 | 'convert_little2big' => false, 45 | ], 46 | 'int' => [ 47 | 'length' => 4, 48 | 'code' => 'l', 49 | 'convert_little2big' => true, 50 | ], 51 | 'ushort' => [ 52 | 'length' => 2, 53 | 'code' => 'n', 54 | 'convert_little2big' => false, 55 | ], 56 | 'short' => [ 57 | 'length' => 2, 58 | 'code' => 's', 59 | 'convert_little2big' => true, 60 | ], 61 | 'int_le' => [ 62 | 'length' => 4, 63 | 'code' => 'l', 64 | 'convert_little2big' => false, 65 | ], 66 | ]; 67 | 68 | /** 69 | * @var bool 70 | */ 71 | protected bool $littleEndian; 72 | 73 | /** 74 | * File constructor. 75 | * 76 | * @param $fileName 77 | * @param string $openMode 78 | * @param false $useIncludePath 79 | * @param null $context 80 | */ 81 | public function __construct($fileName, $openMode = 'r', $useIncludePath = false, $context = null) 82 | { 83 | parent::__construct($fileName, $openMode, $useIncludePath, $context); 84 | $this->isLittleEndian(); 85 | } 86 | 87 | /** 88 | * @param $formatKey 89 | * @param null $convertL2B 90 | * @return mixed 91 | * 92 | */ 93 | protected function getData($formatKey, $convertL2B = null) 94 | { 95 | $length = static::FORMATS[$formatKey]['length']; 96 | $code = static::FORMATS[$formatKey]['code']; 97 | $convert = $convertL2B ?? static::FORMATS[$formatKey]['convert_little2big']; 98 | 99 | $str = $this->fRead($length); 100 | 101 | // Convert little to big only if OS use little 102 | if (true === $convert && true === $this->littleEndian) { 103 | $str = $this->lEndian2bEndian($str); 104 | } 105 | 106 | return unpack($code, $str)[1]; 107 | } 108 | 109 | public function readIntLE(): int 110 | { 111 | return $this->getData('int_le'); 112 | } 113 | 114 | public function readLongLong(): int 115 | { 116 | return $this->getData('longlong'); 117 | } 118 | 119 | public function readDouble(): float 120 | { 121 | return $this->getData('double'); 122 | } 123 | 124 | public function readFloat(): float 125 | { 126 | return $this->getData('float'); 127 | } 128 | 129 | public function readUint(): int 130 | { 131 | return $this->getData('uint'); 132 | } 133 | 134 | public function readInt(): int 135 | { 136 | return $this->getData('int'); 137 | } 138 | 139 | public function readUShort(): int 140 | { 141 | return $this->getData('ushort'); 142 | } 143 | 144 | public function readShort(): int 145 | { 146 | return $this->getData('short'); 147 | } 148 | 149 | /** 150 | * @param $size 151 | * @param ?callable $func 152 | * 153 | * @return array 154 | * 155 | * @throws Exception 156 | */ 157 | public function readBytes($size, callable $func = null): array 158 | { 159 | $bin = $this->fRead($size); 160 | $hex = bin2hex($bin); 161 | $data = str_split($hex, 2); 162 | 163 | foreach ($data as &$val) { 164 | $val = hexdec($val); 165 | 166 | if (isset($func)) { 167 | $val = $func($val); 168 | } 169 | } 170 | 171 | return $data; 172 | } 173 | 174 | /** 175 | * @return float|int 176 | * 177 | * @throws Exception 178 | */ 179 | public function readByte() 180 | { 181 | $bin = $this->fRead(1); 182 | 183 | return hexdec(bin2hex($bin)); 184 | } 185 | 186 | /** 187 | * Reads a boolean value. 188 | * @return bool 189 | * 190 | * @throws Exception 191 | */ 192 | public function readBoolean(): bool 193 | { 194 | return $this->readByte() !== 0; 195 | } 196 | 197 | /** 198 | * Reads a 32-bit color space value. 199 | * 200 | * @return array 201 | */ 202 | public function readSpaceColor(): array 203 | { 204 | $colorSpace = $this->readShort(); 205 | $colorComponents = []; 206 | 207 | for ($i = 0; $i < 4; $i++) { 208 | $colorComponents[] = ($this->readShort() >> 8); 209 | } 210 | 211 | return [ 212 | 'color_mode' => $colorSpace, 213 | 'color_components' => $colorComponents, 214 | ]; 215 | } 216 | 217 | /** 218 | * Reads a string of the given length and converts it to UTF-8 from the internally used MacRoman encoding. 219 | * 220 | * @param null $length 221 | * 222 | * @return string 223 | * 224 | * @throws Exception 225 | */ 226 | public function readString($length = null): string 227 | { 228 | if (!isset($length)) { 229 | $length = $this->readByte(); 230 | } 231 | 232 | return str_replace("\000", '', $this->fRead($length)); 233 | } 234 | 235 | /** 236 | * Reads a unicode string, which is double the length of a normal string and encoded as UTF-16. 237 | * 238 | * @param null $length 239 | * 240 | * @return false|string|string[] 241 | * 242 | * @throws Exception 243 | */ 244 | public function readUnicodeString($length = null): string 245 | { 246 | if (!isset($length)) { 247 | $length = $this->readInt(); 248 | } 249 | 250 | if (!isset($length) || $length <= 0) { 251 | return ''; 252 | } 253 | 254 | $stringU16 = $this->fRead($length * 2); 255 | 256 | return str_replace("\000", '', iconv('UTF-16BE', 'UTF-8', $stringU16)); 257 | } 258 | 259 | /** 260 | * Adobe's lovely signed 32-bit fixed-point number with 8bits.24bits 261 | * http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/PhotoshopFileFormats.htm#50577409_17587 262 | * 263 | * @return float|int|mixed 264 | * 265 | * @throws Exception 266 | */ 267 | public function readPathNumber(): float 268 | { 269 | return current(unpack('c*', $this->fRead(1))) + 270 | hexdec(current(unpack('H*', $this->fRead(3)))) / pow(2, 24); 271 | } 272 | 273 | 274 | /** 275 | * @param int $amt 276 | * @param bool $rel 277 | * @return int 278 | * 279 | * @throws Exception 280 | */ 281 | public function ffseek(int $amt, bool $rel = false): int 282 | { 283 | $status = parent::fseek($amt, $rel ? SEEK_CUR : SEEK_SET); 284 | $this->validateDefaultMethod($status === 0); 285 | 286 | return $status; 287 | } 288 | 289 | /** 290 | * @param $length 291 | * 292 | * @return string 293 | * 294 | * @throws Exception 295 | */ 296 | public function read($length): string 297 | { 298 | return $this->validateDefaultMethod(parent::fread($length)); 299 | } 300 | 301 | /** 302 | * @return int 303 | * 304 | * @throws Exception 305 | */ 306 | public function tell(): int 307 | { 308 | return $this->validateDefaultMethod(parent::ftell()); 309 | } 310 | 311 | /** 312 | * @param mixed $data 313 | * 314 | * @return mixed 315 | * 316 | * @throws Exception 317 | */ 318 | protected function validateDefaultMethod($data) 319 | { 320 | if (false === $data) { 321 | throw new Exception('File error.'); 322 | } 323 | 324 | return $data; 325 | } 326 | 327 | /** 328 | * Convert Little endian to big endian 329 | * @param $num 330 | * 331 | * @return string 332 | */ 333 | protected function lEndian2bEndian($num): string 334 | { 335 | $data = bin2hex($num); 336 | if (strlen($data) <= 2) { 337 | return $num; 338 | } 339 | $u = unpack("H*", strrev(pack("H*", $data))); 340 | 341 | return hex2bin($u[1]); 342 | } 343 | 344 | /** 345 | * @return void 346 | */ 347 | protected function isLittleEndian(): void 348 | { 349 | $testInt = 0x00FF; 350 | $p = pack('S', $testInt); 351 | $this->littleEndian = ($testInt === current(unpack('v', $p))); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/FileStructure/LayerMask/Layer/Layer.php: -------------------------------------------------------------------------------- 1 | file = $file; 50 | $this->header = $header; 51 | 52 | $this->blendingRanges = $this->buildBlendingRanges($this->file); 53 | $this->blendMode = $this->buildBlendMode($this->file); 54 | $this->info = $this->buildInfo($this->file); 55 | $this->legacyLayerName = $this->buildLegacyLayerName($this->file); 56 | $this->mask = $this->buildMask($this->file); 57 | $this->positionAndChannels = $this->buildPositionAndChannels($this->file); 58 | } 59 | 60 | public function parse(): void 61 | { 62 | $this->parsePositionAndChannels(); 63 | $this->parseBlendModes(); 64 | 65 | $layerEnd = $this->file->readInt() + $this->file->tell(); 66 | 67 | $this->parseMaskData(); 68 | $this->parseBlendingRanges(); 69 | $this->parseLegacyLayerName(); 70 | $this->parseLayerInfo($layerEnd); 71 | 72 | $this->file->ffseek($layerEnd); 73 | } 74 | 75 | public function export(): array 76 | { 77 | $position = $this->getPosition(); 78 | 79 | return [ 80 | 'name' => $this->getName(), 81 | 'opacity' => $this->blendMode->getOpacity(), 82 | 'visible' => $this->blendMode->getVisible(), 83 | 'clipped' => $this->blendMode->getClipped(), 84 | 'mask' => $this->mask->export(), 85 | 'top' => $position['top'], 86 | 'left' => $position['left'], 87 | 'right' => $position['right'], 88 | 'bottom' => $position['bottom'], 89 | 'width' => $position['width'], 90 | 'height' => $position['height'], 91 | ]; 92 | } 93 | 94 | public function getPosition(): array 95 | { 96 | try { 97 | $positionData = $this->info->getDataInfo(LayerInfoBuilderInterface::NAME_VECTOR_ORIGINATION) 98 | ->getData()['data']['keyDescriptorList'][0]['data']['keyOriginShapeBBox']; 99 | 100 | $top = $positionData['data'][RectKey::TOP]['value']; 101 | $left = $positionData['data'][RectKey::LEFT]['value']; 102 | $right = $positionData['data'][RectKey::RIGHT]['value']; 103 | $bottom = $positionData['data'][RectKey::BOTTOM]['value']; 104 | 105 | return [ 106 | 'top' => $top, 107 | 'left' => $left, 108 | 'right' => $right, 109 | 'bottom' => $bottom, 110 | 'width' => ($right - $left), 111 | 'height' => ($bottom - $top), 112 | ]; 113 | } catch (Throwable $ex) { 114 | return [ 115 | 'top' => $this->positionAndChannels->getTop(), 116 | 'right' => $this->positionAndChannels->getRight(), 117 | 'bottom' => $this->positionAndChannels->getBottom(), 118 | 'left' => $this->positionAndChannels->getLeft(), 119 | 'width' => $this->positionAndChannels->getWidth(), 120 | 'height' => $this->positionAndChannels->getHeight(), 121 | ]; 122 | } 123 | } 124 | 125 | public function getName(): string 126 | { 127 | try { 128 | return $this->info->getDataInfo(LayerInfoBuilderInterface::NAME_UNICODE_NAME)->getData(); 129 | } catch (Throwable $ex) { 130 | return $this->legacyLayerName->getLegacyName(); 131 | } 132 | } 133 | 134 | public function getInfo(): InfoInterface 135 | { 136 | return $this->info; 137 | } 138 | 139 | public function isFolder(): bool 140 | { 141 | if (isset($this->getInfo()->getData()[LayerInfoBuilderInterface::NAME_SECTION_DIVIDER])) { 142 | return $this->getInfo()->getData()[LayerInfoBuilderInterface::NAME_SECTION_DIVIDER]->getData()['isFolder']; 143 | } 144 | 145 | if (isset($this->getInfo()->getData()[LayerInfoBuilderInterface::NAME_NESTED_SECTION_DIVIDER])) { 146 | return $this->getInfo()->getData()[LayerInfoBuilderInterface::NAME_NESTED_SECTION_DIVIDER]->getData()['isFolder']; 147 | } 148 | 149 | return $this->getName() === ''; 150 | } 151 | 152 | public function isFolderEnd(): bool 153 | { 154 | if (isset($this->getInfo()->getData()[LayerInfoBuilderInterface::NAME_SECTION_DIVIDER])) { 155 | return $this->getInfo()->getData()[LayerInfoBuilderInterface::NAME_SECTION_DIVIDER]->getData()['isHidden']; 156 | } 157 | 158 | if (isset($this->getInfo()->getData()[LayerInfoBuilderInterface::NAME_NESTED_SECTION_DIVIDER])) { 159 | return $this->getInfo()->getData()[LayerInfoBuilderInterface::NAME_NESTED_SECTION_DIVIDER]->getData()['isHidden']; 160 | } 161 | 162 | return $this->getName() === ''; 163 | } 164 | 165 | public function getChannelImage(): ChannelImageInterface 166 | { 167 | if (!isset($this->channelImage)) { 168 | throw new Exception('Layer not parsed. ChannelImage is undefined.'); 169 | } 170 | 171 | return $this->channelImage; 172 | } 173 | 174 | public function parseChannelImage(): void 175 | { 176 | if (isset($this->channelImage)) { 177 | throw new Exception('Parsing error. ParseChannelImage cant be running twice.'); 178 | } 179 | 180 | $this->channelImage = new ChannelImageProxy( 181 | $this->buildChannelImage($this->file, $this->header, [ 182 | 'layerChannelsInfo' => $this->positionAndChannels->getChannelsInfo(), 183 | 'layerWidth' => $this->positionAndChannels->getWidth(), 184 | 'layerHeight' => $this->positionAndChannels->getHeight(), 185 | 'layerOpacity' => $this->blendMode->getOpacity(), 186 | 'layerChannels' => $this->positionAndChannels->getChannels(), 187 | 'layerMaskWidth' => $this->mask->getWidth(), 188 | 'layerMaskHeight' => $this->mask->getHeight(), 189 | ]), 190 | $this->file, 191 | ); 192 | } 193 | 194 | protected function parsePositionAndChannels(): void 195 | { 196 | $this->positionAndChannels->parse(); 197 | } 198 | 199 | protected function parseBlendModes(): void 200 | { 201 | $this->blendMode->parse(); 202 | } 203 | 204 | protected function parseMaskData(): void 205 | { 206 | $this->mask->parse(); 207 | } 208 | 209 | protected function parseBlendingRanges(): void 210 | { 211 | $this->blendingRanges->parse(); 212 | } 213 | 214 | protected function parseLegacyLayerName(): void 215 | { 216 | $this->legacyLayerName->parse(); 217 | } 218 | 219 | protected function parseLayerInfo(int $layerEnd): void 220 | { 221 | $this->info->parse($layerEnd); 222 | } 223 | 224 | protected function buildChannelImage( 225 | FileInterface $file, 226 | HeaderInterface $header, 227 | $layerData 228 | ): ChannelImageInterface 229 | { 230 | return new ChannelImage($file, $header, $layerData); 231 | } 232 | 233 | protected function buildBlendingRanges(FileInterface $file): BlendingRangesInterface 234 | { 235 | return new BlendingRanges($file); 236 | } 237 | 238 | protected function buildBlendMode(FileInterface $file): BlendModeInterface 239 | { 240 | return new BlendMode($file); 241 | } 242 | 243 | protected function buildInfo(FileInterface $file): InfoInterface 244 | { 245 | return new Info($file); 246 | } 247 | 248 | protected function buildLegacyLayerName(FileInterface $file): LegacyLayerNameInterface 249 | { 250 | return new LegacyLayerName($file); 251 | } 252 | 253 | protected function buildMask(FileInterface $file): MaskInterface 254 | { 255 | return new Mask($file); 256 | } 257 | 258 | protected function buildPositionAndChannels(FileInterface $file): PositionAndChannelsInterface 259 | { 260 | return new PositionAndChannels($file); 261 | } 262 | } 263 | --------------------------------------------------------------------------------