Skip to content

Commit 7951d9d

Browse files
committed
Implement selective EXIF removal using FileEye/pel library
- Add fileeye/pel dependency to composer.json for selective EXIF field removal - Rewrite Image_lib::stripExifJpeg() to use FileEye/pel for precise field manipulation - Add exif_to_pel_tags mapping for supported EXIF fields - Implement removeExifFields() to selectively remove EXIF data based on config - Keep fallback method if library is unavailable or parsing fails - Add language strings for new configuration options - Update migration to include Software in default fields to keep This addresses reviewer concern about preserving copyright and other beneficial metadata while removing privacy-sensitive fields like GPS location.
1 parent 4bac566 commit 7951d9d

File tree

4 files changed

+74
-21
lines changed

4 files changed

+74
-21
lines changed

app/Database/Migrations/20260306120000_MigrationEXIFStrippingOptions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function up(): void
2424
],
2525
[
2626
'key' => 'exif_fields_to_keep',
27-
'value' => 'Copyright,Orientation'
27+
'value' => 'Copyright,Orientation,Software'
2828
]
2929
];
3030

app/Language/en/Config.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,4 +328,8 @@
328328
"wholesale_markup" => "",
329329
"work_order_enable" => "Work Order Support",
330330
"work_order_format" => "Work Order Format",
331+
"exif_stripping_enabled" => "Enable EXIF Stripping",
332+
"exif_stripping_enabled_tooltip" => "Remove EXIF metadata from uploaded images to protect privacy. Uses FileEye/pel library for selective field removal.",
333+
"exif_fields_to_keep" => "EXIF Fields to Keep",
334+
"exif_fields_to_keep_tooltip" => "Comma-separated list of EXIF fields to preserve (e.g., Copyright, Orientation, Software). Keeps beneficial metadata while removing privacy-sensitive data like GPS location.",
331335
];

app/Libraries/Image_lib.php

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,23 @@
22

33
namespace App\Libraries;
44

5+
use lsolesen\pel\PelIfd;
6+
use lsolesen\pel\PelJpeg;
7+
use lsolesen\pel\PelTag;
8+
59
class Image_lib
610
{
7-
public function stripEXIF(string $filepath, array $fields_to_remove = []): bool
11+
private array $exif_to_pel_tags = [
12+
'Make' => PelTag::MAKE,
13+
'Model' => PelTag::MODEL,
14+
'Orientation' => PelTag::ORIENTATION,
15+
'Copyright' => PelTag::COPYRIGHT,
16+
'Software' => PelTag::SOFTWARE,
17+
'DateTime' => PelTag::DATE_TIME,
18+
'GPS' => PelTag::GPS_OFFSET,
19+
];
20+
21+
public function stripEXIF(string $filepath, array $fields_to_keep = []): bool
822
{
923
if (!file_exists($filepath)) {
1024
return false;
@@ -18,7 +32,7 @@ public function stripEXIF(string $filepath, array $fields_to_remove = []): bool
1832
}
1933

2034
if ($mimetype === 'image/jpeg' || $mimetype === 'image/jpg') {
21-
return $this->stripExifJpeg($filepath, $fields_to_remove);
35+
return $this->stripExifJpeg($filepath, $fields_to_keep);
2236
}
2337

2438
if ($mimetype === 'image/png') {
@@ -36,26 +50,60 @@ public function stripEXIF(string $filepath, array $fields_to_remove = []): bool
3650
return true;
3751
}
3852

39-
private function stripExifJpeg(string $filepath, array $fields_to_remove = []): bool
53+
private function stripExifJpeg(string $filepath, array $fields_to_keep = []): bool
4054
{
41-
if (!function_exists('exif_read_data')) {
42-
return $this->stripExifFallback($filepath);
43-
}
55+
try {
56+
$data = file_get_contents($filepath);
57+
if ($data === false) {
58+
return false;
59+
}
4460

45-
$image_info = @getimagesize($filepath);
46-
if ($image_info === false) {
47-
return false;
48-
}
61+
$jpeg = new PelJpeg($data);
4962

50-
$image = @imagecreatefromjpeg($filepath);
51-
if ($image === false) {
52-
return false;
63+
$exif = $jpeg->getExif();
64+
if ($exif === null) {
65+
return true;
66+
}
67+
68+
$tiff = $exif->getTiff();
69+
if ($tiff === null) {
70+
return true;
71+
}
72+
73+
$ifd0 = $tiff->getIfd();
74+
if ($ifd0 !== null) {
75+
$this->removeExifFields($ifd0, $fields_to_keep);
76+
77+
$subIfd = $ifd0->getSubIfd(PelTag::EXIF_IFD_POINTER);
78+
if ($subIfd !== null) {
79+
$this->removeExifFields($subIfd, $fields_to_keep);
80+
}
81+
}
82+
83+
$gpsIfd = $tiff->getIfd(PelTag::GPS_IFD_POINTER);
84+
if ($gpsIfd !== null && !in_array('GPS', $fields_to_keep)) {
85+
$tiff->setIfd(null, PelTag::GPS_IFD_POINTER);
86+
}
87+
88+
$jpeg->saveFile($filepath);
89+
return true;
90+
} catch (\Exception $e) {
91+
return $this->stripExifFallback($filepath);
5392
}
93+
}
5494

55-
$result = imagejpeg($image, $filepath, 100);
56-
imagedestroy($image);
95+
private function removeExifFields(PelIfd $ifd, array $fields_to_keep): void
96+
{
97+
$tags_to_remove = array_diff(array_keys($this->exif_to_pel_tags), $fields_to_keep);
5798

58-
return $result;
99+
foreach ($tags_to_remove as $field_name) {
100+
$pel_tag = $this->exif_to_pel_tags[$field_name];
101+
$entry = $ifd->getEntry($pel_tag);
102+
103+
if ($entry !== null) {
104+
$ifd->removeEntry($pel_tag);
105+
}
106+
}
59107
}
60108

61109
private function stripExifPng(string $filepath): bool
@@ -113,18 +161,18 @@ private function stripExifFallback(string $filepath): bool
113161

114162
$markers = [];
115163
$offset = 0;
116-
164+
117165
while ($offset < strlen($content)) {
118166
if ($offset + 4 > strlen($content)) {
119167
break;
120168
}
121-
169+
122170
$marker = ord($content[$offset + 1]);
123-
171+
124172
if (ord($content[$offset]) !== 0xFF) {
125173
break;
126174
}
127-
175+
128176
if ($marker >= 0xE0 && $marker <= 0xEF) {
129177
$marker_len = ord($content[$offset + 2]) * 256 + ord($content[$offset + 3]);
130178
$markers[] = [$offset, $marker_len + 2];

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"codeigniter4/framework": "^4.6.3",
3737
"dompdf/dompdf": "^2.0.3",
3838
"ezyang/htmlpurifier": "^4.17",
39+
"fileeye/pel": "^0.9",
3940
"laminas/laminas-escaper": "2.17.0",
4041
"paragonie/random_compat": "^2.0.21",
4142
"picqer/php-barcode-generator": "^2.4.0",

0 commit comments

Comments
 (0)