Embed YAML source in PNG metadata for round-trip capability (port of upstream PR #234)
PNGs now contain the original YAML harness definition as compressed iTXT metadata. Use read_yaml_from_png() to extract it — share a PNG, recipient can regenerate or edit the harness.
This commit is contained in:
parent
d8260b3fde
commit
c84e6fb3ad
@ -14,6 +14,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
from wireviz.wv_dataclasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak
|
from wireviz.wv_dataclasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak
|
||||||
from wireviz.wv_harness import Harness
|
from wireviz.wv_harness import Harness
|
||||||
|
from wireviz.wv_png_metadata import save_yaml_to_png
|
||||||
from wireviz.wv_utils import (
|
from wireviz.wv_utils import (
|
||||||
expand,
|
expand,
|
||||||
file_read_text,
|
file_read_text,
|
||||||
@ -88,7 +89,7 @@ def parse(
|
|||||||
if not output_formats and not return_types:
|
if not output_formats and not return_types:
|
||||||
raise Exception("No output formats or return types specified")
|
raise Exception("No output formats or return types specified")
|
||||||
|
|
||||||
yaml_data, yaml_file = _get_yaml_data_and_path(inp)
|
yaml_data, yaml_file, yaml_str = _get_yaml_data_and_path(inp)
|
||||||
if not isinstance(yaml_data, dict):
|
if not isinstance(yaml_data, dict):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"Expected a dict as top-level YAML input, but got: {type(yaml_data)}"
|
f"Expected a dict as top-level YAML input, but got: {type(yaml_data)}"
|
||||||
@ -394,6 +395,9 @@ def parse(
|
|||||||
|
|
||||||
if output_formats:
|
if output_formats:
|
||||||
harness.output(filename=output_file, fmt=output_formats, view=False)
|
harness.output(filename=output_file, fmt=output_formats, view=False)
|
||||||
|
# embed YAML source into PNG for round-trip capability
|
||||||
|
if "png" in output_formats and yaml_str:
|
||||||
|
save_yaml_to_png(output_file, yaml_str)
|
||||||
|
|
||||||
if return_types:
|
if return_types:
|
||||||
returns = []
|
returns = []
|
||||||
@ -413,7 +417,7 @@ def parse(
|
|||||||
return tuple(returns) if len(returns) != 1 else returns[0]
|
return tuple(returns) if len(returns) != 1 else returns[0]
|
||||||
|
|
||||||
|
|
||||||
def _get_yaml_data_and_path(inp: Union[str, Path, Dict]) -> Tuple[Dict, Path]:
|
def _get_yaml_data_and_path(inp: Union[str, Path, Dict]) -> Tuple[Dict, Path, str]:
|
||||||
# determine whether inp is a file path, a YAML string, or a Dict
|
# determine whether inp is a file path, a YAML string, or a Dict
|
||||||
if not isinstance(inp, Dict): # received a str or a Path
|
if not isinstance(inp, Dict): # received a str or a Path
|
||||||
try:
|
try:
|
||||||
@ -441,7 +445,8 @@ def _get_yaml_data_and_path(inp: Union[str, Path, Dict]) -> Tuple[Dict, Path]:
|
|||||||
# received a Dict, use as-is
|
# received a Dict, use as-is
|
||||||
yaml_data = inp
|
yaml_data = inp
|
||||||
yaml_path = None
|
yaml_path = None
|
||||||
return yaml_data, yaml_path
|
yaml_str = None
|
||||||
|
return yaml_data, yaml_path, yaml_str
|
||||||
|
|
||||||
|
|
||||||
def _get_output_dir(input_file: Path, default_output_dir: Path) -> Path:
|
def _get_output_dir(input_file: Path, default_output_dir: Path) -> Path:
|
||||||
|
|||||||
63
src/wireviz/wv_png_metadata.py
Normal file
63
src/wireviz/wv_png_metadata.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""Embed and extract YAML source data in PNG metadata (iTXT chunks)."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
from PIL.PngImagePlugin import PngInfo
|
||||||
|
|
||||||
|
|
||||||
|
PNG_KEY_YAML = "wireviz_yaml"
|
||||||
|
PNG_KEY_PREPEND = "wireviz_prepend_yaml"
|
||||||
|
|
||||||
|
|
||||||
|
def save_yaml_to_png(
|
||||||
|
png_path: Path,
|
||||||
|
yaml_input: str,
|
||||||
|
prepend_input: str = "",
|
||||||
|
) -> None:
|
||||||
|
"""Save YAML source as compressed iTXT metadata in a PNG file."""
|
||||||
|
png_path = Path(png_path)
|
||||||
|
if not png_path.suffix == ".png":
|
||||||
|
png_path = png_path.with_suffix(".png")
|
||||||
|
if not png_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
with Image.open(fp=png_path) as im:
|
||||||
|
txt = PngInfo()
|
||||||
|
txt.add_itxt(PNG_KEY_YAML, yaml_input, zip=True)
|
||||||
|
if prepend_input:
|
||||||
|
txt.add_itxt(PNG_KEY_PREPEND, prepend_input, zip=True)
|
||||||
|
im.save(fp=png_path, pnginfo=txt)
|
||||||
|
|
||||||
|
|
||||||
|
def read_yaml_from_png(png_path: Path) -> Tuple[str, Optional[str]]:
|
||||||
|
"""Extract YAML source from a PNG file's iTXT metadata.
|
||||||
|
|
||||||
|
Returns (yaml_input, prepend_input) where prepend_input may be None.
|
||||||
|
"""
|
||||||
|
png_path = Path(png_path)
|
||||||
|
if not png_path.suffix == ".png":
|
||||||
|
png_path = png_path.with_suffix(".png")
|
||||||
|
|
||||||
|
with Image.open(fp=png_path) as im:
|
||||||
|
im.load()
|
||||||
|
yaml_input = im.text.get(PNG_KEY_YAML, "")
|
||||||
|
prepend_input = im.text.get(PNG_KEY_PREPEND)
|
||||||
|
|
||||||
|
return yaml_input, prepend_input
|
||||||
|
|
||||||
|
|
||||||
|
def has_yaml_metadata(png_path: Path) -> bool:
|
||||||
|
"""Check if a PNG file contains embedded WireViz YAML data."""
|
||||||
|
png_path = Path(png_path)
|
||||||
|
if not png_path.suffix == ".png":
|
||||||
|
png_path = png_path.with_suffix(".png")
|
||||||
|
if not png_path.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
with Image.open(fp=png_path) as im:
|
||||||
|
im.load()
|
||||||
|
return PNG_KEY_YAML in im.text
|
||||||
Loading…
x
Reference in New Issue
Block a user