feat(images): enhance PNG processing by stripping ICC profiles
- Implemented functionality to remove embedded ICC profiles from PNG images during preview generation. - Added tests to ensure that the source image remains unchanged while the preview and thumbnail versions do not contain ICC data. - Updated the image manager to utilize the new stripping function in the preview generation process.
This commit is contained in:
parent
57379076bd
commit
74c23ebe87
2 changed files with 63 additions and 6 deletions
|
|
@ -14,9 +14,11 @@ from util.image_manager import (
|
|||
make_profile_assets_editable,
|
||||
prepare_plain_assets_for_patch,
|
||||
prepare_profile_assets_for_patch,
|
||||
preview_profile_png_bytes,
|
||||
registered_image_profiles,
|
||||
scan_generic_image_assets,
|
||||
scan_profile_assets,
|
||||
thumbnail_profile_png_bytes,
|
||||
)
|
||||
from util.rpgmaker_images import decrypt_image_bytes, encrypt_image_bytes
|
||||
|
||||
|
|
@ -161,6 +163,31 @@ class GenericImageManagerTests(unittest.TestCase):
|
|||
self.assertFalse((self.root / ".dazedtl").exists())
|
||||
self.assertFalse((self.root / ".gitignore").exists())
|
||||
|
||||
def test_preview_strips_icc_profile_without_changing_source(self):
|
||||
output = BytesIO()
|
||||
Image.new("RGBA", (10, 8), "red").save(
|
||||
output, format="PNG", icc_profile=b"outdated sRGB profile"
|
||||
)
|
||||
source = output.getvalue()
|
||||
self.source.write_bytes(source)
|
||||
asset = scan_generic_image_assets(self.root, self.root / "assets")[0]
|
||||
|
||||
preview = preview_profile_png_bytes(PROFILE_GENERIC, asset, None)
|
||||
thumbnail = thumbnail_profile_png_bytes(PROFILE_GENERIC, asset, None)
|
||||
|
||||
self.assertIn(b"iCCP", source)
|
||||
self.assertNotIn(b"iCCP", preview)
|
||||
self.assertNotIn(b"iCCP", thumbnail)
|
||||
self.assertEqual(self.source.read_bytes(), source)
|
||||
with Image.open(BytesIO(preview)) as image:
|
||||
self.assertEqual(image.getpixel((0, 0)), (255, 0, 0, 255))
|
||||
|
||||
def test_preview_leaves_png_without_icc_profile_byte_identical(self):
|
||||
source = self.source.read_bytes()
|
||||
asset = scan_generic_image_assets(self.root, self.root / "assets")[0]
|
||||
|
||||
self.assertEqual(preview_profile_png_bytes(PROFILE_GENERIC, asset, None), source)
|
||||
|
||||
|
||||
class ImageEngineDetectionTests(unittest.TestCase):
|
||||
def test_registry_exposes_rpgmaker_and_generic_capabilities(self):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ PROFILE_RPGMAKER_MVMZ = "rpgmaker_mvmz"
|
|||
PROFILE_GENERIC = "generic"
|
||||
SUPPORTED_PLAIN_EXTENSIONS = (".png",)
|
||||
_EXCLUDED_SCAN_DIRS = {".dazedtl", ".git", "__pycache__"}
|
||||
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -659,18 +660,47 @@ def prepare_profile_assets_for_patch(
|
|||
raise ValueError(f"Unsupported image engine profile: {engine_id}")
|
||||
|
||||
|
||||
def _strip_png_iccp_for_display(data: bytes) -> bytes:
|
||||
"""Remove embedded ICC metadata from display bytes without touching the source PNG."""
|
||||
|
||||
if not data.startswith(_PNG_SIGNATURE):
|
||||
return data
|
||||
|
||||
offset = len(_PNG_SIGNATURE)
|
||||
output = bytearray(_PNG_SIGNATURE)
|
||||
removed = False
|
||||
while offset + 12 <= len(data):
|
||||
length = int.from_bytes(data[offset : offset + 4], "big")
|
||||
chunk_end = offset + 12 + length
|
||||
if chunk_end > len(data):
|
||||
return data
|
||||
chunk_type = data[offset + 4 : offset + 8]
|
||||
if chunk_type == b"iCCP":
|
||||
removed = True
|
||||
else:
|
||||
output.extend(data[offset:chunk_end])
|
||||
offset = chunk_end
|
||||
if chunk_type == b"IEND":
|
||||
output.extend(data[offset:])
|
||||
return bytes(output) if removed else data
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def preview_profile_png_bytes(
|
||||
engine_id: str, asset: ImageAsset, key: bytes | None
|
||||
) -> bytes:
|
||||
if engine_id == PROFILE_RPGMAKER_MVMZ:
|
||||
from util.rpgmaker_images import preview_png_bytes
|
||||
|
||||
return preview_png_bytes(asset, key)
|
||||
if asset.has_plain:
|
||||
return asset.plain_path.read_bytes()
|
||||
if asset.has_runtime_plain:
|
||||
return asset.runtime_plain_path.read_bytes()
|
||||
raise FileNotFoundError(asset.runtime_plain_path or asset.plain_path)
|
||||
raw = preview_png_bytes(asset, key)
|
||||
elif asset.has_plain:
|
||||
raw = asset.plain_path.read_bytes()
|
||||
elif asset.has_runtime_plain:
|
||||
raw = asset.runtime_plain_path.read_bytes()
|
||||
else:
|
||||
raise FileNotFoundError(asset.runtime_plain_path or asset.plain_path)
|
||||
return _strip_png_iccp_for_display(raw)
|
||||
|
||||
|
||||
def thumbnail_profile_png_bytes(
|
||||
|
|
|
|||
Loading…
Reference in a new issue