Création, édition et analyse de DOCX
Vue d'ensemble
Un fichier .docx est une archive ZIP contenant des fichiers XML.
Référence rapide
| Tâche | Approche |
|---|---|
| Lire/analyser le contenu | pandoc ou dépacker pour accéder au XML brut |
| Créer un nouveau document | Utiliser docx-js - voir Créer de nouveaux documents ci-dessous |
| Éditer un document existant | Dépacker → éditer le XML → repacker - voir Éditer des documents existants ci-dessous |
Convertir .doc en .docx
Les fichiers .doc hérités doivent être convertis avant édition :
python scripts/office/soffice.py --headless --convert-to docx document.doc
Lire le contenu
# Extraction de texte avec suivi des modifications
pandoc --track-changes=all document.docx -o output.md
# Accès au XML brut
python scripts/office/unpack.py document.docx unpacked/
Convertir en images
python scripts/office/soffice.py --headless --convert-to pdf document.docx
pdftoppm -jpeg -r 150 document.pdf page
Accepter les modifications suivi
Pour générer un document propre avec tous les changements acceptés (nécessite LibreOffice) :
python scripts/accept_changes.py input.docx output.docx
Créer de nouveaux documents
Générer des fichiers .docx avec JavaScript, puis valider. Installation : npm install -g docx
Configuration
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun,
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
InternalHyperlink, Bookmark, FootnoteReferenceRun, PositionalTab,
PositionalTabAlignment, PositionalTabRelativeTo, PositionalTabLeader,
TabStopType, TabStopPosition, Column, SectionType,
TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType,
VerticalAlign, PageNumber, PageBreak } = require('docx');
const doc = new Document({ sections: [{ children: [/* contenu */] }] });
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer));
Validation
Après la création du fichier, validez-le. Si la validation échoue, dépacker, corriger le XML et repacker.
python scripts/office/validate.py doc.docx
Taille de page
// CRITIQUE : docx-js utilise par défaut A4, pas US Letter
// Toujours définir la taille de page explicitement pour des résultats cohérents
sections: [{
properties: {
page: {
size: {
width: 12240, // 8,5 pouces en DXA
height: 15840 // 11 pouces en DXA
},
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // marges de 1 pouce
}
},
children: [/* contenu */]
}]
Tailles de page courantes (unités DXA, 1440 DXA = 1 pouce) :
| Papier | Largeur | Hauteur | Largeur contenu (marges 1") |
|---|---|---|---|
| US Letter | 12 240 | 15 840 | 9 360 |
| A4 (par défaut) | 11 906 | 16 838 | 9 026 |
Orientation paysage : docx-js échange largeur/hauteur en interne, donc passez les dimensions portrait et laissez-le gérer l'échange :
size: {
width: 12240, // Passez le côté COURT en largeur
height: 15840, // Passez le côté LONG en hauteur
orientation: PageOrientation.LANDSCAPE // docx-js les échange dans le XML
},
// Largeur contenu = 15840 - marge gauche - marge droite (utilise le long côté)
Styles (Override les titres intégrés)
Utilisez Arial comme police par défaut (universellement supportée). Gardez les titres en noir pour la lisibilité.
const doc = new Document({
styles: {
default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt par défaut
paragraphStyles: [
// IMPORTANT : Utilisez les IDs exacts pour override les styles intégrés
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 32, bold: true, font: "Arial" },
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel requis pour TOC
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, font: "Arial" },
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } },
]
},
sections: [{
children: [
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Titre")] }),
]
}]
});
Listes (N'UTILISEZ JAMAIS de puces unicode)
// ❌ MAUVAIS - ne jamais insérer manuellement de caractères de puce
new Paragraph({ children: [new TextRun("• Élément")] }) // MAUVAIS
new Paragraph({ children: [new TextRun("\u2022 Élément")] }) // MAUVAIS
// ✅ CORRECT - utiliser la config de numérotation avec LevelFormat.BULLET
const doc = new Document({
numbering: {
config: [
{ reference: "bullets",
levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: "numbers",
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
]
},
sections: [{
children: [
new Paragraph({ numbering: { reference: "bullets", level: 0 },
children: [new TextRun("Élément avec puce")] }),
new Paragraph({ numbering: { reference: "numbers", level: 0 },
children: [new TextRun("Élément numéroté")] }),
]
}]
});
// ⚠️ Chaque référence crée une NUMÉROTATION INDÉPENDANTE
// Même référence = continue (1,2,3 puis 4,5,6)
// Référence différente = recommence (1,2,3 puis 1,2,3)
Tableaux
CRITIQUE : Les tableaux ont besoin de largeurs doubles - définir à la fois columnWidths sur le tableau ET width sur chaque cellule. Sans les deux, les tableaux s'affichent mal sur certaines plates-formes.
// CRITIQUE : Toujours définir la largeur du tableau pour un rendu cohérent
// CRITIQUE : Utiliser ShadingType.CLEAR (pas SOLID) pour éviter les fonds noirs
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const borders = { top: border, bottom: border, left: border, right: border };
new Table({
width: { size: 9360, type: WidthType.DXA }, // Toujours utiliser DXA (les pourcentages ne fonctionnent pas dans Google Docs)
columnWidths: [4680, 4680], // Doivent correspondre à la largeur du tableau (DXA : 1440 = 1 pouce)
rows: [
new TableRow({
children: [
new TableCell({
borders,
width: { size: 4680, type: WidthType.DXA }, // Aussi définir sur chaque cellule
shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // CLEAR pas SOLID
margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Remplissage des cellules (interne, pas ajouté à la largeur)
children: [new Paragraph({ children: [new TextRun("Cellule")] })]
})
]
})
]
})
Calcul de la largeur du tableau :
Toujours utiliser WidthType.DXA — WidthType.PERCENTAGE ne fonctionne pas dans Google Docs.
// Largeur du tableau = somme des columnWidths = largeur du contenu
// US Letter avec marges 1" : 12240 - 2880 = 9360 DXA
width: { size: 9360, type: WidthType.DXA },
columnWidths: [7000, 2360] // Doivent correspondre à la largeur du tableau
Règles de largeur :
- Toujours utiliser
WidthType.DXA— jamaisWidthType.PERCENTAGE(incompatible avec Google Docs) - La largeur du tableau doit égaler la somme de
columnWidths - La
widthde la cellule doit correspondre à lacolumnWidthcorrespondante - Les
marginsde cellule sont un remplissage interne - ils réduisent la zone de contenu, pas ajoutent à la largeur de la cellule - Pour les tableaux pleine largeur : utiliser la largeur du contenu (largeur de page moins marges gauche et droite)
Images
// CRITIQUE : le paramètre type est REQUIS
new Paragraph({
children: [new ImageRun({
type: "png", // Requis : png, jpg, jpeg, gif, bmp, svg
data: fs.readFileSync("image.png"),
transformation: { width: 200, height: 150 },
altText: { title: "Titre", description: "Desc", name: "Nom" } // Les trois requis
})]
})
Sauts de page
// CRITIQUE : PageBreak doit être à l'intérieur d'un Paragraph
new Paragraph({ children: [new PageBreak()] })
// Ou utiliser pageBreakBefore
new Paragraph({ pageBreakBefore: true, children: [new TextRun("Nouvelle page")] })
Hyperliens
// Lien externe
new Paragraph({
children: [new ExternalHyperlink({
children: [new TextRun({ text: "Cliquez ici", style: "Hyperlink" })],
link: "https://example.com",
})]
})
// Lien interne (signet + référence)
// 1. Créer un signet à la destination
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [
new Bookmark({ id: "chapter1", children: [new TextRun("Chapitre 1")] }),
]})
// 2. Lien vers lui
new Paragraph({ children: [new InternalHyperlink({
children: [new TextRun({ text: "Voir Chapitre 1", style: "Hyperlink" })],
anchor: "chapter1",
})]})
Notes de bas de page
const doc = new Document({
footnotes: {
1: { children: [new Paragraph("Source : Rapport annuel 2024")] },
2: { children: [new Paragraph("Voir appendice pour la méthodologie")] },
},
sections: [{
children: [new Paragraph({
children: [
new TextRun("Le revenu a augmenté de 15 %"),
new FootnoteReferenceRun(1),
new TextRun(" avec des métriques ajustées"),
new FootnoteReferenceRun(2),
],
})]
}]
});
Tabulateurs
// Aligner le texte à droite sur la même ligne (p.ex. date opposée à un titre)
new Paragraph({
children: [
new TextRun("Nom de l'entreprise"),
new TextRun("\tJanvier 2025"),
],
tabStops: [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }],
})
// Leader en pointillés (p.ex. style TOC)
new Paragraph({
children: [
new TextRun("Introduction"),
new TextRun({ children: [
new PositionalTab({
alignment: PositionalTabAlignment.RIGHT,
relativeTo: PositionalTabRelativeTo.MARGIN,
leader: PositionalTabLeader.DOT,
}),
"3",
]}),
],
})
Mises en page multi-colonnes
// Colonnes de largeur égale
sections: [{
properties: {
column: {
count: 2, // nombre de colonnes
space: 720, // écart entre les colonnes en DXA (720 = 0,5 pouce)
equalWidth: true,
separate: true, // ligne verticale entre les colonnes
},
},
children: [/* le contenu s'écoule naturellement sur les colonnes */]
}]
// Colonnes de largeur personnalisée (equalWidth doit être false)
sections: [{
properties: {
column: {
equalWidth: false,
children: [
new Column({ width: 5400, space: 720 }),
new Column({ width: 3240 }),
],
},
},
children: [/* contenu */]
}]
Forcer un saut de colonne avec une nouvelle section utilisant type: SectionType.NEXT_COLUMN.
Table des matières
// CRITIQUE : Les titres doivent utiliser HeadingLevel UNIQUEMENT - pas de styles personnalisés
new TableOfContents("Table des matières", { hyperlink: true, headingStyleRange: "1-3" })
En-têtes/Pieds de page
sections: [{
properties: {
page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } // 1440 = 1 pouce
},
headers: {
default: new Header({ children: [new Paragraph({ children: [new TextRun("En-tête")] })] })
},
footers: {
default: new Footer({ children: [new Paragraph({
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })]
})] })
},
children: [/* contenu */]
}]
Règles critiques pour docx-js
- Définir explicitement la taille de page - docx-js utilise par défaut A4 ; utiliser US Letter (12240 x 15840 DXA) pour les documents US
- Paysage : passer les dimensions portrait - docx-js échange largeur/hauteur en interne ; passer le côté court en
width, le côté long enheight, et définirorientation: PageOrientation.LANDSCAPE - Ne jamais utiliser
\n- utiliser des éléments Paragraph séparés - Ne jamais utiliser de puces unicode - utiliser
LevelFormat.BULLETavec la config de numérotation - PageBreak doit être dans Paragraph - standalone crée du XML invalide
- ImageRun nécessite
type- toujours spécifier png/jpg/etc - Toujours définir la
widthdu tableau avec DXA - jamais utiliserWidthType.PERCENTAGE(ne fonctionne pas dans Google Docs) - Les tableaux ont besoin de largeurs doubles - tableau
columnWidthsET cellulewidth, les deux doivent correspondre - Largeur du tableau = somme des columnWidths - pour DXA, s'assurer qu'elles correspondent exactement
- Toujours ajouter des marges de cellule - utiliser
margins: { top: 80, bottom: 80, left: 120, right: 120 }pour un remplissage lisible - Utiliser
ShadingType.CLEAR- jamais SOLID pour l'ombrage du tableau - Ne jamais utiliser les tableaux comme séparateurs/règles - les cellules ont une hauteur minimale et s'affichent comme des cases vides (y compris dans les en-têtes/pieds de page) ; utiliser
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "2E75B6", space: 1 } }sur un Paragraph à la place. Pour les pieds de page à deux colonnes, utiliser les tabulateurs (voir section Tabulateurs), pas les tableaux - TOC nécessite HeadingLevel uniquement - pas de styles personnalisés sur les paragraphes de titre
- Override les styles intégrés - utiliser les IDs exacts : "Heading1", "Heading2", etc.
- Inclure
outlineLevel- requis pour TOC (0 pour H1, 1 pour H2, etc.)
Éditer des documents existants
Suivre les 3 étapes dans l'ordre.
Étape 1 : Dépacker
python scripts/office/unpack.py document.docx unpacked/
Extrait le XML, le formate joliment, fusionne les runs adjacents et convertit les guillemets intelligents en entités XML (“ etc.) pour qu'ils survivent à l'édition. Utiliser --merge-runs false pour ignorer la fusion de runs.
Étape 2 : Éditer le XML
Éditer les fichiers dans unpacked/word/. Voir la section Référence XML ci-dessous pour les motifs.
Utiliser "Claude" comme auteur pour les modifications suivi et commentaires, sauf si l'utilisateur demande explicitement d'utiliser un nom différent.
Utiliser directement l'outil Edit pour remplacer des chaînes. Ne pas écrire de scripts Python. Les scripts introduisent une complexité inutile. L'outil Edit montre exactement ce qui est remplacé.
CRITIQUE : Utiliser des guillemets intelligents pour le nouveau contenu. Lors de l'ajout de texte avec apostrophes ou guillemets, utiliser des entités XML pour produire des guillemets intelligents :
<!-- Utiliser ces entités pour une typographie professionnelle -->
<w:t>Voici’s une citation : “Bonjour”</w:t>
| Entité | Caractère |
|---|---|
‘ |
' (simple gauche) |
’ |
' (simple droite / apostrophe) |
“ |
" (double gauche) |
” |
" (double droite) |
Ajouter des commentaires : Utiliser comment.py pour gérer le boilerplate sur plusieurs fichiers XML (le texte doit être pré-échappé XML) :
python scripts/comment.py unpacked/ 0 "Texte de commentaire avec & et ’"
python scripts/comment.py unpacked/ 1 "Texte de réponse" --parent 0 # réponse au commentaire 0
python scripts/comment.py unpacked/ 0 "Texte" --author "Auteur personnalisé" # nom d'auteur personnalisé
Puis ajouter les marqueurs à document.xml (voir Commentaires dans la Référence XML).
Étape 3 : Repacker
python scripts/office/pack.py unpacked/ output.docx --original document.docx
Valide avec réparation automatique, condense le XML et crée le DOCX. Utiliser --validate false pour ignorer.
La réparation automatique corrigera :
durableId>= 0x7FFFFFFF (régénère un ID valide)xml:space="preserve"manquant sur<w:t>avec espaces
La réparation automatique ne corrigera pas :
- XML malformé, imbrication d'éléments invalide, relations manquantes, violations de schéma
Pièges courants
- Remplacer des éléments
<w:r>entiers : Lors de l'ajout de modifications suivi, remplacer le bloc<w:r>...</w:r>entier par<w:del>...<w:ins>...comme frères. Ne pas injecter de balises de suivi de modifications à l'intérieur d'un run. - Préserver le formatage
<w:rPr>: Copier le bloc<w:rPr>original du run dans vos runs de suivi de modifications pour maintenir le gras, la taille de police, etc.
Référence XML
Conformité au schéma
- Ordre des éléments dans
<w:pPr>:<w:pStyle>,<w:numPr>,<w:spacing>,<w:ind>,<w:jc>,<w:rPr>en dernier - Espaces : Ajouter
xml:space="preserve"à<w:t>avec espaces en début/fin - RSIDs : Doivent être 8 chiffres hex (p.ex.
00AB1234)
Modifications suivi
Insertion :
<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:t>texte inséré</w:t></w:r>
</w:ins>
Suppression :
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>texte supprimé</w:delText></w:r>
</w:del>
À l'intérieur de <w:del> : Utiliser <w:delText> au lieu de <w:t>, et <w:delInstrText> au lieu de <w:instrText>.
Éditions minimales - marquer uniquement ce qui change :
<!-- Changer "30 jours" en "60 jours" -->
<w:r><w:t>Le délai est </w:t></w:r>
<w:del w:id="1" w:author="Claude" w:date="...">
<w:r><w:delText>30</w:delText></w:r>
</w:del>
<w:ins w:id="2" w:author="Claude" w:date="...">
<w:r><w:t>60</w:t></w:r>
</w:ins>
<w:r><w:t> jours.</w:t></w:r>
Suppression de paragraphes/éléments de liste entiers - lors de la suppression de TOUT le contenu d'un paragraphe, marquer aussi le saut de paragraphe comme supprimé pour qu'il fusionne avec le paragraphe suivant. Ajouter <w:del/> à l'intérieur de <w:pPr><w:rPr> :
<w:p>
<w:pPr>
<w:numPr>...</w:numPr> <!-- numérotation de liste si présente -->
<w:rPr>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z"/>
</w:rPr>
</w:pPr>
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>Contenu entier du paragraphe en cours de suppression...</w:delText></w:r>
</w:del>
</w:p>
Sans le <w:del/> dans <w:pPr><w:rPr>, accepter les modifications laisse un paragraphe/élément de liste vide.
Rejeter l'insertion d'un autre auteur - imbriquer la suppression à l'intérieur de son insertion :
<w:ins w:author="Jane" w:id="5">
<w:del w:author="Claude" w:id="10">
<w:r><w:delText>texte inséré par eux</w:delText></w:r>
</w:del>
</w:ins>
Restaurer la suppression d'un autre auteur - ajouter une insertion après (ne pas modifier leur suppression) :
<w:del w:author="Jane" w:id="5">
<w:r><w:delText>texte supprimé</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="10">
<w:r><w:t>texte supprimé</w:t></w:r>
</w:ins>
Commentaires
Après avoir exécuté comment.py (voir Étape 2), ajouter les marqueurs à document.xml. Pour les réponses, utiliser le drapeau --parent et imbriquer les marqueurs à l'intérieur du parent.
CRITIQUE : <w:commentRangeStart> et <w:commentRangeEnd> sont des frères de <w:r>, jamais à l'intérieur de <w:r>.
<!-- Les marqueurs de commentaire sont des enfants directs de w:p, jamais à l'intérieur de w:r -->
<w:commentRangeStart w:id="0"/>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>supprimé</w:delText></w:r>
</w:del>
<w:r><w:t> plus de texte</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<!-- Commentaire 0 avec réponse 1 imbriquée à l'intérieur -->
<w:commentRangeStart w:id="0"/>
<w:commentRangeStart w:id="1"/>
<w:r><w:t>texte</w:t></w:r>
<w:commentRangeEnd w:id="1"/>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="1"/></w:r>
Images
- Ajouter le fichier image à
word/media/ - Ajouter la relation à
word/_rels/document.xml.rels:<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/> - Ajouter le type de contenu à
[Content_Types].xml:<Default Extension="png" ContentType="image/png"/> - Référencer dans document.xml :
<w:drawing> <wp:inline> <wp:extent cx="914400" cy="914400"/> <!-- EMUs : 914400 = 1 pouce --> <a:graphic> <a:graphicData uri=".../picture"> <pic:pic> <pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill> </pic:pic> </a:graphicData> </a:graphic> </wp:inline> </w:drawing>
Dépendances
- pandoc : Extraction de texte
- docx :
npm install -g docx(nouveaux documents) - LibreOffice : Conversion PDF (auto-configuré pour les environnements en bac à sable via
scripts/office/soffice.py) - Poppler :
pdftoppmpour les images