Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,80 @@ 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.

### Changed
- ZIP entries no longer carry the time of the write. Every entry is stamped
with `CoreProperties::$modified` (as a UTC wall-clock time), or with
1980-01-01 00:00:00, the earliest DOS time, when it is not set. Pass
`modified: new DateTimeImmutable()` to get the current time back.
- HTML: a length `margin-left` on a `<table>` is now read as the table indent
and written as `<w:tblInd>`, so a table that set it is indented in Word too.
`margin-left: auto` (centring) is not an indent and is ignored.

### 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`, `\x0B`, …) went into the XML as is,
and Word refused to open the file. In run text, a vertical tab (`\x0B`,
Word's manual line break) is now written as `<w:br/>` and a form feed
(`\x0C`, Word's page break) as `<w:br w:type="page"/>`; `XmlEscape` removes
every other 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
27 changes: 20 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,12 +377,18 @@ 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')
)
```

`styleId()` takes effect only for a style the document defines: `Heading1..6`,
`ListParagraph`, `Caption`, or one registered with `StyleRegistry::paragraph()`.
Word shows a paragraph with an unknown id as Normal.

### TableBuilder

```php
Expand Down Expand Up @@ -450,6 +462,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 +484,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 +648,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
27 changes: 20 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,12 +298,18 @@ 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')
)
```

`styleId()` wirkt nur bei einem Style, den das Dokument definiert: `Heading1..6`,
`ListParagraph`, `Caption` oder einer, der mit `StyleRegistry::paragraph()` registriert ist.
Einen Absatz mit unbekannter ID zeigt Word als Normal.

### TableBuilder

```php
Expand Down Expand Up @@ -371,6 +383,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 +406,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 +571,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
27 changes: 20 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,12 +294,18 @@ $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('С отступами')
)
```

`styleId()` действует только для стиля, который есть в документе: `Heading1..6`,
`ListParagraph`, `Caption` или зарегистрированный через `StyleRegistry::paragraph()`.
Абзац с неизвестным id Word показывает как Normal.

### TableBuilder

```php
Expand Down Expand Up @@ -367,6 +379,7 @@ RunStyleBuilder::new()
->backgroundColor('eeeeee')
->highlight('yellow')
->fontFamily('Arial')
->fontFamilyEastAsia('SimSun') // w:eastAsia: шрифт для текста CJK
->fontSizePt(14.5)
->build();
```
Expand All @@ -387,7 +400,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 +564,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