Compare commits

...

25 Commits

Author SHA1 Message Date
Daniel Rojas
a02f97afcc Add temp/ to .gitignore 2021-10-03 21:37:38 +02:00
Daniel Rojas
f2859864a9 Sort imports 2021-10-02 18:00:31 +02:00
Daniel Rojas
42a15f7eab Implement smart file resolving for images
When resolving a relative image path, first search relative to the main YAML's path, then relative to the prepended YAML if unsuccessful.
2021-10-02 17:52:31 +02:00
Daniel Rojas
d94a249176
Use regex substitution
Co-authored-by: kvid <kvid@users.noreply.github.com>
2021-10-02 17:19:11 +02:00
Daniel Rojas
d00cc8362d Add suggestion by @kvid 2020-12-13 13:12:59 +01:00
Daniel Rojas
0a59e97d29
Apply suggestions from code review by @kvid
Co-authored-by: kvid <kvid@users.noreply.github.com>
2020-12-13 13:00:17 +01:00
Daniel Rojas
6a42a30523 Change parse() function's arguments`
Input to `parse()` used to be `yaml_input` for the actual YAML, and `file_in` for the YAML file path.
THe latter was only used to resolve relative image paths, not for reading the actual file that was passed.
Therefore, `file_in` was changed to `base_path`; this argument receives the *directory* of the original YAML file, which is more intuitive for the actual use case.
2020-12-06 13:30:01 +01:00
Daniel Rojas
2d7770a755 Apply more suggestions by @kvid 2020-12-06 13:09:57 +01:00
Daniel Rojas
426c11b766
Apply some of @kvid's suggestions for svgembed.py
Co-authored-by: kvid <kvid@users.noreply.github.com>
2020-12-06 12:54:55 +01:00
Daniel Rojas
a18550e79e Implement image embedding in Harness.svg() 2020-11-16 19:57:15 +01:00
Daniel Rojas
d669b38392 Separate base64-encoding from file handling 2020-11-16 19:36:22 +01:00
Daniel Rojas
973125cd75 Use Pathlib to overwrite file 2020-11-15 12:30:23 +01:00
Daniel Rojas
cb240af1f7 Delete and rename files only after closing handles 2020-11-15 12:17:31 +01:00
Daniel Rojas
61684425df Remove more debugging stuff 2020-11-15 12:15:35 +01:00
Daniel Rojas
04ddf53c4f Minor fixes 2020-11-15 12:14:48 +01:00
Daniel Rojas
9996b3bc2d Use correct MIME subtype for embedded image 2020-11-15 12:06:15 +01:00
Daniel Rojas
3e092a4fbb Remove debugging lines
Use cf77b3463ba73a3ea702d0b130f829eccf82f91e if required.
2020-11-15 11:46:38 +01:00
Daniel Rojas
cf77b3463b Enable embedding of images in WireViz SVG output 2020-11-15 11:42:41 +01:00
Daniel Rojas
cc93a330fb Add option to [not] overwrite original SVG file 2020-11-15 11:42:17 +01:00
Daniel Rojas
d4dc19cac5 Reference capture group by name 2020-11-15 11:02:01 +01:00
Daniel Rojas
7d0cc07b1d Split long line 2020-11-15 10:55:11 +01:00
Daniel Rojas
37c8e19961 Make regex case-insensitive 2020-11-15 10:50:58 +01:00
Daniel Rojas
6c946ce14e Resolve relative image URLs 2020-11-15 10:50:42 +01:00
Daniel Rojas
bf96f5e858 Implement function to embed images within SVG file 2020-11-15 10:36:45 +01:00
Daniel Rojas
a7e75a05e3 Resolve image paths correctly 2020-11-15 08:55:22 +01:00
6 changed files with 86 additions and 19 deletions

2
.gitignore vendored
View File

@ -10,4 +10,4 @@ dist
venv/ venv/
desktop.ini desktop.ini
thumbs.db thumbs.db
temp/

View File

@ -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

View File

@ -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
View 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)

View File

@ -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__':

View File

@ -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]))