forked from josch/plakativ
add command line interface
This commit is contained in:
parent
d9c14aed59
commit
9c4d5c3fbf
1 changed files with 336 additions and 9 deletions
345
plakativ.py
345
plakativ.py
|
@ -15,8 +15,6 @@
|
|||
from collections import OrderedDict
|
||||
import math
|
||||
import fitz
|
||||
import tkinter
|
||||
import tkinter.filedialog
|
||||
import sys
|
||||
import argparse
|
||||
import os.path
|
||||
|
@ -30,6 +28,21 @@ try:
|
|||
except ImportError:
|
||||
have_img2pdf = False
|
||||
|
||||
have_tkinter = True
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.filedialog
|
||||
except ImportError:
|
||||
have_tkinter = False
|
||||
|
||||
class dummy:
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise Exception("this functionality needs tkinter")
|
||||
|
||||
tkinter = type("", (), {})()
|
||||
tkinter.Frame = dummy
|
||||
tkinter.Menubutton = dummy
|
||||
tkinter.LabelFrame = dummy
|
||||
|
||||
VERSION = "0.2"
|
||||
|
||||
|
@ -47,12 +60,46 @@ PAGE_SIZES = OrderedDict(
|
|||
("Tabloid (11 in × 17 in)", (279.4, 431.8)),
|
||||
]
|
||||
)
|
||||
papersizes = {
|
||||
"letter": "8.5inx11in",
|
||||
"a0": "841mmx1189mm",
|
||||
"a1": "594mmx841mm",
|
||||
"a2": "420mmx594mm",
|
||||
"a3": "297mmx420mm",
|
||||
"a4": "210mmx297mm",
|
||||
"a5": "148mmx210mm",
|
||||
"a6": "105mmx148mm",
|
||||
"legal": "8.5inx14in",
|
||||
"tabloid": "11inx17in",
|
||||
}
|
||||
papernames = {
|
||||
"letter": "Letter",
|
||||
"a0": "A0",
|
||||
"a1": "A1",
|
||||
"a2": "A2",
|
||||
"a3": "A3",
|
||||
"a4": "A4",
|
||||
"a5": "A5",
|
||||
"a6": "A6",
|
||||
"legal": "Legal",
|
||||
"tabloid": "Tabloid",
|
||||
}
|
||||
|
||||
Unit = Enum("Unit", "pt cm mm inch")
|
||||
|
||||
|
||||
def mm_to_pt(length):
|
||||
return (72.0 * length) / 25.4
|
||||
|
||||
|
||||
def cm_to_mm(length):
|
||||
return length * 10.0
|
||||
|
||||
|
||||
def in_to_mm(length):
|
||||
return length / 25.4
|
||||
|
||||
|
||||
def pt_to_mm(length):
|
||||
return (25.4 * length) / 72.0
|
||||
|
||||
|
@ -650,7 +697,12 @@ class Plakativ:
|
|||
clip=sourcerect, # part of the input page to use
|
||||
)
|
||||
|
||||
outdoc.save(outfile, garbage=4, deflate=True)
|
||||
if hasattr(outfile, "write"):
|
||||
# outfile is an object with a write() method
|
||||
outfile.write(outdoc.write(garbage=4, deflate=True))
|
||||
else:
|
||||
# outfile is used as a filename
|
||||
outdoc.save(outfile, garbage=4, deflate=True)
|
||||
|
||||
|
||||
# from Python 3.7 Lib/idlelib/configdialog.py
|
||||
|
@ -1680,13 +1732,200 @@ def compute_layout(
|
|||
|
||||
|
||||
def gui():
|
||||
if not have_tkinter:
|
||||
raise Exception("the GUI requires tkinter")
|
||||
root = tkinter.Tk()
|
||||
app = Application(master=root)
|
||||
app.mainloop()
|
||||
|
||||
|
||||
def parse_num(num, name):
|
||||
if num == "":
|
||||
raise argparse.ArgumentTypeError("%s is empty" % name)
|
||||
unit = None
|
||||
if num.endswith("pt"):
|
||||
unit = Unit.pt
|
||||
elif num.endswith("cm"):
|
||||
unit = Unit.cm
|
||||
elif num.endswith("mm"):
|
||||
unit = Unit.mm
|
||||
elif num.endswith("in"):
|
||||
unit = Unit.inch
|
||||
else:
|
||||
try:
|
||||
num = float(num)
|
||||
except ValueError:
|
||||
msg = (
|
||||
"%s is not a floating point number and doesn't have a "
|
||||
"valid unit: %s" % (name, num)
|
||||
)
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
if unit is None:
|
||||
unit = Unit.pt
|
||||
else:
|
||||
num = num[:-2]
|
||||
try:
|
||||
num = float(num)
|
||||
except ValueError:
|
||||
msg = "%s is not a floating point number: %s" % (name, num)
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
if num < 0:
|
||||
msg = "%s must not be negative: %s" % (name, num)
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
if unit == Unit.cm:
|
||||
num = cm_to_mm(num)
|
||||
elif unit == Unit.pt:
|
||||
num = pt_to_mm(num)
|
||||
elif unit == Unit.inch:
|
||||
num = in_to_mm(num)
|
||||
return num
|
||||
|
||||
|
||||
def parse_borderarg(string):
|
||||
if ":" in string:
|
||||
vals = string.split(":")
|
||||
if len(vals) in [0, 1]:
|
||||
raise argparse.ArgumentTypeError("logic error")
|
||||
elif len(vals) == 2:
|
||||
t = b = parse_num(vals[0], "top/bottom border")
|
||||
r = l = parse_num(vals[1], "right/left border")
|
||||
elif len(vals) == 3:
|
||||
t = parse_num(vals[0], "top border")
|
||||
r = l = parse_num(vals[1], "right/left border")
|
||||
b = parse_num(vals[2], "bottom border")
|
||||
elif len(vals) == 4:
|
||||
t = parse_num(vals[0], "top border")
|
||||
r = parse_num(vals[1], "right border")
|
||||
b = parse_num(vals[2], "bottom border")
|
||||
l = parse_num(vals[3], "left border")
|
||||
else:
|
||||
raise argparse.ArgumentTypeError(
|
||||
"border option can not have more than four values"
|
||||
)
|
||||
else:
|
||||
if string == "":
|
||||
raise argparse.ArgumentTypeError("border option cannot be empty")
|
||||
val = parse_num(string, "border")
|
||||
t, r, b, l = val, val, val, val
|
||||
return t, r, b, l
|
||||
|
||||
|
||||
def parse_pagesize_rectarg(string):
|
||||
if papersizes.get(string.lower()):
|
||||
string = papersizes[string.lower()]
|
||||
if "x" not in string:
|
||||
# if there is no separating "x" in the string, then the string is
|
||||
# interpreted as the width
|
||||
w = parse_num(string, "width")
|
||||
h = None
|
||||
else:
|
||||
w, h = string.split("x", 1)
|
||||
w = parse_num(w, "width")
|
||||
h = parse_num(h, "height")
|
||||
if w is None and h is None:
|
||||
raise argparse.ArgumentTypeError("at least one dimension must be specified")
|
||||
return w, h
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
rendered_papersizes = ""
|
||||
for k, v in sorted(papersizes.items()):
|
||||
rendered_papersizes += " %-8s %s\n" % (papernames[k], v)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description="""\
|
||||
Create large posters by printing and gluing together smaller pages.
|
||||
|
||||
This program is for situations when you want to create a large poster or banner
|
||||
but do not have a printer that supports large sheets of paper. Plakativ allows
|
||||
one to enlarge and split a PDF across multiple pages, creating another PDF with
|
||||
pages of the desired printable size. After printing, the pages can be cut and
|
||||
glued together to form a larger poster. Features:
|
||||
|
||||
- lossless operation
|
||||
- no pixel artifacts when upscaling if PDF contains vector graphics
|
||||
- GUI with preview functionality
|
||||
- complex layouter to save paper
|
||||
- optimize by number of pages, output poster size or multiple of input area
|
||||
|
||||
Options:
|
||||
""",
|
||||
epilog="""\
|
||||
Poster size:
|
||||
There are three ways to set the size of the final poster. The desired method
|
||||
is selected using the mutually exclusive options --size, --factor and
|
||||
--maxpages. The --size option allows one to specify a width and height into
|
||||
which the input will be fitted, swapping width and height as necessary, to
|
||||
create the largest possible poster with those dimensions while keeping the
|
||||
aspect ratio of the input. The --factor option scales the area of the input
|
||||
by the given multiplier. If the input is a DIN A4 page, then a factor of 2
|
||||
will create a DIN A3 poster. The --maxpages option allows one to specify a
|
||||
maximum number of pages one is willing to print out and creates the largest
|
||||
possible poster that can possibly be created with the given number of pages.
|
||||
For example, printing a DIN A1 poster on DIN A4 pages with a border of 15 mm
|
||||
will require 15 pages with the simple layouter engine. With --maxpages=15 a
|
||||
slightly larger poster will be generated but will make better use of the
|
||||
available number of pages of paper. Using the complex layouter, an even
|
||||
bigger poster can be generated with just 15 pages of paper by changing the
|
||||
orientation of some of them.
|
||||
|
||||
Paper sizes:
|
||||
You can specify the short hand paper size names shown in the first column in
|
||||
the table below as arguments to the --pagesize and --imgsize options. The
|
||||
width and height they are mapping to is shown in the second column. Giving
|
||||
the value in the second column has the same effect as giving the short hand
|
||||
in the first column. The values are case insensitive.
|
||||
|
||||
%s
|
||||
|
||||
Borders, cutting and gluing:
|
||||
The border on each page set using the --border option has two purposes.
|
||||
Firstly, the border is useful for printers that do not support borderless
|
||||
printing. Secondly, the border is the area where the individual pages overlap
|
||||
and can be glued together. Before gluing, cut away the border area where the
|
||||
printer was unable to print on. As long as you stay within the distance set
|
||||
by the --border option, you don't need precision tools to do the cutting but
|
||||
can cut freehand using a pair of scissors. You only need to cut the borders
|
||||
from those edges that will end up being glued onto another piece of paper.
|
||||
By keeping even the area at the border your printer could not print on from
|
||||
the paper at the bottom you maintain a larger area for the glue.
|
||||
|
||||
Examples:
|
||||
|
||||
To run the tkinter GUI execute either:
|
||||
|
||||
$ plakativ-gui
|
||||
$ plakativ --gui
|
||||
|
||||
To use plakativ without GUI from the command line you can run:
|
||||
|
||||
$ plakativ --size A1 --output=poster.pdf input.pdf
|
||||
|
||||
This will create a file poster.pdf with multiple DIN A4 pages which, after
|
||||
being cut and glued together will form a DIN A1 poster of the content on the
|
||||
first page of input.pdf.
|
||||
|
||||
Written by Johannes 'josch' Schauer <josch@mister-muffin.de>
|
||||
|
||||
Report bugs at https://gitlab.mister-muffin.de/josch/plakativ/issues
|
||||
"""
|
||||
% rendered_papersizes,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Makes the program operate in verbose mode, printing messages on "
|
||||
"standard error.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-V",
|
||||
"--version",
|
||||
action="version",
|
||||
version="%(prog)s " + VERSION,
|
||||
help="Prints version information and exits.",
|
||||
)
|
||||
gui_group = parser.add_mutually_exclusive_group(required=False)
|
||||
gui_group.add_argument(
|
||||
"--gui",
|
||||
|
@ -1705,11 +1944,75 @@ def main():
|
|||
else:
|
||||
parser.set_defaults(gui=False)
|
||||
|
||||
parser.add_argument(
|
||||
"--mode", choices=["size", "mult", "npages"], help="select poster size"
|
||||
mode_group = parser.add_mutually_exclusive_group(required=False)
|
||||
mode_group.add_argument(
|
||||
"--size",
|
||||
metavar="LxL",
|
||||
dest="mode",
|
||||
type=parse_pagesize_rectarg,
|
||||
help="Poster width/height either as width times height or one of the "
|
||||
"known paper sizes (see below). Cannot be used together with --factor "
|
||||
"or --maxpages.",
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"--factor",
|
||||
metavar="MULT",
|
||||
dest="mode",
|
||||
type=float,
|
||||
help="Poster size as multiple of input page area. Cannot be used "
|
||||
"together with --size or --maxpages.",
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"--maxpages",
|
||||
metavar="NUM",
|
||||
dest="mode",
|
||||
type=int,
|
||||
help="Maximum possible poster size with the given number of pages. "
|
||||
"Cannot be used together with --size or --factor.",
|
||||
)
|
||||
|
||||
parser.add_argument("-o", "--output", help="output filename (default: stdout)")
|
||||
parser.add_argument("input", nargs="?", help="input filename (default: stdin)")
|
||||
parser.add_argument(
|
||||
"--pagenum",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Page number of input PDF to turn into a poster (default: 1)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pagesize",
|
||||
metavar="LxL",
|
||||
type=parse_pagesize_rectarg,
|
||||
default=(210, 297),
|
||||
help="Width and height of the output pages or one of the known paper "
|
||||
"sizes (see below). This is the paper size that you are printing on "
|
||||
"with your printer (default: A4)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--border",
|
||||
metavar="L[:L[:L[:L]]]",
|
||||
type=parse_borderarg,
|
||||
default=(15, 15, 15, 15),
|
||||
help="The borders on each output page for gluing. This specifies how "
|
||||
"much the pages overlap each other. If your printer cannot print "
|
||||
"borderless, then this value should also be larger than the border up "
|
||||
"to which your printer is able to print. One value sets the border on "
|
||||
"all four sides. Multiple values are separated by a colon. With two "
|
||||
"values, the first value sets top and bottom border and the second "
|
||||
"value sets left and right border. With three values, the first value "
|
||||
"sets the top border, the second value the left and right border and "
|
||||
"the third value the bottom border. Four values set top, right, "
|
||||
"bottom and left borders in that order.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--layouter",
|
||||
choices=["simple", "complex"],
|
||||
default="simple",
|
||||
help="The algorithm arranging the individual pages making the poster. "
|
||||
"The simple layout has all pages in the same orientation. The complex "
|
||||
"layout is able to sometimes require less pages for the same poster "
|
||||
"size and is allowed to rotate pages.",
|
||||
)
|
||||
parser.add_argument("-o", "--output", help="output file")
|
||||
parser.add_argument("input", nargs="?", help="input file")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
@ -1717,7 +2020,31 @@ def main():
|
|||
gui()
|
||||
sys.exit(0)
|
||||
|
||||
compute_layout(args.input, args.output, mode=args.mode, size=(297, 420))
|
||||
if not args.input or args.input == "-":
|
||||
args.input = sys.stdin.buffer
|
||||
|
||||
if not args.output or args.output == "-":
|
||||
args.output = sys.stdout.buffer
|
||||
|
||||
if isinstance(args.mode, tuple):
|
||||
mode = "size"
|
||||
elif isinstance(args.mode, float):
|
||||
mode = "mult"
|
||||
elif isinstance(args.mode, int):
|
||||
mode = "npages"
|
||||
else:
|
||||
raise Exception("logic error")
|
||||
|
||||
compute_layout(
|
||||
args.input,
|
||||
args.output,
|
||||
mode,
|
||||
pagenr=args.pagenum - 1, # zero based
|
||||
pagesize=args.pagesize,
|
||||
border=args.border,
|
||||
strategy=args.layouter,
|
||||
**{mode: args.mode},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
Loading…
Reference in a new issue