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
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,58 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- `TtfFile::exportState()` and `TtfFile::fromState()` save a parsed font,
kerning included, as plain data and restore it without parsing — for a
cache across requests. A restore takes well under a millisecond against
0.4–3.6 ms to parse. The state carries `TtfFile::STATE_FORMAT` and a fingerprint of the
font's table directory: `fromState()` refuses, with an
`InvalidArgumentException`, a state of another format, of another font
file, or a damaged one, nested values included. `KerningTable::exportState()`
and `KerningTable::fromState()` do the same for a kerning table. The four
methods are experimental: the state is for the application's own,
trusted cache, and a release that changes its layout bumps
`STATE_FORMAT`, which only makes such a cache parse again. Documented in
`docs/*/USAGE.md` under Custom fonts.
- `PdfFont::$compressionLevel` sets the zlib level for embedded font
bodies (default 6, as before). Level 1 compresses a subset two to five
times faster and the stream is within a few percent of the level-6 size.
A level outside -1..9 is refused with an `InvalidArgumentException`.

### Changed
- Kerning is no longer expanded into glyph pairs. GPOS class-based
subtables (PairPos format 2) are kept as classes, and the whole table is
held as packed binary strings — for the usual value format, slices of the
font itself — searched in place. Fonts with class kerning parsed for tens
of milliseconds and held tens of megabytes: Inter Regular took 35 ms and
26 MB for its 447,676 pairs, Fira Sans 37 ms and 27 MB. They now take
about 1 ms and 0.25 MB; Merriweather's 102,000 explicit pairs take
0.7 ms and 0.5 MB instead of 38 ms and 7 MB. Every lookup answers as
before: compared pair by pair on 77 fonts.
- `KerningTable::addPairs()` and `KerningTable::addClasses()` add packed
pairs and class subtables.
- `KerningTable::pairCount()` returns the same number as before, but now
counts by expanding every class subtable into glyph pairs: it is meant
for diagnostics, not the render path. On a font with class kerning it
takes milliseconds and megabytes, as parsing did before; on a damaged
`GPOS` table it can take far more.
- Font subsetting is about twice as fast: table checksums are summed from
one `unpack()` instead of one call per 32-bit word. Inter Regular
subsets in 0.8 ms instead of 1.9 ms, Noto Sans JP in 4.0 ms instead of
8.2 ms; the subsets are byte-identical.
- Fonts parse two to three times faster: the `cmap` (formats 4 and 12) and
`hmtx` tables are read with one `unpack()` per array instead of one call
per value. Inter Regular parses in 0.7 ms instead of 1.9 ms, Noto Sans JP
(17,000 glyphs) in 3.4 ms instead of 9.2 ms. The parsed tables are
unchanged: identical on 241 fonts compared, the 8 others rejected with
the same errors as before.

### Fixed
- Embedded font subsets kept the source font's `head.checkSumAdjustment`,
and the `head` entry of their table directory was summed with it, so the
subset's checksums were wrong: strict validators and font tools flag such
a font. The adjustment is zeroed for the table checksums and set so the
whole subset sums to `0xB1B0AFBA`, as OpenType requires.
- Embedded TrueType subsets were named after the whole font. Their
`/BaseFont` and `/FontName` now start with a six-letter tag
(ISO 32000-1 §9.6.4), taken from a hash of the subset's bytes, so two
Expand Down
62 changes: 62 additions & 0 deletions docs/de/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ einen Rundgang oder springen Sie direkt zum benötigten Feature.
- [Seitenaufbau und Abschnittsumbrüche](#seitenaufbau-und-abschnittsumbrüche)
- [HTML- / CSS-Eingabe](#html---css-eingabe)
- [Eigene Schriftarten](#eigene-schriftarten)
- [Schriftarten in Prozessen pro Anfrage](#schriftarten-in-prozessen-pro-anfrage)
- [Barcodes](#barcodes)
- [Diagramme](#diagramme)
- [Mathematische Ausdrücke](#mathematische-ausdrücke)
Expand Down Expand Up @@ -553,6 +554,67 @@ PDF/A-1 zulässig); die Ausgabe enthält also die volle CFF-Größe
CFF2 (variables CFF) werden abgelehnt. Kerning, einfache Ligaturen und ToUnicode-CMaps werden
automatisch ausgegeben.

### Schriftarten in Prozessen pro Anfrage

`TtfFile::fromFile()` hält geparste Schriftarten für die Lebensdauer des
Prozesses vor (bis zu `TtfFile::$cacheLimit` Stück): Ein Queue-Worker oder
Octane parst jede Schriftart nur einmal. Unter PHP-FPM beginnt jede Anfrage
leer und parst ihre Schriftarten erneut — etwa eine Millisekunde für eine
lateinische Schrift, mehrere für eine CJK-Schrift. `exportState()` speichert
eine geparste Schriftart als einfache Daten (Ganzzahlen, Strings, Booleans
und Arrays), und `TtfFile::fromState()` stellt sie aus diesen Daten und
derselben Schriftdatei ohne Parsen wieder her:

```php
use Dskripchenko\PhpPdf\Font\Ttf\TtfFile;
use Dskripchenko\PhpPdf\Pdf\PdfFont;

function cachedFont(string $path, string $cacheDir): TtfFile
{
$bytes = (string) file_get_contents($path);
$file = $cacheDir.'/'.basename($path).'.v'.TtfFile::STATE_FORMAT.'.php';

if (is_file($file)) {
try {
return TtfFile::fromState(require $file, $bytes);
} catch (\InvalidArgumentException) {
// andere Schriftdatei oder beschädigter Zustand: unten neu parsen
}
}

$font = new TtfFile($bytes);
$tmp = $file.'.'.getmypid();
file_put_contents($tmp, '<?php return '.var_export($font->exportState(), true).';');
rename($tmp, $file);

return $font;
}

$regular = new PdfFont(cachedFont(__DIR__.'/fonts/DejaVuSans.ttf', __DIR__.'/var/fonts'));
```

Ein mit `var_export()` geschriebener und per `require` gelesener Zustand
liegt im gemeinsamen Speicher von OPcache, die Wiederherstellung kostet
also einen Bruchteil des Parsens. JSON funktioniert ebenso; bei
`serialize()` lesen Sie mit `unserialize($s, ['allowed_classes' => false])`
zurück — ein Zustand enthält nie Objekte.

Der Zustand ist für den eigenen Cache Ihrer Anwendung gedacht:
`fromState()` prüft seine Form, nicht seine Integrität, stellen Sie also nie
einen Zustand aus fremder Quelle wieder her. Einen Zustand mit anderem
`TtfFile::STATE_FORMAT`, von einer anderen Schriftdatei oder einen
beschädigten weist es mit einer `InvalidArgumentException` zurück; dann wird
wie oben neu geparst. Die beiden Methoden sind experimentell: Ein Release,
das das Format ändert, erhöht `STATE_FORMAT`, der Cache verfehlt einmal und
füllt sich neu, sonst bricht nichts. Ist `STATE_FORMAT` wie oben Teil des
Cache-Schlüssels, bleiben Zustände verschiedener Versionen getrennt.

Eingebettete Schriftdaten werden mit zlib-Stufe 6 komprimiert. Wo Dokumente
pro Anfrage erzeugt werden, komprimiert `PdfFont::$compressionLevel = 1;`
eine Teilmenge zwei- bis fünfmal schneller, bei einem um wenige Prozent
größeren Stream. Werte außerhalb von -1..9 werden mit einer
`InvalidArgumentException` abgelehnt.

---

## Barcodes
Expand Down
59 changes: 59 additions & 0 deletions docs/en/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ the feature you need.
- [Page setup and section breaks](#page-setup-and-section-breaks)
- [HTML / CSS input](#html--css-input)
- [Custom fonts](#custom-fonts)
- [Fonts in per-request processes](#fonts-in-per-request-processes)
- [Barcodes](#barcodes)
- [Charts](#charts)
- [Math expressions](#math-expressions)
Expand Down Expand Up @@ -531,6 +532,64 @@ PDF/A-1 too); expect the full CFF size in the output (100–200 KB for a Latin
plus Cyrillic face). CFF2 (variable CFF) fonts are rejected. Kerning, basic ligatures, and ToUnicode CMaps are emitted
automatically.

### Fonts in per-request processes

`TtfFile::fromFile()` keeps parsed fonts for the life of the process
(`TtfFile::$cacheLimit` of them), so a queue worker or Octane parses each
font once. Under PHP-FPM every request starts empty and parses its fonts
again — about a millisecond for a Latin face, several for a CJK one.
`exportState()` saves a parsed font as plain data (ints, strings, bools
and arrays), and `TtfFile::fromState()` restores it from that data and the
same font file without parsing:

```php
use Dskripchenko\PhpPdf\Font\Ttf\TtfFile;
use Dskripchenko\PhpPdf\Pdf\PdfFont;

function cachedFont(string $path, string $cacheDir): TtfFile
{
$bytes = (string) file_get_contents($path);
$file = $cacheDir.'/'.basename($path).'.v'.TtfFile::STATE_FORMAT.'.php';

if (is_file($file)) {
try {
return TtfFile::fromState(require $file, $bytes);
} catch (\InvalidArgumentException) {
// another font file or a damaged state: parse again below
}
}

$font = new TtfFile($bytes);
$tmp = $file.'.'.getmypid();
file_put_contents($tmp, '<?php return '.var_export($font->exportState(), true).';');
rename($tmp, $file);

return $font;
}

$regular = new PdfFont(cachedFont(__DIR__.'/fonts/DejaVuSans.ttf', __DIR__.'/var/fonts'));
```

A state written with `var_export()` and read back with `require` sits in
OPcache's shared memory, so the restore costs a fraction of a parse. JSON
works too; with `serialize()`, read it back with
`unserialize($s, ['allowed_classes' => false])` — a state never holds
objects.

The state is for your application's own cache: `fromState()` checks its
shape, not its integrity, so never restore one that came from outside.
It refuses — with an `InvalidArgumentException` — a state of another
`TtfFile::STATE_FORMAT`, of another font file, or a damaged one; fall back
to parsing, as above. The pair is experimental: a release that changes
the layout bumps `STATE_FORMAT`, the cache misses once and fills again,
and nothing else breaks. Keying the cache by `STATE_FORMAT`, as above,
keeps states of different versions apart.

Embedded font bodies are compressed at zlib level 6. Where documents are
rendered per request, `PdfFont::$compressionLevel = 1;` compresses a
subset two to five times faster for a stream a few percent larger. Values
outside -1..9 are refused with an `InvalidArgumentException`.

---

## Barcodes
Expand Down
60 changes: 60 additions & 0 deletions docs/ru/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [Настройка страницы и разрывы секций](#настройка-страницы-и-разрывы-секций)
- [Вход HTML / CSS](#вход-html--css)
- [Пользовательские шрифты](#пользовательские-шрифты)
- [Шрифты в процессах «на запрос»](#шрифты-в-процессах-на-запрос)
- [Штрихкоды](#штрихкоды)
- [Графики](#графики)
- [Математические выражения](#математические-выражения)
Expand Down Expand Up @@ -547,6 +548,65 @@ OpenType-шрифты на основе CFF (`.otf` с таблицей `CFF `)
с CFF2 (вариативный CFF) отклоняются. Кернинг, базовые лигатуры и ToUnicode CMaps
эмитятся автоматически.

### Шрифты в процессах «на запрос»

`TtfFile::fromFile()` держит разобранные шрифты всё время жизни процесса
(не больше `TtfFile::$cacheLimit`), поэтому воркер очереди или Octane
разбирает каждый шрифт один раз. Под PHP-FPM каждый запрос начинает с нуля
и разбирает шрифты заново — около миллисекунды на латинскую гарнитуру и
несколько на CJK. `exportState()` сохраняет разобранный шрифт как простые
данные (целые, строки, булевы значения и массивы), а `TtfFile::fromState()`
восстанавливает его из этих данных и того же файла шрифта без разбора:

```php
use Dskripchenko\PhpPdf\Font\Ttf\TtfFile;
use Dskripchenko\PhpPdf\Pdf\PdfFont;

function cachedFont(string $path, string $cacheDir): TtfFile
{
$bytes = (string) file_get_contents($path);
$file = $cacheDir.'/'.basename($path).'.v'.TtfFile::STATE_FORMAT.'.php';

if (is_file($file)) {
try {
return TtfFile::fromState(require $file, $bytes);
} catch (\InvalidArgumentException) {
// другой файл шрифта или повреждённое состояние: разбираем заново ниже
}
}

$font = new TtfFile($bytes);
$tmp = $file.'.'.getmypid();
file_put_contents($tmp, '<?php return '.var_export($font->exportState(), true).';');
rename($tmp, $file);

return $font;
}

$regular = new PdfFont(cachedFont(__DIR__.'/fonts/DejaVuSans.ttf', __DIR__.'/var/fonts'));
```

Состояние, записанное через `var_export()` и прочитанное `require`, лежит в
общей памяти OPcache, и восстановление стоит долю разбора. Подойдёт и JSON;
при `serialize()` читайте обратно через
`unserialize($s, ['allowed_classes' => false])` — объектов в состоянии
не бывает.

Состояние предназначено для собственного кэша приложения: `fromState()`
проверяет его форму, но не целостность, поэтому не восстанавливайте
состояние, пришедшее извне. Состояние другого `TtfFile::STATE_FORMAT`, от
другого файла шрифта или повреждённое отклоняется с
`InvalidArgumentException` — тогда шрифт разбирается заново, как в примере.
Пара методов экспериментальная: релиз, меняющий формат, повышает
`STATE_FORMAT`, кэш один раз промахивается и заполняется снова, больше
ничего не ломается. Если `STATE_FORMAT` входит в ключ кэша, как в примере,
состояния разных версий не смешиваются.

Тела встроенных шрифтов сжимаются zlib на уровне 6. Там, где документы
строятся на каждый запрос, `PdfFont::$compressionLevel = 1;` сжимает
подмножество в два–пять раз быстрее, а поток выходит больше на несколько
процентов. Значения вне -1..9 отклоняются с `InvalidArgumentException`.

---

## Штрихкоды
Expand Down
54 changes: 54 additions & 0 deletions docs/zh/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ HTML 转 PDF 一直深入到最底层的页面发射。每个章节都是自包
- [页面设置与分节符](#页面设置与分节符)
- [HTML / CSS 输入](#html--css-输入)
- [自定义字体](#自定义字体)
- [按请求运行的进程中的字体](#按请求运行的进程中的字体)
- [条形码](#条形码)
- [图表](#图表)
- [数学公式](#数学公式)
Expand Down Expand Up @@ -506,6 +507,59 @@ TrueType 轮廓字体(`.ttf`、TrueType 风格的 `.otf`)按需子集化 —
CFF2(可变 CFF)字体会被拒绝。
字距调整、基础连字与 ToUnicode CMap 会自动发射。

### 按请求运行的进程中的字体

`TtfFile::fromFile()` 会在整个进程生命周期内保留已解析的字体(最多
`TtfFile::$cacheLimit` 个),因此队列 worker 或 Octane 对每个字体只解析一次。
在 PHP-FPM 下,每个请求都从零开始并重新解析字体——拉丁字体约需一毫秒,
CJK 字体需要数毫秒。`exportState()` 将已解析的字体保存为纯数据(整数、
字符串、布尔值和数组),`TtfFile::fromState()` 则根据这些数据和同一个
字体文件恢复字体,无需解析:

```php
use Dskripchenko\PhpPdf\Font\Ttf\TtfFile;
use Dskripchenko\PhpPdf\Pdf\PdfFont;

function cachedFont(string $path, string $cacheDir): TtfFile
{
$bytes = (string) file_get_contents($path);
$file = $cacheDir.'/'.basename($path).'.v'.TtfFile::STATE_FORMAT.'.php';

if (is_file($file)) {
try {
return TtfFile::fromState(require $file, $bytes);
} catch (\InvalidArgumentException) {
// 其他字体文件或状态已损坏:在下面重新解析
}
}

$font = new TtfFile($bytes);
$tmp = $file.'.'.getmypid();
file_put_contents($tmp, '<?php return '.var_export($font->exportState(), true).';');
rename($tmp, $file);

return $font;
}

$regular = new PdfFont(cachedFont(__DIR__.'/fonts/DejaVuSans.ttf', __DIR__.'/var/fonts'));
```

用 `var_export()` 写出、再用 `require` 读回的状态会驻留在 OPcache 的共享
内存中,恢复只需解析耗时的一小部分。也可以使用 JSON;若使用
`serialize()`,请用 `unserialize($s, ['allowed_classes' => false])` 读回——
状态中从不包含对象。

状态仅供应用自己的缓存使用:`fromState()` 只检查其结构,不检查其完整性,
因此切勿恢复来自外部的状态。对于 `TtfFile::STATE_FORMAT` 不同、属于其他
字体文件或已损坏的状态,它会抛出 `InvalidArgumentException` 拒绝;此时
按上例重新解析即可。这对方法属于实验性 API:改变格式的版本会提高
`STATE_FORMAT`,缓存未命中一次后重新填充,不会造成其他破坏。像上例那样
把 `STATE_FORMAT` 放进缓存键,不同版本的状态就不会混用。

嵌入的字体数据以 zlib 6 级压缩。在按请求生成文档的场景中,
`PdfFont::$compressionLevel = 1;` 压缩子集的速度快两到五倍,数据流只大
几个百分点。-1..9 以外的取值会以 `InvalidArgumentException` 拒绝。

---

## 条形码
Expand Down
Loading
Loading