| Server IP : 139.59.63.204 / Your IP : 216.73.217.62 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ubuntu-s-1vcpu-1gb-blr1-01 6.8.0-110-generic #110-Ubuntu SMP PREEMPT_DYNAMIC Thu Mar 19 15:09:20 UTC 2026 x86_64 User : root ( 0) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/abyogasms.com/vendor/tecnickcom/tc-lib-pdf-font/test/ |
Upload File : |
<?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.TXT)
* @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.TXT)
* @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');
}
}