Loading src/saref_pypeline/docgen/ts2md_extractor.py +82 −32 Original line number Diff line number Diff line Loading @@ -53,6 +53,8 @@ from docx.table import _Cell, Table from docx.text.paragraph import Paragraph from docx.text.hyperlink import Hyperlink from docx.shape import InlineShape from docx.oxml.ns import nsmap from saref_pypeline._logging import TRACE_LEVEL from saref_pypeline.docgen.utils import OWL_GRAPH, print_admonition, with_flags, pprint_xml Loading @@ -74,6 +76,7 @@ if TYPE_CHECKING: from saref_pypeline.docgen import SiteManager logger = logging.getLogger(__name__) nsmap["v"] = "urn:schemas-microsoft-com:vml" class ExtractFormat(Enum): Loading Loading @@ -234,6 +237,41 @@ def process_substitutions_md(md): return md def parse_dml_crop(el) -> dict | None: """Parse DrawingML cropping (<pic:blipFill><a:srcRect>).""" crop_attrs = el.xpath(".//pic:blipFill/a:srcRect") if not crop_attrs: return None attrs = crop_attrs[0].attrib crop = {} for key in ("l", "r", "t", "b"): if key in attrs: # DrawingML: 1/1000 % crop[key] = int(attrs[key]) / 100000.0 return crop or None def parse_vml_crop(vml) -> dict | None: """Parse VML cropping (<v:imagedata croptop=... cropleft=...>). TODO: non urgent, VML cropping seems to shrink image vertically. """ crop = {} mapping = { "croptop": "t", "cropleft": "l", "cropright": "r", "cropbottom": "b", } for attr, key in mapping.items(): if attr in vml.attrib: raw = vml.attrib[attr] if raw.endswith("f"): # VML "f" units: 1/65536 val = int(raw[:-1]) / 65536.0 else: val = float(raw) crop[key] = val return crop or None def cell_align_style(cell): """ Loading Loading @@ -575,7 +613,8 @@ class TS2MDExtractor: try: style = P_STYLE(paragraph.style.name) except: style = P_STYLE.Normal logger.warning(f"Unknown style {paragraph.style.name}") style = paragraph.style fname = f"extract_{style.name}" if hasattr(self, fname) and callable(getattr(self, fname)): Loading Loading @@ -896,6 +935,10 @@ class TS2MDExtractor: self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD ): """Table title""" if paragraph.text.startswith("Figure"): logger.warning(f"Wrong style for {paragraph.text}") return self.extract_TF(paragraph, extract_format) else: return "" def extract_TAH( Loading Loading @@ -993,44 +1036,41 @@ class TS2MDExtractor: return None, 0, None if isinstance(paragraph, Paragraph): for run in paragraph.runs: # An image inserted via add_picture() is stored as an inline shape if not run._element.xpath(".//pic:pic"): continue el = run._element try: inline_shape: InlineShape = run._element.xpath(".//a:blip/@r:embed") if not inline_shape: continue # Get the relationship id # --- CASE 1: DrawingML --- inline_shape = el.xpath(".//a:blip/@r:embed") if inline_shape: rId = inline_shape[0] # Retrieve the image part from the relationship image_part = paragraph.part.related_parts[rId] # Width from the drawing element (in EMU) cx_attr = run._element.xpath(".//a:xfrm/a:ext/@cx") # width in EMU cx_attr = el.xpath(".//a:xfrm/a:ext/@cx") width_emu = float(cx_attr[0]) if cx_attr else None # Original filename (as stored in the package, not necessarily original filesystem name) filename = image_part.partname # e.g. '/word/media/image1.png' filename = image_part.partname crop = parse_dml_crop(el) return filename, width_emu, crop # Cropping info (fractions of 1.0) crop = {} crop_attrs = run._element.xpath(".//pic:blipFill/a:srcRect") if crop_attrs: attrs = crop_attrs[0].attrib for key in ("l", "r", "t", "b"): if key in attrs: # Convert from 1/1000 percent to fraction crop[key] = int(attrs[key]) / 100000.0 else: crop = None # --- CASE 2: VML --- vml_shape = el.xpath(".//v:imagedata") if vml_shape: vml = vml_shape[0] rId = vml.attrib.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id") image_part = paragraph.part.related_parts[rId] # width in EMU (pt → EMU) style = el.xpath(".//v:shape/@style") width_emu = None if style: m = re.search(r"width:(\d+(?:\.\d+)?)pt", style[0]) if m: pt = float(m.group(1)) width_emu = int(pt * 12700) filename = image_part.partname crop = parse_vml_crop(vml) return filename, width_emu, crop except Exception as ex: print(str(ex)) continue image_paragraph = get_prev_block(self.document, paragraph) return self.get_paragraph_image_info(image_paragraph, i + 1) Loading Loading @@ -1145,3 +1185,13 @@ class TS2MDExtractor: return level*4*" "+ "* " + self.extract_Normal(paragraph, extract_format).strip() def extract_Caption( self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD ): """Table title""" if paragraph.text.startswith("Figure"): logger.warning(f"Wrong style for {paragraph.text}") return self.extract_TF(paragraph, extract_format) else: return "" Loading
src/saref_pypeline/docgen/ts2md_extractor.py +82 −32 Original line number Diff line number Diff line Loading @@ -53,6 +53,8 @@ from docx.table import _Cell, Table from docx.text.paragraph import Paragraph from docx.text.hyperlink import Hyperlink from docx.shape import InlineShape from docx.oxml.ns import nsmap from saref_pypeline._logging import TRACE_LEVEL from saref_pypeline.docgen.utils import OWL_GRAPH, print_admonition, with_flags, pprint_xml Loading @@ -74,6 +76,7 @@ if TYPE_CHECKING: from saref_pypeline.docgen import SiteManager logger = logging.getLogger(__name__) nsmap["v"] = "urn:schemas-microsoft-com:vml" class ExtractFormat(Enum): Loading Loading @@ -234,6 +237,41 @@ def process_substitutions_md(md): return md def parse_dml_crop(el) -> dict | None: """Parse DrawingML cropping (<pic:blipFill><a:srcRect>).""" crop_attrs = el.xpath(".//pic:blipFill/a:srcRect") if not crop_attrs: return None attrs = crop_attrs[0].attrib crop = {} for key in ("l", "r", "t", "b"): if key in attrs: # DrawingML: 1/1000 % crop[key] = int(attrs[key]) / 100000.0 return crop or None def parse_vml_crop(vml) -> dict | None: """Parse VML cropping (<v:imagedata croptop=... cropleft=...>). TODO: non urgent, VML cropping seems to shrink image vertically. """ crop = {} mapping = { "croptop": "t", "cropleft": "l", "cropright": "r", "cropbottom": "b", } for attr, key in mapping.items(): if attr in vml.attrib: raw = vml.attrib[attr] if raw.endswith("f"): # VML "f" units: 1/65536 val = int(raw[:-1]) / 65536.0 else: val = float(raw) crop[key] = val return crop or None def cell_align_style(cell): """ Loading Loading @@ -575,7 +613,8 @@ class TS2MDExtractor: try: style = P_STYLE(paragraph.style.name) except: style = P_STYLE.Normal logger.warning(f"Unknown style {paragraph.style.name}") style = paragraph.style fname = f"extract_{style.name}" if hasattr(self, fname) and callable(getattr(self, fname)): Loading Loading @@ -896,6 +935,10 @@ class TS2MDExtractor: self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD ): """Table title""" if paragraph.text.startswith("Figure"): logger.warning(f"Wrong style for {paragraph.text}") return self.extract_TF(paragraph, extract_format) else: return "" def extract_TAH( Loading Loading @@ -993,44 +1036,41 @@ class TS2MDExtractor: return None, 0, None if isinstance(paragraph, Paragraph): for run in paragraph.runs: # An image inserted via add_picture() is stored as an inline shape if not run._element.xpath(".//pic:pic"): continue el = run._element try: inline_shape: InlineShape = run._element.xpath(".//a:blip/@r:embed") if not inline_shape: continue # Get the relationship id # --- CASE 1: DrawingML --- inline_shape = el.xpath(".//a:blip/@r:embed") if inline_shape: rId = inline_shape[0] # Retrieve the image part from the relationship image_part = paragraph.part.related_parts[rId] # Width from the drawing element (in EMU) cx_attr = run._element.xpath(".//a:xfrm/a:ext/@cx") # width in EMU cx_attr = el.xpath(".//a:xfrm/a:ext/@cx") width_emu = float(cx_attr[0]) if cx_attr else None # Original filename (as stored in the package, not necessarily original filesystem name) filename = image_part.partname # e.g. '/word/media/image1.png' filename = image_part.partname crop = parse_dml_crop(el) return filename, width_emu, crop # Cropping info (fractions of 1.0) crop = {} crop_attrs = run._element.xpath(".//pic:blipFill/a:srcRect") if crop_attrs: attrs = crop_attrs[0].attrib for key in ("l", "r", "t", "b"): if key in attrs: # Convert from 1/1000 percent to fraction crop[key] = int(attrs[key]) / 100000.0 else: crop = None # --- CASE 2: VML --- vml_shape = el.xpath(".//v:imagedata") if vml_shape: vml = vml_shape[0] rId = vml.attrib.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id") image_part = paragraph.part.related_parts[rId] # width in EMU (pt → EMU) style = el.xpath(".//v:shape/@style") width_emu = None if style: m = re.search(r"width:(\d+(?:\.\d+)?)pt", style[0]) if m: pt = float(m.group(1)) width_emu = int(pt * 12700) filename = image_part.partname crop = parse_vml_crop(vml) return filename, width_emu, crop except Exception as ex: print(str(ex)) continue image_paragraph = get_prev_block(self.document, paragraph) return self.get_paragraph_image_info(image_paragraph, i + 1) Loading Loading @@ -1145,3 +1185,13 @@ class TS2MDExtractor: return level*4*" "+ "* " + self.extract_Normal(paragraph, extract_format).strip() def extract_Caption( self, paragraph: Paragraph, extract_format: ExtractFormat = ExtractFormat.MD ): """Table title""" if paragraph.text.startswith("Figure"): logger.warning(f"Wrong style for {paragraph.text}") return self.extract_TF(paragraph, extract_format) else: return ""