# WPS Office DISPIMG Image Extraction

WPS Office (金山 WPS) xlsx files use a proprietary mechanism for in-cell images via
`=DISPIMG("ID_...", 1)` formulas. Standard libraries (openpyxl, pandas) cannot read
these images — both `ws._images` and `read_file` return 0 images.

## How DISPIMG Works Internally

```
=DISIMG("ID_02BF61787C3544A69D1EA0EA275F3B4D",1)
         │
         ▼
xl/cellimages.xml           xl/_rels/cellimages.xml.rels     xl/media/
┌────────────────────┐      ┌────────────────────┐         ┌──────────┐
│ <etc:cellImage>    │      │ rId1 → image1.jpeg │         │ image1.jpeg │
│   <xdr:cNvPr       │      │ rId2 → image2.jpeg │         │ image2.jpeg │
│    name="ID_xxx"/> │ ──── │ ...                │ ──────  │ ...          │
│   <a:blip          │      └────────────────────┘         └──────────┘
│    r:embed="rId1"/>│
│ </etc:cellImage>   │
└────────────────────┘
```

The WPS namespace is `xmlns:etc="http://www.wps.cn/officeDocument/2017/etCustomData"`.

## Extraction Recipe (zipfile + XML)

```python
import zipfile, re, os
from xml.etree import ElementTree as ET

with zipfile.ZipFile(xlsx_path, 'r') as z:
    # 1. ID → rId from cellimages.xml
    xml = z.read('xl/cellimages.xml').decode('utf-8')
    id_to_rid = {}
    for cNvPr in ET.fromstring(xml).iter():
        if cNvPr.tag.endswith('cNvPr'):
            name = cNvPr.get('name', '')
            if name.startswith('ID_'):
                for ci in ET.fromstring(xml).iter():
                    if ci.tag.endswith('cellImage'):
                        for child in ci.iter():
                            if child is cNvPr:
                                for blip in ci.iter():
                                    if blip.tag.endswith('blip'):
                                        embed = blip.get(
                                            '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
                                        if embed: id_to_rid[name] = embed
                                break; break

    # 2. rId → media from _rels/cellimages.xml.rels
    rels_xml = z.read('xl/_rels/cellimages.xml.rels').decode('utf-8')
    rid_to_media = {rel.get('Id'): 'xl/' + rel.get('Target')
                    for rel in ET.fromstring(rels_xml)}
    id_to_media = {k: rid_to_media[v] for k, v in id_to_rid.items() if v in rid_to_media}

    # 3. Parse sheet for DISPIMG formulas
    # An DISPIMG regex: re.compile(r'DISPIMG\("(ID_[^"]+)"')
    # Shared strings for t="s" cells
    # For each row, collect media paths from E and F columns
    # Row mapping: JSON idx 0 → XML Row 2 (Row 1 = header), etc.
```

## Key Pitfalls

1. **Row numbering**: openpyxl's `read_file` may show rows differently from raw XML.
   In raw XML: Row 1 = header, Row 2 = first data row. Always cross-check.

2. **openpyxl can't see these**: `ws._images` returns 0. `read_file` shows `=DISPIMG(...)`
   but no image data. Must use raw zipfile.

3. **ID ordering is random**: ID_xxx doesn't correspond to imageN. Always use rels mapping.

4. **Multiple images per row**: Check both column E and F (chat + data images).
