Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add standard paper sizes to "insert blank page" and "page size" dialogs #1066

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 27 additions & 23 deletions pdfarranger/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@
"""Return the page size in PDF points."""
return self.size.scaled(self.scale).cropped(self.crop)

def size_in_mm(self) -> Dims:
"""Return the page size in mm."""
return self.size_in_points() * 25.4 / 72

def width_in_pixel(self):
return self.size_in_pixel().width

Expand All @@ -302,7 +306,7 @@


class Page(BasePage):
def __init__(self, nfile, npage, zoom, copyname, angle, scale, crop: Sides, hide: Sides, size_orig: Dims, basename, layerpages):
def __init__(self, nfile, npage, zoom, copyname, angle, scale, crop: Sides, hide: Sides, size_orig: Dims, description, layerpages):
super().__init__(nfile, npage, copyname, angle, scale, Sides(*crop), size_orig)
self.zoom = zoom
self.hide = Sides(*hide)
Expand All @@ -311,19 +315,14 @@
self.resample = -1
self.preview = None
"""A low resolution thumbnail"""
#: The name of the original file
self.basename = basename
"""The name of the original file"""
self.description = description
"""The text under the thumbnail"""
self.layerpages = list(layerpages)

def __repr__(self):
return (f"Page({self.nfile}, {self.npage}, {self.zoom}, '{self.copyname}', "
f"{self.angle}, {self.scale}, {self.crop}, {self.hide}, "
f"{self.size_orig}, '{self.basename}', {self.layerpages})")

def description(self):
shortname = os.path.splitext(self.basename)[0]
return "".join([shortname, "\n", _("page"), " ", str(self.npage)])
f"{self.size_orig}, '{self.description}', {self.layerpages})")

def rotate(self, angle: int):
rt = self.rotate_times(angle)
Expand All @@ -345,9 +344,9 @@
def serialize(self):
"""Convert to string for copy/past operations."""
lpdata = [lp.serialize() for lp in self.layerpages]
ts = [self.copyname, self.npage, self.basename, self.angle, self.scale]
ts = [self.copyname, self.npage, self.description, self.angle, self.scale]
ts += list(self.crop) + list(self.hide) + list(lpdata)
return "\n".join([str(v) for v in ts])
return "///".join([str(v) for v in ts])

def duplicate(self, incl_thumbnail=True):
r = copy.copy(self)
Expand Down Expand Up @@ -413,7 +412,7 @@
"""Convert to string for copy/past operations."""
ts = [self.copyname, self.npage, self.angle, self.scale, self.laypos]
ts += list(self.crop) + list(self.offset)
return "\n".join([str(v) for v in ts])
return "///".join([str(v) for v in ts])

def duplicate(self):
r = copy.copy(self)
Expand Down Expand Up @@ -529,21 +528,21 @@
if not askpass:
raise e

def __init__(self, filename, basename, blank_size, stat, tmp_dir, parent):
def __init__(self, filename, description, blank_size, stat, tmp_dir, parent):
self.render_lock = threading.Lock()
self.filename = os.path.abspath(filename)
self.stat = stat
if basename is None: # When importing files
if description is None: # When importing files
self.basename = os.path.basename(filename)
else: # When copy-pasting
self.basename = basename
self.basename = description.split('\n')[0]

Check warning on line 538 in pdfarranger/core.py

View check run for this annotation

Codecov / codecov/patch

pdfarranger/core.py#L538

Added line #L538 was not covered by tests
self.blank_size = blank_size # != None if page is blank
self.password = ""
filemime = mimetypes.guess_type(self.filename)[0]
if not filemime:
raise PDFDocError(_("Unknown file format"))
if filemime == "application/pdf":
if self.filename.startswith(tmp_dir) and basename is None:
if self.filename.startswith(tmp_dir) and description is None:
# In the "Insert Blank Page" we don't need to copy self.filename
self.copyname = self.filename
self.basename = ""
Expand Down Expand Up @@ -609,7 +608,7 @@
self.before = before
self.treerowref = treerowref

def get_pdfdoc(self, filename: str, basename: Optional[str] = None, blank_size=None) -> Optional[Tuple[PDFDoc, int, bool]]:
def get_pdfdoc(self, filename: str, description: Optional[str] = None, blank_size=None) -> Optional[Tuple[PDFDoc, int, bool]]:
"""Get the pdfdoc object for the filename.

pdfqueue is searched for the filename. If it is not found a pdfdoc is created
Expand Down Expand Up @@ -637,7 +636,7 @@
return it_pdfdoc, i + 1, False

try:
pdfdoc = PDFDoc(filename, basename, blank_size, self.stat_cache[filename],
pdfdoc = PDFDoc(filename, description, blank_size, self.stat_cache[filename],
self.app.tmp_dir, self.app.window)
except _UnknownPasswordException:
return None
Expand Down Expand Up @@ -665,17 +664,17 @@
layerpages.append(LayerPage(*ld))
return layerpages

def addpages(self, filename, page=-1, basename=None, angle=0, scale=1.0, crop=Sides(0, 0, 0, 0), hide=Sides(0, 0, 0, 0), layerdata=None):
def addpages(self, filename, page=-1, description=None, angle=0, scale=1.0, crop=Sides(0, 0, 0, 0), hide=Sides(0, 0, 0, 0), layerdata=None):
c = 'pdf' if page == -1 and os.path.splitext(filename)[1].lower() == '.pdf' else 'other'
self.content.append(c)
self.pdfqueue_used = len(self.app.pdfqueue) > 0

doc_data = self.get_pdfdoc(filename, basename)
doc_data = self.get_pdfdoc(filename, description)
if doc_data is None:
return
pdfdoc, nfile, doc_added = doc_data

if (doc_added and pdfdoc.copyname != pdfdoc.filename and basename is None and not
if (doc_added and pdfdoc.copyname != pdfdoc.filename and description is None and not
(filename.startswith(self.app.tmp_dir) and filename.endswith(".png"))):
self.app.import_directory = os.path.split(filename)[0]
self.app.export_directory = self.app.import_directory
Expand All @@ -691,6 +690,11 @@

for npage in range(n_start, n_end + 1):
page = pdfdoc.document.get_page(npage - 1)
if description is None:
shortname = os.path.splitext(pdfdoc.basename)[0]
desc = "".join([shortname, "\n", _("page"), " ", str(npage)])
else:
desc = description
self.pages.append(
Page(
nfile,
Expand All @@ -702,7 +706,7 @@
crop,
hide,
Dims(*page.get_size()),
pdfdoc.basename,
desc,
layerpages,
)
)
Expand All @@ -719,7 +723,7 @@
self.pages.reverse()
with self.app.render_lock():
for p in self.pages:
m = [p, p.description()]
m = [p, p.description]
if self.treerowref:
iter_to = self.app.model.get_iter(self.treerowref.get_path())
if self.before:
Expand Down
2 changes: 1 addition & 1 deletion pdfarranger/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def get_blank_doc(pageadder, pdfqueue, tmpdir, size, npages=1):
nfile = i + 1
return filename, nfile
filename = _create_blank_page(tmpdir, size, npages)
doc_data = pageadder.get_pdfdoc(filename, basename=None, blank_size=size)
doc_data = pageadder.get_pdfdoc(filename, description=None, blank_size=size)
if doc_data is None:
return None, None
nfile = doc_data[1]
Expand Down
166 changes: 131 additions & 35 deletions pdfarranger/pageutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
f = factor
else:
# TODO: allow to change aspect ratio
f = max(*Dims(width, height) / page_size)
f = min(*Dims(width, height) / page_size)

Check warning on line 44 in pdfarranger/pageutils.py

View check run for this annotation

Codecov / codecov/patch

pdfarranger/pageutils.py#L44

Added line #L44 was not covered by tests
# Page size must be in [72, 14400] (PDF standard requirement)
f = max(f, *(Dims(72, 72) / page_size))
f = min(f, *(Dims(14400, 14400) / page_size))
Expand Down Expand Up @@ -116,7 +116,7 @@
""" A form to specify the relative scaling factor """

def __init__(self, current_scale, margin=10):
super().__init__()
super().__init__(valign=Gtk.Align.CENTER, halign=Gtk.Align.CENTER)
self.props.spacing = margin
self.add(Gtk.Label(label=_("Scale factor")))
# Largest page size is 200 inch and smallest is 1 inch
Expand All @@ -131,22 +131,121 @@
return self.entry.get_value() / 100


class _ScalingWidget(Gtk.Box):
""" A form to specify the page width or height """
class PaperSizeWidget(Gtk.Grid):
def __init__(self, size, margin=16):
super().__init__(margin=margin, row_spacing=8, column_spacing=8, row_homogeneous=True)

self.attach(Gtk.Label(label=_("Width"), halign=Gtk.Align.START), 1, 1, 1, 1)
self.width_entry = _LinkedSpinButton(25.4, 5080, 1, 0)
self.w_entry_id = self.width_entry.connect('value-changed', self.width_changed)
self.attach(self.width_entry, 2, 1, 1, 1)
self.attach(Gtk.Label(label=_("mm"), halign=Gtk.Align.START), 4, 1, 1, 1)

self.attach(Gtk.Label(label=_("Height"), halign=Gtk.Align.START), 1, 2, 1, 1)
self.height_entry = _LinkedSpinButton(25.4, 5080, 1, 10)
self.h_entry_id = self.height_entry.connect('value-changed', self.height_changed)
self.attach(self.height_entry, 2, 2, 1, 1)
self.attach(Gtk.Label(label=_("mm"), halign=Gtk.Align.START), 4, 2, 1, 1)

self.attach(Gtk.Label(_("Paper size"), halign=Gtk.Align.START), 1, 3, 1, 1)
self.combo = Gtk.ComboBoxText(margin=0)
self.combo_changed_id = self.combo.connect('changed', self.paper_size_changed)
self.papers = [Gtk.PaperSize.new_custom('Custom', _("Custom"), 0, 0, Gtk.Unit.MM)]
paper_list = ['iso_a3', 'iso_a4', 'iso_a5', 'na_letter', 'na_legal', 'na_ledger']
self.papers += [Gtk.PaperSize.new(p) for p in paper_list]
for p in self.papers:
p.size = [round(p.get_width(Gtk.Unit.MM), 5), round(p.get_height(Gtk.Unit.MM), 5)]
self.combo.append(None, p.get_display_name())
self.attach(self.combo, 2, 3, 1, 1)

self.attach(Gtk.Label(_("Orientation"), halign=Gtk.Align.START), 1, 4, 1, 1)
self.port = Gtk.RadioButton(label=_("Portrait"), group=None)
self.land = Gtk.RadioButton(label=_("Landscape"), group=self.port)
self.port.connect('clicked', self.orientation_clicked)
box1 = Gtk.Box()
box1.pack_start(self.port, True, True, 0)
box1.pack_start(self.land, True, True, 0)
self.attach(box1, 2, 4, 1, 1)

box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box2.pack_start(Gtk.Label("┐"), True, True, 0)
self.ratio_cb = Gtk.CheckButton(margin_top=1)
box2.pack_start(self.ratio_cb, True, True, 0)
box2.pack_start(Gtk.Label("┘"), True, True, 0)
self.attach(box2, 3, 1, 1, 2)

if size is None:
size = [210, 297] # A4 by default
self.ratio_cb.set_sensitive(False)

Check warning on line 179 in pdfarranger/pageutils.py

View check run for this annotation

Codecov / codecov/patch

pdfarranger/pageutils.py#L178-L179

Added lines #L178 - L179 were not covered by tests
else:
self.ratio_cb.set_active(True)
self.ratio_cb.connect('clicked', self.width_changed)

self.ratios = [size[0] / size[1], size[1] / size[0]]
self.set_entry_size(size)
self.select_paper_and_orientation()
self.update_entry_limits()
self.ratio_cb.connect('clicked', self.update_entry_limits)

def update_entry_limits(self, _widget=None):
r = self.ratios if self.ratio_cb.get_active() else [1, 1]
wrange = max(25.4, 25.4 * r[0]), min(5080, 5080 * r[0])
hrange = max(25.4, 25.4 * r[1]), min(5080, 5080 * r[1])
self.width_entry.set_range(*wrange)
self.height_entry.set_range(*hrange)

def width_changed(self, _widget):
if self.ratio_cb.get_active():
width = self.width_entry.get_value()
self.set_entry_size((width, width / self.ratios[0]))
self.select_paper_and_orientation()

def height_changed(self, _widget):
if self.ratio_cb.get_active():
height = self.height_entry.get_value()
self.set_entry_size((height / self.ratios[1], height))
self.select_paper_and_orientation()

Check warning on line 207 in pdfarranger/pageutils.py

View check run for this annotation

Codecov / codecov/patch

pdfarranger/pageutils.py#L204-L207

Added lines #L204 - L207 were not covered by tests

def orientation_clicked(self, _widget):
size = self.get_value(Gtk.Unit.MM)
self.width_entry.set_range(25.4, 5080)
self.height_entry.set_range(25.4, 5080)
self.set_entry_size(sorted(size, reverse=self.land.get_active()))
self.ratios.sort(reverse=self.land.get_active())
self.update_entry_limits()

def paper_size_changed(self, combo):
paper = self.papers[combo.get_active()]
size = paper.get_width(Gtk.Unit.MM), paper.get_height(Gtk.Unit.MM)
self.set_entry_size(sorted(size, reverse=self.land.get_active()))
if round(size[0] / size[1], 5) not in [round(r, 5) for r in self.ratios]:
self.ratio_cb.set_active(False)
self.update_entry_limits()

Check warning on line 223 in pdfarranger/pageutils.py

View check run for this annotation

Codecov / codecov/patch

pdfarranger/pageutils.py#L222-L223

Added lines #L222 - L223 were not covered by tests

def select_paper_and_orientation(self):
size = self.get_value(Gtk.Unit.MM)
self.papers[0].set_size(*size, Gtk.Unit.MM)
size = [round(s, 5) for s in size]
for num, paper in reversed(list(enumerate(self.papers))):
if paper.size in [size, size[::-1]]:
break

Check warning on line 231 in pdfarranger/pageutils.py

View check run for this annotation

Codecov / codecov/patch

pdfarranger/pageutils.py#L231

Added line #L231 was not covered by tests
self.combo.set_active(num)
self.port.set_active(size[0] < size[1])
self.land.set_active(size[0] > size[1])

def __init__(self, label, default, margin=10):
super().__init__()
self.props.spacing = margin
self.add(Gtk.Label(label=label))
self.entry = _LinkedSpinButton(25.4, 5080, 1, 10)
self.entry.set_activates_default(True)
self.add(self.entry)
self.add(Gtk.Label(label=_("mm")))
# A PDF unit is 1/72 inch
self.entry.set_value(default * 25.4 / 72)
def set_entry_size(self, size):
"""Set entry size in mm"""
with GObject.signal_handler_block(self.width_entry, self.w_entry_id):
self.width_entry.set_value(size[0])
with GObject.signal_handler_block(self.height_entry, self.h_entry_id):
self.height_entry.set_value(size[1])

def get_value(self):
return self.entry.get_value() / 25.4 * 72
def get_value(self, unit=Gtk.Unit.POINTS):
"""Get entry size in points or mm"""
size = [self.width_entry.get_value(), self.height_entry.get_value()]
if unit == Gtk.Unit.POINTS:
size = [s * 72 / 25.4 for s in size]
return size


class _CropHideWidget(Gtk.Frame):
Expand Down Expand Up @@ -243,15 +342,19 @@
super().__init__(title=_("Page size"), parent=window)
self.set_resizable(False)
page = model.get_value(model.get_iter(selection[-1]), 0)
paper_widget = PaperSizeWidget(page.size_in_mm(), margin=1)
paper_widget.attach(Gtk.Label(_("Fit mode"), halign=Gtk.Align.START), 1, 5, 1, 1)
self.combo = Gtk.ComboBoxText()
self.combo.append('SCALE', _("Scale"))
self.combo.append('SCALE-ADD-MARG', _("Scale & Add margins"))
self.combo.append('CROP-ADD-MARG', _("Crop & Add margins"))
self.combo.set_active(0)
paper_widget.attach(self.combo, 2, 5, 1, 1)
rel_widget = _RelativeScalingWidget(page.scale)
width_widget = _ScalingWidget(_("Width"), page.width_in_points())
height_widget = _ScalingWidget(_("Height"), page.height_in_points())
self.scale_stack = _RadioStackSwitcher()
self.scale_stack = _RadioStackSwitcher(margin=15)
self.scale_stack.add_named(paper_widget, "Fit", _("Fit to paper"))
self.scale_stack.add_named(rel_widget, "Relative", _("Relative"))
self.scale_stack.add_named(width_widget, "Width", _("Width"))
self.scale_stack.add_named(height_widget, "Height", _("Height"))
pagesizeframe = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE)
pagesizeframe.props.margin = 8
pagesizeframe.add(self.scale_stack)
self.vbox.pack_start(pagesizeframe, True, True, 0)
self.show_all()
Expand All @@ -262,12 +365,9 @@
result = self.run()
val = None
if result == Gtk.ResponseType.OK:
val = self.scale_stack.selected_child.get_value()
if self.scale_stack.selected_name == "Width":
val = val, 0
elif self.scale_stack.selected_name == "Height":
val = 0, val
# else val is a relative scale so we return it as is
s = self.scale_stack
mode = 'SCALE' if s.selected_name == 'Relative' else self.combo.get_active_id()
val = s.selected_child.get_value(), mode
self.destroy()
return val

Expand Down Expand Up @@ -330,19 +430,15 @@
def __init__(self, size, window):
super().__init__(title=_("Insert Blank Page"), parent=window)
self.set_resizable(False)
self.width_widget = _ScalingWidget(_("Width"), size[0])
self.height_widget = _ScalingWidget(_("Height"), size[1])
self.vbox.pack_start(self.width_widget, True, True, 6)
self.vbox.pack_start(self.height_widget, True, True, 6)
self.width_widget.props.spacing = 6
self.height_widget.props.spacing = 6
self.paper_widget = PaperSizeWidget(size)
self.vbox.pack_start(self.paper_widget, True, True, 0)
self.show_all()

def run_get(self):
result = self.run()
r = None
if result == Gtk.ResponseType.OK:
r = self.width_widget.get_value(), self.height_widget.get_value()
r = self.paper_widget.get_value()
self.destroy()
return r

Expand Down
Loading