Skip to content

Commit

Permalink
Create a booklet from multiple pages
Browse files Browse the repository at this point in the history
Closes #500
  • Loading branch information
nicos68 committed Aug 9, 2021
1 parent 29dcd79 commit b16e346
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 10 deletions.
8 changes: 8 additions & 0 deletions data/menu.ui
Expand Up @@ -141,6 +141,10 @@ along with PDF Arranger. If not, see <http://www.gnu.org/licenses/>.
<attribute name="label" translatable="yes">Insert Blan_k Page</attribute>
<attribute name="action">win.insert-blank-page</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Generate Booklet</attribute>
<attribute name="action">win.generate-booklet</attribute>
</item>
</section>
</submenu>
<submenu>
Expand Down Expand Up @@ -331,6 +335,10 @@ along with PDF Arranger. If not, see <http://www.gnu.org/licenses/>.
<attribute name="label" translatable="yes">Insert Blan_k Page</attribute>
<attribute name="action">win.insert-blank-page</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Generate Booklet</attribute>
<attribute name="action">win.generate-booklet</attribute>
</item>
</section>
<section>
<submenu>
Expand Down
51 changes: 48 additions & 3 deletions pdfarranger/exporter.py
Expand Up @@ -30,14 +30,19 @@ def create_blank_page(tmpdir, size):
Create a temporary PDF file with a single empty page.
The size is in PDF unit (1/72 of inch).
"""
fd, filename = tempfile.mkstemp(suffix=".pdf", dir=tmpdir)
os.close(fd)
f = pikepdf.Pdf.new()
f, filename = make_tmp_file(tmpdir)
f.add_blank_page(page_size=size)
f.save(filename)
return filename


def make_tmp_file(tmpdir):
fd, filename = tempfile.mkstemp(suffix=".pdf", dir=tmpdir)
os.close(fd)
f = pikepdf.Pdf.new()
return f, filename


def _mediabox(page, crop):
""" Return the media box for a given page. """
# PDF files which do not have mediabox default to Portrait Letter / ANSI A
Expand Down Expand Up @@ -193,3 +198,43 @@ def num_pages(filepath):
npages = len(pdf.pages)
pdf.close()
return npages


def generate_booklet(pdfqueue, tmp_dir, pages):
file, filename = make_tmp_file(tmp_dir)
content_dict = pikepdf.Dictionary({})
file_indexes = {p.nfile for p in pages}
source_files = {n: pikepdf.open(pdfqueue[n - 1].copyname) for n in file_indexes}
for i in range(len(pages)//2):
even = i % 2 == 0
first = pages[-i - 1 if even else i]
second = pages[i if even else -i - 1]

second_page_size = second.size_in_points()
first_page_size = first.size_in_points()
page_size = [max(second_page_size[0], first_page_size[0]) * 2,
max(second_page_size[1], first_page_size[1])]
first_foreign = file.copy_foreign(source_files[first.nfile].pages[first.npage - 1])
second_foreign = file.copy_foreign(source_files[second.nfile].pages[second.npage - 1])

content_dict[f'/Page{i*2}'] = pikepdf.Page(first_foreign).as_form_xobject()
content_dict[f'/Page{i*2 + 1}'] = pikepdf.Page(second_foreign).as_form_xobject()

content_txt = (f'q 1 0 0 1 0 0 cm /Page{i*2} Do Q'
f' q 1 0 0 1 {first_page_size[0]} 0 cm /Page{i*2 + 1} Do Q ')

newpage = pikepdf.Dictionary(
Type=pikepdf.Name.Page,
MediaBox=[0, 0, *page_size],
Resources=pikepdf.Dictionary(XObject=content_dict),
Contents=pikepdf.Stream(file, content_txt.encode())
)

# workaround for pikepdf <= 2.6.0. See https://github.com/pikepdf/pikepdf/issues/174
if pikepdf.__version__ < '2.7.0':
newpage = file.make_indirect(newpage)
file.pages.append(newpage)

file.save(filename)
return filename

53 changes: 46 additions & 7 deletions pdfarranger/pdfarranger.py
Expand Up @@ -352,6 +352,7 @@ def __create_actions(self):
('select-same-format', self.on_action_select, 'i'),
('about', self.about_dialog),
("insert-blank-page", self.insert_blank_page),
("generate-booklet", self.generate_booklet),
])

main_menu = self.uiXML.get_object("main_menu_button")
Expand All @@ -374,11 +375,47 @@ def insert_blank_page(self, _action, _option, _unknown):
size = model[selection[-1]][0].size_in_points()
page_size = croputils.BlankPageDialog(size, self.window).run_get()
if page_size is not None:
adder = PageAdder(self)
if len(selection) > 0:
adder.move(Gtk.TreeRowReference.new(model, selection[-1]), False)
adder.addpages(exporter.create_blank_page(self.tmp_dir, page_size))
adder.commit(select_added=False, add_to_undomanager=True)
self._insert_pages(model, exporter.create_blank_page(self.tmp_dir, page_size), selection)

def _insert_pages(self, model, file, selection):
adder = PageAdder(self)
if len(selection) > 0:
adder.move(Gtk.TreeRowReference.new(model, selection[-1]), False)
adder.addpages(file)
adder.commit(select_added=False, add_to_undomanager=True)

def generate_booklet(self, _, __, ___):
self.undomanager.commit("generate booklet")
model = self.iconview.get_model()

selection = self.iconview.get_selected_items()
selection.sort(key=lambda x: x.get_indices()[0])
ref_list = [Gtk.TreeRowReference.new(model, path)
for path in selection]
pages = [model.get_value(model.get_iter(ref.get_path()), 0)
for ref in ref_list]

# We need a multiple of 4
blank_page_count = 0 if len(pages) % 4 == 0 else 4 - len(pages) % 4
if blank_page_count > 0:
file = exporter.create_blank_page(self.tmp_dir, pages[0].size)
with self.model_lock:

This comment has been minimized.

Copy link
@mara004

mara004 Aug 10, 2021

Contributor

please see #500 (comment)

for _ in range(blank_page_count):
self._insert_pages(model, file, selection)
added_page_index = selection[-1].get_indices()[-1] + 1
# Fetch the additional blank pages and remove them from the model.
added_page = model.get_value(model.get_iter(added_page_index), 0)
pages.append(added_page)
model.remove(model.get_iter(added_page_index))

self.clear_selected()

adder = PageAdder(self)
booklet = exporter.generate_booklet(self.pdfqueue, self.tmp_dir, pages)
adder.addpages(booklet)
adder.commit(False, False)
self.silent_render()


@staticmethod
def __create_filters(file_type_list):
Expand Down Expand Up @@ -1008,9 +1045,10 @@ def on_action_add_doc_activate(self, _action, _param, _unknown):
adder.commit(select_added=False, add_to_undomanager=True)
chooser.destroy()

def clear_selected(self):
def clear_selected(self, add_to_undomanager=True):
"""Removes the selected elements in the IconView"""
self.undomanager.commit("Delete")
if add_to_undomanager:
self.undomanager.commit("Delete")
model = self.iconview.get_model()
selection = self.iconview.get_selected_items()
selection.sort(reverse=True)
Expand Down Expand Up @@ -1701,6 +1739,7 @@ def iv_selection_changed_event(self, _iconview=None, move_cursor_event=False):
("select-same-file", ne),
("select-same-format", ne),
("crop-white-borders", ne),
("generate-booklet", ne),
]:
self.window.lookup_action(a).set_enabled(e)
self.__update_statusbar()
Expand Down

0 comments on commit b16e346

Please sign in to comment.