Add reMarkable backup system with web UI
- Docker container with rmapi cloud backup, PDF conversion pipeline - Web UI: file browser, multi-select, delete/move, thumbnail preview - Sync: backup from reMarkable cloud, rmdoc→PDF conversion via rmc+Inkscape - Excluded-files mechanism to prevent deleted items from returning after sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
113
remarkable/scripts/backup-official-cloud.sh
Executable file
113
remarkable/scripts/backup-official-cloud.sh
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# reMarkable Backup (offizielle Cloud via rmapi)
|
||||
#
|
||||
# Ziel:
|
||||
# - Inkrementeller Download in "current/".
|
||||
# - Datierte Snapshots in "snapshots/YYYY-MM-DD_HHMMSS/".
|
||||
# - Versionierung via Hardlinks (unveraenderte Dateien belegen kaum Zusatzplatz).
|
||||
#
|
||||
# Nutzung:
|
||||
# ./backup-official-cloud.sh /www/remarkable/notizen /
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
BASE_DIR="${1:-/www/remarkable/notizen}"
|
||||
REMOTE_DIR="${2:-/}"
|
||||
TIMESTAMP="$(date +%F_%H%M%S)"
|
||||
|
||||
CURRENT_DIR="${BASE_DIR}/current"
|
||||
SNAPSHOT_ROOT="${BASE_DIR}/snapshots"
|
||||
SNAPSHOT_DIR="${SNAPSHOT_ROOT}/${TIMESTAMP}"
|
||||
LATEST_LINK="${SNAPSHOT_ROOT}/latest"
|
||||
LOG_DIR="${BASE_DIR}/logs"
|
||||
LOCK_FILE="${BASE_DIR}/.backup.lock"
|
||||
|
||||
# Optional: Pfad, der ein echter Mountpoint sein muss (z. B. /www/remarkable/notizen).
|
||||
# Wenn gesetzt und kein Mountpoint, wird abgebrochen (sicher gegen "auf Root-FS schreiben").
|
||||
MOUNT_CHECK_PATH="${MOUNT_CHECK_PATH:-}"
|
||||
|
||||
# Optional: Anzahl alter Snapshots behalten (0 = unbegrenzt).
|
||||
SNAPSHOT_KEEP="${SNAPSHOT_KEEP:-30}"
|
||||
|
||||
mkdir -p "${CURRENT_DIR}" "${SNAPSHOT_ROOT}" "${LOG_DIR}"
|
||||
|
||||
if [[ -n "${MOUNT_CHECK_PATH}" ]] && ! mountpoint -q "${MOUNT_CHECK_PATH}"; then
|
||||
echo "Fehler: ${MOUNT_CHECK_PATH} ist nicht gemountet. Backup abgebrochen."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v rmapi >/dev/null 2>&1; then
|
||||
echo "Fehler: rmapi nicht im PATH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v rsync >/dev/null 2>&1; then
|
||||
echo "Fehler: rsync fehlt. Bitte installieren."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec 9>"${LOCK_FILE}"
|
||||
if ! flock -n 9; then
|
||||
echo "Backup laeuft bereits (Lock: ${LOCK_FILE})."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_FILE="${LOG_DIR}/backup-${TIMESTAMP}.log"
|
||||
exec > >(tee -a "${LOG_FILE}") 2>&1
|
||||
|
||||
echo "== reMarkable Backup Start =="
|
||||
echo "Base: ${BASE_DIR}"
|
||||
echo "Remote: ${REMOTE_DIR}"
|
||||
echo "Zeit: ${TIMESTAMP}"
|
||||
|
||||
MGET_FLAGS=(-i)
|
||||
if [[ "${BACKUP_FULL:-}" == "1" ]]; then
|
||||
MGET_FLAGS=()
|
||||
fi
|
||||
if [[ "${BACKUP_MIRROR_DELETE:-}" == "1" ]]; then
|
||||
MGET_FLAGS+=(-d)
|
||||
fi
|
||||
|
||||
echo "1) Aktualisiere current/ via rmapi mget ${MGET_FLAGS[*]:-(none)} ..."
|
||||
rmapi mget "${MGET_FLAGS[@]}" -o "${CURRENT_DIR}" "${REMOTE_DIR}"
|
||||
|
||||
# Leere Ordner anlegen (rmapi mget erstellt keine leeren Verzeichnisse)
|
||||
echo "1b) Lege fehlende Ordner an ..."
|
||||
rmapi find "${REMOTE_DIR}" 2>/dev/null | grep '^\[d\]' | sed 's/^\[d\] *//' | while read -r DIR; do
|
||||
LOCAL_DIR="${CURRENT_DIR}/${DIR}"
|
||||
mkdir -p "${LOCAL_DIR}"
|
||||
done
|
||||
|
||||
echo "2) Erzeuge Snapshot: ${SNAPSHOT_DIR}"
|
||||
mkdir -p "${SNAPSHOT_DIR}"
|
||||
|
||||
if [[ -L "${LATEST_LINK}" ]] && [[ -d "$(readlink -f "${LATEST_LINK}")" ]]; then
|
||||
PREV_SNAPSHOT="$(readlink -f "${LATEST_LINK}")"
|
||||
rsync -a --delete --link-dest="${PREV_SNAPSHOT}" "${CURRENT_DIR}/" "${SNAPSHOT_DIR}/"
|
||||
else
|
||||
rsync -a --delete "${CURRENT_DIR}/" "${SNAPSHOT_DIR}/"
|
||||
fi
|
||||
|
||||
ln -sfn "${SNAPSHOT_DIR}" "${LATEST_LINK}"
|
||||
|
||||
if [[ "${SNAPSHOT_KEEP}" =~ ^[0-9]+$ ]] && (( SNAPSHOT_KEEP > 0 )); then
|
||||
echo "3) Pruefe Retention (keep=${SNAPSHOT_KEEP}) ..."
|
||||
mapfile -t SNAPSHOTS < <(ls -1 "${SNAPSHOT_ROOT}" | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}$' | sort)
|
||||
if (( ${#SNAPSHOTS[@]} > SNAPSHOT_KEEP )); then
|
||||
DELETE_COUNT=$(( ${#SNAPSHOTS[@]} - SNAPSHOT_KEEP ))
|
||||
for OLD in "${SNAPSHOTS[@]:0:DELETE_COUNT}"; do
|
||||
rm -rf "${SNAPSHOT_ROOT}/${OLD}"
|
||||
echo " entfernt: ${SNAPSHOT_ROOT}/${OLD}"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# PDF-Konvertierung
|
||||
if command -v remarkable-convert.sh >/dev/null 2>&1; then
|
||||
echo "4) Konvertiere .rmdoc zu PDF ..."
|
||||
remarkable-convert.sh "${CURRENT_DIR}"
|
||||
fi
|
||||
|
||||
echo "== Backup fertig =="
|
||||
echo "Current: ${CURRENT_DIR}"
|
||||
echo "Snapshot: ${SNAPSHOT_DIR}"
|
||||
169
remarkable/scripts/convert-to-pdf.sh
Executable file
169
remarkable/scripts/convert-to-pdf.sh
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env bash
|
||||
# Konvertiert alle .rmdoc-Dateien in /mnt/remarkable/current zu PDFs.
|
||||
# Originale bleiben erhalten. Bereits konvertierte Dateien werden übersprungen.
|
||||
#
|
||||
# Nutzung:
|
||||
# ./convert-to-pdf.sh [BASE_DIR]
|
||||
# BASE_DIR: Verzeichnis mit .rmdoc-Dateien (default: /mnt/remarkable/current)
|
||||
set -euo pipefail
|
||||
|
||||
BASE_DIR="${1:-/mnt/remarkable/current}"
|
||||
TMPDIR_BASE="/tmp/rmdoc_convert"
|
||||
CONVERTED=0
|
||||
SKIPPED=0
|
||||
FAILED=0
|
||||
|
||||
if ! command -v rmc >/dev/null 2>&1; then
|
||||
echo "Fehler: rmc nicht im PATH. Installation: pip3 install rmc --break-system-packages"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v inkscape >/dev/null 2>&1; then
|
||||
echo "Fehler: inkscape nicht im PATH. Installation: apt install inkscape"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v pdfunite >/dev/null 2>&1; then
|
||||
echo "Fehler: pdfunite nicht im PATH. Installation: apt install poppler-utils"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while IFS= read -r -d '' RMDOC; do
|
||||
PDF="${RMDOC%.rmdoc}.pdf"
|
||||
|
||||
# Überspringen falls PDF bereits existiert und neuer als rmdoc
|
||||
if [[ -f "${PDF}" ]] && [[ "${PDF}" -nt "${RMDOC}" ]]; then
|
||||
(( SKIPPED++ )) || true
|
||||
continue
|
||||
fi
|
||||
|
||||
# Primär: rmrl (direkt .rmdoc → PDF, kein Inkscape nötig)
|
||||
if python3 -m rmrl "${RMDOC}" > "${PDF}" 2>/tmp/rmrl_debug.log && [[ -s "${PDF}" ]]; then
|
||||
echo "OK: $(basename "${RMDOC}") (rmrl)"
|
||||
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
|
||||
(( CONVERTED++ )) || true
|
||||
rm -rf "${PDF}" # Platzhalter entfernen falls leer
|
||||
continue
|
||||
fi
|
||||
|
||||
# Primär: rmrl (direkt .rmdoc → PDF, kein Inkscape nötig)
|
||||
if python3 -m rmrl "${RMDOC}" > "${PDF}" 2>/tmp/rmrl_debug.log && [[ -s "${PDF}" ]]; then
|
||||
echo "OK: $(basename "${RMDOC}") (rmrl)"
|
||||
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
|
||||
(( CONVERTED++ )) || true
|
||||
continue
|
||||
fi
|
||||
|
||||
TMPDIR="${TMPDIR_BASE}/$$_$(echo "${RMDOC%.rmdoc}" | md5sum | cut -c1-8)"
|
||||
mkdir -p "${TMPDIR}"
|
||||
|
||||
# rmdoc entpacken
|
||||
if ! unzip -o -q "${RMDOC}" -d "${TMPDIR}" 2>/dev/null; then
|
||||
echo "WARN: Konnte nicht entpacken: ${RMDOC}"
|
||||
rm -rf "${TMPDIR}"
|
||||
(( FAILED++ )) || true
|
||||
continue
|
||||
fi
|
||||
|
||||
# Seitenreihenfolge aus .content lesen, sonst alphabetisch
|
||||
RM_FILES=()
|
||||
CONTENT_FILE="$(find "${TMPDIR}" -maxdepth 1 -name "*.content" | head -1)"
|
||||
DOC_UUID="$(basename "${CONTENT_FILE%.content}")"
|
||||
RM_DIR="${TMPDIR}/${DOC_UUID}"
|
||||
|
||||
if [[ -f "${CONTENT_FILE}" ]] && [[ -d "${RM_DIR}" ]]; then
|
||||
# Seiten-UUIDs aus .content in Reihenfolge extrahieren
|
||||
mapfile -t PAGE_IDS < <(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.load(open('${CONTENT_FILE}'))
|
||||
pages = data.get('cPages', {}).get('pages', [])
|
||||
for p in pages:
|
||||
print(p['id'])
|
||||
except Exception as e:
|
||||
sys.exit(1)
|
||||
" 2>/dev/null)
|
||||
|
||||
RM_FILES=()
|
||||
for PAGE_ID in "${PAGE_IDS[@]}"; do
|
||||
RM_FILE="${RM_DIR}/${PAGE_ID}.rm"
|
||||
if [[ -f "${RM_FILE}" ]]; then
|
||||
RM_FILES+=("${RM_FILE}")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Fallback: alle .rm-Dateien alphabetisch
|
||||
if [[ ${#RM_FILES[@]} -eq 0 ]]; then
|
||||
mapfile -t RM_FILES < <(find "${TMPDIR}" -name "*.rm" | sort)
|
||||
fi
|
||||
|
||||
EMBEDDED_PDF="$(find "${TMPDIR}" -maxdepth 2 -name "*.pdf" | head -1)"
|
||||
|
||||
if [[ ${#RM_FILES[@]} -eq 0 ]]; then
|
||||
# Kein Handschrift-Inhalt — eingebettete PDF direkt nutzen
|
||||
if [[ -n "${EMBEDDED_PDF}" ]]; then
|
||||
cp "${EMBEDDED_PDF}" "${PDF}"
|
||||
echo "OK: $(basename "${RMDOC}") (eingebettete PDF)"
|
||||
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
|
||||
(( CONVERTED++ )) || true
|
||||
else
|
||||
(( SKIPPED++ )) || true
|
||||
fi
|
||||
rm -rf "${TMPDIR}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Annotiertes Import-Dokument: Annotations auf Original-PDF legen
|
||||
if [[ -n "${EMBEDDED_PDF}" ]] && [[ ${#RM_FILES[@]} -gt 0 ]]; then
|
||||
if python3 /usr/local/bin/remarkable-merge.py "${TMPDIR}" "${EMBEDDED_PDF}" "${PDF}" 2>/tmp/merge_debug.log && [[ -s "${PDF}" ]]; then
|
||||
echo "OK: $(basename "${RMDOC}") (PDF + Annotationen)"
|
||||
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
|
||||
(( CONVERTED++ )) || true
|
||||
else
|
||||
cp "${EMBEDDED_PDF}" "${PDF}"
|
||||
echo "WARN: Merge fehlgeschlagen ($(cat /tmp/merge_debug.log | tail -1)), nutze Original-PDF: $(basename "${RMDOC}")"
|
||||
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
|
||||
(( CONVERTED++ )) || true
|
||||
fi
|
||||
rm -rf "${TMPDIR}"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Jede Seite einzeln konvertieren, dann zusammenfügen
|
||||
PAGE_PDFS=()
|
||||
CONVERT_OK=true
|
||||
for RM_FILE in "${RM_FILES[@]}"; do
|
||||
PAGE_PDF="${TMPDIR}/$(basename "${RM_FILE%.rm}").pdf"
|
||||
if rmc -t pdf -o "${PAGE_PDF}" "${RM_FILE}" 2>/dev/null && [[ -s "${PAGE_PDF}" ]]; then
|
||||
PAGE_PDFS+=("${PAGE_PDF}")
|
||||
else
|
||||
CONVERT_OK=false
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${CONVERT_OK}" == true ]] && [[ ${#PAGE_PDFS[@]} -gt 0 ]]; then
|
||||
if [[ ${#PAGE_PDFS[@]} -eq 1 ]]; then
|
||||
cp "${PAGE_PDFS[0]}" "${PDF}"
|
||||
else
|
||||
pdfunite "${PAGE_PDFS[@]}" "${PDF}" 2>/dev/null
|
||||
fi
|
||||
if [[ -s "${PDF}" ]]; then
|
||||
echo "OK: $(basename "${RMDOC}") (${#PAGE_PDFS[@]} Seiten)"
|
||||
pdftoppm -r 72 -jpeg -singlefile "${PDF}" "${PDF%.pdf}.thumb" 2>/dev/null || true
|
||||
(( CONVERTED++ )) || true
|
||||
else
|
||||
rm -f "${PDF}"
|
||||
echo "WARN: Leere PDF für: $(basename "${RMDOC}")"
|
||||
(( FAILED++ )) || true
|
||||
fi
|
||||
else
|
||||
rm -f "${PDF}"
|
||||
echo "WARN: Konvertierung fehlgeschlagen: $(basename "${RMDOC}")"
|
||||
(( FAILED++ )) || true
|
||||
fi
|
||||
|
||||
rm -rf "${TMPDIR}"
|
||||
|
||||
done < <(find "${BASE_DIR}" -name "*.rmdoc" -print0)
|
||||
|
||||
echo "== Konvertierung fertig: ${CONVERTED} konvertiert, ${SKIPPED} übersprungen, ${FAILED} fehlgeschlagen =="
|
||||
142
remarkable/scripts/merge-annotations.py
Executable file
142
remarkable/scripts/merge-annotations.py
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys, os, subprocess, re, shutil, json, tempfile, struct
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
INKSCAPE_TIMEOUT = 30
|
||||
|
||||
def fallback_page(reader, i, path):
|
||||
w = PdfWriter()
|
||||
w.add_page(reader.pages[i])
|
||||
with open(path, 'wb') as fh:
|
||||
w.write(fh)
|
||||
|
||||
tmpdir = sys.argv[1]
|
||||
bg_pdf = sys.argv[2]
|
||||
output = sys.argv[3]
|
||||
|
||||
content_file = next(
|
||||
(os.path.join(tmpdir, f) for f in os.listdir(tmpdir) if f.endswith('.content')), None
|
||||
)
|
||||
if not content_file:
|
||||
sys.exit(1)
|
||||
|
||||
with open(content_file) as f:
|
||||
data = json.load(f)
|
||||
|
||||
doc_uuid = os.path.basename(content_file)[:-8]
|
||||
rm_dir = os.path.join(tmpdir, doc_uuid)
|
||||
page_ids = [p['id'] for p in data.get('cPages', {}).get('pages', [])]
|
||||
|
||||
page_rm = {}
|
||||
for i, pid in enumerate(page_ids):
|
||||
rm = os.path.join(rm_dir, pid + '.rm')
|
||||
if os.path.exists(rm):
|
||||
page_rm[i] = rm
|
||||
|
||||
if not page_rm:
|
||||
sys.exit(1)
|
||||
|
||||
reader = PdfReader(bg_pdf)
|
||||
n_pages = len(reader.pages)
|
||||
work = tempfile.mkdtemp()
|
||||
parts = []
|
||||
|
||||
for i in range(n_pages):
|
||||
part_out = os.path.join(work, 'p' + str(i) + '.pdf')
|
||||
|
||||
if i not in page_rm:
|
||||
fallback_page(reader, i, part_out)
|
||||
parts.append(part_out)
|
||||
continue
|
||||
|
||||
try:
|
||||
# PDF-Seitengröße in Punkten
|
||||
pdf_w_pt = float(reader.pages[i].mediabox.width)
|
||||
pdf_h_pt = float(reader.pages[i].mediabox.height)
|
||||
dpi = 150
|
||||
png_w = int(round(pdf_w_pt * dpi / 72))
|
||||
png_h = int(round(pdf_h_pt * dpi / 72))
|
||||
|
||||
# Bg-Seite als PNG rendern
|
||||
bg_base = os.path.join(work, 'bg' + str(i))
|
||||
subprocess.run(
|
||||
['pdftoppm', '-r', str(dpi), '-png', '-f', str(i+1), '-l', str(i+1),
|
||||
'-singlefile', bg_pdf, bg_base],
|
||||
capture_output=True, timeout=20
|
||||
)
|
||||
bg_png = bg_base + '.png'
|
||||
if not os.path.exists(bg_png):
|
||||
raise FileNotFoundError('PNG fehlt')
|
||||
|
||||
# .rm -> SVG
|
||||
svg_out = os.path.join(work, 'a' + str(i) + '.svg')
|
||||
r = subprocess.run(['rmc', '-t', 'svg', '-o', svg_out, page_rm[i]],
|
||||
capture_output=True, timeout=20)
|
||||
if r.returncode != 0 or not os.path.exists(svg_out):
|
||||
raise RuntimeError('rmc fehlgeschlagen')
|
||||
|
||||
svg = open(svg_out).read()
|
||||
|
||||
# Weissen Hintergrund entfernen
|
||||
svg = re.sub(
|
||||
r'<rect\b[^>]*/?>',
|
||||
lambda m: '' if re.search(r'fill\s*[=:]\s*["\']?\s*(?:white|#fff(?:fff)?)', m.group(0), re.I) else m.group(0),
|
||||
svg
|
||||
)
|
||||
|
||||
# ViewBox X-Offset auslesen
|
||||
vb = re.search(r'viewBox=["\']([^"\']+)["\']', svg)
|
||||
vb_vals = [float(x) for x in vb.group(1).split()] if vb else [0, 0, pdf_w_pt, pdf_h_pt]
|
||||
vb_x = vb_vals[0]
|
||||
vb_y = vb_vals[1]
|
||||
|
||||
# Inneren SVG-Inhalt extrahieren
|
||||
inner_m = re.search(r'<svg[^>]*>(.*)</svg>', svg, re.DOTALL)
|
||||
inner = inner_m.group(1) if inner_m else ''
|
||||
|
||||
# Annotation-Koordinaten sind in PDF-Punkten
|
||||
# Skalierung: PDF-Punkte -> PNG-Pixel
|
||||
pts_to_px = png_w / pdf_w_pt
|
||||
# reMarkable Seitenrand proportional zur PDF-Breite (empirisch: 75.5pt bei A4)
|
||||
rm_margin_left = 75.5 * pdf_w_pt / 595.275
|
||||
# reMarkable Seitenrand proportional zur PDF-Breite (empirisch: 75.5pt bei A4)
|
||||
rm_margin_left = 75.5 * pdf_w_pt / 595.275
|
||||
|
||||
# Composite SVG
|
||||
comp_svg = os.path.join(work, 'c' + str(i) + '.svg')
|
||||
with open(comp_svg, 'w') as fh:
|
||||
fh.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||
fh.write('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"')
|
||||
fh.write(' width="' + str(pdf_w_pt) + 'pt"')
|
||||
fh.write(' height="' + str(pdf_h_pt) + 'pt"')
|
||||
fh.write(' viewBox="0 0 ' + str(png_w) + ' ' + str(png_h) + '">\n')
|
||||
fh.write(' <image xlink:href="file://' + bg_png + '" href="file://' + bg_png + '"')
|
||||
fh.write(' width="' + str(png_w) + '" height="' + str(png_h) + '"/>\n')
|
||||
fh.write(' <g transform="scale(' + str(pts_to_px) + ',' + str(pts_to_px) + ')')
|
||||
fh.write(' translate(' + str(-vb_x + rm_margin_left) + ',' + str(-vb_y) + ')">\n')
|
||||
fh.write(' ' + inner + '\n')
|
||||
fh.write(' </g>\n')
|
||||
fh.write('</svg>')
|
||||
|
||||
# Composite -> PDF
|
||||
r = subprocess.run(
|
||||
['inkscape', comp_svg, '--export-type=pdf', '--export-filename=' + part_out],
|
||||
capture_output=True, timeout=INKSCAPE_TIMEOUT
|
||||
)
|
||||
if r.returncode != 0 or not os.path.exists(part_out) or os.path.getsize(part_out) < 100:
|
||||
raise RuntimeError('inkscape fehlgeschlagen')
|
||||
|
||||
parts.append(part_out)
|
||||
print(' Seite ' + str(i+1) + ' mit Annotation', flush=True)
|
||||
|
||||
except Exception as e:
|
||||
print(' Seite ' + str(i+1) + ' Fallback: ' + str(e), flush=True)
|
||||
fallback_page(reader, i, part_out)
|
||||
parts.append(part_out)
|
||||
|
||||
if len(parts) == 1:
|
||||
shutil.copy(parts[0], output)
|
||||
elif parts:
|
||||
subprocess.run(['pdfunite'] + parts + [output], check=True)
|
||||
|
||||
shutil.rmtree(work)
|
||||
Reference in New Issue
Block a user