Last active
April 28, 2022 14:45
-
-
Save manthey/ab5a2240c6a8cb56ab2514eebf331161 to your computer and use it in GitHub Desktop.
Use tifftools and plantuml to create an svg diagram.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I was doing something like