diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fd52af4 --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +.PHONY: clean clean-test clean-pyc clean-build help +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +try: + from urllib import pathname2url +except: + from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + +lint: ## check style with flake8 + black pdftools tests + flake8 pdftools tests + +test: ## run tests quickly with the default Python + py.test -x --disable-warnings + +coverage: ## check code coverage with the default Python + coverage run --source pdftools -m pytest + coverage report -m + coverage html + $(BROWSER) htmlcov/index.html diff --git a/pdftools/__init__.py b/pdftools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pdftools/cli.py b/pdftools/cli.py new file mode 100644 index 0000000..c00c5d0 --- /dev/null +++ b/pdftools/cli.py @@ -0,0 +1,89 @@ +import click + +from reportlab.lib.units import mm + +from . import nameplate +from . import ruler + + +@click.command() +@click.argument( + "attendees", + type=click.Path( + exists=True, + file_okay=True, + dir_okay=False, + writable=False, + readable=True, + ), +) +@click.argument( + "logo", + type=click.Path( + exists=True, + file_okay=True, + dir_okay=False, + writable=False, + readable=True, + ), + required=False, +) +@click.option( + "-s", + "--size", + type=click.INT, + required=False, + default=10, + show_default="10 mm", +) +@click.option( + "-y", + "--adjust_y", + type=click.INT, + required=False, + default=0, + show_default=" 0 mm", +) +@click.option( + "-x", + "--adjust_x", + type=click.INT, + required=False, + default=0, + show_default=" 0 mm", +) +def nameplates(attendees, logo, size, adjust_x, adjust_y): + """ creates a pdf with name plate cards + + The attendees file should be a tab separated text file with no headers. Put + the company name in the first column, the given name in the second and the + last name in the third column, e.g: + + Hochimin Enterprizes \\t Jane \\t Doe \\n + + + The positioning of the optional logo file can be controlled with the + size and adjustment options. + """ + if logo is not None: + logo = nameplate.Logo(logo, size * mm, adjust_x=adjust_x, adjust_y=adjust_y) + npc = nameplate.NamePlateCards(logo) + npc.generate(attendees, show=True) + + + + +@click.command() +@click.argument( + "pdf", + type=click.Path( + exists=True, + file_okay=True, + dir_okay=False, + writable=False, + readable=True, + ), +) +def addruler(pdf): + """ adds a blue ruler overlay to a pdf file """ + ruler.ruler_overlay(pdf) diff --git a/pdftools/formfill.py b/pdftools/formfill.py new file mode 100644 index 0000000..7f8a477 --- /dev/null +++ b/pdftools/formfill.py @@ -0,0 +1,101 @@ +import pdfrw +import subprocess +import itertools +import tempfile + +from pathlib import Path +from reportlab.pdfgen import canvas +from reportlab.lib.units import mm +from reportlab.lib.pagesizes import A4 + + +def parse_text_file(text_file, ignore_first_line=True): + with open(text_file) as fh: + if ignore_first_line: + next(fh) + lines = [line.strip() for line in fh] + + parts = [line.split("\t") for line in lines if line] + return [tuple(map(str.strip, p)) for p in parts] + + +def fill(original_form, draw_function, values, output_file="filled.pdf"): + p = Path(output_file) + if p.exists(): + p.unlink() + + with tempfile.TemporaryFile() as overlay_fh: + with tempfile.TemporaryFile() as multipage_fh: + + # first create the pages with the values at the right positions + # this must be done first, because we need to know the number of + # pages + c = canvas.Canvas(overlay_fh, pagesize=A4) + for items in values: + draw_function(c, items) + c.showPage() + c.save() + overlay_fh.seek(0) + + olay = pdfrw.PdfReader(overlay_fh) + + # create a temporary pdf that has as many pages as the filled in + # values + original_form = pdfrw.PdfReader(original_form) + writer = pdfrw.PdfWriter() + for i in range(len(olay.pages)): + writer.addpages(original_form.pages) + writer.write(multipage_fh) + multipage_fh.seek(0) + + # merge the overlay and the multipage form + form = pdfrw.PdfReader(multipage_fh) + for form_page, overlay_page in zip(form.pages, olay.pages): + merge_obj = pdfrw.PageMerge() + filled = merge_obj.add(overlay_page)[0] + pdfrw.PageMerge(form_page).add(filled).render() + + # write the combined file to the output + pdfrw.PdfWriter().write(output_file, form) + subprocess.run(["open", output_file]) + + + + + + +if __name__=="__main__": + + attend = parse_text_file("teilnehmertabelle.txt") + + def create_overlay(c, item): + company, addr, name = item + c.setFont("Helvetica", 11) + c.drawString( 75*mm, 234*mm, "Universität Freiburg, IMTEK, CPI") + c.drawString(118*mm, 225*mm, "20126 N") + c.drawString( 58*mm, 216*mm, "VDK-Bestimmung in Jungbier") + + if company.startswith("Wissenschaftsförderung"): + c.setFont("Helvetica", 10) + c.drawString( 35*mm, 204*mm, f"{company}, {addr}") + if company.startswith("Wissenschaftsförderung"): + c.setFont("Helvetica", 11) + c.drawString( 35*mm, 191*mm, f"19.02.2019") + c.drawString(113*mm, 191*mm, f"10:30 Uhr") + c.drawString(150*mm, 191*mm, f"16:00 Uhr") + + c.drawString( 25.8*mm, 177.75*mm, f"X") + + c.drawString( 35*mm, 169*mm, name) + c.drawString( 35*mm, 156*mm, "Teilname Kickoff-Meeting") + + c.drawString( 37.15*mm, 134.35*mm, f"X") + c.drawString( 54*mm, 134*mm, "1") + + c.drawString( 24*mm, 82*mm, "Freiburg, den 19.02.2019") + + c.setFont("Helvetica-Bold", 12) + c.drawString( 117*mm, 100*mm, "1.000") + + #fill("form.pdf", create_overlay, attend) + ruler_overlay("form.pdf") diff --git a/pdftools/logo_imtek.png b/pdftools/logo_imtek.png new file mode 100644 index 0000000..6006365 Binary files /dev/null and b/pdftools/logo_imtek.png differ diff --git a/pdftools/nameplate.py b/pdftools/nameplate.py new file mode 100644 index 0000000..334f0b0 --- /dev/null +++ b/pdftools/nameplate.py @@ -0,0 +1,141 @@ +import subprocess +import itertools + +from pathlib import Path +from PIL import Image +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import mm + + +class Logo: + def __init__(self, path, target_height, adjust_x=0, adjust_y=0): + self.path = path + self.height = target_height + self.width = self._scale_width(target_height) + self.adjust_x = adjust_x + self.adjust_y = adjust_y + self.size = {"width": self.width, "height": self.height} + + def _scale_width(self, height): + img = Image.open(self.path) + img_w2h = img.width / img.height + return height * img_w2h + + +_tmp_pos = [ + # upper card + 39 + 114, + # lower card + 39, +] +y_positions = itertools.cycle([rp * mm for rp in _tmp_pos]) +y_height = 50 * mm +y_padding = 5 * mm + +x_pos = 55 * mm +x_width = 100 * mm +x_padding = 10 * mm + +false_and_true = itertools.cycle([False, True]) + +imtek_logo_path = Path(__file__).parent / "logo_imtek.png" + +class NamePlateCards: + def __init__( + self, + partner_logo=None, + partner_color=None, + name_size=24, + company_size=12, + debug=False, + ): + self.imtek_logo = Logo(imtek_logo_path, 10 * mm) + self.imtek_color = self._color_from_rgb(23, 17, 117) + self.partner_logo = partner_logo + if partner_color is None: + self.partner_color = self._guess_partner_color() + else: + self.partner_color = self._color_from_rgb(partner_color) + self.name_size = name_size + self.company_size = company_size + self.debug = debug + self._canvas = None + + def generate(self, attendees_file, output_file="output.pdf", show=True): + attendees = self.parse_attendees(attendees_file) + for page_break, entry in zip(false_and_true, attendees): + self.draw_card(*entry) + if page_break: + self.canvas.showPage() + self.canvas.save() + if show: + subprocess.run(["open", output_file]) + + @property + def canvas(self): + if self._canvas is None: + self._canvas = canvas.Canvas("output.pdf", pagesize=A4) + return self._canvas + + def _color_from_rgb(self, *args): + return tuple(x / 255 for x in args) + + def _guess_partner_color(self): + if self.partner_logo is None: + return (0, 0, 0) + img = Image.open(self.partner_logo.path) + colors = sorted(img.getcolors(img.width * img.height), reverse=True) + if colors is None: + return (0, 0, 0) + else: + return self._color_from_rgb(*colors[0][1]) + + def parse_attendees(self, attendees_file): + with open(attendees_file, "r") as fh: + lines = [l.strip() for l in fh] + splited = [line.split("\t") for line in lines if line] + return [(c.strip(), f.strip(), l.strip()) for c, f, l in splited] + + def box_helper(self): + if self.debug: + self.canvas.setStrokeColorRGB(1, 0, 0) + self.canvas.rect(0, 0, x_width, y_height) + + def draw_card(self, company, first_name, last_name): + self.canvas.saveState() + y_pos = next(y_positions) + self.canvas.translate(x_pos, y_pos) + self.box_helper() + self.draw_field(company, first_name, last_name) + # next translate is relative to the first + self.canvas.translate(x_width, y_height * 2) + self.canvas.rotate(180) + self.box_helper() + self.draw_field(company, first_name, last_name) + self.canvas.restoreState() + + def draw_field(self, company, first_name, last_name): + self.draw_logo(self.imtek_logo, "left") + if self.partner_logo is not None: + self.draw_logo(self.partner_logo, "right") + self.canvas.setFillColorRGB(*self.imtek_color) + self.canvas.setFont("Helvetica-Bold", self.name_size) + self.canvas.drawString(x_padding, y_padding + 18 * mm, first_name) + self.canvas.drawString(x_padding, y_padding + 8 * mm, last_name) + self.canvas.setFillColorRGB(*self.partner_color) + self.canvas.setFont("Helvetica-Bold", self.company_size) + self.canvas.drawString(x_padding, y_padding, company) + + def draw_logo(self, img, position="left"): + if position == "left": + x_left = x_padding + else: + x_left = x_width - x_padding - img.width + self.canvas.drawImage( + img.path, + x_left + img.adjust_x, + y_height - y_padding - img.height + img.adjust_y, + **img.size, + mask="auto" + ) diff --git a/pdftools/ruler.py b/pdftools/ruler.py new file mode 100644 index 0000000..e88b6d4 --- /dev/null +++ b/pdftools/ruler.py @@ -0,0 +1,26 @@ +from pathlib import Path +from reportlab.lib.units import mm + +from . import formfill + + +def ruler_overlay(original_form): + + def ruler(c, item): + c.setStrokeColorRGB(0, 0, 1) + c.setFillColorRGB(0, 0, 1) + c.setLineWidth(0.5) + c.setFont("Helvetica", 8) + x_grid = [x*mm for x in range(10, 210, 10)] + y_grid = [y*mm for y in range(10, 297, 10)] + c.grid(x_grid, y_grid) + + for x in range(10, 210, 10): + c.drawString((x-2)*mm, 5*mm, str(x)) + for y in range(10, 297, 10): + c.drawString(4*mm, (y-0.5)*mm, str(y)) + + ofp = Path(original_form) + out_path = ofp.parent / f"{ofp.stem}_ruler.pdf" + + formfill.fill(original_form, ruler, [1], output_file=out_path) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..36c3370 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,44 @@ +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" + +[[package]] +category = "main" +description = "PDF file reader/writer library" +name = "pdfrw" +optional = false +python-versions = "*" +version = "0.4" + +[[package]] +category = "main" +description = "Python Imaging Library (Fork)" +name = "pillow" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.4.1" + +[[package]] +category = "main" +description = "The Reportlab Toolkit" +name = "reportlab" +optional = false +python-versions = "*" +version = "3.5.13" + +[package.dependencies] +pillow = ">=4.0.0" + +[metadata] +content-hash = "02a2ad8ff93804f7fa328d882f2dae2863ffa491831651d312cc368638333ddc" +python-versions = "^3.7" + +[metadata.hashes] +click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] +pdfrw = ["0dc0494a0e6561b268542b28ede2280387c2728114f117d3bb5d8e4787b93ef4", "758289edaa3b672e9a1a67504be73c18ec668d4e5b9d5ac9cbc0dc753d8d196b"] +pillow = ["01a501be4ae05fd714d269cb9c9f145518e58e73faa3f140ddb67fae0c2607b1", "051de330a06c99d6f84bcf582960487835bcae3fc99365185dc2d4f65a390c0e", "07c35919f983c2c593498edcc126ad3a94154184899297cc9d27a6587672cbaa", "0ae5289948c5e0a16574750021bd8be921c27d4e3527800dc9c2c1d2abc81bf7", "0b1efce03619cdbf8bcc61cfae81fcda59249a469f31c6735ea59badd4a6f58a", "0cf0208500df8d0c3cad6383cd98a2d038b0678fd4f777a8f7e442c5faeee81d", "163136e09bd1d6c6c6026b0a662976e86c58b932b964f255ff384ecc8c3cefa3", "18e912a6ccddf28defa196bd2021fe33600cbe5da1aa2f2e2c6df15f720b73d1", "24ec3dea52339a610d34401d2d53d0fb3c7fd08e34b20c95d2ad3973193591f1", "267f8e4c0a1d7e36e97c6a604f5b03ef58e2b81c1becb4fccecddcb37e063cc7", "3273a28734175feebbe4d0a4cde04d4ed20f620b9b506d26f44379d3c72304e1", "39fbd5d62167197318a0371b2a9c699ce261b6800bb493eadde2ba30d868fe8c", "4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d", "4baab2d2da57b0d9d544a2ce0f461374dd90ccbcf723fe46689aff906d43a964", "4c678e23006798fc8b6f4cef2eaad267d53ff4c1779bd1af8725cc11b72a63f3", "4d4bc2e6bb6861103ea4655d6b6f67af8e5336e7216e20fff3e18ffa95d7a055", "505738076350a337c1740a31646e1de09a164c62c07db3b996abdc0f9d2e50cf", "5233664eadfa342c639b9b9977190d64ad7aca4edc51a966394d7e08e7f38a9f", "52e2e56fc3706d8791761a157115dc8391319720ad60cc32992350fda74b6be2", "5337ac3280312aa065ed0a8ec1e4b6142e9f15c31baed36b5cd964745853243f", "5ccd97e0f01f42b7e35907272f0f8ad2c3660a482d799a0c564c7d50e83604d4", "5d95cb9f6cced2628f3e4de7e795e98b2659dfcc7176ab4a01a8b48c2c2f488f", "634209852cc06c0c1243cc74f8fdc8f7444d866221de51125f7b696d775ec5ca", "75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522", "7eda4c737637af74bac4b23aa82ea6fbb19002552be85f0b89bc27e3a762d239", "801ddaa69659b36abf4694fed5aa9f61d1ecf2daaa6c92541bbbbb775d97b9fe", "825aa6d222ce2c2b90d34a0ea31914e141a85edefc07e17342f1d2fdf121c07c", "87fe838f9dac0597f05f2605c0700b1926f9390c95df6af45d83141e0c514bd9", "9c215442ff8249d41ff58700e91ef61d74f47dfd431a50253e1a1ca9436b0697", "a3d90022f2202bbb14da991f26ca7a30b7e4c62bf0f8bf9825603b22d7e87494", "a631fd36a9823638fe700d9225f9698fb59d049c942d322d4c09544dc2115356", "a6523a23a205be0fe664b6b8747a5c86d55da960d9586db039eec9f5c269c0e6", "a756ecf9f4b9b3ed49a680a649af45a8767ad038de39e6c030919c2f443eb000", "ac036b6a6bac7010c58e643d78c234c2f7dc8bb7e591bd8bc3555cf4b1527c28", "b117287a5bdc81f1bac891187275ec7e829e961b8032c9e5ff38b70fd036c78f", "ba04f57d1715ca5ff74bb7f8a818bf929a204b3b3c2c2826d1e1cc3b1c13398c", "ba6ef2bd62671c7fb9cdb3277414e87a5cd38b86721039ada1464f7452ad30b2", "c8939dba1a37960a502b1a030a4465c46dd2c2bca7adf05fa3af6bea594e720e", "cd878195166723f30865e05d87cbaf9421614501a4bd48792c5ed28f90fd36ca", "cee815cc62d136e96cf76771b9d3eb58e0777ec18ea50de5cfcede8a7c429aa8", "d1722b7aa4b40cf93ac3c80d3edd48bf93b9208241d166a14ad8e7a20ee1d4f3", "d7c1c06246b05529f9984435fc4fa5a545ea26606e7f450bdbe00c153f5aeaad", "db418635ea20528f247203bf131b40636f77c8209a045b89fa3badb89e1fcea0", "e1555d4fda1db8005de72acf2ded1af660febad09b4708430091159e8ae1963e", "e9c8066249c040efdda84793a2a669076f92a301ceabe69202446abb4c5c5ef9", "e9f13711780c981d6eadd6042af40e172548c54b06266a1aabda7de192db0838", "f0e3288b92ca5dbb1649bd00e80ef652a72b657dc94989fa9c348253d179054b", "f227d7e574d050ff3996049e086e1f18c7bd2d067ef24131e50a1d3fe5831fbc", "f62b1aeb5c2ced8babd4fbba9c74cbef9de309f5ed106184b12d9778a3971f15", "f71ff657e63a9b24cac254bb8c9bd3c89c7a1b5e00ee4b3997ca1c18100dac28", "fc9a12aad714af36cf3ad0275a96a733526571e52710319855628f476dcb144e"] +reportlab = ["069f684cd0aaa518a27dc9124aed29cee8998e21ddf19604e53214ec8462bdd7", "09b68ec01d86b4b120456b3f3202570ec96f57624e3a4fc36f3829323391daa4", "0c32be9a406172c29ea20ff55a709ccac1e7fb09f15aba67cb7b455fd1d3dbe0", "233196cf25e97cfe7c452524ea29d9a4909f1cb66599299233be1efaaaa7a7a3", "2b5e4533f3e5b962835a5ce44467e66d1ecc822761d1b508077b5087a06be338", "2e860bcdace5a558356802a92ae8658d7e5fdaa00ded82e83a3f2987c562cb66", "3546029e63a9a9dc24ee38959eb417678c2425b96cd27b31e09e216dafc94666", "4452b93f9c73b6b70311e7d69082d64da81b38e91bfb4766397630092e6da6fd", "528c74a1c6527d1859c2c7a64a94a1cba485b00175162ea23699ae58a1e94939", "6116e750f98018febc08dfee6df20446cf954adbcfa378d2c703d56c8864aff3", "6b2b3580c647d75ef129172cb3da648cdb24566987b0b59c5ebb80ab770748d6", "727b5f2bed08552d143fc99649b1863c773729f580a416844f9d9967bb0a1ae8", "74c24a3ec0a3d4f8acb13a07192f45bdb54a1cc3c2286241677e7e8bcd5011fa", "98ccd2f8b4f8636db05f3f14db0b471ad6bb4b66ae0dc9052c4822b3bd5d6a7d", "a5905aa567946bc938b489a7249c7890c3fd3c9b7b5680dece5bc551c2ddbe0d", "acbb7f676b8586b770719e9683eda951fdb38eb7970d46fcbf3cdda88d912a64", "b5e30f865add48cf880f1c363eb505b97f2f7baaa88c155f87a335a76515a3e5", "be2a7c33a2c28bbd3f453ffe4f0e5200b88c803a097f4cf52d69c6b53fad7a8f", "c356bb600f59ac64955813d6497a08bfd5d0c451cb5829b61e3913d0ac084e26", "c7ec4ae2393beab584921b1287a04e94fd98c28315e348362d89b85f4b464546", "d476edc831bb3e9ebd04d1403abaf3ea57b3e4c2276c91a54fdfb6efbd3f9d97", "db059e1a0691c872784062421ec51848539eb4f5210142682e61059a5ca7cc55", "dd423a6753509ab14a0ac1b5be39d219c8f8d3781cce3deb4f45eda31969b5e8", "ed9b7c0d71ce6fe2b31c6cde530ad8238632b876a5d599218739bda142a77f7c", "f0a2465af4006f97b05e1f1546d67d3a3213d414894bf28be7f87f550a7f4a55", "f20bfe26e57e8e1f575a9e0325be04dd3562db9f247ffdd73b5d4df6dec53bc2", "f3463f2cb40a1b515ac0133ba859eca58f53b56760da9abb27ed684c565f853c", "facc3c9748ab1525fb8401a1223bce4f24f0d6aa1a9db86c55db75777ccf40f9"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3fd19c1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[tool.poetry] +name = "pdftools" +version = "0.1.0" +description = "Custom tools for pdf related stuff" +authors = ["Holger Frey "] +license = "Beerware" + +[tool.poetry.dependencies] +python = "^3.7" +reportlab = "^3.5" +pdfrw = "^0.4.0" +click = "^7.0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" + +[tool.poetry.scripts] +nameplates = 'pdftools.cli:nameplates' +addruler = 'pdftools.cli:addruler' + +[tool.black] +line-length = 79 +py37 = true +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.tox + | \.venv + | build + | dist +)/ +''' diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..93ab292 --- /dev/null +++ b/readme.md @@ -0,0 +1,43 @@ +PDF helpers for the work at IMTEK +================================= + +This package provides some cli commands: + + - `nameplates` generate a pdf with nameplates + - `addruler` adds a blue ruler to a pdf + + + +nameplates +---------- + + Usage: nameplates [OPTIONS] ATTENDEES [LOGO] + + creates a pdf with name plate cards + + The attendees file should be a tab separated text file with no headers. + Put the company name in the first column, the given name in the second and + the last name in the third column, e.g: + + Hochimin Enterprizes \t Jane \t Doe \n + + The positioning of the optional logo file can be controlled with the size + and adjustment options. + + Options: + -s, --size INTEGER [default: (10 mm)] + -y, --adjust_y INTEGER [default: ( 0 mm)] + -x, --adjust_x INTEGER [default: ( 0 mm)] + --help Show this message and exit. + + + +addruler +-------- + + Usage: addruler [OPTIONS] PDF + + adds a blue ruler overlay to a pdf file + + Options: + --help Show this message and exit.