Skip to content

Instantly share code, notes, and snippets.

@manthey
Last active April 28, 2022 14:45
Show Gist options
  • Save manthey/ab5a2240c6a8cb56ab2514eebf331161 to your computer and use it in GitHub Desktop.
Save manthey/ab5a2240c6a8cb56ab2514eebf331161 to your computer and use it in GitHub Desktop.
Use tifftools and plantuml to create an svg diagram.
import argparse
import base64
import io
import json
import math
import os
import subprocess
import sys
import tempfile
import large_image
import large_image_source_tiff
import PIL.Image
import PIL.ImageColor
import PIL.ImageDraw
import tifftools
import yaml
def get_thumbnail(ifd, factor):
maxwh = max(
ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0],
ifd['tags'][tifftools.Tag.ImageLength.value]['data'][0])
with tempfile.TemporaryDirectory() as tmpdir:
dest = os.path.join(tmpdir, 'oneifd.tiff')
tifftools.write_tiff(ifd, dest, allowExisting=True)
try:
ts = large_image_source_tiff.open(dest)
return ts.getThumbnail(width=int(maxwh * factor), height=int(maxwh * factor))[0]
except Exception:
pass
try:
ts = large_image.open(dest)
return ts.getThumbnail(width=int(maxwh * factor), height=int(maxwh * factor))[0]
except Exception:
pass
img = PIL.Image.open(dest)
img.thumbnail((int(maxwh * factor), int(maxwh * factor)))
output = io.BytesIO()
img.save(output, 'PNG')
img = output.getvalue()
return img
def add_structure(img, ifd, factor):
w = ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0]
h = ifd['tags'][tifftools.Tag.ImageLength.value]['data'][0]
vert = horz = None
if tifftools.Tag.TileWidth.value in ifd['tags']:
vert = ifd['tags'][tifftools.Tag.TileWidth.value]['data'][0]
horz = ifd['tags'][tifftools.Tag.TileLength.value]['data'][0]
elif tifftools.Tag.RowsPerStrip.value in ifd['tags']:
horz = ifd['tags'][tifftools.Tag.RowsPerStrip.value]['data'][0]
if (not horz or horz >= h or horz < 2) and (not vert or vert >= w or vert < 2):
return img
# Functionally, this makes lines thinner and antialiased. Functionally,
# liens are width / rescale in conceptual size
rescale = 2
width = 1
img = PIL.Image.open(io.BytesIO(img))
color = PIL.ImageColor.getcolor('#000', img.mode)
if rescale != 1:
origwh = img.width, img.height
img = img.resize((img.width * rescale, img.height * rescale))
imageDraw = PIL.ImageDraw.Draw(img)
if horz and horz < h and horz >= 2:
for y in range(0, h, horz):
sy = int(round(y * factor * rescale))
imageDraw.line([(0, sy), (img.width, sy)], color, width=width)
if vert and vert < w and vert >= 2:
for x in range(0, w, vert):
sx = int(round(x * factor * rescale))
imageDraw.line([(sx, 0), (sx, img.height)], color, width=width)
if rescale != 1:
img = img.resize(origwh)
output = io.BytesIO()
img.save(output, 'PNG')
img = output.getvalue()
return img
def add_thumbnails(rawyaml, args):
info = tifftools.read_tiff(args.source)
# Get the range of dimensions of the images
maxdim = 0
minmaxdim = None
for ifd in tifftools.commands._iterate_ifds(info['ifds'], subifds=True):
maxwh = max(
ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0],
ifd['tags'][tifftools.Tag.ImageLength.value]['data'][0])
maxdim = max(maxdim, maxwh)
if minmaxdim is None or maxwh < minmaxdim:
minmaxdim = maxwh
if minmaxdim == maxdim:
minmaxdim /= 2
minout = min(minmaxdim, args.minthumb)
maxout = min(maxdim, args.maxthumb)
ref = 0
for ifd in tifftools.commands._iterate_ifds(info['ifds'], subifds=True):
maxwh = max(
ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0],
ifd['tags'][tifftools.Tag.ImageLength.value]['data'][0])
factor = (math.log(maxwh / maxdim) - math.log(minmaxdim / maxdim)) / (
-math.log(minmaxdim / maxdim))
factor = (math.exp(factor * -math.log(minout / maxout) + math.log(
minout / maxout)) * maxout) / maxwh
img = get_thumbnail(ifd, factor)
if img and args.structure:
img = add_structure(img, ifd, factor)
if img:
imgstr = '<img:data:image/png;base64,' + base64.encodebytes(
img).decode().replace('\n', '') + '>'
else:
imgstr = 'Could not decode image'
# Change the yaml. This is an ugly way to inject the results
ref = rawyaml.index('\n', rawyaml.index(' ImageLength: ', ref)) + 1
spaces = rawyaml.rindex('ImageLength: ', 0, ref - 1) - (
rawyaml.rindex('\n', 0, ref - 1) + 1)
rawyaml = (
rawyaml[:ref] +
' ' * spaces + '"Image Thumbnail":\n' +
' ' * spaces + ' "Image": %s\n' % json.dumps(imgstr) +
rawyaml[ref:])
return rawyaml
def generate_uml(args):
cmd = ['tifftools', 'dump', '--yaml'] + args.tifftools_args + [args.source]
if args.verbose:
sys.stdout.write('tifftools command: %r\n' % cmd)
rawyaml = subprocess.check_output(cmd).decode()
if args.thumb:
rawyaml = add_thumbnails(rawyaml, args)
yamldata = yaml.safe_load(rawyaml)
if len(yamldata) == 1:
yamldata = yamldata[list(yamldata.keys())[0]]
jsonuml = '@startjson\n%s\n@endjson\n' % (json.dumps(yamldata))
cmd = ['plantuml', '-pipe'] + args.plantuml_args
if args.verbose:
sys.stdout.write('plantuml command: %r\n' % cmd)
with subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
) as proc:
result = proc.communicate(input=jsonuml.encode())[0]
if args.dest and args.dest != '-':
open(args.dest, 'wb').write(result)
else:
sys.stdout.write(result)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="""
Read tiff files and emit svg UML diagrams of their internal details.
Any unknown arguments are passed to either tifftools or plantuml. If at least
one argument is specified, the default arguments are not used. Unknown
arguments before "--" are sent to "tifftools dump --yaml". Those after -- are
sent to plantuml. The default arguments for tifftools dump are "--max 6
--max-text 40". For plantuml, they are "-tsvg".
""")
parser.add_argument(
'source', help='Path to source image')
parser.add_argument(
'--out', '--dest', dest='dest',
help='The destination file. If not specified or "-", the results are sent to stdout.')
parser.add_argument(
'--thumb', '--thumbnails', '--images', action='store_true',
help='Add image thumbnails to the output.')
parser.add_argument(
'--minthumb', type=int, default=64,
help='The minimum thumbnail size for the lowest resolution image layer.')
parser.add_argument(
'--maxthumb', type=int, default=512,
help='The maximum thumbnail size for the highest resolution image layer.')
parser.add_argument(
'--structure', action='store_true',
help='Draw the tile or strip structure on top of the thumbnail.')
parser.add_argument(
'--verbose', '-v', action='count', default=0, help='Increase verbosity')
args, unknown = parser.parse_known_args()
args.tifftools_args = unknown[:unknown.index('--') if '--' in unknown else len(unknown)] or \
['--max', '6', '--max-text', '40']
args.plantuml_args = unknown[unknown.index('--') + 1 if '--' in unknown else len(unknown):] or \
['-tsvg']
if args.verbose >= 2:
sys.stderr.write('args: %r\n' % args)
generate_uml(args)
@manthey
Copy link
Author

manthey commented Apr 26, 2022

I was doing something like

find . -exec bash -c "python tiff_to_uml.py {} --thumb --structure --out /tmp/{}.svg" \;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment