almost done, kerning todo
[librarian.git] / librarian / font-optimizer / ext / Font-TTF / lib / Font / TTF / OS_2.pm
1 package Font::TTF::OS_2;
2
3 =head1 NAME
4
5 Font::TTF::OS_2 - the OS/2 table in a TTF font
6
7 =head1 DESCRIPTION
8
9 The OS/2 table has two versions and forms, one an extension of the other. This
10 module supports both forms and the switching between them.
11
12 =head1 INSTANCE VARIABLES
13
14 No other variables than those in table and those in the standard:
15
16     Version
17     xAvgCharWidth
18     usWeightClass
19     usWidthClass
20     fsType
21     ySubscriptXSize
22     ySubScriptYSize
23     ySubscriptXOffset
24     ySubscriptYOffset
25     ySuperscriptXSize
26     ySuperscriptYSize
27     ySuperscriptXOffset
28     ySuperscriptYOffset
29     yStrikeoutSize
30     yStrikeoutPosition
31     sFamilyClass
32     bFamilyType
33     bSerifStyle
34     bWeight
35     bProportion
36     bContrast
37     bStrokeVariation
38     bArmStyle
39     bLetterform
40     bMidline
41     bXheight
42     ulUnicodeRange1
43     ulUnicodeRange2
44     ulUnicodeRange3
45     ulUnicodeRange4
46     achVendID
47     fsSelection
48     usFirstCharIndex
49     usLastCharIndex
50     sTypoAscender
51     sTypoDescender
52     sTypoLineGap
53     usWinAscent
54     usWinDescent
55     ulCodePageRange1
56     ulCodePageRange2
57     xHeight
58     CapHeight
59     defaultChar
60     breakChar
61     maxLookups
62
63 Notice that versions 0, 1, 2 & 3 of the table are supported. Notice also that the
64 Panose variable has been broken down into its elements.
65
66 =head1 METHODS
67
68 =cut
69
70 use strict;
71 use vars qw(@ISA @fields @lens @field_info @weights);
72 use Font::TTF::Table;
73
74 @ISA = qw(Font::TTF::Table);
75 @field_info = (
76     'xAvgCharWidth' => 's',
77     'usWeightClass' => 'S',
78     'usWidthClass' => 'S',
79     'fsType' => 's',
80     'ySubscriptXSize' => 's',
81     'ySubScriptYSize' => 's',
82     'ySubscriptXOffset' => 's',
83     'ySubscriptYOffset' => 's',
84     'ySuperscriptXSize' => 's',
85     'ySuperscriptYSize' => 's',
86     'ySuperscriptXOffset' => 's',
87     'ySuperscriptYOffset' => 's',
88     'yStrikeoutSize' => 's',
89     'yStrikeoutPosition' => 's',
90     'sFamilyClass' => 's',
91     'bFamilyType' => 'C',
92     'bSerifStyle' => 'C',
93     'bWeight' => 'C',
94     'bProportion' => 'C',
95     'bContrast' => 'C',
96     'bStrokeVariation' => 'C',
97     'bArmStyle' => 'C',
98     'bLetterform' => 'C',
99     'bMidline' => 'C',
100     'bXheight' => 'C',
101     'ulUnicodeRange1' => 'L',
102     'ulUnicodeRange2' => 'L',
103     'ulUnicodeRange3' => 'L',
104     'ulUnicodeRange4' => 'L',
105     'achVendID' => 'L',
106     'fsSelection' => 'S',
107     'usFirstCharIndex' => 'S',
108     'usLastCharIndex' => 'S',
109     'sTypoAscender' => 'S',
110     'sTypoDescender' => 's',
111     'sTypoLineGap' => 'S',
112     'usWinAscent' => 'S',
113     'usWinDescent' => 'S',
114     '' => '',
115     'ulCodePageRange1' => 'L',
116     'ulCodePageRange2' => 'L',
117     '' => '',
118     'xHeight' => 's',
119     'CapHeight' => 's',
120     'defaultChar' => 'S',
121     'breakChar' => 'S',
122     'maxLookups' => 's',
123     '' => '',            # i.e. v3 is basically same as v2
124     );
125
126 @weights = qw(64 14 27 35 100 20 14 42 63 3 6 35 20 56 56 17 4 49 56 71 31 10 18 3 18 2 166);
127
128 use Font::TTF::Utils;
129
130 sub init
131 {
132     my ($k, $v, $c, $n, $i, $t, $j);
133
134     $n = 0;
135     @lens = (76, 84, 94, 94);
136     for ($j = 0; $j < $#field_info; $j += 2)
137     {
138         if ($field_info[$j] eq '')
139         {
140             $n++;
141             next;
142         }
143         ($k, $v, $c) = TTF_Init_Fields($field_info[$j], $c, $field_info[$j+1]);
144         next unless defined $k && $k ne "";
145         for ($i = $n; $i < 4; $i++)
146         { $fields[$i]{$k} = $v; }
147     }
148 }
149
150
151 =head2 $t->read
152
153 Reads in the various values from disk (see details of OS/2 table)
154
155 =cut
156
157 sub read
158 {
159     my ($self) = @_;
160     my ($dat, $ver);
161
162     $self->SUPER::read or return $self;
163
164     init unless defined $fields[2]{'xAvgCharWidth'};
165     $self->{' INFILE'}->read($dat, 2);
166     $ver = unpack("n", $dat);
167     $self->{'Version'} = $ver;
168     if ($ver < 4)
169     {
170         $self->{' INFILE'}->read($dat, $lens[$ver]);
171         TTF_Read_Fields($self, $dat, $fields[$ver]);
172     }
173     $self;
174 }
175
176
177 =head2 $t->out($fh)
178
179 Writes the table to a file either from memory or by copying.
180
181 =cut
182
183 sub out
184 {
185     my ($self, $fh) = @_;
186     my ($ver);
187
188     return $self->SUPER::out($fh) unless $self->{' read'};
189
190     $ver = $self->{'Version'};
191     $fh->print(pack("n", $ver));
192     $fh->print(TTF_Out_Fields($self, $fields[$ver], $lens[$ver]));
193     $self;
194 }
195
196
197 =head2 $t->XML_element($context, $depth, $key, $value)
198
199 Tidies up the hex values to output them in hex
200
201 =cut
202
203 sub XML_element
204 {
205     my ($self) = shift;
206     my ($context, $depth, $key, $value) = @_;
207     my ($fh) = $context->{'fh'};
208
209     if ($key =~ m/^ul(?:Unicode|CodePage)Range\d$/o)
210     { $fh->printf("%s<%s>%08X</%s>\n", $depth, $key, $value, $key); }
211     elsif ($key eq 'achVendID')
212     { $fh->printf("%s<%s name='%s'/>\n", $depth, $key, pack('N', $value)); }
213     else
214     { return $self->SUPER::XML_element(@_); }
215     $self;
216 }
217
218
219 =head2 $t->XML_end($context, $tag, %attrs)
220
221 Now handle them on the way back in
222
223 =cut
224
225 sub XML_end
226 {
227     my ($self) = shift;
228     my ($context, $tag, %attrs) = @_;
229
230     if ($tag =~ m/^ul(?:Unicode|CodePage)Range\d$/o)
231     { return hex($context->{'text'}); }
232     elsif ($tag eq 'achVendID')
233     { return unpack('N', $attrs{'name'}); }
234     else
235     { return $self->SUPER::XML_end(@_); }
236 }
237
238 =head2 $t->update
239
240 Updates the OS/2 table by getting information from other sources:
241
242 Updates the C<firstChar> and C<lastChar> values based on the MS table in the
243 cmap.
244
245 Updates the sTypoAscender, sTypoDescender & sTypoLineGap to be the same values
246 as Ascender, Descender and Linegap from the hhea table (assuming it is dirty)
247 and also sets usWinAscent to be the sum of Ascender+Linegap and usWinDescent to
248 be the negative of Descender.
249
250 =cut
251
252 sub update
253 {
254     my ($self) = @_;
255     my ($map, @keys, $table, $i, $avg, $hmtx);
256
257     return undef unless ($self->SUPER::update);
258
259     $self->{' PARENT'}{'cmap'}->update;
260     $map = $self->{' PARENT'}{'cmap'}->find_ms || return undef;
261     $hmtx = $self->{' PARENT'}{'hmtx'}->read;
262
263     @keys = sort {$a <=> $b} grep {$_ < 0x10000} keys %{$map->{'val'}};
264
265     $self->{'usFirstCharIndex'} = $keys[0];
266     $self->{'usLastCharIndex'} = $keys[-1];
267
268     $table = $self->{' PARENT'}{'hhea'}->read;
269     
270     # try any way we can to get some real numbers passed around!
271     if (($self->{'fsSelection'} & 128) != 0)
272     {
273         # assume the user knows what they are doing and has sensible values already
274     }
275     elsif ($table->{'Ascender'} != 0 || $table->{'Descender'} != 0)
276     {
277         $self->{'sTypoAscender'} = $table->{'Ascender'};
278         $self->{'sTypoDescender'} = $table->{'Descender'};
279         $self->{'sTypoLineGap'} = $table->{'LineGap'};
280         $self->{'usWinAscent'} = $self->{'sTypoAscender'} + $self->{'sTypoLineGap'};
281         $self->{'usWinDescent'} = -$self->{'sTypoDescender'};
282     }
283     elsif ($self->{'sTypoAscender'} != 0 || $self->{'sTypoDescender'} != 0)
284     {
285         $table->{'Ascender'} = $self->{'sTypoAscender'};
286         $table->{'Descender'} = $self->{'sTypoDescender'};
287         $table->{'LineGap'} = $self->{'sTypoLineGap'};
288         $self->{'usWinAscent'} = $self->{'sTypoAscender'} + $self->{'sTypoLineGap'};
289         $self->{'usWinDescent'} = -$self->{'sTypoDescender'};
290     } 
291     elsif ($self->{'usWinAscent'} != 0 || $self->{'usWinDescent'} != 0)
292     {
293         $self->{'sTypoAscender'} = $table->{'Ascender'} = $self->{'usWinAscent'};
294         $self->{'sTypoDescender'} = $table->{'Descender'} = -$self->{'usWinDescent'};
295         $self->{'sTypoLineGap'} = $table->{'LineGap'} = 0;
296     }
297
298     if ($self->{'Version'} < 3)
299     {
300         for ($i = 0; $i < 26; $i++)
301         { $avg += $hmtx->{'advance'}[$map->{'val'}{$i + 0x0061}] * $weights[$i]; }
302         $avg += $hmtx->{'advance'}[$map->{'val'}{0x0020}] * $weights[-1];
303         $self->{'xAvgCharWidth'} = $avg / 1000;
304     }
305     elsif ($self->{'Version'} > 2)
306     {
307         $i = 0; $avg = 0;
308         foreach (@{$hmtx->{'advance'}})
309         {
310             next unless ($_);
311             $i++;
312             $avg += $_;
313         }
314         $avg /= $i if ($i);
315         $self->{'xAvgCharWidth'} = $avg;
316     }
317
318     foreach $i (keys %{$map->{'val'}})
319     {
320         if ($i >= 0x10000)
321         {
322             $self->{'ulUnicodeRange2'} |= 0x2000000;
323             last;
324         }
325     }
326
327     $self->{'Version'} = 1 if (defined $self->{'ulCodePageRange1'} && $self->{'Version'} < 1);
328     $self->{'Version'} = 2 if (defined $self->{'maxLookups'} && $self->{'Version'} < 2);
329     
330     if ((exists $self->{' PARENT'}{'GPOS'} && $self->{' PARENT'}{'GPOS'}{' read'}) ||
331         (exists $self->{' PARENT'}{'GSUB'} && $self->{' PARENT'}{'GSUB'}{' read'}))
332     {
333         # one or both of GPOS & GSUB exist and have been read or modified; so update usMaxContexts
334         my ($lp, $ls);
335         $lp = $self->{' PARENT'}{'GPOS'}->maxContext if exists $self->{' PARENT'}{'GPOS'};
336         $ls = $self->{' PARENT'}{'GSUB'}->maxContext if exists $self->{' PARENT'}{'GSUB'};
337         $self->{'maxLookups'} = $lp > $ls ? $lp : $ls;
338     }
339     
340     $self;
341 }
342
343 1;
344
345 =head1 BUGS
346
347 None known
348
349 =head1 AUTHOR
350
351 Martin Hosken Martin_Hosken@sil.org. See L<Font::TTF::Font> for copyright and
352 licensing.
353
354 =cut