Initial commit

This commit is contained in:
2026-08-30 22:02:02 +00:00
commit b6bd2277f5
2334 changed files with 646393 additions and 0 deletions
+251
View File
@@ -0,0 +1,251 @@
<?php
/**
* BufferTest.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
/**
* Buffer Test
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @SuppressWarnings("PHPMD.LongVariable")
*/
class BufferTest extends TestUtil
{
public function testSubsetModeDisabledByDefault(): void
{
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$this->assertFalse($stack->isSubsetMode());
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testAddSubsetCharOnMissingFontThrows(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$stack->addSubsetChar('missing', 65);
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testStackMissingKey(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$stack->getFont('missing');
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testStackMissingFontName(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
$stack->add($objnum, '');
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testStackIFileMissing(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
$stack->add($objnum, 'something', '', '/missing/nothere.json');
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testStackIFileNotJson(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
$stack->add($objnum, 'something', '', __DIR__ . '/StackTest.php');
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testStackIFileWrongFormat(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
\file_put_contents($this->getFontPath() . 'badformat.json', '{"bad":"format"}');
$stack->add($objnum, 'something', '', $this->getFontPath() . 'badformat.json');
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testLoadDeafultWidthA(): void
{
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
\file_put_contents($this->getFontPath() . 'test.json', '{"type":"Type1","cw":{"0":100}}');
$stack->add($objnum, 'test', '', $this->getFontPath() . 'test.json');
$font = $stack->getFont('test');
$this->assertEquals(600, $font['dw']);
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testLoadDeafultWidthB(): void
{
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
\file_put_contents($this->getFontPath() . 'test.json', '{"type":"Type1","cw":{"32":123}}');
$stack->add($objnum, 'test', '', $this->getFontPath() . 'test.json');
$font = $stack->getFont('test');
$this->assertEquals(123, $font['dw']);
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testLoadDeafultWidthC(): void
{
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
\file_put_contents(
$this->getFontPath() . 'test.json',
'{"type":"Type1","desc":{"MissingWidth":234},"cw":{"0":600}}',
);
$stack->add($objnum, 'test', '', $this->getFontPath() . 'test.json');
$font = $stack->getFont('test');
$this->assertEquals(234, $font['dw']);
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testLoadWrongType(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
\file_put_contents($this->getFontPath() . 'test.json', '{"type":"WRONG","cw":{"0":600}}');
$stack->add($objnum, 'test', '', $this->getFontPath() . 'test.json');
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testLoadCidOnPdfa(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1, false, true, true);
$objnum = 1;
\file_put_contents($this->getFontPath() . 'test.json', '{"type":"cidfont0","cw":{"0":600}}');
$stack->add($objnum, 'test', '', $this->getFontPath() . 'test.json', false);
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testLoadArtificialStyles(): void
{
$this->setupTest();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
\file_put_contents(
$this->getFontPath() . 'test.json',
'{"type":"Core","cw":{"0":600},"mode":{"bold":true,"italic":true}}',
);
$key = $stack->add($objnum, 'symbol', '', $this->getFontPath() . 'test.json');
$this->assertNotEmpty($key);
}
/**
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Font\Exception
* @throws \RangeException
*/
public function testBuffer(): void
{
$this->setupTest();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(1, false, true, false);
new \Com\Tecnick\Pdf\Font\Import($indir . 'pdfa/pfb/PDFASymbol.pfb', '', 'Type1', 'symbol');
$stack->add($objnum, 'pdfasymbol');
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica.afm');
$stack->add($objnum, 'helvetica');
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica-Bold.afm');
$stack->add($objnum, 'helvetica', 'B');
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica-BoldOblique.afm');
$stack->add($objnum, 'helveticaBI');
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica-Oblique.afm');
$stack->add($objnum, 'helvetica', 'I');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$stack->add($objnum, 'freesans', '');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSansBold.ttf');
$stack->add($objnum, 'freesans', 'B');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSansOblique.ttf');
$stack->add($objnum, 'freesans', 'I');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSansBoldOblique.ttf');
$stack->add($objnum, 'freesans', 'BIUDO', '', true);
$fontkey = $stack->add($objnum, 'freesans', 'BI', '', true);
$this->assertEquals('freesansBI', $fontkey);
$this->assertEquals(10, $objnum);
$this->assertCount(9, $stack->getFonts());
$this->assertCount(1, $stack->getEncDiffs());
$font = $stack->getFont('freesansB');
$this->assertNotEmpty($font);
$this->assertEquals('FreeSansBold', $font['name']);
$this->assertEquals('TrueTypeUnicode', $font['type']);
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/ZapfDingbats.afm');
$stack->add($objnum, 'zapfdingbats', 'BIUDO');
$font = $stack->getFont('zapfdingbats');
$this->assertNotEmpty($font);
}
/**
* @throws \Com\Tecnick\File\Exception
* @throws \Com\Tecnick\Pdf\Font\Exception
* @throws \RangeException
*/
public function testBufferPdfa(): void
{
$this->setupTest();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(1, true, false, true);
new \Com\Tecnick\Pdf\Font\Import($indir . 'pdfa/pfb/PDFAHelveticaBoldOblique.pfb');
$stack->add($objnum, 'arial', 'BIUDO', '', true);
$font = $stack->getFont('pdfahelveticaBI');
$this->assertNotEmpty($font);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
/**
* FontTest.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
/**
* Font Test
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*/
class FontTest extends TestUtil
{
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testFontRejectsUnsafeInputDefinitionPath(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->setupTest();
new \Com\Tecnick\Pdf\Font\Font(
'helvetica',
'',
'../font.json',
false,
true,
false,
true,
new \Com\Tecnick\File\File(),
);
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
/**
* FontTypeTest.php
*
* @since 2026-07-17
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
use Com\Tecnick\Pdf\Font\Exception as FontException;
use Com\Tecnick\Pdf\Font\FontType;
use Com\Tecnick\Pdf\Font\Import;
/**
* FontType enum test
*
* @since 2026-07-17
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*/
class FontTypeTest extends TestUtil
{
public function testCaseBackingValues(): void
{
$this->assertSame('', FontType::Auto->value);
$this->assertSame('Core', FontType::Core->value);
$this->assertSame('TrueType', FontType::TrueType->value);
$this->assertSame('TrueTypeUnicode', FontType::TrueTypeUnicode->value);
$this->assertSame('Type1', FontType::Type1->value);
$this->assertSame('CID0JP', FontType::Cid0Jp->value);
$this->assertSame('CID0KR', FontType::Cid0Kr->value);
$this->assertSame('CID0CS', FontType::Cid0Cs->value);
$this->assertSame('CID0CT', FontType::Cid0Ct->value);
}
/**
* @throws FontException
*/
public function testFromLooseCanonical(): void
{
$this->assertSame(FontType::Auto, FontType::fromLoose(''));
$this->assertSame(FontType::TrueTypeUnicode, FontType::fromLoose('TrueTypeUnicode'));
$this->assertSame(FontType::Cid0Jp, FontType::fromLoose('CID0JP'));
}
/**
* @throws FontException
*/
public function testFromLoosePassesThroughEnumInstance(): void
{
$this->assertSame(FontType::Type1, FontType::fromLoose(FontType::Type1));
}
/**
* @throws FontException
*/
public function testFromLooseRoundTrip(): void
{
foreach (FontType::cases() as $case) {
$this->assertSame($case, FontType::fromLoose($case->value));
}
}
/**
* Font type names are case sensitive, so a wrong-case value is unknown.
*
* @throws FontException
*/
public function testFromLooseIsCaseSensitive(): void
{
$this->bcExpectException(FontException::class);
FontType::fromLoose('type1');
}
/**
* @throws FontException
*/
public function testFromLooseUnknownThrows(): void
{
$this->bcExpectException(FontException::class);
FontType::fromLoose('OpenType');
}
/**
* The widened Import constructor accepts a FontType enum and resolves it the
* same way as the equivalent legacy string.
*
* @throws FontException
* @throws \Com\Tecnick\File\Exception
* @throws \RangeException
*/
public function testImportAcceptsEnum(): void
{
$fin = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/core/Helvetica.afm';
$outdir = \dirname(__DIR__) . '/target/tmptest/fonttype/';
\system('rm -rf ' . $outdir . ' && mkdir -p ' . $outdir);
$import = new Import($fin, $outdir, FontType::Core);
$this->assertNotSame('', $import->getFontName());
}
}
@@ -0,0 +1,271 @@
<?php
/**
* CoreMetricsTest.php
*
* @since 2026-05-05
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test\Import;
use Com\Tecnick\Pdf\Font\Exception as FontException;
use Com\Tecnick\Pdf\Font\Import\Core;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
/**
* Verifies that Import\Core correctly maps AFM widths to WinAnsi byte positions (cw)
* and to Unicode codepoints (cwu) for Helvetica.
*
* @phpstan-import-type TFontData from \Com\Tecnick\Pdf\Font\Load
*/
class CoreMetricsTest extends TestCase
{
private const HELVETICA_AFM = __DIR__ . '/../../util/vendor/tecnickcom/tc-font-mirror/core/Helvetica.afm';
/** @var TFontData */
private static array $fdtTemplate = [
'Ascender' => 0,
'Ascent' => 0,
'AvgWidth' => 0.0,
'CapHeight' => 0,
'CharacterSet' => '',
'Descender' => 0,
'Descent' => 0,
'EncodingScheme' => '',
'FamilyName' => '',
'Flags' => 0,
'FontBBox' => [],
'FontName' => '',
'FullName' => '',
'IsFixedPitch' => false,
'ItalicAngle' => 0,
'Leading' => 0,
'MaxWidth' => 0,
'MissingWidth' => 0,
'StdHW' => 0,
'StdVW' => 0,
'StemH' => 0,
'StemV' => 0,
'UnderlinePosition' => 0,
'UnderlineThickness' => 0,
'Version' => '',
'Weight' => '',
'XHeight' => 0,
'bbox' => '',
'cbbox' => [],
'cidinfo' => ['Ordering' => '', 'Registry' => '', 'Supplement' => 0, 'uni2cid' => []],
'compress' => false,
'ctg' => '',
'ctgdata' => [],
'cw' => [],
'cwu' => [],
'datafile' => '',
'desc' => [
'Ascent' => 0,
'AvgWidth' => 0,
'CapHeight' => 0,
'Descent' => 0,
'Flags' => 0,
'FontBBox' => '',
'ItalicAngle' => 0,
'Leading' => 0,
'MaxWidth' => 0,
'MissingWidth' => 0,
'StemH' => 0,
'StemV' => 0,
'XHeight' => 0,
],
'diff' => '',
'diff_n' => 0,
'dir' => '',
'dw' => 0,
'enc' => '',
'enc_map' => [],
'encodingTables' => [],
'encoding_id' => 0,
'encrypted' => '',
'fakestyle' => false,
'family' => '',
'file' => '',
'file_n' => 0,
'file_name' => '',
'i' => 0,
'ifile' => '',
'indexToLoc' => [],
'input_file' => '',
'isUnicode' => false,
'italicAngle' => 0,
'key' => '',
'lenIV' => 0,
'length1' => 0,
'length2' => 0,
'linked' => false,
'mode' => [
'bold' => false,
'italic' => false,
'linethrough' => false,
'overline' => false,
'underline' => false,
],
'n' => 0,
'name' => '',
'numGlyphs' => 0,
'numHMetrics' => 0,
'originalsize' => 0,
'pdfa' => false,
'platform_id' => 0,
'settype' => '',
'short_offset' => false,
'size1' => 0,
'size2' => 0,
'style' => '',
'subset' => false,
'subsetchars' => [],
'table' => [],
'tot_num_glyphs' => 0,
'type' => '',
'underlinePosition' => 0,
'underlineThickness' => 0,
'unicode' => false,
'unitsPerEm' => 0,
'up' => 0,
'urk' => 0.0,
'ut' => 0,
'weight' => '',
];
/** @return array<string, array{int, int}> */
public static function provideHelveticaWinAnsiWidths(): array
{
return [
// [ winansi_cid, expected_width ]
// Printable ASCII — unchanged by fix, sanity checks
'space' => [0x20, 278],
'A' => [0x41, 667],
'hyphen' => [0x2D, 333],
// WinAnsi 0x800x9F range (formerly wrong/missing)
'Euro' => [0x80, 556],
'quotesinglbase' => [0x82, 222],
'florin' => [0x83, 556],
'quotedblbase' => [0x84, 333],
'ellipsis' => [0x85, 1000],
'dagger' => [0x86, 556],
'daggerdbl' => [0x87, 556],
'circumflex' => [0x88, 333],
'perthousand' => [0x89, 1000],
'Scaron' => [0x8A, 667],
'guilsinglleft' => [0x8B, 333],
'OE' => [0x8C, 1000],
'Zcaron' => [0x8E, 611],
'quoteleft' => [0x91, 222],
'quoteright' => [0x92, 222],
'quotedblleft' => [0x93, 333],
'quotedblright' => [0x94, 333],
'bullet' => [0x95, 350],
'endash' => [0x96, 556],
'emdash' => [0x97, 1000],
'tilde' => [0x98, 333],
'trademark' => [0x99, 1000],
'scaron' => [0x9A, 500],
'guilsinglright' => [0x9B, 333],
'oe' => [0x9C, 944],
'zcaron' => [0x9E, 500],
'Ydieresis' => [0x9F, 667],
];
}
/** @throws FontException */
#[DataProvider('provideHelveticaWinAnsiWidths')]
public function testHelveticaWinAnsiWidth(int $cid, int $expected): void
{
$fdt = $this->getHelveticaMetrics();
$this->assertSame($expected, $fdt['cw'][$cid] ?? null, 'cw[0x' . \dechex($cid) . ']');
}
/** @return array<string, array{int, int}> */
public static function provideHelveticaUnicodeWidths(): array
{
return [
// [ unicode_codepoint, expected_width ]
'emdash' => [0x2014, 1000],
'endash' => [0x2013, 556],
'quoteleft' => [0x2018, 222],
'quoteright' => [0x2019, 222],
'quotedblleft' => [0x201C, 333],
'quotedblright' => [0x201D, 333],
'bullet' => [0x2022, 350],
'ellipsis' => [0x2026, 1000],
'trademark' => [0x2122, 1000],
'Euro' => [0x20AC, 556],
'OE' => [0x0152, 1000],
'oe' => [0x0153, 944],
'Scaron' => [0x0160, 667],
'scaron' => [0x0161, 500],
'Ydieresis' => [0x0178, 667],
'Zcaron' => [0x017D, 611],
'zcaron' => [0x017E, 500],
// fi/fl are not WinAnsi-encoded but must appear in cwu
'fi' => [0xFB01, 500],
'fl' => [0xFB02, 500],
];
}
/** @throws FontException */
#[DataProvider('provideHelveticaUnicodeWidths')]
public function testHelveticaUnicodeWidth(int $codepoint, int $expected): void
{
$fdt = $this->getHelveticaMetrics();
$this->assertArrayHasKey('cwu', $fdt);
$this->assertSame(
$expected,
$fdt['cwu'][$codepoint] ?? null,
'cwu[U+' . \strtoupper(\dechex($codepoint)) . ']',
);
}
/** @throws FontException */
public function testGetFontMetricsHandlesAfmWithoutCharMetrics(): void
{
// an AFM with zero "C" lines must not trigger a DivisionByZeroError in setCharWidths
$afm =
"StartFontMetrics 4.1\n"
. "FontName TestEmpty\n"
. "FullName Test Empty\n"
. "FontBBox 0 -200 1000 700\n"
. "CapHeight 700\n"
. "Ascender 700\n"
. "Descender -200\n"
. "StartCharMetrics 0\n"
. "EndCharMetrics\n"
. "EndFontMetrics\n";
$core = new Core($afm, self::$fdtTemplate, new \Com\Tecnick\File\File());
$fdt = $core->getFontMetrics();
$this->assertSame(0, $fdt['AvgWidth']);
}
/**
* @return TFontData
*
* @throws FontException
*/
private function getHelveticaMetrics(): array
{
$content = \file_get_contents(self::HELVETICA_AFM);
$this->assertIsString($content);
$core = new Core($content, self::$fdtTemplate, new \Com\Tecnick\File\File());
return $core->getFontMetrics();
}
}
@@ -0,0 +1,162 @@
<?php
/**
* ImportInternalsTest.php
*
* @since 2026-05-05
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
use Com\Tecnick\Pdf\Font\Import;
/**
* Tests for protected methods of Import that cannot be reached through the
* normal public constructor path in isolation.
*
* @since 2026-05-05
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*/
class ImportInternalsTest extends TestUtil
{
private function buildImport(): Import
{
$class = new \ReflectionClass(Import::class);
return $class->newInstanceWithoutConstructor();
}
/**
* @param array<int, mixed> $args
*/
private function callStringMethod(object $obj, string $method, array $args = []): string
{
$ref = new \ReflectionMethod($obj, $method);
if (!\is_string($ref->invokeArgs($obj, $args))) {
$this->fail('Expected string return value.');
}
return (string) $ref->invokeArgs($obj, $args);
}
private function setFdt(object $obj, mixed $fdt): void
{
$prop = new \ReflectionProperty($obj, 'fdt');
$prop->setValue($obj, $fdt);
}
// -------------------------------------------------------------------------
// updateCIDtoGIDmap
// -------------------------------------------------------------------------
public function testUpdateCIDtoGIDmapSetsGlyphPairBytes(): void
{
$instance = $this->buildImport();
// 65536 CID slots × 2 bytes = 131072 bytes
$map = str_repeat("\x00", 131072);
$result = $this->callStringMethod($instance, 'updateCIDtoGIDmap', [$map, 65, 42]);
// gid 42 = 0x002A → high byte = 0x00, low byte = 0x2A
$this->assertSame(0, ord($result[65 * 2]));
$this->assertSame(42, ord($result[(65 * 2) + 1]));
}
public function testUpdateCIDtoGIDmapIgnoresCidOutOfRange(): void
{
$instance = $this->buildImport();
$map = str_repeat("\x00", 131072);
$result = $this->callStringMethod($instance, 'updateCIDtoGIDmap', [$map, 0x10000, 5]);
// CID 0x10000 is out of the 0..0xFFFF range → map unchanged
$this->assertSame($map, $result);
}
public function testUpdateCIDtoGIDmapIgnoresGidAbove0xffff(): void
{
$instance = $this->buildImport();
$map = str_repeat("\x00", 131072);
// gid = 0x1002A is outside the representable 16-bit range → entry left as 0 (notdef)
$result = $this->callStringMethod($instance, 'updateCIDtoGIDmap', [$map, 0, 0x1002A]);
$this->assertSame(0, ord($result[0]));
$this->assertSame(0, ord($result[1]));
}
public function testUpdateCIDtoGIDmapIgnoresNegativeGid(): void
{
$instance = $this->buildImport();
$map = str_repeat("\x00", 131072);
// gid < 0 → condition ($gid >= 0) is false → map unchanged
$result = $this->callStringMethod($instance, 'updateCIDtoGIDmap', [$map, 10, -1]);
$this->assertSame($map, $result);
}
// -------------------------------------------------------------------------
// getEncodingTable
// -------------------------------------------------------------------------
public function testGetEncodingTableReturnsCp1252ForType1NonSymbolic(): void
{
$instance = $this->buildImport();
$this->setFdt($instance, ['type' => 'Type1', 'Flags' => 32]);
// Flags & 4 == 0 → non-symbolic Type1 → cp1252
$result = $this->callStringMethod($instance, 'getEncodingTable', ['']);
$this->assertSame('cp1252', $result);
}
public function testGetEncodingTableReturnsEmptyForType1Symbolic(): void
{
$instance = $this->buildImport();
$this->setFdt($instance, ['type' => 'Type1', 'Flags' => 4]);
// Flags & 4 != 0 → symbolic → empty string
$result = $this->callStringMethod($instance, 'getEncodingTable', ['']);
$this->assertSame('', $result);
}
public function testGetEncodingTableReturnsEmptyForTrueTypeUnicode(): void
{
$instance = $this->buildImport();
$this->setFdt($instance, ['type' => 'TrueTypeUnicode', 'Flags' => 0]);
$result = $this->callStringMethod($instance, 'getEncodingTable', ['']);
$this->assertSame('', $result);
}
public function testGetEncodingTablePassesThroughExplicitEncoding(): void
{
$instance = $this->buildImport();
$this->setFdt($instance, ['type' => 'TrueType', 'Flags' => 0]);
$result = $this->callStringMethod($instance, 'getEncodingTable', ['iso-8859-1']);
$this->assertSame('iso-8859-1', $result);
}
// -------------------------------------------------------------------------
// findOutputPath
// -------------------------------------------------------------------------
public function testFindOutputPathReturnsKPathFontsWhenDefined(): void
{
$this->setupTest();
$instance = $this->buildImport();
$result = $this->callStringMethod($instance, 'findOutputPath', ['']);
$this->assertSame(constant('K_PATH_FONTS'), $result);
}
public function testFindOutputPathReturnsProvidedWritablePath(): void
{
$outdir = dirname(__DIR__) . '/target/tmptest/internals/';
system('mkdir -p ' . $outdir);
$instance = $this->buildImport();
$result = $this->callStringMethod($instance, 'findOutputPath', [$outdir]);
$this->assertSame($outdir, $result);
}
}
+293
View File
@@ -0,0 +1,293 @@
<?php
/**
* ImportTest.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
use Com\Tecnick\File\Exception as FileException;
use Com\Tecnick\Pdf\Font\Exception as FontException;
use PHPUnit\Framework\Attributes\DataProvider;
/**
* Import Test
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @SuppressWarnings("PHPMD.LongVariable")
*
* @phpstan-import-type TFontData from \Com\Tecnick\Pdf\Font\Load
*/
class ImportTest extends TestUtil
{
private function expectFontException(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testImportForbiddenProtocol(): void
{
$this->expectFontException();
new \Com\Tecnick\Pdf\Font\Import('phar://test.txt');
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testImportParentDir(): void
{
$this->expectFontException();
new \Com\Tecnick\Pdf\Font\Import('/tmp/something/../test.txt');
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testImportEmptyName(): void
{
$this->expectFontException();
new \Com\Tecnick\Pdf\Font\Import('');
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testImportExist(): void
{
$this->expectFontException();
$fin = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/core/Helvetica.afm';
$outdir = \dirname(__DIR__) . '/target/tmptest/';
\system('rm -rf ' . $outdir . ' && mkdir -p ' . $outdir);
new \Com\Tecnick\Pdf\Font\Import($fin, $outdir);
new \Com\Tecnick\Pdf\Font\Import($fin, $outdir);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testImportWrongFile(): void
{
$this->expectFontException();
new \Com\Tecnick\Pdf\Font\Import(\dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/core/Missing.afm');
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testImportDefaultOutput(): void
{
$this->expectFontException();
new \Com\Tecnick\Pdf\Font\Import(\dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/core/Missing.afm');
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testImportUnsupportedType(): void
{
$this->expectFontException();
$fin = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/core/Helvetica.afm';
$outdir = \dirname(__DIR__) . '/target/tmptest/core/';
\system('rm -rf ' . $outdir . ' && mkdir -p ' . $outdir);
new \Com\Tecnick\Pdf\Font\Import($fin, $outdir, 'ERROR');
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testImportUnsupportedOpenType(): void
{
$this->expectFontException();
$outdir = \dirname(__DIR__) . '/target/tmptest/core/';
\system('rm -rf ' . $outdir . ' && mkdir -p ' . $outdir);
\file_put_contents($outdir . 'test.ttf', 'OTTO 1234');
new \Com\Tecnick\Pdf\Font\Import($outdir . 'test.ttf', $outdir);
}
/**
* @throws FileException
* @throws FontException
* @throws \JsonException
* @throws \RangeException
*/
#[DataProvider('importDataProvider')]
public function testImport(
string $fontdir,
string $font,
string $outname,
string $type = '',
string $encoding = '',
): void {
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/' . $fontdir . '/';
$outdir = \dirname(__DIR__) . '/target/tmptest/' . $fontdir . '/';
\system('rm -rf ' . \dirname(__DIR__) . '/target/tmptest/ && mkdir -p ' . $outdir);
$import = new \Com\Tecnick\Pdf\Font\Import($indir . $font, $outdir, $type, $encoding);
$this->assertEquals($outname, $import->getFontName());
$file = \file_get_contents($outdir . $outname . '.json');
$this->assertNotFalse($file);
/**
* @var array{
* type: mixed,
* name: mixed,
* up: mixed,
* ut: mixed,
* dw: mixed,
* diff: mixed,
* desc: array{
* Flags: mixed,
* FontBBox: mixed,
* ItalicAngle: mixed,
* Ascent: mixed,
* Descent: mixed,
* Leading: mixed,
* CapHeight: mixed,
* XHeight: mixed,
* StemV: mixed,
* StemH: mixed,
* AvgWidth: mixed,
* MaxWidth: mixed,
* MissingWidth: mixed
* }
* } $json
*/
$json = \json_decode($file, true, 512, JSON_THROW_ON_ERROR);
$this->assertArrayHasKey('type', $json);
$this->assertArrayHasKey('name', $json);
$this->assertArrayHasKey('up', $json);
$this->assertArrayHasKey('ut', $json);
$this->assertArrayHasKey('dw', $json);
$this->assertArrayHasKey('diff', $json);
$this->assertArrayHasKey('desc', $json);
$this->assertArrayHasKey('Flags', $json['desc']);
$metric = $import->getFontMetrics();
$this->assertEquals('[' . $metric['bbox'] . ']', $json['desc']['FontBBox']);
$this->assertEqualsWithDelta($metric['italicAngle'], $json['desc']['ItalicAngle'], 0.001);
$this->assertEquals($metric['Ascent'], $json['desc']['Ascent']);
$this->assertEquals($metric['Descent'], $json['desc']['Descent']);
$this->assertEquals($metric['Leading'], $json['desc']['Leading']);
$this->assertEquals($metric['CapHeight'], $json['desc']['CapHeight']);
$this->assertEquals($metric['XHeight'], $json['desc']['XHeight']);
$this->assertEquals($metric['StemV'], $json['desc']['StemV']);
$this->assertEquals($metric['StemH'], $json['desc']['StemH']);
$this->assertEquals($metric['AvgWidth'], $json['desc']['AvgWidth']);
$this->assertEquals($metric['MaxWidth'], $json['desc']['MaxWidth']);
$this->assertEquals($metric['MissingWidth'], $json['desc']['MissingWidth']);
}
/**
* @return array<array<string>>
*/
public static function importDataProvider(): array
{
return [
['core', 'Courier.afm', 'courier'],
['core', 'Courier-Bold.afm', 'courierb'],
['core', 'Courier-BoldOblique.afm', 'courierbi'],
['core', 'Courier-Oblique.afm', 'courieri'],
['core', 'Helvetica.afm', 'helvetica'],
['core', 'Helvetica-Bold.afm', 'helveticab'],
['core', 'Helvetica-BoldOblique.afm', 'helveticabi'],
['core', 'Helvetica-Oblique.afm', 'helveticai'],
['core', 'Symbol.afm', 'symbol'],
['core', 'Times.afm', 'times'],
['core', 'Times-Bold.afm', 'timesb'],
['core', 'Times-BoldItalic.afm', 'timesbi'],
['core', 'Times-Italic.afm', 'timesi'],
['core', 'ZapfDingbats.afm', 'zapfdingbats'],
['pdfa/pfb', 'PDFACourierBoldOblique.pfb', 'pdfacourierbi', '', ''],
['pdfa/pfb', 'PDFACourierBold.pfb', 'pdfacourierb', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFACourierOblique.pfb', 'pdfacourieri', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFACourier.pfb', 'pdfacourier', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFAHelveticaBoldOblique.pfb', 'pdfahelveticabi', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFAHelveticaBold.pfb', 'pdfahelveticab', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFAHelveticaOblique.pfb', 'pdfahelveticai', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFAHelvetica.pfb', 'pdfahelvetica', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFASymbol.pfb', 'pdfasymbol', '', 'symbol'],
['pdfa/pfb', 'PDFATimesBoldItalic.pfb', 'pdfatimesbi', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFATimesBold.pfb', 'pdfatimesb', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFATimesItalic.pfb', 'pdfatimesi', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFATimes.pfb', 'pdfatimes', 'Type1', 'cp1252'],
['pdfa/pfb', 'PDFAZapfDingbats.pfb', 'pdfazapfdingbats'],
['freefont', 'FreeMonoBoldOblique.ttf', 'freemonobi'],
['freefont', 'FreeMonoBold.ttf', 'freemonob'],
['freefont', 'FreeMonoOblique.ttf', 'freemonoi'],
['freefont', 'FreeMono.ttf', 'freemono'],
['freefont', 'FreeSansBoldOblique.ttf', 'freesansbi'],
['freefont', 'FreeSansBold.ttf', 'freesansb'],
['freefont', 'FreeSansOblique.ttf', 'freesansi'],
['freefont', 'FreeSans.ttf', 'freesans'],
['freefont', 'FreeSerifBoldItalic.ttf', 'freeserifbi'],
['freefont', 'FreeSerifBold.ttf', 'freeserifb'],
['freefont', 'FreeSerifItalic.ttf', 'freeserifi'],
['freefont', 'FreeSerif.ttf', 'freeserif'],
['unifont', 'unifont.ttf', 'unifont'],
['cid0', 'cid0cs.ttf', 'cid0cs', 'CID0CS'],
['cid0', 'cid0ct.ttf', 'cid0ct', 'CID0CT'],
['cid0', 'cid0jp.ttf', 'cid0jp', 'CID0JP'],
['cid0', 'cid0kr.ttf', 'cid0kr', 'CID0KR'],
['dejavu/ttf', 'DejaVuSans.ttf', 'dejavusans'],
['dejavu/ttf', 'DejaVuSans-BoldOblique.ttf', 'dejavusansbi'],
['dejavu/ttf', 'DejaVuSans-Bold.ttf', 'dejavusansb'],
['dejavu/ttf', 'DejaVuSans-Oblique.ttf', 'dejavusansi'],
['dejavu/ttf', 'DejaVuSansCondensed.ttf', 'dejavusanscondensed'],
['dejavu/ttf', 'DejaVuSansCondensed-BoldOblique.ttf', 'dejavusanscondensedbi'],
['dejavu/ttf', 'DejaVuSansCondensed-Bold.ttf', 'dejavusanscondensedb'],
['dejavu/ttf', 'DejaVuSansCondensed-Oblique.ttf', 'dejavusanscondensedi'],
['dejavu/ttf', 'DejaVuSansMono.ttf', 'dejavusansmono'],
['dejavu/ttf', 'DejaVuSansMono-BoldOblique.ttf', 'dejavusansmonobi'],
['dejavu/ttf', 'DejaVuSansMono-Bold.ttf', 'dejavusansmonob'],
['dejavu/ttf', 'DejaVuSansMono-Oblique.ttf', 'dejavusansmonoi'],
['dejavu/ttf', 'DejaVuSans-ExtraLight.ttf', 'dejavusansextralight'],
['dejavu/ttf', 'DejaVuSerif.ttf', 'dejavuserif'],
['dejavu/ttf', 'DejaVuSerif-BoldItalic.ttf', 'dejavuserifbi'],
['dejavu/ttf', 'DejaVuSerif-Bold.ttf', 'dejavuserifb'],
['dejavu/ttf', 'DejaVuSerif-Italic.ttf', 'dejavuserifi'],
['dejavu/ttf', 'DejaVuSerifCondensed.ttf', 'dejavuserifcondensed'],
['dejavu/ttf', 'DejaVuSerifCondensed-BoldItalic.ttf', 'dejavuserifcondensedbi'],
['dejavu/ttf', 'DejaVuSerifCondensed-Bold.ttf', 'dejavuserifcondensedb'],
['dejavu/ttf', 'DejaVuSerifCondensed-Italic.ttf', 'dejavuserifcondensedi'],
];
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
/**
* LoadTest.php
*
* @since 2026-05-21
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
/**
* Load Test
*
* @since 2026-05-21
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*/
class LoadTest extends TestUtil
{
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testLoadAppliesFallbackStylesForMissingVariantFiles(): void
{
$load = new LoadTestHarness('customfont', '');
$load->setModeAndMetrics(true, true, 0, 0, 0);
$load->load();
$this->assertSame('customfontBoldItalic', $load->getNameValue());
$this->assertSame(123, $load->getStemVValue());
$this->assertSame(-11, $load->getItalicAngleValue());
$this->assertSame(64, $load->getFlagsValue());
}
public function testFindFontDirectoriesExcludesEmptyRootEntry(): void
{
$load = new LoadTestHarness('customfont', '');
$dirs = $load->exposeFontDirectories();
// An empty entry makes findFontFile() probe the filesystem root
// (is_readable('' . DIRECTORY_SEPARATOR . $file) === is_readable('/<font>.json')),
// which fails and emits a warning under open_basedir restrictions.
// See https://github.com/tecnickcom/tc-lib-pdf/issues/238
$this->assertNotContains('', $dirs);
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testLoadUpdatesExistingBoldAndItalicMetrics(): void
{
$load = new LoadTestHarness('customfont', 'Custom');
$load->setModeAndMetrics(true, true, 100, -20, 1);
$load->load();
$this->assertSame('CustomBoldItalic', $load->getNameValue());
$this->assertSame(175, $load->getStemVValue());
$this->assertSame(-31, $load->getItalicAngleValue());
$this->assertSame(65, $load->getFlagsValue());
}
}
@@ -0,0 +1,83 @@
<?php
/**
* LoadTestHarness.php
*
* @since 2026-05-21
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
class LoadTestHarness extends \Com\Tecnick\Pdf\Font\Load
{
public function __construct(string $key, string $name)
{
parent::__construct();
$this->data['type'] = 'TrueType';
$this->data['key'] = $key;
$this->data['name'] = $name;
$this->data['fakestyle'] = true;
$this->data['cw'][32] = 500;
}
public function setModeAndMetrics(bool $bold, bool $italic, int $stemv, int $italicAngle, int $flags): void
{
$this->data['mode']['bold'] = $bold;
$this->data['mode']['italic'] = $italic;
$this->data['mode']['linethrough'] = false;
$this->data['mode']['overline'] = false;
$this->data['mode']['underline'] = false;
$this->data['desc']['StemV'] = $stemv;
$this->data['desc']['ItalicAngle'] = $italicAngle;
$this->data['desc']['Flags'] = $flags;
$this->data['desc']['MissingWidth'] = 0;
}
public function getNameValue(): string
{
return $this->data['name'];
}
/**
* Expose the resolved font search directories for testing.
*
* @return array<string>
*/
public function exposeFontDirectories(): array
{
return $this->findFontDirectories();
}
public function getStemVValue(): int
{
return $this->data['desc']['StemV'];
}
public function getItalicAngleValue(): int
{
return $this->data['desc']['ItalicAngle'];
}
public function getFlagsValue(): int
{
return $this->data['desc']['Flags'];
}
protected function getFontInfo(): void
{
// Keep the preloaded test data and skip filesystem reads.
}
public function setFileData(): void
{
// Avoid dependency on actual font files for internal branch checks.
}
}
+608
View File
@@ -0,0 +1,608 @@
<?php
/**
* OutputTest.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
use Com\Tecnick\File\Exception as FileException;
use Com\Tecnick\Pdf\Encrypt\Encrypt;
use Com\Tecnick\Pdf\Font\Exception as FontException;
/**
* Output Test
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @SuppressWarnings("PHPMD.LongVariable")
*
* @phpstan-import-type TFontData from \Com\Tecnick\Pdf\Font\Load
*/
class OutputTest extends TestUtil
{
/** @throws \ReflectionException */
private function createEncrypt(): Encrypt
{
$reflector = new \ReflectionClass(Encrypt::class);
$encrypt = $reflector->newInstanceWithoutConstructor();
\assert($encrypt instanceof Encrypt, 'Failed to create Encrypt instance');
return $encrypt;
}
private function prepareTestEnvironment(): void
{
parent::setupTest();
}
/**
* @return TFontData
*/
private function getFontTemplate(): array
{
return [
'Ascender' => 0,
'Ascent' => 0,
'AvgWidth' => 0.0,
'CapHeight' => 0,
'CharacterSet' => '',
'Descender' => 0,
'Descent' => 0,
'EncodingScheme' => '',
'FamilyName' => '',
'Flags' => 0,
'FontBBox' => [],
'FontName' => '',
'FullName' => '',
'IsFixedPitch' => false,
'ItalicAngle' => 0,
'Leading' => 0,
'MaxWidth' => 0,
'MissingWidth' => 0,
'StdHW' => 0,
'StdVW' => 0,
'StemH' => 0,
'StemV' => 0,
'UnderlinePosition' => 0,
'UnderlineThickness' => 0,
'Version' => '',
'Weight' => '',
'XHeight' => 0,
'bbox' => '',
'cbbox' => [],
'cidinfo' => ['Ordering' => '', 'Registry' => '', 'Supplement' => 0, 'uni2cid' => []],
'compress' => false,
'ctg' => '',
'ctgdata' => [],
'cw' => [],
'cwu' => [],
'datafile' => '',
'desc' => [
'Ascent' => 0,
'AvgWidth' => 0,
'CapHeight' => 0,
'Descent' => 0,
'Flags' => 0,
'FontBBox' => '',
'ItalicAngle' => 0,
'Leading' => 0,
'MaxWidth' => 0,
'MissingWidth' => 0,
'StemH' => 0,
'StemV' => 0,
'XHeight' => 0,
],
'diff' => '',
'diff_n' => 0,
'dir' => '',
'dw' => 0,
'enc' => '',
'enc_map' => [],
'encodingTables' => [],
'encoding_id' => 0,
'encrypted' => '',
'fakestyle' => false,
'family' => '',
'file' => '',
'file_n' => 0,
'file_name' => '',
'i' => 0,
'ifile' => '',
'indexToLoc' => [],
'input_file' => '',
'isUnicode' => false,
'italicAngle' => 0,
'key' => '',
'lenIV' => 0,
'length1' => 0,
'length2' => 0,
'linked' => false,
'mode' => [
'bold' => false,
'italic' => false,
'linethrough' => false,
'overline' => false,
'underline' => false,
],
'n' => 0,
'name' => '',
'numGlyphs' => 0,
'numHMetrics' => 0,
'originalsize' => 0,
'pdfa' => false,
'platform_id' => 0,
'settype' => '',
'short_offset' => false,
'size1' => 0,
'size2' => 0,
'style' => '',
'subset' => false,
'subsetchars' => [],
'table' => [],
'tot_num_glyphs' => 0,
'type' => '',
'underlinePosition' => 0,
'underlineThickness' => 0,
'unicode' => false,
'unitsPerEm' => 0,
'up' => 0,
'urk' => 0.0,
'ut' => 0,
'weight' => '',
];
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
* @throws \ReflectionException
*/
public function testOutput(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
new \Com\Tecnick\Pdf\Font\Import($indir . 'pdfa/pfb/PDFASymbol.pfb', '', 'Type1', 'symbol');
$stack->add($objnum, 'pdfasymbol');
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica.afm');
$stack->add($objnum, 'helvetica');
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica-Bold.afm');
$stack->add($objnum, 'helvetica', 'B');
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica-BoldOblique.afm');
$stack->add($objnum, 'helveticaBI');
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica-Oblique.afm');
$stack->add($objnum, 'helvetica', 'I');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$stack->add($objnum, 'freesans', '');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSansBold.ttf');
$stack->add($objnum, 'freesans', 'B');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSansOblique.ttf');
$stack->add($objnum, 'freesans', 'I');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSansBoldOblique.ttf');
$stack->add($objnum, 'freesans', 'BIUDO', '', true);
new \Com\Tecnick\Pdf\Font\Import($indir . 'cid0/cid0jp.ttf', '', 'CID0JP');
$stack->add($objnum, 'cid0jp');
$fonts = $stack->getFonts();
$this->assertCount(10, $fonts);
$encrypt = $this->createEncrypt();
$output = new \Com\Tecnick\Pdf\Font\Output($fonts, $objnum, $encrypt, null);
$this->assertEquals(37, $output->getObjectNumber());
$this->assertNotEmpty($output->getFontsBlock());
$this->assertNotEmpty($output->getOutFontDict());
$keys = [];
foreach ($fonts as $font) {
$keys[] = $font['key'];
}
$this->assertNotEmpty($output->getOutFontDictByKeys($keys));
}
/**
* @throws FileException
* @throws FontException
* @throws \ReflectionException
*/
public function testOutputWithNoFontsReturnsEmptyStrings(): void
{
// Empty font array: constructor still runs without error; all output methods
// return empty strings because there is nothing to iterate over.
$encrypt = $this->createEncrypt();
$output = new \Com\Tecnick\Pdf\Font\Output([], 1, $encrypt, null);
$this->assertSame('', $output->getFontsBlock());
$this->assertSame('', $output->getOutFontDict());
$this->assertSame('', $output->getOutFontDictByKeys([]));
}
/**
* @throws FileException
* @throws FontException
* @throws \ReflectionException
*/
public function testOutputGetFontDefinitionsThrowsOnUnknownFontType(): void
{
// A font entry with an unrecognised type triggers the default branch of the
// match expression inside getFontDefinitions, which throws FontException.
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$encrypt = $this->createEncrypt();
// Build a minimal font array with an unknown type so that getFontDefinitions
// reaches the default throw branch.
$fonts = ['unknown_key' => $this->getFontTemplate()];
$fonts['unknown_key']['type'] = 'UnknownType';
new \Com\Tecnick\Pdf\Font\Output($fonts, 1, $encrypt, null);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
* @throws \ReflectionException
*/
public function testSubsetTrueTypeUnicodeOutputUsesValidCidSystemInfoAndFontStream(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$stack->add($objnum, 'freesans', '', '', true);
// Ensure at least a few glyphs are included in the subset.
foreach ([32, 65, 66, 67, 937, 960] as $ord) {
$stack->addSubsetChar('freesans', $ord);
}
$encrypt = $this->createEncrypt();
$output = new \Com\Tecnick\Pdf\Font\Output($stack->getFonts(), $objnum, $encrypt, null);
$out = $output->getFontsBlock();
$this->assertStringNotContainsString('/Registry () /Ordering ()', $out);
$this->assertStringContainsString('/Registry (Adobe) /Ordering (Identity) /Supplement 0', $out);
$matches = [];
\preg_match_all('/\\/Length1\\s+(\\d+)/', $out, $matches);
$lengthMatches = $matches[1] ?? [];
$lengths = \array_map('intval', $lengthMatches);
$this->assertNotEmpty($lengths);
$this->assertGreaterThan(1000, \max($lengths));
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
* @throws \ReflectionException
*/
public function testSubsetCharMergePreservesUnicodeKeys(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$stack->add($objnum, 'freesans', '', '', true);
$fonts = $stack->getFonts();
if (!isset($fonts['freesans'])) {
$this->fail('Expected freesans font data');
}
$base = $fonts['freesans'];
$base['key'] = 'freesans_dup';
$base['i'] += 1000;
$base['n'] += 1000;
$base['subsetchars'] = [8776 => true, 9999 => false];
$primary = $fonts['freesans'];
$primary['subsetchars'] = [960 => true];
$fonts = \array_replace($fonts, [
'freesans' => $primary,
'freesans_dup' => $base,
]);
$encrypt = $this->createEncrypt();
$output = new \Com\Tecnick\Pdf\Font\Output($fonts, $objnum, $encrypt, null);
$ref = new \ReflectionClass($output);
$prop = $ref->getProperty('subchars');
/** @var array<int, array<int, bool>> $subchars */
$subchars = $prop->getValue($output);
$this->assertIsArray($subchars);
$this->assertNotEmpty($subchars);
$first = \array_values($subchars)[0] ?? null;
$this->assertIsArray($first);
$this->assertArrayHasKey(960, $first);
$this->assertArrayHasKey(8776, $first);
$this->assertArrayNotHasKey(9999, $first);
}
public function testUniToCidPreservesNumericCidKeys(): void
{
$outfont = new OutputTestOutFont();
$font = $this->getFontTemplate();
$font['cidinfo'] = [
'Ordering' => 'Identity',
'Registry' => 'Adobe',
'Supplement' => 0,
'uni2cid' => [960 => 853, 8776 => 3283],
];
$font['cw'] = [32 => 250, 960 => 500, 8776 => 600];
$font['i'] = 1;
$font['n'] = 1;
$font['name'] = 'test';
$font['subset'] = true;
$outfont->runUniToCid($font, 0);
$this->assertArrayHasKey(853, $font['cw']);
$this->assertArrayHasKey(3283, $font['cw']);
$this->assertSame(500, $font['cw'][853] ?? null);
$this->assertSame(600, $font['cw'][3283] ?? null);
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testGetFontFullPathThrowsForMissingFile(): void
{
$this->setupTest();
$outfont = new OutputTestOutFont();
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$outfont->runGetFontFullPath($this->getFontPath(), 'not-here.bin');
}
/**
* @throws FileException
* @throws FontException
* @throws \ReflectionException
*/
public function testOutputRejectsSubsetFromPlainFileData(): void
{
$this->prepareTestEnvironment();
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$tmpfile = $this->getFontPath() . 'plain-font.bin';
\file_put_contents($tmpfile, 'not-gzip-data');
$font = $this->getFontTemplate();
$font['key'] = 'plain';
$font['name'] = 'Plain';
$font['i'] = 1;
$font['n'] = 1;
$font['file'] = 'plain-font.bin';
$font['dir'] = $this->getFontPath();
$font['subset'] = true;
$font['subsetchars'] = [65 => true];
$encrypt = $this->createEncrypt();
\set_error_handler(static fn(): bool => true);
try {
new \Com\Tecnick\Pdf\Font\Output(['plain' => $font], 1, $encrypt, null);
} finally {
\restore_error_handler();
}
}
/**
* @throws FileException
* @throws FontException
* @throws \ReflectionException
*/
public function testOutputBuildsTrueTypeDefinitionWithDefaultEncoding(): void
{
$font = $this->getFontTemplate();
$font['key'] = 'truetypefont';
$font['name'] = 'TrueTypeFont';
$font['type'] = 'TrueType';
$font['i'] = 1;
$font['n'] = 1;
$font['enc'] = 'cp1252';
$font['dw'] = 600;
$font['cw'] = [32 => 250, 65 => 700];
$encrypt = $this->createEncrypt();
$output = new \Com\Tecnick\Pdf\Font\Output(['truetypefont' => $font], 1, $encrypt, null);
$block = $output->getFontsBlock();
$this->assertStringContainsString('/Subtype /TrueType', $block);
$this->assertStringContainsString('/Encoding /WinAnsiEncoding', $block);
}
public function testGetKeyValOutFormatsFloatValues(): void
{
$outfont = new OutputTestOutFont();
$out = $outfont->runGetKeyValOut('ItalicAngle', 12.5);
$this->assertSame(' /ItalicAngle 12.500000', $out);
}
/**
* @throws FileException
* @throws FontException
* @throws \ReflectionException
*/
public function testOutputBuildsCidFont0WhenGlyphOneIsNotDefined(): void
{
$font = $this->getFontTemplate();
$font['key'] = 'cidfont0';
$font['name'] = 'CIDFont0Test';
$font['type'] = 'CIDFont0';
$font['i'] = 1;
$font['n'] = 1;
$font['enc'] = 'Identity-H';
$font['dw'] = 600;
$font['cw'] = [32 => 500, 65 => 700];
$font['cidinfo'] = [
'Registry' => 'Adobe',
'Ordering' => 'Identity',
'Supplement' => 0,
'uni2cid' => [],
];
$encrypt = $this->createEncrypt();
$output = new \Com\Tecnick\Pdf\Font\Output(['cidfont0' => $font], 1, $encrypt, null);
$block = $output->getFontsBlock();
$this->assertStringContainsString('/Subtype /Type0', $block);
$this->assertStringContainsString('/Subtype /CIDFontType0', $block);
}
/**
* Build a subset-enabled FreeSans font stack for cache tests.
*
* @return array<string, TFontData>
*
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
private function buildSubsetFonts(int $objnum): array
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$stack->add($objnum, 'freesans', '', '', true);
foreach ([32, 65, 66, 67, 937, 960] as $ord) {
$stack->addSubsetChar('freesans', $ord);
}
return $stack->getFonts();
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
* @throws \ReflectionException
*/
public function testSubsetCacheStoresOnMissAndReusesOnHit(): void
{
$objnum = 1;
$fonts = $this->buildSubsetFonts($objnum);
$cache = new SpyFontSubsetCache();
$encrypt = $this->createEncrypt();
$firstObj = new \Com\Tecnick\Pdf\Font\Output($fonts, $objnum, $encrypt, null, $cache);
$first = $firstObj->getFontsBlock();
$secondObj = new \Com\Tecnick\Pdf\Font\Output($fonts, $objnum, $encrypt, null, $cache);
$second = $secondObj->getFontsBlock();
$this->assertSame($first, $second);
$this->assertCount(1, $cache->store, 'subset should be stored exactly once');
$storedKey = \array_key_first($cache->store);
$this->assertIsString($storedKey);
$this->assertSame([$storedKey], $cache->setCalls, 'subset should be computed and stored exactly once');
$this->assertSame(
[$storedKey, $storedKey],
$cache->getCalls,
'cache should be consulted on every pass (miss then hit) for the same key',
);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
* @throws \ReflectionException
*/
public function testSubsetCacheHitProducesIdenticalOutputToNoCache(): void
{
$objnum = 1;
$fonts = $this->buildSubsetFonts($objnum);
$encrypt = $this->createEncrypt();
$noCacheObj = new \Com\Tecnick\Pdf\Font\Output($fonts, $objnum, $encrypt, null, null);
$noCache = $noCacheObj->getFontsBlock();
$cache = new SpyFontSubsetCache();
// First pass populates the cache (miss), second pass reads from it (hit).
new \Com\Tecnick\Pdf\Font\Output($fonts, $objnum, $encrypt, null, $cache);
$hitObj = new \Com\Tecnick\Pdf\Font\Output($fonts, $objnum, $encrypt, null, $cache);
$hit = $hitObj->getFontsBlock();
$this->assertSame($noCache, $hit, 'a cache hit must reproduce the no-cache output byte-for-byte');
}
/**
* @throws FileException
* @throws FontException
* @throws \ReflectionException
*/
public function testSubsetCacheKeyIsStableAndDiscriminating(): void
{
$this->setupTest();
$encrypt = $this->createEncrypt();
$output = new OutputTestOutput([], 1, $encrypt, null);
$font = $this->getFontTemplate();
$font['platform_id'] = 3;
$font['encoding_id'] = 1;
$font['type'] = 'TrueTypeUnicode';
$data = 'FONT-PROGRAM-BYTES';
$keyA = $output->runSubsetCacheKey($data, $font, [65 => true, 66 => true]);
$keyOrder = $output->runSubsetCacheKey($data, $font, [66 => true, 65 => true]);
$this->assertSame($keyA, $keyOrder, 'char insertion order must not change the key');
$keyDisabled = $output->runSubsetCacheKey($data, $font, [65 => true, 66 => true, 67 => false]);
$this->assertSame($keyA, $keyDisabled, 'disabled chars must not change the key');
$keyChars = $output->runSubsetCacheKey($data, $font, [65 => true]);
$this->assertNotSame($keyA, $keyChars, 'a different char set must change the key');
$keyBytes = $output->runSubsetCacheKey('OTHER-BYTES', $font, [65 => true, 66 => true]);
$this->assertNotSame($keyA, $keyBytes, 'different font bytes must change the key');
$fontAlt = $font;
$fontAlt['encoding_id'] = 10;
$keyCmap = $output->runSubsetCacheKey($data, $fontAlt, [65 => true, 66 => true]);
$this->assertNotSame($keyA, $keyCmap, 'different cmap selection metrics must change the key');
}
}
@@ -0,0 +1,44 @@
<?php
/**
* OutputTestOutFont.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
/**
* @phpstan-import-type TFontData from \Com\Tecnick\Pdf\Font\Load
*/
class OutputTestOutFont extends \Com\Tecnick\Pdf\Font\OutFont
{
/**
* @param TFontData $font
*/
public function runUniToCid(array &$font, int $cidoffset): void
{
$this->uniToCid($font, $cidoffset);
}
/**
* @throws \Com\Tecnick\Pdf\Font\Exception
*/
public function runGetFontFullPath(string $fontdir, string $file): string
{
return $this->getFontFullPath($fontdir, $file);
}
public function runGetKeyValOut(string $key, mixed $val): string
{
return $this->getKeyValOut($key, $val);
}
}
@@ -0,0 +1,34 @@
<?php
/**
* OutputTestOutput.php
*
* @since 2026-06-16
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
/**
* Test helper exposing protected Output members.
*
* @phpstan-import-type TFontData from \Com\Tecnick\Pdf\Font\Load
*/
class OutputTestOutput extends \Com\Tecnick\Pdf\Font\Output
{
/**
* @param TFontData $font Extracted font metrics.
* @param array<int, bool> $subchars Subset characters (charcode => enabled).
*/
public function runSubsetCacheKey(string $font_data, array $font, array $subchars): string
{
return $this->subsetCacheKey($font_data, $font, $subchars);
}
}
@@ -0,0 +1,60 @@
<?php
/**
* SpyFontSubsetCache.php
*
* @since 2026-06-16
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
use Com\Tecnick\Pdf\Font\FontSubsetCacheInterface;
/**
* In-memory FontSubsetCacheInterface implementation that records access for
* assertions in the test-suite.
*/
class SpyFontSubsetCache implements FontSubsetCacheInterface
{
/**
* Stored subset font programs keyed by cache key.
*
* @var array<string, string>
*/
public array $store = [];
/**
* Keys requested via get(), in call order.
*
* @var array<int, string>
*/
public array $getCalls = [];
/**
* Keys written via set(), in call order.
*
* @var array<int, string>
*/
public array $setCalls = [];
public function get(string $key): ?string
{
$this->getCalls[] = $key;
return $this->store[$key] ?? null;
}
public function set(string $key, string $subsetFont): void
{
$this->setCalls[] = $key;
$this->store[$key] = $subsetFont;
}
}
+485
View File
@@ -0,0 +1,485 @@
<?php
/**
* StackTest.php
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
use Com\Tecnick\File\Exception as FileException;
use Com\Tecnick\Pdf\Font\Exception as FontException;
/**
* Buffer Test
*
* @since 2011-05-23
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @SuppressWarnings("PHPMD.LongVariable")
*/
class StackTest extends TestUtil
{
private function prepareTestEnvironment(): void
{
parent::setupTest();
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testStack(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(0.75, true, true, true);
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$cfont = $stack->insert($objnum, 'freesans', '', 12, -0.1, 0.9, '', null);
$this->assertNotEmpty($cfont);
$this->assertNotEmpty($cfont['cbbox']);
$this->bcAssertEqualsWithDelta([0.162, 0.0, 7.0308, 8.748], $stack->getCharBBox(65), 0.0001);
new \Com\Tecnick\Pdf\Font\Import($indir . 'pdfa/pfb/PDFATimes.pfb');
$afont = $stack->insert($objnum, 'times', '', 14, 0.3, 1.2, '', null);
$this->assertNotEmpty($afont);
new \Com\Tecnick\Pdf\Font\Import($indir . 'pdfa/pfb/PDFAHelveticaBoldOblique.pfb');
$bfont = $stack->insert($objnum, 'helvetica', 'BIUDO', null, null, null, '', null);
$this->assertNotEmpty($bfont);
$this->assertEquals("BT /F3 14.000000 Tf ET\r", $bfont['out']);
$this->assertEquals('pdfahelveticaBI', $bfont['key']);
$this->assertEquals('Type1', $bfont['type']);
$this->bcAssertEqualsWithDelta(14, $bfont['size'], 0.0001);
$this->bcAssertEqualsWithDelta(0.3, $bfont['spacing'], 0.0001);
$this->bcAssertEqualsWithDelta(1.2, $bfont['stretching'], 0.0001);
$this->bcAssertEqualsWithDelta(18.6667, $bfont['usize'], 0.0001);
$this->bcAssertEqualsWithDelta(0.014, $bfont['cratio'], 0.0001);
$this->bcAssertEqualsWithDelta(-1.554, $bfont['up'], 0.0001);
$this->bcAssertEqualsWithDelta(0.966, $bfont['ut'], 0.0001);
$this->bcAssertEqualsWithDelta(4.6704, $bfont['dw'], 0.0001);
$this->bcAssertEqualsWithDelta(13.342, $bfont['ascent'], 0.0001);
$this->bcAssertEqualsWithDelta(-3.08, $bfont['descent'], 0.0001);
$this->bcAssertEqualsWithDelta(16.422, $bfont['height'], 0.0001);
$this->bcAssertEqualsWithDelta(5.131, $bfont['midpoint'], 0.0001);
$this->bcAssertEqualsWithDelta(10.136, $bfont['capheight'], 0.0001);
$this->bcAssertEqualsWithDelta(7.56, $bfont['xheight'], 0.0001);
$this->bcAssertEqualsWithDelta(9.492, $bfont['avgwidth'], 0.0001);
$this->bcAssertEqualsWithDelta(16.8, $bfont['maxwidth'], 0.0001);
$this->bcAssertEqualsWithDelta(4.6704, $bfont['missingwidth'], 0.0001);
$this->bcAssertEqualsWithDelta([-1.092, -3.08, 18.5976, 13.342], $bfont['fbbox'], 0.0001);
$fkey = $stack->getCurrentFontKey();
$this->assertEquals('pdfahelveticaBI', $fkey);
$font = $stack->getCurrentFont();
$this->assertEquals($bfont, $font);
$this->assertTrue($stack->isCharDefined(65));
$this->assertFalse($stack->isCharDefined(300));
$this->assertEquals(75, $stack->replaceChar(65, 75));
$this->assertEquals(65, $stack->replaceChar(65, 300));
$this->assertEquals([0, 0, 0, 0], $stack->getCharBBox(300));
$this->bcAssertEqualsWithDelta(12.1296, $stack->getCharWidth(65), 0.0001);
$this->bcAssertEqualsWithDelta(0, $stack->getCharWidth(173), 0.0001);
$this->bcAssertEqualsWithDelta(4.6704, $stack->getCharWidth(300), 0.0001);
$uniarr = [65, 173, 300];
$this->bcAssertEqualsWithDelta(17.52, $stack->getOrdArrWidth($uniarr), 0.0001);
$subs = [
65 => [400, 75],
173 => [76, 300],
300 => [400, 77],
];
$this->assertEquals([65, 173, 77], $stack->replaceMissingChars($uniarr, $subs));
$font = $stack->popLastFont();
$this->assertEquals($bfont, $font);
$font = $stack->getCurrentFont();
$this->assertEquals($afont, $font);
$fkey = $stack->getCurrentFontKey();
$this->assertEquals('pdfatimes', $fkey);
$type = $stack->getCurrentFontType();
$this->assertEquals('Type1', $type);
$ftype = $stack->isCurrentUnicodeFont();
$this->assertFalse($ftype);
$ftype = $stack->isCurrentByteFont();
$this->assertTrue($ftype);
$uniarr = [65, 173, 300, 32, 65, 173, 300, 32, 65, 173, 300];
$widths = $stack->getOrdArrDims($uniarr);
$this->assertEquals(11, $widths['chars']);
$this->assertEquals(2, $widths['spaces']);
$this->bcAssertEqualsWithDelta(60.9384, $widths['totwidth'], 0.0001);
$this->bcAssertEqualsWithDelta(8.76, $widths['totspacewidth'], 0.0001);
$this->assertEquals(6, $widths['words']);
$split = $widths['split'][5] ?? null;
$this->assertIsArray($split);
$this->assertEquals(11, $split['pos']);
$this->assertEquals(8203, $split['ord']);
$this->assertEquals('BN', $split['septype']);
$this->bcAssertEqualsWithDelta(4.92, $split['wordwidth'], 0.0001);
$this->assertEquals(2, $split['spaces']);
$this->bcAssertEqualsWithDelta(60.9384, $split['totwidth'], 0.0001);
$this->bcAssertEqualsWithDelta(8.76, $split['totspacewidth'], 0.0001);
$outfont = $stack->getOutCurrentFont();
$this->assertEquals("BT /F2 14.000000 Tf ET\r", $outfont);
$font = $stack->cloneFont($objnum, null, null, 13, 0.3, 0.7);
$this->assertEquals(13, $font['size']);
$this->assertEquals(0.3, $font['spacing']);
$this->assertEquals(0.7, $font['stretching']);
$font = $stack->cloneFont($objnum, 0, 'BI', 17, 0.7, 1.3);
$this->assertEquals('BI', $font['style']);
$this->assertEquals(17, $font['size']);
$this->assertEquals(0.7, $font['spacing']);
$this->assertEquals(1.3, $font['stretching']);
$fname = $stack->getFontFamilyName('unknown');
$this->assertEquals('freesansBI', $fname);
new \Com\Tecnick\Pdf\Font\Import($indir . 'pdfa/pfb/PDFACourier.pfb');
$bfont = $stack->insert($objnum, 'courier', '', null, null, null, '', null);
$this->assertNotEmpty($bfont);
$fname = $stack->getFontFamilyName('freesans');
$this->assertEquals('freesans', $fname);
$fname = $stack->getFontFamilyName('cursive');
$this->assertEquals('pdfatimes', $fname);
$fname = $stack->getFontFamilyName('unknown');
$this->assertEquals('pdfacourier', $fname);
}
/** @throws FontException */
public function testEmptyStack(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->prepareTestEnvironment();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$stack->popLastFont();
}
/** @throws FontException */
public function testStackMissingFont(): void
{
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$this->prepareTestEnvironment();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$objnum = 1;
$stack->insert($objnum, 'missing');
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testHasCurrentFont(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(0.75, true, true, true);
$this->assertFalse($stack->hasCurrentFont());
$this->assertSame(0, $stack->getStackSize());
$this->assertSame(-1, $stack->getCurrentFontIndex());
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$font = $stack->insert($objnum, 'freesans', '', 12);
$this->assertTrue($stack->hasCurrentFont());
$this->assertSame(1, $stack->getStackSize());
$this->assertSame(0, $stack->getCurrentFontIndex());
$this->assertSame($font['out'], $stack->getOutCurrentFont());
$stack->cloneFont($objnum, null, null, 13);
$this->assertSame(2, $stack->getStackSize());
$this->assertSame(1, $stack->getCurrentFontIndex());
$stack->popLastFont();
$this->assertTrue($stack->hasCurrentFont());
$this->assertSame(1, $stack->getStackSize());
$this->assertSame(0, $stack->getCurrentFontIndex());
$stack->popLastFont();
$this->assertFalse($stack->hasCurrentFont());
$this->assertSame(0, $stack->getStackSize());
$this->assertSame(-1, $stack->getCurrentFontIndex());
}
/**
* Only the paragraph and segment separators, the whitespace and the boundary neutrals
* split words. The type table lists the code points whose bidirectional type is not L,
* so the letters that are missing from it must not be taken as separators.
*
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testWordSplitOnSeparatorsOnly(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(0.75, true, true, true);
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$stack->insert($objnum, 'freesans', '', 12, 0, 1, '', null);
// LATIN A, CJK UNIFIED IDEOGRAPH-4E00, HANGUL SYLLABLE GA and DEVANAGARI KA are
// all of bidirectional type L: one word, closed by the trailing ZWSP.
$widths = $stack->getOrdArrDims([0x41, 0x4E00, 0xAC00, 0x0915]);
$this->assertEquals(4, $widths['chars']);
$this->assertEquals(0, $widths['spaces']);
$this->assertEquals(1, $widths['words']);
// The same letters around a space: two words plus the trailing ZWSP.
$widths = $stack->getOrdArrDims([0x41, 0x4E00, 0x20, 0xAC00, 0x0915]);
$this->assertEquals(1, $widths['spaces']);
$this->assertEquals(2, $widths['words']);
$split = $widths['split'][0] ?? null;
$this->assertIsArray($split);
$this->assertEquals('WS', $split['septype']);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testUnicodeOrdAddedToSubsetChars(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(0.75, true, true, true);
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$stack->insert($objnum, 'freesans', '', 12, 0, 1, '', true);
// Use pi and almost-equal to ensure non-latin BMP code points are tracked.
$stack->getOrdArrDims([960, 8776]);
$fonts = $stack->getFonts();
$fkey = $stack->getCurrentFontKey();
$currentFont = $fonts[$fkey] ?? null;
$this->assertIsArray($currentFont);
$this->assertArrayHasKey(960, $currentFont['subsetchars']);
$this->assertArrayHasKey(8776, $currentFont['subsetchars']);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testFractionalFontSize(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(0.75, true, true, true);
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$font = $stack->insert($objnum, 'freesans', '', 10.5);
$this->bcAssertEqualsWithDelta(10.5, $font['size'], 0.0001);
$this->assertEquals("BT /F1 10.500000 Tf ET\r", $font['out']);
$clone = $stack->cloneFont($objnum, null, null, 11.25);
$this->bcAssertEqualsWithDelta(11.25, $clone['size'], 0.0001);
$this->assertEquals("BT /F1 11.250000 Tf ET\r", $clone['out']);
}
/**
* Cloning a font with a different style must load the definition file of the
* requested style, not reuse the one of the source font (issue #19).
*
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testCloneFontLoadsStyledDefinitionFile(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(0.75, true, true, true);
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSansBold.ttf');
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSansBoldOblique.ttf');
$regular = $stack->insert($objnum, 'freesans', '', 12);
$this->assertEquals('freesans', $regular['key']);
// the bold style has never been loaded before: the clone must load it from
// the bold definition file found in the same directory of the source font
$clone = $stack->cloneFont($objnum, null, 'B', 12);
$this->assertEquals('freesansB', $clone['key']);
$this->assertEquals('B', $clone['style']);
$font = $stack->getFont('freesansB');
$this->assertEquals('FreeSansBold', $font['name']);
$this->assertEquals('freesansb.json', \basename($font['ifile']));
$this->assertFalse($font['fakestyle']);
// the real bold glyphs are wider than the regular ones
$regularWidth = $regular['cw'][65] ?? 0.0;
$cloneWidth = $clone['cw'][65] ?? 0.0;
$this->assertGreaterThan(0.0, $regularWidth);
$this->assertGreaterThan($regularWidth, $cloneWidth);
// decoration-only style letters must be ignored when resolving the file
$clone = $stack->cloneFont($objnum, 0, 'BIUDO', 14);
$this->assertEquals('freesansBI', $clone['key']);
$this->assertEquals('BIUDO', $clone['style']);
$font = $stack->getFont('freesansBI');
$this->assertEquals('FreeSansBoldOblique', $font['name']);
$this->assertEquals('freesansbi.json', \basename($font['ifile']));
$this->assertFalse($font['fakestyle']);
}
/**
* Cloning a font with a different style whose definition file does not exist
* must fall back to the autodetection with the artificial style emulation.
*
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testCloneFontFallsBackToArtificialStyleWhenStyledFileIsMissing(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(0.75, true, true, true);
new \Com\Tecnick\Pdf\Font\Import($indir . 'freefont/FreeSans.ttf');
$regular = $stack->insert($objnum, 'freesans', '', 12);
$clone = $stack->cloneFont($objnum, null, 'B', 12);
$this->assertEquals('freesansB', $clone['key']);
$font = $stack->getFont('freesansB');
$this->assertEquals('freesans.json', \basename($font['ifile']));
$this->assertTrue($font['fakestyle']);
$this->assertTrue($font['mode']['bold']);
// artificial bold reuses the regular glyph widths
$regularWidth = $regular['cw'][65] ?? 0.0;
$cloneWidth = $clone['cw'][65] ?? 0.0;
$this->assertGreaterThan(0.0, $regularWidth);
$this->bcAssertEqualsWithDelta($regularWidth, $cloneWidth, 0.0001);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testCloneFontRejectsOutOfRangeIndex(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica.afm');
$stack->insert($objnum, 'helvetica');
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$stack->cloneFont($objnum, 99);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testReplaceMissingCharsKeepsOriginalWhenNoSubstitutesProvided(): void
{
$this->prepareTestEnvironment();
$indir = \dirname(__DIR__) . '/util/vendor/tecnickcom/tc-font-mirror/';
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
new \Com\Tecnick\Pdf\Font\Import($indir . 'core/Helvetica.afm');
$stack->insert($objnum, 'helvetica');
$this->assertSame([400], $stack->replaceMissingChars([400], []));
}
/** @throws FontException */
public function testGetFontFamilyNameRejectsEmptyString(): void
{
$this->prepareTestEnvironment();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$stack->getFontFamilyName('');
}
/** @throws FontException */
public function testGetCharWidthFailsWithoutCurrentFont(): void
{
$this->prepareTestEnvironment();
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
$this->bcExpectException(\Com\Tecnick\Pdf\Font\Exception::class);
$stack->getCharWidth(65);
}
/** @throws FontException */
public function testMalformedCharBoxDataIsIgnored(): void
{
$this->prepareTestEnvironment();
$objnum = 1;
$stack = new \Com\Tecnick\Pdf\Font\Stack(1);
\file_put_contents(
$this->getFontPath() . 'badbbox.json',
'{"type":"Type1","desc":{"FontBBox":"[0 0 0 0]"},"cw":{"65":400},"cbbox":{"65":[1,2,3]}}',
);
$stack->insert($objnum, 'badbbox', '', null, null, null, $this->getFontPath() . 'badbbox.json', null);
$this->assertSame([0.0, 0.0, 0.0, 0.0], $stack->getCharBBox(65));
}
}
@@ -0,0 +1,138 @@
<?php
/**
* StackUnicodeWidthTest.php
*
* @since 2026-05-05
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
use Com\Tecnick\File\Exception as FileException;
use Com\Tecnick\Pdf\Font\Exception as FontException;
use Com\Tecnick\Pdf\Font\Import;
use Com\Tecnick\Pdf\Font\Stack;
/**
* Verifies that Stack::getCharWidth() resolves widths for non-ASCII Unicode
* codepoints via the cwu table populated during Core font import.
*/
class StackUnicodeWidthTest extends TestUtil
{
private const HELVETICA_AFM = __DIR__ . '/../util/vendor/tecnickcom/tc-font-mirror/core/Helvetica.afm';
private function prepareTestEnvironment(): void
{
parent::setupTest();
}
private function fontPath(): string
{
return parent::getFontPath();
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
private function loadHelveticaStack(): Stack
{
$this->prepareTestEnvironment();
new Import((string) \realpath(self::HELVETICA_AFM), $this->fontPath());
$objnum = 1;
$stack = new Stack(0.75, true, true, false);
$stack->insert($objnum, 'helvetica', '', 10);
return $stack;
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testStackResolvesEmdashByUnicode(): void
{
$stack = $this->loadHelveticaStack();
// em-dash U+2014 → AFM width 1000, at 10pt with stretching=1: 1000 * (10/1000) * 1 = 10.0
$this->bcAssertEqualsWithDelta(10.0, $stack->getCharWidth(0x2014), 0.001);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testStackResolvesEndashByUnicode(): void
{
$stack = $this->loadHelveticaStack();
// en-dash U+2013 → AFM width 556
$this->bcAssertEqualsWithDelta(5.56, $stack->getCharWidth(0x2013), 0.001);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testStackResolvesBulletByUnicode(): void
{
$stack = $this->loadHelveticaStack();
// bullet U+2022 → AFM width 350
$this->bcAssertEqualsWithDelta(3.5, $stack->getCharWidth(0x2022), 0.001);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testStackResolvesEuroByUnicode(): void
{
$stack = $this->loadHelveticaStack();
// Euro U+20AC → AFM width 556
$this->bcAssertEqualsWithDelta(5.56, $stack->getCharWidth(0x20AC), 0.001);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testStackResolvesSpaceByCodepoint(): void
{
$stack = $this->loadHelveticaStack();
// space U+0020 → AFM width 278 (sanity check, no regression)
$this->bcAssertEqualsWithDelta(2.78, $stack->getCharWidth(0x20), 0.001);
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testStackReturnZeroForShy(): void
{
$stack = $this->loadHelveticaStack();
$this->assertSame(0.0, $stack->getCharWidth(0xAD));
}
/**
* @throws FileException
* @throws FontException
* @throws \RangeException
*/
public function testStackReturnZeroForZwsp(): void
{
$stack = $this->loadHelveticaStack();
$this->assertSame(0.0, $stack->getCharWidth(0x200B));
}
}
+467
View File
@@ -0,0 +1,467 @@
<?php
/**
* SubsetTest.php
*
* @since 2026-05-01
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
/**
* Subset Test
*
* @since 2026-05-01
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*/
class SubsetTest extends TestUtil
{
private function setProp(object $obj, string $name, mixed $value): void
{
$prop = new \ReflectionProperty($obj, $name);
$prop->setValue($obj, $value);
}
/** @return array<array-key, mixed> */
private function getDefaultFdt(): array
{
$ref = new \ReflectionClass(\Com\Tecnick\Pdf\Font\Subset::class);
$prop = $ref->getProperty('fdt');
$instance = $ref->newInstanceWithoutConstructor();
return (static fn(mixed $value): array => \is_array($value) ? $value : [])($prop->getValue($instance));
}
/**
* @param array<array-key, mixed> $table
*
* @return array<array-key, mixed>
*/
private function getTableRecord(array $table, string $tag): array
{
if (!isset($table[$tag]) || !\is_array($table[$tag])) {
$this->fail('Missing or invalid table record: ' . $tag);
}
return $table[$tag];
}
/** @param array<array-key, mixed> $table */
private function getTableRecordString(array $table, string $tag, string $field): string
{
$record = $this->getTableRecord($table, $tag);
if (!isset($record[$field]) || !\is_string($record[$field])) {
$this->fail('Missing or invalid table string field: ' . $tag . '.' . $field);
}
return $record[$field];
}
/** @param array<array-key, mixed> $table */
private function getTableRecordInt(array $table, string $tag, string $field): int
{
$record = $this->getTableRecord($table, $tag);
if (!isset($record[$field]) || !\is_int($record[$field])) {
$this->fail('Missing or invalid table int field: ' . $tag . '.' . $field);
}
return $record[$field];
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testTableChecksumPadsTrailingBytes(): void
{
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
public function __construct() {}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function checksum(string $table, int $length): int
{
return $this->getTableChecksum($table, $length);
}
};
// 3-byte table must be zero-padded to a 4-byte word before summing.
$this->assertSame(0x01020300, $subset->checksum("\x01\x02\x03", 3));
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testTableChecksumHandlesMixedFullAndPartialWords(): void
{
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
public function __construct() {}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function checksum(string $table, int $length): int
{
return $this->getTableChecksum($table, $length);
}
};
$table = "\x11\x22\x33\x44\x55\x66\x77";
// 0x11223344 + 0x55667700, modulo 2^32
$this->assertSame(0x6688AA44, $subset->checksum($table, 7));
}
// -------------------------------------------------------------------------
// removeUnusedTables
// -------------------------------------------------------------------------
public function testRemoveUnusedTablesDropsUnknownTableTags(): void
{
// Build an anonymous subclass that exposes removeUnusedTables and lets us
// inspect the resulting fdt without running the full constructor chain.
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
public function __construct() {}
public function run(): void
{
$this->removeUnusedTables();
}
/** @return array<string, mixed> */
public function getTable(): array
{
return $this->fdt['table'];
}
};
// A font binary with 8 bytes of head data (for substr to not be empty)
$font = str_repeat("\x00", 64);
$this->setProp($subset, 'font', $font);
$this->setProp($subset, 'offset', 12);
// Provide two tables: 'head' (known → kept) and 'xxxx' (unknown → removed)
$this->setProp($subset, 'fdt', array_merge($this->getDefaultFdt(), [
'table' => [
'head' => ['offset' => 0, 'length' => 8, 'checkSum' => 0, 'data' => ''],
'xxxx' => ['offset' => 0, 'length' => 8, 'checkSum' => 0, 'data' => ''],
],
]));
$subset->run();
$table = $subset->getTable();
$this->assertArrayHasKey('head', $table);
$this->assertArrayNotHasKey('xxxx', $table);
}
// -------------------------------------------------------------------------
// addProcessedTables
// -------------------------------------------------------------------------
public function testAddProcessedTablesBuildsLocaAndGlyfFromSubsetGlyphs(): void
{
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
public function __construct() {}
public function run(): void
{
$this->addProcessedTables();
}
/** @return array<string, mixed> */
public function getTable(): array
{
return $this->fdt['table'];
}
};
// Font data: 12 bytes (glyf table at offset 0, 12 bytes of raw glyph data)
$font = str_repeat("\xAB", 12);
$this->setProp($subset, 'font', $font);
$this->setProp($subset, 'offset', 0);
// subglyphs: only glyph 0 is in the subset
$this->setProp($subset, 'subglyphs', [0 => true]);
// Inject the fdt state the method needs
$this->setProp($subset, 'fdt', array_merge($this->getDefaultFdt(), [
'tot_num_glyphs' => 2,
'short_offset' => false, // long (Offset32) loca entries
'indexToLoc' => [0 => 0, 1 => 8], // glyph 0 is 8 bytes
'table' => [
'glyf' => ['offset' => 0, 'length' => 12, 'checkSum' => 0, 'data' => ''],
'loca' => ['offset' => 0, 'length' => 0, 'checkSum' => 0, 'data' => ''],
],
]));
$subset->run();
$table = $subset->getTable();
// loca must have been rebuilt
$this->assertNotEmpty($this->getTableRecordString($table, 'loca', 'data'));
// glyf must contain the extracted glyph bytes (8 bytes for glyph 0, padded to multiple of 4)
$this->assertNotEmpty($this->getTableRecordString($table, 'glyf', 'data'));
// The checksum must have been computed (non-zero for non-empty data)
$this->assertNotSame(
0,
$this->getTableRecordInt($table, 'loca', 'checkSum') + $this->getTableRecordInt($table, 'glyf', 'checkSum'),
);
}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function testBuildSubsetFontKeepsTableDirectoryOffsetsIntact(): void
{
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
public function __construct() {}
/** @throws \Com\Tecnick\Pdf\Font\Exception */
public function run(): void
{
$this->buildSubsetFont();
}
public function getSubFont(): string
{
return $this->subfont;
}
};
$this->setProp($subset, 'fdt', array_merge($this->getDefaultFdt(), [
'table' => [
// Offsets in fdt are relative to the 12-byte sfnt header.
'head' => [
'checkSum' => 0,
'data' => str_repeat("\x00", 12),
'length' => 12,
'offset' => 12,
],
],
]));
$subset->run();
$subfont = $subset->getSubFont();
// With a single table, the table directory offset must point to byte 28
// (12-byte sfnt header + 16-byte table record).
$offset = \unpack('N', \substr($subfont, 20, 4));
$this->assertIsArray($offset);
$this->assertSame(28, $offset[1]);
}
public function testAddProcessedTablesUsesNextAvailableLocaIndexWhenImmediateIsMissing(): void
{
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
public function __construct() {}
public function run(): void
{
$this->addProcessedTables();
}
/** @return array<string, mixed> */
public function getTable(): array
{
return $this->fdt['table'];
}
};
// 8 bytes for glyph 0 followed by 8 bytes for glyph 1.
$font = str_repeat("\xAB", 16);
$this->setProp($subset, 'font', $font);
$this->setProp($subset, 'offset', 0);
$this->setProp($subset, 'subglyphs', [0 => true]);
// Simulate parser output where index 1 was removed as duplicate-empty marker.
// Glyph 0 must still use index 2 as the closing boundary.
$this->setProp($subset, 'fdt', array_merge($this->getDefaultFdt(), [
'tot_num_glyphs' => 3,
'short_offset' => false,
'indexToLoc' => [0 => 0, 2 => 8, 3 => 16],
'table' => [
'glyf' => ['offset' => 0, 'length' => 16, 'checkSum' => 0, 'data' => ''],
'loca' => ['offset' => 0, 'length' => 0, 'checkSum' => 0, 'data' => ''],
],
]));
$subset->run();
$table = $subset->getTable();
// Glyph 0 must not be dropped just because index 1 is missing.
$this->assertNotEmpty($this->getTableRecordString($table, 'glyf', 'data'));
}
public function testAddCompositeGlyphsPreservesNumericGlyphIndexes(): void
{
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
private bool $added = false;
public function __construct() {}
public function run(): void
{
$this->addCompositeGlyphs();
}
/** @return array<int, bool> */
public function getSubglyphs(): array
{
return $this->subglyphs;
}
/**
* @param array<int, bool> $new_sga
* @param int $key
*
* @return array<int, bool>
*/
protected function findCompositeGlyphs(array $new_sga, int $key): array
{
if (!$this->added) {
$this->added = true;
$new_sga[3283] = true;
}
return $new_sga;
}
};
$this->setProp($subset, 'subglyphs', [853 => true]);
$subset->run();
$subglyphs = $subset->getSubglyphs();
$this->assertArrayHasKey(853, $subglyphs);
$this->assertArrayHasKey(3283, $subglyphs);
}
public function testAddCompositeGlyphsSkipsDisabledDerivedGlyphs(): void
{
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
private bool $first = true;
public function __construct() {}
public function run(): void
{
$this->addCompositeGlyphs();
}
/** @return array<int, bool> */
public function getSubglyphs(): array
{
return $this->subglyphs;
}
/**
* @param array<int, bool> $new_sga
* @param int $key
*
* @return array<int, bool>
*/
protected function findCompositeGlyphs(array $new_sga, int $key): array
{
if ($this->first) {
$this->first = false;
$new_sga[400] = false;
$new_sga[401] = true;
}
return $new_sga;
}
};
$this->setProp($subset, 'subglyphs', [100 => true]);
$subset->run();
$subglyphs = $subset->getSubglyphs();
$this->assertArrayHasKey(100, $subglyphs);
$this->assertArrayHasKey(401, $subglyphs);
$this->assertArrayNotHasKey(400, $subglyphs);
}
public function testFindCompositeGlyphsParsesScaleAndTwoByTwoComponents(): void
{
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
public function __construct() {}
/**
* @param array<int, bool> $newSga
*
* @return array<int, bool>
*/
public function runFindCompositeGlyphs(array $newSga, int $key): array
{
return $this->findCompositeGlyphs($newSga, $key);
}
};
// Glyph header: numberOfContours = -1 (composite), 8 bytes bbox.
// Component 1: MORE_COMPONENTS + WE_HAVE_AN_X_AND_Y_SCALE, glyph 5.
// Component 2: WE_HAVE_A_TWO_BY_TWO, glyph 6.
$font =
"\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00"
. "\x00\x60\x00\x05\x00\x00\x00\x00\x00\x00"
. "\x00\x80\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
$this->setProp($subset, 'font', $font);
$this->setProp($subset, 'fbyte', new \Com\Tecnick\File\Byte($font));
$this->setProp($subset, 'subglyphs', []);
$this->setProp($subset, 'fdt', \array_replace_recursive($this->getDefaultFdt(), [
'indexToLoc' => [0 => 0],
'table' => [
'glyf' => ['offset' => 0, 'length' => \strlen($font), 'checkSum' => 0, 'data' => ''],
],
]));
$newSga = $subset->runFindCompositeGlyphs([], 0);
$this->assertArrayHasKey(5, $newSga);
$this->assertArrayHasKey(6, $newSga);
}
public function testAddProcessedTablesCreatesShortLocaAndPadsTables(): void
{
$subset = new class() extends \Com\Tecnick\Pdf\Font\Subset {
public function __construct() {}
public function run(): void
{
$this->addProcessedTables();
}
/** @return array<string, mixed> */
public function getTable(): array
{
return $this->fdt['table'];
}
};
// One glyph with 3 bytes to force padding in both loca and glyf tables.
$font = "\xAA\xBB\xCC";
$this->setProp($subset, 'font', $font);
$this->setProp($subset, 'offset', 0);
$this->setProp($subset, 'subglyphs', [0 => true]);
$this->setProp($subset, 'fdt', \array_replace_recursive($this->getDefaultFdt(), [
'tot_num_glyphs' => 1,
'short_offset' => true,
'indexToLoc' => [0 => 0, 1 => 3],
'table' => [
'glyf' => ['offset' => 0, 'length' => 3, 'checkSum' => 0, 'data' => ''],
],
]));
$subset->run();
$table = $subset->getTable();
$this->assertArrayHasKey('loca', $table);
$this->assertSame(4, $this->getTableRecordInt($table, 'loca', 'length'));
$this->assertSame(4, $this->getTableRecordInt($table, 'glyf', 'length'));
$this->assertSame(0, \strlen($this->getTableRecordString($table, 'loca', 'data')) % 4);
$this->assertSame(0, \strlen($this->getTableRecordString($table, 'glyf', 'data')) % 4);
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
/**
* TestUtil.php
*
* @since 2020-12-19
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-color software library.
*/
namespace Test;
use PHPUnit\Framework\TestCase;
/**
* Web Color class test
*
* @since 2020-12-19
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2015-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @preserveGlobalState disabled
* @runTestsInSeparateProcesses
*/
class TestUtil extends TestCase
{
protected function setupTest(): void
{
if (!\defined('K_PATH_FONTS')) {
\define('K_PATH_FONTS', \dirname(__DIR__) . '/target/tmptest/');
}
$fontPath = (string) \constant('K_PATH_FONTS');
\system('rm -rf ' . $fontPath . ' && mkdir -p ' . $fontPath);
}
protected function getFontPath(): string
{
if (\defined('K_PATH_FONTS')) {
return (string) \constant('K_PATH_FONTS');
}
return '';
}
public function bcAssertEqualsWithDelta(
mixed $expected,
mixed $actual,
float $delta = 0.01,
string $message = '',
): void {
parent::assertEqualsWithDelta($expected, $actual, $delta, $message);
}
/**
* @param class-string<\Throwable> $exception
*/
public function bcExpectException(string $exception): void
{
parent::expectException($exception);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,569 @@
<?php
/**
* TypeOneInternalsTest.php
*
* @since 2026-05-05
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* This file is part of tc-lib-pdf-font software library.
*/
namespace Test;
use Com\Tecnick\Pdf\Font\Exception as FontException;
use Com\Tecnick\Pdf\Font\Import\TypeOne;
/**
* Tests for protected methods of Import\TypeOne exercised via reflection.
*
* @since 2026-05-05
* @category Library
* @package PdfFont
* @author Nicola Asuni <info@tecnick.com>
* @copyright 2011-2026 Nicola Asuni - Tecnick.com LTD
* @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE)
* @link https://github.com/tecnickcom/tc-lib-pdf-font
*
* @phpstan-import-type TFontData from \Com\Tecnick\Pdf\Font\Load
*/
class TypeOneInternalsTest extends TestUtil
{
/** @var TFontData */
private static array $fdtDefaults = [
'Ascender' => 0,
'Ascent' => 700,
'AvgWidth' => 0.0,
'CapHeight' => 680,
'CharacterSet' => '',
'Descender' => -200,
'Descent' => -200,
'EncodingScheme' => '',
'FamilyName' => '',
'Flags' => 0,
'FontBBox' => [],
'FontName' => '',
'FullName' => '',
'IsFixedPitch' => false,
'ItalicAngle' => 0,
'Leading' => 0,
'MaxWidth' => 0,
'MissingWidth' => 0,
'StdHW' => 0,
'StdVW' => 0,
'StemH' => 0,
'StemV' => 0,
'UnderlinePosition' => 0,
'UnderlineThickness' => 0,
'Version' => '',
'Weight' => '',
'XHeight' => 0,
'bbox' => '',
'cbbox' => [],
'cidinfo' => ['Ordering' => '', 'Registry' => '', 'Supplement' => 0, 'uni2cid' => []],
'compress' => false,
'ctg' => '',
'ctgdata' => [],
'cw' => [],
'cwu' => [],
'datafile' => '',
'desc' => [
'Ascent' => 0,
'AvgWidth' => 0,
'CapHeight' => 0,
'Descent' => 0,
'Flags' => 0,
'FontBBox' => '',
'ItalicAngle' => 0,
'Leading' => 0,
'MaxWidth' => 0,
'MissingWidth' => 0,
'StemH' => 0,
'StemV' => 0,
'XHeight' => 0,
],
'diff' => '',
'diff_n' => 0,
'dir' => '',
'dw' => 0,
'enc' => '',
'enc_map' => [],
'encodingTables' => [],
'encoding_id' => 0,
'encrypted' => '',
'fakestyle' => false,
'family' => '',
'file' => '',
'file_n' => 0,
'file_name' => '',
'i' => 0,
'ifile' => '',
'indexToLoc' => [],
'input_file' => '',
'isUnicode' => false,
'italicAngle' => 0,
'key' => '',
'lenIV' => 4,
'length1' => 0,
'length2' => 0,
'linked' => false,
'mode' => [
'bold' => false,
'italic' => false,
'linethrough' => false,
'overline' => false,
'underline' => false,
],
'n' => 0,
'name' => '',
'numGlyphs' => 0,
'numHMetrics' => 0,
'originalsize' => 0,
'pdfa' => false,
'platform_id' => 0,
'settype' => '',
'short_offset' => false,
'size1' => 0,
'size2' => 0,
'style' => '',
'subset' => false,
'subsetchars' => [],
'table' => [],
'tot_num_glyphs' => 0,
'type' => 'Type1',
'underlinePosition' => 0,
'underlineThickness' => 0,
'unicode' => false,
'unitsPerEm' => 0,
'up' => 0,
'urk' => 1.0,
'ut' => 0,
'weight' => 'normal',
];
private function buildTypeOne(): TypeOne
{
$class = new \ReflectionClass(TypeOne::class);
$instance = $class->newInstanceWithoutConstructor();
$this->setProp($instance, 'fdt', self::$fdtDefaults);
$this->setProp($instance, 'font', '');
return $instance;
}
/**
* @param array<int, mixed> $args
*/
private function callMethod(object $obj, string $method, array $args = []): mixed
{
$ref = new \ReflectionMethod($obj, $method);
return $ref->invokeArgs($obj, $args);
}
/**
* @param array<int, mixed> $args
*/
private function callIntMethod(object $obj, string $method, array $args = []): int
{
return $this->expectInt($this->callMethod($obj, $method, $args), 'Expected int result.');
}
/**
* @param array<int, mixed> $args
*
* @return array<int, array<int, string>>
*/
private function callArrayMethod(object $obj, string $method, array $args = []): array
{
return $this->expectIntStringMatrix($this->callMethod($obj, $method, $args), 'Expected array result.');
}
private function setProp(object $obj, string $name, mixed $value): void
{
$prop = new \ReflectionProperty($obj, $name);
$prop->setValue($obj, $value);
}
/** @return array<string, mixed> */
private function getFontData(object $obj): array
{
$prop = new \ReflectionProperty($obj, 'fdt');
return $this->expectFontData($prop->getValue($obj));
}
/** @param array<string, mixed> $fontData */
private function getFontIntValue(array $fontData, string $key): int
{
if (!isset($fontData[$key]) || !\is_int($fontData[$key])) {
$this->fail('Expected int font field: ' . $key);
}
return $fontData[$key];
}
/**
* @param array<string, mixed> $fontData
*
* @return array<int, string>
*/
private function getFontStringMap(array $fontData, string $key): array
{
if (!isset($fontData[$key]) || !\is_array($fontData[$key])) {
$this->fail('Expected array font field: ' . $key);
}
/** @var array<int, string> $value */
$value = $fontData[$key];
return $value;
}
private function expectInt(mixed $value, string $message): int
{
if (!\is_int($value)) {
$this->fail($message);
}
return $value;
}
/** @return array<int, array<int, string>> */
private function expectIntStringMatrix(mixed $value, string $message): array
{
if (!\is_array($value)) {
$this->fail($message);
}
/** @var array<int, array<int, string>> $value */
return $value;
}
/** @return array<string, mixed> */
private function expectFontData(mixed $value): array
{
if (!\is_array($value)) {
$this->fail('Expected font data array.');
}
/** @var array<string, mixed> $value */
return $value;
}
/** @param array<string, mixed> $fontData */
private function getFontStringValue(array $fontData, string $key): string
{
if (!isset($fontData[$key]) || !\is_string($fontData[$key])) {
$this->fail('Expected string font field: ' . $key);
}
return $fontData[$key];
}
// -------------------------------------------------------------------------
// extractFontInfo
// -------------------------------------------------------------------------
public function testExtractFontInfoParsesWeight(): void
{
$instance = $this->buildTypeOne();
$this->setProp(
$instance,
'font',
"/FontName /Helvetica-Bold def\n/FontBBox {0 -200 1000 700} def\n/Weight (Bold) def",
);
$this->callMethod($instance, 'extractFontInfo');
$fdt = $this->getFontData($instance);
// the parsed /Weight must survive (it previously was hard-coded to 'Book',
// which disabled the bold StemV heuristic in extractStem)
$this->assertSame('bold', $this->getFontStringValue($fdt, 'weight'));
}
public function testExtractFontInfoDefaultsWeightToBook(): void
{
$instance = $this->buildTypeOne();
$this->setProp($instance, 'font', "/FontName /Helvetica def\n/FontBBox {0 -200 1000 700} def");
$this->callMethod($instance, 'extractFontInfo');
$fdt = $this->getFontData($instance);
$this->assertSame('Book', $this->getFontStringValue($fdt, 'weight'));
}
// -------------------------------------------------------------------------
// extractStem
// -------------------------------------------------------------------------
public function testExtractStemReadsStdVwAndStdHw(): void
{
$instance = $this->buildTypeOne();
$eplain = '/StdVW [85] def /StdHW [40] def /CapHeight [690] def';
$this->callMethod($instance, 'extractStem', [$eplain]);
$fdt = $this->getFontData($instance);
$this->assertSame(85, $this->getFontIntValue($fdt, 'StemV'));
$this->assertSame(40, $this->getFontIntValue($fdt, 'StemH'));
$this->assertSame(690, $this->getFontIntValue($fdt, 'CapHeight'));
}
public function testExtractStemUsesBoldDefaultWhenStdVwAbsent(): void
{
$instance = $this->buildTypeOne();
$fdt = self::$fdtDefaults;
$fdt['weight'] = 'bold';
$this->setProp($instance, 'fdt', $fdt);
$this->callMethod($instance, 'extractStem', ['']);
$fdt = $this->getFontData($instance);
$this->assertSame(123, $this->getFontIntValue($fdt, 'StemV'));
$this->assertSame(30, $this->getFontIntValue($fdt, 'StemH'));
}
public function testExtractStemUsesDefaultsWhenNoMatchingKeys(): void
{
$instance = $this->buildTypeOne();
$this->callMethod($instance, 'extractStem', ['']);
$fdt = $this->getFontData($instance);
$this->assertSame(70, $this->getFontIntValue($fdt, 'StemV'));
$this->assertSame(30, $this->getFontIntValue($fdt, 'StemH'));
// CapHeight falls back to Ascent (700)
$this->assertSame(700, $this->getFontIntValue($fdt, 'CapHeight'));
}
// -------------------------------------------------------------------------
// getRandomBytes
// -------------------------------------------------------------------------
public function testGetRandomBytesDefaultsToFourWhenMissing(): void
{
$instance = $this->buildTypeOne();
$this->callMethod($instance, 'getRandomBytes', ['no lenIV here']);
$fdt = $this->getFontData($instance);
$this->assertSame(4, $this->getFontIntValue($fdt, 'lenIV'));
}
public function testGetRandomBytesParsesExplicitLenIV(): void
{
$instance = $this->buildTypeOne();
$this->callMethod($instance, 'getRandomBytes', ['/lenIV 8 def']);
$fdt = $this->getFontData($instance);
$this->assertSame(8, $this->getFontIntValue($fdt, 'lenIV'));
}
public function testGetRandomBytesParsesLenIVZero(): void
{
$instance = $this->buildTypeOne();
$this->callMethod($instance, 'getRandomBytes', ['/lenIV 0 def']);
$fdt = $this->getFontData($instance);
$this->assertSame(0, $this->getFontIntValue($fdt, 'lenIV'));
}
// -------------------------------------------------------------------------
// getCharstringData
// -------------------------------------------------------------------------
public function testGetCharstringDataReturnsEmptyMatchesWhenNoneFound(): void
{
$instance = $this->buildTypeOne();
$fdt = self::$fdtDefaults;
$fdt['enc'] = '';
$this->setProp($instance, 'fdt', $fdt);
$eplain = '/CharStrings 0 dict dup begin end';
$result = $this->callArrayMethod($instance, 'getCharstringData', [$eplain]);
$this->assertIsArray($result);
$this->assertCount(0, $result);
}
public function testGetCharstringDataReturnsEmptyWhenEncNotInMap(): void
{
$instance = $this->buildTypeOne();
$fdt = self::$fdtDefaults;
$fdt['enc'] = 'nonexistent_encoding';
$this->setProp($instance, 'fdt', $fdt);
$eplain = '/CharStrings 0 dict dup begin end';
$result = $this->callArrayMethod($instance, 'getCharstringData', [$eplain]);
$this->assertIsArray($result);
}
public function testGetCharstringDataPopulatesEncMapForKnownEncoding(): void
{
$instance = $this->buildTypeOne();
$fdt = self::$fdtDefaults;
$fdt['enc'] = 'cp1252';
$this->setProp($instance, 'fdt', $fdt);
$eplain = '/CharStrings 0 dict dup begin end';
$this->callMethod($instance, 'getCharstringData', [$eplain]);
$fdt = $this->getFontData($instance);
$this->assertNotEmpty($this->getFontStringMap($fdt, 'enc_map'));
}
// -------------------------------------------------------------------------
// getCid
// -------------------------------------------------------------------------
public function testGetCidReturnsImapValueWhenCharNameFound(): void
{
$instance = $this->buildTypeOne();
$imap = ['A' => 65, 'B' => 66];
$val = [0 => '', 1 => 'A', 2 => ''];
$result = $this->callIntMethod($instance, 'getCid', [$imap, $val]);
$this->assertSame(65, $result);
}
public function testGetCidReturnsZeroWhenEncMapEmpty(): void
{
$instance = $this->buildTypeOne();
$fdt = self::$fdtDefaults;
$fdt['enc_map'] = [];
$this->setProp($instance, 'fdt', $fdt);
$imap = [];
$val = [0 => '', 1 => 'Z', 2 => ''];
$result = $this->callIntMethod($instance, 'getCid', [$imap, $val]);
$this->assertSame(0, $result);
}
public function testGetCidReturnsZeroWhenCharNotFoundInEncMap(): void
{
$instance = $this->buildTypeOne();
$fdt = self::$fdtDefaults;
$fdt['enc_map'] = ['a' => 97];
$this->setProp($instance, 'fdt', $fdt);
$imap = [];
$val = [0 => '', 1 => 'missing', 2 => ''];
$result = $this->callIntMethod($instance, 'getCid', [$imap, $val]);
$this->assertSame(0, $result);
}
public function testGetCidClampsLargeCidToThousand(): void
{
$instance = $this->buildTypeOne();
// Build enc_map where the glyph name resolves to a CID > 1000
$encMap = [];
$encMap[1001] = 'BigChar';
$fdt = self::$fdtDefaults;
$fdt['enc_map'] = $encMap;
$this->setProp($instance, 'fdt', $fdt);
$imap = [];
$val = [0 => '', 1 => 'BigChar', 2 => ''];
$result = $this->callIntMethod($instance, 'getCid', [$imap, $val]);
$this->assertSame(1000, $result);
}
// -------------------------------------------------------------------------
// decodeNumber
// -------------------------------------------------------------------------
public function testDecodeNumberHandlesByte32To246(): void
{
$instance = $this->buildTypeOne();
// ccom[0] = 139 → decoded value = 139 - 139 = 0
$ccom = [139];
/** @var array<int, int> $cdec */
$cdec = [];
/** @var array<int, int> $cwidths */
$cwidths = [];
$cck = 0;
$cid = 0;
$newIdx = $this->callIntMethod($instance, 'decodeNumber', [0, &$cck, &$cid, &$ccom, &$cdec, &$cwidths]);
/** @var array<int, int> $cdec */
$this->assertSame(1, $newIdx);
$this->assertSame(0, $cdec[0] ?? null);
}
public function testDecodeNumberHandlesBytes247To250(): void
{
$instance = $this->buildTypeOne();
// ccom[0] = 247, ccom[1] = 0 → value = (247-247)*256 + 0 + 108 = 108
$ccom = [247, 0];
/** @var array<int, int> $cdec */
$cdec = [];
/** @var array<int, int> $cwidths */
$cwidths = [];
$cck = 0;
$cid = 0;
$newIdx = $this->callIntMethod($instance, 'decodeNumber', [0, &$cck, &$cid, &$ccom, &$cdec, &$cwidths]);
/** @var array<int, int> $cdec */
$this->assertSame(2, $newIdx);
$this->assertSame(108, $cdec[0] ?? null);
}
public function testDecodeNumberHandlesBytes251To254(): void
{
$instance = $this->buildTypeOne();
// ccom[0] = 251, ccom[1] = 0 → value = -(251-251)*256 - 0 - 108 = -108
$ccom = [251, 0];
/** @var array<int, int> $cdec */
$cdec = [];
/** @var array<int, int> $cwidths */
$cwidths = [];
$cck = 0;
$cid = 0;
$newIdx = $this->callIntMethod($instance, 'decodeNumber', [0, &$cck, &$cid, &$ccom, &$cdec, &$cwidths]);
/** @var array<int, int> $cdec */
$this->assertSame(2, $newIdx);
$this->assertSame(-108, $cdec[0] ?? null);
}
public function testDecodeNumberHandlesByte255FourByteInt(): void
{
$instance = $this->buildTypeOne();
// ccom[0]=255, ccom[1..4] = big-endian int 500 = 0x000001F4
$ccom = [255, 0, 0, 1, 0xF4];
/** @var array<int, int> $cdec */
$cdec = [];
/** @var array<int, int> $cwidths */
$cwidths = [];
$cck = 0;
$cid = 0;
$newIdx = $this->callIntMethod($instance, 'decodeNumber', [0, &$cck, &$cid, &$ccom, &$cdec, &$cwidths]);
/** @var array<int, int> $cdec */
$this->assertSame(5, $newIdx);
// The 4-byte sequence packs as little-endian 'l' → 0x000001F4 LE = 0xF4010000 BE
// unpack('li', "\x00\x00\x01\xF4") depends on system endianness; just assert it returned an int.
$this->assertIsInt($cdec[0] ?? null);
}
public function testDecodeNumberHsbwCommandUpdatesWidth(): void
{
$instance = $this->buildTypeOne();
// Build a 2-element decode stack: cdec[0]=width(300), cdec[1]=13 (hsbw)
// When ccom[$idx]=value<32 and cck>0 and the value==13 → hsbw: cwidths[$cid] = cdec[$cck-1]
$ccom = [13]; // hsbw opcode (value < 32)
/** @var array<int, int> $cdec */
$cdec = [0 => 300];
/** @var array<int, int> $cwidths */
$cwidths = [];
$cck = 1; // stack has one element already
$cid = 7;
$this->callMethod($instance, 'decodeNumber', [0, &$cck, &$cid, &$ccom, &$cdec, &$cwidths]);
/** @var array<int, int> $cwidths */
$this->assertArrayHasKey(7, $cwidths);
$this->assertSame(300, $cwidths[7] ?? null);
}
// -------------------------------------------------------------------------
// storeFontData error paths
// -------------------------------------------------------------------------
public function testStoreFontDataThrowsOnInvalidMarker(): void
{
$instance = $this->buildTypeOne();
// First byte not 128 → invalid binary Type1
$this->setProp($instance, 'font', "\x00\x00\x00\x00\x00\x00");
$this->bcExpectException(FontException::class);
$this->callMethod($instance, 'storeFontData', []);
}
}