Created
April 13, 2019 09:40
-
-
Save dberzano/93126ec67dbcde7336ceb257d9b0ce49 to your computer and use it in GitHub Desktop.
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
#!/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