add complex layout strategy

This commit is contained in:
Johannes 'josch' Schauer 2019-07-17 11:38:02 +02:00
parent b5304041ca
commit ba2d6d27e4
Signed by: josch
GPG key ID: F2CBA5C78FBD83E1
2 changed files with 507 additions and 116 deletions

View file

@ -27,12 +27,12 @@ VERSION = "0.1"
PAGE_SIZES = OrderedDict( PAGE_SIZES = OrderedDict(
[ [
("custom", (None, None)), ("custom", (None, None)),
("A0 (84.1 cm × 118.9 cm)", (841, 1189)), ("A0 (841 mm × 1189 mm)", (841, 1189)),
("A1 (59.4 cm × 84.1 cm)", (594, 841)), ("A1 (594 mm × 841 mm)", (594, 841)),
("A2 (42.0 cm × 59.4 cm)", (420, 594)), ("A2 (420 mm × 594 mm)", (420, 594)),
("A3 (29.7 cm × 42.0 cm)", (297, 420)), ("A3 (297 mm × 420 mm)", (297, 420)),
("A4 (21.0 cm × 29.7 cm)", (210, 297)), ("A4 (210 mm × 297 mm)", (210, 297)),
("A5 (14.8 cm × 21.0 cm)", (148, 210)), ("A5 (148 mm × 210 mm)", (148, 210)),
("Letter (8.5 in × 11 in)", (215.9, 279.4)), ("Letter (8.5 in × 11 in)", (215.9, 279.4)),
("Legal (8.5 in × 14 in)", (215.9, 355.6)), ("Legal (8.5 in × 14 in)", (215.9, 355.6)),
("Tabloid (11 in × 17 in)", (279.4, 431.8)), ("Tabloid (11 in × 17 in)", (279.4, 431.8)),
@ -60,6 +60,189 @@ class LayoutNotComputedException(PlakativException):
pass pass
def simple_cover(n, m, x, y):
pages_x_portrait = math.ceil(n / x)
pages_y_portrait = math.ceil(m / y)
pages_x_landscape = math.ceil(n / y)
pages_y_landscape = math.ceil(m / x)
if pages_x_portrait * pages_y_portrait <= pages_x_landscape * pages_y_landscape:
pages_x = pages_x_portrait
pages_y = pages_y_portrait
portrait = True
size = pages_x * x, pages_y * y
else:
pages_x = pages_x_landscape
pages_y = pages_y_landscape
portrait = False
size = pages_x * y, pages_y * x
config = list()
for py in range(pages_y):
for px in range(pages_x):
if portrait:
posx = px * x
posy = py * y
else:
posx = px * y
posy = py * x
config.append((posx, posy, portrait))
return config, size
# the function complex_cover() is based on a heuristic proposed by
# stackoverflow user m69 https://stackoverflow.com/users/4907604/m69 as a reply
# to this question https://stackoverflow.com/questions/39306507
#
# In addition to computing the number of required rectangles it also returns
# the position of the rectangles.
#
# In contrast to the solution by m69 this algorithm mandates at least one page
# in each corner of the result. This means that the minimum solution size is
# four. The advantage is, that the layout computed by this algorithm will
# always be completely be inside the poster without leaving the poster area.
# This in turn will make assembling the poster easier.
#
# This heuristic is not optimal. We always use a regular simple cover for the
# hole in the center (if it exists) but there are situations where the hole in
# the center should be covered by a complex cover instead. We do not consider
# this improvement because:
# - it's required very seldom
# - it makes the resulting layout more complicated to glue together
# - there is no proof that the improved version is optimal either
# - we save some cpu cycles
def complex_cover(n, m, x, y):
# each tuple-entry represents one of the corners of the poster
# upper-left, upper-right, lower-right, lower-left
portrait = (
(True, True, True, False),
(True, True, False, False),
(True, False, True, False),
(True, False, False, True),
(True, False, False, False),
)
X = lambda r, d: x if portrait[r][d] else y
Y = lambda r, d: y if portrait[r][d] else x
if x == y:
# if page sizes are square, only one rotation has to be checked
num_rotations = 1
elif n == m:
# if the poster size is a square, rotation 4 and 5 (which are itself
# just rotations of rotations 2 and 1, respectively) do not need to be
# checked
num_rotations = 3
else:
num_rotations = 5
minimum = math.ceil((n * m) / (x * y))
config, _ = simple_cover(n, m, x, y)
cover = len(config)
if cover == minimum:
return config
for r in range(num_rotations):
# w0 -> width of upper left corner pages
for w0 in range(1, math.ceil(n / X(r, 0))):
# w1 -> width of upper right corner pages
w1 = math.ceil((n - w0 * X(r, 0)) / X(r, 1))
if w1 < 0:
w1 = 0
# h0 -> height of upper left corner pages
for h0 in range(1, math.ceil(m / Y(r, 0))):
# h3 -> height of lower left corner pages
h3 = math.ceil((m - h0 * Y(r, 0)) / Y(r, 3))
if h3 < 0:
h3 = 0
# w2 -> width of lower right corner pages
for w2 in range(1, math.ceil(n / X(r, 2))):
# w3 -> width of lower left corner pages
w3 = math.ceil((n - w2 * X(r, 2)) / X(r, 3))
if w3 < 0:
w3 = 0
# h2 -> height of lower right corner pages
for h2 in range(1, math.ceil(m / Y(r, 2))):
# h1 -> height of upper right corner pages
h1 = math.ceil((m - h2 * Y(r, 2)) / Y(r, 1))
if h1 < 0:
h1 = 0
newconfig = list()
# upper-left (w0,h0)
for i in range(w0):
for j in range(h0):
newconfig.append(
(i * X(r, 0), j * Y(r, 0), portrait[r][0])
)
# upper-right (w1,h1)
for i in range(w1):
for j in range(h1):
newconfig.append(
(
n - w1 * X(r, 1) + i * X(r, 1),
j * Y(r, 1),
portrait[r][1],
)
)
# lower-right (w2,h2)
for i in range(w2):
for j in range(h2):
newconfig.append(
(
n - w2 * X(r, 2) + i * X(r, 2),
m - h2 * Y(r, 2) + j * Y(r, 2),
portrait[r][2],
)
)
# lower-left (w3,h3)
for i in range(w3):
for j in range(h3):
newconfig.append(
(
i * X(r, 3),
m - h3 * Y(r, 3) + j * Y(r, 3),
portrait[r][3],
)
)
# if neither rectangle 0 overlaps with rectangle 2 nor
# does rectangle 1 overlap with rectangle 3 in the center,
# then a center cover has to be added
X4 = n - w0 * X(r, 0) - w2 * X(r, 2)
Y4 = m - h1 * Y(r, 1) - h3 * Y(r, 3)
simple_config = []
if X4 > 0 and Y4 > 0:
simple_config, (sx, sy) = simple_cover(X4, Y4, x, y)
# shift the results such that they are in the center
for (cx, cy, p) in simple_config:
newconfig.append(
(
w0 * X(r, 0) + (X4 - sx) / 2 + cx,
h1 * Y(r, 1) + (Y4 - sy) / 2 + cy,
p,
)
)
else:
X4 = n - w1 * X(r, 1) - w3 * X(r, 3)
Y4 = m - h0 * Y(r, 0) - h2 * Y(r, 2)
if X4 > 0 and Y4 > 0:
simple_config, (sx, sy) = simple_cover(X4, Y4, x, y)
# shift the results such that they are in the center
for (cx, cy, p) in simple_config:
newconfig.append(
(
w3 * X(r, 3) + (X4 - sx) / 2 + cx,
h0 * Y(r, 0) + (Y4 - sy) / 2 + cy,
p,
)
)
total = len(newconfig)
# shortcut to cut computation short in case a
# solution with the minimal possible number of
# pages is found
if total == minimum:
return newconfig
if total < cover:
cover = total
config = newconfig
return config
class Plakativ: class Plakativ:
def __init__(self, infile, pagenr=0): def __init__(self, infile, pagenr=0):
self.doc = fitz.open(infile) self.doc = fitz.open(infile)
@ -102,6 +285,7 @@ class Plakativ:
npages=None, npages=None,
pagesize=(210, 297), pagesize=(210, 297),
border=(0, 0, 0, 0), border=(0, 0, 0, 0),
strategy="simple",
): ):
border_top, border_right, border_bottom, border_left = border border_top, border_right, border_bottom, border_left = border
@ -136,26 +320,6 @@ class Plakativ:
poster_height = math.sqrt(area * inpage_height / inpage_width) poster_height = math.sqrt(area * inpage_height / inpage_width)
else: else:
raise Exception("unsupported mode: %s" % mode) raise Exception("unsupported mode: %s" % mode)
pages_x_portrait = math.ceil(poster_width / printable_width)
pages_y_portrait = math.ceil(poster_height / printable_height)
pages_x_landscape = math.ceil(poster_width / printable_height)
pages_y_landscape = math.ceil(poster_height / printable_width)
portrait = True
if (
pages_x_portrait * pages_y_portrait
> pages_x_landscape * pages_y_landscape
):
portrait = False
if portrait:
pages_x = pages_x_portrait
pages_y = pages_y_portrait
else:
pages_x = pages_x_landscape
pages_y = pages_y_landscape
elif mode == "npages": elif mode == "npages":
# stupid bruteforce algorithm to determine the largest printable # stupid bruteforce algorithm to determine the largest printable
# postersize with N pages # postersize with N pages
@ -178,7 +342,7 @@ class Plakativ:
if area_portrait > best_area: if area_portrait > best_area:
best_area = area_portrait best_area = area_portrait
best = (x, y, True, poster_width, poster_height) best = (poster_width, poster_height)
width_landscape = x * printable_height width_landscape = x * printable_height
height_landscape = y * printable_width height_landscape = y * printable_width
@ -193,12 +357,83 @@ class Plakativ:
if area_landscape > best_area: if area_landscape > best_area:
best_area = area_landscape best_area = area_landscape
best = (x, y, False, poster_width, poster_height) best = (poster_width, poster_height)
poster_width, poster_height = best
if strategy == "complex":
# bisect poster sizes until we find the largest size that can
# be printed given the available number of pages
# we already know the maximum size for a solution utilizing the
# simple cover algorithm, so this will be the minimum known
# poster size
min_area_mult = (poster_width * poster_height) / (
inpage_width * inpage_height
)
# to avoid floating point errors later
min_area_mult *= 0.9999
min_area_npages = len(
complex_cover(
poster_width, poster_height, printable_width, printable_height
)
)
# the maximum possible size is a poster of the area created by
# multiplying the individual page areas by the maximum number
# of pages available
max_area_mult = (npages * printable_width * printable_height) / (
inpage_width * inpage_height
)
max_area_npages = len(
complex_cover(
math.sqrt(max_area_mult) * inpage_width,
math.sqrt(max_area_mult) * inpage_height,
printable_width,
printable_height,
)
)
while True:
if abs(min_area_mult - max_area_mult) < 0.001:
break
new_area_mult = (min_area_mult + max_area_mult) / 2
new_area_npages = len(
complex_cover(
math.sqrt(new_area_mult) * inpage_width,
math.sqrt(new_area_mult) * inpage_height,
printable_width,
printable_height,
)
)
if new_area_npages > npages:
max_area_mult = new_area_mult
else:
min_area_mult = new_area_mult
poster_width = inpage_width * math.sqrt(min_area_mult)
poster_height = inpage_height * math.sqrt(min_area_mult)
pages_x, pages_y, portrait, poster_width, poster_height = best
else: else:
raise Exception("unsupported mode: %s" % mode) raise Exception("unsupported mode: %s" % mode)
pages_x_portrait = math.ceil(poster_width / printable_width)
pages_y_portrait = math.ceil(poster_height / printable_height)
pages_x_landscape = math.ceil(poster_width / printable_height)
pages_y_landscape = math.ceil(poster_height / printable_width)
portrait = True
if pages_x_portrait * pages_y_portrait > pages_x_landscape * pages_y_landscape:
portrait = False
if portrait:
pages_x = pages_x_portrait
pages_y = pages_y_portrait
else:
pages_x = pages_x_landscape
pages_y = pages_y_landscape
# size of the bounding box of all pages after they have been glued together # size of the bounding box of all pages after they have been glued together
if portrait: if portrait:
self.layout["overallsize"] = ( self.layout["overallsize"] = (
@ -211,9 +446,6 @@ class Plakativ:
pages_y * printable_width + (border_right + border_left), pages_y * printable_width + (border_right + border_left),
) )
# size of output poster is always proportional to size of input page
self.layout["postersize"] = poster_width, poster_height
# position of the poster relative to upper left corner of layout["overallsize"] # position of the poster relative to upper left corner of layout["overallsize"]
if portrait: if portrait:
self.layout["posterpos"] = ( self.layout["posterpos"] = (
@ -226,24 +458,84 @@ class Plakativ:
border_right + (pages_y * printable_width - poster_height) / 2, border_right + (pages_y * printable_width - poster_height) / 2,
) )
# positions are relative to upper left corner of poster # positions are relative to self.layout["posterpos"]
self.layout["positions"] = [] self.layout["positions"] = []
for y in range(pages_y): for y in range(pages_y):
for x in range(pages_x): for x in range(pages_x):
if portrait: if portrait:
posx = x * printable_width posx = (
posy = y * printable_height x * printable_width
- (pages_x * printable_width - poster_width) / 2
)
posy = (
y * printable_height
- (pages_y * printable_height - poster_height) / 2
)
else: else:
posx = x * printable_height posx = (
posy = y * printable_width x * printable_height
- (pages_x * printable_height - poster_width) / 2
)
posy = (
y * printable_width
- (pages_y * printable_width - poster_height) / 2
)
self.layout["positions"].append((posx, posy, portrait)) self.layout["positions"].append((posx, posy, portrait))
if strategy == "complex":
positions_complex = complex_cover(
poster_width, poster_height, printable_width, printable_height
)
if len(positions_complex) < len(self.layout["positions"]):
self.layout["positions"] = positions_complex
# figure out the borders around the final poster by analyzing
# the computed positions and storing the largest border size in
# each dimension
poster_top = poster_right = poster_bottom = poster_left = 0
for (posx, posy, p) in self.layout["positions"]:
if p:
top = posy - border_top
if top < 0 and -top > poster_top:
poster_top = -top
right = posx + printable_width + border_right - poster_width
if right > 0 and right > poster_right:
poster_right = right
bottom = posy + printable_height + border_bottom - poster_height
if bottom > 0 and bottom > poster_bottom:
poster_bottom = bottom
left = posx - border_left
if left < 0 and -left > poster_left:
poster_left = -left
else:
top = posy - border_left
if top < 0 and -top > poster_top:
poster_top = -top
right = posx + printable_height + border_top - poster_width
if right > 0 and right > poster_right:
poster_right = right
bottom = posy + printable_width + border_right - poster_height
if bottom > 0 and bottom > poster_bottom:
poster_bottom = bottom
left = posx - border_bottom
if left < 0 and -left > poster_left:
poster_left = -left
self.layout["overallsize"] = (
poster_width + poster_left + poster_right,
poster_height + poster_top + poster_bottom,
)
self.layout["posterpos"] = (poster_left, poster_top)
# size of output poster is always proportional to size of input page
self.layout["postersize"] = poster_width, poster_height
if mode == "size": if mode == "size":
mult = (poster_width * poster_height) / (inpage_width * inpage_height) mult = (poster_width * poster_height) / (inpage_width * inpage_height)
npages = pages_x * pages_y npages = len(self.layout["positions"])
elif mode == "mult": elif mode == "mult":
postersize = poster_width, poster_height postersize = poster_width, poster_height
npages = pages_x * pages_y npages = len(self.layout["positions"])
elif mode == "npages": elif mode == "npages":
postersize = poster_width, poster_height postersize = poster_width, poster_height
mult = (poster_width * poster_height) / (inpage_width * inpage_height) mult = (poster_width * poster_height) / (inpage_width * inpage_height)
@ -271,16 +563,18 @@ class Plakativ:
-1, width=page_width, height=page_height # insert after last page -1, width=page_width, height=page_height # insert after last page
) )
target_x = x - self.layout["posterpos"][0]
target_y = y - self.layout["posterpos"][1]
target_xoffset = 0
target_yoffset = 0
if portrait: if portrait:
target_x = x - self.layout["border_left"]
target_y = y - self.layout["border_top"]
target_width = self.layout["output_pagesize"][0] target_width = self.layout["output_pagesize"][0]
target_height = self.layout["output_pagesize"][1] target_height = self.layout["output_pagesize"][1]
else: else:
target_x = x - self.layout["border_bottom"]
target_y = y - self.layout["border_left"]
target_width = self.layout["output_pagesize"][1] target_width = self.layout["output_pagesize"][1]
target_height = self.layout["output_pagesize"][0] target_height = self.layout["output_pagesize"][0]
target_xoffset = 0
target_yoffset = 0
if target_x < 0: if target_x < 0:
target_xoffset = -target_x target_xoffset = -target_x
target_width += target_x target_width += target_x
@ -487,31 +781,11 @@ class Application(tkinter.Frame):
if hasattr(self, "plakativ"): if hasattr(self, "plakativ"):
self.postersize.callback = self.on_postersize self.postersize.callback = self.on_postersize
layouter_group = tkinter.LabelFrame(frame1.interior, text="Layouter") self.layouter = LayouterWidget(frame1.interior)
layouter_group.pack(fill=tkinter.X) self.layouter.pack(fill=tkinter.X)
self.layouter.set("simple")
self.layouter = tkinter.IntVar() if hasattr(self, "plakativ"):
self.layouter.set(1) self.postersize.callback = self.on_layouter
layouter1 = tkinter.Radiobutton(
layouter_group, text="Simple", variable=self.layouter, value=1
)
layouter1.pack(anchor=tkinter.W)
layouter2 = tkinter.Radiobutton(
layouter_group,
text="Advanced",
variable=self.layouter,
value=2,
state=tkinter.DISABLED,
)
layouter2.pack(anchor=tkinter.W)
layouter3 = tkinter.Radiobutton(
layouter_group,
text="Complex",
variable=self.layouter,
value=3,
state=tkinter.DISABLED,
)
layouter3.pack(anchor=tkinter.W)
output_group = tkinter.LabelFrame(frame1.interior, text="Output options") output_group = tkinter.LabelFrame(frame1.interior, text="Output options")
output_group.pack(fill=tkinter.X) output_group.pack(fill=tkinter.X)
@ -522,6 +796,12 @@ class Application(tkinter.Frame):
tkinter.Checkbutton( tkinter.Checkbutton(
output_group, text="Print poster border", state=tkinter.DISABLED output_group, text="Print poster border", state=tkinter.DISABLED
).pack(anchor=tkinter.W) ).pack(anchor=tkinter.W)
tkinter.Checkbutton(
output_group, text="Print page number", state=tkinter.DISABLED
).pack(anchor=tkinter.W)
tkinter.Checkbutton(
output_group, text="Print layout cover page", state=tkinter.DISABLED
).pack(anchor=tkinter.W)
option_group = tkinter.LabelFrame(frame1.interior, text="Program options") option_group = tkinter.LabelFrame(frame1.interior, text="Program options")
option_group.pack(fill=tkinter.X) option_group.pack(fill=tkinter.X)
@ -565,9 +845,10 @@ class Application(tkinter.Frame):
pagenum, _ = value pagenum, _ = value
mode, (custom_size, size), mult, npages = self.postersize.value mode, (custom_size, size), mult, npages = self.postersize.value
bordersize = self.bordersize.value bordersize = self.bordersize.value
strategy = self.layouter.value
self.plakativ.set_input_pagenr(pagenum - 1) self.plakativ.set_input_pagenr(pagenum - 1)
size, mult, npages = self.plakativ.compute_layout( size, mult, npages = self.plakativ.compute_layout(
mode, size, mult, npages, pagesize, bordersize mode, size, mult, npages, pagesize, bordersize, strategy
) )
self.postersize.set(mode, (custom_size, size), mult, npages) self.postersize.set(mode, (custom_size, size), mult, npages)
self.draw() self.draw()
@ -579,9 +860,10 @@ class Application(tkinter.Frame):
pagenum, _ = self.input.value pagenum, _ = self.input.value
mode, (custom_size, size), mult, npages = self.postersize.value mode, (custom_size, size), mult, npages = self.postersize.value
bordersize = self.bordersize.value bordersize = self.bordersize.value
strategy = self.layouter.value
self.plakativ.set_input_pagenr(pagenum - 1) self.plakativ.set_input_pagenr(pagenum - 1)
size, mult, npages = self.plakativ.compute_layout( size, mult, npages = self.plakativ.compute_layout(
mode, size, mult, npages, pagesize, bordersize mode, size, mult, npages, pagesize, bordersize, strategy
) )
self.postersize.set(mode, (custom_size, size), mult, npages) self.postersize.set(mode, (custom_size, size), mult, npages)
self.draw() self.draw()
@ -590,9 +872,10 @@ class Application(tkinter.Frame):
_, pagesize = self.pagesize.value _, pagesize = self.pagesize.value
pagenum, _ = self.input.value pagenum, _ = self.input.value
mode, (custom_size, size), mult, npages = self.postersize.value mode, (custom_size, size), mult, npages = self.postersize.value
strategy = self.layouter.value
self.plakativ.set_input_pagenr(pagenum - 1) self.plakativ.set_input_pagenr(pagenum - 1)
size, mult, npages = self.plakativ.compute_layout( size, mult, npages = self.plakativ.compute_layout(
mode, size, mult, npages, pagesize, value mode, size, mult, npages, pagesize, value, strategy
) )
self.postersize.set(mode, (custom_size, size), mult, npages) self.postersize.set(mode, (custom_size, size), mult, npages)
self.draw() self.draw()
@ -602,13 +885,26 @@ class Application(tkinter.Frame):
pagenum, _ = self.input.value pagenum, _ = self.input.value
_, pagesize = self.pagesize.value _, pagesize = self.pagesize.value
border = self.bordersize.value border = self.bordersize.value
strategy = self.layouter.value
self.plakativ.set_input_pagenr(pagenum - 1) self.plakativ.set_input_pagenr(pagenum - 1)
size, mult, npages = self.plakativ.compute_layout( size, mult, npages = self.plakativ.compute_layout(
mode, size, mult, npages, pagesize, border mode, size, mult, npages, pagesize, border, strategy
) )
self.draw() self.draw()
return (mode, (custom_size, size), mult, npages) return (mode, (custom_size, size), mult, npages)
def on_layouter(self, value):
_, pagesize = self.pagesize.value
pagenum, _ = self.input.value
mode, (custom_size, size), mult, npages = self.postersize.value
border = self.bordersize.value
self.plakativ.set_input_pagenr(pagenum - 1)
size, mult, npages = self.plakativ.compute_layout(
mode, size, mult, npages, pagesize, border, value
)
self.postersize.set(mode, (custom_size, size), mult, npages)
self.draw()
def on_resize(self, event): def on_resize(self, event):
self.canvas_size = (event.width, event.height) self.canvas_size = (event.width, event.height)
self.draw() self.draw()
@ -666,48 +962,59 @@ class Application(tkinter.Frame):
) )
self.canvas.image = tkimg self.canvas.image = tkimg
# self.canvas.create_text(
# self.canvas_size[0] / 2,
# self.canvas_size[1] / 2,
# text="%d" % len(self.plakativ.layout["positions"]),
# fill="grey",
# font=("TkDefaultFont", 40),
# anchor=tkinter.CENTER,
# )
# draw rectangles # draw rectangles
# TODO: also draw numbers indicating the page number
for (x, y, portrait) in self.plakativ.layout["positions"]: for (x, y, portrait) in self.plakativ.layout["positions"]:
x0 = ( x0 = (x + self.plakativ.layout["posterpos"][0]) * zoom_1 + (
x * zoom_1 self.canvas_size[0] - zoom_1 * self.plakativ.layout["overallsize"][0]
+ ( ) / 2
self.canvas_size[0] y0 = (y + self.plakativ.layout["posterpos"][1]) * zoom_1 + (
- zoom_1 * self.plakativ.layout["overallsize"][0] self.canvas_size[1] - zoom_1 * self.plakativ.layout["overallsize"][1]
) ) / 2
/ 2
)
y0 = (
y * zoom_1
+ (
self.canvas_size[1]
- zoom_1 * self.plakativ.layout["overallsize"][1]
)
/ 2
)
if portrait: if portrait:
x1 = x0 + self.plakativ.layout["output_pagesize"][0] * zoom_1 page_width = self.plakativ.layout["output_pagesize"][0] * zoom_1
y1 = y0 + self.plakativ.layout["output_pagesize"][1] * zoom_1 page_height = self.plakativ.layout["output_pagesize"][1] * zoom_1
top = self.plakativ.layout["border_top"] * zoom_1
right = self.plakativ.layout["border_right"] * zoom_1
bottom = self.plakativ.layout["border_bottom"] * zoom_1
left = self.plakativ.layout["border_left"] * zoom_1
else: else:
x1 = x0 + self.plakativ.layout["output_pagesize"][1] * zoom_1 # page is rotated 90 degrees clockwise
y1 = y0 + self.plakativ.layout["output_pagesize"][0] * zoom_1 page_width = self.plakativ.layout["output_pagesize"][1] * zoom_1
self.canvas.create_rectangle(x0, y0, x1, y1, outline="red") page_height = self.plakativ.layout["output_pagesize"][0] * zoom_1
if portrait: top = self.plakativ.layout["border_left"] * zoom_1
top = self.plakativ.layout["border_top"] right = self.plakativ.layout["border_top"] * zoom_1
right = self.plakativ.layout["border_right"] bottom = self.plakativ.layout["border_right"] * zoom_1
bottom = self.plakativ.layout["border_bottom"] left = self.plakativ.layout["border_bottom"] * zoom_1
left = self.plakativ.layout["border_left"] # inner rectangle
else:
top = self.plakativ.layout["border_left"]
right = self.plakativ.layout["border_top"]
bottom = self.plakativ.layout["border_right"]
left = self.plakativ.layout["border_bottom"]
self.canvas.create_rectangle( self.canvas.create_rectangle(
x0 + zoom_1 * left, x0,
y0 + zoom_1 * top, y0,
x1 - zoom_1 * right, x0 + page_width - left - right,
y1 - zoom_1 * bottom, y0 + page_height - top - bottom,
outline="blue", outline="blue",
) )
# outer rectangle
self.canvas.create_rectangle(
x0 - left,
y0 - top,
x0 - left + page_width,
y0 - top + page_height,
outline="red",
)
# filename = "out_%03d.ps" % len(self.plakativ.layout["positions"])
# self.canvas.postscript(file=filename)
# print("saved ", filename)
def on_open_button(self): def on_open_button(self):
filename = tkinter.filedialog.askopenfilename( filename = tkinter.filedialog.askopenfilename(
@ -725,8 +1032,9 @@ class Application(tkinter.Frame):
mode, (custom_size, size), mult, npages = self.postersize.value mode, (custom_size, size), mult, npages = self.postersize.value
_, pagesize = self.pagesize.value _, pagesize = self.pagesize.value
border = self.bordersize.value border = self.bordersize.value
strategy = self.layouter.value
size, mult, npages = self.plakativ.compute_layout( size, mult, npages = self.plakativ.compute_layout(
mode, size, mult, npages, pagesize, border mode, size, mult, npages, pagesize, border, strategy
) )
# update input widget # update input widget
width, height = self.plakativ.get_input_page_size() width, height = self.plakativ.get_input_page_size()
@ -748,6 +1056,7 @@ class Application(tkinter.Frame):
self.pagesize.callback = self.on_pagesize self.pagesize.callback = self.on_pagesize
self.bordersize.callback = self.on_bordersize self.bordersize.callback = self.on_bordersize
self.postersize.callback = self.on_postersize self.postersize.callback = self.on_postersize
self.layouter.callback = self.on_layouter
def on_save_button(self): def on_save_button(self):
base, ext = os.path.splitext(os.path.basename(self.filename)) base, ext = os.path.splitext(os.path.basename(self.filename))
@ -764,6 +1073,50 @@ class Application(tkinter.Frame):
self.plakativ.render(filename) self.plakativ.render(filename)
class LayouterWidget(tkinter.LabelFrame):
def __init__(self, parent, *args, **kw):
tkinter.LabelFrame.__init__(self, parent, text="Layouter", *args, **kw)
self.callback = None
self.variables = {"strategy": tkinter.StringVar()}
def callback(varname, idx, op):
assert op == "w"
self.on_strategy(self.variables["strategy"].get())
self.variables["strategy"].trace("w", callback)
layouter1 = tkinter.Radiobutton(
self, text="Simple", variable=self.variables["strategy"], value="simple"
)
layouter1.pack(anchor=tkinter.W)
layouter3 = tkinter.Radiobutton(
self, text="Complex", variable=self.variables["strategy"], value="complex"
)
layouter3.pack(anchor=tkinter.W)
def on_strategy(self, value):
if getattr(self, "value", None) is None:
return
strategy = self.value
self.set(value)
def set(self, strategy):
# before setting self.value, check if the effective value is different
# from before or otherwise we do not need to execute the callback in
# the end
state_changed = True
if getattr(self, "value", None) is not None:
state_changed = self.value != strategy
# execute callback if necessary
if state_changed and self.callback is not None:
pagesize = self.callback(strategy)
self.value = strategy
if self.variables["strategy"].get() != strategy:
self.variables["strategy"].set(strategy)
class InputWidget(tkinter.LabelFrame): class InputWidget(tkinter.LabelFrame):
def __init__(self, parent, *args, **kw): def __init__(self, parent, *args, **kw):
tkinter.LabelFrame.__init__(self, parent, text="Input properties", *args, **kw) tkinter.LabelFrame.__init__(self, parent, text="Input properties", *args, **kw)
@ -1123,7 +1476,7 @@ class PostersizeWidget(tkinter.LabelFrame):
tkinter.Radiobutton( tkinter.Radiobutton(
self, self,
text="Factor of input page size", text="Factor of input page area",
variable=self.variables["radio"], variable=self.variables["radio"],
value="mult", value="mult",
state=tkinter.DISABLED, state=tkinter.DISABLED,
@ -1274,9 +1627,10 @@ def compute_layout(
pagenr=0, pagenr=0,
pagesize=(210, 297), pagesize=(210, 297),
border=(0, 0, 0, 0), border=(0, 0, 0, 0),
strategy="simple",
): ):
plakativ = Plakativ(infile, pagenr) plakativ = Plakativ(infile, pagenr)
plakativ.compute_layout(mode, size, mult, npages, pagesize, border) plakativ.compute_layout(mode, size, mult, npages, pagesize, border, strategy)
plakativ.render(outfile) plakativ.render(outfile)
@ -1324,4 +1678,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()
__all__ = ["Plakativ", "compute_layout"] __all__ = ["Plakativ", "compute_layout", "simple_cover", "complex_cover"]

47
test.py
View file

@ -55,16 +55,18 @@ _formats = {
"dina4_landscape": (297, 210), "dina4_landscape": (297, 210),
"dina3_portrait": (297, 420), "dina3_portrait": (297, 420),
"dina3_landscape": (420, 297), "dina3_landscape": (420, 297),
"dina1_portrait": (594, 841),
} }
@pytest.mark.parametrize( @pytest.mark.parametrize(
"postersize,input_pagesize,output_pagesize,expected", "postersize,input_pagesize,output_pagesize,strategy,expected",
[ [
( (
_formats["dina3_portrait"], _formats["dina3_portrait"],
_formats["dina4_portrait"], _formats["dina4_portrait"],
None, _formats["dina4_portrait"],
"simple",
[ [
( (
["0", "380.8549", "337.72779", "841.8898"], ["0", "380.8549", "337.72779", "841.8898"],
@ -87,7 +89,8 @@ _formats = {
( (
_formats["dina3_landscape"], _formats["dina3_landscape"],
_formats["dina4_landscape"], _formats["dina4_landscape"],
None, _formats["dina4_portrait"],
"simple",
[ [
( (
["0", "257.5478", "461.03489", "595.2756"], ["0", "257.5478", "461.03489", "595.2756"],
@ -107,9 +110,41 @@ _formats = {
), ),
], ],
), ),
(
_formats["dina1_portrait"],
_formats["dina4_landscape"],
_formats["dina4_portrait"],
"complex",
[
(
['0', '202.67716', '269.29136', '595.2756'],
['1.9999999', '0', '0', '1.9999999', '56.692934', '-405.35429'],
),
(
['212.59844', '202.67716', '510.23625', '595.2756'],
['1.9999999', '0', '0', '1.9999999', '-425.1968', '-405.35429'],
),
(
['449.29136', '325.98423', '841.8898', '595.2756'],
['1.9999998', '0', '0', '1.9999998', '-898.58267', '-651.9683'],
),
(
['331.65354', '0', '629.2913', '392.59846'],
['1.9999999', '0', '0', '1.9999999', '-663.307', '56.692934'],
),
(
['572.59848', '0', '841.8898', '392.59846'],
['1.9999999', '0', '0', '1.9999999', '-1145.1968', '56.692934'],
),
(
['0', '0', '392.59843', '269.29136'],
['2', '0', '0', '2', '56.692934', '56.69287'],
),
],
),
], ],
) )
def test_cases(postersize, input_pagesize, output_pagesize, expected): def test_cases(postersize, input_pagesize, output_pagesize, strategy, expected):
width = mm_to_pt(input_pagesize[0]) width = mm_to_pt(input_pagesize[0])
height = mm_to_pt(input_pagesize[1]) height = mm_to_pt(input_pagesize[1])
@ -140,7 +175,9 @@ def test_cases(postersize, input_pagesize, output_pagesize, expected):
fd, outfile = tempfile.mkstemp(prefix="plakativ") fd, outfile = tempfile.mkstemp(prefix="plakativ")
os.close(fd) os.close(fd)
plakativ.compute_layout(infile, outfile, mode="size", size=postersize, border=(20, 20, 20, 20)) plakativ.compute_layout(
infile, outfile, mode="size", size=postersize, pagesize=output_pagesize, border=(20, 20, 20, 20), strategy=strategy
)
os.unlink(infile) os.unlink(infile)
reader = pdfrw.PdfReader(outfile) reader = pdfrw.PdfReader(outfile)