TTC ファイルを分割するためには BREAKTTC.EXE という Microsoft 製のソフトウェアがあるのですが,今は公開されてないようですし,いちいち Windows を立ち上げるのも面倒です。TTC の構造については仕様が公開されているので Perl で書いてみました。
non seekable なストリームから読み込みできるようにするために汚いコードになってしまってます。
分割するだけなら楽なんですよね。結合するのは,各テーブルの内容を比較しないといけないので面倒。まして non seekable stream だと事実上無理じゃないかなぁ。
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use POSIX qw( ceil );
use File::Spec::Functions qw( :ALL );
my %OPTION;
GetOptions(
'help|h|?' => \$OPTION{help},
'verbose|v' => \$OPTION{verbose},
'quiet|q' => \$OPTION{quiet},
'template|t' => \$OPTION{template},
'index|i' => \$OPTION{index},
) or die 'Bad options';
if (! $OPTION{template}) {
if (@ARGV) {
my (undef, undef, $filename) = splitpath($ARGV[0]);
$filename =~ s{ [.] .*? \z }{}xmso;
$OPTION{template} = "${filename}_%02d.ttf";
}
else {
$OPTION{template} = 'font_%02d.ttf';
}
}
my $handle;
if (@ARGV) {
open $handle, '<', $ARGV[0]
or die $!;
binmode $handle;
}
else {
$handle = \*STDIN;
}
binmode $handle;
my $ttc_header = read_ttc_header($handle);
my $numFonts = $ttc_header->{numFonts};
my $src_headers = [];
for my $i (0 .. $numFonts - 1) {
my $offset = tell $handle;
my $table_offset = $ttc_header->{OffsetTable}->[$i];
# seek $handle, $table_offset - $offset, 1 or die $!;
skip_handle($handle, $table_offset - $offset);
$src_headers->[$i] = read_header($handle);
}
my $tables = merge_tables($src_headers);
my $dst_headers = [];
for my $i (0 .. $numFonts - 1) {
$dst_headers->[$i] = trans_header($src_headers->[$i]);
}
my $dst_handles = [];
for my $i (0 .. $numFonts - 1) {
my $filename = sprintf $OPTION{template}, $i + 1;
open my $h, '>', $filename
or die "open: $!";
binmode $h;
$dst_handles->[$i] = $h;
write_header($dst_handles->[$i], $dst_headers->[$i]);
}
copy_bodies_for_fonts($dst_handles, $handle, $tables);
for my $i (0 .. $numFonts - 1) {
close $dst_handles->[$i];
}
close $handle;
exit;
sub read_ttc_header {
my ($src) = @_;
my $header = {};
my $buf;
read $src, $buf, 4
or die "read: $!";
$header->{TTCTag} = $buf;
die "not TTC file"
unless $header->{TTCTag} eq 'ttcf';
read $src, $buf, 4
or die "read: $!";
($header->{versionLow},
$header->{versionHigh})
= unpack 'nn', $buf;
$header->{Version} = $header->{versionLow} . '.' . $header->{versionHigh};
die "unknown version: " . $header->{Version}
if $header->{Version} != 1.0
&& $header->{Version} != 2.0;
print {*STDERR} "TTC Version: ", $header->{Version}, "\n"
if $OPTION{verbose};
read $src, $buf, 4
or die "read: $!";
$header->{numFonts} = unpack 'N', $buf;
print {*STDERR} "TTC numFonts: ", $header->{numFonts}, "\n"
if $OPTION{verbose};
for my $i (0 .. $header->{numFonts} - 1) {
read $src, $buf, 4
or die "read: $!";
$header->{OffsetTable}->[$i] = unpack 'N', $buf;
}
$header->{OffsetTable} = [ sort @{ $header->{OffsetTable} } ];
if ($header->{Version} >= 2.0) {
read $src, $buf, 4+4+4
or die "read: $!";
}
return $header;
}
sub read_header {
my ($src) = @_;
my $header = {};
my $buf;
read $src, $buf, 4 or die $!;
$header->{version} = $buf;
read $src, $buf, 2+2+2+2 or die $!;
($header->{numTables},
$header->{searchRange},
$header->{entrySelector},
$header->{rangeShift})
= unpack 'nnnn', $buf;
my $numTables = $header->{numTables};
$header->{table} = {};
$header->{tags} = [];
while ($numTables -- > 0) {
read $src, $buf, 4+4+4+4 or die $!;
my $tag = substr $buf, 0, 4, '';
my ($checkSum, $offset, $length)
= unpack 'NNN', $buf;
$header->{table}->{$tag}
= {
tag => $tag,
checkSum => $checkSum,
offset => $offset,
length => $length,
};
push @{ $header->{tags} }, $tag;
}
return $header;
}
sub trans_header {
my $header = clone_header($_[0]);
$header->{entrySelector}
= ceil(log $header->{numTables} / log 2);
$header->{searchRange}
= (1 << $header->{entrySelector}) * 16;
$header->{rangeShift}
= $header->{numTables} * 16
- $header->{searchRange};
$header->{tags}
= [
sort {
$header->{table}->{$a}->{offset}
<=>
$header->{table}->{$b}->{offset}
}
@{ $header->{tags} }
];
my $length
= 4+2+2+2+2 + (4+4+4+4) * $header->{numTables};
$header->{length} = ceil($length / 4) * 4;
$header->{trail} = $header->{length} - $length;
my $offset = $header->{length};
foreach my $tag (@{ $header->{tags} }) {
my $table = $header->{table}->{$tag};
$table->{offset} = $offset;
$offset += $table->{length};
$offset += 3 - ($offset + 3) % 4;
}
return $header;
}
sub clone_header {
my ($src) = @_;
# use Storable qw( dclone );
# return dclone($_[0]);
my $dst = { %$src };
$dst->{tags} = [ @{ $src->{tags} } ];
$dst->{table} = {};
while (my ($tag, $table) = each %{ $src->{table} }) {
$dst->{table}->{$tag} = { %$table };
}
return $dst;
}
sub write_header {
my ($dst, $header) = @_;
print {$dst} $header->{version};
print {$dst}
pack 'nnnn', $header->{numTables},
$header->{searchRange},
$header->{entrySelector},
$header->{rangeShift};
foreach my $tag (@{ $header->{tags} }) {
my $table = $header->{table}->{$tag};
print {$dst} $tag;
print {$dst}
pack 'NNN', $table->{checkSum},
$table->{offset},
$table->{length};
}
print {$dst} "\x00" x $header->{trail};
}
sub merge_tables {
my ($headers) = @_;
my $table_by_ofs = {};
my $i = 0;
foreach my $header (@$headers) {
foreach my $tag (@{ $header->{tags} }) {
my $table = $header->{table}->{$tag};
$table_by_ofs->{$table->{offset}}->{table} = $table;
push @{ $table_by_ofs->{$table->{offset}}->{id} }, $i;
}
$i ++;
}
my @tables = map {
{
%{ $table_by_ofs->{$_}->{table} },
id => $table_by_ofs->{$_}->{id},
}
}
sort { $a <=> $b }
keys %$table_by_ofs;
return \@tables;
}
sub copy_bodies_for_fonts {
my ($dst_handles, $src_handle, $tables) = @_;
my $offset = tell $src_handle;
foreach my $table (@$tables) {
# seek $src_handle, $table->{offset} - $offset, 1 or die $!;
skip_handle($src_handle, $table->{offset} - $offset);
$offset = $table->{offset};
my $data = read_handle($src_handle, $table->{length});
$offset += $table->{length};
my $trail = 3 - ($table->{length} + 3) % 4;
$trail = "\x00" x $trail;
foreach my $i (@{ $table->{id} }) {
my $dst_handle = $dst_handles->[$i];
print {$dst_handle} $data, $trail;
}
}
}
sub skip_handle {
my ($src, $length) = @_;
my $buf;
my $unit = 4096;
die "cannot seek backward: $length"
if $length < 0;
while ($length > 0) {
$unit = $length if $length < $unit;
read $src, $buf, $unit or die $!;
$length -= $unit;
}
}
sub read_handle {
my ($src, $length) = @_;
my $data;
my $buf;
my $unit = 4096;
while ($length > 0) {
$unit = $length if $length < $unit;
read $src, $buf, $unit or die $!;
$data .= $buf;
$length -= $unit;
}
return $data;
}
0 件のコメント:
コメントを投稿