Skip to content

Instantly share code, notes, and snippets.

@dberzano
Created April 13, 2019 09:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dberzano/93126ec67dbcde7336ceb257d9b0ce49 to your computer and use it in GitHub Desktop.
Save dberzano/93126ec67dbcde7336ceb257d9b0ce49 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
"""Generate a LaTeX longtable automatically spanning on multiple pages with images proportionally
scaled appropriately. Adds a scale ruler too. Image names must have a certain format.
"""
from __future__ import print_function
import sys
import os.path
import re
import json
from glob import glob
import colorama
import jinja2
from PIL import Image
####################################################################################################
# All the configuration goes here
####################################################################################################
IMAGE_DIR = "Annexe" # Directory where to find the images
OUTPUT_TEX = "A02-table.tex" # Output file
REF_HEIGHT_MM = 6 # Image with the lowest height will be this tall (other images taller)
WIDTH_LIMIT_MM = 120 # Width of the table
SCALE_RULER_SIZE_MM = 20 # Scale represented by the scale ruler (NOT the actual width on paper)
SCALE_RULER_N_SQUARES = 4 # Number of squares of the scale ruler
SCALE_RULER_HEIGHT_MM = 3 # Actual height on paper, in mm, of the scale ruler
SCALE_RULER_DIST_MM = 10 # Distance between end of table and scale ruler
HORIZ_PAD_MM = 4 # Horizontal cell padding (half on each side of the image)
####################################################################################################
IMAGE_LIMIT = 0 # Limit number of images (for debug; leave it to 0 for all images)
DRAW_BORDERS = False # Whether to draw borders (for debug)
PRINT_DEBUG = False # Print JSON output of generated values (for debug)
####################################################################################################
TEMPLATE = jinja2.Template(r"""
% This file has been automatically generated: do not even bother changing it manually!
{
\setlength\tabcolsep{0pt}
%
\begin{longtable}{ {{vborder}}>{\centering\arraybackslash}m{ {{width_limit_mm}}mm }{{vborder}} }{{hborder}}
% Footer: scale ruler at the end of every page
\begin{tikzpicture}
\draw[draw=none] (0mm,{{scale_ruler_height_mm}}mm) rectangle ({{scale_ruler_size_actual_mm}}mm,{{scale_ruler_height_mm+scale_ruler_dist_mm}}mm); % vspace
\draw[draw=none] (0mm,{{scale_ruler_height_mm}}mm) rectangle ({{scale_ruler_size_actual_mm}}mm,9mm) node[pos=0.5] { \bf\footnotesize {{scale_ruler_label}} }; % label
{%- for _ in range(scale_ruler_n_squares) %}
{{ "\\draw" if loop.index0 % 2 else "\\filldraw" }}[thin,black] ({{loop.index0*scale_ruler_size_actual_mm/scale_ruler_n_squares}}mm,0mm) rectangle ({{(loop.index0+1)*scale_ruler_size_actual_mm/scale_ruler_n_squares}}mm,{{scale_ruler_height_mm}}mm);
{%- endfor %}
\end{tikzpicture}
\endfoot
{%- for row in table %}
\begin{tabular}{ {{vborder}} %
{%- for img in row %}
>{\centering\arraybackslash}m{ {{img["bbox_w_mm"]}}mm }{{vborder}} %
{%- endfor %}
}{{hborder}}
{%- for img in row %}
\includegraphics[width={{img["scaled_w_mm"]}}mm]{{"{"+img["fn"]+"}"}} {{ "\\\\"+hborder if loop.last else "& %" }}
{%- endfor %}
{%- for img in row %}
\bf {{img["index"]}} {{ "\\\\"+hborder if loop.last else "& %" }}
{%- endfor %}
\end{tabular} \\{{hborder}}
{%- endfor %}
\end{longtable}
}
""")
def get_images():
"""Gets images from the given folder matching a certain pattern. Output will be a dictionary
containing the filename (fn), the image index (index), the pixels per millimeter (pxmm),
and width and height (w_px, h_px).
"""
images = []
for jpg in sorted(glob(os.path.join(IMAGE_DIR, "*.jpg"))):
match = re.search(r'Type([0-9]+).*([0-9]+)mm-([0-9]+)px\.jpg$', jpg)
if not match:
fatal("{jpg} does not match the given format".format(jpg=jpg))
img_index = int(match.group(1))
scale_mm = int(match.group(2))
scale_px = int(match.group(3))
with Image.open(jpg) as img:
img_info = {"fn": jpg, "index": img_index, "pxmm": scale_px/scale_mm, "w_px": img.width,
"h_px": img.height}
images.append(img_info)
return images
def gen_table(images):
""" Organize content in a table; number of columns is not fixed, we fit as many objects as we
can on each column.
"""
table = []
cur_row = []
width_left_mm = WIDTH_LIMIT_MM
for img in images[0:IMAGE_LIMIT] if IMAGE_LIMIT else images:
if img["scaled_w_pad_mm"] > WIDTH_LIMIT_MM:
fatal("Image {f} does not fit on a row of {w} mm".format(f=img["fn"], w=WIDTH_LIMIT_MM))
if width_left_mm < img["scaled_w_pad_mm"]:
# New row is needed
table.append(cur_row)
cur_row = []
width_left_mm = WIDTH_LIMIT_MM
cur_row.append(img)
width_left_mm -= img["scaled_w_pad_mm"]
if cur_row:
table.append(cur_row)
# Optimize space on each row: calculate size of bounding boxes to fit uniformly each row
for row in table:
mult = WIDTH_LIMIT_MM / sum([x["scaled_w_pad_mm"] for x in row])
for img in row:
img["bbox_w_mm"] = img["scaled_w_pad_mm"] * mult
return table
def fatal(msg):
"""Print a fatal error in red and exit with nonzero status code.
"""
sys.stderr.write(colorama.Fore.RED)
sys.stderr.write(msg)
sys.stderr.write(colorama.Style.RESET_ALL)
sys.stderr.write("\n")
sys.exit(1)
def info(msg):
"""Print a message in green.
"""
sys.stderr.write(colorama.Fore.GREEN)
sys.stderr.write(msg)
sys.stderr.write(colorama.Style.RESET_ALL)
sys.stderr.write("\n")
def main():
"""Entry point.
"""
images = get_images()
# Find largest pxmm value
max_pxmm = max([x["pxmm"] for x in images])
# Compute new image size
for img in images:
factor = float(max_pxmm) / float(img["pxmm"])
img["scaled_w_px"] = int(float(img["w_px"]) * factor)
img["scaled_h_px"] = int(float(img["h_px"]) * factor)
del factor
# The following scaled height value in pixels corresponds to REF_HEIGHT_MM in mm
min_h = min([x["scaled_h_px"] for x in images])
scale = float(REF_HEIGHT_MM) / float(min_h) # [mm]/[px]
for img in images:
img["scaled_h_mm"] = img["scaled_h_px"] * scale
img["scaled_w_mm"] = img["scaled_w_px"] * scale
img["scaled_w_pad_mm"] = img["scaled_w_mm"] + HORIZ_PAD_MM # width with added padding
del min_h
# Generate the table (and print it on screen)
table = gen_table(images)
if PRINT_DEBUG:
print(json.dumps(table, indent=4))
# Determine the actual size of the ruler on paper
scale_ruler_size_actual_mm = max_pxmm * scale * SCALE_RULER_SIZE_MM
# Write output file
with open(OUTPUT_TEX, "w") as outf:
outf.write(TEMPLATE.render(table=table,
width_limit_mm=WIDTH_LIMIT_MM,
scale_ruler_label="%.0f cm" % (float(SCALE_RULER_SIZE_MM)/10.0),
scale_ruler_height_mm=SCALE_RULER_HEIGHT_MM,
scale_ruler_n_squares=SCALE_RULER_N_SQUARES,
scale_ruler_size_actual_mm=scale_ruler_size_actual_mm,
scale_ruler_dist_mm=SCALE_RULER_DIST_MM,
hborder="\\hline" if DRAW_BORDERS else "",
vborder="|" if DRAW_BORDERS else ""))
info("Output has been generated to {f}".format(f=OUTPUT_TEX))
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment