mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-10 08:00:32 +02:00
155 lines
5.1 KiB
Python
155 lines
5.1 KiB
Python
"""Bounded Matroska/WebM metadata reader for native TC260 AIGC labels.
|
|
|
|
TC260-PG-20257A stores the label as a Matroska ``SimpleTag`` whose
|
|
``TagName`` is ``AIGC`` and whose ``TagString`` is the normative JSON object.
|
|
The walker seeks over unrelated elements such as clusters instead of reading
|
|
their payloads.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, BinaryIO
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
|
|
from remove_ai_watermarks.metadata import MAX_TC260_VALUE_BYTES, parse_tc260_aigc_json
|
|
|
|
_EBML_MAGIC = b"\x1aE\xdf\xa3"
|
|
_SEGMENT_ID = 0x18538067
|
|
_TAGS_ID = 0x1254C367
|
|
_TAG_ID = 0x7373
|
|
_SIMPLE_TAG_ID = 0x67C8
|
|
_TAG_NAME_ID = 0x45A3
|
|
_TAG_STRING_ID = 0x4487
|
|
_MAX_TAG_NAME_BYTES = 256
|
|
|
|
|
|
def _vint_length(first: int, *, maximum: int) -> int | None:
|
|
"""Return an EBML variable-integer length from its first byte."""
|
|
mask = 0x80
|
|
for length in range(1, maximum + 1):
|
|
if first & mask:
|
|
return length
|
|
mask >>= 1
|
|
return None
|
|
|
|
|
|
def _read_element_header(
|
|
stream: BinaryIO,
|
|
pos: int,
|
|
limit: int,
|
|
) -> tuple[int, int, int] | None:
|
|
"""Return ``(element_id, payload_start, element_end)`` inside ``limit``."""
|
|
if pos < 0 or pos >= limit:
|
|
return None
|
|
stream.seek(pos)
|
|
first_raw = stream.read(1)
|
|
if not first_raw:
|
|
return None
|
|
id_length = _vint_length(first_raw[0], maximum=4)
|
|
if id_length is None or pos + id_length >= limit:
|
|
return None
|
|
element_id_raw = first_raw + stream.read(id_length - 1)
|
|
if len(element_id_raw) != id_length:
|
|
return None
|
|
element_id = int.from_bytes(element_id_raw, "big")
|
|
|
|
size_first_raw = stream.read(1)
|
|
if not size_first_raw:
|
|
return None
|
|
size_length = _vint_length(size_first_raw[0], maximum=8)
|
|
if size_length is None:
|
|
return None
|
|
size_rest = stream.read(size_length - 1)
|
|
if len(size_rest) != size_length - 1:
|
|
return None
|
|
marker = 1 << (8 - size_length)
|
|
size_value = int.from_bytes(bytes([size_first_raw[0] & (marker - 1)]) + size_rest, "big")
|
|
payload_start = pos + id_length + size_length
|
|
if payload_start > limit:
|
|
return None
|
|
unknown_size = size_value == (1 << (7 * size_length)) - 1
|
|
element_end = limit if unknown_size else payload_start + size_value
|
|
if element_end > limit:
|
|
return None
|
|
return element_id, payload_start, element_end
|
|
|
|
|
|
def _iter_elements(
|
|
stream: BinaryIO,
|
|
start: int,
|
|
end: int,
|
|
) -> Iterator[tuple[int, int, int]]:
|
|
"""Yield valid direct children from one bounded EBML region."""
|
|
pos = start
|
|
while pos < end:
|
|
header = _read_element_header(stream, pos, end)
|
|
if header is None:
|
|
return
|
|
element_id, payload_start, element_end = header
|
|
yield element_id, payload_start, element_end
|
|
if element_end <= pos:
|
|
return
|
|
pos = element_end
|
|
|
|
|
|
def _read_bounded(
|
|
stream: BinaryIO,
|
|
start: int,
|
|
end: int,
|
|
maximum: int,
|
|
) -> bytes | None:
|
|
size = end - start
|
|
if size < 0 or size > maximum:
|
|
return None
|
|
stream.seek(start)
|
|
value = stream.read(size)
|
|
return value if len(value) == size else None
|
|
|
|
|
|
def _simple_tag_payloads(
|
|
stream: BinaryIO,
|
|
start: int,
|
|
end: int,
|
|
) -> tuple[bytes, ...]:
|
|
name: bytes | None = None
|
|
values: list[bytes] = []
|
|
for element_id, payload_start, element_end in _iter_elements(stream, start, end):
|
|
if element_id == _TAG_NAME_ID:
|
|
name = _read_bounded(stream, payload_start, element_end, _MAX_TAG_NAME_BYTES)
|
|
elif element_id == _TAG_STRING_ID:
|
|
value = _read_bounded(stream, payload_start, element_end, MAX_TC260_VALUE_BYTES)
|
|
if value is not None:
|
|
values.append(value)
|
|
if name != b"AIGC":
|
|
return ()
|
|
return tuple(value for value in values if parse_tc260_aigc_json(value) is not None)
|
|
|
|
|
|
def tc260_aigc_payloads(path: str | Path) -> tuple[bytes, ...]:
|
|
"""Read validated TC260 values from Matroska/WebM ``SimpleTag`` entries."""
|
|
found: list[bytes] = []
|
|
try:
|
|
with open(path, "rb") as stream:
|
|
if stream.read(4) != _EBML_MAGIC:
|
|
return ()
|
|
stream.seek(0, 2)
|
|
file_size = stream.tell()
|
|
for element_id, payload_start, element_end in _iter_elements(stream, 0, file_size):
|
|
if element_id != _SEGMENT_ID:
|
|
continue
|
|
for child_id, child_start, child_end in _iter_elements(stream, payload_start, element_end):
|
|
if child_id != _TAGS_ID:
|
|
continue
|
|
for tag_id, tag_start, tag_end in _iter_elements(stream, child_start, child_end):
|
|
if tag_id != _TAG_ID:
|
|
continue
|
|
for simple_id, simple_start, simple_end in _iter_elements(stream, tag_start, tag_end):
|
|
if simple_id == _SIMPLE_TAG_ID:
|
|
found.extend(_simple_tag_payloads(stream, simple_start, simple_end))
|
|
except OSError:
|
|
return ()
|
|
return tuple(found)
|