✨ initial commit
This commit is contained in:
commit
4dc9f46879
4 changed files with 190 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/pdfsign.venv
|
||||
/*.png
|
||||
/*.pdf
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Yavook.de
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
64
pdfsign.py
Normal file
64
pdfsign.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env python3
|
||||
# make_text_invisible.py
|
||||
# Usage: python make_text_invisible.py text.pdf out.pdf
|
||||
# Produces a single-page PDF identical visually but with text made invisible (selectable/searchable).
|
||||
# Requires: pip install pymupdf
|
||||
|
||||
import sys
|
||||
import fitz # pymupdf
|
||||
import os
|
||||
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: python make_text_invisible.py text.pdf sig.png out.pdf", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
in_path, sig_path, out_path = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
|
||||
doc = fitz.open(in_path)
|
||||
if doc.page_count < 1:
|
||||
print("Input has no pages", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# operate only on first page (single-page input assumed)
|
||||
page = doc.load_page(0)
|
||||
w, h = page.rect.width, page.rect.height
|
||||
|
||||
# create new doc and a blank page with same size
|
||||
out = fitz.open()
|
||||
newp = out.new_page(width=w, height=h)
|
||||
|
||||
# # copy original page's visible content as an image to preserve exact appearance
|
||||
# pix = page.get_pixmap(matrix=fitz.Matrix(1, 1), alpha=False)
|
||||
# img_bytes = pix.tobytes("png")
|
||||
# newp.insert_image(fitz.Rect(0, 0, w, h), stream=img_bytes)
|
||||
|
||||
newp.insert_image(newp.rect, filename=sig_path, keep_proportion=False)
|
||||
|
||||
# extract text spans (positions, fonts, sizes)
|
||||
td = page.get_text("dict")
|
||||
|
||||
for block in td.get("blocks", []):
|
||||
for line in block.get("lines", []):
|
||||
for span in line.get("spans", []):
|
||||
|
||||
txt = span.get("text", "")
|
||||
if not txt:
|
||||
continue
|
||||
|
||||
origin = span.get("origin")
|
||||
if origin:
|
||||
x, y = origin[0], origin[1]
|
||||
else:
|
||||
bbox = span.get("bbox", [0,0,0,0])
|
||||
x, y = bbox[0], bbox[3]
|
||||
|
||||
# Use PDF invisible text rendering: render_mode=3 (neither fill nor stroke) is invisible but selectable.
|
||||
try:
|
||||
size = span.get("size", 12)
|
||||
newp.insert_text((x, y), txt, fontsize=size, color=(0, 0, 0, 0), render_mode=3)
|
||||
except Exception:
|
||||
print(f"Failed to insert text: '{txt}'")
|
||||
|
||||
out.save(out_path, garbage=4, deflate=True)
|
||||
out.close()
|
||||
doc.close()
|
||||
102
pdfsign.sh
Executable file
102
pdfsign.sh
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
#!/bin/bash
|
||||
|
||||
#################
|
||||
# PREREQUISITES #
|
||||
#################
|
||||
|
||||
# executables in PATH
|
||||
for prereq in "python3" "gs" "pdftk"; do
|
||||
if ! command -v "${prereq}" &> /dev/null; then
|
||||
echo "required executable '${prereq}' not found!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# python virtual env with PyMuPDF
|
||||
VENV_DIR="pdfsign.venv"
|
||||
if [ -d "${VENV_DIR}" ]; then
|
||||
# venv exists, activate it
|
||||
source "${VENV_DIR}/bin/activate"
|
||||
else
|
||||
for prereq in "venv"; do
|
||||
if ! python3 -c "import ${prereq}" &> /dev/null; then
|
||||
echo "python module '${prereq}' not found!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# create venv and activate
|
||||
python3 -m "venv" "${VENV_DIR}"
|
||||
source "${VENV_DIR}/bin/activate"
|
||||
# install PyMuPDF
|
||||
python3 -m "pip" install PyMuPDF
|
||||
fi
|
||||
|
||||
|
||||
############
|
||||
# CLI ARGS #
|
||||
############
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: ${0} <input.pdf> <signed.png> [PAGE]"
|
||||
echo
|
||||
echo "Replaces page PAGE with the signed version."
|
||||
echo "If PAGE is not given, the last page is replaced."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IN_TEXT_PDF="${1}"
|
||||
IN_SCAN_IMG="${2}"
|
||||
|
||||
OUT_PDF="${IN_TEXT_PDF%%.*}_pdfsign.pdf"
|
||||
|
||||
IN_TEXT_PDF_LEN="$( pdftk "${IN_TEXT_PDF}" dump_data | grep NumberOfPages | awk '{print $2}' )"
|
||||
IN_PAGE="${3:-${IN_TEXT_PDF_LEN}}"
|
||||
|
||||
RANGES="B1"
|
||||
if [ ${IN_PAGE} -gt 1 ]; then
|
||||
RANGES="A1-$(( IN_PAGE - 1 )) ${RANGES}"
|
||||
fi
|
||||
|
||||
if [ ${IN_PAGE} -lt ${IN_TEXT_PDF_LEN} ]; then
|
||||
RANGES="${RANGES} A$(( IN_PAGE + 1 ))-${IN_TEXT_PDF_LEN}"
|
||||
fi
|
||||
|
||||
|
||||
################
|
||||
# MAIN PROGRAM #
|
||||
################
|
||||
|
||||
GSFLAGS="-sDEVICE=pdfwrite -dSAFER -dNOPAUSE -dQUIET -dBATCH"
|
||||
|
||||
set -e
|
||||
|
||||
# extract IN_PAGE (text-only) from IN_TEXT_PDF
|
||||
gs ${GSFLAGS} \
|
||||
-dFirstPage="${IN_PAGE}" -dLastPage="${IN_PAGE}" \
|
||||
-dFILTERIMAGE -dFILTERVECTOR -dFILTERTEXT=false \
|
||||
-sOutputFile=".pdfsign_textonly.pdf" \
|
||||
"${IN_TEXT_PDF}"
|
||||
|
||||
# use IN_SCAN_IMG as background, layering text from extracted page on top
|
||||
python3 ./pdfsign.py \
|
||||
".pdfsign_textonly.pdf" "${IN_SCAN_IMG}" \
|
||||
".pdfsign_signed.pdf"
|
||||
|
||||
rm ".pdfsign_textonly.pdf"
|
||||
|
||||
# replace IN_PAGE in IN_TEXT_PDF layered page
|
||||
pdftk A="${IN_TEXT_PDF}" B=".pdfsign_signed.pdf" \
|
||||
cat ${RANGES} \
|
||||
output ".pdfsign_concat.pdf"
|
||||
|
||||
rm ".pdfsign_signed.pdf"
|
||||
|
||||
# postprocess/compress pdf
|
||||
gs ${GSFLAGS} \
|
||||
-sPAPERSIZE=a4 -dFIXEDMEDIA -dPDFFitPage \
|
||||
-dCompatibilityLevel=1.7 -dPDFSETTINGS=/ebook \
|
||||
-sOutputFile="${OUT_PDF}" \
|
||||
".pdfsign_concat.pdf"
|
||||
|
||||
rm ".pdfsign_concat.pdf"
|
||||
Loading…
Reference in a new issue