Repository navigation
Sections and floating pictures - #10
dskripchenko merged 4 commits into
Conversation
A Document takes further sections after the first (followingSections), each with its own page setup, start (SectionStart: next page, continuous, even or odd page) and header/footer parts. Every section but the last ends with its w:sectPr in the pPr of its last paragraph, or of a paragraph added when it ends in a table. A section without a header or footer of a kind shows the previous section's, as in Word. DocumentBuilder::newSection() starts one. PageSetup takes a custom width and height in twips, so a section's page can be any size; the writer writes it as given and the reader keeps a size no PaperSize matches instead of turning it into A4. DocxReader reads every section: body, page setup, start, headers and footers. Code that reads only $document->section now sees the first section of a multi-section file. The HTML serializer writes every section's body, with a page break before each that starts on a new page. The conformance reference document gains a landscape section and a custom-size one; it validates against the ECMA-376 Transitional schemas. README in all four languages and the changelog describe it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Image::$position (ImagePosition) writes a picture as <wp:anchor>: on each axis an offset in EMU from the page, margin, column or character (horizontally) or the page, margin, paragraph or line (vertically), or an alignment there; in front of or behind the text; wrapped none, square or top and bottom; stacked by zOrder. Placed on the page from a header or footer, a picture repeats on every page: letterheads, page backgrounds, stamps. An offsetYEmu without a position now floats the picture that far below its paragraph, as the property always described; the writer ignored it before. DocxReader reads the anchor's position back (and still fills offsetYEmu), so a read document keeps its floating pictures when written again. Image::fromPx() and the builder's image helpers take a position. The conformance reference document gains a page-positioned picture behind the text in the header and a square-wrapped one in the body; both validate against the ECMA-376 Transitional schemas. README in all four languages and the changelog describe it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typed class constants arrived in PHP 8.3; the library supports 8.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reference document now ends its first section in a second table, so python-docx finds two tables. The smoke check also asserts the three sections python-docx reads: portrait, landscape, and the custom width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thank you, @serpentblade — this is a great contribution, and it closes the two writer gaps that the library had listed as out of scope since v1. I reviewed both features in depth before merging: the per-section Released in v1.18.0, now on Packagist. The behaviour change for multi-section documents ( Thanks again for splitting it into self-contained commits and for opening it from your personal fork — that kept the whole round smooth. 🤖 Generated with Claude Code |
Thanks for merging #7 and releasing 1.17.0. This PR adds the two writer features we couldn't work around in our exporter: several sections per document, and floating pictures. It's two commits, one per feature, plus two small test follow-ups: the consumer smoke check belongs with the first (the reference document gains a second table and three sections), and a PHP 8.2 fix belongs with the second. Each feature passes the full suite and
composer stanon its own, so either can be taken alone. As you asked, it comes from my personal fork, so you can edit it before merging.feat: several sections per document, and custom page sizestest: consumer smoke checks the reference document's sectionsfeat: floating pictures (wp:anchor)test: no typed class constant, for PHP 8.2New constructor parameters are appended at the end, so positional callers are unaffected. The CHANGELOG has one
[Unreleased]section, and the README is updated in all four languages. The conformance reference document exercises both features, and it validates against the ECMA-376 Transitional schemas withscripts/conformance/xsd-check.sh.1. Sections
A
Documenthad exactly oneSection, so one page setup and one set of headers covered the whole document. A landscape appendix, or a cover page with a different header, needed a second document.Model.
Documentgains$followingSections(the sections after the first) andsections(), which lists them all.Sectiongains$start(SectionStart:NextPage,Continuous,EvenPage,OddPage).DocumentBuilder::newSection(?PageSetup, SectionStart)ends the current section and starts the next. The content, headers and footers added after it belong to the new section, and it keeps the page setup unless given one.Writer. Each section gets its own header and footer parts. Every section but the last ends with its
<w:sectPr>in the pPr of its last paragraph, as Word writes it:A section that ends in a table gets an added paragraph to carry its sectPr.
w:typeis written only when the start isn't a new page, which is what a section does without one.titlePgis per section. A section without a header or footer of a kind writes no reference, so it shows the previous section's, which is Word's rule. That means an empty header list means "inherit", not "blank", and theSectiondocblock says so. A single-section document writes the same bytes as before: the bodies are rendered first, as they were, and each sectPr is filled in once the header parts have their ids.Custom page sizes. Sections with different pages need sizes beyond
PaperSize.PageSetupgains$widthTwipsand$heightTwips(both or neither), withpageWidthTwips(),pageHeightTwips()andeffectiveOrientation(). A custom page is landscape when it's wider than it is tall.contentWidthTwips()uses the custom size.Reader.
DocxReadersplits the body at each paragraph-level sectPr and reads each section's body, page setup, start, header and footer references andtitlePg. Before, it read the last section's page setup and the first section's headers and applied them to the whole body.SectionReadergainsfromSectPr()andstart(), and keeps a page size that noPaperSizematches as a custom size instead of turning it into A4. A document without any sectPr reads as before.One behaviour change, called out in the CHANGELOG: code that reads only
$document->sectionnow gets the first section of a multi-section file, not the whole body.sections()returns all of it. The HTML serializer now writes every section's body, with a page break before each section that starts on a new page. It still takes headers, footers and page settings from the first section.2. Floating pictures
The writer only wrote
<wp:inline>.Image::$offsetYEmuwas documented as the offset of a floating object, and the reader and the HTML serializer used it, but the writer ignored it.Model.
Imagegains$position(ImagePosition). On each axis, the picture sits either at an offset in EMU from what it's measured from or at an alignment there. It's measured from the page, margin, column or character horizontally (HorizontalAnchor), or the page, margin, paragraph or line vertically (VerticalAnchor). The alignment is aPositionAlign, and the constructor refuses one on the wrong axis.behindText,wrap(ImageWrap:None,Square,TopAndBottom) andzOrder(relativeHeight) set how it sits with the text.Image::fromPx()and the builder'simageFromBytes(),imageFromDataUrl()andimageFromFile()take aposition.Writer. A picture with a position is written as
<wp:anchor>, with the children in the order CT_Anchor requires:simplePos,positionH,positionV,extent,effectExtent, the wrap,docPr,cNvGraphicFramePr,graphic. Every required attribute is written. Without a position, the picture stays<wp:inline>, byte for byte as before. A picture placed on the page from a header or footer repeats on every page that header covers, which gives letterheads, page backgrounds and stamps.offsetYEmu. AnImagewith a non-zerooffsetYEmuand nopositionis now written floating that far below the top of its paragraph, as the property always described.effectivePosition()makes that one rule in one place.Reader.
ImageReaderreads an anchor's position into$positionand still fillsoffsetYEmu, so a document that is read and written back keeps its floating pictures where they were. Origins the model has no case for are read as the nearest one: a*Marginorigin becomes the margin, and a tight or through wrap becomes square.Tests
composer install && vendor/bin/phpunit: OK (494 tests, 7368 assertions). Before: OK (472 tests, 7271 assertions).composer stan: no errors. The first commit alone passes with 486 tests.New tests:
tests/Writer/SectionsTest.php: where each sectPr goes, header parts per section, inheritance, a section ending in a table,titlePgper section, custom sizes, and that a single section is unchanged.tests/Build/SectionsBuilderTest.phpcoversnewSection().tests/Reader/SectionsReaderTest.php: a three-section round trip (bodies, headers, first-page header, page setups including a custom size, start types), and write → read → write.tests/Writer/FloatingImageTest.php: the anchor's attributes and child order, alignments, wraps,offsetYEmualone, a header picture on the page, the wrong-axis guard, the builder helpers, and a read-back of the position.SerializerTestgains one test for the section bodies.scripts/conformance/consumer-smoke.shnow also checks that python-docx reads three sections with the right page sizes. LibreOffice 26.8 renders the conformance reference as three pages: A4 portrait, A4 landscape with its own header, and a custom size that inherits it. The header's page-positioned picture sits behind the text, and the body picture has text wrapped around it.🤖 Generated with Claude Code