Skip to content

Instantly share code, notes, and snippets.

@bubach
Last active May 5, 2019 18:12
Show Gist options
  • Select an option

  • Save bubach/4b0dd943b7821c8e6290e29e794472dd to your computer and use it in GitHub Desktop.

Select an option

Save bubach/4b0dd943b7821c8e6290e29e794472dd to your computer and use it in GitHub Desktop.
Very compact TrueType Font subsetting
<?php
namespace PdfBuilder\Font;
use PdfBuilder\Exception\PdfException;
use PdfBuilder\Stream\Stream;
use Exception;
/**
* TTF parser, sub-setter & metrics extractor
*
* @package PdfBuilder
* @author Christoffer Bubach
* @link https://github.com/bubach/pdfbuilder
* @license MIT
*/
class Parser extends Stream
{
/**
* @var array Font table information.
*/
protected $fontTables = [];
/**
* @var int|bool Font table information for internal use.
*/
protected $numberOfHMetrics, $numGlyphs, $indexToLocFormat, $cMapSegCount, $glyphNames;
/**
* @var Stream Holds subsetted font data
*/
protected $subsetStream;
/**
* @var array
*/
protected $subsettedChars, $subsettedGlyphs, $startCount, $endCount, $idDelta, $idRange = [];
/**
* Constants for index positions in
* subsetted glyph's & font table's
* array structures.
*/
const WIDTH = 0;
const CHECKSUM = 0;
const OFFSET = 1;
const LENGTH = 2;
const DATA = 3;
const LSB = 3;
const COMPONENTS = 4;
const NAME = 5;
const SSID = 6;
/**
* @var array Generated font metrics data
*/
public $fontInfo = [
'FontName' => null,
'FamilyName' => null,
'StyleName' => 'Regular',
'Flags' => 4,
'Ascent' => null,
'Descent' => null,
'CapHeight' => null,
'StemV' => null,
'IsFixedPitch' => null,
'FontBBox' => [],
'ItalicAngle' => null,
'Bold' => null,
'MissingWidth' => null,
'UnitsPerEm' => null,
'UnderlinePosition' => null,
'UnderlineThickness' => null,
'Encoding' => null,
'CharacterWidths' => [],
'ToUnicode' => [0],
];
/**
* Constructor.
*
* @param string $filename Name of TTF file to load
* @throws Exception
*/
public function __construct($filename)
{
$this->resource = fopen($filename, 'rb');
if (!$this->resource) {
throw new Exception('Font file not found!');
}
parent::__construct($this->resource);
$this->parseTableDirectories();
}
/**
* Parse the Table Directory
*
* @throws PdfException
* @return $this
*/
protected function parseTableDirectories()
{
if ($this->read(4) !== "\x00\x01\x00\x00") {
throw new PdfException('Unrecognized file format');
}
$numTables = $this->readUShort();
$this->skip(3 * 2);
for ($i = 0; $i < $numTables; $i++) {
$tag = $this->read(4);
$this->fontTables[$tag] = [
self::CHECKSUM => $this->read(4),
self::OFFSET => $this->readULong(),
self::LENGTH => $this->readULong()
];
$this->setMarker($tag, $this->fontTables[$tag][self::OFFSET]);
}
return $this->parseHeadHheaMaxp();
}
/**
* Parse the head table, general font information.
*
* @throws PdfException
* @return $this
*/
protected function parseHeadHheaMaxp()
{
$this->seek($this->getMarker('head') + 3 * 4);
if ($this->readULong() != 0x5F0F3CF5) {
throw new PdfException('Incorrect magic number');
}
$this->skip(2);
$this->fontInfo['UnitsPerEm'] = $this->readUShort();
$this->skip(2 * 8);
$this->fontInfo['FontBBox'] = [
$this->readShort(),
$this->readShort(),
$this->readShort(),
$this->readShort(),
];
$this->skip(3 * 2);
$this->indexToLocFormat = $this->readShort();
$this->seek($this->getMarker('hhea') + 4 + 15 * 2);
$this->numberOfHMetrics = $this->readUShort();
$this->seek($this->getMarker('maxp') + 4);
$this->numGlyphs = $this->readUShort();
return $this->parseCmap();
}
/**
* Parse the cmap-table, unicode-character to glyph-id mapping.
*
* @throws PdfException
* @return $this
*/
protected function parseCmap()
{
$this->seek($this->getMarker('cmap') + 2);
$numTables = $this->readUShort();
for ($i = 0; $i < $numTables; $i++) {
$platformId = $this->readUShort();
$encodingId = $this->readUShort();
$tableOffset = $this->readULong();
if ($platformId == 3 && $encodingId == 1) {
$this->seek($this->getMarker('cmap') + $tableOffset);
$this->setMarker('cmap.offset');
break;
}
}
if ($this->offset == $this->getMarker('cmap') || $this->readUShort() != 4) {
throw new PdfException('Unexpected sub-table format or missing Unicode encoding');
}
$this->skip(2 * 2);
$this->cMapSegCount = $this->readUShort() / 2;
foreach(['endCount' => 3 * 2, 'startCount' => 2, 'idDelta' => 0, 'idRange' => 0] as $name => $skip) {
$this->skip($skip);
$this->setMarker('cmap.' . $name);
for ($i = 0; $i < $this->cMapSegCount; $i++) {
$this->{$name}[$i] = ($name == 'idDelta' ? $this->readShort() : $this->readUShort());
}
}
$this->setMarker('cmap.glyphIdArray');
return $this->parseName();
}
/**
* Parse general font information, 'name'-table.
*
* @throws PdfException
* @return $this
*/
protected function parseName()
{
$this->seek($this->getMarker('name') + 2);
$count = $this->readUShort();
$stringOffset = $this->readUShort();
for ($i = 0; $i < $count; $i++) {
$this->skip(3 * 2);
$nameId = $this->readUShort();
$length = $this->readUShort();
$offset = $this->readUShort();
if (in_array($nameId, [1, 2, 6])) {
$position = $this->getOffset();
$this->seek($this->getMarker('name') + $stringOffset + $offset);
if ($nameId == 1) {
$this->fontInfo['FamilyName'] = $this->read($length);
} elseif ($nameId == 2) {
$this->fontInfo['StyleName'] = $this->read($length);
} elseif ($nameId == 6) {
$this->fontInfo['FontName'] = $this->read($length);
}
if (!empty($this->fontInfo['FamilyName']) && !empty($this->fontInfo['FontName'])) {
break;
}
$this->seek($position);
}
}
if (empty($this->fontInfo['FontName'])) {
throw new PdfException('PostScript name not found');
}
return $this->parseOS2();
}
/**
* OS/2 table, contains line spacing, font style and weight.
* Rudimentary fallback values for fonts without OS/2-table.
*
* @return $this
*/
protected function parseOS2()
{
if (isset($this->fontTables['OS/2'])) {
$this->seek('OS/2');
$version = $this->readUShort();
$this->skip(2);
$usWeightClass = $this->readUShort();
$this->skip(4 + 11 * 2 + 10 + 4 * 4 + 4);
$fsSelection = $this->readUShort();
$this->fontInfo['Bold'] = ($fsSelection & 32) != 0;
$this->skip(2 * 2);
$this->fontInfo['Ascent'] = $this->readShort();
$this->fontInfo['Descent'] = $this->readShort();
$this->fontInfo['CapHeight'] = 0;
if ($version >= 2) {
$this->skip(3 * 2 + 2 * 4 + 2);
$this->fontInfo['CapHeight'] = $this->readShort();
}
if ($usWeightClass >= 600) {
$this->fontInfo['Flags'] = $this->fontInfo['Flags'] | 262144;
}
} else {
$usWeightClass = 500;
$this->fontInfo['Ascent'] = ($this->fontInfo['FontBBox'][3] * (1000 / $this->fontInfo['UnitsPerEm']));
$this->fontInfo['Descent'] = ($this->fontInfo['FontBBox'][1] * (1000 / $this->fontInfo['UnitsPerEm']));
$this->fontInfo['CapHeight'] = $this->fontInfo['Ascent'];
}
$this->fontInfo['StemV'] = 50 + intval(pow(($usWeightClass / 65.0), 2));
return $this->parsePost();
}
/**
* Parse glyph names, 'post'-table.
*
* @return $this
*/
protected function parsePost()
{
$this->seek('post');
$this->glyphNames = ($this->readULong() == 0x20000) ? true : false;
$this->fontInfo['ItalicAngle'] = $this->readShort();
$this->skip(2);
$this->fontInfo['UnderlinePosition'] = $this->readShort();
$this->fontInfo['UnderlineThickness'] = $this->readShort();
$this->fontInfo['IsFixedPitch'] = ($this->readULong() != 0);
if ($this->fontInfo['ItalicAngle'] != 0) {
$this->fontInfo['Flags'] = $this->fontInfo['Flags'] | 64;
}
if ($this->fontInfo['IsFixedPitch']) {
$this->fontInfo['Flags'] = $this->fontInfo['Flags'] | 1;
}
return $this;
}
/**
* Get combined glyph data from hmtx, loca, glyf & post-tables.
*
* @param $index
* @return array
* @throws PdfException
*/
public function getGlyph($index)
{
if ($index > $this->numGlyphs || $index < 0) {
throw new PdfException('Glyph data not found for glyph Id: ' . $index);
}
$this->seek('hmtx');
$this->skip(($index > $this->numberOfHMetrics ? $this->numberOfHMetrics * 4 : $index * 4));
$advancedWith = $this->readUShort();
$lsb = $this->readShort();
if ($index > $this->numberOfHMetrics) {
$this->skip(($index - $this->numberOfHMetrics) * 2);
$lsb = $this->readShort();
}
$this->seek('loca');
$itl = $this->indexToLocFormat;
$this->skip($index * (($itl == 0) ? 2 : 4));
$offset = (($itl == 0) ? $this->readUShort() * 2 : $this->readULong());
$length = (($itl == 0) ? ($this->readUShort() * 2) - $offset : $this->readULong() - $offset);
return [
self::OFFSET => $offset,
self::LENGTH => $length,
self::WIDTH => $advancedWith,
self::LSB => $lsb,
self::COMPONENTS => $this->getGlyphComponents($offset),
self::NAME => $this->getGlyphName($index)
];
}
/**
* Get the glyph components, font drawing points,
* outlines, anti-aliasing information.
*
* @param $offset
* @return array
*/
protected function getGlyphComponents($offset)
{
$this->seek($this->getMarker('glyf') + $offset);
$components = [];
if ($this->readShort() < 0) {
$this->skip(4 * 2);
$offset = 5 * 2;
do {
$flags = $this->readUShort();
$components[$offset + 2] = $this->readUShort();
$skip = ($flags & 1) ? 4 : 2;
$skip += ($flags & 8) ? 2 : 0;
$skip += ($flags & 64) ? 4 : 0;
$skip += ($flags & 128) ? 8 : 0;
$this->skip($skip);
$offset += 2 * 2 + $skip;
} while ($flags & 32);
}
return $components;
}
/**
* Get cmap data for specified character
*
* @param $char
* @return int
*/
public function getCmapGlyphId($char)
{
$this->seek('cmap.glyphIdArray');
for ($i = 0; $i < $this->cMapSegCount; $i++) {
if ($this->idRange[$i] > 0) {
$this->seek($this->getMarker('cmap.idRange') + 2 * $i + $this->idRange[$i]);
}
for ($c = $this->startCount[$i]; $c <= $this->endCount[$i]; $c++) {
if ($c !== 0xFFFF) {
if ($this->idRange[$i] > 0) {
$gid = $this->readUShort();
}
if ($char == $c) {
$gid = (isset($gid) ? ($gid > 0 ? $gid + $this->idDelta[$i] : $gid): $c + $this->idDelta[$i]);
$gid = ($gid >= 65536) ? $gid - 65536 : $gid;
if ($gid > 0) {
return $gid;
}
}
}
}
}
return false;
}
/**
* Get the glyph name
*
* @param $glyphId
* @return int|string
*/
public function getGlyphName($glyphId)
{
$this->seek($this->getMarker('post') + 16 + (4 * 4) + 2 + (2 * $glyphId));
$glyphNameIndex = $this->readUShort();
$this->skip(2 * ($this->numGlyphs - $glyphId - 1));
if ($glyphNameIndex >= 258) {
$glyphNameIndex -= 258;
for ($i = 0; $i < $this->numGlyphs; $i++) {
if ($i == $glyphNameIndex) {
return $this->read($this->readByte());
}
$this->skip($this->readByte());
}
}
return $glyphNameIndex;
}
/**
* Subset font to characters from specified encoding.
*
* @param string $encoding
* @param null $stream
* @return Stream
* @throws PdfException
*/
public function subsetFont($encoding = 'cp1252', $stream = null)
{
if (!$stream instanceof Stream) {
$stream = new Stream();
}
$this->subsetStream = $stream;
$this->addGlyph(0);
$this->fontInfo['Encoding'] = $encoding;
$this->fontInfo['MissingWidth'] = $this->subsettedGlyphs[0][self::WIDTH];
foreach (range(0, 255) as $char) {
$this->fontInfo['CharacterWidths'][$char] = $this->fontInfo['MissingWidth'];
if ($encoding == 'cp1252' && in_array($char, [129, 141, 143, 144, 157])) {
continue;
}
$unicode = mb_convert_encoding(chr($char), 'UTF-8', $encoding);
$unicode = hexdec(bin2hex(iconv('UTF-8', 'UCS-2', $unicode)));
if ($glyphId = $this->getCmapGlyphId($unicode)) {
$this->subsettedChars[$unicode] = $glyphId;
$this->addGlyph($glyphId);
$this->fontInfo['CharacterWidths'][$char] = $this->subsettedGlyphs[$glyphId][self::WIDTH];
}
end($this->fontInfo['ToUnicode']);
$lastKey = key($this->fontInfo['ToUnicode']);
$last =& $this->fontInfo['ToUnicode'][$lastKey];
if (!is_array($last) && $last == $unicode - 1) {
$last = [$last, 2];
} elseif (is_array($last) && $last[0] == $unicode - $last[1]) {
$last[1]++;
} else {
$this->fontInfo['ToUnicode'][$char] = $unicode;
}
}
ksort($this->subsettedChars);
return $this->buildCmap();
}
/**
* Add glyph to subsetted font
*
* @param $id
* @return void
* @throws PdfException
*/
public function addGlyph($id)
{
if (!isset($this->subsettedGlyphs[$id])) {
$subsetCount = count($this->subsettedGlyphs);
$this->subsettedGlyphs[$id] = $this->getGlyph($id);
$this->subsettedGlyphs[$id][self::SSID] = $subsetCount;
foreach ($this->subsettedGlyphs[$id][self::COMPONENTS] as $cid) {
$this->addGlyph($cid);
}
}
}
/**
* Build cmap-table, can probably be removed if
* always using ToUnicode mappings in the PDF.
*
* @return Stream
*/
protected function buildCmap()
{
$segments = [];
$i = -1;
foreach ($this->subsettedChars as $char => $unicode) {
if (isset($segments[$i][1]) && $segments[$i][1] + 1 == $char) {
$segments[$i][1]++;
} else {
$segments[] = [$char, $char];
$i++;
}
}
$segments[] = [0xFFFF, 0xFFFF];
$glyphIdArray = '';
$segCount = count($segments);
$startCount = $endCount = $idDelta = $idRangeOffset = [];
for ($i = 0; $i < $segCount; $i++) {
list($start, $end) = $segments[$i];
$startCount[] = $start;
$endCount[] = $end;
if ($start != $end) {
$idDelta[] = 0;
$idRangeOffset[] = strlen($glyphIdArray) + ($segCount - $i) * 2;
for ($c = $start; $c <= $end; $c++) {
$glyphId = $this->subsettedChars[$c];
$ssid = $this->subsettedGlyphs[$glyphId][self::SSID];
$glyphIdArray .= pack('n', $ssid);
}
} else {
$idRangeOffset[] = $ssid = 0;
if ($start < 0xFFFF) {
$glyphId = $this->subsettedChars[$start];
$ssid = $this->subsettedGlyphs[$glyphId][self::SSID];
}
$idDelta[] = $ssid - $start;
}
}
$searchRange = 2 * (pow(2, floor(log($segCount, 2))));
$entrySelector = log($searchRange / 2, 2);
$rangeShift = 2 * $segCount - $searchRange;
$cmap = pack('nnnn', 2 * $segCount, $searchRange, $entrySelector, $rangeShift);
foreach (['endCount', 'startCount', 'idDelta', 'idRangeOffset'] as $name) {
foreach (${$name} as $val) {
$cmap .= pack('n', $val);
}
$cmap = (($name == 'endCount') ? $cmap . pack('n', 0) : $cmap);
}
$cmap .= $glyphIdArray;
$data = pack('nnnnN', 0, 1, 3, 1, 12);
$data .= pack('nnn', 4, 6 + strlen($cmap), 0);
$this->setTable('cmap', $data . $cmap);
return $this->buildHheaHmtxLocaMaxp();
}
/**
* Build hhea, hmtx, loca & maxp-table
*
* @return Stream
*/
protected function buildHheaHmtxLocaMaxp()
{
$count = pack('n', count($this->subsettedGlyphs));
$offset = 0;
$hmtx = $loca = '';
foreach (['hhea' => 4 + 15 * 2, 'maxp' => 4] as $table => $skip) {
$this->loadTable($table);
$data = substr_replace($this->fontTables[$table][self::DATA], $count, $skip, 2);
$this->setTable($table, $data);
}
foreach ((array)$this->subsettedGlyphs as $id => $glyph) {
$hmtx .= pack('nn', $glyph[self::WIDTH], $glyph[self::LSB]);
$loca .= ($this->indexToLocFormat == 0) ? pack('n', $offset / 2) : pack('N', $offset);
$offset += $this->subsettedGlyphs[$id][self::LENGTH];
}
$this->setTable('hmtx', $hmtx);
$loca .= ($this->indexToLocFormat == 0) ? pack('n', $offset / 2) : pack('N', $offset);
$this->setTable('loca', $loca);
return $this->buildGlyf();
}
/**
* Build glyph-table
*
* @param string $data
* @return Stream
*/
protected function buildGlyf($data = '')
{
foreach ((array)$this->subsettedGlyphs as $glyph) {
$this->seek($this->getMarker('glyf') + $glyph[self::OFFSET]);
$glyphData = ($glyph[self::LENGTH] > 0) ? $this->read($glyph[self::LENGTH]) : '';
if (isset($glyph[self::COMPONENTS])) {
foreach ($glyph[self::COMPONENTS] as $offset => $cid) {
$ssid = $this->subsettedGlyphs[$cid][self::SSID];
$glyphData = substr_replace($glyphData, pack('n', $ssid), $offset, 2);
}
}
$data .= $glyphData;
}
$this->setTable('glyf', $data);
return $this->buildPost();
}
/**
* Build post-table
*
* @return Stream
*/
protected function buildPost()
{
$this->seek('post');
if ($this->glyphNames) {
$numNames = 0;
$names = '';
$data = $this->read(2 * 4 + 2 * 2 + 5 * 4);
$data .= pack('n', count($this->subsettedGlyphs));
foreach ((array)$this->subsettedGlyphs as $glyph) {
if (is_string($glyph[self::NAME])) {
$data .= pack('n', 258 + $numNames);
$names .= chr(strlen($glyph[self::NAME])) . $glyph[self::NAME];
$numNames++;
} else {
$data .= pack('n', $glyph[self::NAME]);
}
}
$data .= $names;
} else {
$this->skip(4);
$data = "\x00\x03\x00\x00";
$data .= $this->read(4 + 2 * 2 + 5 * 4);
}
$this->setTable('post', $data);
return $this->buildFont();
}
/**
* Build subsetted font offset table and return stream object.
*
* @return Stream
*/
protected function buildFont()
{
$tables = [];
$checkFor = ['cmap', 'cvt ', 'fpgm', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'prep'];
foreach ($checkFor as $tableName) {
if (isset($this->fontTables[$tableName])) {
$tables[] = $tableName;
}
}
$checkSums = null;
$offset = 12 + 16 * count($tables);
$searchRange = pow(2, floor(log(count($tables)) / log(2))) * 16;
$entrySelector = floor(log(count($tables)) / log(2));
$rangeShift = count($tables) * 16 - $searchRange;
$offsetTable = pack('nnnnnn', 1, 0, count($tables), $searchRange, $entrySelector, $rangeShift);
foreach ($tables as $tableName) {
if (!isset($this->fontTables[$tableName][self::DATA])) {
$this->loadTable($tableName);
}
$table =& $this->fontTables[$tableName];
$table[self::OFFSET] = $offset;
$offsetTable .= $tableName . $table[self::CHECKSUM] . pack('NN', $table[self::OFFSET], $table[self::LENGTH]);
$checkSums .= $table[self::CHECKSUM];
$offset += strlen($table[self::DATA]);
}
$this->subsetStream->write($offsetTable);
$checkSums = $this->checkSum($offsetTable) . $checkSums;
$a = unpack('n2', $this->checkSum($checkSums));
$high = 0xB1B0 + ($a[1] ^ 0xFFFF);
$low = 0xAFBA + ($a[2] ^ 0xFFFF) + 1;
$checkSumAdjustment = pack('nn', $high + ($low >> 16), $low);
$head =& $this->fontTables['head'][self::DATA];
$head = substr_replace($head, $checkSumAdjustment, 8, 4);
foreach ($tables as $tableName) {
$this->subsetStream->write($this->fontTables[$tableName][self::DATA]);
}
return $this->subsetStream;
}
/**
* Set table data
*
* @param $tag
* @param $data
* @return void
*/
protected function setTable($tag, $data)
{
if ((strlen($data) % 4) > 0) {
$data = str_pad($data, strlen($data) + 4 - (strlen($data) % 4), "\x00");
}
$this->fontTables[$tag] = [
self::DATA => $data,
self::LENGTH => strlen($data),
self::CHECKSUM => $this->checkSum($data),
];
}
/**
* Load table
*
* @param $tag
* @return void
*/
protected function loadTable($tag)
{
$this->seek($tag);
$length = $this->fontTables[$tag][self::LENGTH];
if (($length % 4) > 0) {
$length += 4 - ($length % 4);
}
$this->fontTables[$tag][self::DATA] = $this->read($length);
}
/**
* Calculate table-data checksum
*
* @param $data
* @param int $high
* @param int $low
* @return string
*/
protected function checkSum($data, $high = 0, $low = 0)
{
for ($i = 0; $i < strlen($data); $i += 4) {
$high += (ord($data[$i]) << 8) + ord($data[$i + 1]);
$low += (ord($data[$i + 2]) << 8) + ord($data[$i + 3]);
}
return pack('nn', $high + ($low >> 16), $low);
}
}
<?php
namespace PdfBuilder\Stream;
use PdfBuilder\Interfaces\StreamInterface;
/**
* Stream-class, handle low level stream commands
* for reading / writing binary files.
*
* @package PdfBuilder
* @author Christoffer Bubach
* @link https://github.com/bubach/pdfbuilder
* @license MIT
*/
class Stream implements StreamInterface
{
/**
* @var resource Stream resource
*/
protected $resource;
/**
* @var int Stream offset
*/
protected $offset = 0;
/**
* @var array Named offsets for later lookup
*/
protected $marks = [];
/**
* Constructor, will use memory stream if not
* provided with a valid stream pointer.
*
* @param $stream Resource pointer to stream
*/
public function __construct($stream = null)
{
if ($stream == null) {
$this->resource = fopen('php://temp', 'wb');
} else {
$this->resource = $stream;
}
}
/**
* Get stream resource.
*
* @param bool $rewinded
* @return resource
*/
public function getResource($rewinded = false)
{
if ($rewinded) {
$this->seek(0);
}
return $this->resource;
}
/**
* Get the current stream offset
*
* @return int
*/
public function getOffset()
{
return $this->offset;
}
/**
* Closes the stream.
*
* @return void
*/
public function close()
{
if ($this->resource !== null) {
@fclose($this->resource);
$this->resource = null;
}
}
/**
* Go to offset in stream. Can use string for
* accessing previously set mark.
*
* @param int $offset
*/
public function seek($offset)
{
if (is_string($offset)) {
fseek($this->resource, $this->marks[$offset]);
} elseif ($offset < 0) {
fseek($this->resource, $offset, SEEK_END);
} else {
fseek($this->resource, $offset);
}
$this->offset = ftell($this->resource);
}
/**
* Skip ahead N bytes.
*
* @param $bytes
*/
public function skip($bytes)
{
fseek($this->resource, $bytes, SEEK_CUR);
$this->offset += $bytes;
}
/**
* Mark current or provided offset as 'name'.
*
* @param $name
* @param null $offset
*/
public function setMarker($name, $offset = null)
{
$offset = (is_null($offset) ? $this->offset : $offset);
$this->marks[$name] = $offset;
}
/**
* Get marker offset
*
* @param $name
* @return bool
*/
public function getMarker($name)
{
return isset($this->marks[$name]) ? $this->marks[$name] : false;
}
/**
* Read a number of bytes and rewind stream to
* previous position.
*
* @param $sizeInBytes
* @return string
*/
public function peek($sizeInBytes)
{
$data = fread($this->resource, $sizeInBytes);
fseek($this->resource, -$sizeInBytes, SEEK_CUR);
return $data;
}
/**
* Check stream EOF.
*
* @return bool
*/
public function isEnd()
{
return $this->offset >= $this->getSize();
}
/**
* Get stream size
*
* @return int
*/
public function getSize()
{
$stat = fstat($this->resource);
return $stat['size'];
}
/**
* Compare next x bytes in stream to provided bytes in array.
*
* @param $sizeInBytes
* @param $bytes
* @return bool
*/
public function compare($sizeInBytes, $bytes)
{
$data = $this->peek($sizeInBytes);
if (is_array($bytes)) {
$source = $bytes;
$bytes = null;
foreach ($source as $byte) {
$bytes .= is_int($byte) ? chr($byte) : $byte;
}
}
return ($data === $bytes);
}
/**
* Read a byte from the stream.
*
* @return int
*/
public function readByte()
{
$byte = fgetc($this->resource);
if ($byte !== false) {
$this->offset++;
}
return ord($byte);
}
/**
* Write a byte to the stream.
*
* @param $byte
* @return int
*/
public function writeByte($byte)
{
if (fwrite($this->resource, chr($byte))) {
$this->offset++;
}
return chr($byte);
}
/**
* Read string from stream.
*
* @param int $size
* @return string
*/
public function read($size = 1)
{
$data = fread($this->resource, $size);
if ($data !== false) {
$this->offset += $size;
} else {
$this->offset = ftell($this->resource);
}
return $data;
}
/**
* Write a string to the stream.
*
* @param $data
* @param null $length
* @return $this
*/
public function write($data, $length = null)
{
if (is_null($length)) {
$length = strlen($data);
}
if (fwrite($this->resource, $data, $length)) {
$this->offset += $length;
} else {
$this->offset = ftell($this->resource);
}
return $this;
}
/**
* Return unsigned 16-bit int
*
* @return int
*/
public function readUShort()
{
$data = unpack('nn', $this->read(2));
return $data['n'];
}
/**
* Get signed 16-bit int
*
* @return int
*/
public function readShort()
{
$data = unpack('nn', $this->read(2));
if ($data['n'] >= 0x8000) {
$data['n'] -= 65536;
}
return $data['n'];
}
/**
* Get unsigned 32-bit int
*
* @return int
*/
public function readULong()
{
$data = unpack('NN', $this->read(4));
return $data['N'];
}
/**
* Destructor, closes stream automatically.
*/
public function __destruct()
{
$this->close();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment