Skip to content

Instantly share code, notes, and snippets.

@epoz
Last active June 23, 2021 11:54
Show Gist options
  • Select an option

  • Save epoz/ce8854e16e9202dcd267 to your computer and use it in GitHub Desktop.

Select an option

Save epoz/ce8854e16e9202dcd267 to your computer and use it in GitHub Desktop.
Create a huge tiled (potentially gigapixel) image from a bunch of loose images.
import os
import sys
import re
import math
import random
import PIL.Image
import warnings
import json
from progress.bar import Bar
import textbase
import xml.etree.ElementTree as ET
import traceback
# Disable the warnings for giant images
PIL.Image.MAX_IMAGE_PIXELS = None
warnings.simplefilter("ignore", PIL.Image.DecompressionBombWarning)
MAX_WIDTH = 175000
MAX_HEIGHT = 100000
class Last:
pass
class BitmapFile(object):
def __init__(self, path, width, height):
self.path = path
self.width = width
self.height = height
self.is_break = False
self.metadata = {}
def __str__(self):
return "%s %sx%s" % (self.path, self.width, self.height)
def __lt__(self, other):
return self.path < other.path
__repr__ = __str__
def layout(
project,
frame=0,
layout_mode="horizontal",
background_color="#ffffff",
onestripe=False,
):
width = MAX_WIDTH + 1
height = MAX_HEIGHT + 1
iterations = 0
scale_factor = 1
iter_max = 10
while (width > MAX_WIDTH) or (height > MAX_HEIGHT):
if layout_mode == "hstrip":
data = make_h_strip(
project, background_color=background_color, scale_factor=scale_factor
)
else:
data = horzvert_layout(
project,
frame=frame,
background_color=background_color,
scale_factor=scale_factor,
layout_mode=layout_mode,
onestripe=onestripe,
)
iterations += 1
if iterations > iter_max:
raise Exception(f"Layout iterations exceeds {iter_max}")
scale_factor -= 0.1
if scale_factor <= 0:
raise Exception("Layout scale_fator <= 0")
width = data.get("width", 0)
height = data.get("height", 0)
return data
def make_h_strip(files, background_color="#ffffff", scale_factor=1):
stripe_height = int(max(f.height for f in files) * scale_factor)
x, y = 0, 0
data = {"images": [], "background_color": background_color}
for f in files:
ratio = f.width / f.height
new_width = int(stripe_height * ratio)
tmp = {
"filename": f.path,
"x": x,
"y": y,
"width": new_width,
"height": stripe_height,
}
x += new_width
data["images"].append(tmp)
data["width"] = x
data["height"] = stripe_height
return data
def horzvert_layout(
files,
layout_mode="horizontal",
background_color="#ffffff",
frame=0,
scale_factor=1,
onestripe=False,
):
"""Do the layout and produce a usable dict output that can be persisted with the Project.
We used to save these as attributes in the File objects.
"""
# Allow overrriding the row_height by having a paramater passed in
if len(files) < 1:
return {}
# Make a copy so we don't 'empty' the incoming files var
files = files[:]
if layout_mode == "horizontal":
stripe_height = int(max(f.height for f in files) * scale_factor)
if frame == "slide":
frame = stripe_height / 2
stripe_height += frame * 2
if layout_mode.startswith("vertical"):
stripe_width = int(max(f.width for f in files) * scale_factor)
if frame == "slide":
frame = stripe_width / 2
stripe_width += frame * 2
# If a frame was passed in, adjust the x,y of all items to give them that much spacing as a frame
try:
frame = int(frame)
except ValueError:
frame = 0
# Calculate a new width/height for the files
# based on making them all the same height
for f in files:
if layout_mode == "horizontal":
f.new_height = stripe_height
if f.height != stripe_height:
ratio = float(f.width) / float(f.height)
f.new_width = int(stripe_height * ratio)
else:
f.new_width = f.width
elif layout_mode.startswith("vertical"):
f.new_width = stripe_width
if f.width != stripe_width:
ratio = float(f.height) / float(f.width)
f.new_height = int(stripe_width * ratio)
else:
f.new_height = f.height
else:
f.new_width = f.width
f.new_height = f.height
# Given the files, how many should there be per row
# and how wide should a row be?
# calc_row_width_height
if onestripe:
count_per_stripe = len(files) + 1
else:
count_per_stripe = int(round(math.sqrt(len(files))))
average_width = sum(f.new_width for f in files) / len(files)
average_height = sum(f.new_height for f in files) / len(files)
if layout_mode == "horizontal":
stripe_size = count_per_stripe * \
(average_width + frame * count_per_stripe)
stripe_width = stripe_size
elif layout_mode.startswith("vertical"):
stripe_size = count_per_stripe * \
(average_height + frame * count_per_stripe)
stripe_height = stripe_size
else:
stripe_size = count_per_stripe * average_height
stripe_width = stripe_height = stripe_size
# Make the stripes by calculating an offset for where the
# images should be placed
new_files = []
stripe_idx, stripes = 0, []
x, y = 0, 0
thefile = None
cur_size = 0
if len(files) == 1:
margin = stripe_size + 1
else:
margin = stripe_size * 0.965
idx = 0
while files or thefile:
idx += 1
if not thefile:
thefile = files.pop(0) # just feels wrong to name it 'file'
new_files.append(thefile)
if layout_mode == "horizontal":
if (cur_size + thefile.new_width) < margin:
thefile.x = x
thefile.y = y
thefile.stripe = stripe_idx
x += thefile.new_width
cur_size += thefile.new_width
dontfit = True if thefile.is_break else False
thefile = None
x += frame
else:
dontfit = True
if dontfit and (cur_size > 0):
stripes.append(cur_size)
stripe_idx += 1
cur_size = 0
x = 0
y += stripe_height
y += frame
elif layout_mode.startswith("vertical"):
if (cur_size + thefile.new_height) < margin:
thefile.x = x
thefile.y = y
thefile.stripe = stripe_idx
y += thefile.new_height
cur_size += thefile.new_height
dontfit = True if thefile.is_break else False
thefile = None
y += frame
else:
dontfit = True
if dontfit and (cur_size > 0):
stripes.append(cur_size)
stripe_idx += 1
cur_size = 0
y = 0
x += stripe_width
x += frame
else:
thefile.x = random.randint(0, stripe_width - thefile.width)
thefile.y = random.randint(0, stripe_height - thefile.height)
thefile = None
if len(stripes) < (stripe_idx + 1):
stripes.append(cur_size)
if layout_mode == "horizontal":
# In horizontal layout_mode, each stripe has an actual width that is less than the stripe_width
# To make the layout nicely centered, adjust each x with an offset.
for f in new_files:
offset = (stripe_width - stripes[f.stripe]) / 2
f.x = f.x + offset
canvas_width = stripe_width
canvas_height = stripe_height * len(stripes)
elif layout_mode == "vertical":
for f in new_files:
offset = (stripe_height - stripes[f.stripe]) / 2
f.y = f.y + offset
canvas_width = stripe_width * len(stripes)
canvas_height = stripe_height
elif layout_mode == "verticaltop":
canvas_width = stripe_width * len(stripes)
canvas_height = stripe_height
else:
canvas_width = stripe_width
canvas_height = stripe_height
data = {
"version": 1,
"width": int(canvas_width),
"height": int(canvas_height),
"background_color": background_color,
"images": [],
}
# And save all the modified attributes
for f in new_files:
random_colour = "%x" % random.randint(0, 180)
tmp = {
"filename": f.path,
"fill_style": "#%s" % (random_colour * 3),
"x": int(f.x),
"y": int(f.y),
"width": int(f.new_width),
"height": int(f.new_height),
"metadata": f.metadata and json.loads(f.metadata) or {},
}
data["images"].append(tmp)
return data
def make_bitmap(layout_data, filepath, show_progress=True):
"Given the layout coordinates for @project, generate a bitmap and save it under @filename"
# Make the gigantic bitmap, if it is too large try and scale down the size using horzvert_layout iteratively
if layout_data["width"] > MAX_WIDTH:
raise Exception("Width %s is > %s" % (layout_data["width"], MAX_WIDTH))
if layout_data["height"] > MAX_HEIGHT:
raise Exception("Height %s is > %s" %
(layout_data["height"], MAX_HEIGHT))
size = len(layout_data.get("images", []))
if size < 1:
raise Exception("There are no images?")
width, height = layout_data["width"], layout_data["height"]
if show_progress:
bar = Bar(f"Generating {filepath} of {width} x {height}", max=size)
msgs = []
large = PIL.Image.new(
"RGBA",
(layout_data["width"], layout_data["height"]),
color=layout_data["background_color"],
)
for f in layout_data.get("images", []):
if show_progress:
bar.next()
try:
img = PIL.Image.open(f["filename"])
i_width, i_height = img.size
if i_width != f["width"] or i_height != f["height"]:
img = img.resize((f["width"], f["height"]),
PIL.Image.ANTIALIAS)
except IOError:
msgs.append("Problem with %s" % f["filename"])
continue
if img.mode == "RGBA":
large.paste(img, (f["x"], f["y"]), img)
else:
large.paste(img, (f["x"], f["y"]))
large = large.convert("RGB")
large.save(filepath)
if show_progress:
bar.finish()
return msgs
def read_files_filepaths(filepaths):
files = []
for filepath in filepaths:
if not os.path.exists(filepath):
continue
try:
img = PIL.Image.open(filepath)
except PIL.Image.DecompressionBombError:
print(f"Problem PIL.Image.DecompressionBombError with {filepath}")
continue
width, height = img.size
last_file = BitmapFile(filepath, width, height)
files.append(last_file)
return files
def read_files_file(filename):
files = []
last_file = None
for line in open(filename).readlines():
tmp = line.strip()
if not tmp and last_file:
last_file.is_break = True
files.append(last_file)
return read_files_filepaths(files)
def read_files_directory(path, regex=r".*jpg"):
files = []
for filename in sorted(os.listdir(path)):
if filename.startswith("."):
continue
if regex and not re.match(regex, filename):
continue
filepath = os.path.join(path, filename)
files.append(filepath)
return read_files_filepaths(files)
def render_pdfs(filepaths, cachepath):
for filepath in filepaths:
_, filename = os.path.split(filepath)
filename = filename.replace(".pdf", "")
newfilename = os.path.join(cachepath, filename)
os.system(
f'pdftocairo -r 300 -jpeg -singlefile "{filepath}" "{newfilename}"')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment