2012年11月26日月曜日

breakttc を Perl で書いてみた

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 件のコメント:

コメントを投稿