diff --git a/.gitattributes b/.gitattributes index 09b7aea..d372ca0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,13 @@ -/.git* export-ignore -/.php-cs-fixer.dist.php export-ignore -/phpstan.neon.dist export-ignore -/phpunit.xml.dist export-ignore -/tests/ export-ignore -/tests/Fixtures/* -text +* text=auto eol=lf + +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.php-cs-fixer.dist.php export-ignore +/docs export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore + +/docs/** linguist-documentation +/tests/Fixtures/** -linguist-detectable diff --git a/.gitignore b/.gitignore index 9533606..8346dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ /vendor/ /composer.lock /.phpunit.cache/ +/.phpunit.result.cache /.php-cs-fixer.cache -/var/ +/coverage/ +/coverage.xml +/clover.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index c432805..6028db4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## [Unreleased] + +- Add documentation. + ## [0.8.0] - Support transformed WOFF2 `glyf`, `loca`, and `hmtx` tables. diff --git a/README.md b/README.md index 02960aa..9c80ec0 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,145 @@ -# alto/font +# ALTO Font -Font file reading and metadata for PHP: parse OpenType/TrueType/WOFF/WOFF2, -discover installed fonts, and expose per-glyph facts. +Read OpenType, TrueType, WOFF, and WOFF2 font files from PHP. -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![PHP](https://img.shields.io/badge/php-%3E%3D8.4-777bb4.svg)](composer.json) +  ![PHP Version](https://img.shields.io/badge/PHP-8.4%2B-00B7FF?logoColor=00B7FF&labelColor=050608) +  ![CI](https://img.shields.io/github/actions/workflow/status/altophp/font/CI.yml?branch=main&label=Tests&labelColor=050608&color=00B7FF) +  [![Packagist](https://img.shields.io/packagist/v/alto/font?label=Packagist&labelColor=050608&color=00B7FF)](https://packagist.org/packages/alto/font) +  ![License](https://img.shields.io/github/license/altophp/font?label=License&labelColor=050608&color=00B7FF) +  [![GitHub Sponsors](https://img.shields.io/github/sponsors/smnandre?logo=githubsponsors&logoColor=00B7FF&label=%20Sponsor&labelColor=050608&color=00B7FF)](https://github.com/sponsors/smnandre) -`alto/font` answers "what is this font, and what are the facts about this -glyph" -- metadata, discovery, per-glyph metrics and outlines. It does not -shape text, does not apply kerning, and does not draw anything. Turning a -string of text into positioned, drawn glyphs is a different package's job -(`alto/svg-font` and friends); this one only reads and reports facts. +ALTO Font answers what a font contains: names, descriptors, licensing metadata, face dimensions, +character maps, glyph metrics, outlines, collections, and variable-font axes. It deliberately does +not shape text, apply kerning, or render glyphs. ```php use Alto\Font\Font; -use Alto\Font\FontFinder; -use Alto\Font\FontQuery; $font = Font::fromFile(__DIR__.'/fonts/Inter.ttf'); -$font->getDescriptor()->family; // 'Inter' -$font->face()->unitsPerEm; // 1000 -$font->face()->ascender; // 968 - -$glyphId = $font->glyphIdForCodepoint(mb_ord('A')); -$font->glyphMetrics($glyphId)->advanceWidth; -$font->glyphOutline($glyphId); // raw Contour/PathCommand geometry - -$finder = FontFinder::fromDirectories(__DIR__.'/fonts'); -$bold = $finder->get(FontQuery::family('Inter')->weight(700)); +echo $font->metadata()->family; +echo $font->face()->unitsPerEm; +echo $font->getMetrics('A')->advanceWidth; ``` -## Features - -- `Font::fromFile()` -- load an OpenType/TrueType/WOFF/WOFF2 font file. -- `FontFace` -- `unitsPerEm`, `ascender`, `descender`, `glyphCount`, raw table - access. -- `FontDescriptor` -- family, subfamily, full name, PostScript name, weight, - style, stretch, inferred from the `name`/subfamily strings. -- `FontMetadata` -- copyright, manufacturer, designer, version, license - description and URL, read from the `name` table. -- Per-glyph facts: `glyphIdForCodepoint()`, `glyphMetrics()` (advance width, - side bearings), `glyphOutline()` (contours as generic move/line/quad - commands, transformable, not tied to any output format). -- Variable fonts: `withVariations()` resolves `fvar`/`avar`/`gvar`/`HVAR` axes - and instances into concrete per-instance metrics and outlines. -- Discovery: `FontFinder::system()` / `::fromDirectories($dirs)` / `::fromLocator()`, - resolving a `FontQuery` (family/weight/style/stretch) to the best-matching `Font` - via `has()` / `find()` / `get()`. `Locator\FontLocatorInterface` is the one - injectable seam, for tests or custom/non-directory font sources. - -## Format support - -| Format | Extension(s) | Support | -| --- | --- | --- | -| TrueType (`glyf` outlines) | `.ttf` | Supported, including compound glyphs | -| OpenType with `glyf` outlines | `.otf` | Supported | -| WOFF v1 | `.woff` | Supported (`ext-zlib`, always available) | -| WOFF2 | `.woff2` | Supported, including transformed `glyf`/`loca` and `hmtx` tables. Requires `ext-brotli` or the `brotli` CLI binary on `PATH` | -| CFF/CFF2 outlines (Type 2 charstrings) | `.otf` | Not supported yet -- rejects cleanly | -| TrueType/OpenType collections | `.ttc`, `.otc` | Supported: `Font::fromFile($path, faceIndex: $n)` selects a face; `FontFace::$faceIndex`/`$faceCount` report the file's shape | -| Compressed (WOFF2) font collections | `.woff2` | Not supported yet -- rejects cleanly | -| Color glyph tables (`COLR`/`CPAL`, `sbix`, `SVG `, `CBDT`/`CBLC`) | `.ttf`, `.otf` | Not supported yet | -| Variable fonts (`fvar`/`avar`/`gvar`/`HVAR`) | `.ttf`, `.otf`, `.woff`, `.woff2` | Supported for `glyf`-based outlines | - -Every unsupported case raises a typed exception (`UnsupportedFontException` or -`InvalidFontException`) instead of degrading silently. +The package has no PHP package runtime dependencies. Unsupported containers and font features fail +with typed exceptions instead of returning partial data. ## Installation +Install ALTO Font with Composer: + ```bash composer require alto/font ``` -## Development +ALTO Font requires PHP 8.4 or later with Iconv and Zlib. Both extensions are included in most PHP +distributions. WOFF2 additionally requires the Brotli PHP extension or the `brotli` executable. + +## Quick Start + +Load a font and inspect its face, descriptor, and one glyph: + +```php +use Alto\Font\Font; + +$font = Font::fromFile(__DIR__.'/fonts/Inter-Regular.ttf'); +$descriptor = $font->getDescriptor(); + +printf( + "%s %s, %d units per em\n", + $descriptor->family, + $descriptor->subfamily, + $font->face()->unitsPerEm, +); + +$metrics = $font->getMetrics('A'); +$outline = $font->glyphOutline($metrics->glyphId); +``` + +Metrics and outlines use the font's design units. Read [Getting started](docs/getting-started.md) +for scaling and outline inspection. + +## Format Support + +| Format | Support | +| --- | --- | +| TrueType and OpenType with `glyf` outlines | Supported | +| WOFF 1 | Supported | +| WOFF2 | Supported, including transformed `glyf`, `loca`, and `hmtx` | +| TTC and OTC collections | Supported with `faceIndex` | +| Variable `glyf` fonts | Supported through `fvar`, `avar`, `gvar`, and `HVAR` | +| CFF/CFF2 outlines, WOFF2 collections, and color glyph rendering | Not supported | + +Read [Font formats](docs/formats.md) for requirements, boundaries, and failure types. + +## Discovery + +Find the closest face for a family, weight, style, and stretch query: + +```php +use Alto\Font\FontFinder; +use Alto\Font\FontQuery; + +$finder = FontFinder::fromDirectories(__DIR__.'/fonts'); +$font = $finder->get(FontQuery::family('Inter')->weight(700)->italic()); +``` + +ALTO Font can search application directories, system fonts, or a custom locator. Read +[Font discovery](docs/discovery.md) for matching and absence policies. + +## Metadata and Glyphs + +`FontFace` exposes structural metrics and table records. `FontDescriptor` provides names and +CSS-like matching values, while `FontMetadata` includes optional publisher and licensing fields. + +Character lookup returns a font-specific glyph identifier. From it, retrieve metrics or neutral +contour geometry made of move, line, quadratic-curve, and close commands. + +See [Font metadata](docs/metadata.md) and [Glyphs](docs/glyphs.md). + +## Variable Fonts + +Inspect axes and select immutable coordinates: + +```php +$boldCondensed = $font->withVariations([ + 'wght' => 700, + 'wdth' => 85, +]); +``` + +Selected coordinates affect supported glyph metrics and outlines. Read +[Variable fonts](docs/variations.md) for axes, named instances, clamping, and observable results. +The [complete guide](docs/index.md) links every topic. + +## Contributing + +Contributions of all kinds are welcome. Visit the +[project on GitHub](https://github.com/altophp/font) to +[report a bug](https://github.com/altophp/font/issues/new), +[suggest a feature](https://github.com/altophp/font/issues/new), or +[open a pull request](https://github.com/altophp/font/pulls). + +Before submitting code, run: ```bash -composer install -composer check # phpstan + cs-check + test -composer test # phpunit only -composer cs-fix # apply coding-standard fixes +# Runs PHP CS Fixer, PHPStan, and PHPUnit +composer qa ``` +Changes to public behavior should include tests and documentation. + +## Support + +ALTO Font is open source. You can support its continued development through +[GitHub Sponsors](https://github.com/sponsors/smnandre). + +Sharing this package with others or +[starring it on GitHub](https://github.com/altophp/font) is also much +appreciated. + ## License -Alto Font is available under the [MIT License](LICENSE). +ALTO Font is released by [ALTO PHP](https://altophp.com) under the +[MIT License](LICENSE). diff --git a/composer.json b/composer.json index 379a86b..d5b3abd 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "alto/font", - "description": "Font file reading and metadata: parse OpenType/TrueType/WOFF/WOFF2, discover installed fonts, and expose per-glyph facts (metrics, outlines) without any drawing or shaping.", + "description": "Read OpenType, TrueType, WOFF, and WOFF2 font files from PHP.", "keywords": [ "font", "opentype", @@ -8,7 +8,7 @@ "woff", "woff2" ], - "homepage": "https://altophp.com/font/", + "homepage": "https://altophp.com/font", "license": "MIT", "type": "library", "authors": [ @@ -19,22 +19,28 @@ ], "support": { "issues": "https://github.com/altophp/font/issues", - "source": "https://github.com/altophp/font" + "source": "https://github.com/altophp/font", + "docs": "https://altophp.com/font" }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/smnandre" + } + ], "require": { - "php": ">=8.4", + "php": "^8.4", + "ext-iconv": "*", "ext-zlib": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.68", + "friendsofphp/php-cs-fixer": "^3.94", "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^12" + "phpunit/phpunit": "^12.5" }, "suggest": { - "ext-brotli": "Decompresses WOFF2 font data without shelling out to the brotli binary.", - "brotli": "Provides Brotli decompression for WOFF2 when ext-brotli is not installed." + "ext-brotli": "Decompresses WOFF2 font data without shelling out to the brotli binary." }, - "minimum-stability": "stable", "autoload": { "psr-4": { "Alto\\Font\\": "src/" @@ -49,14 +55,14 @@ "sort-packages": true }, "scripts": { - "check": [ - "@phpstan", - "@cs-check", + "cs": "vendor/bin/php-cs-fixer fix --dry-run --diff --sequential", + "cs:fix": "vendor/bin/php-cs-fixer fix --sequential", + "qa": [ + "@cs", + "@sa", "@test" ], - "cs-check": "php-cs-fixer fix --dry-run --diff", - "cs-fix": "php-cs-fixer fix", - "phpstan": "phpstan analyse --no-progress", + "sa": "vendor/bin/phpstan analyse --memory-limit=-1", "test": "phpunit" } } diff --git a/docs/discovery.md b/docs/discovery.md new file mode 100644 index 0000000..8c2bbdc --- /dev/null +++ b/docs/discovery.md @@ -0,0 +1,90 @@ +# Font discovery + +`FontFinder` searches font files and selects the closest face for a family, +weight, style, and stretch query. + +## Search application directories + +```php +use Alto\Font\FontFinder; + +$finder = FontFinder::fromDirectories( + __DIR__.'/fonts', + __DIR__.'/vendor-fonts', +); + +$font = $finder->get('Inter'); +``` + +Directories are searched recursively. Invalid and unsupported files are +ignored while candidates are inspected. + +Use the method matching the absence policy of your application: + +```php +$finder->has('Inter'); // bool +$finder->find('Inter'); // Font|null +$finder->get('Inter'); // Font or FontNotFoundException +``` + +## Select a face + +Build an immutable query for more control: + +```php +use Alto\Font\Descriptor\FontStretch; +use Alto\Font\FontQuery; + +$query = FontQuery::family('Inter') + ->weight(700) + ->italic() + ->stretch(new FontStretch(100)); + +$font = $finder->get($query); +``` + +The finder prefers exact static faces. A variable font can satisfy `wght` and +`wdth` requests when it exposes those axes; the returned `Font` already +contains the selected coordinates. + +For simple calls, weight and style can be passed directly: + +```php +use Alto\Font\Descriptor\FontStyle; + +$font = $finder->get('Inter', weight: 700, style: FontStyle::Italic); +``` + +## Search system fonts + +```php +$finder = FontFinder::system(); +$font = $finder->find('Helvetica'); +``` + +System availability differs between machines. Do not rely on a system font in +portable tests or deterministic builds; provide a controlled font directory +instead. + +## Provide another source + +Implement `FontLocatorInterface` when paths come from an application index or +another non-directory source: + +```php +use Alto\Font\FontFinder; +use Alto\Font\Locator\FontLocatorInterface; + +$locator = new class implements FontLocatorInterface { + public function fonts(): iterable + { + yield __DIR__.'/fonts/Inter-Regular.ttf'; + yield __DIR__.'/fonts/Inter-Bold.ttf'; + } +}; + +$finder = FontFinder::fromLocator($locator); +``` + +The locator yields local file paths. Loading and candidate caching remain the +finder's responsibility. diff --git a/docs/formats.md b/docs/formats.md new file mode 100644 index 0000000..19f2b83 --- /dev/null +++ b/docs/formats.md @@ -0,0 +1,61 @@ +# Font formats + +`Font::fromFile()` detects the container from its contents. The filename +extension is used when reporting `FontMetadata::$format`, but it does not make +an unsupported font readable. + +| Format | Support | Requirement or boundary | +| --- | --- | --- | +| TrueType with `glyf` outlines | Supported | Includes compound glyphs | +| OpenType with `glyf` outlines | Supported | CFF and CFF2 outlines are rejected | +| WOFF 1 | Supported | Requires the Zlib extension | +| WOFF2 | Supported | Requires `ext-brotli` or the `brotli` executable | +| TTC and OTC collections | Supported | Select a face with `faceIndex` | +| WOFF2 collections | Not supported | Rejected explicitly | +| Variable `glyf` fonts | Supported | Includes `fvar`, `avar`, `gvar`, and `HVAR` | +| Color glyphs | Not supported | COLR, CPAL, SVG, sbix, CBDT, and CBLC are not rendered | + +## Font collections + +Select a zero-based face when loading a TrueType or OpenType collection: + +```php +use Alto\Font\Font; + +$font = Font::fromFile(__DIR__.'/fonts/Collection.ttc', faceIndex: 1); + +echo $font->face()->faceIndex; +echo $font->face()->faceCount; +``` + +An index outside the collection raises `InvalidFontException`. + +## WOFF2 decompression + +WOFF2 uses Brotli compression. Alto Font first uses the PHP Brotli extension +when it is available, then falls back to the `brotli` command-line program. +If neither is available, loading a WOFF2 file fails rather than silently +returning incomplete data. + +WOFF2 support includes transformed `glyf`, `loca`, and `hmtx` tables for +single-font files. + +## Failure types + +Catch the shared interface when the recovery action is the same for every +font-loading problem: + +```php +use Alto\Font\Exception\FontExceptionInterface; +use Alto\Font\Font; + +try { + $font = Font::fromFile($path); +} catch (FontExceptionInterface $error) { + // Reject the file or try another candidate. +} +``` + +Use `UnsupportedFontException` when you need to distinguish a valid but +unsupported feature from an `InvalidFontException` caused by malformed data +or an invalid selection. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..eaabd23 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,57 @@ +# Getting started + +Load a font once, then use the returned `Font` object for metadata and glyph +queries. + +```php +use Alto\Font\Font; + +$font = Font::fromFile(__DIR__.'/fonts/Inter-Regular.ttf'); +$face = $font->face(); +$descriptor = $font->getDescriptor(); + +printf( + "%s %s, %d units per em\n", + $descriptor->family, + $descriptor->subfamily, + $face->unitsPerEm, +); +``` + +## Inspect a character + +`getMetrics()` is the shortest route when you have exactly one character: + +```php +$metrics = $font->getMetrics('A'); + +echo $metrics->advanceWidth; +echo $metrics->leftSideBearing; +``` + +Metrics use the font's design units. Divide by `unitsPerEm` and multiply by +your target font size when converting them to another coordinate system. + +To inspect the outline, resolve the Unicode code point first: + +```php +$glyphId = $font->glyphIdForCodepoint(ord('A')); + +if (null === $glyphId) { + throw new RuntimeException('The font does not contain A.'); +} + +$outline = $font->glyphOutline($glyphId); + +foreach ($outline->contours as $contour) { + foreach ($contour->commands as $command) { + printf("%s %s\n", $command->type, implode(' ', $command->coordinates)); + } +} +``` + +The outline contains generic move, line, quadratic-curve, and close commands. +It is geometry, not an SVG or another rendered format. + +Continue with [Metadata](metadata.md), [Glyphs](glyphs.md), or +[Discovery](discovery.md), depending on the job your application performs. diff --git a/docs/glyphs.md b/docs/glyphs.md new file mode 100644 index 0000000..40cbb61 --- /dev/null +++ b/docs/glyphs.md @@ -0,0 +1,84 @@ +# Glyphs + +Font files map Unicode code points to glyph identifiers. Glyph identifiers are +font-specific and must not be reused with another font. + +## Resolve a character + +```php +$glyphId = $font->glyphIdForCodepoint(0x00E9); // é + +if (null === $glyphId) { + // This font has no glyph for the character. +} +``` + +`glyphIdForCodepoint()` returns `null` when the font's character map has no +entry. It does not perform font fallback. + +When working with one character, `getMetrics()` resolves it and reports a +clear failure if it is absent: + +```php +$metrics = $font->getMetrics('A'); + +echo $metrics->glyphId->value; +echo $metrics->advanceWidth; +echo $metrics->leftSideBearing; +``` + +Passing an empty string, more than one Unicode code point, or a missing +character raises `InvalidFontException`. Use the explicit code-point method +when absence is expected. + +## Read metrics by identifier + +```php +use Alto\Font\Glyph\GlyphId; + +$glyphId = new GlyphId(42); +$metrics = $font->glyphMetrics($glyphId); +``` + +The advance width and side bearing use the font's design units, not pixels. + +## Read an outline + +```php +$outline = $font->glyphOutline($glyphId); + +if ($outline->isEmpty()) { + // Spaces and other non-drawing glyphs can have no contours. +} + +foreach ($outline->contours as $contour) { + foreach ($contour->commands as $command) { + // M, L, Q, or Z with their design-unit coordinates. + } +} +``` + +`M`, `L`, `Q`, and `Z` represent move, line, quadratic curve, and close-path +commands. Alto Font exposes this neutral geometry without serializing it to +SVG, a bitmap, or another drawing format. + +## Transform geometry + +Outlines, contours, and path commands accept a two-dimensional affine +transform: + +```php +$scale = 16 / $font->face()->unitsPerEm; + +$scaled = $outline->transform( + xx: $scale, + yx: 0, + xy: 0, + yy: -$scale, + dx: 0, + dy: 16, +); +``` + +The example scales the outline to 16 units, flips the font's upward Y axis, +and moves the baseline. It still does not draw the result. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..04552bb --- /dev/null +++ b/docs/index.md @@ -0,0 +1,29 @@ +# Alto Font + +Alto Font reads font files and exposes their metadata, glyph metrics, outlines, +and variable-font axes. It does not shape text, apply kerning, or draw glyphs. + +```php +use Alto\Font\Font; + +$font = Font::fromFile(__DIR__.'/fonts/Inter-Regular.woff2'); + +$family = $font->metadata()->family; +$advanceWidth = $font->getMetrics('A')->advanceWidth; +``` + +## Introduction + +- [Installation](installation.md): install the package and check its runtime requirements. +- [Getting started](getting-started.md): load a font and inspect one glyph. + +## Fonts + +- [Formats](formats.md): understand supported containers, outlines, and optional WOFF2 requirements. +- [Discovery](discovery.md): find the best matching font in directories or the operating system. +- [Metadata](metadata.md): inspect names, descriptors, dimensions, and licensing fields. +- [Glyphs](glyphs.md): resolve characters to glyphs and read metrics and outlines. +- [Variations](variations.md): inspect axes and select a variable-font instance. + +Alto Font reports facts stored in fonts. Text layout, fallback, bidirectional +text, shaping, and rendering belong to higher-level packages. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..493801d --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,33 @@ +# Installation + +Alto Font requires PHP 8.4 or later and the Zlib extension. + +```bash +composer require alto/font +``` + +WOFF2 files additionally require either the `brotli` PHP extension or the +`brotli` command-line program on `PATH`. TrueType, OpenType, WOFF, and font +collections do not require that optional dependency. + +## Verify the installation + +Use a font file that belongs to your application or test fixtures: + +```php +metadata()->family; +``` + +The script prints the family stored in the font, such as `Inter`. + +Loading failures implement `Alto\Font\Exception\FontExceptionInterface`. +Unsupported font features raise `UnsupportedFontException`; malformed files +and invalid selections raise `InvalidFontException`. diff --git a/docs/metadata.md b/docs/metadata.md new file mode 100644 index 0000000..f1570c2 --- /dev/null +++ b/docs/metadata.md @@ -0,0 +1,65 @@ +# Font metadata + +Alto Font exposes three related views of a loaded face. Choose the smallest +one that answers the current question. + +## Face metrics + +`FontFace` contains structural values used to interpret glyph geometry: + +```php +$face = $font->face(); + +$face->unitsPerEm; +$face->ascender; +$face->descender; +$face->glyphCount; +$face->tables; +``` + +Metrics and outlines use design units. For a target size of 16 pixels, a value +can be scaled with `16 / $face->unitsPerEm`. + +For collections, `faceIndex` identifies the selected face and `faceCount` +reports the number of faces in the file. + +## Matching descriptors + +`getDescriptor()` returns the naming and CSS-like characteristics used by +font discovery: + +```php +$descriptor = $font->getDescriptor(); + +echo $descriptor->family; +echo $descriptor->subfamily; +echo $descriptor->weight->css(); +echo $descriptor->style->value; +echo $descriptor->stretch->css(); +``` + +Weight, style, and stretch are inferred from the font's subfamily names. They +are useful for selection but do not replace a full CSS font-matching engine. + +## Descriptive metadata + +`metadata()` includes the descriptor and optional fields from the OpenType +`name` table: + +```php +$metadata = $font->metadata(); + +echo $metadata->family; +echo $metadata->format->value; +echo $metadata->version; +echo $metadata->license; +echo $metadata->licenseUrl; +``` + +Available optional fields include full and PostScript names, copyright, +manufacturer, designer and vendor details, description, version, and license +information. A missing name-table record is returned as `null`; Alto Font does +not invent a replacement value. + +The license fields describe the font. They do not grant rights beyond the +license supplied by its publisher. diff --git a/docs/variations.md b/docs/variations.md new file mode 100644 index 0000000..5745e4a --- /dev/null +++ b/docs/variations.md @@ -0,0 +1,66 @@ +# Variable fonts + +A variable font exposes axes such as weight (`wght`) or width (`wdth`). Alto +Font can inspect these axes and return a new `Font` instance at selected +coordinates. + +## Inspect the axes + +```php +$variations = $font->variations(); + +if (null === $variations) { + // This is a static font. +} + +foreach ($variations->axes as $axis) { + printf( + "%s: %g to %g, default %g\n", + $axis->tag, + $axis->minimum, + $axis->maximum, + $axis->default, + ); +} +``` + +An axis also exposes its optional human-readable name and whether the font +marks it as hidden. Named instances are available through +`$variations->instances`. + +## Select coordinates + +```php +$boldCondensed = $font->withVariations([ + 'wght' => 700, + 'wdth' => 85, +]); + +$coordinates = $boldCondensed->variationCoordinates(); +$metrics = $boldCondensed->getMetrics('A'); +``` + +`withVariations()` is immutable: the original font remains at its default +coordinates. Values outside an axis range are clamped. Unknown axes and calls +on static fonts raise `InvalidFontException`. + +You can also build coordinates incrementally: + +```php +use Alto\Font\Variation\VariationCoordinates; + +$coordinates = VariationCoordinates::defaults($variations) + ->with('wght', 650) + ->with('wdth', 90); + +$selected = $font->withVariations($coordinates); +``` + +## Observable results + +Selected coordinates affect the data returned by `glyphMetrics()` and +`glyphOutline()` when the corresponding font tables provide variations. Alto +Font applies `avar`, `gvar`, and `HVAR` data for supported `glyf`-based fonts. + +The package resolves font data only. It does not select optical sizes from a +CSS context or shape a run of text at the chosen coordinates. diff --git a/src/Binary/BinaryReader.php b/src/Binary/BinaryReader.php index 2fd1da5..ec022c5 100644 --- a/src/Binary/BinaryReader.php +++ b/src/Binary/BinaryReader.php @@ -16,6 +16,9 @@ use Alto\Font\Exception\InvalidFontException; use Alto\Font\OpenType\Table\TableRecord; +/** + * @author Simon André + */ final readonly class BinaryReader { public function __construct(private string $data, private string $source) {} diff --git a/src/Descriptor/FontDescriptor.php b/src/Descriptor/FontDescriptor.php index 7dd319b..4e68349 100644 --- a/src/Descriptor/FontDescriptor.php +++ b/src/Descriptor/FontDescriptor.php @@ -15,6 +15,9 @@ use Alto\Font\FontFace; +/** + * @author Simon André + */ final readonly class FontDescriptor { public function __construct( diff --git a/src/Descriptor/FontStretch.php b/src/Descriptor/FontStretch.php index e1b8395..624a1ba 100644 --- a/src/Descriptor/FontStretch.php +++ b/src/Descriptor/FontStretch.php @@ -13,6 +13,9 @@ namespace Alto\Font\Descriptor; +/** + * @author Simon André + */ final readonly class FontStretch { public function __construct(public int $percentage) {} diff --git a/src/Descriptor/FontStyle.php b/src/Descriptor/FontStyle.php index e371be2..06ec807 100644 --- a/src/Descriptor/FontStyle.php +++ b/src/Descriptor/FontStyle.php @@ -13,6 +13,9 @@ namespace Alto\Font\Descriptor; +/** + * @author Simon André + */ enum FontStyle: string { case Normal = 'normal'; diff --git a/src/Descriptor/FontWeight.php b/src/Descriptor/FontWeight.php index 27c7e9e..c62c2f8 100644 --- a/src/Descriptor/FontWeight.php +++ b/src/Descriptor/FontWeight.php @@ -13,6 +13,9 @@ namespace Alto\Font\Descriptor; +/** + * @author Simon André + */ final readonly class FontWeight { public function __construct(public int $value) {} diff --git a/src/Exception/FontExceptionInterface.php b/src/Exception/FontExceptionInterface.php index b3f7a71..eb7fa4b 100644 --- a/src/Exception/FontExceptionInterface.php +++ b/src/Exception/FontExceptionInterface.php @@ -13,4 +13,7 @@ namespace Alto\Font\Exception; +/** + * @author Simon André + */ interface FontExceptionInterface extends \Throwable {} diff --git a/src/Exception/FontNotFoundException.php b/src/Exception/FontNotFoundException.php index 9041973..c3ff1cb 100644 --- a/src/Exception/FontNotFoundException.php +++ b/src/Exception/FontNotFoundException.php @@ -13,4 +13,7 @@ namespace Alto\Font\Exception; +/** + * @author Simon André + */ final class FontNotFoundException extends \RuntimeException implements FontExceptionInterface {} diff --git a/src/Exception/InvalidFontException.php b/src/Exception/InvalidFontException.php index ed83983..1e58425 100644 --- a/src/Exception/InvalidFontException.php +++ b/src/Exception/InvalidFontException.php @@ -13,4 +13,7 @@ namespace Alto\Font\Exception; +/** + * @author Simon André + */ final class InvalidFontException extends \RuntimeException implements FontExceptionInterface {} diff --git a/src/Exception/UnsupportedFontException.php b/src/Exception/UnsupportedFontException.php index 69e18a1..ad73ee3 100644 --- a/src/Exception/UnsupportedFontException.php +++ b/src/Exception/UnsupportedFontException.php @@ -13,4 +13,7 @@ namespace Alto\Font\Exception; +/** + * @author Simon André + */ final class UnsupportedFontException extends \RuntimeException implements FontExceptionInterface {} diff --git a/src/Font.php b/src/Font.php index e77e965..39445a0 100644 --- a/src/Font.php +++ b/src/Font.php @@ -25,6 +25,9 @@ use Alto\Font\Variation\FontVariations; use Alto\Font\Variation\VariationCoordinates; +/** + * @author Simon André + */ final readonly class Font { public function __construct( diff --git a/src/FontFace.php b/src/FontFace.php index 813eb13..a9a784d 100644 --- a/src/FontFace.php +++ b/src/FontFace.php @@ -13,6 +13,9 @@ namespace Alto\Font; +/** + * @author Simon André + */ final readonly class FontFace { /** diff --git a/src/FontFinder.php b/src/FontFinder.php index 1a5168d..2bb8128 100644 --- a/src/FontFinder.php +++ b/src/FontFinder.php @@ -22,6 +22,9 @@ use Alto\Font\Locator\FontLocatorInterface; use Alto\Font\Metadata\FontMetadata; +/** + * @author Simon André + */ final class FontFinder { /** diff --git a/src/FontQuery.php b/src/FontQuery.php index aeb1b9c..ba57315 100644 --- a/src/FontQuery.php +++ b/src/FontQuery.php @@ -17,6 +17,9 @@ use Alto\Font\Descriptor\FontStyle; use Alto\Font\Descriptor\FontWeight; +/** + * @author Simon André + */ final readonly class FontQuery { public function __construct( diff --git a/src/Glyph/Contour.php b/src/Glyph/Contour.php index b163224..5368ead 100644 --- a/src/Glyph/Contour.php +++ b/src/Glyph/Contour.php @@ -13,6 +13,9 @@ namespace Alto\Font\Glyph; +/** + * @author Simon André + */ final readonly class Contour { /** diff --git a/src/Glyph/GlyphId.php b/src/Glyph/GlyphId.php index 6a372e3..4089fbc 100644 --- a/src/Glyph/GlyphId.php +++ b/src/Glyph/GlyphId.php @@ -15,6 +15,9 @@ use Alto\Font\Exception\InvalidFontException; +/** + * @author Simon André + */ final readonly class GlyphId { public function __construct(public int $value) diff --git a/src/Glyph/GlyphMetrics.php b/src/Glyph/GlyphMetrics.php index b1b39d6..ea04a7d 100644 --- a/src/Glyph/GlyphMetrics.php +++ b/src/Glyph/GlyphMetrics.php @@ -13,6 +13,9 @@ namespace Alto\Font\Glyph; +/** + * @author Simon André + */ final readonly class GlyphMetrics { public function __construct( diff --git a/src/Glyph/GlyphOutline.php b/src/Glyph/GlyphOutline.php index ee951ac..0a66b8f 100644 --- a/src/Glyph/GlyphOutline.php +++ b/src/Glyph/GlyphOutline.php @@ -13,6 +13,9 @@ namespace Alto\Font\Glyph; +/** + * @author Simon André + */ final readonly class GlyphOutline { /** diff --git a/src/Glyph/GlyphPoint.php b/src/Glyph/GlyphPoint.php index 5d57e4d..2de5286 100644 --- a/src/Glyph/GlyphPoint.php +++ b/src/Glyph/GlyphPoint.php @@ -13,6 +13,9 @@ namespace Alto\Font\Glyph; +/** + * @author Simon André + */ final readonly class GlyphPoint { public function __construct( diff --git a/src/Glyph/PathCommand.php b/src/Glyph/PathCommand.php index 2ed5ba4..65fd2fe 100644 --- a/src/Glyph/PathCommand.php +++ b/src/Glyph/PathCommand.php @@ -13,6 +13,9 @@ namespace Alto\Font\Glyph; +/** + * @author Simon André + */ final readonly class PathCommand { /** diff --git a/src/Loader/FontLoader.php b/src/Loader/FontLoader.php index d607f90..ab649bd 100644 --- a/src/Loader/FontLoader.php +++ b/src/Loader/FontLoader.php @@ -16,6 +16,9 @@ use Alto\Font\Font; use Alto\Font\OpenType\SfntFont; +/** + * @author Simon André + */ final readonly class FontLoader implements FontLoaderInterface { public function load(string|\Stringable $file, int $faceIndex = 0): Font diff --git a/src/Loader/FontLoaderInterface.php b/src/Loader/FontLoaderInterface.php index 0b41a2b..206da07 100644 --- a/src/Loader/FontLoaderInterface.php +++ b/src/Loader/FontLoaderInterface.php @@ -15,6 +15,9 @@ use Alto\Font\Font; +/** + * @author Simon André + */ interface FontLoaderInterface { public function load(string|\Stringable $file, int $faceIndex = 0): Font; diff --git a/src/Locator/FontLocator.php b/src/Locator/FontLocator.php index 8f8090f..4f5c077 100644 --- a/src/Locator/FontLocator.php +++ b/src/Locator/FontLocator.php @@ -18,6 +18,9 @@ * of directories - explicit ones, the OS-standard system font directories, or * both combined. Skips unreadable subdirectories rather than failing. */ +/** + * @author Simon André + */ final readonly class FontLocator implements FontLocatorInterface { private const array EXTENSIONS = ['ttf', 'otf', 'woff', 'woff2', 'ttc', 'otc']; diff --git a/src/Locator/FontLocatorInterface.php b/src/Locator/FontLocatorInterface.php index 04c6a54..2da6927 100644 --- a/src/Locator/FontLocatorInterface.php +++ b/src/Locator/FontLocatorInterface.php @@ -13,6 +13,9 @@ namespace Alto\Font\Locator; +/** + * @author Simon André + */ interface FontLocatorInterface { /** diff --git a/src/Metadata/FontFormat.php b/src/Metadata/FontFormat.php index 3a8c4e4..f252d27 100644 --- a/src/Metadata/FontFormat.php +++ b/src/Metadata/FontFormat.php @@ -13,6 +13,9 @@ namespace Alto\Font\Metadata; +/** + * @author Simon André + */ enum FontFormat: string { case TrueType = 'truetype'; diff --git a/src/Metadata/FontMetadata.php b/src/Metadata/FontMetadata.php index ef9f33a..38bc021 100644 --- a/src/Metadata/FontMetadata.php +++ b/src/Metadata/FontMetadata.php @@ -16,6 +16,9 @@ use Alto\Font\Descriptor\FontDescriptor; use Alto\Font\FontFace; +/** + * @author Simon André + */ final readonly class FontMetadata { public function __construct( diff --git a/src/OpenType/SfntFont.php b/src/OpenType/SfntFont.php index 3d8b187..9544ab7 100644 --- a/src/OpenType/SfntFont.php +++ b/src/OpenType/SfntFont.php @@ -34,6 +34,9 @@ use Alto\Font\Variation\Table\HvarTable; use Alto\Font\Variation\VariationCoordinates; +/** + * @author Simon André + */ final class SfntFont { private const int ARG_1_AND_2_ARE_WORDS = 0x0001; @@ -694,7 +697,10 @@ private function simpleGlyphVariationDeltas( } } - return [\array_slice($xDeltas, 0, $pointCount), \array_slice($yDeltas, 0, $pointCount)]; + return [ + array_values(\array_slice($xDeltas, 0, $pointCount)), + array_values(\array_slice($yDeltas, 0, $pointCount)), + ]; } /** diff --git a/src/OpenType/Table/CmapTable.php b/src/OpenType/Table/CmapTable.php index 66c71f2..16eb109 100644 --- a/src/OpenType/Table/CmapTable.php +++ b/src/OpenType/Table/CmapTable.php @@ -16,6 +16,9 @@ use Alto\Font\Binary\BinaryReader; use Alto\Font\Exception\InvalidFontException; +/** + * @author Simon André + */ final readonly class CmapTable { /** diff --git a/src/OpenType/Table/NameTable.php b/src/OpenType/Table/NameTable.php index 995bb86..2e2ca5f 100644 --- a/src/OpenType/Table/NameTable.php +++ b/src/OpenType/Table/NameTable.php @@ -15,6 +15,9 @@ use Alto\Font\Binary\BinaryReader; +/** + * @author Simon André + */ final class NameTable { /** diff --git a/src/OpenType/Table/TableRecord.php b/src/OpenType/Table/TableRecord.php index 90d9627..1cb5f79 100644 --- a/src/OpenType/Table/TableRecord.php +++ b/src/OpenType/Table/TableRecord.php @@ -13,6 +13,9 @@ namespace Alto\Font\OpenType\Table; +/** + * @author Simon André + */ final readonly class TableRecord { public function __construct( diff --git a/src/OpenType/Woff2Decoder.php b/src/OpenType/Woff2Decoder.php index 240a02d..ce4845c 100644 --- a/src/OpenType/Woff2Decoder.php +++ b/src/OpenType/Woff2Decoder.php @@ -20,6 +20,9 @@ /** * @internal */ +/** + * @author Simon André + */ final class Woff2Decoder { private const array KNOWN_TAGS = [ @@ -799,12 +802,23 @@ private static function brotliDecompress(string $compressedData): string throw new UnsupportedFontException('WOFF2 Brotli decompression requires ext-brotli or the brotli binary.'); } - fwrite($pipes[0], $compressedData); - fclose($pipes[0]); - $decompressed = stream_get_contents($pipes[1]); - $error = stream_get_contents($pipes[2]); - fclose($pipes[1]); - fclose($pipes[2]); + $pipeResources = (array) $pipes; + + if (!isset($pipeResources[0], $pipeResources[1], $pipeResources[2]) + || !\is_resource($pipeResources[0]) + || !\is_resource($pipeResources[1]) + || !\is_resource($pipeResources[2])) { + proc_close($process); + + throw new InvalidFontException('WOFF2 Brotli decompression failed to open process pipes.'); + } + + fwrite($pipeResources[0], $compressedData); + fclose($pipeResources[0]); + $decompressed = stream_get_contents($pipeResources[1]); + $error = stream_get_contents($pipeResources[2]); + fclose($pipeResources[1]); + fclose($pipeResources[2]); $exitCode = proc_close($process); if (0 !== $exitCode || !\is_string($decompressed)) { diff --git a/src/Text/UnicodeString.php b/src/Text/UnicodeString.php index 50935c9..881fde2 100644 --- a/src/Text/UnicodeString.php +++ b/src/Text/UnicodeString.php @@ -15,6 +15,9 @@ use Alto\Font\Exception\InvalidFontException; +/** + * @author Simon André + */ final class UnicodeString { /** diff --git a/src/Variation/FontVariations.php b/src/Variation/FontVariations.php index 1af0151..e1bc699 100644 --- a/src/Variation/FontVariations.php +++ b/src/Variation/FontVariations.php @@ -15,6 +15,9 @@ use Alto\Font\Exception\InvalidFontException; +/** + * @author Simon André + */ final readonly class FontVariations { /** diff --git a/src/Variation/ItemStore/DeltaSetIndexMap.php b/src/Variation/ItemStore/DeltaSetIndexMap.php index 0cd1442..057ffe3 100644 --- a/src/Variation/ItemStore/DeltaSetIndexMap.php +++ b/src/Variation/ItemStore/DeltaSetIndexMap.php @@ -16,6 +16,9 @@ use Alto\Font\Binary\BinaryReader; use Alto\Font\Exception\InvalidFontException; +/** + * @author Simon André + */ final readonly class DeltaSetIndexMap { private const int INNER_INDEX_BIT_COUNT_MASK = 0x0F; diff --git a/src/Variation/ItemStore/ItemVariationStore.php b/src/Variation/ItemStore/ItemVariationStore.php index b76d4f6..bfa6052 100644 --- a/src/Variation/ItemStore/ItemVariationStore.php +++ b/src/Variation/ItemStore/ItemVariationStore.php @@ -18,6 +18,9 @@ use Alto\Font\Variation\FontVariations; use Alto\Font\Variation\NormalizedCoordinates; +/** + * @author Simon André + */ final readonly class ItemVariationStore { private const int LONG_WORDS = 0x8000; diff --git a/src/Variation/ItemStore/TupleRegion.php b/src/Variation/ItemStore/TupleRegion.php index 17a5c06..6b19320 100644 --- a/src/Variation/ItemStore/TupleRegion.php +++ b/src/Variation/ItemStore/TupleRegion.php @@ -16,6 +16,9 @@ use Alto\Font\Variation\FontVariations; use Alto\Font\Variation\NormalizedCoordinates; +/** + * @author Simon André + */ final readonly class TupleRegion { /** diff --git a/src/Variation/ItemStore/TupleVariation.php b/src/Variation/ItemStore/TupleVariation.php index 6dc9cb4..c55f79d 100644 --- a/src/Variation/ItemStore/TupleVariation.php +++ b/src/Variation/ItemStore/TupleVariation.php @@ -17,6 +17,9 @@ use Alto\Font\Variation\NormalizedCoordinates; use Alto\Font\Variation\VariationDeltas; +/** + * @author Simon André + */ final readonly class TupleVariation { /** diff --git a/src/Variation/NormalizedCoordinates.php b/src/Variation/NormalizedCoordinates.php index 6e39baa..19b9a5a 100644 --- a/src/Variation/NormalizedCoordinates.php +++ b/src/Variation/NormalizedCoordinates.php @@ -13,6 +13,9 @@ namespace Alto\Font\Variation; +/** + * @author Simon André + */ final readonly class NormalizedCoordinates { /** diff --git a/src/Variation/Table/AvarTable.php b/src/Variation/Table/AvarTable.php index cee2114..6f536d4 100644 --- a/src/Variation/Table/AvarTable.php +++ b/src/Variation/Table/AvarTable.php @@ -17,6 +17,9 @@ use Alto\Font\Exception\InvalidFontException; use Alto\Font\Variation\FontVariations; +/** + * @author Simon André + */ final readonly class AvarTable { /** diff --git a/src/Variation/Table/FvarTable.php b/src/Variation/Table/FvarTable.php index 2e89e91..6df9582 100644 --- a/src/Variation/Table/FvarTable.php +++ b/src/Variation/Table/FvarTable.php @@ -22,6 +22,9 @@ /** * @internal */ +/** + * @author Simon André + */ final class FvarTable { /** diff --git a/src/Variation/Table/GvarTable.php b/src/Variation/Table/GvarTable.php index f1209f3..bea4c2a 100644 --- a/src/Variation/Table/GvarTable.php +++ b/src/Variation/Table/GvarTable.php @@ -21,6 +21,9 @@ use Alto\Font\Variation\NormalizedCoordinates; use Alto\Font\Variation\VariationDeltas; +/** + * @author Simon André + */ final readonly class GvarTable { private const int LONG_OFFSETS = 0x0001; diff --git a/src/Variation/Table/HvarTable.php b/src/Variation/Table/HvarTable.php index 273a369..5923be7 100644 --- a/src/Variation/Table/HvarTable.php +++ b/src/Variation/Table/HvarTable.php @@ -20,6 +20,9 @@ use Alto\Font\Variation\ItemStore\ItemVariationStore; use Alto\Font\Variation\NormalizedCoordinates; +/** + * @author Simon André + */ final readonly class HvarTable { private function __construct( diff --git a/src/Variation/VariationAxis.php b/src/Variation/VariationAxis.php index 2275ff7..a0f9f6b 100644 --- a/src/Variation/VariationAxis.php +++ b/src/Variation/VariationAxis.php @@ -13,6 +13,9 @@ namespace Alto\Font\Variation; +/** + * @author Simon André + */ final readonly class VariationAxis { public const int HIDDEN_AXIS = 0x0001; diff --git a/src/Variation/VariationCoordinates.php b/src/Variation/VariationCoordinates.php index 9688b27..8c43ad8 100644 --- a/src/Variation/VariationCoordinates.php +++ b/src/Variation/VariationCoordinates.php @@ -15,6 +15,9 @@ use Alto\Font\Variation\Table\AvarTable; +/** + * @author Simon André + */ final readonly class VariationCoordinates { /** diff --git a/src/Variation/VariationDeltas.php b/src/Variation/VariationDeltas.php index cca2c31..6e67e5e 100644 --- a/src/Variation/VariationDeltas.php +++ b/src/Variation/VariationDeltas.php @@ -13,6 +13,9 @@ namespace Alto\Font\Variation; +/** + * @author Simon André + */ final readonly class VariationDeltas { /** diff --git a/src/Variation/VariationInstance.php b/src/Variation/VariationInstance.php index 0f66c09..80a2f98 100644 --- a/src/Variation/VariationInstance.php +++ b/src/Variation/VariationInstance.php @@ -13,6 +13,9 @@ namespace Alto\Font\Variation; +/** + * @author Simon André + */ final readonly class VariationInstance { /** diff --git a/tests/OpenType/SfntFontTest.php b/tests/OpenType/SfntFontTest.php index 54e98a4..0456c72 100644 --- a/tests/OpenType/SfntFontTest.php +++ b/tests/OpenType/SfntFontTest.php @@ -29,7 +29,6 @@ use PHPUnit\Framework\TestCase; #[CoversClass(SfntFont::class)] -#[CoversClass(Woff2Decoder::class)] final class SfntFontTest extends TestCase { use ContourAssertions;