├── .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 |
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 |
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 | File header section
137 | HeaderInterface methods
138 |
139 |
140 |
141 |
142 | Signature
143 |
144 |
145 |
146 | Version
147 | getVersion
148 |
149 |
150 | Reserved
151 | -
152 |
153 |
154 | Channels
155 | getChannels
156 |
157 |
158 | height
159 | getRows (Alias: getHeight)
160 |
161 |
162 | width
163 | getCols (Alias: getWidth)
164 |
165 |
166 | Depth
167 | getDepth
168 |
169 |
170 | Color mode
171 | getMode (Convert mode number to text: modeName)
172 |
173 |
174 | -
175 | parse
176 |
177 |
178 | -
179 | getNumPixels
180 |
181 |
182 | -
183 | getChannelLength
184 |
185 |
186 | -
187 | getFileLength
188 |
189 |
190 |
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 |
--------------------------------------------------------------------------------