Skip to content
Merged
63 changes: 63 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,69 @@ file. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- `DocumentBuilder::coreProperties(CoreProperties)`: title, author, dates
and the rest of `docProps/core.xml` from the fluent builder. Until now
`build()` never set them, and builder users had to construct the
`Document` by hand to get a core part.
- `Paragraph::$styleId` and `ParagraphBuilder::styleId()`: any paragraph
style id (`Normal`, `Title`, `Quote`, a custom one registered with
`StyleRegistry::paragraph()`) is written as `<w:pStyle>`. Only `Heading1..6`
(through `headingLevel`) could be referenced before. An explicit id wins
over the heading level.
- `ParagraphStyle::$pageBreakBefore` (`w:pageBreakBefore`) and
`ParagraphStyle::$widowControl` (`w:widowControl`; `null` leaves it to the
style, `false` writes `w:val="0"`), with `ParagraphBuilder::pageBreakBefore()`
and `widowControl()`. Both are written at their place in `w:pPr` and read
back. A page break before a paragraph no longer needs a `PageBreak` block and
its empty paragraph. HTML: `pageBreakBefore` is serialized as
`break-before: page`, and `break-before: page` / `page-break-before: always`
are parsed back.
- `RunStyle::$fontFamilyEastAsia` (`w:eastAsia` on `<w:rFonts>`), with
`RunStyle::withFontFamilyEastAsia()` and
`RunStyleBuilder::fontFamilyEastAsia()`: the font Word uses for CJK text.
Written in document and style run properties, and read back from an
explicit `w:eastAsia`.
- Table indent: `TableStyle::$indentTwips` and `TableBuilder::indent()` /
`indentMm()` / `indentPt()` write `<w:tblInd w:type="dxa">`, which moves
the table in from the leading margin (negative values pull it out). The
reader parses it back; HTML carries it as `margin-left` on the `<table>`
in both directions.
- Exact row height: `TableRow::$heightRule` (`RowHeightRule::AtLeast`, the
default, or `RowHeightRule::Exact`) and `TableRowBuilder::exactHeight()`.
The writer emitted every `w:trHeight` as `w:hRule="atLeast"`, so a row could
not be held to a fixed height; the rule is now written and read back.

### Fixed
- A tab or a newline in run text was written as is inside `<w:t>`, where
Word shows it as a space at best. The writer now splits the text into
`<w:t>`, `<w:tab/>` and `<w:br/>` (for "\n", "\r\n" and "\r"). The reader
reads `<w:tab/>` as "\t", so a document read and written back lost its
tabs; a `<pre>` from HTML lost its line breaks.
- Text with an invalid UTF-8 sequence was written as an empty string: the
whole run (or attribute) was dropped without a word. `XmlEscape` now
replaces the sequence with U+FFFD and keeps the rest.
- A control character in text (`\x01`, a vertical tab pasted from Word, …)
went into the XML as is, and Word refused to open the file. `XmlEscape`
now removes every character XML 1.0 does not allow.
- The same document now gives the same bytes on every write. ZipArchive put
the current time on every entry, so two writes a couple of seconds apart
differed, and a checksum, a cache key or a diff of generated files was
useless. Every entry now carries `CoreProperties::$modified` (as a UTC
wall-clock time), or 1980-01-01 00:00:00 when it is not set. The time is
written into the ZIP headers directly: `ZipArchive::setMtimeName()` alone
converts through the process's `TZ`, so the bytes would still differ
between machines in different time zones.
- The table grid computed when `Table::$gridColumnsTwips` is not set repeated
a spanning cell's full width for every column it spans: a 2000-twip cell
with `gridSpan: 2` gave two 2000-twip columns, and the grid came out wider
than the table. The spanned columns now share the cell's width, and a
column takes its width from the first row where an unspanned cell with a
width covers it (a title row spanning the table no longer decides the
columns below it).

## [1.16.0] — 2026-10-20

### Added
Expand Down
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,12 @@ HTML pipeline produces.

### DocumentBuilder

Entry point. Accumulates body, header/footer, watermark, page setup.
Entry point. Accumulates body, header/footer, watermark, page setup,
core properties (`docProps/core.xml`: title, author, dates).

```php
use Dskripchenko\PhpDocx\Build\DocumentBuilder;
use Dskripchenko\PhpDocx\Style\CoreProperties;
use Dskripchenko\PhpDocx\Style\PageSetup;
use Dskripchenko\PhpDocx\Style\PaperSize;
use Dskripchenko\PhpDocx\Style\Orientation;
Expand All @@ -331,6 +333,7 @@ $doc = DocumentBuilder::new()
orientation: Orientation::Portrait,
))
->watermark('CONFIDENTIAL')
->coreProperties(new CoreProperties(title: 'Report', creator: 'Acme Inc.'))
->heading(1, 'Report')
->paragraph('Body')
->build(); // → Document AST
Expand All @@ -339,6 +342,9 @@ $bytes = DocumentBuilder::new()->paragraph('Hi')->toBytes();
$count = DocumentBuilder::new()->paragraph('Hi')->toFile('out.docx');
```

Output is byte-for-byte reproducible: every ZIP entry carries
`CoreProperties::$modified` (UTC), or 1980-01-01 00:00:00 when it is not set.

### ParagraphBuilder

Inside `->paragraph(fn ($p) => …)`:
Expand Down Expand Up @@ -371,6 +377,8 @@ Paragraph-level styling:
```php
->paragraph(fn ($p) => $p
->alignCenter() // or alignRight()/alignJustify()
->styleId('Quote') // a style id from the StyleRegistry
->pageBreakBefore() // start on a new page; widowControl(false) turns widow control off
->indentMm(left: 20, firstLine: 10)
->spacingPt(before: 6, after: 12)
->text('Indented & spaced')
Expand Down Expand Up @@ -450,6 +458,7 @@ RunStyleBuilder::new()
->backgroundColor('eeeeee')
->highlight('yellow')
->fontFamily('Arial')
->fontFamilyEastAsia('SimSun') // w:eastAsia: the font for CJK text
->fontSizePt(14.5)
->build();
```
Expand All @@ -471,7 +480,7 @@ Length::px(100); // 1500 (CSS px @ 96 DPI)

Most builders expose unit-aware shortcuts:

- TableBuilder: `widthPt/widthMm/widthCm/widthInches`, `cellMarginsMm/cellMarginsPt`
- TableBuilder: `widthPt/widthMm/widthCm/widthInches`, `cellMarginsMm/cellMarginsPt`, `indent/indentMm/indentPt`
- TableCellBuilder: `widthPt/Mm/Cm/Inches`, `paddingMm/Pt/Cm/Inches`
- ColumnBuilder: `widthPt/Mm/Cm/Inches/Px`
- ParagraphBuilder: `indentMm/Cm/Pt/Inches`, `spacingPt/Mm`
Expand Down Expand Up @@ -635,25 +644,25 @@ All elements live under `Dskripchenko\PhpDocx\Element` namespace.
|---|---|---|
| `Document` | root | `{ section: Section, watermarkText: ?string }` |
| `Section` | container | `{ body, header, footer, pageSetup, firstHeader, firstFooter, evenHeader, evenFooter }` |
| `Paragraph` | BlockElement | `{ children: InlineElement[], style: ParagraphStyle, headingLevel: ?int }` |
| `Paragraph` | BlockElement | `{ children: InlineElement[], style: ParagraphStyle, headingLevel: ?int, styleId: ?string }` |
| `Run` | InlineElement | `{ text: string, style: RunStyle }` |
| `Hyperlink` | InlineElement | `{ href: ?string, anchor: ?string, children: InlineElement[] }` |
| `Bookmark` | InlineElement | `{ name: string, children: InlineElement[] }` |
| `Image` | both | `{ binary, format, widthEmu, heightEmu, altText }` |
| `Field` | InlineElement | `{ instruction: string, style: RunStyle }` |
| `LineBreak`, `PageBreak`, `HorizontalRule` | both | marker elements |
| `Table` | BlockElement | `{ rows: TableRow[], style, caption, gridColumnsTwips }` |
| `TableRow` | element | `{ cells: TableCell[], isHeader, heightTwips, cantSplit }` |
| `TableRow` | element | `{ cells: TableCell[], isHeader, heightTwips, cantSplit, heightRule }` |
| `TableCell` | element | `{ children: BlockElement[], style: CellStyle }` |
| `ListNode` | BlockElement | `{ items: ListItem[], ordered, format, startAt }` |
| `ListItem` | element | `{ children: InlineElement[], nestedList: ?ListNode }` |

Styles live under `Dskripchenko\PhpDocx\Style`:

- `RunStyle` — font, weight, italic, color, size, highlight, …
- `ParagraphStyle` — alignment, indents, spacing, borders
- `RunStyle` — font (and `fontFamilyEastAsia` for CJK text), weight, italic, color, size, highlight, …
- `ParagraphStyle` — alignment, indents, spacing, borders, pagination (keepWithNext, keepLines, pageBreakBefore, widowControl)
- `CellStyle` — width, padding, borders, valign, gridSpan, rowSpan
- `TableStyle` — width, borders, alignment, cell margins, layout
- `TableStyle` — width, borders, alignment, cell margins, layout, indent (`indentTwips`)
- `PageSetup`, `PaperSize`, `Orientation`, `Alignment`, `VerticalAlign`,
`BorderStyle`, `Border`, `BorderSet`

Expand Down
23 changes: 16 additions & 7 deletions docs/de/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,12 @@ HTML-Pipeline.
### DocumentBuilder

Einstiegspunkt. Sammelt Body, Kopf-/Fußzeilen, Wasserzeichen,
Seiteneinrichtung.
Seiteneinrichtung, Dokumenteigenschaften (`docProps/core.xml`: Titel,
Autor, Daten).

```php
use Dskripchenko\PhpDocx\Build\DocumentBuilder;
use Dskripchenko\PhpDocx\Style\CoreProperties;
use Dskripchenko\PhpDocx\Style\PageSetup;
use Dskripchenko\PhpDocx\Style\PaperSize;
use Dskripchenko\PhpDocx\Style\Orientation;
Expand All @@ -252,6 +254,7 @@ $doc = DocumentBuilder::new()
orientation: Orientation::Portrait,
))
->watermark('VERTRAULICH')
->coreProperties(new CoreProperties(title: 'Bericht', creator: 'Acme GmbH'))
->heading(1, 'Bericht')
->paragraph('Inhalt')
->build(); // → Document AST
Expand All @@ -260,6 +263,9 @@ $bytes = DocumentBuilder::new()->paragraph('Hallo')->toBytes();
$count = DocumentBuilder::new()->paragraph('Hallo')->toFile('out.docx');
```

Die Ausgabe ist byte-genau reproduzierbar: jeder ZIP-Eintrag trägt
`CoreProperties::$modified` (UTC) bzw. 1980-01-01 00:00:00, wenn nicht gesetzt.

### ParagraphBuilder

Innerhalb `->paragraph(fn ($p) => …)`:
Expand Down Expand Up @@ -292,6 +298,8 @@ Absatz-Stil:
```php
->paragraph(fn ($p) => $p
->alignCenter() // oder alignRight()/alignJustify()
->styleId('Quote') // eine Style-ID aus der StyleRegistry
->pageBreakBefore() // auf neuer Seite beginnen; widowControl(false) schaltet die Absatzkontrolle aus
->indentMm(left: 20, firstLine: 10)
->spacingPt(before: 6, after: 12)
->text('Eingerückt')
Expand Down Expand Up @@ -371,6 +379,7 @@ RunStyleBuilder::new()
->backgroundColor('eeeeee')
->highlight('yellow')
->fontFamily('Arial')
->fontFamilyEastAsia('SimSun') // w:eastAsia: die Schrift für CJK-Text
->fontSizePt(14.5)
->build();
```
Expand All @@ -393,7 +402,7 @@ Length::px(100); // 1500 (CSS px @ 96 DPI)
Die meisten Builder bieten einheiten-bewusste Shortcuts:

- TableBuilder: `widthPt/widthMm/widthCm/widthInches`,
`cellMarginsMm/cellMarginsPt`
`cellMarginsMm/cellMarginsPt`, `indent/indentMm/indentPt`
- TableCellBuilder: `widthPt/Mm/Cm/Inches`, `paddingMm/Pt/Cm/Inches`
- ColumnBuilder: `widthPt/Mm/Cm/Inches/Px`
- ParagraphBuilder: `indentMm/Cm/Pt/Inches`, `spacingPt/Mm`
Expand Down Expand Up @@ -558,26 +567,26 @@ Alle Elemente leben unter `Dskripchenko\PhpDocx\Element`.
|---|---|---|
| `Document` | root | `{ section: Section, watermarkText: ?string }` |
| `Section` | container | `{ body, header, footer, pageSetup, firstHeader, firstFooter, evenHeader, evenFooter }` |
| `Paragraph` | BlockElement | `{ children: InlineElement[], style, headingLevel: ?int }` |
| `Paragraph` | BlockElement | `{ children: InlineElement[], style, headingLevel: ?int, styleId: ?string }` |
| `Run` | InlineElement | `{ text: string, style: RunStyle }` |
| `Hyperlink` | InlineElement | `{ href: ?string, anchor: ?string, children }` |
| `Bookmark` | InlineElement | `{ name: string, children }` |
| `Image` | beides | `{ binary, format, widthEmu, heightEmu, altText }` |
| `Field` | InlineElement | `{ instruction: string, style: RunStyle }` |
| `LineBreak`, `PageBreak`, `HorizontalRule` | beides | Marker-Elemente |
| `Table` | BlockElement | `{ rows: TableRow[], style, caption, gridColumnsTwips }` |
| `TableRow` | element | `{ cells: TableCell[], isHeader, heightTwips, cantSplit }` |
| `TableRow` | element | `{ cells: TableCell[], isHeader, heightTwips, cantSplit, heightRule }` |
| `TableCell` | element | `{ children: BlockElement[], style: CellStyle }` |
| `ListNode` | BlockElement | `{ items: ListItem[], ordered, format, startAt }` |
| `ListItem` | element | `{ children: InlineElement[], nestedList: ?ListNode }` |

Styles liegen unter `Dskripchenko\PhpDocx\Style`:

- `RunStyle` — Schrift, Gewicht, Kursiv, Farbe, Grösse, Highlight, …
- `ParagraphStyle` — Ausrichtung, Einzüge, Abstände, Rahmen
- `RunStyle` — Schrift (und `fontFamilyEastAsia` für CJK-Text), Gewicht, Kursiv, Farbe, Grösse, Highlight, …
- `ParagraphStyle` — Ausrichtung, Einzüge, Abstände, Rahmen, Seitenumbruch (keepWithNext, keepLines, pageBreakBefore, widowControl)
- `CellStyle` — Breite, Padding, Rahmen, vertikale Ausrichtung,
gridSpan, rowSpan
- `TableStyle` — Breite, Rahmen, Ausrichtung, Zellenränder, Layout
- `TableStyle` — Breite, Rahmen, Ausrichtung, Zellenränder, Layout, Einzug (`indentTwips`)
- `PageSetup`, `PaperSize`, `Orientation`, `Alignment`, `VerticalAlign`,
`BorderStyle`, `Border`, `BorderSet`

Expand Down
23 changes: 16 additions & 7 deletions docs/ru/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,12 @@ Namespace `Build` предоставляет fluent API для поэтапно

### DocumentBuilder

Точка входа. Накапливает body, header/footer, watermark, page setup.
Точка входа. Накапливает body, header/footer, watermark, page setup,
свойства документа (`docProps/core.xml`: название, автор, даты).

```php
use Dskripchenko\PhpDocx\Build\DocumentBuilder;
use Dskripchenko\PhpDocx\Style\CoreProperties;
use Dskripchenko\PhpDocx\Style\PageSetup;
use Dskripchenko\PhpDocx\Style\PaperSize;
use Dskripchenko\PhpDocx\Style\Orientation;
Expand All @@ -248,6 +250,7 @@ $doc = DocumentBuilder::new()
orientation: Orientation::Portrait,
))
->watermark('КОНФИДЕНЦИАЛЬНО')
->coreProperties(new CoreProperties(title: 'Отчёт', creator: 'ООО «Ромашка»'))
->heading(1, 'Отчёт')
->paragraph('Тело документа')
->build(); // → Document AST
Expand All @@ -256,6 +259,9 @@ $bytes = DocumentBuilder::new()->paragraph('Привет')->toBytes();
$count = DocumentBuilder::new()->paragraph('Привет')->toFile('out.docx');
```

Результат воспроизводим байт в байт: у каждой записи ZIP время
`CoreProperties::$modified` (UTC), а без него — 1980-01-01 00:00:00.

### ParagraphBuilder

Внутри `->paragraph(fn ($p) => …)`:
Expand Down Expand Up @@ -288,6 +294,8 @@ $count = DocumentBuilder::new()->paragraph('Привет')->toFile('out.docx');
```php
->paragraph(fn ($p) => $p
->alignCenter() // alignRight()/alignJustify()
->styleId('Quote') // id стиля из StyleRegistry
->pageBreakBefore() // с новой страницы; widowControl(false) выключает запрет висячих строк
->indentMm(left: 20, firstLine: 10)
->spacingPt(before: 6, after: 12)
->text('С отступами')
Expand Down Expand Up @@ -367,6 +375,7 @@ RunStyleBuilder::new()
->backgroundColor('eeeeee')
->highlight('yellow')
->fontFamily('Arial')
->fontFamilyEastAsia('SimSun') // w:eastAsia: шрифт для текста CJK
->fontSizePt(14.5)
->build();
```
Expand All @@ -387,7 +396,7 @@ Length::px(100); // 1500 (CSS px @ 96 DPI)

Большинство builder'ов имеют unit-aware shortcut'ы:

- TableBuilder: `widthPt/widthMm/widthCm/widthInches`, `cellMarginsMm/cellMarginsPt`
- TableBuilder: `widthPt/widthMm/widthCm/widthInches`, `cellMarginsMm/cellMarginsPt`, `indent/indentMm/indentPt`
- TableCellBuilder: `widthPt/Mm/Cm/Inches`, `paddingMm/Pt/Cm/Inches`
- ColumnBuilder: `widthPt/Mm/Cm/Inches/Px`
- ParagraphBuilder: `indentMm/Cm/Pt/Inches`, `spacingPt/Mm`
Expand Down Expand Up @@ -551,25 +560,25 @@ foreach ($variables as $v) {
|---|---|---|
| `Document` | root | `{ section: Section, watermarkText: ?string }` |
| `Section` | container | `{ body, header, footer, pageSetup, firstHeader, firstFooter, evenHeader, evenFooter }` |
| `Paragraph` | BlockElement | `{ children: InlineElement[], style, headingLevel: ?int }` |
| `Paragraph` | BlockElement | `{ children: InlineElement[], style, headingLevel: ?int, styleId: ?string }` |
| `Run` | InlineElement | `{ text: string, style: RunStyle }` |
| `Hyperlink` | InlineElement | `{ href: ?string, anchor: ?string, children }` |
| `Bookmark` | InlineElement | `{ name: string, children }` |
| `Image` | both | `{ binary, format, widthEmu, heightEmu, altText }` |
| `Field` | InlineElement | `{ instruction: string, style: RunStyle }` |
| `LineBreak`, `PageBreak`, `HorizontalRule` | both | marker-элементы |
| `Table` | BlockElement | `{ rows: TableRow[], style, caption, gridColumnsTwips }` |
| `TableRow` | element | `{ cells: TableCell[], isHeader, heightTwips, cantSplit }` |
| `TableRow` | element | `{ cells: TableCell[], isHeader, heightTwips, cantSplit, heightRule }` |
| `TableCell` | element | `{ children: BlockElement[], style: CellStyle }` |
| `ListNode` | BlockElement | `{ items: ListItem[], ordered, format, startAt }` |
| `ListItem` | element | `{ children: InlineElement[], nestedList: ?ListNode }` |

Стили — namespace `Dskripchenko\PhpDocx\Style`:

- `RunStyle` — шрифт, weight, italic, color, size, highlight, …
- `ParagraphStyle` — alignment, indents, spacing, borders
- `RunStyle` — шрифт (и `fontFamilyEastAsia` для текста CJK), weight, italic, color, size, highlight, …
- `ParagraphStyle` — alignment, indents, spacing, borders, разбиение на страницы (keepWithNext, keepLines, pageBreakBefore, widowControl)
- `CellStyle` — width, padding, borders, valign, gridSpan, rowSpan
- `TableStyle` — width, borders, alignment, cell margins, layout
- `TableStyle` — width, borders, alignment, cell margins, layout, отступ (`indentTwips`)
- `PageSetup`, `PaperSize`, `Orientation`, `Alignment`, `VerticalAlign`,
`BorderStyle`, `Border`, `BorderSet`

Expand Down
Loading
Loading