generated from jric11/baseProject
Initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 0x80–0x9F 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);
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+1439
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', []);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user