From 51b430dc5444a026b5d5c57200e3fbb62bcd21e5 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 20 Apr 2026 17:38:51 -0400 Subject: [PATCH 1/6] add marker params for gif format --- dmeta/params.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dmeta/params.py b/dmeta/params.py index 4eccf93..3563020 100644 --- a/dmeta/params.py +++ b/dmeta/params.py @@ -44,6 +44,16 @@ JPEG_COM = 0xFE # Comment JPEG_APP_FIRST, JPEG_APP_LAST = 0xE0, 0xEF # APP0..APP15 JPEG_STANDALONE_MARKERS = frozenset({0x00, 0x01, JPEG_SOI, JPEG_EOI} | set(range(0xD0, 0xD8))) + +# GIF block markers per GIF89a specification. +GIF_TRAILER = 0x3B +GIF_EXTENSION_INTRODUCER = 0x21 +GIF_IMAGE_DESCRIPTOR = 0x2C +GIF_EXT_GRAPHIC_CONTROL = 0xF9 # per-frame timing (kept) +GIF_EXT_COMMENT = 0xFE +GIF_EXT_PLAIN_TEXT = 0x01 +GIF_EXT_APPLICATION = 0xFF +GIF_APP_EXT_NETSCAPE_IDENTIFIER = b"NETSCAPE2.0" # animation loop (kept) INVALID_CONFIG_FILE_NAME_ERROR = "Config file name is not a string." CONFIG_FILE_DOES_NOT_EXIST_ERROR = "Given config file doesn't exist." UPDATE_COMMAND_WITH_NO_CONFIG_FILE_ERROR = "No config file provided. Set the .json config file with --config command." From 584aa91d65067817b3ab3da11126876686049c41 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 20 Apr 2026 17:40:44 -0400 Subject: [PATCH 2/6] Add function to clear metadata from GIFF files --- dmeta/functions.py | 99 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/dmeta/functions.py b/dmeta/functions.py index 4cff4db..5318c8d 100644 --- a/dmeta/functions.py +++ b/dmeta/functions.py @@ -11,7 +11,10 @@ from .params import CORE_XML_MAP, APP_XML_MAP, OVERVIEW, DMETA_VERSION, \ UPDATE_COMMAND_WITH_NO_CONFIG_FILE_ERROR, SUPPORTED_MICROSOFT_FORMATS, \ JPEG_MARKER_PREFIX, JPEG_SOI, JPEG_EOI, JPEG_SOS, JPEG_COM, \ - JPEG_APP_FIRST, JPEG_APP_LAST, JPEG_STANDALONE_MARKERS + JPEG_APP_FIRST, JPEG_APP_LAST, JPEG_STANDALONE_MARKERS, \ + GIF_TRAILER, GIF_EXTENSION_INTRODUCER, GIF_IMAGE_DESCRIPTOR, \ + GIF_EXT_GRAPHIC_CONTROL, GIF_EXT_COMMENT, GIF_EXT_PLAIN_TEXT, \ + GIF_EXT_APPLICATION, GIF_APP_EXT_NETSCAPE_IDENTIFIER def overwrite_metadata( @@ -334,6 +337,100 @@ def clear_jpeg_metadata(jpeg_file_name, in_place=False, verbose=False): return output_path +def clear_gif_metadata(gif_file_name, in_place=False, verbose=False): + """ + Remove all metadata from a GIF file without re-encoding pixel data. + + Preserves per-frame Graphic Control and NETSCAPE2.0 loop blocks; removes + Comment, Plain Text, and all other Application Extensions. + + :param gif_file_name: path to original GIF file + :type gif_file_name: str + :param in_place: if True, overwrite the original file with cleaned version + :type in_place: bool + :param verbose: if True, print detailed output + :type verbose: bool + :return: path to cleaned GIF file + """ + if not os.path.exists(gif_file_name) or not gif_file_name.lower().endswith(".gif"): + return + + with open(gif_file_name, "rb") as f: + data = f.read() + if not (data.startswith(b"GIF87a") or data.startswith(b"GIF89a")): + return + + n = len(data) + + def skip_sub_blocks(start): + j = start + while j < n: + size = data[j] + j += 1 + if size == 0: + return j + j += size + return j + + # Header + Logical Screen Descriptor + optional Global Color Table. + out = bytearray(data[:13]) + packed = data[10] + i = 13 + if packed & 0x80: + gct = 3 * (1 << ((packed & 0x07) + 1)) + out += data[i:i + gct] + i += gct + + # Walk data stream per GIF89a; drop metadata-bearing extensions only. + while i < n: + b = data[i] + if b == GIF_TRAILER: + out += bytes([GIF_TRAILER]) + break + if b == GIF_EXTENSION_INTRODUCER and i + 1 < n: + label = data[i + 1] + if label == GIF_EXT_GRAPHIC_CONTROL: + out += data[i:i + 8] + i += 8 + elif label == GIF_EXT_APPLICATION and i + 2 < n: + ident_len = data[i + 2] + ident = data[i + 3:i + 3 + ident_len] + j = skip_sub_blocks(i + 3 + ident_len) + if ident == GIF_APP_EXT_NETSCAPE_IDENTIFIER: + out += data[i:j] + i = j + elif label in (GIF_EXT_COMMENT, GIF_EXT_PLAIN_TEXT): + i = skip_sub_blocks(i + 2) + else: + i = skip_sub_blocks(i + 2) + elif b == GIF_IMAGE_DESCRIPTOR and i + 10 <= n: + packed2 = data[i + 9] + j = i + 10 + if packed2 & 0x80: + j += 3 * (1 << ((packed2 & 0x07) + 1)) + j += 1 # LZW minimum code size + j = skip_sub_blocks(j) + out += data[i:j] + i = j + else: + break + + if in_place: + output_path = gif_file_name + else: + base, ext = os.path.splitext(gif_file_name) + output_path = base + "_cleaned" + ext + + with open(output_path, "wb") as f: + f.write(bytes(out)) + + if verbose: + action = "overwritten" if in_place else f"saved to {output_path}" + print(f"Metadata cleared for: {gif_file_name} ({action})") + + return output_path + + def extract_metadata(microsoft_file_name): """ Extract all the editable metadata from the given Microsoft file. From d6c3bee05b52974cbf38689ec99b7667256e6eb0 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 20 Apr 2026 17:41:56 -0400 Subject: [PATCH 3/6] Update `CHANGELOG.md` --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e472f..a1f0a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- GIF params in `params.py` +- `clear_gif_metadata` function in `functions.py` - JPEG params in `params.py` - `clear_jpeg_metadata` function in `functions.py` - `clear_png_metadata` function in `functions.py` From 07f9d47c7f256d70b71ee80629b7bd135f6ff0d8 Mon Sep 17 00:00:00 2001 From: AHReccese Date: Mon, 20 Apr 2026 17:46:39 -0400 Subject: [PATCH 4/6] add sample giff file with full metadata --- tests/test.gif | Bin 0 -> 12776 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/test.gif diff --git a/tests/test.gif b/tests/test.gif new file mode 100644 index 0000000000000000000000000000000000000000..4187adb1f4f46062d5693385497c99468e4a5a46 GIT binary patch literal 12776 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 JJB Date: Mon, 20 Apr 2026 18:09:22 -0400 Subject: [PATCH 5/6] add tests for gif removal functionality --- tests/test_dmeta.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_dmeta.py b/tests/test_dmeta.py index 776adda..a88b65a 100644 --- a/tests/test_dmeta.py +++ b/tests/test_dmeta.py @@ -3,6 +3,7 @@ from dmeta.functions import update, update_all, clear, clear_all from dmeta.functions import clear_jpeg_metadata from dmeta.functions import clear_png_metadata +from dmeta.functions import clear_gif_metadata from dmeta.functions import extract_metadata @@ -93,3 +94,19 @@ def test12(): clear_jpeg_metadata(jpeg_file, in_place=True, verbose=False) with Image.open(jpeg_file) as img: assert img.info == {} + + +def test13(): + # clear the metadata of the .gif file [not inplace] + gif_file = os.path.join(TESTS_DIR_PATH, "test.gif") + output_path = clear_gif_metadata(gif_file, in_place=False, verbose=False) + with Image.open(output_path) as img: + assert "comment" not in img.info + + +def test14(): + # clear the metadata of the .gif file [inplace] + gif_file = os.path.join(TESTS_DIR_PATH, "test.gif") + clear_gif_metadata(gif_file, in_place=True, verbose=False) + with Image.open(gif_file) as img: + assert "comment" not in img.info From 916fb304b84859aa72d1abd8f04f8ad9c38c94da Mon Sep 17 00:00:00 2001 From: AHReccese Date: Wed, 22 Apr 2026 17:40:50 -0400 Subject: [PATCH 6/6] simplify elif, else + extra marker --- dmeta/functions.py | 7 +++---- dmeta/params.py | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/dmeta/functions.py b/dmeta/functions.py index 5318c8d..bd2927a 100644 --- a/dmeta/functions.py +++ b/dmeta/functions.py @@ -13,8 +13,8 @@ JPEG_MARKER_PREFIX, JPEG_SOI, JPEG_EOI, JPEG_SOS, JPEG_COM, \ JPEG_APP_FIRST, JPEG_APP_LAST, JPEG_STANDALONE_MARKERS, \ GIF_TRAILER, GIF_EXTENSION_INTRODUCER, GIF_IMAGE_DESCRIPTOR, \ - GIF_EXT_GRAPHIC_CONTROL, GIF_EXT_COMMENT, GIF_EXT_PLAIN_TEXT, \ - GIF_EXT_APPLICATION, GIF_APP_EXT_NETSCAPE_IDENTIFIER + GIF_EXT_GRAPHIC_CONTROL, GIF_EXT_APPLICATION, \ + GIF_APP_EXT_NETSCAPE_IDENTIFIER def overwrite_metadata( @@ -399,9 +399,8 @@ def skip_sub_blocks(start): if ident == GIF_APP_EXT_NETSCAPE_IDENTIFIER: out += data[i:j] i = j - elif label in (GIF_EXT_COMMENT, GIF_EXT_PLAIN_TEXT): - i = skip_sub_blocks(i + 2) else: + # Comment, Plain Text, and any other extension carry metadata: drop. i = skip_sub_blocks(i + 2) elif b == GIF_IMAGE_DESCRIPTOR and i + 10 <= n: packed2 = data[i + 9] diff --git a/dmeta/params.py b/dmeta/params.py index 3563020..c3a1ba0 100644 --- a/dmeta/params.py +++ b/dmeta/params.py @@ -50,8 +50,6 @@ GIF_EXTENSION_INTRODUCER = 0x21 GIF_IMAGE_DESCRIPTOR = 0x2C GIF_EXT_GRAPHIC_CONTROL = 0xF9 # per-frame timing (kept) -GIF_EXT_COMMENT = 0xFE -GIF_EXT_PLAIN_TEXT = 0x01 GIF_EXT_APPLICATION = 0xFF GIF_APP_EXT_NETSCAPE_IDENTIFIER = b"NETSCAPE2.0" # animation loop (kept) INVALID_CONFIG_FILE_NAME_ERROR = "Config file name is not a string."