Compare commits
3 Commits
26b80f7a0b
...
5f54ab1f0d
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f54ab1f0d | |||
| c84e6fb3ad | |||
| d8260b3fde |
@ -14,6 +14,7 @@ if __name__ == "__main__":
|
||||
|
||||
from wireviz.wv_dataclasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak
|
||||
from wireviz.wv_harness import Harness
|
||||
from wireviz.wv_png_metadata import save_yaml_to_png
|
||||
from wireviz.wv_utils import (
|
||||
expand,
|
||||
file_read_text,
|
||||
@ -88,7 +89,7 @@ def parse(
|
||||
if not output_formats and not return_types:
|
||||
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):
|
||||
raise TypeError(
|
||||
f"Expected a dict as top-level YAML input, but got: {type(yaml_data)}"
|
||||
@ -394,6 +395,9 @@ def parse(
|
||||
|
||||
if output_formats:
|
||||
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:
|
||||
returns = []
|
||||
@ -413,7 +417,7 @@ def parse(
|
||||
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
|
||||
if not isinstance(inp, Dict): # received a str or a Path
|
||||
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
|
||||
yaml_data = inp
|
||||
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:
|
||||
|
||||
@ -19,7 +19,7 @@ format_codes = {
|
||||
"g": "gv",
|
||||
"h": "html",
|
||||
"p": "png",
|
||||
# "P": "pdf", # TODO: support PDF
|
||||
"P": "pdf",
|
||||
"s": "svg",
|
||||
"t": "tsv",
|
||||
}
|
||||
|
||||
@ -293,6 +293,42 @@ class AdditionalComponent(GraphicalComponent):
|
||||
self.explicit_qty = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class StripSpec:
|
||||
"""Strip length specification for sleeve or insulation."""
|
||||
length: float = 0
|
||||
length_unit: str = "mm"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.length} {self.length_unit}" if self.length > 0 else ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Strip:
|
||||
"""Wire stripping specifications for a connector."""
|
||||
sleeve: Optional[StripSpec] = None
|
||||
insulation: Optional[StripSpec] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if isinstance(self.sleeve, str):
|
||||
self.sleeve = Strip._parse_length(self.sleeve)
|
||||
if isinstance(self.insulation, str):
|
||||
self.insulation = Strip._parse_length(self.insulation)
|
||||
|
||||
@staticmethod
|
||||
def _parse_length(spec: str) -> StripSpec:
|
||||
parts = spec.strip().split(" ", 1)
|
||||
try:
|
||||
length = float(parts[0])
|
||||
except (ValueError, IndexError):
|
||||
raise Exception(
|
||||
f"Strip length '{spec}' must be a number, "
|
||||
"or number and unit separated by a space"
|
||||
)
|
||||
unit = parts[1].strip() if len(parts) > 1 else "mm"
|
||||
return StripSpec(length=length, length_unit=unit)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TopLevelGraphicalComponent(GraphicalComponent): # abstract class
|
||||
# component properties
|
||||
@ -317,6 +353,7 @@ class Connector(TopLevelGraphicalComponent):
|
||||
shorts_hide_lable: bool = False
|
||||
# pin information in particular
|
||||
pincount: Optional[int] = None
|
||||
strip: Optional[Strip] = None
|
||||
pins: List[Pin] = field(default_factory=list) # legacy
|
||||
pinlabels: List[Pin] = field(default_factory=list) # legacy
|
||||
pincolors: List[str] = field(default_factory=list) # legacy
|
||||
@ -367,6 +404,11 @@ class Connector(TopLevelGraphicalComponent):
|
||||
if isinstance(self.image, dict):
|
||||
self.image = Image(**self.image)
|
||||
|
||||
if isinstance(self.strip, dict):
|
||||
self.strip = Strip(**self.strip)
|
||||
elif self.strip is None:
|
||||
self.strip = Strip()
|
||||
|
||||
self.ports_left = False
|
||||
self.ports_right = False
|
||||
self.visible_pins = {}
|
||||
|
||||
@ -63,6 +63,16 @@ def gv_node_component(component: Component) -> Table:
|
||||
str(component.color) if component.color else None,
|
||||
]
|
||||
|
||||
# strip info for connectors
|
||||
line_strip = None
|
||||
if isinstance(component, Connector) and (component.strip.sleeve or component.strip.insulation):
|
||||
strip_parts = []
|
||||
if component.strip.sleeve:
|
||||
strip_parts.append(f"Strip sleeve: {component.strip.sleeve}")
|
||||
if component.strip.insulation:
|
||||
strip_parts.append(f"Strip insulation: {component.strip.insulation}")
|
||||
line_strip = [Td(" | ".join(strip_parts))]
|
||||
|
||||
if component.additional_parameters:
|
||||
line_additional_parameters = nested_table_dict(component.additional_parameters)
|
||||
else:
|
||||
@ -87,6 +97,7 @@ def gv_node_component(component: Component) -> Table:
|
||||
line_name,
|
||||
line_pn,
|
||||
line_info,
|
||||
line_strip,
|
||||
line_additional_parameters,
|
||||
line_ports,
|
||||
line_image,
|
||||
|
||||
@ -440,7 +440,7 @@ class Harness:
|
||||
# graphical output
|
||||
graph = self.graph
|
||||
for f in fmt:
|
||||
if f in ("png", "svg", "html"):
|
||||
if f in ("png", "svg", "html", "pdf"):
|
||||
if f == "html": # if HTML format is specified,
|
||||
f = "svg" # generate SVG for embedding into HTML
|
||||
# SVG file will be renamed/deleted later
|
||||
@ -471,10 +471,7 @@ class Harness:
|
||||
# HTML output
|
||||
if "html" in fmt:
|
||||
generate_html_output(filename, bomlist, self.metadata, self.options)
|
||||
# PDF output
|
||||
if "pdf" in fmt:
|
||||
# TODO: implement PDF output
|
||||
print("PDF output is not yet supported")
|
||||
# PDF output is handled by GraphViz in the format loop above
|
||||
# delete SVG if not needed
|
||||
if "html" in fmt and not "svg" in fmt:
|
||||
# SVG file was just needed to generate HTML
|
||||
|
||||
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