Translate rtl form
Two portable agent skills (SKILL.md format) for Hebrew/Arabic RTL work: authoring new RTL content (HTML/PDF/Markdown) and translating existing RTL PDF forms to LTR in place. Usable by any AI agent.
npx -y skills add Nitzan94/agent-rtl-skills --skill translate-rtl-formAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Translate filled, text-based RTL PDF forms while preserving the original boxes, lines, logos, and numbers. Use for Hebrew/Arabic payslips, certificates, tax forms, invoices, and official forms that need an English/LTR version with the same layout. Not for scanned PDFs, prose PDFs, or authoring new RTL documents.
SKILL.md
8.3 KB, as published. Nobody here has run it
Translate an RTL PDF Form In Place (RTL → LTR mirror)
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.
Why this approach (and what it rejects)
Two tempting approaches are wrong, in order of temptation:
- Rebuild the form from scratch (e.g. in HTML) — loses fidelity to the original; the output should BE the same form, not a lookalike.
- Translate in place at the original RTL coordinates — English placed at RTL 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.
Inputs
source: path to the source PDF.target_language: language to translate into (default: English).
Setup
PyMuPDF + Pillow via uv (no system pip needed):
uv run --with PyMuPDF --with pillow python <your-script>.py
View output with a PDF-rendering read (renders pages visually). Never trust "it ran".
Files in this skill
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 engine pipeline (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.
Steps
1. Get the source PDF and classify it
Open it, note page count, and run classify_page per page:
- Real Unicode Hebrew (
UNICODE-Hebrew, U+0590–U+05FF) or Arabic (UNICODE-Arabic, U+0600–U+06FF) — use directly. - Legacy codepage read as Latin-1 (gibberish like
êåúî) — recover witht.encode('latin1').decode(cp)wherecpiscp1255(Hebrew) orcp1256(Arabic), then reverse for logical order. - No text + images present →
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.
2. Extract spans + build the translation map
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:
- Map by decoded phrase → dictionary (
DICT) — robust; reports misses when a cluster has no entry. Prefer this when the form may vary. - Map by first-seen index only for a frozen, known source (brittle: one
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.
3. Capture logos
capture_logos(page) → (xref, rect) per image, so they can be re-pasted upright
after the mirror.
4. Empty the form
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.
5. Mirror the form (RTL→LTR)
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.
6. Refill the text (LTR, fit-to-cell)
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.
7. Enlarge if dense
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.
8. Render and VISUALLY verify — iterate
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.
9. Deliver
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.
10. Review-round corrections loop
When the reviewer returns a punch-list, don't eyeball-nudge:
- Confirm WHICH file each note is about — disambiguate by field names/values that only exist on one template.
- Dump the ground truth: source spans sorted by (y, x) with text, bbox,
mirrored-x
W-x1, size; and the index→map (or decoded-phrase→map) table. - Classify each note: wording → edit the one map string; layout (value in the
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. - Re-render and visually confirm EVERY item each round before replying.
Gotchas
- Mirror reverses already-LTR runs. Numeric sequences that read left-to-right in
the source (e.g. a months header
1…12) come out reversed (12…1). Useis_numeric_liketo detect numeric/date/code runs and re-place them un-mirrored. - RTL extraction returns VISUAL (reversed) order — re-map to logical, don't copy.
Cross-check leave/balance rows with
prev + credit = new. - Some PDFs glue label + value into one span (for example, a Hebrew label plus an
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. pipmay be unavailable — useuv run --with ...(no venv dance).- White-fill redaction punches holes in shaded headers — use
fill=False. - Target language is usually longer than RTL source — expect to shrink fonts or enlarge the page. A duplicated token means a suffix is in both the translation string and an original number span; drop one.
- Index-mapping is brittle. It rests on
len(spans) == len(map); one added/ removed/merged span shifts every mapping. Prefer the dictionary path when the source may change. - One map per template. Different employers/forms need their own map but share the same engine. The map is throwaway per document; the engine is the reusable asset.
- Fit-size self-box trap. If you shift an item's x then compute its width against 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.