Skip to content

Commit 34eb13b

Browse files
MegaChrizPowerKiKi
authored andcommitted
Added image quality support, with the maximum quality as default
This sets the quality of JPEG files to their maximum. It does this by: - defining a property called `$imageQuality` and a method called `getImageQuality()` to PhpOffice\PhpWord\Element\Image - setting the property `$imageQuality` to `100` for JPG, but keep default compression level for PNG - refactor fragile string callbacks into proper closure that use the new `$imageQuality` In the case of a PNG, this is not the _quality_ of the image, but the _filesize_ of the image. So we use the default value for best speed/filesize balance.
1 parent ee42aa3 commit 34eb13b

File tree

4 files changed

+103
-64
lines changed

4 files changed

+103
-64
lines changed

src/PhpWord/Element/Image.php

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class Image extends AbstractElement
8989
/**
9090
* Image function.
9191
*
92-
* @var string
92+
* @var null|callable(resource): void
9393
*/
9494
private $imageFunc;
9595

@@ -100,6 +100,16 @@ class Image extends AbstractElement
100100
*/
101101
private $imageExtension;
102102

103+
/**
104+
* Image quality.
105+
*
106+
* Functions imagepng() and imagejpeg() have an optional parameter for
107+
* quality.
108+
*
109+
* @var null|int
110+
*/
111+
private $imageQuality;
112+
103113
/**
104114
* Is memory image.
105115
*
@@ -249,13 +259,21 @@ public function getImageCreateFunction()
249259
/**
250260
* Get image function.
251261
*
252-
* @return string
262+
* @return null|callable(resource): void
253263
*/
254-
public function getImageFunction()
264+
public function getImageFunction(): ?callable
255265
{
256266
return $this->imageFunc;
257267
}
258268

269+
/**
270+
* Get image quality.
271+
*/
272+
public function getImageQuality(): ?int
273+
{
274+
return $this->imageQuality;
275+
}
276+
259277
/**
260278
* Get image extension.
261279
*
@@ -317,20 +335,13 @@ public function setMediaIndex($value): void
317335
}
318336

319337
/**
320-
* Get image string data.
321-
*
322-
* @param bool $base64
323-
*
324-
* @return null|string
325-
*
326-
* @since 0.11.0
338+
* Get image string.
327339
*/
328-
public function getImageStringData($base64 = false)
340+
public function getImageString(): ?string
329341
{
330342
$source = $this->source;
331343
$actualSource = null;
332344
$imageBinary = null;
333-
$imageData = null;
334345
$isTemp = false;
335346

336347
// Get actual source from archive image or other source
@@ -367,7 +378,8 @@ public function getImageStringData($base64 = false)
367378
imagesavealpha($imageResource, true);
368379
}
369380
ob_start();
370-
call_user_func($this->imageFunc, $imageResource);
381+
$callback = $this->imageFunc;
382+
$callback($imageResource);
371383
$imageBinary = ob_get_contents();
372384
ob_end_clean();
373385
} elseif ($this->sourceType == self::SOURCE_STRING) {
@@ -379,20 +391,36 @@ public function getImageStringData($base64 = false)
379391
fclose($fileHandle);
380392
}
381393
}
382-
if ($imageBinary !== null) {
383-
if ($base64) {
384-
$imageData = chunk_split(base64_encode($imageBinary));
385-
} else {
386-
$imageData = chunk_split(bin2hex($imageBinary));
387-
}
388-
}
389394

390395
// Delete temporary file if necessary
391396
if ($isTemp === true) {
392397
@unlink($actualSource);
393398
}
394399

395-
return $imageData;
400+
return $imageBinary;
401+
}
402+
403+
/**
404+
* Get image string data.
405+
*
406+
* @param bool $base64
407+
*
408+
* @return null|string
409+
*
410+
* @since 0.11.0
411+
*/
412+
public function getImageStringData($base64 = false)
413+
{
414+
$imageBinary = $this->getImageString();
415+
if ($imageBinary === null) {
416+
return null;
417+
}
418+
419+
if ($base64) {
420+
return chunk_split(base64_encode($imageBinary));
421+
}
422+
423+
return chunk_split(bin2hex($imageBinary));
396424
}
397425

398426
/**
@@ -503,31 +531,45 @@ private function setFunctions(): void
503531
switch ($this->imageType) {
504532
case 'image/png':
505533
$this->imageCreateFunc = $this->sourceType == self::SOURCE_STRING ? 'imagecreatefromstring' : 'imagecreatefrompng';
506-
$this->imageFunc = 'imagepng';
534+
$this->imageFunc = function ($resource): void {
535+
imagepng($resource, null, $this->imageQuality);
536+
};
507537
$this->imageExtension = 'png';
538+
$this->imageQuality = -1;
508539

509540
break;
510541
case 'image/gif':
511542
$this->imageCreateFunc = $this->sourceType == self::SOURCE_STRING ? 'imagecreatefromstring' : 'imagecreatefromgif';
512-
$this->imageFunc = 'imagegif';
543+
$this->imageFunc = function ($resource): void {
544+
imagegif($resource);
545+
};
513546
$this->imageExtension = 'gif';
547+
$this->imageQuality = null;
514548

515549
break;
516550
case 'image/jpeg':
517551
case 'image/jpg':
518552
$this->imageCreateFunc = $this->sourceType == self::SOURCE_STRING ? 'imagecreatefromstring' : 'imagecreatefromjpeg';
519-
$this->imageFunc = 'imagejpeg';
553+
$this->imageFunc = function ($resource): void {
554+
imagejpeg($resource, null, $this->imageQuality);
555+
};
520556
$this->imageExtension = 'jpg';
557+
$this->imageQuality = 100;
521558

522559
break;
523560
case 'image/bmp':
524561
case 'image/x-ms-bmp':
525562
$this->imageType = 'image/bmp';
563+
$this->imageFunc = null;
526564
$this->imageExtension = 'bmp';
565+
$this->imageQuality = null;
527566

528567
break;
529568
case 'image/tiff':
569+
$this->imageType = 'image/tiff';
570+
$this->imageFunc = null;
530571
$this->imageExtension = 'tif';
572+
$this->imageQuality = null;
531573

532574
break;
533575
}

src/PhpWord/Media.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ public static function addElement($container, $mediaType, $source, ?Image $image
7474
$mediaData['imageType'] = $image->getImageType();
7575
if ($isMemImage) {
7676
$mediaData['isMemImage'] = true;
77-
$mediaData['createFunction'] = $image->getImageCreateFunction();
78-
$mediaData['imageFunction'] = $image->getImageFunction();
77+
$mediaData['imageString'] = $image->getImageString();
7978
}
8079
$target = "{$container}_image{$mediaTypeCount}.{$extension}";
8180
$image->setTarget($target);

src/PhpWord/Writer/AbstractWriter.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -347,17 +347,8 @@ protected function addFilesToPackage(ZipArchive $zip, $elements): void
347347

348348
// Retrive GD image content or get local media
349349
if (isset($element['isMemImage']) && $element['isMemImage']) {
350-
$image = call_user_func($element['createFunction'], $element['source']);
351-
if ($element['imageType'] === 'image/png') {
352-
// PNG images need to preserve alpha channel information
353-
imagesavealpha($image, true);
354-
}
355-
ob_start();
356-
call_user_func($element['imageFunction'], $image);
357-
$imageContents = ob_get_contents();
358-
ob_end_clean();
350+
$imageContents = $element['imageString'];
359351
$zip->addFromString($target, $imageContents);
360-
imagedestroy($image);
361352
} else {
362353
$this->addFileToPackage($zip, $element['source'], $target);
363354
}

tests/PhpWordTests/Element/ImageTest.php

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323

2424
/**
2525
* Test class for PhpOffice\PhpWord\Element\Image.
26-
*
27-
* @runTestsInSeparateProcesses
2826
*/
2927
class ImageTest extends AbstractWebServerEmbeddedTest
3028
{
@@ -65,33 +63,40 @@ public function testConstructWithStyle(): void
6563

6664
/**
6765
* Valid image types.
66+
*
67+
* @dataProvider providerImages
6868
*/
69-
public function testImages(): void
69+
public function testImages($source, $type, $extension, $createFunction, $imageFunction, $imageQuality): void
7070
{
71-
$images = [
72-
['mars.jpg', 'image/jpeg', 'jpg', 'imagecreatefromjpeg', 'imagejpeg'],
73-
['mario.gif', 'image/gif', 'gif', 'imagecreatefromgif', 'imagegif'],
74-
['firefox.png', 'image/png', 'png', 'imagecreatefrompng', 'imagepng'],
75-
['duke_nukem.bmp', 'image/bmp', 'bmp', null, null],
76-
['angela_merkel.tif', 'image/tiff', 'tif', null, null],
77-
];
78-
79-
foreach ($images as $imageData) {
80-
[$source, $type, $extension, $createFunction, $imageFunction] = $imageData;
81-
$nam = ucfirst(strtok($source, '.'));
82-
$source = __DIR__ . "/../_files/images/{$source}";
83-
$image = new Image($source, null, null, $nam);
84-
self::assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $image);
85-
self::assertEquals($source, $image->getSource());
86-
self::assertEquals($nam, $image->getName());
87-
self::assertEquals(md5($source), $image->getMediaId());
88-
self::assertEquals($type, $image->getImageType());
89-
self::assertEquals($extension, $image->getImageExtension());
90-
self::assertEquals($createFunction, $image->getImageCreateFunction());
91-
self::assertEquals($imageFunction, $image->getImageFunction());
92-
self::assertFalse($image->isMemImage());
93-
self::assertNotNull($image->getImageStringData());
71+
$nam = ucfirst(strtok($source, '.'));
72+
$source = __DIR__ . "/../_files/images/{$source}";
73+
$image = new Image($source, null, null, $nam);
74+
self::assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $image);
75+
self::assertEquals($source, $image->getSource());
76+
self::assertEquals($nam, $image->getName());
77+
self::assertEquals(md5($source), $image->getMediaId());
78+
self::assertEquals($type, $image->getImageType());
79+
self::assertEquals($extension, $image->getImageExtension());
80+
self::assertEquals($createFunction, $image->getImageCreateFunction());
81+
if ($imageFunction) {
82+
self::assertNotNull($image->getImageFunction());
83+
} else {
84+
self::assertNull($image->getImageFunction());
9485
}
86+
self::assertEquals($imageQuality, $image->getImageQuality());
87+
self::assertFalse($image->isMemImage());
88+
self::assertNotNull($image->getImageStringData());
89+
}
90+
91+
public function providerImages(): array
92+
{
93+
return [
94+
['mars.jpg', 'image/jpeg', 'jpg', 'imagecreatefromjpeg', true, 100],
95+
['mario.gif', 'image/gif', 'gif', 'imagecreatefromgif', true, null],
96+
['firefox.png', 'image/png', 'png', 'imagecreatefrompng', true, -1],
97+
['duke_nukem.bmp', 'image/bmp', 'bmp', null, false, null],
98+
['angela_merkel.tif', 'image/tiff', 'tif', null, false, null],
99+
];
95100
}
96101

97102
/**
@@ -204,7 +209,8 @@ public function testConstructFromString(): void
204209
self::assertEquals('image/jpeg', $image->getImageType());
205210
self::assertEquals('jpg', $image->getImageExtension());
206211
self::assertEquals('imagecreatefromstring', $image->getImageCreateFunction());
207-
self::assertEquals('imagejpeg', $image->getImageFunction());
212+
self::assertNotNull($image->getImageFunction());
213+
self::assertEquals(100, $image->getImageQuality());
208214
self::assertTrue($image->isMemImage());
209215

210216
self::assertNotNull($image->getImageStringData());
@@ -225,7 +231,8 @@ public function testConstructFromGd(): void
225231
self::assertEquals('image/png', $image->getImageType());
226232
self::assertEquals('png', $image->getImageExtension());
227233
self::assertEquals('imagecreatefrompng', $image->getImageCreateFunction());
228-
self::assertEquals('imagepng', $image->getImageFunction());
234+
self::assertNotNull($image->getImageFunction());
235+
self::assertEquals(-1, $image->getImageQuality());
229236
self::assertTrue($image->isMemImage());
230237

231238
self::assertNotNull($image->getImageStringData());

0 commit comments

Comments
 (0)