Skip to content

Commit 1d07e9f

Browse files
committed
feat: add clearMetadata() method to provide privacy options when using imagick handler
1 parent 958cd54 commit 1d07e9f

File tree

11 files changed

+218
-2
lines changed

11 files changed

+218
-2
lines changed

system/Images/Handlers/BaseHandler.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,4 +769,19 @@ public function getHeight()
769769
{
770770
return ($this->resource !== null) ? $this->_getHeight() : $this->height;
771771
}
772+
773+
/**
774+
* Clears image metadata.
775+
*
776+
* This method has no use in the GDHandler,
777+
* since all the data are cleared automatically.
778+
*
779+
* GDHandler can't preserve the image metadata.
780+
*
781+
* @param array<int|string, array<int, string>|string> $data
782+
*/
783+
public function clearMetadata(array $data = []): static
784+
{
785+
return $this;
786+
}
772787
}

system/Images/Handlers/ImageMagickHandler.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,4 +537,51 @@ public function reorient(bool $silent = false)
537537
default => $this,
538538
};
539539
}
540+
541+
/**
542+
* Clears metadata from the image based on specified parameters.
543+
*
544+
* Configuration for metadata clearing:
545+
* - If empty, all metadata is stripped
546+
* - If contains 'except' key, keeps only those properties
547+
* - Otherwise, deletes only the specified keys
548+
*
549+
* @param array<int|string, array<int, string>|string> $data
550+
*
551+
* @return $this
552+
*
553+
* @throws ImagickException
554+
*/
555+
public function clearMetadata(array $data = []): static
556+
{
557+
$this->ensureResource();
558+
559+
// Strip all metadata when no parameters are provided
560+
if ($data === []) {
561+
$this->resource->stripImage();
562+
563+
return $this;
564+
}
565+
566+
// Keep only properties specified in 'except' array
567+
if (isset($data['except'])) {
568+
$propertiesToKeep = (array) $data['except'];
569+
$allPropertyNames = $this->resource->getImageProperties('*', false);
570+
571+
foreach ($allPropertyNames as $property) {
572+
if (! in_array($property, $propertiesToKeep, true)) {
573+
$this->resource->deleteImageProperty($property);
574+
}
575+
}
576+
577+
return $this;
578+
}
579+
580+
// Delete only specific properties
581+
foreach ($data as $property) {
582+
$this->resource->deleteImageProperty($property);
583+
}
584+
585+
return $this;
586+
}
540587
}

tests/system/Images/GDHandlerTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,4 +454,13 @@ public function testImageReorientPortrait(): void
454454
$this->assertSame(['red' => 62, 'green' => 62, 'blue' => 62, 'alpha' => 0], $rgb);
455455
}
456456
}
457+
458+
public function testClearMetadataReturnsSelf(): void
459+
{
460+
$this->handler->withFile($this->path);
461+
462+
$result = $this->handler->clearMetadata();
463+
464+
$this->assertSame($this->handler, $result);
465+
}
457466
}

tests/system/Images/ImageMagickHandlerTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,4 +443,92 @@ public function testImageReorientPortrait(): void
443443
$this->assertSame(['red' => 62, 'green' => 62, 'blue' => 62, 'alpha' => 0], $rgb);
444444
}
445445
}
446+
447+
public function testClearMetadataEnsuresResource(): void
448+
{
449+
$this->expectException(ImageException::class);
450+
$this->handler->clearMetadata();
451+
}
452+
453+
public function testClearMetadataReturnsSelf(): void
454+
{
455+
$this->handler->withFile($this->path);
456+
457+
$result = $this->handler->clearMetadata();
458+
459+
$this->assertSame($this->handler, $result);
460+
}
461+
462+
public function testClearMetadataAll(): void
463+
{
464+
$this->handler->withFile($this->path);
465+
/** @var Imagick $imagick */
466+
$imagick = $this->handler->getResource();
467+
$before = $imagick->getImageProperties();
468+
$this->assertCount(14, $before);
469+
470+
$this->handler
471+
->clearMetadata()
472+
->save($this->root . 'ci-logo-no-metadata.png');
473+
474+
$this->handler->withFile($this->root . 'ci-logo-no-metadata.png');
475+
/** @var Imagick $imagick */
476+
$imagick = $this->handler->getResource();
477+
$after = $imagick->getImageProperties();
478+
479+
$this->assertCount(9, $after);
480+
}
481+
482+
public function testClearMetadataExcept(): void
483+
{
484+
$this->handler->withFile($this->path);
485+
/** @var Imagick $imagick */
486+
$imagick = $this->handler->getResource();
487+
$before = $imagick->getImageProperties();
488+
$this->assertCount(14, $before);
489+
490+
// Keep 2 properties
491+
$this->handler
492+
->clearMetadata(['except' => ['png:bKGD', 'png:cHRM']])
493+
->save($this->root . 'ci-logo-no-metadata.png');
494+
495+
$this->handler->withFile($this->root . 'ci-logo-no-metadata.png');
496+
/** @var Imagick $imagick */
497+
$imagick = $this->handler->getResource();
498+
$after = $imagick->getImageProperties();
499+
500+
$this->assertArrayHasKey('png:bKGD', $after);
501+
$this->assertArrayHasKey('png:cHRM', $after);
502+
$this->assertArrayNotHasKey('png:gAMA', $after);
503+
504+
$this->assertCount(12, $after);
505+
}
506+
507+
public function testClearMetadataSpecific(): void
508+
{
509+
$this->handler->withFile($this->path);
510+
/** @var Imagick $imagick */
511+
$imagick = $this->handler->getResource();
512+
$before = $imagick->getImageProperties();
513+
514+
$this->assertArrayNotHasKey('png:tIME', $before);
515+
$this->assertCount(14, $before);
516+
517+
// Delete only 1
518+
$this->handler
519+
->clearMetadata(['png:gAMA'])
520+
->save($this->root . 'ci-logo-no-metadata.png');
521+
522+
$this->handler->withFile($this->root . 'ci-logo-no-metadata.png');
523+
/** @var Imagick $imagick */
524+
$imagick = $this->handler->getResource();
525+
$after = $imagick->getImageProperties();
526+
527+
$this->assertArrayHasKey('png:bKGD', $after);
528+
$this->assertArrayHasKey('png:cHRM', $after);
529+
$this->assertArrayHasKey('png:tIME', $after);
530+
$this->assertArrayNotHasKey('png:gAMA', $after);
531+
532+
$this->assertCount(14, $after);
533+
}
446534
}

user_guide_src/source/changelogs/v4.7.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Libraries
6464

6565
**Email:** Added support for choosing the SMTP authorization method. You can change it via ``Config\Email::$SMTPAuthMethod`` option.
6666
**Image:** The ``ImageMagickHandler`` has been rewritten to rely solely on the PHP ``imagick`` extension.
67+
**Image:** Added ``ImageMagickHandler::clearMetadata()`` method to remove image metadata for privacy protection.
6768

6869
Helpers and Functions
6970
=====================

user_guide_src/source/libraries/images.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,30 @@ The possible options that are recognized are as follows:
260260
- ``vOffset`` Additional offset on the y axis, in pixels
261261
- ``fontPath`` The full server path to the TTF font you wish to use. System font will be used if none is given.
262262
- ``fontSize`` The font size to use. When using the GD handler with the system font, valid values are between ``1`` to ``5``.
263+
264+
Clearing Image Metadata
265+
=======================
266+
267+
This method provides control over which metadata is preserved or removed from an image.
268+
269+
.. important:: The GD image library automatically strips all metadata during processing,
270+
so this method has no additional effect when using the GD handler.
271+
This behavior is built into GD itself and cannot be modified.
272+
273+
.. note:: Some essential technical metadata (dimensions, color depth) will be regenerated
274+
during save operations as they're required for image display. However, all privacy-sensitive
275+
information such as GPS location, camera details, and timestamps can be completely removed.
276+
277+
The method supports three different operations depending on the provided parameters:
278+
279+
**Clear all metadata** - When an empty array is passed, all metadata is stripped from the image.
280+
281+
.. literalinclude:: images/015.php
282+
283+
**Keep only specific properties** - When using the 'except' key, only the specified properties are preserved.
284+
285+
.. literalinclude:: images/016.php
286+
287+
**Delete specific properties** - When providing a list of property names, only those properties are removed.
288+
289+
.. literalinclude:: images/017.php
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
service('image', 'imagick')
4+
->withFile('/path/to/image/mypic.jpg')
5+
->clearMetadata()
6+
->save('/path/to/new/image.jpg');
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
service('image', 'imagick')
4+
->withFile('/path/to/image/mypic.jpg')
5+
->clearMetadata([
6+
'except' => ['exif:Copyright', 'exif:Author'],
7+
])
8+
->save('/path/to/new/image.jpg');
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
service('image', 'imagick')
4+
->withFile('/path/to/image/mypic.jpg')
5+
->clearMetadata([
6+
'exif:GPSLatitude',
7+
'exif:GPSLongitude',
8+
'exif:GPSAltitude',
9+
])
10+
->save('/path/to/new/image.jpg');

utils/phpstan-baseline/loader.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# total 3253 errors
1+
# total 3259 errors
22
includes:
33
- argument.type.neon
44
- assign.propertyType.neon

0 commit comments

Comments
 (0)