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
8 changes: 8 additions & 0 deletions src/Fonts/BlobReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ public function __construct($bytes)
$this->bytes = $bytes;
}

/**
* @return int How many bytes there are to read
*/
public function length()
{
return strlen($this->bytes);
}

public function read($length)
{
$data = substr($this->bytes, $this->pos, $length);
Expand Down
9 changes: 4 additions & 5 deletions src/Fonts/Color/BitmapSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,12 @@ abstract class BitmapSource implements ColorGlyphSource
protected $scale;

/**
* @param FileReader $reader The font file
* @param LoggerInterface $logger Told of a glyph the format has a bitmap for that cannot be drawn
* @param ColorFontFile $file The font
*/
public function __construct(FileReader $reader, LoggerInterface $logger)
public function __construct(ColorFontFile $file)
{
$this->reader = $reader;
$this->logger = $logger;
$this->reader = $file->reader;
$this->logger = $file->logger;
}

/**
Expand Down
19 changes: 7 additions & 12 deletions src/Fonts/Color/CbdtSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

namespace Mpdf\Fonts\Color;

use Mpdf\Fonts\FileReader;
use Mpdf\Log\Context as LogContext;
use Mpdf\TTFontFile;
use Psr\Log\LoggerInterface;

/**
* Colour glyphs as PNG bitmaps, in CBDT with CBLC to index them: Noto Color Emoji's format.
Expand Down Expand Up @@ -39,16 +36,14 @@ class CbdtSource extends BitmapSource
private $subtables = [];

/**
* @param TTFontFile $font The font, its table directory read
* @param FileReader $reader The font file
* @param int $unitsPerEm
* @param LoggerInterface $logger Told of a bitmap in an image format mPDF does not draw
* @param ColorFontFile $file The font, whose logger is told of a bitmap in an image format mPDF does
* not draw
*/
public function __construct(TTFontFile $font, FileReader $reader, $unitsPerEm, LoggerInterface $logger)
public function __construct(ColorFontFile $file)
{
parent::__construct($reader, $logger);
$cblc = $font->getTablePosition('CBLC')[0];
list($cbdt, $cbdtLength) = $font->getTablePosition('CBDT');
parent::__construct($file);
$cblc = $file->table('CBLC')[0];
list($cbdt, $cbdtLength) = $file->table('CBDT');
$this->cbdtEnd = $cbdt + $cbdtLength;

$header = $this->reader->fieldsAt($cblc + 4, 4, 'N');
Expand All @@ -74,7 +69,7 @@ public function __construct(TTFontFile $font, FileReader $reader, $unitsPerEm, L
}

list($ppem, $array, $subtableCount) = $strike;
$this->scale = $unitsPerEm / $ppem;
$this->scale = $file->unitsPerEm / $ppem;

for ($i = 0; $i < $subtableCount; $i++) {
// The array ends with the file, if not before
Expand Down
78 changes: 78 additions & 0 deletions src/Fonts/Color/ColorFontFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace Mpdf\Fonts\Color;

use Mpdf\Fonts\FileReader;
use Mpdf\Fonts\GlyphOutline;
use Mpdf\TTFontFile;
use Psr\Log\LoggerInterface;

/**
* An open font, as each ColorGlyphSource drawing it reads it: its tables, its file, the log a glyph
* that cannot be drawn is reported to, and its outlines, decoded once for every source that draws
* with them.
*/
class ColorFontFile
{

/**
* @var FileReader
*/
public $reader;

/**
* @var LoggerInterface Told of a glyph the font has that cannot be drawn
*/
public $logger;

/**
* @var int
*/
public $unitsPerEm;

/**
* @var TTFontFile
*/
private $font;

/**
* @var GlyphOutline|null
*/
private $outline;

/**
* @param TTFontFile $font The font, its table directory read
* @param FileReader $reader The font file
* @param int $unitsPerEm
* @param LoggerInterface $logger Told of a glyph the font has that cannot be drawn
*/
public function __construct(TTFontFile $font, FileReader $reader, $unitsPerEm, LoggerInterface $logger)
{
$this->font = $font;
$this->reader = $reader;
$this->unitsPerEm = $unitsPerEm;
$this->logger = $logger;
}

/**
* @param string $tag
*
* @return int[] [where the table starts, its length], each 0 where the font has none
*/
public function table($tag)
{
return $this->font->getTablePosition($tag);
}

/**
* @return GlyphOutline
*/
public function outline()
{
if ($this->outline === null) {
$this->outline = new GlyphOutline($this->font, $this->reader, $this->logger);
}

return $this->outline;
}
}
46 changes: 24 additions & 22 deletions src/Fonts/Color/ColorFormats.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

namespace Mpdf\Fonts\Color;

use Mpdf\Fonts\FileReader;
use Mpdf\Mpdf;
use Mpdf\TTFontFile;
use Psr\Log\LoggerInterface;

/**
* The colour formats mPDF draws, and which of a font's it draws the font in.
Expand All @@ -18,6 +15,7 @@ class ColorFormats
* carries several is drawn in
*/
const SOURCES = [
'COLRv0' => 'Mpdf\Fonts\Color\ColrV0Source',
'CBDT' => 'Mpdf\Fonts\Color\CbdtSource',
'sbix' => 'Mpdf\Fonts\Color\SbixSource',
];
Expand Down Expand Up @@ -66,8 +64,28 @@ public static function drawn(array $font, Mpdf $mpdf)
}

/**
* Whether a font draws nothing in a document: its glyphs exist only in a colour format, and the
* document may not draw colour. Its characters are left blank, unless a backup font has them.
* What draws a font's glyphs in a document, each glyph by the first that has it: the format it is
* drawn in, where the document may draw colour, then its outlines, where it has any
*
* @param array $font The font, as Mpdf::$fonts holds it
* @param Mpdf $mpdf The document
*
* @return string[] The ColorGlyphSource classes
*/
public static function sources(array $font, Mpdf $mpdf)
{
$format = self::drawn($font, $mpdf);
$sources = $format === '' ? [] : [self::SOURCES[$format]];
if (!empty($font['hasOutlines'])) {
$sources[] = 'Mpdf\Fonts\Color\OutlineSource';
}

return $sources;
}

/**
* Whether a font written as Type3 draws nothing in a document: nothing draws its glyphs - see
* sources() - so its characters are left blank, unless a backup font has them.
*
* @param array $font The font, as Mpdf::$fonts holds it
* @param Mpdf $mpdf The document
Expand All @@ -76,7 +94,7 @@ public static function drawn(array $font, Mpdf $mpdf)
*/
public static function blank(array $font, Mpdf $mpdf)
{
return !empty($font['colorFormats']) && self::drawable($font['colorFormats']) && !self::inColor($mpdf);
return !empty($font['colorFormats']) && self::drawable($font['colorFormats']) && !self::sources($font, $mpdf);
}

/**
Expand All @@ -93,20 +111,4 @@ public static function choose(array $fontFormats, $color)

return $formats ? reset($formats) : '';
}

/**
* @param string $format One of the keys of SOURCES
* @param TTFontFile $font The font, its table directory read
* @param FileReader $reader The font file
* @param int $unitsPerEm
* @param LoggerInterface $logger Told of a glyph the format has but mPDF cannot draw
*
* @return ColorGlyphSource
*/
public static function source($format, TTFontFile $font, FileReader $reader, $unitsPerEm, LoggerInterface $logger)
{
$class = self::SOURCES[$format];

return new $class($font, $reader, $unitsPerEm, $logger);
}
}
144 changes: 144 additions & 0 deletions src/Fonts/Color/ColrV0Source.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

namespace Mpdf\Fonts\Color;

use Mpdf\Fonts\GlyphOutline;

/**
* Colour glyphs as layers of outlines, in COLR version 0 with CPAL for their colours: Twemoji's
* format, and what a version 1 font carries for renderers that know only this.
*
* A colour glyph is a list of layers, each another glyph's outline filled in one colour of a
* palette, drawn bottom to top. A layer whose palette index is 0xFFFF is filled in the colour of the
* text, which is what a layer that sets no colour inside a Type3 glyph is drawn in, as is a layer
* naming a colour the palette does not have. The first palette is drawn.
*
* Every read is checked for coming up short - see FontReader::fieldsAt() - and a glyph whose layers
* run past the ones COLR counts draws nothing.
*
* @see https://learn.microsoft.com/en-us/typography/opentype/spec/colr
* @see https://learn.microsoft.com/en-us/typography/opentype/spec/cpal
*/
class ColrV0Source implements ColorGlyphSource
{

const FOREGROUND = 0xFFFF;

/**
* @var \Mpdf\Fonts\FileReader
*/
private $reader;

/**
* @var GlyphOutline
*/
private $outline;

/**
* @var int[][] Base glyph id => [its first layer record, how many layers]
*/
private $baseGlyphs = [];

/**
* @var int Where the layer records start
*/
private $layers = 0;

/**
* @var int How many layer records there are
*/
private $layerCount = 0;

/**
* @var int[][] The first palette, each colour as [red, green, blue, alpha] from 0 to 255
*/
private $palette = [];

/**
* @param ColorFontFile $file The font
*/
public function __construct(ColorFontFile $file)
{
$this->reader = $file->reader;
$this->outline = $file->outline();

// numBaseGlyphRecords, baseGlyphRecordsOffset, layerRecordsOffset, numLayerRecords
$colr = $file->table('COLR')[0];
$header = $this->reader->fieldsAt($colr + 2, 12, 'nbase/Nrecords/Nlayers/ncount');
if ($header === null) {
return;
}

list($baseCount, $records, $layers, $this->layerCount) = $header;
$this->layers = $colr + $layers;

// Each glyph id, its first layer and how many
$values = $this->reader->fieldsAt($colr + $records, $baseCount * 6, 'n*');
for ($i = 0; $values !== null && $i < 3 * $baseCount; $i += 3) {
$this->baseGlyphs[$values[$i]] = [$values[$i + 1], $values[$i + 2]];
}

$this->palette = $this->firstPalette($file->table('CPAL')[0]);
}

/**
* @inheritdoc
*/
public function draw($glyph, GlyphResources $resources)
{
if (!isset($this->baseGlyphs[$glyph])) {
return null;
}

list($first, $count) = $this->baseGlyphs[$glyph];
$records = $first + $count > $this->layerCount ? null : $this->reader->fieldsAt($this->layers + $first * 4, $count * 4, 'n*');
if ($records === null) {
return null;
}

$content = '';
foreach (array_chunk($records, 2) as $layer) {
list($layerGlyph, $index) = $layer;
$path = $this->outline->path($layerGlyph);
if ($path === '') {
continue;
}

if ($index === self::FOREGROUND || !isset($this->palette[$index])) {
$content .= $path . "f\n";
continue;
}

list($red, $green, $blue, $alpha) = $this->palette[$index];
$content .= sprintf("q %s%.3F %.3F %.3F rg\n%sf\nQ\n", $alpha < 255 ? $resources->alpha($alpha / 255) . ' ' : '', $red / 255, $green / 255, $blue / 255, $path);
}

return $content === '' ? null : $content;
}

/**
* @param int $cpal Where CPAL starts
*
* @return int[][] The first palette's colours, or none where CPAL has no palette or its first runs
* past the colour records
*/
private function firstPalette($cpal)
{
// numPaletteEntries, numPalettes, numColorRecords, colorRecordsArrayOffset, colorRecordIndices[0]
$header = $this->reader->fieldsAt($cpal + 2, 12, 'nentries/npalettes/nrecords/Ncolors/nfirst');
if ($header === null || $header[1] === 0 || $header[4] + $header[0] > $header[2]) {
return [];
}

list($entries, , , $colors, $first) = $header;

// Each colour is stored blue, green, red, alpha
$bgra = $this->reader->fieldsAt($cpal + $colors + $first * 4, $entries * 4, 'C*');
$palette = [];
for ($i = 0; $bgra !== null && $i < 4 * $entries; $i += 4) {
$palette[] = [$bgra[$i + 2], $bgra[$i + 1], $bgra[$i], $bgra[$i + 3]];
}

return $palette;
}
}
Loading
Loading