File: //usr/local/share/perl5/PDF/API2/Resource/CIDFont/TrueType/FontFile.pm
package PDF::API2::Resource::CIDFont::TrueType::FontFile;
use base 'PDF::API2::Basic::PDF::Dict';
use strict;
use warnings;
our $VERSION = '2.047'; # VERSION
use Carp;
use Encode qw(:all);
use Font::TTF::Font;
use POSIX qw(ceil floor);
use PDF::API2::Util;
use PDF::API2::Basic::PDF::Utils;
our $cmap = {};
sub _look_for_cmap {
my $map = shift();
my $filename = lc($map);
$filename =~ s/[^a-z0-9]+//gi;
return { %{$cmap->{$filename}} } if defined $cmap->{$filename};
eval "require 'PDF/API2/Resource/CIDFont/CMap/$filename.cmap'";
unless ($@) {
return { %{$cmap->{$filename}} };
}
else {
die "requested cmap '$map' not installed";
}
}
sub readcffindex {
my ($fh, $off) = @_;
my @idx;
my $index = [];
my $buf;
seek($fh, $off, 0);
read($fh, $buf, 3);
my ($count, $offsize) = unpack('nC', $buf);
foreach (0 .. $count) {
read($fh, $buf, $offsize);
$buf = substr("\x00\x00\x00$buf", -4, 4);
my $id = unpack('N', $buf);
push @idx, $id;
}
my $dataoff = tell($fh) - 1;
foreach my $i (0 .. $count - 1) {
push @{$index}, {
'OFF' => $dataoff + $idx[$i],
'LEN' => $idx[$i + 1] - $idx[$i],
};
}
return $index;
}
sub readcffdict {
my ($fh, $off, $len, $foff, $buf) = @_;
my @idx;
my $dict = {};
seek($fh, $off, 0);
my @st;
while (tell($fh) < ($off + $len)) {
read($fh, $buf, 1);
my $b0 = unpack('C', $buf);
my $v = '';
if ($b0 == 12) { # two byte commands
read($fh, $buf, 1);
my $b1 = unpack('C', $buf);
if ($b1 == 0) {
$dict->{'Copyright'} = { 'SID' => splice(@st, -1) };
}
elsif ($b1 == 1) {
$dict->{'isFixedPitch'} = splice(@st, -1);
}
elsif ($b1 == 2) {
$dict->{'ItalicAngle'} = splice(@st, -1);
}
elsif ($b1 == 3) {
$dict->{'UnderlinePosition'} = splice(@st, -1);
}
elsif ($b1 == 4) {
$dict->{'UnderlineThickness'} = splice(@st, -1);
}
elsif ($b1 == 5) {
$dict->{'PaintType'} = splice(@st, -1);
}
elsif ($b1 == 6) {
$dict->{'CharstringType'} = splice(@st,-1);
}
elsif ($b1 == 7) {
$dict->{'FontMatrix'} = [ splice(@st, -4) ];
}
elsif ($b1 == 8) {
$dict->{'StrokeWidth'} = splice(@st, -1);
}
elsif ($b1 == 20) {
$dict->{'SyntheticBase'} = splice(@st, -1);
}
elsif ($b1 == 21) {
$dict->{'PostScript'} = { 'SID' => splice(@st, -1) };
}
elsif ($b1 == 22) {
$dict->{'BaseFontName'} = { 'SID' => splice(@st, -1) };
}
elsif ($b1 == 23) {
$dict->{'BaseFontBlend'} = [ splice(@st, 0) ];
}
elsif ($b1 == 24) {
$dict->{'MultipleMaster'} = [ splice(@st, 0) ];
}
elsif ($b1 == 25) {
$dict->{'BlendAxisTypes'} = [ splice(@st, 0) ];
}
elsif ($b1 == 30) {
$dict->{'ROS'} = [ splice(@st, -3) ];
}
elsif ($b1 == 31) {
$dict->{'CIDFontVersion'} = splice(@st, -1);
}
elsif ($b1 == 32) {
$dict->{'CIDFontRevision'} = splice(@st, -1);
}
elsif ($b1 == 33) {
$dict->{'CIDFontType'} = splice(@st, -1);
}
elsif ($b1 == 34) {
$dict->{'CIDCount'} = splice(@st, -1);
}
elsif ($b1 == 35) {
$dict->{'UIDBase'} = splice(@st, -1);
}
elsif ($b1 == 36) {
$dict->{'FDArray'} = { 'OFF' => $foff + splice(@st, -1) };
}
elsif ($b1 == 37) {
$dict->{'FDSelect'} = { 'OFF' => $foff + splice(@st, -1) };
}
elsif ($b1 == 38) {
$dict->{'FontName'} = { 'SID' => splice(@st, -1) };
}
elsif ($b1 == 39) {
$dict->{'Chameleon'} = splice(@st, -1);
}
next;
}
elsif ($b0 < 28) { # commands
if ($b0 == 0) {
$dict->{'Version'} = { 'SID' => splice(@st, -1) };
}
elsif ($b0 == 1) {
$dict->{'Notice'} = { 'SID' => splice(@st, -1) };
}
elsif ($b0 == 2) {
$dict->{'FullName'} = { 'SID' => splice(@st, -1) };
}
elsif ($b0 == 3) {
$dict->{'FamilyName'} = { 'SID' => splice(@st, -1) };
}
elsif ($b0 == 4) {
$dict->{'Weight'} = { 'SID' => splice(@st, -1) };
}
elsif ($b0 == 5) {
$dict->{'FontBBX'} = [ splice(@st, -4) ];
}
elsif ($b0 == 13) {
$dict->{'UniqueID'} = splice(@st, -1);
}
elsif ($b0 == 14) {
$dict->{'XUID'} = [splice(@st, 0)];
}
elsif ($b0 == 15) {
$dict->{'CharSet'} = { 'OFF' => $foff + splice(@st, -1) };
}
elsif ($b0 == 16) {
$dict->{'Encoding'} = { 'OFF' => $foff + splice(@st, -1) };
}
elsif ($b0 == 17) {
$dict->{'CharStrings'} = { 'OFF' => $foff + splice(@st, -1) };
}
elsif ($b0 == 18) {
$dict->{'Private'} = {
'LEN' => splice(@st, -1),
'OFF' => $foff + splice(@st, -1),
};
}
next;
}
elsif ($b0 == 28) { # int16
read($fh, $buf, 2);
$v = unpack('n!', $buf);
}
elsif ($b0 == 29) { # int32
read($fh, $buf, 4);
$v = unpack('N!', $buf);
}
elsif ($b0 == 30) { # float
my $e = 1;
while ($e) {
read($fh, $buf, 1);
my $v0 = unpack('C', $buf);
foreach my $m ($v0 >> 8, $v0 & 0xf) {
if ($m < 10) {
$v .= $m;
}
elsif ($m == 10) {
$v .= '.';
}
elsif ($m == 11) {
$v .= 'E+';
}
elsif ($m == 12) {
$v .= 'E-';
}
elsif ($m == 14) {
$v .= '-';
}
elsif ($m == 15) {
$e = 0;
last;
}
}
}
}
elsif ($b0 == 31) { # command
$v = "c=$b0";
next;
}
elsif ($b0 < 247) { # 1 byte signed
$v = $b0 - 139;
}
elsif ($b0 < 251) { # 2 byte plus
read($fh, $buf, 1);
$v = unpack('C', $buf);
$v = ($b0 - 247) * 256 + ($v + 108);
}
elsif ($b0 < 255) { # 2 byte minus
read($fh, $buf, 1);
$v = unpack('C', $buf);
$v = -($b0 - 251) * 256 - $v - 108;
}
push @st, $v;
}
return $dict;
}
sub read_kern_table {
my ($font, $upem, $self) = @_;
my $fh = $font->{' INFILE'};
my $data;
my $buf;
return unless $font->{'kern'};
seek($fh, $font->{'kern'}->{' OFFSET'} + 2, 0);
read($fh, $buf, 2);
my $num = unpack('n', $buf);
foreach my $n (1 .. $num) {
read($fh, $buf, 6);
my ($ver, $len, $cov) = unpack('n3', $buf);
$len -= 6;
my $fmt = $cov >> 8;
if ($fmt == 0) {
$data ||= {};
read($fh, $buf, 8);
my $nc = unpack('n', $buf);
foreach (1 .. $nc) {
read($fh, $buf, 6);
my ($idx1, $idx2, $val) = unpack('n2n!', $buf);
if ($val < 0) {
$val = -floor($val * 1000 / $upem);
}
else {
$val = -ceil($val * 1000 / $upem);
}
if ($val != 0) {
$data->{"$idx1:$idx2"} = $val;
$data->{join(':',
($self->data->{'g2n'}->[$idx1] // ''),
($self->data->{'g2n'}->[$idx2] // ''))} = $val;
}
}
}
elsif ($fmt == 2) {
read($fh, $buf, $len);
}
else {
read($fh, $buf, $len);
}
}
return $data;
}
sub readcffstructs {
my $font = shift();
my $fh = $font->{' INFILE'};
my $data = {};
# read CFF table
seek($fh, $font->{'CFF '}->{' OFFSET'}, 0);
my $buf;
read($fh, $buf, 4);
my ($cffmajor, $cffminor, $cffheadsize, $cffglobaloffsize) = unpack('C4', $buf);
$data->{'name'} = readcffindex($fh, $font->{'CFF '}->{' OFFSET'} + $cffheadsize);
foreach my $dict (@{$data->{'name'}}) {
seek($fh, $dict->{'OFF'}, 0);
read($fh, $dict->{'VAL'}, $dict->{'LEN'});
}
$data->{'topdict'} = readcffindex($fh, $data->{'name'}->[-1]->{'OFF'} + $data->{'name'}->[-1]->{'LEN'});
foreach my $dict (@{$data->{'topdict'}}) {
$dict->{'VAL'} = readcffdict($fh, $dict->{'OFF'}, $dict->{'LEN'}, $font->{'CFF '}->{' OFFSET'});
}
$data->{'string'} = readcffindex($fh, $data->{'topdict'}->[-1]->{'OFF'} + $data->{'topdict'}->[-1]->{'LEN'});
foreach my $dict (@{$data->{'string'}}) {
seek($fh, $dict->{'OFF'}, 0);
read($fh, $dict->{'VAL'}, $dict->{'LEN'});
}
push @{$data->{'string'}}, { 'VAL' => '001.000' };
push @{$data->{'string'}}, { 'VAL' => '001.001' };
push @{$data->{'string'}}, { 'VAL' => '001.002' };
push @{$data->{'string'}}, { 'VAL' => '001.003' };
push @{$data->{'string'}}, { 'VAL' => 'Black' };
push @{$data->{'string'}}, { 'VAL' => 'Bold' };
push @{$data->{'string'}}, { 'VAL' => 'Book' };
push @{$data->{'string'}}, { 'VAL' => 'Light' };
push @{$data->{'string'}}, { 'VAL' => 'Medium' };
push @{$data->{'string'}}, { 'VAL' => 'Regular' };
push @{$data->{'string'}}, { 'VAL' => 'Roman' };
push @{$data->{'string'}}, { 'VAL' => 'Semibold' };
foreach my $dict (@{$data->{'topdict'}}) {
foreach my $k (keys %{$dict->{'VAL'}}) {
my $dt = $dict->{'VAL'}->{$k};
if ($k eq 'ROS') {
$dict->{'VAL'}->{$k}->[0] = $data->{'string'}->[$dict->{'VAL'}->{$k}->[0] - 391]->{'VAL'};
$dict->{'VAL'}->{$k}->[1] = $data->{'string'}->[$dict->{'VAL'}->{$k}->[1] - 391]->{'VAL'};
next;
}
next unless ref($dt) eq 'HASH' and defined($dt->{'SID'});
if ($dt->{'SID'} >= 379) {
$dict->{'VAL'}->{$k} = $data->{'string'}->[$dt->{'SID'} - 391]->{'VAL'};
}
}
}
my $dict = {};
foreach my $k (qw(CIDCount CIDFontVersion FamilyName FontBBX FullName ROS Weight XUID)) {
$dict->{$k} = $data->{'topdict'}->[0]->{'VAL'}->{$k} if defined $data->{'topdict'}->[0]->{'VAL'}->{$k};
}
return $dict;
}
sub new {
my ($class, $pdf, $file, %opts) = @_;
my $data = {};
confess "cannot find font '$file'" unless -f $file;
my $font = Font::TTF::Font->open($file);
$data->{'obj'} = $font;
$class = ref($class) if ref($class);
my $self = $class->SUPER::new();
$self->{'Filter'} = PDFArray(PDFName('FlateDecode'));
$self->{' font'} = $font;
$self->{' data'} = $data;
$data->{'noembed'} = $opts{'embed'} ? 0 : 1;
$data->{'iscff'} = defined($font->{'CFF '}) ? 1 : 0;
$self->{'Subtype'} = PDFName('CIDFontType0C') if $data->{'iscff'};
$data->{'fontfamily'} = $font->{'name'}->read->find_name(1);
$data->{'fontname'} = $font->{'name'}->read->find_name(4);
$font->{'OS/2'}->read();
my @stretch = qw(
Normal
UltraCondensed
ExtraCondensed
Condensed
SemiCondensed
Normal
SemiExpanded
Expanded
ExtraExpanded
UltraExpanded
);
$data->{'fontstretch'} = $stretch[$font->{'OS/2'}->{'usWidthClass'}] || 'Normal';
$data->{'fontweight'} = $font->{'OS/2'}->{'usWeightClass'};
$data->{'panose'} = pack('n', $font->{'OS/2'}->{'sFamilyClass'});
foreach my $p (qw[bFamilyType bSerifStyle bWeight bProportion bContrast bStrokeVariation bArmStyle bLetterform bMidline bXheight]) {
$data->{'panose'} .= pack('C', $font->{'OS/2'}->{$p});
}
$data->{'apiname'} = join('', map { ucfirst(lc(substr($_, 0, 2))) } split m/[^A-Za-z0-9\s]+/, $data->{'fontname'});
$data->{'fontname'} =~ s/[\x00-\x1f\s]//g;
$data->{'altname'} = $font->{'name'}->find_name(1);
$data->{'altname'} =~ s/[\x00-\x1f\s]//g;
$data->{'subname'} = $font->{'name'}->find_name(2);
$data->{'subname'} =~ s/[\x00-\x1f\s]//g;
$font->{'cmap'}->read->find_ms();
if (defined $font->{'cmap'}->find_ms()) {
$data->{'issymbol'} = ($font->{'cmap'}->find_ms->{'Platform'} == 3 and $font->{'cmap'}->read->find_ms->{'Encoding'} == 0) || 0;
}
else {
$data->{'issymbol'} = 0;
}
$data->{'upem'} = $font->{'head'}->read->{'unitsPerEm'};
$data->{'fontbbox'} = [
int($font->{'head'}->{'xMin'} * 1000 / $data->{'upem'}),
int($font->{'head'}->{'yMin'} * 1000 / $data->{'upem'}),
int($font->{'head'}->{'xMax'} * 1000 / $data->{'upem'}),
int($font->{'head'}->{'yMax'} * 1000 / $data->{'upem'}),
];
$data->{'stemv'} = 0;
$data->{'stemh'} = 0;
$data->{'missingwidth'} = int($font->{'hhea'}->read->{'advanceWidthMax'} * 1000 / $data->{'upem'}) || 1000;
$data->{'maxwidth'} = int($font->{'hhea'}->{'advanceWidthMax'} * 1000 / $data->{'upem'});
$data->{'ascender'} = int($font->{'hhea'}->read->{'Ascender'} * 1000 / $data->{'upem'});
$data->{'descender'} = int($font->{'hhea'}{'Descender'} * 1000 / $data->{'upem'});
$data->{'flags'} = 0;
$data->{'flags'} |= 1 if $font->{'OS/2'}->read->{'bProportion'} == 9;
$data->{'flags'} |= 2 unless $font->{'OS/2'}{'bSerifStyle'} > 10 and $font->{'OS/2'}{'bSerifStyle'} < 14;
$data->{'flags'} |= 8 if $font->{'OS/2'}{'bFamilyType'} == 2;
$data->{'flags'} |= 32; # if $font->{'OS/2'}{'bFamilyType'} > 3;
$data->{'flags'} |= 64 if $font->{'OS/2'}{'bLetterform'} > 8;
$data->{'capheight'} = $font->{'OS/2'}->{'CapHeight'} || int($data->{'fontbbox'}->[3] * 0.8);
$data->{'xheight'} = $font->{'OS/2'}->{'xHeight'} || int($data->{'fontbbox'}->[3] * 0.4);
if ($data->{'issymbol'}) {
$data->{'e2u'} = [0xf000 .. 0xf0ff];
}
else {
$data->{'e2u'} = [unpack('U*', decode('cp1252', pack('C*', 0 .. 255)))];
}
if ($font->{'post'}->read->{'FormatType'} == 3 and defined $font->{'cmap'}->read->find_ms()) {
$data->{'g2n'} = [];
foreach my $u (sort { $a <=> $b } keys %{$font->{'cmap'}->read->find_ms->{'val'}}) {
my $n = nameByUni($u);
$data->{'g2n'}->[$font->{'cmap'}->read->find_ms->{'val'}->{$u}] = $n;
}
}
else {
$data->{'g2n'} = [ map { $_ || '.notdef' } @{$font->{'post'}->read->{'VAL'}} ];
}
$data->{'italicangle'} = $font->{'post'}->{'italicAngle'};
$data->{'isfixedpitch'} = $font->{'post'}->{'isFixedPitch'};
$data->{'underlineposition'} = $font->{'post'}->{'underlinePosition'};
$data->{'underlinethickness'} = $font->{'post'}->{'underlineThickness'};
if ($self->iscff()) {
$data->{'cff'} = readcffstructs($font);
}
if (defined $data->{'cff'}->{'ROS'}) {
my %cffcmap = (
'Adobe:Japan1' => 'japanese',
'Adobe:Korea1' => 'korean',
'Adobe:CNS1' => 'traditional',
'Adobe:GB1' => 'simplified',
);
my $key = $data->{'cff'}->{'ROS'}->[0] . ':' . $data->{'cff'}->{'ROS'}->[1];
my $ccmap = _look_for_cmap($cffcmap{$key} // $key);
$data->{'u2g'} = $ccmap->{'u2g'};
$data->{'g2u'} = $ccmap->{'g2u'};
}
else {
$data->{'u2g'} = {};
my $gmap = $font->{'cmap'}->read->find_ms->{'val'};
foreach my $u (sort {$a <=> $b} keys %$gmap) {
my $uni = $u || 0;
$data->{'u2g'}->{$uni} = $gmap->{$uni};
}
$data->{'g2u'} = [ map { $_ || 0 } $font->{'cmap'}->read->reverse() ];
}
if ($data->{'issymbol'}) {
map { $data->{'u2g'}->{$_} ||= $font->{'cmap'}->read->ms_lookup($_) } (0xf000 .. 0xf0ff);
map { $data->{'u2g'}->{$_ & 0xff} ||= $font->{'cmap'}->read->ms_lookup($_) } (0xf000 .. 0xf0ff);
}
$data->{'e2n'} = [ map { $data->{'g2n'}->[$data->{'u2g'}->{$_} || 0] || '.notdef' } @{$data->{'e2u'}} ];
$data->{'e2g'} = [ map { $data->{'u2g'}->{$_ || 0} || 0 } @{$data->{'e2u'}} ];
$data->{'u2e'} = {};
foreach my $n (reverse 0 .. 255) {
$data->{'u2e'}->{$data->{'e2u'}->[$n]} //= $n;
}
$data->{'u2n'} = { map { $data->{'g2u'}->[$_] => $data->{'g2n'}->[$_] } (0 .. (scalar @{$data->{'g2u'}} - 1)) };
$data->{'wx'} = [];
foreach my $i (0 .. (scalar @{$data->{'g2u'}} - 1)) {
my $hmtx = $font->{'hmtx'}->read->{'advance'}->[$i];
if ($hmtx) {
$data->{'wx'}->[$i] = int($hmtx * 1000 / $data->{'upem'});
}
else {
$data->{'wx'}->[$i] = $data->{'missingwidth'};
}
}
$data->{'kern'} = read_kern_table($font, $data->{'upem'}, $self);
delete $data->{'kern'} unless defined $data->{'kern'};
$data->{'fontname'} =~ s/\s+//g;
$data->{'fontfamily'} =~ s/\s+//g;
$data->{'apiname'} =~ s/\s+//g;
$data->{'altname'} =~ s/\s+//g;
$data->{'subname'} =~ s/\s+//g;
$self->subsetByCId(0);
return ($self, $data);
}
sub font { return $_[0]->{' font'} }
sub data { return $_[0]->{' data'} }
sub iscff { return $_[0]->data->{'iscff'} }
sub haveKernPairs { return $_[0]->data->{'kern'} ? 1 : 0 }
sub kernPairCid {
my ($self, $i1, $i2) = @_;
return 0 if $i1 == 0 and $i2 == 0;
return $self->data->{'kern'}->{"$i1:$i2"} || 0;
}
sub subsetByCId {
my ($self, $g) = @_;
$self->data->{'subset'} = 1;
vec($self->data->{'subvec'}, $g, 1) = 1;
return if $self->iscff();
if (defined $self->font->{'loca'}->read->{'glyphs'}->[$g]) {
$self->font->{'loca'}->read->{'glyphs'}->[$g]->read();
foreach my $ref ($self->font->{'loca'}->{'glyphs'}->[$g]->get_refs()) {
vec($self->data->{'subvec'}, $ref, 1) = 1;
}
}
}
sub subvec {
my $self = shift();
return 1 if $self->iscff();
my $g = shift();
return vec($self->data->{'subvec'}, $g, 1);
}
sub glyphNum {
my $self = shift();
return $self->font->{'maxp'}->read->{'numGlyphs'};
}
sub outobjdeep {
my ($self, $fh, $pdf) = @_;
my $f = $self->font();
if ($self->iscff()) {
$f->{'CFF '}->read_dat();
$self->{' stream'} = $f->{'CFF '}->{' dat'};
}
else {
if ($self->data->{'subset'} and not $self->data->{'nosubset'}) {
$f->{'glyf'}->read();
for (my $i = 0; $i < $self->glyphNum(); $i++) {
next if $self->subvec($i);
$f->{'loca'}{'glyphs'}->[$i] = undef;
}
}
unless ($self->data->{'noembed'}) {
$self->{' stream'} = '';
my $ffh;
CORE::open($ffh, '+>', \$self->{' stream'});
binmode($ffh, ':raw');
$f->out($ffh, 'cmap', 'cvt ', 'fpgm', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'prep');
$self->{'Length1'} = PDFNum(length($self->{' stream'}));
CORE::close($ffh);
}
}
$self->SUPER::outobjdeep($fh, $pdf);
}
1;