Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional image to connectors and cables #153

Merged
merged 21 commits into from
Oct 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
43ec3bf
Add an image attribute to both Connectors and Cables
kvid Jul 29, 2020
c8c4005
Add a caption attribute to both Connectors and Cables
kvid Aug 3, 2020
0cb7118
Remove border between image and caption
kvid Aug 3, 2020
06e6c49
Expand example 08 with images and captions
kvid Aug 4, 2020
3dc2a55
Add an image_scale attribute to both Connectors and Cables
kvid Aug 6, 2020
41b3f3a
Add an image_size attribute to both Connectors and Cables
kvid Aug 6, 2020
3d7f027
Add fixedsize as third image_size value
kvid Aug 12, 2020
204379a
Move image attributes into Image dataclass to fix change requests
kvid Aug 15, 2020
c23679d
Add info about Image dataclass
kvid Aug 16, 2020
d289f95
Add a minor image scaling to example 08
kvid Aug 16, 2020
0c6b6f3
Compute sensible default values for unspecified image attributes
kvid Aug 16, 2020
c217079
Test image.width and image.height without using 'is not None'
kvid Aug 18, 2020
285a28d
Avoid some bad combinations of default values
kvid Aug 19, 2020
19c415c
Add aspect_ratio() function that reads image size from file
kvid Aug 19, 2020
9950f7f
Improve the image.fixedsize default value computation
kvid Aug 21, 2020
c0a11d0
Simplify the image.fixedsize default value computation
kvid Aug 21, 2020
ede29cb
Make Image.gv_dir an InitVar
kvid Aug 29, 2020
7c2fdd6
Improve error handling in aspect_ratio()
kvid Aug 29, 2020
dabd4ba
Add advanced image usage documentation
kvid Aug 31, 2020
a94ff3d
Add pillow (PIL) as dependecy
kvid Oct 10, 2020
50d4dc7
Adjust image related attributes of ex08.yml
kvid Oct 11, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/advanced_image_usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Advanced Image Usage

In rare cases when the [ordinary image scaling functionality](syntax.md#images) is insufficient, a couple of extra optional image attributes can be set to offer extra image cell space and scaling functionality when combined with the image dimension attributes `width` and `height`, but in most cases their default values below are sufficient:
- `scale: <str>` (how an image will use the available cell space) is default `false` if no dimension is set, or `true` if only one dimension is set, or `both` if both dimensions are set.
- `fixedsize: <bool>` (scale to fixed size or expand to minimum size) is default `false` when no dimension is set or if a `scale` value is set, and `true` otherwise.
- When `fixedsize` is true and only one dimension is set, then the other dimension is calculated using the image aspect ratio. If reading the aspect ratio fails, then 1:1 ratio is assumed.

See explanations of all supported values for these attributes in subsections below.

## The effect of `fixedsize` boolean values

- When `false`, any `width` or `height` values are _minimum_ values used to expand the image cell size for more available space, but cell contents or other size demands in the table might expand this cell even more than specified by `width` or `height`.
- When `true`, both `width` and `height` values are required by Graphwiz and specify the fixed size of the image cell, distorting any image inside if it don't fit. Any borders are normally drawn around the fixed size, and therefore, WireViz enclose the image cell in an extra table without borders when `fixedsize` is true to keep the borders around the outer non-fixed cell.

## The effect of `scale` string values:

- When `false`, the image is not scaled.
- When `true`, the image is scaled proportionally to fit within the available image cell space.
- When `width`, the image width is expanded (height is normally unchanged) to fill the available image cell space width.
- When `height`, the image height is expanded (width is normally unchanged) to fill the available image cell space height.
- When `both`, both image width and height are expanded independently to fill the available image cell space.

In all cases (except `true`) the image might get distorted when a specified fixed image cell size limits the available space to less than what an unscaled image needs.

In the WireViz diagrams there are no other space demanding cells in the same row, and hence, there are never extra available image cell space height unless a greater image cell `height` also is set.

## Usage examples

All examples of `image` attribute combinations below also require the mandatory `src` attribute to be set.

- Expand the image proportionally to fit within a minimum height and the node width:
```yaml
height: 100 # Expand image cell to this minimum height
fixedsize: false # Avoid scaling to a fixed size
# scale default value is true in this case
```

- Increase the space around the image by expanding the image cell space (width and/or height) to a larger value without scaling the image:
```yaml
width: 200 # Expand image cell to this minimum width
height: 100 # Expand image cell to this minimum height
scale: false # Avoid scaling the image
# fixedsize default value is false in this case
```

- Stretch the image width to fill the available space in the node:
```yaml
scale: width # Expand image width to fill the available image cell space
# fixedsize default value is false in this case
```

- Stretch the image height to a minimum value:
```yaml
height: 100 # Expand image cell to this minimum height
scale: height # Expand image height to fill the available image cell space
# fixedsize default value is false in this case
```

## How Graphviz support this image scaling

The connector and cable nodes are rendered using a HTML `<table>` containing an image cell `<td>` with `width`, `height`, and `fixedsize` attributes containing an image `<img>` with `src` and `scale` attributes. See also the [Graphviz doc](https://graphviz.org/doc/info/shapes.html#html), but note that WireViz uses default values as described above.
11 changes: 10 additions & 1 deletion examples/ex08.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# contributed by @cocide
# and later extended to include images

connectors:
Key:
Expand All @@ -7,14 +8,22 @@ connectors:
pins: [T, R, S]
pinlabels: [Dot, Dash, Ground]
show_pincount: false
image:
src: resources/stereo-phone-plug-TRS.png
caption: Tip, Ring, and Sleeve

cables:
W1:
gauge: 24 AWG
length: 0.2
color: BK # Cable jacket color
color_code: DIN
wirecount: 3
shield: true
shield: SN # Matching the shield color in the image
image:
src: resources/cable-WH+BN+GN+shield.png
height: 70 # Scale the image size slightly down
caption: Cross-section

connections:
-
Expand Down
Binary file added examples/resources/cable-WH+BN+GN+shield.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/resources/stereo-phone-plug-TRS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.
graphviz
pillow
pyyaml
setuptools
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def read(fname):
long_description_content_type='text/markdown',
install_requires=[
'pyyaml',
'pillow',
'graphviz',
],
license='GPLv3',
Expand Down
50 changes: 48 additions & 2 deletions src/wireviz/DataClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,48 @@
# -*- coding: utf-8 -*-

from typing import Optional, List, Any, Union
from dataclasses import dataclass, field
from wireviz.wv_helper import int2tuple
from dataclasses import dataclass, field, InitVar
from pathlib import Path
from wireviz.wv_helper import int2tuple, aspect_ratio
from wireviz import wv_colors


@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[str] = None # false | true | width | height | both
# Attributes of the image cell <td> containing the image:
width: Optional[int] = None
height: Optional[int] = None
fixedsize: Optional[bool] = None
# Contents of the text cell <td> just below the image cell:
caption: Optional[str] = None
# See also HTML doc at https://graphviz.org/doc/info/shapes.html#html

def __post_init__(self, gv_dir):

if self.fixedsize is None:
# Default True if any dimension specified unless self.scale also is specified.
self.fixedsize = (self.width or self.height) and self.scale is None

if self.scale is None:
self.scale = "false" if not self.width and not self.height \
else "both" if self.width and self.height \
else "true" # When only one dimension is specified.

if self.fixedsize:
# If only one dimension is specified, compute the other
# 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))
else:
if self.width:
self.height = self.width / aspect_ratio(gv_dir.joinpath(self.src))
formatc1702 marked this conversation as resolved.
Show resolved Hide resolved


@dataclass
class Connector:
name: str
Expand All @@ -18,6 +55,7 @@ class Connector:
type: Optional[str] = None
subtype: Optional[str] = None
pincount: Optional[int] = None
image: Optional[Image] = None
notes: Optional[str] = None
pinlabels: List[Any] = field(default_factory=list)
pins: List[Any] = field(default_factory=list)
Expand All @@ -29,6 +67,10 @@ class Connector:
loops: List[Any] = field(default_factory=list)

def __post_init__(self):

if isinstance(self.image, dict):
self.image = Image(**self.image)

self.ports_left = False
self.ports_right = False
self.visible_pins = {}
Expand Down Expand Up @@ -91,6 +133,7 @@ class Cable:
color: Optional[str] = None
wirecount: Optional[int] = None
shield: bool = False
image: Optional[Image] = None
notes: Optional[str] = None
colors: List[Any] = field(default_factory=list)
color_code: Optional[str] = None
Expand All @@ -99,6 +142,9 @@ class Cable:

def __post_init__(self):

if isinstance(self.image, dict):
self.image = Image(**self.image)

if isinstance(self.gauge, str): # gauge and unit specified
try:
g, u = self.gauge.split(' ')
Expand Down
6 changes: 5 additions & 1 deletion src/wireviz/Harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from wireviz.wv_helper import awg_equiv, mm2_equiv, tuplelist2tsv, \
nested_html_table, flatten2d, index_if_list, html_line_breaks, \
graphviz_line_breaks, remove_line_breaks, open_file_read, open_file_write, \
manufacturer_info_field
html_image, html_caption, manufacturer_info_field
from collections import Counter
from typing import List
from pathlib import Path
Expand Down Expand Up @@ -98,6 +98,8 @@ def create_graph(self) -> Graph:
f'{connector.pincount}-pin' if connector.show_pincount else None,
connector.color, '<!-- colorbar -->' if connector.color else None],
'<!-- connector table -->' if connector.style != 'simple' else None,
[html_image(connector.image)],
[html_caption(connector.image)],
[html_line_breaks(connector.notes)]]
html.extend(nested_html_table(rows))

Expand Down Expand Up @@ -173,6 +175,8 @@ def create_graph(self) -> Graph:
f'{cable.length} m' if cable.length > 0 else None,
cable.color, '<!-- colorbar -->' if cable.color else None],
'<!-- wire table -->',
[html_image(cable.image)],
[html_caption(cable.image)],
[html_line_breaks(cable.notes)]]
html.extend(nested_html_table(rows))

Expand Down
5 changes: 5 additions & 0 deletions src/wireviz/wireviz.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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

if sec == 'connectors':
if not attribs.get('autogenerate', False):
harness.add_connector(name=key, **attribs)
Expand Down
42 changes: 41 additions & 1 deletion src/wireviz/wv_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def nested_html_table(rows):
# input: list, each item may be scalar or list
# output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar
# purpose: create the appearance of one table, where cell widths are independent between rows
# attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag
html = []
html.append('<table border="0" cellspacing="0" cellpadding="0">')
for row in rows:
Expand All @@ -43,7 +44,8 @@ def nested_html_table(rows):
html.append(' <table border="0" cellspacing="0" cellpadding="3" cellborder="1"><tr>')
for cell in row:
if cell is not None:
html.append(f' <td balign="left">{cell}</td>')
# Inject attributes to the preceeding <td> tag where needed
html.append(f' <td balign="left">{cell}</td>'.replace('><tdX', ''))
kvid marked this conversation as resolved.
Show resolved Hide resolved
html.append(' </tr></table>')
html.append(' </td></tr>')
elif row is not None:
Expand All @@ -53,6 +55,30 @@ def nested_html_table(rows):
html.append('</table>')
return html

def html_image(image):
if not image:
return None
# The leading attributes belong to the preceeding tag. See where used below.
html = f'{html_size_attr(image)}><img scale="{image.scale}" src="{image.src}"/>'
if image.fixedsize:
# Close the preceeding tag and enclose the image cell in a table without
# borders to avoid narrow borders when the fixed width < the node width.
html = f'''>
<table border="0" cellspacing="0" cellborder="0"><tr>
<td{html}</td>
</tr></table>
'''
return f'''<tdX{' sides="TLR"' if image.caption else ''}{html}'''

def html_caption(image):
return f'<tdX sides="BLR">{html_line_breaks(image.caption)}' if image and image.caption else None

def html_size_attr(image):
# Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object
return ((f' width="{image.width}"' if image.width else '')
+ (f' height="{image.height}"' if image.height else '')
+ ( ' fixedsize="true"' if image.fixedsize else '')) if image else ''


def expand(yaml_data):
# yaml_data can be:
Expand Down Expand Up @@ -132,6 +158,20 @@ def open_file_write(filename):
def open_file_append(filename):
return open(filename, 'a', encoding='UTF-8')


def aspect_ratio(image_src):
try:
from PIL import Image
image = Image.open(image_src)
if image.width > 0 and image.height > 0:
return image.width / image.height
print(f'aspect_ratio(): Invalid image size {image.width} x {image.height}')
# ModuleNotFoundError and FileNotFoundError are the most expected, but all are handled equally.
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 manufacturer_info_field(manufacturer, mpn):
if manufacturer or mpn:
return f'{manufacturer if manufacturer else "MPN"}{": " + str(mpn) if mpn else ""}'
Expand Down