Skip to content

Commit

Permalink
Add support for merging pages
Browse files Browse the repository at this point in the history
  • Loading branch information
kbengs authored and jeromerobert committed Apr 23, 2023
1 parent fd64de6 commit 3f3b53f
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 14 deletions.
8 changes: 8 additions & 0 deletions data/menu.ui
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ along with PDF Arranger. If not, see <http://www.gnu.org/licenses/>.
<attribute name="label" translatable="yes">_Split Pages</attribute>
<attribute name="action">win.split</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Merge Pages</attribute>
<attribute name="action">win.merge</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Cu_t</attribute>
<attribute name="action">win.cut</attribute>
Expand Down Expand Up @@ -373,6 +377,10 @@ along with PDF Arranger. If not, see <http://www.gnu.org/licenses/>.
<attribute name="label" translatable="yes">_Split Pages</attribute>
<attribute name="action">win.split</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Merge Pages</attribute>
<attribute name="action">win.merge</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Insert Blan_k Page</attribute>
<attribute name="action">win.insert-blank-page</attribute>
Expand Down
6 changes: 4 additions & 2 deletions pdfarranger/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,15 @@ def layer_support():
return layer_support


def create_blank_page(tmpdir, size):
def create_blank_page(tmpdir, size, npages=1):
"""
Create a temporary PDF file with a single empty page.
Create a temporary PDF file with npages empty pages.
The size is in PDF unit (1/72 of inch).
"""
f, filename = make_tmp_file(tmpdir)
f.add_blank_page(page_size=size)
for __ in range(npages - 1):
f.pages.append(f.pages[0])
f.save(filename)
return filename

Expand Down
89 changes: 89 additions & 0 deletions pdfarranger/pageutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,95 @@ def run_get(self):
return r


class MergePagesDialog(BaseDialog):

def __init__(self, window, size, equal):
super().__init__(title=_("Merge Pages"), parent=window)
self.size = size
self.set_resizable(False)
max_margin = int(((14400 - max(*size)) / 2) * 25.4 / 72)
self.marg = Gtk.SpinButton.new_with_range(0, max_margin, 1)
self.marg.set_activates_default(True)
self.marg.connect('value-changed', self.on_sb_value_changed)
self.cols = Gtk.SpinButton.new_with_range(1, 2, 1)
self.cols.set_activates_default(True)
self.cols.connect('value-changed', self.on_sb_value_changed)
self.rows = Gtk.SpinButton.new_with_range(1, 1, 1)
self.rows.set_activates_default(True)
self.rows.connect('value-changed', self.on_sb_value_changed)

marg_lbl1 = Gtk.Label(_("Margin"), halign=Gtk.Align.START)
marg_lbl2 = Gtk.Label(_("mm"), halign=Gtk.Align.START)
cols_lbl1 = Gtk.Label(_("Columns"), halign=Gtk.Align.START)
rows_lbl1 = Gtk.Label(_("Rows"), halign=Gtk.Align.START)
grid1 = Gtk.Grid(column_spacing=12, row_spacing=12, margin=12, halign=Gtk.Align.CENTER)
grid1.attach(marg_lbl1, 0, 1, 1, 1)
grid1.attach(self.marg, 1, 1, 1, 1)
grid1.attach(marg_lbl2, 2, 1, 1, 1)
grid1.attach(cols_lbl1, 0, 2, 1, 1)
grid1.attach(self.cols, 1, 2, 1, 1)
grid1.attach(rows_lbl1, 0, 3, 1, 1)
grid1.attach(self.rows, 1, 3, 1, 1)
self.vbox.pack_start(grid1, False, False, 8)

self.hor = Gtk.RadioButton(label=_("Horizontal"), group=None)
vrt = Gtk.RadioButton(label=_("Vertical"), group=self.hor)
self.l_r = Gtk.RadioButton(label=_("Left to Right"), group=None)
r_l = Gtk.RadioButton(label=_("Right to Left"), group=self.l_r)
self.t_b = Gtk.RadioButton(label=_("Top to Bottom"), group=None)
b_t = Gtk.RadioButton(label=_("Bottom to Top"), group=self.t_b)
grid2 = Gtk.Grid(column_spacing=6, row_spacing=12, margin=12, halign=Gtk.Align.CENTER)
grid2.attach(self.hor, 0, 1, 1, 1)
grid2.attach(vrt, 1, 1, 1, 1)
grid2.attach(self.l_r, 0, 2, 1, 1)
grid2.attach(r_l, 1, 2, 1, 1)
grid2.attach(self.t_b, 0, 3, 1, 1)
grid2.attach(b_t, 1, 3, 1, 1)
frame1 = Gtk.Frame(label=_("Page Order"), margin=8)
frame1.add(grid2)
self.vbox.pack_start(frame1, False, False, 0)

t = "" if equal else _("Non-uniform page size - using max size")
warn_lbl = Gtk.Label(t, margin=8, wrap=True, width_chars=36, max_width_chars=36)
self.vbox.pack_start(warn_lbl, False, False, 0)
self.size_lbl = Gtk.Label(halign=Gtk.Align.CENTER, margin_bottom=16)
self.vbox.pack_start(self.size_lbl, False, False, 0)
self.show_all()

def size_with_margin(self):
width = self.size[0] + 2 * self.marg.get_value() * 72 / 25.4
height = self.size[1] + 2 * self.marg.get_value() * 72 / 25.4
return width, height

def on_sb_value_changed(self, _button):
width, height = self.size_with_margin()
self.cols.set_range(1, 14400 // width)
self.rows.set_range(1, 14400 // height)
cols = int(self.cols.get_value())
rows = int(self.rows.get_value())
width = str(round(cols * width * 25.4 / 72, 1))
height = str(round(rows * height * 25.4 / 72, 1))
t = _("Merged page size:") + " " + width + _("mm") + " \u00D7 " + height + _("mm")
self.size_lbl.set_label(t)

def run_get(self):
self.cols.set_value(2)
result = self.run()
if result != Gtk.ResponseType.OK:
self.destroy()
return None
cols = int(self.cols.get_value())
rows = int(self.rows.get_value())
range_cols = range(cols) if self.l_r.get_active() else range(cols)[::-1]
range_rows = range(rows) if self.t_b.get_active() else range(rows)[::-1]
if self.hor.get_active():
order = [(row, col) for row in range_rows for col in range_cols]
else:
order = [(row, col) for col in range_cols for row in range_rows]
self.destroy()
return cols, rows, order, self.size_with_margin()


class PastePageLayerDialog():

def __init__(self, app, dpage, lpage, laypos):
Expand Down
82 changes: 70 additions & 12 deletions pdfarranger/pdfarranger.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ def __create_actions(self):
('undo', self.undomanager.undo),
('redo', self.undomanager.redo),
('split', self.split_pages),
('merge', self.merge_pages),
('metadata', self.edit_metadata),
('cut', self.on_action_cut),
('copy', self.on_action_copy),
Expand Down Expand Up @@ -1511,29 +1512,30 @@ def on_action_paste(self, _action, mode, _unknown):
self.update_max_zoom_level()
self.silent_render()
elif pastemode in ['OVERLAY', 'UNDERLAY'] and not data_is_filepaths:
self.paste_as_layer(data, laypos=pastemode)
selection = self.iconview.get_selected_items()
self.paste_as_layer(data, selection, laypos=pastemode)

def paste_as_layer(self, data, laypos):
def paste_as_layer(self, data, destination, laypos, offset_xy=None):
page_stack = []
pageadder = PageAdder(self)
for filename, npage, _basename, angle, scale, crop, layerdata in data:
d = [[filename, npage, angle, scale, laypos, crop, [0] * 4]] + layerdata
page_stack.append(pageadder.get_layerpages(d))
if page_stack is None:
return
selection = self.iconview.get_selected_items()
if not self.is_paste_layer_available(selection):
if not self.is_paste_layer_available(destination):
return
dpage = self.model[selection[-1]][0]
dpage = self.model[destination[-1]][0]
lpage = page_stack[0][0]
offset_xy = pageutils.PastePageLayerDialog(self, dpage, lpage, laypos).get_offset()
if offset_xy is None:
return
self.undomanager.commit("Add Layer")
self.set_unsaved(True)
offset_xy = pageutils.PastePageLayerDialog(self, dpage, lpage, laypos).get_offset()
if offset_xy is None:
return
self.undomanager.commit("Add Layer")
self.set_unsaved(True)

off_x, off_y = offset_xy # Fraction of the page size differance at left & top
for num, row in enumerate(reversed(selection)):
for num, row in enumerate(reversed(destination)):
dpage = self.model[row][0]
layerpage_stack = page_stack[num % len(page_stack)]

Expand Down Expand Up @@ -1576,13 +1578,13 @@ def paste_as_layer(self, data, laypos):
dpage.layerpages.append(lp)

dpage.resample = -1
self.render()
self.silent_render()

def is_paste_layer_available(self, selection):
if len(selection) == 0:
return False
if not layer_support:
msg = _("Pikepdf >= 3 is needed for overlay/underlay support.")
msg = _("Pikepdf >= 3 is needed for overlay/underlay/merge support.")
self.error_message_dialog(msg)
return layer_support

Expand Down Expand Up @@ -2108,6 +2110,7 @@ def iv_selection_changed_event(self, _iconview=None, move_cursor_event=False):
("cut", ne),
("copy", ne),
("split", ne),
("merge", ne),
("select-same-file", ne),
("select-same-format", ne),
("crop-white-borders", ne),
Expand Down Expand Up @@ -2359,6 +2362,61 @@ def split_pages(self, _action, _parameter, _unknown):
self.update_max_zoom_level()
GObject.idle_add(self.render)

def get_size_info(self, selection):
sizes = [self.model[row][0].size_in_points() for row in reversed(selection)]
max_width = max(s[0] for s in sizes)
min_width = min(s[0] for s in sizes)
max_height = max(s[1] for s in sizes)
min_height = min(s[1] for s in sizes)
equal = max_width == min_width and max_height == min_height
return sizes, (max_width, max_height), equal

def merge_pages(self, _action, _parameter, _unknown):
"""Merge selected pages."""
selection = self.iconview.get_selected_items()
if not self.is_paste_layer_available(selection):
return
data = self.copy_pages(add_hash=False)
data = self.deserialize(data.split('\n;\n'))
sizes, max_size, equal = self.get_size_info(selection)
r = pageutils.MergePagesDialog(self.window, max_size, equal).run_get()
if r is None:
return
cols, rows, add_order, size = r
self.undomanager.commit("Merge")
self.set_unsaved(True)
self.clear_selected()

ndpage = selection[-1].get_indices()[0]
before = ndpage < len(self.model)
ref = Gtk.TreeRowReference.new(self.model, selection[-1]) if before else None
wdpage, hdpage = size[0] * cols, size[1] * rows
ndpages = -(len(data) // -(cols * rows))
file = exporter.create_blank_page(self.tmp_dir, (wdpage, hdpage), ndpages)
adder = PageAdder(self)
adder.move(ref, before)
adder.addpages(file)
adder.commit(select_added=False, add_to_undomanager=False)

nlpage = 0
while ndpage < len(self.model) and nlpage < len(data):
for row, col in add_order:
wlpage, hlpage = sizes[nlpage]
wdiff, hdiff = wdpage - wlpage, hdpage - hlpage
off_x = off_y = 0.5
if wdiff != 0:
off_x = (col * wdpage / cols + 0.5 * wdpage / cols - wlpage / 2) / wdiff
if hdiff != 0:
off_y = (row * hdpage / rows + 0.5 * hdpage / rows - hlpage / 2) / hdiff
dest = self.model[ndpage].path
self.paste_as_layer([data[nlpage]], dest, 'OVERLAY', (off_x, off_y))
nlpage += 1
if nlpage > len(data) - 1:
break
ndpage += 1
self.update_iconview_geometry()
self.update_max_zoom_level()

def edit_metadata(self, _action, _parameter, _unknown):
files = [(pdf.copyname, pdf.password) for pdf in self.pdfqueue]
if metadata.edit(self.metadata, files, self.window):
Expand Down

0 comments on commit 3f3b53f

Please sign in to comment.