mirror of
https://github.com/firecrawl/anydoc.git
synced 2026-09-14 14:18:33 +08:00
2306 lines
115 KiB
Python
2306 lines
115 KiB
Python
#!/usr/bin/env python3
|
||
"""Regenerate the committed fixture corpus in tests/fixtures/.
|
||
|
||
Sources live in tests/fixture-src/. Real-producer files are generated through
|
||
LibreOffice (headless) and Pandoc; `handmade-*` files are assembled directly for
|
||
precise edge cases. Malformed fixtures encode their expected outcome under the
|
||
unified recovery policy in the filename: `name--recovers.ext`, `name--skips.ext`,
|
||
`name--ignores.ext`, `name--errors.ext`. Files under abuse/ are resource-abuse
|
||
shapes and are excluded from the baseline snapshot run.
|
||
|
||
Usage: python tests/gen_fixtures.py [--skip-office]
|
||
"""
|
||
|
||
import base64
|
||
import os
|
||
import re
|
||
import shutil
|
||
import struct
|
||
import subprocess
|
||
import sys
|
||
import zipfile
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
SRC = ROOT / "tests" / "fixture-src"
|
||
OUT = ROOT / "tests" / "fixtures"
|
||
SOFFICE = r"C:\Program Files\LibreOffice\program\soffice.exe"
|
||
ZIP_DATE = (2026, 1, 1, 0, 0, 0)
|
||
|
||
DOT_PNG = base64.b64decode(
|
||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8"
|
||
"z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
||
)
|
||
|
||
|
||
def run(cmd, **kw):
|
||
print("+", " ".join(str(c) for c in cmd))
|
||
subprocess.run(cmd, check=True, **kw)
|
||
|
||
|
||
def convert_lo(src, filter_spec, out_dir, final_name):
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
profile = ROOT / "target" / "lo-profile"
|
||
ext = filter_spec.split(":")[0]
|
||
produced = out_dir / (src.stem + "." + ext)
|
||
# The very first soffice launch may only initialize its profile; retry.
|
||
for attempt in range(3):
|
||
run([
|
||
SOFFICE, "--headless", "--norestore",
|
||
f"-env:UserInstallation={profile.as_uri()}",
|
||
"--convert-to", filter_spec, "--outdir", str(out_dir), str(src),
|
||
])
|
||
if produced.exists():
|
||
break
|
||
else:
|
||
raise RuntimeError(f"soffice produced no output for {src} -> {filter_spec}")
|
||
target = out_dir / final_name
|
||
if produced != target:
|
||
if target.exists():
|
||
target.unlink()
|
||
produced.rename(target)
|
||
if ext == "rtf":
|
||
anchor_local_paths(target)
|
||
|
||
|
||
# LibreOffice resolves a relative hyperlink against the source file's own
|
||
# location on RTF export, so the generating machine's absolute path lands in
|
||
# the fixture. Anchor it to a fixed root: the fixture tests that a `HYPERLINK`
|
||
# field carrying a file URL is read, not which directory generated it.
|
||
FIXTURE_URI_ROOT = b"file:///anydoc"
|
||
|
||
|
||
def anchor_local_paths(path):
|
||
data = path.read_bytes()
|
||
anchored = re.sub(rb'file:///[^"]*?/tests/fixture-src/',
|
||
FIXTURE_URI_ROOT + b"/tests/fixture-src/", data)
|
||
if anchored != data:
|
||
path.write_bytes(anchored)
|
||
|
||
|
||
def write_cfb(path, streams):
|
||
"""Minimal CFB v3 writer (512-byte sectors) for handmade OLE2 fixtures.
|
||
|
||
Streams are padded to the 4096-byte mini-stream cutoff so everything
|
||
lives in regular FAT sectors; no mini-FAT is written. `streams` is a
|
||
list of (name, bytes) in CFB name order (shorter names first, then
|
||
case-insensitive).
|
||
"""
|
||
SECT = 512
|
||
ENDOFCHAIN = 0xFFFFFFFE
|
||
FATSECT = 0xFFFFFFFD
|
||
FREESECT = 0xFFFFFFFF
|
||
|
||
fat = [] # sector id -> next sector id
|
||
sectors = [] # sector payloads, id == index
|
||
|
||
def add_chain(data):
|
||
if not data:
|
||
return ENDOFCHAIN
|
||
padded = data + b"\x00" * (-len(data) % SECT)
|
||
first = len(sectors)
|
||
count = len(padded) // SECT
|
||
for i in range(count):
|
||
sectors.append(padded[i * SECT:(i + 1) * SECT])
|
||
fat.append(first + i + 1 if i + 1 < count else ENDOFCHAIN)
|
||
return first
|
||
|
||
def dir_entry(name, typ, color, left, right, child, start, size):
|
||
raw = name.encode("utf-16-le") + b"\x00\x00"
|
||
return (raw + b"\x00" * (64 - len(raw))
|
||
+ struct.pack("<HBBIII", len(raw), typ, color, left, right, child)
|
||
+ b"\x00" * 16 # clsid
|
||
+ b"\x00" * 4 # state bits
|
||
+ b"\x00" * 16 # ctime/mtime
|
||
+ struct.pack("<IQ", start, size))
|
||
|
||
NONE = 0xFFFFFFFF
|
||
entries = [None] # placeholder for root
|
||
starts = []
|
||
for name, data in streams:
|
||
padded = data + b"\x00" * (4096 - len(data)) if 0 < len(data) < 4096 else data
|
||
starts.append((add_chain(padded), len(padded)))
|
||
for i, (name, _) in enumerate(streams):
|
||
sid = i + 1
|
||
right = sid + 1 if sid < len(streams) else NONE
|
||
start, size = starts[i]
|
||
entries.append(dir_entry(name, 2, 1, NONE, right, NONE, start, size))
|
||
child = 1 if streams else NONE
|
||
entries[0] = dir_entry("Root Entry", 5, 1, NONE, NONE, child, ENDOFCHAIN, 0)
|
||
|
||
dir_data = b"".join(entries)
|
||
dir_data += b"\x00" * (-len(dir_data) % SECT)
|
||
dir_start = add_chain(dir_data)
|
||
|
||
# FAT sectors (each holds 128 entries), marked FATSECT in the FAT itself.
|
||
fat_count = 1
|
||
while (len(fat) + fat_count + 127) // 128 > fat_count:
|
||
fat_count += 1
|
||
fat_start = len(sectors)
|
||
fat_ids = list(range(fat_start, fat_start + fat_count))
|
||
full_fat = fat + [FATSECT] * fat_count
|
||
full_fat += [FREESECT] * (fat_count * 128 - len(full_fat))
|
||
for i in range(fat_count):
|
||
sectors.append(struct.pack("<128I", *full_fat[i * 128:(i + 1) * 128]))
|
||
|
||
difat = fat_ids + [FREESECT] * (109 - len(fat_ids))
|
||
header = (b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" + b"\x00" * 16
|
||
+ struct.pack("<HHHHHHIIIIIIIII",
|
||
0x003E, 0x0003, 0xFFFE, 9, 6, 0, 0, 0,
|
||
fat_count, dir_start, 0, 4096,
|
||
ENDOFCHAIN, 0, ENDOFCHAIN)
|
||
+ struct.pack("<I", 0)
|
||
+ struct.pack("<109I", *difat))
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
path.write_bytes(header + b"".join(sectors))
|
||
|
||
|
||
def write_zip(path, entries, mimetype_first=None):
|
||
"""entries: list of (name, bytes). Deterministic timestamps."""
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
|
||
if mimetype_first is not None:
|
||
info = zipfile.ZipInfo("mimetype", date_time=ZIP_DATE)
|
||
z.writestr(info, mimetype_first, compress_type=zipfile.ZIP_STORED)
|
||
for name, data in entries:
|
||
info = zipfile.ZipInfo(name, date_time=ZIP_DATE)
|
||
info.compress_type = zipfile.ZIP_DEFLATED
|
||
z.writestr(info, data)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Handmade DOCX: complete numbering matrix + style inheritance (P7.4a-f, H6, M1)
|
||
|
||
W = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
|
||
R = 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"'
|
||
|
||
CONTENT_TYPES_BASE = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
|
||
{extra}</Types>"""
|
||
|
||
ROOT_RELS = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||
</Relationships>"""
|
||
|
||
|
||
def para(text, ppr=""):
|
||
return f"<w:p>{ppr}<w:r><w:t xml:space=\"preserve\">{text}</w:t></w:r></w:p>"
|
||
|
||
|
||
def numpara(text, num_id, ilvl=0, pstyle=None):
|
||
ps = f'<w:pStyle w:val="{pstyle}"/>' if pstyle else ""
|
||
ppr = (f"<w:pPr>{ps}<w:numPr><w:ilvl w:val=\"{ilvl}\"/>"
|
||
f"<w:numId w:val=\"{num_id}\"/></w:numPr></w:pPr>")
|
||
return para(text, ppr)
|
||
|
||
|
||
def numbering_docx():
|
||
styles = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:styles {W}>
|
||
<w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults>
|
||
<w:style w:type="paragraph" w:styleId="HeadBase">
|
||
<w:name w:val="Head Base"/><w:pPr><w:outlineLvl w:val="1"/></w:pPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="SubHead">
|
||
<w:name w:val="Sub Head"/><w:basedOn w:val="HeadBase"/>
|
||
<w:rPr><w:b/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="NumberedPara">
|
||
<w:name w:val="Numbered Para"/>
|
||
<w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="BoldPara">
|
||
<w:name w:val="Bold Para"/><w:rPr><w:b/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="character" w:styleId="PlainChar">
|
||
<w:name w:val="Plain Char"/><w:rPr><w:b w:val="0"/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="character" w:styleId="ToggleChar">
|
||
<w:name w:val="Toggle Char"/><w:rPr><w:b/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="BoldBase">
|
||
<w:name w:val="Bold Base"/><w:rPr><w:b/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="DoubleBold">
|
||
<w:name w:val="Double Bold"/><w:basedOn w:val="BoldBase"/>
|
||
<w:rPr><w:b w:val="true"/></w:rPr>
|
||
</w:style>
|
||
<w:style w:type="numbering" w:styleId="ListNumStyle">
|
||
<w:name w:val="List Num Style"/>
|
||
<w:pPr><w:numPr><w:numId w:val="4"/></w:numPr></w:pPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="ListLevelOne">
|
||
<w:name w:val="List Level One"/>
|
||
<w:pPr><w:numPr><w:ilvl w:val="5"/><w:numId w:val="7"/></w:numPr></w:pPr>
|
||
</w:style>
|
||
<w:style w:type="paragraph" w:styleId="ListLevelTwo">
|
||
<w:name w:val="List Level Two"/>
|
||
<w:pPr><w:numPr><w:ilvl w:val="0"/><w:numId w:val="7"/></w:numPr></w:pPr>
|
||
</w:style>
|
||
</w:styles>"""
|
||
|
||
numbering = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:numbering {W}>
|
||
<w:abstractNum w:abstractNumId="0">
|
||
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1."/></w:lvl>
|
||
<w:lvl w:ilvl="1"><w:start w:val="1"/><w:numFmt w:val="lowerRoman"/><w:lvlText w:val="%2)"/><w:lvlRestart w:val="0"/></w:lvl>
|
||
<w:lvl w:ilvl="2"><w:start w:val="1"/><w:numFmt w:val="bullet"/><w:lvlText w:val="•"/></w:lvl>
|
||
</w:abstractNum>
|
||
<w:abstractNum w:abstractNumId="1">
|
||
<w:numStyleLink w:val="ListNumStyle"/>
|
||
</w:abstractNum>
|
||
<w:abstractNum w:abstractNumId="2">
|
||
<w:styleLink w:val="ListNumStyle"/>
|
||
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="upperLetter"/><w:lvlText w:val="%1:"/></w:lvl>
|
||
</w:abstractNum>
|
||
<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
|
||
<w:num w:numId="2"><w:abstractNumId w:val="0"/></w:num>
|
||
<w:num w:numId="3"><w:abstractNumId w:val="0"/>
|
||
<w:lvlOverride w:ilvl="0"><w:startOverride w:val="10"/></w:lvlOverride>
|
||
</w:num>
|
||
<w:num w:numId="4"><w:abstractNumId w:val="2"/></w:num>
|
||
<w:num w:numId="5"><w:abstractNumId w:val="1"/></w:num>
|
||
<w:num w:numId="6"><w:abstractNumId w:val="0"/>
|
||
<w:lvlOverride w:ilvl="0">
|
||
<w:startOverride w:val="7"/>
|
||
<w:lvl w:ilvl="0"><w:start w:val="1"/><w:numFmt w:val="upperLetter"/><w:lvlText w:val="%1)"/></w:lvl>
|
||
</w:lvlOverride>
|
||
</w:num>
|
||
<w:abstractNum w:abstractNumId="3">
|
||
<w:lvl w:ilvl="0"><w:pStyle w:val="ListLevelOne"/><w:start w:val="1"/><w:numFmt w:val="decimal"/><w:lvlText w:val="%1."/></w:lvl>
|
||
<w:lvl w:ilvl="1"><w:pStyle w:val="ListLevelTwo"/><w:start w:val="1"/><w:numFmt w:val="lowerLetter"/><w:lvlText w:val="%2."/></w:lvl>
|
||
</w:abstractNum>
|
||
<w:num w:numId="7"><w:abstractNumId w:val="3"/></w:num>
|
||
</w:numbering>"""
|
||
|
||
body = "".join([
|
||
numpara("One-one", 1),
|
||
numpara("One-two", 1),
|
||
numpara("One-two-a roman", 1, 1),
|
||
numpara("One-two-b roman", 1, 1),
|
||
numpara("Deep bullet", 1, 2),
|
||
numpara("One-three", 1),
|
||
para("Interruption paragraph."),
|
||
numpara("One-four continues the count", 1),
|
||
numpara("Two-one independent counter", 2),
|
||
numpara("Ten-start via override", 3),
|
||
numpara("Letter-A via numStyleLink", 5),
|
||
numpara("Level replaced and start overridden together", 6),
|
||
# suppressed numbering: style brings numId 1, direct numPr zeroes it
|
||
numpara("Suppressed numbering paragraph", 0, 0, pstyle="NumberedPara"),
|
||
# style-provided numbering only (no direct numPr)
|
||
para("Style-numbered paragraph", '<w:pPr><w:pStyle w:val="NumberedPara"/></w:pPr>'),
|
||
# S3: style-based numbering resolves its level through the abstract
|
||
# levels' pStyle bindings; the ilvl inside the styles' numPr (5 and
|
||
# 0 above - both wrong on purpose) must be ignored.
|
||
para("pStyle-bound level one", '<w:pPr><w:pStyle w:val="ListLevelOne"/></w:pPr>'),
|
||
para("pStyle-bound level two", '<w:pPr><w:pStyle w:val="ListLevelTwo"/></w:pPr>'),
|
||
para("pStyle-bound level one again", '<w:pPr><w:pStyle w:val="ListLevelOne"/></w:pPr>'),
|
||
# heading level via basedOn-inherited outlineLvl
|
||
para("Inherited subheading", '<w:pPr><w:pStyle w:val="SubHead"/></w:pPr>'),
|
||
# Toggle semantics (ECMA-376 17.7.3):
|
||
# (a) style-level false is a no-op -> stays bold
|
||
('<w:p><w:pPr><w:pStyle w:val="BoldPara"/></w:pPr>'
|
||
'<w:r><w:t xml:space="preserve">Bold here, </w:t></w:r>'
|
||
'<w:r><w:rPr><w:rStyle w:val="PlainChar"/></w:rPr>'
|
||
'<w:t xml:space="preserve">and style-false keeps it bold</w:t></w:r></w:p>'),
|
||
# (b) character-style true toggles inherited bold off
|
||
('<w:p><w:pPr><w:pStyle w:val="BoldPara"/></w:pPr>'
|
||
'<w:r><w:rPr><w:rStyle w:val="ToggleChar"/></w:rPr>'
|
||
'<w:t xml:space="preserve">toggled off by a true in the character style</w:t>'
|
||
'</w:r></w:p>'),
|
||
# (c) two trues along one basedOn chain cancel out
|
||
('<w:p><w:pPr><w:pStyle w:val="DoubleBold"/></w:pPr>'
|
||
'<w:r><w:t xml:space="preserve">double toggle cancels to plain</w:t></w:r></w:p>'),
|
||
# (d) direct formatting is absolute; "off" is a valid ST_OnOff value
|
||
('<w:p><w:pPr><w:pStyle w:val="BoldPara"/></w:pPr>'
|
||
'<w:r><w:rPr><w:b w:val="off"/></w:rPr>'
|
||
'<w:t xml:space="preserve">direct off wins absolutely</w:t></w:r></w:p>'),
|
||
# (e) direct "on" is a valid ST_OnOff value
|
||
('<w:p><w:r><w:rPr><w:i w:val="on"/></w:rPr>'
|
||
'<w:t xml:space="preserve">direct on makes this italic</w:t></w:r></w:p>'),
|
||
])
|
||
document = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:document {W} {R}><w:body>{body}</w:body></w:document>')
|
||
doc_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
|
||
</Relationships>"""
|
||
ct = CONTENT_TYPES_BASE.format(extra=(
|
||
'<Override PartName="/word/numbering.xml" ContentType='
|
||
'"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>'))
|
||
write_zip(OUT / "docx" / "handmade-numbering.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", document),
|
||
("word/_rels/document.xml.rels", doc_rels),
|
||
("word/styles.xml", styles),
|
||
("word/numbering.xml", numbering),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Handmade DOCX: chart + SmartArt + image rel (P7.5, P7.10, M4)
|
||
|
||
def rich_docx():
|
||
chart = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
||
<c:chart>
|
||
<c:title><c:tx><c:rich><a:p><a:r><a:t>Quarterly Widgets</a:t></a:r></a:p></c:rich></c:tx></c:title>
|
||
<c:plotArea>
|
||
<c:barChart>
|
||
<c:ser>
|
||
<c:tx><c:strRef><c:f>S</c:f><c:strCache><c:pt c:idx="0"><c:v>Widgets</c:v></c:pt></c:strCache></c:strRef></c:tx>
|
||
<c:cat><c:strRef><c:f>C</c:f><c:strCache>
|
||
<c:pt c:idx="0"><c:v>Q1</c:v></c:pt><c:pt c:idx="1"><c:v>Q2</c:v></c:pt>
|
||
</c:strCache></c:strRef></c:cat>
|
||
<c:val><c:numRef><c:f>V</c:f><c:numCache>
|
||
<c:pt c:idx="0"><c:v>10</c:v></c:pt><c:pt c:idx="1"><c:v>14</c:v></c:pt>
|
||
</c:numCache></c:numRef></c:val>
|
||
</c:ser>
|
||
</c:barChart>
|
||
<c:catAx><c:title><c:tx><c:rich><a:p><a:r><a:t>Quarter</a:t></a:r></a:p></c:rich></c:tx></c:title></c:catAx>
|
||
<c:valAx><c:title><c:tx><c:rich><a:p><a:r><a:t>Units</a:t></a:r></a:p></c:rich></c:tx></c:title></c:valAx>
|
||
</c:plotArea>
|
||
</c:chart>
|
||
</c:chartSpace>"""
|
||
|
||
diagram_data = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<dgm:dataModel xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
||
<dgm:ptLst>
|
||
<dgm:pt dgm:modelId="1" dgm:type="doc"/>
|
||
<dgm:pt dgm:modelId="2"><dgm:t><a:bodyPr/><a:p><a:r><a:t>Plan</a:t></a:r></a:p></dgm:t></dgm:pt>
|
||
<dgm:pt dgm:modelId="3"><dgm:t><a:bodyPr/><a:p><a:r><a:t>Build</a:t></a:r></a:p></dgm:t></dgm:pt>
|
||
<dgm:pt dgm:modelId="4"><dgm:t><a:bodyPr/><a:p><a:r><a:t>Ship</a:t></a:r></a:p></dgm:t></dgm:pt>
|
||
</dgm:ptLst>
|
||
</dgm:dataModel>"""
|
||
|
||
drawing_chart = (
|
||
'<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing">'
|
||
'<wp:extent cx="3000000" cy="2000000"/><wp:docPr id="1" name="Chart 1" descr="widget chart"/>'
|
||
'<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
||
'<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart">'
|
||
'<c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" r:id="rId10"/>'
|
||
"</a:graphicData></a:graphic></wp:inline></w:drawing>"
|
||
)
|
||
drawing_diagram = (
|
||
'<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing">'
|
||
'<wp:extent cx="3000000" cy="2000000"/><wp:docPr id="2" name="Diagram 1"/>'
|
||
'<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
||
'<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/diagram">'
|
||
'<dgm:relIds xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" r:dm="rId20" r:lo="" r:qs="" r:cs=""/>'
|
||
"</a:graphicData></a:graphic></wp:inline></w:drawing>"
|
||
)
|
||
drawing_image = (
|
||
'<w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing">'
|
||
'<wp:extent cx="100000" cy="100000"/><wp:docPr id="3" name="Dot" descr="tiny dot image"/>'
|
||
'<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
||
'<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">'
|
||
'<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">'
|
||
'<pic:blipFill><a:blip r:embed="rId30"/></pic:blipFill></pic:pic>'
|
||
"</a:graphicData></a:graphic></wp:inline></w:drawing>"
|
||
)
|
||
|
||
drawing_ole = (
|
||
'<w:object xmlns:o="urn:schemas-microsoft-com:office:office" '
|
||
'xmlns:v="urn:schemas-microsoft-com:vml">'
|
||
'<v:shape id="ole1" style="width:100pt;height:50pt"/>'
|
||
'<o:OLEObject Type="Embed" ProgID="Excel.Sheet.12" ShapeID="ole1" r:id="rId40"/>'
|
||
"</w:object>"
|
||
)
|
||
|
||
body = (
|
||
para("Rich objects follow.")
|
||
+ f"<w:p><w:r>{drawing_chart}</w:r></w:p>"
|
||
+ f"<w:p><w:r>{drawing_diagram}</w:r></w:p>"
|
||
+ f"<w:p><w:r>{drawing_image}</w:r></w:p>"
|
||
+ f"<w:p><w:r>{drawing_ole}</w:r></w:p>"
|
||
+ para("After the objects.")
|
||
)
|
||
document = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:document {W} {R}><w:body>{body}</w:body></w:document>')
|
||
doc_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||
<Relationship Id="rId10" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" Target="charts/chart1.xml"/>
|
||
<Relationship Id="rId20" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData" Target="diagrams/data1.xml"/>
|
||
<Relationship Id="rId30" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/dot.png"/>
|
||
<Relationship Id="rId40" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" Target="embeddings/oleObject1.bin"/>
|
||
</Relationships>"""
|
||
styles = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:styles {W}><w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults></w:styles>')
|
||
ct = CONTENT_TYPES_BASE.format(extra=(
|
||
'<Default Extension="png" ContentType="image/png"/>'
|
||
'<Default Extension="bin" ContentType="application/vnd.openxmlformats-officedocument.oleObject"/>'
|
||
'<Override PartName="/word/charts/chart1.xml" ContentType='
|
||
'"application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>'
|
||
'<Override PartName="/word/diagrams/data1.xml" ContentType='
|
||
'"application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml"/>'))
|
||
write_zip(OUT / "docx" / "handmade-rich.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", document),
|
||
("word/_rels/document.xml.rels", doc_rels),
|
||
("word/styles.xml", styles),
|
||
("word/charts/chart1.xml", chart),
|
||
("word/diagrams/data1.xml", diagram_data),
|
||
("word/media/dot.png", DOT_PNG),
|
||
("word/embeddings/oleObject1.bin", b"OLE-PAYLOAD-STAND-IN" * 4),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Handmade PPTX: placeholder inheritance through layout and master (H7)
|
||
|
||
PPTX_NS = ('xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
|
||
'xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" '
|
||
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"')
|
||
|
||
|
||
def inherit_pptx():
|
||
presentation = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:presentation {PPTX_NS}>
|
||
<p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst>
|
||
</p:presentation>"""
|
||
pres_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
|
||
</Relationships>"""
|
||
# Master: body text is bold with a character bullet at level 1.
|
||
master = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:sldMaster {PPTX_NS}>
|
||
<p:cSld><p:spTree>
|
||
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>
|
||
</p:spTree></p:cSld>
|
||
<p:txStyles>
|
||
<p:titleStyle><a:lvl1pPr><a:buNone/><a:defRPr/></a:lvl1pPr></p:titleStyle>
|
||
<p:bodyStyle><a:lvl1pPr><a:buChar char="•"/><a:defRPr b="1"/></a:lvl1pPr>
|
||
<a:lvl2pPr><a:buChar char="-"/><a:defRPr i="1"/></a:lvl2pPr></p:bodyStyle>
|
||
<p:otherStyle/>
|
||
</p:txStyles>
|
||
</p:sldMaster>"""
|
||
# Layout: overrides the body placeholder's level-1 bullet to roman
|
||
# numbering starting at 3.
|
||
layout = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:sldLayout {PPTX_NS}>
|
||
<p:cSld><p:spTree>
|
||
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>
|
||
<p:sp><p:nvSpPr><p:cNvPr id="2" name="Title"/><p:cNvSpPr/><p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr>
|
||
<p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>Layout title text</a:t></a:r></a:p></p:txBody></p:sp>
|
||
<p:sp><p:nvSpPr><p:cNvPr id="3" name="Body"/><p:cNvSpPr/><p:nvPr><p:ph idx="1"/></p:nvPr></p:nvSpPr>
|
||
<p:spPr/><p:txBody><a:bodyPr/>
|
||
<a:lstStyle><a:lvl1pPr><a:buAutoNum type="romanLcPeriod" startAt="3"/></a:lvl1pPr></a:lstStyle>
|
||
<a:p><a:endParaRPr/></a:p></p:txBody></p:sp>
|
||
</p:spTree></p:cSld>
|
||
</p:sldLayout>"""
|
||
slide = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:sld {PPTX_NS}>
|
||
<p:cSld><p:spTree>
|
||
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>
|
||
<p:sp><p:nvSpPr><p:cNvPr id="2" name="Title"/><p:cNvSpPr/><p:nvPr><p:ph type="ctrTitle"/></p:nvPr></p:nvSpPr>
|
||
<p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>Inherited Title Slide</a:t></a:r></a:p></p:txBody></p:sp>
|
||
<p:sp><p:nvSpPr><p:cNvPr id="3" name="Content"/><p:cNvSpPr/><p:nvPr><p:ph idx="1"/></p:nvPr></p:nvSpPr>
|
||
<p:spPr/><p:txBody><a:bodyPr/>
|
||
<a:p><a:r><a:t>Roman three bold via master</a:t></a:r></a:p>
|
||
<a:p><a:r><a:t>Roman four </a:t></a:r><a:r><a:rPr b="0"/><a:t>with bold turned off</a:t></a:r></a:p>
|
||
<a:p><a:pPr lvl="1"/><a:r><a:t>Dash bullet italic via master level two</a:t></a:r></a:p>
|
||
<a:p><a:pPr><a:buNone/></a:pPr><a:r><a:t>Plain closing line</a:t></a:r></a:p>
|
||
</p:txBody></p:sp>
|
||
</p:spTree></p:cSld>
|
||
</p:sld>"""
|
||
slide_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
|
||
</Relationships>"""
|
||
layout_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>
|
||
</Relationships>"""
|
||
ct = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
|
||
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
|
||
<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
|
||
<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
|
||
</Types>"""
|
||
root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
|
||
</Relationships>"""
|
||
write_zip(OUT / "pptx" / "handmade-inherit.pptx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", root_rels),
|
||
("ppt/presentation.xml", presentation),
|
||
("ppt/_rels/presentation.xml.rels", pres_rels),
|
||
("ppt/slides/slide1.xml", slide),
|
||
("ppt/slides/_rels/slide1.xml.rels", slide_rels),
|
||
("ppt/slideLayouts/slideLayout1.xml", layout),
|
||
("ppt/slideLayouts/_rels/slideLayout1.xml.rels", layout_rels),
|
||
("ppt/slideMasters/slideMaster1.xml", master),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R8: titles keep their shape-order position; p:oleObj payloads extracted
|
||
|
||
def order_pptx():
|
||
presentation = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:presentation {PPTX_NS}>
|
||
<p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst>
|
||
</p:presentation>"""
|
||
pres_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
|
||
</Relationships>"""
|
||
slide = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:sld {PPTX_NS}>
|
||
<p:cSld><p:spTree>
|
||
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>
|
||
<p:sp><p:nvSpPr><p:cNvPr id="2" name="Lead"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
|
||
<p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>Kicker before the title</a:t></a:r></a:p></p:txBody></p:sp>
|
||
<p:sp><p:nvSpPr><p:cNvPr id="3" name="Title"/><p:cNvSpPr/><p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr>
|
||
<p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>Title placed second</a:t></a:r></a:p></p:txBody></p:sp>
|
||
<p:sp><p:nvSpPr><p:cNvPr id="4" name="Body"/><p:cNvSpPr/><p:nvPr><p:ph idx="1"/></p:nvPr></p:nvSpPr>
|
||
<p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>Body after the title</a:t></a:r></a:p></p:txBody></p:sp>
|
||
<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="5" name="Sheet"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
|
||
<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/presentationml/2006/ole">
|
||
<p:oleObj name="Quarterly numbers" progId="Excel.Sheet.12" r:id="rId2"/>
|
||
</a:graphicData></a:graphic></p:graphicFrame>
|
||
</p:spTree></p:cSld></p:sld>"""
|
||
slide_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" Target="../embeddings/oleObject1.bin"/>
|
||
</Relationships>"""
|
||
root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
|
||
</Relationships>"""
|
||
ct = CONTENT_TYPES_BASE.format(extra=(
|
||
'<Default Extension="bin" ContentType="application/vnd.openxmlformats-officedocument.oleObject"/>'))
|
||
write_zip(OUT / "pptx" / "handmade-order.pptx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", root_rels),
|
||
("ppt/presentation.xml", presentation),
|
||
("ppt/_rels/presentation.xml.rels", pres_rels),
|
||
("ppt/slides/slide1.xml", slide),
|
||
("ppt/slides/_rels/slide1.xml.rels", slide_rels),
|
||
("ppt/embeddings/oleObject1.bin", b"OLE-PAYLOAD-STAND-IN" * 4),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R7: compressed .doc text decodes via the FIB language id's code page
|
||
|
||
def word_doc_stream(lid, text_bytes, far_east=False):
|
||
"""A minimal WordDocument stream: FIB base + legacy single-piece text
|
||
(fcMin/fcMac, no Clx), with the text stored fc-compressed (one byte per
|
||
CP) at offset 0x400."""
|
||
fib = bytearray(0x400)
|
||
struct.pack_into("<H", fib, 0x00, 0xA5EC) # wIdent
|
||
struct.pack_into("<H", fib, 0x02, 0x00C1) # nFib (Word 97)
|
||
flags = 0x4000 if far_east else 0 # fFarEast
|
||
struct.pack_into("<H", fib, 0x0A, flags)
|
||
if far_east:
|
||
struct.pack_into("<H", fib, 0x3C, lid) # FibRgW97.lidFE
|
||
else:
|
||
struct.pack_into("<H", fib, 0x06, lid) # FibBase.lid
|
||
struct.pack_into("<I", fib, 0x18, 0x400) # fcMin
|
||
struct.pack_into("<I", fib, 0x1C, 0x400 + len(text_bytes)) # fcMac
|
||
struct.pack_into("<I", fib, 0x4C, len(text_bytes)) # ccpText (byte CPs)
|
||
return bytes(fib) + text_bytes
|
||
|
||
|
||
def encoded_docs():
|
||
d = OUT / "doc"
|
||
sj = "こんにちは世界。\r日本語の段落です。\r".encode("shift_jis")
|
||
write_cfb(d / "handmade-shiftjis.doc",
|
||
[("WordDocument", word_doc_stream(0x0411, sj, far_east=True))])
|
||
ru = "Привет, мир!\rВторой абзац по-русски.\r".encode("cp1251")
|
||
write_cfb(d / "handmade-cyrillic.doc",
|
||
[("WordDocument", word_doc_stream(0x0419, ru))])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R5: internal ODF gaps keep their coordinates; only trailing filler elided
|
||
|
||
def gaps_ods():
|
||
content = """<?xml version="1.0"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
|
||
<office:body><office:spreadsheet>
|
||
<table:table table:name="RowGap">
|
||
<table:table-row><table:table-cell office:value-type="string"><text:p>top</text:p></table:table-cell></table:table-row>
|
||
<table:table-row table:number-rows-repeated="120"><table:table-cell/></table:table-row>
|
||
<table:table-row><table:table-cell office:value-type="string"><text:p>row 122 after the gap</text:p></table:table-cell></table:table-row>
|
||
<table:table-row table:number-rows-repeated="500"><table:table-cell/></table:table-row>
|
||
</table:table>
|
||
<table:table table:name="CellGap">
|
||
<table:table-row>
|
||
<table:table-cell office:value-type="string"><text:p>left</text:p></table:table-cell>
|
||
<table:table-cell table:number-columns-repeated="1010"/>
|
||
<table:table-cell office:value-type="string"><text:p>column 1012</text:p></table:table-cell>
|
||
<table:table-cell table:number-columns-repeated="200"/>
|
||
</table:table-row>
|
||
</table:table>
|
||
</office:spreadsheet></office:body></office:document-content>"""
|
||
write_zip(OUT / "ods" / "handmade-gaps.ods",
|
||
[("content.xml", content)],
|
||
mimetype_first="application/vnd.oasis.opendocument.spreadsheet")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R1: ISO 29500 Strict namespaces and rels-driven part discovery
|
||
|
||
STRICT_W = 'xmlns:w="http://purl.oclc.org/ooxml/wordprocessingml/main"'
|
||
STRICT_PPTX_NS = ('xmlns:a="http://purl.oclc.org/ooxml/drawingml/main" '
|
||
'xmlns:p="http://purl.oclc.org/ooxml/presentationml/main" '
|
||
'xmlns:r="http://purl.oclc.org/ooxml/officeDocument/relationships"')
|
||
|
||
|
||
def strict_alt_ooxml():
|
||
ct = CONTENT_TYPES_BASE.format(extra="")
|
||
|
||
# Strict DOCX: every namespace and relationship type from the Strict
|
||
# family; styles resolved through the document's typed relationship.
|
||
strict_doc = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:document {STRICT_W}><w:body>'
|
||
'<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr>'
|
||
'<w:r><w:t>Strict heading</w:t></w:r></w:p>'
|
||
'<w:p><w:r><w:t xml:space="preserve">Strict body with </w:t></w:r>'
|
||
'<w:r><w:rPr><w:rStyle w:val="Bold"/></w:rPr>'
|
||
'<w:t>toggled bold</w:t></w:r></w:p>'
|
||
'</w:body></w:document>')
|
||
strict_styles = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:styles {STRICT_W}>'
|
||
'<w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults>'
|
||
'<w:style w:type="paragraph" w:styleId="Heading1">'
|
||
'<w:name w:val="heading 1"/></w:style>'
|
||
'<w:style w:type="character" w:styleId="Bold">'
|
||
'<w:name w:val="Bold"/><w:rPr><w:b/></w:rPr></w:style>'
|
||
'</w:styles>')
|
||
strict_root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" Target="word/document.xml"/>
|
||
</Relationships>"""
|
||
strict_doc_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://purl.oclc.org/ooxml/officeDocument/relationships/styles" Target="styles.xml"/>
|
||
</Relationships>"""
|
||
write_zip(OUT / "docx" / "handmade-strict.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", strict_root_rels),
|
||
("word/document.xml", strict_doc),
|
||
("word/_rels/document.xml.rels", strict_doc_rels),
|
||
("word/styles.xml", strict_styles),
|
||
])
|
||
|
||
# Alternate-location DOCX: no part lives at its conventional path; the
|
||
# main part, styles, and footnotes are all found through relationships.
|
||
alt_doc = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:document {W}><w:body>'
|
||
'<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr>'
|
||
'<w:r><w:t>Relocated heading</w:t></w:r></w:p>'
|
||
'<w:p><w:r><w:t xml:space="preserve">Body found via rels</w:t></w:r>'
|
||
'<w:r><w:rPr><w:rStyle w:val="FootnoteRef"/></w:rPr>'
|
||
'<w:footnoteReference w:id="2"/></w:r></w:p>'
|
||
'</w:body></w:document>')
|
||
alt_styles = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:styles {W}>'
|
||
'<w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults>'
|
||
'<w:style w:type="paragraph" w:styleId="Heading1">'
|
||
'<w:name w:val="heading 1"/></w:style>'
|
||
'<w:style w:type="character" w:styleId="FootnoteRef">'
|
||
'<w:name w:val="footnote reference"/></w:style>'
|
||
'</w:styles>')
|
||
alt_footnotes = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:footnotes {W}>'
|
||
'<w:footnote w:type="separator" w:id="0"><w:p/></w:footnote>'
|
||
'<w:footnote w:id="2"><w:p><w:r>'
|
||
'<w:t>A relocated footnote</w:t></w:r></w:p></w:footnote>'
|
||
'</w:footnotes>')
|
||
alt_root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="content/main.xml"/>
|
||
</Relationships>"""
|
||
alt_doc_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="wordstyles.xml"/>
|
||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="n/footnotes.xml"/>
|
||
</Relationships>"""
|
||
write_zip(OUT / "docx" / "handmade-altpath.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", alt_root_rels),
|
||
("content/main.xml", alt_doc),
|
||
("content/_rels/main.xml.rels", alt_doc_rels),
|
||
("content/wordstyles.xml", alt_styles),
|
||
("content/n/footnotes.xml", alt_footnotes),
|
||
])
|
||
|
||
# Strict PPTX: Strict namespaces plus Strict relationship types down the
|
||
# presentation -> slide chain.
|
||
strict_pres = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<p:presentation {STRICT_PPTX_NS}>'
|
||
'<p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst>'
|
||
'</p:presentation>')
|
||
strict_slide = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<p:sld {STRICT_PPTX_NS}>'
|
||
'<p:cSld><p:spTree>'
|
||
'<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>'
|
||
'<p:sp><p:nvSpPr><p:cNvPr id="2" name="Title"/><p:cNvSpPr/>'
|
||
'<p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr>'
|
||
'<p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>Strict slide title</a:t></a:r></a:p></p:txBody></p:sp>'
|
||
'<p:sp><p:nvSpPr><p:cNvPr id="3" name="Body"/><p:cNvSpPr/>'
|
||
'<p:nvPr><p:ph idx="1"/></p:nvPr></p:nvSpPr>'
|
||
'<p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>Strict body text</a:t></a:r></a:p></p:txBody></p:sp>'
|
||
'</p:spTree></p:cSld></p:sld>')
|
||
strict_pres_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://purl.oclc.org/ooxml/officeDocument/relationships/slide" Target="slides/slide1.xml"/>
|
||
</Relationships>"""
|
||
strict_pptx_root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument" Target="ppt/presentation.xml"/>
|
||
</Relationships>"""
|
||
write_zip(OUT / "pptx" / "handmade-strict.pptx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", strict_pptx_root_rels),
|
||
("ppt/presentation.xml", strict_pres),
|
||
("ppt/_rels/presentation.xml.rels", strict_pres_rels),
|
||
("ppt/slides/slide1.xml", strict_slide),
|
||
])
|
||
|
||
# Alternate-location PPTX: presentation and slide at unconventional
|
||
# paths, reachable only through relationships.
|
||
alt_pres = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<p:presentation {PPTX_NS}>'
|
||
'<p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst>'
|
||
'</p:presentation>')
|
||
alt_slide = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<p:sld {PPTX_NS}>'
|
||
'<p:cSld><p:spTree>'
|
||
'<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>'
|
||
'<p:sp><p:nvSpPr><p:cNvPr id="2" name="Title"/><p:cNvSpPr/>'
|
||
'<p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr>'
|
||
'<p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>Relocated deck title</a:t></a:r></a:p></p:txBody></p:sp>'
|
||
'</p:spTree></p:cSld></p:sld>')
|
||
alt_pres_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="s/one.xml"/>
|
||
</Relationships>"""
|
||
alt_pptx_root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="deck/pres.xml"/>
|
||
</Relationships>"""
|
||
write_zip(OUT / "pptx" / "handmade-altpath.pptx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", alt_pptx_root_rels),
|
||
("deck/pres.xml", alt_pres),
|
||
("deck/_rels/pres.xml.rels", alt_pres_rels),
|
||
("deck/s/one.xml", alt_slide),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Handmade EPUB: rowspan, ol attrs, CSS display:none, non-heading anchors (M7, H13)
|
||
|
||
def features_epub():
|
||
css = "p.hidden { display: none; }\n.crossed { text-decoration: line-through; }\n"
|
||
ch1 = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>One</title>
|
||
<link rel="stylesheet" type="text/css" href="style.css"/></head><body>
|
||
<h1>Feature Chapter</h1>
|
||
<p style="font-weight: bold">Inline-styled bold paragraph.</p>
|
||
<p class="hidden">This hidden paragraph must not appear.</p>
|
||
<p><span class="crossed">Struck via class.</span></p>
|
||
<table>
|
||
<tr><td rowspan="2">Tall</td><td>B1</td></tr>
|
||
<tr><td>B2</td></tr>
|
||
<tr><td>A3</td><td>B3</td></tr>
|
||
</table>
|
||
<ol reversed="reversed" start="3"><li>three</li><li>two</li><li>one</li></ol>
|
||
<ol type="a"><li>alpha one</li><li value="5">jumps to five</li><li>six</li></ol>
|
||
<ol start="-1"><li>minus one</li><li>zero</li><li>one</li></ol>
|
||
<ol reversed="reversed" start="1"><li>one</li><li>zero</li><li>minus one</li></ol>
|
||
<p>See <a href="ch2.xhtml#target-span">the marked span</a> in chapter two,
|
||
or <a href="https://example.com/x">an external page</a>,
|
||
or <a href="notes.txt">a relative resource</a>.</p>
|
||
</body></html>"""
|
||
ch2 = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Two</title></head><body>
|
||
<h1>Second Chapter</h1>
|
||
<p>Leading text. <span id="target-span">This span is the anchor target.</span></p>
|
||
<p>Back to <a href="ch1.xhtml">chapter one's start</a>.</p>
|
||
</body></html>"""
|
||
opf = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
|
||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||
<dc:identifier id="uid">urn:uuid:00000000-0000-0000-0000-00000000f1x7</dc:identifier>
|
||
<dc:title>Feature Book</dc:title><dc:language>en</dc:language>
|
||
</metadata>
|
||
<manifest>
|
||
<item id="c1" href="ch1.xhtml" media-type="application/xhtml+xml"/>
|
||
<item id="c2" href="ch2.xhtml" media-type="application/xhtml+xml"/>
|
||
<item id="css" href="style.css" media-type="text/css"/>
|
||
</manifest>
|
||
<spine><itemref idref="c1"/><itemref idref="c2"/></spine>
|
||
</package>"""
|
||
container = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||
<rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles>
|
||
</container>"""
|
||
write_zip(OUT / "epub" / "handmade-features.epub", [
|
||
("META-INF/container.xml", container),
|
||
("OEBPS/content.opf", opf),
|
||
("OEBPS/ch1.xhtml", ch1),
|
||
("OEBPS/ch2.xhtml", ch2),
|
||
("OEBPS/style.css", css),
|
||
], mimetype_first="application/epub+zip")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Math: OMML (docx, pptx, rtf) and MathML (odt, epub) convert to LaTeX
|
||
|
||
M_NS = 'xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"'
|
||
MC_NS = 'xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"'
|
||
A14_NS = 'xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main"'
|
||
|
||
|
||
def omml_run(text):
|
||
return f'<m:r><m:t xml:space="preserve">{text}</m:t></m:r>'
|
||
|
||
|
||
# x = (-b ± √(b² - 4ac)) / 2a
|
||
OMML_QUADRATIC = (
|
||
"<m:oMath>" + omml_run("x") + omml_run("=")
|
||
+ "<m:f><m:num>" + omml_run("-b±")
|
||
+ '<m:rad><m:radPr><m:degHide m:val="1"/></m:radPr><m:deg/><m:e>'
|
||
+ "<m:sSup><m:e>" + omml_run("b") + "</m:e><m:sup>" + omml_run("2") + "</m:sup></m:sSup>"
|
||
+ omml_run("-4ac") + "</m:e></m:rad></m:num>"
|
||
+ "<m:den>" + omml_run("2a") + "</m:den></m:f></m:oMath>"
|
||
)
|
||
# ∑_{i=1}^{n} x_i = sin(θ)
|
||
OMML_SUM = (
|
||
'<m:oMath><m:nary><m:naryPr><m:chr m:val="∑"/><m:limLoc m:val="undOvr"/></m:naryPr>'
|
||
+ "<m:sub>" + omml_run("i=1") + "</m:sub><m:sup>" + omml_run("n") + "</m:sup>"
|
||
+ "<m:e><m:sSub><m:e>" + omml_run("x") + "</m:e><m:sub>" + omml_run("i") + "</m:sub></m:sSub></m:e></m:nary>"
|
||
+ omml_run("=")
|
||
+ '<m:func><m:fName><m:r><m:rPr><m:sty m:val="p"/></m:rPr><m:t>sin</m:t></m:r></m:fName>'
|
||
+ "<m:e><m:d><m:e>" + omml_run("θ") + "</m:e></m:d></m:e></m:func></m:oMath>"
|
||
)
|
||
# where 𝐱 ∈ ℝ (normal text run plus styled alphanumerics)
|
||
OMML_WHERE = (
|
||
'<m:oMath><m:r><m:rPr><m:nor/></m:rPr><m:t xml:space="preserve">where </m:t></m:r>'
|
||
+ omml_run("𝐱 ∈ ℝ") + "</m:oMath>"
|
||
)
|
||
|
||
|
||
def math_docx():
|
||
document = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:document {W} {M_NS}><w:body>
|
||
<w:p><w:r><w:t xml:space="preserve">The roots are </w:t></w:r>{OMML_QUADRATIC}<w:r><w:t xml:space="preserve"> for any a.</w:t></w:r></w:p>
|
||
<w:p><m:oMathPara>{OMML_SUM}{OMML_WHERE}</m:oMathPara></w:p>
|
||
<w:p><w:r><w:t>A price of $5 or $6 is not math.</w:t></w:r></w:p>
|
||
</w:body></w:document>"""
|
||
styles = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:styles {W}>'
|
||
'<w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults></w:styles>')
|
||
write_zip(OUT / "docx" / "handmade-math.docx", [
|
||
("[Content_Types].xml", CONTENT_TYPES_BASE.format(extra="")),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", document),
|
||
("word/styles.xml", styles),
|
||
])
|
||
|
||
|
||
def math_pptx():
|
||
"""PowerPoint wraps an equation shape in an a14 AlternateContent whose
|
||
fallback is a picture of the equation."""
|
||
presentation = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:presentation {PPTX_NS}>
|
||
<p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst>
|
||
</p:presentation>"""
|
||
pres_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
|
||
</Relationships>"""
|
||
slide = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:sld {PPTX_NS} {MC_NS} {A14_NS} {M_NS}>
|
||
<p:cSld><p:spTree>
|
||
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>
|
||
<p:sp><p:nvSpPr><p:cNvPr id="2" name="Title"/><p:cNvSpPr/><p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr>
|
||
<p:spPr/><p:txBody><a:bodyPr/><a:p><a:r><a:t>Quadratic formula</a:t></a:r></a:p></p:txBody></p:sp>
|
||
<mc:AlternateContent><mc:Choice Requires="a14">
|
||
<p:sp><p:nvSpPr><p:cNvPr id="3" name="Equation"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
|
||
<p:spPr/><p:txBody><a:bodyPr/>
|
||
<a:p><a:r><a:t>Solve with </a:t></a:r><a14:m><m:oMathPara>{OMML_QUADRATIC}</m:oMathPara></a14:m></a:p>
|
||
<a:p><a14:m><m:oMathPara>{OMML_SUM}</m:oMathPara></a14:m></a:p>
|
||
</p:txBody></p:sp>
|
||
</mc:Choice><mc:Fallback>
|
||
<p:sp><p:nvSpPr><p:cNvPr id="3" name="Equation"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
|
||
<p:spPr><a:blipFill><a:blip r:embed="rId2"/><a:stretch><a:fillRect/></a:stretch></a:blipFill></p:spPr>
|
||
<p:txBody><a:bodyPr/><a:p/></p:txBody></p:sp>
|
||
</mc:Fallback></mc:AlternateContent>
|
||
</p:spTree></p:cSld></p:sld>"""
|
||
slide_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image1.png"/>
|
||
</Relationships>"""
|
||
root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
|
||
</Relationships>"""
|
||
ct = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Default Extension="png" ContentType="image/png"/>
|
||
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
|
||
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
|
||
</Types>"""
|
||
write_zip(OUT / "pptx" / "handmade-math.pptx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", root_rels),
|
||
("ppt/presentation.xml", presentation),
|
||
("ppt/_rels/presentation.xml.rels", pres_rels),
|
||
("ppt/slides/slide1.xml", slide),
|
||
("ppt/slides/_rels/slide1.xml.rels", slide_rels),
|
||
("ppt/media/image1.png", DOT_PNG),
|
||
])
|
||
|
||
|
||
def math_odt():
|
||
"""LibreOffice stores a formula as an object directory holding MathML
|
||
(with a StarMath annotation) next to a replacement metafile; ODF also
|
||
allows the MathML inline in the draw:object."""
|
||
formula = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
|
||
<semantics>
|
||
<mrow><mi>E</mi><mo stretchy="false">=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow>
|
||
<annotation encoding="StarMath 5.0">E = m c^2</annotation>
|
||
</semantics>
|
||
</math>"""
|
||
content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
||
xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
|
||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||
xmlns:math="http://www.w3.org/1998/Math/MathML" office:version="1.3">
|
||
<office:body><office:text>
|
||
<text:p>Einstein wrote <draw:frame draw:name="Object1" text:anchor-type="as-char">
|
||
<draw:object xlink:href="./Object 1" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
|
||
<draw:image xlink:href="./ObjectReplacements/Object 1" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
|
||
</draw:frame> on the board.</text:p>
|
||
<text:p>Inline object: <draw:frame draw:name="Object2" text:anchor-type="as-char"><draw:object>
|
||
<math:math><math:semantics><math:mrow><math:mfrac><math:mi>a</math:mi><math:mi>b</math:mi></math:mfrac>
|
||
<math:mo>+</math:mo><math:msqrt><math:mi>x</math:mi></math:msqrt></math:mrow></math:semantics></math:math>
|
||
</draw:object></draw:frame> done.</text:p>
|
||
</office:text></office:body></office:document-content>"""
|
||
manifest = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.3">
|
||
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.text"/>
|
||
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
|
||
<manifest:file-entry manifest:full-path="Object 1/content.xml" manifest:media-type="text/xml"/>
|
||
<manifest:file-entry manifest:full-path="Object 1/" manifest:media-type="application/vnd.oasis.opendocument.formula"/>
|
||
<manifest:file-entry manifest:full-path="ObjectReplacements/Object 1" manifest:media-type=""/>
|
||
</manifest:manifest>"""
|
||
write_zip(OUT / "odt" / "handmade-math.odt", [
|
||
("content.xml", content),
|
||
("META-INF/manifest.xml", manifest),
|
||
("Object 1/content.xml", formula),
|
||
("ObjectReplacements/Object 1", b"VCLMTF\x00replacement-stand-in"),
|
||
], mimetype_first="application/vnd.oasis.opendocument.text")
|
||
|
||
|
||
def math_epub():
|
||
ch = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Math</title></head><body>
|
||
<h1>Formulas</h1>
|
||
<p>Euler: <math xmlns="http://www.w3.org/1998/Math/MathML"><semantics>
|
||
<mrow><msup><mi>e</mi><mrow><mi>i</mi><mi>π</mi></mrow></msup><mo>+</mo><mn>1</mn><mo>=</mo><mn>0</mn></mrow>
|
||
<annotation encoding="application/x-tex">e^{i\\pi} + 1 = 0</annotation>
|
||
</semantics></math> holds.</p>
|
||
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
|
||
<mrow><munderover><mo>∑</mo><mrow><mi>k</mi><mo>=</mo><mn>0</mn></mrow><mi>∞</mi></munderover>
|
||
<mfrac><msup><mi>x</mi><mi>k</mi></msup><mrow><mi>k</mi><mo>!</mo></mrow></mfrac>
|
||
<mo>=</mo><mi>exp</mi><mo></mo><mfenced><mi>x</mi></mfenced></mrow>
|
||
</math>
|
||
<p>Vectors: <math xmlns="http://www.w3.org/1998/Math/MathML"><mover><mi>v</mi><mo>→</mo></mover><mo>·</mo><mover><mi>w</mi><mo>→</mo></mover></math>.</p>
|
||
</body></html>"""
|
||
opf = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
|
||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||
<dc:identifier id="uid">urn:uuid:00000000-0000-0000-0000-0000000math0</dc:identifier>
|
||
<dc:title>Math Book</dc:title><dc:language>en</dc:language>
|
||
</metadata>
|
||
<manifest><item id="c1" href="ch1.xhtml" media-type="application/xhtml+xml" properties="mathml"/></manifest>
|
||
<spine><itemref idref="c1"/></spine>
|
||
</package>"""
|
||
container = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||
<rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles>
|
||
</container>"""
|
||
write_zip(OUT / "epub" / "handmade-math.epub", [
|
||
("META-INF/container.xml", container),
|
||
("OEBPS/content.opf", opf),
|
||
("OEBPS/ch1.xhtml", ch),
|
||
], mimetype_first="application/epub+zip")
|
||
|
||
|
||
def math_rtf():
|
||
"""Word's rtf: a math zone nests `{\\*\\moMath ...}` (inside a
|
||
`\\*\\moMathPara` when displayed) and a `\\mmathPict` picture fallback;
|
||
run style is a parameter word on the run and flags are `on`."""
|
||
def r(text):
|
||
return "{\\mr\\mscr0\\msty2 " + text + "}"
|
||
parts = [
|
||
"{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0 Cambria Math;}}",
|
||
"\\pard The roots are {\\mmath{\\*\\moMath{\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\f0\\insrsid1 }" + r("x") + r("=")
|
||
+ "{\\mf{\\mfPr{\\mctrlPr\\f0 }}{\\mnum" + r("-b\\'b1") + "{\\mrad{\\mradPr{\\mdegHide on}{\\mctrlPr\\f0 }}{\\mdeg}"
|
||
+ "{\\me{\\msSup{\\me" + r("b") + "}{\\msup" + r("2") + "}}" + r("-4ac") + "}}}{\\mden" + r("2a") + "}}}"
|
||
+ "{\\mmathPict{\\*\\mmathPr}{\\pict\\pngblip\\picw1\\pich1 89504e47}}} for any a.\\par",
|
||
"\\pard {\\mmath{\\*\\moMathPara{\\moMathParaPr{\\mjc centerGroup}}{\\*\\moMath"
|
||
+ "{\\mnary{\\mnaryPr{\\mchr \\u8721 ?}{\\mlimLoc undOvr}{\\mctrlPr\\f0 }}"
|
||
+ "{\\msub" + r("i=1") + "}{\\msup" + r("n") + "}{\\me{\\msSub{\\me" + r("x") + "}{\\msub" + r("i") + "}}}}" + r("=")
|
||
+ "{\\mfunc{\\mfName{\\mr\\mscr0\\msty0 sin}}{\\me{\\md{\\mdPr{\\mctrlPr\\f0 }}{\\me" + r("\\u952 ?") + "}}}}}"
|
||
+ "{\\*\\moMath{\\mr\\mnor where }" + r("\\u8722 ?1<x") + "}}}\\par",
|
||
"\\pard A price of $5 or $6 is not math.\\par",
|
||
"}",
|
||
]
|
||
(OUT / "rtf" / "handmade-math.rtf").write_bytes("\n".join(parts).encode("cp1252"))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R17a: merged ranges in spreadsheets become spanning grid cells
|
||
|
||
def merged_xlsx():
|
||
ct = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
||
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
||
</Types>"""
|
||
root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
||
</Relationships>"""
|
||
workbook = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||
<sheets><sheet name="Merged" sheetId="1" r:id="rId1"/></sheets></workbook>"""
|
||
wb_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
||
</Relationships>"""
|
||
sheet = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||
<sheetData>
|
||
<row r="1">
|
||
<c r="A1" t="inlineStr"><is><t>Merged across</t></is></c>
|
||
<c r="C1" t="inlineStr"><is><t xml:space="preserve"> padded </t></is></c>
|
||
</row>
|
||
<row r="2">
|
||
<c r="A2" t="inlineStr"><is><t>tall</t></is></c>
|
||
<c r="B2" t="inlineStr"><is><t>b2</t></is></c>
|
||
<c r="C2"><v>3.5</v></c>
|
||
</row>
|
||
<row r="3"><c r="B3" t="inlineStr"><is><t>b3</t></is></c></row>
|
||
</sheetData>
|
||
<mergeCells count="2"><mergeCell ref="A1:B1"/><mergeCell ref="A2:A3"/></mergeCells>
|
||
</worksheet>"""
|
||
write_zip(OUT / "xlsx" / "handmade-merged.xlsx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", root_rels),
|
||
("xl/workbook.xml", workbook),
|
||
("xl/_rels/workbook.xml.rels", wb_rels),
|
||
("xl/worksheets/sheet1.xml", sheet),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Handmade XLSB: binary SpreadsheetML records (MS-XLSB). Exercises the
|
||
# variable-length record framing (two-byte ids, multi-byte sizes via a long
|
||
# shared string), RK integer/float cells with and without the /100 bit,
|
||
# number formats, a hidden row, a hidden column, a hidden sheet, and a merge
|
||
# extending past the populated range.
|
||
|
||
def xlsb_rec(rec_id, payload=b""):
|
||
out = bytearray()
|
||
if rec_id < 0x80:
|
||
out.append(rec_id)
|
||
else:
|
||
out.append((rec_id & 0x7F) | 0x80)
|
||
out.append(rec_id >> 7)
|
||
size = len(payload)
|
||
while True:
|
||
low = size & 0x7F
|
||
size >>= 7
|
||
if size == 0:
|
||
out.append(low)
|
||
break
|
||
out.append(low | 0x80)
|
||
return bytes(out) + payload
|
||
|
||
|
||
def xlsb_str(s):
|
||
data = s.encode("utf-16-le")
|
||
return struct.pack("<I", len(data) // 2) + data
|
||
|
||
|
||
def xlsb_cell(col, style=0):
|
||
return struct.pack("<II", col, style)
|
||
|
||
|
||
def xlsb_row(r, hidden=False):
|
||
flags = 0x10 if hidden else 0
|
||
return xlsb_rec(0, struct.pack("<IIHBBBI", r, 0, 0, 0, flags, 0, 0))
|
||
|
||
|
||
def sheet_xlsb():
|
||
root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.bin"/>
|
||
</Relationships>"""
|
||
wb_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.bin"/>
|
||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.bin"/>
|
||
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.bin"/>
|
||
<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.bin"/>
|
||
</Relationships>"""
|
||
workbook = (
|
||
xlsb_rec(153, struct.pack("<II", 0, 0) + xlsb_str("")) # BrtWbProp, 1900 dates
|
||
+ xlsb_rec(156, struct.pack("<II", 0, 1) + xlsb_str("rId1") + xlsb_str("Data"))
|
||
+ xlsb_rec(156, struct.pack("<II", 1, 2) + xlsb_str("rId2") + xlsb_str("Secret"))
|
||
)
|
||
# cellXfs: 0 General, 1 percent, 2 currency, 3 builtin date (ifmt 14).
|
||
xf = lambda ifmt: xlsb_rec(47, struct.pack("<HH", 0, ifmt) + bytes(12))
|
||
styles = (
|
||
xlsb_rec(44, struct.pack("<H", 164) + xlsb_str("0.0%"))
|
||
+ xlsb_rec(44, struct.pack("<H", 165) + xlsb_str('"$"#,##0.00'))
|
||
+ xlsb_rec(617, struct.pack("<I", 4))
|
||
+ xf(0) + xf(164) + xf(165) + xf(14)
|
||
+ xlsb_rec(618)
|
||
)
|
||
# The long entry forces a record size beyond one byte.
|
||
sst_items = ["Region", "north " * 40]
|
||
shared = b"".join(xlsb_rec(19, b"\x00" + xlsb_str(s)) for s in sst_items)
|
||
isst = lambda col, i: xlsb_rec(7, xlsb_cell(col) + struct.pack("<I", i))
|
||
real = lambda col, style, v: xlsb_rec(5, xlsb_cell(col, style) + struct.pack("<d", v))
|
||
rk = lambda col, style, v: xlsb_rec(2, xlsb_cell(col, style) + struct.pack("<I", v))
|
||
st = lambda col, s: xlsb_rec(6, xlsb_cell(col) + xlsb_str(s))
|
||
data = (
|
||
xlsb_rec(60, struct.pack("<IIIIH", 3, 3, 0, 0, 1)) # column D hidden
|
||
+ xlsb_row(0)
|
||
+ isst(0, 0) + isst(1, 1) + st(2, "Notes") + st(3, "hidden column")
|
||
+ xlsb_row(1)
|
||
+ rk(0, 0, (42 << 2) | 2) # RK integer 42
|
||
+ rk(1, 1, (65 << 2) | 2 | 1) # RK integer/100: 65% via 0.65
|
||
+ real(2, 2, 1234.5) # currency
|
||
+ xlsb_row(2, hidden=True)
|
||
+ st(0, "hidden row")
|
||
+ xlsb_row(3)
|
||
+ rk(0, 0, struct.unpack("<Q", struct.pack("<d", 1.5))[0] >> 32) # RK float
|
||
+ rk(1, 0, (struct.unpack("<Q", struct.pack("<d", 1.5))[0] >> 32) | 1) # RK float/100
|
||
+ real(2, 3, 46096.0) # builtin date format
|
||
+ xlsb_row(4)
|
||
+ st(0, "wide merge")
|
||
+ xlsb_rec(4, xlsb_cell(1) + b"\x01") # TRUE
|
||
+ xlsb_rec(3, xlsb_cell(2) + b"\x07") # #DIV/0!
|
||
+ xlsb_rec(177, struct.pack("<I", 1)) # BrtBeginMergeCells
|
||
+ xlsb_rec(176, struct.pack("<IIII", 4, 5, 0, 2)) # A5:C6, past the populated rows
|
||
+ xlsb_rec(178) # BrtEndMergeCells
|
||
)
|
||
secret = xlsb_row(0) + st(0, "must not appear")
|
||
write_zip(OUT / "xlsb" / "handmade-sheet.xlsb", [
|
||
("_rels/.rels", root_rels),
|
||
("xl/workbook.bin", workbook),
|
||
("xl/_rels/workbook.bin.rels", wb_rels),
|
||
("xl/styles.bin", styles),
|
||
("xl/sharedStrings.bin", shared),
|
||
("xl/worksheets/sheet1.bin", data),
|
||
("xl/worksheets/sheet2.bin", secret),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R16: ODF style:default-style beneath named chains; full ISO durations
|
||
|
||
def defaults_odf():
|
||
styles_xml = """<?xml version="1.0"?>
|
||
<office:document-styles xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
||
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0">
|
||
<office:styles>
|
||
<style:default-style style:family="paragraph">
|
||
<style:text-properties fo:font-weight="bold"/>
|
||
</style:default-style>
|
||
<style:style style:name="Norm" style:family="paragraph"/>
|
||
<style:style style:name="Off" style:family="paragraph">
|
||
<style:text-properties fo:font-weight="normal"/>
|
||
</style:style>
|
||
</office:styles>
|
||
</office:document-styles>"""
|
||
content_xml = """<?xml version="1.0"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
|
||
<office:body><office:text>
|
||
<text:p text:style-name="Norm">Bold via the family default style</text:p>
|
||
<text:p text:style-name="Off">Explicitly back to normal weight</text:p>
|
||
<text:p>Unstyled paragraph also inherits the default</text:p>
|
||
</office:text></office:body></office:document-content>"""
|
||
write_zip(OUT / "odt" / "handmade-defaults.odt", [
|
||
("content.xml", content_xml),
|
||
("styles.xml", styles_xml),
|
||
], mimetype_first="application/vnd.oasis.opendocument.text")
|
||
|
||
durations = """<?xml version="1.0"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
|
||
<office:body><office:spreadsheet><table:table table:name="Durations">
|
||
<table:table-row>
|
||
<table:table-cell office:value-type="time" office:time-value="P1DT2H"/>
|
||
<table:table-cell office:value-type="time" office:time-value="PT26H30M15S"/>
|
||
<table:table-cell office:value-type="time" office:time-value="P2W"/>
|
||
<table:table-cell office:value-type="time" office:time-value="-PT1H5M"/>
|
||
<table:table-cell office:value-type="time" office:time-value="P1M"/>
|
||
</table:table-row>
|
||
<table:table-row>
|
||
<table:table-cell office:value-type="time" office:time-value="P0.5D"/>
|
||
<table:table-cell office:value-type="time" office:time-value="PT1.5H"/>
|
||
<table:table-cell office:value-type="time" office:time-value="PT1.5M"/>
|
||
<table:table-cell office:value-type="time" office:time-value="PT1.25S"/>
|
||
<table:table-cell office:value-type="time" office:time-value="PT99999999999999999999H"/>
|
||
</table:table-row>
|
||
</table:table></office:spreadsheet></office:body></office:document-content>"""
|
||
write_zip(OUT / "ods" / "handmade-durations.ods",
|
||
[("content.xml", durations)],
|
||
mimetype_first="application/vnd.oasis.opendocument.spreadsheet")
|
||
|
||
# S7: list headers render without markers, start-value restarts split
|
||
# the run, and continue-list resolves by the target's xml:id.
|
||
lists_xml = """<?xml version="1.0"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
||
xmlns:xml="http://www.w3.org/XML/1998/namespace">
|
||
<office:automatic-styles>
|
||
<text:list-style style:name="Num">
|
||
<text:list-level-style-number text:level="1" style:num-format="1"/>
|
||
</text:list-style>
|
||
</office:automatic-styles>
|
||
<office:body><office:text>
|
||
<text:list xml:id="lstA" text:style-name="Num">
|
||
<text:list-header><text:p>Header without a marker</text:p></text:list-header>
|
||
<text:list-item><text:p>one</text:p></text:list-item>
|
||
<text:list-item><text:p>two</text:p></text:list-item>
|
||
<text:list-item text:start-value="10"><text:p>ten via restart</text:p></text:list-item>
|
||
<text:list-item><text:p>eleven</text:p></text:list-item>
|
||
</text:list>
|
||
<text:p>Interruption paragraph.</text:p>
|
||
<text:list text:continue-list="lstA" text:style-name="Num">
|
||
<text:list-item><text:p>twelve continues lstA</text:p></text:list-item>
|
||
</text:list>
|
||
</office:text></office:body></office:document-content>"""
|
||
write_zip(OUT / "odt" / "handmade-lists.odt",
|
||
[("content.xml", lists_xml)],
|
||
mimetype_first="application/vnd.oasis.opendocument.text")
|
||
|
||
# S18: encryption is detected from the actual manifest:encryption-data
|
||
# element - a comment mentioning it must not classify the document as
|
||
# encrypted, while a real element must.
|
||
plain_content = """<?xml version="1.0"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
|
||
<office:body><office:text><text:p>Not encrypted at all.</text:p></office:text></office:body>
|
||
</office:document-content>"""
|
||
manifest_comment = """<?xml version="1.0"?>
|
||
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
|
||
<!-- this comment mentions encryption-data but the package is plain -->
|
||
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.text"/>
|
||
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
|
||
</manifest:manifest>"""
|
||
write_zip(OUT / "odt" / "handmade-manifestcomment.odt", [
|
||
("content.xml", plain_content),
|
||
("META-INF/manifest.xml", manifest_comment),
|
||
], mimetype_first="application/vnd.oasis.opendocument.text")
|
||
manifest_encrypted = """<?xml version="1.0"?>
|
||
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
|
||
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml">
|
||
<manifest:encryption-data manifest:checksum-type="SHA1/1K" manifest:checksum="AAAA">
|
||
<manifest:algorithm manifest:algorithm-name="Blowfish CFB" manifest:initialisation-vector="BBBB"/>
|
||
<manifest:key-derivation manifest:key-derivation-name="PBKDF2" manifest:iteration-count="1024" manifest:salt="CCCC"/>
|
||
</manifest:encryption-data>
|
||
</manifest:file-entry>
|
||
</manifest:manifest>"""
|
||
write_zip(OUT / "malformed" / "encrypted--errors.odt", [
|
||
("content.xml", b"\x00\x01ciphertext-stand-in"),
|
||
("META-INF/manifest.xml", manifest_encrypted),
|
||
], mimetype_first="application/vnd.oasis.opendocument.text")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R15: DOCX gridBefore/gridAfter, legacy hMerge, ST_OnOff tblHeader
|
||
|
||
def tables_docx():
|
||
def tc(text, extra=""):
|
||
return (f'<w:tc><w:tcPr>{extra}</w:tcPr>'
|
||
f'<w:p><w:r><w:t xml:space="preserve">{text}</w:t></w:r></w:p></w:tc>')
|
||
|
||
document = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:document {W}><w:body>
|
||
<w:tbl>
|
||
<w:tr><w:trPr><w:tblHeader/></w:trPr>
|
||
{tc("Head A")}{tc("Head B")}{tc("Head C", '<w:vMerge w:val="restart"/>')}</w:tr>
|
||
<w:tr><w:trPr><w:tblHeader w:val="0"/><w:gridBefore w:val="2"/></w:trPr>
|
||
{tc("under C", '<w:vMerge/>')}</w:tr>
|
||
<w:tr><w:trPr><w:gridAfter w:val="1"/></w:trPr>
|
||
{tc("legacy start", '<w:hMerge w:val="restart"/>')}{tc("legacy cont", '<w:hMerge/>')}</w:tr>
|
||
</w:tbl>
|
||
<w:p><w:r><w:t>after the table</w:t></w:r></w:p>
|
||
</w:body></w:document>"""
|
||
styles = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:styles {W}>'
|
||
'<w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults></w:styles>')
|
||
ct = CONTENT_TYPES_BASE.format(extra="")
|
||
write_zip(OUT / "docx" / "handmade-tables.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", document),
|
||
("word/styles.xml", styles),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# S12: a valid document referencing one part many times must convert with a
|
||
# single retained asset; the part is cached and the asset deduplicated. (The
|
||
# budget re-charging itself is covered by unit tests - a fixture big enough
|
||
# to cross the 512 MiB budget would slow the mutation harness down.)
|
||
|
||
def manyrefs_docx():
|
||
drawing = ('<w:p><w:r><w:drawing>'
|
||
'<a:blip xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"'
|
||
' r:embed="rId10"/>'
|
||
'</w:drawing></w:r></w:p>')
|
||
document = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:document {W} {R}><w:body>'
|
||
'<w:p><w:r><w:t>Seventy references to one image follow.</w:t></w:r></w:p>'
|
||
+ drawing * 70 +
|
||
'</w:body></w:document>')
|
||
doc_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId10" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/logo.png"/>
|
||
</Relationships>"""
|
||
ct = CONTENT_TYPES_BASE.format(extra='<Default Extension="png" ContentType="image/png"/>')
|
||
write_zip(OUT / "docx" / "handmade-manyrefs.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", document),
|
||
("word/_rels/document.xml.rels", doc_rels),
|
||
("word/media/logo.png", b"\x89PNG\r\n\x1a\n" + b"\x00" * (64 * 1024)),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# S9: links to another slide resolve to the target slide's start anchor
|
||
# instead of a dead relative reference to slides/slideN.xml.
|
||
|
||
def links_pptx():
|
||
presentation = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:presentation {PPTX_NS}>
|
||
<p:sldIdLst><p:sldId id="256" r:id="rId1"/><p:sldId id="257" r:id="rId2"/></p:sldIdLst>
|
||
</p:presentation>"""
|
||
pres_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
|
||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide2.xml"/>
|
||
</Relationships>"""
|
||
|
||
def slide(body):
|
||
return f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:sld {PPTX_NS}>
|
||
<p:cSld><p:spTree>
|
||
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>
|
||
{body}
|
||
</p:spTree></p:cSld></p:sld>"""
|
||
|
||
slide1 = slide(
|
||
'<p:sp><p:nvSpPr><p:cNvPr id="2" name="Body"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>'
|
||
'<p:spPr/><p:txBody><a:bodyPr/>'
|
||
'<a:p><a:r><a:rPr><a:hlinkClick r:id="rId5"/></a:rPr><a:t>Jump to the second slide</a:t></a:r></a:p>'
|
||
'<a:p><a:r><a:rPr><a:hlinkClick r:id="rId6"/></a:rPr><a:t>External link</a:t></a:r></a:p>'
|
||
'</p:txBody></p:sp>')
|
||
slide1_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slide2.xml"/>
|
||
<Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://example.com/" TargetMode="External"/>
|
||
</Relationships>"""
|
||
slide2 = slide(
|
||
'<p:sp><p:nvSpPr><p:cNvPr id="2" name="Body"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>'
|
||
'<p:spPr/><p:txBody><a:bodyPr/>'
|
||
'<a:p><a:r><a:t>Second slide content</a:t></a:r></a:p>'
|
||
'</p:txBody></p:sp>')
|
||
ct = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
|
||
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
|
||
<Override PartName="/ppt/slides/slide2.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
|
||
</Types>"""
|
||
root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
|
||
</Relationships>"""
|
||
write_zip(OUT / "pptx" / "handmade-links.pptx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", root_rels),
|
||
("ppt/presentation.xml", presentation),
|
||
("ppt/_rels/presentation.xml.rels", pres_rels),
|
||
("ppt/slides/slide1.xml", slide1),
|
||
("ppt/slides/_rels/slide1.xml.rels", slide1_rels),
|
||
("ppt/slides/slide2.xml", slide2),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# S5: standard Word OLE markup - a VML preview image next to the
|
||
# o:OLEObject; the object's identity and payload must win over the preview.
|
||
|
||
def ole_docx():
|
||
V = 'xmlns:v="urn:schemas-microsoft-com:vml"'
|
||
O = 'xmlns:o="urn:schemas-microsoft-com:office:office"'
|
||
document = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:document {W} {R} {V} {O}><w:body>
|
||
<w:p><w:r><w:t>Before the object</w:t></w:r></w:p>
|
||
<w:p><w:r><w:object>
|
||
<v:shape id="_x0000_i1025" style="width:96pt;height:48pt">
|
||
<v:imagedata r:id="rId3" o:title="preview"/>
|
||
</v:shape>
|
||
<o:OLEObject Type="Embed" ProgID="Excel.Sheet.12" ShapeID="_x0000_i1025" r:id="rId2"/>
|
||
</w:object></w:r></w:p>
|
||
</w:body></w:document>"""
|
||
doc_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" Target="embeddings/oleObject1.bin"/>
|
||
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/preview.png"/>
|
||
</Relationships>"""
|
||
styles = (f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||
f'<w:styles {W}>'
|
||
'<w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults></w:styles>')
|
||
ct = CONTENT_TYPES_BASE.format(extra=(
|
||
'<Default Extension="bin" ContentType="application/vnd.openxmlformats-officedocument.oleObject"/>'
|
||
'<Default Extension="png" ContentType="image/png"/>'))
|
||
write_zip(OUT / "docx" / "handmade-ole.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", document),
|
||
("word/_rels/document.xml.rels", doc_rels),
|
||
("word/styles.xml", styles),
|
||
("word/embeddings/oleObject1.bin", b"DOCX-OLE-PAYLOAD" * 4),
|
||
("word/media/preview.png", b"\x89PNG\r\n\x1a\nfakepreview"),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# S2: outline level is a tri-state property; direct paragraph properties
|
||
# override the style chain, and outlineLvl 9 is the explicit off state.
|
||
|
||
def outline_docx():
|
||
def p(text, ppr=""):
|
||
pr = f"<w:pPr>{ppr}</w:pPr>" if ppr else ""
|
||
return f'<w:p>{pr}<w:r><w:t>{text}</w:t></w:r></w:p>'
|
||
|
||
document = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:document {W}><w:body>
|
||
{p("Style heading stays a heading", '<w:pStyle w:val="Outlined"/>')}
|
||
{p("Direct level overrides the style", '<w:pStyle w:val="Outlined"/><w:outlineLvl w:val="2"/>')}
|
||
{p("Direct nine turns the style heading off", '<w:pStyle w:val="Outlined"/><w:outlineLvl w:val="9"/>')}
|
||
{p("Direct outline without a style", '<w:outlineLvl w:val="0"/>')}
|
||
{p("Child style nine stops inheritance", '<w:pStyle w:val="NotOutlined"/>')}
|
||
</w:body></w:document>"""
|
||
styles = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:styles {W}>
|
||
<w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults>
|
||
<w:style w:type="paragraph" w:styleId="Outlined"><w:name w:val="Outlined"/>
|
||
<w:pPr><w:outlineLvl w:val="1"/></w:pPr></w:style>
|
||
<w:style w:type="paragraph" w:styleId="NotOutlined"><w:name w:val="NotOutlined"/>
|
||
<w:basedOn w:val="Outlined"/><w:pPr><w:outlineLvl w:val="9"/></w:pPr></w:style>
|
||
</w:styles>"""
|
||
ct = CONTENT_TYPES_BASE.format(extra="")
|
||
write_zip(OUT / "docx" / "handmade-outline.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", document),
|
||
("word/styles.xml", styles),
|
||
])
|
||
|
||
|
||
def blockstyle_odt():
|
||
"""LibreOffice names its quote and code containers Quotations and
|
||
Preformatted Text; automatic styles reach them through parent-style-name."""
|
||
styles_xml = """<?xml version="1.0"?>
|
||
<office:document-styles xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0">
|
||
<office:styles>
|
||
<style:style style:name="Quotations" style:family="paragraph" style:parent-style-name="Standard"/>
|
||
<style:style style:name="Preformatted_20_Text" style:display-name="Preformatted Text"
|
||
style:family="paragraph" style:parent-style-name="Standard"/>
|
||
</office:styles></office:document-styles>"""
|
||
content_xml = """<?xml version="1.0"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
|
||
<office:automatic-styles>
|
||
<style:style style:name="P1" style:family="paragraph"
|
||
style:parent-style-name="Preformatted_20_Text"/>
|
||
</office:automatic-styles>
|
||
<office:body><office:text>
|
||
<text:p>Body before.</text:p>
|
||
<text:p text:style-name="Preformatted_20_Text">fn main() {</text:p>
|
||
<text:p text:style-name="P1"><text:s text:c="4"/>println!("ok");</text:p>
|
||
<text:p text:style-name="Preformatted_20_Text">}</text:p>
|
||
<text:p>Body between.</text:p>
|
||
<text:p text:style-name="Quotations">A quotation.</text:p>
|
||
<text:p text:style-name="Quotations">Its second paragraph.</text:p>
|
||
<text:p>Body after.</text:p>
|
||
</office:text></office:body></office:document-content>"""
|
||
write_zip(OUT / "odt" / "handmade-blockstyle.odt",
|
||
[("content.xml", content_xml), ("styles.xml", styles_xml)],
|
||
mimetype_first="application/vnd.oasis.opendocument.text")
|
||
|
||
|
||
def blockstyle_rtf():
|
||
parts = [
|
||
rb"{\rtf1\ansi\ansicpg1252\deff0",
|
||
rb"{\fonttbl{\f0\fcharset0 Arial;}}",
|
||
# The style name is the trailing text of each stylesheet group; \s4
|
||
# reaches its container only through the \sbasedon chain.
|
||
rb"{\stylesheet{\s0 Normal;}{\s1 Preformatted Text;}"
|
||
rb"{\s2 Quotations;}{\s3 Quote;}{\s4\sbasedon1 Listing;}}",
|
||
rb"\pard\plain\s0 Body before.\par",
|
||
rb"\pard\plain\s1 fn main() \{\par",
|
||
rb"\pard\plain\s4 println!(\'22ok\'22);\par",
|
||
rb"\pard\plain\s1 \}\par",
|
||
rb"\pard\plain\s0 Body between.\par",
|
||
rb"\pard\plain\s2 A quotation.\par",
|
||
rb"\pard\plain\s3 Its second paragraph.\par",
|
||
rb"\pard\plain\s0 Body after.\par",
|
||
rb"}",
|
||
]
|
||
(OUT / "rtf").mkdir(parents=True, exist_ok=True)
|
||
(OUT / "rtf" / "handmade-blockstyle.rtf").write_bytes(b"\n".join(parts))
|
||
|
||
|
||
def blockstyle_doc():
|
||
"""Binary .doc whose STSH names a Source Code and a Quote style, bound to
|
||
paragraphs through a PAPX FKP page."""
|
||
CODE_ISTD, QUOTE_ISTD = 15, 16
|
||
|
||
def std(name, istd):
|
||
stdf = bytearray(10)
|
||
struct.pack_into("<H", stdf, 0, 0x0FFE) # sti: not built-in
|
||
struct.pack_into("<H", stdf, 2, (0x0FFF << 4) | 1) # istdBase nil, paragraph
|
||
struct.pack_into("<H", stdf, 4, 2) # cupx: papx + chpx
|
||
chars = name.encode("utf-16-le")
|
||
xstz = struct.pack("<H", len(name)) + chars + b"\x00\x00"
|
||
# UPX payloads follow the name, each 2-byte aligned in the record.
|
||
upx = struct.pack("<H", 2) + struct.pack("<H", istd) + struct.pack("<H", 0)
|
||
record = bytes(stdf) + xstz + upx
|
||
assert len(record) % 2 == 0
|
||
return struct.pack("<H", len(record)) + record
|
||
|
||
cstd = QUOTE_ISTD + 1
|
||
stshi = bytearray(18)
|
||
struct.pack_into("<H", stshi, 0, cstd) # cstd
|
||
struct.pack_into("<H", stshi, 2, 10) # cbSTDBaseInFile
|
||
stsh = struct.pack("<H", len(stshi)) + bytes(stshi)
|
||
for istd in range(cstd):
|
||
if istd == CODE_ISTD:
|
||
stsh += std("Source Code", istd)
|
||
elif istd == QUOTE_ISTD:
|
||
stsh += std("Quote", istd)
|
||
else:
|
||
stsh += struct.pack("<H", 0)
|
||
|
||
paras = ["Body before.\r", "fn main() {\r", "}\r", "A quotation.\r", "Body after.\r"]
|
||
istds = [0, CODE_ISTD, CODE_ISTD, QUOTE_ISTD, 0]
|
||
text = "".join(paras).encode("cp1252")
|
||
|
||
# PAPX FKP page: rgfc boundaries, then one BX per paragraph pointing at a
|
||
# PAPX blob (cb 0 form: byte 0 zero, byte 1 length in words).
|
||
fkp = bytearray(512)
|
||
fc = 0x400
|
||
bounds = [fc]
|
||
for p in paras:
|
||
fc += len(p)
|
||
bounds.append(fc)
|
||
for i, b in enumerate(bounds):
|
||
struct.pack_into("<I", fkp, i * 4, b)
|
||
count = len(paras)
|
||
blob_at = {}
|
||
off = (count + 1) * 4 + count * 13
|
||
off += off % 2
|
||
for istd in dict.fromkeys(istds):
|
||
fkp[off] = 0
|
||
fkp[off + 1] = 1
|
||
struct.pack_into("<H", fkp, off + 2, istd)
|
||
blob_at[istd] = off // 2
|
||
off += 4
|
||
for k, istd in enumerate(istds):
|
||
fkp[(count + 1) * 4 + k * 13] = blob_at[istd]
|
||
fkp[511] = count
|
||
|
||
PN = 3
|
||
word_doc = bytearray(word_doc_stream(0x0409, text))
|
||
word_doc += b"\x00" * (PN * 512 - len(word_doc))
|
||
word_doc += fkp
|
||
# PlcfBtePapx: one FKP page covering the whole text.
|
||
plcf = struct.pack("<II", 0x400, bounds[-1]) + struct.pack("<I", PN)
|
||
struct.pack_into("<II", word_doc, 0xA2, 0, len(stsh))
|
||
struct.pack_into("<II", word_doc, 0x102, len(stsh), len(plcf))
|
||
|
||
write_cfb(OUT / "doc" / "handmade-blockstyle.doc",
|
||
[("0Table", stsh + plcf), ("WordDocument", bytes(word_doc))])
|
||
|
||
|
||
def blockstyle_docx():
|
||
def p(text, style, runs=None):
|
||
body = runs or f'<w:r><w:t xml:space="preserve">{text}</w:t></w:r>'
|
||
return f'<w:p><w:pPr><w:pStyle w:val="{style}"/></w:pPr>{body}</w:p>'
|
||
|
||
# Syntax highlighting: Pandoc splits a code line into styled runs.
|
||
highlighted = ('<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">fn</w:t></w:r>'
|
||
'<w:r><w:t xml:space="preserve"> main() {</w:t></w:r>')
|
||
document = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:document {W}><w:body>
|
||
{para("Body before.")}
|
||
{p("", "SourceCode", highlighted)}
|
||
{p(" println!(\"ok\");", "SourceCode")}
|
||
{p("}", "SourceCode")}
|
||
{para("Body between.")}
|
||
{p("A quotation.", "BlockText")}
|
||
{p("Its second paragraph.", "BlockText")}
|
||
{para("Body between.")}
|
||
{p("Word quote.", "Quote")}
|
||
{p("Word intense quote.", "IntenseQuote")}
|
||
{para("Body between.")}
|
||
{p("HTML preformatted line.", "HTMLPreformatted")}
|
||
{p("Inherits Source Code.", "SourceCodeChild")}
|
||
{para("Body after.")}
|
||
</w:body></w:document>"""
|
||
styles = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<w:styles {W}>
|
||
<w:docDefaults><w:rPrDefault><w:rPr/></w:rPrDefault></w:docDefaults>
|
||
<w:style w:type="paragraph" w:styleId="SourceCode"><w:name w:val="Source Code"/></w:style>
|
||
<w:style w:type="paragraph" w:styleId="SourceCodeChild"><w:name w:val="Listing"/>
|
||
<w:basedOn w:val="SourceCode"/></w:style>
|
||
<w:style w:type="paragraph" w:styleId="BlockText"><w:name w:val="Block Text"/></w:style>
|
||
<w:style w:type="paragraph" w:styleId="Quote"><w:name w:val="Quote"/></w:style>
|
||
<w:style w:type="paragraph" w:styleId="IntenseQuote"><w:name w:val="Intense Quote"/></w:style>
|
||
<w:style w:type="paragraph" w:styleId="HTMLPreformatted"><w:name w:val="HTML Preformatted"/></w:style>
|
||
</w:styles>"""
|
||
ct = CONTENT_TYPES_BASE.format(extra="")
|
||
write_zip(OUT / "docx" / "handmade-blockstyle.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", document),
|
||
("word/styles.xml", styles),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R14: binary PPT per-slide master selection with tri-state level defaults
|
||
|
||
def ppt_rec(ver_inst, rec_type, body):
|
||
return struct.pack("<HHI", ver_inst, rec_type, len(body)) + body
|
||
|
||
|
||
def ppt_container(instance, rec_type, body):
|
||
return ppt_rec((instance << 4) | 0xF, rec_type, body)
|
||
|
||
|
||
def multimaster_ppt():
|
||
# TxMasterStyleAtom instance 1 (body text), one level: PF carries
|
||
# hasBullet, CF carries exactly one specified style bit (tri-state).
|
||
def tx_master(bullet_on, cf_mask, cf_style):
|
||
pf = struct.pack("<IH", 0x0001, 0x0001 if bullet_on else 0x0000)
|
||
cf = struct.pack("<IH", cf_mask, cf_style)
|
||
return ppt_rec(1 << 4, 0x0FA3, struct.pack("<H", 1) + pf + cf)
|
||
|
||
def slide(master_id, text):
|
||
slide_atom = ppt_rec(2, 0x03EF, struct.pack("<I", 0) + b"\x00" * 8
|
||
+ struct.pack("<IIHH", master_id, 0, 0, 0))
|
||
th = ppt_rec(0, 0x0F9F, struct.pack("<I", 1))
|
||
tb = ppt_rec(0, 0x0FA8, text.encode("ascii"))
|
||
return ppt_container(0, 0x03EE, slide_atom + th + tb)
|
||
|
||
def persist_atom(persist_ref, sid):
|
||
return ppt_rec(0, 0x03F3, struct.pack("<IIIII", persist_ref, 0, 0, sid, 0))
|
||
|
||
slide_list = ppt_container(0, 0x0FF0, persist_atom(4, 256) + persist_atom(5, 257))
|
||
master_list = ppt_container(1, 0x0FF0, persist_atom(2, 1001) + persist_atom(3, 1002))
|
||
doc = ppt_container(0, 0x03E8, slide_list + master_list)
|
||
master_a = ppt_container(0, 0x03F8, tx_master(True, 0x0001, 0x0001)) # bullet+bold
|
||
master_b = ppt_container(0, 0x03F8, tx_master(False, 0x0002, 0x0002)) # no bullet, italic
|
||
slide1 = slide(1001, "Alpha master body text\r")
|
||
slide2 = slide(1002, "Beta master body text\r")
|
||
|
||
stream = b""
|
||
offsets = {}
|
||
for pid, blob in [(1, doc), (2, master_a), (3, master_b), (4, slide1), (5, slide2)]:
|
||
offsets[pid] = len(stream)
|
||
stream += blob
|
||
entries = struct.pack("<I", 1 | (5 << 20)) + struct.pack(
|
||
"<5I", *(offsets[i] for i in range(1, 6)))
|
||
off_dir = len(stream)
|
||
stream += ppt_rec(0, 0x1772, entries)
|
||
off_edit = len(stream)
|
||
stream += ppt_rec(0, 0x0FF5, struct.pack(
|
||
"<IIIIIIHH", 0, 0, 0, off_dir, 1, 6, 0, 0))
|
||
current_user = ppt_rec(0, 0x0FF6, struct.pack("<III", 20, 0xE391C05F, off_edit))
|
||
write_cfb(OUT / "ppt" / "handmade-multimaster.ppt", [
|
||
("Current User", current_user),
|
||
("PowerPoint Document", stream),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# S20: notes pages pair to slides via NotesAtom.slideIdRef, not list order -
|
||
# this deck has notes only on the SECOND slide, which order-based zipping
|
||
# would misattribute to the first.
|
||
|
||
def sparsenotes_ppt():
|
||
def slide(text):
|
||
slide_atom = ppt_rec(2, 0x03EF, struct.pack("<I", 0) + b"\x00" * 8
|
||
+ struct.pack("<IIHH", 0, 0, 0, 0))
|
||
th = ppt_rec(0, 0x0F9F, struct.pack("<I", 1))
|
||
tb = ppt_rec(0, 0x0FA8, text.encode("ascii"))
|
||
return ppt_container(0, 0x03EE, slide_atom + th + tb)
|
||
|
||
def notes(slide_ref, text):
|
||
notes_atom = ppt_rec(1, 0x03F1, struct.pack("<IHH", slide_ref, 0, 0))
|
||
th = ppt_rec(0, 0x0F9F, struct.pack("<I", 2))
|
||
tb = ppt_rec(0, 0x0FA8, text.encode("ascii"))
|
||
return ppt_container(0, 0x03F0, notes_atom + th + tb)
|
||
|
||
def persist_atom(persist_ref, sid):
|
||
return ppt_rec(0, 0x03F3, struct.pack("<IIIII", persist_ref, 0, 0, sid, 0))
|
||
|
||
slide_list = ppt_container(0, 0x0FF0, persist_atom(2, 256) + persist_atom(3, 257))
|
||
notes_list = ppt_container(2, 0x0FF0, persist_atom(4, 1301))
|
||
doc = ppt_container(0, 0x03E8, slide_list + notes_list)
|
||
s1 = slide("First slide text\r")
|
||
s2 = slide("Second slide text\r")
|
||
n2 = notes(257, "Notes for the second slide\r")
|
||
|
||
stream = b""
|
||
offsets = {}
|
||
for pid, blob in [(1, doc), (2, s1), (3, s2), (4, n2)]:
|
||
offsets[pid] = len(stream)
|
||
stream += blob
|
||
entries = struct.pack("<I", 1 | (4 << 20)) + struct.pack(
|
||
"<4I", *(offsets[i] for i in range(1, 5)))
|
||
off_dir = len(stream)
|
||
stream += ppt_rec(0, 0x1772, entries)
|
||
off_edit = len(stream)
|
||
stream += ppt_rec(0, 0x0FF5, struct.pack(
|
||
"<IIIIIIHH", 0, 0, 0, off_dir, 1, 5, 0, 0))
|
||
current_user = ppt_rec(0, 0x0FF6, struct.pack("<III", 20, 0xE391C05F, off_edit))
|
||
write_cfb(OUT / "ppt" / "handmade-sparsenotes.ppt", [
|
||
("Current User", current_user),
|
||
("PowerPoint Document", stream),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R13: \sbasedon style chains; boundary-matched merges in nonuniform tables
|
||
|
||
def merge_rtf():
|
||
parts = [
|
||
rb"{\rtf1\ansi\ansicpg1252\deff0",
|
||
rb"{\fonttbl{\f0\fcharset0 Arial;}}",
|
||
rb"{\stylesheet{\s1\b\outlinelevel1 Base Bold;}"
|
||
rb"{\s2\sbasedon1\i Child Italic;}}",
|
||
# The paragraph carries only \s2: bold and the outline level must
|
||
# arrive through the \sbasedon chain.
|
||
rb"\pard\s2 Inherited heading\par",
|
||
rb"\pard\plain Plain paragraph between tables.\par",
|
||
# Nonuniform boundaries: row 2's wide cell spans what rows 1 and 3
|
||
# divide differently; the \clvmrg chain matches by \cellx boundary.
|
||
rb"\trowd\clvmgf\cellx3000\cellx6000\cellx9000 A1\cell B1\cell C1\cell\row",
|
||
rb"\trowd\clvmrg\cellx3000\cellx9000 \cell Wide B2\cell\row",
|
||
rb"\trowd\cellx4500\cellx9000 A3 wide\cell B3 wide\cell\row",
|
||
# A stray \clvmrg with no matching chain above stays visible.
|
||
rb"\trowd\clvmrg\cellx4500\cellx9000 stray continuation\cell X\cell\row",
|
||
rb"}",
|
||
]
|
||
(OUT / "rtf").mkdir(parents=True, exist_ok=True)
|
||
(OUT / "rtf" / "handmade-merge.rtf").write_bytes(b"\n".join(parts))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# R10/R11: per-chapter CSS cascades and spine-only anchor classification
|
||
|
||
def css_links_epub():
|
||
css_a = ".mark { font-weight: bold; }\n"
|
||
css_b = ".mark { font-style: italic; font-weight: normal; }\n"
|
||
ch1 = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>One</title>
|
||
<link rel="stylesheet" type="text/css" href="style-a.css"/></head><body>
|
||
<h1>Sheet A Chapter</h1>
|
||
<p><span class="mark">Bold via sheet A.</span></p>
|
||
<p>See <a href="pic.png">the picture</a>,
|
||
<a href="data.bin">the download</a>,
|
||
<a href="appendix.xhtml">the non-linear appendix</a>,
|
||
or <a href="ch2.xhtml">chapter two</a>.</p>
|
||
</body></html>"""
|
||
ch2 = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Two</title>
|
||
<link rel="stylesheet" type="text/css" href="style-b.css"/>
|
||
<style>p.local { text-decoration: line-through; }</style></head><body>
|
||
<h1>Sheet B Chapter</h1>
|
||
<p><span class="mark">Italic via sheet B only.</span></p>
|
||
<p class="local">Struck via the chapter style block.</p>
|
||
</body></html>"""
|
||
ch3 = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Three</title>
|
||
<link rel="stylesheet" type="text/css" href="style-a.css"/>
|
||
<link rel="stylesheet" type="text/css" href="style-b.css"/></head><body>
|
||
<h1>Cascade Chapter</h1>
|
||
<p><span class="mark">Sheet B wins the conflict.</span></p>
|
||
</body></html>"""
|
||
ch4 = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Four</title>
|
||
<style>
|
||
span.secret { display: none; }
|
||
.secret { display: block; }
|
||
span { display: none; }
|
||
.visible { display: block; }
|
||
p.strong { font-weight: bold; }
|
||
.strong { font-weight: normal; }
|
||
.gone { display: none; }
|
||
</style></head><body>
|
||
<h1>Specificity Chapter</h1>
|
||
<p><span class="secret">Hidden: tag.class outweighs the restoring class.</span>after the span</p>
|
||
<p><span class="visible">Restored: the class rule outweighs the hiding tag rule.</span></p>
|
||
<p class="strong">Bold: tag.class beats the later class rule.</p>
|
||
<p class="gone">Never shown.</p>
|
||
</body></html>"""
|
||
appendix = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Appendix</title></head><body>
|
||
<p>Non-linear content, not converted.</p>
|
||
</body></html>"""
|
||
opf = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
|
||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||
<dc:identifier id="uid">urn:uuid:00000000-0000-0000-0000-00000000c55l</dc:identifier>
|
||
<dc:title>Cascade Book</dc:title><dc:language>en</dc:language>
|
||
</metadata>
|
||
<manifest>
|
||
<item id="c1" href="ch1.xhtml" media-type="application/xhtml+xml"/>
|
||
<item id="c2" href="ch2.xhtml" media-type="application/xhtml+xml"/>
|
||
<item id="c3" href="ch3.xhtml" media-type="application/xhtml+xml"/>
|
||
<item id="c4" href="ch4.xhtml" media-type="application/xhtml+xml"/>
|
||
<item id="app" href="appendix.xhtml" media-type="application/xhtml+xml"/>
|
||
<item id="cssa" href="style-a.css" media-type="text/css"/>
|
||
<item id="cssb" href="style-b.css" media-type="text/css"/>
|
||
<item id="pic" href="pic.png" media-type="image/png"/>
|
||
<item id="bin" href="data.bin" media-type="application/octet-stream"/>
|
||
</manifest>
|
||
<spine><itemref idref="c1"/><itemref idref="c2"/><itemref idref="c3"/><itemref idref="c4"/>
|
||
<itemref idref="app" linear="no"/></spine>
|
||
</package>"""
|
||
container = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||
<rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles>
|
||
</container>"""
|
||
write_zip(OUT / "epub" / "handmade-css-links.epub", [
|
||
("META-INF/container.xml", container),
|
||
("OEBPS/content.opf", opf),
|
||
("OEBPS/ch1.xhtml", ch1),
|
||
("OEBPS/ch2.xhtml", ch2),
|
||
("OEBPS/ch3.xhtml", ch3),
|
||
("OEBPS/ch4.xhtml", ch4),
|
||
("OEBPS/appendix.xhtml", appendix),
|
||
("OEBPS/style-a.css", css_a),
|
||
("OEBPS/style-b.css", css_b),
|
||
("OEBPS/pic.png", DOT_PNG),
|
||
("OEBPS/data.bin", b"NOT-A-DOCUMENT"),
|
||
], mimetype_first="application/epub+zip")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Handmade RTF: \bin payload, list table, nested table, per-font charset (H11, H12, M13)
|
||
|
||
def bin_rtf():
|
||
payload = b"AB{C}D\\E}}{F.." # 14 raw bytes full of grouping chars
|
||
assert len(payload) == 14
|
||
parts = [
|
||
rb"{\rtf1\ansi\ansicpg1252\deff0",
|
||
rb"{\fonttbl{\f0\fcharset0 Arial;}{\f1\fcharset204 Arial;}}",
|
||
rb"{\stylesheet{\s1\outlinelevel0\b Heading One;}{\s0 Normal;}}",
|
||
rb"{\*\listtable{\list\listtemplateid1{\listlevel\levelnfc2\levelstartat4"
|
||
rb"{\leveltext \'02\'00.;}{\levelnumbers \'01;}}\listid100}}",
|
||
rb"{\*\listoverridetable{\listoverride\listid100\listoverridecount0\ls1}"
|
||
# S6: per-level override records - an overridden start, and an
|
||
# overriding format via an embedded \listlevel
|
||
rb"{\listoverride\listid100\listoverridecount1"
|
||
rb"{\lfolevel\listoverridestartat\levelstartat10}\ls2}"
|
||
rb"{\listoverride\listid100\listoverridecount1"
|
||
rb"{\lfolevel\listoverrideformat{\listlevel\levelnfc0\levelstartat7}}\ls3}}",
|
||
rb"\pard\s1\b Heading via style\b0\par",
|
||
rb"\pard Before the binary blob.\par",
|
||
rb"{\*\pict\wmetafile8\bin14 " + payload + rb"}",
|
||
rb"\pard After the binary blob: braces {stay} balanced.\par",
|
||
rb"\pard{\listtext IV.\tab}\ls1\ilvl0 Roman four via list table\par",
|
||
rb"\pard{\listtext V.\tab}\ls1\ilvl0 Roman five\par",
|
||
rb"\pard{\listtext X.\tab}\ls2\ilvl0 Roman ten via start override\par",
|
||
rb"\pard{\listtext XI.\tab}\ls2\ilvl0 Roman eleven\par",
|
||
rb"\pard{\listtext 7.\tab}\ls3\ilvl0 Decimal seven via format override\par",
|
||
# simple table with a merged first row and a header row
|
||
rb"\trowd\trhdr\clmgf\cellx4000\clmrg\cellx8000 Wide header\cell\cell\row",
|
||
rb"\trowd\cellx4000\cellx8000 L\cell R\cell\row",
|
||
# nested table inside the first cell of an outer table
|
||
rb"\trowd\cellx4000\cellx8000",
|
||
rb"\pard\intbl\itap2 nested A\nestcell nested B\nestcell"
|
||
rb"{\*\nesttableprops\trowd\cellx2000\cellx4000\nestrow}",
|
||
rb"\pard\intbl outer second\cell\row",
|
||
# cp1251 text under \f1 (fcharset204): bytes for Russian "Da"
|
||
rb"\pard\f1 \'c4\'e0\f0\par",
|
||
rb"\pard Persian join: \u1605?\u1740?\u8204?\u1582?\par",
|
||
rb"}",
|
||
]
|
||
data = b"\n".join(parts)
|
||
(OUT / "rtf").mkdir(parents=True, exist_ok=True)
|
||
(OUT / "rtf" / "handmade-bin.rtf").write_bytes(data)
|
||
# unbalanced variant: drop the final closing brace -> recovers
|
||
(OUT / "malformed").mkdir(parents=True, exist_ok=True)
|
||
(OUT / "malformed" / "unbalanced--recovers.rtf").write_bytes(data[: data.rfind(b"}")])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Handmade CSVs (M8)
|
||
|
||
def csvs():
|
||
d = OUT / "csv"
|
||
d.mkdir(parents=True, exist_ok=True)
|
||
(d / "handmade-quoted.csv").write_text(
|
||
'name,desc,qty\n" padded ","comma, inside",3\nplain,"multi\nline",4\n',
|
||
encoding="utf-8", newline="")
|
||
(d / "handmade-semicolon.csv").write_text(
|
||
'a;b;c\n"1,5";"2,5";x\n"3,0";y;z\n', encoding="utf-8", newline="")
|
||
(d / "handmade-utf16.csv").write_bytes(
|
||
b"\xff\xfe" + "col1,col2\nnaïve,café\nΑθήνα,数据\n".encode("utf-16-le"))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Malformed set (recover / skip / ignore / error annotations in filenames)
|
||
|
||
def zip_without(src, dst, drop):
|
||
with zipfile.ZipFile(src) as zin:
|
||
entries = [(i.filename, zin.read(i.filename))
|
||
for i in zin.infolist() if i.filename != drop]
|
||
write_zip(dst, entries)
|
||
|
||
|
||
def zip_replace(src, dst, name, data):
|
||
with zipfile.ZipFile(src) as zin:
|
||
entries = [(i.filename, data if i.filename == name else zin.read(i.filename))
|
||
for i in zin.infolist()]
|
||
write_zip(dst, entries)
|
||
|
||
|
||
def malformed():
|
||
m = OUT / "malformed"
|
||
m.mkdir(parents=True, exist_ok=True)
|
||
docx = OUT / "docx" / "text.docx"
|
||
if docx.exists():
|
||
raw = docx.read_bytes()
|
||
(m / "truncated--errors.docx").write_bytes(raw[: len(raw) * 3 // 5])
|
||
zip_without(docx, m / "missing-styles--skips.docx", "word/styles.xml")
|
||
zip_replace(docx, m / "corrupt-styles--skips.docx", "word/styles.xml",
|
||
b"\x00\x01garbage <notxml")
|
||
# hand-built recovery-class XML shapes
|
||
unclosed_doc = (f'<?xml version="1.0"?><w:document {W}><w:body>'
|
||
f'<w:p><w:r><w:t>Unclosed paragraph text')
|
||
mismatched_doc = (f'<?xml version="1.0"?><w:document {W}><w:body>'
|
||
f'<w:p><w:r><w:t>Mismatched tags</w:t></w:r></w:q>'
|
||
f'</w:body></w:document>')
|
||
styles_min = (f'<?xml version="1.0"?><w:styles {W}/>')
|
||
ct = CONTENT_TYPES_BASE.format(extra="")
|
||
for name, doc in [("unclosed--recovers.docx", unclosed_doc),
|
||
("mismatched--recovers.docx", mismatched_doc)]:
|
||
write_zip(m / name, [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", doc),
|
||
("word/styles.xml", styles_min),
|
||
])
|
||
(m / "empty--errors.docx").write_bytes(b"")
|
||
doc = OUT / "doc" / "text.doc"
|
||
if doc.exists():
|
||
(m / "truncated--errors.doc").write_bytes(doc.read_bytes()[:4096])
|
||
ppt = OUT / "ppt" / "pres.ppt"
|
||
if ppt.exists():
|
||
# Corrupt every UserEditAtom record type so the persist directory is
|
||
# structurally unusable -> the labelled raw-recovery path must run.
|
||
raw = ppt.read_bytes().replace(b"\xf5\x0f", b"\xf5\xff")
|
||
(m / "brokenpersist--recovers.ppt").write_bytes(raw)
|
||
|
||
|
||
def abuse():
|
||
a = OUT / "abuse"
|
||
a.mkdir(parents=True, exist_ok=True)
|
||
ct = CONTENT_TYPES_BASE.format(extra="")
|
||
# The bomb must live in a part the parser actually reads: a document.xml
|
||
# that decompresses to ~192 MiB crosses the 128 MiB per-entry cap.
|
||
bomb_doc = (f'<?xml version="1.0"?><w:document {W}><w:body><w:p><w:r><w:t>'
|
||
+ "A" * (192 * 1024 * 1024)
|
||
+ "</w:t></w:r></w:p></w:body></w:document>")
|
||
write_zip(a / "zipbomb--errors.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", bomb_doc),
|
||
])
|
||
# R2: the bomb lives in an *optional* part (an image behind a
|
||
# relationship). Resource limits must propagate out of recovery paths
|
||
# instead of being skipped like an ordinary broken image.
|
||
image_doc = (f'<?xml version="1.0"?><w:document {W} {R}><w:body>'
|
||
'<w:p><w:r><w:t>before the image</w:t></w:r></w:p>'
|
||
'<w:p><w:r><w:drawing>'
|
||
'<a:blip xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"'
|
||
' r:embed="rId10"/>'
|
||
'</w:drawing></w:r></w:p>'
|
||
'</w:body></w:document>')
|
||
image_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId10" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
|
||
</Relationships>"""
|
||
write_zip(a / "imagebomb--errors.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", image_doc),
|
||
("word/_rels/document.xml.rels", image_rels),
|
||
("word/media/image1.png", b"\x00" * (192 * 1024 * 1024)),
|
||
])
|
||
depth = 40_000
|
||
deep_doc = (f'<?xml version="1.0"?><w:document {W}><w:body><w:p><w:r><w:t>'
|
||
+ "<x>" * depth + "deep" + "</x>" * depth
|
||
+ "</w:t></w:r></w:p></w:body></w:document>")
|
||
write_zip(a / "deepxml--errors.docx", [
|
||
("[Content_Types].xml", ct),
|
||
("_rels/.rels", ROOT_RELS),
|
||
("word/document.xml", deep_doc),
|
||
])
|
||
# R6: pathologically nested binary PPT containers; the bounded record
|
||
# walk must fail with ResourceLimit instead of exhausting the stack.
|
||
nested = struct.pack("<HHI", 0x0000, 0x0FA8, 4) + b"deep"
|
||
for _ in range(200):
|
||
nested = struct.pack("<HHI", 0x000F, 0x03EE, len(nested)) + nested
|
||
write_cfb(a / "deepnest--errors.ppt", [
|
||
("Current User", b""),
|
||
("PowerPoint Document", nested),
|
||
])
|
||
repeat_ods = """<?xml version="1.0"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
|
||
<office:body><office:spreadsheet><table:table table:name="S">
|
||
<table:table-row table:number-rows-repeated="9000000">
|
||
<table:table-cell office:value-type="string" table:number-columns-repeated="1000"><text:p>x</text:p></table:table-cell>
|
||
</table:table-row>
|
||
</table:table></office:spreadsheet></office:body></office:document-content>"""
|
||
write_zip(a / "hugerepeat--errors.ods",
|
||
[("content.xml", repeat_ods)],
|
||
mimetype_first="application/vnd.oasis.opendocument.spreadsheet")
|
||
# S1: a single cell whose declared span area is astronomically large; the
|
||
# complete area must be charged against the expansion budget *before* any
|
||
# per-position work happens.
|
||
span_ods = """<?xml version="1.0"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
|
||
<office:body><office:spreadsheet><table:table table:name="S">
|
||
<table:table-row>
|
||
<table:table-cell table:number-rows-spanned="2000000000" table:number-columns-spanned="2000000000"><text:p>x</text:p></table:table-cell>
|
||
</table:table-row>
|
||
</table:table></office:spreadsheet></office:body></office:document-content>"""
|
||
write_zip(a / "hugespan--errors.ods",
|
||
[("content.xml", span_ods)],
|
||
mimetype_first="application/vnd.oasis.opendocument.spreadsheet")
|
||
# S1: a repeated row whose cells are structurally present but empty used
|
||
# to bypass the budget entirely (nothing content-bearing was ever
|
||
# charged) while still looping over the repeat count.
|
||
emptyrepeat_ods = """<?xml version="1.0"?>
|
||
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
|
||
<office:body><office:spreadsheet><table:table table:name="S">
|
||
<table:table-row table:number-rows-repeated="9000000000000000"><table:table-cell><text:p/></table:table-cell></table:table-row>
|
||
<table:table-row><table:table-cell><text:p>tail</text:p></table:table-cell></table:table-row>
|
||
</table:table></office:spreadsheet></office:body></office:document-content>"""
|
||
write_zip(a / "emptyrowrepeat--errors.ods",
|
||
[("content.xml", emptyrepeat_ods)],
|
||
mimetype_first="application/vnd.oasis.opendocument.spreadsheet")
|
||
# S1: a DrawingML table cell with an astronomically large span area.
|
||
span_pres = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:presentation {PPTX_NS}>
|
||
<p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst>
|
||
</p:presentation>"""
|
||
span_pres_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
|
||
</Relationships>"""
|
||
span_slide = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<p:sld {PPTX_NS}>
|
||
<p:cSld><p:spTree>
|
||
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>
|
||
<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="2" name="T"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr>
|
||
<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">
|
||
<a:tbl><a:tr>
|
||
<a:tc gridSpan="2000000000" rowSpan="2000000000"><a:txBody><a:bodyPr/><a:p><a:r><a:t>x</a:t></a:r></a:p></a:txBody></a:tc>
|
||
</a:tr></a:tbl>
|
||
</a:graphicData></a:graphic></p:graphicFrame>
|
||
</p:spTree></p:cSld></p:sld>"""
|
||
span_ct = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||
<Default Extension="xml" ContentType="application/xml"/>
|
||
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
|
||
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
|
||
</Types>"""
|
||
span_root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
|
||
</Relationships>"""
|
||
write_zip(a / "hugespan--errors.pptx", [
|
||
("[Content_Types].xml", span_ct),
|
||
("_rels/.rels", span_root_rels),
|
||
("ppt/presentation.xml", span_pres),
|
||
("ppt/_rels/presentation.xml.rels", span_pres_rels),
|
||
("ppt/slides/slide1.xml", span_slide),
|
||
])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# A minimal PDF: Helvetica as /F1 and a 1x1 gray image as /Im1 on every page,
|
||
# one content stream per page. Object numbers are fixed by position, so the
|
||
# xref table is exact rather than reconstructed by the reader.
|
||
def handmade_pdf(pages):
|
||
n = len(pages)
|
||
objs = {
|
||
1: b"<< /Type /Catalog /Pages 2 0 R >>",
|
||
2: b"<< /Type /Pages /Kids [" + b" ".join(b"%d 0 R" % (5 + 2 * i) for i in range(n))
|
||
+ b"] /Count %d >>" % n,
|
||
3: b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
||
4: b"<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray"
|
||
b" /BitsPerComponent 8 /Length 1 >>\nstream\n\x80\nendstream",
|
||
}
|
||
for i, content in enumerate(pages):
|
||
page, stream = 5 + 2 * i, 6 + 2 * i
|
||
objs[page] = (b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]"
|
||
b" /Resources << /Font << /F1 3 0 R >> /XObject << /Im1 4 0 R >> >>"
|
||
b" /Contents %d 0 R >>" % stream)
|
||
objs[stream] = b"<< /Length %d >>\nstream\n" % len(content) + content + b"\nendstream"
|
||
out = bytearray(b"%PDF-1.4\n")
|
||
offsets = []
|
||
for num in sorted(objs):
|
||
offsets.append(len(out))
|
||
out += b"%d 0 obj\n" % num + objs[num] + b"\nendobj\n"
|
||
xref = len(out)
|
||
out += b"xref\n0 %d\n0000000000 65535 f \n" % (len(objs) + 1)
|
||
for off in offsets:
|
||
out += b"%010d 00000 n \n" % off
|
||
out += (b"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n"
|
||
% (len(objs) + 1, xref))
|
||
return bytes(out)
|
||
|
||
|
||
TEXT_PAGE = b"BT /F1 24 Tf 72 700 Td (Text on the first page) Tj ET"
|
||
IMAGE_PAGE = b"q 468 0 0 648 72 72 cm /Im1 Do Q"
|
||
|
||
|
||
def ocr_pdfs():
|
||
"""A scan (image-only pages) and a mixed document (a text page then a scanned one)."""
|
||
(OUT / "pdf" / "handmade-scanned.pdf").write_bytes(handmade_pdf([IMAGE_PAGE, IMAGE_PAGE]))
|
||
(OUT / "pdf" / "handmade-mixed.pdf").write_bytes(handmade_pdf([TEXT_PAGE, IMAGE_PAGE]))
|
||
|
||
|
||
def main():
|
||
skip_office = "--skip-office" in sys.argv
|
||
for sub in ["odt", "docx", "doc", "rtf", "ods", "xlsx", "xls", "csv",
|
||
"odp", "pptx", "ppt", "epub", "pdf", "malformed", "abuse"]:
|
||
(OUT / sub).mkdir(parents=True, exist_ok=True)
|
||
|
||
if not skip_office:
|
||
text = SRC / "text.fodt"
|
||
convert_lo(text, "odt", OUT / "odt", "text.odt")
|
||
convert_lo(text, "docx", OUT / "docx", "text.docx")
|
||
convert_lo(text, "doc:MS Word 97", OUT / "doc", "text.doc")
|
||
convert_lo(text, "rtf:Rich Text Format", OUT / "rtf", "text.rtf")
|
||
convert_lo(text, "pdf:writer_pdf_Export", OUT / "pdf", "text.pdf")
|
||
sheet = SRC / "sheet.fods"
|
||
convert_lo(sheet, "ods", OUT / "ods", "sheet.ods")
|
||
convert_lo(sheet, "xlsx:Calc MS Excel 2007 XML", OUT / "xlsx", "sheet.xlsx")
|
||
convert_lo(sheet, "xls:MS Excel 97", OUT / "xls", "sheet.xls")
|
||
convert_lo(sheet, "csv:Text - txt - csv (StarCalc)", OUT / "csv", "sheet.csv")
|
||
pres = SRC / "pres.fodp"
|
||
convert_lo(pres, "odp", OUT / "odp", "pres.odp")
|
||
convert_lo(pres, "pptx:Impress MS PowerPoint 2007 XML", OUT / "pptx", "pres.pptx")
|
||
convert_lo(pres, "ppt:MS PowerPoint 97", OUT / "ppt", "pres.ppt")
|
||
(SRC / "dot.png").write_bytes(DOT_PNG)
|
||
run(["pandoc", str(SRC / "book.md"), "-o", str(OUT / "epub" / "book.epub"),
|
||
"--resource-path", str(SRC)])
|
||
|
||
numbering_docx()
|
||
rich_docx()
|
||
inherit_pptx()
|
||
strict_alt_ooxml()
|
||
gaps_ods()
|
||
encoded_docs()
|
||
order_pptx()
|
||
css_links_epub()
|
||
merge_rtf()
|
||
multimaster_ppt()
|
||
sparsenotes_ppt()
|
||
tables_docx()
|
||
outline_docx()
|
||
blockstyle_docx()
|
||
blockstyle_odt()
|
||
blockstyle_rtf()
|
||
blockstyle_doc()
|
||
ole_docx()
|
||
links_pptx()
|
||
manyrefs_docx()
|
||
defaults_odf()
|
||
merged_xlsx()
|
||
sheet_xlsb()
|
||
features_epub()
|
||
bin_rtf()
|
||
math_docx()
|
||
math_pptx()
|
||
ocr_pdfs()
|
||
math_odt()
|
||
math_epub()
|
||
math_rtf()
|
||
csvs()
|
||
malformed()
|
||
abuse()
|
||
print("fixture generation complete")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|