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/
desktop.ini
thumbs.db
temp/

View File

@ -32,7 +32,6 @@ OneOrMoreWires = Union[Wire, Tuple[Wire, ...]] # One or a tuple of wires
@dataclass
class Image:
gv_dir: InitVar[Path] # Directory of .gv file injected as context during parsing
# Attributes of the image object <img>:
src: str
scale: Optional[ImageScale] = None
@ -44,7 +43,7 @@ class Image:
caption: Optional[MultilineHypertext] = None
# 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:
# 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.
if self.height:
if not self.width:
self.width = self.height * aspect_ratio(gv_dir.joinpath(self.src))
self.width = self.height * aspect_ratio(self.src)
else:
if self.width:
self.height = self.width / aspect_ratio(gv_dir.joinpath(self.src))
self.height = self.width / aspect_ratio(self.src)
@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_helper import awg_equiv, mm2_equiv, tuplelist2tsv, flatten2d, \
open_file_read, open_file_write
from wireviz.svgembed import embed_svg_images, embed_svg_images_file
class Harness:
@ -341,13 +342,9 @@ class Harness:
return data.read()
@property
def svg(self):
from io import BytesIO
def svg(self) -> str:
graph = self.create_graph()
data = BytesIO()
data.write(graph.pipe(format='svg'))
data.seek(0)
return data.read()
return embed_svg_images(graph.pipe(format='svg').decode('utf-8'), Path.cwd())
def output(self, filename: (str, Path), view: bool = False, cleanup: bool = True, fmt: tuple = ('pdf', )) -> None:
# graphical output
@ -356,6 +353,8 @@ class Harness:
graph.format = f
graph.render(filename=filename, view=view, cleanup=cleanup)
graph.save(filename=f'{filename}.gv')
if 'svg' in fmt:
embed_svg_images_file(f'{filename}.svg')
# bom output
bomlist = bom_list(self.bom())
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
from pathlib import Path
import sys
from typing import Any, Tuple
from typing import Any, List, Tuple
import yaml
@ -14,15 +14,16 @@ if __name__ == '__main__':
from wireviz import __version__
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
: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
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
@ -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 ty == dict:
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')
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 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 = 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():
@ -235,6 +237,8 @@ def main():
with open_file_read(args.input_file) as fh:
yaml_input = fh.read()
base_path = [Path(args.input_file).parent]
if 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')
@ -242,6 +246,7 @@ def main():
with open_file_read(args.prepend_file) as fh:
prepend = fh.read()
yaml_input = prepend + yaml_input
base_path.append(Path(args.prepend_file).parent)
if not args.output_file:
file_out = args.input_file
@ -251,7 +256,7 @@ def main():
file_out = args.output_file
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__':

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pathlib import Path
from typing import List
import re
@ -117,3 +118,22 @@ def aspect_ratio(image_src):
except Exception as error:
print(f'aspect_ratio(): {type(error).__name__}: {error}')
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]))