Skip to content

Commit

Permalink
✨ add support for image compression
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianMindee committed Oct 21, 2024
1 parent 4088e5d commit 76cd859
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 2 deletions.
15 changes: 15 additions & 0 deletions src/Error/MindeeImageException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

/**
* @file
* Mindee Image handling Exceptions.
*/

namespace Mindee\Error;

/**
* Exceptions relating to the handling of images.
*/
class MindeeImageException extends MindeeException
{
}
37 changes: 37 additions & 0 deletions src/Image/ImageCompressor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Mindee\Image;

use Mindee\Error\MindeeImageException;

/**
* Image compressor class to handle image compression.
*/
class ImageCompressor
{
/**
* @param \Imagick|\SplFileObject $inputImage Input image.
* @param integer|null $quality Quality to apply to the image.
* @param integer|null $maxWidth Maximum width to constrain the image to.
* Defaults to the image's size if unset.
* @param integer|null $maxHeight Maximum Height to constrain the image to.
* Defaults to the image's size if unset.
* @return \CURLFile Curlfile handle for the image.
* @throws MindeeImageException Throws if image processing fails.
*/
public static function compressImage(
$inputImage,
?int $quality = 85,
?int $maxWidth = null,
?int $maxHeight = null
): \CURLFile {
try {
$image = ImageUtils::toMagickImage($inputImage);
ImageUtils::resizeImage($image, $maxWidth, $maxHeight);
ImageUtils::compressImageQuality($image, $quality);
return ImageUtils::toCURLFile($image);
} catch (\Exception $e) {
throw new MindeeImageException("Image compression failed.\n" . $e->getMessage());
}
}
}
91 changes: 91 additions & 0 deletions src/Image/ImageUtils.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace Mindee\Image;

use Mindee\Error\MindeeImageException;

/**
* Miscellaneous image operations.
*/
class ImageUtils
{
/**
* @param mixed $image Image handle.
* @return \Imagick|\CURLFile|\SplFileObject|resource A valid Imagick handle, CURLFile, or resource.
* The resulting image is formatted to jpeg.
* @throws MindeeImageException Throws if something goes wrong during image conversion.
*/
public static function toMagickImage($image): \Imagick
{
try {
if ($image instanceof \Imagick) {
$imagickHandle = $image;
$imagickHandle->setImageFormat('jpeg');
} elseif ($image instanceof \SplFileObject) {
$imagickHandle = new \Imagick();
$imagickHandle->readImage($image->getRealPath());
} elseif ($image instanceof \CURLFile) {
$imagickHandle = new \Imagick();
$imagickHandle->readImage($image->getFilename());
} elseif (is_resource($image)) {
$imagickHandle = new \Imagick();
$imagickHandle->readImageBlob($image);
} else {
throw new MindeeImageException('Input image must be a SplFileObject, resource or Imagick handle.');
}
$imagickHandle->setImageFormat('jpeg');
return $imagickHandle;
} catch (MindeeImageException $e) {
throw $e;
} catch (\Exception $e) {
throw new MindeeImageException("Conversion to MagickImage failed.\n" . $e->getMessage());
}
}

/**
* Resizes a provided MiniMagick Image with the given width & height, if present.
*
* @param \Imagick $image Imagick image handle.
* @param integer|null $width Width to comply with.
* @param integer|null $height Height to comply with.
* @return void
* @throws \ImagickException Throws if resizing fails.
*/
public static function resizeImage(\Imagick $image, ?int $width = null, int $height = null)
{
$width ??= $image->getImageWidth();
$height ??= $image->getImageHeight();
$image->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 1);
}


/**
* Compresses the quality of the provided MiniMagick image.
* @param \Imagick $image Imagick image handle.
* @param integer $quality Quality to apply to the image. This operation is independent of a JPG's base quality.
* @return void
* @throws \ImagickException Throws if compression fails.
*/
public static function compressImageQuality(\Imagick $image, int $quality = 85)
{
$image->setImageCompressionQuality($quality);
}

/**
* Converts an Imagick into a valid CURLFile handle.
* @param \Imagick $image Imagick image handle.
* @return \CURLFile
* @throws MindeeImageException Throws if the image can't be converted back into a CURLFile.
*/
public static function toCURLFile(\Imagick $image): \CURLFile
{
try {
$tempFile = tempnam(sys_get_temp_dir(), 'convert_image_');
file_put_contents($tempFile, $image->getImageBlob());
$filenameWithoutExtension = pathinfo($image->getFilename(), PATHINFO_FILENAME);
return new \CURLFile($tempFile, 'image/jpeg', $filenameWithoutExtension . '.jpg');
} catch (\Exception $e) {
throw new MindeeImageException("Conversion to CURLFile failed.\n" . $e->getMessage());
}
}
}
33 changes: 33 additions & 0 deletions src/Input/LocalInputSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
namespace Mindee\Input;

use CURLFile;
use Mindee\Error\MindeeImageException;
use Mindee\Error\MindeeMimeTypeException;
use Mindee\Error\MindeePDFException;
use Mindee\Error\MindeeSourceException;
use Mindee\Image\ImageCompressor;
use setasign\Fpdi\Fpdi;
use setasign\Fpdi\PdfParser\PdfParserException;
use setasign\Fpdi\PdfReader\PdfReaderException;
Expand Down Expand Up @@ -302,4 +304,35 @@ public function enableStrictMode()
{
$this->throwsOnClose = true;
}

/**
* @param integer $quality Quality of the output file.
* @param integer $maxWidth Maximum width (Ignored for PDFs).
* @param integer $maxHeight Maximum height (Ignored for PDFs).
* @param boolean $forceSourceText Whether to force the operation on PDFs with source text.
* This will attempt to re-render PDF text over the rasterized original. If disabled, ignored the operation.
* WARNING: this operation is strongly discouraged.
* @param boolean $disableSourceText If the PDF has source text, whether to re-apply it to the original or not.
* Needs force_source_text to work.
* @return void
* @throws MindeeImageException Throws if image handling goes wrong.
*/
public function compress(
int $quality = 85,
int $maxWidth = null,
int $maxHeight = null,
bool $forceSourceText = false,
bool $disableSourceText = true
): void {
$splHandle = $fileObject = new \SplFileObject($this->fileObject->getFilename());
if ($this->isPDF()) {
// TODO
} else {
$this->fileObject = ImageCompressor::compressImage($splHandle, $quality, $maxWidth, $maxHeight);
// echo "FILEOBJ: ".$this->fileObject->getPostFilename();
$this->fileMimetype = 'image/jpeg';
$pathInfo = pathinfo($this->filePath);
$this->filePath = $pathInfo['dirname'] . DIRECTORY_SEPARATOR . $pathInfo['filename'] . '.jpeg';
}
}
}
58 changes: 56 additions & 2 deletions tests/Input/LocalInputSourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Mindee\Client;
use Mindee\Error\MindeePDFException;
use Mindee\Error\MindeeSourceException;
use Mindee\Image\ImageCompressor;
use Mindee\Input\PathInput;
use PHPUnit\Framework\TestCase;
use setasign\Fpdi\Fpdi;
Expand All @@ -20,6 +21,8 @@ class LocalInputSourceTest extends TestCase
private string $oldKey;
protected Client $dummyClient;
protected string $fileTypesDir;
protected string $productsDir;
protected string $outputDir;

protected function setUp(): void
{
Expand All @@ -31,7 +34,10 @@ protected function setUp(): void
) . "/tests/resources/file_types/";
$this->productsDir = (
getenv('GITHUB_WORKSPACE') ?: "."
) . "/tests/resources/products/";
) . "/tests/resources/products/";
$this->outputDir = (
getenv('GITHUB_WORKSPACE') ?: "."
) . "/tests/resources/output/";
}

protected function tearDown(): void
Expand Down Expand Up @@ -93,7 +99,7 @@ public function testPDFCutNPages(array $indexes)
}
$basePdf->Close();
$cutPdf->Close();
} catch (PdfParserException|PdfReaderException $e) {
} catch (PdfParserException | PdfReaderException $e) {
throw new MindeePDFException("Failed to read PDF file.");
}
}
Expand Down Expand Up @@ -233,4 +239,52 @@ public function testShouldSendCorrectResultsForBrokenFixableInvoicePdf()
$sourceDocFixed = $this->dummyClient->sourceFromPath($this->fileTypesDir . '/pdf/broken_invoice.pdf', true);
$this->assertEquals($sourceDocFixed->readContents()[1], $sourceDocOriginal->readContents()[1]);
}

public function testImageQualityCompressionFromInputSource()
{
$receiptInput = $this->dummyClient->sourceFromPath($this->fileTypesDir . '/receipt.jpg');
$receiptInput->compress(80);
file_put_contents(
$this->outputDir . "/compress_indirect.jpg",
file_get_contents($receiptInput->fileObject->getFilename())
);
$sizeOriginal = filesize($this->fileTypesDir . '/receipt.jpg');
$sizeCompressed = filesize($this->outputDir . "/compress_indirect.jpg");
$this->assertGreaterThan($sizeCompressed, $sizeOriginal);
}

public function testDirectImageQualityCompression()
{
$receiptInput = $this->dummyClient->sourceFromPath($this->fileTypesDir . '/receipt.jpg');
$sizeOriginal = filesize($this->fileTypesDir . '/receipt.jpg');
$compresses = [
100 => ImageCompressor::compressImage($receiptInput->fileObject, 100),
85 => ImageCompressor::compressImage($receiptInput->fileObject),
50 => ImageCompressor::compressImage($receiptInput->fileObject, 50),
10 => ImageCompressor::compressImage($receiptInput->fileObject, 10),
1 => ImageCompressor::compressImage($receiptInput->fileObject, 1)
];

$outputFiles = [
100 => $this->outputDir . "/compress100.jpg",
85 => $this->outputDir . "/compress85.jpg",
50 => $this->outputDir . "/compress50.jpg",
10 => $this->outputDir . "/compress10.jpg",
1 => $this->outputDir . "/compress1.jpg",
];

$compressSize = [];
foreach ($compresses as $key => $value) {
file_put_contents(
$outputFiles[$key],
file_get_contents($value->getFilename())
);
$compressSize[$key] = filesize($outputFiles[$key]);
}
$this->assertGreaterThan($compressSize[85], $compressSize[100]);
$this->assertGreaterThan($sizeOriginal, $compressSize[85]);
$this->assertGreaterThan($compressSize[50], $sizeOriginal);
$this->assertGreaterThan($compressSize[10], $compressSize[50]);
$this->assertGreaterThan($compressSize[1], $compressSize[10]);
}
}

0 comments on commit 76cd859

Please sign in to comment.