translate-rtl-form — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited translate-rtl-form (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Translate a filled PDF form by emptying ONLY its text and refilling the translation. The original form — boxes, shading, lines, logos, every number — stays. For a right-to-left source (Hebrew/Arabic) going to English, MIRROR the whole layout so it reads left-to-right (labels left, values right, columns L→R). This edits the original file; it does NOT rebuild the form.
Two tempting approaches are wrong, in order of temptation:
the output should BE the same form, not a lookalike.
positions reads backwards. You must mirror to LTR.
The proven path: empty the text → rasterize the emptied form → flip it L↔R → re-paste logos upright → write the translated text on top, fitted to each cell. The background is a literal picture of the real form, so fidelity is guaranteed.
source: path to the source PDF.target_language: language to translate into (default: English).PyMuPDF + Pillow via uv (no system pip needed):
uv run --with PyMuPDF --with pillow python <your-script>.pyView output with a PDF-rendering read (renders pages visually). Never trust "it ran".
scripts/pdf_form_engine.py — the reusable, language-agnostic engine:classify_page, extract_spans, span_dump, recover_legacy_text, is_numeric_like, capture_logos, mirror_rect, empty_form, rasterize, fit_text, place_items. Import it; don't reinvent it.
scripts/example_translate.py — a runnable, SYNTHETIC demo of the full enginepipeline (build sample → classify → extract → empty → mirror-flip → refill from a map → report misses). It uses placeholder Latin labels and a generated form so it runs with no font deps or PII; the RTL encoding-detection/decoding is documented in the steps below. Copy it as your starting point and swap in your real source + map.
Open it, note page count, and run classify_page per page:
UNICODE-Hebrew, U+0590–U+05FF) or Arabic(UNICODE-Arabic, U+0600–U+06FF) — use directly.
êåúî) — recover witht.encode('latin1').decode(cp) where cp is cp1255 (Hebrew) or cp1256 (Arabic), then reverse for logical order.
SCANNED/no-text: scanned / no text layer.STOP and tell the user — this skill can't do scanned PDFs (that needs OCR). Success: page count known; encoding identified or scanned-PDF refusal surfaced.
Collect every text span (text + bbox + size). Start with span_dump(page, codepage="cp1255") for Hebrew legacy PDFs or span_dump(page, codepage="cp1256") for Arabic legacy PDFs; omit codepage for real Unicode PDFs. This dump gives text, decoded text, bbox, mirrored x, font, and size for manual maps and corrections.
Classify source-language spans vs numbers. For phrase reconstruction, cluster by line (same y, ≤3px) + small x-gap (≤14px) so header phrases merge but table columns stay separate; reconstruct each phrase in logical order (RTL extraction often returns visual/reversed order, and some PDFs glue a label and adjacent value into one span). Map each phrase → target_language:
DICT) — robust; reports misses when acluster has no entry. Prefer this when the form may vary.
added/removed span shifts every mapping — see gotchas). Carry numbers embedded inside a source span into the translation. Report any unmapped cluster under a MISSES: line — never drop silently. Success: every cluster mapped or misses listed; print to sanity-check.
capture_logos(page) → (xref, rect) per image, so they can be re-pasted upright after the mirror.
empty_form(page, spans) — redacts ALL glyphs with fill=False, keeping shaded backgrounds, lines, and logos; removes only text. Success: page has graphics + logos, no text.
rasterize(page, flip=True) → flipped PNG; place as the new page background. Then re-paste each logo upright with mirror_rect(W, rect) (covers the backwards copy in the raster). Skip the flip (place text at original positions) only if target and source share direction.
place_items(op, items, W, mirror=True) places each item at mirrored x' = W - x1, left-aligned, font shrunk to the gap to the nearest content on the right (floor ~3.4). Numbers use their original text; phrases use the translation. Flowing paragraph regions: place as whole translated text blocks (insert_textbox, check return ≥ 0). Success: 0 misses; text reads L→R; nothing overlaps.
If cells are too small to read, scale the finished page up (~1.5×) by drawing it onto a larger page with show_pdf_page. Keeps layout + order, just bigger.
Render the output and look at every page: overlaps, missing/garbled text, displaced numbers, untranslated spans, reversed numeric sequences (see gotchas). Fix and re-run until clean. Show the user before delivering.
Save the final PDF(s). If you deliver by email or another channel, confirm with the owner first, and ask the reviewer to mark issues by field/page.
When the reviewer returns a punch-list, don't eyeball-nudge:
only exist on one template.
mirrored-x W-x1, size; and the index→map (or decoded-phrase→map) table.
wrong cell, two fields glued) → SUPPRESS the offending span(s) and re-place the pieces with an explicit (text, mirrored-x, baseline-y, size) list read off the dump.
the source (e.g. a months header 1…12) come out reversed (12…1). Use is_numeric_like to detect numeric/date/code runs and re-place them un-mirrored.
Cross-check leave/balance rows with prev + credit = new.
English or numeric value). Treat the glued string as the source key, or suppress the glued span and re-place the label/value manually from span_dump.
uv run --with ... (no venv dance).fill=False.enlarge the page. A duplicated token means a suffix is in both the translation string and an original number span; drop one.
len(spans) == len(map); one added/removed/merged span shifts every mapping. Prefer the dictionary path when the source may change.
same engine. The map is throwaway per document; the engine is the reusable asset.
the un-shifted boxes, the scan for "nearest content to the right" can catch the item's OWN box → width collapses → font shrinks to floor. Compute the gap at the ORIGINAL x, render at the shifted x.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.