Insère automatiquement les schémas dans les supports ; module 3 illustré
inserer.py ajoute chaque figure (SVG + repli PNG) après l'introduction de sa section, avec une légende numérotée. Il est idempotent (les cadres schema-* sont retirés puis réinsérés) et garde mimetype en tête du zip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
343a557332
commit
8012ce98b3
@@ -0,0 +1,172 @@
|
||||
"""Insère les schémas générés dans les supports .odt.
|
||||
|
||||
Usage : python schemas/inserer.py [Module_03_PWM_ADC.odt ...]
|
||||
Sans argument : traite chaque support pour lequel un fichier mXX_*.py existe.
|
||||
|
||||
Idempotent : les figures déjà insérées (cadres « schema-* ») sont retirées puis réinsérées.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from generer import DOSSIER_PNG, DOSSIER_SVG, modules_figures
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
|
||||
NS = {
|
||||
"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
|
||||
"text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
|
||||
"draw": "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
|
||||
"style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
||||
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
|
||||
"svg": "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
|
||||
"xlink": "http://www.w3.org/1999/xlink",
|
||||
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
|
||||
}
|
||||
STYLE_IMAGE = "Schema_Image"
|
||||
STYLE_LEGENDE = "Schema_Legende"
|
||||
STYLE_CADRE = "Schema_Cadre"
|
||||
|
||||
|
||||
def q(nom):
|
||||
prefixe, local = nom.split(":")
|
||||
return f"{{{NS[prefixe]}}}{local}"
|
||||
|
||||
|
||||
def normaliser(texte):
|
||||
return re.sub(r"\s+", " ", texte.replace("’", "'")).strip()
|
||||
|
||||
|
||||
def taille_png(chemin):
|
||||
with open(chemin, "rb") as f:
|
||||
entete = f.read(24)
|
||||
return struct.unpack(">II", entete[16:24])
|
||||
|
||||
|
||||
def ajouter_styles(racine):
|
||||
auto = racine.find(q("office:automatic-styles"))
|
||||
existants = {s.get(q("style:name")) for s in auto}
|
||||
if STYLE_IMAGE not in existants:
|
||||
s = etree.SubElement(auto, q("style:style"), {q("style:name"): STYLE_IMAGE, q("style:family"): "paragraph"})
|
||||
etree.SubElement(s, q("style:paragraph-properties"), {
|
||||
q("fo:text-align"): "center", q("fo:margin-top"): "0.3cm",
|
||||
q("fo:margin-bottom"): "0.1cm", q("fo:keep-with-next"): "always"})
|
||||
if STYLE_LEGENDE not in existants:
|
||||
s = etree.SubElement(auto, q("style:style"), {q("style:name"): STYLE_LEGENDE, q("style:family"): "paragraph"})
|
||||
etree.SubElement(s, q("style:paragraph-properties"), {
|
||||
q("fo:text-align"): "center", q("fo:margin-top"): "0cm", q("fo:margin-bottom"): "0.4cm"})
|
||||
etree.SubElement(s, q("style:text-properties"), {
|
||||
q("fo:font-style"): "italic", q("fo:font-size"): "9pt", q("fo:color"): "#4b5563"})
|
||||
if STYLE_CADRE not in existants:
|
||||
s = etree.SubElement(auto, q("style:style"), {q("style:name"): STYLE_CADRE, q("style:family"): "graphic"})
|
||||
etree.SubElement(s, q("style:graphic-properties"), {
|
||||
q("style:vertical-pos"): "top", q("style:vertical-rel"): "baseline",
|
||||
q("fo:border"): "none", q("fo:padding"): "0cm"})
|
||||
|
||||
|
||||
def retirer_figures(corps):
|
||||
styles = (STYLE_IMAGE, STYLE_LEGENDE)
|
||||
for p in list(corps.iter(q("text:p"))):
|
||||
if p.get(q("text:style-name")) in styles:
|
||||
p.getparent().remove(p)
|
||||
|
||||
|
||||
def trouver_point_insertion(corps, ancre):
|
||||
"""Renvoie l'élément après lequel insérer : le paragraphe d'introduction qui suit le titre."""
|
||||
cible = normaliser(ancre)
|
||||
for p in corps.iter(q("text:p"), q("text:h")):
|
||||
if normaliser("".join(p.itertext())) == cible:
|
||||
suivant = p.getnext()
|
||||
while suivant is not None and suivant.tag == q("text:p") and not "".join(suivant.itertext()).strip():
|
||||
suivant = suivant.getnext()
|
||||
if suivant is not None and suivant.tag == q("text:p"):
|
||||
return suivant
|
||||
return p
|
||||
raise ValueError(f"ancre introuvable : {ancre!r}")
|
||||
|
||||
|
||||
def construire_figure(f, numero):
|
||||
largeur, hauteur = taille_png(DOSSIER_PNG / f"{f.id}.png")
|
||||
h_cm = f.largeur_cm * hauteur / largeur
|
||||
p_image = etree.Element(q("text:p"), {q("text:style-name"): STYLE_IMAGE})
|
||||
cadre = etree.SubElement(p_image, q("draw:frame"), {
|
||||
q("draw:style-name"): STYLE_CADRE, q("draw:name"): f"schema-{f.id}",
|
||||
q("text:anchor-type"): "as-char", q("svg:width"): f"{f.largeur_cm:.2f}cm",
|
||||
q("svg:height"): f"{h_cm:.2f}cm", q("draw:z-index"): "0"})
|
||||
for ext, mime in (("svg", "image/svg+xml"), ("png", "image/png")):
|
||||
etree.SubElement(cadre, q("draw:image"), {
|
||||
q("xlink:href"): f"Pictures/{f.id}.{ext}", q("xlink:type"): "simple",
|
||||
q("xlink:show"): "embed", q("xlink:actuate"): "onLoad", q("draw:mime-type"): mime})
|
||||
etree.SubElement(cadre, q("svg:title")).text = f.legende
|
||||
p_legende = etree.Element(q("text:p"), {q("text:style-name"): STYLE_LEGENDE})
|
||||
p_legende.text = f"Figure {numero} — {f.legende}"
|
||||
return p_image, p_legende
|
||||
|
||||
|
||||
def maj_manifest(xml, figures):
|
||||
racine = etree.fromstring(xml)
|
||||
for e in list(racine):
|
||||
if e.get(q("manifest:full-path"), "").startswith("Pictures/schema-") or \
|
||||
re.match(r"Pictures/m\d\d-", e.get(q("manifest:full-path"), "")):
|
||||
racine.remove(e)
|
||||
for f in figures:
|
||||
for ext, mime in (("svg", "image/svg+xml"), ("png", "image/png")):
|
||||
etree.SubElement(racine, q("manifest:file-entry"), {
|
||||
q("manifest:full-path"): f"Pictures/{f.id}.{ext}", q("manifest:media-type"): mime})
|
||||
return etree.tostring(racine, xml_declaration=True, encoding="UTF-8")
|
||||
|
||||
|
||||
def traiter(odt, figures):
|
||||
num_module = int(figures[0].id[1:3])
|
||||
with zipfile.ZipFile(odt) as z:
|
||||
entrees = {i.filename: z.read(i.filename) for i in z.infolist()}
|
||||
ordre = [i.filename for i in z.infolist()]
|
||||
racine = etree.fromstring(entrees["content.xml"])
|
||||
corps = racine.find(f"{q('office:body')}/{q('office:text')}")
|
||||
ajouter_styles(racine)
|
||||
retirer_figures(corps)
|
||||
for n, f in enumerate(figures, 1):
|
||||
point = trouver_point_insertion(corps, f.ancre)
|
||||
p_image, p_legende = construire_figure(f, f"{num_module}.{n}")
|
||||
point.addnext(p_image)
|
||||
p_image.addnext(p_legende)
|
||||
entrees["content.xml"] = etree.tostring(racine, xml_declaration=True, encoding="UTF-8")
|
||||
entrees["META-INF/manifest.xml"] = maj_manifest(entrees["META-INF/manifest.xml"], figures)
|
||||
# Images : on retire les anciennes versions puis on ajoute les nouvelles
|
||||
ordre = [n for n in ordre if not re.match(r"Pictures/m\d\d-", n)]
|
||||
for f in figures:
|
||||
entrees[f"Pictures/{f.id}.svg"] = (DOSSIER_SVG / f"{f.id}.svg").read_bytes()
|
||||
entrees[f"Pictures/{f.id}.png"] = (DOSSIER_PNG / f"{f.id}.png").read_bytes()
|
||||
ordre += [f"Pictures/{f.id}.svg", f"Pictures/{f.id}.png"]
|
||||
# Réécriture : mimetype en premier et non compressé (exigence ODF)
|
||||
fd, temp = tempfile.mkstemp(suffix=".odt", dir=odt.parent)
|
||||
os.close(fd)
|
||||
with zipfile.ZipFile(temp, "w") as z:
|
||||
z.writestr(zipfile.ZipInfo("mimetype"), entrees["mimetype"], compress_type=zipfile.ZIP_STORED)
|
||||
for nom in ordre:
|
||||
if nom != "mimetype":
|
||||
z.writestr(nom, entrees[nom], compress_type=zipfile.ZIP_DEFLATED)
|
||||
os.replace(temp, odt)
|
||||
print(f"{odt.name} : {len(figures)} figure(s) insérée(s)")
|
||||
|
||||
|
||||
def main():
|
||||
par_module = modules_figures()
|
||||
cibles = [RACINE / a for a in sys.argv[1:]] or \
|
||||
[p for p in sorted(RACINE.glob("Module_*.odt")) if f"m{p.name[7:9]}" in par_module]
|
||||
for odt in cibles:
|
||||
prefixe = f"m{odt.name[7:9]}"
|
||||
if prefixe not in par_module:
|
||||
print(f"{odt.name} : aucune figure déclarée, ignoré")
|
||||
continue
|
||||
traiter(odt, par_module[prefixe])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user