1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /* $Id$ */
19 
20 package org.apache.fop.complexscripts.fonts;
21 
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Map;
27 
28 import org.apache.commons.logging.Log;
29 import org.apache.commons.logging.LogFactory;
30 
31 import org.apache.fop.complexscripts.scripts.ScriptProcessor;
32 import org.apache.fop.complexscripts.util.GlyphSequence;
33 import org.apache.fop.complexscripts.util.GlyphTester;
34 
35 // CSOFF: LineLengthCheck
36 
37 /**
38  * <p>The <code>GlyphPositioningTable</code> class is a glyph table that implements
39  * <code>GlyphPositioning</code> functionality.</p>
40  *
41  * <p>This work was originally authored by Glenn Adams (gadams@apache.org).</p>
42  */
43 public class GlyphPositioningTable extends GlyphTable {
44 
45     /** logging instance */
46     private static final Log log = LogFactory.getLog(GlyphPositioningTable.class);
47 
48     /** single positioning subtable type */
49     public static final int GPOS_LOOKUP_TYPE_SINGLE = 1;
50     /** multiple positioning subtable type */
51     public static final int GPOS_LOOKUP_TYPE_PAIR = 2;
52     /** cursive positioning subtable type */
53     public static final int GPOS_LOOKUP_TYPE_CURSIVE = 3;
54     /** mark to base positioning subtable type */
55     public static final int GPOS_LOOKUP_TYPE_MARK_TO_BASE = 4;
56     /** mark to ligature positioning subtable type */
57     public static final int GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE = 5;
58     /** mark to mark positioning subtable type */
59     public static final int GPOS_LOOKUP_TYPE_MARK_TO_MARK = 6;
60     /** contextual positioning subtable type */
61     public static final int GPOS_LOOKUP_TYPE_CONTEXTUAL = 7;
62     /** chained contextual positioning subtable type */
63     public static final int GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL = 8;
64     /** extension positioning subtable type */
65     public static final int GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING = 9;
66 
67     /**
68      * Instantiate a <code>GlyphPositioningTable</code> object using the specified lookups
69      * and subtables.
70      * @param gdef glyph definition table that applies
71      * @param lookups a map of lookup specifications to subtable identifier strings
72      * @param subtables a list of identified subtables
73      */
GlyphPositioningTable(GlyphDefinitionTable gdef, Map lookups, List subtables, Map<String, ScriptProcessor> processors)74     public GlyphPositioningTable(GlyphDefinitionTable gdef, Map lookups, List subtables,
75                                  Map<String, ScriptProcessor> processors) {
76         super(gdef, lookups, processors);
77         if ((subtables == null) || (subtables.size() == 0)) {
78             throw new AdvancedTypographicTableFormatException("subtables must be non-empty");
79         } else {
80             for (Object o : subtables) {
81                 if (o instanceof GlyphPositioningSubtable) {
82                     addSubtable((GlyphSubtable) o);
83                 } else {
84                     throw new AdvancedTypographicTableFormatException("subtable must be a glyph positioning subtable");
85                 }
86             }
87             freezeSubtables();
88         }
89     }
90 
91     /**
92      * Map a lookup type name to its constant (integer) value.
93      * @param name lookup type name
94      * @return lookup type
95      */
getLookupTypeFromName(String name)96     public static int getLookupTypeFromName(String name) {
97         int t;
98         String s = name.toLowerCase();
99         if ("single".equals(s)) {
100             t = GPOS_LOOKUP_TYPE_SINGLE;
101         } else if ("pair".equals(s)) {
102             t = GPOS_LOOKUP_TYPE_PAIR;
103         } else if ("cursive".equals(s)) {
104             t = GPOS_LOOKUP_TYPE_CURSIVE;
105         } else if ("marktobase".equals(s)) {
106             t = GPOS_LOOKUP_TYPE_MARK_TO_BASE;
107         } else if ("marktoligature".equals(s)) {
108             t = GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE;
109         } else if ("marktomark".equals(s)) {
110             t = GPOS_LOOKUP_TYPE_MARK_TO_MARK;
111         } else if ("contextual".equals(s)) {
112             t = GPOS_LOOKUP_TYPE_CONTEXTUAL;
113         } else if ("chainedcontextual".equals(s)) {
114             t = GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
115         } else if ("extensionpositioning".equals(s)) {
116             t = GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING;
117         } else {
118             t = -1;
119         }
120         return t;
121     }
122 
123     /**
124      * Map a lookup type constant (integer) value to its name.
125      * @param type lookup type
126      * @return lookup type name
127      */
getLookupTypeName(int type)128     public static String getLookupTypeName(int type) {
129         String tn;
130         switch (type) {
131         case GPOS_LOOKUP_TYPE_SINGLE:
132             tn = "single";
133             break;
134         case GPOS_LOOKUP_TYPE_PAIR:
135             tn = "pair";
136             break;
137         case GPOS_LOOKUP_TYPE_CURSIVE:
138             tn = "cursive";
139             break;
140         case GPOS_LOOKUP_TYPE_MARK_TO_BASE:
141             tn = "marktobase";
142             break;
143         case GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE:
144             tn = "marktoligature";
145             break;
146         case GPOS_LOOKUP_TYPE_MARK_TO_MARK:
147             tn = "marktomark";
148             break;
149         case GPOS_LOOKUP_TYPE_CONTEXTUAL:
150             tn = "contextual";
151             break;
152         case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
153             tn = "chainedcontextual";
154             break;
155         case GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING:
156             tn = "extensionpositioning";
157             break;
158         default:
159             tn = "unknown";
160             break;
161         }
162         return tn;
163     }
164 
165     /**
166      * Create a positioning subtable according to the specified arguments.
167      * @param type subtable type
168      * @param id subtable identifier
169      * @param sequence subtable sequence
170      * @param flags subtable flags
171      * @param format subtable format
172      * @param coverage subtable coverage table
173      * @param entries subtable entries
174      * @return a glyph subtable instance
175      */
createSubtable(int type, String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)176     public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
177         GlyphSubtable st = null;
178         switch (type) {
179         case GPOS_LOOKUP_TYPE_SINGLE:
180             st = SingleSubtable.create(id, sequence, flags, format, coverage, entries);
181             break;
182         case GPOS_LOOKUP_TYPE_PAIR:
183             st = PairSubtable.create(id, sequence, flags, format, coverage, entries);
184             break;
185         case GPOS_LOOKUP_TYPE_CURSIVE:
186             st = CursiveSubtable.create(id, sequence, flags, format, coverage, entries);
187             break;
188         case GPOS_LOOKUP_TYPE_MARK_TO_BASE:
189             st = MarkToBaseSubtable.create(id, sequence, flags, format, coverage, entries);
190             break;
191         case GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE:
192             st = MarkToLigatureSubtable.create(id, sequence, flags, format, coverage, entries);
193             break;
194         case GPOS_LOOKUP_TYPE_MARK_TO_MARK:
195             st = MarkToMarkSubtable.create(id, sequence, flags, format, coverage, entries);
196             break;
197         case GPOS_LOOKUP_TYPE_CONTEXTUAL:
198             st = ContextualSubtable.create(id, sequence, flags, format, coverage, entries);
199             break;
200         case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL:
201             st = ChainedContextualSubtable.create(id, sequence, flags, format, coverage, entries);
202             break;
203         default:
204             break;
205         }
206         return st;
207     }
208 
209     /**
210      * Create a positioning subtable according to the specified arguments.
211      * @param type subtable type
212      * @param id subtable identifier
213      * @param sequence subtable sequence
214      * @param flags subtable flags
215      * @param format subtable format
216      * @param coverage list of coverage table entries
217      * @param entries subtable entries
218      * @return a glyph subtable instance
219      */
createSubtable(int type, String id, int sequence, int flags, int format, List coverage, List entries)220     public static GlyphSubtable createSubtable(int type, String id, int sequence, int flags, int format, List coverage, List entries) {
221         return createSubtable(type, id, sequence, flags, format, GlyphCoverageTable.createCoverageTable(coverage), entries);
222     }
223 
224     /**
225      * Perform positioning processing using all matching lookups.
226      * @param gs an input glyph sequence
227      * @param script a script identifier
228      * @param language a language identifier
229      * @param fontSize size in device units
230      * @param widths array of default advancements for each glyph
231      * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order,
232      * with one 4-tuple for each element of glyph sequence
233      * @return true if some adjustment is not zero; otherwise, false
234      */
position(GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments)235     public boolean position(GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments) {
236         Map<LookupSpec, List<LookupTable>> lookups = matchLookups(script, language, "*");
237         if ((lookups != null) && (lookups.size() > 0)) {
238             ScriptProcessor sp = ScriptProcessor.getInstance(script, processors);
239             return sp.position(this, gs, script, language, fontSize, lookups, widths, adjustments);
240         } else {
241             return false;
242         }
243     }
244 
245     private abstract static class SingleSubtable extends GlyphPositioningSubtable {
SingleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)246         SingleSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
247             super(id, sequence, flags, format, coverage);
248         }
249         /** {@inheritDoc} */
getType()250         public int getType() {
251             return GPOS_LOOKUP_TYPE_SINGLE;
252         }
253         /** {@inheritDoc} */
isCompatible(GlyphSubtable subtable)254         public boolean isCompatible(GlyphSubtable subtable) {
255             return subtable instanceof SingleSubtable;
256         }
257         /** {@inheritDoc} */
position(GlyphPositioningState ps)258         public boolean position(GlyphPositioningState ps) {
259             int gi = ps.getGlyph();
260             int ci;
261             if ((ci = getCoverageIndex(gi)) < 0) {
262                 return false;
263             } else {
264                 Value v = getValue(ci, gi);
265                 if (v != null) {
266                     if (ps.adjust(v)) {
267                         ps.setAdjusted(true);
268                     }
269                     ps.consume(1);
270                 }
271                 return true;
272             }
273         }
274         /**
275          * Obtain positioning value for coverage index.
276          * @param ci coverage index
277          * @param gi input glyph index
278          * @return positioning value or null if none applies
279          */
getValue(int ci, int gi)280         public abstract Value getValue(int ci, int gi);
create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)281         static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
282             if (format == 1) {
283                 return new SingleSubtableFormat1(id, sequence, flags, format, coverage, entries);
284             } else if (format == 2) {
285                 return new SingleSubtableFormat2(id, sequence, flags, format, coverage, entries);
286             } else {
287                 throw new UnsupportedOperationException();
288             }
289         }
290     }
291 
292     private static class SingleSubtableFormat1 extends SingleSubtable {
293         private Value value;
294         private int ciMax;
SingleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)295         SingleSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
296             super(id, sequence, flags, format, coverage, entries);
297             populate(entries);
298         }
299         /** {@inheritDoc} */
getEntries()300         public List getEntries() {
301             if (value != null) {
302                 List entries = new ArrayList(1);
303                 entries.add(value);
304                 return entries;
305             } else {
306                 return null;
307             }
308         }
309         /** {@inheritDoc} */
getValue(int ci, int gi)310         public Value getValue(int ci, int gi) {
311             if ((value != null) && (ci <= ciMax)) {
312                 return value;
313             } else {
314                 return null;
315             }
316         }
populate(List entries)317         private void populate(List entries) {
318             if ((entries == null) || (entries.size() != 1)) {
319                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null and contain exactly one entry");
320             } else {
321                 Value v;
322                 Object o = entries.get(0);
323                 if (o instanceof Value) {
324                     v = (Value) o;
325                 } else {
326                     throw new AdvancedTypographicTableFormatException("illegal entries entry, must be Value, but is: " + ((o != null) ? o.getClass() : null));
327                 }
328                 assert this.value == null;
329                 this.value = v;
330                 this.ciMax = getCoverageSize() - 1;
331             }
332         }
333     }
334 
335     private static class SingleSubtableFormat2 extends SingleSubtable {
336         private Value[] values;
SingleSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)337         SingleSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
338             super(id, sequence, flags, format, coverage, entries);
339             populate(entries);
340         }
341         /** {@inheritDoc} */
getEntries()342         public List getEntries() {
343             if (values != null) {
344                 List entries = new ArrayList(values.length);
345                 Collections.addAll(entries, values);
346                 return entries;
347             } else {
348                 return null;
349             }
350         }
351         /** {@inheritDoc} */
getValue(int ci, int gi)352         public Value getValue(int ci, int gi) {
353             if ((values != null) && (ci < values.length)) {
354                 return values [ ci ];
355             } else {
356                 return null;
357             }
358         }
populate(List entries)359         private void populate(List entries) {
360             if (entries == null) {
361                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
362             } else if (entries.size() != 1) {
363                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 1 entry");
364             } else {
365                 Object o;
366                 if (((o = entries.get(0)) == null) || !(o instanceof Value[])) {
367                     throw new AdvancedTypographicTableFormatException("illegal entries, single entry must be a Value[], but is: " + ((o != null) ? o.getClass() : null));
368                 } else {
369                     Value[] va = (Value[]) o;
370                     if (va.length != getCoverageSize()) {
371                         throw new AdvancedTypographicTableFormatException("illegal values array, " + entries.size() + " values present, but requires " + getCoverageSize() + " values");
372                     } else {
373                         assert this.values == null;
374                         this.values = va;
375                     }
376                 }
377             }
378         }
379     }
380 
381     private abstract static class PairSubtable extends GlyphPositioningSubtable {
PairSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)382         PairSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
383             super(id, sequence, flags, format, coverage);
384         }
385         /** {@inheritDoc} */
getType()386         public int getType() {
387             return GPOS_LOOKUP_TYPE_PAIR;
388         }
389         /** {@inheritDoc} */
isCompatible(GlyphSubtable subtable)390         public boolean isCompatible(GlyphSubtable subtable) {
391             return subtable instanceof PairSubtable;
392         }
393         /** {@inheritDoc} */
position(GlyphPositioningState ps)394         public boolean position(GlyphPositioningState ps) {
395             boolean applied = false;
396             int gi = ps.getGlyph(0);
397             int ci;
398             if ((ci = getCoverageIndex(gi)) >= 0) {
399                 int[] counts = ps.getGlyphsAvailable(0);
400                 int nga = counts[0];
401                 if (nga > 1) {
402                     int[] iga = ps.getGlyphs(0, 2, null, counts);
403                     if ((iga != null) && (iga.length == 2)) {
404                         PairValues pv = getPairValues(ci, iga[0], iga[1]);
405                         if (pv != null) {
406                             int offset = 0;
407                             int offsetLast = counts[0] + counts[1];
408                             // skip any ignored glyphs prior to first non-ignored glyph
409                             for ( ; offset < offsetLast; ++offset) {
410                                 if (!ps.isIgnoredGlyph(offset)) {
411                                     break;
412                                 } else {
413                                     ps.consume(1);
414                                 }
415                             }
416                             // adjust first non-ignored glyph if first value isn't null
417                             Value v1 = pv.getValue1();
418                             if (v1 != null) {
419                                 if (ps.adjust(v1, offset)) {
420                                     ps.setAdjusted(true);
421                                 }
422                                 ps.consume(1);          // consume first non-ignored glyph
423                                 ++offset;
424                             }
425                             // skip any ignored glyphs prior to second non-ignored glyph
426                             for ( ; offset < offsetLast; ++offset) {
427                                 if (!ps.isIgnoredGlyph(offset)) {
428                                     break;
429                                 } else {
430                                     ps.consume(1);
431                                 }
432                             }
433                             // adjust second non-ignored glyph if second value isn't null
434                             Value v2 = pv.getValue2();
435                             if (v2 != null) {
436                                 if (ps.adjust(v2, offset)) {
437                                     ps.setAdjusted(true);
438                                 }
439                                 ps.consume(1);          // consume second non-ignored glyph
440                                 ++offset;
441                             }
442                             applied = true;
443                         }
444                     }
445                 }
446             }
447             return applied;
448         }
449         /**
450          * Obtain associated pair values.
451          * @param ci coverage index
452          * @param gi1 first input glyph index
453          * @param gi2 second input glyph index
454          * @return pair values or null if none applies
455          */
getPairValues(int ci, int gi1, int gi2)456         public abstract PairValues getPairValues(int ci, int gi1, int gi2);
create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)457         static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
458             if (format == 1) {
459                 return new PairSubtableFormat1(id, sequence, flags, format, coverage, entries);
460             } else if (format == 2) {
461                 return new PairSubtableFormat2(id, sequence, flags, format, coverage, entries);
462             } else {
463                 throw new UnsupportedOperationException();
464             }
465         }
466     }
467 
468     private static class PairSubtableFormat1 extends PairSubtable {
469         private PairValues[][] pvm;                     // pair values matrix
PairSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)470         PairSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
471             super(id, sequence, flags, format, coverage, entries);
472             populate(entries);
473         }
474         /** {@inheritDoc} */
getEntries()475         public List getEntries() {
476             if (pvm != null) {
477                 List entries = new ArrayList(1);
478                 entries.add(pvm);
479                 return entries;
480             } else {
481                 return null;
482             }
483         }
484         /** {@inheritDoc} */
getPairValues(int ci, int gi1, int gi2)485         public PairValues getPairValues(int ci, int gi1, int gi2) {
486             if ((pvm != null) && (ci < pvm.length)) {
487                 PairValues[] pvt = pvm [ ci ];
488                 for (PairValues pv : pvt) {
489                     if (pv != null) {
490                         int g = pv.getGlyph();
491                         if (g < gi2) {
492                             continue;
493                         } else if (g == gi2) {
494                             return pv;
495                         } else {
496                             break;
497                         }
498                     }
499                 }
500             }
501             return null;
502         }
populate(List entries)503         private void populate(List entries) {
504             if (entries == null) {
505                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
506             } else if (entries.size() != 1) {
507                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 1 entry");
508             } else {
509                 Object o;
510                 if (((o = entries.get(0)) == null) || !(o instanceof PairValues[][])) {
511                     throw new AdvancedTypographicTableFormatException("illegal entries, first (and only) entry must be a PairValues[][], but is: " + ((o != null) ? o.getClass() : null));
512                 } else {
513                     pvm = (PairValues[][]) o;
514                 }
515             }
516         }
517     }
518 
519     private static class PairSubtableFormat2 extends PairSubtable {
520         private GlyphClassTable cdt1;                   // class def table 1
521         private GlyphClassTable cdt2;                   // class def table 2
522         private int nc1;                                // class 1 count
523         private int nc2;                                // class 2 count
524         private PairValues[][] pvm;                     // pair values matrix
PairSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)525         PairSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
526             super(id, sequence, flags, format, coverage, entries);
527             populate(entries);
528         }
529         /** {@inheritDoc} */
getEntries()530         public List getEntries() {
531             if (pvm != null) {
532                 List entries = new ArrayList(5);
533                 entries.add(cdt1);
534                 entries.add(cdt2);
535                 entries.add(nc1);
536                 entries.add(nc2);
537                 entries.add(pvm);
538                 return entries;
539             } else {
540                 return null;
541             }
542         }
543         /** {@inheritDoc} */
getPairValues(int ci, int gi1, int gi2)544         public PairValues getPairValues(int ci, int gi1, int gi2) {
545             if (pvm != null) {
546                 int c1 = cdt1.getClassIndex(gi1, 0);
547                 if ((c1 >= 0) && (c1 < nc1) && (c1 < pvm.length)) {
548                     PairValues[] pvt = pvm [ c1 ];
549                     if (pvt != null) {
550                         int c2 = cdt2.getClassIndex(gi2, 0);
551                         if ((c2 >= 0) && (c2 < nc2) && (c2 < pvt.length)) {
552                             return pvt [ c2 ];
553                         }
554                     }
555                 }
556             }
557             return null;
558         }
populate(List entries)559         private void populate(List entries) {
560             if (entries == null) {
561                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
562             } else if (entries.size() != 5) {
563                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 5 entries");
564             } else {
565                 Object o;
566                 if (((o = entries.get(0)) == null) || !(o instanceof GlyphClassTable)) {
567                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an GlyphClassTable, but is: " + ((o != null) ? o.getClass() : null));
568                 } else {
569                     cdt1 = (GlyphClassTable) o;
570                 }
571                 if (((o = entries.get(1)) == null) || !(o instanceof GlyphClassTable)) {
572                     throw new AdvancedTypographicTableFormatException("illegal entries, second entry must be an GlyphClassTable, but is: " + ((o != null) ? o.getClass() : null));
573                 } else {
574                     cdt2 = (GlyphClassTable) o;
575                 }
576                 if (((o = entries.get(2)) == null) || !(o instanceof Integer)) {
577                     throw new AdvancedTypographicTableFormatException("illegal entries, third entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
578                 } else {
579                     nc1 = (Integer) (o);
580                 }
581                 if (((o = entries.get(3)) == null) || !(o instanceof Integer)) {
582                     throw new AdvancedTypographicTableFormatException("illegal entries, fourth entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
583                 } else {
584                     nc2 = (Integer) (o);
585                 }
586                 if (((o = entries.get(4)) == null) || !(o instanceof PairValues[][])) {
587                     throw new AdvancedTypographicTableFormatException("illegal entries, fifth entry must be a PairValues[][], but is: " + ((o != null) ? o.getClass() : null));
588                 } else {
589                     pvm = (PairValues[][]) o;
590                 }
591             }
592         }
593     }
594 
595     private abstract static class CursiveSubtable extends GlyphPositioningSubtable {
CursiveSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)596         CursiveSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
597             super(id, sequence, flags, format, coverage);
598         }
599         /** {@inheritDoc} */
getType()600         public int getType() {
601             return GPOS_LOOKUP_TYPE_CURSIVE;
602         }
603         /** {@inheritDoc} */
isCompatible(GlyphSubtable subtable)604         public boolean isCompatible(GlyphSubtable subtable) {
605             return subtable instanceof CursiveSubtable;
606         }
607         /** {@inheritDoc} */
position(GlyphPositioningState ps)608         public boolean position(GlyphPositioningState ps) {
609             boolean applied = false;
610             int gi = ps.getGlyph(0);
611             int ci;
612             if ((ci = getCoverageIndex(gi)) >= 0) {
613                 int[] counts = ps.getGlyphsAvailable(0);
614                 int nga = counts[0];
615                 if (nga > 1) {
616                     int[] iga = ps.getGlyphs(0, 2, null, counts);
617                     if ((iga != null) && (iga.length == 2)) {
618                         // int gi1 = gi;
619                         int ci1 = ci;
620                         int gi2 = iga [ 1 ];
621                         int ci2 = getCoverageIndex(gi2);
622                         Anchor[] aa = getExitEntryAnchors(ci1, ci2);
623                         if (aa != null) {
624                             Anchor exa = aa [ 0 ];
625                             Anchor ena = aa [ 1 ];
626                             // int exw = ps.getWidth ( gi1 );
627                             int enw = ps.getWidth(gi2);
628                             if ((exa != null) && (ena != null)) {
629                                 Value v = ena.getAlignmentAdjustment(exa);
630                                 v.adjust(-enw, 0, 0, 0);
631                                 if (ps.adjust(v)) {
632                                     ps.setAdjusted(true);
633                                 }
634                             }
635                             // consume only first glyph of exit/entry glyph pair
636                             ps.consume(1);
637                             applied = true;
638                         }
639                     }
640                 }
641             }
642             return applied;
643         }
644         /**
645          * Obtain exit anchor for first glyph with coverage index <code>ci1</code> and entry anchor for second
646          * glyph with coverage index <code>ci2</code>.
647          * @param ci1 coverage index of first glyph (may be negative)
648          * @param ci2 coverage index of second glyph (may be negative)
649          * @return array of two anchors or null if either coverage index is negative or corresponding anchor is
650          * missing, where the first entry is the exit anchor of the first glyph and the second entry is the
651          * entry anchor of the second glyph
652          */
getExitEntryAnchors(int ci1, int ci2)653         public abstract Anchor[] getExitEntryAnchors(int ci1, int ci2);
create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)654         static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
655             if (format == 1) {
656                 return new CursiveSubtableFormat1(id, sequence, flags, format, coverage, entries);
657             } else {
658                 throw new UnsupportedOperationException();
659             }
660         }
661     }
662 
663     private static class CursiveSubtableFormat1 extends CursiveSubtable {
664         private Anchor[] aa;                            // anchor array, where even entries are entry anchors, and odd entries are exit anchors
CursiveSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)665         CursiveSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
666             super(id, sequence, flags, format, coverage, entries);
667             populate(entries);
668         }
669         /** {@inheritDoc} */
getEntries()670         public List getEntries() {
671             if (aa != null) {
672                 List entries = new ArrayList(1);
673                 entries.add(aa);
674                 return entries;
675             } else {
676                 return null;
677             }
678         }
679         /** {@inheritDoc} */
getExitEntryAnchors(int ci1, int ci2)680         public Anchor[] getExitEntryAnchors(int ci1, int ci2) {
681             if ((ci1 >= 0) && (ci2 >= 0)) {
682                 int ai1 = (ci1 * 2) + 1; // ci1 denotes glyph with exit anchor
683                 int ai2 = (ci2 * 2) + 0; // ci2 denotes glyph with entry anchor
684                 if ((aa != null) && (ai1 < aa.length) && (ai2 < aa.length)) {
685                     Anchor exa = aa [ ai1 ];
686                     Anchor ena = aa [ ai2 ];
687                     if ((exa != null) && (ena != null)) {
688                         return new Anchor[] { exa, ena };
689                     }
690                 }
691             }
692             return null;
693         }
populate(List entries)694         private void populate(List entries) {
695             if (entries == null) {
696                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
697             } else if (entries.size() != 1) {
698                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 1 entry");
699             } else {
700                 Object o;
701                 if (((o = entries.get(0)) == null) || !(o instanceof Anchor[])) {
702                     throw new AdvancedTypographicTableFormatException("illegal entries, first (and only) entry must be a Anchor[], but is: " + ((o != null) ? o.getClass() : null));
703                 } else if ((((Anchor[]) o) .length % 2) != 0) {
704                     throw new AdvancedTypographicTableFormatException("illegal entries, Anchor[] array must have an even number of entries, but has: " + ((Anchor[]) o) .length);
705                 } else {
706                     aa = (Anchor[]) o;
707                 }
708             }
709         }
710     }
711 
712     private abstract static class MarkToBaseSubtable extends GlyphPositioningSubtable {
MarkToBaseSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)713         MarkToBaseSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
714             super(id, sequence, flags, format, coverage);
715         }
716         /** {@inheritDoc} */
getType()717         public int getType() {
718             return GPOS_LOOKUP_TYPE_MARK_TO_BASE;
719         }
720         /** {@inheritDoc} */
isCompatible(GlyphSubtable subtable)721         public boolean isCompatible(GlyphSubtable subtable) {
722             return subtable instanceof MarkToBaseSubtable;
723         }
724         /** {@inheritDoc} */
position(GlyphPositioningState ps)725         public boolean position(GlyphPositioningState ps) {
726             boolean applied = false;
727             int giMark = ps.getGlyph();
728             int ciMark;
729             if ((ciMark = getCoverageIndex(giMark)) >= 0) {
730                 MarkAnchor ma = getMarkAnchor(ciMark, giMark);
731                 if (ma != null) {
732                     for (int i = 0, n = ps.getPosition(); i < n; i++) {
733                         int gi = ps.getGlyph(-(i + 1));
734                         int unprocessedGlyph = ps.getUnprocessedGlyph(-(i + 1));
735                         if (ps.isMark(gi) && ps.isMark(unprocessedGlyph)) {
736                             continue;
737                         } else {
738                             Anchor a = getBaseAnchor(gi, ma.getMarkClass());
739                             if (a != null) {
740                                 Value v = a.getAlignmentAdjustment(ma);
741                                 // start experimental fix for END OF AYAH in Lateef/Scheherazade
742                                 int[] aa = ps.getAdjustment();
743                                 if (aa[2] == 0) {
744                                     v.adjust(0, 0, -ps.getWidth(giMark), 0);
745                                 }
746                                 // end experimental fix for END OF AYAH in Lateef/Scheherazade
747                                 if (OTFScript.KHMER.equals(ps.script)) {
748                                     v.adjust(-ps.getWidth(gi), -v.yPlacement, 0, 0);
749                                 }
750                                 if (ps.adjust(v)) {
751                                     ps.setAdjusted(true);
752                                 }
753                             }
754                             ps.consume(1);
755                             applied = true;
756                             break;
757                         }
758                     }
759                 }
760             }
761             return applied;
762         }
763         /**
764          * Obtain mark anchor associated with mark coverage index.
765          * @param ciMark coverage index
766          * @param giMark input glyph index of mark glyph
767          * @return mark anchor or null if none applies
768          */
getMarkAnchor(int ciMark, int giMark)769         public abstract MarkAnchor getMarkAnchor(int ciMark, int giMark);
770         /**
771          * Obtain anchor associated with base glyph index and mark class.
772          * @param giBase input glyph index of base glyph
773          * @param markClass class number of mark glyph
774          * @return anchor or null if none applies
775          */
getBaseAnchor(int giBase, int markClass)776         public abstract Anchor getBaseAnchor(int giBase, int markClass);
create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)777         static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
778             if (format == 1) {
779                 return new MarkToBaseSubtableFormat1(id, sequence, flags, format, coverage, entries);
780             } else {
781                 throw new UnsupportedOperationException();
782             }
783         }
784     }
785 
786     private static class MarkToBaseSubtableFormat1 extends MarkToBaseSubtable {
787         private GlyphCoverageTable bct;                 // base coverage table
788         private int nmc;                                // mark class count
789         private MarkAnchor[] maa;                       // mark anchor array, ordered by mark coverage index
790         private Anchor[][] bam;                         // base anchor matrix, ordered by base coverage index, then by mark class
MarkToBaseSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)791         MarkToBaseSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
792             super(id, sequence, flags, format, coverage, entries);
793             populate(entries);
794         }
795         /** {@inheritDoc} */
getEntries()796         public List getEntries() {
797             if ((bct != null) && (maa != null) && (nmc > 0) && (bam != null)) {
798                 List entries = new ArrayList(4);
799                 entries.add(bct);
800                 entries.add(nmc);
801                 entries.add(maa);
802                 entries.add(bam);
803                 return entries;
804             } else {
805                 return null;
806             }
807         }
808         /** {@inheritDoc} */
getMarkAnchor(int ciMark, int giMark)809         public MarkAnchor getMarkAnchor(int ciMark, int giMark) {
810             if ((maa != null) && (ciMark < maa.length)) {
811                 return maa [ ciMark ];
812             } else {
813                 return null;
814             }
815         }
816         /** {@inheritDoc} */
getBaseAnchor(int giBase, int markClass)817         public Anchor getBaseAnchor(int giBase, int markClass) {
818             int ciBase;
819             if ((bct != null) && ((ciBase = bct.getCoverageIndex(giBase)) >= 0)) {
820                 if ((bam != null) && (ciBase < bam.length)) {
821                     Anchor[] ba = bam [ ciBase ];
822                     if ((ba != null) && (markClass < ba.length)) {
823                         return ba [ markClass ];
824                     }
825                 }
826             }
827             return null;
828         }
populate(List entries)829         private void populate(List entries) {
830             if (entries == null) {
831                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
832             } else if (entries.size() != 4) {
833                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 4 entries");
834             } else {
835                 Object o;
836                 if (((o = entries.get(0)) == null) || !(o instanceof GlyphCoverageTable)) {
837                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an GlyphCoverageTable, but is: " + ((o != null) ? o.getClass() : null));
838                 } else {
839                     bct = (GlyphCoverageTable) o;
840                 }
841                 if (((o = entries.get(1)) == null) || !(o instanceof Integer)) {
842                     throw new AdvancedTypographicTableFormatException("illegal entries, second entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
843                 } else {
844                     nmc = (Integer) (o);
845                 }
846                 if (((o = entries.get(2)) == null) || !(o instanceof MarkAnchor[])) {
847                     throw new AdvancedTypographicTableFormatException("illegal entries, third entry must be a MarkAnchor[], but is: " + ((o != null) ? o.getClass() : null));
848                 } else {
849                     maa = (MarkAnchor[]) o;
850                 }
851                 if (((o = entries.get(3)) == null) || !(o instanceof Anchor[][])) {
852                     throw new AdvancedTypographicTableFormatException("illegal entries, fourth entry must be a Anchor[][], but is: " + ((o != null) ? o.getClass() : null));
853                 } else {
854                     bam = (Anchor[][]) o;
855                 }
856             }
857         }
858     }
859 
860     private abstract static class MarkToLigatureSubtable extends GlyphPositioningSubtable {
MarkToLigatureSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)861         MarkToLigatureSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
862             super(id, sequence, flags, format, coverage);
863         }
864         /** {@inheritDoc} */
getType()865         public int getType() {
866             return GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE;
867         }
868         /** {@inheritDoc} */
isCompatible(GlyphSubtable subtable)869         public boolean isCompatible(GlyphSubtable subtable) {
870             return subtable instanceof MarkToLigatureSubtable;
871         }
872         /** {@inheritDoc} */
position(GlyphPositioningState ps)873         public boolean position(GlyphPositioningState ps) {
874             boolean applied = false;
875             int giMark = ps.getGlyph();
876             int ciMark;
877             if ((ciMark = getCoverageIndex(giMark)) >= 0) {
878                 MarkAnchor ma = getMarkAnchor(ciMark, giMark);
879                 int mxc = getMaxComponentCount();
880                 if (ma != null) {
881                     for (int i = 0, n = ps.getPosition(); i < n; i++) {
882                         int glyphIndex = ps.getUnprocessedGlyph(-(i + 1));
883                         if (ps.isMark(glyphIndex)) {
884                             continue;
885                         } else {
886                             Anchor anchor = getLigatureAnchor(glyphIndex, mxc, i, ma.getMarkClass());
887                             if (anchor != null) {
888                                 if (ps.adjust(anchor.getAlignmentAdjustment(ma))) {
889                                     ps.setAdjusted(true);
890                                 }
891                             }
892                             ps.consume(1);
893                             applied = true;
894                             break;
895                         }
896                     }
897                 }
898             }
899             return applied;
900         }
901         /**
902          * Obtain mark anchor associated with mark coverage index.
903          * @param ciMark coverage index
904          * @param giMark input glyph index of mark glyph
905          * @return mark anchor or null if none applies
906          */
getMarkAnchor(int ciMark, int giMark)907         public abstract MarkAnchor getMarkAnchor(int ciMark, int giMark);
908         /**
909          * Obtain maximum component count.
910          * @return maximum component count (>=0)
911          */
getMaxComponentCount()912         public abstract int getMaxComponentCount();
913         /**
914          * Obtain anchor associated with ligature glyph index and mark class.
915          * @param giLig input glyph index of ligature glyph
916          * @param maxComponents maximum component count
917          * @param component component number (0...maxComponents-1)
918          * @param markClass class number of mark glyph
919          * @return anchor or null if none applies
920          */
getLigatureAnchor(int giLig, int maxComponents, int component, int markClass)921         public abstract Anchor getLigatureAnchor(int giLig, int maxComponents, int component, int markClass);
create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)922         static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
923             if (format == 1) {
924                 return new MarkToLigatureSubtableFormat1(id, sequence, flags, format, coverage, entries);
925             } else {
926                 throw new UnsupportedOperationException();
927             }
928         }
929     }
930 
931     private static class MarkToLigatureSubtableFormat1 extends MarkToLigatureSubtable {
932         private GlyphCoverageTable lct;                 // ligature coverage table
933         private int nmc;                                // mark class count
934         private int mxc;                                // maximum ligature component count
935         private MarkAnchor[] maa;                       // mark anchor array, ordered by mark coverage index
936         private Anchor[][][] lam;                       // ligature anchor matrix, ordered by ligature coverage index, then ligature component, then mark class
MarkToLigatureSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)937         MarkToLigatureSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
938             super(id, sequence, flags, format, coverage, entries);
939             populate(entries);
940         }
941         /** {@inheritDoc} */
getEntries()942         public List getEntries() {
943             if (lam != null) {
944                 List entries = new ArrayList(5);
945                 entries.add(lct);
946                 entries.add(nmc);
947                 entries.add(mxc);
948                 entries.add(maa);
949                 entries.add(lam);
950                 return entries;
951             } else {
952                 return null;
953             }
954         }
955         /** {@inheritDoc} */
getMarkAnchor(int ciMark, int giMark)956         public MarkAnchor getMarkAnchor(int ciMark, int giMark) {
957             if ((maa != null) && (ciMark < maa.length)) {
958                 return maa [ ciMark ];
959             } else {
960                 return null;
961             }
962         }
963         /** {@inheritDoc} */
getMaxComponentCount()964         public int getMaxComponentCount() {
965             return mxc;
966         }
967         /** {@inheritDoc} */
getLigatureAnchor(int giLig, int maxComponents, int component, int markClass)968         public Anchor getLigatureAnchor(int giLig, int maxComponents, int component, int markClass) {
969             int ciLig;
970             if ((lct != null) && ((ciLig = lct.getCoverageIndex(giLig)) >= 0)) {
971                 if ((lam != null) && (ciLig < lam.length)) {
972                     Anchor[][] lcm = lam [ ciLig ];
973                     if (component < maxComponents) {
974                         Anchor[] la = lcm [ component ];
975                         if ((la != null) && (markClass < la.length)) {
976                             return la [ markClass ];
977                         }
978                     }
979                 }
980             }
981             return null;
982         }
populate(List entries)983         private void populate(List entries) {
984             if (entries == null) {
985                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
986             } else if (entries.size() != 5) {
987                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 5 entries");
988             } else {
989                 Object o;
990                 if (((o = entries.get(0)) == null) || !(o instanceof GlyphCoverageTable)) {
991                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an GlyphCoverageTable, but is: " + ((o != null) ? o.getClass() : null));
992                 } else {
993                     lct = (GlyphCoverageTable) o;
994                 }
995                 if (((o = entries.get(1)) == null) || !(o instanceof Integer)) {
996                     throw new AdvancedTypographicTableFormatException("illegal entries, second entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
997                 } else {
998                     nmc = (Integer) (o);
999                 }
1000                 if (((o = entries.get(2)) == null) || !(o instanceof Integer)) {
1001                     throw new AdvancedTypographicTableFormatException("illegal entries, third entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
1002                 } else {
1003                     mxc = (Integer) (o);
1004                 }
1005                 if (((o = entries.get(3)) == null) || !(o instanceof MarkAnchor[])) {
1006                     throw new AdvancedTypographicTableFormatException("illegal entries, fourth entry must be a MarkAnchor[], but is: " + ((o != null) ? o.getClass() : null));
1007                 } else {
1008                     maa = (MarkAnchor[]) o;
1009                 }
1010                 if (((o = entries.get(4)) == null) || !(o instanceof Anchor[][][])) {
1011                     throw new AdvancedTypographicTableFormatException("illegal entries, fifth entry must be a Anchor[][][], but is: " + ((o != null) ? o.getClass() : null));
1012                 } else {
1013                     lam = (Anchor[][][]) o;
1014                 }
1015             }
1016         }
1017     }
1018 
1019     private abstract static class MarkToMarkSubtable extends GlyphPositioningSubtable {
MarkToMarkSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)1020         MarkToMarkSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1021             super(id, sequence, flags, format, coverage);
1022         }
1023         /** {@inheritDoc} */
getType()1024         public int getType() {
1025             return GPOS_LOOKUP_TYPE_MARK_TO_MARK;
1026         }
1027         /** {@inheritDoc} */
isCompatible(GlyphSubtable subtable)1028         public boolean isCompatible(GlyphSubtable subtable) {
1029             return subtable instanceof MarkToMarkSubtable;
1030         }
1031         /** {@inheritDoc} */
position(GlyphPositioningState ps)1032         public boolean position(GlyphPositioningState ps) {
1033             boolean applied = false;
1034             int giMark1 = ps.getGlyph();
1035             int ciMark1;
1036             if ((ciMark1 = getCoverageIndex(giMark1)) >= 0) {
1037                 MarkAnchor ma = getMark1Anchor(ciMark1, giMark1);
1038                 if (ma != null) {
1039                     if (ps.hasPrev()) {
1040                         Anchor anchor = getMark2Anchor(ps.getUnprocessedGlyph(-1), ma.getMarkClass());
1041                         if (anchor != null) {
1042                             if (ps.adjust(anchor.getAlignmentAdjustment(ma))) {
1043                                 ps.setAdjusted(true);
1044                             }
1045                         }
1046                         ps.consume(1);
1047                         applied = true;
1048                     }
1049                 }
1050             }
1051             return applied;
1052         }
1053         /**
1054          * Obtain mark 1 anchor associated with mark 1 coverage index.
1055          * @param ciMark1 mark 1 coverage index
1056          * @param giMark1 input glyph index of mark 1 glyph
1057          * @return mark 1 anchor or null if none applies
1058          */
getMark1Anchor(int ciMark1, int giMark1)1059         public abstract MarkAnchor getMark1Anchor(int ciMark1, int giMark1);
1060         /**
1061          * Obtain anchor associated with mark 2 glyph index and mark 1 class.
1062          * @param giMark2 input glyph index of mark 2 glyph
1063          * @param markClass class number of mark 1 glyph
1064          * @return anchor or null if none applies
1065          */
getMark2Anchor(int giBase, int markClass)1066         public abstract Anchor getMark2Anchor(int giBase, int markClass);
create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)1067         static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1068             if (format == 1) {
1069                 return new MarkToMarkSubtableFormat1(id, sequence, flags, format, coverage, entries);
1070             } else {
1071                 throw new UnsupportedOperationException();
1072             }
1073         }
1074     }
1075 
1076     private static class MarkToMarkSubtableFormat1 extends MarkToMarkSubtable {
1077         private GlyphCoverageTable mct2;                // mark 2 coverage table
1078         private int nmc;                                // mark class count
1079         private MarkAnchor[] maa;                       // mark1 anchor array, ordered by mark1 coverage index
1080         private Anchor[][] mam;                         // mark2 anchor matrix, ordered by mark2 coverage index, then by mark1 class
MarkToMarkSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)1081         MarkToMarkSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1082             super(id, sequence, flags, format, coverage, entries);
1083             populate(entries);
1084         }
1085         /** {@inheritDoc} */
getEntries()1086         public List getEntries() {
1087             if ((mct2 != null) && (maa != null) && (nmc > 0) && (mam != null)) {
1088                 List entries = new ArrayList(4);
1089                 entries.add(mct2);
1090                 entries.add(nmc);
1091                 entries.add(maa);
1092                 entries.add(mam);
1093                 return entries;
1094             } else {
1095                 return null;
1096             }
1097         }
1098         /** {@inheritDoc} */
getMark1Anchor(int ciMark1, int giMark1)1099         public MarkAnchor getMark1Anchor(int ciMark1, int giMark1) {
1100             if ((maa != null) && (ciMark1 < maa.length)) {
1101                 return maa [ ciMark1 ];
1102             } else {
1103                 return null;
1104             }
1105         }
1106         /** {@inheritDoc} */
getMark2Anchor(int giMark2, int markClass)1107         public Anchor getMark2Anchor(int giMark2, int markClass) {
1108             int ciMark2;
1109             if ((mct2 != null) && ((ciMark2 = mct2.getCoverageIndex(giMark2)) >= 0)) {
1110                 if ((mam != null) && (ciMark2 < mam.length)) {
1111                     Anchor[] ma = mam [ ciMark2 ];
1112                     if ((ma != null) && (markClass < ma.length)) {
1113                         return ma [ markClass ];
1114                     }
1115                 }
1116             }
1117             return null;
1118         }
populate(List entries)1119         private void populate(List entries) {
1120             if (entries == null) {
1121                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
1122             } else if (entries.size() != 4) {
1123                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 4 entries");
1124             } else {
1125                 Object o;
1126                 if (((o = entries.get(0)) == null) || !(o instanceof GlyphCoverageTable)) {
1127                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an GlyphCoverageTable, but is: " + ((o != null) ? o.getClass() : null));
1128                 } else {
1129                     mct2 = (GlyphCoverageTable) o;
1130                 }
1131                 if (((o = entries.get(1)) == null) || !(o instanceof Integer)) {
1132                     throw new AdvancedTypographicTableFormatException("illegal entries, second entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
1133                 } else {
1134                     nmc = (Integer) (o);
1135                 }
1136                 if (((o = entries.get(2)) == null) || !(o instanceof MarkAnchor[])) {
1137                     throw new AdvancedTypographicTableFormatException("illegal entries, third entry must be a MarkAnchor[], but is: " + ((o != null) ? o.getClass() : null));
1138                 } else {
1139                     maa = (MarkAnchor[]) o;
1140                 }
1141                 if (((o = entries.get(3)) == null) || !(o instanceof Anchor[][])) {
1142                     throw new AdvancedTypographicTableFormatException("illegal entries, fourth entry must be a Anchor[][], but is: " + ((o != null) ? o.getClass() : null));
1143                 } else {
1144                     mam = (Anchor[][]) o;
1145                 }
1146             }
1147         }
1148     }
1149 
1150     private abstract static class ContextualSubtable extends GlyphPositioningSubtable {
ContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)1151         ContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1152             super(id, sequence, flags, format, coverage);
1153         }
1154         /** {@inheritDoc} */
getType()1155         public int getType() {
1156             return GPOS_LOOKUP_TYPE_CONTEXTUAL;
1157         }
1158         /** {@inheritDoc} */
isCompatible(GlyphSubtable subtable)1159         public boolean isCompatible(GlyphSubtable subtable) {
1160             return subtable instanceof ContextualSubtable;
1161         }
1162         /** {@inheritDoc} */
position(GlyphPositioningState ps)1163         public boolean position(GlyphPositioningState ps) {
1164             boolean applied = false;
1165             int gi = ps.getGlyph();
1166             int ci;
1167             if ((ci = getCoverageIndex(gi)) >= 0) {
1168                 int[] rv = new int[1];
1169                 RuleLookup[] la = getLookups(ci, gi, ps, rv);
1170                 if (la != null) {
1171                     ps.apply(la, rv[0]);
1172                     applied = true;
1173                 }
1174             }
1175             return applied;
1176         }
1177         /**
1178          * Obtain rule lookups set associated current input glyph context.
1179          * @param ci coverage index of glyph at current position
1180          * @param gi glyph index of glyph at current position
1181          * @param ps glyph positioning state
1182          * @param rv array of ints used to receive multiple return values, must be of length 1 or greater,
1183          * where the first entry is used to return the input sequence length of the matched rule
1184          * @return array of rule lookups or null if none applies
1185          */
getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv)1186         public abstract RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv);
create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)1187         static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1188             if (format == 1) {
1189                 return new ContextualSubtableFormat1(id, sequence, flags, format, coverage, entries);
1190             } else if (format == 2) {
1191                 return new ContextualSubtableFormat2(id, sequence, flags, format, coverage, entries);
1192             } else if (format == 3) {
1193                 return new ContextualSubtableFormat3(id, sequence, flags, format, coverage, entries);
1194             } else {
1195                 throw new UnsupportedOperationException();
1196             }
1197         }
1198     }
1199 
1200     private static class ContextualSubtableFormat1 extends ContextualSubtable {
1201         private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
ContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries)1202         ContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1203             super(id, sequence, flags, format, coverage, entries);
1204             populate(entries);
1205         }
1206         /** {@inheritDoc} */
getEntries()1207         public List getEntries() {
1208             if (rsa != null) {
1209                 List entries = new ArrayList(1);
1210                 entries.add(rsa);
1211                 return entries;
1212             } else {
1213                 return null;
1214             }
1215         }
1216         /** {@inheritDoc} */
resolveLookupReferences(Map<String, LookupTable> lookupTables)1217         public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
1218             GlyphTable.resolveLookupReferences(rsa, lookupTables);
1219         }
1220         /** {@inheritDoc} */
getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv)1221         public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
1222             assert ps != null;
1223             assert (rv != null) && (rv.length > 0);
1224             assert rsa != null;
1225             if (rsa.length > 0) {
1226                 RuleSet rs = rsa [ 0 ];
1227                 if (rs != null) {
1228                     Rule[] ra = rs.getRules();
1229                     for (Rule r : ra) {
1230                         if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) {
1231                             ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
1232                             int[] iga = cr.getGlyphs(gi);
1233                             if (matches(ps, iga, 0, rv)) {
1234                                 return r.getLookups();
1235                             }
1236                         }
1237                     }
1238                 }
1239             }
1240             return null;
1241         }
matches(GlyphPositioningState ps, int[] glyphs, int offset, int[] rv)1242         static boolean matches(GlyphPositioningState ps, int[] glyphs, int offset, int[] rv) {
1243             if ((glyphs == null) || (glyphs.length == 0)) {
1244                 return true;                            // match null or empty glyph sequence
1245             } else {
1246                 boolean reverse = offset < 0;
1247                 GlyphTester ignores = ps.getIgnoreDefault();
1248                 int[] counts = ps.getGlyphsAvailable(offset, reverse, ignores);
1249                 int nga = counts[0];
1250                 int ngm = glyphs.length;
1251                 if (nga < ngm) {
1252                     return false;                       // insufficient glyphs available to match
1253                 } else {
1254                     int[] ga = ps.getGlyphs(offset, ngm, reverse, ignores, null, counts);
1255                     for (int k = 0; k < ngm; k++) {
1256                         if (ga [ k ] != glyphs [ k ]) {
1257                             return false;               // match fails at ga [ k ]
1258                         }
1259                     }
1260                     if (rv != null) {
1261                         rv[0] = counts[0] + counts[1];
1262                     }
1263                     return true;                        // all glyphs match
1264                 }
1265             }
1266         }
1267         private void populate(List entries) {
1268             if (entries == null) {
1269                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
1270             } else if (entries.size() != 1) {
1271                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 1 entry");
1272             } else {
1273                 Object o;
1274                 if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
1275                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
1276                 } else {
1277                     rsa = (RuleSet[]) o;
1278                 }
1279             }
1280         }
1281     }
1282 
1283     private static class ContextualSubtableFormat2 extends ContextualSubtable {
1284         private GlyphClassTable cdt;                    // class def table
1285         private int ngc;                                // class set count
1286         private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
1287         ContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1288             super(id, sequence, flags, format, coverage, entries);
1289             populate(entries);
1290         }
1291         /** {@inheritDoc} */
1292         public List getEntries() {
1293             if (rsa != null) {
1294                 List entries = new ArrayList(3);
1295                 entries.add(cdt);
1296                 entries.add(ngc);
1297                 entries.add(rsa);
1298                 return entries;
1299             } else {
1300                 return null;
1301             }
1302         }
1303         /** {@inheritDoc} */
1304         public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
1305             GlyphTable.resolveLookupReferences(rsa, lookupTables);
1306         }
1307         /** {@inheritDoc} */
1308         public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
1309             assert ps != null;
1310             assert (rv != null) && (rv.length > 0);
1311             assert rsa != null;
1312             if (rsa.length > 0) {
1313                 RuleSet rs = rsa [ 0 ];
1314                 if (rs != null) {
1315                     Rule[] ra = rs.getRules();
1316                     for (Rule r : ra) {
1317                         if ((r != null) && (r instanceof ChainedClassSequenceRule)) {
1318                             ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
1319                             int[] ca = cr.getClasses(cdt.getClassIndex(gi, ps.getClassMatchSet(gi)));
1320                             if (matches(ps, cdt, ca, 0, rv)) {
1321                                 return r.getLookups();
1322                             }
1323                         }
1324                     }
1325                 }
1326             }
1327             return null;
1328         }
1329         static boolean matches(GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv) {
1330             if ((cdt == null) || (classes == null) || (classes.length == 0)) {
1331                 return true;                            // match null class definitions, null or empty class sequence
1332             } else {
1333                 boolean reverse = offset < 0;
1334                 GlyphTester ignores = ps.getIgnoreDefault();
1335                 int[] counts = ps.getGlyphsAvailable(offset, reverse, ignores);
1336                 int nga = counts[0];
1337                 int ngm = classes.length;
1338                 if (nga < ngm) {
1339                     return false;                       // insufficient glyphs available to match
1340                 } else {
1341                     int[] ga = ps.getGlyphs(offset, ngm, reverse, ignores, null, counts);
1342                     for (int k = 0; k < ngm; k++) {
1343                         int gi = ga [ k ];
1344                         int ms = ps.getClassMatchSet(gi);
1345                         int gc = cdt.getClassIndex(gi, ms);
1346                         if ((gc < 0) || (gc >= cdt.getClassSize(ms))) {
1347                             return false;               // none or invalid class fails mat ch
1348                         } else if (gc != classes [ k ]) {
1349                             return false;               // match fails at ga [ k ]
1350                         }
1351                     }
1352                     if (rv != null) {
1353                         rv[0] = counts[0] + counts[1];
1354                     }
1355                     return true;                        // all glyphs match
1356                 }
1357             }
1358         }
1359         private void populate(List entries) {
1360             if (entries == null) {
1361                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
1362             } else if (entries.size() != 3) {
1363                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 3 entries");
1364             } else {
1365                 Object o;
1366                 if (((o = entries.get(0)) == null) || !(o instanceof GlyphClassTable)) {
1367                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an GlyphClassTable, but is: " + ((o != null) ? o.getClass() : null));
1368                 } else {
1369                     cdt = (GlyphClassTable) o;
1370                 }
1371                 if (((o = entries.get(1)) == null) || !(o instanceof Integer)) {
1372                     throw new AdvancedTypographicTableFormatException("illegal entries, second entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
1373                 } else {
1374                     ngc = (Integer) (o);
1375                 }
1376                 if (((o = entries.get(2)) == null) || !(o instanceof RuleSet[])) {
1377                     throw new AdvancedTypographicTableFormatException("illegal entries, third entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
1378                 } else {
1379                     rsa = (RuleSet[]) o;
1380                     if (rsa.length != ngc) {
1381                         throw new AdvancedTypographicTableFormatException("illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes");
1382                     }
1383                 }
1384             }
1385         }
1386     }
1387 
1388     private static class ContextualSubtableFormat3 extends ContextualSubtable {
1389         private RuleSet[] rsa;                          // rule set array, containing a single rule set
1390         ContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1391             super(id, sequence, flags, format, coverage, entries);
1392             populate(entries);
1393         }
1394         /** {@inheritDoc} */
1395         public List getEntries() {
1396             if (rsa != null) {
1397                 List entries = new ArrayList(1);
1398                 entries.add(rsa);
1399                 return entries;
1400             } else {
1401                 return null;
1402             }
1403         }
1404         /** {@inheritDoc} */
1405         public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
1406             GlyphTable.resolveLookupReferences(rsa, lookupTables);
1407         }
1408         /** {@inheritDoc} */
1409         public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
1410             assert ps != null;
1411             assert (rv != null) && (rv.length > 0);
1412             assert rsa != null;
1413             if (rsa.length > 0) {
1414                 RuleSet rs = rsa [ 0 ];
1415                 if (rs != null) {
1416                     Rule[] ra = rs.getRules();
1417                     for (Rule r : ra) {
1418                         if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) {
1419                             ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
1420                             GlyphCoverageTable[] gca = cr.getCoverages();
1421                             if (matches(ps, gca, 0, rv)) {
1422                                 return r.getLookups();
1423                             }
1424                         }
1425                     }
1426                 }
1427             }
1428             return null;
1429         }
1430         static boolean matches(GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv) {
1431             if ((gca == null) || (gca.length == 0)) {
1432                 return true;                            // match null or empty coverage array
1433             } else {
1434                 boolean reverse = offset < 0;
1435                 GlyphTester ignores = ps.getIgnoreDefault();
1436                 int[] counts = ps.getGlyphsAvailable(offset, reverse, ignores);
1437                 int nga = counts[0];
1438                 int ngm = gca.length;
1439                 if (nga < ngm) {
1440                     return false;                       // insufficient glyphs available to match
1441                 } else {
1442                     int[] ga = ps.getGlyphs(offset, ngm, reverse, ignores, null, counts);
1443                     for (int k = 0; k < ngm; k++) {
1444                         GlyphCoverageTable ct = gca [ k ];
1445                         if (ct != null) {
1446                             if (ct.getCoverageIndex(ga [ k ]) < 0) {
1447                                 return false;           // match fails at ga [ k ]
1448                             }
1449                         }
1450                     }
1451                     if (rv != null) {
1452                         rv[0] = counts[0] + counts[1];
1453                     }
1454                     return true;                        // all glyphs match
1455                 }
1456             }
1457         }
1458         private void populate(List entries) {
1459             if (entries == null) {
1460                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
1461             } else if (entries.size() != 1) {
1462                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 1 entry");
1463             } else {
1464                 Object o;
1465                 if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
1466                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
1467                 } else {
1468                     rsa = (RuleSet[]) o;
1469                 }
1470             }
1471         }
1472     }
1473 
1474     private abstract static class ChainedContextualSubtable extends GlyphPositioningSubtable {
1475         ChainedContextualSubtable(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1476             super(id, sequence, flags, format, coverage);
1477         }
1478         /** {@inheritDoc} */
1479         public int getType() {
1480             return GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL;
1481         }
1482         /** {@inheritDoc} */
1483         public boolean isCompatible(GlyphSubtable subtable) {
1484             return subtable instanceof ChainedContextualSubtable;
1485         }
1486         /** {@inheritDoc} */
1487         public boolean position(GlyphPositioningState ps) {
1488             boolean applied = false;
1489             int gi = ps.getGlyph();
1490             int ci;
1491             if ((ci = getCoverageIndex(gi)) >= 0) {
1492                 int[] rv = new int[1];
1493                 RuleLookup[] la = getLookups(ci, gi, ps, rv);
1494                 if (la != null) {
1495                     ps.apply(la, rv[0]);
1496                     applied = true;
1497                 }
1498             }
1499             return applied;
1500         }
1501         /**
1502          * Obtain rule lookups set associated current input glyph context.
1503          * @param ci coverage index of glyph at current position
1504          * @param gi glyph index of glyph at current position
1505          * @param ps glyph positioning state
1506          * @param rv array of ints used to receive multiple return values, must be of length 1 or greater,
1507          * where the first entry is used to return the input sequence length of the matched rule
1508          * @return array of rule lookups or null if none applies
1509          */
1510         public abstract RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv);
1511         static GlyphPositioningSubtable create(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1512             if (format == 1) {
1513                 return new ChainedContextualSubtableFormat1(id, sequence, flags, format, coverage, entries);
1514             } else if (format == 2) {
1515                 return new ChainedContextualSubtableFormat2(id, sequence, flags, format, coverage, entries);
1516             } else if (format == 3) {
1517                 return new ChainedContextualSubtableFormat3(id, sequence, flags, format, coverage, entries);
1518             } else {
1519                 throw new UnsupportedOperationException();
1520             }
1521         }
1522     }
1523 
1524     private static class ChainedContextualSubtableFormat1 extends ChainedContextualSubtable {
1525         private RuleSet[] rsa;                          // rule set array, ordered by glyph coverage index
1526         ChainedContextualSubtableFormat1(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1527             super(id, sequence, flags, format, coverage, entries);
1528             populate(entries);
1529         }
1530         /** {@inheritDoc} */
1531         public List getEntries() {
1532             if (rsa != null) {
1533                 List entries = new ArrayList(1);
1534                 entries.add(rsa);
1535                 return entries;
1536             } else {
1537                 return null;
1538             }
1539         }
1540         /** {@inheritDoc} */
1541         public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
1542             GlyphTable.resolveLookupReferences(rsa, lookupTables);
1543         }
1544         /** {@inheritDoc} */
1545         public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
1546             assert ps != null;
1547             assert (rv != null) && (rv.length > 0);
1548             assert rsa != null;
1549             if (rsa.length > 0) {
1550                 RuleSet rs = rsa [ 0 ];
1551                 if (rs != null) {
1552                     Rule[] ra = rs.getRules();
1553                     for (Rule r : ra) {
1554                         if ((r != null) && (r instanceof ChainedGlyphSequenceRule)) {
1555                             ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r;
1556                             int[] iga = cr.getGlyphs(gi);
1557                             if (matches(ps, iga, 0, rv)) {
1558                                 int[] bga = cr.getBacktrackGlyphs();
1559                                 if (matches(ps, bga, -1, null)) {
1560                                     int[] lga = cr.getLookaheadGlyphs();
1561                                     if (matches(ps, lga, rv[0], null)) {
1562                                         return r.getLookups();
1563                                     }
1564                                 }
1565                             }
1566                         }
1567                     }
1568                 }
1569             }
1570             return null;
1571         }
1572         private boolean matches(GlyphPositioningState ps, int[] glyphs, int offset, int[] rv) {
1573             return ContextualSubtableFormat1.matches(ps, glyphs, offset, rv);
1574         }
1575         private void populate(List entries) {
1576             if (entries == null) {
1577                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
1578             } else if (entries.size() != 1) {
1579                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 1 entry");
1580             } else {
1581                 Object o;
1582                 if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
1583                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
1584                 } else {
1585                     rsa = (RuleSet[]) o;
1586                 }
1587             }
1588         }
1589     }
1590 
1591     private static class ChainedContextualSubtableFormat2 extends ChainedContextualSubtable {
1592         private GlyphClassTable icdt;                   // input class def table
1593         private GlyphClassTable bcdt;                   // backtrack class def table
1594         private GlyphClassTable lcdt;                   // lookahead class def table
1595         private int ngc;                                // class set count
1596         private RuleSet[] rsa;                          // rule set array, ordered by class number [0...ngc - 1]
1597         ChainedContextualSubtableFormat2(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1598             super(id, sequence, flags, format, coverage, entries);
1599             populate(entries);
1600         }
1601         /** {@inheritDoc} */
1602         public List getEntries() {
1603             if (rsa != null) {
1604                 List entries = new ArrayList(5);
1605                 entries.add(icdt);
1606                 entries.add(bcdt);
1607                 entries.add(lcdt);
1608                 entries.add(ngc);
1609                 entries.add(rsa);
1610                 return entries;
1611             } else {
1612                 return null;
1613             }
1614         }
1615         /** {@inheritDoc} */
1616         public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
1617             GlyphTable.resolveLookupReferences(rsa, lookupTables);
1618         }
1619         /** {@inheritDoc} */
1620         public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
1621             assert ps != null;
1622             assert (rv != null) && (rv.length > 0);
1623             assert rsa != null;
1624             if (rsa.length > 0) {
1625                 RuleSet rs = rsa [ 0 ];
1626                 if (rs != null) {
1627                     Rule[] ra = rs.getRules();
1628                     for (Rule r : ra) {
1629                         if ((r != null) && (r instanceof ChainedClassSequenceRule)) {
1630                             ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r;
1631                             int[] ica = cr.getClasses(icdt.getClassIndex(gi, ps.getClassMatchSet(gi)));
1632                             if (matches(ps, icdt, ica, 0, rv)) {
1633                                 int[] bca = cr.getBacktrackClasses();
1634                                 if (matches(ps, bcdt, bca, -1, null)) {
1635                                     int[] lca = cr.getLookaheadClasses();
1636                                     if (matches(ps, lcdt, lca, rv[0], null)) {
1637                                         return r.getLookups();
1638                                     }
1639                                 }
1640                             }
1641                         }
1642                     }
1643                 }
1644             }
1645             return null;
1646         }
1647         private boolean matches(GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv) {
1648             return ContextualSubtableFormat2.matches(ps, cdt, classes, offset, rv);
1649         }
1650         private void populate(List entries) {
1651             if (entries == null) {
1652                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
1653             } else if (entries.size() != 5) {
1654                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 5 entries");
1655             } else {
1656                 Object o;
1657                 if (((o = entries.get(0)) == null) || !(o instanceof GlyphClassTable)) {
1658                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an GlyphClassTable, but is: " + ((o != null) ? o.getClass() : null));
1659                 } else {
1660                     icdt = (GlyphClassTable) o;
1661                 }
1662                 if (((o = entries.get(1)) != null) && !(o instanceof GlyphClassTable)) {
1663                     throw new AdvancedTypographicTableFormatException("illegal entries, second entry must be an GlyphClassTable, but is: " + o.getClass());
1664                 } else {
1665                     bcdt = (GlyphClassTable) o;
1666                 }
1667                 if (((o = entries.get(2)) != null) && !(o instanceof GlyphClassTable)) {
1668                     throw new AdvancedTypographicTableFormatException("illegal entries, third entry must be an GlyphClassTable, but is: " + o.getClass());
1669                 } else {
1670                     lcdt = (GlyphClassTable) o;
1671                 }
1672                 if (((o = entries.get(3)) == null) || !(o instanceof Integer)) {
1673                     throw new AdvancedTypographicTableFormatException("illegal entries, fourth entry must be an Integer, but is: " + ((o != null) ? o.getClass() : null));
1674                 } else {
1675                     ngc = (Integer) (o);
1676                 }
1677                 if (((o = entries.get(4)) == null) || !(o instanceof RuleSet[])) {
1678                     throw new AdvancedTypographicTableFormatException("illegal entries, fifth entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
1679                 } else {
1680                     rsa = (RuleSet[]) o;
1681                     if (rsa.length != ngc) {
1682                         throw new AdvancedTypographicTableFormatException("illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes");
1683                     }
1684                 }
1685             }
1686         }
1687     }
1688 
1689     private static class ChainedContextualSubtableFormat3 extends ChainedContextualSubtable {
1690         private RuleSet[] rsa;                          // rule set array, containing a single rule set
1691         ChainedContextualSubtableFormat3(String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries) {
1692             super(id, sequence, flags, format, coverage, entries);
1693             populate(entries);
1694         }
1695         /** {@inheritDoc} */
1696         public List getEntries() {
1697             if (rsa != null) {
1698                 List entries = new ArrayList(1);
1699                 entries.add(rsa);
1700                 return entries;
1701             } else {
1702                 return null;
1703             }
1704         }
1705         /** {@inheritDoc} */
1706         public void resolveLookupReferences(Map<String, LookupTable> lookupTables) {
1707             GlyphTable.resolveLookupReferences(rsa, lookupTables);
1708         }
1709         /** {@inheritDoc} */
1710         public RuleLookup[] getLookups(int ci, int gi, GlyphPositioningState ps, int[] rv) {
1711             assert ps != null;
1712             assert (rv != null) && (rv.length > 0);
1713             assert rsa != null;
1714             if (rsa.length > 0) {
1715                 RuleSet rs = rsa [ 0 ];
1716                 if (rs != null) {
1717                     Rule[] ra = rs.getRules();
1718                     for (Rule r : ra) {
1719                         if ((r != null) && (r instanceof ChainedCoverageSequenceRule)) {
1720                             ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r;
1721                             GlyphCoverageTable[] igca = cr.getCoverages();
1722                             if (matches(ps, igca, 0, rv)) {
1723                                 GlyphCoverageTable[] bgca = cr.getBacktrackCoverages();
1724                                 if (matches(ps, bgca, -1, null)) {
1725                                     GlyphCoverageTable[] lgca = cr.getLookaheadCoverages();
1726                                     if (matches(ps, lgca, rv[0], null)) {
1727                                         return r.getLookups();
1728                                     }
1729                                 }
1730                             }
1731                         }
1732                     }
1733                 }
1734             }
1735             return null;
1736         }
matches(GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv)1737         private boolean matches(GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv) {
1738             return ContextualSubtableFormat3.matches(ps, gca, offset, rv);
1739         }
populate(List entries)1740         private void populate(List entries) {
1741             if (entries == null) {
1742                 throw new AdvancedTypographicTableFormatException("illegal entries, must be non-null");
1743             } else if (entries.size() != 1) {
1744                 throw new AdvancedTypographicTableFormatException("illegal entries, " + entries.size() + " entries present, but requires 1 entry");
1745             } else {
1746                 Object o;
1747                 if (((o = entries.get(0)) == null) || !(o instanceof RuleSet[])) {
1748                     throw new AdvancedTypographicTableFormatException("illegal entries, first entry must be an RuleSet[], but is: " + ((o != null) ? o.getClass() : null));
1749                 } else {
1750                     rsa = (RuleSet[]) o;
1751                 }
1752             }
1753         }
1754     }
1755 
1756     /**
1757      * The <code>DeviceTable</code> class implements a positioning device table record, comprising
1758      * adjustments to be made to scaled design units according to the scaled size.
1759      */
1760     public static class DeviceTable {
1761 
1762         private final int startSize;
1763         private final int endSize;
1764         private final int[] deltas;
1765 
1766         /**
1767          * Instantiate a DeviceTable.
1768          * @param startSize the
1769          * @param endSize the ending (scaled) size
1770          * @param deltas adjustments for each scaled size
1771          */
DeviceTable(int startSize, int endSize, int[] deltas)1772         public DeviceTable(int startSize, int endSize, int[] deltas) {
1773             assert startSize >= 0;
1774             assert startSize <= endSize;
1775             assert deltas != null;
1776             assert deltas.length == (endSize - startSize) + 1;
1777             this.startSize = startSize;
1778             this.endSize = endSize;
1779             this.deltas = deltas;
1780         }
1781 
1782         /** @return the start size */
getStartSize()1783         public int getStartSize() {
1784             return startSize;
1785         }
1786 
1787         /** @return the end size */
getEndSize()1788         public int getEndSize() {
1789             return endSize;
1790         }
1791 
1792         /** @return the deltas */
getDeltas()1793         public int[] getDeltas() {
1794             return deltas;
1795         }
1796 
1797         /**
1798          * Find device adjustment. asf.todo at present, assumes that 1 device unit equals one point
1799          * @param fontSize the font size to search for
1800          * @return an adjustment if font size matches an entry
1801          */
findAdjustment(int fontSize)1802         public int findAdjustment(int fontSize) {
1803             // [TODO] at present, assumes that 1 device unit equals one point
1804             int fs = fontSize / 1000;
1805             if (fs < startSize) {
1806                 return 0;
1807             } else if (fs <= endSize) {
1808                 return deltas [ fs - startSize ] * 1000;
1809             } else {
1810                 return 0;
1811             }
1812         }
1813 
1814         /** {@inheritDoc} */
toString()1815         public String toString() {
1816             return "{ start = " + startSize + ", end = " + endSize + ", deltas = " + Arrays.toString(deltas) + "}";
1817         }
1818 
1819     }
1820 
1821     /**
1822      * The <code>Value</code> class implements a positioning value record, comprising placement
1823      * and advancement information in X and Y axes, and optionally including device data used to
1824      * perform device (grid-fitted) specific fine grain adjustments.
1825      */
1826     public static class Value {
1827 
1828         /** X_PLACEMENT value format flag */
1829         public static final int X_PLACEMENT             = 0x0001;
1830         /** Y_PLACEMENT value format flag */
1831         public static final int Y_PLACEMENT             = 0x0002;
1832         /** X_ADVANCE value format flag */
1833         public static final int X_ADVANCE               = 0x0004;
1834         /** Y_ADVANCE value format flag */
1835         public static final int Y_ADVANCE               = 0x0008;
1836         /** X_PLACEMENT_DEVICE value format flag */
1837         public static final int X_PLACEMENT_DEVICE      = 0x0010;
1838         /** Y_PLACEMENT_DEVICE value format flag */
1839         public static final int Y_PLACEMENT_DEVICE      = 0x0020;
1840         /** X_ADVANCE_DEVICE value format flag */
1841         public static final int X_ADVANCE_DEVICE        = 0x0040;
1842         /** Y_ADVANCE_DEVICE value format flag */
1843         public static final int Y_ADVANCE_DEVICE        = 0x0080;
1844 
1845         /** X_PLACEMENT value index (within adjustments arrays) */
1846         public static final int IDX_X_PLACEMENT         = 0;
1847         /** Y_PLACEMENT value index (within adjustments arrays) */
1848         public static final int IDX_Y_PLACEMENT         = 1;
1849         /** X_ADVANCE value index (within adjustments arrays) */
1850         public static final int IDX_X_ADVANCE           = 2;
1851         /** Y_ADVANCE value index (within adjustments arrays) */
1852         public static final int IDX_Y_ADVANCE           = 3;
1853 
1854         private int xPlacement;                         // x placement
1855         private int yPlacement;                         // y placement
1856         private int xAdvance;                           // x advance
1857         private int yAdvance;                           // y advance
1858         private final DeviceTable xPlaDevice;           // x placement device table
1859         private final DeviceTable yPlaDevice;           // y placement device table
1860         private final DeviceTable xAdvDevice;           // x advance device table
1861         private final DeviceTable yAdvDevice;           // x advance device table
1862 
1863         /**
1864          * Instantiate a Value.
1865          * @param xPlacement the x placement or zero
1866          * @param yPlacement the y placement or zero
1867          * @param xAdvance the x advance or zero
1868          * @param yAdvance the y advance or zero
1869          * @param xPlaDevice the x placement device table or null
1870          * @param yPlaDevice the y placement device table or null
1871          * @param xAdvDevice the x advance device table or null
1872          * @param yAdvDevice the y advance device table or null
1873          */
Value(int xPlacement, int yPlacement, int xAdvance, int yAdvance, DeviceTable xPlaDevice, DeviceTable yPlaDevice, DeviceTable xAdvDevice, DeviceTable yAdvDevice)1874         public Value(int xPlacement, int yPlacement, int xAdvance, int yAdvance, DeviceTable xPlaDevice, DeviceTable yPlaDevice, DeviceTable xAdvDevice, DeviceTable yAdvDevice) {
1875             this.xPlacement = xPlacement;
1876             this.yPlacement = yPlacement;
1877             this.xAdvance = xAdvance;
1878             this.yAdvance = yAdvance;
1879             this.xPlaDevice = xPlaDevice;
1880             this.yPlaDevice = yPlaDevice;
1881             this.xAdvDevice = xAdvDevice;
1882             this.yAdvDevice = yAdvDevice;
1883         }
1884 
1885         /** @return the x placement */
getXPlacement()1886         public int getXPlacement() {
1887             return xPlacement;
1888         }
1889 
1890         /** @return the y placement */
getYPlacement()1891         public int getYPlacement() {
1892             return yPlacement;
1893         }
1894 
1895         /** @return the x advance */
getXAdvance()1896         public int getXAdvance() {
1897             return xAdvance;
1898         }
1899 
1900         /** @return the y advance */
getYAdvance()1901         public int getYAdvance() {
1902             return yAdvance;
1903         }
1904 
1905         /** @return the x placement device table */
getXPlaDevice()1906         public DeviceTable getXPlaDevice() {
1907             return xPlaDevice;
1908         }
1909 
1910         /** @return the y placement device table */
getYPlaDevice()1911         public DeviceTable getYPlaDevice() {
1912             return yPlaDevice;
1913         }
1914 
1915         /** @return the x advance device table */
getXAdvDevice()1916         public DeviceTable getXAdvDevice() {
1917             return xAdvDevice;
1918         }
1919 
1920         /** @return the y advance device table */
getYAdvDevice()1921         public DeviceTable getYAdvDevice() {
1922             return yAdvDevice;
1923         }
1924 
1925         /**
1926          * Apply value to specific adjustments to without use of device table adjustments.
1927          * @param xPlacement the x placement or zero
1928          * @param yPlacement the y placement or zero
1929          * @param xAdvance the x advance or zero
1930          * @param yAdvance the y advance or zero
1931          */
adjust(int xPlacement, int yPlacement, int xAdvance, int yAdvance)1932         public void adjust(int xPlacement, int yPlacement, int xAdvance, int yAdvance) {
1933             this.xPlacement += xPlacement;
1934             this.yPlacement += yPlacement;
1935             this.xAdvance += xAdvance;
1936             this.yAdvance += yAdvance;
1937         }
1938 
1939         /**
1940          * Apply value to adjustments using font size for device table adjustments.
1941          * @param adjustments array of four integers containing X,Y placement and X,Y advance adjustments
1942          * @param fontSize font size for device table adjustments
1943          * @return true if some adjustment was made
1944          */
adjust(int[] adjustments, int fontSize)1945         public boolean adjust(int[] adjustments, int fontSize) {
1946             boolean adjust = false;
1947             int dv;
1948             if ((dv = xPlacement) != 0) {
1949                 adjustments [ IDX_X_PLACEMENT ] += dv;
1950                 adjust = true;
1951             }
1952             if ((dv = yPlacement) != 0) {
1953                 adjustments [ IDX_Y_PLACEMENT ] += dv;
1954                 adjust = true;
1955             }
1956             if ((dv = xAdvance) != 0) {
1957                 adjustments [ IDX_X_ADVANCE ] += dv;
1958                 adjust = true;
1959             }
1960             if ((dv = yAdvance) != 0) {
1961                 adjustments [ IDX_Y_ADVANCE ] += dv;
1962                 adjust = true;
1963             }
1964             if (fontSize != 0) {
1965                 DeviceTable dt;
1966                 if ((dt = xPlaDevice) != null) {
1967                     if ((dv = dt.findAdjustment(fontSize)) != 0) {
1968                         adjustments [ IDX_X_PLACEMENT ] += dv;
1969                         adjust = true;
1970                     }
1971                 }
1972                 if ((dt = yPlaDevice) != null) {
1973                     if ((dv = dt.findAdjustment(fontSize)) != 0) {
1974                         adjustments [ IDX_Y_PLACEMENT ] += dv;
1975                         adjust = true;
1976                     }
1977                 }
1978                 if ((dt = xAdvDevice) != null) {
1979                     if ((dv = dt.findAdjustment(fontSize)) != 0) {
1980                         adjustments [ IDX_X_ADVANCE ] += dv;
1981                         adjust = true;
1982                     }
1983                 }
1984                 if ((dt = yAdvDevice) != null) {
1985                     if ((dv = dt.findAdjustment(fontSize)) != 0) {
1986                         adjustments [ IDX_Y_ADVANCE ] += dv;
1987                         adjust = true;
1988                     }
1989                 }
1990             }
1991             return adjust;
1992         }
1993 
1994         /** {@inheritDoc} */
toString()1995         public String toString() {
1996             StringBuffer sb = new StringBuffer();
1997             boolean first = true;
1998             sb.append("{ ");
1999             if (xPlacement != 0) {
2000                 if (!first) {
2001                     sb.append(", ");
2002                 } else {
2003                     first = false;
2004                 }
2005                 sb.append("xPlacement = " + xPlacement);
2006             }
2007             if (yPlacement != 0) {
2008                 if (!first) {
2009                     sb.append(", ");
2010                 } else {
2011                     first = false;
2012                 }
2013                 sb.append("yPlacement = " + yPlacement);
2014             }
2015             if (xAdvance != 0) {
2016                 if (!first) {
2017                     sb.append(", ");
2018                 } else {
2019                     first = false;
2020                 }
2021                 sb.append("xAdvance = " + xAdvance);
2022             }
2023             if (yAdvance != 0) {
2024                 if (!first) {
2025                     sb.append(", ");
2026                 } else {
2027                     first = false;
2028                 }
2029                 sb.append("yAdvance = " + yAdvance);
2030             }
2031             if (xPlaDevice != null) {
2032                 if (!first) {
2033                     sb.append(", ");
2034                 } else {
2035                     first = false;
2036                 }
2037                 sb.append("xPlaDevice = " + xPlaDevice);
2038             }
2039             if (yPlaDevice != null) {
2040                 if (!first) {
2041                     sb.append(", ");
2042                 } else {
2043                     first = false;
2044                 }
2045                 sb.append("xPlaDevice = " + yPlaDevice);
2046             }
2047             if (xAdvDevice != null) {
2048                 if (!first) {
2049                     sb.append(", ");
2050                 } else {
2051                     first = false;
2052                 }
2053                 sb.append("xAdvDevice = " + xAdvDevice);
2054             }
2055             if (yAdvDevice != null) {
2056                 if (!first) {
2057                     sb.append(", ");
2058                 } else {
2059                     first = false;
2060                 }
2061                 sb.append("xAdvDevice = " + yAdvDevice);
2062             }
2063             sb.append(" }");
2064             return sb.toString();
2065         }
2066 
2067     }
2068 
2069     /**
2070      * The <code>PairValues</code> class implements a pair value record, comprising a glyph id (or zero)
2071      * and two optional positioning values.
2072      */
2073     public static class PairValues {
2074 
2075         private final int glyph;                        // glyph id (or 0)
2076         private final Value value1;                     // value for first glyph in pair (or null)
2077         private final Value value2;                     // value for second glyph in pair (or null)
2078 
2079         /**
2080          * Instantiate a PairValues.
2081          * @param glyph the glyph id (or zero)
2082          * @param value1 the value of the first glyph in pair (or null)
2083          * @param value2 the value of the second glyph in pair (or null)
2084          */
PairValues(int glyph, Value value1, Value value2)2085         public PairValues(int glyph, Value value1, Value value2) {
2086             assert glyph >= 0;
2087             this.glyph = glyph;
2088             this.value1 = value1;
2089             this.value2 = value2;
2090         }
2091 
2092         /** @return the glyph id */
getGlyph()2093         public int getGlyph() {
2094             return glyph;
2095         }
2096 
2097         /** @return the first value */
getValue1()2098         public Value getValue1() {
2099             return value1;
2100         }
2101 
2102         /** @return the second value */
getValue2()2103         public Value getValue2() {
2104             return value2;
2105         }
2106 
2107         /** {@inheritDoc} */
toString()2108         public String toString() {
2109             StringBuffer sb = new StringBuffer();
2110             boolean first = true;
2111             sb.append("{ ");
2112             if (glyph != 0) {
2113                 if (!first) {
2114                     sb.append(", ");
2115                 } else {
2116                     first = false;
2117                 }
2118                 sb.append("glyph = " + glyph);
2119             }
2120             if (value1 != null) {
2121                 if (!first) {
2122                     sb.append(", ");
2123                 } else {
2124                     first = false;
2125                 }
2126                 sb.append("value1 = " + value1);
2127             }
2128             if (value2 != null) {
2129                 if (!first) {
2130                     sb.append(", ");
2131                 } else {
2132                     first = false;
2133                 }
2134                 sb.append("value2 = " + value2);
2135             }
2136             sb.append(" }");
2137             return sb.toString();
2138         }
2139 
2140     }
2141 
2142     /**
2143      * The <code>Anchor</code> class implements a anchor record, comprising an X,Y coordinate pair,
2144      * an optional anchor point index (or -1), and optional X or Y device tables (or null if absent).
2145      */
2146     public static class Anchor {
2147 
2148         private final int x;                            // xCoordinate (in design units)
2149         private final int y;                            // yCoordinate (in design units)
2150         private final int anchorPoint;                  // anchor point index (or -1)
2151         private final DeviceTable xDevice;              // x device table
2152         private final DeviceTable yDevice;              // y device table
2153 
2154         /**
2155          * Instantiate an Anchor (format 1).
2156          * @param x the x coordinate
2157          * @param y the y coordinate
2158          */
Anchor(int x, int y)2159         public Anchor(int x, int y) {
2160             this (x, y, -1, null, null);
2161         }
2162 
2163         /**
2164          * Instantiate an Anchor (format 2).
2165          * @param x the x coordinate
2166          * @param y the y coordinate
2167          * @param anchorPoint anchor index (or -1)
2168          */
Anchor(int x, int y, int anchorPoint)2169         public Anchor(int x, int y, int anchorPoint) {
2170             this (x, y, anchorPoint, null, null);
2171         }
2172 
2173         /**
2174          * Instantiate an Anchor (format 3).
2175          * @param x the x coordinate
2176          * @param y the y coordinate
2177          * @param xDevice the x device table (or null if not present)
2178          * @param yDevice the y device table (or null if not present)
2179          */
Anchor(int x, int y, DeviceTable xDevice, DeviceTable yDevice)2180         public Anchor(int x, int y, DeviceTable xDevice, DeviceTable yDevice) {
2181             this (x, y, -1, xDevice, yDevice);
2182         }
2183 
2184         /**
2185          * Instantiate an Anchor based on an existing anchor.
2186          * @param a the existing anchor
2187          */
Anchor(Anchor a)2188         protected Anchor(Anchor a) {
2189             this (a.x, a.y, a.anchorPoint, a.xDevice, a.yDevice);
2190         }
2191 
Anchor(int x, int y, int anchorPoint, DeviceTable xDevice, DeviceTable yDevice)2192         private Anchor(int x, int  y, int anchorPoint, DeviceTable xDevice, DeviceTable yDevice) {
2193             assert (anchorPoint >= 0) || (anchorPoint == -1);
2194             this.x = x;
2195             this.y = y;
2196             this.anchorPoint = anchorPoint;
2197             this.xDevice = xDevice;
2198             this.yDevice = yDevice;
2199         }
2200 
2201         /** @return the x coordinate */
getX()2202         public int getX() {
2203             return x;
2204         }
2205 
2206         /** @return the y coordinate */
getY()2207         public int getY() {
2208             return y;
2209         }
2210 
2211         /** @return the anchor point index (or -1 if not specified) */
getAnchorPoint()2212         public int getAnchorPoint() {
2213             return anchorPoint;
2214         }
2215 
2216         /** @return the x device table (or null if not specified) */
getXDevice()2217         public DeviceTable getXDevice() {
2218             return xDevice;
2219         }
2220 
2221         /** @return the y device table (or null if not specified) */
getYDevice()2222         public DeviceTable getYDevice() {
2223             return yDevice;
2224         }
2225 
2226         /**
2227          * Obtain adjustment value required to align the specified anchor
2228          * with this anchor.
2229          * @param a the anchor to align
2230          * @return the adjustment value needed to effect alignment
2231          */
getAlignmentAdjustment(Anchor a)2232         public Value getAlignmentAdjustment(Anchor a) {
2233             assert a != null;
2234             // TODO - handle anchor point
2235             // TODO - handle device tables
2236             return new Value(x - a.x, y - a.y, 0, 0, null, null, null, null);
2237         }
2238 
2239         /** {@inheritDoc} */
toString()2240         public String toString() {
2241             StringBuffer sb = new StringBuffer();
2242             sb.append("{ [" + x + "," + y + "]");
2243             if (anchorPoint != -1) {
2244                 sb.append(", anchorPoint = " + anchorPoint);
2245             }
2246             if (xDevice != null) {
2247                 sb.append(", xDevice = " + xDevice);
2248             }
2249             if (yDevice != null) {
2250                 sb.append(", yDevice = " + yDevice);
2251             }
2252             sb.append(" }");
2253             return sb.toString();
2254         }
2255 
2256     }
2257 
2258     /**
2259      * The <code>MarkAnchor</code> class is a subclass of the <code>Anchor</code> class, adding a mark
2260      * class designation.
2261      */
2262     public static class MarkAnchor extends Anchor {
2263 
2264         private final int markClass;                            // mark class
2265 
2266         /**
2267          * Instantiate a MarkAnchor
2268          * @param markClass the mark class
2269          * @param a the underlying anchor (whose fields are copied)
2270          */
MarkAnchor(int markClass, Anchor a)2271         public MarkAnchor(int markClass, Anchor a) {
2272             super(a);
2273             this.markClass = markClass;
2274         }
2275 
2276         /** @return the mark class */
getMarkClass()2277         public int getMarkClass() {
2278             return markClass;
2279         }
2280 
2281         /** {@inheritDoc} */
toString()2282         public String toString() {
2283             return "{ markClass = " + markClass + ", anchor = " + super.toString() + " }";
2284         }
2285 
2286     }
2287 
2288 }
2289