1 package Font::TTF::GPOS;
5 Font::TTF::GPOS - Support for Opentype GPOS tables in conjunction with TTOpen
9 The GPOS table is one of the most complicated tables in the TTF spec and the
10 corresponding data structure abstraction is also not trivial. While much of the
11 structure of a GPOS is shared with a GSUB table via the L<Font::TTF::Ttopen>
13 =head1 INSTANCE VARIABLES
15 Here we describe the additions and lookup specific information for GPOS tables.
16 Unfortunately there is no one abstraction which seems to work comfortable for
17 all GPOS tables, so we will also examine how the variables are used for different
20 The following are the values allowed in the ACTION_TYPE and MATCH_TYPE variables:
26 This can take any of the following values
32 The ACTION is an array of anchor tables
36 Offset. There is no RULE array. The ADJUST variable contains a value record (see
37 later in this description)
41 The ACTION is a value record.
45 Pair adjustment. The ACTION contains an array of two value records for the matched
50 Exit and Entry records. The ACTION contains an array of two anchors corresponding
51 to the exit and entry anchors for the glyph.
55 Indicates a lookup based contextual rule as per the GSUB table.
61 This can take any of the following values
71 An array of class values
75 An array of coverage tables
81 The following variables are added for Attachment Positioning Subtables:
87 This contains an array of glyphs to match against for all RULES. It is much like
88 having the same MATCH string in all RULES. In the cases it is used so far, it only
89 ever contains one element.
93 This contains a Mark array consisting of each element being a subarray of two
100 The class that this mark uses on its base
104 The anchor with which to attach this mark glyph
108 The base table for mark to base, ligature or mark attachment positioning is
109 structured with the ACTION containing an array of anchors corresponding to each
110 attachment class. For ligatures, there is more than one RULE in the RULE array
111 corresponding to each glyph in the coverage table.
115 Other variables which are provided for informational purposes are:
121 Value format for the adjustment of the glyph matched by the coverage table.
125 Value format used in pair adjustment for the second glyph in the pair
131 There is a subtype used in GPOS tables called a value record. It is used to adjust
132 the position of a glyph from its default position. The value record is variable
133 length with a bitfield at the beginning to indicate which of the following
134 entries are included. The bitfield is not stored since it is recalculated at
141 Horizontal adjustment for placement (not affecting other unattached glyphs)
145 Vertical adjustment for placement (not affecting other unattached glyphs)
149 Adjust the advance width glyph (used only in horizontal writing systems)
153 Adjust the vertical advance (used only in vertical writing systems)
157 Device table for device specific adjustment of horizontal placement
161 Device table for device specific adjustment of vertical placement
165 Device table for device specific adjustment of horizontal advance
169 Device table for device specific adjustment of vertical advance
173 Horizontal placement metric id (for Multiple Master fonts - but that's all I know!)
177 Vertical placement metric id
181 Horizontal advance metric id
185 Vertical advance metric id
189 =head1 CORRESPONDANCE TO LAYOUT TYPES
191 Here is what is stored in the ACTION_TYPE and MATCH_TYPE for each of the known
194 1.1 1.2 2.1 2.2 3 4 5 6 7.1 7.2 7.3 8.1 8.2 8.3
195 ACTION_TYPE o v p p e a a a l l l l l l
196 MATCH_TYPE g c g c o g c o
204 use Font::TTF::Ttopen;
205 use Font::TTF::Delta;
206 use Font::TTF::Anchor;
207 use Font::TTF::Utils;
210 @ISA = qw(Font::TTF::Ttopen);
215 Reads the subtable into the data structures
221 my ($self, $fh, $main_lookup, $sindex) = @_;
222 my ($type) = $main_lookup->{'TYPE'};
223 my ($loc) = $fh->tell();
224 my ($lookup) = $main_lookup->{'SUB'}[$sindex];
225 my ($dat, $mcount, $scount, $i, $j, $count, $fmt, $fmt2, $cover, $srec, $subst);
226 my ($c1, $c2, $s, $moff, $boff);
232 ($fmt, $cover) = TTF_Unpack('S2', $dat);
236 $count = TTF_Unpack('S', $dat);
241 ($fmt, $cover, $count) = TTF_Unpack("S3", $dat);
243 unless ($fmt == 3 && ($type == 7 || $type == 8))
244 { $lookup->{'COVERAGE'} = $self->read_cover($cover, $loc, $lookup, $fh, 1); }
246 $lookup->{'FORMAT'} = $fmt;
247 if ($type == 1 && $fmt == 1)
249 $lookup->{'VFMT'} = $count;
250 $lookup->{'ADJUST'} = $self->read_value($count, $loc, $lookup, $fh);
251 $lookup->{'ACTION_TYPE'} = 'o';
252 } elsif ($type == 1 && $fmt == 2)
254 $lookup->{'VFMT'} = $count;
256 $mcount = unpack('n', $dat);
257 for ($i = 0; $i < $mcount; $i++)
258 { push (@{$lookup->{'RULES'}}, [{'ACTION' =>
259 [$self->read_value($count, $loc, $lookup, $fh)]}]); }
260 $lookup->{'ACTION_TYPE'} = 'v';
261 } elsif ($type == 2 && $fmt == 1)
263 $lookup->{'VFMT'} = $count;
265 ($fmt2, $mcount) = unpack('n2', $dat);
266 $lookup->{'VFMT2'} = $fmt2;
267 $fh->read($dat, $mcount << 1);
268 foreach $s (unpack('n*', $dat))
270 $fh->seek($loc + $s, 0);
272 $scount = TTF_Unpack('S', $dat);
274 for ($i = 0; $i < $scount; $i++)
278 $srec->{'MATCH'} = [TTF_Unpack('S', $dat)];
279 $srec->{'ACTION'} = [$self->read_value($count, $loc, $lookup, $fh),
280 $self->read_value($fmt2, $loc, $lookup, $fh)];
281 push (@$subst, $srec);
283 push (@{$lookup->{'RULES'}}, $subst);
285 $lookup->{'ACTION_TYPE'} = 'p';
286 $lookup->{'MATCH_TYPE'} = 'g';
287 } elsif ($type == 2 && $fmt == 2)
290 ($lookup->{'VFMT2'}, $c1, $c2, $mcount, $scount) = TTF_Unpack('S*', $dat);
291 $lookup->{'CLASS'} = $self->read_cover($c1, $loc, $lookup, $fh, 0);
292 $lookup->{'MATCH'} = [$self->read_cover($c2, $loc, $lookup, $fh, 0)];
293 $lookup->{'VFMT'} = $count;
294 for ($i = 0; $i < $mcount; $i++)
297 for ($j = 0; $j < $scount; $j++)
300 $srec->{'ACTION'} = [$self->read_value($lookup->{'VFMT'}, $loc, $lookup, $fh),
301 $self->read_value($lookup->{'VFMT2'}, $loc, $lookup, $fh)];
302 push (@$subst, $srec);
304 push (@{$lookup->{'RULES'}}, $subst);
306 $lookup->{'ACTION_TYPE'} = 'p';
307 $lookup->{'MATCH_TYPE'} = 'c';
308 } elsif ($type == 3 && $fmt == 1)
310 $fh->read($dat, $count << 2);
311 for ($i = 0; $i < $count; $i++)
312 { push (@{$lookup->{'RULES'}}, [{'ACTION' =>
313 [$self->read_anchor(TTF_Unpack('S', substr($dat, $i << 2, 2)),
315 $self->read_anchor(TTF_Unpack('S', substr($dat, ($i << 2) + 2, 2)),
316 $loc, $lookup, $fh)]}]); }
317 $lookup->{'ACTION_TYPE'} = 'e';
318 } elsif ($type == 4 || $type == 5 || $type == 6)
320 my (@offs, $mloc, $thisloc, $ncomp, $k);
322 $lookup->{'MATCH'} = [$lookup->{'COVERAGE'}];
323 $lookup->{'COVERAGE'} = $self->read_cover($count, $loc, $lookup, $fh, 1);
325 ($mcount, $moff, $boff) = TTF_Unpack('S*', $dat);
326 $fh->seek($loc + $moff, 0);
328 $count = TTF_Unpack('S', $dat);
329 for ($i = 0; $i < $count; $i++)
332 push (@{$lookup->{'MARKS'}}, [TTF_Unpack('S', $dat),
333 $self->read_anchor(TTF_Unpack('S', substr($dat, 2, 2)) + $moff,
334 $loc, $lookup, $fh)]);
336 $fh->seek($loc + $boff, 0);
338 $count = TTF_Unpack('S', $dat);
339 $mloc = $fh->tell() - 2;
343 $fh->read($dat, $count << 1);
344 @offs = TTF_Unpack('S*', $dat);
346 for ($i = 0; $i < $count; $i++)
350 $thisloc = $mloc + $offs[$i];
351 $fh->seek($thisloc, 0);
353 $ncomp = TTF_Unpack('S', $dat);
356 for ($j = 0; $j < $ncomp; $j++)
359 $fh->read($dat, $mcount << 1);
360 for ($k = 0; $k < $mcount; $k++)
361 { push (@$subst, $self->read_anchor(TTF_Unpack('S', substr($dat, $k << 1, 2)) + $thisloc - $loc,
362 $loc, $lookup, $fh)); }
364 push (@{$lookup->{'RULES'}[$i]}, {'ACTION' => $subst});
367 $lookup->{'ACTION_TYPE'} = 'a';
368 } elsif ($type == 7 || $type == 8)
369 { $self->read_context($lookup, $fh, $type - 2, $fmt, $cover, $count, $loc); }
376 Returns the table type number for the extension table
386 Outputs the subtable to the given filehandle
392 my ($self, $fh, $main_lookup, $index, $ctables, $base) = @_;
393 my ($type) = $main_lookup->{'TYPE'};
394 my ($lookup) = $main_lookup->{'SUB'}[$index];
395 my ($fmt) = $lookup->{'FORMAT'};
396 my ($out, $r, $s, $t, $i, $j, $vfmt, $vfmt2, $loc1);
397 my ($num) = $#{$lookup->{'RULES'}} + 1;
401 if ($type == 1 && $fmt == 1)
403 $out = pack('n2', $fmt, Font::TTF::Ttopen::ref_cache($lookup->{'COVERAGE'}, $ctables, 2 + $base));
404 $vfmt = $self->fmt_value($lookup->{'ADJUST'});
405 $out .= pack('n', $vfmt) . $self->out_value($lookup->{'ADJUST'}, $vfmt, $ctables, 6 + $base);
406 } elsif ($type == 1 && $fmt == 2)
409 foreach $r (@{$lookup->{'RULES'}})
410 { $vfmt |= $self->fmt_value($r->[0]{'ACTION'}[0]); }
411 $out = pack('n4', $fmt, Font::TTF::Ttopen::ref_cache($lookup->{'COVERAGE'}, $ctables, 2 + $base),
412 $vfmt, $#{$lookup->{'RULES'}} + 1);
413 foreach $r (@{$lookup->{'RULES'}})
414 { $out .= $self->out_value($r->[0]{'ACTION'}[0], $vfmt, $ctables, length($out) + $base); }
415 } elsif ($type == 2 && $fmt < 3)
419 foreach $r (@{$lookup->{'RULES'}})
423 $vfmt |= $self->fmt_value($t->{'ACTION'}[0]);
424 $vfmt2 |= $self->fmt_value($t->{'ACTION'}[1]);
429 # start PairPosFormat1 subtable
432 Font::TTF::Ttopen::ref_cache($lookup->{'COVERAGE'}, $ctables, 2 + $base),
435 $#{$lookup->{'RULES'}} + 1); # PairSetCount
437 $off += length($out);
438 $off += 2 * ($#{$lookup->{'RULES'}} + 1); # there will be PairSetCount offsets here
441 foreach $r (@{$lookup->{'RULES'}}) # foreach PairSet table
443 # write offset to this PairSet at end of PairPosFormat1 table
444 if (defined $cache{"$r"})
445 { $out .= pack('n', $cache{"$r"}); }
448 $out .= pack('n', $off);
451 # generate PairSet itself (using $off as eventual offset within PairPos subtable)
452 my $pairset = pack('n', $#{$r} + 1); # PairValueCount
453 foreach $t (@$r) # foreach PairValueRecord
455 $pairset .= pack('n', $t->{'MATCH'}[0]); # SecondGlyph - MATCH has only one entry
457 $self->out_value($t->{'ACTION'}[0], $vfmt, $ctables, $off + length($pairset) + $base);
459 $self->out_value($t->{'ACTION'}[1], $vfmt2, $ctables, $off + length($pairset) + $base);
461 $off += length($pairset);
462 $pairsets .= $pairset;
466 die "internal error: PairPos size not as calculated" if (length($out) != $off);
469 $out = pack('n8', $fmt, Font::TTF::Ttopen::ref_cache($lookup->{'COVERAGE'}, $ctables, 2 + $base),
471 Font::TTF::Ttopen::ref_cache($lookup->{'CLASS'}, $ctables, 8 + $base),
472 Font::TTF::Ttopen::ref_cache($lookup->{'MATCH'}[0], $ctables, 10 + $base),
473 $lookup->{'CLASS'}{'max'} + 1, $lookup->{'MATCH'}[0]{'max'} + 1);
475 for ($i = 0; $i <= $lookup->{'CLASS'}{'max'}; $i++)
477 for ($j = 0; $j <= $lookup->{'MATCH'}[0]{'max'}; $j++)
479 $out .= $self->out_value($lookup->{'RULES'}[$i][$j]{'ACTION'}[0], $vfmt, $ctables, length($out) + $base);
480 $out .= $self->out_value($lookup->{'RULES'}[$i][$j]{'ACTION'}[1], $vfmt2, $ctables, length($out) + $base);
484 } elsif ($type == 3 && $fmt == 1)
486 $out = pack('n3', $fmt, Font::TTF::Ttopen::ref_cache($lookup->{'COVERAGE'}, $ctables, 2 + $base),
487 $#{$lookup->{'RULES'}} + 1);
488 foreach $r (@{$lookup->{'RULES'}})
490 $out .= pack('n2', Font::TTF::Ttopen::ref_cache($r->[0]{'ACTION'}[0], $ctables, length($out) + $base),
491 Font::TTF::Ttopen::ref_cache($r->[0]{'ACTION'}[1], $ctables, length($out) + 2 + $base));
493 } elsif ($type == 4 || $type == 5 || $type == 6)
495 my ($loc_off, $loc_t, $ltables);
497 $out = pack('n7', $fmt, Font::TTF::Ttopen::ref_cache($lookup->{'MATCH'}[0], $ctables, 2 + $base),
498 Font::TTF::Ttopen::ref_cache($lookup->{'COVERAGE'}, $ctables, 4 + $base),
499 $#{$lookup->{'RULES'}[0][0]{'ACTION'}} + 1, 12, ($#{$lookup->{'MARKS'}} + 4) << 2,
500 $#{$lookup->{'MARKS'}} + 1);
501 foreach $r (@{$lookup->{'MARKS'}})
502 { $out .= pack('n2', $r->[0], Font::TTF::Ttopen::ref_cache($r->[1], $mtables, length($out) + 2)); }
503 push (@reftables, [$mtables, 12]);
505 $loc_t = length($out);
506 substr($out, 10, 2) = pack('n', $loc_t);
507 $out .= pack('n', $#{$lookup->{'RULES'}} + 1);
510 $loc1 = length($out);
511 $out .= pack('n*', (0) x ($#{$lookup->{'RULES'}} + 1));
514 for ($i = 0; $i <= $#{$lookup->{'RULES'}}; $i++)
519 $loc_t = length($out);
520 substr($out, $loc1 + ($i << 1), 2) = TTF_Pack('S', $loc_t - $loc1 + 2);
523 $r = $lookup->{'RULES'}[$i];
524 $out .= pack('n', $#{$r} + 1) if ($type == 5);
527 foreach $s (@{$t->{'ACTION'}})
528 { $out .= pack('n', Font::TTF::Ttopen::ref_cache($s, $ltables, length($out))); }
530 push (@reftables, [$ltables, $loc_t]) if ($type == 5);
532 push (@reftables, [$ltables, $loc_t]) unless ($type == 5);
533 $out = Font::TTF::Ttopen::out_final($fh, $out, \@reftables, 1);
534 } elsif ($type == 7 || $type == 8)
535 { $out = $self->out_context($lookup, $fh, $type - 2, $fmt, $ctables, $out, $num, $base); }
536 # push (@reftables, [$ctables, 0]);
541 =head2 $t->read_value($format, $base, $lookup, $fh)
543 Reads a value record from the current location in the file, according to the
550 my ($self, $fmt, $base, $lookup, $fh) = @_;
556 for ($i = 0; $i < 12; $i++)
558 $s++ if ($flag & $fmt);
562 $fh->read($dat, $s << 1);
564 foreach $s (qw(XPlacement YPlacement XAdvance YAdvance))
566 $res->{$s} = TTF_Unpack('s', substr($dat, $i++ << 1, 2)) if ($fmt & $flag);
570 foreach $s (qw(XPlaDevice YPlaDevice XAdvDevice YAdvDevice))
573 { $res->{$s} = $self->read_delta(TTF_Unpack('S', substr($i++ << 1, 2)),
574 $base, $lookup, $fh); }
578 foreach $s (qw(XIdPlacement YIdPlacement XIdAdvance YIdAdvance))
580 $res->{$s} = TTF_Unpack('S', substr($dat, $i++ << 1, 2)) if ($fmt & $flag);
587 =head2 $t->read_delta($offset, $base, $lookup, $fh)
589 Reads a delta (device table) at the given offset if it hasn't already been read.
590 Store the offset and item in the lookup cache ($lookup->{' CACHE'})
596 my ($self, $offset, $base, $lookup, $fh) = @_;
597 my ($loc) = $fh->tell();
600 return undef unless $offset;
601 $str = sprintf("%X", $base + $offset);
602 return $lookup->{' CACHE'}{$str} if defined $lookup->{' CACHE'}{$str};
603 $fh->seek($base + $offset, 0);
604 $res = Font::TTF::Delta->new->read($fh);
606 $lookup->{' CACHE'}{$str} = $res;
611 =head2 $t->read_anchor($offset, $base, $lookup, $fh)
613 Reads an Anchor table at the given offset if it hasn't already been read.
619 my ($self, $offset, $base, $lookup, $fh) = @_;
620 my ($loc) = $fh->tell();
623 return undef unless $offset;
624 $str = sprintf("%X", $base + $offset);
625 return $lookup->{' CACHE'}{$str} if defined $lookup->{' CACHE'}{$str};
626 $fh->seek($base + $offset, 0);
627 $res = Font::TTF::Anchor->new->read($fh);
629 $lookup->{' CACHE'}{$str} = $res;
636 Returns the value format for a given value record
642 my ($self, $value) = @_;
646 foreach $n (reverse qw(XPlacement YPlacement XAdvance YAdvance XPlaDevice YPlaDevice
647 XAdvDevice YAdvDevice XIdPlacement YIdPlacement XIdAdvance
651 $fmt |= 1 if (defined $value->{$n} && (ref $value->{$n} || $value->{$n}));
659 Returns the output string for the outputting of the value for a given format. Also
660 updates the offset cache for any device tables referenced.
666 my ($self, $value, $fmt, $tables, $offset) = @_;
667 my ($n, $flag, $out);
670 foreach $n (qw(XPlacement YPlacement XAdvance YAdvance))
672 $out .= pack('n', $value->{$n}) if ($flag & $fmt);
675 foreach $n (qw(XPlaDevice YPlaDevice XAdvDevice YAdvDevice))
679 $out .= pack('n', Font::TTF::Ttopen::ref_cache(
680 $value->{$n}, $tables, $offset + length($out)));
684 foreach $n (qw(XIdPlacement YIdPlacement XIdAdvance YIdAdvance))
686 $out .= pack('n', $value->{$n}) if ($flag & $fmt);
695 Martin Hosken Martin_Hosken@sil.org. See L<Font::TTF::Font> for copyright and