102 Zeilen
4 KiB
Python
Ausführbare Datei
102 Zeilen
4 KiB
Python
Ausführbare Datei
#!/usr/bin/env python3
|
|
|
|
#Dependencies: python3.7 or later and gs (Ghostscript)
|
|
|
|
import argparse, os, re, subprocess, sys, tempfile
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Create a correctly cropped PDF from one line of text. Only Latin-1 character set is supported.")
|
|
parser.add_argument("-t", "--text", required=True, help="Text to be converted to PDF.")
|
|
parser.add_argument("-o", "--output", required=True, help="Output PDF file.")
|
|
parser.add_argument("-s", "--size", type=int, default=12, help="Font size in points (default 12).")
|
|
parser.add_argument("-m", "--margin", type=int, default=1, help="Margin in points (default 1).")
|
|
args = parser.parse_args()
|
|
text_to_pdf(args.text, args.output, args.size, args.margin)
|
|
|
|
# Keep this function in sync with pdf-sign
|
|
def text_to_pdf(text, output, size, margin):
|
|
# Validate text
|
|
text_chars=set(map(ord, text))
|
|
latin1_chars=set([*range(0x20, 0x7f), *range(0xa0, 0x100)])
|
|
if not text_chars.issubset(latin1_chars):
|
|
die(f"Error: Only non-control latin-1 characters are supported. Unsupported characters: {', '.join(map(hex, text_chars - latin1_chars))}")
|
|
text.encode('latin-1').decode('latin-1') # Assertion. E.i., an exception here indicates a bug.
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
# Write postscript file
|
|
ps_file=os.path.join(tempdir, "file.ps")
|
|
text_len=len(text)
|
|
w=text_len * size + margin * 2
|
|
h=size * 3 + margin * 2
|
|
x=size + margin
|
|
y=size + margin
|
|
ps_text = (text
|
|
.replace("\\", "\\\\")
|
|
.replace("(", "\\(")
|
|
.replace(")", "\\)")
|
|
)
|
|
def write_ps():
|
|
with open(ps_file, "w", encoding="latin-1") as f:
|
|
f.write('\n'.join([
|
|
"%!PS-Adobe-3.0",
|
|
f"%%BoundingBox: 0 0 {w} {h}",
|
|
"%%Pages: 1",
|
|
"%%EndComments",
|
|
"%%Page: 1 1",
|
|
f"/DejaVuSansMono findfont {size} scalefont setfont",
|
|
f"{x} {y} moveto",
|
|
f"({ps_text}) show",
|
|
"showpage"]))
|
|
# Write postscript file with too big bounding box
|
|
write_ps()
|
|
# Get correct bounding box
|
|
bbox_result = subprocess.run([
|
|
"gs", '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
|
"-sDEVICE=bbox",
|
|
ps_file
|
|
], stderr=subprocess.PIPE, text=True, check=True)
|
|
bbox = m(r'.*%%HiResBoundingBox: (\d+(\.\d+)?) (\d+(\.\d+)?) (\d+(\.\d+)?) (\d+(\.\d+)?)\n.*', bbox_result.stderr)
|
|
if not bbox:
|
|
die("Error: Unable to extract bounding box.")
|
|
# Adjust variables for bounding box
|
|
llx, lly, urx, ury = float(bbox[1]), float(bbox[3]), float(bbox[5]), float(bbox[7])
|
|
llx, lly, urx, ury = llx - margin, lly - margin, urx + margin, ury + margin
|
|
w=urx - llx
|
|
h=ury - lly
|
|
x-=llx
|
|
y-=lly
|
|
# Write postscript file with correct bounding box
|
|
write_ps()
|
|
# Convert to PDF
|
|
gs_cmd = [
|
|
"gs", '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
|
"-o", output,
|
|
"-sDEVICE=pdfwrite",
|
|
f"-dDEVICEWIDTHPOINTS={w}",
|
|
f"-dDEVICEHEIGHTPOINTS={h}",
|
|
"-dFIXEDMEDIA",
|
|
"-c", "[ /PAGES pdfmark",
|
|
"-f", ps_file
|
|
]
|
|
subprocess.run(gs_cmd, check=True, stdout=subprocess.DEVNULL)
|
|
|
|
def m(pattern, string):
|
|
match = re.match(pattern, string, re.DOTALL)
|
|
if not match:
|
|
return None
|
|
if(match.group(0) != string):
|
|
die(f'Pattern /${pattern}/ matched only part of string')
|
|
ret = []
|
|
for index in range((match.lastindex or 0)+1):
|
|
ret.append(match.group(index))
|
|
return ret
|
|
|
|
def die(reason):
|
|
print(reason, file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
pyver = sys.version_info
|
|
if not (pyver.major == 3 and pyver.minor >= 7):
|
|
die("Requires python 3.7 or later")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|