Skip to content

fix(layout): mirrored margins keep even-page body text at the inside margin #452

Closed

Description

@coygeek

Summary

When a two-page document uses mirrored margins with a 108-point inside margin and a 36-point outside margin, PDF export places body text 108 points from the left edge on both pages. The even page should start at 36 points. This shifts the text area on facing pages and changes the space reserved for binding.

Steps to reproduce

Use the CLI build stated under Runtime or environment, Python 3 and Poppler's pdftotext. Save the following script as proof.py, then run it in a disposable output directory:

#!/usr/bin/env python3
"""Reproduce mirrored-margin placement using only synthetic documents."""
import hashlib, json, pathlib, subprocess, sys, zipfile
import xml.etree.ElementTree as ET

cli = str(pathlib.Path(sys.argv[1]).resolve())
root = pathlib.Path(sys.argv[2]).resolve()
root.mkdir(parents=True, exist_ok=True)
commands = []
def run(*args):
    p = subprocess.run([str(a) for a in args], capture_output=True, text=True, timeout=60)
    commands.append({'args': [str(a) for a in args], 'exit': p.returncode,
                     'stdout': p.stdout, 'stderr': p.stderr})
    if p.returncode:
        raise RuntimeError(p.stderr)
    return p.stdout

mirror = root / 'mirror.docx'
run(cli, 'run', '--template', 'blank',
    '--cmd', 'text.insert={"text":"First page"}',
    '--cmd', 'insert.pageBreak',
    '--cmd', 'text.insert={"text":"Second page"}',
    '--cmd', 'layout.margins={"preset":"mirrored"}',
    '--cmd', 'layout.margins={"left":108,"right":36,"gutter":0}',
    '--save', mirror)
ns = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
with zipfile.ZipFile(mirror) as z:
    entries = {n: z.read(n) for n in z.namelist()}
settings = ET.fromstring(entries['word/settings.xml'])
flag = settings.find('w:mirrorMargins', ns)
assert flag is not None
settings.remove(flag)
control = root / 'setting-off.docx'
with zipfile.ZipFile(control, 'w', zipfile.ZIP_DEFLATED) as z:
    for name, data in entries.items():
        z.writestr(name, ET.tostring(settings, encoding='utf-8', xml_declaration=True)
                   if name == 'word/settings.xml' else data)
# A normal-margin document at the expected even-page offset checks the coordinate reader.
swapped = root / 'swapped-control.docx'
run(cli, 'run', '--file', control,
    '--cmd', 'layout.margins={"left":36,"right":108}', '--save', swapped)
results = {}
for name in ('mirror', 'setting-off', 'swapped-control'):
    docx = root / (name + '.docx')
    model = root / (name + '.json')
    pdf = root / (name + '.pdf')
    run(cli, 'convert', docx, model)
    d = json.loads(model.read_text())
    run(cli, 'convert', docx, pdf)
    xml = run('pdftotext', '-bbox', pdf, '-')
    (root / (name + '.bbox.html')).write_text(xml)
    pages = ET.fromstring(xml).findall('.//{http://www.w3.org/1999/xhtml}page')
    words = [[{'text': w.text, 'xMin': float(w.attrib['xMin'])}
              for w in p.findall('.//{http://www.w3.org/1999/xhtml}word')] for p in pages]
    assert len(words) == 2, words
    assert words[0][0]['text'] == 'First' and words[1][0]['text'] == 'Second', words
    results[name] = {'mirrorMargins': d['settings']['mirrorMargins'],
                     'firstWordX': [p[0]['xMin'] for p in words], 'words': words}
assert results['mirror']['mirrorMargins'] is True
assert results['setting-off']['mirrorMargins'] is False
assert results['swapped-control']['mirrorMargins'] is False
assert results['setting-off']['firstWordX'] == [108.0, 108.0]
assert results['swapped-control']['firstWordX'] == [36.0, 36.0]
results['mirroredContractRestored'] = results['mirror']['firstWordX'] == [108.0, 36.0]
(root / 'results.json').write_text(json.dumps(results, indent=2) + '\n')
(root / 'commands.json').write_text(json.dumps(commands, indent=2) + '\n')
print(json.dumps(results, indent=2))
print('CLI sha256:', hashlib.sha256(pathlib.Path(cli).read_bytes()).hexdigest())
python3 proof.py "$(command -v wordcraft-cli)" ./margin-proof

The script creates a blank document through public commands, inserts "First page", a page break and "Second page", selects layout.margins with preset="mirrored", then sets left=108, right=36 and gutter=0. It saves and reopens the DOCX, exports PDF and measures the first word on each page. It also runs two controls: an identical DOCX with only w:mirrorMargins removed, and a nonmirrored document with left=36 and right=108. These steps were executed on the stated baseline. Every subprocess has a 60-second timeout.

Expected behavior

For the mirrored document, the first page should place its first word at x=108 points and the even page at x=36 points. The inside margin should remain next to the binding on each facing page. Disabling mirrored margins should keep both pages at x=108. The fixed 36-point left-margin control should place both pages at x=36.

Actual behavior

The mirrored PDF places its first words at [108,108] points, identical to the setting-off control. The reopened mirrored DOCX has settings.mirrorMargins=true, so the failure occurs despite the saved setting being present. The fixed-left-margin control produces [36,36], confirming that the converter and coordinate reader can observe the expected even-page offset. All three exports succeed, contain two pages and retain the expected body text.

Evidence

  • Expected source: docs/layout-parity.md:47 describes mirror margins as implemented, with possible differences or missing comparison against Word; crates/engine/src/cmd/page.rs:12 exposes layout.margins and its mirrored preset, and lines 139-141 preserve that setting during numeric margin changes. docs/file-format-parity.md:50 lists mirror-margin DOCX reading and writing as supported.
  • Failure source: Independently executed proof.py, with captured output and coordinate results. The public DOCX/PDF conversions show mirrored [108,108], setting-off [108,108] and fixed-left-margin [36,36]. crates/layout/src/lib.rs:1042 and :1084 corroborate the unswapped body left edge.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Restoration check

Rerun the same proof on the changed build. The mirrored document must retain settings.mirrorMargins=true and produce first-word coordinates [108,36]. The setting-off control must remain [108,108], and the fixed-left-margin control must remain [36,36]. All three PDFs must still contain two pages and the expected First/Second text. Clearing the mirror setting or applying the outside margin to both pages does not restore this contract.

Runtime or environment

Commit 4615cef, locally built wordcraft-cli 0.4.0, macOS and pdftotext 26.09.0. CLI SHA-256: 0424c3df03f1e565a5c862f5cef3158342d3726a18d1a40181e088b4df4f0f6b. All documents were synthetic and generated through the public CLI. No source changes or private documents were involved.

Independent recheck used pinned current main e4b7d155f15a7784197ba34eeea0b54490d70eb2, a locally built wordcraft-cli 0.5.0 with SHA-256 6292c6e5b24074eed6193529e3003d0d9a987e13db803ad99cb14e4b0f201651. Both checkout identities and both CLI versions and hashes were checked, and the supplied build logs identify their source checkouts. The exact companion again records mirrored and setting-off coordinates [108,108], and fixed-left control coordinates [36,36]. The mirrored DOCX readback retains mirrorMargins=true. All current probes used synthetic inputs, disposable configuration and output directories, and child-process limits of 60 seconds. No source edits or installed GUI run were performed.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The verified two-page PDF uses the wrong even-page text position for unequal inside/outside margins, changing printed alignment and binding space. The text remains present and the DOCX retains the setting. The demonstrated scope is this ordinary facing-page layout.
  • Workaround status: unverified
  • Workaround basis: Disabling mirroring or assigning a fixed left margin works as a control but does not preserve alternating inside/outside margins. No practical workaround preserving automatic mirrored margins was executed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions