-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
EXIF stripping improvements #4405
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
base: master
Are you sure you want to change the base?
Changes from all commits
4988b47
cd768f3
c4fe8e4
a3fd6c6
d366ea5
a3c465c
bf4b90b
e5fcd75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| <?php | ||
|
|
||
| namespace App\Database\Migrations; | ||
|
|
||
| use CodeIgniter\Database\Migration; | ||
| use Config\Database; | ||
|
|
||
| class MigrationEXIFStrippingOptions extends Migration | ||
| { | ||
| /** | ||
| * Perform a migration step. | ||
| */ | ||
| public function up(): void | ||
| { | ||
| log_message('info', 'Migrating EXIF Stripping Options'); | ||
|
|
||
| $db = Database::connect(); | ||
|
|
||
| $configs = [ | ||
| [ | ||
| 'key' => 'exif_fields_to_keep', | ||
| 'value' => 'Copyright,Orientation,Software' | ||
| ] | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ]; | ||
jekkos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| foreach ($configs as $config) { | ||
| $existing = $db->table('app_config') | ||
| ->where('key', $config['key']) | ||
| ->get() | ||
| ->getRow(); | ||
|
|
||
| if ($existing === null) { | ||
| $db->table('app_config')->insert($config); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Revert a migration step. | ||
| */ | ||
| public function down(): void | ||
| { | ||
| $db = Database::connect(); | ||
|
|
||
| $db->table('app_config') | ||
| ->where('key', 'exif_fields_to_keep') | ||
| ->delete(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| <?php | ||
|
|
||
| namespace App\Libraries; | ||
|
|
||
| use lsolesen\pel\PelIfd; | ||
| use lsolesen\pel\PelJpeg; | ||
| use lsolesen\pel\PelTag; | ||
|
|
||
| class Image_lib | ||
| { | ||
| private array $exif_to_pel_tags = [ | ||
| 'Make' => PelTag::MAKE, | ||
| 'Model' => PelTag::MODEL, | ||
| 'Orientation' => PelTag::ORIENTATION, | ||
| 'Copyright' => PelTag::COPYRIGHT, | ||
| 'Software' => PelTag::SOFTWARE, | ||
| 'DateTime' => PelTag::DATE_TIME, | ||
| ]; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| public function stripEXIF(string $filepath, array $fields_to_keep = []): bool | ||
| { | ||
| if (!file_exists($filepath)) { | ||
| return false; | ||
| } | ||
|
|
||
| $mimetype = mime_content_type($filepath); | ||
| $allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; | ||
|
|
||
| if (!in_array($mimetype, $allowed_types)) { | ||
| return false; | ||
| } | ||
|
|
||
| if ($mimetype === 'image/jpeg' || $mimetype === 'image/jpg') { | ||
| return $this->stripExifJpeg($filepath, $fields_to_keep); | ||
| } | ||
|
|
||
| if ($mimetype === 'image/png') { | ||
| return $this->stripExifPng($filepath); | ||
| } | ||
|
|
||
| if ($mimetype === 'image/gif') { | ||
| return $this->stripExifGif($filepath); | ||
| } | ||
|
|
||
| if ($mimetype === 'image/webp') { | ||
| return $this->stripExifWebp($filepath); | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| private function stripExifJpeg(string $filepath, array $fields_to_keep = []): bool | ||
| { | ||
| try { | ||
| $data = file_get_contents($filepath); | ||
| if ($data === false) { | ||
| return false; | ||
| } | ||
|
|
||
| $jpeg = new PelJpeg($data); | ||
|
|
||
| $exif = $jpeg->getExif(); | ||
| if ($exif === null) { | ||
| return true; | ||
| } | ||
|
|
||
| $tiff = $exif->getTiff(); | ||
| if ($tiff === null) { | ||
| return true; | ||
| } | ||
|
|
||
| $ifd0 = $tiff->getIfd(); | ||
| if ($ifd0 !== null) { | ||
| $this->removeExifFields($ifd0, $fields_to_keep); | ||
|
|
||
| $subIfd = $ifd0->getSubIfd(PelTag::EXIF_IFD_POINTER); | ||
| if ($subIfd !== null) { | ||
| $this->removeExifFields($subIfd, $fields_to_keep); | ||
| } | ||
|
|
||
| if (!in_array('GPS', $fields_to_keep)) { | ||
| $ifd0->removeEntry(PelTag::GPS_INFO_IFD_POINTER); | ||
| } | ||
|
Comment on lines
+81
to
+83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: #!/bin/bash
# Check if there's a way to verify GPS removal or if there are any tests
rg -n 'GPS|gps' app/Libraries/Image_lib.php -A 2 -B 2
# Look for any test files that might verify this behavior
fd -e php -g '*test*' | xargs -I {} rg -l 'Image_lib|stripEXIF|GPS' {} 2>/dev/null || echo "No test files found"Repository: opensourcepos/opensourcepos Length of output: 286 π Web query:
π‘ Result: In fileeye/pel (PEL), GPS metadata lives in the GPS sub-IFD (type Remove GPS entries (keeps an empty GPS IFD)$gps = $ifd0->getSubIfd(PelIfd::GPS);
if ($gps) {
foreach ($gps->getEntries() as $tag => $_entry) {
unset($gps[$tag]); // PelIfd::offsetUnset()
}
}
Remove the GPS sub-IFD entirely (so it wonβt be written back)When serializing, PEL writes sub-IFDs based on the parentβs internal Practical workaround: remove the GPS element from the private $rp = new ReflectionProperty($ifd0, 'sub');
$rp->setAccessible(true);
$sub = $rp->getValue($ifd0);
unset($sub[PelIfd::GPS]);
$rp->setValue($ifd0, $sub);If you want, paste your current snippet (how youβre calling Citations:
GPS data is not being removed; only the pointer tag is deleted. In PEL, GPS metadata is stored as a sub-IFD in IFD0's internal To properly remove GPS, remove the GPS sub-IFD from IFD0's internal list: $rp = new ReflectionProperty($ifd0, 'sub');
$rp->setAccessible(true);
$sub = $rp->getValue($ifd0);
unset($sub[PelIfd::GPS]);
$rp->setValue($ifd0, $sub);π€ Prompt for AI Agents |
||
| } | ||
|
|
||
| $jpeg->saveFile($filepath); | ||
| return true; | ||
| } catch (\Exception $e) { | ||
| return $this->stripExifFallback($filepath); | ||
| } | ||
| } | ||
|
|
||
| private function removeExifFields(PelIfd $ifd, array $fields_to_keep): void | ||
| { | ||
| $tags_to_remove = array_diff(array_keys($this->exif_to_pel_tags), $fields_to_keep); | ||
|
|
||
| foreach ($tags_to_remove as $field_name) { | ||
| $pel_tag = $this->exif_to_pel_tags[$field_name]; | ||
| $entry = $ifd->getEntry($pel_tag); | ||
|
|
||
| if ($entry !== null) { | ||
| $ifd->removeEntry($pel_tag); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private function stripExifPng(string $filepath): bool | ||
| { | ||
| $image = @imagecreatefrompng($filepath); | ||
| if ($image === false) { | ||
| return false; | ||
| } | ||
|
|
||
| imagealphablending($image, false); | ||
| imagesavealpha($image, true); | ||
|
|
||
| $result = imagepng($image, $filepath, 9); | ||
| imagedestroy($image); | ||
|
|
||
| return $result; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private function stripExifGif(string $filepath): bool | ||
| { | ||
| $content = file_get_contents($filepath); | ||
| if ($content === false) { | ||
| return false; | ||
| } | ||
|
|
||
| if (strpos($content, "\x21\xF9\x04") !== false || strpos($content, 'NETSCAPE2.0') !== false) { | ||
| return true; | ||
| } | ||
|
Comment on lines
+130
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Animated GIF detection is overly broad and will skip processing most static GIFs. The Graphic Control Extension ( A more reliable approach is to count Image Descriptor blocks ( π Proposed fix for accurate animated GIF detection private function stripExifGif(string $filepath): bool
{
$content = file_get_contents($filepath);
if ($content === false) {
return false;
}
- if (strpos($content, "\x21\xF9\x04") !== false || strpos($content, 'NETSCAPE2.0') !== false) {
+ // Check for NETSCAPE loop extension (strong indicator of animation)
+ if (strpos($content, 'NETSCAPE2.0') !== false) {
+ return true;
+ }
+
+ // Count Image Descriptor blocks (0x2C) - multiple frames indicate animation
+ if (substr_count($content, "\x2C") > 1) {
return true;
}
$image = `@imagecreatefromgif`($filepath); |
||
|
|
||
| $image = @imagecreatefromgif($filepath); | ||
| if ($image === false) { | ||
| return false; | ||
| } | ||
|
|
||
| $result = imagegif($image, $filepath); | ||
| imagedestroy($image); | ||
|
|
||
| return $result; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private function stripExifWebp(string $filepath): bool | ||
| { | ||
| if (!function_exists('imagecreatefromwebp')) { | ||
| return false; | ||
| } | ||
|
|
||
| $image = @imagecreatefromwebp($filepath); | ||
| if ($image === false) { | ||
| return false; | ||
| } | ||
|
|
||
| $result = imagewebp($image, $filepath, 100); | ||
| imagedestroy($image); | ||
|
|
||
| return $result; | ||
| } | ||
|
|
||
| private function stripExifFallback(string $filepath): bool | ||
| { | ||
| $content = file_get_contents($filepath); | ||
| if ($content === false) { | ||
| return false; | ||
| } | ||
|
|
||
| $markers = []; | ||
| $offset = 0; | ||
|
|
||
| while ($offset < strlen($content)) { | ||
| if ($offset + 4 > strlen($content)) { | ||
| break; | ||
| } | ||
|
|
||
| $marker = ord($content[$offset + 1]); | ||
|
|
||
| if (ord($content[$offset]) !== 0xFF) { | ||
| break; | ||
| } | ||
|
|
||
| if ($marker >= 0xE0 && $marker <= 0xEF) { | ||
| $marker_len = ord($content[$offset + 2]) * 256 + ord($content[$offset + 3]); | ||
| $markers[] = [$offset, $marker_len + 2]; | ||
| $offset += $marker_len + 2; | ||
| } elseif ($marker === 0xD8 || $marker === 0xD9) { | ||
| $offset += 2; | ||
| } elseif ($marker === 0x00 || $marker === 0xD0 || $marker === 0xD1 || $marker === 0xD2 || $marker === 0xD3 || $marker === 0xD4 || $marker === 0xD5 || $marker === 0xD6 || $marker === 0xD7) { | ||
| $offset += 2; | ||
| } elseif ($marker === 0x01) { | ||
| $offset += 2; | ||
| } else { | ||
| if ($offset + 4 > strlen($content)) { | ||
| break; | ||
| } | ||
| $marker_len = ord($content[$offset + 2]) * 256 + ord($content[$offset + 3]); | ||
| $offset += $marker_len + 2; | ||
| } | ||
| } | ||
|
|
||
| if (empty($markers)) { | ||
| return true; | ||
| } | ||
|
|
||
| $marker_names = []; | ||
| foreach ($markers as $marker_info) { | ||
| $marker_byte = ord($content[$marker_info[0] + 1]); | ||
| $marker_names[] = 'APP' . ($marker_byte - 0xE0); | ||
| } | ||
| log_message('warning', "stripExifFallback: Removing all APP markers from {$filepath}: " . implode(', ', $marker_names)); | ||
|
|
||
| $new_content = $content; | ||
| foreach (array_reverse($markers) as $marker_info) { | ||
| $new_content = substr_replace($new_content, '', $marker_info[0], $marker_info[1]); | ||
| } | ||
|
|
||
| return file_put_contents($filepath, $new_content) !== false; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.