feat(pptx): traduire les phrases entieres, pas les fragments de meme style
Les runs adjacents d'un paragraphe PowerPoint portant le meme style (empreinte du a:rPr serialise, comme dans le traducteur Word) sont fusionnes en une seule unite de traduction. Une phrase coupee en plusieurs segments de meme style est desormais traduite entiere ; la traduction va dans le premier run, les suivants sont vides (les elements restent en place). Les sauts de ligne (a:br) et champs (a:fld) ferment le groupe : jamais de fusion a travers un saut de ligne. 5 tests nouveaux (TestRunMerging).
This commit is contained in:
@@ -44,6 +44,21 @@ def _insert_a_rpr_child(rPr, child) -> None:
|
||||
rPr.append(child)
|
||||
|
||||
|
||||
def _arpr_signature(run_element) -> str:
|
||||
"""Signature de style d'un run DrawingML : XML sérialisé de son <a:rPr>.
|
||||
|
||||
Deux runs adjacents dont les signatures sont identiques portent
|
||||
exactement le même style : les fusionner ne change rien au rendu.
|
||||
Un run sans <a:rPr> a la signature vide — commune à tous les runs
|
||||
sans style explicite. Même approche que `_rpr_signature` du
|
||||
traducteur Word.
|
||||
"""
|
||||
rPr = run_element.find(f"{{{_NS_A}}}rPr")
|
||||
if rPr is None:
|
||||
return ""
|
||||
return etree.tostring(rPr, encoding="unicode")
|
||||
|
||||
|
||||
from core.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -871,28 +886,104 @@ class PowerPointTranslator:
|
||||
def _collect_from_text_frame(
|
||||
self, text_frame, text_elements: List[Tuple[str, Callable[[str], None]]]
|
||||
) -> None:
|
||||
"""Collect text from a text frame, preserving leading/trailing whitespace."""
|
||||
"""Collect text from a text frame, preserving leading/trailing whitespace.
|
||||
|
||||
Les runs ADJACENTS d'un même paragraphe portant le même style
|
||||
(même <a:rPr> sérialisé — voir `_arpr_signature`) sont fusionnés
|
||||
en UNE unité de traduction, comme le fait le traducteur Word :
|
||||
une phrase coupée en plusieurs runs de même style est traduite
|
||||
entière au lieu d'être traduite fragment par fragment. À
|
||||
l'écriture, la traduction va dans le <a:t> du premier run du
|
||||
groupe (entourée des espaces de bord du groupe) et les <a:t>
|
||||
des runs suivants sont VIDÉS — les éléments <a:r> restent en
|
||||
place : leur style sert de référence visuelle et python-pptx
|
||||
doit continuer de fonctionner.
|
||||
|
||||
Le parcours suit les enfants de <a:p> dans l'ordre du document :
|
||||
tout enfant autre que <a:r> — saut de ligne <a:br>, champ
|
||||
<a:fld>… — ferme le groupe en cours, car une traduction
|
||||
fusionnée doit rester sur une seule ligne. Les runs vides ou
|
||||
tout blancs restent dans leur groupe : leur texte participe à
|
||||
la concaténation (espace séparateur) et leur <a:t> sera vidé
|
||||
comme les autres ; un groupe entièrement blanc n'est pas traduit.
|
||||
"""
|
||||
if not text_frame.text.strip():
|
||||
return
|
||||
|
||||
tag_r = f"{{{_NS_A}}}r"
|
||||
tag_t = f"{{{_NS_A}}}t"
|
||||
|
||||
for paragraph in text_frame.paragraphs:
|
||||
if not paragraph.text.strip():
|
||||
continue
|
||||
|
||||
for run in paragraph.runs:
|
||||
if run.text and run.text.strip():
|
||||
original = run.text
|
||||
leading = original[: len(original) - len(original.lstrip())]
|
||||
trailing = original[len(original.rstrip()) :]
|
||||
stripped = original.strip()
|
||||
# Découpe du paragraphe en groupes de runs adjacents de même
|
||||
# signature de style.
|
||||
groups: List[List] = []
|
||||
last_signature: Optional[str] = None
|
||||
for child in paragraph._p:
|
||||
if child.tag != tag_r:
|
||||
# a:br, a:fld, a:endParaRPr… : la fusion s'arrête.
|
||||
# Le groupe vide sert de barrière : le run qui suit ne
|
||||
# sera jamais rattaché au groupe d'avant.
|
||||
groups.append([])
|
||||
last_signature = None
|
||||
continue
|
||||
signature = _arpr_signature(child)
|
||||
if groups and groups[-1] and signature == last_signature:
|
||||
groups[-1].append(child)
|
||||
else:
|
||||
groups.append([child])
|
||||
last_signature = signature
|
||||
|
||||
def make_setter(r, lead: str, trail: str):
|
||||
def setter(text: str) -> None:
|
||||
r.text = lead + text.strip() + trail
|
||||
for run_elems in groups:
|
||||
if not run_elems:
|
||||
continue
|
||||
|
||||
return setter
|
||||
combined = "".join(
|
||||
(t.text or "")
|
||||
for r in run_elems
|
||||
for t in r.findall(tag_t)
|
||||
)
|
||||
stripped = combined.strip()
|
||||
if not stripped:
|
||||
# Groupe entièrement blanc : rien à traduire, on
|
||||
# laisse le rendu tel quel.
|
||||
continue
|
||||
leading = combined[: len(combined) - len(combined.lstrip())]
|
||||
trailing = combined[len(combined.rstrip()):]
|
||||
|
||||
text_elements.append((stripped, make_setter(run, leading, trailing)))
|
||||
# Premier run portant un <a:t> (en pratique le premier du
|
||||
# groupe) : c'est lui qui reçoit la traduction.
|
||||
first_r = next(
|
||||
(r for r in run_elems if r.find(tag_t) is not None),
|
||||
run_elems[0],
|
||||
)
|
||||
siblings = [r for r in run_elems if r is not first_r]
|
||||
|
||||
def make_group_setter(first_r, siblings, lead: str, trail: str):
|
||||
def setter(text: str) -> None:
|
||||
# La traduction entière du groupe va dans le
|
||||
# premier run, entourée des espaces de bord du
|
||||
# groupe. L'accesseur text de l'élément a:r
|
||||
# (CT_RegularTextRun) échappe les caractères de
|
||||
# contrôle à notre place.
|
||||
first_r.text = lead + text.strip() + trail
|
||||
# Les runs fusionnés sont vidés, pas supprimés :
|
||||
# leur style (a:rPr) reste une référence visuelle
|
||||
# et python-pptx continue de fonctionner.
|
||||
for sib in siblings:
|
||||
for t_elem in sib.findall(tag_t):
|
||||
t_elem.text = ""
|
||||
|
||||
return setter
|
||||
|
||||
text_elements.append(
|
||||
(
|
||||
stripped,
|
||||
make_group_setter(first_r, siblings, leading, trailing),
|
||||
)
|
||||
)
|
||||
|
||||
def _get_element_path(self, element) -> str:
|
||||
"""Get a unique XPath-like path for an element within its XML tree.
|
||||
|
||||
Reference in New Issue
Block a user