92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
"""Extract images from a DOCX package into a media folder."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
from docx.oxml.ns import qn
|
|
|
|
|
|
def _r_id_from_blip(blip) -> str | None:
|
|
if blip is None:
|
|
return None
|
|
return blip.get(qn("r:embed")) or blip.get(
|
|
"{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed"
|
|
)
|
|
|
|
|
|
def iter_blip_rids(paragraph_element) -> list[str]:
|
|
"""Collect relationship ids for drawings in a paragraph."""
|
|
if paragraph_element is None:
|
|
return []
|
|
rids: list[str] = []
|
|
for blip in paragraph_element.findall(".//" + qn("a:blip")):
|
|
rid = _r_id_from_blip(blip)
|
|
if rid:
|
|
rids.append(rid)
|
|
return rids
|
|
|
|
|
|
class MediaExtractor:
|
|
"""Copy word/media parts referenced by rIds into dest_dir."""
|
|
|
|
def __init__(self, docx_path: str | Path, dest_dir: str | Path, *, rel_prefix: str):
|
|
self.docx_path = Path(docx_path)
|
|
self.dest_dir = Path(dest_dir)
|
|
self.rel_prefix = rel_prefix.replace("\\", "/").rstrip("/")
|
|
self.dest_dir.mkdir(parents=True, exist_ok=True)
|
|
self._zf = zipfile.ZipFile(self.docx_path, "r")
|
|
self._cache: dict[str, str] = {} # rid -> relative path for md
|
|
self._used_names: set[str] = set()
|
|
|
|
def close(self) -> None:
|
|
self._zf.close()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
self.close()
|
|
|
|
def extract_rid(self, document_part, rid: str) -> str | None:
|
|
"""Return path relative to the .md file, or None on failure."""
|
|
if rid in self._cache:
|
|
return self._cache[rid]
|
|
try:
|
|
rel = document_part.rels[rid]
|
|
except KeyError:
|
|
return None
|
|
target = getattr(rel, "target_ref", None) or getattr(rel, "target_part", None)
|
|
if target is None:
|
|
return None
|
|
# target_ref like "media/image1.png"
|
|
if hasattr(rel, "target_part"):
|
|
part = rel.target_part
|
|
blob = part.blob
|
|
name = os.path.basename(part.partname)
|
|
else:
|
|
ref = str(target).lstrip("/")
|
|
zip_name = "word/" + ref if not ref.startswith("word/") else ref
|
|
try:
|
|
blob = self._zf.read(zip_name)
|
|
except KeyError:
|
|
return None
|
|
name = os.path.basename(ref)
|
|
|
|
stem, ext = os.path.splitext(name)
|
|
if not ext:
|
|
ext = ".png"
|
|
safe = f"{stem}{ext}"
|
|
if safe in self._used_names:
|
|
digest = hashlib.md5(blob).hexdigest()[:8]
|
|
safe = f"{stem}_{digest}{ext}"
|
|
self._used_names.add(safe)
|
|
out_path = self.dest_dir / safe
|
|
out_path.write_bytes(blob)
|
|
rel_path = f"{self.rel_prefix}/{safe}" if self.rel_prefix else safe
|
|
self._cache[rid] = rel_path
|
|
return rel_path
|