summaryrefslogtreecommitdiff
path: root/bruder.py
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2024-04-20 18:18:20 +0200
committerjaseg <git@jaseg.de>2024-04-20 18:18:20 +0200
commit66da7861f368255a2db5be0b6c1c532385687851 (patch)
treec99bb7c026e1504f379ae9f18b8aea5a8a29aa56 /bruder.py
parent065db66e958d3d221c0a8d52cf3015caa58a45c3 (diff)
downloadtaep-66da7861f368255a2db5be0b6c1c532385687851.tar.gz
taep-66da7861f368255a2db5be0b6c1c532385687851.tar.bz2
taep-66da7861f368255a2db5be0b6c1c532385687851.zip
Tidy up repo, add README
Diffstat (limited to 'bruder.py')
-rw-r--r--bruder.py307
1 files changed, 0 insertions, 307 deletions
diff --git a/bruder.py b/bruder.py
deleted file mode 100644
index 1b46033..0000000
--- a/bruder.py
+++ /dev/null
@@ -1,307 +0,0 @@
-#!/usr/bin/env python3
-
-import tempfile
-import shutil
-import webbrowser
-import re
-import base64
-import copy
-import subprocess
-import shlex
-import os
-import sys
-import math
-from pathlib import Path
-
-import click
-from bs4 import BeautifulSoup
-
-from svg_util import *
-
-
-def run_cargo_command(binary, *args, **kwargs):
- # By default, try a number of options:
- candidates = [
- # somewhere in $PATH
- binary,
- # wasi-wrapper in $PATH
- f'wasi-{binary}',
- # in user-local cargo installation
- Path.home() / '.cargo' / 'bin' / binary,
- # wasi-wrapper in user-local pip installation
- Path.home() / '.local' / 'bin' / f'wasi-{binary}',
- # next to our current python interpreter (e.g. in virtualenv)
- str(Path(sys.executable).parent / f'wasi-{binary}')
- ]
-
- return run_command(binary, *args, candidates=candidates, **kwargs)
-
-
-def run_command(binary, *args, candidates=[], **kwargs):
- cmd_args = []
- for key, value in kwargs.items():
- if value is not None:
- if value is False:
- continue
-
- if len(key) > 1:
- cmd_args.append(f'--{key.replace("_", "-")}')
- else:
- cmd_args.append(f'-{key}')
-
- if value is not True:
- cmd_args.append(str(value))
- cmd_args.extend(map(str, args))
-
- # By default, try a number of options:
- if not candidates:
- candidates = [binary]
-
- # if envvar is set, try that first.
- if (env_var := os.environ.get(Path(binary).name.replace('-', '_').upper())):
- candidates = [str(Path(env_var).expanduser()), *candidates]
-
- for cand in candidates:
- try:
- res = subprocess.run([cand, *cmd_args], check=True)
- break
- except FileNotFoundError:
- continue
- else:
- raise SystemError(f'{binary} executable not found')
-
-
-def simplify_and_open_svg(data):
- with tempfile.NamedTemporaryFile('w', suffix='.svg') as tmp_in_svg,\
- tempfile.NamedTemporaryFile('r', suffix='.svg') as tmp_out_svg:
- tmp_in_svg.write(data)
- tmp_in_svg.flush()
-
- try:
- run_cargo_command('usvg', *shlex.split(os.environ.get('USVG_OPTIONS', '')), tmp_in_svg.name, tmp_out_svg.name)
- except SystemError:
- raise click.ClickException('Cannot find usvg. Please install usvg using cargo, or pass the full path to the usvg binary in the USVG environment variable.')
- except subprocess.CalledProcessError as e:
- raise click.ClickException(f'usvg exited with return code {e.returncode}.')
-
- return BeautifulSoup(tmp_out_svg.read(), 'xml')
-
-
-def print_tape(png_file):
- try:
- run_command('ptouch-print', '--image', png_file)
- except SystemError:
- raise click.ClickException('Cannot find ptouch-print. Please install ptouch-print from the upstream repo at https://git.familie-radermacher.ch/linux/ptouch-print.git . You can pass the full path to the ptouch-print binary in the PTOUCH_PRINT environment variable if it\'s not in $PATH.')
- except subprocess.CalledProcessError as e:
- raise click.ClickException(f'ptouch-print exited with return code {e.returncode}.')
-
-
-def do_dither(soup, magic_color, dpi, pixel_height):
- for i, path in enumerate(list(soup.find_all('path'))):
- if path.get('stroke').lower() != magic_color:
- continue
- path_id = path.get('id', '<no id?>')
-
- commands = list(parse_path_d(path.get('d', '')))
- if len(commands) != 2:
- print('Path', path_id, 'has magic color, but has more than two nodes. Ignoring.', file=sys.stderr)
- continue
- if commands[1][0] != 'L':
- print('Path', path_id, 'has magic color, but has a curve. Ignoring.', file=sys.stderr)
- continue
- if commands[0][0] != 'M':
- print('Path', path_id, 'has magic color, but is malformed (does not start with M command). Ignoring.', file=sys.stderr)
- continue
- (_c1, (x1, y1)), (_c2, (x2, y2)) = commands
-
- mat = Transform.parse_svg(path.get('transform', ''))
- for parent in path.parents:
- xf = Transform.parse_svg(parent.get('transform', ''))
- mat = xf * mat # make sure we apply the parent transform from the left, i.e. after the child transform
- x1, y1 = mat.transform_point(x1, y1)
- x2, y2 = mat.transform_point(x2, y2)
- path_len = math.dist((x1, y1), (x2, y2))
-
- if math.isclose(path_len, 0, abs_tol=1e-3):
- print('Path', path_id, 'has magic color, but has (almost) zero length. Ignoring.', file=sys.stderr)
- continue
-
- if not (stroke_w := path.get('stroke-width')):
- print('Path', path_id, 'has magic color, but has no defined stroke width. Ignoring.', file=sys.stderr)
- continue
- stroke_w = float(re.match('[-0-9.]+', stroke_w).group(0))
-
- path_angle = math.atan2((y2-y1), (x2-x1))
- dx, dy = (x2-x1)/path_len, (y2-y1)/path_len
-
- sx1, sy1 = mat.transform_point(x1-dy*stroke_w/2, y1+dx*stroke_w/2)
- sx2, sy2 = mat.transform_point(x1+dy*stroke_w/2, y1-dx*stroke_w/2)
- stroke_w = round(math.dist((sx1, sy1), (sx2, sy2)), 3)
-
- out_soup = copy.copy(soup)
-
- #print(f'found path {path_id} of length {path_len:2f} and angle {math.degrees(path_angle):.1f} deg with physical stroke width {stroke_w:.2f} from ({x1:.2f}, {y1:.2f}) to ({x2:.2f}, {y2:.2f})', file=sys.stderr)
- #out_soup.find('svg').append(out_soup.new_tag('path', fill='none', stroke='blue', stroke_width=f'24px',
- # d=f'M {x1} {y1} L {x2} {y2}'))
- xf = Transform.translate(0, stroke_w/2) * Transform.rotate(-path_angle) * Transform.translate(-x1, -y1)
- g = out_soup.new_tag('g', id='transform-group', transform=xf.as_svg())
- k = list(out_soup.find('svg').contents)
- for c in k:
- g.append(c.extract())
- out_soup.find('svg').append(g)
- out_soup.find('path', id=path['id']).parent.decompose()
- out_soup.find('svg')['viewBox'] = f'0 0 {path_len} {stroke_w}'
- out_soup.find('svg')['width'] = f'{path_len}mm'
- out_soup.find('svg')['height'] = f'{stroke_w}mm'
-
- with tempfile.NamedTemporaryFile('w', suffix='.svg') as tmp_svg,\
- tempfile.NamedTemporaryFile('rb', suffix='.png') as tmp_png,\
- tempfile.NamedTemporaryFile('rb', suffix='.png') as tmp_dither:
- tmp_svg.write(out_soup.prettify())
- tmp_svg.flush()
- run_cargo_command('resvg', tmp_svg.name, tmp_png.name, width=round(Inch(path_len, 'mm')*dpi), height=pixel_height)
-
- run_command('didder', 'edm', '--serpentine', 'FloydSteinberg', palette='black white', i=tmp_png.name, o=tmp_dither.name)
- yield (x1, y1, path_angle, stroke_w, path_len), tmp_dither.read()
-
-
-def make_preview(input_svg, out_file, *dither_args, assembly_labels=False, **dither_kwargs):
- imgs = []
- labels = []
- soup = simplify_and_open_svg(input_svg)
-
- for tape_num, ((x1, y1, path_angle, stroke_w, path_len), img) in enumerate(do_dither(soup, *dither_args, **dither_kwargs), start=1):
- xf = f'translate({x1} {y1}) rotate({math.degrees(path_angle)}) translate(0 {-stroke_w/2})'
- imgs.append(Tag('image', width=path_len, height=stroke_w, preserveAspectRatio='none',
- id=f'preview_image_{tape_num}',
- x=0, y=0,
- transform=xf,
- xlink__href=f'data:image/png;base64,{base64.b64encode(img).decode()}'))
- labels.append(Tag('path', fill='none', stroke_width='0.2px', stroke='red', transform=xf,
- d=f'M 0 0 h {path_len} v {stroke_w} h {-path_len} Z'))
- labels.append(Tag('text', fill='red', stroke='none', font_size=f'{stroke_w*0.8}px', transform=xf,
- x='2px', y=f'{stroke_w*0.9}px', children=[f'{tape_num}']))
-
- layer = Tag('g', inkscape__layer='Preview', inkscape__groupmode='layer', id='layer_preview', children=[
- Tag('g', id='preview_images', children=imgs),
- ])
-
- if assembly_labels:
- layer.children.append(Tag('g', id='assembly_instructions', children=labels))
-
- vbx, vby, vbw, vbh = map(float, soup.find('svg')['viewBox'].split())
- bounds = (vbx, vby), (vbx+vbw, vby+vbh)
- svg = setup_svg([layer], bounds, inkscape=True)
-
- if out_file is not None:
- out_file.write(str(svg))
- else:
- with tempfile.NamedTemporaryFile(suffix='.svg', mode='w', delete=False) as f:
- f.write(str(svg))
- f.flush()
- webbrowser.open_new_tab(f'file://{f.name}')
-
-
-@click.group()
-def cli():
- pass
-
-@cli.command()
-@click.option('--num-rows', type=int, default=5, help='Number of tapes')
-@click.option('--tape-width', type=float, default=24, help='Width of tape')
-@click.option('--tape-border', type=float, default=3, help='Width of empty border at the edges of the tape in mm')
-@click.option('--tape-spacing', type=float, default=2, help='Space between tapes')
-@click.option('--tape-length', type=float, default=250, help='Length of tape segments')
-@click.option('--magic-color', type=str, default='#cc0000', help='SVG color of tape')
-@click.argument('output_svg', type=click.File(mode='w'), default='-')
-def template(num_rows, tape_width, tape_border, tape_spacing, tape_length, magic_color, output_svg):
- pitch = tape_width + tape_spacing
- tags = [Tag('g', inkscape__layer='Layer 1', inkscape__groupmode='layer', id='layer1', children=[
- Tag('g', id='g1', children=[
- Tag('g', id=f'tape{i}', children=[
- Tag('path', id=f'tape{i}_outline', fill='none', stroke='black', opacity='0.3', stroke_width=f'{tape_width}px',
- d=f'M 0 {tape_width/2 + i*pitch} {tape_length} {tape_width/2 + i*pitch}'),
- Tag('path', id=f'tape{i}_printable_area', fill='none', stroke=magic_color, stroke_width=f'{tape_width-2*tape_border}px',
- d=f'M 0 {tape_width/2 + i*pitch} {tape_length} {tape_width/2 + i*pitch}'),
- ])
- for i in range(num_rows)
- ])
- ])]
-
- bounds = (0, 0), (tape_length, num_rows*tape_width + (num_rows-1)*tape_spacing)
- svg = setup_svg(tags, bounds, margin=tape_width, inkscape=True)
- output_svg.write(str(svg))
-
-
-@cli.command('print')
-@click.option('--magic-color', type=str, default='#cc0000', help='SVG color of tape')
-@click.option('--dpi', type=float, default=180, help='Printer bitmap resolution in DPI')
-@click.option('--pixel-height', type=int, default=127, help='Printer tape vertical pixel height')
-@click.option('--confirm/--no-confirm', default=True, help='Ask for confirmation before printing each tape')
-@click.option('--tape', type=str, default='-', help='The index numbers of which tapes to print. Comma-separate list, each entry is either a single number or a "3-5" style range where both ends are included.')
-@click.argument('input_svg', type=click.File(mode='r'), default='-')
-def cli_print(input_svg, tape, magic_color, dpi, pixel_height, confirm):
- with tempfile.TemporaryDirectory() as tmpdir:
- out = {}
-
- soup = simplify_and_open_svg(input_svg.read())
- for i, (_tape_pos, img) in enumerate(do_dither(soup, magic_color=magic_color, dpi=dpi, pixel_height=pixel_height), start=1):
- f = Path(tmpdir) / f'dither_tape_{i}.png'
- f.write_bytes(img)
- out[i] = f
-
- selected = set()
- for entry in tape.split(','):
- start, sep, stop = entry.partition('-')
- if not sep:
- selected.add(int(start))
- else:
- start = int(start) if start else min(out)
- stop = int(stop) if stop else max(out)
- selected |= set(range(start, stop+1))
-
- for tape in sorted(selected):
- if confirm:
- if not click.confirm(f'Do you want to continue and print tape {tape}?'):
- break
- print_tape(out[tape])
-
-
-@cli.command()
-@click.option('--magic-color', type=str, default='#cc0000', help='SVG color of tape')
-@click.option('--dpi', type=float, default=180, help='Printer bitmap resolution in DPI')
-@click.option('--pixel-height', type=int, default=127, help='Printer tape vertical pixel height')
-@click.argument('input_svg', type=click.File(mode='r'), default='-')
-@click.argument('output_svg', type=click.File(mode='w'), required=False)
-def preview(input_svg, output_svg, magic_color, dpi, pixel_height):
- make_preview(input_svg.read(), output_svg, magic_color=magic_color, dpi=dpi, pixel_height=pixel_height, assembly_labels=False)
-
-
-@cli.command()
-@click.option('--magic-color', type=str, default='#cc0000', help='SVG color of tape')
-@click.option('--dpi', type=float, default=180, help='Printer bitmap resolution in DPI')
-@click.option('--pixel-height', type=int, default=127, help='Printer tape vertical pixel height')
-@click.argument('input_svg', type=click.File(mode='r'), default='-')
-@click.argument('output_svg', type=click.File(mode='w'), required=False)
-def assembly(input_svg, output_svg, magic_color, dpi, pixel_height):
- make_preview(input_svg.read(), output_svg, magic_color=magic_color, dpi=dpi, pixel_height=pixel_height, assembly_labels=True)
-
-
-@cli.command()
-@click.option('--magic-color', type=str, default='#cc0000', help='SVG color of tape')
-@click.option('--dpi', type=float, default=180, help='Printer bitmap resolution in DPI')
-@click.option('--pixel-height', type=int, default=127, help='Printer tape vertical pixel height')
-@click.argument('input_svg', type=click.File(mode='r'), default='-')
-@click.argument('output_dir', type=click.Path(file_okay=False, dir_okay=True, path_type=Path))
-def dither(input_svg, output_dir, magic_color, dpi, pixel_height):
- output_dir.mkdir(exist_ok=True)
- soup = simplify_and_open_svg(input_svg.read())
- for i, (_tape_pos, img) in enumerate(do_dither(soup, magic_color=magic_color, dpi=dpi, pixel_height=pixel_height), start=1):
- outfile = output_dir / f'dither_tape_{i}.png'
- outfile.write_bytes(img)
- print(f'Wrote {outfile}')
-
-
-if __name__ == '__main__':
- cli()
-