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
|
from collections import OrderedDict
|
||||||
import math
|
import math
|
||||||
import fitz
|
import fitz
|
||||||
import tkinter
|
|
||||||
import tkinter.filedialog
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import os.path
|
import os.path
|
||||||
|
@ -30,6 +28,21 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
have_img2pdf = False
|
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"
|
VERSION = "0.2"
|
||||||
|
|
||||||
|
@ -47,12 +60,46 @@ PAGE_SIZES = OrderedDict(
|
||||||
("Tabloid (11 in × 17 in)", (279.4, 431.8)),
|
("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):
|
def mm_to_pt(length):
|
||||||
return (72.0 * length) / 25.4
|
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):
|
def pt_to_mm(length):
|
||||||
return (25.4 * length) / 72.0
|
return (25.4 * length) / 72.0
|
||||||
|
|
||||||
|
@ -650,7 +697,12 @@ class Plakativ:
|
||||||
clip=sourcerect, # part of the input page to use
|
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
|
# from Python 3.7 Lib/idlelib/configdialog.py
|
||||||
|
@ -1680,13 +1732,200 @@ def compute_layout(
|
||||||
|
|
||||||
|
|
||||||
def gui():
|
def gui():
|
||||||
|
if not have_tkinter:
|
||||||
|
raise Exception("the GUI requires tkinter")
|
||||||
root = tkinter.Tk()
|
root = tkinter.Tk()
|
||||||
app = Application(master=root)
|
app = Application(master=root)
|
||||||
app.mainloop()
|
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():
|
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 = parser.add_mutually_exclusive_group(required=False)
|
||||||
gui_group.add_argument(
|
gui_group.add_argument(
|
||||||
"--gui",
|
"--gui",
|
||||||
|
@ -1705,11 +1944,75 @@ def main():
|
||||||
else:
|
else:
|
||||||
parser.set_defaults(gui=False)
|
parser.set_defaults(gui=False)
|
||||||
|
|
||||||
parser.add_argument(
|
mode_group = parser.add_mutually_exclusive_group(required=False)
|
||||||
"--mode", choices=["size", "mult", "npages"], help="select poster size"
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -1717,7 +2020,31 @@ def main():
|
||||||
gui()
|
gui()
|
||||||
sys.exit(0)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
Loading…
Reference in a new issue