Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for writing new IPTC segments for jpeg format and new XMP segments for PNG and GIF images formats #1570

Merged
merged 20 commits into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 114 additions & 26 deletions MediaGalleryMetadata/Model/AddIptcMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface;
use Magento\MediaGalleryMetadataApi\Model\FileInterface;
use Magento\MediaGalleryMetadataApi\Model\SegmentInterface;
use Magento\MediaGalleryMetadata\Model\Jpeg\FileReader;
use Magento\Framework\Filesystem\DriverInterface;
use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory;
use Magento\Framework\Exception\LocalizedException;

/**
* Add metadata to the IPTC data
Expand All @@ -20,57 +24,141 @@ class AddIptcMetadata
private const IPTC_DESCRIPTION_SEGMENT = '2#120';
private const IPTC_KEYWORDS_SEGMENT = '2#025';

/**
* @var DriverInterface
*/
private $driver;

/**
* @var FileReader
*/
private $fileReader;

/**
* @var FileInterfaceFactory
*/
private $fileFactory;

/**
* @param FileInterfaceFactory $fileFactory
* @param DriverInterface $driver
* @param FileReader $fileReader
*/
public function __construct(
FileInterfaceFactory $fileFactory,
DriverInterface $driver,
FileReader $fileReader
) {
$this->fileFactory = $fileFactory;
$this->driver = $driver;
$this->fileReader = $fileReader;
}

/**
* Write metadata
*
* @param FileInterface $file
* @param MetadataInterface $metadata
* @param SegmentInterface $segment
* @return string
* @param null|SegmentInterface $segment
*/
public function execute(FileInterface $file, MetadataInterface $metadata, SegmentInterface $segment): string
public function execute(FileInterface $file, MetadataInterface $metadata, ?SegmentInterface $segment): FileInterface
{
if (is_callable('iptcembed')) {
$iptcData = iptcparse($segment->getData());
if (!empty($metadata->getTitle())) {
$iptcData[self::IPTC_TITLE_SEGMENT][0] = $metadata->getTitle();
}
if (!is_callable('iptcembed') && !is_callable('iptcparse')) {
throw new LocalizedException(__('iptcembed() && iptcparse() must be enabled in php configuration'));
}

$iptcData = $segment ? iptcparse($segment->getData()) : [];

if (!empty($metadata->getDescription())) {
$iptcData[self::IPTC_DESCRIPTION_SEGMENT][0] = $metadata->getDescription();
}
if (!empty($metadata->getTitle())) {
$iptcData[self::IPTC_TITLE_SEGMENT][0] = $metadata->getTitle();
}

if (!empty($metadata->getKeywords())) {
foreach ($metadata->getKeywords() as $key => $keyword) {
$iptcData[self::IPTC_KEYWORDS_SEGMENT][$key] = $keyword;
}
}
if (!empty($metadata->getDescription())) {
$iptcData[self::IPTC_DESCRIPTION_SEGMENT][0] = $metadata->getDescription();
}

if (!empty($metadata->getKeywords())) {
$iptcData = $this->writeKeywords($metadata->getKeywords(), $iptcData);
}

$newData = '';
$newData = '';

foreach ($iptcData as $tag => $values) {
foreach ($values as $value) {
$newData .= $this->iptcMaketag(2, substr($tag, 2), $value);
}
foreach ($iptcData as $tag => $values) {
foreach ($values as $value) {
$newData .= $this->iptcMaketag(2, (int) substr($tag, 2), $value);
}
$content = iptcembed($newData, $file->getPath());
}

$this->writeFile($file->getPath(), iptcembed($newData, $file->getPath()));

$fileWithIptc = $this->fileReader->execute($file->getPath());

return $this->fileFactory->create([
'path' => $fileWithIptc->getPath(),
'segments' => $this->getSegmentsWithIptc($fileWithIptc, $file)
]);
}

/**
* Return iptc segment from file.
*
* @param FileInterface $fileWithIptc
* @param FileInterface $originFile
*/
private function getSegmentsWithIptc(FileInterface $fileWithIptc, $originFile): array
{
$segments = $fileWithIptc->getSegments();
$originFileSegments = $originFile->getSegments();

return $content;
foreach ($segments as $key => $segment) {
if ($segment->getName() === 'APP13') {
$originFileSegments[$key] = $segments[$key];
return $originFileSegments;
}
}
return $originFileSegments;
}

/**
* Write keywords field to the iptc segment.
*
* @param array $keywords
* @param array $iptcData
*/
private function writeKeywords(array $keywords, array $iptcData): array
{
foreach ($keywords as $key => $keyword) {
$iptcData[self::IPTC_KEYWORDS_SEGMENT][$key] = $keyword;
}
return $iptcData;
}

/**
* Write iptc data to the image directly to the file.
*
* @param string $filePath
* @param string $content
*/
private function writeFile(string $filePath, string $content): void
{
$resource = $this->driver->fileOpen($filePath, 'wb');

$this->driver->fileWrite($resource, $content);
$this->driver->fileClose($resource);
}

/**
* Create new iptc tag text
*
* @param int $rec
* @param string $tag
* @param int $tag
* @param string $value
*/
private function iptcMaketag($rec, $tag, $value)
private function iptcMaketag(int $rec, int $tag, string $value)
{
//phpcs:disable Magento2.Functions.DiscouragedFunction
$length = strlen($value);
$retval = chr(0x1C) . chr($rec) . chr((int)$tag);
$retval = chr(0x1C) . chr($rec) . chr($tag);

if ($length < 0x8000) {
$retval .= chr($length >> 8) . chr($length & 0xFF);
Expand Down
27 changes: 12 additions & 15 deletions MediaGalleryMetadata/Model/Gif/FileReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ private function getSegments($resource): array
if ($separator == $gifFrameSeparator) {
$segments[] = $this->segmentFactory->create([
'name' => 'frame',
'data' => $this->readFrame($resource)
'data' => $gifFrameSeparator . $this->readFrame($resource)
]);
continue;
}
Expand All @@ -154,7 +154,6 @@ private function getSegments($resource): array
}

$segments[] = $this->getExtensionSegment($resource);

} while (!$this->driver->endOfFile($resource));

return $segments;
Expand All @@ -169,28 +168,29 @@ private function getSegments($resource): array
*/
private function getExtensionSegment($resource): SegmentInterface
{
$gifExtensionSeparator = pack("C", ord("!"));
$extensionCodeBinary = $this->read($resource, 1);
//phpcs:ignore Magento2.Functions.DiscouragedFunction
$extensionCode = unpack('C', $extensionCodeBinary)[1];

if ($extensionCode == 0xF9) {
return $this->segmentFactory->create([
'name' => 'Graphics Control Extension',
'data' => $extensionCodeBinary . $this->readBlock($resource)
'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource)
]);
}

if ($extensionCode == 0xFE) {
return $this->segmentFactory->create([
'name' => 'comment',
'data' => $extensionCodeBinary . $this->readBlock($resource)
'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource)
]);
}

if ($extensionCode != 0xFF) {
return $this->segmentFactory->create([
'name' => 'unknown',
'data' => $extensionCodeBinary . $this->readBlock($resource)
'name' => 'Programm extension',
'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource)
]);
}

Expand All @@ -206,14 +206,15 @@ private function getExtensionSegment($resource): SegmentInterface
if ($name == 'XMP DataXMP') {
return $this->segmentFactory->create([
'name' => $name,
'data' => $extensionCodeBinary . $blockLengthBinary
'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary
. $name . $this->readBlockWithSubblocks($resource)
]);
}

return $this->segmentFactory->create([
'name' => $name,
'data' => $extensionCodeBinary . $blockLengthBinary . $name . $this->readBlock($resource)
'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary
. $name . $this->readBlock($resource)
]);
}

Expand Down Expand Up @@ -300,17 +301,13 @@ private function readBlockWithSubblocks($resource): string
{
$data = '';
$subLength = $this->read($resource, 1);
$blocks = 0;

while ($subLength !== "\0") {
$blocks++;
$data .= $subLength;

$data .= $this->read($resource, ord($subLength));
$data .= $subLength . $this->read($resource, ord($subLength));
$subLength = $this->read($resource, 1);
}

return $data;
return $data . $subLength;
}

/**
Expand All @@ -327,6 +324,6 @@ private function readBlock($resource): string
if ($blockLength == 0) {
return '';
}
return $blockLength . $this->read($resource, $blockLength) . $this->read($resource, 1);
return $blockLengthBinary . $this->read($resource, $blockLength) . $this->read($resource, 1);
}
}
76 changes: 76 additions & 0 deletions MediaGalleryMetadata/Model/Gif/FileWriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\MediaGalleryMetadata\Model\Gif;

use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem\DriverInterface;
use Magento\MediaGalleryMetadataApi\Model\FileInterface;
use Magento\MediaGalleryMetadataApi\Model\FileWriterInterface;
use Magento\MediaGalleryMetadataApi\Model\SegmentInterface;
use Magento\MediaGalleryMetadata\Model\SegmentNames;

/**
* File segments writer
*/
class FileWriter implements FileWriterInterface
{
/**
* @var DriverInterface
*/
private $driver;

/**
* @var SegmentNames
*/
private $segmentNames;

/**
* @param DriverInterface $driver
* @param SegmentNames $segmentNames
*/
public function __construct(
DriverInterface $driver,
SegmentNames $segmentNames
) {
$this->driver = $driver;
$this->segmentNames = $segmentNames;
}

/**
* Write file object to the filesystem
*
* @param FileInterface $file
* @throws LocalizedException
* @throws FileSystemException
*/
public function execute(FileInterface $file): void
{
$resource = $this->driver->fileOpen($file->getPath(), 'wb');

$this->writeSegments($resource, $file->getSegments());
$this->driver->fileClose($resource);
}

/**
* Write gif segment
*
* @param resource $resource
* @param SegmentInterface[] $segments
*/
private function writeSegments($resource, array $segments): void
{
foreach ($segments as $segment) {
$this->driver->fileWrite(
$resource,
$segment->getData()
);
}
$this->driver->fileWrite($resource, pack("C", ord(";")));
}
}
8 changes: 4 additions & 4 deletions MediaGalleryMetadata/Model/Gif/Segment/XmpReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ class XmpReader implements MetadataReaderInterface
/**
* see XMP Specification Part 3, 1.1.2 GIF
*/
private const MAGIC_TRAILER_LENGTH = 257;
private const MAGIC_TRAILER_LENGTH = 258;
private const MAGIC_TRAILER_START = "\x01\xFF\xFE";
private const MAGIC_TRAILER_END = "\x03\x02\x01\x00";
private const MAGIC_TRAILER_END = "\x03\x02\x01\x00\x00";

/**
* @var MetadataInterfaceFactory
Expand Down Expand Up @@ -84,10 +84,10 @@ private function isXmp(SegmentInterface $segment): bool
*/
private function getXmpData(SegmentInterface $segment): string
{
$xmp = substr($segment->getData(), 13);
$xmp = substr($segment->getData(), 14);

if (substr($xmp, -self::MAGIC_TRAILER_LENGTH, 3) !== self::MAGIC_TRAILER_START
|| substr($xmp, -4) !== self::MAGIC_TRAILER_END
|| substr($xmp, -5) !== self::MAGIC_TRAILER_END
) {
throw new LocalizedException(__('XMP data is corrupted'));
}
Expand Down
Loading