Split CLI into sub-commands and add print command
This commit is contained in:
parent
58bdba8d03
commit
065db66e95
1 changed files with 154 additions and 58 deletions
212
bruder.py
212
bruder.py
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import tempfile
|
||||
import shutil
|
||||
import webbrowser
|
||||
import re
|
||||
import base64
|
||||
import copy
|
||||
|
|
@ -57,7 +58,7 @@ def run_command(binary, *args, candidates=[], **kwargs):
|
|||
candidates = [binary]
|
||||
|
||||
# if envvar is set, try that first.
|
||||
if (env_var := os.environ.get(binary.upper())):
|
||||
if (env_var := os.environ.get(Path(binary).name.replace('-', '_').upper())):
|
||||
candidates = [str(Path(env_var).expanduser()), *candidates]
|
||||
|
||||
for cand in candidates:
|
||||
|
|
@ -70,57 +71,32 @@ def run_command(binary, *args, candidates=[], **kwargs):
|
|||
raise SystemError(f'{binary} executable not found')
|
||||
|
||||
|
||||
@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()
|
||||
@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='-')
|
||||
def dither(input_svg, magic_color, dpi, pixel_height):
|
||||
tmpdir = Path('/tmp/foo') # FIXME debug
|
||||
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(input_svg.read())
|
||||
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 ClickException('Cannot find usvg. Please install usvg using cargo, or pass the full path to the usvg binary in the USVG environment variable.')
|
||||
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}.')
|
||||
|
||||
soup = BeautifulSoup(tmp_out_svg.read(), 'xml')
|
||||
return BeautifulSoup(tmp_out_svg.read(), 'xml')
|
||||
|
||||
preview_images = []
|
||||
tape_num = 1
|
||||
|
||||
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
|
||||
|
|
@ -164,7 +140,7 @@ def dither(input_svg, magic_color, dpi, pixel_height):
|
|||
|
||||
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)
|
||||
#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)
|
||||
|
|
@ -184,28 +160,148 @@ def dither(input_svg, magic_color, dpi, pixel_height):
|
|||
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)
|
||||
shutil.copy(tmp_png.name, f'/tmp/debug_{i}.png')
|
||||
|
||||
run_command('didder', 'edm', '--serpentine', 'FloydSteinberg', palette='black white', i=tmp_png.name, o=tmp_dither.name)
|
||||
shutil.copy(tmp_dither.name, f'/tmp/dither_{tape_num}.png')
|
||||
preview_images.append(Tag('image', width=path_len, height=stroke_w, preserveAspectRatio='none',
|
||||
id=f'preview_image_{tape_num}',
|
||||
x=0, y=0,
|
||||
transform=f'translate({x1} {y1}) rotate({math.degrees(path_angle)}) translate(0 {-stroke_w/2})',
|
||||
xlink__href=f'data:image/png;base64,{base64.b64encode(tmp_dither.read()).decode()}'))
|
||||
yield (x1, y1, path_angle, stroke_w, path_len), tmp_dither.read()
|
||||
|
||||
print('wrote', f'/tmp/dither_{tape_num}.png')
|
||||
tape_num += 1
|
||||
|
||||
tags = [Tag('g', inkscape__layer='Layer 1', inkscape__groupmode='layer', id='layer1', children=[
|
||||
Tag('g', id='g1', children=preview_images)
|
||||
])]
|
||||
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(tags, bounds, inkscape=True)
|
||||
Path('/tmp/preview.svg').write_text(str(svg))
|
||||
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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue