This document is a development-time reverse-engineering record. It preserves the format evidence, implementation rationale, and validation findings.
Date: 2026-07-19 (Asia/Tokyo)
| Stage | Status | Result / evidence |
|---|---|---|
| A. Paragraph formatting | COMPLETE | API, synthesis, tests, decoded-IWA audit, and output/verify_a.pages complete |
| B. Document settings | COMPLETE | Single-section geometry and six header/footer variants; output/verify_b.pages complete |
| C. Text insertion/deletion | COMPLETE / MANUAL PASS | Control-character page breaks, paragraph/run deletion, and style-boundary regression fixes passed Pages PDF rendering |
| D. Creation/properties | COMPLETE | Bundled empty template, Document(), core-properties facade, verify and audit complete |
| E. Tables | COMPLETE / MANUAL PASS | Existing table read/write passed rendering; full graph-cloning add_table() and verify_h_add_table.pages are structurally complete |
| F. Pictures | COMPLETE / MANUAL PASS | Corrected full/thumbnail replacement passed rendering; graph-cloning add_picture() and verify_h_add_picture.pages are structurally complete |
| G. Remaining API/ledger | COMPLETE / MANUAL PASS | Hyperlink/style verify passed rendering; exhaustive 59-file/662-method upstream ledger complete |
| H. Final graph construction | CORRECTED | add_picture passed; stale 2x2 UID/sidecar/header dimensions in add_table are corrected and verify_h_add_table.pages regenerated |
| I. Scripts/highlight | CORRECTED | Bisect proved type-2021/field-8 invalid; final verify matches the real type-2022/field-5 paragraph-background graph |
| J. Comments/annotation | COMPLETE | Real type-2013/3056/212 graph parsed; ranges, author color, replies, and safe text edits exposed |
| K. OSS packaging | COMPLETE | Distribution renamed to python-pages, import package to pages, public docs scrubbed, and private fixtures excluded |
add_picture, subscript, and superscript passed PDF rendering checks. Bisect A
(text insertion only) preserves
page-7 decoration while negative-control B (type-2021/field-8 background)
makes Pages discard that page's character decoration. The final
verify_i_highlight.pages was rebuilt with the Pages-authored type-2022/field-5
paragraph representation. New-comment graph creation remains explicitly
excluded for the reasons in Stage J.
This project now has a dependency-free, three-layer Python library validated
against three private modern .pages fixtures. The low layer performs lossless protobuf/IWA/
Snappy operations, the middle layer manages containers, package metadata,
stylesheets, and text run tables, and api.py provides a python-docx-shaped
Document -> paragraphs -> runs -> font API. Bold, italic, underline,
strikethrough, subscript, superscript, font name, point size, sRGB font color,
standard-palette paragraph background, and existing comments are readable.
Character formatting is writable with tri-state
inheritance where applicable. Stage A additionally
implements paragraph alignment, line spacing, before/after spacing, three
indents, and tab stops through a python-docx-shaped ParagraphFormat API.
Stage B adds a singleton sections collection with page geometry, margins,
orientation, and first/even/primary header/footer access.
Existing comment and reply text can be safely edited in place. The final graph
cloner adds fresh object/component IDs, UUID-map entries,
cross-component links, attachment runs, and package data references for new
tables and inline PNG images based on the Pages-authored feature fixture.
The previously missing renderer requirement is now established: a synthesized
style is added to TSS.StylesheetArchive.styles, to its parent's children in
parent_to_children_style_map, and to the stylesheet MessageInfo's packed
object_references. The user manually confirmed in Pages on 2026-07-19 that a
file patched with these three links renders the synthesized bold style. Pages
was not launched or automated during this implementation.
The distribution is python-pages, in homage to python-docx, while the import
package is pages. Source modules live under src/pages, the console command
is python-pages, and setuptools includes the bundled minimal template through
the pages package-data declaration. samples/, output/, reference clones,
and crash reports remain excluded from Git and distributions.
Core-property writes now use Metadata/PythonPagesCoreProperties.plist.
The reader retains an explicit compatibility fallback for the former extension
member, but no old project-name import package or CLI alias is shipped. Public
documentation contains no private fixture body text; exact text assertions
remain only in local tests because the underlying fixtures are not distributed.
An unrelated PyPI distribution already owns the pages distribution/import
name, so public instructions require an isolated environment and warn against
co-installing it with python-pages.
At that stage, rename verification covered the then-current 116 local tests, a
clean import pages, Document() loading the relocated bundled template, CLI
help and fixture inspection, TOML metadata parsing, and Git ignore checks for
private/generated packages. A development-environment wheel build was attempted
with build isolation disabled, but that host Python had no setuptools backend
installed and external dependency fetches were disabled.
The bundled default.pages was audited separately because it is package data.
Its body and three previews are blank, but an unreachable legacy image object
still carried a QR-code PNG. That payload was replaced with a deterministic
216×216 white RGB PNG, its DataInfo digest/length were updated, and both stored
filenames were changed to template-placeholder.png variants. The sanitized
template remains an empty 7-IWA/636-segment package and passes both ZIP checks.
A Pages rendering check showed that the first generated fixture A opened
normally but the target text did not render bold. That version referenced named
style 1731516, whose character properties contained only bold=1. This is
consistent with a document-specific font family that does not style-link its
regular face to its bold face.
The current compatibility helper no longer relies on trait-only font
resolution. It synthesizes an anonymous style with field 5 explicitly set to
the document's bold PostScript face, while retaining field 1 bold=1, and performs all
stylesheet and PackageMetadata registration in the same transaction.
Both samples are single ZIP files with direct Index/*.iwa members. Neither
contains a nested Index.zip; the loader and writer nevertheless support that
older package layout too.
Each IWA member is a sequence of custom iWork Snappy frames:
- byte
0x00; - a 24-bit little-endian compressed byte count;
- one raw Snappy block (not the standard Snappy framed stream).
The concatenated decoded bytes contain repeated archive segments. Each segment
starts with a protobuf varint header length, followed by a TSP.ArchiveInfo,
then the payload bytes described by its repeated TSP.MessageInfo records.
MessageInfo.type selects the schema and MessageInfo.length gives each
payload length. Object dependencies are packed uint64 values in
MessageInfo.object_references field 5.
Implemented components:
src/pages/snappy.py: all four raw Snappy tag types, custom frame decode, and a correctness-first literal-only encoder.src/pages/protobuf.py: lossless protobuf wire parser/encoder for varint, fixed32, fixed64, and length-delimited fields. Untouched fields retain their exact original encoding and order.src/pages/iwa.py: ArchiveInfo/MessageInfo segmentation, payload-length updates, and IWA reassembly.src/pages/pages.py: outer ZIP/direct Index/nested Index.zip handling.
Parsing inventory:
| Sample | IWA files | Archive segments | Body storage ID | Body code points |
|---|---|---|---|---|
private_fixture_a.pages |
7 | 636 | 1732514 | 5,901 |
private_fixture_b.pages |
7 | 606 | 1732514 | 10,148 |
The binary in a locally installed Pages application bundle was read statically only.
It was never launched, loaded, automated, or addressed through AppleScript.
Its static Info.plist identifies Pages 15.1.1, build 7044.0.273. The relevant
generated protobuf metadata and Objective-C archiving strings are in:
Contents/Frameworks/TSText.framework/Versions/A/TSText
Static string extraction confirmed the current binary contains
TSWP.StorageArchive, TSWP.CharacterStylePropertiesArchive,
TSWP.CharacterStyleArchive, TSWP.ParagraphStyleArchive, and fields including
bold, font_name, font_name_null, and the related undefined markers.
tools/inspect_schema.py reproduces this static-only inspection.
Field numbers were reconstructed by combining the binary names, protobuf-wire
inspection of both samples, and comparison with the open-source iWork schemas
used by numbers-parser and other iWork readers. The useful partial schemas are
checked in as schema/TSPArchiveInfo.proto and schema/TSWP_partial.proto.
They cover the messages used by the editor and intentionally mark unneeded
submessages as opaque bytes.
Primary external format references consulted:
- https://github.com/masaccio/numbers-parser
- https://github.com/obriensp/iWorkFileFormat
- https://github.com/psobot/keynote-parser
- https://pkg.go.dev/github.com/orcastor/iwork-converter/proto/TSWP
- https://oss.sheetjs.com/notes/iwa/
Three sources were compared before implementation:
- real type-2022 paragraph styles and the body field-5 attribute table from both supplied Pages packages;
- static strings in the Pages
TSTextframework, includingParagraphStylePropertiesArchive,LineSpacingArchive,TabArchive, and each property name below; ref/numbers-parser/src/protos/TSWPArchives.protoand its alignment mapping innumbers_parser/cell.py. Noref/keynote-parsercheckout is present in this workspace, so it could not supply an additional local schema source.
TSWP.ParagraphStyleArchive is IWA type 2022. Its field 11 is optional direct
character formatting and field 12 is ParagraphStylePropertiesArchive:
| Field | Wire form | Meaning |
|---|---|---|
| 1 | varint | alignment: left=0, right=1, center=2, justified=3, auto=4 |
| 4 | fixed32 | default tab interval (decoded, not yet exposed) |
| 7 | fixed32 | first-line indent in points |
| 11 | fixed32 | left indent in points |
| 12 | varint | line-spacing-null marker |
| 13 | message | line spacing: mode field 1, amount fixed32 field 2, baseline rule field 3 |
| 19 | fixed32 | right indent in points |
| 20 | fixed32 | space after in points |
| 21 | fixed32 | space before in points |
| 24 | varint | tabs-null marker |
| 25 | message | repeated tabs; position field 1, alignment field 2, leader field 3 |
Line-spacing modes are relative=0, minimum=1, exact=2, maximum=3, and
space-between=4. Tab alignment is left=0, center=1, right=2, decimal=3.
Observed sample tabs include a 57.5-point stop and a 451-point right-aligned
stop. Named Body and Caption styles contain complete inherited defaults;
anonymous styles contain direct subsets and refer to their named parent.
The field-5 paragraph attribute table can contain more than one boundary inside one newline-delimited paragraph (the first sample paragraph has entries at UTF-16 offsets 0 and 1). A paragraph edit therefore preserves every offset and synthesizes a corresponding derivative for each active parent/character context. It must not apply the adjacent-style compaction used for character runs. The decoded-IWA audit caught and prevented that loss before completion.
ParagraphProperties,LineSpacing, andParagraphTabretain unknown paragraph-property fields while exposing the known values.- Anonymous type-2022 styles are synthesized from a real paragraph template,
retain their source parent and direct character bytes, compute dynamic
override_count, and use a(parent, character bytes, properties)cache. - Every new paragraph style receives the same stylesheet three-point registration and PackageMetadata UUID/external-reference bookkeeping as a character style.
WD_ALIGN_PARAGRAPH,WD_LINE_SPACING,WD_TAB_ALIGNMENT,WD_TAB_LEADER,Length,Pt,Inches, andCmare exposed byapi.py.Paragraph.alignmentandParagraph.paragraph_formatsupport alignment, fixed or multiple line spacing, spacing before/after, left/right/first-line indents, and sorted tab-stop addition/clearing.Noneremoves a direct override and restores inheritance.
Deliberately deferred Stage-A properties are pagination controls, keep-with-
next/keep-together/widow control, contextual spacing, borders/background,
list formatting, default tab intervals, mutable individual TabStop objects,
and indexed tab deletion. Their schemas or APIs are not required by the
master's Stage-A surface; they remain candidates for Stage G. DOCX XML mutation
assertions are excluded because Pages has no WordprocessingML representation.
tests/test_paragraph_format.py adds thirteen behaviors adapted
from python-docx paragraph-format and tab-stop expectations, including all
tri-state setters, save/reload, cache and three-link registration, preservation
of direct character properties, and preservation of explicit paragraph-table
boundaries and opaque-field retention. The complete suite passes 38/38 under
unittest discover.
output/verify_a.pages centers the second paragraph, uses exact 24-point line
spacing, 12/8-point before/after spacing, 18-point left/right indents, a
9-point first-line indent, and a 72-point centered dot-leader tab. Reparse and
ZIP validation passed (zipfile.testzip() == None, unzip -t clean, seven IWA
files and 637 segments).
The decoded-IWA diff changed only the intended members and objects:
| Member | Decoded before | Decoded after | Objects |
|---|---|---|---|
Index/Document.iwa |
39,133 | 39,138 | storage 1732514 only |
Index/DocumentStylesheet.iwa |
340,187 | 340,302 | stylesheet 1732588 + new type-2022 style 44990410 |
Index/Metadata.iwa |
22,381 | 22,422 | PackageMetadata object 2 only |
Body text and all paragraph-table boundary offsets are unchanged. The storage delta is only the replacement style reference plus its MessageInfo object reference; the stylesheet and metadata deltas are the required registration.
The Pages executable's embedded TPArchives.proto descriptor was decoded
statically from disk; the application was not launched. It identifies the
package root as TP.DocumentArchive (IWA type 10000) and gives the following
current fields, all observed in both samples:
| Field | Wire form | Meaning | Fixture A value |
|---|---|---|---|
| 30 | fixed32 float | page width | 499 pt |
| 31 | fixed32 float | page height | 709 pt |
| 32 | fixed32 float | left margin | 11.9055 pt |
| 33 | fixed32 float | right margin | 11.9055 pt |
| 34 | fixed32 float | top margin | 14.1732 pt |
| 35 | fixed32 float | bottom margin | 11.9055 pt |
| 36 | fixed32 float | header distance | 35.4331 pt |
| 37 | fixed32 float | footer distance | 42.5197 pt |
| 38 | fixed32 float | page scale | 1.0 |
| 42 | varint | orientation: portrait=0, landscape=1 | 0 |
The BODY storage's MessageInfo references one TP.SectionArchive (type 10011,
object 1732271). Its fields 23, 24, and 25 reference first/even/odd
TP.SectionTemplateArchive objects (type 10143). Each template has three
header references in repeated field 1 and three footer references in repeated
field 2. These are the Pages left/center/right fragments. All 18 referenced
objects are existing kind=1 TSWP.StorageArchive objects with a paragraph
style boundary at UTF-16 offset zero; the supplied documents leave their text
fields empty.
numbers-parser does not contain the Pages-only TPArchives.proto; its
TN.DocumentArchive and sheet print schema provide analogous size/margin and
header/footer structures but are not used as field-number authority here. No
local keynote-parser checkout exists. The embedded descriptor and both real
sample graphs agree exactly. The reconstructed subset is checked in as
schema/TP_partial.proto.
- New middle layer
document.pyresolves the type-10000 root, the section dependency, all three templates, and their six groups of storage fragments. Document.sectionsreturns oneSection, matching Pages' package-wide page setup. Width, height, four margins, header/footer distances, and orientation are readable and writable as point-basedLengthvalues.WD_ORIENTATIONandWD_ORIENTexpose portrait/landscape enum values.header,footer,first_page_header/footer, andeven_page_header/footerexpose their three Pages fragments through.paragraphs; each paragraph supports text reading and writing.different_first_page_header_footerreads/writes SectionArchive field 18.
Pages multiple sections are deliberately represented as one API section
because page geometry lives on TP.DocumentArchive, as permitted by the
master plan. Previous-section linking, section start kinds, gutters, mirrored
margins, and header/footer creation/deletion have no implemented Pages mapping
and are deferred/excluded. Arbitrary replacement of a header/footer that
already contains nonzero attribute boundaries is deferred to Stage C's general
offset engine; the current writer handles the supplied existing zero-boundary
storages and rejects unsafe cases explicitly.
Ten Stage-B tests adapted from python-docx section behaviors cover the singleton collection, all geometry values and setters, orientation, first/even/primary variants, header/footer text, section flags, real object resolution, ZIP save, and reload. The full dependency-free suite passes 48/48.
output/verify_b.pages changes the document to 709×499-point landscape,
36-point left/right margins, 42-point top/bottom margins, 18-point
header/footer distances, and adds visible primary header/footer strings.
Self-reparse reports seven IWA files and 636 segments; zipfile.testzip() and
unzip -t both pass.
The decoded-IWA diff changes only Index/Document.iwa, from 39,133 to 39,189
bytes, with the same 102 segments. Object-level changes are exactly:
- object 1/type 10000: eight geometry/orientation fields;
- object 1739944/type 2001: primary header text field 3;
- object 1740019/type 2001: primary footer text field 3.
No body text, attribute table, stylesheet, PackageMetadata, or unrelated object changed.
Static TSWP.StorageArchive schema extraction and the numbers-parser proto
agree on four offset-bearing table shapes. Object tables are fields 5, 7–9,
11–12, 15–18, 21–23, and 27–28; ParaData tables are 6, 14, and 24; string
tables are 19–20; overlapping TSP.Range tables are 25–26. All use UTF-16
code-unit positions. Both supplied BODY objects currently use fields 5–9,
12, 14, 17–19, 24, and 28.
The offset engine rewrites every indexed table after a text replacement, preserves a boundary exactly at the replacement start, removes interior boundaries, shifts the end and all later boundaries, and adjusts both ends of overlapping ranges. Astral Unicode is covered by tests. Text remains in the existing first field-3 chunk; the supplied samples each have one chunk.
Completed and audited sub-stages:
- C1 middle-layer arbitrary-length replacement:
output/verify_c1_replace.pages; - C2 public
Run.textsetter:output/verify_c2_run_text.pages; - C3
Paragraph.add_run():output/verify_c3_add_run.pages; - C4
Document.add_paragraph(text, style)with named style resolution:output/verify_c4_add_paragraph.pages; - C5
Document.add_heading(text, level)usingTitle,Heading,Heading 2, andHeading 3:output/verify_c5_add_heading.pages; - C6
Document.add_page_break()mapped to the Pages body control character0x05:output/verify_c6_page_break.pages; - C7
Run.clear()/Run.delete()andParagraph.clear()/Paragraph.delete():output/verify_c7_delete.pages.
C1–C7 decoded diffs change only Index/Document.iwa, object 1732514/type
2001, and retain 636 total segments. C6 no longer synthesizes a paragraph
style or changes the stylesheet/PackageMetadata.
page_break_before field 14 exists in the static Pages 15.1.1 protobuf
descriptor and numbers-parser schema, but user PDF rendering proved that it is
not the representation used by Pages' Insert Page Break operation. The feature
fixture contains 0x05 at UTF-16 offset 74 with no special character or
paragraph-style boundary at that offset; this is the effective page-break
encoding. The
supplied theme defines Title and Heading levels 1–3. Since Pages has no fixed
nine-level docx hierarchy, API levels 4–9 deliberately fall back to Heading 3;
levels outside 0–9 are rejected like python-docx.
The first C6 implementation and C7 deletion could create two field-5 entries at the same UTF-16 offset. Pages treated the resulting paragraph-style table as invalid, flattening existing headings outside the edited range. The indexed table rewriter now keeps the later restore boundary when deletion produces a collision, and C6 does not touch field 5. Regression tests assert unique offsets and preservation of existing heading style IDs.
All seven verify packages reparse as seven IWA files, pass ZipFile.testzip()
and unzip -t, and retain 636 segments. The corrected full suite passes
74/74. Run.text continues to reject embedded paragraph separators;
paragraph insertion and page breaks have explicit APIs, avoiding ambiguous
attribute-table behavior.
The smaller package is private_fixture_a.pages (106,152 uncompressed package
bytes versus 427,302 for private_fixture_b.pages), so it is the template source.
Its body was reduced to one empty paragraph using the Stage-C UTF-16 offset
engine, saved, reloaded, and saved again as the no-op-normalized
src/pages/data/default.pages. The 51,611-byte template remains a complete
seven-IWA Pages package with 636 segments. Relative to its source, only body
StorageArchive object 1732514/type 2001 changes; all interior attribute-table
boundaries are removed with the text.
Pages' native metadata was checked in all three requested sources: both sample
packages, the static Pages/TSKit protobuf strings, and numbers-parser's
TSPArchiveMessages.proto. Type 11011 is not a Dublin Core record:
| Location / field | Meaning |
|---|---|
TSP.DocumentMetadata 1 |
collaboration-mode bool |
TSP.DocumentMetadata 3 |
data digest/version records |
Metadata/Properties.plist |
package UUIDs, revision, format version, multipage/external-data flags |
Metadata/DocumentIdentifier |
native package document UUID |
The exact type-11011 subset is recorded in
schema/TSPDocumentMetadata.proto. Neither native location has fields for
title, subject, author, keywords, category, comments, or the python-docx date
properties. Annotation authors describe comment/activity identities and are
not document authors, so mapping core_properties.author there would be
semantically wrong.
Document()opens the bundled empty template; package-data configuration includes it in installed distributions.Document.core_propertiesexposes all python-docx string fields,created,modified,last_printed, and positive integerrevision.- Strings retain python-docx's 255-character limit. Datetimes normalize to UTC and revisions use the same positive-integer setter rule.
- Because Pages has no native equivalent, values are stored in the explicit
Metadata/PythonPagesCoreProperties.plistextension. Pages-native UUID/revision metadata is not overloaded. This member is library metadata: Pages itself neither displays nor edits these Dublin Core values. tools/diff_iwa.pynow reports added and removed ZIP members as well as changed existing members; previously an added metadata member was omitted from its audit set.
output/verify_d.pages was built from Document(), adds a visible heading and
body paragraph, and sets representative core properties. Against the bundled
template its decoded diff changes only body object 1732514/type 2001 and adds
the named extension plist. It retains seven IWA files and 636 segments. The
package self-reparses, ZipFile.testzip() returns no bad member, and unzip -t
passes. A no-op Document().save() has an empty member diff. The full suite
passes 72/72.
Excluded as having no Pages counterpart: native Pages display/edit behavior for Dublin Core properties. The compatibility facade and round-trip format are implemented, but claiming that these values are Pages-native would be incorrect. The template intentionally follows the requested package-clone model and retains its native document UUID; unique native identity generation would require coordinated revision-identifier rewrites across Properties, PackageMetadata, and view/history archives and is deferred rather than making an inconsistent partial rewrite.
The initial inventory compared both supplied packages with the static TSText
schema strings and numbers-parser's TSTArchives.proto / table model code.
Both Pages packages contain exactly the same unused table-theme defaults:
| Type | Count per sample | Meaning |
|---|---|---|
| 6003 | 6 | table styles |
| 6004 | 102 | cell styles |
| 6008 | 6 | table style presets |
| 6247 | 12 | table-related style network defaults |
| 6366 | 1 | table-related default container |
Neither sample contains any type 6000 TableInfoArchive, 6001
TableModelArchive, 6002 Tile, 6005 TableDataList, 6006
HeaderStorageBucket, 6007 WPTableInfoArchive, or 6011
TableDataListSegment. Consequently there is no real row/column count, tile
storage buffer, string table, rich-text payload, WP text-flow owner, or table
object reference to dump or mutate.
The static schema does establish the expected read chain: type 6007 wraps type 6000 and a TSWP storage reference; type 6000 field 2 points to type 6001; TableModel fields 6/7 are row/column counts and field 4 is DataStore; DataStore fields 3/4 point to tile storage and the string table; type-6002 row info uses fields 6/7 for the packed cell buffer and offsets. numbers-parser can decode that binary cell format, but it operates on real model/tile objects and cannot manufacture the missing Pages WP ownership graph from style defaults.
The former fixture blocker is resolved by samples/fixture_features.pages,
which contains a 2x2 table with one populated, bold B2 cell. The earlier
empty-sample inventory remains useful as proof that table theme defaults are
not themselves document tables.
Pages does not wrap this inline table in type 6007. Body StorageArchive field 9 has an object run at UTF-16 offset 73 to attachment 1734178/type 2003, whose field 1 points directly to TableInfo 1733304/type 6000. Its resolved graph is:
1734178 -> 1733304 (TableInfo) -> 1733299 (TableModel) -> embedded DataStore.
| Archive | Field | Meaning in fixture |
|---|---|---|
| TableInfo/type 6000 | 2 | TableModel reference |
| TableModel/type 6001 | 4 | embedded DataStore |
| TableModel/type 6001 | 6 / 7 / 8 | rows=2 / columns=2 / Table 1 |
| TableModel/type 6001 | 9 / 10 / 11 | header-row/header-column/footer-row counts |
| TableModel/type 6001 | 24–27 | default body/header/footer text styles |
| DataStore | 3 | embedded TileStorage: repeated field 1 tile links, field 2 tile size |
| DataStore | 4 / 5 | string/style TableDataList references |
| Tile/type 6002 | 5 | repeated TileRowInfo |
| TileRowInfo | 1 / 2 | tile-local row index / stored cell count |
| TileRowInfo | 5–8 | storage version, cell bytes, signed int16 offsets, wide-offset flag |
| TableDataList/type 6005 | 1 / 2 / 3 | list type, next key, repeated keyed entries |
Only B2 is materialized. Tile 1733269 has tile row index 1, offsets
[-1, 0, -1 ...], and one 28-byte version-5 text-cell buffer. Its flags are
0x21048: string key 1, text-style key 1, suggestion key 5, and text-format
key 1. String key 1 resolves through DataStore field 4 to the populated B2
cell. Text-style key 1 resolves through field 5 to anonymous paragraph
style 1734152/type 2022. That style has direct CharacterProperties bold=1
and inherits the remaining HelveticaNeue 10 pt properties from parent
1731527. This agrees with numbers-parser's version-5 cell decoder and effective
style-parent lookup.
New tables.py is the middle-layer TST graph/tile/data-list reader. The public
API now exposes Document.tables, Table.rows, Table.columns,
Table.cell(row, col), TableCell.text, TableCell.paragraphs/runs, and the
effective read-only cell run font properties. Omitted cells read as empty.
The populated string cell can be changed without rewriting its tile: the
setter updates only its uniquely referenced string TableDataList entry. A
save/reload test proves that the new text and the B2 effective bold property
both survive.
output/verify_e.pages changes B2 to a neutral replacement string while
retaining its bold style. Its decoded diff changes exactly
Index/Tables/DataList-1733266-2.iwa, object 1733266/type 6005, from 43 to 65
decoded bytes. It retains 37 IWA files and 639 segments; self-reparse,
zipfile.testzip(), and unzip -t all pass.
Creating storage for an omitted cell in an existing sparse table remains
separate from table construction. Final Stage H implements add_table() by
cloning the complete fixture graph instead of manufacturing a partial graph;
see the Stage-H audit below.
The fixture body attachment table has a second run at UTF-16 offset 75:
attachment 1734500/type 2003 field 1 points to ImageArchive 1734421/type 3005.
Its MessageInfo references two geometry helpers (1734422/1734423, type 3097)
and image style 1731560/type 3016. The relevant data fields agree exactly with
TSDArchives.proto:
| ImageArchive field | Meaning | Fixture value |
|---|---|---|
| 3 | image style reference | 1731560 |
| 4 | original logical size | 481.894 × 481.894 |
| 9 | natural pixel size | 1024 × 1024 |
| 11 | full-size TSP.DataReference | DataInfo 24 |
| 12 | thumbnail TSP.DataReference | DataInfo 25 |
PackageMetadata repeated field 4 holds the two DataInfo records. DataInfo uses field 1 identifier, field 2 SHA-1 digest, fields 3/4 preferred/stored filename, field 10 image attributes, and field 18 materialized byte length. DataInfo 24 maps to the 1,303,354-byte 1024px PNG; DataInfo 25 maps to the 98,264-byte 256px editing thumbnail.
New pictures.py resolves only body-attached type-3005 images and exposes the
full/thumbnail package data. Document.inline_shapes (with pictures alias)
provides filename, bytes, natural width/height, and
replace(path, thumbnail_path=None). A PNG replacement updates the full
DataInfo digest/length, its pixel-size attribute, and ImageArchive natural size.
For sources no larger than 256px the same PNG is also a valid thumbnail. Larger
sources either take an explicitly supplied thumbnail matching Pages' 256px
maximum scaling rule, or preserve the original Pages thumbnail unchanged.
The first Stage-F verify passed ZIP/IWA validation but hung or crashed Pages
twice during the user's manual test. Binary comparison identified a concrete
contract violation missed by structural checks: both DataInfo 24 and thumbnail
DataInfo 25 contained the same 1024×1024 PNG, whereas the Pages-authored graph
uses 1024×1024 and 256×256 respectively. They consequently also had the same
SHA-1 digest and materialized length under two distinct data identifiers.
The supplied crash summary places the SIGABRT assertion on
TSPDataManager.Datas, while QuickLook thumbnail generation was active; this
strongly ties the failure to that duplicated data identity/invalid thumbnail
pair rather than the IWA container. The corrected writer validates the
expected thumbnail dimensions before mutating any object, cannot put a large
source into the small slot, and writes distinct full/thumbnail digests.
Regression tests also prove that omitting a thumbnail for a large source leaves
DataInfo 25 and its package member intact.
output/verify_f.pages replaces the fixture image with a visible cyan/magenta
checkerboard with a corresponding 256px checkerboard thumbnail, both retaining
the Pages-authored RGB/non-interlaced PNG shape and 72-dpi pHYs chunk. Its
decoded/member diff is limited to the two expected Data/*.png members and PackageMetadata object 2/type
11006 in Index/Metadata.iwa; ImageArchive is byte-identical because the
natural dimensions did not change. It retains 37 IWA files and 639 segments,
and self-reparse, zipfile.testzip(), and unzip -t pass. A separate mutation
test uses a 512×256 PNG plus 256×128 thumbnail and proves that only ImageArchive
1734421/type 3005 is additionally changed for the updated natural size.
Final Stage H implements add_picture() with independent DataInfo identifiers,
ImageArchive/geometry identities, UUID registration, attachment insertion, and
UTF-16 table shifting. Existing-image replacement and new-image insertion now
share the same validated full/thumbnail metadata rules.
The fixture hyperlink is encoded in body StorageArchive field 11, an
ObjectAttributeTable used for smart fields. Its runs are (0, nil),
(53, 1734811), (71, nil), precisely covering the fixture hyperlink in UTF-16
coordinates. Object 1734811/type 2032 is TSWP.HyperlinkFieldArchive: field 1
is its SmartField UUID and field 2 is the UTF-8 URL.
hyperlinks.py resolves these ranges, while Document.hyperlinks and
Paragraph.hyperlinks expose label text, url/address, and contained runs.
Both label and address are writable. Label replacement uses the central text
offset engine, so the end boundary in field 11 and every other downstream body
table shift together. URL replacement changes only the type-2032 object.
The stylesheet facade now indexes all registered named paragraph and character
styles. Document.styles supports iteration and name lookup; Style exposes
name, stable Pages identifier as style_id, and WD_STYLE_TYPE.
Paragraph.style and Run.style read their effective named ancestor and apply
an existing named style with type validation. Paragraph application preserves
explicit field-5 boundaries and retains the duplicate-offset regression guard.
output/verify_g.pages applies Heading 2 to a fixture paragraph and changes
the hyperlink to a neutral visible label with target https://example.com.
Its decoded diff changes only Index/Document.iwa, body
Storage 1732539/type 2001 and HyperlinkField 1734811/type 2032. It retains 37
IWA files and 639 segments, and self-reparse, zipfile.testzip(), and
unzip -t pass.
components.py performs schema-light cloning without aliasing the source
objects. It follows MessageInfo object references, assigns a new ID to each
owned object, recursively remaps embedded TSP.Reference messages, and remaps
the packed MessageInfo object/data reference lists. Standalone
Tables/* components receive a new component ID, locator, and IWA member;
objects that live in Document or the existing CalculationEngine are
appended there. PackageMetadata external-reference and object-UUID-map
membership is mirrored exactly from the Pages-authored source graph. Shared
stylesheet objects remain shared.
Cloned table identities also receive deterministic fresh TSP.UUID/CFUUID values and a new uppercase table UUID string. The body insertion is a new paragraph separator plus U+FFFC, with a field-9 attachment boundary at that character. The central text replacement engine shifts every pre-existing offset table before the new boundary is added.
The source attachment 1734178 reaches a 50-object owned closure: 19 objects in
CalculationEngine, 30 standalone Tile/DataList/HeaderStorageBucket
components, and the attachment itself. All 50 are cloned. The cloned Tile
records are initialized as blank version-5 wide-offset rows, row/column header
buckets are resized, TableModel fields 6/7/8 become the requested dimensions
and a unique table name, and the complete secondary calculation/UID graph is
retained. The current one-tile implementation accepts 1–256 rows. Named table
style selection is excluded because the public style facade has no table-style
objects; the Pages-authored template style is retained.
output/verify_h_add_table.pages appends a labelled blank 3x4 table. Its audit
is intentionally expansive but closed: 30 new Index/Tables/*.iwa members,
19 added CalculationEngine segments, one added type-2003 attachment, the body
Storage mutation, and PackageMetadata registration. No pre-existing table,
style, or media object changes. The result has 67 IWA files and 689 segments;
self-reparse, zipfile.testzip(), and unzip -t pass.
The first manual render exposed a second dimension source: although
TableModel fields 6/7 and the tile row buffers said 3x4, Pages rendered the
cloned source's 2x2 grid. The stale values were the type-6267
ColumnRowUIDMapArchive, type-6305 StrokeSidecarArchive column/row counts,
and header field-4 cell counts. The corrected writer rebuilds a sorted set of
four column UIDs and three row UIDs with both inverse index arrays, sets the
sidecar to columns=4/rows=3, and writes three row headers with four cells plus
four column headers with three cells. Tile field 4 is 3, its three RowInfo
records each have four wide offsets, and all seven dimension representations
now agree. A dedicated regression test asserts each one. The corrected verify
package retains the same closed 67-file/689-segment graph shape.
The image closure clones the type-2003 attachment, type-3005 ImageArchive, and two type-3097 geometry helpers while sharing the existing type-3016 image style. It allocates DataInfo 26/27, unique stored filenames, two ComponentData references to the new ImageArchive, and independent SHA-1/materialized-length records. A dependency-free PNG decoder supports non-interlaced 8-bit RGB/RGBA sources and generates a nearest-neighbor thumbnail with a 256-pixel maximum; recognized color/DPI chunks are retained. Logical dimensions preserve aspect ratio and honor optional width/height.
output/verify_h_add_picture.pages appends a labelled 512x320 cyan/magenta
checkerboard and an automatically generated 256x160 thumbnail. Its decoded
diff is limited to added Data members, four added Document segments, the body
Storage mutation, and PackageMetadata. It retains 37 IWA files and has 643
segments; full/thumbnail digests differ, and self-reparse, zipfile.testzip(),
and unzip -t pass.
The updated fixture_features.pages contains one root comment and one reply.
Its real graph agrees with the static Pages strings and numbers-parser schemas:
| Archive/type | Fields established from real data |
|---|---|
TSWP.StorageArchive/2001 |
field 25 overlapping range table; location=0, length=26, highlighting object 1734975 |
TSWP.HighlightArchive/2013 |
field 1 CommentStorage reference; field 2 text-attribute UUID string |
TSD.CommentStorageArchive/3056 |
field 1 text; field 2 creation date; field 3 author; field 4 replies; field 5 storage UUID |
TSK.AnnotationAuthorArchive/212 |
field 1 name; field 2 color; fields 3–5 public identities |
TSK.AnnotationAuthorStorageArchive/213 |
repeated field 1 author references |
The range selects exactly the first 26 UTF-16 units of neutral fixture text. Highlight 1734975 references root storage 1734976; its repeated field 4 references reply 1734977. Both point to the fixture's private author record. TSP.Date field 1 is an IEEE-754 double in seconds from the 2001-01-01 Apple epoch; both root and reply timestamps decode correctly.
Object 1731480/type 10001 is not an annotation graph. Its field 1 is a
TSS.ThemeArchive; extension field 110 is
TSWP.ThemePresetsArchive, whose repeated field 6 is
character_style_presets. References 1731545/1731546 are the named theme
presets Red Bold and White. Neither is referenced by the body Storage or
defines a text range. This is established by the proto extension declaration,
the archive's Blank theme identifier/stylesheet reference, the preset names,
and a regression test. Treating path 1→110→6 as review annotation state was an
incorrect interpretation.
Document.comments implements collection length, iteration, indexing, get,
root text, author, timestamp, comment ID, reply traversal, UTF-16 anchor
positions, and anchor text. Existing root and reply text have safe setters.
Document.annotation_highlights exposes the actual review ranges with their
selected text, linked Comment, and author color; fixture author QA decodes
to FAEF5A. Review highlights are UI annotations and are not expected in PDF
output.
Comments.add_comment() remains explicitly excluded: current Pages adds
calculation/view-state/activity objects (types 4008, 10131–10133, and related
metadata) alongside the five essential annotation objects. Cloning only the
visible graph would risk collaboration-state corruption; one sample is not
enough to distinguish required state from transient UI state.
The numbers-parser proto and static strings in the Pages 15.1.1 TSText
framework agree on the following CharacterStylePropertiesArchive fields:
| Field | Wire form | Meaning |
|---|---|---|
| 10 | varint enum | script: baseline=0, superscript=1, subscript=2 |
| 14 | fixed32 float | explicit baseline shift in points |
| 25 | varint bool | background-color-null marker |
| 26 | message | background TSP.Color |
| 43 / 44 | bool / message | modern tsd_stroke_null / TSD.StrokeArchive |
| 45 / 46 | bool / message | modern tsd_fill_null / TSD.FillArchive |
| 47 | varint bool | apply the TSD fill to the text container rather than glyphs |
No supplied fixture contains a non-default script. The latest feature fixture
does contain a real format-menu character background. Pages created anonymous
type-2022 style 1735176; its field-11 character properties contain exactly one
field: field 26 with a green TSP.Color (88FA4E after 8-bit conversion).
There is no field-25 null marker and no field 7, 46, or 47. Body paragraph-style
table field 5 applies this style to the background paragraph range. It is not
referenced from character-style table field 8. The outer style fields are
exactly (1, 10, 11, 12): super points to named Body, override count is 1,
field 11 contains field 26, and field 12 is an explicitly present empty
ParagraphStylePropertiesArchive.
Although type 2021 and type 2022 reuse the same character-properties message, their placement is semantic. The user-rendered bisect proves that putting field 26 in an anonymous type-2021 style referenced from field 8 is invalid: Pages fails character-style resolution for the affected page rather than treating it as a run background.
By contrast, theme styles 1731545/1731546 with (7,46) and (1,7,46) are
foreground-color presets, not review highlights or character backgrounds.
Field 7 is font color and field 46 mirrors it as a TSD fill. Field 47 occurs
only in full paragraph-style defaults in these fixtures.
CharacterProperties includes vertical_align, baseline_shift, and
background_color. They
participate in signatures, style caching, dynamic override counts, synthesis,
and decoding. Font.subscript and
Font.superscript implement python-docx's shared tri-state vertical-alignment
behavior: setting either True replaces the other, False removes only the
matching state, and None removes the direct script override.
WD_COLOR_INDEX and its WD_COLOR alias expose the complete python-docx
highlight palette. The native API is Paragraph.background_color (arbitrary
RGB) and Paragraph.highlight_color (indexed palette). It synthesizes an
anonymous type-2022 style, writes field 26 only inside field 11, retains the
required empty field 12, and references the style through Storage field 5.
None and INHERITED remove it. Font.highlight_color remains available as a
python-docx-shaped facade, but Pages has no observed run-level equivalent: its
getter and setter operate on the run's entire containing paragraph. Foreground
font.color.rgb remains the separate field-7 plus field-46 representation.
Pages has no distinct automatic indexed-color token, so AUTO is written as
black and reparses as BLACK. Arbitrary Pages colors that are not an exact
palette member return None from the indexed-color facade.
Low-level type-2021 synthesis rejects CharacterProperties.background_color
by default. The only escape hatch is the explicitly named
experimental_allow_paragraph_background=True, retained so the known-bad
bisect B can be reproduced; it is not used by the public formatting API.
The prior claims that field 47 or the (7,46) theme fill caused the page-7
shift were disproved and are withdrawn. (7,46) is foreground/theme color.
The actual failure is the style class and reference table: field-26 background
was synthesized as type 2021 and installed in field 8, while Pages-authored
data uses type 2022 and field 5.
The requested controlled bisect now uses identical appended text and operation order. A adds only the plain line and changes body Storage 1732514. C applies a known-safe bold preset; it also changes only Storage 1732514 because style 1731516 is reused. B applies the now-known-invalid direct character background and differs from A only by the four appended field-8 run boundaries, two new type-2021 styles, stylesheet 401 registration, and PackageMetadata. Its styles contain field 26 only. The user's PDF result was decisive: A preserved all page-7 bold and underline; B removed all of it and rendered no background. This reconciles the binary invariant (all earlier objects and 16 existing field-8 runs unchanged) with the visual failure: a newly registered style graph can invalidate Pages' resolver without mutating the earlier style data.
The final verification file is no longer B. It appends two paragraphs and applies yellow/green backgrounds using two anonymous type-2022 styles through field 5. The character-style table is byte-for-byte unchanged from the source.
The verification generator also exposed an insertion-boundary regression:
formatting a newly added run and then adding another run extended the
first style across the second text. Paragraph.add_run() now explicitly
restores inherited character formatting over an unstyled inserted run, with a
regression test.
Six upstream text/test_font.py behavior groups for script and highlight are
now explicitly marked migrated in test_python_docx_ledger.py. API, wire,
save/reparse, mutual-exclusion, palette, appended-run isolation, preservation
of all pre-existing run boundaries, exact Pages-authored style bytes, and the
comment/annotation and real-background tests bring the dependency-free suite
to 116 passing tests after the package-rename compatibility test.
output/verify_i_subsuper.pages adds an H₂O/x² line. It contains exactly two
new type-2021 styles, each with override count 1 and only field 10 (values 2
and 1 respectively). output/verify_i_highlight.pages adds separate yellow-
and bright-green-background paragraphs. Its two new styles are type 2022 with
outer fields (1,10,11,12), override count 1, field-11 content (26), and an
empty field 12, exactly matching fixture style 1735176 apart from identifiers,
parent, and color. They appear at new field-5 positions 5902 and 5929. The
complete character-style table remains byte-for-byte identical to the source,
including all page-7 bold/bold-off ranges.
For each file the decoded diff changes only Index/Document.iwa (body Storage
1732514), Index/DocumentStylesheet.iwa (stylesheet 1732588 plus styles
44990410/44990411), and Index/Metadata.iwa (PackageMetadata 2). The final
highlight stylesheet grows from 340,187 to 340,377 decoded bytes and the
package has 638 segments.
Self-reparse, ZipFile.testzip(), and unzip -t all pass. Output packages are
ignored by Git as required.
The bisect outputs are:
verify_i_highlight_bisect_a.pages: identical line, no style; 636 segments, source diff limited to Storage 1732514.verify_i_highlight_bisect_b.pages: known-invalid type-2021/field-8 field-26 negative control; 638 segments. The user confirmed it destroys page-7 text decoration and renders no background.verify_i_highlight_bisect_c.pages: identical line with bold style 1731516; 636 segments and only Storage 1732514 changed.verify_i_highlight.pages: corrected type-2022/field-5 implementation; not byte-identical to B.
output/control_fixture.pages is SHA-256-identical to the updated fixture
(a7a9ab44…9000f), reparses as 37 IWA files/644 segments, and passes both ZIP
checks. If its genuine Pages-authored background is absent from PDF, PDF export
is not a valid background-color oracle; use Pages screen rendering for the
corrected final verify. This control does not affect the already conclusive A/B
decoration regression result.
src/pages/comments.py implements the real field-25/type-2013/type-3056
graph described above, including reply recursion and Apple-epoch dates. Public
Comment/Comments proxies are exported from pages, and
Document.comments exposes them. Eight applicable upstream
tests/test_comments.py behaviors moved from fully excluded to partially
migrated; Pages-specific reply/anchor behavior and the safe text setter have
additional local tests.
output/verify_j_comments.pages changes root text to Stage J: root comment edited and its reply to Stage J: reply edited. Its decoded diff contains
only Index/Document.iwa objects 1734976 and 1734977; no reference, range,
author, UUID, metadata, or collaboration object changes. The package remains
37 IWA files/643 segments, and self-reparse, zipfile.testzip(), and
unzip -t pass.
The main body is object 1732514, archive type 2001
(TSWP.StorageArchive) in Index/Document.iwa.
Relevant StorageArchive fields:
| Field | Meaning |
|---|---|
| 1 | storage kind; 0 is BODY |
| 2 | stylesheet reference |
| 3 | repeated UTF-8 text chunks |
| 5 | paragraph-style attribute table |
| 8 | character-style attribute table |
table_char_style is an ObjectAttributeTable. Each repeated field-1 entry has
character_index in field 1 and an optional TSP.Reference in field 2. The
indices are UTF-16 code-unit offsets, matching Cocoa NSRange. A missing object
reference is a run boundary that reverts to paragraph styling.
The referenced style objects have archive type 2021
(TSWP.CharacterStyleArchive). Their field 11 contains
CharacterStylePropertiesArchive, whose key fields are:
| Field | Meaning |
|---|---|
| 1 | bold boolean |
| 2 | italic boolean |
| 3 | font size, fixed32 float |
| 4 | font-name-null marker |
| 5 | PostScript font-name string |
| 6 | font-color-null marker |
| 7 | TSP.Color font color |
| 10 | baseline/superscript/subscript enum |
| 11 | underline enum |
| 12 | strikethrough enum |
| 14 | baseline shift, fixed32 float |
| 25 | background-color-null marker |
| 26 | TSP.Color background/highlight color |
| 43 / 44 | modern TSD stroke null/value |
| 45 / 46 | modern TSD fill null/value |
| 47 | fill text container rather than glyphs |
The color field was confirmed from both the extracted schema and real sample
objects 1731520/1731521/40613724/40613726. RGB colors use TSP.Color.model=1,
fixed32 floats in fields 3/4/5, alpha in field 6, and RGB color space in field
12. Pages 15 also writes fixed32 field 13 as 1.0; the writer follows that
observed shape.
Paragraph styles are archive type 2022. They use the same field 11 character properties and additionally use field 12 for paragraph properties.
Concrete sample correlations:
- Fixture A character style 42372726 has only
bold=1; it is active over UTF-16[5183,5202)and[5416,5434). - Fixture A character style 40613725 has only
bold=0; it is active over[5107,5108)and[5260,5314). - Fixture A style 40613723 and fixture B style 66597713 have
bold=1plusunderline=1at their referenced runs. - The reusable named character style 1731516 (
Emphasis) contains onlybold=1. The editor deliberately uses this style because it changes only weight and leaves font, size, and color inherited.
Font names are direct strings, not indexes into a separate font table in these
messages. Examples found in each sample include paragraph style 1731492 with
bold=1 and the document-specific bold face; fixture A style 1731479 has
bold=1 with the corresponding regular face, and anonymous paragraph styles
39858906/43092615 carry that regular face without an explicit bold field. The samples demonstrate
that the bold flag and selected PostScript face are independent properties and
can sometimes look contradictory. Run-level bold-only character styles omit
field 5 and rely on Pages' inherited-font/trait resolution.
PagesDocument.make_bold(start, length) accepts Python code-point coordinates
and converts them to UTF-16. make_substring_bold(text, occurrence=1) performs
the search and then calls the range API. The fixture-specific compatibility
default selects its explicit bold face; passing font_name=None retains the
trait-only mode.
For the selected interval the generalized editor:
- determines the style active at the end boundary;
- removes run boundaries inside the interval;
- selects an anonymous type-2021 style that has a parent reference as the structural template;
- allocates
max(PackageMetadata.last_object_identifier, all object IDs)+1; - clones the template ArchiveInfo, version, anonymous
TSS.StyleArchive, and base-style dependency; - encodes the requested
CharacterProperties;override_countis the number of non-Noneproperties rather than a constant; - appends the new segment to
Index/DocumentStylesheet.iwa; - adds the new style reference to stylesheet field 1, adds it below the template's parent in stylesheet field 5, and adds it to the stylesheet MessageInfo field-5 reference list;
- increments PackageMetadata field 1, adds an object-ID/UUID entry to the
DocumentStylesheetcomponent's field-11 object UUID map, and adds a strong(DocumentStylesheet component, new style object)reference to theDocumentcomponent's field-6 external-reference map; - inserts the new style boundary at the start and a restore boundary at the end, and adds the object ID to the storage MessageInfo reference list;
- updates payload and ArchiveInfo lengths and writes the archive in 64-KiB IWA frames.
The object UUID is deterministic UUIDv5 over document revision, object ID, and
the complete property signature. The current fixture A output uses
2f1dd3a3-f46f-5e7b-8a45-5a2592893b74.
No original sample is modified in place. Generated examples are under
output/.
Commands used:
PYTHONPATH=src python3 -m unittest discover -s tests -v
PYTHONPATH=src python3 tools/diff_iwa.py samples/private_fixture_a.pages output/private_fixture_a_bold.pages
PYTHONPATH=src python3 tools/diff_iwa.py samples/private_fixture_b.pages output/private_fixture_b_bold.pages
unzip -t output/private_fixture_a_bold.pages
unzip -t output/private_fixture_b_bold.pagesAll 96 automated tests pass. They cover the two end-to-end edits, no-op byte
preservation, all three stylesheet links, PackageMetadata registration,
property-sensitive style caching, dynamic override_count, real sample color
decoding, every requested API property, save/reload, paragraph/run derivation,
multi-MessageInfo object retention, Stage-A paragraph formatting, Stage-B
section/header-footer behavior, Stage-C text insertion and deletion, existing
and newly cloned tables/images, hyperlinks, styles, and the upstream ledger.
End-to-end edits:
| Output | Selected text | New UTF-16 run | Result |
|---|---|---|---|
output/private_fixture_a_bold.pages |
a 13-code-point body substring | [24,37) -> new style 44990410 |
PASS |
output/private_fixture_b_bold.pages |
a 9-code-point body substring | [100,109) -> new style 66610570 |
PASS |
Direct inspection of the final fixture A output confirmed:
- Python
ZipFile.testzip()returned no bad member; unzip -treturned “No errors detected”;- all seven IWA members re-parsed with this implementation;
- the segment total changed from 636 to 637, exactly accounting for the new type-2021 object;
- body text was byte/Unicode-identical after the style-only edit;
- the target run at UTF-16 24 references object 44990410 and the restore run at 37 has no object reference;
- object 44990410 is type 2021 in
Index/DocumentStylesheet.iwa, hasoverride_count=2, and its character properties are exactly field 1bold=1and field 5 set to the fixture's explicit bold face; - its ArchiveInfo retains the template base-style reference 1731514;
- stylesheet field 1 contains 44990410 exactly once;
- the field-5 parent entry for 1731514 contains child 44990410 exactly once;
- stylesheet MessageInfo field 5 contains 44990410 exactly once;
- storage object 1732514 references new style 44990410 exactly once;
- PackageMetadata
last_object_identifier, the maximum parsed object ID, and the new ID all equal 44990410; - the
DocumentStylesheetUUID map grew from 507 to 508 entries and contains the new object; - the
Documentcomponent external-reference map contains exactly one strong(1732588, 44990410)entry.
Only three outer ZIP members changed, all expected:
| Member | Decoded before | Decoded after | Delta | Purpose |
|---|---|---|---|---|
Index/Document.iwa |
39,133 | 39,152 | +19 | run entries and object reference |
Index/DocumentStylesheet.iwa |
340,187 | 340,270 | +83 | type-401 three-point registration and one new type-2021 segment |
Index/Metadata.iwa |
22,381 | 22,422 | +41 | last ID, UUID map, external reference |
The fixture B audit has the same semantic shape: only storage object 1732514,
stylesheet object 1732588 plus new style 66610570, and PackageMetadata object 2
changed. Its decoded deltas are 50,485→50,505, 339,193→339,276, and
21,709→21,751 respectively. SequenceMatcher byte opcodes are included by the
reproducible tools/diff_iwa.py report. No unrelated object changed.
The new fixture A object differs from structural template 40613723 only where
required: ArchiveInfo identifier, payload lengths, exact character properties,
and override_count. The anonymous super style, its parent reference 1731514,
version, and header base-style references are retained. Untouched IWA members
retain their exact original compressed bytes.
The requested paths were reviewed directly:
containers.py:new_message_id()increments its maximum and writesPackageMetadata.last_object_identifier;allocate_object_identifier()does the same and also guards against a stale metadata maximum by comparing all parsed object IDs.model.py:add_component_reference()writes the same strong component/object pair used here forDocument -> (DocumentStylesheet, style ID).- The object UUID map is updated here because the sample metadata and the
successful diagnostic require a UUID-addressable object in the existing
DocumentStylesheetcomponent. add_component_metadata()creates an entirely new component/IWA locator. A character style is a segment in the existingDocumentStylesheetcomponent, so adding another component record would be incorrect.iwafile.pyrefreshes MessageInfo payload lengths and writes at most 64-KiB uncompressed chunks. The local writer already refreshed lengths; it now also uses the same 64-KiB chunk boundary after a changed file can no longer reuse the original frame sizes.
No additional component metadata or weak reference is warranted for this write path.
All seven files under ref/python-docx/tests/text/ were scanned: 94 behavior
methods total. The original character/run behavior categories are adapted in
tests/test_python_docx_compat.py: run text, font access,
tri-state bold/italic, underline, font name, size, color, strike, paragraph
text, and paragraph runs. Stage A adds thirteen paragraph-format/style-synthesis
cases in tests/test_paragraph_format.py, covering the applicable behavioral
surface of test_parfmt.py and test_tabstops.py. Parameter variants are also
covered in the broader API tests.
Stage B also scanned ref/python-docx/tests/test_section.py. Ten applicable
Pages-backed behavior groups are adapted in tests/test_sections.py. Its
WordprocessingML element/relationship assertions, multiple-section linkage,
and header/footer-part lifecycle tests are excluded because Pages uses shared
TP section templates and TSWP fragment storages rather than DOCX parts.
The final scan is enforced by tests/test_python_docx_ledger.py: all 59
test_*.py files and all 662 upstream it_*/test_* behavior methods must be
accounted for exactly once. PARTIAL means the public behavior relevant to a
Pages counterpart has tests in this project while DOCX XML/relationship variants
or unsupported properties in the same file are excluded. EXCLUDED means the
whole file tests a DOCX container/internal layer or an API with no implemented
Pages counterpart.
| Disposition | Upstream files | Reason / local coverage |
|---|---|---|
| PARTIAL (20) | dml/test_color.py; styles/test_latent.py; styles/test_style.py; styles/test_styles.py; test_api.py; test_blkcntnr.py; test_comments.py; test_document.py; test_enum.py; test_section.py; test_shape.py; test_shared.py; test_table.py; text/test_font.py; text/test_hyperlink.py; text/test_pagebreak.py; text/test_paragraph.py; text/test_parfmt.py; text/test_run.py; text/test_tabstops.py |
RGB/length/enums, document/paragraph/run/font, comments, paragraph format, single section/header/footer, existing and newly inserted tables/images, hyperlinks, and named styles are adapted in API-level tests. Unsupported DOCX-only properties and XML assertions inside these files are excluded. |
| EXCLUDED — image codecs (7) | image/test_bmp.py; image/test_gif.py; image/test_helpers.py; image/test_image.py; image/test_jpeg.py; image/test_png.py; image/test_tiff.py |
These test python-docx's independent image header/DPI codec. python-pages preserves and replaces package PNG bytes; it is not a general image codec. |
| EXCLUDED — OPC (10) | opc/parts/test_coreprops.py; opc/test_coreprops.py; opc/test_oxml.py; opc/test_package.py; opc/test_packuri.py; opc/test_part.py; opc/test_phys_pkg.py; opc/test_pkgreader.py; opc/test_pkgwriter.py; opc/test_rel.py |
DOCX OPC parts, relationships, content types, PackURI, and physical-package rules have no IWA counterpart. Core-properties public behavior is separately adapted. |
| EXCLUDED — OXML (11) | oxml/parts/test_document.py; oxml/test__init__.py; oxml/test_comments.py; oxml/test_document.py; oxml/test_ns.py; oxml/test_section.py; oxml/test_styles.py; oxml/test_table.py; oxml/test_xmlchemy.py; oxml/text/test_hyperlink.py; oxml/text/test_run.py |
WordprocessingML element classes, namespaces, XPath, and XMLchemy mutation are format-specific. Equivalent public behavior is tested against IWA wire objects. |
| EXCLUDED — DOCX parts (8) | parts/test_comments.py; parts/test_document.py; parts/test_hdrftr.py; parts/test_image.py; parts/test_numbering.py; parts/test_settings.py; parts/test_story.py; parts/test_styles.py |
Relationship/part lifecycle and Word numbering/settings internals do not map to Pages components. Public document, header/footer, image, and style behavior is covered elsewhere. |
| EXCLUDED — unavailable public areas (3) | test_drawing.py; test_package.py; test_settings.py |
Settings have no implemented Pages facade. Drawing/package tests are DOCX inline-XML and package-part behavior, while public PNG insertion is covered separately. |
Method-level exclusions within PARTIAL files cover DOCX XML/OPC mutation, multiple sections, table merge/row-column mutation, non-PNG image codecs, remaining paragraph pagination/tab mutation, and font properties without an implemented Pages counterpart. The six subscript, superscript, and highlight behavior groups are now explicitly migrated rather than excluded. Highlight uses the documented paragraph-granularity mapping because the only Pages-authored counterpart is a paragraph style, not a character run style.
Final aggregate: 20/59 upstream files are partially migrated at their public
Pages-backed behavior layer and 39/59 are excluded with the category reasons
above; the ledger accounts for all 662 upstream behavior methods. The current
standalone suite discovers 81 tests under unittest discover; nine optional
fixture or ledger tests skip when their local inputs are absent. Pytest is not
required.
The requested explicit-face foundation and object model are implemented for the supplied samples. The editor performs full stylesheet and component-map bookkeeping, not merely a run-table patch.
Known limitations:
- The library did not launch Pages. Rendering checks confirmed
add_picture, subscript, and superscript. Screen-level rendering was not recorded for the correctedadd_tablegrid dimensions, type-2022/field-5 paragraph background, or comment text editing. - New comment creation is excluded because Pages writes collaboration, calculation-engine, and view-state objects beyond the visible annotation graph. Existing root/reply reading and text editing are implemented.
- API setters merge all ten known character properties on the selected run. A source style containing additional, unsupported character properties can lose those unknown direct overrides when replaced.
- The compatibility helper's default explicit font is fixture-specific. Callers editing other fonts must supply the appropriate PostScript name.
- The generic wire layer preserves unknown fields but does not implement the
semantic merge algorithm for
ArchiveInfo.should_mergepatch payloads. The edited body storage here is a normal full payload. - Encrypted documents, malformed ZIP filename encodings on older Python, and every historical IWA/package variant are outside this project's coverage.
- ZIP container bytes, compression ratios, and timestamps are not preserved; uncompressed no-op members are preserved exactly.
- Named paragraph-style values are exposed as inherited (
None) rather than flattened computed values, matching python-docx direct-format semantics. - Pages page setup is package-wide, so
Document.sectionsintentionally has one entry. Multiple DOCX sections and previous-header/footer linking are not emulated. - Pages headers and footers are exposed as their three left/center/right fragments. Their specialized setter is not yet routed through the complete body offset engine for styled or attachment-bearing fragment text.
add_table()andadd_picture()require the target document to contain a Pages-authored table or image graph respectively, used as the structural template. A new table uses one 256-row tile and retains the template table style; named table styles, more than 256 rows, merge operations, and adding content to a sparse omitted cell are excluded.- Automatic thumbnails accept non-interlaced 8-bit RGB/RGBA PNG. Other image codecs and PNG color models require a caller-provided preprocessing step.
- Pages exposes the observed field-26 background at paragraph granularity.
Run.font.highlight_colortherefore affects its containing paragraph; a run-granular mapping is excluded because the type-2021/field-8 experiment provably breaks character-style rendering. - Screen-level rendering was not recorded for the corrected paragraph- background verification file. PDF evidence is usable only if the byte-identical control fixture's genuine background also appears in PDF export.
Within these explicit format-specific exclusions, the requested python-docx public surface is implemented or fully accounted for by the final ledger.