Compare commits
25 Commits
master
...
backup/bug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a02f97afcc | ||
|
|
f2859864a9 | ||
|
|
42a15f7eab | ||
|
|
d94a249176 | ||
|
|
d00cc8362d | ||
|
|
0a59e97d29 | ||
|
|
6a42a30523 | ||
|
|
2d7770a755 | ||
|
|
426c11b766 | ||
|
|
a18550e79e | ||
|
|
d669b38392 | ||
|
|
973125cd75 | ||
|
|
cb240af1f7 | ||
|
|
61684425df | ||
|
|
04ddf53c4f | ||
|
|
9996b3bc2d | ||
|
|
3e092a4fbb | ||
|
|
cf77b3463b | ||
|
|
cc93a330fb | ||
|
|
d4dc19cac5 | ||
|
|
7d0cc07b1d | ||
|
|
37c8e19961 | ||
|
|
6c946ce14e | ||
|
|
bf96f5e858 | ||
|
|
a7e75a05e3 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -10,4 +10,4 @@ dist
|
|||||||
venv/
|
venv/
|
||||||
desktop.ini
|
desktop.ini
|
||||||
thumbs.db
|
thumbs.db
|
||||||
|
temp/
|
||||||
|
|||||||
@ -32,7 +32,6 @@ OneOrMoreWires = Union[Wire, Tuple[Wire, ...]] # One or a tuple of wires
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Image:
|
class Image:
|
||||||
gv_dir: InitVar[Path] # Directory of .gv file injected as context during parsing
|
|
||||||
# Attributes of the image object <img>:
|
# Attributes of the image object <img>:
|
||||||
src: str
|
src: str
|
||||||
scale: Optional[ImageScale] = None
|
scale: Optional[ImageScale] = None
|
||||||
@ -44,7 +43,7 @@ class Image:
|
|||||||
caption: Optional[MultilineHypertext] = None
|
caption: Optional[MultilineHypertext] = None
|
||||||
# See also HTML doc at https://graphviz.org/doc/info/shapes.html#html
|
# See also HTML doc at https://graphviz.org/doc/info/shapes.html#html
|
||||||
|
|
||||||
def __post_init__(self, gv_dir):
|
def __post_init__(self) -> None:
|
||||||
|
|
||||||
if self.fixedsize is None:
|
if self.fixedsize is None:
|
||||||
# Default True if any dimension specified unless self.scale also is specified.
|
# Default True if any dimension specified unless self.scale also is specified.
|
||||||
@ -60,10 +59,10 @@ class Image:
|
|||||||
# because Graphviz requires both when fixedsize=True.
|
# because Graphviz requires both when fixedsize=True.
|
||||||
if self.height:
|
if self.height:
|
||||||
if not self.width:
|
if not self.width:
|
||||||
self.width = self.height * aspect_ratio(gv_dir.joinpath(self.src))
|
self.width = self.height * aspect_ratio(self.src)
|
||||||
else:
|
else:
|
||||||
if self.width:
|
if self.width:
|
||||||
self.height = self.width / aspect_ratio(gv_dir.joinpath(self.src))
|
self.height = self.width / aspect_ratio(self.src)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@ -18,6 +18,7 @@ from wireviz.wv_bom import manufacturer_info_field, component_table_entry, \
|
|||||||
from wireviz.wv_html import generate_html_output
|
from wireviz.wv_html import generate_html_output
|
||||||
from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \
|
from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \
|
||||||
open_file_read, open_file_write
|
open_file_read, open_file_write
|
||||||
|
from wireviz.svgembed import embed_svg_images, embed_svg_images_file
|
||||||
|
|
||||||
|
|
||||||
class Harness:
|
class Harness:
|
||||||
@ -341,13 +342,9 @@ class Harness:
|
|||||||
return data.read()
|
return data.read()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def svg(self):
|
def svg(self) -> str:
|
||||||
from io import BytesIO
|
|
||||||
graph = self.create_graph()
|
graph = self.create_graph()
|
||||||
data = BytesIO()
|
return embed_svg_images(graph.pipe(format='svg').decode('utf-8'), Path.cwd())
|
||||||
data.write(graph.pipe(format='svg'))
|
|
||||||
data.seek(0)
|
|
||||||
return data.read()
|
|
||||||
|
|
||||||
def output(self, filename: (str, Path), view: bool = False, cleanup: bool = True, fmt: tuple = ('pdf', )) -> None:
|
def output(self, filename: (str, Path), view: bool = False, cleanup: bool = True, fmt: tuple = ('pdf', )) -> None:
|
||||||
# graphical output
|
# graphical output
|
||||||
@ -356,6 +353,8 @@ class Harness:
|
|||||||
graph.format = f
|
graph.format = f
|
||||||
graph.render(filename=filename, view=view, cleanup=cleanup)
|
graph.render(filename=filename, view=view, cleanup=cleanup)
|
||||||
graph.save(filename=f'{filename}.gv')
|
graph.save(filename=f'{filename}.gv')
|
||||||
|
if 'svg' in fmt:
|
||||||
|
embed_svg_images_file(f'{filename}.svg')
|
||||||
# bom output
|
# bom output
|
||||||
bomlist = bom_list(self.bom())
|
bomlist = bom_list(self.bom())
|
||||||
with open_file_write(f'{filename}.bom.tsv') as file:
|
with open_file_write(f'{filename}.bom.tsv') as file:
|
||||||
|
|||||||
44
src/wireviz/svgembed.py
Normal file
44
src/wireviz/svgembed.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
mime_subtype_replacements = {'jpg': 'jpeg', 'tif': 'tiff'}
|
||||||
|
|
||||||
|
def embed_svg_images(svg_in: str, base_path: Union[str, Path] = Path.cwd()) -> str:
|
||||||
|
images_b64 = {} # cache of base64-encoded images
|
||||||
|
|
||||||
|
def image_tag(pre: str, url: str, post: str) -> str:
|
||||||
|
return f'<image{pre} xlink:href="{url}"{post}>'
|
||||||
|
|
||||||
|
def replace(match: re.Match) -> str:
|
||||||
|
imgurl = match['URL']
|
||||||
|
if not imgurl in images_b64: # only encode/cache every unique URL once
|
||||||
|
imgurl_abs = (Path(base_path) / imgurl).resolve()
|
||||||
|
images_b64[imgurl] = base64.b64encode(imgurl_abs.read_bytes()).decode('utf-8')
|
||||||
|
return image_tag(
|
||||||
|
match['PRE'] or '',
|
||||||
|
f'data:image/{get_mime_subtype(imgurl)};base64, {images_b64[imgurl]}',
|
||||||
|
match['POST'] or '')
|
||||||
|
|
||||||
|
pattern = re.compile(
|
||||||
|
image_tag(r'(?P<PRE> [^>]*?)?', r'(?P<URL>[^"]*?)', r'(?P<POST> [^>]*?)?'), re.IGNORECASE)
|
||||||
|
return pattern.sub(replace, svg_in)
|
||||||
|
|
||||||
|
|
||||||
|
def get_mime_subtype(filename: Union[str, Path]) -> str:
|
||||||
|
mime_subtype = Path(filename).suffix.lstrip('.').lower()
|
||||||
|
if mime_subtype in mime_subtype_replacements:
|
||||||
|
mime_subtype = mime_subtype_replacements[mime_subtype]
|
||||||
|
return mime_subtype
|
||||||
|
|
||||||
|
|
||||||
|
def embed_svg_images_file(filename_in: Union[str, Path], overwrite: bool = True) -> None:
|
||||||
|
filename_in = Path(filename_in).resolve()
|
||||||
|
filename_out = filename_in.with_suffix('.b64.svg')
|
||||||
|
filename_out.write_text(embed_svg_images(filename_in.read_text(), filename_in.parent))
|
||||||
|
if overwrite:
|
||||||
|
filename_out.replace(filename_in)
|
||||||
@ -5,7 +5,7 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Tuple
|
from typing import Any, List, Tuple
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@ -14,15 +14,16 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
from wireviz import __version__
|
from wireviz import __version__
|
||||||
from wireviz.Harness import Harness
|
from wireviz.Harness import Harness
|
||||||
from wireviz.wv_helper import expand, open_file_read
|
from wireviz.wv_helper import expand, open_file_read, smart_file_resolve
|
||||||
|
|
||||||
|
|
||||||
def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, str, Tuple[str]) = None) -> Any:
|
def parse(yaml_input: str, base_path: (str, Path, List) = None, file_out: (str, Path) = None, return_types: (None, str, Tuple[str]) = None) -> Any:
|
||||||
"""
|
"""
|
||||||
Parses yaml input string and does the high-level harness conversion
|
Parses yaml input string and does the high-level harness conversion
|
||||||
|
|
||||||
:param yaml_input: a string containing the yaml input data
|
:param yaml_input: a string containing the yaml input data
|
||||||
:param file_out:
|
:param base_path: base path used to resolve any relative paths to image files
|
||||||
|
:param file_out: filename of the generated output
|
||||||
:param return_types: if None, then returns None; if the value is a string, then a
|
:param return_types: if None, then returns None; if the value is a string, then a
|
||||||
corresponding data format will be returned; if the value is a tuple of strings,
|
corresponding data format will be returned; if the value is a tuple of strings,
|
||||||
then for every valid format in the `return_types` tuple, another return type
|
then for every valid format in the `return_types` tuple, another return type
|
||||||
@ -44,10 +45,11 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st
|
|||||||
if len(yaml_data[sec]) > 0:
|
if len(yaml_data[sec]) > 0:
|
||||||
if ty == dict:
|
if ty == dict:
|
||||||
for key, attribs in yaml_data[sec].items():
|
for key, attribs in yaml_data[sec].items():
|
||||||
# The Image dataclass might need to open an image file with a relative path.
|
|
||||||
image = attribs.get('image')
|
image = attribs.get('image')
|
||||||
if isinstance(image, dict):
|
if isinstance(image, dict):
|
||||||
image['gv_dir'] = Path(file_out if file_out else '').parent # Inject context
|
image_path = image['src']
|
||||||
|
if image_path and not Path(image_path).is_absolute(): # resolve relative image path
|
||||||
|
image['src'] = smart_file_resolve(image_path, base_path)
|
||||||
|
|
||||||
if sec == 'connectors':
|
if sec == 'connectors':
|
||||||
if not attribs.get('autogenerate', False):
|
if not attribs.get('autogenerate', False):
|
||||||
@ -209,7 +211,7 @@ def parse_file(yaml_file: str, file_out: (str, Path) = None) -> None:
|
|||||||
file_out = fn
|
file_out = fn
|
||||||
file_out = os.path.abspath(file_out)
|
file_out = os.path.abspath(file_out)
|
||||||
|
|
||||||
parse(yaml_input, file_out=file_out)
|
parse(yaml_input, base_path=Path(yaml_file).parent, file_out=file_out)
|
||||||
|
|
||||||
|
|
||||||
def parse_cmdline():
|
def parse_cmdline():
|
||||||
@ -235,6 +237,8 @@ def main():
|
|||||||
with open_file_read(args.input_file) as fh:
|
with open_file_read(args.input_file) as fh:
|
||||||
yaml_input = fh.read()
|
yaml_input = fh.read()
|
||||||
|
|
||||||
|
base_path = [Path(args.input_file).parent]
|
||||||
|
|
||||||
if args.prepend_file:
|
if args.prepend_file:
|
||||||
if not os.path.exists(args.prepend_file):
|
if not os.path.exists(args.prepend_file):
|
||||||
print(f'Error: prepend input file {args.prepend_file} inaccessible or does not exist, check path')
|
print(f'Error: prepend input file {args.prepend_file} inaccessible or does not exist, check path')
|
||||||
@ -242,6 +246,7 @@ def main():
|
|||||||
with open_file_read(args.prepend_file) as fh:
|
with open_file_read(args.prepend_file) as fh:
|
||||||
prepend = fh.read()
|
prepend = fh.read()
|
||||||
yaml_input = prepend + yaml_input
|
yaml_input = prepend + yaml_input
|
||||||
|
base_path.append(Path(args.prepend_file).parent)
|
||||||
|
|
||||||
if not args.output_file:
|
if not args.output_file:
|
||||||
file_out = args.input_file
|
file_out = args.input_file
|
||||||
@ -251,7 +256,7 @@ def main():
|
|||||||
file_out = args.output_file
|
file_out = args.output_file
|
||||||
file_out = os.path.abspath(file_out)
|
file_out = os.path.abspath(file_out)
|
||||||
|
|
||||||
parse(yaml_input, file_out=file_out)
|
parse(yaml_input, base_path=base_path, file_out=file_out)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -117,3 +118,22 @@ def aspect_ratio(image_src):
|
|||||||
except Exception as error:
|
except Exception as error:
|
||||||
print(f'aspect_ratio(): {type(error).__name__}: {error}')
|
print(f'aspect_ratio(): {type(error).__name__}: {error}')
|
||||||
return 1 # Assume 1:1 when unable to read actual image size
|
return 1 # Assume 1:1 when unable to read actual image size
|
||||||
|
|
||||||
|
def smart_file_resolve(filename: str, possible_paths: (str, List[str])) -> Path:
|
||||||
|
if not isinstance(possible_paths, List):
|
||||||
|
possible_paths = [possible_paths]
|
||||||
|
filename = Path(filename)
|
||||||
|
if filename.is_absolute():
|
||||||
|
if filename.exists():
|
||||||
|
return filename
|
||||||
|
else:
|
||||||
|
raise Exception(f'{filename} does not exist.')
|
||||||
|
else: # search all possible paths in decreasing order of precedence
|
||||||
|
possible_paths = [Path(path).resolve() for path in possible_paths if path is not None]
|
||||||
|
for possible_path in possible_paths:
|
||||||
|
resolved_path = (possible_path / filename).resolve()
|
||||||
|
if (resolved_path).exists():
|
||||||
|
return resolved_path
|
||||||
|
else:
|
||||||
|
raise Exception(f'{filename} was not found in any of the following locations: \n' +
|
||||||
|
'\n'.join([str(x) for x in possible_paths]))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user