feat(rtl): rendu droite-a-gauche complet pour Word, PowerPoint, Excel et PDF
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m37s

- source unique RTL_LANGUAGES/is_rtl dans core/languages.py (fin des 3 copies)
- Word: bidi partout (corps, tableaux bidiVisual, notes/fin/commentaires,
  zones de texte, 6 zones d'en-tetes/pieds), insertion OOXML ordonnee,
  polices cs elargies aux 11 langues
- PowerPoint: alignements explicites preserves, notes du presentateur,
  indice de police <a:cs> insert a sa place
- Excel: feuilles affichees de droite a gauche, feuilles graphiques ignorees
- PDF: faconnage bidi (arabic-reshaper + python-bidi), polices par ecriture
  (arabe/hebreu), TTF enregistree pour le PDF recompose
- 46 tests nouveaux (tests/test_translators/test_rtl_layout.py), 253 au total
This commit is contained in:
2026-09-01 20:22:46 +02:00
parent fddd7b7428
commit f22f645fab
12 changed files with 1727 additions and 80 deletions

View File

@@ -20,16 +20,30 @@ from pptx.shapes.group import GroupShape
from pptx.enum.shapes import MSO_SHAPE_TYPE
from services.providers.base import TranslationProvider
from core.languages import is_rtl
# DrawingML namespace used by pptx XML
_NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main"
# Languages written right-to-left
RTL_LANGUAGES: frozenset = frozenset(
{"ar", "he", "fa", "ur", "ku", "ps", "ug", "sd", "yi", "dv", "ckb"}
# a:rPr children that must come AFTER <a:latin>/<a:ea>/<a:cs> per the
# DrawingML CT_TextCharacterProperties sequence. Inserting <a:ea>/<a:cs>
# before them keeps the XML schema-valid (PowerPoint repairs otherwise).
_A_RPR_HINT_SUCCESSORS = tuple(
f"{{{_NS_A}}}{tag}"
for tag in ("sym", "hlinkClick", "hlinkMouseOver", "rtl", "extLst")
)
def _insert_a_rpr_child(rPr, child) -> None:
"""Insert child into a:rPr at its schema position (before sym,
hlink*, rtl and extLst — or appended at the end when none exist)."""
for existing in rPr:
if existing.tag in _A_RPR_HINT_SUCCESSORS:
existing.addprevious(child)
return
rPr.append(child)
from core.logging import get_logger
logger = get_logger(__name__)
@@ -58,8 +72,10 @@ def _set_pptx_paragraph_rtl(paragraph) -> None:
"""
Enable RTL mode on a PowerPoint paragraph.
Sets rtl="1" and algn="r" on the <a:pPr> element, which controls
both text direction and horizontal alignment in DrawingML.
Sets rtl="1" on the <a:pPr> element, which controls the paragraph
text direction. The horizontal alignment (algn) is only forced to
"r" when the paragraph has no explicit alignment or is aligned
"l" (left) — a centered or justified title keeps its alignment.
"""
p_elem = paragraph._p
tag_pPr = f"{{{_NS_A}}}pPr"
@@ -68,14 +84,20 @@ def _set_pptx_paragraph_rtl(paragraph) -> None:
pPr = etree.Element(tag_pPr)
p_elem.insert(0, pPr)
pPr.set("rtl", "1")
pPr.set("algn", "r")
if pPr.get("algn") in (None, "", "l"):
pPr.set("algn", "r")
def _apply_rtl_to_presentation(presentation: Presentation) -> None:
"""Apply RTL direction to every paragraph in all slides."""
"""Apply RTL direction to every paragraph in all slides and notes."""
for slide in presentation.slides:
for shape in slide.shapes:
_apply_rtl_to_shape(shape)
# Speaker notes are translated like any other text — they read
# right-to-left too when the target language does.
if slide.has_notes_slide and slide.notes_slide.notes_text_frame is not None:
for paragraph in slide.notes_slide.notes_text_frame.paragraphs:
_set_pptx_paragraph_rtl(paragraph)
def _apply_rtl_to_shape(shape) -> None:
@@ -111,11 +133,10 @@ def _ea_typeface_for_target(target_language: str):
return _EA_TYPEFACES.get(code) or _EA_TYPEFACES.get(base)
def _apply_ea_font_hints(presentation: Presentation, target_language: str) -> None:
"""Set the <a:ea> (east-asian) typeface on every run for CJK targets.
Blanket application is safe — the hint only affects CJK glyphs.
"""
def _apply_run_typeface_hint(
presentation: Presentation, tag: str, typeface: str, log_event: str
) -> None:
"""Set the <a:{tag}> typeface hint on every run of every slide shape."""
def _hint_shape(shape) -> int:
hinted = 0
@@ -133,29 +154,59 @@ def _apply_ea_font_hints(presentation: Presentation, target_language: str) -> No
def _hint_text_frame(text_frame) -> int:
hinted = 0
tag_rPr = f"{{{_NS_A}}}rPr"
tag_ea = f"{{{_NS_A}}}ea"
tag_hint = f"{{{_NS_A}}}{tag}"
for paragraph in text_frame.paragraphs:
for run in paragraph.runs:
rPr = run._r.find(tag_rPr)
if rPr is None:
rPr = etree.SubElement(run._r, tag_rPr)
ea = rPr.find(tag_ea)
if ea is None:
ea = etree.SubElement(rPr, tag_ea)
if not ea.get("typeface"):
ea.set("typeface", typeface)
# a:rPr must be the FIRST child of a:r (before a:t).
rPr = etree.Element(tag_rPr)
run._r.insert(0, rPr)
hint = rPr.find(tag_hint)
if hint is None:
hint = etree.Element(tag_hint)
_insert_a_rpr_child(rPr, hint)
if not hint.get("typeface"):
hint.set("typeface", typeface)
hinted += 1
return hinted
typeface = _ea_typeface_for_target(target_language)
if not typeface:
return
total = 0
for slide in presentation.slides:
for shape in slide.shapes:
total += _hint_shape(shape)
# Speaker notes get the RTL direction — they need the typeface
# hint too.
if slide.has_notes_slide and slide.notes_slide.notes_text_frame is not None:
total += _hint_text_frame(slide.notes_slide.notes_text_frame)
if total:
_log_info("pptx_ea_font_hints_applied", runs=total, typeface=typeface)
_log_info(log_event, runs=total, typeface=typeface)
def _apply_ea_font_hints(presentation: Presentation, target_language: str) -> None:
"""Set the <a:ea> (east-asian) typeface on every run for CJK targets.
Blanket application is safe — the hint only affects CJK glyphs.
"""
typeface = _ea_typeface_for_target(target_language)
if not typeface:
return
_apply_run_typeface_hint(
presentation, "ea", typeface, "pptx_ea_font_hints_applied"
)
def _apply_cs_font_hints(presentation: Presentation, target_language: str) -> None:
"""Set the <a:cs> (complex-script) typeface on every run for RTL targets.
Blanket application is safe — the hint only affects Arabic-script
glyphs, and Arial covers the whole Arabic script on every platform.
"""
if not is_rtl(target_language):
return
_apply_run_typeface_hint(
presentation, "cs", "Arial", "pptx_cs_font_hints_applied"
)
class PptxProcessorError(Exception):
@@ -397,13 +448,17 @@ class PowerPointTranslator:
)
# Apply RTL layout when the target language is written right-to-left.
if target_language.lower() in RTL_LANGUAGES:
if is_rtl(target_language):
_apply_rtl_to_presentation(presentation)
# CJK font hint so the target script renders with a proper
# typeface instead of shape-dependent fallbacks.
_apply_ea_font_hints(presentation, target_language)
# Arabic-script font hint for RTL targets (same idea as the
# CJK hint, via the <a:cs> typeface).
_apply_cs_font_hints(presentation, target_language)
if translate_images:
try:
self._translate_images(presentation, target_language)