#!/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']*/?>', 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, re.DOTALL) inner = inner_m.group(1) if inner_m else '' # GlyphRange-Highlights (Textmarkierungen) aus .rm lesen highlight_svg = '' try: import rmscene RM_SCALE = 72.0 / 226 # rmscene screen-px → SVG-Punkte (wie rmc) HIGHLIGHT_COLORS = { 9: ('rgb(255,235,0)', 0.4), # HIGHLIGHT (gelb) 3: ('rgb(251,247,25)', 0.4), # YELLOW 4: ('rgb(0,255,0)', 0.4), # GREEN 5: ('rgb(255,192,203)', 0.4), # PINK 6: ('rgb(78,105,201)', 0.4), # BLUE 7: ('rgb(179,62,57)', 0.4), # RED } with open(page_rm[i], 'rb') as rmf: blocks = list(rmscene.read_blocks(rmf)) for block in blocks: if not (hasattr(block, 'item') and hasattr(block.item, 'value')): continue glyph = block.item.value if not hasattr(glyph, 'rectangles'): continue try: color_id = int(glyph.color) except Exception: color_id = 9 fill, opacity = HIGHLIGHT_COLORS.get(color_id, ('rgb(255,235,0)', 0.4)) for rect in glyph.rectangles: highlight_svg += ( f'\n' ) except Exception as e: print(f' GlyphRange Fehler: {e}', flush=True) # Highlights VOR den Stift-Annotationen einblenden if highlight_svg: print(f' {highlight_svg.count(" 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('\n') fh.write('\n') fh.write(' \n') fh.write(' \n') fh.write(' ' + inner + '\n') fh.write(' \n') fh.write('') # 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)