diff options
Diffstat (limited to 'src/java/org/apache')
201 files changed, 28751 insertions, 1466 deletions
diff --git a/src/java/org/apache/fop/apps/FOUserAgent.java b/src/java/org/apache/fop/apps/FOUserAgent.java index 7d1736586..c13a624cf 100644 --- a/src/java/org/apache/fop/apps/FOUserAgent.java +++ b/src/java/org/apache/fop/apps/FOUserAgent.java @@ -654,6 +654,24 @@ public class FOUserAgent { } /** + * Check whether complex script features are enabled. + * + * @return true if FOP is to use complex script features + */ + public boolean isComplexScriptFeaturesEnabled() { + return factory.isComplexScriptFeaturesEnabled(); + } + + /** + * Control whether complex script features should be enabled + * + * @param useComplexScriptFeatures true if FOP is to use complex script features + */ + public void setComplexScriptFeaturesEnabled(boolean useComplexScriptFeatures) { + factory.setComplexScriptFeaturesEnabled ( useComplexScriptFeatures ); + } + + /** * Activates accessibility (for output formats that support it). * * @param accessibility <code>true</code> to enable accessibility support diff --git a/src/java/org/apache/fop/apps/FopFactory.java b/src/java/org/apache/fop/apps/FopFactory.java index 2aca8bf99..18c84a036 100644 --- a/src/java/org/apache/fop/apps/FopFactory.java +++ b/src/java/org/apache/fop/apps/FopFactory.java @@ -143,6 +143,10 @@ public class FopFactory implements ImageContext { /** Page width */ private String pageWidth = FopFactoryConfigurator.DEFAULT_PAGE_WIDTH; + /** Complex scripts support enabled */ + private boolean useComplexScriptFeatures + = FopFactoryConfigurator.DEFAULT_COMPLEX_SCRIPT_FEATURES; + /** @see #setBreakIndentInheritanceOnReferenceAreaBoundary(boolean) */ private boolean breakIndentInheritanceOnReferenceAreaBoundary = FopFactoryConfigurator.DEFAULT_BREAK_INDENT_INHERITANCE; @@ -212,6 +216,19 @@ public class FopFactory implements ImageContext { } /** + * Sets complex script support. + * @param value <code>true</code> to enable complex script features, + * <code>false</code> otherwise + */ + void setComplexScriptFeaturesEnabled(boolean value) { + this.useComplexScriptFeatures = value; + } + + boolean isComplexScriptFeaturesEnabled() { + return useComplexScriptFeatures; + } + + /** * Returns a new {@link Fop} instance. FOP will be configured with a default user agent * instance. * <p> diff --git a/src/java/org/apache/fop/apps/FopFactoryConfigurator.java b/src/java/org/apache/fop/apps/FopFactoryConfigurator.java index 9097f23e3..4ed59d0fe 100644 --- a/src/java/org/apache/fop/apps/FopFactoryConfigurator.java +++ b/src/java/org/apache/fop/apps/FopFactoryConfigurator.java @@ -69,6 +69,9 @@ public class FopFactoryConfigurator { /** Defines the default target resolution (72dpi) for FOP */ public static final float DEFAULT_TARGET_RESOLUTION = GraphicsConstants.DEFAULT_DPI; //dpi + /** Defines the default complex script support */ + public static final boolean DEFAULT_COMPLEX_SCRIPT_FEATURES = true; + private static final String PREFER_RENDERER = "prefer-renderer"; /** logger instance */ @@ -269,6 +272,13 @@ public class FopFactoryConfigurator { } } + // configure complex script support + Configuration csConfig = cfg.getChild("complex-scripts"); + if (csConfig != null) { + this.factory.setComplexScriptFeaturesEnabled + (!csConfig.getAttributeAsBoolean ( "disabled", false )); + } + // configure font manager new FontManagerConfigurator(cfg, baseURI).configure(factory.getFontManager(), strict); diff --git a/src/java/org/apache/fop/area/Area.java b/src/java/org/apache/fop/area/Area.java index 1f14ed740..435876d65 100644 --- a/src/java/org/apache/fop/area/Area.java +++ b/src/java/org/apache/fop/area/Area.java @@ -25,6 +25,7 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.traits.BorderProps; +import org.apache.fop.traits.WritingModeTraitsGetter; // If the area appears more than once in the output // or if the area has external data it is cached @@ -41,27 +42,6 @@ public class Area extends AreaTreeObject implements Serializable { private static final long serialVersionUID = 6342888466142626492L; - // stacking directions - /** - * Stacking left to right - */ - public static final int LR = 0; - - /** - * Stacking right to left - */ - public static final int RL = 1; - - /** - * Stacking top to bottom - */ - public static final int TB = 2; - - /** - * Stacking bottom to top - */ - public static final int BT = 3; - // orientations for reference areas /** * Normal orientation @@ -130,16 +110,20 @@ public class Area extends AreaTreeObject implements Serializable { protected int bpd; /** + * Resolved bidirectional level for area. + */ + protected int bidiLevel = -1; + + /** * Traits for this area stored in a HashMap */ - protected Map<Integer, Object> props = null; + protected transient Map<Integer, Object> traits = null; /** * logging instance */ protected static final Log log = LogFactory.getLog(Area.class); - /** * Get the area class of this area. * @@ -226,6 +210,32 @@ public class Area extends AreaTreeObject implements Serializable { } /** + * Set the bidirectional embedding level. + * + * @param bidiLevel the bidirectional embedding level + */ + public void setBidiLevel ( int bidiLevel ) { + this.bidiLevel = bidiLevel; + } + + /** + * Reset the bidirectional embedding level to default + * value (-1). + */ + public void resetBidiLevel() { + setBidiLevel(-1); + } + + /** + * Get the bidirectional embedding level. + * + * @return the bidirectional embedding level + */ + public int getBidiLevel() { + return bidiLevel; + } + + /** * Return the sum of region border- and padding-before * * @return width in millipoints @@ -379,10 +389,23 @@ public class Area extends AreaTreeObject implements Serializable { * @param prop the value of the trait */ public void addTrait(Integer traitCode, Object prop) { - if (props == null) { - props = new java.util.HashMap<Integer, Object>(20); + if (traits == null) { + traits = new java.util.HashMap<Integer, Object>(20); + } + traits.put(traitCode, prop); + } + + /** + * Set traits on this area, copying from an existing traits map. + * + * @param traits the map of traits + */ + public void setTraits ( Map traits ) { + if ( traits != null ) { + this.traits = new java.util.HashMap ( traits ); + } else { + this.traits = null; } - props.put(traitCode, prop); } /** @@ -391,12 +414,12 @@ public class Area extends AreaTreeObject implements Serializable { * @return the map of traits */ public Map<Integer, Object> getTraits() { - return this.props; + return this.traits; } /** @return true if the area has traits */ public boolean hasTraits() { - return (this.props != null); + return (this.traits != null); } /** @@ -406,7 +429,7 @@ public class Area extends AreaTreeObject implements Serializable { * @return the trait value */ public Object getTrait(Integer traitCode) { - return (props != null ? props.get(traitCode) : null); + return (traits != null ? traits.get(traitCode) : null); } /** @@ -445,6 +468,14 @@ public class Area extends AreaTreeObject implements Serializable { } /** + * Sets the writing mode traits for this area. Default implementation + * does nothing. + * @param wmtg a WM traits getter + */ + public void setWritingModeTraits(WritingModeTraitsGetter wmtg) { + } + + /** * {@inheritDoc} * @return ipd and bpd of area */ @@ -457,4 +488,3 @@ public class Area extends AreaTreeObject implements Serializable { return sb.toString(); } } - diff --git a/src/java/org/apache/fop/area/AreaTreeHandler.java b/src/java/org/apache/fop/area/AreaTreeHandler.java index 6448e7c5d..c87a728e6 100644 --- a/src/java/org/apache/fop/area/AreaTreeHandler.java +++ b/src/java/org/apache/fop/area/AreaTreeHandler.java @@ -73,6 +73,9 @@ public class AreaTreeHandler extends FOEventHandler { /** The AreaTreeModel in use */ protected AreaTreeModel model; + // Flag for controlling complex script features (default: true). + private boolean useComplexScriptFeatures = true; + // Keeps track of all meaningful id references private IDTracker idTracker; @@ -108,6 +111,8 @@ public class AreaTreeHandler extends FOEventHandler { this.idTracker = new IDTracker(); + this.useComplexScriptFeatures = userAgent.isComplexScriptFeaturesEnabled(); + if (log.isDebugEnabled()) { statistics = new Statistics(); } @@ -169,6 +174,15 @@ public class AreaTreeHandler extends FOEventHandler { } /** + * Check whether complex script features are enabled. + * + * @return true if using complex script features + */ + public boolean isComplexScriptFeaturesEnabled() { + return useComplexScriptFeatures; + } + + /** * Prepare AreaTreeHandler for document processing This is called from * FOTreeBuilder.startDocument() * diff --git a/src/java/org/apache/fop/area/AreaTreeParser.java b/src/java/org/apache/fop/area/AreaTreeParser.java index d8c22f2e8..961160a55 100644 --- a/src/java/org/apache/fop/area/AreaTreeParser.java +++ b/src/java/org/apache/fop/area/AreaTreeParser.java @@ -700,7 +700,7 @@ public class AreaTreeParser { public void startElement(Attributes attributes) { InlineArea inl = new InlineArea(); transferForeignObjects(attributes, inl); - inl.setOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); + inl.setBlockProgressionOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); setAreaAttributes(attributes, inl); setTraits(attributes, inl, SUBSET_COMMON); setTraits(attributes, inl, SUBSET_BOX); @@ -720,7 +720,7 @@ public class AreaTreeParser { public void startElement(Attributes attributes) { InlineParent ip = new InlineParent(); transferForeignObjects(attributes, ip); - ip.setOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); + ip.setBlockProgressionOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); setAreaAttributes(attributes, ip); setTraits(attributes, ip, SUBSET_COMMON); setTraits(attributes, ip, SUBSET_BOX); @@ -741,7 +741,7 @@ public class AreaTreeParser { public void startElement(Attributes attributes) { InlineBlockParent ibp = new InlineBlockParent(); transferForeignObjects(attributes, ibp); - ibp.setOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); + ibp.setBlockProgressionOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); setAreaAttributes(attributes, ibp); setTraits(attributes, ibp, SUBSET_COMMON); setTraits(attributes, ibp, SUBSET_BOX); @@ -769,7 +769,7 @@ public class AreaTreeParser { setTraits(attributes, text, SUBSET_COLOR); setTraits(attributes, text, SUBSET_FONT); text.setBaselineOffset(XMLUtil.getAttributeAsInt(attributes, "baseline", 0)); - text.setOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); + text.setBlockProgressionOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); text.setTextLetterSpaceAdjust(XMLUtil.getAttributeAsInt(attributes, "tlsadjust", 0)); text.setTextWordSpaceAdjust(XMLUtil.getAttributeAsInt(attributes, @@ -791,8 +791,14 @@ public class AreaTreeParser { int[] letterAdjust = ConversionUtils.toIntArray( lastAttributes.getValue("letter-adjust"), "\\s"); + int level = XMLUtil.getAttributeAsInt(lastAttributes, "level", -1); + boolean reversed = XMLUtil.getAttributeAsBoolean(lastAttributes, "reversed", false); + int[][] gposAdjustments + = XMLUtil.getAttributeAsPositionAdjustments(lastAttributes, "position-adjust"); content.flip(); - WordArea word = new WordArea(content.toString().trim(), offset, letterAdjust); + WordArea word = new WordArea + ( offset, level, content.toString().trim(), letterAdjust, + null, gposAdjustments, reversed ); AbstractTextArea text = getCurrentText(); word.setParentArea(text); text.addChildArea(word); @@ -811,7 +817,8 @@ public class AreaTreeParser { if (content.position() > 0) { content.flip(); boolean adjustable = XMLUtil.getAttributeAsBoolean(lastAttributes, "adj", true); - SpaceArea space = new SpaceArea(content.charAt(0), offset, adjustable); + int level = XMLUtil.getAttributeAsInt(lastAttributes, "level", -1); + SpaceArea space = new SpaceArea(offset, level, content.charAt(0), adjustable); AbstractTextArea text = getCurrentText(); space.setParentArea(text); text.addChildArea(space); @@ -821,7 +828,7 @@ public class AreaTreeParser { setTraits(lastAttributes, space, SUBSET_COMMON); setTraits(lastAttributes, space, SUBSET_BOX); setTraits(lastAttributes, space, SUBSET_COLOR); - space.setOffset(offset); + space.setBlockProgressionOffset(offset); Area parent = (Area)areaStack.peek(); parent.addChildArea(space); } @@ -842,7 +849,8 @@ public class AreaTreeParser { setTraits(attributes, leader, SUBSET_BOX); setTraits(attributes, leader, SUBSET_COLOR); setTraits(attributes, leader, SUBSET_FONT); - leader.setOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); + leader.setBlockProgressionOffset + ( XMLUtil.getAttributeAsInt(attributes, "offset", 0) ); String ruleStyle = attributes.getValue("ruleStyle"); if (ruleStyle != null) { leader.setRuleStyle(ruleStyle); @@ -857,7 +865,8 @@ public class AreaTreeParser { private class InlineViewportMaker extends AbstractMaker { public void startElement(Attributes attributes) { - InlineViewport viewport = new InlineViewport(null); + int level = XMLUtil.getAttributeAsInt(attributes, "level", -1); + InlineViewport viewport = new InlineViewport(null, level); transferForeignObjects(attributes, viewport); setAreaAttributes(attributes, viewport); setTraits(attributes, viewport, SUBSET_COMMON); @@ -865,7 +874,8 @@ public class AreaTreeParser { setTraits(attributes, viewport, SUBSET_COLOR); viewport.setContentPosition(XMLUtil.getAttributeAsRectangle2D(attributes, "pos")); viewport.setClip(XMLUtil.getAttributeAsBoolean(attributes, "clip", false)); - viewport.setOffset(XMLUtil.getAttributeAsInt(attributes, "offset", 0)); + viewport.setBlockProgressionOffset + ( XMLUtil.getAttributeAsInt(attributes, "offset", 0) ); Area parent = (Area)areaStack.peek(); parent.addChildArea(viewport); areaStack.push(viewport); @@ -1025,6 +1035,7 @@ public class AreaTreeParser { private void setAreaAttributes(Attributes attributes, Area area) { area.setIPD(Integer.parseInt(attributes.getValue("ipd"))); area.setBPD(Integer.parseInt(attributes.getValue("bpd"))); + area.setBidiLevel(XMLUtil.getAttributeAsInt(attributes, "level", -1)); } private static final Object[] SUBSET_COMMON = new Object[] { diff --git a/src/java/org/apache/fop/area/Block.java b/src/java/org/apache/fop/area/Block.java index 423dcfafa..c6e31f71d 100644 --- a/src/java/org/apache/fop/area/Block.java +++ b/src/java/org/apache/fop/area/Block.java @@ -58,7 +58,6 @@ public class Block extends BlockParent { */ public static final int FIXED = 3; - private int stacking = TB; private int positioning = STACK; /** if true, allow BPD update */ @@ -133,5 +132,13 @@ public class Block extends BlockParent { return (startIndent != null ? startIndent : 0); } + /** + * @return the end-indent trait + */ + public int getEndIndent() { + Integer endIndent = (Integer)getTrait(Trait.END_INDENT); + return (endIndent != null ? endIndent : 0); + } + } diff --git a/src/java/org/apache/fop/area/BodyRegion.java b/src/java/org/apache/fop/area/BodyRegion.java index 2dd8a9a7c..2fd0b014b 100644 --- a/src/java/org/apache/fop/area/BodyRegion.java +++ b/src/java/org/apache/fop/area/BodyRegion.java @@ -22,6 +22,7 @@ package org.apache.fop.area; import java.util.List; import org.apache.fop.fo.pagination.RegionBody; +import org.apache.fop.traits.WritingModeTraitsGetter; /** * This class is a container for the areas that may be generated by @@ -140,6 +141,17 @@ public class BodyRegion extends RegionReference { } /** + * Sets the writing mode traits for the main reference area of + * this body region area. + * @param wmtg a WM traits getter + */ + public void setWritingModeTraits(WritingModeTraitsGetter wmtg) { + if ( getMainReference() != null ) { + getMainReference().setWritingModeTraits ( wmtg ); + } + } + + /** * Clone this object. * * @return a shallow copy of this object diff --git a/src/java/org/apache/fop/area/CTM.java b/src/java/org/apache/fop/area/CTM.java index 07c3bbc02..73943ed65 100644 --- a/src/java/org/apache/fop/area/CTM.java +++ b/src/java/org/apache/fop/area/CTM.java @@ -25,10 +25,12 @@ import java.awt.geom.Rectangle2D; import java.io.Serializable; import org.apache.fop.datatypes.FODimension; +import org.apache.fop.traits.WritingMode; import static org.apache.fop.fo.Constants.EN_LR_TB; import static org.apache.fop.fo.Constants.EN_RL_TB; import static org.apache.fop.fo.Constants.EN_TB_RL; +import static org.apache.fop.fo.Constants.EN_TB_LR; /** * Describe a PDF or PostScript style coordinate transformation matrix (CTM). @@ -42,7 +44,7 @@ public class CTM implements Serializable { private double a, b, c, d, e, f; private static final CTM CTM_LRTB = new CTM(1, 0, 0, 1, 0, 0); - private static final CTM CTM_RLTB = new CTM(-1, 0, 0, 1, 0, 0); + private static final CTM CTM_RLTB = new CTM(1, 0, 0, 1, 0, 0); private static final CTM CTM_TBRL = new CTM(0, 1, -1, 0, 0, 0); /** @@ -126,28 +128,25 @@ public class CTM implements Serializable { * Return a CTM which will transform coordinates for a particular writing-mode * into normalized first quandrant coordinates. * @param wm A writing mode constant from fo.properties.WritingMode, ie. - * one of LR_TB, RL_TB, TB_RL. + * one of LR_TB, RL_TB, TB_RL, TB_LR. * @param ipd The inline-progression dimension of the reference area whose * CTM is being set.. * @param bpd The block-progression dimension of the reference area whose * CTM is being set. * @return a new CTM with the required transform */ - public static CTM getWMctm(int wm, int ipd, int bpd) { + public static CTM getWMctm(WritingMode wm, int ipd, int bpd) { CTM wmctm; - switch (wm) { + switch (wm.getEnumValue()) { case EN_LR_TB: return new CTM(CTM_LRTB); case EN_RL_TB: - wmctm = new CTM(CTM_RLTB); - wmctm.e = ipd; - return wmctm; - //return CTM_RLTB.translate(ipd, 0); + return new CTM(CTM_RLTB); case EN_TB_RL: // CJK + case EN_TB_LR: // CJK wmctm = new CTM(CTM_TBRL); wmctm.e = bpd; return wmctm; - //return CTM_TBRL.translate(0, ipd); default: return null; } @@ -284,7 +283,7 @@ public class CTM implements Serializable { * @return CTM the coordinate transformation matrix (CTM) */ public static CTM getCTMandRelDims(int absRefOrient, - int writingMode, + WritingMode writingMode, Rectangle2D absVPrect, FODimension reldims) { int width, height; @@ -335,12 +334,18 @@ public class CTM implements Serializable { * can set ipd and bpd appropriately based on the writing mode. */ - if (writingMode == EN_LR_TB || writingMode == EN_RL_TB) { + switch ( writingMode.getEnumValue() ) { + default: + case EN_LR_TB: + case EN_RL_TB: reldims.ipd = width; reldims.bpd = height; - } else { + break; + case EN_TB_LR: + case EN_TB_RL: reldims.ipd = height; reldims.bpd = width; + break; } // Set a rectangle to be the writing-mode relative version??? // Now transform for writing mode diff --git a/src/java/org/apache/fop/area/LineArea.java b/src/java/org/apache/fop/area/LineArea.java index edf60578a..1d3262487 100644 --- a/src/java/org/apache/fop/area/LineArea.java +++ b/src/java/org/apache/fop/area/LineArea.java @@ -21,6 +21,7 @@ package org.apache.fop.area; import java.io.Serializable; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import org.apache.fop.area.inline.InlineArea; @@ -125,6 +126,24 @@ public class LineArea extends Area { } /** + * <p>Set (en masse) the inline child areas of this line area.</p> + * <p> Used by bidirectional processing after line area consituent reordering.</p> + * @param inlineAreas the list of inline areas + */ + public void setInlineAreas ( List inlineAreas ) { + for ( Iterator<InlineArea> it = inlineAreas.iterator(); it.hasNext();) { + InlineArea ia = it.next(); + Area pa = ia.getParentArea(); + if ( pa == null ) { + ia.setParentArea ( this ); + } else { + assert pa == this; + } + } + this.inlineAreas = inlineAreas; + } + + /** * Get the inline child areas of this line area. * * @return the list of inline areas @@ -149,6 +168,21 @@ public class LineArea extends Area { } /** + * Get the end indent of this line area. + * The end indent is used for offsetting the end of + * the inline areas for alignment or other indents. + * + * @return the end indent value + */ + public int getEndIndent() { + if (hasTrait(Trait.END_INDENT)) { + return getTraitAsInteger(Trait.END_INDENT); + } else { + return 0; + } + } + + /** * Updates the extents of the line area from its children. */ public void updateExtentsFromChildren() { @@ -178,17 +212,21 @@ public class LineArea extends Area { * @param ipdVariation the difference between old and new ipd */ public void handleIPDVariation(int ipdVariation) { + int si = getStartIndent(); + int ei = getEndIndent(); switch (adjustingInfo.lineAlignment) { case EN_START: - // nothing to do in this case + // adjust end indent + addTrait(Trait.END_INDENT, ei - ipdVariation); break; case EN_CENTER: - // re-compute indent - addTrait(Trait.START_INDENT, getStartIndent() - ipdVariation / 2); + // adjust start and end indents + addTrait(Trait.START_INDENT, si - ipdVariation / 2); + addTrait(Trait.END_INDENT, ei - ipdVariation / 2); break; case EN_END: - // re-compute indent - addTrait(Trait.START_INDENT, getStartIndent() - ipdVariation); + // adjust start indent + addTrait(Trait.START_INDENT, si - ipdVariation); break; case EN_JUSTIFY: // compute variation factor @@ -198,7 +236,7 @@ public class LineArea extends Area { // if the LineArea has already been added to the area tree, // call finalize(); otherwise, wait for the LineLM to call it if (adjustingInfo.bAddedToAreaTree) { - finalise(); + finish(); } break; default: @@ -211,7 +249,7 @@ public class LineArea extends Area { * and destroy the AdjustingInfo object if there are * no UnresolvedAreas left */ - public void finalise() { + public void finish() { if (adjustingInfo.lineAlignment == EN_JUSTIFY) { if (log.isTraceEnabled()) { log.trace("Applying variation factor to justified line: " + adjustingInfo); diff --git a/src/java/org/apache/fop/area/LinkResolver.java b/src/java/org/apache/fop/area/LinkResolver.java index 1e135701f..b458a1baf 100644 --- a/src/java/org/apache/fop/area/LinkResolver.java +++ b/src/java/org/apache/fop/area/LinkResolver.java @@ -20,6 +20,7 @@ package org.apache.fop.area; // Java +import java.util.ArrayList; import java.util.List; import java.io.Serializable; @@ -33,6 +34,7 @@ public class LinkResolver implements Resolvable, Serializable { private boolean resolved = false; private String idRef; private Area area; + private transient List<Resolvable> dependents = null; /** * Create a new link resolver. @@ -79,8 +81,35 @@ public class LinkResolver implements Resolvable, Serializable { public void resolveIDRef(String id, PageViewport pv) { if (idRef.equals(id) && pv != null) { resolved = true; - Trait.InternalLink iLink = new Trait.InternalLink(pv.getKey(), idRef); - area.addTrait(Trait.INTERNAL_LINK, iLink); + if ( area != null ) { + Trait.InternalLink iLink = new Trait.InternalLink(pv.getKey(), idRef); + area.addTrait(Trait.INTERNAL_LINK, iLink); + area = null; // break circular reference from basic link area to this resolver + } + resolveDependents(id, pv); } } + + /** + * Add dependent resolvable. Used to resolve second-order resolvables that + * depend on resolution of this resolver. + * @param dependent resolvable + */ + public void addDependent(Resolvable dependent) { + if ( dependents == null ) { + dependents = new ArrayList<Resolvable>(); + } + dependents.add(dependent); + } + + private void resolveDependents(String id, PageViewport pv) { + if ( dependents != null ) { + List<PageViewport> pages = new ArrayList<PageViewport>(); + pages.add(pv); + for ( Resolvable r : dependents ) { + r.resolveIDRef(id, pages); + } + } + } + } diff --git a/src/java/org/apache/fop/area/MainReference.java b/src/java/org/apache/fop/area/MainReference.java index a6112011d..44f4cc8c8 100644 --- a/src/java/org/apache/fop/area/MainReference.java +++ b/src/java/org/apache/fop/area/MainReference.java @@ -19,6 +19,8 @@ package org.apache.fop.area; +import org.apache.fop.traits.WritingModeTraitsGetter; + import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -124,5 +126,16 @@ public class MainReference extends Area { return parent.getColumnGap(); } + /** + * Sets the writing mode traits for the spans of this main + * reference area. + * @param wmtg a WM traits getter + */ + public void setWritingModeTraits(WritingModeTraitsGetter wmtg) { + for ( Span s : (List<Span>) getSpans() ) { + s.setWritingModeTraits ( wmtg ); + } + } + } diff --git a/src/java/org/apache/fop/area/Page.java b/src/java/org/apache/fop/area/Page.java index a1d9e389f..3a08809dc 100644 --- a/src/java/org/apache/fop/area/Page.java +++ b/src/java/org/apache/fop/area/Page.java @@ -33,6 +33,7 @@ import org.apache.fop.fo.pagination.RegionBody; import org.apache.fop.fo.pagination.SimplePageMaster; import org.apache.fop.fo.properties.CommonMarginBlock; import org.apache.fop.layoutmgr.TraitSetter; +import org.apache.fop.traits.WritingModeTraitsGetter; import static org.apache.fop.fo.Constants.FO_REGION_AFTER; import static org.apache.fop.fo.Constants.FO_REGION_BEFORE; @@ -304,6 +305,29 @@ public class Page extends AreaTreeObject implements Serializable, Cloneable { return unresolved; } + /** + * Sets the writing mode traits for the region viewports of + * this page. + * @param wmtg a WM traits getter + */ + public void setWritingModeTraits(WritingModeTraitsGetter wmtg) { + if (regionBefore != null) { + regionBefore.setWritingModeTraits(wmtg); + } + if (regionStart != null) { + regionStart.setWritingModeTraits(wmtg); + } + if (regionBody != null) { + regionBody.setWritingModeTraits(wmtg); + } + if (regionEnd != null) { + regionEnd.setWritingModeTraits(wmtg); + } + if (regionAfter != null) { + regionAfter.setWritingModeTraits(wmtg); + } + } + } diff --git a/src/java/org/apache/fop/area/PageViewport.java b/src/java/org/apache/fop/area/PageViewport.java index ff4b2fdaa..f335fa128 100644 --- a/src/java/org/apache/fop/area/PageViewport.java +++ b/src/java/org/apache/fop/area/PageViewport.java @@ -34,6 +34,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.fop.fo.flow.Marker; import org.apache.fop.fo.pagination.SimplePageMaster; +import org.apache.fop.traits.WritingModeTraitsGetter; import static org.apache.fop.fo.Constants.FO_REGION_BODY; import static org.apache.fop.fo.Constants.EN_FSWP; @@ -654,4 +655,15 @@ public class PageViewport extends AreaTreeObject implements Resolvable, Cloneabl public RegionReference getRegionReference(int id) { return getPage().getRegionViewport(id).getRegionReference(); } + + /** + * Sets the writing mode traits for the page associated with this viewport. + * @param wmtg a WM traits getter + */ + public void setWritingModeTraits(WritingModeTraitsGetter wmtg) { + if ( page != null ) { + page.setWritingModeTraits(wmtg); + } + } + } diff --git a/src/java/org/apache/fop/area/RegionViewport.java b/src/java/org/apache/fop/area/RegionViewport.java index 6cc700a7b..0104b53ad 100644 --- a/src/java/org/apache/fop/area/RegionViewport.java +++ b/src/java/org/apache/fop/area/RegionViewport.java @@ -19,6 +19,8 @@ package org.apache.fop.area; +import org.apache.fop.traits.WritingModeTraitsGetter; + import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.io.IOException; @@ -106,7 +108,7 @@ public class RegionViewport extends Area implements Cloneable, Viewport { out.writeFloat((float) viewArea.getWidth()); out.writeFloat((float) viewArea.getHeight()); out.writeBoolean(clip); - out.writeObject(props); + out.writeObject(traits); out.writeObject(regionReference); } @@ -115,7 +117,7 @@ public class RegionViewport extends Area implements Cloneable, Viewport { viewArea = new Rectangle2D.Float(in.readFloat(), in.readFloat(), in.readFloat(), in.readFloat()); clip = in.readBoolean(); - props = (HashMap)in.readObject(); + traits = (HashMap)in.readObject(); setRegionReference((RegionReference) in.readObject()); } @@ -128,13 +130,25 @@ public class RegionViewport extends Area implements Cloneable, Viewport { public Object clone() { RegionViewport rv = new RegionViewport((Rectangle2D)viewArea.clone()); rv.regionReference = (RegionReference)regionReference.clone(); - if (props != null) { - rv.props = new HashMap(props); + if (traits != null) { + rv.traits = new HashMap(traits); } if (foreignAttributes != null) { rv.foreignAttributes = new HashMap(foreignAttributes); } return rv; } + + /** + * Sets the writing mode traits for the region reference of + * this region viewport + * @param wmtg a WM traits getter + */ + public void setWritingModeTraits(WritingModeTraitsGetter wmtg) { + if (regionReference != null) { + regionReference.setWritingModeTraits(wmtg); + } + } + } diff --git a/src/java/org/apache/fop/area/Span.java b/src/java/org/apache/fop/area/Span.java index c2fd4679c..f25140097 100644 --- a/src/java/org/apache/fop/area/Span.java +++ b/src/java/org/apache/fop/area/Span.java @@ -19,8 +19,12 @@ package org.apache.fop.area; +import java.util.Iterator; import java.util.List; +import org.apache.fop.fo.Constants; +import org.apache.fop.traits.WritingModeTraitsGetter; + /** * The span-reference-area. * This is a block-area with 0 border and padding that is stacked @@ -183,6 +187,28 @@ public class Span extends Area { return (areaCount == 0); } + /** + * Sets the writing mode traits for the main reference area of + * this span area. + * @param wmtg a WM traits getter + */ + public void setWritingModeTraits(WritingModeTraitsGetter wmtg) { + switch ( wmtg.getColumnProgressionDirection().getEnumValue() ) { + case Constants.EN_RL: + setBidiLevel(1); + for ( Iterator<NormalFlow> it = flowAreas.iterator(); it.hasNext();) { + it.next().setBidiLevel(1); + } + break; + default: + resetBidiLevel(); + for ( Iterator<NormalFlow> it = flowAreas.iterator(); it.hasNext();) { + it.next().resetBidiLevel(); + } + break; + } + } + /** {@inheritDoc} */ @Override public String toString() { diff --git a/src/java/org/apache/fop/area/Trait.java b/src/java/org/apache/fop/area/Trait.java index d9194559d..9942a1370 100644 --- a/src/java/org/apache/fop/area/Trait.java +++ b/src/java/org/apache/fop/area/Trait.java @@ -26,6 +26,8 @@ import org.apache.xmlgraphics.image.loader.ImageInfo; import org.apache.fop.fonts.FontTriplet; import org.apache.fop.traits.BorderProps; +import org.apache.fop.traits.Direction; +import org.apache.fop.traits.WritingMode; import org.apache.fop.util.ColorUtil; import static org.apache.fop.fo.Constants.EN_REPEAT; @@ -156,8 +158,19 @@ public final class Trait implements Serializable { /** For navigation in the document structure. */ public static final Integer STRUCTURE_TREE_ELEMENT = 37; + /** writing mode trait */ + public static final Integer WRITING_MODE = 38; + /** inline progression direction trait */ + public static final Integer INLINE_PROGRESSION_DIRECTION = 39; + /** block progression direction trait */ + public static final Integer BLOCK_PROGRESSION_DIRECTION = 40; + /** column progression direction trait */ + public static final Integer COLUMN_PROGRESSION_DIRECTION = 41; + /** shift direction trait */ + public static final Integer SHIFT_DIRECTION = 42; + /** Maximum value used by trait keys */ - public static final int MAX_TRAIT_KEY = 37; + public static final int MAX_TRAIT_KEY = 42; private static final TraitInfo[] TRAIT_INFO = new TraitInfo[MAX_TRAIT_KEY + 1]; @@ -221,6 +234,14 @@ public final class Trait implements Serializable { put(SPACE_AFTER, new TraitInfo("space-after", Integer.class)); put(IS_REFERENCE_AREA, new TraitInfo("is-reference-area", Boolean.class)); put(IS_VIEWPORT_AREA, new TraitInfo("is-viewport-area", Boolean.class)); + put(WRITING_MODE, + new TraitInfo("writing-mode", WritingMode.class)); + put(INLINE_PROGRESSION_DIRECTION, + new TraitInfo("inline-progression-direction", Direction.class)); + put(BLOCK_PROGRESSION_DIRECTION, + new TraitInfo("block-progression-direction", Direction.class)); + put(SHIFT_DIRECTION, + new TraitInfo("shift-direction", Direction.class)); } diff --git a/src/java/org/apache/fop/area/inline/AbstractTextArea.java b/src/java/org/apache/fop/area/inline/AbstractTextArea.java index 1558e8160..348820939 100644 --- a/src/java/org/apache/fop/area/inline/AbstractTextArea.java +++ b/src/java/org/apache/fop/area/inline/AbstractTextArea.java @@ -188,7 +188,7 @@ public abstract class AbstractTextArea extends InlineParent { @Override int getVirtualOffset() { - return getOffset(); + return getBlockProgressionOffset(); } @Override diff --git a/src/java/org/apache/fop/area/inline/BasicLinkArea.java b/src/java/org/apache/fop/area/inline/BasicLinkArea.java index fce913944..c91181946 100644 --- a/src/java/org/apache/fop/area/inline/BasicLinkArea.java +++ b/src/java/org/apache/fop/area/inline/BasicLinkArea.java @@ -20,6 +20,7 @@ package org.apache.fop.area.inline; import org.apache.fop.area.Area; +import org.apache.fop.area.LinkResolver; /** * An inline area produced by an fo:basic-link element. This class implements a different @@ -33,6 +34,8 @@ public class BasicLinkArea extends InlineParent { private static final long serialVersionUID = 5183753430412208151L; + private LinkResolver resolver; + @Override public void setParentArea(Area parentArea) { super.setParentArea(parentArea); @@ -42,12 +45,28 @@ public class BasicLinkArea extends InlineParent { * called after all of the children areas have been added to this area. */ /* Make this area start at its beforest child. */ - setOffset(getOffset() + minChildOffset); + setBlockProgressionOffset(getBlockProgressionOffset() + minChildOffset); /* Update children offsets accordingly. */ for (InlineArea inline : inlines) { - inline.setOffset(inline.getOffset() - minChildOffset); + inline.setBlockProgressionOffset(inline.getBlockProgressionOffset() - minChildOffset); } setBPD(getVirtualBPD()); } + /** + * Establish (or remove) back-pointer to link resolver. + * @param resolver the link resolver that will resolve this basic link or null + */ + public void setResolver(LinkResolver resolver) { + assert ( resolver == null ) || ( this.resolver == null ); + this.resolver = resolver; + } + + /** + * Obtain back-pointer to link resolver. + * @return resolver the link resolver that will resolve this basic link or null + */ + public LinkResolver getResolver() { + return this.resolver; + } } diff --git a/src/java/org/apache/fop/area/inline/FilledArea.java b/src/java/org/apache/fop/area/inline/FilledArea.java index e0207ada6..dfb3af58b 100644 --- a/src/java/org/apache/fop/area/inline/FilledArea.java +++ b/src/java/org/apache/fop/area/inline/FilledArea.java @@ -50,10 +50,9 @@ public class FilledArea extends InlineParent { * @param v the offset */ /* - public void setOffset(int v) { + public void setBlockProgressionOffset(int v) { setChildOffset(inlines.listIterator(), v); } - */ private void setChildOffset(ListIterator childrenIterator, int v) { while (childrenIterator.hasNext()) { @@ -63,10 +62,11 @@ public class FilledArea extends InlineParent { } else if (child instanceof InlineViewport) { // nothing } else { - child.setOffset(v); + child.setBlockProgressionOffset(v); } } } + */ /** * Set the unit width for the areas to fill the full width. @@ -129,5 +129,6 @@ public class FilledArea extends InlineParent { setIPD(getIPD() + adjustingInfo.applyVariationFactor(variationFactor)); return false; } + } diff --git a/src/java/org/apache/fop/area/inline/InlineArea.java b/src/java/org/apache/fop/area/inline/InlineArea.java index d62e6f721..5fa156ac8 100644 --- a/src/java/org/apache/fop/area/inline/InlineArea.java +++ b/src/java/org/apache/fop/area/inline/InlineArea.java @@ -20,10 +20,12 @@ package org.apache.fop.area.inline; import java.io.Serializable; +import java.util.List; import org.apache.fop.area.Area; import org.apache.fop.area.LineArea; import org.apache.fop.area.Trait; +import org.apache.fop.complexscripts.bidi.InlineRun; /** * Inline Area @@ -79,7 +81,7 @@ public class InlineArea extends Area { /** * offset position from before edge of parent area */ - protected int offset = 0; + protected int blockProgressionOffset = 0; /** * parent area @@ -101,6 +103,23 @@ public class InlineArea extends Area { protected InlineAdjustingInfo adjustingInfo = null; /** + * Default constructor for inline area. + */ + public InlineArea() { + this ( 0, -1 ); + } + + /** + * Instantiate inline area. + * @param blockProgressionOffset a block progression offset or zero + * @param bidiLevel a resolved bidi level or -1 + */ + protected InlineArea ( int blockProgressionOffset, int bidiLevel ) { + this.blockProgressionOffset = blockProgressionOffset; + setBidiLevel(bidiLevel); + } + + /** * @return the adjustment information object */ public InlineAdjustingInfo getAdjustingInfo() { @@ -138,25 +157,25 @@ public class InlineArea extends Area { } /** - * Set the offset of this inline area. + * Set the block progression offset of this inline area. * This is used to set the offset of the inline area * which is relative to the before edge of the parent area. * - * @param offset the offset + * @param blockProgressionOffset the offset */ - public void setOffset(int offset) { - this.offset = offset; + public void setBlockProgressionOffset(int blockProgressionOffset) { + this.blockProgressionOffset = blockProgressionOffset; } /** - * Get the offset of this inline area. + * Get the block progression offset of this inline area. * This returns the offset of the inline area - * which is relative to the before edge of the parent area. + * relative to the before edge of the parent area. * - * @return the offset + * @return the blockProgressionOffset */ - public int getOffset() { - return offset; + public int getBlockProgressionOffset() { + return blockProgressionOffset; } /** @@ -266,7 +285,7 @@ public class InlineArea extends Area { * @see BasicLinkArea */ int getVirtualOffset() { - return getOffset(); + return getBlockProgressionOffset(); } /** @@ -278,5 +297,43 @@ public class InlineArea extends Area { int getVirtualBPD() { return getBPD(); } -} + /** + * Collection bidi inline runs. + * @param runs current list of inline runs + * @return modified list of inline runs, having appended new run + */ + public List collectInlineRuns ( List runs ) { + assert runs != null; + runs.add ( new InlineRun ( this, new int[] {getBidiLevel()}) ); + return runs; + } + + /** + * Determine if inline area IA is an ancestor inline area or same as this area. + * @param ia inline area to test + * @return true if specified inline area is an ancestor or same as this area + */ + public boolean isAncestorOrSelf ( InlineArea ia ) { + return ( ia == this ) || isAncestor ( ia ); + } + + /** + * Determine if inline area IA is an ancestor inline area of this area. + * @param ia inline area to test + * @return true if specified inline area is an ancestor of this area + */ + public boolean isAncestor ( InlineArea ia ) { + for ( Area p = getParentArea(); p != null;) { + if ( p == ia ) { + return true; + } else if ( p instanceof InlineArea ) { + p = ( (InlineArea) p ).getParentArea(); + } else { + p = null; + } + } + return false; + } + +} diff --git a/src/java/org/apache/fop/area/inline/InlineParent.java b/src/java/org/apache/fop/area/inline/InlineParent.java index 521080469..0e1f32dff 100644 --- a/src/java/org/apache/fop/area/inline/InlineParent.java +++ b/src/java/org/apache/fop/area/inline/InlineParent.java @@ -20,6 +20,7 @@ package org.apache.fop.area.inline; import java.util.List; +import java.util.Iterator; import org.apache.fop.area.Area; @@ -62,6 +63,7 @@ public class InlineParent extends InlineArea { if (autoSize) { increaseIPD(childArea.getAllocIPD()); } + updateLevel ( childArea.getBidiLevel() ); int childOffset = childArea.getVirtualOffset(); minChildOffset = Math.min(minChildOffset, childOffset); maxAfterEdge = Math.max(maxAfterEdge, childOffset + childArea.getVirtualBPD()); @@ -69,7 +71,7 @@ public class InlineParent extends InlineArea { @Override int getVirtualOffset() { - return getOffset() + minChildOffset; + return getBlockProgressionOffset() + minChildOffset; } @Override @@ -110,5 +112,37 @@ public class InlineParent extends InlineArea { return hasUnresolvedAreas; } -} + @Override + public List collectInlineRuns ( List runs ) { + for ( Iterator<InlineArea> it = getChildAreas().iterator(); it.hasNext();) { + InlineArea ia = it.next(); + runs = ia.collectInlineRuns ( runs ); + } + return runs; + } + + /** + * Reset bidirectionality level of all children to default (-1), + * signalling that they will inherit the level of their parent text area. + */ + public void resetChildrenLevel() { + for ( Iterator it = inlines.iterator(); it.hasNext();) { + ( (InlineArea) it.next() ) .resetBidiLevel(); + } + } + private void updateLevel ( int newLevel ) { + if ( newLevel >= 0 ) { + int curLevel = getBidiLevel(); + if ( curLevel >= 0 ) { + if ( newLevel < curLevel ) { + setBidiLevel ( newLevel ); + } + } else { + setBidiLevel ( newLevel ); + } + } + } + + +} diff --git a/src/java/org/apache/fop/area/inline/InlineViewport.java b/src/java/org/apache/fop/area/inline/InlineViewport.java index 68cc9a797..202a7dad4 100644 --- a/src/java/org/apache/fop/area/inline/InlineViewport.java +++ b/src/java/org/apache/fop/area/inline/InlineViewport.java @@ -50,6 +50,17 @@ public class InlineViewport extends InlineArea implements Viewport { * @param child the child content area of this viewport */ public InlineViewport(Area child) { + this(child, -1); + } + + /** + * Create a new viewport area with the content area. + * + * @param child the child content area of this viewport + * @param bidiLevel the bidirectional embedding level (or -1 if not defined) + */ + public InlineViewport(Area child, int bidiLevel) { + super(0, bidiLevel); this.content = child; } @@ -121,7 +132,7 @@ public class InlineViewport extends InlineArea implements Viewport { out.writeFloat((float) contentPosition.getHeight()); } out.writeBoolean(clip); - out.writeObject(props); + out.writeObject(traits); out.writeObject(content); } @@ -134,7 +145,7 @@ public class InlineViewport extends InlineArea implements Viewport { in.readFloat()); } this.clip = in.readBoolean(); - this.props = (HashMap) in.readObject(); + this.traits = (HashMap) in.readObject(); this.content = (Area) in.readObject(); } diff --git a/src/java/org/apache/fop/area/inline/Space.java b/src/java/org/apache/fop/area/inline/Space.java index bf683bb22..b097e4349 100644 --- a/src/java/org/apache/fop/area/inline/Space.java +++ b/src/java/org/apache/fop/area/inline/Space.java @@ -27,4 +27,10 @@ public class Space extends InlineArea { private static final long serialVersionUID = -8748265505356839796L; + /** + * Default constructor. + */ + public Space() { + } + } diff --git a/src/java/org/apache/fop/area/inline/SpaceArea.java b/src/java/org/apache/fop/area/inline/SpaceArea.java index ebfcc5ec8..c3786ec0d 100644 --- a/src/java/org/apache/fop/area/inline/SpaceArea.java +++ b/src/java/org/apache/fop/area/inline/SpaceArea.java @@ -38,14 +38,15 @@ public class SpaceArea extends InlineArea { /** * Create a space area - * @param s the space character - * @param o the offset for the next area - * @param a is this space adjustable? + * @param space the space character + * @param blockProgressionOffset the offset for the next area + * @param adjustable is this space adjustable? + * @param bidiLevel the bidirectional embedding level (or -1 if not defined) */ - public SpaceArea(char s, int o, boolean a) { - space = s; - offset = o; - isAdjustable = a; + public SpaceArea(int blockProgressionOffset, int bidiLevel, char space, boolean adjustable) { + super ( blockProgressionOffset, bidiLevel ); + this.space = space; + this.isAdjustable = adjustable; } /** @return Returns the space. */ @@ -57,4 +58,5 @@ public class SpaceArea extends InlineArea { public boolean isAdjustable() { return this.isAdjustable; } + } diff --git a/src/java/org/apache/fop/area/inline/TextArea.java b/src/java/org/apache/fop/area/inline/TextArea.java index 15d005025..1d27827f7 100644 --- a/src/java/org/apache/fop/area/inline/TextArea.java +++ b/src/java/org/apache/fop/area/inline/TextArea.java @@ -19,6 +19,10 @@ package org.apache.fop.area.inline; +import java.util.Arrays; + +import org.apache.fop.util.CharUtilities; + /** * A text inline area. */ @@ -57,7 +61,7 @@ public class TextArea extends AbstractTextArea { * @param offset the offset for the next area */ public void addWord(String word, int offset) { - addWord(word, offset, null); + addWord(word, 0, null, null, null, offset); } /** @@ -65,25 +69,51 @@ public class TextArea extends AbstractTextArea { * * @param word the word string * @param offset the offset for the next area + * @param level bidirectional level that applies to entire word + */ + public void addWord(String word, int offset, int level) { + addWord(word, 0, null, makeLevels(level, word.length()), null, offset); + } + + /** + * Create and add a WordArea child to this TextArea. + * + * @param word the word string + * @param ipd the word's ipd * @param letterAdjust the letter adjustment array (may be null) + * @param levels array of resolved bidirectional levels of word characters, + * or null if default level + * @param gposAdjustments array of general position adjustments or null if none apply + * @param blockProgressionOffset the offset for the next area */ - public void addWord(String word, int offset, int[] letterAdjust) { - WordArea wordArea = new WordArea(word, offset, letterAdjust); + public void addWord + ( String word, int ipd, int[] letterAdjust, int[] levels, + int[][] gposAdjustments, int blockProgressionOffset ) { + int minWordLevel = findMinLevel ( levels ); + WordArea wordArea = new WordArea + ( blockProgressionOffset, minWordLevel, word, letterAdjust, levels, gposAdjustments ); + wordArea.setIPD ( ipd ); addChildArea(wordArea); wordArea.setParentArea(this); + updateLevel(minWordLevel); } /** * Create and add a SpaceArea child to this TextArea * - * @param space the space character - * @param offset the offset for the next area + * @param space the space character + * @param ipd the space's ipd + * @param blockProgressionOffset the offset for the next area * @param adjustable is this space adjustable? + * @param level resolved bidirection level of space character */ - public void addSpace(char space, int offset, boolean adjustable) { - SpaceArea spaceArea = new SpaceArea(space, offset, adjustable); + public void addSpace + ( char space, int ipd, boolean adjustable, int blockProgressionOffset, int level ) { + SpaceArea spaceArea = new SpaceArea(blockProgressionOffset, level, space, adjustable); + spaceArea.setIPD ( ipd ); addChildArea(spaceArea); spaceArea.setParentArea(this); + updateLevel(level); } /** @@ -113,7 +143,55 @@ public class TextArea extends AbstractTextArea { /** {@inheritDoc} */ @Override public String toString() { - return "TextArea{text=" + getText() + "}"; + StringBuffer sb = new StringBuffer(super.toString()); + sb.append(" {text=\""); + sb.append(CharUtilities.toNCRefs(getText())); + sb.append("\""); + sb.append("}"); + return sb.toString(); } + + private void updateLevel ( int newLevel ) { + if ( newLevel >= 0 ) { + int curLevel = getBidiLevel(); + if ( curLevel >= 0 ) { + if ( newLevel < curLevel ) { + setBidiLevel ( newLevel ); + } + } else { + setBidiLevel ( newLevel ); + } + } + } + + private static int findMinLevel ( int[] levels ) { + if ( levels != null ) { + int lMin = Integer.MAX_VALUE; + for ( int i = 0, n = levels.length; i < n; i++ ) { + int l = levels [ i ]; + if ( ( l >= 0 ) && ( l < lMin ) ) { + lMin = l; + } + } + if ( lMin == Integer.MAX_VALUE ) { + return -1; + } else { + return lMin; + } + } else { + return -1; + } + } + + private int[] makeLevels ( int level, int count ) { + if ( level >= 0 ) { + int[] levels = new int [ count ]; + Arrays.fill ( levels, level ); + return levels; + } else { + return null; + } + } + } diff --git a/src/java/org/apache/fop/area/inline/UnresolvedPageNumber.java b/src/java/org/apache/fop/area/inline/UnresolvedPageNumber.java index d3bb91045..71a3dfa62 100644 --- a/src/java/org/apache/fop/area/inline/UnresolvedPageNumber.java +++ b/src/java/org/apache/fop/area/inline/UnresolvedPageNumber.java @@ -19,12 +19,13 @@ package org.apache.fop.area.inline; +import java.util.List; + import org.apache.fop.area.PageViewport; import org.apache.fop.area.Resolvable; +import org.apache.fop.complexscripts.bidi.InlineRun; import org.apache.fop.fonts.Font; -import java.util.List; - /** * Unresolvable page number area. * This is a word area that resolves itself to a page number @@ -83,12 +84,24 @@ public class UnresolvedPageNumber extends TextArea implements Resolvable { } /** + * Get the (resolved or unresolved) text. + * + * @return the text + */ + public String getText() { + return text; + } + + /** * Resolve the page number idref * This resolves the idref for this object by getting the page number * string from the first page in the list of pages that apply * for this ID. The page number text is then set to the String value * of the page number. * + * TODO: [GA] May need to run bidi algorithm and script processor + * on resolved page number. + * * @param id an id whose PageViewports have been determined * @param pages the list of PageViewports associated with this ID */ @@ -103,7 +116,7 @@ public class UnresolvedPageNumber extends TextArea implements Resolvable { // replace the text removeText(); text = page.getPageNumberString(); - addWord(text, 0); + addWord(text, 0, getBidiLevel()); // update ipd if (font != null) { handleIPDVariation(font.getWordWidth(text) - getIPD()); @@ -137,4 +150,21 @@ public class UnresolvedPageNumber extends TextArea implements Resolvable { int lineStretch, int lineShrink) { return true; } + + /** + * Collection bidi inline runs. + * Override of @{link InlineParent} implementation. + * + * N.B. [GA] without this override, the page-number-citation_writing_mode_rl + * layout engine test will fail. It may be that the test needs to + * be updated rather than using this override. + * @param runs current list of inline runs + * @return modified list of inline runs, having appended new run + */ + @Override + public List collectInlineRuns ( List runs ) { + assert runs != null; + runs.add ( new InlineRun ( this, new int[] {getBidiLevel()}) ); + return runs; + } } diff --git a/src/java/org/apache/fop/area/inline/WordArea.java b/src/java/org/apache/fop/area/inline/WordArea.java index 7f62fe8d7..00ddc9e1d 100644 --- a/src/java/org/apache/fop/area/inline/WordArea.java +++ b/src/java/org/apache/fop/area/inline/WordArea.java @@ -19,6 +19,12 @@ package org.apache.fop.area.inline; +import java.util.Arrays; +import java.util.List; + +import org.apache.fop.complexscripts.bidi.InlineRun; +import org.apache.fop.complexscripts.util.CharMirror; + /** * A string of characters without spaces */ @@ -29,22 +35,63 @@ public class WordArea extends InlineArea { /** The text for this word area */ protected String word; - /** The correction offset for the next area */ - protected int offset = 0; - /** An array of width for adjusting the individual letters (optional) */ protected int[] letterAdjust; /** + * An array of resolved bidirectional levels corresponding to each character + * in word (optional) + */ + protected int[] levels; + + /** + * An array of glyph positioning adjustments to apply to each glyph 'char' in word (optional) + */ + protected int[][] gposAdjustments; + + /** + * A flag indicating whether the content of word is reversed in relation to + * its original logical order. + */ + protected boolean reversed; + + /** + * Create a word area + * @param blockProgressionOffset the offset for this area + * @param level the bidirectional embedding level (or -1 if not defined) for word as a group + * @param word the word string + * @param letterAdjust the letter adjust array (may be null) + * @param levels array of per-character (glyph) bidirectional levels, + * in case word area is heterogenously leveled + * @param gposAdjustments array of general position adjustments or null if none apply + * @param reversed true if word is known to be reversed at construction time + */ + public WordArea + ( int blockProgressionOffset, int level, String word, int[] letterAdjust, int[] levels, + int[][] gposAdjustments, boolean reversed ) { + super ( blockProgressionOffset, level ); + int length = ( word != null ) ? word.length() : 0; + this.word = word; + this.letterAdjust = maybeAdjustLength ( letterAdjust, length ); + this.levels = maybePopulateLevels ( levels, level, length ); + this.gposAdjustments = maybeAdjustLength ( gposAdjustments, length ); + this.reversed = reversed; + } + + /** * Create a word area - * @param w the word string - * @param o the offset for the next area - * @param la the letter adjust array (may be null) + * @param blockProgressionOffset the offset for this area + * @param level the bidirectional embedding level (or -1 if not defined) for word as a group + * @param word the word string + * @param letterAdjust the letter adjust array (may be null) + * @param levels array of per-character (glyph) bidirectional levels, + * in case word area is heterogenously leveled + * @param gposAdjustments array of general position adjustments or null if none apply */ - public WordArea(String w, int o, int[] la) { - word = w; - offset = o; - this.letterAdjust = la; + public WordArea + ( int blockProgressionOffset, int level, String word, int[] letterAdjust, int[] levels, + int[][] gposAdjustments ) { + this ( blockProgressionOffset, level, word, letterAdjust, levels, gposAdjustments, false ); } /** @return Returns the word. */ @@ -52,20 +99,211 @@ public class WordArea extends InlineArea { return word; } - /** @return Returns the offset. */ - @Override - public int getOffset() { - return offset; + /** @return the array of letter adjust widths */ + public int[] getLetterAdjustArray() { + return this.letterAdjust; } - /** @param o The offset to set. */ + + /** + * Obtain per-character (glyph) bidi levels. + * @return a (possibly empty) array of levels or null (if none resolved) + */ + public int[] getBidiLevels() { + return levels; + } + + /** + * <p>Obtain per-character (glyph) bidi levels over a specified subsequence.</p> + * <p>If word has been reversed, then the subsequence is over the reversed word.</p> + * @param start starting (inclusive) index of subsequence + * @param end ending (exclusive) index of subsequence + * @return a (possibly null) array of per-character (glyph) levels over the specified + * sequence + */ + public int[] getBidiLevels ( int start, int end ) { + assert start <= end; + if ( this.levels != null ) { + int n = end - start; + int[] levels = new int [ n ]; + for ( int i = 0; i < n; i++ ) { + levels[i] = this.levels [ start + i ]; + } + return levels; + } else { + return null; + } + } + + /** + * <p>Obtain per-character (glyph) level at a specified index position.</p> + * <p>If word has been reversed, then the position is relative to the reversed word.</p> + * @param position the index of the (possibly reversed) character from which to obtain the + * level + * @return a resolved bidirectional level or, if not specified, then -1 + */ + public int bidiLevelAt ( int position ) { + if ( position > word.length() ) { + throw new IndexOutOfBoundsException(); + } else if ( levels != null ) { + return levels [ position ]; + } else { + return -1; + } + } + @Override - public void setOffset(int o) { - offset = o; + public List collectInlineRuns ( List runs ) { + assert runs != null; + InlineRun r; + if ( getBidiLevels() != null ) { + r = new InlineRun ( this, getBidiLevels() ); + } else { + r = new InlineRun ( this, -1, word.length() ); + } + runs.add ( r ); + return runs; } - /** @return the array of letter adjust widths */ - public int[] getLetterAdjustArray() { - return this.letterAdjust; + /** + * Obtain per-character (glyph) position adjustments. + * @return a (possibly empty) array of adjustments, each having four elements, or null + * if no adjustments apply + */ + public int[][] getGlyphPositionAdjustments() { + return gposAdjustments; + } + + /** + * <p>Obtain per-character (glyph) position adjustments at a specified index position.</p> + * <p>If word has been reversed, then the position is relative to the reversed word.</p> + * @param position the index of the (possibly reversed) character from which to obtain the + * level + * @return an array of adjustments or null if none applies + */ + public int[] glyphPositionAdjustmentsAt ( int position ) { + if ( position > word.length() ) { + throw new IndexOutOfBoundsException(); + } else if ( gposAdjustments != null ) { + return gposAdjustments [ position ]; + } else { + return null; + } + } + + /** + * <p>Reverse characters and corresponding per-character levels and glyph position + * adjustments.</p> + * @param mirror if true, then perform mirroring if mirrorred characters + */ + public void reverse ( boolean mirror ) { + if ( word.length() > 0 ) { + word = ( ( new StringBuffer ( word ) ) .reverse() ) .toString(); + if ( levels != null ) { + reverse ( levels ); + } + if ( gposAdjustments != null ) { + reverse ( gposAdjustments ); + } + reversed = !reversed; + if ( mirror ) { + word = CharMirror.mirror ( word ); + } + } + } + + /** + * <p>Perform mirroring on mirrorable characters.</p> + */ + public void mirror() { + if ( word.length() > 0 ) { + word = CharMirror.mirror ( word ); + } + } + + /** + * <p>Determined if word has been reversed (in relation to original logical order).</p> + * <p>If a word is reversed, then both its characters (glyphs) and corresponding per-character + * levels are in reverse order.</p> + * <p>Note: this information is used in order to process non-spacing marks during rendering as + * well as provide hints for caret direction.</p> + * @return true if word is reversed + */ + public boolean isReversed() { + return reversed; + } + + /* + * If int[] array is not of specified length, then create + * a new copy of the first length entries. + */ + private static int[] maybeAdjustLength ( int[] ia, int length ) { + if ( ia != null ) { + if ( ia.length == length ) { + return ia; + } else { + int[] iaNew = new int [ length ]; + for ( int i = 0, n = ia.length; i < n; i++ ) { + if ( i < length ) { + iaNew [ i ] = ia [ i ]; + } else { + break; + } + } + return iaNew; + } + } else { + return ia; + } + } + + /* + * If int[][] matrix is not of specified length, then create + * a new shallow copy of the first length entries. + */ + private static int[][] maybeAdjustLength ( int[][] im, int length ) { + if ( im != null ) { + if ( im.length == length ) { + return im; + } else { + int[][] imNew = new int [ length ][]; + for ( int i = 0, n = im.length; i < n; i++ ) { + if ( i < length ) { + imNew [ i ] = im [ i ]; + } else { + break; + } + } + return imNew; + } + } else { + return im; + } + } + + private static int[] maybePopulateLevels ( int[] levels, int level, int count ) { + if ( ( levels == null ) && ( level >= 0 ) ) { + levels = new int[count]; + Arrays.fill ( levels, level ); + } + return maybeAdjustLength ( levels, count ); + } + + private static void reverse ( int[] a ) { + for ( int i = 0, n = a.length, m = n / 2; i < m; i++ ) { + int k = n - i - 1; + int t = a [ k ]; + a [ k ] = a [ i ]; + a [ i ] = t; + } + } + + private static void reverse ( int[][] aa ) { + for ( int i = 0, n = aa.length, m = n / 2; i < m; i++ ) { + int k = n - i - 1; + int[] t = aa [ k ]; + aa [ k ] = aa [ i ]; + aa [ i ] = t; + } } } diff --git a/src/java/org/apache/fop/cli/CommandLineOptions.java b/src/java/org/apache/fop/cli/CommandLineOptions.java index 845c11a0d..bfcdb163e 100644 --- a/src/java/org/apache/fop/cli/CommandLineOptions.java +++ b/src/java/org/apache/fop/cli/CommandLineOptions.java @@ -118,6 +118,8 @@ public class CommandLineOptions { private int targetResolution = 0; /* control memory-conservation policy */ private boolean conserveMemoryPolicy = false; + /* true if a complex script features are enabled */ + private boolean useComplexScriptFeatures = true; private FopFactory factory = FopFactory.newInstance(); private FOUserAgent foUserAgent; @@ -181,6 +183,9 @@ public class CommandLineOptions { addXSLTParameter("fop-output-format", getOutputFormat()); addXSLTParameter("fop-version", Version.getVersion()); foUserAgent.setConserveMemoryPolicy(conserveMemoryPolicy); + if (!useComplexScriptFeatures) { + foUserAgent.setComplexScriptFeaturesEnabled(false); + } } else { return false; } @@ -378,6 +383,8 @@ public class CommandLineOptions { getPDFEncryptionParams().setAllowEditContent(false); } else if (args[i].equals("-noannotations")) { getPDFEncryptionParams().setAllowEditAnnotations(false); + } else if (args[i].equals("-nocs")) { + useComplexScriptFeatures = false; } else if (args[i].equals("-nofillinforms")) { getPDFEncryptionParams().setAllowFillInForms(false); } else if (args[i].equals("-noaccesscontent")) { @@ -1175,6 +1182,7 @@ public class CommandLineOptions { + " -q quiet mode \n" + " -c cfg.xml use additional configuration file cfg.xml\n" + " -l lang the language to use for user information \n" + + " -nocs disable complex script features\n" + " -r relaxed/less strict validation (where available)\n" + " -dpi xxx target resolution in dots per inch (dpi) where xxx is a number\n" + " -s for area tree XML, down to block areas only\n" diff --git a/src/java/org/apache/fop/complexscripts/bidi/BidiClass.java b/src/java/org/apache/fop/complexscripts/bidi/BidiClass.java new file mode 100644 index 000000000..6e462eadc --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/bidi/BidiClass.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id: License.java 1039179 2010-11-25 21:04:09Z vhennebert $ */ + +package org.apache.fop.complexscripts.bidi; + +import java.util.Arrays; +import org.apache.fop.complexscripts.bidi.BidiConstants; + +// CSOFF: WhitespaceAfterCheck +// CSOFF: LineLengthCheck + +/* + * !!! THIS IS A GENERATED FILE !!! + * If updates to the source are needed, then: + * - apply the necessary modifications to + * 'src/codegen/unicode/java/org/apache/fop/complexscripts/bidi/GenerateBidiClass.java' + * - run 'ant codegen-unicode', which will generate a new BidiClass.java + * in 'src/java/org/apache/fop/complexscripts/bidi' + * - commit BOTH changed files + */ + +/** Bidirectional class utilities. */ +public final class BidiClass { + +private BidiClass() { +} + +private static byte[] bcL1 = { +15,15,15,15,15,15,15,15,15,17,16,17,18,16,15,15,15,15,15,15,15,15,15,15,15,15,15,15,16,16,16,17,18,19,19,11,11,11,19,19,19, +19,19,10,13,10,13,13,9,9,9,9,9,9,9,9,9,9,13,19,19,19,19,19,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,19,19,19, +19,19,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,19,19,19,19,15,15,15,15,15,15,16,15,15,15,15,15,15,15,15,15,15, +15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,13,19,11,11,11,11,19,19,19,19,1,19,19,15,19,19,11,11,9,9,19,1,19,19,19,9,1, +19,19,19,19,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, +1,1,19,1,1,1,1,1,1,1,1 +}; + +private static byte[] bcR1 = { +4,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14, +14,14,14,14,14,4,14,4,14,14,4,14,14,4,14,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, +4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,12,12,12,12,5,5,19,19,5,11,11,5,13,5,19,19,14,14,14,14,14,14,14,14,14,14,14,5,5,5,5,5,5,5,5, +5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,14,14,14,14,14,14,14,14,14,14,14,14,14,14, +14,14,14,14,14,14,14,12,12,12,12,12,12,12,12,12,12,11,12,12,5,5,5,14,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, +5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, +5,5,5,5,5,5,5,5,5,5,5,5,5,14,14,14,14,14,14,14,12,19,14,14,14,14,14,14,5,5,14,14,19,14,14,14,14,5,5,9,9,9,9,9,9,9,9,9,9,5, +5,5,5,5,5 +}; + +private static int[] bcS1 = { +256,443,444,448,452,660,661,688,697,699,706,710,720,722,736,741,748,749,750,751,768,880,884,885,886,890,891,894,900,902,903, +904,908,910,931,1014,1015,1154,1155,1160,1162,1329,1369,1370,1377,1417,1418,1792,1806,1807,1808,1809,1810,1840,1867,1869,1958, +1969,1970,1984,1994,2027,2036,2038,2039,2042,2043,2048,2070,2074,2075,2084,2085,2088,2089,2094,2096,2111,2112,2137,2140,2142, +2143,2304,2307,2308,2362,2363,2364,2365,2366,2369,2377,2381,2382,2384,2385,2392,2402,2404,2406,2416,2417,2418,2425,2433,2434, +2437,2447,2451,2474,2482,2486,2492,2493,2494,2497,2503,2507,2509,2510,2519,2524,2527,2530,2534,2544,2546,2548,2554,2555,2561, +2563,2565,2575,2579,2602,2610,2613,2616,2620,2622,2625,2631,2635,2641,2649,2654,2662,2672,2674,2677,2689,2691,2693,2703,2707, +2730,2738,2741,2748,2749,2750,2753,2759,2761,2763,2765,2768,2784,2786,2790,2801,2817,2818,2821,2831,2835,2858,2866,2869,2876, +2877,2878,2879,2880,2881,2887,2891,2893,2902,2903,2908,2911,2914,2918,2928,2929,2930,2946,2947,2949,2958,2962,2969,2972,2974, +2979,2984,2990,3006,3008,3009,3014,3018,3021,3024,3031,3046,3056,3059,3065,3066,3073,3077,3086,3090,3114,3125,3133,3134,3137, +3142,3146,3157,3160,3168,3170,3174,3192,3199,3202,3205,3214,3218,3242,3253,3260,3261,3262,3263,3264,3270,3271,3274,3276,3285, +3294,3296,3298,3302,3313,3330,3333,3342,3346,3389,3390,3393,3398,3402,3405,3406,3415,3424,3426,3430,3440,3449,3450,3458,3461, +3482,3507,3517,3520,3530,3535,3538,3542,3544,3570,3572,3585,3633,3634,3636,3647,3648,3654,3655,3663,3664,3674,3713,3716,3719, +3722,3725,3732,3737,3745,3749,3751,3754,3757,3761,3762,3764,3771,3773,3776,3782,3784,3792,3804,3840,3841,3844,3859,3864,3866, +3872,3882,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3902,3904,3913,3953,3967,3968,3973,3974,3976,3981,3993,4030,4038, +4039,4046,4048,4053,4057,4096,4139,4141,4145,4146,4152,4153,4155,4157,4159,4160,4170,4176,4182,4184,4186,4190,4193,4194,4197, +4199,4206,4209,4213,4226,4227,4229,4231,4237,4238,4239,4240,4250,4253,4254,4256,4304,4347,4348,4352,4682,4688,4696,4698,4704, +4746,4752,4786,4792,4800,4802,4808,4824,4882,4888,4957,4960,4961,4969,4992,5008,5024,5120,5121,5741,5743,5760,5761,5787,5788, +5792,5867,5870,5888,5902,5906,5920,5938,5941,5952,5970,5984,5998,6002,6016,6068,6070,6071,6078,6086,6087,6089,6100,6103,6104, +6107,6108,6109,6112,6128,6144,6150,6151,6155,6158,6160,6176,6211,6212,6272,6313,6314,6320,6400,6432,6435,6439,6441,6448,6450, +6451,6457,6464,6468,6470,6480,6512,6528,6576,6593,6600,6608,6618,6622,6656,6679,6681,6686,6688,6741,6742,6743,6744,6752,6753, +6754,6755,6757,6765,6771,6783,6784,6800,6816,6823,6824,6912,6916,6917,6964,6965,6966,6971,6972,6973,6978,6979,6981,6992,7002, +7009,7019,7028,7040,7042,7043,7073,7074,7078,7080,7082,7086,7088,7104,7142,7143,7144,7146,7149,7150,7151,7154,7164,7168,7204, +7212,7220,7222,7227,7232,7245,7248,7258,7288,7294,7376,7379,7380,7393,7394,7401,7405,7406,7410,7424,7468,7522,7544,7545,7579, +7616,7676,7680,7960,7968,8008,8016,8025,8027,8029,8031,8064,8118,8125,8126,8127,8130,8134,8141,8144,8150,8157,8160,8173,8178, +8182,8189,8192,8203,8206,8207,8208,8214,8216,8217,8218,8219,8221,8222,8223,8224,8232,8233,8234,8235,8236,8237,8238,8239,8240, +8245,8249,8250,8251,8255,8257,8260,8261,8262,8263,8274,8275,8276,8277,8287,8288,8293,8298,8304,8305,8308,8314,8316,8317,8318, +8319,8320,8330,8332,8333,8334,8336,8352,8400,8413,8417,8418,8421,8448,8450,8451,8455,8456,8458,8468,8469,8470,8472,8473,8478, +8484,8485,8486,8487,8488,8489,8490,8494,8495,8501,8505,8506,8508,8512,8517,8522,8523,8524,8526,8527,8528,8544,8579,8581,8585, +8592,8597,8602,8604,8608,8609,8611,8612,8614,8615,8622,8623,8654,8656,8658,8659,8660,8661,8692,8722,8723,8724,8960,8968,8972, +8992,8994,9001,9002,9003,9014,9083,9084,9085,9109,9110,9115,9140,9180,9186,9216,9280,9312,9352,9372,9450,9472,9655,9656,9665, +9666,9720,9728,9839,9840,9900,9901,9985,10088,10089,10090,10091,10092,10093,10094,10095,10096,10097,10098,10099,10100,10101, +10102,10132,10176,10181,10182,10183,10188,10190,10214,10215,10216,10217,10218,10219,10220,10221,10222,10223,10224,10240,10496, +10627,10628,10629,10630,10631,10632,10633,10634,10635,10636,10637,10638,10639,10640,10641,10642,10643,10644,10645,10646,10647, +10648,10649,10712,10713,10714,10715,10716,10748,10749,10750,11008,11056,11077,11079,11088,11264,11312,11360,11389,11390,11493, +11499,11503,11513,11517,11518,11520,11568,11631,11632,11647,11648,11680,11688,11696,11704,11712,11720,11728,11736,11744,11776, +11778,11779,11780,11781,11782,11785,11786,11787,11788,11789,11790,11799,11800,11802,11803,11804,11805,11806,11808,11809,11810, +11811,11812,11813,11814,11815,11816,11817,11818,11823,11824,11904,11931,12032,12272,12288,12289,12292,12293,12294,12295,12296, +12297,12298,12299,12300,12301,12302,12303,12304,12305,12306,12308,12309,12310,12311,12312,12313,12314,12315,12316,12317,12318, +12320,12321,12330,12336,12337,12342,12344,12347,12348,12349,12350,12353,12441,12443,12445,12447,12448,12449,12539,12540,12543, +12549,12593,12688,12690,12694,12704,12736,12784,12800,12829,12832,12842,12880,12881,12896,12924,12927,12928,12938,12977,12992, +13004,13008,13056,13175,13179,13278,13280,13311,13312,19904,19968,40960,40981,40982,42128,42192,42232,42238,42240,42508,42509, +42512,42528,42538,42560,42606,42607,42608,42611,42620,42622,42623,42624,42656,42726,42736,42738,42752,42775,42784,42786,42864, +42865,42888,42889,42891,42896,42912,43002,43003,43010,43011,43014,43015,43019,43020,43043,43045,43047,43048,43056,43062,43064, +43065,43072,43124,43136,43138,43188,43204,43214,43216,43232,43250,43256,43259,43264,43274,43302,43310,43312,43335,43346,43359, +43360,43392,43395,43396,43443,43444,43446,43450,43452,43453,43457,43471,43472,43486,43520,43561,43567,43569,43571,43573,43584, +43587,43588,43596,43597,43600,43612,43616,43632,43633,43639,43642,43643,43648,43696,43697,43698,43701,43703,43705,43710,43712, +43713,43714,43739,43741,43742,43777,43785,43793,43808,43816,43968,44003,44005,44006,44008,44009,44011,44012,44013,44016,44032, +55216,55243,57344,63744,64048,64112,64256,64275,64285,64286,64287,64297,64298,64311,64312,64317,64318,64319,64320,64322,64323, +64325,64326,64336,64434,64450,64467,64830,64831,64832,64848,64912,64914,64968,64976,65008,65020,65021,65022,65024,65040,65047, +65048,65049,65056,65072,65073,65075,65077,65078,65079,65080,65081,65082,65083,65084,65085,65086,65087,65088,65089,65090,65091, +65092,65093,65095,65096,65097,65101,65104,65105,65106,65108,65109,65110,65112,65113,65114,65115,65116,65117,65118,65119,65120, +65122,65123,65124,65128,65129,65130,65131,65136,65141,65142,65277,65279,65281,65283,65284,65285,65286,65288,65289,65290,65291, +65292,65293,65294,65296,65306,65307,65308,65311,65313,65339,65340,65341,65342,65343,65344,65345,65371,65372,65373,65374,65375, +65376,65377,65378,65379,65380,65382,65392,65393,65438,65440,65474,65482,65490,65498,65504,65506,65507,65508,65509,65512,65513, +65517,65520,65529,65532,65534,65536,65549,65576,65596,65599,65616,65664,65792,65793,65794,65799,65847,65856,65909,65913,65930, +65936,66000,66045,66176,66208,66304,66336,66352,66369,66370,66378,66432,66463,66464,66504,66512,66513,66560,66640,66720,67584, +67590,67592,67593,67594,67638,67639,67641,67644,67645,67647,67670,67671,67672,67680,67840,67862,67868,67871,67872,67898,67903, +67904,68096,68097,68100,68101,68103,68108,68112,68116,68117,68120,68121,68148,68152,68155,68159,68160,68168,68176,68185,68192, +68221,68223,68224,68352,68406,68409,68416,68438,68440,68448,68467,68472,68480,68608,68681,69216,69247,69632,69633,69634,69635, +69688,69703,69714,69734,69760,69762,69763,69808,69811,69815,69817,69819,69821,69822,73728,74752,74864,77824,92160,110592,118784, +119040,119081,119141,119143,119146,119149,119155,119163,119171,119173,119180,119210,119214,119296,119362,119365,119552,119648, +119808,119894,119966,119970,119973,119977,119982,119995,119997,120005,120071,120077,120086,120094,120123,120128,120134,120138, +120146,120488,120513,120514,120539,120540,120571,120572,120597,120598,120629,120630,120655,120656,120687,120688,120713,120714, +120745,120746,120771,120772,120782,124928,126976,127024,127136,127153,127169,127185,127232,127248,127280,127344,127462,127504, +127552,127568,127744,127792,127799,127872,127904,127942,127968,128000,128064,128066,128140,128141,128249,128256,128292,128293, +128336,128507,128513,128530,128534,128536,128538,128540,128544,128552,128557,128560,128565,128581,128640,128768,131070,131072, +173824,177984,194560,196606,262142,327678,393214,458750,524286,589822,655358,720894,786430,851966,917502,917505,917506,917536, +917632,917760,918000,983038,983040,1048574,1048576,1114110 +}; + +private static int[] bcE1 = { +442,443,447,451,659,660,687,696,698,705,709,719,721,735,740,747,748,749,750,767,879,883,884,885,887,890,893,894,901,902,903, +906,908,929,1013,1014,1153,1154,1159,1161,1319,1366,1369,1375,1415,1417,1418,1805,1806,1807,1808,1809,1839,1866,1868,1957, +1968,1969,1983,1993,2026,2035,2037,2038,2041,2042,2047,2069,2073,2074,2083,2084,2087,2088,2093,2095,2110,2111,2136,2139,2141, +2142,2303,2306,2307,2361,2362,2363,2364,2365,2368,2376,2380,2381,2383,2384,2391,2401,2403,2405,2415,2416,2417,2423,2431,2433, +2435,2444,2448,2472,2480,2482,2489,2492,2493,2496,2500,2504,2508,2509,2510,2519,2525,2529,2531,2543,2545,2547,2553,2554,2555, +2562,2563,2570,2576,2600,2608,2611,2614,2617,2620,2624,2626,2632,2637,2641,2652,2654,2671,2673,2676,2677,2690,2691,2701,2705, +2728,2736,2739,2745,2748,2749,2752,2757,2760,2761,2764,2765,2768,2785,2787,2799,2801,2817,2819,2828,2832,2856,2864,2867,2873, +2876,2877,2878,2879,2880,2884,2888,2892,2893,2902,2903,2909,2913,2915,2927,2928,2929,2935,2946,2947,2954,2960,2965,2970,2972, +2975,2980,2986,3001,3007,3008,3010,3016,3020,3021,3024,3031,3055,3058,3064,3065,3066,3075,3084,3088,3112,3123,3129,3133,3136, +3140,3144,3149,3158,3161,3169,3171,3183,3198,3199,3203,3212,3216,3240,3251,3257,3260,3261,3262,3263,3268,3270,3272,3275,3277, +3286,3294,3297,3299,3311,3314,3331,3340,3344,3386,3389,3392,3396,3400,3404,3405,3406,3415,3425,3427,3439,3445,3449,3455,3459, +3478,3505,3515,3517,3526,3530,3537,3540,3542,3551,3571,3572,3632,3633,3635,3642,3647,3653,3654,3662,3663,3673,3675,3714,3716, +3720,3722,3725,3735,3743,3747,3749,3751,3755,3760,3761,3763,3769,3772,3773,3780,3782,3789,3801,3805,3840,3843,3858,3863,3865, +3871,3881,3891,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3903,3911,3948,3966,3967,3972,3973,3975,3980,3991,4028,4037, +4038,4044,4047,4052,4056,4058,4138,4140,4144,4145,4151,4152,4154,4156,4158,4159,4169,4175,4181,4183,4185,4189,4192,4193,4196, +4198,4205,4208,4212,4225,4226,4228,4230,4236,4237,4238,4239,4249,4252,4253,4255,4293,4346,4347,4348,4680,4685,4694,4696,4701, +4744,4749,4784,4789,4798,4800,4805,4822,4880,4885,4954,4959,4960,4968,4988,5007,5017,5108,5120,5740,5742,5759,5760,5786,5787, +5788,5866,5869,5872,5900,5905,5908,5937,5940,5942,5969,5971,5996,6000,6003,6067,6069,6070,6077,6085,6086,6088,6099,6102,6103, +6106,6107,6108,6109,6121,6137,6149,6150,6154,6157,6158,6169,6210,6211,6263,6312,6313,6314,6389,6428,6434,6438,6440,6443,6449, +6450,6456,6459,6464,6469,6479,6509,6516,6571,6592,6599,6601,6617,6618,6655,6678,6680,6683,6687,6740,6741,6742,6743,6750,6752, +6753,6754,6756,6764,6770,6780,6783,6793,6809,6822,6823,6829,6915,6916,6963,6964,6965,6970,6971,6972,6977,6978,6980,6987,7001, +7008,7018,7027,7036,7041,7042,7072,7073,7077,7079,7081,7082,7087,7097,7141,7142,7143,7145,7148,7149,7150,7153,7155,7167,7203, +7211,7219,7221,7223,7231,7241,7247,7257,7287,7293,7295,7378,7379,7392,7393,7400,7404,7405,7409,7410,7467,7521,7543,7544,7578, +7615,7654,7679,7957,7965,8005,8013,8023,8025,8027,8029,8061,8116,8124,8125,8126,8129,8132,8140,8143,8147,8155,8159,8172,8175, +8180,8188,8190,8202,8205,8206,8207,8213,8215,8216,8217,8218,8220,8221,8222,8223,8231,8232,8233,8234,8235,8236,8237,8238,8239, +8244,8248,8249,8250,8254,8256,8259,8260,8261,8262,8273,8274,8275,8276,8286,8287,8292,8297,8303,8304,8305,8313,8315,8316,8317, +8318,8319,8329,8331,8332,8333,8334,8348,8377,8412,8416,8417,8420,8432,8449,8450,8454,8455,8457,8467,8468,8469,8471,8472,8477, +8483,8484,8485,8486,8487,8488,8489,8493,8494,8500,8504,8505,8507,8511,8516,8521,8522,8523,8525,8526,8527,8543,8578,8580,8584, +8585,8596,8601,8603,8607,8608,8610,8611,8613,8614,8621,8622,8653,8655,8657,8658,8659,8660,8691,8721,8722,8723,8959,8967,8971, +8991,8993,9000,9001,9002,9013,9082,9083,9084,9108,9109,9114,9139,9179,9185,9203,9254,9290,9351,9371,9449,9471,9654,9655,9664, +9665,9719,9727,9838,9839,9899,9900,9983,10087,10088,10089,10090,10091,10092,10093,10094,10095,10096,10097,10098,10099,10100, +10101,10131,10175,10180,10181,10182,10186,10188,10213,10214,10215,10216,10217,10218,10219,10220,10221,10222,10223,10239,10495, +10626,10627,10628,10629,10630,10631,10632,10633,10634,10635,10636,10637,10638,10639,10640,10641,10642,10643,10644,10645,10646, +10647,10648,10711,10712,10713,10714,10715,10747,10748,10749,11007,11055,11076,11078,11084,11097,11310,11358,11388,11389,11492, +11498,11502,11505,11516,11517,11519,11557,11621,11631,11632,11647,11670,11686,11694,11702,11710,11718,11726,11734,11742,11775, +11777,11778,11779,11780,11781,11784,11785,11786,11787,11788,11789,11798,11799,11801,11802,11803,11804,11805,11807,11808,11809, +11810,11811,11812,11813,11814,11815,11816,11817,11822,11823,11825,11929,12019,12245,12283,12288,12291,12292,12293,12294,12295, +12296,12297,12298,12299,12300,12301,12302,12303,12304,12305,12307,12308,12309,12310,12311,12312,12313,12314,12315,12316,12317, +12319,12320,12329,12335,12336,12341,12343,12346,12347,12348,12349,12351,12438,12442,12444,12446,12447,12448,12538,12539,12542, +12543,12589,12686,12689,12693,12703,12730,12771,12799,12828,12830,12841,12879,12880,12895,12923,12926,12927,12937,12976,12991, +13003,13007,13054,13174,13178,13277,13279,13310,13311,19893,19967,40907,40980,40981,42124,42182,42231,42237,42239,42507,42508, +42511,42527,42537,42539,42605,42606,42607,42610,42611,42621,42622,42623,42647,42725,42735,42737,42743,42774,42783,42785,42863, +42864,42887,42888,42890,42894,42897,42921,43002,43009,43010,43013,43014,43018,43019,43042,43044,43046,43047,43051,43061,43063, +43064,43065,43123,43127,43137,43187,43203,43204,43215,43225,43249,43255,43258,43259,43273,43301,43309,43311,43334,43345,43347, +43359,43388,43394,43395,43442,43443,43445,43449,43451,43452,43456,43469,43471,43481,43487,43560,43566,43568,43570,43572,43574, +43586,43587,43595,43596,43597,43609,43615,43631,43632,43638,43641,43642,43643,43695,43696,43697,43700,43702,43704,43709,43711, +43712,43713,43714,43740,43741,43743,43782,43790,43798,43814,43822,44002,44004,44005,44007,44008,44010,44011,44012,44013,44025, +55203,55238,55291,63743,64045,64109,64217,64262,64279,64285,64286,64296,64297,64310,64311,64316,64317,64318,64319,64321,64322, +64324,64325,64335,64433,64449,64466,64829,64830,64831,64847,64911,64913,64967,64975,65007,65019,65020,65021,65023,65039,65046, +65047,65048,65049,65062,65072,65074,65076,65077,65078,65079,65080,65081,65082,65083,65084,65085,65086,65087,65088,65089,65090, +65091,65092,65094,65095,65096,65100,65103,65104,65105,65106,65108,65109,65111,65112,65113,65114,65115,65116,65117,65118,65119, +65121,65122,65123,65126,65128,65129,65130,65131,65140,65141,65276,65278,65279,65282,65283,65284,65285,65287,65288,65289,65290, +65291,65292,65293,65295,65305,65306,65307,65310,65312,65338,65339,65340,65341,65342,65343,65344,65370,65371,65372,65373,65374, +65375,65376,65377,65378,65379,65381,65391,65392,65437,65439,65470,65479,65487,65495,65500,65505,65506,65507,65508,65510,65512, +65516,65518,65528,65531,65533,65535,65547,65574,65594,65597,65613,65629,65786,65792,65793,65794,65843,65855,65908,65912,65929, +65930,65947,66044,66045,66204,66256,66334,66339,66368,66369,66377,66378,66461,66463,66499,66511,66512,66517,66639,66717,66729, +67589,67591,67592,67593,67637,67638,67640,67643,67644,67646,67669,67670,67671,67679,67839,67861,67867,67870,67871,67897,67902, +67903,68095,68096,68099,68100,68102,68107,68111,68115,68116,68119,68120,68147,68151,68154,68158,68159,68167,68175,68184,68191, +68220,68222,68223,68351,68405,68408,68415,68437,68439,68447,68466,68471,68479,68607,68680,69215,69246,69631,69632,69633,69634, +69687,69702,69709,69733,69743,69761,69762,69807,69810,69814,69816,69818,69820,69821,69825,74606,74850,74867,78894,92728,110593, +119029,119078,119140,119142,119145,119148,119154,119162,119170,119172,119179,119209,119213,119261,119361,119364,119365,119638, +119665,119892,119964,119967,119970,119974,119980,119993,119995,120003,120069,120074,120084,120092,120121,120126,120132,120134, +120144,120485,120512,120513,120538,120539,120570,120571,120596,120597,120628,120629,120654,120655,120686,120687,120712,120713, +120744,120745,120770,120771,120779,120831,126975,127019,127123,127150,127166,127183,127199,127242,127278,127337,127386,127490, +127546,127560,127569,127776,127797,127868,127891,127940,127946,127984,128062,128064,128139,128140,128247,128252,128291,128292, +128317,128359,128511,128528,128532,128534,128536,128538,128542,128549,128555,128557,128563,128576,128591,128709,128883,131071, +173782,177972,178205,195101,196607,262143,327679,393215,458751,524287,589823,655359,720895,786431,851967,917504,917505,917535, +917631,917759,917999,921599,983039,1048573,1048575,1114109,1114111 +}; + +private static byte[] bcC1 = { +1,1,1,1,1,1,1,1,19,1,19,19,1,19,1,19,19,19,1,19,14,1,19,19,1,1,1,19,19,1,19,1,1,1,1,19,1,1,14,14,1,1,1,1,1,1,19,5,5,12,5,14, +5,14,5,5,14,5,5,4,4,14,4,19,19,4,4,4,14,4,14,4,14,4,14,4,4,4,4,14,4,4,4,14,1,1,14,1,14,1,1,14,1,14,1,1,14,1,14,1,1,1,1,1,1, +14,1,1,1,1,1,1,1,14,1,1,14,1,1,14,1,1,1,1,14,1,1,11,1,1,11,14,1,1,1,1,1,1,1,1,14,1,14,14,14,14,1,1,1,14,1,14,14,1,1,1,1,1, +1,1,14,1,1,14,14,1,1,14,1,1,14,1,11,14,1,1,1,1,1,1,1,14,1,1,14,1,14,1,1,14,14,1,1,1,14,1,1,1,1,14,1,1,1,1,1,1,1,1,1,1,1,14, +1,1,1,14,1,1,1,1,19,11,19,1,1,1,1,1,1,1,14,1,14,14,14,1,1,14,1,19,1,1,1,1,1,1,1,14,1,1,1,1,1,1,1,14,1,1,1,14,1,1,1,1,1,1,1, +1,14,1,1,14,1,1,1,14,1,1,1,1,1,1,1,1,1,1,14,1,14,14,1,1,1,1,14,1,14,11,1,1,14,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,1,14,14,1,1, +1,14,1,1,1,1,1,1,14,1,1,1,1,14,1,14,1,14,19,19,19,19,1,1,1,14,1,14,1,14,1,14,14,1,14,1,1,1,1,1,1,1,14,1,14,1,14,1,14,1,1,1, +1,1,14,1,14,1,1,1,1,1,14,1,14,1,14,1,14,1,1,1,1,14,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,1,1,1,1,19,1,19,1,1,1,18,1, +19,19,1,1,1,1,1,14,1,14,1,1,14,1,1,14,1,1,1,14,1,14,1,14,1,1,1,11,1,14,1,19,19,19,19,14,18,1,1,1,1,1,14,1,1,1,14,1,14,1,1, +14,1,14,19,19,1,1,1,1,1,1,1,1,1,19,1,14,1,1,1,1,14,1,14,14,1,14,1,14,1,14,14,1,1,1,1,1,14,1,1,14,1,14,1,14,1,14,1,1,1,1,1, +14,1,14,1,1,1,14,1,14,1,1,1,1,14,1,14,1,14,1,14,1,1,1,1,14,1,14,1,1,1,1,1,1,1,14,1,14,1,14,1,14,1,1,1,1,1,1,1,1,14,14,1,1, +1,1,1,1,1,1,1,1,1,19,1,19,1,1,19,1,1,19,1,19,1,1,19,18,15,1,4,19,19,19,19,19,19,19,19,19,19,18,16,2,6,8,3,7,13,11,19,19,19, +19,19,19,13,19,19,19,19,19,19,19,18,15,15,15,9,1,9,10,19,19,19,1,9,10,19,19,19,1,11,14,14,14,14,14,19,1,19,1,19,1,19,1,19, +19,1,19,1,19,1,19,1,19,1,11,1,1,1,19,1,19,1,19,19,19,1,1,19,1,1,1,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19, +19,10,11,19,19,19,19,19,19,19,19,19,1,19,19,19,1,19,19,19,19,19,19,19,19,9,1,19,19,19,19,19,19,19,19,19,19,1,19,19,19,19,19, +19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,1,19,19,19,19,19,19,19,19,19,19, +19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,1,1,1,1,1,19,1,14,19,19,19,1,1,1,1,14,1,1, +1,1,1,1,1,1,1,14,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19, +19,18,19,19,1,1,1,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,1,14,19,1,19,1,1,1,19,19,1,14,19,1, +1,19,1,19,1,1,1,1,1,1,1,1,19,1,1,19,1,1,19,19,1,19,1,1,1,19,1,19,1,1,19,1,19,1,19,1,19,1,1,1,1,19,1,1,1,1,1,19,1,1,1,1,1,14, +14,19,14,19,19,1,1,1,14,1,19,19,19,1,1,1,19,1,1,1,1,1,1,14,1,14,1,14,1,1,14,1,19,1,1,11,11,1,19,1,1,1,14,1,1,14,1,1,1,1,1, +14,1,1,14,1,1,1,14,1,1,14,1,14,1,14,1,1,1,1,1,1,14,1,14,1,14,1,14,1,14,1,1,1,1,1,1,1,1,1,1,14,1,14,1,14,1,14,1,14,1,1,1,1, +1,1,1,1,1,1,1,14,1,14,1,1,1,14,1,1,1,1,1,1,1,1,1,1,4,14,4,10,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,19,19,5,5,5,5,5,15,5,5,19,5,14, +19,19,19,19,14,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,13,19,13,19,13,19,19,19,19,19,19,19, +19,11,19,10,10,19,19,11,11,19,5,5,5,5,15,19,11,11,11,19,19,19,19,10,13,10,13,9,13,19,19,19,1,19,19,19,19,19,19,1,19,19,19, +19,19,19,19,19,19,19,1,1,1,1,1,1,1,1,1,11,19,19,19,11,19,19,19,15,19,19,15,1,1,1,1,1,1,1,1,19,1,1,1,19,19,19,19,19,1,14,1, +1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,19,4,4,4,4,4,14,4,14,4,14,4,4,4,4,4,4,14,4,14,4,4,4,4, +4,4,4,4,4,4,19,4,4,4,4,4,4,4,4,4,12,4,1,14,1,1,14,1,19,1,14,1,1,1,14,1,14,1,1,1,1,1,1,1,1,1,1,1,1,1,14,1,1,15,14,1,14,1,14, +1,19,14,19,19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,19,1,1,1,19,1,1,1,19,1,1,1,19,1,1,1,19,1,9,4,19,19,19,19,19,19, +9,1,1,1,1,1,1,1,19,19,19,19,19,19,19,19,19,19,1,19,19,19,1,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,15,1,1,1,1,15, +15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,14,15,15,1,15,1,15 +}; + +/** + * Lookup bidi class for character expressed as unicode scalar value. + * @param ch a unicode scalar value + * @return bidi class + */ +public static int getBidiClass ( int ch ) { + if ( ch <= 0x00FF ) { + return bcL1 [ ch - 0x0000 ]; + } else if ( ( ch >= 0x0590 ) && ( ch <= 0x06FF ) ) { + return bcR1 [ ch - 0x0590 ]; + } else { + return getBidiClass ( ch, bcS1, bcE1, bcC1 ); + } +} + +private static int getBidiClass ( int ch, int[] sa, int[] ea, byte[] ca ) { + int k = Arrays.binarySearch ( sa, ch ); + if ( k >= 0 ) { + return ca [ k ]; + } else { + k = - ( k + 1 ); + if ( k == 0 ) { + return BidiConstants.L; + } else if ( ch <= ea [ k - 1 ] ) { + return ca [ k - 1 ]; + } else { + return BidiConstants.L; + } + } +} + +} diff --git a/src/java/org/apache/fop/complexscripts/bidi/BidiConstants.java b/src/java/org/apache/fop/complexscripts/bidi/BidiConstants.java new file mode 100644 index 000000000..ef29223be --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/bidi/BidiConstants.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.bidi; + + +/** + * Constants used for bidirectional processing. + * @author Glenn Adams + */ +public interface BidiConstants { + + // bidi character class + + /** first external (official) category */ + int FIRST = 1; + + // strong category + /** left-to-right class */ + int L = 1; + /** left-to-right embedding class */ + int LRE = 2; + /** left-to-right override class */ + int LRO = 3; + /** right-to-left class */ + int R = 4; + /** right-to-left arabic class */ + int AL = 5; + /** right-to-left embedding class */ + int RLE = 6; + /** right-to-left override class */ + int RLO = 7; + + // weak category + /** pop directional formatting class */ + int PDF = 8; + /** european number class */ + int EN = 9; + /** european number separator class */ + int ES = 10; + /** european number terminator class */ + int ET = 11; + /** arabic number class */ + int AN = 12; + /** common number separator class */ + int CS = 13; + /** non-spacing mark class */ + int NSM = 14; + /** boundary neutral class */ + int BN = 15; + + // neutral category + /** paragraph separator class */ + int B = 16; + /** segment separator class */ + int S = 17; + /** whitespace class */ + int WS = 18; + /** other neutrals class */ + int ON = 19; + + /** last external (official) category */ + int LAST = 19; + + // implementation specific categories + /** placeholder for low surrogate */ + int SURROGATE = 20; + + // other constants + /** last + /** maximum bidirectional levels */ + int MAX_LEVELS = 61; + /** override flag */ + int OVERRIDE = 128; +} diff --git a/src/java/org/apache/fop/complexscripts/bidi/BidiResolver.java b/src/java/org/apache/fop/complexscripts/bidi/BidiResolver.java new file mode 100644 index 000000000..70bbe857a --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/bidi/BidiResolver.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.bidi; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; +import java.util.Vector; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.area.LineArea; +import org.apache.fop.area.inline.InlineArea; +import org.apache.fop.fo.pagination.PageSequence; + +// CSOFF: EmptyForIteratorPadCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: SimplifyBooleanReturnCheck + +/** + * <p>A utility class for performing bidirectional resolution processing.</p> + * + * @author Glenn Adams + */ +public final class BidiResolver { + + /** + * logging instance + */ + private static final Log log = LogFactory.getLog(BidiResolver.class); // CSOK: ConstantNameCheck + + private BidiResolver() { + } + + /** + * Resolve inline directionality. + * @param ps a page sequence FO instance + */ + public static void resolveInlineDirectionality ( PageSequence ps ) { + if (log.isDebugEnabled()) { + log.debug ( "BD: RESOLVE: " + ps ); + } + List ranges = pruneEmptyRanges ( ps.collectDelimitedTextRanges ( new Stack() ) ); + resolveInlineDirectionality ( ranges ); + } + + /** + * Reorder line area. + * @param la a line area instance + */ + public static void reorder ( LineArea la ) { + + // 1. collect inline levels + List runs = collectRuns ( la.getInlineAreas(), new Vector() ); + if (log.isDebugEnabled()) { + dumpRuns ( "BD: REORDER: INPUT:", runs ); + } + + // 2. split heterogeneous inlines + runs = splitRuns ( runs ); + if (log.isDebugEnabled()) { + dumpRuns ( "BD: REORDER: SPLIT INLINES:", runs ); + } + + // 3. determine minimum and maximum levels + int[] mm = computeMinMaxLevel ( runs, null ); + if (log.isDebugEnabled()) { + log.debug( "BD: REORDER: { min = " + mm[0] + ", max = " + mm[1] + "}" ); + } + + // 4. reorder from maximum level to minimum odd level + int mn = mm[0]; + int mx = mm[1]; + if ( mx > 0 ) { + for ( int l1 = mx, l2 = ( ( mn & 1 ) == 0 ) ? ( mn + 1 ) : mn; l1 >= l2; l1-- ) { + runs = reorderRuns ( runs, l1 ); + } + } + if (log.isDebugEnabled()) { + dumpRuns ( "BD: REORDER: REORDERED RUNS:", runs ); + } + + // 5. reverse word consituents (characters and glyphs) while mirroring + boolean mirror = true; + reverseWords ( runs, mirror ); + if (log.isDebugEnabled()) { + dumpRuns ( "BD: REORDER: REORDERED WORDS:", runs ); + } + + // 6. replace line area's inline areas with reordered runs' inline areas + replaceInlines ( la, replicateSplitWords ( runs ) ); + } + + private static void resolveInlineDirectionality ( List ranges ) { + for ( Iterator it = ranges.iterator(); it.hasNext(); ) { + DelimitedTextRange r = (DelimitedTextRange) it.next(); + r.resolve(); + if (log.isDebugEnabled()) { + log.debug ( r ); + } + } + } + + private static List collectRuns ( List inlines, List runs ) { + for ( Iterator it = inlines.iterator(); it.hasNext(); ) { + InlineArea ia = (InlineArea) it.next(); + runs = ia.collectInlineRuns ( runs ); + } + return runs; + } + + private static List splitRuns ( List runs ) { + List runsNew = new Vector(); + for ( Iterator it = runs.iterator(); it.hasNext(); ) { + InlineRun ir = (InlineRun) it.next(); + if ( ir.isHomogenous() ) { + runsNew.add ( ir ); + } else { + runsNew.addAll ( ir.split() ); + } + } + if ( ! runsNew.equals ( runs ) ) { + runs = runsNew; + } + return runs; + } + + private static int[] computeMinMaxLevel ( List runs, int[] mm ) { + if ( mm == null ) { + mm = new int[] {Integer.MAX_VALUE, Integer.MIN_VALUE}; + } + for ( Iterator it = runs.iterator(); it.hasNext(); ) { + InlineRun ir = (InlineRun) it.next(); + ir.updateMinMax ( mm ); + } + return mm; + } + private static List reorderRuns ( List runs, int level ) { + assert level >= 0; + List runsNew = new Vector(); + for ( int i = 0, n = runs.size(); i < n; i++ ) { + InlineRun iri = (InlineRun) runs.get(i); + if ( iri.getMinLevel() < level ) { + runsNew.add ( iri ); + } else { + int s = i; + int e = s; + while ( e < n ) { + InlineRun ire = (InlineRun) runs.get(e); + if ( ire.getMinLevel() < level ) { + break; + } else { + e++; + } + } + if ( s < e ) { + runsNew.addAll ( reverseRuns ( runs, s, e ) ); + } + i = e - 1; + } + } + if ( ! runsNew.equals ( runs ) ) { + runs = runsNew; + } + return runs; + } + private static List reverseRuns ( List runs, int s, int e ) { + int n = e - s; + Vector runsNew = new Vector ( n ); + if ( n > 0 ) { + for ( int i = 0; i < n; i++ ) { + int k = ( n - i - 1 ); + InlineRun ir = (InlineRun) runs.get(s + k); + ir.reverse(); + runsNew.add ( ir ); + } + } + return runsNew; + } + private static void reverseWords ( List runs, boolean mirror ) { + for ( Iterator it = runs.iterator(); it.hasNext(); ) { + InlineRun ir = (InlineRun) it.next(); + ir.maybeReverseWord ( mirror ); + } + } + private static List replicateSplitWords ( List runs ) { + // [TBD] for each run which inline word area appears multiple times in + // runs, replicate that word + return runs; + } + private static void replaceInlines ( LineArea la, List runs ) { + List<InlineArea> inlines = new ArrayList<InlineArea>(); + for ( Iterator it = runs.iterator(); it.hasNext(); ) { + InlineRun ir = (InlineRun) it.next(); + inlines.add ( ir.getInline() ); + } + la.setInlineAreas ( unflattenInlines ( inlines ) ); + } + private static List unflattenInlines ( List<InlineArea> inlines ) { + return new UnflattenProcessor ( inlines ) .unflatten(); + } + private static void dumpRuns ( String header, List runs ) { + log.debug ( header ); + for ( Iterator it = runs.iterator(); it.hasNext(); ) { + InlineRun ir = (InlineRun) it.next(); + log.debug ( ir ); + } + } + + private static List pruneEmptyRanges ( Stack ranges ) { + Vector rv = new Vector(); + for ( Iterator it = ranges.iterator(); it.hasNext(); ) { + DelimitedTextRange r = (DelimitedTextRange) it.next(); + if ( ! r.isEmpty() ) { + rv.add ( r ); + } + } + return rv; + } + +} diff --git a/src/java/org/apache/fop/complexscripts/bidi/DelimitedTextRange.java b/src/java/org/apache/fop/complexscripts/bidi/DelimitedTextRange.java new file mode 100644 index 000000000..9939db971 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/bidi/DelimitedTextRange.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.bidi; + +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.fo.CharIterator; +import org.apache.fop.fo.FONode; +import org.apache.fop.fo.FObj; +import org.apache.fop.traits.Direction; +import org.apache.fop.traits.WritingModeTraits; +import org.apache.fop.traits.WritingModeTraitsGetter; +import org.apache.fop.util.CharUtilities; + +// CSOFF: EmptyForIteratorPadCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck + +/** + * The <code>DelimitedTextRange</code> class implements the "delimited text range" as described + * by XML-FO 1.1 §5.8, which contains a flattened sequence of characters. Any FO that generates + * block areas serves as a delimiter. + * + * @author Glenn Adams + */ +public class DelimitedTextRange { + private FONode fn; // node that generates this text range + private StringBuffer buffer; // flattened character sequence of generating FO nodes + private List intervals; // list of intervals over buffer of generating FO nodes + /** + * Primary constructor. + * @param fn node that generates this text range + */ + public DelimitedTextRange ( FONode fn ) { + this.fn = fn; + this.buffer = new StringBuffer(); + this.intervals = new Vector(); + } + /** + * Obtain node that generated this text range. + * @return node that generated this text range + */ + public FONode getNode() { + return fn; + } + /** + * Append interval using characters from character iterator IT. + * @param it character iterator + * @param fn node that generates interval being appended + */ + public void append ( CharIterator it, FONode fn ) { + if ( it != null ) { + int s = buffer.length(); + int e = s; + while ( it.hasNext() ) { + char c = it.nextChar(); + buffer.append ( c ); + e++; + } + intervals.add ( new TextInterval ( fn, s, e ) ); + } + } + /** + * Append interval using character C. + * @param c character + * @param fn node that generates interval being appended + */ + public void append ( char c, FONode fn ) { + if ( c != 0 ) { + int s = buffer.length(); + int e = s + 1; + buffer.append ( c ); + intervals.add ( new TextInterval ( fn, s, e ) ); + } + } + /** + * Determine if range is empty. + * @return true if range is empty + */ + public boolean isEmpty() { + return buffer.length() == 0; + } + /** + * Resolve bidirectional levels for this range. + */ + public void resolve() { + WritingModeTraitsGetter tg; + if ( ( tg = WritingModeTraits.getWritingModeTraitsGetter ( getNode() ) ) != null ) { + resolve ( tg.getInlineProgressionDirection() ); + } + } + @Override + public String toString() { + StringBuffer sb = new StringBuffer ( "DR: " + fn.getLocalName() + " { <" + CharUtilities.toNCRefs ( buffer.toString() ) + ">" ); + sb.append ( ", intervals <" ); + boolean first = true; + for ( Iterator it = intervals.iterator(); it.hasNext(); ) { + TextInterval ti = (TextInterval) it.next(); + if ( first ) { + first = false; + } else { + sb.append(','); + } + sb.append ( ti.toString() ); + } + sb.append("> }"); + return sb.toString(); + } + private void resolve ( Direction paragraphEmbeddingLevel ) { + int [] levels; + if ( ( levels = UnicodeBidiAlgorithm.resolveLevels ( buffer, paragraphEmbeddingLevel ) ) != null ) { + assignLevels ( levels ); + assignBlockLevel ( paragraphEmbeddingLevel ); + assignTextLevels(); + } + } + /** + * <p>Assign resolved levels to all text intervals of this delimited text range.</p> + * <p>Has a possible side effect of replacing the intervals array with a new array + * containing new text intervals, such that each text interval is associated with + * a single level run.</p> + * @param levels array of levels each corresponding to each index of the delimited + * text range + */ + private void assignLevels ( int[] levels ) { + Vector intervalsNew = new Vector ( intervals.size() ); + for ( Iterator it = intervals.iterator(); it.hasNext(); ) { + TextInterval ti = (TextInterval) it.next(); + intervalsNew.addAll ( assignLevels ( ti, levels ) ); + } + if ( ! intervalsNew.equals ( intervals ) ) { + intervals = intervalsNew; + } + } + /** + * <p>Assign resolved levels to a specified text interval over this delimited text + * range.</p> + * <p>Returns a list of text intervals containing either (1) the single, input text + * interval or (2) two or more new text intervals obtained from sub-dividing the input + * text range into level runs, i.e., runs of text assigned to a single level.</p> + * @param ti a text interval to which levels are to be assigned + * @param levels array of levels each corresponding to each index of the delimited + * text range + * @return a list of text intervals as described above + */ + private static final Log log = LogFactory.getLog(BidiResolver.class); // CSOK: ConstantNameCheck + private List assignLevels ( TextInterval ti, int[] levels ) { + Vector tiv = new Vector(); + FONode fn = ti.getNode(); + int fnStart = ti.getStart(); // start of node's text in delimited text range + for ( int i = fnStart, n = ti.getEnd(); i < n; ) { + int s = i; // inclusive start index of interval in delimited text range + int e = s; // exclusive end index of interval in delimited text range + int l = levels [ s ]; // current run level + while ( e < n ) { // skip to end of run level or end of interval + if ( levels [ e ] != l ) { + break; + } else { + e++; + } + } + if ( ( ti.getStart() == s ) && ( ti.getEnd() == e ) ) { + ti.setLevel ( l ); // reuse interval, assigning it single level + } else { + ti = new TextInterval ( fn, fnStart, s, e, l ); // subdivide interval + } + if (log.isDebugEnabled()) { + log.debug ( "AL(" + l + "): " + ti ); + } + tiv.add ( ti ); + i = e; + } + return tiv; + } + /** + * <p>Assign resolved levels for each interval to source #PCDATA in the associated FOText.</p> + */ + private void assignTextLevels() { + for ( Iterator it = intervals.iterator(); it.hasNext(); ) { + TextInterval ti = (TextInterval) it.next(); + ti.assignTextLevels(); + } + } + private void assignBlockLevel ( Direction paragraphEmbeddingLevel ) { + int defaultLevel = ( paragraphEmbeddingLevel == Direction.RL ) ? 1 : 0; + for ( Iterator it = intervals.iterator(); it.hasNext(); ) { + TextInterval ti = (TextInterval) it.next(); + assignBlockLevel ( ti.getNode(), defaultLevel ); + } + } + private void assignBlockLevel ( FONode node, int defaultLevel ) { + for ( FONode fn = node; fn != null; fn = fn.getParent() ) { + if ( fn instanceof FObj ) { + FObj fo = (FObj) fn; + if ( fo.isBidiRangeBlockItem() ) { + if ( fo.getBidiLevel() < 0 ) { + fo.setBidiLevel ( defaultLevel ); + } + break; + } + } + } + } +} + diff --git a/src/java/org/apache/fop/complexscripts/bidi/InlineRun.java b/src/java/org/apache/fop/complexscripts/bidi/InlineRun.java new file mode 100644 index 000000000..6ea62494a --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/bidi/InlineRun.java @@ -0,0 +1,310 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.bidi; + +import java.util.Arrays; +import java.util.List; +import java.util.Vector; + +import org.apache.fop.area.inline.Anchor; +import org.apache.fop.area.inline.InlineArea; +import org.apache.fop.area.inline.InlineBlockParent; +import org.apache.fop.area.inline.InlineParent; +import org.apache.fop.area.inline.InlineViewport; +import org.apache.fop.area.inline.Leader; +import org.apache.fop.area.inline.Space; +import org.apache.fop.area.inline.SpaceArea; +import org.apache.fop.area.inline.TextArea; +import org.apache.fop.area.inline.UnresolvedPageNumber; +import org.apache.fop.area.inline.WordArea; +import org.apache.fop.traits.Direction; +import org.apache.fop.util.CharUtilities; + +// CSOFF: EmptyForIteratorPadCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: SimplifyBooleanReturnCheck + +/** + * The <code>InlineRun</code> class is a utility class, the instances of which are used + * to capture a sequence of reordering levels associated with an inline area. + * + * @author Glenn Adams + */ +public class InlineRun { + private InlineArea inline; + private int[] levels; + private int minLevel; + private int maxLevel; + private int reversals; + /** + * Primary constructor. + * @param inline which generated this inline run + * @param levels levels array + */ + public InlineRun ( InlineArea inline, int[] levels ) { + assert inline != null; + assert levels != null; + this.inline = inline; + this.levels = levels; + setMinMax ( levels ); + } + /** + * Alternate constructor. + * @param inline which generated this inline run + * @param level for each index + * @param count of indices + */ + public InlineRun ( InlineArea inline, int level, int count ) { + this ( inline, makeLevels ( level, count ) ); + } + /** + * Obtain inline area that generated this inline run. + * @return inline area that generated this inline run. + */ + public InlineArea getInline() { + return inline; + } + /** + * Obtain minimum bidi level for this run. + * @return minimum bidi level + */ + public int getMinLevel() { + return minLevel; + } + /** + * Obtain maximum bidi level for this run. + * @return maximum bidi level + */ + public int getMaxLevel() { + return maxLevel; + } + private void setMinMax ( int[] levels ) { + int mn = Integer.MAX_VALUE; + int mx = Integer.MIN_VALUE; + if ( ( levels != null ) && ( levels.length > 0 ) ) { + for ( int i = 0, n = levels.length; i < n; i++ ) { + int l = levels [ i ]; + if ( l < mn ) { + mn = l; + } + if ( l > mx ) { + mx = l; + } + } + } else { + mn = mx = -1; + } + this.minLevel = mn; + this.maxLevel = mx; + } + /** + * Determine if this run has homogenous (same valued) bidi levels. + * @return true if homogenous + */ + public boolean isHomogenous() { + return minLevel == maxLevel; + } + /** + * Split this inline run into homogenous runs. + * @return list of new runs + */ + public List split() { + List runs = new Vector(); + for ( int i = 0, n = levels.length; i < n; ) { + int l = levels [ i ]; + int s = i; + int e = s; + while ( e < n ) { + if ( levels [ e ] != l ) { + break; + } else { + e++; + } + } + if ( s < e ) { + runs.add ( new InlineRun ( inline, l, e - s ) ); + } + i = e; + } + assert runs.size() < 2 : "heterogeneous inlines not yet supported!!"; + return runs; + } + /** + * Update a min/max array to correspond with this run's min/max values. + * @param mm reference to min/max array + */ + public void updateMinMax ( int[] mm ) { + if ( minLevel < mm[0] ) { + mm[0] = minLevel; + } + if ( maxLevel > mm[1] ) { + mm[1] = maxLevel; + } + } + /** + * Determine if run needs mirroring. + * @return true if run is homogenous and odd (i.e., right to left) + */ + public boolean maybeNeedsMirroring() { + return ( minLevel == maxLevel ) && ( ( minLevel & 1 ) != 0 ); + } + /** + * Reverse run (by incrementing reversal count, not actually reversing). + */ + public void reverse() { + reversals++; + } + /** + * Reverse inline area if it is a word area and it requires + * reversal. + * @param mirror if true then also mirror characters + */ + public void maybeReverseWord ( boolean mirror ) { + if ( inline instanceof WordArea ) { + WordArea w = (WordArea) inline; + // if not already reversed, then reverse now + if ( ! w.isReversed() ) { + if ( ( reversals & 1 ) != 0 ) { + w.reverse ( mirror ); + } else if ( mirror && maybeNeedsMirroring() ) { + w.mirror(); + } + } + } + } + @Override + public boolean equals ( Object o ) { + if ( o instanceof InlineRun ) { + InlineRun ir = (InlineRun) o; + if ( ir.inline != inline ) { + return false; + } else if ( ir.minLevel != minLevel ) { + return false; + } else if ( ir.maxLevel != maxLevel ) { + return false; + } else if ( ( ir.levels != null ) && ( levels != null ) ) { + if ( ir.levels.length != levels.length ) { + return false; + } else { + for ( int i = 0, n = levels.length; i < n; i++ ) { + if ( ir.levels[i] != levels[i] ) { + return false; + } + } + return true; + } + } else if ( ( ir.levels == null ) && ( levels == null ) ) { + return true; + } else { + return false; + } + } else { + return false; + } + } + @Override + public int hashCode() { + int l = ( inline != null ) ? inline.hashCode() : 0; + l = ( l ^ minLevel ) + ( l << 19 ); + l = ( l ^ maxLevel ) + ( l << 11 ); + return l; + } + @Override + public String toString() { + StringBuffer sb = new StringBuffer( "RR: { type = \'" ); + char c; + String content = null; + if ( inline instanceof WordArea ) { + c = 'W'; + content = ( (WordArea) inline ) .getWord(); + } else if ( inline instanceof SpaceArea ) { + c = 'S'; + content = ( (SpaceArea) inline ) .getSpace(); + } else if ( inline instanceof Anchor ) { + c = 'A'; + } else if ( inline instanceof Leader ) { + c = 'L'; + } else if ( inline instanceof Space ) { + c = 'S'; + } else if ( inline instanceof UnresolvedPageNumber ) { + c = '#'; + content = ( (UnresolvedPageNumber) inline ) .getText(); + } else if ( inline instanceof InlineBlockParent ) { + c = 'B'; + } else if ( inline instanceof InlineViewport ) { + c = 'V'; + } else if ( inline instanceof InlineParent ) { + c = 'I'; + } else { + c = '?'; + } + sb.append ( c ); + sb.append ( "\', levels = \'" ); + sb.append ( generateLevels ( levels ) ); + sb.append ( "\', min = " ); + sb.append ( minLevel ); + sb.append ( ", max = " ); + sb.append ( maxLevel ); + sb.append ( ", reversals = " ); + sb.append ( reversals ); + sb.append ( ", content = <" ); + sb.append ( CharUtilities.toNCRefs ( content ) ); + sb.append ( "> }" ); + return sb.toString(); + } + private String generateLevels ( int[] levels ) { + StringBuffer lb = new StringBuffer(); + int maxLevel = -1; + int numLevels = levels.length; + for ( int i = 0; i < numLevels; i++ ) { + int l = levels [ i ]; + if ( l > maxLevel ) { + maxLevel = l; + } + } + if ( maxLevel < 0 ) { + // leave level buffer empty + } else if ( maxLevel < 10 ) { + // use string of decimal digits + for ( int i = 0; i < numLevels; i++ ) { + lb.append ( (char) ( '0' + levels [ i ] ) ); + } + } else { + // use comma separated list + boolean first = true; + for ( int i = 0; i < numLevels; i++ ) { + if ( first ) { + first = false; + } else { + lb.append(','); + } + lb.append ( levels [ i ] ); + } + } + return lb.toString(); + } + private static int[] makeLevels ( int level, int count ) { + int[] levels = new int [ count ]; + Arrays.fill ( levels, level ); + return levels; + } +} + diff --git a/src/java/org/apache/fop/complexscripts/bidi/TextInterval.java b/src/java/org/apache/fop/complexscripts/bidi/TextInterval.java new file mode 100644 index 000000000..7d4ab105b --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/bidi/TextInterval.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.bidi; + +import org.apache.fop.fo.FONode; +import org.apache.fop.fo.FOText; +import org.apache.fop.fo.flow.AbstractPageNumberCitation; +import org.apache.fop.fo.flow.AbstractGraphics; +import org.apache.fop.fo.flow.BidiOverride; +import org.apache.fop.fo.flow.Character; +import org.apache.fop.fo.flow.Leader; + +// CSOFF: LineLengthCheck +// CSOFF: SimplifyBooleanReturnCheck + +/** + * The <code>TextInterval</code> class is a utility class, the instances of which are used + * to record backpointers to associated nodes over sub-intervals of a delimited text range. + * + * @author Glenn Adams + */ +class TextInterval { + private FONode fn; // associated node + private int textStart; // starting index within delimited text range of associated node's text + private int start; // starting index within delimited text range + private int end; // ending index within delimited text range + private int level; // resolved level or default (-1) + TextInterval ( FONode fn, int start, int end ) { + this ( fn, start, start, end, -1 ); + } + TextInterval ( FONode fn, int textStart, int start, int end, int level ) { + this.fn = fn; + this.textStart = textStart; + this.start = start; + this.end = end; + this.level = level; + } + FONode getNode() { + return fn; + } + int getTextStart() { + return textStart; + } + int getStart() { + return start; + } + int getEnd() { + return end; + } + int getLevel() { + return level; + } + void setLevel ( int level ) { + this.level = level; + } + public int length() { + return end - start; + } + public String getText() { + if ( fn instanceof FOText ) { + return ( (FOText) fn ) .getCharSequence() .toString(); + } else if ( fn instanceof Character ) { + return new String ( new char[] {( (Character) fn ) .getCharacter()} ); + } else { + return null; + } + } + public void assignTextLevels() { + if ( fn instanceof FOText ) { + ( (FOText) fn ) .setBidiLevel ( level, start - textStart, end - textStart ); + } else if ( fn instanceof Character ) { + ( (Character) fn ) .setBidiLevel ( level ); + } else if ( fn instanceof AbstractPageNumberCitation ) { + ( (AbstractPageNumberCitation) fn ) .setBidiLevel ( level ); + } else if ( fn instanceof AbstractGraphics ) { + ( (AbstractGraphics) fn ) .setBidiLevel ( level ); + } else if ( fn instanceof Leader ) { + ( (Leader) fn ) .setBidiLevel ( level ); + } + } + public boolean equals ( Object o ) { + if ( o instanceof TextInterval ) { + TextInterval ti = (TextInterval) o; + if ( ti.getNode() != fn ) { + return false; + } else if ( ti.getStart() != start ) { + return false; + } else if ( ti.getEnd() != end ) { + return false; + } else { + return true; + } + } else { + return false; + } + } + public int hashCode() { + int l = ( fn != null ) ? fn.hashCode() : 0; + l = ( l ^ start ) + ( l << 19 ); + l = ( l ^ end ) + ( l << 11 ); + return l; + } + public String toString() { + StringBuffer sb = new StringBuffer(); + char c; + if ( fn instanceof FOText ) { + c = 'T'; + } else if ( fn instanceof Character ) { + c = 'C'; + } else if ( fn instanceof BidiOverride ) { + c = 'B'; + } else if ( fn instanceof AbstractPageNumberCitation ) { + c = '#'; + } else if ( fn instanceof AbstractGraphics ) { + c = 'G'; + } else if ( fn instanceof Leader ) { + c = 'L'; + } else { + c = '?'; + } + sb.append ( c ); + sb.append ( "[" + start + "," + end + "][" + textStart + "](" + level + ")" ); + return sb.toString(); + } +} + diff --git a/src/java/org/apache/fop/complexscripts/bidi/UnflattenProcessor.java b/src/java/org/apache/fop/complexscripts/bidi/UnflattenProcessor.java new file mode 100644 index 000000000..d91bed572 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/bidi/UnflattenProcessor.java @@ -0,0 +1,361 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.bidi; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Stack; + +import org.apache.fop.area.Area; +import org.apache.fop.area.LinkResolver; +import org.apache.fop.area.inline.BasicLinkArea; +import org.apache.fop.area.inline.FilledArea; +import org.apache.fop.area.inline.InlineArea; +import org.apache.fop.area.inline.InlineParent; +import org.apache.fop.area.inline.SpaceArea; +import org.apache.fop.area.inline.TextArea; +import org.apache.fop.area.inline.UnresolvedPageNumber; + +// CSOFF: EmptyForIteratorPadCheck +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: SimplifyBooleanReturnCheck + +/** + * The <code>UnflattenProcessor</code> class is used to reconstruct (by unflattening) a line + * area's internal area hierarachy after leaf inline area reordering is completed. + * + * @author Glenn Adams + */ +class UnflattenProcessor { + private List<InlineArea> il; // list of flattened inline areas being unflattened + private List<InlineArea> ilNew; // list of unflattened inline areas being constructed + private int iaLevelLast; // last (previous) level of current inline area (if applicable) or -1 + private TextArea tcOrig; // original text area container + private TextArea tcNew; // new text area container being constructed + private Stack<InlineParent> icOrig; // stack of original inline parent containers + private Stack<InlineParent> icNew; // stack of new inline parent containers being constructed + UnflattenProcessor ( List<InlineArea> inlines ) { + this.il = inlines; + this.ilNew = new ArrayList<InlineArea>(); + this.iaLevelLast = -1; + this.icOrig = new Stack<InlineParent>(); + this.icNew = new Stack<InlineParent>(); + } + List unflatten() { + if ( il != null ) { + for ( Iterator<InlineArea> it = il.iterator(); it.hasNext(); ) { + process ( it.next() ); + } + } + finishAll(); + return ilNew; + } + private void process ( InlineArea ia ) { + process ( findInlineContainers ( ia ), findTextContainer ( ia ), ia ); + } + private void process ( List<InlineParent> ich, TextArea tc, InlineArea ia ) { + if ( ( tcNew == null ) || ( tc != tcNew ) ) { + maybeFinishTextContainer ( tc, ia ); + maybeFinishInlineContainers ( ich, tc, ia ); + update ( ich, tc, ia ); + } else { + // skip inline area whose text container is the current new text container, + // which occurs in the context of the inline runs produced by a filled area + } + } + private boolean shouldFinishTextContainer ( TextArea tc, InlineArea ia ) { + if ( ( tcOrig != null ) && ( tc != tcOrig ) ) { + return true; + } else if ( ( iaLevelLast != -1 ) && ( ia.getBidiLevel() != iaLevelLast ) ) { + return true; + } else { + return false; + } + } + private void finishTextContainer() { + finishTextContainer ( null, null ); + } + private void finishTextContainer ( TextArea tc, InlineArea ia ) { + if ( tcNew != null ) { + updateIPD ( tcNew ); + if ( ! icNew.empty() ) { + icNew.peek().addChildArea ( tcNew ); + } else { + ilNew.add ( tcNew ); + } + } + tcNew = null; + } + private void maybeFinishTextContainer ( TextArea tc, InlineArea ia ) { + if ( shouldFinishTextContainer ( tc, ia ) ) { + finishTextContainer ( tc, ia ); + } + } + private boolean shouldFinishInlineContainer ( List<InlineParent> ich, TextArea tc, InlineArea ia ) { + if ( ( ich == null ) || ich.isEmpty() ) { + return ! icOrig.empty(); + } else { + if ( ! icOrig.empty() ) { + InlineParent ic = ich.get(0); + InlineParent ic0 = icOrig.peek(); + return ( ic != ic0 ) && ! isInlineParentOf ( ic, ic0 ); + } else { + return false; + } + } + } + private void finishInlineContainer() { + finishInlineContainer ( null, null, null ); + } + private void finishInlineContainer ( List<InlineParent> ich, TextArea tc, InlineArea ia ) { + if ( ( ich != null ) && ! ich.isEmpty() ) { // finish non-matching inner inline container(s) + for ( Iterator<InlineParent> it = ich.iterator(); it.hasNext(); ) { + InlineParent ic = it.next(); + InlineParent ic0 = icOrig.empty() ? null : icOrig.peek(); + if ( ic0 == null ) { + assert icNew.empty(); + } else if ( ic != ic0 ) { + assert ! icNew.empty(); + InlineParent icO0 = icOrig.pop(); + InlineParent icN0 = icNew.pop(); + assert icO0 != null; + assert icN0 != null; + if ( icNew.empty() ) { + ilNew.add ( icN0 ); + } else { + icNew.peek().addChildArea ( icN0 ); + } + if ( ! icOrig.empty() && ( icOrig.peek() == ic ) ) { + break; + } + } else { + break; + } + } + } else { // finish all inline containers + while ( ! icNew.empty() ) { + InlineParent icO0 = icOrig.pop(); + InlineParent icN0 = icNew.pop(); + assert icO0 != null; + assert icN0 != null; + if ( icNew.empty() ) { + ilNew.add ( icN0 ); + } else { + icNew.peek().addChildArea ( icN0 ); + } + } + } + } + private void maybeFinishInlineContainers ( List<InlineParent> ich, TextArea tc, InlineArea ia ) { + if ( shouldFinishInlineContainer ( ich, tc, ia ) ) { + finishInlineContainer ( ich, tc, ia ); + } + } + private void finishAll() { + finishTextContainer(); + finishInlineContainer(); + } + private void update ( List<InlineParent> ich, TextArea tc, InlineArea ia ) { + if ( ! alreadyUnflattened ( ia ) ) { + if ( ( ich != null ) && ! ich.isEmpty() ) { + pushInlineContainers ( ich ); + } + if ( tc != null ) { + pushTextContainer ( tc, ia ); + } else { + pushNonTextInline ( ia ); + } + iaLevelLast = ia.getBidiLevel(); + tcOrig = tc; + } else if ( tcNew != null ) { + finishTextContainer(); + tcOrig = null; + } else { + tcOrig = null; + } + } + private boolean alreadyUnflattened ( InlineArea ia ) { + for ( Iterator<InlineArea> it = ilNew.iterator(); it.hasNext(); ) { + if ( ia.isAncestorOrSelf ( it.next() ) ) { + return true; + } + } + return false; + } + private void pushInlineContainers ( List<InlineParent> ich ) { + LinkedList<InlineParent> icl = new LinkedList<InlineParent>(); + for ( Iterator<InlineParent> it = ich.iterator(); it.hasNext(); ) { + InlineParent ic = it.next(); + if ( icOrig.search ( ic ) >= 0 ) { + break; + } else { + icl.addFirst ( ic ); + } + } + for ( Iterator<InlineParent> it = icl.iterator(); it.hasNext(); ) { + InlineParent ic = it.next(); + icOrig.push ( ic ); + icNew.push ( generateInlineContainer ( ic ) ); + } + } + private void pushTextContainer ( TextArea tc, InlineArea ia ) { + if ( tc instanceof UnresolvedPageNumber ) { + tcNew = tc; + } else { + if ( tcNew == null ) { + tcNew = generateTextContainer ( tc ); + } + tcNew.addChildArea ( ia ); + } + } + private void pushNonTextInline ( InlineArea ia ) { + if ( icNew.empty() ) { + ilNew.add ( ia ); + } else { + icNew.peek().addChildArea ( ia ); + } + } + private InlineParent generateInlineContainer ( InlineParent i ) { + if ( i instanceof BasicLinkArea ) { + return generateBasicLinkArea ( (BasicLinkArea) i ); + } else if ( i instanceof FilledArea ) { + return generateFilledArea ( (FilledArea) i ); + } else { + return generateInlineContainer0 ( i ); + } + } + private InlineParent generateBasicLinkArea ( BasicLinkArea l ) { + BasicLinkArea lc = new BasicLinkArea(); + if ( l != null ) { + initializeInlineContainer ( lc, l ); + initializeLinkArea ( lc, l ); + } + return lc; + } + private void initializeLinkArea ( BasicLinkArea lc, BasicLinkArea l ) { + assert lc != null; + assert l != null; + LinkResolver r = l.getResolver(); + if ( r != null ) { + String[] idrefs = r.getIDRefs(); + if ( idrefs.length > 0 ) { + String idref = idrefs[0]; + LinkResolver lr = new LinkResolver ( idref, lc ); + lc.setResolver ( lr ); + r.addDependent ( lr ); + } + } + } + private InlineParent generateFilledArea ( FilledArea f ) { + FilledArea fc = new FilledArea(); + if ( f != null ) { + initializeInlineContainer ( fc, f ); + initializeFilledArea ( fc, f ); + } + return fc; + } + private void initializeFilledArea ( FilledArea fc, FilledArea f ) { + assert fc != null; + assert f != null; + fc.setIPD ( f.getIPD() ); + fc.setUnitWidth ( f.getUnitWidth() ); + } + private InlineParent generateInlineContainer0 ( InlineParent i ) { + InlineParent ic = new InlineParent(); + if ( i != null ) { + initializeInlineContainer ( ic, i ); + } + return ic; + } + private void initializeInlineContainer ( InlineParent ic, InlineParent i ) { + assert ic != null; + assert i != null; + ic.setTraits ( i.getTraits() ); + ic.setBPD ( i.getBPD() ); + ic.setBlockProgressionOffset ( i.getBlockProgressionOffset() ); + } + private TextArea generateTextContainer ( TextArea t ) { + TextArea tc = new TextArea(); + if ( t != null ) { + tc.setTraits ( t.getTraits() ); + tc.setBPD ( t.getBPD() ); + tc.setBlockProgressionOffset ( t.getBlockProgressionOffset() ); + tc.setBaselineOffset ( t.getBaselineOffset() ); + tc.setTextWordSpaceAdjust ( t.getTextWordSpaceAdjust() ); + tc.setTextLetterSpaceAdjust ( t.getTextLetterSpaceAdjust() ); + } + return tc; + } + private void updateIPD ( TextArea tc ) { + int numAdjustable = 0; + for ( Iterator it = tc.getChildAreas().iterator(); it.hasNext(); ) { + InlineArea ia = (InlineArea) it.next(); + if ( ia instanceof SpaceArea ) { + SpaceArea sa = (SpaceArea) ia; + if ( sa.isAdjustable() ) { + numAdjustable++; + } + } + } + if ( numAdjustable > 0 ) { + tc.setIPD ( tc.getIPD() + ( numAdjustable * tc.getTextWordSpaceAdjust() ) ); + } + } + private TextArea findTextContainer ( InlineArea ia ) { + assert ia != null; + TextArea t = null; + while ( t == null ) { + if ( ia instanceof TextArea ) { + t = (TextArea) ia; + } else { + Area p = ia.getParentArea(); + if ( p instanceof InlineArea ) { + ia = (InlineArea) p; + } else { + break; + } + } + } + return t; + } + private List<InlineParent> findInlineContainers ( InlineArea ia ) { + assert ia != null; + List<InlineParent> ich = new ArrayList<InlineParent>(); + Area a = ia.getParentArea(); + while ( a != null ) { + if ( a instanceof InlineArea ) { + if ( ( a instanceof InlineParent ) && ! ( a instanceof TextArea ) ) { + ich.add ( (InlineParent) a ); + } + a = ( (InlineArea) a ) .getParentArea(); + } else { + a = null; + } + } + return ich; + } + private boolean isInlineParentOf ( InlineParent ic0, InlineParent ic1 ) { + assert ic0 != null; + return ic0.getParentArea() == ic1; + } +} diff --git a/src/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java b/src/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java new file mode 100644 index 000000000..0f24cba9d --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java @@ -0,0 +1,839 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.bidi; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.traits.Direction; +import org.apache.fop.util.CharUtilities; + +// CSOFF: AvoidNestedBlocksCheck +// CSOFF: EmptyForIteratorPadCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: ParameterNumberCheck + +/** + * The <code>UnicodeBidiAlgorithm</code> class implements functionality prescribed by + * the Unicode Bidirectional Algorithm, Unicode Standard Annex #9. + * + * @author Glenn Adams + */ +public final class UnicodeBidiAlgorithm implements BidiConstants { + + /** + * logging instance + */ + private static final Log log = LogFactory.getLog(BidiResolver.class); // CSOK: ConstantNameCheck + + private UnicodeBidiAlgorithm() { + } + + /** + * Resolve the directionality levels of each character in a character seqeunce. + * If some character is encoded in the character sequence as a Unicode Surrogate Pair, + * then the directionality level of each of the two members of the pair will be identical. + * @return null if bidirectional processing is not required; otherwise, returns an array + * of integers, where each integer corresponds to exactly one UTF-16 + * encoding element present in the input character sequence, and where each integer denotes + * the directionality level of the corresponding encoding element + * @param cs input character sequence representing a UTF-16 encoded string + * @param defaultLevel the default paragraph level, which must be zero (LR) or one (RL) + */ + public static int[] resolveLevels ( CharSequence cs, Direction defaultLevel ) { + int[] chars = new int [ cs.length() ]; + if ( convertToScalar ( cs, chars ) || ( defaultLevel == Direction.RL ) ) { + return resolveLevels ( chars, ( defaultLevel == Direction.RL ) ? 1 : 0, new int [ chars.length ] ); + } else { + return null; + } + } + + /** + * Resolve the directionality levels of each character in a character seqeunce. + * @return null if bidirectional processing is not required; otherwise, returns an array + * of integers, where each integer corresponds to exactly one UTF-16 + * encoding element present in the input character sequence, and where each integer denotes + * the directionality level of the corresponding encoding element + * @param chars array of input characters represented as unicode scalar values + * @param defaultLevel the default paragraph level, which must be zero (LR) or one (RL) + * @param levels array to receive levels, one for each character in chars array + */ + public static int[] resolveLevels ( int[] chars, int defaultLevel, int[] levels ) { + return resolveLevels ( chars, getClasses ( chars ), defaultLevel, levels, false ); + } + + /** + * Resolve the directionality levels of each character in a character seqeunce. + * @return null if bidirectional processing is not required; otherwise, returns an array + * of integers, where each integer corresponds to exactly one UTF-16 + * encoding element present in the input character sequence, and where each integer denotes + * the directionality level of the corresponding encoding element + * @param chars array of input characters represented as unicode scalar values + * @param classes array containing one bidi class per character in chars array + * @param defaultLevel the default paragraph level, which must be zero (LR) or one (RL) + * @param levels array to receive levels, one for each character in chars array + * @param useRuleL1 true if rule L1 should be used + */ + public static int[] resolveLevels ( int[] chars, int[] classes, int defaultLevel, int[] levels, boolean useRuleL1 ) { + int[] ica = classes; + int[] wca = copySequence ( ica ); + int[] ea = new int [ levels.length ]; + resolveExplicit ( wca, defaultLevel, ea ); + resolveRuns ( wca, defaultLevel, ea, levelsFromEmbeddings ( ea, levels ) ); + if ( useRuleL1 ) { + resolveSeparators ( ica, wca, defaultLevel, levels ); + } + dump ( "RL: CC(" + ( ( chars != null ) ? chars.length : -1 ) + ")", chars, classes, defaultLevel, levels ); + return levels; + } + + private static int[] copySequence ( int[] ta ) { + int[] na = new int [ ta.length ]; + System.arraycopy ( ta, 0, na, 0, na.length ); + return na; + } + + private static void resolveExplicit ( int[] wca, int defaultLevel, int[] ea ) { + int[] es = new int [ MAX_LEVELS ]; /* embeddings stack */ + int ei = 0; /* embeddings stack index */ + int ec = defaultLevel; /* current embedding level */ + for ( int i = 0, n = wca.length; i < n; i++ ) { + int bc = wca [ i ]; /* bidi class of current char */ + int el; /* embedding level to assign to current char */ + switch ( bc ) { + case LRE: // start left-to-right embedding + case RLE: // start right-to-left embedding + case LRO: // start left-to-right override + case RLO: // start right-to-left override + { + int en; /* new embedding level */ + if ( ( bc == RLE ) || ( bc == RLO ) ) { + en = ( ( ec & ~OVERRIDE ) + 1 ) | 1; + } else { + en = ( ( ec & ~OVERRIDE ) + 2 ) & ~1; + } + if ( en < ( MAX_LEVELS + 1 ) ) { + es [ ei++ ] = ec; + if ( ( bc == LRO ) || ( bc == RLO ) ) { + ec = en | OVERRIDE; + } else { + ec = en & ~OVERRIDE; + } + } else { + // max levels exceeded, so don't change level or override + } + el = ec; + break; + } + case PDF: // pop directional formatting + { + el = ec; + if ( ei > 0 ) { + ec = es [ --ei ]; + } else { + // ignore isolated PDF + } + break; + } + case B: // paragraph separator + { + el = ec = defaultLevel; + ei = 0; + break; + } + default: + { + el = ec; + break; + } + } + switch ( bc ) { + case BN: + break; + case LRE: case RLE: case LRO: case RLO: case PDF: + wca [ i ] = BN; + break; + default: + if ( ( el & OVERRIDE ) != 0 ) { + wca [ i ] = directionOfLevel ( el ); + } + break; + } + ea [ i ] = el; + } + } + + private static int directionOfLevel ( int level ) { + return ( ( level & 1 ) != 0 ) ? R : L; + } + + private static int levelOfEmbedding ( int embedding ) { + return embedding & ~OVERRIDE; + } + + private static int[] levelsFromEmbeddings ( int[] ea, int[] la ) { + assert ea != null; + assert la != null; + assert la.length == ea.length; + for ( int i = 0, n = la.length; i < n; i++ ) { + la [ i ] = levelOfEmbedding ( ea [ i ] ); + } + return la; + } + + private static void resolveRuns ( int[] wca, int defaultLevel, int[] ea, int[] la ) { + if ( la.length != wca.length ) { + throw new IllegalArgumentException ( "levels sequence length must match classes sequence length" ); + } else if ( la.length != ea.length ) { + throw new IllegalArgumentException ( "levels sequence length must match embeddings sequence length" ); + } else { + for ( int i = 0, n = ea.length, lPrev = defaultLevel; i < n; ) { + int s = i; + int e = s; + int l = findNextNonRetainedFormattingLevel ( wca, ea, s, lPrev ); + while ( e < n ) { + if ( la [ e ] != l ) { + if ( startsWithRetainedFormattingRun ( wca, ea, e ) ) { + e += getLevelRunLength ( ea, e ); + } else { + break; + } + } else { + e++; + } + } + lPrev = resolveRun ( wca, defaultLevel, ea, la, s, e, l, lPrev ); + i = e; + } + } + } + + private static int findNextNonRetainedFormattingLevel ( int[] wca, int[] ea, int start, int lPrev ) { + int s = start; + int e = wca.length; + while ( s < e ) { + if ( startsWithRetainedFormattingRun ( wca, ea, s ) ) { + s += getLevelRunLength ( ea, s ); + } else { + break; + } + } + if ( s < e ) { + return levelOfEmbedding ( ea [ s ] ); + } else { + return lPrev; + } + } + + private static int getLevelRunLength ( int[] ea, int start ) { + assert start < ea.length; + int nl = 0; + for ( int s = start, e = ea.length, l0 = levelOfEmbedding ( ea [ start ] ); s < e; s++ ) { + if ( levelOfEmbedding ( ea [ s ] ) == l0 ) { + nl++; + } else { + break; + } + } + return nl; + } + + private static boolean startsWithRetainedFormattingRun ( int[] wca, int[] ea, int start ) { + int nl = getLevelRunLength ( ea, start ); + if ( nl > 0 ) { + int nc = getRetainedFormattingRunLength ( wca, start ); + return ( nc >= nl ); + } else { + return false; + } + } + + private static int getRetainedFormattingRunLength ( int[] wca, int start ) { + assert start < wca.length; + int nc = 0; + for ( int s = start, e = wca.length; s < e; s++ ) { + if ( wca [ s ] == BidiConstants.BN ) { + nc++; + } else { + break; + } + } + return nc; + } + + private static int resolveRun ( int[] wca, int defaultLevel, int[] ea, int[] la, int start, int end, int level, int levelPrev ) { + + // determine start of run direction + int sor = directionOfLevel ( max ( levelPrev, level ) ); + + // determine end of run direction + int le = -1; + if ( end == wca.length ) { + le = max ( level, defaultLevel ); + } else { + for ( int i = end; i < wca.length; i++ ) { + if ( wca [ i ] != BidiConstants.BN ) { + le = max ( level, la [ i ] ); + break; + } + } + if ( le < 0 ) { + le = max ( level, defaultLevel ); + } + } + int eor = directionOfLevel ( le ); + + if (log.isDebugEnabled()) { + log.debug ( "BR[" + padLeft ( start, 3 ) + "," + padLeft ( end, 3 ) + "] :" + padLeft ( level, 2 ) + ": SOR(" + getClassName(sor) + "), EOR(" + getClassName(eor) + ")" ); + } + + resolveWeak ( wca, defaultLevel, ea, la, start, end, level, sor, eor ); + resolveNeutrals ( wca, defaultLevel, ea, la, start, end, level, sor, eor ); + resolveImplicit ( wca, defaultLevel, ea, la, start, end, level, sor, eor ); + + // if this run is all retained formatting, then return prior level, otherwise this run's level + return isRetainedFormatting ( wca, start, end ) ? levelPrev : level; + } + + private static void resolveWeak ( int[] wca, int defaultLevel, int[] ea, int[] la, int start, int end, int level, int sor, int eor ) { + + // W1 - X BN* NSM -> X BN* X + for ( int i = start, n = end, bcPrev = sor; i < n; i++ ) { + int bc = wca [ i ]; + if ( bc == NSM ) { + wca [ i ] = bcPrev; + } else if ( bc != BN ) { + bcPrev = bc; + } + } + + // W2 - AL ... EN -> AL ... AN + for ( int i = start, n = end, bcPrev = sor; i < n; i++ ) { + int bc = wca [ i ]; + if ( bc == EN ) { + if ( bcPrev == AL ) { + wca [ i ] = AN; + } + } else if ( isStrong ( bc ) ) { + bcPrev = bc; + } + } + + // W3 - AL -> R + for ( int i = start, n = end; i < n; i++ ) { + int bc = wca [ i ]; + if ( bc == AL ) { + wca [ i ] = R; + } + } + + // W4 - EN BN* ES BN* EN -> EN BN* EN BN* EN; XN BN* CS BN* XN -> XN BN* XN BN* XN + for ( int i = start, n = end, bcPrev = sor; i < n; i++ ) { + int bc = wca [ i ]; + if ( bc == ES ) { + int bcNext = eor; + for ( int j = i + 1; j < n; j++ ) { + if ( ( bc = wca [ j ] ) != BN ) { + bcNext = bc; + break; + } + } + if ( ( bcPrev == EN ) && ( bcNext == EN ) ) { + wca [ i ] = EN; + } + } else if ( bc == CS ) { + int bcNext = eor; + for ( int j = i + 1; j < n; j++ ) { + if ( ( bc = wca [ j ] ) != BN ) { + bcNext = bc; + break; + } + } + if ( ( bcPrev == EN ) && ( bcNext == EN ) ) { + wca [ i ] = EN; + } else if ( ( bcPrev == AN ) && ( bcNext == AN ) ) { + wca [ i ] = AN; + } + } + if ( bc != BN ) { + bcPrev = bc; + } + } + + // W5 - EN (ET|BN)* -> EN (EN|BN)*; (ET|BN)* EN -> (EN|BN)* EN + for ( int i = start, n = end, bcPrev = sor; i < n; i++ ) { + int bc = wca [ i ]; + if ( bc == ET ) { + int bcNext = eor; + for ( int j = i + 1; j < n; j++ ) { + bc = wca [ j ]; + if ( ( bc != BN ) && ( bc != ET ) ) { + bcNext = bc; + break; + } + } + if ( ( bcPrev == EN ) || ( bcNext == EN ) ) { + wca [ i ] = EN; + } + } else if ( ( bc != BN ) && ( bc != ET ) ) { + bcPrev = bc; + } + } + + // W6 - BN* (ET|ES|CS) BN* -> ON* ON ON* + for ( int i = start, n = end; i < n; i++ ) { + int bc = wca [ i ]; + if ( ( bc == ET ) || ( bc == ES ) || ( bc == CS ) ) { + wca [ i ] = ON; + resolveAdjacentBoundaryNeutrals ( wca, start, end, i, ON ); + } + } + + // W7 - L ... EN -> L ... L + for ( int i = start, n = end, bcPrev = sor; i < n; i++ ) { + int bc = wca [ i ]; + if ( bc == EN ) { + if ( bcPrev == L ) { + wca [ i ] = L; + } + } else if ( ( bc == L ) || ( bc == R ) ) { + bcPrev = bc; + } + } + + } + + private static void resolveNeutrals ( int[] wca, int defaultLevel, int[] ea, int[] la, int start, int end, int level, int sor, int eor ) { + + // N1 - (L|R) N+ (L|R) -> L L+ L | R R+ R; (AN|EN) N+ R -> (AN|EN) R+ R; R N+ (AN|EN) -> R R+ (AN|EN) + for ( int i = start, n = end, bcPrev = sor; i < n; i++ ) { + int bc = wca [ i ]; + if ( isNeutral ( bc ) ) { + int bcNext = eor; + for ( int j = i + 1; j < n; j++ ) { + bc = wca [ j ]; + if ( ( bc == L ) || ( bc == R ) ) { + bcNext = bc; + break; + } else if ( ( bc == AN ) || ( bc == EN ) ) { + bcNext = R; + break; + } else if ( isNeutral ( bc ) ) { + continue; + } else if ( isRetainedFormatting ( bc ) ) { + continue; + } else { + break; + } + } + if ( bcPrev == bcNext ) { + wca [ i ] = bcPrev; + resolveAdjacentBoundaryNeutrals ( wca, start, end, i, bcPrev ); + } + } else if ( ( bc == L ) || ( bc == R ) ) { + bcPrev = bc; + } else if ( ( bc == AN ) || ( bc == EN ) ) { + bcPrev = R; + } + } + + // N2 - N -> embedding level + for ( int i = start, n = end; i < n; i++ ) { + int bc = wca [ i ]; + if ( isNeutral ( bc ) ) { + int bcEmbedding = directionOfLevel ( levelOfEmbedding ( ea [ i ] ) ); + wca [ i ] = bcEmbedding; + resolveAdjacentBoundaryNeutrals ( wca, start, end, i, bcEmbedding ); + } + } + + } + + private static void resolveAdjacentBoundaryNeutrals ( int[] wca, int start, int end, int index, int bcNew ) { + if ( ( index < start ) || ( index >= end ) ) { + throw new IllegalArgumentException(); + } else { + for ( int i = index - 1; i >= start; i-- ) { + int bc = wca [ i ]; + if ( bc == BN ) { + wca [ i ] = bcNew; + } else { + break; + } + } + for ( int i = index + 1; i < end; i++ ) { + int bc = wca [ i ]; + if ( bc == BN ) { + wca [ i ] = bcNew; + } else { + break; + } + } + } + } + + private static void resolveImplicit ( int[] wca, int defaultLevel, int[] ea, int[] la, int start, int end, int level, int sor, int eor ) { + for ( int i = start, n = end; i < n; i++ ) { + int bc = wca [ i ]; // bidi class + int el = la [ i ]; // embedding level + int ed = 0; // embedding level delta + if ( ( el & 1 ) == 0 ) { // even + if ( bc == R ) { + ed = 1; + } else if ( bc == AN ) { + ed = 2; + } else if ( bc == EN ) { + ed = 2; + } + } else { // odd + if ( bc == L ) { + ed = 1; + } else if ( bc == EN ) { + ed = 1; + } else if ( bc == AN ) { + ed = 1; + } + } + la [ i ] = el + ed; + } + } + + /** + * Resolve separators and boundary neutral levels to account for UAX#9 3.4 L1 while taking into + * account retention of formatting codes (5.2). + * @param ica original input class array (sequence) + * @param wca working copy of original intput class array (sequence), as modified by prior steps + * @param dl default paragraph level + * @param la array of output levels to be adjusted, as produced by bidi algorithm + */ + private static void resolveSeparators ( int[] ica, int[] wca, int dl, int[] la ) { + // steps (1) through (3) + for ( int i = 0, n = ica.length; i < n; i++ ) { + int ic = ica[i]; + if ( ( ic == BidiConstants.S ) || ( ic == BidiConstants.B ) ) { + la[i] = dl; + for ( int k = i - 1; k >= 0; k-- ) { + int pc = ica[k]; + if ( isRetainedFormatting ( pc ) ) { + continue; + } if ( pc == BidiConstants.WS ) { + la[k] = dl; + } else { + break; + } + } + } + } + // step (4) - consider end of input sequence to be end of line, but skip any trailing boundary neutrals and retained formatting codes + for ( int i = ica.length; i > 0; i-- ) { + int k = i - 1; + int ic = ica[k]; + if ( isRetainedFormatting ( ic ) ) { + continue; + } else if ( ic == BidiConstants.WS ) { + la[k] = dl; + } else { + break; + } + } + // step (5) - per section 5.2 + for ( int i = 0, n = ica.length; i < n; i++ ) { + int ic = ica[i]; + if ( isRetainedFormatting ( ic ) ) { + if ( i == 0 ) { + la[i] = dl; + } else { + la[i] = la [ i - 1 ]; + } + } + } + } + + private static boolean isStrong ( int bc ) { + switch ( bc ) { + case L: + case R: + case AL: + return true; + default: + return false; + } + } + + private static boolean isNeutral ( int bc ) { + switch ( bc ) { + case WS: + case ON: + case S: + case B: + return true; + default: + return false; + } + } + + private static boolean isRetainedFormatting ( int bc ) { + switch ( bc ) { + case LRE: + case LRO: + case RLE: + case RLO: + case PDF: + case BN: + return true; + default: + return false; + } + } + + private static boolean isRetainedFormatting ( int[] ca, int s, int e ) { + for ( int i = s; i < e; i++ ) { + if ( ! isRetainedFormatting ( ca[i] ) ) { + return false; + } + } + return true; + } + + private static int max ( int x, int y ) { + if ( x > y ) { + return x; + } else { + return y; + } + } + + private static int[] getClasses ( int[] chars ) { + int[] classes = new int [ chars.length ]; + int bc; + for ( int i = 0, n = chars.length; i < n; i++ ) { + int ch = chars [ i ]; + if ( ch >= 0 ) { + bc = BidiClass.getBidiClass ( chars [ i ] ); + } else { + bc = SURROGATE; + } + classes [ i ] = bc; + } + return classes; + } + + /** + * Convert character sequence (a UTF-16 encoded string) to an array of unicode scalar values + * expressed as integers. If a valid UTF-16 surrogate pair is encountered, it is converted to + * two integers, the first being the equivalent unicode scalar value, and the second being + * negative one (-1). This special mechanism is used to track the use of surrogate pairs while + * working with unicode scalar values, and permits maintaining indices that apply both to the + * input UTF-16 and out scalar value sequences. + * @return a boolean indicating that content is present that triggers bidirectional processing + * @param cs a UTF-16 encoded character sequence + * @param chars an integer array to accept the converted scalar values, where the length of the + * array must be the same as the length of the input character sequence + * @throws IllegalArgumentException if the input sequence is not a valid UTF-16 string, e.g., + * if it contains an isolated UTF-16 surrogate + */ + private static boolean convertToScalar ( CharSequence cs, int[] chars ) throws IllegalArgumentException { + boolean triggered = false; + if ( chars.length != cs.length() ) { + throw new IllegalArgumentException ( "characters array length must match input sequence length" ); + } + for ( int i = 0, n = chars.length; i < n; ) { + int chIn = cs.charAt ( i ); + int chOut; + if ( chIn < 0xD800 ) { + chOut = chIn; + } else if ( chIn < 0xDC00 ) { + int chHi = chIn; + int chLo; + if ( ( i + 1 ) < n ) { + chLo = cs.charAt ( i + 1 ); + if ( ( chLo >= 0xDC00 ) && ( chLo <= 0xDFFF ) ) { + chOut = convertToScalar ( chHi, chLo ); + } else { + throw new IllegalArgumentException ( "isolated high surrogate" ); + } + } else { + throw new IllegalArgumentException ( "truncated surrogate pair" ); + } + } else if ( chIn < 0xE000 ) { + throw new IllegalArgumentException ( "isolated low surrogate" ); + } else { + chOut = chIn; + } + if ( ! triggered && triggersBidi ( chOut ) ) { + triggered = true; + } + if ( ( chOut & 0xFF0000 ) == 0 ) { + chars [ i++ ] = chOut; + } else { + chars [ i++ ] = chOut; + chars [ i++ ] = -1; + } + } + return triggered; + } + + /** + * Convert UTF-16 surrogate pair to unicode scalar valuee. + * @return a unicode scalar value + * @param chHi high (most significant or first) surrogate + * @param chLo low (least significant or second) surrogate + * @throws IllegalArgumentException if one of the input surrogates is not valid + */ + private static int convertToScalar ( int chHi, int chLo ) { + if ( ( chHi < 0xD800 ) || ( chHi > 0xDBFF ) ) { + throw new IllegalArgumentException ( "bad high surrogate" ); + } else if ( ( chLo < 0xDC00 ) || ( chLo > 0xDFFF ) ) { + throw new IllegalArgumentException ( "bad low surrogate" ); + } else { + return ( ( ( chHi & 0x03FF ) << 10 ) | ( chLo & 0x03FF ) ) + 0x10000; + } + } + + /** + * Determine of character CH triggers bidirectional processing. Bidirectional + * processing is deemed triggerable if CH is a strong right-to-left character, + * an arabic letter or number, or is a right-to-left embedding or override + * character. + * @return true if character triggers bidirectional processing + * @param ch a unicode scalar value + */ + private static boolean triggersBidi ( int ch ) { + switch ( BidiClass.getBidiClass ( ch ) ) { + case R: + case AL: + case AN: + case RLE: + case RLO: + return true; + default: + return false; + } + } + + private static void dump ( String header, int[] chars, int[] classes, int defaultLevel, int[] levels ) { + log.debug ( header ); + log.debug ( "BD: default level(" + defaultLevel + ")" ); + StringBuffer sb = new StringBuffer(); + if ( chars != null ) { + for ( int i = 0, n = chars.length; i < n; i++ ) { + int ch = chars [ i ]; + sb.setLength(0); + if ( ( ch > 0x20 ) && ( ch < 0x7F ) ) { + sb.append ( (char) ch ); + } else { + sb.append ( CharUtilities.charToNCRef ( ch ) ); + } + for ( int k = sb.length(); k < 12; k++ ) { + sb.append ( ' ' ); + } + sb.append ( ": " + padRight ( getClassName ( classes[i] ), 4 ) + " " + levels[i] ); + log.debug ( sb ); + } + } else { + for ( int i = 0, n = classes.length; i < n; i++ ) { + sb.setLength(0); + for ( int k = sb.length(); k < 12; k++ ) { + sb.append ( ' ' ); + } + sb.append ( ": " + padRight ( getClassName ( classes[i] ), 4 ) + " " + levels[i] ); + log.debug ( sb ); + } + } + } + + private static String getClassName ( int bc ) { + switch ( bc ) { + case L: // left-to-right + return "L"; + case LRE: // left-to-right embedding + return "LRE"; + case LRO: // left-to-right override + return "LRO"; + case R: // right-to-left + return "R"; + case AL: // right-to-left arabic + return "AL"; + case RLE: // right-to-left embedding + return "RLE"; + case RLO: // right-to-left override + return "RLO"; + case PDF: // pop directional formatting + return "PDF"; + case EN: // european number + return "EN"; + case ES: // european number separator + return "ES"; + case ET: // european number terminator + return "ET"; + case AN: // arabic number + return "AN"; + case CS: // common number separator + return "CS"; + case NSM: // non-spacing mark + return "NSM"; + case BN: // boundary neutral + return "BN"; + case B: // paragraph separator + return "B"; + case S: // segment separator + return "S"; + case WS: // whitespace + return "WS"; + case ON: // other neutrals + return "ON"; + case SURROGATE: // placeholder for low surrogate + return "SUR"; + default: + return "?"; + } + } + + private static String padLeft ( int n, int width ) { + return padLeft ( Integer.toString ( n ), width ); + } + + private static String padLeft ( String s, int width ) { + StringBuffer sb = new StringBuffer(); + for ( int i = s.length(); i < width; i++ ) { + sb.append(' '); + } + sb.append ( s ); + return sb.toString(); + } + + /* not used yet + private static String padRight ( int n, int width ) { + return padRight ( Integer.toString ( n ), width ); + } + */ + + private static String padRight ( String s, int width ) { + StringBuffer sb = new StringBuffer ( s ); + for ( int i = sb.length(); i < width; i++ ) { + sb.append(' '); + } + return sb.toString(); + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/AdvancedTypographicTableFormatException.java b/src/java/org/apache/fop/complexscripts/fonts/AdvancedTypographicTableFormatException.java new file mode 100644 index 000000000..4f3e341d6 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/AdvancedTypographicTableFormatException.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +/** + * Exception thrown when attempting to decode a truetype font file and a format + * constraint is violated. + * @author Glenn Adams + */ +public class AdvancedTypographicTableFormatException extends RuntimeException { + /** + * Instantiate ATT format exception. + */ + public AdvancedTypographicTableFormatException() { + super(); + } + /** + * Instantiate ATT format exception. + * @param message a message string + */ + public AdvancedTypographicTableFormatException(String message) { + super(message); + } + /** + * Instantiate ATT format exception. + * @param message a message string + * @param cause a <code>Throwable</code> that caused this exception + */ + public AdvancedTypographicTableFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphClassMapping.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphClassMapping.java new file mode 100644 index 000000000..4afc747a2 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphClassMapping.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +// CSOFF: LineLengthCheck + +/** + * The <code>GlyphClassMapping</code> interface provides glyph identifier to class + * index mapping support. + * @author Glenn Adams + */ +public interface GlyphClassMapping { + + /** + * Obtain size of class table, i.e., ciMax + 1, where ciMax is the maximum + * class index. + * @param set for coverage set based class mappings, indicates set index, otherwise ignored + * @return size of class table + */ + int getClassSize ( int set ); + + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the class table. + * @param gid glyph identifier (code) + * @param set for coverage set based class mappings, indicates set index, otherwise ignored + * @return non-negative glyph class index or -1 if glyph identifiers is not mapped by table + */ + int getClassIndex ( int gid, int set ); + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphClassTable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphClassTable.java new file mode 100644 index 000000000..88478531e --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphClassTable.java @@ -0,0 +1,277 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.util.Arrays; +import java.util.List; +import java.util.Iterator; + +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck + +/** + * Base class implementation of glyph class table. + * @author Glenn Adams + */ +public final class GlyphClassTable extends GlyphMappingTable implements GlyphClassMapping { + + /** empty mapping table */ + public static final int GLYPH_CLASS_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY; + + /** mapped mapping table */ + public static final int GLYPH_CLASS_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED; + + /** range based mapping table */ + public static final int GLYPH_CLASS_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE; + + /** empty mapping table */ + public static final int GLYPH_CLASS_TYPE_COVERAGE_SET = 3; + + private GlyphClassMapping cm; + + private GlyphClassTable ( GlyphClassMapping cm ) { + assert cm != null; + assert cm instanceof GlyphMappingTable; + this.cm = cm; + } + + /** {@inheritDoc} */ + public int getType() { + return ( (GlyphMappingTable) cm ) .getType(); + } + + /** {@inheritDoc} */ + public List getEntries() { + return ( (GlyphMappingTable) cm ) .getEntries(); + } + + /** {@inheritDoc} */ + public int getClassSize ( int set ) { + return cm.getClassSize ( set ); + } + + /** {@inheritDoc} */ + public int getClassIndex ( int gid, int set ) { + return cm.getClassIndex ( gid, set ); + } + + /** + * Create glyph class table. + * @param entries list of mapped or ranged class entries, or null or empty list + * @return a new covera table instance + */ + public static GlyphClassTable createClassTable ( List entries ) { + GlyphClassMapping cm; + if ( ( entries == null ) || ( entries.size() == 0 ) ) { + cm = new EmptyClassTable ( entries ); + } else if ( isMappedClass ( entries ) ) { + cm = new MappedClassTable ( entries ); + } else if ( isRangeClass ( entries ) ) { + cm = new RangeClassTable ( entries ); + } else if ( isCoverageSetClass ( entries ) ) { + cm = new CoverageSetClassTable ( entries ); + } else { + cm = null; + } + assert cm != null : "unknown class type"; + return new GlyphClassTable ( cm ); + } + + private static boolean isMappedClass ( List entries ) { + if ( ( entries == null ) || ( entries.size() == 0 ) ) { + return false; + } else { + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( ! ( o instanceof Integer ) ) { + return false; + } + } + return true; + } + } + + private static boolean isRangeClass ( List entries ) { + if ( ( entries == null ) || ( entries.size() == 0 ) ) { + return false; + } else { + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( ! ( o instanceof MappingRange ) ) { + return false; + } + } + return true; + } + } + + private static boolean isCoverageSetClass ( List entries ) { + if ( ( entries == null ) || ( entries.size() == 0 ) ) { + return false; + } else { + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( ! ( o instanceof GlyphCoverageTable ) ) { + return false; + } + } + return true; + } + } + + private static class EmptyClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping { + public EmptyClassTable ( List entries ) { + super ( entries ); + } + /** {@inheritDoc} */ + public int getClassSize ( int set ) { + return 0; + } + /** {@inheritDoc} */ + public int getClassIndex ( int gid, int set ) { + return -1; + } + } + + private static class MappedClassTable extends GlyphMappingTable.MappedMappingTable implements GlyphClassMapping { + private int firstGlyph; + private int[] gca; + private int gcMax = -1; + public MappedClassTable ( List entries ) { + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + List entries = new java.util.ArrayList(); + entries.add ( Integer.valueOf ( firstGlyph ) ); + if ( gca != null ) { + for ( int i = 0, n = gca.length; i < n; i++ ) { + entries.add ( Integer.valueOf ( gca [ i ] ) ); + } + } + return entries; + } + /** {@inheritDoc} */ + public int getMappingSize() { + return gcMax + 1; + } + /** {@inheritDoc} */ + public int getMappedIndex ( int gid ) { + int i = gid - firstGlyph; + if ( ( i >= 0 ) && ( i < gca.length ) ) { + return gca [ i ]; + } else { + return -1; + } + } + /** {@inheritDoc} */ + public int getClassSize ( int set ) { + return getMappingSize(); + } + /** {@inheritDoc} */ + public int getClassIndex ( int gid, int set ) { + return getMappedIndex ( gid ); + } + private void populate ( List entries ) { + // obtain entries iterator + Iterator it = entries.iterator(); + // extract first glyph + int firstGlyph = 0; + if ( it.hasNext() ) { + Object o = it.next(); + if ( o instanceof Integer ) { + firstGlyph = ( (Integer) o ) . intValue(); + } else { + throw new AdvancedTypographicTableFormatException ( "illegal entry, first entry must be Integer denoting first glyph value, but is: " + o ); + } + } + // extract glyph class array + int i = 0, n = entries.size() - 1, gcMax = -1; + int[] gca = new int [ n ]; + while ( it.hasNext() ) { + Object o = it.next(); + if ( o instanceof Integer ) { + int gc = ( (Integer) o ) . intValue(); + gca [ i++ ] = gc; + if ( gc > gcMax ) { + gcMax = gc; + } + } else { + throw new AdvancedTypographicTableFormatException ( "illegal mapping entry, must be Integer: " + o ); + } + } + assert i == n; + assert this.gca == null; + this.firstGlyph = firstGlyph; + this.gca = gca; + this.gcMax = gcMax; + } + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("{ firstGlyph = " + firstGlyph + ", classes = {"); + for ( int i = 0, n = gca.length; i < n; i++ ) { + if ( i > 0 ) { + sb.append(','); + } + sb.append ( Integer.toString ( gca [ i ] ) ); + } + sb.append("} }"); + return sb.toString(); + } + } + + private static class RangeClassTable extends GlyphMappingTable.RangeMappingTable implements GlyphClassMapping { + public RangeClassTable ( List entries ) { + super ( entries ); + } + /** {@inheritDoc} */ + public int getMappedIndex ( int gid, int s, int m ) { + return m; + } + /** {@inheritDoc} */ + public int getClassSize ( int set ) { + return getMappingSize(); + } + /** {@inheritDoc} */ + public int getClassIndex ( int gid, int set ) { + return getMappedIndex ( gid ); + } + } + + private static class CoverageSetClassTable extends GlyphMappingTable.EmptyMappingTable implements GlyphClassMapping { + public CoverageSetClassTable ( List entries ) { + throw new UnsupportedOperationException ( "coverage set class table not yet supported" ); + } + /** {@inheritDoc} */ + public int getType() { + return GLYPH_CLASS_TYPE_COVERAGE_SET; + } + /** {@inheritDoc} */ + public int getClassSize ( int set ) { + return 0; + } + /** {@inheritDoc} */ + public int getClassIndex ( int gid, int set ) { + return -1; + } + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphCoverageMapping.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphCoverageMapping.java new file mode 100644 index 000000000..e8fde9474 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphCoverageMapping.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +// CSOFF: LineLengthCheck + +/** + * The <code>GlyphCoverageMapping</code> interface provides glyph identifier to coverage + * index mapping support. + * @author Glenn Adams + */ +public interface GlyphCoverageMapping { + + /** + * Obtain size of coverage table, i.e., ciMax + 1, where ciMax is the maximum + * coverage index. + * @return size of coverage table + */ + int getCoverageSize(); + + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the coverage table. + * @param gid glyph identifier (code) + * @return non-negative glyph coverage index or -1 if glyph identifiers is not mapped by table + */ + int getCoverageIndex ( int gid ); + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphCoverageTable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphCoverageTable.java new file mode 100644 index 000000000..a9b0fc2b5 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphCoverageTable.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.util.Arrays; +import java.util.List; +import java.util.Iterator; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +// CSOFF: LineLengthCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: NoWhitespaceAfterCheck + +/** + * Base class implementation of glyph coverage table. + * @author Glenn Adams + */ +public final class GlyphCoverageTable extends GlyphMappingTable implements GlyphCoverageMapping { + + /* logging instance */ + private static final Log log = LogFactory.getLog(GlyphCoverageTable.class); // CSOK: ConstantNameCheck + + /** empty mapping table */ + public static final int GLYPH_COVERAGE_TYPE_EMPTY = GLYPH_MAPPING_TYPE_EMPTY; + + /** mapped mapping table */ + public static final int GLYPH_COVERAGE_TYPE_MAPPED = GLYPH_MAPPING_TYPE_MAPPED; + + /** range based mapping table */ + public static final int GLYPH_COVERAGE_TYPE_RANGE = GLYPH_MAPPING_TYPE_RANGE; + + private GlyphCoverageMapping cm; + + private GlyphCoverageTable ( GlyphCoverageMapping cm ) { + assert cm != null; + assert cm instanceof GlyphMappingTable; + this.cm = cm; + } + + /** {@inheritDoc} */ + public int getType() { + return ( (GlyphMappingTable) cm ) .getType(); + } + + /** {@inheritDoc} */ + public List getEntries() { + return ( (GlyphMappingTable) cm ) .getEntries(); + } + + /** {@inheritDoc} */ + public int getCoverageSize() { + return cm.getCoverageSize(); + } + + /** {@inheritDoc} */ + public int getCoverageIndex ( int gid ) { + return cm.getCoverageIndex ( gid ); + } + + /** + * Create glyph coverage table. + * @param entries list of mapped or ranged coverage entries, or null or empty list + * @return a new covera table instance + */ + public static GlyphCoverageTable createCoverageTable ( List entries ) { + GlyphCoverageMapping cm; + if ( ( entries == null ) || ( entries.size() == 0 ) ) { + cm = new EmptyCoverageTable ( entries ); + } else if ( isMappedCoverage ( entries ) ) { + cm = new MappedCoverageTable ( entries ); + } else if ( isRangeCoverage ( entries ) ) { + cm = new RangeCoverageTable ( entries ); + } else { + cm = null; + } + assert cm != null : "unknown coverage type"; + return new GlyphCoverageTable ( cm ); + } + + private static boolean isMappedCoverage ( List entries ) { + if ( ( entries == null ) || ( entries.size() == 0 ) ) { + return false; + } else { + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( ! ( o instanceof Integer ) ) { + return false; + } + } + return true; + } + } + + private static boolean isRangeCoverage ( List entries ) { + if ( ( entries == null ) || ( entries.size() == 0 ) ) { + return false; + } else { + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( ! ( o instanceof MappingRange ) ) { + return false; + } + } + return true; + } + } + + private static class EmptyCoverageTable extends GlyphMappingTable.EmptyMappingTable implements GlyphCoverageMapping { + public EmptyCoverageTable ( List entries ) { + super ( entries ); + } + /** {@inheritDoc} */ + public int getCoverageSize() { + return 0; + } + /** {@inheritDoc} */ + public int getCoverageIndex ( int gid ) { + return -1; + } + } + + private static class MappedCoverageTable extends GlyphMappingTable.MappedMappingTable implements GlyphCoverageMapping { + private int[] map; + public MappedCoverageTable ( List entries ) { + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + List entries = new java.util.ArrayList(); + if ( map != null ) { + for ( int i = 0, n = map.length; i < n; i++ ) { + entries.add ( Integer.valueOf ( map [ i ] ) ); + } + } + return entries; + } + /** {@inheritDoc} */ + public int getMappingSize() { + return ( map != null ) ? map.length : 0; + } + public int getMappedIndex ( int gid ) { + int i; + if ( ( i = Arrays.binarySearch ( map, gid ) ) >= 0 ) { + return i; + } else { + return -1; + } + } + /** {@inheritDoc} */ + public int getCoverageSize() { + return getMappingSize(); + } + /** {@inheritDoc} */ + public int getCoverageIndex ( int gid ) { + return getMappedIndex ( gid ); + } + private void populate ( List entries ) { + int i = 0, n = entries.size(), gidMax = -1; + int[] map = new int [ n ]; + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( o instanceof Integer ) { + int gid = ( (Integer) o ) . intValue(); + if ( ( gid >= 0 ) && ( gid < 65536 ) ) { + if ( gid > gidMax ) { + map [ i++ ] = gidMax = gid; + } else { + log.info ( "ignoring out of order or duplicate glyph index: " + gid ); + } + } else { + throw new AdvancedTypographicTableFormatException ( "illegal glyph index: " + gid ); + } + } else { + throw new AdvancedTypographicTableFormatException ( "illegal coverage entry, must be Integer: " + o ); + } + } + assert i == n; + assert this.map == null; + this.map = map; + } + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append('{'); + for ( int i = 0, n = map.length; i < n; i++ ) { + if ( i > 0 ) { + sb.append(','); + } + sb.append ( Integer.toString ( map [ i ] ) ); + } + sb.append('}'); + return sb.toString(); + } + } + + private static class RangeCoverageTable extends GlyphMappingTable.RangeMappingTable implements GlyphCoverageMapping { + public RangeCoverageTable ( List entries ) { + super ( entries ); + } + /** {@inheritDoc} */ + public int getMappedIndex ( int gid, int s, int m ) { + return m + gid - s; + } + /** {@inheritDoc} */ + public int getCoverageSize() { + return getMappingSize(); + } + /** {@inheritDoc} */ + public int getCoverageIndex ( int gid ) { + return getMappedIndex ( gid ); + } + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphDefinition.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphDefinition.java new file mode 100644 index 000000000..a3d511dce --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphDefinition.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +// CSOFF: LineLengthCheck + +/** + * The <code>GlyphDefinition</code> interface is a marker interface implemented by a glyph definition + * subtable. + * @author Glenn Adams + */ +public interface GlyphDefinition { + + /** + * Determine if some definition is available for a specific glyph. + * @param gi a glyph index + * @return true if some (unspecified) definition is available for the specified glyph + */ + boolean hasDefinition ( int gi ); + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphDefinitionSubtable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphDefinitionSubtable.java new file mode 100644 index 000000000..ec7e1e3d2 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphDefinitionSubtable.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +// CSOFF: LineLengthCheck +// CSOFF: InnerAssignmentCheck + +/** + * The <code>GlyphDefinitionSubtable</code> implements an abstract base of a glyph definition subtable, + * providing a default implementation of the <code>GlyphDefinition</code> interface. + * @author Glenn Adams + */ +public abstract class GlyphDefinitionSubtable extends GlyphSubtable implements GlyphDefinition { + + /** + * Instantiate a <code>GlyphDefinitionSubtable</code>. + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param mapping subtable coverage table + */ + protected GlyphDefinitionSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping ) { + super ( id, sequence, flags, format, mapping ); + } + + /** {@inheritDoc} */ + public int getTableType() { + return GlyphTable.GLYPH_TABLE_TYPE_DEFINITION; + } + + /** {@inheritDoc} */ + public String getTypeName() { + return GlyphDefinitionTable.getLookupTypeName ( getType() ); + } + + /** {@inheritDoc} */ + public boolean usesReverseScan() { + return false; + } + + /** {@inheritDoc} */ + public boolean hasDefinition ( int gi ) { + GlyphCoverageMapping cvm; + if ( ( cvm = getCoverage() ) != null ) { + if ( cvm.getCoverageIndex ( gi ) >= 0 ) { + return true; + } + } + GlyphClassMapping clm; + if ( ( clm = getClasses() ) != null ) { + if ( clm.getClassIndex ( gi, 0 ) >= 0 ) { + return true; + } + } + return false; + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphDefinitionTable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphDefinitionTable.java new file mode 100644 index 000000000..d5cd04615 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphDefinitionTable.java @@ -0,0 +1,451 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.nio.CharBuffer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.scripts.ScriptProcessor; +import org.apache.fop.complexscripts.util.GlyphSequence; + +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck + +/** + * The <code>GlyphDefinitionTable</code> class is a glyph table that implements + * glyph definition functionality according to the OpenType GDEF table. + * @author Glenn Adams + */ +public class GlyphDefinitionTable extends GlyphTable { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GlyphDefinitionTable.class); // CSOK: ConstantNameCheck + + /** glyph class subtable type */ + public static final int GDEF_LOOKUP_TYPE_GLYPH_CLASS = 1; + /** attachment point subtable type */ + public static final int GDEF_LOOKUP_TYPE_ATTACHMENT_POINT = 2; + /** ligature caret subtable type */ + public static final int GDEF_LOOKUP_TYPE_LIGATURE_CARET = 3; + /** mark attachment subtable type */ + public static final int GDEF_LOOKUP_TYPE_MARK_ATTACHMENT = 4; + + /** pre-defined glyph class - base glyph */ + public static final int GLYPH_CLASS_BASE = 1; + /** pre-defined glyph class - ligature glyph */ + public static final int GLYPH_CLASS_LIGATURE = 2; + /** pre-defined glyph class - mark glyph */ + public static final int GLYPH_CLASS_MARK = 3; + /** pre-defined glyph class - component glyph */ + public static final int GLYPH_CLASS_COMPONENT = 4; + + /** singleton glyph class table */ + private GlyphClassSubtable gct; + /** singleton attachment point table */ + // private AttachmentPointSubtable apt; // NOT YET USED + /** singleton ligature caret table */ + // private LigatureCaretSubtable lct; // NOT YET USED + /** singleton mark attachment table */ + private MarkAttachmentSubtable mat; + + /** + * Instantiate a <code>GlyphDefinitionTable</code> object using the specified subtables. + * @param subtables a list of identified subtables + */ + public GlyphDefinitionTable ( List subtables ) { + super ( null, new HashMap(0) ); + if ( ( subtables == null ) || ( subtables.size() == 0 ) ) { + throw new AdvancedTypographicTableFormatException ( "subtables must be non-empty" ); + } else { + for ( Iterator it = subtables.iterator(); it.hasNext();) { + Object o = it.next(); + if ( o instanceof GlyphDefinitionSubtable ) { + addSubtable ( (GlyphSubtable) o ); + } else { + throw new AdvancedTypographicTableFormatException ( "subtable must be a glyph definition subtable" ); + } + } + freezeSubtables(); + } + } + + /** + * Reorder combining marks in glyph sequence so that they precede (within the sequence) the base + * character to which they are applied. N.B. In the case of LTR segments, marks are not reordered by this, + * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede + * their base glyph. + * @param gs an input glyph sequence + * @param gpa associated glyph position adjustments (also reordered) + * @param script a script identifier + * @param language a language identifier + * @return the reordered (output) glyph sequence + */ + public GlyphSequence reorderCombiningMarks ( GlyphSequence gs, int[][] gpa, String script, String language ) { + ScriptProcessor sp = ScriptProcessor.getInstance ( script ); + return sp.reorderCombiningMarks ( this, gs, gpa, script, language ); + } + + /** {@inheritDoc} */ + protected void addSubtable ( GlyphSubtable subtable ) { + if ( subtable instanceof GlyphClassSubtable ) { + this.gct = (GlyphClassSubtable) subtable; + } else if ( subtable instanceof AttachmentPointSubtable ) { + // TODO - not yet used + // this.apt = (AttachmentPointSubtable) subtable; + } else if ( subtable instanceof LigatureCaretSubtable ) { + // TODO - not yet used + // this.lct = (LigatureCaretSubtable) subtable; + } else if ( subtable instanceof MarkAttachmentSubtable ) { + this.mat = (MarkAttachmentSubtable) subtable; + } else { + throw new UnsupportedOperationException ( "unsupported glyph definition subtable type: " + subtable ); + } + } + + /** + * Determine if glyph belongs to pre-defined glyph class. + * @param gid a glyph identifier (index) + * @param gc a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + * @return true if glyph belongs to specified glyph class + */ + public boolean isGlyphClass ( int gid, int gc ) { + if ( gct != null ) { + return gct.isGlyphClass ( gid, gc ); + } else { + return false; + } + } + + /** + * Determine glyph class. + * @param gid a glyph identifier (index) + * @return a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + */ + public int getGlyphClass ( int gid ) { + if ( gct != null ) { + return gct.getGlyphClass ( gid ); + } else { + return -1; + } + } + + /** + * Determine if glyph belongs to (font specific) mark attachment class. + * @param gid a glyph identifier (index) + * @param mac a (font specific) mark attachment class + * @return true if glyph belongs to specified mark attachment class + */ + public boolean isMarkAttachClass ( int gid, int mac ) { + if ( mat != null ) { + return mat.isMarkAttachClass ( gid, mac ); + } else { + return false; + } + } + + /** + * Determine mark attachment class. + * @param gid a glyph identifier (index) + * @return a non-negative mark attachment class, or -1 if no class defined + */ + public int getMarkAttachClass ( int gid ) { + if ( mat != null ) { + return mat.getMarkAttachClass ( gid ); + } else { + return -1; + } + } + + /** + * Map a lookup type name to its constant (integer) value. + * @param name lookup type name + * @return lookup type + */ + public static int getLookupTypeFromName ( String name ) { + int t; + String s = name.toLowerCase(); + if ( "glyphclass".equals ( s ) ) { + t = GDEF_LOOKUP_TYPE_GLYPH_CLASS; + } else if ( "attachmentpoint".equals ( s ) ) { + t = GDEF_LOOKUP_TYPE_ATTACHMENT_POINT; + } else if ( "ligaturecaret".equals ( s ) ) { + t = GDEF_LOOKUP_TYPE_LIGATURE_CARET; + } else if ( "markattachment".equals ( s ) ) { + t = GDEF_LOOKUP_TYPE_MARK_ATTACHMENT; + } else { + t = -1; + } + return t; + } + + /** + * Map a lookup type constant (integer) value to its name. + * @param type lookup type + * @return lookup type name + */ + public static String getLookupTypeName ( int type ) { + String tn = null; + switch ( type ) { + case GDEF_LOOKUP_TYPE_GLYPH_CLASS: + tn = "glyphclass"; + break; + case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT: + tn = "attachmentpoint"; + break; + case GDEF_LOOKUP_TYPE_LIGATURE_CARET: + tn = "ligaturecaret"; + break; + case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT: + tn = "markattachment"; + break; + default: + tn = "unknown"; + break; + } + return tn; + } + + /** + * Create a definition subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags (must be zero) + * @param format subtable format + * @param mapping subtable mapping table + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + GlyphSubtable st = null; + switch ( type ) { + case GDEF_LOOKUP_TYPE_GLYPH_CLASS: + st = GlyphClassSubtable.create ( id, sequence, flags, format, mapping, entries ); + break; + case GDEF_LOOKUP_TYPE_ATTACHMENT_POINT: + st = AttachmentPointSubtable.create ( id, sequence, flags, format, mapping, entries ); + break; + case GDEF_LOOKUP_TYPE_LIGATURE_CARET: + st = LigatureCaretSubtable.create ( id, sequence, flags, format, mapping, entries ); + break; + case GDEF_LOOKUP_TYPE_MARK_ATTACHMENT: + st = MarkAttachmentSubtable.create ( id, sequence, flags, format, mapping, entries ); + break; + default: + break; + } + return st; + } + + private abstract static class GlyphClassSubtable extends GlyphDefinitionSubtable { + GlyphClassSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + super ( id, sequence, flags, format, mapping ); + } + /** {@inheritDoc} */ + public int getType() { + return GDEF_LOOKUP_TYPE_GLYPH_CLASS; + } + /** + * Determine if glyph belongs to pre-defined glyph class. + * @param gid a glyph identifier (index) + * @param gc a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + * @return true if glyph belongs to specified glyph class + */ + public abstract boolean isGlyphClass ( int gid, int gc ); + /** + * Determine glyph class. + * @param gid a glyph identifier (index) + * @return a pre-defined glyph class (GLYPH_CLASS_BASE|GLYPH_CLASS_LIGATURE|GLYPH_CLASS_MARK|GLYPH_CLASS_COMPONENT). + */ + public abstract int getGlyphClass ( int gid ); + static GlyphDefinitionSubtable create ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + if ( format == 1 ) { + return new GlyphClassSubtableFormat1 ( id, sequence, flags, format, mapping, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class GlyphClassSubtableFormat1 extends GlyphClassSubtable { + GlyphClassSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + super ( id, sequence, flags, format, mapping, entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + return null; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof GlyphClassSubtable; + } + /** {@inheritDoc} */ + public boolean isGlyphClass ( int gid, int gc ) { + GlyphClassMapping cm = getClasses(); + if ( cm != null ) { + return cm.getClassIndex ( gid, 0 ) == gc; + } else { + return false; + } + } + /** {@inheritDoc} */ + public int getGlyphClass ( int gid ) { + GlyphClassMapping cm = getClasses(); + if ( cm != null ) { + return cm.getClassIndex ( gid, 0 ); + } else { + return -1; + } + } + } + + private abstract static class AttachmentPointSubtable extends GlyphDefinitionSubtable { + AttachmentPointSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + super ( id, sequence, flags, format, mapping ); + } + /** {@inheritDoc} */ + public int getType() { + return GDEF_LOOKUP_TYPE_ATTACHMENT_POINT; + } + static GlyphDefinitionSubtable create ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + if ( format == 1 ) { + return new AttachmentPointSubtableFormat1 ( id, sequence, flags, format, mapping, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class AttachmentPointSubtableFormat1 extends AttachmentPointSubtable { + AttachmentPointSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + super ( id, sequence, flags, format, mapping, entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + return null; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof AttachmentPointSubtable; + } + } + + private abstract static class LigatureCaretSubtable extends GlyphDefinitionSubtable { + LigatureCaretSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + super ( id, sequence, flags, format, mapping ); + } + /** {@inheritDoc} */ + public int getType() { + return GDEF_LOOKUP_TYPE_LIGATURE_CARET; + } + static GlyphDefinitionSubtable create ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + if ( format == 1 ) { + return new LigatureCaretSubtableFormat1 ( id, sequence, flags, format, mapping, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class LigatureCaretSubtableFormat1 extends LigatureCaretSubtable { + LigatureCaretSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + super ( id, sequence, flags, format, mapping, entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + return null; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof LigatureCaretSubtable; + } + } + + private abstract static class MarkAttachmentSubtable extends GlyphDefinitionSubtable { + MarkAttachmentSubtable ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + super ( id, sequence, flags, format, mapping ); + } + /** {@inheritDoc} */ + public int getType() { + return GDEF_LOOKUP_TYPE_MARK_ATTACHMENT; + } + /** + * Determine if glyph belongs to (font specific) mark attachment class. + * @param gid a glyph identifier (index) + * @param mac a (font specific) mark attachment class + * @return true if glyph belongs to specified mark attachment class + */ + public abstract boolean isMarkAttachClass ( int gid, int mac ); + /** + * Determine mark attachment class. + * @param gid a glyph identifier (index) + * @return a non-negative mark attachment class, or -1 if no class defined + */ + public abstract int getMarkAttachClass ( int gid ); + static GlyphDefinitionSubtable create ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + if ( format == 1 ) { + return new MarkAttachmentSubtableFormat1 ( id, sequence, flags, format, mapping, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MarkAttachmentSubtableFormat1 extends MarkAttachmentSubtable { + MarkAttachmentSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphMappingTable mapping, List entries ) { + super ( id, sequence, flags, format, mapping, entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + return null; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof MarkAttachmentSubtable; + } + /** {@inheritDoc} */ + public boolean isMarkAttachClass ( int gid, int mac ) { + GlyphClassMapping cm = getClasses(); + if ( cm != null ) { + return cm.getClassIndex ( gid, 0 ) == mac; + } else { + return false; + } + } + /** {@inheritDoc} */ + public int getMarkAttachClass ( int gid ) { + GlyphClassMapping cm = getClasses(); + if ( cm != null ) { + return cm.getClassIndex ( gid, 0 ); + } else { + return -1; + } + } + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphMappingTable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphMappingTable.java new file mode 100644 index 000000000..a0791e8f6 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphMappingTable.java @@ -0,0 +1,322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.util.Arrays; +import java.util.List; +import java.util.Iterator; + +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck + +/** + * Base class implementation of glyph mapping table. This base + * class maps glyph indices to arbitrary integers (mappping indices), and + * is used to implement both glyph coverage and glyph class maps. + * @author Glenn Adams + */ +public class GlyphMappingTable { + + /** empty mapping table */ + public static final int GLYPH_MAPPING_TYPE_EMPTY = 0; + + /** mapped mapping table */ + public static final int GLYPH_MAPPING_TYPE_MAPPED = 1; + + /** range based mapping table */ + public static final int GLYPH_MAPPING_TYPE_RANGE = 2; + + /** + * Obtain mapping type. + * @return mapping format type + */ + public int getType() { + return -1; + } + + /** + * Obtain mapping entries. + * @return list of mapping entries + */ + public List getEntries() { + return null; + } + + /** + * Obtain size of mapping table, i.e., ciMax + 1, where ciMax is the maximum + * mapping index. + * @return size of mapping table + */ + public int getMappingSize() { + return 0; + } + + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the mapping table. + * @param gid glyph identifier (code) + * @return non-negative glyph mapping index or -1 if glyph identifiers is not mapped by table + */ + public int getMappedIndex ( int gid ) { + return -1; + } + + /** empty mapping table base class */ + protected static class EmptyMappingTable extends GlyphMappingTable { + /** + * Construct empty mapping table. + */ + public EmptyMappingTable() { + this ( (List) null ); + } + /** + * Construct empty mapping table with entries (ignored). + * @param entries list of entries (ignored) + */ + public EmptyMappingTable ( List entries ) { + } + /** {@inheritDoc} */ + public int getType() { + return GLYPH_MAPPING_TYPE_EMPTY; + } + /** {@inheritDoc} */ + public List getEntries() { + return new java.util.ArrayList(); + } + /** {@inheritDoc} */ + public int getMappingSize() { + return 0; + } + /** {@inheritDoc} */ + public int getMappedIndex ( int gid ) { + return -1; + } + } + + /** mapped mapping table base class */ + protected static class MappedMappingTable extends GlyphMappingTable { + /** + * Construct mapped mapping table. + */ + public MappedMappingTable() { + } + /** {@inheritDoc} */ + public int getType() { + return GLYPH_MAPPING_TYPE_MAPPED; + } + } + + /** range mapping table base class */ + protected abstract static class RangeMappingTable extends GlyphMappingTable { + private int[] sa = null; // array of range (inclusive) starts + private int[] ea = null; // array of range (inclusive) ends + private int[] ma = null; // array of range mapped values + private int miMax = -1; + /** + * Construct range mapping table. + * @param entries of mapping ranges + */ + public RangeMappingTable ( List entries ) { + populate ( entries ); + } + /** {@inheritDoc} */ + public int getType() { + return GLYPH_MAPPING_TYPE_RANGE; + } + /** {@inheritDoc} */ + public List getEntries() { + List entries = new java.util.ArrayList(); + if ( sa != null ) { + for ( int i = 0, n = sa.length; i < n; i++ ) { + entries.add ( new MappingRange ( sa [ i ], ea [ i ], ma [ i ] ) ); + } + } + return entries; + } + /** {@inheritDoc} */ + public int getMappingSize() { + return miMax + 1; + } + /** {@inheritDoc} */ + public int getMappedIndex ( int gid ) { + int i, mi; + if ( ( i = Arrays.binarySearch ( sa, gid ) ) >= 0 ) { + mi = getMappedIndex ( gid, sa [ i ], ma [ i ] ); // matches start of (some) range + } else if ( ( i = - ( i + 1 ) ) == 0 ) { + mi = -1; // precedes first range + } else if ( gid > ea [ --i ] ) { + mi = -1; // follows preceding (or last) range + } else { + mi = getMappedIndex ( gid, sa [ i ], ma [ i ] ); // intersects (some) range + } + return mi; + } + /** + * Map glyph identifier (code) to coverge index. Returns -1 if glyph identifier is not in the domain of + * the mapping table. + * @param gid glyph identifier (code) + * @param s start of range + * @param m mapping value + * @return non-negative glyph mapping index or -1 if glyph identifiers is not mapped by table + */ + public abstract int getMappedIndex ( int gid, int s, int m ); + private void populate ( List entries ) { + int i = 0, n = entries.size(), gidMax = -1, miMax = -1; + int[] sa = new int [ n ]; + int[] ea = new int [ n ]; + int[] ma = new int [ n ]; + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( o instanceof MappingRange ) { + MappingRange r = (MappingRange) o; + int gs = r.getStart(); + int ge = r.getEnd(); + int mi = r.getIndex(); + if ( ( gs < 0 ) || ( gs > 65535 ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal glyph range: [" + gs + "," + ge + "]: bad start index" ); + } else if ( ( ge < 0 ) || ( ge > 65535 ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal glyph range: [" + gs + "," + ge + "]: bad end index" ); + } else if ( gs > ge ) { + throw new AdvancedTypographicTableFormatException ( "illegal glyph range: [" + gs + "," + ge + "]: start index exceeds end index" ); + } else if ( gs < gidMax ) { + throw new AdvancedTypographicTableFormatException ( "out of order glyph range: [" + gs + "," + ge + "]" ); + } else if ( mi < 0 ) { + throw new AdvancedTypographicTableFormatException ( "illegal mapping index: " + mi ); + } else { + int miLast; + sa [ i ] = gs; + ea [ i ] = gidMax = ge; + ma [ i ] = mi; + if ( ( miLast = mi + ( ge - gs ) ) > miMax ) { + miMax = miLast; + } + i++; + } + } else { + throw new AdvancedTypographicTableFormatException ( "illegal mapping entry, must be Integer: " + o ); + } + } + assert i == n; + assert this.sa == null; + assert this.ea == null; + assert this.ma == null; + this.sa = sa; + this.ea = ea; + this.ma = ma; + this.miMax = miMax; + } + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append('{'); + for ( int i = 0, n = sa.length; i < n; i++ ) { + if ( i > 0 ) { + sb.append(','); + } + sb.append ( '[' ); + sb.append ( Integer.toString ( sa [ i ] ) ); + sb.append ( Integer.toString ( ea [ i ] ) ); + sb.append ( "]:" ); + sb.append ( Integer.toString ( ma [ i ] ) ); + } + sb.append('}'); + return sb.toString(); + } + } + + /** + * The <code>MappingRange</code> class encapsulates a glyph [start,end] range and + * a mapping index. + */ + public static class MappingRange { + + private final int gidStart; // first glyph in range (inclusive) + private final int gidEnd; // last glyph in range (inclusive) + private final int index; // mapping index; + + /** + * Instantiate a mapping range. + */ + public MappingRange() { + this ( 0, 0, 0 ); + } + + /** + * Instantiate a specific mapping range. + * @param gidStart start of range + * @param gidEnd end of range + * @param index mapping index + */ + public MappingRange ( int gidStart, int gidEnd, int index ) { + if ( ( gidStart < 0 ) || ( gidEnd < 0 ) || ( index < 0 ) ) { + throw new AdvancedTypographicTableFormatException(); + } else if ( gidStart > gidEnd ) { + throw new AdvancedTypographicTableFormatException(); + } else { + this.gidStart = gidStart; + this.gidEnd = gidEnd; + this.index = index; + } + } + + /** @return start of range */ + public int getStart() { + return gidStart; + } + + /** @return end of range */ + public int getEnd() { + return gidEnd; + } + + /** @return mapping index */ + public int getIndex() { + return index; + } + + /** @return interval as a pair of integers */ + public int[] getInterval() { + return new int[] { gidStart, gidEnd }; + } + + /** + * Obtain interval, filled into first two elements of specified array, or returning new array. + * @param interval an array of length two or greater or null + * @return interval as a pair of integers, filled into specified array + */ + public int[] getInterval ( int[] interval ) { + if ( ( interval == null ) || ( interval.length != 2 ) ) { + throw new IllegalArgumentException(); + } else { + interval[0] = gidStart; + interval[1] = gidEnd; + } + return interval; + } + + /** @return length of interval */ + public int getLength() { + return gidStart - gidEnd; + } + + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioning.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioning.java new file mode 100644 index 000000000..92fae7506 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioning.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +// CSOFF: LineLengthCheck + +/** + * The <code>GlyphPositioning</code> interface is implemented by a glyph positioning subtable + * that supports the determination of glyph positioning information based on script and + * language of the corresponding character content. + * @author Glenn Adams + */ +public interface GlyphPositioning { + + /** + * Perform glyph positioning at the current index, mutating the positioning state object as required. + * Only the context associated with the current index is processed. + * @param ps glyph positioning state object + * @return true if the glyph subtable applies, meaning that the current context matches the + * associated input context glyph coverage table; note that returning true does not mean any position + * adjustment occurred; it only means that no further glyph subtables for the current lookup table + * should be applied. + */ + boolean position ( GlyphPositioningState ps ); + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioningState.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioningState.java new file mode 100644 index 000000000..ea62a7239 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioningState.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: LineLengthCheck +// CSOFF: ParameterNumberCheck + +/** + * The <code>GlyphPositioningState</code> implements an state object used during glyph positioning + * processing. + * @author Glenn Adams + */ + +public class GlyphPositioningState extends GlyphProcessingState { + + /** font size */ + private int fontSize; + /** default advancements */ + private int[] widths; + /** current adjustments */ + private int[][] adjustments; + /** if true, then some adjustment was applied */ + private boolean adjusted; + + /** + * Construct glyph positioning state. + * @param gs input glyph sequence + * @param script script identifier + * @param language language identifier + * @param feature feature identifier + * @param fontSize font size (in micropoints) + * @param widths array of design advancements (in glyph index order) + * @param adjustments positioning adjustments to which positioning is applied + * @param sct script context tester (or null) + */ + public GlyphPositioningState ( GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct ) { + super ( gs, script, language, feature, sct ); + this.fontSize = fontSize; + this.widths = widths; + this.adjustments = adjustments; + } + + /** + * Construct glyph positioning state using an existing state object using shallow copy + * except as follows: input glyph sequence is copied deep except for its characters array. + * @param ps existing positioning state to copy from + */ + public GlyphPositioningState ( GlyphPositioningState ps ) { + super ( ps ); + this.fontSize = ps.fontSize; + this.widths = ps.widths; + this.adjustments = ps.adjustments; + } + + /** + * Obtain design advancement (width) of glyph at specified index. + * @param gi glyph index + * @return design advancement, or zero if glyph index is not present + */ + public int getWidth ( int gi ) { + if ( ( widths != null ) && ( gi < widths.length ) ) { + return widths [ gi ]; + } else { + return 0; + } + } + + /** + * Perform adjustments at current position index. + * @param v value containing adjustments + * @return true if a non-zero adjustment was made + */ + public boolean adjust ( GlyphPositioningTable.Value v ) { + return adjust ( v, 0 ); + } + + /** + * Perform adjustments at specified offset from current position index. + * @param v value containing adjustments + * @param offset from current position index + * @return true if a non-zero adjustment was made + */ + public boolean adjust ( GlyphPositioningTable.Value v, int offset ) { + assert v != null; + if ( ( index + offset ) < indexLast ) { + return v.adjust ( adjustments [ index + offset ], fontSize ); + } else { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Obtain current adjustments at current position index. + * @return array of adjustments (int[4]) at current position + */ + public int[] getAdjustment() { + return getAdjustment ( 0 ); + } + + /** + * Obtain current adjustments at specified offset from current position index. + * @param offset from current position index + * @return array of adjustments (int[4]) at specified offset + * @throws IndexOutOfBoundsException if offset is invalid + */ + public int[] getAdjustment ( int offset ) throws IndexOutOfBoundsException { + if ( ( index + offset ) < indexLast ) { + return adjustments [ index + offset ]; + } else { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Apply positioning subtable to current state at current position (only), + * resulting in the consumption of zero or more input glyphs. + * @param st the glyph positioning subtable to apply + * @return true if subtable applied, or false if it did not (e.g., its + * input coverage table did not match current input context) + */ + public boolean apply ( GlyphPositioningSubtable st ) { + assert st != null; + updateSubtableState ( st ); + boolean applied = st.position ( this ); + resetSubtableState(); + return applied; + } + + /** + * Apply a sequence of matched rule lookups to the <code>nig</code> input glyphs + * starting at the current position. If lookups are non-null and non-empty, then + * all input glyphs specified by <code>nig</code> are consumed irregardless of + * whether any specified lookup applied. + * @param lookups array of matched lookups (or null) + * @param nig number of glyphs in input sequence, starting at current position, to which + * the lookups are to apply, and to be consumed once the application has finished + * @return true if lookups are non-null and non-empty; otherwise, false + */ + public boolean apply ( GlyphTable.RuleLookup[] lookups, int nig ) { + if ( ( lookups != null ) && ( lookups.length > 0 ) ) { + // apply each rule lookup to extracted input glyph array + for ( int i = 0, n = lookups.length; i < n; i++ ) { + GlyphTable.RuleLookup l = lookups [ i ]; + if ( l != null ) { + GlyphTable.LookupTable lt = l.getLookup(); + if ( lt != null ) { + // perform positioning on a copy of previous state + GlyphPositioningState ps = new GlyphPositioningState ( this ); + // apply lookup table positioning + if ( lt.position ( ps, l.getSequenceIndex() ) ) { + setAdjusted ( true ); + } + } + } + } + consume ( nig ); + return true; + } else { + return false; + } + } + + /** + * Apply default application semantices; namely, consume one input glyph. + */ + public void applyDefault() { + super.applyDefault(); + } + + /** + * Set adjusted state, used to record effect of non-zero adjustment. + * @param adjusted true if to set adjusted state, otherwise false to + * clear adjusted state + */ + public void setAdjusted ( boolean adjusted ) { + this.adjusted = adjusted; + } + + /** + * Get adjusted state. + * @return adjusted true if some non-zero adjustment occurred and + * was recorded by {@link #setAdjusted}; otherwise, false. + */ + public boolean getAdjusted() { + return adjusted; + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioningSubtable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioningSubtable.java new file mode 100644 index 000000000..4325a3547 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioningSubtable.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.util.List; + +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: ParameterNumberCheck + +/** + * The <code>GlyphPositioningSubtable</code> implements an abstract base of a glyph subtable, + * providing a default implementation of the <code>GlyphPositioning</code> interface. + * @author Glenn Adams + */ +public abstract class GlyphPositioningSubtable extends GlyphSubtable implements GlyphPositioning { + + /** + * Instantiate a <code>GlyphPositioningSubtable</code>. + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage subtable coverage table + */ + protected GlyphPositioningSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage ) { + super ( id, sequence, flags, format, coverage ); + } + + /** {@inheritDoc} */ + public int getTableType() { + return GlyphTable.GLYPH_TABLE_TYPE_POSITIONING; + } + + /** {@inheritDoc} */ + public String getTypeName() { + return GlyphPositioningTable.getLookupTypeName ( getType() ); + } + + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof GlyphPositioningSubtable; + } + + /** {@inheritDoc} */ + public boolean usesReverseScan() { + return false; + } + + /** {@inheritDoc} */ + public boolean position ( GlyphPositioningState ps ) { + return false; + } + + /** + * Apply positioning using specified state and subtable array. For each position in input sequence, + * apply subtables in order until some subtable applies or none remain. If no subtable applied or no + * input was consumed for a given position, then apply default action (no adjustments and advance). + * If <code>sequenceIndex</code> is non-negative, then apply subtables only when current position + * matches <code>sequenceIndex</code> in relation to the starting position. Furthermore, upon + * successful application at <code>sequenceIndex</code>, then discontinue processing the remaining + * @param ps positioning state + * @param sta array of subtables to apply + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return true if a non-zero adjustment occurred + */ + public static final boolean position ( GlyphPositioningState ps, GlyphPositioningSubtable[] sta, int sequenceIndex ) { + int sequenceStart = ps.getPosition(); + boolean appliedOneShot = false; + while ( ps.hasNext() ) { + boolean applied = false; + if ( ! appliedOneShot && ps.maybeApplicable() ) { + for ( int i = 0, n = sta.length; ! applied && ( i < n ); i++ ) { + if ( sequenceIndex < 0 ) { + applied = ps.apply ( sta [ i ] ); + } else if ( ps.getPosition() == ( sequenceStart + sequenceIndex ) ) { + applied = ps.apply ( sta [ i ] ); + if ( applied ) { + appliedOneShot = true; + } + } + } + } + if ( ! applied || ! ps.didConsume() ) { + ps.applyDefault(); + } + ps.next(); + } + return ps.getAdjusted(); + } + + /** + * Apply positioning. + * @param gs input glyph sequence + * @param script tag + * @param language tag + * @param feature tag + * @param fontSize the font size + * @param sta subtable array + * @param widths array + * @param adjustments array (receives output adjustments) + * @param sct script context tester + * @return true if a non-zero adjustment occurred + */ + public static final boolean position ( GlyphSequence gs, String script, String language, String feature, int fontSize, GlyphPositioningSubtable[] sta, int[] widths, int[][] adjustments, ScriptContextTester sct ) { + return position ( new GlyphPositioningState ( gs, script, language, feature, fontSize, widths, adjustments, sct ), sta, -1 ); + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioningTable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioningTable.java new file mode 100644 index 000000000..f8ac4e504 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphPositioningTable.java @@ -0,0 +1,2264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.scripts.ScriptProcessor; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.GlyphTester; + +// CSOFF: LineLengthCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: ParameterNumberCheck + +/** + * The <code>GlyphPositioningTable</code> class is a glyph table that implements + * <code>GlyphPositioning</code> functionality. + * @author Glenn Adams + */ +public class GlyphPositioningTable extends GlyphTable { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GlyphPositioningTable.class); // CSOK: ConstantNameCheck + + /** single positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_SINGLE = 1; + /** multiple positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_PAIR = 2; + /** cursive positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_CURSIVE = 3; + /** mark to base positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_MARK_TO_BASE = 4; + /** mark to ligature positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE = 5; + /** mark to mark positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_MARK_TO_MARK = 6; + /** contextual positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_CONTEXTUAL = 7; + /** chained contextual positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL = 8; + /** extension positioning subtable type */ + public static final int GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING = 9; + + /** + * Instantiate a <code>GlyphPositioningTable</code> object using the specified lookups + * and subtables. + * @param gdef glyph definition table that applies + * @param lookups a map of lookup specifications to subtable identifier strings + * @param subtables a list of identified subtables + */ + public GlyphPositioningTable ( GlyphDefinitionTable gdef, Map lookups, List subtables ) { + super ( gdef, lookups ); + if ( ( subtables == null ) || ( subtables.size() == 0 ) ) { + throw new AdvancedTypographicTableFormatException ( "subtables must be non-empty" ); + } else { + for ( Iterator it = subtables.iterator(); it.hasNext();) { + Object o = it.next(); + if ( o instanceof GlyphPositioningSubtable ) { + addSubtable ( (GlyphSubtable) o ); + } else { + throw new AdvancedTypographicTableFormatException ( "subtable must be a glyph positioning subtable" ); + } + } + freezeSubtables(); + } + } + + /** + * Map a lookup type name to its constant (integer) value. + * @param name lookup type name + * @return lookup type + */ + public static int getLookupTypeFromName ( String name ) { + int t; + String s = name.toLowerCase(); + if ( "single".equals ( s ) ) { + t = GPOS_LOOKUP_TYPE_SINGLE; + } else if ( "pair".equals ( s ) ) { + t = GPOS_LOOKUP_TYPE_PAIR; + } else if ( "cursive".equals ( s ) ) { + t = GPOS_LOOKUP_TYPE_CURSIVE; + } else if ( "marktobase".equals ( s ) ) { + t = GPOS_LOOKUP_TYPE_MARK_TO_BASE; + } else if ( "marktoligature".equals ( s ) ) { + t = GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE; + } else if ( "marktomark".equals ( s ) ) { + t = GPOS_LOOKUP_TYPE_MARK_TO_MARK; + } else if ( "contextual".equals ( s ) ) { + t = GPOS_LOOKUP_TYPE_CONTEXTUAL; + } else if ( "chainedcontextual".equals ( s ) ) { + t = GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + } else if ( "extensionpositioning".equals ( s ) ) { + t = GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING; + } else { + t = -1; + } + return t; + } + + /** + * Map a lookup type constant (integer) value to its name. + * @param type lookup type + * @return lookup type name + */ + public static String getLookupTypeName ( int type ) { + String tn; + switch ( type ) { + case GPOS_LOOKUP_TYPE_SINGLE: + tn = "single"; + break; + case GPOS_LOOKUP_TYPE_PAIR: + tn = "pair"; + break; + case GPOS_LOOKUP_TYPE_CURSIVE: + tn = "cursive"; + break; + case GPOS_LOOKUP_TYPE_MARK_TO_BASE: + tn = "marktobase"; + break; + case GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE: + tn = "marktoligature"; + break; + case GPOS_LOOKUP_TYPE_MARK_TO_MARK: + tn = "marktomark"; + break; + case GPOS_LOOKUP_TYPE_CONTEXTUAL: + tn = "contextual"; + break; + case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL: + tn = "chainedcontextual"; + break; + case GPOS_LOOKUP_TYPE_EXTENSION_POSITIONING: + tn = "extensionpositioning"; + break; + default: + tn = "unknown"; + break; + } + return tn; + } + + /** + * Create a positioning subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage subtable coverage table + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + GlyphSubtable st = null; + switch ( type ) { + case GPOS_LOOKUP_TYPE_SINGLE: + st = SingleSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GPOS_LOOKUP_TYPE_PAIR: + st = PairSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GPOS_LOOKUP_TYPE_CURSIVE: + st = CursiveSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GPOS_LOOKUP_TYPE_MARK_TO_BASE: + st = MarkToBaseSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE: + st = MarkToLigatureSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GPOS_LOOKUP_TYPE_MARK_TO_MARK: + st = MarkToMarkSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GPOS_LOOKUP_TYPE_CONTEXTUAL: + st = ContextualSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL: + st = ChainedContextualSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + default: + break; + } + return st; + } + + /** + * Create a positioning subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage list of coverage table entries + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, List coverage, List entries ) { + return createSubtable ( type, id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ), entries ); + } + + /** + * Perform positioning processing using all matching lookups. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param fontSize size in device units + * @param widths array of default advancements for each glyph + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position ( GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments ) { + Map/*<LookupSpec,List<LookupTable>>*/ lookups = matchLookups ( script, language, "*" ); + if ( ( lookups != null ) && ( lookups.size() > 0 ) ) { + ScriptProcessor sp = ScriptProcessor.getInstance ( script ); + return sp.position ( this, gs, script, language, fontSize, lookups, widths, adjustments ); + } else { + return false; + } + } + + private abstract static class SingleSubtable extends GlyphPositioningSubtable { + SingleSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GPOS_LOOKUP_TYPE_SINGLE; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof SingleSubtable; + } + /** {@inheritDoc} */ + public boolean position ( GlyphPositioningState ps ) { + int gi = ps.getGlyph(), ci; + if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) { + return false; + } else { + Value v = getValue ( ci, gi ); + if ( v != null ) { + if ( ps.adjust(v) ) { + ps.setAdjusted ( true ); + } + ps.consume(1); + } + return true; + } + } + /** + * Obtain positioning value for coverage index. + * @param ci coverage index + * @param gi input glyph index + * @return positioning value or null if none applies + */ + public abstract Value getValue ( int ci, int gi ); + static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new SingleSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 2 ) { + return new SingleSubtableFormat2 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class SingleSubtableFormat1 extends SingleSubtable { + private Value value; + private int ciMax; + SingleSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( value != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( value ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public Value getValue ( int ci, int gi ) { + if ( ( value != null ) && ( ci <= ciMax ) ) { + return value; + } else { + return null; + } + } + private void populate ( List entries ) { + if ( ( entries == null ) || ( entries.size() != 1 ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null and contain exactly one entry" ); + } else { + Value v; + Object o = entries.get(0); + if ( o instanceof Value ) { + v = (Value) o; + } else { + throw new AdvancedTypographicTableFormatException ( "illegal entries entry, must be Value, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } + assert this.value == null; + this.value = v; + this.ciMax = getCoverageSize() - 1; + } + } + } + + private static class SingleSubtableFormat2 extends SingleSubtable { + private Value[] values; + SingleSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( values != null ) { + List entries = new ArrayList ( values.length ); + for ( int i = 0, n = values.length; i < n; i++ ) { + entries.add ( values[i] ); + } + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public Value getValue ( int ci, int gi ) { + if ( ( values != null ) && ( ci < values.length ) ) { + return values [ ci ]; + } else { + return null; + } + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof Value[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, single entry must be a Value[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + Value[] va = (Value[]) o; + if ( va.length != getCoverageSize() ) { + throw new AdvancedTypographicTableFormatException ( "illegal values array, " + entries.size() + " values present, but requires " + getCoverageSize() + " values" ); + } else { + assert this.values == null; + this.values = va; + } + } + } + } + } + + private abstract static class PairSubtable extends GlyphPositioningSubtable { + PairSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GPOS_LOOKUP_TYPE_PAIR; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof PairSubtable; + } + /** {@inheritDoc} */ + public boolean position ( GlyphPositioningState ps ) { + boolean applied = false; + int gi = ps.getGlyph(0), ci; + if ( ( ci = getCoverageIndex ( gi ) ) >= 0 ) { + int[] counts = ps.getGlyphsAvailable ( 0 ); + int nga = counts[0]; + if ( nga > 1 ) { + int[] iga = ps.getGlyphs ( 0, 2, null, counts ); + if ( ( iga != null ) && ( iga.length == 2 ) ) { + PairValues pv = getPairValues ( ci, iga[0], iga[1] ); + if ( pv != null ) { + Value v1 = pv.getValue1(); + if ( v1 != null ) { + if ( ps.adjust(v1, 0) ) { + ps.setAdjusted ( true ); + } + } + Value v2 = pv.getValue2(); + if ( v2 != null ) { + if ( ps.adjust(v2, 1) ) { + ps.setAdjusted ( true ); + } + } + ps.consume ( counts[0] + counts[1] ); + applied = true; + } + } + } + } + return applied; + } + /** + * Obtain associated pair values. + * @param ci coverage index + * @param gi1 first input glyph index + * @param gi2 second input glyph index + * @return pair values or null if none applies + */ + public abstract PairValues getPairValues ( int ci, int gi1, int gi2 ); + static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new PairSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 2 ) { + return new PairSubtableFormat2 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class PairSubtableFormat1 extends PairSubtable { + private PairValues[][] pvm; // pair values matrix + PairSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( pvm != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( pvm ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public PairValues getPairValues ( int ci, int gi1, int gi2 ) { + if ( ( pvm != null ) && ( ci < pvm.length ) ) { + PairValues[] pvt = pvm [ ci ]; + for ( int i = 0, n = pvt.length; i < n; i++ ) { + PairValues pv = pvt [ i ]; + if ( pv != null ) { + int g = pv.getGlyph(); + if ( g < gi2 ) { + continue; + } else if ( g == gi2 ) { + return pv; + } else { + break; + } + } + } + } + return null; + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof PairValues[][] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first (and only) entry must be a PairValues[][], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + pvm = (PairValues[][]) o; + } + } + } + } + + private static class PairSubtableFormat2 extends PairSubtable { + private GlyphClassTable cdt1; // class def table 1 + private GlyphClassTable cdt2; // class def table 2 + private int nc1; // class 1 count + private int nc2; // class 2 count + private PairValues[][] pvm; // pair values matrix + PairSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( pvm != null ) { + List entries = new ArrayList ( 5 ); + entries.add ( cdt1 ); + entries.add ( cdt2 ); + entries.add ( Integer.valueOf ( nc1 ) ); + entries.add ( Integer.valueOf ( nc2 ) ); + entries.add ( pvm ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public PairValues getPairValues ( int ci, int gi1, int gi2 ) { + if ( pvm != null ) { + int c1 = cdt1.getClassIndex ( gi1, 0 ); + if ( ( c1 >= 0 ) && ( c1 < nc1 ) && ( c1 < pvm.length ) ) { + PairValues[] pvt = pvm [ c1 ]; + if ( pvt != null ) { + int c2 = cdt2.getClassIndex ( gi2, 0 ); + if ( ( c2 >= 0 ) && ( c2 < nc2 ) && ( c2 < pvt.length ) ) { + return pvt [ c2 ]; + } + } + } + } + return null; + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 5 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 5 entries" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + cdt1 = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, second entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + cdt2 = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, third entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + nc1 = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fourth entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + nc2 = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(4) ) == null ) || ! ( o instanceof PairValues[][] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fifth entry must be a PairValues[][], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + pvm = (PairValues[][]) o; + } + } + } + } + + private abstract static class CursiveSubtable extends GlyphPositioningSubtable { + CursiveSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GPOS_LOOKUP_TYPE_CURSIVE; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof CursiveSubtable; + } + /** {@inheritDoc} */ + public boolean position ( GlyphPositioningState ps ) { + boolean applied = false; + int gi = ps.getGlyph(0), ci; + if ( ( ci = getCoverageIndex ( gi ) ) >= 0 ) { + int[] counts = ps.getGlyphsAvailable ( 0 ); + int nga = counts[0]; + if ( nga > 1 ) { + int[] iga = ps.getGlyphs ( 0, 2, null, counts ); + if ( ( iga != null ) && ( iga.length == 2 ) ) { + // int gi1 = gi; + int ci1 = ci; + int gi2 = iga [ 1 ]; + int ci2 = getCoverageIndex ( gi2 ); + Anchor[] aa = getExitEntryAnchors ( ci1, ci2 ); + if ( aa != null ) { + Anchor exa = aa [ 0 ]; + Anchor ena = aa [ 1 ]; + // int exw = ps.getWidth ( gi1 ); + int enw = ps.getWidth ( gi2 ); + if ( ( exa != null ) && ( ena != null ) ) { + Value v = ena.getAlignmentAdjustment ( exa ); + v.adjust ( - enw, 0, 0, 0 ); + if ( ps.adjust ( v ) ) { + ps.setAdjusted ( true ); + } + } + // consume only first glyph of exit/entry glyph pair + ps.consume ( 1 ); + applied = true; + } + } + } + } + return applied; + } + /** + * Obtain exit anchor for first glyph with coverage index <code>ci1</code> and entry anchor for second + * glyph with coverage index <code>ci2</code>. + * @param ci1 coverage index of first glyph (may be negative) + * @param ci2 coverage index of second glyph (may be negative) + * @return array of two anchors or null if either coverage index is negative or corresponding anchor is + * missing, where the first entry is the exit anchor of the first glyph and the second entry is the + * entry anchor of the second glyph + */ + public abstract Anchor[] getExitEntryAnchors ( int ci1, int ci2 ); + static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new CursiveSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class CursiveSubtableFormat1 extends CursiveSubtable { + 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 ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( aa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( aa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public Anchor[] getExitEntryAnchors ( int ci1, int ci2 ) { + if ( ( ci1 >= 0 ) && ( ci2 >= 0 ) ) { + int ai1 = ( ci1 * 2 ) + 1; // ci1 denotes glyph with exit anchor + int ai2 = ( ci2 * 2 ) + 0; // ci2 denotes glyph with entry anchor + if ( ( aa != null ) && ( ai1 < aa.length ) && ( ai2 < aa.length ) ) { + Anchor exa = aa [ ai1 ]; + Anchor ena = aa [ ai2 ]; + if ( ( exa != null ) && ( ena != null ) ) { + return new Anchor[] { exa, ena }; + } + } + } + return null; + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof Anchor[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first (and only) entry must be a Anchor[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else if ( ( ( (Anchor[]) o ) . length % 2 ) != 0 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, Anchor[] array must have an even number of entries, but has: " + ( (Anchor[]) o ) . length ); + } else { + aa = (Anchor[]) o; + } + } + } + } + + private abstract static class MarkToBaseSubtable extends GlyphPositioningSubtable { + MarkToBaseSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GPOS_LOOKUP_TYPE_MARK_TO_BASE; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof MarkToBaseSubtable; + } + /** {@inheritDoc} */ + public boolean position ( GlyphPositioningState ps ) { + boolean applied = false; + int giMark = ps.getGlyph(), ciMark; + if ( ( ciMark = getCoverageIndex ( giMark ) ) >= 0 ) { + MarkAnchor ma = getMarkAnchor ( ciMark, giMark ); + if ( ma != null ) { + for ( int i = 0, n = ps.getPosition(); i < n; i++ ) { + int gi = ps.getGlyph ( - ( i + 1 ) ); + if ( ps.isMark ( gi ) ) { + continue; + } else { + Anchor a = getBaseAnchor ( gi, ma.getMarkClass() ); + if ( a != null ) { + Value v = a.getAlignmentAdjustment ( ma ); + // start experimental fix for END OF AYAH in Lateef/Scheherazade + int[] aa = ps.getAdjustment(); + if ( aa[2] == 0 ) { + v.adjust ( 0, 0, - ps.getWidth ( giMark ), 0 ); + } + // end experimental fix for END OF AYAH in Lateef/Scheherazade + if ( ps.adjust ( v ) ) { + ps.setAdjusted ( true ); + } + } + ps.consume(1); + applied = true; + break; + } + } + } + } + return applied; + } + /** + * Obtain mark anchor associated with mark coverage index. + * @param ciMark coverage index + * @param giMark input glyph index of mark glyph + * @return mark anchor or null if none applies + */ + public abstract MarkAnchor getMarkAnchor ( int ciMark, int giMark ); + /** + * Obtain anchor associated with base glyph index and mark class. + * @param giBase input glyph index of base glyph + * @param markClass class number of mark glyph + * @return anchor or null if none applies + */ + public abstract Anchor getBaseAnchor ( int giBase, int markClass ); + static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new MarkToBaseSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MarkToBaseSubtableFormat1 extends MarkToBaseSubtable { + private GlyphCoverageTable bct; // base coverage table + private int nmc; // mark class count + private MarkAnchor[] maa; // mark anchor array, ordered by mark coverage index + 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 ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( ( bct != null ) && ( maa != null ) && ( nmc > 0 ) && ( bam != null ) ) { + List entries = new ArrayList ( 4 ); + entries.add ( bct ); + entries.add ( Integer.valueOf ( nmc ) ); + entries.add ( maa ); + entries.add ( bam ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public MarkAnchor getMarkAnchor ( int ciMark, int giMark ) { + if ( ( maa != null ) && ( ciMark < maa.length ) ) { + return maa [ ciMark ]; + } else { + return null; + } + } + /** {@inheritDoc} */ + public Anchor getBaseAnchor ( int giBase, int markClass ) { + int ciBase; + if ( ( bct != null ) && ( ( ciBase = bct.getCoverageIndex ( giBase ) ) >= 0 ) ) { + if ( ( bam != null ) && ( ciBase < bam.length ) ) { + Anchor[] ba = bam [ ciBase ]; + if ( ( ba != null ) && ( markClass < ba.length ) ) { + return ba [ markClass ]; + } + } + } + return null; + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 4 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 4 entries" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphCoverageTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an GlyphCoverageTable, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + bct = (GlyphCoverageTable) o; + } + if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + nmc = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof MarkAnchor[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, third entry must be a MarkAnchor[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + maa = (MarkAnchor[]) o; + } + if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Anchor[][] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fourth entry must be a Anchor[][], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + bam = (Anchor[][]) o; + } + } + } + } + + private abstract static class MarkToLigatureSubtable extends GlyphPositioningSubtable { + MarkToLigatureSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GPOS_LOOKUP_TYPE_MARK_TO_LIGATURE; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof MarkToLigatureSubtable; + } + /** {@inheritDoc} */ + public boolean position ( GlyphPositioningState ps ) { + boolean applied = false; + int giMark = ps.getGlyph(), ciMark; + if ( ( ciMark = getCoverageIndex ( giMark ) ) >= 0 ) { + MarkAnchor ma = getMarkAnchor ( ciMark, giMark ); + int mxc = getMaxComponentCount(); + if ( ma != null ) { + for ( int i = 0, n = ps.getPosition(); i < n; i++ ) { + int gi = ps.getGlyph ( - ( i + 1 ) ); + if ( ps.isMark ( gi ) ) { + continue; + } else { + Anchor a = getLigatureAnchor ( gi, mxc, i, ma.getMarkClass() ); + if ( a != null ) { + if ( ps.adjust ( a.getAlignmentAdjustment ( ma ) ) ) { + ps.setAdjusted ( true ); + } + } + ps.consume(1); + applied = true; + break; + } + } + } + } + return applied; + } + /** + * Obtain mark anchor associated with mark coverage index. + * @param ciMark coverage index + * @param giMark input glyph index of mark glyph + * @return mark anchor or null if none applies + */ + public abstract MarkAnchor getMarkAnchor ( int ciMark, int giMark ); + /** + * Obtain maximum component count. + * @return maximum component count (>=0) + */ + public abstract int getMaxComponentCount(); + /** + * Obtain anchor associated with ligature glyph index and mark class. + * @param giLig input glyph index of ligature glyph + * @param maxComponents maximum component count + * @param component component number (0...maxComponents-1) + * @param markClass class number of mark glyph + * @return anchor or null if none applies + */ + public abstract Anchor getLigatureAnchor ( int giLig, int maxComponents, int component, int markClass ); + static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new MarkToLigatureSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MarkToLigatureSubtableFormat1 extends MarkToLigatureSubtable { + private GlyphCoverageTable lct; // ligature coverage table + private int nmc; // mark class count + private int mxc; // maximum ligature component count + private MarkAnchor[] maa; // mark anchor array, ordered by mark coverage index + 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 ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( lam != null ) { + List entries = new ArrayList ( 5 ); + entries.add ( lct ); + entries.add ( Integer.valueOf ( nmc ) ); + entries.add ( Integer.valueOf ( mxc ) ); + entries.add ( maa ); + entries.add ( lam ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public MarkAnchor getMarkAnchor ( int ciMark, int giMark ) { + if ( ( maa != null ) && ( ciMark < maa.length ) ) { + return maa [ ciMark ]; + } else { + return null; + } + } + /** {@inheritDoc} */ + public int getMaxComponentCount() { + return mxc; + } + /** {@inheritDoc} */ + public Anchor getLigatureAnchor ( int giLig, int maxComponents, int component, int markClass ) { + int ciLig; + if ( ( lct != null ) && ( ( ciLig = lct.getCoverageIndex ( giLig ) ) >= 0 ) ) { + if ( ( lam != null ) && ( ciLig < lam.length ) ) { + Anchor[][] lcm = lam [ ciLig ]; + if ( component < maxComponents ) { + Anchor[] la = lcm [ component ]; + if ( ( la != null ) && ( markClass < la.length ) ) { + return la [ markClass ]; + } + } + } + } + return null; + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 5 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 5 entries" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphCoverageTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an GlyphCoverageTable, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + lct = (GlyphCoverageTable) o; + } + if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + nmc = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, third entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + mxc = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof MarkAnchor[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fourth entry must be a MarkAnchor[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + maa = (MarkAnchor[]) o; + } + if ( ( ( o = entries.get(4) ) == null ) || ! ( o instanceof Anchor[][][] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fifth entry must be a Anchor[][][], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + lam = (Anchor[][][]) o; + } + } + } + } + + private abstract static class MarkToMarkSubtable extends GlyphPositioningSubtable { + MarkToMarkSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GPOS_LOOKUP_TYPE_MARK_TO_MARK; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof MarkToMarkSubtable; + } + /** {@inheritDoc} */ + public boolean position ( GlyphPositioningState ps ) { + boolean applied = false; + int giMark1 = ps.getGlyph(), ciMark1; + if ( ( ciMark1 = getCoverageIndex ( giMark1 ) ) >= 0 ) { + MarkAnchor ma = getMark1Anchor ( ciMark1, giMark1 ); + if ( ma != null ) { + if ( ps.hasPrev() ) { + Anchor a = getMark2Anchor ( ps.getGlyph(-1), ma.getMarkClass() ); + if ( a != null ) { + if ( ps.adjust ( a.getAlignmentAdjustment ( ma ) ) ) { + ps.setAdjusted ( true ); + } + } + ps.consume(1); + applied = true; + } + } + } + return applied; + } + /** + * Obtain mark 1 anchor associated with mark 1 coverage index. + * @param ciMark1 mark 1 coverage index + * @param giMark1 input glyph index of mark 1 glyph + * @return mark 1 anchor or null if none applies + */ + public abstract MarkAnchor getMark1Anchor ( int ciMark1, int giMark1 ); + /** + * Obtain anchor associated with mark 2 glyph index and mark 1 class. + * @param giMark2 input glyph index of mark 2 glyph + * @param markClass class number of mark 1 glyph + * @return anchor or null if none applies + */ + public abstract Anchor getMark2Anchor ( int giBase, int markClass ); + static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new MarkToMarkSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MarkToMarkSubtableFormat1 extends MarkToMarkSubtable { + private GlyphCoverageTable mct2; // mark 2 coverage table + private int nmc; // mark class count + private MarkAnchor[] maa; // mark1 anchor array, ordered by mark1 coverage index + 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 ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( ( mct2 != null ) && ( maa != null ) && ( nmc > 0 ) && ( mam != null ) ) { + List entries = new ArrayList ( 4 ); + entries.add ( mct2 ); + entries.add ( Integer.valueOf ( nmc ) ); + entries.add ( maa ); + entries.add ( mam ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public MarkAnchor getMark1Anchor ( int ciMark1, int giMark1 ) { + if ( ( maa != null ) && ( ciMark1 < maa.length ) ) { + return maa [ ciMark1 ]; + } else { + return null; + } + } + /** {@inheritDoc} */ + public Anchor getMark2Anchor ( int giMark2, int markClass ) { + int ciMark2; + if ( ( mct2 != null ) && ( ( ciMark2 = mct2.getCoverageIndex ( giMark2 ) ) >= 0 ) ) { + if ( ( mam != null ) && ( ciMark2 < mam.length ) ) { + Anchor[] ma = mam [ ciMark2 ]; + if ( ( ma != null ) && ( markClass < ma.length ) ) { + return ma [ markClass ]; + } + } + } + return null; + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 4 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 4 entries" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphCoverageTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an GlyphCoverageTable, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + mct2 = (GlyphCoverageTable) o; + } + if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + nmc = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof MarkAnchor[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, third entry must be a MarkAnchor[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + maa = (MarkAnchor[]) o; + } + if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Anchor[][] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fourth entry must be a Anchor[][], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + mam = (Anchor[][]) o; + } + } + } + } + + private abstract static class ContextualSubtable extends GlyphPositioningSubtable { + ContextualSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GPOS_LOOKUP_TYPE_CONTEXTUAL; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof ContextualSubtable; + } + /** {@inheritDoc} */ + public boolean position ( GlyphPositioningState ps ) { + boolean applied = false; + int gi = ps.getGlyph(), ci; + if ( ( ci = getCoverageIndex ( gi ) ) >= 0 ) { + int[] rv = new int[1]; + RuleLookup[] la = getLookups ( ci, gi, ps, rv ); + if ( la != null ) { + ps.apply ( la, rv[0] ); + applied = true; + } + } + return applied; + } + /** + * Obtain rule lookups set associated current input glyph context. + * @param ci coverage index of glyph at current position + * @param gi glyph index of glyph at current position + * @param ps glyph positioning state + * @param rv array of ints used to receive multiple return values, must be of length 1 or greater, + * where the first entry is used to return the input sequence length of the matched rule + * @return array of rule lookups or null if none applies + */ + public abstract RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv ); + static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new ContextualSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 2 ) { + return new ContextualSubtableFormat2 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 3 ) { + return new ContextualSubtableFormat3 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ContextualSubtableFormat1 extends ContextualSubtable { + private RuleSet[] rsa; // rule set array, ordered by glyph coverage index + ContextualSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv ) { + assert ps != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedGlyphSequenceRule ) ) { + ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r; + int[] iga = cr.getGlyphs ( gi ); + if ( matches ( ps, iga, 0, rv ) ) { + return r.getLookups(); + } + } + } + } + } + return null; + } + static boolean matches ( GlyphPositioningState ps, int[] glyphs, int offset, int[] rv ) { + if ( ( glyphs == null ) || ( glyphs.length == 0 ) ) { + return true; // match null or empty glyph sequence + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ps.getIgnoreDefault(); + int[] counts = ps.getGlyphsAvailable ( offset, reverse, ignores ); + int nga = counts[0]; + int ngm = glyphs.length; + if ( nga < ngm ) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ps.getGlyphs ( offset, ngm, reverse, ignores, null, counts ); + for ( int k = 0; k < ngm; k++ ) { + if ( ga [ k ] != glyphs [ k ] ) { + return false; // match fails at ga [ k ] + } + } + if ( rv != null ) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + } + } + } + } + + private static class ContextualSubtableFormat2 extends ContextualSubtable { + private GlyphClassTable cdt; // class def table + private int ngc; // class set count + private RuleSet[] rsa; // rule set array, ordered by class number [0...ngc - 1] + ContextualSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 3 ); + entries.add ( cdt ); + entries.add ( Integer.valueOf ( ngc ) ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv ) { + assert ps != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedClassSequenceRule ) ) { + ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r; + int[] ca = cr.getClasses ( cdt.getClassIndex ( gi, ps.getClassMatchSet ( gi ) ) ); + if ( matches ( ps, cdt, ca, 0, rv ) ) { + return r.getLookups(); + } + } + } + } + } + return null; + } + static boolean matches ( GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv ) { + if ( ( cdt == null ) || ( classes == null ) || ( classes.length == 0 ) ) { + return true; // match null class definitions, null or empty class sequence + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ps.getIgnoreDefault(); + int[] counts = ps.getGlyphsAvailable ( offset, reverse, ignores ); + int nga = counts[0]; + int ngm = classes.length; + if ( nga < ngm ) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ps.getGlyphs ( offset, ngm, reverse, ignores, null, counts ); + for ( int k = 0; k < ngm; k++ ) { + int gi = ga [ k ]; + int ms = ps.getClassMatchSet ( gi ); + int gc = cdt.getClassIndex ( gi, ms ); + if ( ( gc < 0 ) || ( gc >= cdt.getClassSize ( ms ) ) ) { + return false; // none or invalid class fails mat ch + } else if ( gc != classes [ k ] ) { + return false; // match fails at ga [ k ] + } + } + if ( rv != null ) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 3 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 3 entries" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + cdt = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + ngc = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, third entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + if ( rsa.length != ngc ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes" ); + } + } + } + } + } + + private static class ContextualSubtableFormat3 extends ContextualSubtable { + private RuleSet[] rsa; // rule set array, containing a single rule set + ContextualSubtableFormat3 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv ) { + assert ps != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedCoverageSequenceRule ) ) { + ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r; + GlyphCoverageTable[] gca = cr.getCoverages(); + if ( matches ( ps, gca, 0, rv ) ) { + return r.getLookups(); + } + } + } + } + } + return null; + } + static boolean matches ( GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv ) { + if ( ( gca == null ) || ( gca.length == 0 ) ) { + return true; // match null or empty coverage array + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ps.getIgnoreDefault(); + int[] counts = ps.getGlyphsAvailable ( offset, reverse, ignores ); + int nga = counts[0]; + int ngm = gca.length; + if ( nga < ngm ) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ps.getGlyphs ( offset, ngm, reverse, ignores, null, counts ); + for ( int k = 0; k < ngm; k++ ) { + GlyphCoverageTable ct = gca [ k ]; + if ( ct != null ) { + if ( ct.getCoverageIndex ( ga [ k ] ) < 0 ) { + return false; // match fails at ga [ k ] + } + } + } + if ( rv != null ) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + } + } + } + } + + private abstract static class ChainedContextualSubtable extends GlyphPositioningSubtable { + ChainedContextualSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GPOS_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof ChainedContextualSubtable; + } + /** {@inheritDoc} */ + public boolean position ( GlyphPositioningState ps ) { + boolean applied = false; + int gi = ps.getGlyph(), ci; + if ( ( ci = getCoverageIndex ( gi ) ) >= 0 ) { + int[] rv = new int[1]; + RuleLookup[] la = getLookups ( ci, gi, ps, rv ); + if ( la != null ) { + ps.apply ( la, rv[0] ); + applied = true; + } + } + return applied; + } + /** + * Obtain rule lookups set associated current input glyph context. + * @param ci coverage index of glyph at current position + * @param gi glyph index of glyph at current position + * @param ps glyph positioning state + * @param rv array of ints used to receive multiple return values, must be of length 1 or greater, + * where the first entry is used to return the input sequence length of the matched rule + * @return array of rule lookups or null if none applies + */ + public abstract RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv ); + static GlyphPositioningSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new ChainedContextualSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 2 ) { + return new ChainedContextualSubtableFormat2 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 3 ) { + return new ChainedContextualSubtableFormat3 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ChainedContextualSubtableFormat1 extends ChainedContextualSubtable { + private RuleSet[] rsa; // rule set array, ordered by glyph coverage index + ChainedContextualSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv ) { + assert ps != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedGlyphSequenceRule ) ) { + ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r; + int[] iga = cr.getGlyphs ( gi ); + if ( matches ( ps, iga, 0, rv ) ) { + int[] bga = cr.getBacktrackGlyphs(); + if ( matches ( ps, bga, -1, null ) ) { + int[] lga = cr.getLookaheadGlyphs(); + if ( matches ( ps, lga, rv[0], null ) ) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + private boolean matches ( GlyphPositioningState ps, int[] glyphs, int offset, int[] rv ) { + return ContextualSubtableFormat1.matches ( ps, glyphs, offset, rv ); + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + } + } + } + } + + private static class ChainedContextualSubtableFormat2 extends ChainedContextualSubtable { + private GlyphClassTable icdt; // input class def table + private GlyphClassTable bcdt; // backtrack class def table + private GlyphClassTable lcdt; // lookahead class def table + private int ngc; // class set count + private RuleSet[] rsa; // rule set array, ordered by class number [0...ngc - 1] + ChainedContextualSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 5 ); + entries.add ( icdt ); + entries.add ( bcdt ); + entries.add ( lcdt ); + entries.add ( Integer.valueOf ( ngc ) ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv ) { + assert ps != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedClassSequenceRule ) ) { + ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r; + int[] ica = cr.getClasses ( icdt.getClassIndex ( gi, ps.getClassMatchSet ( gi ) ) ); + if ( matches ( ps, icdt, ica, 0, rv ) ) { + int[] bca = cr.getBacktrackClasses(); + if ( matches ( ps, bcdt, bca, -1, null ) ) { + int[] lca = cr.getLookaheadClasses(); + if ( matches ( ps, lcdt, lca, rv[0], null ) ) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + private boolean matches ( GlyphPositioningState ps, GlyphClassTable cdt, int[] classes, int offset, int[] rv ) { + return ContextualSubtableFormat2.matches ( ps, cdt, classes, offset, rv ); + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 5 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 5 entries" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + icdt = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(1) ) != null ) && ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, second entry must be an GlyphClassTable, but is: " + o.getClass() ); + } else { + bcdt = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(2) ) != null ) && ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, third entry must be an GlyphClassTable, but is: " + o.getClass() ); + } else { + lcdt = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fourth entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + ngc = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(4) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fifth entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + if ( rsa.length != ngc ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes" ); + } + } + } + } + } + + private static class ChainedContextualSubtableFormat3 extends ChainedContextualSubtable { + private RuleSet[] rsa; // rule set array, containing a single rule set + ChainedContextualSubtableFormat3 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphPositioningState ps, int[] rv ) { + assert ps != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedCoverageSequenceRule ) ) { + ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r; + GlyphCoverageTable[] igca = cr.getCoverages(); + if ( matches ( ps, igca, 0, rv ) ) { + GlyphCoverageTable[] bgca = cr.getBacktrackCoverages(); + if ( matches ( ps, bgca, -1, null ) ) { + GlyphCoverageTable[] lgca = cr.getLookaheadCoverages(); + if ( matches ( ps, lgca, rv[0], null ) ) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + private boolean matches ( GlyphPositioningState ps, GlyphCoverageTable[] gca, int offset, int[] rv ) { + return ContextualSubtableFormat3.matches ( ps, gca, offset, rv ); + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + } + } + } + } + + /** + * The <code>DeviceTable</code> class implements a positioning device table record, comprising + * adjustments to be made to scaled design units according to the scaled size. + */ + public static class DeviceTable { + + private final int startSize; + private final int endSize; + private final int[] deltas; + + /** + * Instantiate a DeviceTable. + * @param startSize the + * @param endSize the ending (scaled) size + * @param deltas adjustments for each scaled size + */ + public DeviceTable ( int startSize, int endSize, int[] deltas ) { + assert startSize >= 0; + assert startSize <= endSize; + assert deltas != null; + assert deltas.length == ( endSize - startSize ) + 1; + this.startSize = startSize; + this.endSize = endSize; + this.deltas = deltas; + } + + /** @return the start size */ + public int getStartSize() { + return startSize; + } + + /** @return the end size */ + public int getEndSize() { + return endSize; + } + + /** @return the deltas */ + public int[] getDeltas() { + return deltas; + } + + /** + * Find device adjustment. + * @param fontSize the font size to search for + * @return an adjustment if font size matches an entry + * @asf.todo at present, assumes that 1 device unit equals one point + */ + public int findAdjustment ( int fontSize ) { + int fs = fontSize / 1000; + if ( fs < startSize ) { + return 0; + } else if ( fs <= endSize ) { + return deltas [ fs - startSize ] * 1000; + } else { + return 0; + } + } + + /** {@inheritDoc} */ + public String toString() { + return "{ start = " + startSize + ", end = " + endSize + ", deltas = " + Arrays.toString ( deltas ) + "}"; + } + + } + + /** + * The <code>Value</code> class implements a positioning value record, comprising placement + * and advancement information in X and Y axes, and optionally including device data used to + * perform device (grid-fitted) specific fine grain adjustments. + */ + public static class Value { + + /** X_PLACEMENT value format flag */ + public static final int X_PLACEMENT = 0x0001; + /** Y_PLACEMENT value format flag */ + public static final int Y_PLACEMENT = 0x0002; + /** X_ADVANCE value format flag */ + public static final int X_ADVANCE = 0x0004; + /** Y_ADVANCE value format flag */ + public static final int Y_ADVANCE = 0x0008; + /** X_PLACEMENT_DEVICE value format flag */ + public static final int X_PLACEMENT_DEVICE = 0x0010; + /** Y_PLACEMENT_DEVICE value format flag */ + public static final int Y_PLACEMENT_DEVICE = 0x0020; + /** X_ADVANCE_DEVICE value format flag */ + public static final int X_ADVANCE_DEVICE = 0x0040; + /** Y_ADVANCE_DEVICE value format flag */ + public static final int Y_ADVANCE_DEVICE = 0x0080; + + /** X_PLACEMENT value index (within adjustments arrays) */ + public static final int IDX_X_PLACEMENT = 0; + /** Y_PLACEMENT value index (within adjustments arrays) */ + public static final int IDX_Y_PLACEMENT = 1; + /** X_ADVANCE value index (within adjustments arrays) */ + public static final int IDX_X_ADVANCE = 2; + /** Y_ADVANCE value index (within adjustments arrays) */ + public static final int IDX_Y_ADVANCE = 3; + + private int xPlacement; // x placement + private int yPlacement; // y placement + private int xAdvance; // x advance + private int yAdvance; // y advance + private final DeviceTable xPlaDevice; // x placement device table + private final DeviceTable yPlaDevice; // y placement device table + private final DeviceTable xAdvDevice; // x advance device table + private final DeviceTable yAdvDevice; // x advance device table + + /** + * Instantiate a Value. + * @param xPlacement the x placement or zero + * @param yPlacement the y placement or zero + * @param xAdvance the x advance or zero + * @param yAdvance the y advance or zero + * @param xPlaDevice the x placement device table or null + * @param yPlaDevice the y placement device table or null + * @param xAdvDevice the x advance device table or null + * @param yAdvDevice the y advance device table or null + */ + public Value ( int xPlacement, int yPlacement, int xAdvance, int yAdvance, DeviceTable xPlaDevice, DeviceTable yPlaDevice, DeviceTable xAdvDevice, DeviceTable yAdvDevice ) { + this.xPlacement = xPlacement; + this.yPlacement = yPlacement; + this.xAdvance = xAdvance; + this.yAdvance = yAdvance; + this.xPlaDevice = xPlaDevice; + this.yPlaDevice = yPlaDevice; + this.xAdvDevice = xAdvDevice; + this.yAdvDevice = yAdvDevice; + } + + /** @return the x placement */ + public int getXPlacement() { + return xPlacement; + } + + /** @return the y placement */ + public int getYPlacement() { + return yPlacement; + } + + /** @return the x advance */ + public int getXAdvance() { + return xAdvance; + } + + /** @return the y advance */ + public int getYAdvance() { + return yAdvance; + } + + /** @return the x placement device table */ + public DeviceTable getXPlaDevice() { + return xPlaDevice; + } + + /** @return the y placement device table */ + public DeviceTable getYPlaDevice() { + return yPlaDevice; + } + + /** @return the x advance device table */ + public DeviceTable getXAdvDevice() { + return xAdvDevice; + } + + /** @return the y advance device table */ + public DeviceTable getYAdvDevice() { + return yAdvDevice; + } + + /** + * Apply value to specific adjustments to without use of device table adjustments. + * @param xPlacement the x placement or zero + * @param yPlacement the y placement or zero + * @param xAdvance the x advance or zero + * @param yAdvance the y advance or zero + */ + public void adjust ( int xPlacement, int yPlacement, int xAdvance, int yAdvance ) { + this.xPlacement += xPlacement; + this.yPlacement += yPlacement; + this.xAdvance += xAdvance; + this.yAdvance += yAdvance; + } + + /** + * Apply value to adjustments using font size for device table adjustments. + * @param adjustments array of four integers containing X,Y placement and X,Y advance adjustments + * @param fontSize font size for device table adjustments + * @return true if some adjustment was made + */ + public boolean adjust ( int[] adjustments, int fontSize ) { + boolean adjust = false; + int dv; + if ( ( dv = xPlacement ) != 0 ) { + adjustments [ IDX_X_PLACEMENT ] += dv; + adjust = true; + } + if ( ( dv = yPlacement ) != 0 ) { + adjustments [ IDX_Y_PLACEMENT ] += dv; + adjust = true; + } + if ( ( dv = xAdvance ) != 0 ) { + adjustments [ IDX_X_ADVANCE ] += dv; + adjust = true; + } + if ( ( dv = yAdvance ) != 0 ) { + adjustments [ IDX_Y_ADVANCE ] += dv; + adjust = true; + } + if ( fontSize != 0 ) { + DeviceTable dt; + if ( ( dt = xPlaDevice ) != null ) { + if ( ( dv = dt.findAdjustment ( fontSize ) ) != 0 ) { + adjustments [ IDX_X_PLACEMENT ] += dv; + adjust = true; + } + } + if ( ( dt = yPlaDevice ) != null ) { + if ( ( dv = dt.findAdjustment ( fontSize ) ) != 0 ) { + adjustments [ IDX_Y_PLACEMENT ] += dv; + adjust = true; + } + } + if ( ( dt = xAdvDevice ) != null ) { + if ( ( dv = dt.findAdjustment ( fontSize ) ) != 0 ) { + adjustments [ IDX_X_ADVANCE ] += dv; + adjust = true; + } + } + if ( ( dt = yAdvDevice ) != null ) { + if ( ( dv = dt.findAdjustment ( fontSize ) ) != 0 ) { + adjustments [ IDX_Y_ADVANCE ] += dv; + adjust = true; + } + } + } + return adjust; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + boolean first = true; + sb.append ( "{ " ); + if ( xPlacement != 0 ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "xPlacement = " + xPlacement ); + } + if ( yPlacement != 0 ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "yPlacement = " + yPlacement ); + } + if ( xAdvance != 0 ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "xAdvance = " + xAdvance ); + } + if ( yAdvance != 0 ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "yAdvance = " + yAdvance ); + } + if ( xPlaDevice != null ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "xPlaDevice = " + xPlaDevice ); + } + if ( yPlaDevice != null ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "xPlaDevice = " + yPlaDevice ); + } + if ( xAdvDevice != null ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "xAdvDevice = " + xAdvDevice ); + } + if ( yAdvDevice != null ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "xAdvDevice = " + yAdvDevice ); + } + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The <code>PairValues</code> class implements a pair value record, comprising a glyph id (or zero) + * and two optional positioning values. + */ + public static class PairValues { + + private final int glyph; // glyph id (or 0) + private final Value value1; // value for first glyph in pair (or null) + private final Value value2; // value for second glyph in pair (or null) + + /** + * Instantiate a PairValues. + * @param glyph the glyph id (or zero) + * @param value1 the value of the first glyph in pair (or null) + * @param value2 the value of the second glyph in pair (or null) + */ + public PairValues ( int glyph, Value value1, Value value2 ) { + assert glyph >= 0; + this.glyph = glyph; + this.value1 = value1; + this.value2 = value2; + } + + /** @return the glyph id */ + public int getGlyph() { + return glyph; + } + + /** @return the first value */ + public Value getValue1() { + return value1; + } + + /** @return the second value */ + public Value getValue2() { + return value2; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + boolean first = true; + sb.append ( "{ " ); + if ( glyph != 0 ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "glyph = " + glyph ); + } + if ( value1 != null ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "value1 = " + value1 ); + } + if ( value2 != null ) { + if ( ! first ) { + sb.append ( ", " ); + } else { + first = false; + } + sb.append ( "value2 = " + value2 ); + } + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The <code>Anchor</code> class implements a anchor record, comprising an X,Y coordinate pair, + * an optional anchor point index (or -1), and optional X or Y device tables (or null if absent). + */ + public static class Anchor { + + private final int x; // xCoordinate (in design units) + private final int y; // yCoordinate (in design units) + private final int anchorPoint; // anchor point index (or -1) + private final DeviceTable xDevice; // x device table + private final DeviceTable yDevice; // y device table + + /** + * Instantiate an Anchor (format 1). + * @param x the x coordinate + * @param y the y coordinate + */ + public Anchor ( int x, int y ) { + this ( x, y, -1, null, null ); + } + + /** + * Instantiate an Anchor (format 2). + * @param x the x coordinate + * @param y the y coordinate + * @param anchorPoint anchor index (or -1) + */ + public Anchor ( int x, int y, int anchorPoint ) { + this ( x, y, anchorPoint, null, null ); + } + + /** + * Instantiate an Anchor (format 3). + * @param x the x coordinate + * @param y the y coordinate + * @param xDevice the x device table (or null if not present) + * @param yDevice the y device table (or null if not present) + */ + public Anchor ( int x, int y, DeviceTable xDevice, DeviceTable yDevice ) { + this ( x, y, -1, xDevice, yDevice ); + } + + /** + * Instantiate an Anchor based on an existing anchor. + * @param a the existing anchor + */ + protected Anchor ( Anchor a ) { + this ( a.x, a.y, a.anchorPoint, a.xDevice, a.yDevice ); + } + + private Anchor ( int x, int y, int anchorPoint, DeviceTable xDevice, DeviceTable yDevice ) { + assert ( anchorPoint >= 0 ) || ( anchorPoint == -1 ); + this.x = x; + this.y = y; + this.anchorPoint = anchorPoint; + this.xDevice = xDevice; + this.yDevice = yDevice; + } + + /** @return the x coordinate */ + public int getX() { + return x; + } + + /** @return the y coordinate */ + public int getY() { + return y; + } + + /** @return the anchor point index (or -1 if not specified) */ + public int getAnchorPoint() { + return anchorPoint; + } + + /** @return the x device table (or null if not specified) */ + public DeviceTable getXDevice() { + return xDevice; + } + + /** @return the y device table (or null if not specified) */ + public DeviceTable getYDevice() { + return yDevice; + } + + /** + * Obtain adjustment value required to align the specified anchor + * with this anchor. + * @param a the anchor to align + * @return the adjustment value needed to effect alignment + */ + public Value getAlignmentAdjustment ( Anchor a ) { + assert a != null; + // TODO - handle anchor point + // TODO - handle device tables + return new Value ( x - a.x, y - a.y, 0, 0, null, null, null, null ); + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append ( "{ [" + x + "," + y + "]" ); + if ( anchorPoint != -1 ) { + sb.append ( ", anchorPoint = " + anchorPoint ); + } + if ( xDevice != null ) { + sb.append ( ", xDevice = " + xDevice ); + } + if ( yDevice != null ) { + sb.append ( ", yDevice = " + yDevice ); + } + sb.append(" }"); + return sb.toString(); + } + + } + + /** + * The <code>MarkAnchor</code> class is a subclass of the <code>Anchor</code> class, adding a mark + * class designation. + */ + public static class MarkAnchor extends Anchor { + + private final int markClass; // mark class + + /** + * Instantiate a MarkAnchor + * @param markClass the mark class + * @param a the underlying anchor (whose fields are copied) + */ + public MarkAnchor ( int markClass, Anchor a ) { + super ( a ); + this.markClass = markClass; + } + + /** @return the mark class */ + public int getMarkClass() { + return markClass; + } + + /** {@inheritDoc} */ + public String toString() { + return "{ markClass = " + markClass + ", anchor = " + super.toString() + " }"; + } + + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphProcessingState.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphProcessingState.java new file mode 100644 index 000000000..acccdc86c --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphProcessingState.java @@ -0,0 +1,1135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.GlyphContextTester; +import org.apache.fop.complexscripts.util.GlyphTester; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck + +/** + * The <code>GlyphProcessingState</code> implements a common, base state object used during glyph substitution + * and positioning processing. + * @author Glenn Adams + */ + +public class GlyphProcessingState { + + /** governing glyph definition table */ + protected GlyphDefinitionTable gdef; + /** governing script */ + protected String script; + /** governing language */ + protected String language; + /** governing feature */ + protected String feature; + /** current input glyph sequence */ + protected GlyphSequence igs; + /** current index in input sequence */ + protected int index; + /** last (maximum) index of input sequence (exclusive) */ + protected int indexLast; + /** consumed, updated after each successful subtable application */ + protected int consumed; + /** lookup flags */ + protected int lookupFlags; + /** class match set */ + protected int classMatchSet; + /** script specific context tester or null */ + protected ScriptContextTester sct; + /** glyph context tester or null */ + protected GlyphContextTester gct; + /** ignore base glyph tester */ + protected GlyphTester ignoreBase; + /** ignore ligature glyph tester */ + protected GlyphTester ignoreLigature; + /** ignore mark glyph tester */ + protected GlyphTester ignoreMark; + /** default ignore glyph tester */ + protected GlyphTester ignoreDefault; + + /** + * Construct glyph processing state. + * @param gs input glyph sequence + * @param script script identifier + * @param language language identifier + * @param feature feature identifier + * @param sct script context tester (or null) + */ + protected GlyphProcessingState ( GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct ) { + this.script = script; + this.language = language; + this.feature = feature; + this.igs = gs; + this.indexLast = gs.getGlyphCount(); + this.sct = sct; + this.gct = ( sct != null ) ? sct.getTester ( feature ) : null; + this.ignoreBase = new GlyphTester() { public boolean test(int gi, int flags) { return isIgnoredBase(gi, flags); } }; + this.ignoreLigature = new GlyphTester() { public boolean test(int gi, int flags) { return isIgnoredLigature(gi, flags); } }; + this.ignoreMark = new GlyphTester() { public boolean test(int gi, int flags) { return isIgnoredMark(gi, flags); } }; + } + + /** + * Construct glyph processing state using an existing state object using shallow copy + * except as follows: input glyph sequence is copied deep except for its characters array. + * @param s existing processing state to copy from + */ + protected GlyphProcessingState ( GlyphProcessingState s ) { + this ( new GlyphSequence ( s.igs ), s.script, s.language, s.feature, s.sct ); + setPosition ( s.index ); + } + + /** + * Set governing glyph definition table. + * @param gdef glyph definition table (or null, to unset) + */ + public void setGDEF ( GlyphDefinitionTable gdef ) { + if ( this.gdef == null ) { + this.gdef = gdef; + } else if ( gdef == null ) { + this.gdef = null; + } + } + + /** + * Obtain governing glyph definition table. + * @return glyph definition table (or null, to not set) + */ + public GlyphDefinitionTable getGDEF() { + return gdef; + } + + /** + * Set governing lookup flags + * @param flags lookup flags (or zero, to unset) + */ + public void setLookupFlags ( int flags ) { + if ( this.lookupFlags == 0 ) { + this.lookupFlags = flags; + } else if ( flags == 0 ) { + this.lookupFlags = 0; + } + } + + /** + * Obtain governing lookup flags. + * @return lookup flags (zero may indicate unset or no flags) + */ + public int getLookupFlags() { + return lookupFlags; + } + + /** + * Obtain governing class match set. + * @param gi glyph index that may be used to determine which match set applies + * @return class match set (zero may indicate unset or no set) + */ + public int getClassMatchSet ( int gi ) { + return 0; + } + + /** + * Set default ignore tester. + * @param ignoreDefault glyph tester (or null, to unset) + */ + public void setIgnoreDefault ( GlyphTester ignoreDefault ) { + if ( this.ignoreDefault == null ) { + this.ignoreDefault = ignoreDefault; + } else if ( ignoreDefault == null ) { + this.ignoreDefault = null; + } + } + + /** + * Obtain governing default ignores tester. + * @return default ignores tester + */ + public GlyphTester getIgnoreDefault() { + return ignoreDefault; + } + + /** + * Update glyph subtable specific state. Each time a + * different glyph subtable is to be applied, it is used + * to update this state prior to application, after which + * this state is to be reset. + * @param st glyph subtable to use for update + */ + public void updateSubtableState ( GlyphSubtable st ) { + setGDEF ( st.getGDEF() ); + setLookupFlags ( st.getFlags() ); + setIgnoreDefault ( getIgnoreTester ( getLookupFlags() ) ); + } + + /** + * Reset glyph subtable specific state. + */ + public void resetSubtableState() { + setGDEF ( null ); + setLookupFlags ( 0 ); + setIgnoreDefault ( null ); + } + + /** + * Obtain current position index in input glyph sequence. + * @return current index + */ + public int getPosition() { + return index; + } + + /** + * Set (seek to) position index in input glyph sequence. + * @param index to seek to + * @throws IndexOutOfBoundsException if index is less than zero + * or exceeds last valid position + */ + public void setPosition ( int index ) throws IndexOutOfBoundsException { + if ( ( index >= 0 ) && ( index <= indexLast ) ) { + this.index = index; + } else { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Obtain last valid position index in input glyph sequence. + * @return current last index + */ + public int getLastPosition() { + return indexLast; + } + + /** + * Determine if at least one glyph remains in + * input sequence. + * @return true if one or more glyph remains + */ + public boolean hasNext() { + return hasNext ( 1 ); + } + + /** + * Determine if at least <code>count</code> glyphs remain in + * input sequence. + * @param count of glyphs to test + * @return true if at least <code>count</code> glyphs are available + */ + public boolean hasNext ( int count ) { + return ( index + count ) <= indexLast; + } + + /** + * Update the current position index based upon previously consumed + * glyphs, i.e., add the consuemd count to the current position index. + * If no glyphs were previously consumed, then forces exactly one + * glyph to be consumed. + * @return the new (updated) position index + */ + public int next() { + if ( index < indexLast ) { + // force consumption of at least one input glyph + if ( consumed == 0 ) { + consumed = 1; + } + index += consumed; consumed = 0; + if ( index > indexLast ) { + index = indexLast; + } + } + return index; + } + + /** + * Determine if at least one backtrack (previous) glyph is present + * in input sequence. + * @return true if one or more glyph remains + */ + public boolean hasPrev() { + return hasPrev ( 1 ); + } + + /** + * Determine if at least <code>count</code> backtrack (previous) glyphs + * are present in input sequence. + * @param count of glyphs to test + * @return true if at least <code>count</code> glyphs are available + */ + public boolean hasPrev ( int count ) { + return ( index - count ) >= 0; + } + + /** + * Update the current position index based upon previously consumed + * glyphs, i.e., subtract the consuemd count from the current position index. + * If no glyphs were previously consumed, then forces exactly one + * glyph to be consumed. This method is used to traverse an input + * glyph sequence in reverse order. + * @return the new (updated) position index + */ + public int prev() { + if ( index > 0 ) { + // force consumption of at least one input glyph + if ( consumed == 0 ) { + consumed = 1; + } + index -= consumed; consumed = 0; + if ( index < 0 ) { + index = 0; + } + } + return index; + } + + /** + * Record the consumption of <code>count</code> glyphs such that + * this consumption never exceeds the number of glyphs in the input glyph + * sequence. + * @param count of glyphs to consume + * @return newly adjusted consumption count + * @throws IndexOutOfBoundsException if count would cause consumption + * to exceed count of glyphs in input glyph sequence + */ + public int consume ( int count ) throws IndexOutOfBoundsException { + if ( ( consumed + count ) <= indexLast ) { + consumed += count; + return consumed; + } else { + throw new IndexOutOfBoundsException(); + } + } + + /** + * Determine if any consumption has occurred. + * @return true if consumption count is greater than zero + */ + public boolean didConsume() { + return consumed > 0; + } + + /** + * Obtain reference to input glyph sequence, which must not be modified. + * @return input glyph sequence + */ + public GlyphSequence getInput() { + return igs; + } + + /** + * Obtain glyph at specified offset from current position. + * @param offset from current position + * @return glyph at specified offset from current position + * @throws IndexOutOfBoundsException if no glyph available at offset + */ + public int getGlyph ( int offset ) throws IndexOutOfBoundsException { + int i = index + offset; + if ( ( i >= 0 ) && ( i < indexLast ) ) { + return igs.getGlyph ( i ); + } else { + throw new IndexOutOfBoundsException ( "attempting index at " + i ); + } + } + + /** + * Obtain glyph at current position. + * @return glyph at current position + * @throws IndexOutOfBoundsException if no glyph available + */ + public int getGlyph() throws IndexOutOfBoundsException { + return getGlyph ( 0 ); + } + + /** + * Set (replace) glyph at specified offset from current position. + * @param offset from current position + * @param glyph to set at specified offset from current position + * @throws IndexOutOfBoundsException if specified offset is not valid position + */ + public void setGlyph ( int offset, int glyph ) throws IndexOutOfBoundsException { + int i = index + offset; + if ( ( i >= 0 ) && ( i < indexLast ) ) { + igs.setGlyph ( i, glyph ); + } else { + throw new IndexOutOfBoundsException ( "attempting index at " + i ); + } + } + + /** + * Obtain character association of glyph at specified offset from current position. + * @param offset from current position + * @return character association of glyph at current position + * @throws IndexOutOfBoundsException if offset results in an invalid index into input glyph sequence + */ + public GlyphSequence.CharAssociation getAssociation ( int offset ) throws IndexOutOfBoundsException { + int i = index + offset; + if ( ( i >= 0 ) && ( i < indexLast ) ) { + return igs.getAssociation ( i ); + } else { + throw new IndexOutOfBoundsException ( "attempting index at " + i ); + } + } + + /** + * Obtain character association of glyph at current position. + * @return character association of glyph at current position + * @throws IndexOutOfBoundsException if no glyph available + */ + public GlyphSequence.CharAssociation getAssociation() throws IndexOutOfBoundsException { + return getAssociation ( 0 ); + } + + /** + * Obtain <code>count</code> glyphs starting at specified offset from current position. If + * <code>reverseOrder</code> is true, then glyphs are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of glyphs to obtain + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored) + * @param glyphs array to use to fetch glyphs + * @param counts int[2] array to receive fetched glyph counts, where counts[0] will + * receive the number of glyphs obtained, and counts[1] will receive the number of glyphs + * ignored + * @return array of glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphs ( int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException { + if ( count < 0 ) { + count = getGlyphsAvailable ( offset, reverseOrder, ignoreTester ) [ 0 ]; + } + int start = index + offset; + if ( start < 0 ) { + throw new IndexOutOfBoundsException ( "will attempt index at " + start ); + } else if ( ! reverseOrder && ( ( start + count ) > indexLast ) ) { + throw new IndexOutOfBoundsException ( "will attempt index at " + ( start + count ) ); + } else if ( reverseOrder && ( ( start + 1 ) < count ) ) { + throw new IndexOutOfBoundsException ( "will attempt index at " + ( start - count ) ); + } + if ( glyphs == null ) { + glyphs = new int [ count ]; + } else if ( glyphs.length != count ) { + throw new IllegalArgumentException ( "glyphs array is non-null, but its length (" + glyphs.length + "), is not equal to count (" + count + ")" ); + } + if ( ! reverseOrder ) { + return getGlyphsForward ( start, count, ignoreTester, glyphs, counts ); + } else { + return getGlyphsReverse ( start, count, ignoreTester, glyphs, counts ); + } + } + + private int[] getGlyphsForward ( int start, int count, GlyphTester ignoreTester, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + for ( int i = start, n = indexLast, k = 0; i < n; i++ ) { + int gi = getGlyph ( i - index ); + if ( gi == 65535 ) { + ignored++; + } else { + if ( ( ignoreTester == null ) || ! ignoreTester.test ( gi, getLookupFlags() ) ) { + if ( k < count ) { + glyphs [ k++ ] = gi; counted++; + } else { + break; + } + } else { + ignored++; + } + } + } + if ( ( counts != null ) && ( counts.length > 1 ) ) { + counts[0] = counted; + counts[1] = ignored; + } + return glyphs; + } + + private int[] getGlyphsReverse ( int start, int count, GlyphTester ignoreTester, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + for ( int i = start, k = 0; i >= 0; i-- ) { + int gi = getGlyph ( i - index ); + if ( gi == 65535 ) { + ignored++; + } else { + if ( ( ignoreTester == null ) || ! ignoreTester.test ( gi, getLookupFlags() ) ) { + if ( k < count ) { + glyphs [ k++ ] = gi; counted++; + } else { + break; + } + } else { + ignored++; + } + } + } + if ( ( counts != null ) && ( counts.length > 1 ) ) { + counts[0] = counted; + counts[1] = ignored; + } + return glyphs; + } + + /** + * Obtain <code>count</code> glyphs starting at specified offset from current position. If + * offset is negative, then glyphs are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of glyphs to obtain + * @param glyphs array to use to fetch glyphs + * @param counts int[2] array to receive fetched glyph counts, where counts[0] will + * receive the number of glyphs obtained, and counts[1] will receive the number of glyphs + * ignored + * @return array of glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphs ( int offset, int count, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException { + return getGlyphs ( offset, count, offset < 0, ignoreDefault, glyphs, counts ); + } + + /** + * Obtain all glyphs starting from current position to end of input glyph sequence. + * @return array of available glyphs + * @throws IndexOutOfBoundsException if no glyph available + */ + public int[] getGlyphs() throws IndexOutOfBoundsException { + return getGlyphs ( 0, indexLast - index, false, null, null, null ); + } + + /** + * Obtain <code>count</code> ignored glyphs starting at specified offset from current position. If + * <code>reverseOrder</code> is true, then glyphs are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of glyphs to obtain + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored) + * @param glyphs array to use to fetch glyphs + * @param counts int[2] array to receive fetched glyph counts, where counts[0] will + * receive the number of glyphs obtained, and counts[1] will receive the number of glyphs + * ignored + * @return array of glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getIgnoredGlyphs ( int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, int[] glyphs, int[] counts ) throws IndexOutOfBoundsException { + return getGlyphs ( offset, count, reverseOrder, new NotGlyphTester ( ignoreTester ), glyphs, counts ); + } + + /** + * Obtain <code>count</code> ignored glyphs starting at specified offset from current position. If <code>offset</code> is + * negative, then fetch in reverse order. + * @param offset from current position + * @param count number of glyphs to obtain + * @return array of glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getIgnoredGlyphs ( int offset, int count ) throws IndexOutOfBoundsException { + return getIgnoredGlyphs ( offset, count, offset < 0, ignoreDefault, null, null ); + } + + /** + * Determine number of glyphs available starting at specified offset from current position. If + * <code>reverseOrder</code> is true, then search backwards in input glyph sequence. + * @param offset from current position + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs to count (or null, in which case none are ignored) + * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs ignored + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphsAvailable ( int offset, boolean reverseOrder, GlyphTester ignoreTester ) throws IndexOutOfBoundsException { + int start = index + offset; + if ( ( start < 0 ) || ( start > indexLast ) ) { + return new int[] { 0, 0 }; + } else if ( ! reverseOrder ) { + return getGlyphsAvailableForward ( start, ignoreTester ); + } else { + return getGlyphsAvailableReverse ( start, ignoreTester ); + } + } + + private int[] getGlyphsAvailableForward ( int start, GlyphTester ignoreTester ) throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + if ( ignoreTester == null ) { + counted = indexLast - start; + } else { + for ( int i = start, n = indexLast; i < n; i++ ) { + int gi = getGlyph ( i - index ); + if ( gi == 65535 ) { + ignored++; + } else { + if ( ignoreTester.test ( gi, getLookupFlags() ) ) { + ignored++; + } else { + counted++; + } + } + } + } + return new int[] { counted, ignored }; + } + + private int[] getGlyphsAvailableReverse ( int start, GlyphTester ignoreTester ) throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + if ( ignoreTester == null ) { + counted = start + 1; + } else { + for ( int i = start; i >= 0; i-- ) { + int gi = getGlyph ( i - index ); + if ( gi == 65535 ) { + ignored++; + } else { + if ( ignoreTester.test ( gi, getLookupFlags() ) ) { + ignored++; + } else { + counted++; + } + } + } + } + return new int[] { counted, ignored }; + } + + /** + * Determine number of glyphs available starting at specified offset from current position. If + * <code>reverseOrder</code> is true, then search backwards in input glyph sequence. Uses the + * default ignores tester. + * @param offset from current position + * @param reverseOrder true if to obtain in reverse order + * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs ignored + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphsAvailable ( int offset, boolean reverseOrder ) throws IndexOutOfBoundsException { + return getGlyphsAvailable ( offset, reverseOrder, ignoreDefault ); + } + + /** + * Determine number of glyphs available starting at specified offset from current position. If + * offset is negative, then search backwards in input glyph sequence. Uses the + * default ignores tester. + * @param offset from current position + * @return an int[2] array where counts[0] is the number of glyphs available, and counts[1] is the number of glyphs ignored + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int[] getGlyphsAvailable ( int offset ) throws IndexOutOfBoundsException { + return getGlyphsAvailable ( offset, offset < 0 ); + } + + /** + * Obtain <code>count</code> character associations of glyphs starting at specified offset from current position. If + * <code>reverseOrder</code> is true, then associations are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of associations to obtain + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored) + * @param associations array to use to fetch associations + * @param counts int[2] array to receive fetched association counts, where counts[0] will + * receive the number of associations obtained, and counts[1] will receive the number of glyphs whose + * associations were ignored + * @return array of associations + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public GlyphSequence.CharAssociation[] getAssociations ( int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, GlyphSequence.CharAssociation[] associations, int[] counts ) + throws IndexOutOfBoundsException { + if ( count < 0 ) { + count = getGlyphsAvailable ( offset, reverseOrder, ignoreTester ) [ 0 ]; + } + int start = index + offset; + if ( start < 0 ) { + throw new IndexOutOfBoundsException ( "will attempt index at " + start ); + } else if ( ! reverseOrder && ( ( start + count ) > indexLast ) ) { + throw new IndexOutOfBoundsException ( "will attempt index at " + ( start + count ) ); + } else if ( reverseOrder && ( ( start + 1 ) < count ) ) { + throw new IndexOutOfBoundsException ( "will attempt index at " + ( start - count ) ); + } + if ( associations == null ) { + associations = new GlyphSequence.CharAssociation [ count ]; + } else if ( associations.length != count ) { + throw new IllegalArgumentException ( "associations array is non-null, but its length (" + associations.length + "), is not equal to count (" + count + ")" ); + } + if ( ! reverseOrder ) { + return getAssociationsForward ( start, count, ignoreTester, associations, counts ); + } else { + return getAssociationsReverse ( start, count, ignoreTester, associations, counts ); + } + } + + private GlyphSequence.CharAssociation[] getAssociationsForward ( int start, int count, GlyphTester ignoreTester, GlyphSequence.CharAssociation[] associations, int[] counts ) + throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + for ( int i = start, n = indexLast, k = 0; i < n; i++ ) { + int gi = getGlyph ( i - index ); + if ( gi == 65535 ) { + ignored++; + } else { + if ( ( ignoreTester == null ) || ! ignoreTester.test ( gi, getLookupFlags() ) ) { + if ( k < count ) { + associations [ k++ ] = getAssociation ( i - index ); counted++; + } else { + break; + } + } else { + ignored++; + } + } + } + if ( ( counts != null ) && ( counts.length > 1 ) ) { + counts[0] = counted; + counts[1] = ignored; + } + return associations; + } + + private GlyphSequence.CharAssociation[] getAssociationsReverse ( int start, int count, GlyphTester ignoreTester, GlyphSequence.CharAssociation[] associations, int[] counts ) + throws IndexOutOfBoundsException { + int counted = 0; + int ignored = 0; + for ( int i = start, k = 0; i >= 0; i-- ) { + int gi = getGlyph ( i - index ); + if ( gi == 65535 ) { + ignored++; + } else { + if ( ( ignoreTester == null ) || ! ignoreTester.test ( gi, getLookupFlags() ) ) { + if ( k < count ) { + associations [ k++ ] = getAssociation ( i - index ); counted++; + } else { + break; + } + } else { + ignored++; + } + } + } + if ( ( counts != null ) && ( counts.length > 1 ) ) { + counts[0] = counted; + counts[1] = ignored; + } + return associations; + } + + /** + * Obtain <code>count</code> character associations of glyphs starting at specified offset from current position. If + * offset is negative, then search backwards in input glyph sequence. Uses the + * default ignores tester. + * @param offset from current position + * @param count number of associations to obtain + * @return array of associations + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public GlyphSequence.CharAssociation[] getAssociations ( int offset, int count ) throws IndexOutOfBoundsException { + return getAssociations ( offset, count, offset < 0, ignoreDefault, null, null ); + } + + /** + * Obtain <code>count</code> character associations of ignored glyphs starting at specified offset from current position. If + * <code>reverseOrder</code> is true, then glyphs are returned in reverse order starting at specified offset + * and going in reverse towards beginning of input glyph sequence. + * @param offset from current position + * @param count number of character associations to obtain + * @param reverseOrder true if to obtain in reverse order + * @param ignoreTester glyph tester to use to determine which glyphs are ignored (or null, in which case none are ignored) + * @param associations array to use to fetch associations + * @param counts int[2] array to receive fetched association counts, where counts[0] will + * receive the number of associations obtained, and counts[1] will receive the number of glyphs whose + * associations were ignored + * @return array of associations + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public GlyphSequence.CharAssociation[] getIgnoredAssociations ( int offset, int count, boolean reverseOrder, GlyphTester ignoreTester, GlyphSequence.CharAssociation[] associations, int[] counts ) + throws IndexOutOfBoundsException { + return getAssociations ( offset, count, reverseOrder, new NotGlyphTester ( ignoreTester ), associations, counts ); + } + + /** + * Obtain <code>count</code> character associations of ignored glyphs starting at specified offset from current position. If + * offset is negative, then search backwards in input glyph sequence. Uses the + * default ignores tester. + * @param offset from current position + * @param count number of character associations to obtain + * @return array of associations + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public GlyphSequence.CharAssociation[] getIgnoredAssociations ( int offset, int count ) throws IndexOutOfBoundsException { + return getIgnoredAssociations ( offset, count, offset < 0, ignoreDefault, null, null ); + } + + /** + * Replace subsequence of input glyph sequence starting at specified offset from current position and of + * length <code>count</code> glyphs with a subsequence of the sequence <code>gs</code> starting from the specified + * offset <code>gsOffset</code> of length <code>gsCount</code> glyphs. + * @param offset from current position + * @param count number of glyphs to replace, which, if negative means all glyphs from offset to end of input sequence + * @param gs glyph sequence from which to obtain replacement glyphs + * @param gsOffset offset of first glyph in replacement sequence + * @param gsCount count of glyphs in replacement sequence starting at <code>gsOffset</code> + * @return true if replacement occurred, or false if replacement would result in no change to input glyph sequence + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public boolean replaceInput ( int offset, int count, GlyphSequence gs, int gsOffset, int gsCount ) throws IndexOutOfBoundsException { + int nig = ( igs != null ) ? igs.getGlyphCount() : 0; + int position = getPosition() + offset; + if ( position < 0 ) { + position = 0; + } else if ( position > nig ) { + position = nig; + } + if ( ( count < 0 ) || ( ( position + count ) > nig ) ) { + count = nig - position; + } + int nrg = ( gs != null ) ? gs.getGlyphCount() : 0; + if ( gsOffset < 0 ) { + gsOffset = 0; + } else if ( gsOffset > nrg ) { + gsOffset = nrg; + } + if ( ( gsCount < 0 ) || ( ( gsOffset + gsCount ) > nrg ) ) { + gsCount = nrg - gsOffset; + } + int ng = nig + gsCount - count; + IntBuffer gb = IntBuffer.allocate ( ng ); + List al = new ArrayList ( ng ); + for ( int i = 0, n = position; i < n; i++ ) { + gb.put ( igs.getGlyph ( i ) ); + al.add ( igs.getAssociation ( i ) ); + } + for ( int i = gsOffset, n = gsOffset + gsCount; i < n; i++ ) { + gb.put ( gs.getGlyph ( i ) ); + al.add ( gs.getAssociation ( i ) ); + } + for ( int i = position + count, n = nig; i < n; i++ ) { + gb.put ( igs.getGlyph ( i ) ); + al.add ( igs.getAssociation ( i ) ); + } + gb.flip(); + if ( igs.compareGlyphs ( gb ) != 0 ) { + this.igs = new GlyphSequence ( igs.getCharacters(), gb, al ); + this.indexLast = gb.limit(); + return true; + } else { + return false; + } + } + + /** + * Replace subsequence of input glyph sequence starting at specified offset from current position and of + * length <code>count</code> glyphs with all glyphs in the replacement sequence <code>gs</code>. + * @param offset from current position + * @param count number of glyphs to replace, which, if negative means all glyphs from offset to end of input sequence + * @param gs glyph sequence from which to obtain replacement glyphs + * @return true if replacement occurred, or false if replacement would result in no change to input glyph sequence + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public boolean replaceInput ( int offset, int count, GlyphSequence gs ) throws IndexOutOfBoundsException { + return replaceInput ( offset, count, gs, 0, gs.getGlyphCount() ); + } + + /** + * Erase glyphs in input glyph sequence starting at specified offset from current position, where each glyph + * in the specified <code>glyphs</code> array is matched, one at a time, and when a (forward searching) match is found + * in the input glyph sequence, the matching glyph is replaced with the glyph index 65535. + * @param offset from current position + * @param glyphs array of glyphs to erase + * @return the number of glyphs erased, which may be less than the number of specified glyphs + * @throws IndexOutOfBoundsException if offset or count results in an + * invalid index into input glyph sequence + */ + public int erase ( int offset, int[] glyphs ) throws IndexOutOfBoundsException { + int start = index + offset; + if ( ( start < 0 ) || ( start > indexLast ) ) { + throw new IndexOutOfBoundsException ( "will attempt index at " + start ); + } else { + int erased = 0; + for ( int i = start - index, n = indexLast - start; i < n; i++ ) { + int gi = getGlyph ( i ); + if ( gi == glyphs [ erased ] ) { + setGlyph ( i, 65535 ); + erased++; + } + } + return erased; + } + } + + /** + * Determine if is possible that the current input sequence satisfies a script specific + * context testing predicate. If no predicate applies, then application is always possible. + * @return true if no script specific context tester applies or if a specified tester returns + * true for the current input sequence context + */ + public boolean maybeApplicable() { + if ( gct == null ) { + return true; + } else { + return gct.test ( script, language, feature, igs, index, getLookupFlags() ); + } + } + + /** + * Apply default application semantices; namely, consume one glyph. + */ + public void applyDefault() { + consumed += 1; + } + + /** + * Determine if specified glyph is a base glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @return true if glyph definition table records glyph as a base glyph; otherwise, false + */ + public boolean isBase ( int gi ) { + if ( gdef != null ) { + return gdef.isGlyphClass ( gi, GlyphDefinitionTable.GLYPH_CLASS_BASE ); + } else { + return false; + } + } + + /** + * Determine if specified glyph is an ignored base glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @param flags that apply to lookup in scope + * @return true if glyph definition table records glyph as a base glyph; otherwise, false + */ + public boolean isIgnoredBase ( int gi, int flags ) { + return ( ( flags & GlyphSubtable.LF_IGNORE_BASE ) != 0 ) && isBase ( gi ); + } + + /** + * Determine if specified glyph is an ligature glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false + */ + public boolean isLigature ( int gi ) { + if ( gdef != null ) { + return gdef.isGlyphClass ( gi, GlyphDefinitionTable.GLYPH_CLASS_LIGATURE ); + } else { + return false; + } + } + + /** + * Determine if specified glyph is an ignored ligature glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @param flags that apply to lookup in scope + * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false + */ + public boolean isIgnoredLigature ( int gi, int flags ) { + return ( ( flags & GlyphSubtable.LF_IGNORE_LIGATURE ) != 0 ) && isLigature ( gi ); + } + + /** + * Determine if specified glyph is a mark glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @return true if glyph definition table records glyph as a mark glyph; otherwise, false + */ + public boolean isMark ( int gi ) { + if ( gdef != null ) { + return gdef.isGlyphClass ( gi, GlyphDefinitionTable.GLYPH_CLASS_MARK ); + } else { + return false; + } + } + + /** + * Determine if specified glyph is an ignored ligature glyph according to the governing + * glyph definition table. + * @param gi glyph index to test + * @param flags that apply to lookup in scope + * @return true if glyph definition table records glyph as a ligature glyph; otherwise, false + */ + public boolean isIgnoredMark ( int gi, int flags ) { + if ( ( flags & GlyphSubtable.LF_IGNORE_MARK ) != 0 ) { + return isMark ( gi ); + } else if ( ( flags & GlyphSubtable.LF_MARK_ATTACHMENT_TYPE ) != 0 ) { + int lac = ( flags & GlyphSubtable.LF_MARK_ATTACHMENT_TYPE ) >> 8; + int gac = gdef.getMarkAttachClass ( gi ); + return ( gac != lac ); + } else { + return false; + } + } + + /** + * Obtain an ignored glyph tester that corresponds to the specified lookup flags. + * @param flags lookup flags + * @return a glyph tester + */ + public GlyphTester getIgnoreTester ( int flags ) { + if ( ( flags & GlyphSubtable.LF_IGNORE_BASE ) != 0 ) { + if ( ( flags & (GlyphSubtable.LF_IGNORE_LIGATURE | GlyphSubtable.LF_IGNORE_MARK) ) == 0 ) { + return ignoreBase; + } else { + return getCombinedIgnoreTester ( flags ); + } + } + if ( ( flags & GlyphSubtable.LF_IGNORE_LIGATURE ) != 0 ) { + if ( ( flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_MARK) ) == 0 ) { + return ignoreLigature; + } else { + return getCombinedIgnoreTester ( flags ); + } + } + if ( ( flags & GlyphSubtable.LF_IGNORE_MARK ) != 0 ) { + if ( ( flags & (GlyphSubtable.LF_IGNORE_BASE | GlyphSubtable.LF_IGNORE_LIGATURE) ) == 0 ) { + return ignoreMark; + } else { + return getCombinedIgnoreTester ( flags ); + } + } + return null; + } + + /** + * Obtain an ignored glyph tester that corresponds to the specified multiple (combined) lookup flags. + * @param flags lookup flags + * @return a glyph tester + */ + public GlyphTester getCombinedIgnoreTester ( int flags ) { + GlyphTester[] gta = new GlyphTester [ 3 ]; + int ngt = 0; + if ( ( flags & GlyphSubtable.LF_IGNORE_BASE ) != 0 ) { + gta [ ngt++ ] = ignoreBase; + } + if ( ( flags & GlyphSubtable.LF_IGNORE_LIGATURE ) != 0 ) { + gta [ ngt++ ] = ignoreLigature; + } + if ( ( flags & GlyphSubtable.LF_IGNORE_MARK ) != 0 ) { + gta [ ngt++ ] = ignoreMark; + } + return getCombinedOrTester ( gta, ngt ); + } + + /** + * Obtain an combined OR glyph tester. + * @param gta an array of glyph testers + * @param ngt number of glyph testers present in specified array + * @return a combined OR glyph tester + */ + public GlyphTester getCombinedOrTester ( GlyphTester[] gta, int ngt ) { + if ( ngt > 0 ) { + return new CombinedOrGlyphTester ( gta, ngt ); + } else { + return null; + } + } + + /** + * Obtain an combined AND glyph tester. + * @param gta an array of glyph testers + * @param ngt number of glyph testers present in specified array + * @return a combined AND glyph tester + */ + public GlyphTester getCombinedAndTester ( GlyphTester[] gta, int ngt ) { + if ( ngt > 0 ) { + return new CombinedAndGlyphTester ( gta, ngt ); + } else { + return null; + } + } + + /** combined OR glyph tester */ + private static class CombinedOrGlyphTester implements GlyphTester { + private GlyphTester[] gta; + private int ngt; + CombinedOrGlyphTester ( GlyphTester[] gta, int ngt ) { + this.gta = gta; + this.ngt = ngt; + } + /** {@inheritDoc} */ + public boolean test ( int gi, int flags ) { + for ( int i = 0, n = ngt; i < n; i++ ) { + GlyphTester gt = gta [ i ]; + if ( gt != null ) { + if ( gt.test ( gi, flags ) ) { + return true; + } + } + } + return false; + } + } + + /** combined AND glyph tester */ + private static class CombinedAndGlyphTester implements GlyphTester { + private GlyphTester[] gta; + private int ngt; + CombinedAndGlyphTester ( GlyphTester[] gta, int ngt ) { + this.gta = gta; + this.ngt = ngt; + } + /** {@inheritDoc} */ + public boolean test ( int gi, int flags ) { + for ( int i = 0, n = ngt; i < n; i++ ) { + GlyphTester gt = gta [ i ]; + if ( gt != null ) { + if ( ! gt.test ( gi, flags ) ) { + return false; + } + } + } + return true; + } + } + + /** NOT glyph tester */ + private static class NotGlyphTester implements GlyphTester { + private GlyphTester gt; + NotGlyphTester ( GlyphTester gt ) { + this.gt = gt; + } + /** {@inheritDoc} */ + public boolean test ( int gi, int flags ) { + if ( gt != null ) { + if ( gt.test ( gi, flags ) ) { + return false; + } + } + return true; + } + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitution.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitution.java new file mode 100644 index 000000000..b8f9d02bf --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitution.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +// CSOFF: LineLengthCheck + +/** + * The <code>GlyphSubstitution</code> interface is implemented by a glyph substitution subtable + * that supports the determination of glyph substitution information based on script and + * language of the corresponding character content. + * @author Glenn Adams + */ +public interface GlyphSubstitution { + + /** + * Perform glyph substitution at the current index, mutating the substitution state object as required. + * Only the context associated with the current index is processed. + * @param ss glyph substitution state object + * @return true if the glyph subtable was applied, meaning that the current context matches the + * associated input context glyph coverage table + */ + boolean substitute ( GlyphSubstitutionState ss ); + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitutionState.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitutionState.java new file mode 100644 index 000000000..4af8c371e --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitutionState.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck + +/** + * The <code>GlyphSubstitutionState</code> implements an state object used during glyph substitution + * processing. + * @author Glenn Adams + */ + +public class GlyphSubstitutionState extends GlyphProcessingState { + + /** alternates index */ + private int[] alternatesIndex; + /** current output glyph sequence */ + private IntBuffer ogb; + /** current output glyph to character associations */ + private List oal; + /** character association predications */ + private boolean predications; + + /** + * Construct glyph substitution state. + * @param gs input glyph sequence + * @param script script identifier + * @param language language identifier + * @param feature feature identifier + * @param sct script context tester (or null) + */ + public GlyphSubstitutionState ( GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct ) { + super ( gs, script, language, feature, sct ); + this.ogb = IntBuffer.allocate ( gs.getGlyphCount() ); + this.oal = new ArrayList ( gs.getGlyphCount() ); + this.predications = gs.getPredications(); + } + + /** + * Construct glyph substitution state using an existing state object using shallow copy + * except as follows: input glyph sequence is copied deep except for its characters array. + * @param ss existing positioning state to copy from + */ + public GlyphSubstitutionState ( GlyphSubstitutionState ss ) { + super ( ss ); + this.ogb = IntBuffer.allocate ( indexLast ); + this.oal = new ArrayList ( indexLast ); + } + + /** + * Set alternates indices. + * @param alternates array of alternates indices ordered by coverage index + */ + public void setAlternates ( int[] alternates ) { + this.alternatesIndex = alternates; + } + + /** + * Obtain alternates index associated with specified coverage index. An alternates + * index is used to select among stylistic alternates of a glyph at a particular + * coverage index. This information must be provided by the document itself (in the + * form of an extension attribute value), since a font has no way to determine which + * alternate the user desires. + * @param ci coverage index + * @return an alternates index + */ + public int getAlternatesIndex ( int ci ) { + if ( alternatesIndex == null ) { + return 0; + } else if ( ( ci < 0 ) || ( ci > alternatesIndex.length ) ) { + return 0; + } else { + return alternatesIndex [ ci ]; + } + } + + /** + * Put (write) glyph into glyph output buffer. + * @param glyph to write + * @param a character association that applies to glyph + * @param predication a predication value to add to association A if predications enabled + */ + public void putGlyph ( int glyph, GlyphSequence.CharAssociation a, Object predication ) { + if ( ! ogb.hasRemaining() ) { + ogb = growBuffer ( ogb ); + } + ogb.put ( glyph ); + if ( predications && ( predication != null ) ) { + a.setPredication ( feature, predication ); + } + oal.add ( a ); + } + + /** + * Put (write) array of glyphs into glyph output buffer. + * @param glyphs to write + * @param associations array of character associations that apply to glyphs + * @param predication optional predicaion object to be associated with glyphs' associations + */ + public void putGlyphs ( int[] glyphs, GlyphSequence.CharAssociation[] associations, Object predication ) { + assert glyphs != null; + assert associations != null; + assert associations.length >= glyphs.length; + for ( int i = 0, n = glyphs.length; i < n; i++ ) { + putGlyph ( glyphs [ i ], associations [ i ], predication ); + } + } + + /** + * Obtain output glyph sequence. + * @return newly constructed glyph sequence comprised of original + * characters, output glyphs, and output associations + */ + public GlyphSequence getOutput() { + int position = ogb.position(); + if ( position > 0 ) { + ogb.limit ( position ); + ogb.rewind(); + return new GlyphSequence ( igs.getCharacters(), ogb, oal ); + } else { + return igs; + } + } + + /** + * Apply substitution subtable to current state at current position (only), + * resulting in the consumption of zero or more input glyphs, and possibly + * replacing the current input glyphs starting at the current position, in + * which case it is possible that indexLast is altered to be either less than + * or greater than its value prior to this application. + * @param st the glyph substitution subtable to apply + * @return true if subtable applied, or false if it did not (e.g., its + * input coverage table did not match current input context) + */ + public boolean apply ( GlyphSubstitutionSubtable st ) { + assert st != null; + updateSubtableState ( st ); + boolean applied = st.substitute ( this ); + resetSubtableState(); + return applied; + } + + /** + * Apply a sequence of matched rule lookups to the <code>nig</code> input glyphs + * starting at the current position. If lookups are non-null and non-empty, then + * all input glyphs specified by <code>nig</code> are consumed irregardless of + * whether any specified lookup applied. + * @param lookups array of matched lookups (or null) + * @param nig number of glyphs in input sequence, starting at current position, to which + * the lookups are to apply, and to be consumed once the application has finished + * @return true if lookups are non-null and non-empty; otherwise, false + */ + public boolean apply ( GlyphTable.RuleLookup[] lookups, int nig ) { + // int nbg = index; + int nlg = indexLast - ( index + nig ); + int nog = 0; + if ( ( lookups != null ) && ( lookups.length > 0 ) ) { + // apply each rule lookup to extracted input glyph array + for ( int i = 0, n = lookups.length; i < n; i++ ) { + GlyphTable.RuleLookup l = lookups [ i ]; + if ( l != null ) { + GlyphTable.LookupTable lt = l.getLookup(); + if ( lt != null ) { + // perform substitution on a copy of previous state + GlyphSubstitutionState ss = new GlyphSubstitutionState ( this ); + // apply lookup table substitutions + GlyphSequence gs = lt.substitute ( ss, l.getSequenceIndex() ); + // replace current input sequence starting at current position with result + if ( replaceInput ( 0, -1, gs ) ) { + nog = gs.getGlyphCount() - nlg; + } + } + } + } + // output glyphs and associations + putGlyphs ( getGlyphs ( 0, nog, false, null, null, null ), getAssociations ( 0, nog, false, null, null, null ), null ); + // consume replaced input glyphs + consume ( nog ); + return true; + } else { + return false; + } + } + + /** + * Apply default application semantices; namely, consume one input glyph, + * writing that glyph (and its association) to the output glyphs (and associations). + */ + public void applyDefault() { + super.applyDefault(); + int gi = getGlyph(); + if ( gi != 65535 ) { + putGlyph ( gi, getAssociation(), null ); + } + } + + private static IntBuffer growBuffer ( IntBuffer ib ) { + int capacity = ib.capacity(); + int capacityNew = capacity * 2; + IntBuffer ibNew = IntBuffer.allocate ( capacityNew ); + ib.rewind(); + return ibNew.put ( ib ); + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitutionSubtable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitutionSubtable.java new file mode 100644 index 000000000..d111b465f --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitutionSubtable.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck + +/** + * The <code>GlyphSubstitutionSubtable</code> implements an abstract base of a glyph substitution subtable, + * providing a default implementation of the <code>GlyphSubstitution</code> interface. + * @author Glenn Adams + */ +public abstract class GlyphSubstitutionSubtable extends GlyphSubtable implements GlyphSubstitution { + + /** + * Instantiate a <code>GlyphSubstitutionSubtable</code>. + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage subtable coverage table + */ + protected GlyphSubstitutionSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage ) { + super ( id, sequence, flags, format, coverage ); + } + + /** {@inheritDoc} */ + public int getTableType() { + return GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION; + } + + /** {@inheritDoc} */ + public String getTypeName() { + return GlyphSubstitutionTable.getLookupTypeName ( getType() ); + } + + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof GlyphSubstitutionSubtable; + } + + /** {@inheritDoc} */ + public boolean usesReverseScan() { + return false; + } + + /** {@inheritDoc} */ + public boolean substitute ( GlyphSubstitutionState ss ) { + return false; + } + + /** + * Apply substitutions using specified state and subtable array. For each position in input sequence, + * apply subtables in order until some subtable applies or none remain. If no subtable applied or no + * input was consumed for a given position, then apply default action (copy input glyph and advance). + * If <code>sequenceIndex</code> is non-negative, then apply subtables only when current position + * matches <code>sequenceIndex</code> in relation to the starting position. Furthermore, upon + * successful application at <code>sequenceIndex</code>, then apply default action for all remaining + * glyphs in input sequence. + * @param ss substitution state + * @param sta array of subtables to apply + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return output glyph sequence + */ + public static final GlyphSequence substitute ( GlyphSubstitutionState ss, GlyphSubstitutionSubtable[] sta, int sequenceIndex ) { + int sequenceStart = ss.getPosition(); + boolean appliedOneShot = false; + while ( ss.hasNext() ) { + boolean applied = false; + if ( ! appliedOneShot && ss.maybeApplicable() ) { + for ( int i = 0, n = sta.length; ! applied && ( i < n ); i++ ) { + if ( sequenceIndex < 0 ) { + applied = ss.apply ( sta [ i ] ); + } else if ( ss.getPosition() == ( sequenceStart + sequenceIndex ) ) { + applied = ss.apply ( sta [ i ] ); + if ( applied ) { + appliedOneShot = true; + } + } + } + } + if ( ! applied || ! ss.didConsume() ) { + ss.applyDefault(); + } + ss.next(); + } + return ss.getOutput(); + } + + /** + * Apply substitutions. + * @param gs input glyph sequence + * @param script tag + * @param language tag + * @param feature tag + * @param sta subtable array + * @param sct script context tester + * @return output glyph sequence + */ + public static final GlyphSequence substitute ( GlyphSequence gs, String script, String language, String feature, GlyphSubstitutionSubtable[] sta, ScriptContextTester sct ) { + return substitute ( new GlyphSubstitutionState ( gs, script, language, feature, sct ), sta, -1 ); + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitutionTable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitutionTable.java new file mode 100644 index 000000000..1b724a63b --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubstitutionTable.java @@ -0,0 +1,1474 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.nio.CharBuffer; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.scripts.ScriptProcessor; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.GlyphTester; + +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck + +/** + * The <code>GlyphSubstitutionTable</code> class is a glyph table that implements + * <code>GlyphSubstitution</code> functionality. + * @author Glenn Adams + */ +public class GlyphSubstitutionTable extends GlyphTable { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GlyphSubstitutionTable.class); // CSOK: ConstantNameCheck + + /** single substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_SINGLE = 1; + /** multiple substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_MULTIPLE = 2; + /** alternate substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_ALTERNATE = 3; + /** ligature substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_LIGATURE = 4; + /** contextual substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_CONTEXTUAL = 5; + /** chained contextual substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL = 6; + /** extension substitution substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION = 7; + /** reverse chained contextual single substitution subtable type */ + public static final int GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE = 8; + + /** + * Instantiate a <code>GlyphSubstitutionTable</code> object using the specified lookups + * and subtables. + * @param gdef glyph definition table that applies + * @param lookups a map of lookup specifications to subtable identifier strings + * @param subtables a list of identified subtables + */ + public GlyphSubstitutionTable ( GlyphDefinitionTable gdef, Map lookups, List subtables ) { + super ( gdef, lookups ); + if ( ( subtables == null ) || ( subtables.size() == 0 ) ) { + throw new AdvancedTypographicTableFormatException ( "subtables must be non-empty" ); + } else { + for ( Iterator it = subtables.iterator(); it.hasNext();) { + Object o = it.next(); + if ( o instanceof GlyphSubstitutionSubtable ) { + addSubtable ( (GlyphSubtable) o ); + } else { + throw new AdvancedTypographicTableFormatException ( "subtable must be a glyph substitution subtable" ); + } + } + freezeSubtables(); + } + } + + /** + * Perform substitution processing using all matching lookups. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute ( GlyphSequence gs, String script, String language ) { + GlyphSequence ogs; + Map/*<LookupSpec,List<LookupTable>>*/ lookups = matchLookups ( script, language, "*" ); + if ( ( lookups != null ) && ( lookups.size() > 0 ) ) { + ScriptProcessor sp = ScriptProcessor.getInstance ( script ); + ogs = sp.substitute ( this, gs, script, language, lookups ); + } else { + ogs = gs; + } + return ogs; + } + + /** + * Map a lookup type name to its constant (integer) value. + * @param name lookup type name + * @return lookup type + */ + public static int getLookupTypeFromName ( String name ) { + int t; + String s = name.toLowerCase(); + if ( "single".equals ( s ) ) { + t = GSUB_LOOKUP_TYPE_SINGLE; + } else if ( "multiple".equals ( s ) ) { + t = GSUB_LOOKUP_TYPE_MULTIPLE; + } else if ( "alternate".equals ( s ) ) { + t = GSUB_LOOKUP_TYPE_ALTERNATE; + } else if ( "ligature".equals ( s ) ) { + t = GSUB_LOOKUP_TYPE_LIGATURE; + } else if ( "contextual".equals ( s ) ) { + t = GSUB_LOOKUP_TYPE_CONTEXTUAL; + } else if ( "chainedcontextual".equals ( s ) ) { + t = GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + } else if ( "extensionsubstitution".equals ( s ) ) { + t = GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION; + } else if ( "reversechainiingcontextualsingle".equals ( s ) ) { + t = GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE; + } else { + t = -1; + } + return t; + } + + /** + * Map a lookup type constant (integer) value to its name. + * @param type lookup type + * @return lookup type name + */ + public static String getLookupTypeName ( int type ) { + String tn = null; + switch ( type ) { + case GSUB_LOOKUP_TYPE_SINGLE: + tn = "single"; + break; + case GSUB_LOOKUP_TYPE_MULTIPLE: + tn = "multiple"; + break; + case GSUB_LOOKUP_TYPE_ALTERNATE: + tn = "alternate"; + break; + case GSUB_LOOKUP_TYPE_LIGATURE: + tn = "ligature"; + break; + case GSUB_LOOKUP_TYPE_CONTEXTUAL: + tn = "contextual"; + break; + case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL: + tn = "chainedcontextual"; + break; + case GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION: + tn = "extensionsubstitution"; + break; + case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE: + tn = "reversechainiingcontextualsingle"; + break; + default: + tn = "unknown"; + break; + } + return tn; + } + + /** + * Create a substitution subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage subtable coverage table + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + GlyphSubtable st = null; + switch ( type ) { + case GSUB_LOOKUP_TYPE_SINGLE: + st = SingleSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GSUB_LOOKUP_TYPE_MULTIPLE: + st = MultipleSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GSUB_LOOKUP_TYPE_ALTERNATE: + st = AlternateSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GSUB_LOOKUP_TYPE_LIGATURE: + st = LigatureSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GSUB_LOOKUP_TYPE_CONTEXTUAL: + st = ContextualSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL: + st = ChainedContextualSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + case GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE: + st = ReverseChainedSingleSubtable.create ( id, sequence, flags, format, coverage, entries ); + break; + default: + break; + } + return st; + } + + /** + * Create a substitution subtable according to the specified arguments. + * @param type subtable type + * @param id subtable identifier + * @param sequence subtable sequence + * @param flags subtable flags + * @param format subtable format + * @param coverage list of coverage table entries + * @param entries subtable entries + * @return a glyph subtable instance + */ + public static GlyphSubtable createSubtable ( int type, String id, int sequence, int flags, int format, List coverage, List entries ) { + return createSubtable ( type, id, sequence, flags, format, GlyphCoverageTable.createCoverageTable ( coverage ), entries ); + } + + private abstract static class SingleSubtable extends GlyphSubstitutionSubtable { + SingleSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GSUB_LOOKUP_TYPE_SINGLE; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof SingleSubtable; + } + /** {@inheritDoc} */ + public boolean substitute ( GlyphSubstitutionState ss ) { + int gi = ss.getGlyph(), ci; + if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) { + return false; + } else { + int go = getGlyphForCoverageIndex ( ci, gi ); + if ( ( go < 0 ) || ( go > 65535 ) ) { + go = 65535; + } + ss.putGlyph ( go, ss.getAssociation(), Boolean.TRUE ); + ss.consume(1); + return true; + } + } + /** + * Obtain glyph for coverage index. + * @param ci coverage index + * @param gi original glyph index + * @return substituted glyph value + * @throws IllegalArgumentException if coverage index is not valid + */ + public abstract int getGlyphForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException; + static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new SingleSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 2 ) { + return new SingleSubtableFormat2 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class SingleSubtableFormat1 extends SingleSubtable { + private int delta; + private int ciMax; + SingleSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + List entries = new ArrayList ( 1 ); + entries.add ( Integer.valueOf ( delta ) ); + return entries; + } + /** {@inheritDoc} */ + public int getGlyphForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException { + if ( ci <= ciMax ) { + return gi + delta; + } else { + throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + ciMax ); + } + } + private void populate ( List entries ) { + if ( ( entries == null ) || ( entries.size() != 1 ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null and contain exactly one entry" ); + } else { + Object o = entries.get(0); + int delta = 0; + if ( o instanceof Integer ) { + delta = ( (Integer) o ) . intValue(); + } else { + throw new AdvancedTypographicTableFormatException ( "illegal entries entry, must be Integer, but is: " + o ); + } + this.delta = delta; + this.ciMax = getCoverageSize() - 1; + } + } + } + + private static class SingleSubtableFormat2 extends SingleSubtable { + private int[] glyphs; + SingleSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + List entries = new ArrayList ( glyphs.length ); + for ( int i = 0, n = glyphs.length; i < n; i++ ) { + entries.add ( Integer.valueOf ( glyphs[i] ) ); + } + return entries; + } + /** {@inheritDoc} */ + public int getGlyphForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException { + if ( glyphs == null ) { + return -1; + } else if ( ci >= glyphs.length ) { + throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + glyphs.length ); + } else { + return glyphs [ ci ]; + } + } + private void populate ( List entries ) { + int i = 0, n = entries.size(); + int[] glyphs = new int [ n ]; + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( o instanceof Integer ) { + int gid = ( (Integer) o ) .intValue(); + if ( ( gid >= 0 ) && ( gid < 65536 ) ) { + glyphs [ i++ ] = gid; + } else { + throw new AdvancedTypographicTableFormatException ( "illegal glyph index: " + gid ); + } + } else { + throw new AdvancedTypographicTableFormatException ( "illegal entries entry, must be Integer: " + o ); + } + } + assert i == n; + assert this.glyphs == null; + this.glyphs = glyphs; + } + } + + private abstract static class MultipleSubtable extends GlyphSubstitutionSubtable { + public MultipleSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GSUB_LOOKUP_TYPE_MULTIPLE; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof MultipleSubtable; + } + /** {@inheritDoc} */ + public boolean substitute ( GlyphSubstitutionState ss ) { + int gi = ss.getGlyph(), ci; + if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) { + return false; + } else { + int[] ga = getGlyphsForCoverageIndex ( ci, gi ); + if ( ga != null ) { + ss.putGlyphs ( ga, GlyphSequence.CharAssociation.replicate ( ss.getAssociation(), ga.length ), Boolean.TRUE ); + ss.consume(1); + } + return true; + } + } + /** + * Obtain glyph sequence for coverage index. + * @param ci coverage index + * @param gi original glyph index + * @return sequence of glyphs to substitute for input glyph + * @throws IllegalArgumentException if coverage index is not valid + */ + public abstract int[] getGlyphsForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException; + static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new MultipleSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class MultipleSubtableFormat1 extends MultipleSubtable { + private int[][] gsa; // glyph sequence array, ordered by coverage index + MultipleSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( gsa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( gsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public int[] getGlyphsForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException { + if ( gsa == null ) { + return null; + } else if ( ci >= gsa.length ) { + throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + gsa.length ); + } else { + return gsa [ ci ]; + } + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof int[][] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an int[][], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + gsa = (int[][]) o; + } + } + } + } + + private abstract static class AlternateSubtable extends GlyphSubstitutionSubtable { + public AlternateSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GSUB_LOOKUP_TYPE_ALTERNATE; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof AlternateSubtable; + } + /** {@inheritDoc} */ + public boolean substitute ( GlyphSubstitutionState ss ) { + int gi = ss.getGlyph(), ci; + if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) { + return false; + } else { + int[] ga = getAlternatesForCoverageIndex ( ci, gi ); + int ai = ss.getAlternatesIndex ( ci ); + int go; + if ( ( ai < 0 ) || ( ai >= ga.length ) ) { + go = gi; + } else { + go = ga [ ai ]; + } + if ( ( go < 0 ) || ( go > 65535 ) ) { + go = 65535; + } + ss.putGlyph ( go, ss.getAssociation(), Boolean.TRUE ); + ss.consume(1); + return true; + } + } + /** + * Obtain glyph alternates for coverage index. + * @param ci coverage index + * @param gi original glyph index + * @return sequence of glyphs to substitute for input glyph + * @throws IllegalArgumentException if coverage index is not valid + */ + public abstract int[] getAlternatesForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException; + static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new AlternateSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class AlternateSubtableFormat1 extends AlternateSubtable { + private int[][] gaa; // glyph alternates array, ordered by coverage index + AlternateSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + List entries = new ArrayList ( gaa.length ); + for ( int i = 0, n = gaa.length; i < n; i++ ) { + entries.add ( gaa[i] ); + } + return entries; + } + /** {@inheritDoc} */ + public int[] getAlternatesForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException { + if ( gaa == null ) { + return null; + } else if ( ci >= gaa.length ) { + throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + gaa.length ); + } else { + return gaa [ ci ]; + } + } + private void populate ( List entries ) { + int i = 0, n = entries.size(); + int[][] gaa = new int [ n ][]; + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( o instanceof int[] ) { + gaa [ i++ ] = (int[]) o; + } else { + throw new AdvancedTypographicTableFormatException ( "illegal entries entry, must be int[]: " + o ); + } + } + assert i == n; + assert this.gaa == null; + this.gaa = gaa; + } + } + + private abstract static class LigatureSubtable extends GlyphSubstitutionSubtable { + public LigatureSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GSUB_LOOKUP_TYPE_LIGATURE; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof LigatureSubtable; + } + /** {@inheritDoc} */ + public boolean substitute ( GlyphSubstitutionState ss ) { + int gi = ss.getGlyph(), ci; + if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) { + return false; + } else { + LigatureSet ls = getLigatureSetForCoverageIndex ( ci, gi ); + if ( ls != null ) { + boolean reverse = false; + GlyphTester ignores = ss.getIgnoreDefault(); + int[] counts = ss.getGlyphsAvailable ( 0, reverse, ignores ); + int nga = counts[0], ngi; + if ( nga > 1 ) { + int[] iga = ss.getGlyphs ( 0, nga, reverse, ignores, null, counts ); + Ligature l = findLigature ( ls, iga ); + if ( l != null ) { + int go = l.getLigature(); + if ( ( go < 0 ) || ( go > 65535 ) ) { + go = 65535; + } + int nmg = 1 + l.getNumComponents(); + // fetch matched number of component glyphs to determine matched and ignored count + ss.getGlyphs ( 0, nmg, reverse, ignores, null, counts ); + nga = counts[0]; + ngi = counts[1]; + // fetch associations of matched component glyphs + GlyphSequence.CharAssociation[] laa = ss.getAssociations ( 0, nga ); + // output ligature glyph and its association + ss.putGlyph ( go, GlyphSequence.CharAssociation.join ( laa ), Boolean.TRUE ); + // fetch and output ignored glyphs (if necessary) + if ( ngi > 0 ) { + ss.putGlyphs ( ss.getIgnoredGlyphs ( 0, ngi ), ss.getIgnoredAssociations ( 0, ngi ), null ); + } + ss.consume ( nga + ngi ); + } + } + } + return true; + } + } + private Ligature findLigature ( LigatureSet ls, int[] glyphs ) { + Ligature[] la = ls.getLigatures(); + int k = -1; + int maxComponents = -1; + for ( int i = 0, n = la.length; i < n; i++ ) { + Ligature l = la [ i ]; + if ( l.matchesComponents ( glyphs ) ) { + int nc = l.getNumComponents(); + if ( nc > maxComponents ) { + maxComponents = nc; + k = i; + } + } + } + if ( k >= 0 ) { + return la [ k ]; + } else { + return null; + } + } + /** + * Obtain ligature set for coverage index. + * @param ci coverage index + * @param gi original glyph index + * @return ligature set (or null if none defined) + * @throws IllegalArgumentException if coverage index is not valid + */ + public abstract LigatureSet getLigatureSetForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException; + static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new LigatureSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class LigatureSubtableFormat1 extends LigatureSubtable { + private LigatureSet[] ligatureSets; + public LigatureSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + List entries = new ArrayList ( ligatureSets.length ); + for ( int i = 0, n = ligatureSets.length; i < n; i++ ) { + entries.add ( ligatureSets[i] ); + } + return entries; + } + /** {@inheritDoc} */ + public LigatureSet getLigatureSetForCoverageIndex ( int ci, int gi ) throws IllegalArgumentException { + if ( ligatureSets == null ) { + return null; + } else if ( ci >= ligatureSets.length ) { + throw new IllegalArgumentException ( "coverage index " + ci + " out of range, maximum coverage index is " + ligatureSets.length ); + } else { + return ligatureSets [ ci ]; + } + } + private void populate ( List entries ) { + int i = 0, n = entries.size(); + LigatureSet[] ligatureSets = new LigatureSet [ n ]; + for ( Iterator it = entries.iterator(); it.hasNext();) { + Object o = it.next(); + if ( o instanceof LigatureSet ) { + ligatureSets [ i++ ] = (LigatureSet) o; + } else { + throw new AdvancedTypographicTableFormatException ( "illegal ligatures entry, must be LigatureSet: " + o ); + } + } + assert i == n; + assert this.ligatureSets == null; + this.ligatureSets = ligatureSets; + } + } + + private abstract static class ContextualSubtable extends GlyphSubstitutionSubtable { + public ContextualSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GSUB_LOOKUP_TYPE_CONTEXTUAL; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof ContextualSubtable; + } + /** {@inheritDoc} */ + public boolean substitute ( GlyphSubstitutionState ss ) { + int gi = ss.getGlyph(), ci; + if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) { + return false; + } else { + int[] rv = new int[1]; + RuleLookup[] la = getLookups ( ci, gi, ss, rv ); + if ( la != null ) { + ss.apply ( la, rv[0] ); + } + return true; + } + } + /** + * Obtain rule lookups set associated current input glyph context. + * @param ci coverage index of glyph at current position + * @param gi glyph index of glyph at current position + * @param ss glyph substitution state + * @param rv array of ints used to receive multiple return values, must be of length 1 or greater, + * where the first entry is used to return the input sequence length of the matched rule + * @return array of rule lookups or null if none applies + */ + public abstract RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv ); + static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new ContextualSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 2 ) { + return new ContextualSubtableFormat2 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 3 ) { + return new ContextualSubtableFormat3 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ContextualSubtableFormat1 extends ContextualSubtable { + private RuleSet[] rsa; // rule set array, ordered by glyph coverage index + ContextualSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv ) { + assert ss != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedGlyphSequenceRule ) ) { + ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r; + int[] iga = cr.getGlyphs ( gi ); + if ( matches ( ss, iga, 0, rv ) ) { + return r.getLookups(); + } + } + } + } + } + return null; + } + static boolean matches ( GlyphSubstitutionState ss, int[] glyphs, int offset, int[] rv ) { + if ( ( glyphs == null ) || ( glyphs.length == 0 ) ) { + return true; // match null or empty glyph sequence + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ss.getIgnoreDefault(); + int[] counts = ss.getGlyphsAvailable ( offset, reverse, ignores ); + int nga = counts[0]; + int ngm = glyphs.length; + if ( nga < ngm ) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ss.getGlyphs ( offset, ngm, reverse, ignores, null, counts ); + for ( int k = 0; k < ngm; k++ ) { + if ( ga [ k ] != glyphs [ k ] ) { + return false; // match fails at ga [ k ] + } + } + if ( rv != null ) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + } + } + } + } + + private static class ContextualSubtableFormat2 extends ContextualSubtable { + private GlyphClassTable cdt; // class def table + private int ngc; // class set count + private RuleSet[] rsa; // rule set array, ordered by class number [0...ngc - 1] + ContextualSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 3 ); + entries.add ( cdt ); + entries.add ( Integer.valueOf ( ngc ) ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv ) { + assert ss != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedClassSequenceRule ) ) { + ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r; + int[] ca = cr.getClasses ( cdt.getClassIndex ( gi, ss.getClassMatchSet ( gi ) ) ); + if ( matches ( ss, cdt, ca, 0, rv ) ) { + return r.getLookups(); + } + } + } + } + } + return null; + } + static boolean matches ( GlyphSubstitutionState ss, GlyphClassTable cdt, int[] classes, int offset, int[] rv ) { + if ( ( cdt == null ) || ( classes == null ) || ( classes.length == 0 ) ) { + return true; // match null class definitions, null or empty class sequence + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ss.getIgnoreDefault(); + int[] counts = ss.getGlyphsAvailable ( offset, reverse, ignores ); + int nga = counts[0]; + int ngm = classes.length; + if ( nga < ngm ) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ss.getGlyphs ( offset, ngm, reverse, ignores, null, counts ); + for ( int k = 0; k < ngm; k++ ) { + int gi = ga [ k ]; + int ms = ss.getClassMatchSet ( gi ); + int gc = cdt.getClassIndex ( gi, ms ); + if ( ( gc < 0 ) || ( gc >= cdt.getClassSize ( ms ) ) ) { + return false; // none or invalid class fails mat ch + } else if ( gc != classes [ k ] ) { + return false; // match fails at ga [ k ] + } + } + if ( rv != null ) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 3 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 3 entries" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + cdt = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(1) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, second entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + ngc = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(2) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, third entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + if ( rsa.length != ngc ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes" ); + } + } + } + } + } + + private static class ContextualSubtableFormat3 extends ContextualSubtable { + private RuleSet[] rsa; // rule set array, containing a single rule set + ContextualSubtableFormat3 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv ) { + assert ss != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedCoverageSequenceRule ) ) { + ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r; + GlyphCoverageTable[] gca = cr.getCoverages(); + if ( matches ( ss, gca, 0, rv ) ) { + return r.getLookups(); + } + } + } + } + } + return null; + } + static boolean matches ( GlyphSubstitutionState ss, GlyphCoverageTable[] gca, int offset, int[] rv ) { + if ( ( gca == null ) || ( gca.length == 0 ) ) { + return true; // match null or empty coverage array + } else { + boolean reverse = offset < 0; + GlyphTester ignores = ss.getIgnoreDefault(); + int[] counts = ss.getGlyphsAvailable ( offset, reverse, ignores ); + int nga = counts[0]; + int ngm = gca.length; + if ( nga < ngm ) { + return false; // insufficient glyphs available to match + } else { + int[] ga = ss.getGlyphs ( offset, ngm, reverse, ignores, null, counts ); + for ( int k = 0; k < ngm; k++ ) { + GlyphCoverageTable ct = gca [ k ]; + if ( ct != null ) { + if ( ct.getCoverageIndex ( ga [ k ] ) < 0 ) { + return false; // match fails at ga [ k ] + } + } + } + if ( rv != null ) { + rv[0] = counts[0] + counts[1]; + } + return true; // all glyphs match + } + } + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + } + } + } + } + + private abstract static class ChainedContextualSubtable extends GlyphSubstitutionSubtable { + public ChainedContextualSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof ChainedContextualSubtable; + } + /** {@inheritDoc} */ + public boolean substitute ( GlyphSubstitutionState ss ) { + int gi = ss.getGlyph(), ci; + if ( ( ci = getCoverageIndex ( gi ) ) < 0 ) { + return false; + } else { + int[] rv = new int[1]; + RuleLookup[] la = getLookups ( ci, gi, ss, rv ); + if ( la != null ) { + ss.apply ( la, rv[0] ); + return true; + } else { + return false; + } + } + } + /** + * Obtain rule lookups set associated current input glyph context. + * @param ci coverage index of glyph at current position + * @param gi glyph index of glyph at current position + * @param ss glyph substitution state + * @param rv array of ints used to receive multiple return values, must be of length 1 or greater + * @return array of rule lookups or null if none applies + */ + public abstract RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv ); + static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new ChainedContextualSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 2 ) { + return new ChainedContextualSubtableFormat2 ( id, sequence, flags, format, coverage, entries ); + } else if ( format == 3 ) { + return new ChainedContextualSubtableFormat3 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ChainedContextualSubtableFormat1 extends ChainedContextualSubtable { + private RuleSet[] rsa; // rule set array, ordered by glyph coverage index + ChainedContextualSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv ) { + assert ss != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedGlyphSequenceRule ) ) { + ChainedGlyphSequenceRule cr = (ChainedGlyphSequenceRule) r; + int[] iga = cr.getGlyphs ( gi ); + if ( matches ( ss, iga, 0, rv ) ) { + int[] bga = cr.getBacktrackGlyphs(); + if ( matches ( ss, bga, -1, null ) ) { + int[] lga = cr.getLookaheadGlyphs(); + if ( matches ( ss, lga, rv[0], null ) ) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + private boolean matches ( GlyphSubstitutionState ss, int[] glyphs, int offset, int[] rv ) { + return ContextualSubtableFormat1.matches ( ss, glyphs, offset, rv ); + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + } + } + } + } + + private static class ChainedContextualSubtableFormat2 extends ChainedContextualSubtable { + private GlyphClassTable icdt; // input class def table + private GlyphClassTable bcdt; // backtrack class def table + private GlyphClassTable lcdt; // lookahead class def table + private int ngc; // class set count + private RuleSet[] rsa; // rule set array, ordered by class number [0...ngc - 1] + ChainedContextualSubtableFormat2 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 5 ); + entries.add ( icdt ); + entries.add ( bcdt ); + entries.add ( lcdt ); + entries.add ( Integer.valueOf ( ngc ) ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv ) { + assert ss != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedClassSequenceRule ) ) { + ChainedClassSequenceRule cr = (ChainedClassSequenceRule) r; + int[] ica = cr.getClasses ( icdt.getClassIndex ( gi, ss.getClassMatchSet ( gi ) ) ); + if ( matches ( ss, icdt, ica, 0, rv ) ) { + int[] bca = cr.getBacktrackClasses(); + if ( matches ( ss, bcdt, bca, -1, null ) ) { + int[] lca = cr.getLookaheadClasses(); + if ( matches ( ss, lcdt, lca, rv[0], null ) ) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + private boolean matches ( GlyphSubstitutionState ss, GlyphClassTable cdt, int[] classes, int offset, int[] rv ) { + return ContextualSubtableFormat2.matches ( ss, cdt, classes, offset, rv ); + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 5 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 5 entries" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an GlyphClassTable, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + icdt = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(1) ) != null ) && ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, second entry must be an GlyphClassTable, but is: " + o.getClass() ); + } else { + bcdt = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(2) ) != null ) && ! ( o instanceof GlyphClassTable ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, third entry must be an GlyphClassTable, but is: " + o.getClass() ); + } else { + lcdt = (GlyphClassTable) o; + } + if ( ( ( o = entries.get(3) ) == null ) || ! ( o instanceof Integer ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fourth entry must be an Integer, but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + ngc = ((Integer)(o)).intValue(); + } + if ( ( ( o = entries.get(4) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, fifth entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + if ( rsa.length != ngc ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, RuleSet[] length is " + rsa.length + ", but expected " + ngc + " glyph classes" ); + } + } + } + } + } + + private static class ChainedContextualSubtableFormat3 extends ChainedContextualSubtable { + private RuleSet[] rsa; // rule set array, containing a single rule set + ChainedContextualSubtableFormat3 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + if ( rsa != null ) { + List entries = new ArrayList ( 1 ); + entries.add ( rsa ); + return entries; + } else { + return null; + } + } + /** {@inheritDoc} */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + GlyphTable.resolveLookupReferences ( rsa, lookupTables ); + } + /** {@inheritDoc} */ + public RuleLookup[] getLookups ( int ci, int gi, GlyphSubstitutionState ss, int[] rv ) { + assert ss != null; + assert ( rv != null ) && ( rv.length > 0 ); + assert rsa != null; + if ( rsa.length > 0 ) { + RuleSet rs = rsa [ 0 ]; + if ( rs != null ) { + Rule[] ra = rs.getRules(); + for ( int i = 0, n = ra.length; i < n; i++ ) { + Rule r = ra [ i ]; + if ( ( r != null ) && ( r instanceof ChainedCoverageSequenceRule ) ) { + ChainedCoverageSequenceRule cr = (ChainedCoverageSequenceRule) r; + GlyphCoverageTable[] igca = cr.getCoverages(); + if ( matches ( ss, igca, 0, rv ) ) { + GlyphCoverageTable[] bgca = cr.getBacktrackCoverages(); + if ( matches ( ss, bgca, -1, null ) ) { + GlyphCoverageTable[] lgca = cr.getLookaheadCoverages(); + if ( matches ( ss, lgca, rv[0], null ) ) { + return r.getLookups(); + } + } + } + } + } + } + } + return null; + } + private boolean matches ( GlyphSubstitutionState ss, GlyphCoverageTable[] gca, int offset, int[] rv ) { + return ContextualSubtableFormat3.matches ( ss, gca, offset, rv ); + } + private void populate ( List entries ) { + if ( entries == null ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, must be non-null" ); + } else if ( entries.size() != 1 ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, " + entries.size() + " entries present, but requires 1 entry" ); + } else { + Object o; + if ( ( ( o = entries.get(0) ) == null ) || ! ( o instanceof RuleSet[] ) ) { + throw new AdvancedTypographicTableFormatException ( "illegal entries, first entry must be an RuleSet[], but is: " + ( ( o != null ) ? o.getClass() : null ) ); + } else { + rsa = (RuleSet[]) o; + } + } + } + } + + private abstract static class ReverseChainedSingleSubtable extends GlyphSubstitutionSubtable { + public ReverseChainedSingleSubtable ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage ); + } + /** {@inheritDoc} */ + public int getType() { + return GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE; + } + /** {@inheritDoc} */ + public boolean isCompatible ( GlyphSubtable subtable ) { + return subtable instanceof ReverseChainedSingleSubtable; + } + /** {@inheritDoc} */ + public boolean usesReverseScan() { + return true; + } + static GlyphSubstitutionSubtable create ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + if ( format == 1 ) { + return new ReverseChainedSingleSubtableFormat1 ( id, sequence, flags, format, coverage, entries ); + } else { + throw new UnsupportedOperationException(); + } + } + } + + private static class ReverseChainedSingleSubtableFormat1 extends ReverseChainedSingleSubtable { + ReverseChainedSingleSubtableFormat1 ( String id, int sequence, int flags, int format, GlyphCoverageTable coverage, List entries ) { + super ( id, sequence, flags, format, coverage, entries ); + populate ( entries ); + } + /** {@inheritDoc} */ + public List getEntries() { + return null; + } + private void populate ( List entries ) { + } + } + + /** + * The <code>Ligature</code> class implements a ligature lookup result in terms of + * a ligature glyph (code) and the <emph>N+1...</emph> components that comprise the ligature, + * where the <emph>Nth</emph> component was consumed in the coverage table lookup mapping to + * this ligature instance. + */ + public static class Ligature { + + private final int ligature; // (resulting) ligature glyph + private final int[] components; // component glyph codes (note that first component is implied) + + /** + * Instantiate a ligature. + * @param ligature glyph id + * @param components sequence of <emph>N+1...</emph> component glyph (or character) identifiers + */ + public Ligature ( int ligature, int[] components ) { + if ( ( ligature < 0 ) || ( ligature > 65535 ) ) { + throw new AdvancedTypographicTableFormatException ( "invalid ligature glyph index: " + ligature ); + } else if ( components == null ) { + throw new AdvancedTypographicTableFormatException ( "invalid ligature components, must be non-null array" ); + } else { + for ( int i = 0, n = components.length; i < n; i++ ) { + int gc = components [ i ]; + if ( ( gc < 0 ) || ( gc > 65535 ) ) { + throw new AdvancedTypographicTableFormatException ( "invalid component glyph index: " + gc ); + } + } + this.ligature = ligature; + this.components = components; + } + } + + /** @return ligature glyph id */ + public int getLigature() { + return ligature; + } + + /** @return array of <emph>N+1...</emph> components */ + public int[] getComponents() { + return components; + } + + /** @return components count */ + public int getNumComponents() { + return components.length; + } + + /** + * Determine if input sequence at offset matches ligature's components. + * @param glyphs array of glyph components to match (including first, implied glyph) + * @return true if matches + */ + public boolean matchesComponents ( int[] glyphs ) { + if ( glyphs.length < ( components.length + 1 ) ) { + return false; + } else { + for ( int i = 0, n = components.length; i < n; i++ ) { + if ( glyphs [ i + 1 ] != components [ i ] ) { + return false; + } + } + return true; + } + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("{components={"); + for ( int i = 0, n = components.length; i < n; i++ ) { + if ( i > 0 ) { + sb.append(','); + } + sb.append(Integer.toString(components[i])); + } + sb.append("},ligature="); + sb.append(Integer.toString(ligature)); + sb.append("}"); + return sb.toString(); + } + + } + + /** + * The <code>LigatureSet</code> class implements a set of ligatures. + */ + public static class LigatureSet { + + private final Ligature[] ligatures; // set of ligatures all of which share the first (implied) component + private final int maxComponents; // maximum number of components (including first) + + /** + * Instantiate a set of ligatures. + * @param ligatures collection of ligatures + */ + public LigatureSet ( List ligatures ) { + this ( (Ligature[]) ligatures.toArray ( new Ligature [ ligatures.size() ] ) ); + } + + /** + * Instantiate a set of ligatures. + * @param ligatures array of ligatures + */ + public LigatureSet ( Ligature[] ligatures ) { + if ( ligatures == null ) { + throw new AdvancedTypographicTableFormatException ( "invalid ligatures, must be non-null array" ); + } else { + this.ligatures = ligatures; + int ncMax = -1; + for ( int i = 0, n = ligatures.length; i < n; i++ ) { + Ligature l = ligatures [ i ]; + int nc = l.getNumComponents() + 1; + if ( nc > ncMax ) { + ncMax = nc; + } + } + maxComponents = ncMax; + } + } + + /** @return array of ligatures in this ligature set */ + public Ligature[] getLigatures() { + return ligatures; + } + + /** @return count of ligatures in this ligature set */ + public int getNumLigatures() { + return ligatures.length; + } + + /** @return maximum number of components in one ligature (including first component) */ + public int getMaxComponents() { + return maxComponents; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("{ligs={"); + for ( int i = 0, n = ligatures.length; i < n; i++ ) { + if ( i > 0 ) { + sb.append(','); + } + sb.append(ligatures[i]); + } + sb.append("}}"); + return sb.toString(); + } + + } + +} + diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphSubtable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubtable.java new file mode 100644 index 000000000..ba0141e44 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphSubtable.java @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.lang.ref.WeakReference; + +import java.util.List; +import java.util.Map; + +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck + +/** + * The <code>GlyphSubtable</code> implements an abstract glyph subtable that + * encapsulates identification, type, format, and coverage information. + * @author Glenn Adams + */ +public abstract class GlyphSubtable implements Comparable { + + /** lookup flag - right to left */ + public static final int LF_RIGHT_TO_LEFT = 0x0001; + /** lookup flag - ignore base glyphs */ + public static final int LF_IGNORE_BASE = 0x0002; + /** lookup flag - ignore ligatures */ + public static final int LF_IGNORE_LIGATURE = 0x0004; + /** lookup flag - ignore marks */ + public static final int LF_IGNORE_MARK = 0x0008; + /** lookup flag - use mark filtering set */ + public static final int LF_USE_MARK_FILTERING_SET = 0x0010; + /** lookup flag - reserved */ + public static final int LF_RESERVED = 0x0E00; + /** lookup flag - mark attachment type */ + public static final int LF_MARK_ATTACHMENT_TYPE = 0xFF00; + /** internal flag - use reverse scan */ + public static final int LF_INTERNAL_USE_REVERSE_SCAN = 0x10000; + + /** lookup identifier, having form of "lu%d" where %d is index of lookup in lookup list; shared by multiple subtables in a single lookup */ + private String lookupId; + /** subtable sequence (index) number in lookup, zero based */ + private int sequence; + /** subtable flags */ + private int flags; + /** subtable format */ + private int format; + /** subtable mapping table */ + private GlyphMappingTable mapping; + /** weak reference to parent (gsub or gpos) table */ + private WeakReference table; + + /** + * Instantiate this glyph subtable. + * @param lookupId lookup identifier, having form of "lu%d" where %d is index of lookup in lookup list + * @param sequence subtable sequence (within lookup), starting with zero + * @param flags subtable flags + * @param format subtable format + * @param mapping subtable mapping table + */ + protected GlyphSubtable ( String lookupId, int sequence, int flags, int format, GlyphMappingTable mapping ) + { + if ( ( lookupId == null ) || ( lookupId.length() == 0 ) ) { + throw new AdvancedTypographicTableFormatException ( "invalid lookup identifier, must be non-empty string" ); + } else if ( mapping == null ) { + throw new AdvancedTypographicTableFormatException ( "invalid mapping table, must not be null" ); + } else { + this.lookupId = lookupId; + this.sequence = sequence; + this.flags = flags; + this.format = format; + this.mapping = mapping; + } + } + + /** @return this subtable's lookup identifer */ + public String getLookupId() { + return lookupId; + } + + /** @return this subtable's table type */ + public abstract int getTableType(); + + /** @return this subtable's type */ + public abstract int getType(); + + /** @return this subtable's type name */ + public abstract String getTypeName(); + + /** + * Determine if a glyph subtable is compatible with this glyph subtable. Two glyph subtables are + * compatible if the both may appear in a single lookup table. + * @param subtable a glyph subtable to determine compatibility + * @return true if specified subtable is compatible with this glyph subtable, where by compatible + * is meant that they share the same lookup type + */ + public abstract boolean isCompatible ( GlyphSubtable subtable ); + + /** @return true if subtable uses reverse scanning of glyph sequence, meaning from the last glyph + * in a glyph sequence to the first glyph + */ + public abstract boolean usesReverseScan(); + + /** @return this subtable's sequence (index) within lookup */ + public int getSequence() { + return sequence; + } + + /** @return this subtable's flags */ + public int getFlags() { + return flags; + } + + /** @return this subtable's format */ + public int getFormat() { + return format; + } + + /** @return this subtable's governing glyph definition table or null if none available */ + public GlyphDefinitionTable getGDEF() { + GlyphTable gt = getTable(); + if ( gt != null ) { + return gt.getGlyphDefinitions(); + } else { + return null; + } + } + + /** @return this subtable's coverage mapping or null if mapping is not a coverage mapping */ + public GlyphCoverageMapping getCoverage() { + if ( mapping instanceof GlyphCoverageMapping ) { + return (GlyphCoverageMapping) mapping; + } else { + return null; + } + } + + /** @return this subtable's class mapping or null if mapping is not a class mapping */ + public GlyphClassMapping getClasses() { + if ( mapping instanceof GlyphClassMapping ) { + return (GlyphClassMapping) mapping; + } else { + return null; + } + } + + /** @return this subtable's lookup entries */ + public abstract List getEntries(); + + /** @return this subtable's parent table (or null if undefined) */ + public synchronized GlyphTable getTable() { + WeakReference r = this.table; + return ( r != null ) ? (GlyphTable) r.get() : null; + } + + /** + * Establish a weak reference from this subtable to its parent + * table. If table parameter is specified as <code>null</code>, then + * clear and remove weak reference. + * @param table the table or null + * @throws IllegalStateException if table is already set to non-null + */ + public synchronized void setTable ( GlyphTable table ) throws IllegalStateException { + WeakReference r = this.table; + if ( table == null ) { + this.table = null; + if ( r != null ) { + r.clear(); + } + } else if ( r == null ) { + this.table = new WeakReference ( table ); + } else { + throw new IllegalStateException ( "table already set" ); + } + } + + /** + * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void resolveLookupReferences ( Map/*<String,GlyphTable.LookupTable>*/ lookupTables ) { + } + + /** + * Map glyph id to coverage index. + * @param gid glyph id + * @return the corresponding coverage index of the specified glyph id + */ + public int getCoverageIndex ( int gid ) { + if ( mapping instanceof GlyphCoverageMapping ) { + return ( (GlyphCoverageMapping) mapping ) .getCoverageIndex ( gid ); + } else { + return -1; + } + } + + /** + * Map glyph id to coverage index. + * @return the corresponding coverage index of the specified glyph id + */ + public int getCoverageSize() { + if ( mapping instanceof GlyphCoverageMapping ) { + return ( (GlyphCoverageMapping) mapping ) .getCoverageSize(); + } else { + return 0; + } + } + + /** {@inheritDoc} */ + public int hashCode() { + int hc = sequence; + hc = ( hc * 3 ) + ( lookupId.hashCode() ^ hc ); + return hc; + } + + /** + * {@inheritDoc} + * @return true if the lookup identifier and the sequence number of the specified subtable is the same + * as the lookup identifier and sequence number of this subtable + */ + public boolean equals ( Object o ) { + if ( o instanceof GlyphSubtable ) { + GlyphSubtable st = (GlyphSubtable) o; + return lookupId.equals ( st.lookupId ) && ( sequence == st.sequence ); + } else { + return false; + } + } + + /** + * {@inheritDoc} + * @return the result of comparing the lookup identifier and the sequence number of the specified subtable with + * the lookup identifier and sequence number of this subtable + */ + public int compareTo ( Object o ) { + int d; + if ( o instanceof GlyphSubtable ) { + GlyphSubtable st = (GlyphSubtable) o; + if ( ( d = lookupId.compareTo ( st.lookupId ) ) == 0 ) { + if ( sequence < st.sequence ) { + d = -1; + } else if ( sequence > st.sequence ) { + d = 1; + } + } + } else { + d = -1; + } + return d; + } + + /** + * Determine if any of the specified subtables uses reverse scanning. + * @param subtables array of glyph subtables + * @return true if any of the specified subtables uses reverse scanning. + */ + public static boolean usesReverseScan ( GlyphSubtable[] subtables ) { + if ( ( subtables == null ) || ( subtables.length == 0 ) ) { + return false; + } else { + for ( int i = 0, n = subtables.length; i < n; i++ ) { + if ( subtables[i].usesReverseScan() ) { + return true; + } + } + return false; + } + } + + /** + * Determine consistent flags for a set of subtables. + * @param subtables array of glyph subtables + * @return consistent flags + * @throws IllegalStateException if inconsistent flags + */ + public static int getFlags ( GlyphSubtable[] subtables ) throws IllegalStateException { + if ( ( subtables == null ) || ( subtables.length == 0 ) ) { + return 0; + } else { + int flags = 0; + // obtain first non-zero value of flags in array of subtables + for ( int i = 0, n = subtables.length; i < n; i++ ) { + int f = subtables[i].getFlags(); + if ( flags == 0 ) { + flags = f; + break; + } + } + // enforce flag consistency + for ( int i = 0, n = subtables.length; i < n; i++ ) { + int f = subtables[i].getFlags(); + if ( f != flags ) { + throw new IllegalStateException ( "inconsistent lookup flags " + f + ", expected " + flags ); + } + } + return flags | ( usesReverseScan ( subtables ) ? LF_INTERNAL_USE_REVERSE_SCAN : 0 ); + } + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/GlyphTable.java b/src/java/org/apache/fop/complexscripts/fonts/GlyphTable.java new file mode 100644 index 000000000..91f8d4924 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/GlyphTable.java @@ -0,0 +1,1300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: EmptyForIteratorPadCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: ParameterNumberCheck +// CSOFF: SimplifyBooleanReturnCheck + +/** + * Base class for all advanced typographic glyph tables. + * @author Glenn Adams + */ +public class GlyphTable { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GlyphTable.class); // CSOK: ConstantNameCheck + + /** substitution glyph table type */ + public static final int GLYPH_TABLE_TYPE_SUBSTITUTION = 1; + /** positioning glyph table type */ + public static final int GLYPH_TABLE_TYPE_POSITIONING = 2; + /** justification glyph table type */ + public static final int GLYPH_TABLE_TYPE_JUSTIFICATION = 3; + /** baseline glyph table type */ + public static final int GLYPH_TABLE_TYPE_BASELINE = 4; + /** definition glyph table type */ + public static final int GLYPH_TABLE_TYPE_DEFINITION = 5; + + // (optional) glyph definition table in table types other than glyph definition table + private GlyphTable gdef; + + // map from lookup specs to lists of strings, each of which identifies a lookup table (consisting of one or more subtables) + private Map/*<LookupSpec,List<String>>*/ lookups; + + // map from lookup identifiers to lookup tables + private Map/*<String,LookupTable>*/ lookupTables; + + // if true, then prevent further subtable addition + private boolean frozen; + + /** + * Instantiate glyph table with specified lookups. + * @param gdef glyph definition table that applies + * @param lookups map from lookup specs to lookup tables + */ + public GlyphTable ( GlyphTable gdef, Map/*<LookupSpec,List<String>>*/ lookups ) { + if ( ( gdef != null ) && ! ( gdef instanceof GlyphDefinitionTable ) ) { + throw new AdvancedTypographicTableFormatException ( "bad glyph definition table" ); + } else if ( lookups == null ) { + throw new AdvancedTypographicTableFormatException ( "lookups must be non-null map" ); + } else { + this.gdef = gdef; + this.lookups = lookups; + this.lookupTables = new LinkedHashMap/*<String,List<LookupTable>>*/(); + } + } + + /** + * Obtain glyph definition table. + * @return (possibly null) glyph definition table + */ + public GlyphDefinitionTable getGlyphDefinitions() { + return (GlyphDefinitionTable) gdef; + } + + /** + * Obtain list of all lookup specifications. + * @return (possibly empty) list of all lookup specifications + */ + public List/*<LookupSpec>*/ getLookups() { + return matchLookupSpecs ( "*", "*", "*" ); + } + + /** + * Obtain ordered list of all lookup tables, where order is by lookup identifier, which + * lexicographic ordering follows the lookup list order. + * @return (possibly empty) ordered list of all lookup tables + */ + public List/*<LookupTable>*/ getLookupTables() { + TreeSet/*<String>*/ lids = new TreeSet/*<String>*/ ( lookupTables.keySet() ); + List/*<LookupTable>*/ ltl = new ArrayList/*<LookupTable>*/ ( lids.size() ); + for ( Iterator it = lids.iterator(); it.hasNext(); ) { + String lid = (String) it.next(); + ltl.add ( lookupTables.get ( lid ) ); + } + return ltl; + } + + /** + * Obtain lookup table by lookup id. This method is used by test code, and provides + * access to embedded lookups not normally accessed by {script, language, feature} lookup spec. + * @param lid lookup id + * @return table associated with lookup id or null if none + */ + public LookupTable getLookupTable ( String lid ) { + return (LookupTable) lookupTables.get ( lid ); + } + + /** + * Add a subtable. + * @param subtable a (non-null) glyph subtable + */ + protected void addSubtable ( GlyphSubtable subtable ) { + // ensure table is not frozen + if ( frozen ) { + throw new IllegalStateException ( "glyph table is frozen, subtable addition prohibited" ); + } + // set subtable's table reference to this table + subtable.setTable ( this ); + // add subtable to this table's subtable collection + String lid = subtable.getLookupId(); + if ( lookupTables.containsKey ( lid ) ) { + LookupTable lt = (LookupTable) lookupTables.get ( lid ); + lt.addSubtable ( subtable ); + } else { + LookupTable lt = new LookupTable ( lid, subtable ); + lookupTables.put ( lid, lt ); + } + } + + /** + * Freeze subtables, i.e., do not allow further subtable addition, and + * create resulting cached state. + */ + protected void freezeSubtables() { + if ( ! frozen ) { + for ( Iterator it = lookupTables.values().iterator(); it.hasNext(); ) { + LookupTable lt = (LookupTable) it.next(); + lt.freezeSubtables ( lookupTables ); + } + frozen = true; + } + } + + /** + * Match lookup specifications according to <script,language,feature> tuple, where + * '*' is a wildcard for a tuple component. + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + * @return a (possibly empty) array of matching lookup specifications + */ + public List/*<LookupSpec>*/ matchLookupSpecs ( String script, String language, String feature ) { + Set/*<LookupSpec>*/ keys = lookups.keySet(); + List/*<LookupSpec>*/ matches = new ArrayList/*<LookupSpec>*/(); + for ( Iterator it = keys.iterator(); it.hasNext();) { + LookupSpec ls = (LookupSpec) it.next(); + if ( ! "*".equals(script) ) { + if ( ! ls.getScript().equals ( script ) ) { + continue; + } + } + if ( ! "*".equals(language) ) { + if ( ! ls.getLanguage().equals ( language ) ) { + continue; + } + } + if ( ! "*".equals(feature) ) { + if ( ! ls.getFeature().equals ( feature ) ) { + continue; + } + } + matches.add ( ls ); + } + return matches; + } + + /** + * Match lookup specifications according to <script,language,feature> tuple, where + * '*' is a wildcard for a tuple component. + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + * @return a (possibly empty) map from matching lookup specifications to lists of corresponding lookup tables + */ + public Map/*<LookupSpec,List<LookupTable>>*/ matchLookups ( String script, String language, String feature ) { + List/*<LookupSpec>*/ lsl = matchLookupSpecs ( script, language, feature ); + Map lm = new LinkedHashMap(); + for ( Iterator it = lsl.iterator(); it.hasNext(); ) { + LookupSpec ls = (LookupSpec) it.next(); + lm.put ( ls, findLookupTables ( ls ) ); + } + return lm; + } + + /** + * Obtain ordered list of glyph lookup tables that match a specific lookup specification. + * @param ls a (non-null) lookup specification + * @return a (possibly empty) ordered list of lookup tables whose corresponding lookup specifications match the specified lookup spec + */ + public List/*<LookupTable>*/ findLookupTables ( LookupSpec ls ) { + TreeSet/*<LookupTable>*/ lts = new TreeSet/*<LookupTable>*/(); + List/*<String>*/ ids; + if ( ( ids = (List/*<String>*/) lookups.get ( ls ) ) != null ) { + for ( Iterator it = ids.iterator(); it.hasNext();) { + String lid = (String) it.next(); + LookupTable lt; + if ( ( lt = (LookupTable) lookupTables.get ( lid ) ) != null ) { + lts.add ( lt ); + } + } + } + return new ArrayList/*<LookupTable>*/ ( lts ); + } + + /** + * Assemble ordered array of lookup table use specifications according to the specified features and candidate lookups, + * where the order of the array is in accordance to the order of the applicable lookup list. + * @param features array of feature identifiers to apply + * @param lookups a mapping from lookup specifications to lists of look tables from which to select lookup tables according to the specified features + * @return ordered array of assembled lookup table use specifications + */ + public UseSpec[] assembleLookups ( String[] features, Map/*<LookupSpec,List<LookupTable>>*/ lookups ) { + TreeSet/*<UseSpec>*/ uss = new TreeSet/*<UseSpec>*/(); + for ( int i = 0, n = features.length; i < n; i++ ) { + String feature = features[i]; + for ( Iterator it = lookups.entrySet().iterator(); it.hasNext(); ) { + Map.Entry/*<LookupSpec,List<LookupTable>>*/ e = (Map.Entry/*<LookupSpec,List<LookupTable>>*/) it.next(); + LookupSpec ls = (LookupSpec) e.getKey(); + if ( ls.getFeature().equals ( feature ) ) { + List/*<LookupTable>*/ ltl = (List/*<LookupTable>*/) e.getValue(); + if ( ltl != null ) { + for ( Iterator ltit = ltl.iterator(); ltit.hasNext(); ) { + LookupTable lt = (LookupTable) ltit.next(); + uss.add ( new UseSpec ( lt, feature ) ); + } + } + } + } + } + return (UseSpec[]) uss.toArray ( new UseSpec [ uss.size() ] ); + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(super.toString()); + sb.append("{"); + sb.append("lookups={"); + sb.append(lookups.toString()); + sb.append("},lookupTables={"); + sb.append(lookupTables.toString()); + sb.append("}}"); + return sb.toString(); + } + + /** + * Obtain glyph table type from name. + * @param name of table type to map to type value + * @return glyph table type (as an integer constant) + */ + public static int getTableTypeFromName ( String name ) { + int t; + String s = name.toLowerCase(); + if ( "gsub".equals ( s ) ) { + t = GLYPH_TABLE_TYPE_SUBSTITUTION; + } else if ( "gpos".equals ( s ) ) { + t = GLYPH_TABLE_TYPE_POSITIONING; + } else if ( "jstf".equals ( s ) ) { + t = GLYPH_TABLE_TYPE_JUSTIFICATION; + } else if ( "base".equals ( s ) ) { + t = GLYPH_TABLE_TYPE_BASELINE; + } else if ( "gdef".equals ( s ) ) { + t = GLYPH_TABLE_TYPE_DEFINITION; + } else { + t = -1; + } + return t; + } + + /** + * Resolve references to lookup tables in a collection of rules sets. + * @param rsa array of rule sets + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public static void resolveLookupReferences ( RuleSet[] rsa, Map/*<String,LookupTable>*/ lookupTables ) { + if ( ( rsa != null ) && ( lookupTables != null ) ) { + for ( int i = 0, n = rsa.length; i < n; i++ ) { + RuleSet rs = rsa [ i ]; + if ( rs != null ) { + rs.resolveLookupReferences ( lookupTables ); + } + } + } + } + + /** + * A structure class encapsulating a lookup specification as a <script,language,feature> tuple. + */ + public static class LookupSpec implements Comparable { + + private final String script; + private final String language; + private final String feature; + + /** + * Instantiate lookup spec. + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + */ + public LookupSpec ( String script, String language, String feature ) { + if ( ( script == null ) || ( script.length() == 0 ) ) { + throw new AdvancedTypographicTableFormatException ( "script must be non-empty string" ); + } else if ( ( language == null ) || ( language.length() == 0 ) ) { + throw new AdvancedTypographicTableFormatException ( "language must be non-empty string" ); + } else if ( ( feature == null ) || ( feature.length() == 0 ) ) { + throw new AdvancedTypographicTableFormatException ( "feature must be non-empty string" ); + } else if ( script.equals("*") ) { + throw new AdvancedTypographicTableFormatException ( "script must not be wildcard" ); + } else if ( language.equals("*") ) { + throw new AdvancedTypographicTableFormatException ( "language must not be wildcard" ); + } else if ( feature.equals("*") ) { + throw new AdvancedTypographicTableFormatException ( "feature must not be wildcard" ); + } else { + this.script = script.trim(); + this.language = language.trim(); + this.feature = feature.trim(); + } + } + + /** @return script identifier */ + public String getScript() { + return script; + } + + /** @return language identifier */ + public String getLanguage() { + return language; + } + + /** @return feature identifier */ + public String getFeature() { + return feature; + } + + /** {@inheritDoc} */ + public int hashCode() { + int hc = 0; + hc = 7 * hc + ( hc ^ script.hashCode() ); + hc = 11 * hc + ( hc ^ language.hashCode() ); + hc = 17 * hc + ( hc ^ feature.hashCode() ); + return hc; + } + + /** {@inheritDoc} */ + public boolean equals ( Object o ) { + if ( o instanceof LookupSpec ) { + LookupSpec l = (LookupSpec) o; + if ( ! l.script.equals ( script ) ) { + return false; + } else if ( ! l.language.equals ( language ) ) { + return false; + } else if ( ! l.feature.equals ( feature ) ) { + return false; + } else { + return true; + } + } else { + return false; + } + } + + /** {@inheritDoc} */ + public int compareTo ( Object o ) { + int d; + if ( o instanceof LookupSpec ) { + LookupSpec ls = (LookupSpec) o; + if ( ( d = script.compareTo ( ls.script ) ) == 0 ) { + if ( ( d = language.compareTo ( ls.language ) ) == 0 ) { + if ( ( d = feature.compareTo ( ls.feature ) ) == 0 ) { + d = 0; + } + } + } + } else { + d = -1; + } + return d; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(super.toString()); + sb.append("{"); + sb.append("<'" + script + "'"); + sb.append(",'" + language + "'"); + sb.append(",'" + feature + "'"); + sb.append(">}"); + return sb.toString(); + } + + } + + /** + * The <code>LookupTable</code> class comprising an identifier and an ordered list + * of glyph subtables, each of which employ the same lookup identifier. + */ + public static class LookupTable implements Comparable { + + private final String id; // lookup identifiers + private final List/*<GlyphSubtable>*/ subtables; // list of subtables + private boolean doesSub; // performs substitutions + private boolean doesPos; // performs positioning + private boolean frozen; // if true, then don't permit further subtable additions + // frozen state + private GlyphSubtable[] subtablesArray; + private static GlyphSubtable[] subtablesArrayEmpty = new GlyphSubtable[0]; + + /** + * Instantiate a LookupTable. + * @param id the lookup table's identifier + * @param subtable an initial subtable (or null) + */ + public LookupTable ( String id, GlyphSubtable subtable ) { + this ( id, makeSingleton ( subtable ) ); + } + + /** + * Instantiate a LookupTable. + * @param id the lookup table's identifier + * @param subtables a pre-poplated list of subtables or null + */ + public LookupTable ( String id, List/*<GlyphSubtable>*/ subtables ) { + assert id != null; + assert id.length() != 0; + this.id = id; + this.subtables = new LinkedList/*<GlyphSubtable>*/(); + if ( subtables != null ) { + for ( Iterator it = subtables.iterator(); it.hasNext(); ) { + GlyphSubtable st = (GlyphSubtable) it.next(); + addSubtable ( st ); + } + } + } + + /** @return the identifier */ + public String getId() { + return id; + } + + /** @return the subtables as an array */ + public GlyphSubtable[] getSubtables() { + if ( frozen ) { + return ( subtablesArray != null ) ? subtablesArray : subtablesArrayEmpty; + } else { + if ( doesSub ) { + return (GlyphSubtable[]) subtables.toArray ( new GlyphSubstitutionSubtable [ subtables.size() ] ); + } else if ( doesPos ) { + return (GlyphSubtable[]) subtables.toArray ( new GlyphPositioningSubtable [ subtables.size() ] ); + } else { + return null; + } + } + } + + /** + * Add a subtable into this lookup table's collecion of subtables according to its + * natural order. + * @param subtable to add + * @return true if subtable was not already present, otherwise false + */ + public boolean addSubtable ( GlyphSubtable subtable ) { + boolean added = false; + // ensure table is not frozen + if ( frozen ) { + throw new IllegalStateException ( "glyph table is frozen, subtable addition prohibited" ); + } + // validate subtable to ensure consistency with current subtables + validateSubtable ( subtable ); + // insert subtable into ordered list + for ( ListIterator/*<GlyphSubtable>*/ lit = subtables.listIterator(0); lit.hasNext(); ) { + GlyphSubtable st = (GlyphSubtable) lit.next(); + int d; + if ( ( d = subtable.compareTo ( st ) ) < 0 ) { + // insert within list + lit.set ( subtable ); + lit.add ( st ); + added = true; + } else if ( d == 0 ) { + // duplicate entry is ignored + added = false; subtable = null; + } + } + // append at end of list + if ( ! added && ( subtable != null ) ) { + subtables.add ( subtable ); + added = true; + } + return added; + } + + private void validateSubtable ( GlyphSubtable subtable ) { + if ( subtable == null ) { + throw new AdvancedTypographicTableFormatException ( "subtable must be non-null" ); + } + if ( subtable instanceof GlyphSubstitutionSubtable ) { + if ( doesPos ) { + throw new AdvancedTypographicTableFormatException ( "subtable must be positioning subtable, but is: " + subtable ); + } else { + doesSub = true; + } + } + if ( subtable instanceof GlyphPositioningSubtable ) { + if ( doesSub ) { + throw new AdvancedTypographicTableFormatException ( "subtable must be substitution subtable, but is: " + subtable ); + } else { + doesPos = true; + } + } + if ( subtables.size() > 0 ) { + GlyphSubtable st = (GlyphSubtable) subtables.get(0); + if ( ! st.isCompatible ( subtable ) ) { + throw new AdvancedTypographicTableFormatException ( "subtable " + subtable + " is not compatible with subtable " + st ); + } + } + } + + /** + * Freeze subtables, i.e., do not allow further subtable addition, and + * create resulting cached state. In addition, resolve any references to + * lookup tables that appear in this lookup table's subtables. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void freezeSubtables ( Map/*<String,LookupTable>*/ lookupTables ) { + if ( ! frozen ) { + GlyphSubtable[] sta = getSubtables(); + resolveLookupReferences ( sta, lookupTables ); + this.subtablesArray = sta; + this.frozen = true; + } + } + + private void resolveLookupReferences ( GlyphSubtable[] subtables, Map/*<String,LookupTable>*/ lookupTables ) { + if ( subtables != null ) { + for ( int i = 0, n = subtables.length; i < n; i++ ) { + GlyphSubtable st = subtables [ i ]; + if ( st != null ) { + st.resolveLookupReferences ( lookupTables ); + } + } + } + } + + /** + * Determine if this glyph table performs substitution. + * @return true if it performs substitution + */ + public boolean performsSubstitution() { + return doesSub; + } + + /** + * Perform substitution processing using this lookup table's subtables. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + * @param sct a script specific context tester (or null) + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute ( GlyphSequence gs, String script, String language, String feature, ScriptContextTester sct ) { + if ( performsSubstitution() ) { + return GlyphSubstitutionSubtable.substitute ( gs, script, language, feature, (GlyphSubstitutionSubtable[]) subtablesArray, sct ); + } else { + return gs; + } + } + + /** + * Perform substitution processing on an existing glyph substitution state object using this lookup table's subtables. + * @param ss a glyph substitution state object + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute ( GlyphSubstitutionState ss, int sequenceIndex ) { + if ( performsSubstitution() ) { + return GlyphSubstitutionSubtable.substitute ( ss, (GlyphSubstitutionSubtable[]) subtablesArray, sequenceIndex ); + } else { + return ss.getInput(); + } + } + + /** + * Determine if this glyph table performs positioning. + * @return true if it performs positioning + */ + public boolean performsPositioning() { + return doesPos; + } + + /** + * Perform positioning processing using this lookup table's subtables. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param feature a feature identifier + * @param fontSize size in device units + * @param widths array of default advancements for each glyph in font + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @param sct a script specific context tester (or null) + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position ( GlyphSequence gs, String script, String language, String feature, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct ) { + if ( performsPositioning() ) { + return GlyphPositioningSubtable.position ( gs, script, language, feature, fontSize, (GlyphPositioningSubtable[]) subtablesArray, widths, adjustments, sct ); + } else { + return false; + } + } + + /** + * Perform positioning processing on an existing glyph positioning state object using this lookup table's subtables. + * @param ps a glyph positioning state object + * @param sequenceIndex if non negative, then apply subtables only at specified sequence index + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position ( GlyphPositioningState ps, int sequenceIndex ) { + if ( performsPositioning() ) { + return GlyphPositioningSubtable.position ( ps, (GlyphPositioningSubtable[]) subtablesArray, sequenceIndex ); + } else { + return false; + } + } + + /** {@inheritDoc} */ + public int hashCode() { + return id.hashCode(); + } + + /** + * {@inheritDoc} + * @return true if identifier of the specified lookup table is the same + * as the identifier of this lookup table + */ + public boolean equals ( Object o ) { + if ( o instanceof LookupTable ) { + LookupTable lt = (LookupTable) o; + return id.equals ( lt.id ); + } else { + return false; + } + } + + /** + * {@inheritDoc} + * @return the result of comparing the identifier of the specified lookup table with + * the identifier of this lookup table; lookup table identifiers take the form + * "lu(DIGIT)+", with comparison based on numerical ordering of numbers expressed by + * (DIGIT)+. + */ + public int compareTo ( Object o ) { + if ( o instanceof LookupTable ) { + LookupTable lt = (LookupTable) o; + assert id.startsWith ( "lu" ); + int i = Integer.parseInt ( id.substring ( 2 ) ); + assert lt.id.startsWith ( "lu" ); + int j = Integer.parseInt ( lt.id.substring ( 2 ) ); + if ( i < j ) { + return -1; + } else if ( i > j ) { + return 1; + } else { + return 0; + } + } else { + return -1; + } + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append ( "{ " ); + sb.append ( "id = " + id ); + sb.append ( ", subtables = " + subtables ); + sb.append ( " }" ); + return sb.toString(); + } + + private static List/*<GlyphSubtable>*/ makeSingleton ( GlyphSubtable subtable ) { + if ( subtable == null ) { + return null; + } else { + List/*<GlyphSubtable>*/ stl = new ArrayList/*<GlyphSubtable>*/ ( 1 ); + stl.add ( subtable ); + return stl; + } + } + + } + + /** + * The <code>UseSpec</code> class comprises a lookup table reference + * and the feature that selected the lookup table. + */ + public static class UseSpec implements Comparable { + + /** lookup table to apply */ + private final LookupTable lookupTable; + /** feature that caused selection of the lookup table */ + private final String feature; + + /** + * Construct a glyph lookup table use specification. + * @param lookupTable a glyph lookup table + * @param feature a feature that caused lookup table selection + */ + public UseSpec ( LookupTable lookupTable, String feature ) { + this.lookupTable = lookupTable; + this.feature = feature; + } + + /** @return the lookup table */ + public LookupTable getLookupTable() { + return lookupTable; + } + + /** @return the feature that selected this lookup table */ + public String getFeature() { + return feature; + } + + /** + * Perform substitution processing using this use specification's lookup table. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param sct a script specific context tester (or null) + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute ( GlyphSequence gs, String script, String language, ScriptContextTester sct ) { + return lookupTable.substitute ( gs, script, language, feature, sct ); + } + + /** + * Perform positioning processing using this use specification's lookup table. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param fontSize size in device units + * @param widths array of default advancements for each glyph in font + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @param sct a script specific context tester (or null) + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position ( GlyphSequence gs, String script, String language, int fontSize, int[] widths, int[][] adjustments, ScriptContextTester sct ) { + return lookupTable.position ( gs, script, language, feature, fontSize, widths, adjustments, sct ); + } + + /** {@inheritDoc} */ + public int hashCode() { + return lookupTable.hashCode(); + } + + /** {@inheritDoc} */ + public boolean equals ( Object o ) { + if ( o instanceof UseSpec ) { + UseSpec u = (UseSpec) o; + return lookupTable.equals ( u.lookupTable ); + } else { + return false; + } + } + + /** {@inheritDoc} */ + public int compareTo ( Object o ) { + if ( o instanceof UseSpec ) { + UseSpec u = (UseSpec) o; + return lookupTable.compareTo ( u.lookupTable ); + } else { + return -1; + } + } + + } + + /** + * The <code>RuleLookup</code> class implements a rule lookup record, comprising + * a glyph sequence index and a lookup table index (in an applicable lookup list). + */ + public static class RuleLookup { + + private final int sequenceIndex; // index into input glyph sequence + private final int lookupIndex; // lookup list index + private LookupTable lookup; // resolved lookup table + + /** + * Instantiate a RuleLookup. + * @param sequenceIndex the index into the input sequence + * @param lookupIndex the lookup table index + */ + public RuleLookup ( int sequenceIndex, int lookupIndex ) { + this.sequenceIndex = sequenceIndex; + this.lookupIndex = lookupIndex; + this.lookup = null; + } + + /** @return the sequence index */ + public int getSequenceIndex() { + return sequenceIndex; + } + + /** @return the lookup index */ + public int getLookupIndex() { + return lookupIndex; + } + + /** @return the lookup table */ + public LookupTable getLookup() { + return lookup; + } + + /** + * Resolve references to lookup tables. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + if ( lookupTables != null ) { + String lid = "lu" + Integer.toString ( lookupIndex ); + LookupTable lt = (LookupTable) lookupTables.get ( lid ); + if ( lt != null ) { + this.lookup = lt; + } else { + log.warn ( "unable to resolve glyph lookup table reference '" + lid + "' amongst lookup tables: " + lookupTables.values() ); + } + } + } + + /** {@inheritDoc} */ + public String toString() { + return "{ sequenceIndex = " + sequenceIndex + ", lookupIndex = " + lookupIndex + " }"; + } + + } + + /** + * The <code>Rule</code> class implements an array of rule lookup records. + */ + public abstract static class Rule { + + private final RuleLookup[] lookups; // rule lookups + private final int inputSequenceLength; // input sequence length + + /** + * Instantiate a Rule. + * @param lookups the rule's lookups + * @param inputSequenceLength the number of glyphs in the input sequence for this rule + */ + protected Rule ( RuleLookup[] lookups, int inputSequenceLength ) { + assert lookups != null; + this.lookups = lookups; + this.inputSequenceLength = inputSequenceLength; + } + + /** @return the lookups */ + public RuleLookup[] getLookups() { + return lookups; + } + + /** @return the input sequence length */ + public int getInputSequenceLength() { + return inputSequenceLength; + } + + /** + * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + if ( lookups != null ) { + for ( int i = 0, n = lookups.length; i < n; i++ ) { + RuleLookup l = lookups [ i ]; + if ( l != null ) { + l.resolveLookupReferences ( lookupTables ); + } + } + } + } + + /** {@inheritDoc} */ + public String toString() { + return "{ lookups = " + Arrays.toString ( lookups ) + ", inputSequenceLength = " + inputSequenceLength + " }"; + } + + } + + /** + * The <code>GlyphSequenceRule</code> class implements a subclass of <code>Rule</code> + * that supports matching on a specific glyph sequence. + */ + public static class GlyphSequenceRule extends Rule { + + private final int[] glyphs; // glyphs + + /** + * Instantiate a GlyphSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param glyphs the rule's glyph sequence to match, starting with second glyph in sequence + */ + public GlyphSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, int[] glyphs ) { + super ( lookups, inputSequenceLength ); + assert glyphs != null; + this.glyphs = glyphs; + } + + /** + * Obtain glyphs. N.B. that this array starts with the second + * glyph of the input sequence. + * @return the glyphs + */ + public int[] getGlyphs() { + return glyphs; + } + + /** + * Obtain glyphs augmented by specified first glyph entry. + * @param firstGlyph to fill in first glyph entry + * @return the glyphs augmented by first glyph + */ + public int[] getGlyphs ( int firstGlyph ) { + int[] ga = new int [ glyphs.length + 1 ]; + ga [ 0 ] = firstGlyph; + System.arraycopy ( glyphs, 0, ga, 1, glyphs.length ); + return ga; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append ( "{ " ); + sb.append ( "lookups = " + Arrays.toString ( getLookups() ) ); + sb.append ( ", glyphs = " + Arrays.toString ( glyphs ) ); + sb.append ( " }" ); + return sb.toString(); + } + + } + + /** + * The <code>ClassSequenceRule</code> class implements a subclass of <code>Rule</code> + * that supports matching on a specific glyph class sequence. + */ + public static class ClassSequenceRule extends Rule { + + private final int[] classes; // glyph classes + + /** + * Instantiate a ClassSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param classes the rule's glyph class sequence to match, starting with second glyph in sequence + */ + public ClassSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, int[] classes ) { + super ( lookups, inputSequenceLength ); + assert classes != null; + this.classes = classes; + } + + /** + * Obtain glyph classes. N.B. that this array starts with the class of the second + * glyph of the input sequence. + * @return the classes + */ + public int[] getClasses() { + return classes; + } + + /** + * Obtain glyph classes augmented by specified first class entry. + * @param firstClass to fill in first class entry + * @return the classes augmented by first class + */ + public int[] getClasses ( int firstClass ) { + int[] ca = new int [ classes.length + 1 ]; + ca [ 0 ] = firstClass; + System.arraycopy ( classes, 0, ca, 1, classes.length ); + return ca; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append ( "{ " ); + sb.append ( "lookups = " + Arrays.toString ( getLookups() ) ); + sb.append ( ", classes = " + Arrays.toString( classes ) ); + sb.append ( " }" ); + return sb.toString(); + } + + } + + /** + * The <code>CoverageSequenceRule</code> class implements a subclass of <code>Rule</code> + * that supports matching on a specific glyph coverage sequence. + */ + public static class CoverageSequenceRule extends Rule { + + private final GlyphCoverageTable[] coverages; // glyph coverages + + /** + * Instantiate a ClassSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param coverages the rule's glyph coverage sequence to match, starting with first glyph in sequence + */ + public CoverageSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages ) { + super ( lookups, inputSequenceLength ); + assert coverages != null; + this.coverages = coverages; + } + + /** @return the coverages */ + public GlyphCoverageTable[] getCoverages() { + return coverages; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append ( "{ " ); + sb.append ( "lookups = " + Arrays.toString ( getLookups() ) ); + sb.append ( ", coverages = " + Arrays.toString( coverages ) ); + sb.append ( " }" ); + return sb.toString(); + } + + } + + /** + * The <code>ChainedGlyphSequenceRule</code> class implements a subclass of <code>GlyphSequenceRule</code> + * that supports matching on a specific glyph sequence in a specific chained contextual. + */ + public static class ChainedGlyphSequenceRule extends GlyphSequenceRule { + + private final int[] backtrackGlyphs; // backtrack glyphs + private final int[] lookaheadGlyphs; // lookahead glyphs + + /** + * Instantiate a ChainedGlyphSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param glyphs the rule's input glyph sequence to match, starting with second glyph in sequence + * @param backtrackGlyphs the rule's backtrack glyph sequence to match, starting with first glyph in sequence + * @param lookaheadGlyphs the rule's lookahead glyph sequence to match, starting with first glyph in sequence + */ + public ChainedGlyphSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, int[] glyphs, int[] backtrackGlyphs, int[] lookaheadGlyphs ) { + super ( lookups, inputSequenceLength, glyphs ); + assert backtrackGlyphs != null; + assert lookaheadGlyphs != null; + this.backtrackGlyphs = backtrackGlyphs; + this.lookaheadGlyphs = lookaheadGlyphs; + } + + /** @return the backtrack glyphs */ + public int[] getBacktrackGlyphs() { + return backtrackGlyphs; + } + + /** @return the lookahead glyphs */ + public int[] getLookaheadGlyphs() { + return lookaheadGlyphs; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append ( "{ " ); + sb.append ( "lookups = " + Arrays.toString ( getLookups() ) ); + sb.append ( ", glyphs = " + Arrays.toString ( getGlyphs() ) ); + sb.append ( ", backtrackGlyphs = " + Arrays.toString ( backtrackGlyphs ) ); + sb.append ( ", lookaheadGlyphs = " + Arrays.toString ( lookaheadGlyphs ) ); + sb.append ( " }" ); + return sb.toString(); + } + + } + + /** + * The <code>ChainedClassSequenceRule</code> class implements a subclass of <code>ClassSequenceRule</code> + * that supports matching on a specific glyph class sequence in a specific chained contextual. + */ + public static class ChainedClassSequenceRule extends ClassSequenceRule { + + private final int[] backtrackClasses; // backtrack classes + private final int[] lookaheadClasses; // lookahead classes + + /** + * Instantiate a ChainedClassSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param classes the rule's input glyph class sequence to match, starting with second glyph in sequence + * @param backtrackClasses the rule's backtrack glyph class sequence to match, starting with first glyph in sequence + * @param lookaheadClasses the rule's lookahead glyph class sequence to match, starting with first glyph in sequence + */ + public ChainedClassSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, int[] classes, int[] backtrackClasses, int[] lookaheadClasses ) { + super ( lookups, inputSequenceLength, classes ); + assert backtrackClasses != null; + assert lookaheadClasses != null; + this.backtrackClasses = backtrackClasses; + this.lookaheadClasses = lookaheadClasses; + } + + /** @return the backtrack classes */ + public int[] getBacktrackClasses() { + return backtrackClasses; + } + + /** @return the lookahead classes */ + public int[] getLookaheadClasses() { + return lookaheadClasses; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append ( "{ " ); + sb.append ( "lookups = " + Arrays.toString ( getLookups() ) ); + sb.append ( ", classes = " + Arrays.toString ( getClasses() ) ); + sb.append ( ", backtrackClasses = " + Arrays.toString ( backtrackClasses ) ); + sb.append ( ", lookaheadClasses = " + Arrays.toString ( lookaheadClasses ) ); + sb.append ( " }" ); + return sb.toString(); + } + + } + + /** + * The <code>ChainedCoverageSequenceRule</code> class implements a subclass of <code>CoverageSequenceRule</code> + * that supports matching on a specific glyph class sequence in a specific chained contextual. + */ + public static class ChainedCoverageSequenceRule extends CoverageSequenceRule { + + private final GlyphCoverageTable[] backtrackCoverages; // backtrack coverages + private final GlyphCoverageTable[] lookaheadCoverages; // lookahead coverages + + /** + * Instantiate a ChainedCoverageSequenceRule. + * @param lookups the rule's lookups + * @param inputSequenceLength number of glyphs constituting input sequence (to be consumed) + * @param coverages the rule's input glyph class sequence to match, starting with first glyph in sequence + * @param backtrackCoverages the rule's backtrack glyph class sequence to match, starting with first glyph in sequence + * @param lookaheadCoverages the rule's lookahead glyph class sequence to match, starting with first glyph in sequence + */ + public ChainedCoverageSequenceRule ( RuleLookup[] lookups, int inputSequenceLength, GlyphCoverageTable[] coverages, GlyphCoverageTable[] backtrackCoverages, GlyphCoverageTable[] lookaheadCoverages ) { + super ( lookups, inputSequenceLength, coverages ); + assert backtrackCoverages != null; + assert lookaheadCoverages != null; + this.backtrackCoverages = backtrackCoverages; + this.lookaheadCoverages = lookaheadCoverages; + } + + /** @return the backtrack coverages */ + public GlyphCoverageTable[] getBacktrackCoverages() { + return backtrackCoverages; + } + + /** @return the lookahead coverages */ + public GlyphCoverageTable[] getLookaheadCoverages() { + return lookaheadCoverages; + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append ( "{ " ); + sb.append ( "lookups = " + Arrays.toString ( getLookups() ) ); + sb.append ( ", coverages = " + Arrays.toString ( getCoverages() ) ); + sb.append ( ", backtrackCoverages = " + Arrays.toString ( backtrackCoverages ) ); + sb.append ( ", lookaheadCoverages = " + Arrays.toString ( lookaheadCoverages ) ); + sb.append ( " }" ); + return sb.toString(); + } + + } + + /** + * The <code>RuleSet</code> class implements a collection of rules, which + * may or may not be the same rule type. + */ + public static class RuleSet { + + private final Rule[] rules; // set of rules + + /** + * Instantiate a Rule Set. + * @param rules the rules + * @throws AdvancedTypographicTableFormatException if rules or some element of rules is null + */ + public RuleSet ( Rule[] rules ) throws AdvancedTypographicTableFormatException { + // enforce rules array instance + if ( rules == null ) { + throw new AdvancedTypographicTableFormatException ( "rules[] is null" ); + } + this.rules = rules; + } + + /** @return the rules */ + public Rule[] getRules() { + return rules; + } + + /** + * Resolve references to lookup tables, e.g., in RuleLookup, to the lookup tables themselves. + * @param lookupTables map from lookup table identifers, e.g. "lu4", to lookup tables + */ + public void resolveLookupReferences ( Map/*<String,LookupTable>*/ lookupTables ) { + if ( rules != null ) { + for ( int i = 0, n = rules.length; i < n; i++ ) { + Rule r = rules [ i ]; + if ( r != null ) { + r.resolveLookupReferences ( lookupTables ); + } + } + } + } + + /** {@inheritDoc} */ + public String toString() { + return "{ rules = " + Arrays.toString ( rules ) + " }"; + } + + } + + /** + * The <code>HomogenousRuleSet</code> class implements a collection of rules, which + * must be the same rule type (i.e., same concrete rule class) or null. + */ + public static class HomogeneousRuleSet extends RuleSet { + + /** + * Instantiate a Homogeneous Rule Set. + * @param rules the rules + * @throws AdvancedTypographicTableFormatException if some rule[i] is not an instance of rule[0] + */ + public HomogeneousRuleSet ( Rule[] rules ) throws AdvancedTypographicTableFormatException { + super ( rules ); + // find first non-null rule + Rule r0 = null; + for ( int i = 1, n = rules.length; ( r0 == null ) && ( i < n ); i++ ) { + if ( rules[i] != null ) { + r0 = rules[i]; + } + } + // enforce rule instance homogeneity + if ( r0 != null ) { + Class c = r0.getClass(); + for ( int i = 1, n = rules.length; i < n; i++ ) { + Rule r = rules[i]; + if ( ( r != null ) && ! c.isInstance ( r ) ) { + throw new AdvancedTypographicTableFormatException ( "rules[" + i + "] is not an instance of " + c.getName() ); + } + } + } + + } + + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/IncompatibleSubtableException.java b/src/java/org/apache/fop/complexscripts/fonts/IncompatibleSubtableException.java new file mode 100644 index 000000000..0ad8fc105 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/IncompatibleSubtableException.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +/** + * Exception thrown during when attempting to map glyphs to associated characters + * in the case that the associated characters do not represent a compact interval. + * @author Glenn Adams + */ +public class IncompatibleSubtableException extends RuntimeException { + /** + * Instantiate incompatible subtable exception + */ + public IncompatibleSubtableException() { + super(); + } + /** + * Instantiate incompatible subtable exception + * @param message a message string + */ + public IncompatibleSubtableException(String message) { + super(message); + } +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/OTFAdvancedTypographicTableReader.java b/src/java/org/apache/fop/complexscripts/fonts/OTFAdvancedTypographicTableReader.java new file mode 100644 index 000000000..539f9af30 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/OTFAdvancedTypographicTableReader.java @@ -0,0 +1,3797 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.fonts.AdvancedTypographicTableFormatException; +import org.apache.fop.complexscripts.fonts.GlyphClassTable; +import org.apache.fop.complexscripts.fonts.GlyphCoverageTable; +import org.apache.fop.complexscripts.fonts.GlyphDefinitionSubtable; +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; +import org.apache.fop.complexscripts.fonts.GlyphMappingTable; +import org.apache.fop.complexscripts.fonts.GlyphPositioningSubtable; +import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; +import org.apache.fop.complexscripts.fonts.GlyphSubstitutionSubtable; +import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable; +import org.apache.fop.complexscripts.fonts.GlyphSubtable; +import org.apache.fop.complexscripts.fonts.GlyphTable; +import org.apache.fop.fonts.truetype.FontFileReader; +import org.apache.fop.fonts.truetype.TTFDirTabEntry; +import org.apache.fop.fonts.truetype.TTFFile; + +// CSOFF: AvoidNestedBlocksCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: SimplifyBooleanReturnCheck +// CSOFF: LineLengthCheck + +/** + * OpenType Font (OTF) advanced typographic table reader. Used by @{Link org.apache.fop.fonts.truetype.TTFFile} + * to read advanced typographic tables (GDEF, GSUB, GPOS). + * + * @author Glenn Adams + */ +public final class OTFAdvancedTypographicTableReader { + + // logging state + private static Log log = LogFactory.getLog(OTFAdvancedTypographicTableReader.class); + // instance state + private TTFFile ttf; // parent font file reader + private FontFileReader in; // input reader + private GlyphDefinitionTable gdef; // glyph definition table + private GlyphSubstitutionTable gsub; // glyph substitution table + private GlyphPositioningTable gpos; // glyph positioning table + // transient parsing state + private transient Map/*<String,Object[3]>*/ seScripts; // script-tag => Object[3] : { default-language-tag, List(language-tag), seLanguages } + private transient Map/*<String,Object[2]>*/ seLanguages; // language-tag => Object[2] : { "f<required-feature-index>", List("f<feature-index>") + private transient Map/*<String,List<String>>*/ seFeatures; // "f<feature-index>" => Object[2] : { feature-tag, List("lu<lookup-index>") } + private transient GlyphMappingTable seMapping; // subtable entry mappings + private transient List seEntries; // subtable entry entries + private transient List seSubtables; // subtable entry subtables + + /** + * Construct an <code>OTFAdvancedTypographicTableReader</code> instance. + * @param ttf parent font file reader (must be non-null) + * @param in font file reader (must be non-null) + */ + public OTFAdvancedTypographicTableReader ( TTFFile ttf, FontFileReader in ) { + assert ttf != null; + assert in != null; + this.ttf = ttf; + this.in = in; + } + + /** + * Read all advanced typographic tables. + * @throws AdvancedTypographicTableFormatException if ATT table has invalid format + */ + public void readAll() throws AdvancedTypographicTableFormatException { + try { + readGDEF(); + readGSUB(); + readGPOS(); + } catch ( AdvancedTypographicTableFormatException e ) { + resetATStateAll(); + throw e; + } catch ( IOException e ) { + resetATStateAll(); + throw new AdvancedTypographicTableFormatException ( e.getMessage(), e ); + } finally { + resetATState(); + } + } + + /** + * Determine if advanced (typographic) table is present. + * @return true if advanced (typographic) table is present + */ + public boolean hasAdvancedTable() { + return ( gdef != null ) || ( gsub != null ) || ( gpos != null ); + } + + /** + * Returns the GDEF table or null if none present. + * @return the GDEF table + */ + public GlyphDefinitionTable getGDEF() { + return gdef; + } + + /** + * Returns the GSUB table or null if none present. + * @return the GSUB table + */ + public GlyphSubstitutionTable getGSUB() { + return gsub; + } + + /** + * Returns the GPOS table or null if none present. + * @return the GPOS table + */ + public GlyphPositioningTable getGPOS() { + return gpos; + } + + private void readLangSysTable(String tableTag, long langSysTable, String langSysTag) throws IOException { + in.seekSet(langSysTable); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys table: " + langSysTag ); + } + // read lookup order (reorder) table offset + int lo = in.readTTFUShort(); + // read required feature index + int rf = in.readTTFUShort(); + String rfi; + if ( rf != 65535 ) { + rfi = "f" + rf; + } else { + rfi = null; + } + // read (non-required) feature count + int nf = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys table reorder table: " + lo ); + log.debug(tableTag + " lang sys table required feature index: " + rf ); + log.debug(tableTag + " lang sys table non-required feature count: " + nf ); + } + // read (non-required) feature indices + int[] fia = new int[nf]; + List fl = new java.util.ArrayList(); + for ( int i = 0; i < nf; i++ ) { + int fi = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys table non-required feature index: " + fi ); + } + fia[i] = fi; + fl.add ( "f" + fi ); + } + if ( seLanguages == null ) { + seLanguages = new java.util.LinkedHashMap(); + } + seLanguages.put ( langSysTag, new Object[] { rfi, fl } ); + } + + private static String defaultTag = "dflt"; + + private void readScriptTable(String tableTag, long scriptTable, String scriptTag) throws IOException { + in.seekSet(scriptTable); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script table: " + scriptTag ); + } + // read default language system table offset + int dl = in.readTTFUShort(); + String dt = defaultTag; + if ( dl > 0 ) { + if (log.isDebugEnabled()) { + log.debug(tableTag + " default lang sys tag: " + dt ); + log.debug(tableTag + " default lang sys table offset: " + dl ); + } + } + // read language system record count + int nl = in.readTTFUShort(); + List ll = new java.util.ArrayList(); + if ( nl > 0 ) { + String[] lta = new String[nl]; + int[] loa = new int[nl]; + // read language system records + for ( int i = 0, n = nl; i < n; i++ ) { + String lt = in.readTTFString(4); + int lo = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys tag: " + lt ); + log.debug(tableTag + " lang sys table offset: " + lo ); + } + lta[i] = lt; + loa[i] = lo; + if ( dl == lo ) { + dl = 0; + dt = lt; + } + ll.add ( lt ); + } + // read non-default language system tables + for ( int i = 0, n = nl; i < n; i++ ) { + readLangSysTable ( tableTag, scriptTable + loa [ i ], lta [ i ] ); + } + } + // read default language system table (if specified) + if ( dl > 0 ) { + readLangSysTable ( tableTag, scriptTable + dl, dt ); + } else if ( dt != null ) { + if (log.isDebugEnabled()) { + log.debug(tableTag + " lang sys default: " + dt ); + } + } + seScripts.put ( scriptTag, new Object[] { dt, ll, seLanguages } ); + seLanguages = null; + } + + private void readScriptList(String tableTag, long scriptList) throws IOException { + in.seekSet(scriptList); + // read script record count + int ns = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script list record count: " + ns ); + } + if ( ns > 0 ) { + String[] sta = new String[ns]; + int[] soa = new int[ns]; + // read script records + for ( int i = 0, n = ns; i < n; i++ ) { + String st = in.readTTFString(4); + int so = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script tag: " + st ); + log.debug(tableTag + " script table offset: " + so ); + } + sta[i] = st; + soa[i] = so; + } + // read script tables + for ( int i = 0, n = ns; i < n; i++ ) { + seLanguages = null; + readScriptTable ( tableTag, scriptList + soa [ i ], sta [ i ] ); + } + } + } + + private void readFeatureTable(String tableTag, long featureTable, String featureTag, int featureIndex) throws IOException { + in.seekSet(featureTable); + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature table: " + featureTag ); + } + // read feature params offset + int po = in.readTTFUShort(); + // read lookup list indices count + int nl = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature table parameters offset: " + po ); + log.debug(tableTag + " feature table lookup list index count: " + nl ); + } + // read lookup table indices + int[] lia = new int[nl]; + List lul = new java.util.ArrayList(); + for ( int i = 0; i < nl; i++ ) { + int li = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature table lookup index: " + li ); + } + lia[i] = li; + lul.add ( "lu" + li ); + } + seFeatures.put ( "f" + featureIndex, new Object[] { featureTag, lul } ); + } + + private void readFeatureList(String tableTag, long featureList) throws IOException { + in.seekSet(featureList); + // read feature record count + int nf = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature list record count: " + nf ); + } + if ( nf > 0 ) { + String[] fta = new String[nf]; + int[] foa = new int[nf]; + // read feature records + for ( int i = 0, n = nf; i < n; i++ ) { + String ft = in.readTTFString(4); + int fo = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature tag: " + ft ); + log.debug(tableTag + " feature table offset: " + fo ); + } + fta[i] = ft; + foa[i] = fo; + } + // read feature tables + for ( int i = 0, n = nf; i < n; i++ ) { + if (log.isDebugEnabled()) { + log.debug(tableTag + " feature index: " + i ); + } + readFeatureTable ( tableTag, featureList + foa [ i ], fta [ i ], i ); + } + } + } + + static final class GDEFLookupType { + static final int GLYPH_CLASS = 1; + static final int ATTACHMENT_POINT = 2; + static final int LIGATURE_CARET = 3; + static final int MARK_ATTACHMENT = 4; + private GDEFLookupType() { + } + public static int getSubtableType ( int lt ) { + int st; + switch ( lt ) { + case GDEFLookupType.GLYPH_CLASS: + st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_GLYPH_CLASS; + break; + case GDEFLookupType.ATTACHMENT_POINT: + st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_ATTACHMENT_POINT; + break; + case GDEFLookupType.LIGATURE_CARET: + st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_LIGATURE_CARET; + break; + case GDEFLookupType.MARK_ATTACHMENT: + st = GlyphDefinitionTable.GDEF_LOOKUP_TYPE_MARK_ATTACHMENT; + break; + default: + st = -1; + break; + } + return st; + } + public static String toString(int type) { + String s; + switch ( type ) { + case GLYPH_CLASS: + s = "GlyphClass"; + break; + case ATTACHMENT_POINT: + s = "AttachmentPoint"; + break; + case LIGATURE_CARET: + s = "LigatureCaret"; + break; + case MARK_ATTACHMENT: + s = "MarkAttachment"; + break; + default: + s = "?"; + break; + } + return s; + } + } + + static final class GSUBLookupType { + static final int SINGLE = 1; + static final int MULTIPLE = 2; + static final int ALTERNATE = 3; + static final int LIGATURE = 4; + static final int CONTEXTUAL = 5; + static final int CHAINED_CONTEXTUAL = 6; + static final int EXTENSION = 7; + static final int REVERSE_CHAINED_SINGLE = 8; + private GSUBLookupType() { + } + public static int getSubtableType ( int lt ) { + int st; + switch ( lt ) { + case GSUBLookupType.SINGLE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_SINGLE; + break; + case GSUBLookupType.MULTIPLE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_MULTIPLE; + break; + case GSUBLookupType.ALTERNATE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_ALTERNATE; + break; + case GSUBLookupType.LIGATURE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_LIGATURE; + break; + case GSUBLookupType.CONTEXTUAL: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CONTEXTUAL; + break; + case GSUBLookupType.CHAINED_CONTEXTUAL: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_CHAINED_CONTEXTUAL; + break; + case GSUBLookupType.EXTENSION: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_EXTENSION_SUBSTITUTION; + break; + case GSUBLookupType.REVERSE_CHAINED_SINGLE: + st = GlyphSubstitutionTable.GSUB_LOOKUP_TYPE_REVERSE_CHAINED_SINGLE; + break; + default: + st = -1; + break; + } + return st; + } + public static String toString(int type) { + String s; + switch ( type ) { + case SINGLE: + s = "Single"; + break; + case MULTIPLE: + s = "Multiple"; + break; + case ALTERNATE: + s = "Alternate"; + break; + case LIGATURE: + s = "Ligature"; + break; + case CONTEXTUAL: + s = "Contextual"; + break; + case CHAINED_CONTEXTUAL: + s = "ChainedContextual"; + break; + case EXTENSION: + s = "Extension"; + break; + case REVERSE_CHAINED_SINGLE: + s = "ReverseChainedSingle"; + break; + default: + s = "?"; + break; + } + return s; + } + } + + static final class GPOSLookupType { + static final int SINGLE = 1; + static final int PAIR = 2; + static final int CURSIVE = 3; + static final int MARK_TO_BASE = 4; + static final int MARK_TO_LIGATURE = 5; + static final int MARK_TO_MARK = 6; + static final int CONTEXTUAL = 7; + static final int CHAINED_CONTEXTUAL = 8; + static final int EXTENSION = 9; + private GPOSLookupType() { + } + public static String toString(int type) { + String s; + switch ( type ) { + case SINGLE: + s = "Single"; + break; + case PAIR: + s = "Pair"; + break; + case CURSIVE: + s = "Cursive"; + break; + case MARK_TO_BASE: + s = "MarkToBase"; + break; + case MARK_TO_LIGATURE: + s = "MarkToLigature"; + break; + case MARK_TO_MARK: + s = "MarkToMark"; + break; + case CONTEXTUAL: + s = "Contextual"; + break; + case CHAINED_CONTEXTUAL: + s = "ChainedContextual"; + break; + case EXTENSION: + s = "Extension"; + break; + default: + s = "?"; + break; + } + return s; + } + } + + static final class LookupFlag { + static final int RIGHT_TO_LEFT = 0x0001; + static final int IGNORE_BASE_GLYPHS = 0x0002; + static final int IGNORE_LIGATURE = 0x0004; + static final int IGNORE_MARKS = 0x0008; + static final int USE_MARK_FILTERING_SET = 0x0010; + static final int MARK_ATTACHMENT_TYPE = 0xFF00; + private LookupFlag() { + } + public static String toString(int flags) { + StringBuffer sb = new StringBuffer(); + boolean first = true; + if ( ( flags & RIGHT_TO_LEFT ) != 0 ) { + if ( first ) { + first = false; + } else { + sb.append ( '|' ); + } + sb.append ( "RightToLeft" ); + } + if ( ( flags & IGNORE_BASE_GLYPHS ) != 0 ) { + if ( first ) { + first = false; + } else { + sb.append ( '|' ); + } + sb.append ( "IgnoreBaseGlyphs" ); + } + if ( ( flags & IGNORE_LIGATURE ) != 0 ) { + if ( first ) { + first = false; + } else { + sb.append ( '|' ); + } + sb.append ( "IgnoreLigature" ); + } + if ( ( flags & IGNORE_MARKS ) != 0 ) { + if ( first ) { + first = false; + } else { + sb.append ( '|' ); + } + sb.append ( "IgnoreMarks" ); + } + if ( ( flags & USE_MARK_FILTERING_SET ) != 0 ) { + if ( first ) { + first = false; + } else { + sb.append ( '|' ); + } + sb.append ( "UseMarkFilteringSet" ); + } + if ( sb.length() == 0 ) { + sb.append ( '-' ); + } + return sb.toString(); + } + } + + private GlyphCoverageTable readCoverageTableFormat1(String label, long tableOffset, int coverageFormat) throws IOException { + List entries = new java.util.ArrayList(); + in.seekSet(tableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read glyph count + int ng = in.readTTFUShort(); + int[] ga = new int[ng]; + for ( int i = 0, n = ng; i < n; i++ ) { + int g = in.readTTFUShort(); + ga[i] = g; + entries.add ( Integer.valueOf(g) ); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(label + " glyphs: " + toString(ga) ); + } + return GlyphCoverageTable.createCoverageTable ( entries ); + } + + private GlyphCoverageTable readCoverageTableFormat2(String label, long tableOffset, int coverageFormat) throws IOException { + List entries = new java.util.ArrayList(); + in.seekSet(tableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read range record count + int nr = in.readTTFUShort(); + for ( int i = 0, n = nr; i < n; i++ ) { + // read range start + int s = in.readTTFUShort(); + // read range end + int e = in.readTTFUShort(); + // read range coverage (mapping) index + int m = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(label + " range[" + i + "]: [" + s + "," + e + "]: " + m ); + } + entries.add ( new GlyphCoverageTable.MappingRange ( s, e, m ) ); + } + return GlyphCoverageTable.createCoverageTable ( entries ); + } + + private GlyphCoverageTable readCoverageTable(String label, long tableOffset) throws IOException { + GlyphCoverageTable gct; + long cp = in.getCurrentPos(); + in.seekSet(tableOffset); + // read coverage table format + int cf = in.readTTFUShort(); + if ( cf == 1 ) { + gct = readCoverageTableFormat1 ( label, tableOffset, cf ); + } else if ( cf == 2 ) { + gct = readCoverageTableFormat2 ( label, tableOffset, cf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported coverage table format: " + cf ); + } + in.seekSet ( cp ); + return gct; + } + + private GlyphClassTable readClassDefTableFormat1(String label, long tableOffset, int classFormat) throws IOException { + List entries = new java.util.ArrayList(); + in.seekSet(tableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read start glyph + int sg = in.readTTFUShort(); + entries.add ( Integer.valueOf(sg) ); + // read glyph count + int ng = in.readTTFUShort(); + // read glyph classes + int[] ca = new int[ng]; + for ( int i = 0, n = ng; i < n; i++ ) { + int gc = in.readTTFUShort(); + ca[i] = gc; + entries.add ( Integer.valueOf(gc) ); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(label + " glyph classes: " + toString(ca) ); + } + return GlyphClassTable.createClassTable ( entries ); + } + + private GlyphClassTable readClassDefTableFormat2(String label, long tableOffset, int classFormat) throws IOException { + List entries = new java.util.ArrayList(); + in.seekSet(tableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read range record count + int nr = in.readTTFUShort(); + for ( int i = 0, n = nr; i < n; i++ ) { + // read range start + int s = in.readTTFUShort(); + // read range end + int e = in.readTTFUShort(); + // read range glyph class (mapping) index + int m = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(label + " range[" + i + "]: [" + s + "," + e + "]: " + m ); + } + entries.add ( new GlyphClassTable.MappingRange ( s, e, m ) ); + } + return GlyphClassTable.createClassTable ( entries ); + } + + private GlyphClassTable readClassDefTable(String label, long tableOffset) throws IOException { + GlyphClassTable gct; + long cp = in.getCurrentPos(); + in.seekSet(tableOffset); + // read class table format + int cf = in.readTTFUShort(); + if ( cf == 1 ) { + gct = readClassDefTableFormat1 ( label, tableOffset, cf ); + } else if ( cf == 2 ) { + gct = readClassDefTableFormat2 ( label, tableOffset, cf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported class definition table format: " + cf ); + } + in.seekSet ( cp ); + return gct; + } + + private void readSingleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read delta glyph + int dg = in.readTTFShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " single substitution subtable format: " + subtableFormat + " (delta)" ); + log.debug(tableTag + " single substitution coverage table offset: " + co ); + log.debug(tableTag + " single substitution delta: " + dg ); + } + // read coverage table + seMapping = readCoverageTable ( tableTag + " single substitution coverage", subtableOffset + co ); + seEntries.add ( Integer.valueOf ( dg ) ); + } + + private void readSingleSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read glyph count + int ng = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " single substitution subtable format: " + subtableFormat + " (mapped)" ); + log.debug(tableTag + " single substitution coverage table offset: " + co ); + log.debug(tableTag + " single substitution glyph count: " + ng ); + } + // read coverage table + seMapping = readCoverageTable ( tableTag + " single substitution coverage", subtableOffset + co ); + // read glyph substitutions + int[] gsa = new int[ng]; + for ( int i = 0, n = ng; i < n; i++ ) { + int gs = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " single substitution glyph[" + i + "]: " + gs ); + } + gsa[i] = gs; + seEntries.add ( Integer.valueOf ( gs ) ); + } + } + + private int readSingleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read substitution subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readSingleSubTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 2 ) { + readSingleSubTableFormat2 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported single substitution subtable format: " + sf ); + } + return sf; + } + + private void readMultipleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read sequence count + int ns = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " multiple substitution subtable format: " + subtableFormat + " (mapped)" ); + log.debug(tableTag + " multiple substitution coverage table offset: " + co ); + log.debug(tableTag + " multiple substitution sequence count: " + ns ); + } + // read coverage table + seMapping = readCoverageTable ( tableTag + " multiple substitution coverage", subtableOffset + co ); + // read sequence table offsets + int[] soa = new int[ns]; + for ( int i = 0, n = ns; i < n; i++ ) { + soa[i] = in.readTTFUShort(); + } + // read sequence tables + int[][] gsa = new int [ ns ] []; + for ( int i = 0, n = ns; i < n; i++ ) { + int so = soa[i]; + int[] ga; + if ( so > 0 ) { + in.seekSet(subtableOffset + so); + // read glyph count + int ng = in.readTTFUShort(); + ga = new int[ng]; + for ( int j = 0; j < ng; j++ ) { + ga[j] = in.readTTFUShort(); + } + } else { + ga = null; + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " multiple substitution sequence[" + i + "]: " + toString ( ga ) ); + } + gsa [ i ] = ga; + } + seEntries.add ( gsa ); + } + + private int readMultipleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read substitution subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readMultipleSubTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported multiple substitution subtable format: " + sf ); + } + return sf; + } + + private void readAlternateSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read alternate set count + int ns = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " alternate substitution subtable format: " + subtableFormat + " (mapped)" ); + log.debug(tableTag + " alternate substitution coverage table offset: " + co ); + log.debug(tableTag + " alternate substitution alternate set count: " + ns ); + } + // read coverage table + seMapping = readCoverageTable ( tableTag + " alternate substitution coverage", subtableOffset + co ); + // read alternate set table offsets + int[] soa = new int[ns]; + for ( int i = 0, n = ns; i < n; i++ ) { + soa[i] = in.readTTFUShort(); + } + // read alternate set tables + for ( int i = 0, n = ns; i < n; i++ ) { + int so = soa[i]; + in.seekSet(subtableOffset + so); + // read glyph count + int ng = in.readTTFUShort(); + int[] ga = new int[ng]; + for ( int j = 0; j < ng; j++ ) { + int gs = in.readTTFUShort(); + ga[j] = gs; + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " alternate substitution alternate set[" + i + "]: " + toString ( ga ) ); + } + seEntries.add ( ga ); + } + } + + private int readAlternateSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read substitution subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readAlternateSubTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported alternate substitution subtable format: " + sf ); + } + return sf; + } + + private void readLigatureSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read ligature set count + int ns = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " ligature substitution subtable format: " + subtableFormat + " (mapped)" ); + log.debug(tableTag + " ligature substitution coverage table offset: " + co ); + log.debug(tableTag + " ligature substitution ligature set count: " + ns ); + } + // read coverage table + seMapping = readCoverageTable ( tableTag + " ligature substitution coverage", subtableOffset + co ); + // read ligature set table offsets + int[] soa = new int[ns]; + for ( int i = 0, n = ns; i < n; i++ ) { + soa[i] = in.readTTFUShort(); + } + // read ligature set tables + for ( int i = 0, n = ns; i < n; i++ ) { + int so = soa[i]; + in.seekSet(subtableOffset + so); + // read ligature table count + int nl = in.readTTFUShort(); + int[] loa = new int[nl]; + for ( int j = 0; j < nl; j++ ) { + loa[j] = in.readTTFUShort(); + } + List ligs = new java.util.ArrayList(); + for ( int j = 0; j < nl; j++ ) { + int lo = loa[j]; + in.seekSet(subtableOffset + so + lo); + // read ligature glyph id + int lg = in.readTTFUShort(); + // read ligature (input) component count + int nc = in.readTTFUShort(); + int[] ca = new int [ nc - 1 ]; + // read ligature (input) component glyph ids + for ( int k = 0; k < nc - 1; k++ ) { + ca[k] = in.readTTFUShort(); + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " ligature substitution ligature set[" + i + "]: ligature(" + lg + "), components: " + toString ( ca ) ); + } + ligs.add ( new GlyphSubstitutionTable.Ligature ( lg, ca ) ); + } + seEntries.add ( new GlyphSubstitutionTable.LigatureSet ( ligs ) ); + } + } + + private int readLigatureSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read substitution subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readLigatureSubTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported ligature substitution subtable format: " + sf ); + } + return sf; + } + + private GlyphTable.RuleLookup[] readRuleLookups(int numLookups, String header) throws IOException { + GlyphTable.RuleLookup[] la = new GlyphTable.RuleLookup [ numLookups ]; + for ( int i = 0, n = numLookups; i < n; i++ ) { + int sequenceIndex = in.readTTFUShort(); + int lookupIndex = in.readTTFUShort(); + la [ i ] = new GlyphTable.RuleLookup ( sequenceIndex, lookupIndex ); + // dump info if debugging and header is non-null + if ( log.isDebugEnabled() && ( header != null ) ) { + log.debug(header + "lookup[" + i + "]: " + la[i]); + } + } + return la; + } + + private void readContextualSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read rule set count + int nrs = in.readTTFUShort(); + // read rule set offsets + int[] rsoa = new int [ nrs ]; + for ( int i = 0; i < nrs; i++ ) { + rsoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual substitution format: " + subtableFormat + " (glyphs)" ); + log.debug(tableTag + " contextual substitution coverage table offset: " + co ); + log.debug(tableTag + " contextual substitution rule set count: " + nrs ); + for ( int i = 0; i < nrs; i++ ) { + log.debug(tableTag + " contextual substitution rule set offset[" + i + "]: " + rsoa[i] ); + } + } + // read coverage table + GlyphCoverageTable ct; + if ( co > 0 ) { + ct = readCoverageTable ( tableTag + " contextual substitution coverage", subtableOffset + co ); + } else { + ct = null; + } + // read rule sets + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ nrs ]; + String header = null; + for ( int i = 0; i < nrs; i++ ) { + GlyphTable.RuleSet rs; + int rso = rsoa [ i ]; + if ( rso > 0 ) { + // seek to rule set [ i ] + in.seekSet ( subtableOffset + rso ); + // read rule count + int nr = in.readTTFUShort(); + // read rule offsets + int[] roa = new int [ nr ]; + GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ]; + for ( int j = 0; j < nr; j++ ) { + roa [ j ] = in.readTTFUShort(); + } + // read glyph sequence rules + for ( int j = 0; j < nr; j++ ) { + GlyphTable.GlyphSequenceRule r; + int ro = roa [ j ]; + if ( ro > 0 ) { + // seek to rule [ j ] + in.seekSet ( subtableOffset + rso + ro ); + // read glyph count + int ng = in.readTTFUShort(); + // read rule lookup count + int nl = in.readTTFUShort(); + // read glyphs + int[] glyphs = new int [ ng - 1 ]; + for ( int k = 0, nk = glyphs.length; k < nk; k++ ) { + glyphs [ k ] = in.readTTFUShort(); + } + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + r = new GlyphTable.GlyphSequenceRule ( lookups, ng, glyphs ); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new GlyphTable.HomogeneousRuleSet ( ra ); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add ( rsa ); + } + + private void readContextualSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read class def table offset + int cdo = in.readTTFUShort(); + // read class rule set count + int ngc = in.readTTFUShort(); + // read class rule set offsets + int[] csoa = new int [ ngc ]; + for ( int i = 0; i < ngc; i++ ) { + csoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual substitution format: " + subtableFormat + " (glyph classes)" ); + log.debug(tableTag + " contextual substitution coverage table offset: " + co ); + log.debug(tableTag + " contextual substitution class set count: " + ngc ); + for ( int i = 0; i < ngc; i++ ) { + log.debug(tableTag + " contextual substitution class set offset[" + i + "]: " + csoa[i] ); + } + } + // read coverage table + GlyphCoverageTable ct; + if ( co > 0 ) { + ct = readCoverageTable ( tableTag + " contextual substitution coverage", subtableOffset + co ); + } else { + ct = null; + } + // read class definition table + GlyphClassTable cdt; + if ( cdo > 0 ) { + cdt = readClassDefTable ( tableTag + " contextual substitution class definition", subtableOffset + cdo ); + } else { + cdt = null; + } + // read rule sets + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ ngc ]; + String header = null; + for ( int i = 0; i < ngc; i++ ) { + int cso = csoa [ i ]; + GlyphTable.RuleSet rs; + if ( cso > 0 ) { + // seek to rule set [ i ] + in.seekSet ( subtableOffset + cso ); + // read rule count + int nr = in.readTTFUShort(); + // read rule offsets + int[] roa = new int [ nr ]; + GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ]; + for ( int j = 0; j < nr; j++ ) { + roa [ j ] = in.readTTFUShort(); + } + // read glyph sequence rules + for ( int j = 0; j < nr; j++ ) { + int ro = roa [ j ]; + GlyphTable.ClassSequenceRule r; + if ( ro > 0 ) { + // seek to rule [ j ] + in.seekSet ( subtableOffset + cso + ro ); + // read glyph count + int ng = in.readTTFUShort(); + // read rule lookup count + int nl = in.readTTFUShort(); + // read classes + int[] classes = new int [ ng - 1 ]; + for ( int k = 0, nk = classes.length; k < nk; k++ ) { + classes [ k ] = in.readTTFUShort(); + } + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + r = new GlyphTable.ClassSequenceRule ( lookups, ng, classes ); + } else { + assert ro > 0 : "unexpected null subclass rule offset"; + r = null; + } + ra [ j ] = r; + } + rs = new GlyphTable.HomogeneousRuleSet ( ra ); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add ( cdt ); + seEntries.add ( Integer.valueOf ( ngc ) ); + seEntries.add ( rsa ); + } + + private void readContextualSubTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read glyph (input sequence length) count + int ng = in.readTTFUShort(); + // read substitution lookup count + int nl = in.readTTFUShort(); + // read glyph coverage offsets, one per glyph input sequence length count + int[] gcoa = new int [ ng ]; + for ( int i = 0; i < ng; i++ ) { + gcoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual substitution format: " + subtableFormat + " (glyph sets)" ); + log.debug(tableTag + " contextual substitution glyph input sequence length count: " + ng ); + log.debug(tableTag + " contextual substitution lookup count: " + nl ); + for ( int i = 0; i < ng; i++ ) { + log.debug(tableTag + " contextual substitution coverage table offset[" + i + "]: " + gcoa[i] ); + } + } + // read coverage tables + GlyphCoverageTable[] gca = new GlyphCoverageTable [ ng ]; + for ( int i = 0; i < ng; i++ ) { + int gco = gcoa [ i ]; + GlyphCoverageTable gct; + if ( gco > 0 ) { + gct = readCoverageTable ( tableTag + " contextual substitution coverage[" + i + "]", subtableOffset + gco ); + } else { + gct = null; + } + gca [ i ] = gct; + } + // read rule lookups + String header = null; + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + // construct rule, rule set, and rule set array + GlyphTable.Rule r = new GlyphTable.CoverageSequenceRule ( lookups, ng, gca ); + GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet ( new GlyphTable.Rule[] {r} ); + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[] {rs}; + // store results + assert ( gca != null ) && ( gca.length > 0 ); + seMapping = gca[0]; + seEntries.add ( rsa ); + } + + private int readContextualSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read substitution subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readContextualSubTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 2 ) { + readContextualSubTableFormat2 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 3 ) { + readContextualSubTableFormat3 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported contextual substitution subtable format: " + sf ); + } + return sf; + } + + private void readChainedContextualSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read rule set count + int nrs = in.readTTFUShort(); + // read rule set offsets + int[] rsoa = new int [ nrs ]; + for ( int i = 0; i < nrs; i++ ) { + rsoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual substitution format: " + subtableFormat + " (glyphs)" ); + log.debug(tableTag + " chained contextual substitution coverage table offset: " + co ); + log.debug(tableTag + " chained contextual substitution rule set count: " + nrs ); + for ( int i = 0; i < nrs; i++ ) { + log.debug(tableTag + " chained contextual substitution rule set offset[" + i + "]: " + rsoa[i] ); + } + } + // read coverage table + GlyphCoverageTable ct; + if ( co > 0 ) { + ct = readCoverageTable ( tableTag + " chained contextual substitution coverage", subtableOffset + co ); + } else { + ct = null; + } + // read rule sets + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ nrs ]; + String header = null; + for ( int i = 0; i < nrs; i++ ) { + GlyphTable.RuleSet rs; + int rso = rsoa [ i ]; + if ( rso > 0 ) { + // seek to rule set [ i ] + in.seekSet ( subtableOffset + rso ); + // read rule count + int nr = in.readTTFUShort(); + // read rule offsets + int[] roa = new int [ nr ]; + GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ]; + for ( int j = 0; j < nr; j++ ) { + roa [ j ] = in.readTTFUShort(); + } + // read glyph sequence rules + for ( int j = 0; j < nr; j++ ) { + GlyphTable.ChainedGlyphSequenceRule r; + int ro = roa [ j ]; + if ( ro > 0 ) { + // seek to rule [ j ] + in.seekSet ( subtableOffset + rso + ro ); + // read backtrack glyph count + int nbg = in.readTTFUShort(); + // read backtrack glyphs + int[] backtrackGlyphs = new int [ nbg ]; + for ( int k = 0, nk = backtrackGlyphs.length; k < nk; k++ ) { + backtrackGlyphs [ k ] = in.readTTFUShort(); + } + // read input glyph count + int nig = in.readTTFUShort(); + // read glyphs + int[] glyphs = new int [ nig - 1 ]; + for ( int k = 0, nk = glyphs.length; k < nk; k++ ) { + glyphs [ k ] = in.readTTFUShort(); + } + // read lookahead glyph count + int nlg = in.readTTFUShort(); + // read lookahead glyphs + int[] lookaheadGlyphs = new int [ nlg ]; + for ( int k = 0, nk = lookaheadGlyphs.length; k < nk; k++ ) { + lookaheadGlyphs [ k ] = in.readTTFUShort(); + } + // read rule lookup count + int nl = in.readTTFUShort(); + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + r = new GlyphTable.ChainedGlyphSequenceRule ( lookups, nig, glyphs, backtrackGlyphs, lookaheadGlyphs ); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new GlyphTable.HomogeneousRuleSet ( ra ); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add ( rsa ); + } + + private void readChainedContextualSubTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read backtrack class def table offset + int bcdo = in.readTTFUShort(); + // read input class def table offset + int icdo = in.readTTFUShort(); + // read lookahead class def table offset + int lcdo = in.readTTFUShort(); + // read class set count + int ngc = in.readTTFUShort(); + // read class set offsets + int[] csoa = new int [ ngc ]; + for ( int i = 0; i < ngc; i++ ) { + csoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual substitution format: " + subtableFormat + " (glyph classes)" ); + log.debug(tableTag + " chained contextual substitution coverage table offset: " + co ); + log.debug(tableTag + " chained contextual substitution class set count: " + ngc ); + for ( int i = 0; i < ngc; i++ ) { + log.debug(tableTag + " chained contextual substitution class set offset[" + i + "]: " + csoa[i] ); + } + } + // read coverage table + GlyphCoverageTable ct; + if ( co > 0 ) { + ct = readCoverageTable ( tableTag + " chained contextual substitution coverage", subtableOffset + co ); + } else { + ct = null; + } + // read backtrack class definition table + GlyphClassTable bcdt; + if ( bcdo > 0 ) { + bcdt = readClassDefTable ( tableTag + " contextual substitution backtrack class definition", subtableOffset + bcdo ); + } else { + bcdt = null; + } + // read input class definition table + GlyphClassTable icdt; + if ( icdo > 0 ) { + icdt = readClassDefTable ( tableTag + " contextual substitution input class definition", subtableOffset + icdo ); + } else { + icdt = null; + } + // read lookahead class definition table + GlyphClassTable lcdt; + if ( lcdo > 0 ) { + lcdt = readClassDefTable ( tableTag + " contextual substitution lookahead class definition", subtableOffset + lcdo ); + } else { + lcdt = null; + } + // read rule sets + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ ngc ]; + String header = null; + for ( int i = 0; i < ngc; i++ ) { + int cso = csoa [ i ]; + GlyphTable.RuleSet rs; + if ( cso > 0 ) { + // seek to rule set [ i ] + in.seekSet ( subtableOffset + cso ); + // read rule count + int nr = in.readTTFUShort(); + // read rule offsets + int[] roa = new int [ nr ]; + GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ]; + for ( int j = 0; j < nr; j++ ) { + roa [ j ] = in.readTTFUShort(); + } + // read glyph sequence rules + for ( int j = 0; j < nr; j++ ) { + int ro = roa [ j ]; + GlyphTable.ChainedClassSequenceRule r; + if ( ro > 0 ) { + // seek to rule [ j ] + in.seekSet ( subtableOffset + cso + ro ); + // read backtrack glyph class count + int nbc = in.readTTFUShort(); + // read backtrack glyph classes + int[] backtrackClasses = new int [ nbc ]; + for ( int k = 0, nk = backtrackClasses.length; k < nk; k++ ) { + backtrackClasses [ k ] = in.readTTFUShort(); + } + // read input glyph class count + int nic = in.readTTFUShort(); + // read input glyph classes + int[] classes = new int [ nic - 1 ]; + for ( int k = 0, nk = classes.length; k < nk; k++ ) { + classes [ k ] = in.readTTFUShort(); + } + // read lookahead glyph class count + int nlc = in.readTTFUShort(); + // read lookahead glyph classes + int[] lookaheadClasses = new int [ nlc ]; + for ( int k = 0, nk = lookaheadClasses.length; k < nk; k++ ) { + lookaheadClasses [ k ] = in.readTTFUShort(); + } + // read rule lookup count + int nl = in.readTTFUShort(); + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual substitution lookups @rule[" + i + "][" + j + "]: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + r = new GlyphTable.ChainedClassSequenceRule ( lookups, nic, classes, backtrackClasses, lookaheadClasses ); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new GlyphTable.HomogeneousRuleSet ( ra ); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add ( icdt ); + seEntries.add ( bcdt ); + seEntries.add ( lcdt ); + seEntries.add ( Integer.valueOf ( ngc ) ); + seEntries.add ( rsa ); + } + + private void readChainedContextualSubTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read backtrack glyph count + int nbg = in.readTTFUShort(); + // read backtrack glyph coverage offsets + int[] bgcoa = new int [ nbg ]; + for ( int i = 0; i < nbg; i++ ) { + bgcoa [ i ] = in.readTTFUShort(); + } + // read input glyph count + int nig = in.readTTFUShort(); + // read input glyph coverage offsets + int[] igcoa = new int [ nig ]; + for ( int i = 0; i < nig; i++ ) { + igcoa [ i ] = in.readTTFUShort(); + } + // read lookahead glyph count + int nlg = in.readTTFUShort(); + // read lookahead glyph coverage offsets + int[] lgcoa = new int [ nlg ]; + for ( int i = 0; i < nlg; i++ ) { + lgcoa [ i ] = in.readTTFUShort(); + } + // read substitution lookup count + int nl = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual substitution format: " + subtableFormat + " (glyph sets)" ); + log.debug(tableTag + " chained contextual substitution backtrack glyph count: " + nbg ); + for ( int i = 0; i < nbg; i++ ) { + log.debug(tableTag + " chained contextual substitution backtrack coverage table offset[" + i + "]: " + bgcoa[i] ); + } + log.debug(tableTag + " chained contextual substitution input glyph count: " + nig ); + for ( int i = 0; i < nig; i++ ) { + log.debug(tableTag + " chained contextual substitution input coverage table offset[" + i + "]: " + igcoa[i] ); + } + log.debug(tableTag + " chained contextual substitution lookahead glyph count: " + nlg ); + for ( int i = 0; i < nlg; i++ ) { + log.debug(tableTag + " chained contextual substitution lookahead coverage table offset[" + i + "]: " + lgcoa[i] ); + } + log.debug(tableTag + " chained contextual substitution lookup count: " + nl ); + } + // read backtrack coverage tables + GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg]; + for ( int i = 0; i < nbg; i++ ) { + int bgco = bgcoa [ i ]; + GlyphCoverageTable bgct; + if ( bgco > 0 ) { + bgct = readCoverageTable ( tableTag + " chained contextual substitution backtrack coverage[" + i + "]", subtableOffset + bgco ); + } else { + bgct = null; + } + bgca[i] = bgct; + } + // read input coverage tables + GlyphCoverageTable[] igca = new GlyphCoverageTable[nig]; + for ( int i = 0; i < nig; i++ ) { + int igco = igcoa [ i ]; + GlyphCoverageTable igct; + if ( igco > 0 ) { + igct = readCoverageTable ( tableTag + " chained contextual substitution input coverage[" + i + "]", subtableOffset + igco ); + } else { + igct = null; + } + igca[i] = igct; + } + // read lookahead coverage tables + GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg]; + for ( int i = 0; i < nlg; i++ ) { + int lgco = lgcoa [ i ]; + GlyphCoverageTable lgct; + if ( lgco > 0 ) { + lgct = readCoverageTable ( tableTag + " chained contextual substitution lookahead coverage[" + i + "]", subtableOffset + lgco ); + } else { + lgct = null; + } + lgca[i] = lgct; + } + // read rule lookups + String header = null; + if (log.isDebugEnabled()) { + header = tableTag + " chained contextual substitution lookups: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + // construct rule, rule set, and rule set array + GlyphTable.Rule r = new GlyphTable.ChainedCoverageSequenceRule ( lookups, nig, igca, bgca, lgca ); + GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet ( new GlyphTable.Rule[] {r} ); + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[] {rs}; + // store results + assert ( igca != null ) && ( igca.length > 0 ); + seMapping = igca[0]; + seEntries.add ( rsa ); + } + + private int readChainedContextualSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read substitution subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readChainedContextualSubTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 2 ) { + readChainedContextualSubTableFormat2 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 3 ) { + readChainedContextualSubTableFormat3 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported chained contextual substitution subtable format: " + sf ); + } + return sf; + } + + private void readExtensionSubTableFormat1(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read extension lookup type + int lt = in.readTTFUShort(); + // read extension offset + long eo = in.readTTFULong(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " extension substitution subtable format: " + subtableFormat ); + log.debug(tableTag + " extension substitution lookup type: " + lt ); + log.debug(tableTag + " extension substitution lookup table offset: " + eo ); + } + // read referenced subtable from extended offset + readGSUBSubtable ( lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo ); + } + + private int readExtensionSubTable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read substitution subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readExtensionSubTableFormat1 ( lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported extension substitution subtable format: " + sf ); + } + return sf; + } + + private void readReverseChainedSingleSubTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GSUB"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read backtrack glyph count + int nbg = in.readTTFUShort(); + // read backtrack glyph coverage offsets + int[] bgcoa = new int [ nbg ]; + for ( int i = 0; i < nbg; i++ ) { + bgcoa [ i ] = in.readTTFUShort(); + } + // read lookahead glyph count + int nlg = in.readTTFUShort(); + // read backtrack glyph coverage offsets + int[] lgcoa = new int [ nlg ]; + for ( int i = 0; i < nlg; i++ ) { + lgcoa [ i ] = in.readTTFUShort(); + } + // read substitution (output) glyph count + int ng = in.readTTFUShort(); + // read substitution (output) glyphs + int[] glyphs = new int [ ng ]; + for ( int i = 0, n = ng; i < n; i++ ) { + glyphs [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " reverse chained contextual substitution format: " + subtableFormat ); + log.debug(tableTag + " reverse chained contextual substitution coverage table offset: " + co ); + log.debug(tableTag + " reverse chained contextual substitution backtrack glyph count: " + nbg ); + for ( int i = 0; i < nbg; i++ ) { + log.debug(tableTag + " reverse chained contextual substitution backtrack coverage table offset[" + i + "]: " + bgcoa[i] ); + } + log.debug(tableTag + " reverse chained contextual substitution lookahead glyph count: " + nlg ); + for ( int i = 0; i < nlg; i++ ) { + log.debug(tableTag + " reverse chained contextual substitution lookahead coverage table offset[" + i + "]: " + lgcoa[i] ); + } + log.debug(tableTag + " reverse chained contextual substitution glyphs: " + toString(glyphs) ); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable ( tableTag + " reverse chained contextual substitution coverage", subtableOffset + co ); + // read backtrack coverage tables + GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg]; + for ( int i = 0; i < nbg; i++ ) { + int bgco = bgcoa[i]; + GlyphCoverageTable bgct; + if ( bgco > 0 ) { + bgct = readCoverageTable ( tableTag + " reverse chained contextual substitution backtrack coverage[" + i + "]", subtableOffset + bgco ); + } else { + bgct = null; + } + bgca[i] = bgct; + } + // read lookahead coverage tables + GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg]; + for ( int i = 0; i < nlg; i++ ) { + int lgco = lgcoa[i]; + GlyphCoverageTable lgct; + if ( lgco > 0 ) { + lgct = readCoverageTable ( tableTag + " reverse chained contextual substitution lookahead coverage[" + i + "]", subtableOffset + lgco ); + } else { + lgct = null; + } + lgca[i] = lgct; + } + // store results + seMapping = ct; + seEntries.add ( bgca ); + seEntries.add ( lgca ); + seEntries.add ( glyphs ); + } + + private int readReverseChainedSingleSubTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read substitution subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readReverseChainedSingleSubTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported reverse chained single substitution subtable format: " + sf ); + } + return sf; + } + + private void readGSUBSubtable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException { + initATSubState(); + int subtableFormat = -1; + switch ( lookupType ) { + case GSUBLookupType.SINGLE: + subtableFormat = readSingleSubTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GSUBLookupType.MULTIPLE: + subtableFormat = readMultipleSubTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GSUBLookupType.ALTERNATE: + subtableFormat = readAlternateSubTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GSUBLookupType.LIGATURE: + subtableFormat = readLigatureSubTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GSUBLookupType.CONTEXTUAL: + subtableFormat = readContextualSubTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GSUBLookupType.CHAINED_CONTEXTUAL: + subtableFormat = readChainedContextualSubTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GSUBLookupType.REVERSE_CHAINED_SINGLE: + subtableFormat = readReverseChainedSingleSubTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GSUBLookupType.EXTENSION: + subtableFormat = readExtensionSubTable ( lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset ); + break; + default: + break; + } + extractSESubState ( GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableFormat ); + resetATSubState(); + } + + private GlyphPositioningTable.DeviceTable readPosDeviceTable(long subtableOffset, long deviceTableOffset) throws IOException { + long cp = in.getCurrentPos(); + in.seekSet(subtableOffset + deviceTableOffset); + // read start size + int ss = in.readTTFUShort(); + // read end size + int es = in.readTTFUShort(); + // read delta format + int df = in.readTTFUShort(); + int s1, m1, dm, dd, s2; + if ( df == 1 ) { + s1 = 14; m1 = 0x3; dm = 1; dd = 4; s2 = 2; + } else if ( df == 2 ) { + s1 = 12; m1 = 0xF; dm = 7; dd = 16; s2 = 4; + } else if ( df == 3 ) { + s1 = 8; m1 = 0xFF; dm = 127; dd = 256; s2 = 8; + } else { + log.debug ( "unsupported device table delta format: " + df + ", ignoring device table" ); + return null; + } + // read deltas + int n = ( es - ss ) + 1; + if ( n < 0 ) { + log.debug ( "invalid device table delta count: " + n + ", ignoring device table" ); + return null; + } + int[] da = new int [ n ]; + for ( int i = 0; ( i < n ) && ( s2 > 0 );) { + int p = in.readTTFUShort(); + for ( int j = 0, k = 16 / s2; j < k; j++ ) { + int d = ( p >> s1 ) & m1; + if ( d > dm ) { + d -= dd; + } + if ( i < n ) { + da [ i++ ] = d; + } else { + break; + } + p <<= s2; + } + } + in.seekSet(cp); + return new GlyphPositioningTable.DeviceTable ( ss, es, da ); + } + + private GlyphPositioningTable.Value readPosValue(long subtableOffset, int valueFormat) throws IOException { + // XPlacement + int xp; + if ( ( valueFormat & GlyphPositioningTable.Value.X_PLACEMENT ) != 0 ) { + xp = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + } else { + xp = 0; + } + // YPlacement + int yp; + if ( ( valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT ) != 0 ) { + yp = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + } else { + yp = 0; + } + // XAdvance + int xa; + if ( ( valueFormat & GlyphPositioningTable.Value.X_ADVANCE ) != 0 ) { + xa = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + } else { + xa = 0; + } + // YAdvance + int ya; + if ( ( valueFormat & GlyphPositioningTable.Value.Y_ADVANCE ) != 0 ) { + ya = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + } else { + ya = 0; + } + // XPlaDevice + GlyphPositioningTable.DeviceTable xpd; + if ( ( valueFormat & GlyphPositioningTable.Value.X_PLACEMENT_DEVICE ) != 0 ) { + int xpdo = in.readTTFUShort(); + xpd = readPosDeviceTable ( subtableOffset, xpdo ); + } else { + xpd = null; + } + // YPlaDevice + GlyphPositioningTable.DeviceTable ypd; + if ( ( valueFormat & GlyphPositioningTable.Value.Y_PLACEMENT_DEVICE ) != 0 ) { + int ypdo = in.readTTFUShort(); + ypd = readPosDeviceTable ( subtableOffset, ypdo ); + } else { + ypd = null; + } + // XAdvDevice + GlyphPositioningTable.DeviceTable xad; + if ( ( valueFormat & GlyphPositioningTable.Value.X_ADVANCE_DEVICE ) != 0 ) { + int xado = in.readTTFUShort(); + xad = readPosDeviceTable ( subtableOffset, xado ); + } else { + xad = null; + } + // YAdvDevice + GlyphPositioningTable.DeviceTable yad; + if ( ( valueFormat & GlyphPositioningTable.Value.Y_ADVANCE_DEVICE ) != 0 ) { + int yado = in.readTTFUShort(); + yad = readPosDeviceTable ( subtableOffset, yado ); + } else { + yad = null; + } + return new GlyphPositioningTable.Value ( xp, yp, xa, ya, xpd, ypd, xad, yad ); + } + + private void readSinglePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read value format + int vf = in.readTTFUShort(); + // read value + GlyphPositioningTable.Value v = readPosValue ( subtableOffset, vf ); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " single positioning subtable format: " + subtableFormat + " (delta)" ); + log.debug(tableTag + " single positioning coverage table offset: " + co ); + log.debug(tableTag + " single positioning value: " + v ); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable ( tableTag + " single positioning coverage", subtableOffset + co ); + // store results + seMapping = ct; + seEntries.add ( v ); + } + + private void readSinglePosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read value format + int vf = in.readTTFUShort(); + // read value count + int nv = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " single positioning subtable format: " + subtableFormat + " (mapped)" ); + log.debug(tableTag + " single positioning coverage table offset: " + co ); + log.debug(tableTag + " single positioning value count: " + nv ); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable ( tableTag + " single positioning coverage", subtableOffset + co ); + // read positioning values + GlyphPositioningTable.Value[] pva = new GlyphPositioningTable.Value[nv]; + for ( int i = 0, n = nv; i < n; i++ ) { + GlyphPositioningTable.Value pv = readPosValue ( subtableOffset, vf ); + if (log.isDebugEnabled()) { + log.debug(tableTag + " single positioning value[" + i + "]: " + pv ); + } + pva[i] = pv; + } + // store results + seMapping = ct; + seEntries.add ( pva ); + } + + private int readSinglePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read positionining subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readSinglePosTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 2 ) { + readSinglePosTableFormat2 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported single positioning subtable format: " + sf ); + } + return sf; + } + + private GlyphPositioningTable.PairValues readPosPairValues(long subtableOffset, boolean hasGlyph, int vf1, int vf2) throws IOException { + // read glyph (if present) + int glyph; + if ( hasGlyph ) { + glyph = in.readTTFUShort(); + } else { + glyph = 0; + } + // read first value (if present) + GlyphPositioningTable.Value v1; + if ( vf1 != 0 ) { + v1 = readPosValue ( subtableOffset, vf1 ); + } else { + v1 = null; + } + // read second value (if present) + GlyphPositioningTable.Value v2; + if ( vf2 != 0 ) { + v2 = readPosValue ( subtableOffset, vf2 ); + } else { + v2 = null; + } + return new GlyphPositioningTable.PairValues ( glyph, v1, v2 ); + } + + private GlyphPositioningTable.PairValues[] readPosPairSetTable(long subtableOffset, int pairSetTableOffset, int vf1, int vf2) throws IOException { + String tableTag = "GPOS"; + long cp = in.getCurrentPos(); + in.seekSet(subtableOffset + pairSetTableOffset); + // read pair values count + int npv = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair set table offset: " + pairSetTableOffset ); + log.debug(tableTag + " pair set table values count: " + npv ); + } + // read pair values + GlyphPositioningTable.PairValues[] pva = new GlyphPositioningTable.PairValues [ npv ]; + for ( int i = 0, n = npv; i < n; i++ ) { + GlyphPositioningTable.PairValues pv = readPosPairValues ( subtableOffset, true, vf1, vf2 ); + pva [ i ] = pv; + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair set table value[" + i + "]: " + pv); + } + } + in.seekSet(cp); + return pva; + } + + private void readPairPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read value format for first glyph + int vf1 = in.readTTFUShort(); + // read value format for second glyph + int vf2 = in.readTTFUShort(); + // read number (count) of pair sets + int nps = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair positioning subtable format: " + subtableFormat + " (glyphs)" ); + log.debug(tableTag + " pair positioning coverage table offset: " + co ); + log.debug(tableTag + " pair positioning value format #1: " + vf1 ); + log.debug(tableTag + " pair positioning value format #2: " + vf2 ); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable ( tableTag + " pair positioning coverage", subtableOffset + co ); + // read pair value matrix + GlyphPositioningTable.PairValues[][] pvm = new GlyphPositioningTable.PairValues [ nps ][]; + for ( int i = 0, n = nps; i < n; i++ ) { + // read pair set offset + int pso = in.readTTFUShort(); + // read pair set table at offset + pvm [ i ] = readPosPairSetTable ( subtableOffset, pso, vf1, vf2 ); + } + // store results + seMapping = ct; + seEntries.add ( pvm ); + } + + private void readPairPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read value format for first glyph + int vf1 = in.readTTFUShort(); + // read value format for second glyph + int vf2 = in.readTTFUShort(); + // read class def 1 offset + int cd1o = in.readTTFUShort(); + // read class def 2 offset + int cd2o = in.readTTFUShort(); + // read number (count) of classes in class def 1 table + int nc1 = in.readTTFUShort(); + // read number (count) of classes in class def 2 table + int nc2 = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair positioning subtable format: " + subtableFormat + " (glyph classes)" ); + log.debug(tableTag + " pair positioning coverage table offset: " + co ); + log.debug(tableTag + " pair positioning value format #1: " + vf1 ); + log.debug(tableTag + " pair positioning value format #2: " + vf2 ); + log.debug(tableTag + " pair positioning class def table #1 offset: " + cd1o ); + log.debug(tableTag + " pair positioning class def table #2 offset: " + cd2o ); + log.debug(tableTag + " pair positioning class #1 count: " + nc1 ); + log.debug(tableTag + " pair positioning class #2 count: " + nc2 ); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable ( tableTag + " pair positioning coverage", subtableOffset + co ); + // read class definition table #1 + GlyphClassTable cdt1 = readClassDefTable ( tableTag + " pair positioning class definition #1", subtableOffset + cd1o ); + // read class definition table #2 + GlyphClassTable cdt2 = readClassDefTable ( tableTag + " pair positioning class definition #2", subtableOffset + cd2o ); + // read pair value matrix + GlyphPositioningTable.PairValues[][] pvm = new GlyphPositioningTable.PairValues [ nc1 ] [ nc2 ]; + for ( int i = 0; i < nc1; i++ ) { + for ( int j = 0; j < nc2; j++ ) { + GlyphPositioningTable.PairValues pv = readPosPairValues ( subtableOffset, false, vf1, vf2 ); + pvm [ i ] [ j ] = pv; + if (log.isDebugEnabled()) { + log.debug(tableTag + " pair set table value[" + i + "][" + j + "]: " + pv); + } + } + } + // store results + seMapping = ct; + seEntries.add ( cdt1 ); + seEntries.add ( cdt2 ); + seEntries.add ( Integer.valueOf ( nc1 ) ); + seEntries.add ( Integer.valueOf ( nc2 ) ); + seEntries.add ( pvm ); + } + + private int readPairPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read positioning subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readPairPosTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 2 ) { + readPairPosTableFormat2 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported pair positioning subtable format: " + sf ); + } + return sf; + } + + private GlyphPositioningTable.Anchor readPosAnchor(long anchorTableOffset) throws IOException { + GlyphPositioningTable.Anchor a; + long cp = in.getCurrentPos(); + in.seekSet(anchorTableOffset); + // read anchor table format + int af = in.readTTFUShort(); + if ( af == 1 ) { + // read x coordinate + int x = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + // read y coordinate + int y = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + a = new GlyphPositioningTable.Anchor ( x, y ); + } else if ( af == 2 ) { + // read x coordinate + int x = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + // read y coordinate + int y = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + // read anchor point index + int ap = in.readTTFUShort(); + a = new GlyphPositioningTable.Anchor ( x, y, ap ); + } else if ( af == 3 ) { + // read x coordinate + int x = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + // read y coordinate + int y = ttf.convertTTFUnit2PDFUnit ( in.readTTFShort() ); + // read x device table offset + int xdo = in.readTTFUShort(); + // read y device table offset + int ydo = in.readTTFUShort(); + // read x device table (if present) + GlyphPositioningTable.DeviceTable xd; + if ( xdo != 0 ) { + xd = readPosDeviceTable ( cp, xdo ); + } else { + xd = null; + } + // read y device table (if present) + GlyphPositioningTable.DeviceTable yd; + if ( ydo != 0 ) { + yd = readPosDeviceTable ( cp, ydo ); + } else { + yd = null; + } + a = new GlyphPositioningTable.Anchor ( x, y, xd, yd ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported positioning anchor format: " + af ); + } + in.seekSet(cp); + return a; + } + + private void readCursivePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read entry/exit count + int ec = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " cursive positioning subtable format: " + subtableFormat ); + log.debug(tableTag + " cursive positioning coverage table offset: " + co ); + log.debug(tableTag + " cursive positioning entry/exit count: " + ec ); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable ( tableTag + " cursive positioning coverage", subtableOffset + co ); + // read entry/exit records + GlyphPositioningTable.Anchor[] aa = new GlyphPositioningTable.Anchor [ ec * 2 ]; + for ( int i = 0, n = ec; i < n; i++ ) { + // read entry anchor offset + int eno = in.readTTFUShort(); + // read exit anchor offset + int exo = in.readTTFUShort(); + // read entry anchor + GlyphPositioningTable.Anchor ena; + if ( eno > 0 ) { + ena = readPosAnchor ( subtableOffset + eno ); + } else { + ena = null; + } + // read exit anchor + GlyphPositioningTable.Anchor exa; + if ( exo > 0 ) { + exa = readPosAnchor ( subtableOffset + exo ); + } else { + exa = null; + } + aa [ ( i * 2 ) + 0 ] = ena; + aa [ ( i * 2 ) + 1 ] = exa; + if (log.isDebugEnabled()) { + if ( ena != null ) { + log.debug(tableTag + " cursive entry anchor [" + i + "]: " + ena ); + } + if ( exa != null ) { + log.debug(tableTag + " cursive exit anchor [" + i + "]: " + exa ); + } + } + } + // store results + seMapping = ct; + seEntries.add ( aa ); + } + + private int readCursivePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read positioning subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readCursivePosTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported cursive positioning subtable format: " + sf ); + } + return sf; + } + + private void readMarkToBasePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read mark coverage offset + int mco = in.readTTFUShort(); + // read base coverage offset + int bco = in.readTTFUShort(); + // read mark class count + int nmc = in.readTTFUShort(); + // read mark array offset + int mao = in.readTTFUShort(); + // read base array offset + int bao = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning subtable format: " + subtableFormat ); + log.debug(tableTag + " mark-to-base positioning mark coverage table offset: " + mco ); + log.debug(tableTag + " mark-to-base positioning base coverage table offset: " + bco ); + log.debug(tableTag + " mark-to-base positioning mark class count: " + nmc ); + log.debug(tableTag + " mark-to-base positioning mark array offset: " + mao ); + log.debug(tableTag + " mark-to-base positioning base array offset: " + bao ); + } + // read mark coverage table + GlyphCoverageTable mct = readCoverageTable ( tableTag + " mark-to-base positioning mark coverage", subtableOffset + mco ); + // read base coverage table + GlyphCoverageTable bct = readCoverageTable ( tableTag + " mark-to-base positioning base coverage", subtableOffset + bco ); + // read mark anchor array + // seek to mark array + in.seekSet(subtableOffset + mao); + // read mark count + int nm = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning mark count: " + nm ); + } + // read mark anchor array, where i:{0...markCount} + GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor [ nm ]; + for ( int i = 0; i < nm; i++ ) { + // read mark class + int mc = in.readTTFUShort(); + // read mark anchor offset + int ao = in.readTTFUShort(); + GlyphPositioningTable.Anchor a; + if ( ao > 0 ) { + a = readPosAnchor ( subtableOffset + mao + ao ); + } else { + a = null; + } + GlyphPositioningTable.MarkAnchor ma; + if ( a != null ) { + ma = new GlyphPositioningTable.MarkAnchor ( mc, a ); + } else { + ma = null; + } + maa [ i ] = ma; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning mark anchor[" + i + "]: " + ma); + } + + } + // read base anchor matrix + // seek to base array + in.seekSet(subtableOffset + bao); + // read base count + int nb = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning base count: " + nb ); + } + // read anchor matrix, where i:{0...baseCount - 1}, j:{0...markClassCount - 1} + GlyphPositioningTable.Anchor[][] bam = new GlyphPositioningTable.Anchor [ nb ] [ nmc ]; + for ( int i = 0; i < nb; i++ ) { + for ( int j = 0; j < nmc; j++ ) { + // read base anchor offset + int ao = in.readTTFUShort(); + GlyphPositioningTable.Anchor a; + if ( ao > 0 ) { + a = readPosAnchor ( subtableOffset + bao + ao ); + } else { + a = null; + } + bam [ i ] [ j ] = a; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-base positioning base anchor[" + i + "][" + j + "]: " + a); + } + } + } + // store results + seMapping = mct; + seEntries.add ( bct ); + seEntries.add ( Integer.valueOf ( nmc ) ); + seEntries.add ( maa ); + seEntries.add ( bam ); + } + + private int readMarkToBasePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read positioning subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readMarkToBasePosTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported mark-to-base positioning subtable format: " + sf ); + } + return sf; + } + + private void readMarkToLigaturePosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read mark coverage offset + int mco = in.readTTFUShort(); + // read ligature coverage offset + int lco = in.readTTFUShort(); + // read mark class count + int nmc = in.readTTFUShort(); + // read mark array offset + int mao = in.readTTFUShort(); + // read ligature array offset + int lao = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning subtable format: " + subtableFormat ); + log.debug(tableTag + " mark-to-ligature positioning mark coverage table offset: " + mco ); + log.debug(tableTag + " mark-to-ligature positioning ligature coverage table offset: " + lco ); + log.debug(tableTag + " mark-to-ligature positioning mark class count: " + nmc ); + log.debug(tableTag + " mark-to-ligature positioning mark array offset: " + mao ); + log.debug(tableTag + " mark-to-ligature positioning ligature array offset: " + lao ); + } + // read mark coverage table + GlyphCoverageTable mct = readCoverageTable ( tableTag + " mark-to-ligature positioning mark coverage", subtableOffset + mco ); + // read ligature coverage table + GlyphCoverageTable lct = readCoverageTable ( tableTag + " mark-to-ligature positioning ligature coverage", subtableOffset + lco ); + // read mark anchor array + // seek to mark array + in.seekSet(subtableOffset + mao); + // read mark count + int nm = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning mark count: " + nm ); + } + // read mark anchor array, where i:{0...markCount} + GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor [ nm ]; + for ( int i = 0; i < nm; i++ ) { + // read mark class + int mc = in.readTTFUShort(); + // read mark anchor offset + int ao = in.readTTFUShort(); + GlyphPositioningTable.Anchor a; + if ( ao > 0 ) { + a = readPosAnchor ( subtableOffset + mao + ao ); + } else { + a = null; + } + GlyphPositioningTable.MarkAnchor ma; + if ( a != null ) { + ma = new GlyphPositioningTable.MarkAnchor ( mc, a ); + } else { + ma = null; + } + maa [ i ] = ma; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning mark anchor[" + i + "]: " + ma); + } + } + // read ligature anchor matrix + // seek to ligature array + in.seekSet(subtableOffset + lao); + // read ligature count + int nl = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning ligature count: " + nl ); + } + // read ligature attach table offsets + int[] laoa = new int [ nl ]; + for ( int i = 0; i < nl; i++ ) { + laoa [ i ] = in.readTTFUShort(); + } + // iterate over ligature attach tables, recording maximum component count + int mxc = 0; + for ( int i = 0; i < nl; i++ ) { + int lato = laoa [ i ]; + in.seekSet ( subtableOffset + lao + lato ); + // read component count + int cc = in.readTTFUShort(); + if ( cc > mxc ) { + mxc = cc; + } + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning maximum component count: " + mxc ); + } + // read anchor matrix, where i:{0...ligatureCount - 1}, j:{0...maxComponentCount - 1}, k:{0...markClassCount - 1} + GlyphPositioningTable.Anchor[][][] lam = new GlyphPositioningTable.Anchor [ nl ][][]; + for ( int i = 0; i < nl; i++ ) { + int lato = laoa [ i ]; + // seek to ligature attach table for ligature[i] + in.seekSet ( subtableOffset + lao + lato ); + // read component count + int cc = in.readTTFUShort(); + GlyphPositioningTable.Anchor[][] lcm = new GlyphPositioningTable.Anchor [ cc ] [ nmc ]; + for ( int j = 0; j < cc; j++ ) { + for ( int k = 0; k < nmc; k++ ) { + // read ligature anchor offset + int ao = in.readTTFUShort(); + GlyphPositioningTable.Anchor a; + if ( ao > 0 ) { + a = readPosAnchor ( subtableOffset + lao + lato + ao ); + } else { + a = null; + } + lcm [ j ] [ k ] = a; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-ligature positioning ligature anchor[" + i + "][" + j + "][" + k + "]: " + a); + } + } + } + lam [ i ] = lcm; + } + // store results + seMapping = mct; + seEntries.add ( lct ); + seEntries.add ( Integer.valueOf ( nmc ) ); + seEntries.add ( Integer.valueOf ( mxc ) ); + seEntries.add ( maa ); + seEntries.add ( lam ); + } + + private int readMarkToLigaturePosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read positioning subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readMarkToLigaturePosTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported mark-to-ligature positioning subtable format: " + sf ); + } + return sf; + } + + private void readMarkToMarkPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read mark #1 coverage offset + int m1co = in.readTTFUShort(); + // read mark #2 coverage offset + int m2co = in.readTTFUShort(); + // read mark class count + int nmc = in.readTTFUShort(); + // read mark #1 array offset + int m1ao = in.readTTFUShort(); + // read mark #2 array offset + int m2ao = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning subtable format: " + subtableFormat ); + log.debug(tableTag + " mark-to-mark positioning mark #1 coverage table offset: " + m1co ); + log.debug(tableTag + " mark-to-mark positioning mark #2 coverage table offset: " + m2co ); + log.debug(tableTag + " mark-to-mark positioning mark class count: " + nmc ); + log.debug(tableTag + " mark-to-mark positioning mark #1 array offset: " + m1ao ); + log.debug(tableTag + " mark-to-mark positioning mark #2 array offset: " + m2ao ); + } + // read mark #1 coverage table + GlyphCoverageTable mct1 = readCoverageTable ( tableTag + " mark-to-mark positioning mark #1 coverage", subtableOffset + m1co ); + // read mark #2 coverage table + GlyphCoverageTable mct2 = readCoverageTable ( tableTag + " mark-to-mark positioning mark #2 coverage", subtableOffset + m2co ); + // read mark #1 anchor array + // seek to mark array + in.seekSet(subtableOffset + m1ao); + // read mark count + int nm1 = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning mark #1 count: " + nm1 ); + } + // read mark anchor array, where i:{0...mark1Count} + GlyphPositioningTable.MarkAnchor[] maa = new GlyphPositioningTable.MarkAnchor [ nm1 ]; + for ( int i = 0; i < nm1; i++ ) { + // read mark class + int mc = in.readTTFUShort(); + // read mark anchor offset + int ao = in.readTTFUShort(); + GlyphPositioningTable.Anchor a; + if ( ao > 0 ) { + a = readPosAnchor ( subtableOffset + m1ao + ao ); + } else { + a = null; + } + GlyphPositioningTable.MarkAnchor ma; + if ( a != null ) { + ma = new GlyphPositioningTable.MarkAnchor ( mc, a ); + } else { + ma = null; + } + maa [ i ] = ma; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning mark #1 anchor[" + i + "]: " + ma); + } + } + // read mark #2 anchor matrix + // seek to mark #2 array + in.seekSet(subtableOffset + m2ao); + // read mark #2 count + int nm2 = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning mark #2 count: " + nm2 ); + } + // read anchor matrix, where i:{0...mark2Count - 1}, j:{0...markClassCount - 1} + GlyphPositioningTable.Anchor[][] mam = new GlyphPositioningTable.Anchor [ nm2 ] [ nmc ]; + for ( int i = 0; i < nm2; i++ ) { + for ( int j = 0; j < nmc; j++ ) { + // read mark anchor offset + int ao = in.readTTFUShort(); + GlyphPositioningTable.Anchor a; + if ( ao > 0 ) { + a = readPosAnchor ( subtableOffset + m2ao + ao ); + } else { + a = null; + } + mam [ i ] [ j ] = a; + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark-to-mark positioning mark #2 anchor[" + i + "][" + j + "]: " + a); + } + } + } + // store results + seMapping = mct1; + seEntries.add ( mct2 ); + seEntries.add ( Integer.valueOf ( nmc ) ); + seEntries.add ( maa ); + seEntries.add ( mam ); + } + + private int readMarkToMarkPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read positioning subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readMarkToMarkPosTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported mark-to-mark positioning subtable format: " + sf ); + } + return sf; + } + + private void readContextualPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read rule set count + int nrs = in.readTTFUShort(); + // read rule set offsets + int[] rsoa = new int [ nrs ]; + for ( int i = 0; i < nrs; i++ ) { + rsoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual positioning subtable format: " + subtableFormat + " (glyphs)" ); + log.debug(tableTag + " contextual positioning coverage table offset: " + co ); + log.debug(tableTag + " contextual positioning rule set count: " + nrs ); + for ( int i = 0; i < nrs; i++ ) { + log.debug(tableTag + " contextual positioning rule set offset[" + i + "]: " + rsoa[i] ); + } + } + // read coverage table + GlyphCoverageTable ct; + if ( co > 0 ) { + ct = readCoverageTable ( tableTag + " contextual positioning coverage", subtableOffset + co ); + } else { + ct = null; + } + // read rule sets + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ nrs ]; + String header = null; + for ( int i = 0; i < nrs; i++ ) { + GlyphTable.RuleSet rs; + int rso = rsoa [ i ]; + if ( rso > 0 ) { + // seek to rule set [ i ] + in.seekSet ( subtableOffset + rso ); + // read rule count + int nr = in.readTTFUShort(); + // read rule offsets + int[] roa = new int [ nr ]; + GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ]; + for ( int j = 0; j < nr; j++ ) { + roa [ j ] = in.readTTFUShort(); + } + // read glyph sequence rules + for ( int j = 0; j < nr; j++ ) { + GlyphTable.GlyphSequenceRule r; + int ro = roa [ j ]; + if ( ro > 0 ) { + // seek to rule [ j ] + in.seekSet ( subtableOffset + rso + ro ); + // read glyph count + int ng = in.readTTFUShort(); + // read rule lookup count + int nl = in.readTTFUShort(); + // read glyphs + int[] glyphs = new int [ ng - 1 ]; + for ( int k = 0, nk = glyphs.length; k < nk; k++ ) { + glyphs [ k ] = in.readTTFUShort(); + } + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + r = new GlyphTable.GlyphSequenceRule ( lookups, ng, glyphs ); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new GlyphTable.HomogeneousRuleSet ( ra ); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add ( rsa ); + } + + private void readContextualPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read class def table offset + int cdo = in.readTTFUShort(); + // read class rule set count + int ngc = in.readTTFUShort(); + // read class rule set offsets + int[] csoa = new int [ ngc ]; + for ( int i = 0; i < ngc; i++ ) { + csoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual positioning subtable format: " + subtableFormat + " (glyph classes)" ); + log.debug(tableTag + " contextual positioning coverage table offset: " + co ); + log.debug(tableTag + " contextual positioning class set count: " + ngc ); + for ( int i = 0; i < ngc; i++ ) { + log.debug(tableTag + " contextual positioning class set offset[" + i + "]: " + csoa[i] ); + } + } + // read coverage table + GlyphCoverageTable ct; + if ( co > 0 ) { + ct = readCoverageTable ( tableTag + " contextual positioning coverage", subtableOffset + co ); + } else { + ct = null; + } + // read class definition table + GlyphClassTable cdt; + if ( cdo > 0 ) { + cdt = readClassDefTable ( tableTag + " contextual positioning class definition", subtableOffset + cdo ); + } else { + cdt = null; + } + // read rule sets + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ ngc ]; + String header = null; + for ( int i = 0; i < ngc; i++ ) { + int cso = csoa [ i ]; + GlyphTable.RuleSet rs; + if ( cso > 0 ) { + // seek to rule set [ i ] + in.seekSet ( subtableOffset + cso ); + // read rule count + int nr = in.readTTFUShort(); + // read rule offsets + int[] roa = new int [ nr ]; + GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ]; + for ( int j = 0; j < nr; j++ ) { + roa [ j ] = in.readTTFUShort(); + } + // read glyph sequence rules + for ( int j = 0; j < nr; j++ ) { + int ro = roa [ j ]; + GlyphTable.ClassSequenceRule r; + if ( ro > 0 ) { + // seek to rule [ j ] + in.seekSet ( subtableOffset + cso + ro ); + // read glyph count + int ng = in.readTTFUShort(); + // read rule lookup count + int nl = in.readTTFUShort(); + // read classes + int[] classes = new int [ ng - 1 ]; + for ( int k = 0, nk = classes.length; k < nk; k++ ) { + classes [ k ] = in.readTTFUShort(); + } + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + r = new GlyphTable.ClassSequenceRule ( lookups, ng, classes ); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new GlyphTable.HomogeneousRuleSet ( ra ); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add ( cdt ); + seEntries.add ( Integer.valueOf ( ngc ) ); + seEntries.add ( rsa ); + } + + private void readContextualPosTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read glyph (input sequence length) count + int ng = in.readTTFUShort(); + // read positioning lookup count + int nl = in.readTTFUShort(); + // read glyph coverage offsets, one per glyph input sequence length count + int[] gcoa = new int [ ng ]; + for ( int i = 0; i < ng; i++ ) { + gcoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " contextual positioning subtable format: " + subtableFormat + " (glyph sets)" ); + log.debug(tableTag + " contextual positioning glyph input sequence length count: " + ng ); + log.debug(tableTag + " contextual positioning lookup count: " + nl ); + for ( int i = 0; i < ng; i++ ) { + log.debug(tableTag + " contextual positioning coverage table offset[" + i + "]: " + gcoa[i] ); + } + } + // read coverage tables + GlyphCoverageTable[] gca = new GlyphCoverageTable [ ng ]; + for ( int i = 0; i < ng; i++ ) { + int gco = gcoa [ i ]; + GlyphCoverageTable gct; + if ( gco > 0 ) { + gct = readCoverageTable ( tableTag + " contextual positioning coverage[" + i + "]", subtableOffset + gcoa[i] ); + } else { + gct = null; + } + gca [ i ] = gct; + } + // read rule lookups + String header = null; + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + // construct rule, rule set, and rule set array + GlyphTable.Rule r = new GlyphTable.CoverageSequenceRule ( lookups, ng, gca ); + GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet ( new GlyphTable.Rule[] {r} ); + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[] {rs}; + // store results + assert ( gca != null ) && ( gca.length > 0 ); + seMapping = gca[0]; + seEntries.add ( rsa ); + } + + private int readContextualPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read positioning subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readContextualPosTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 2 ) { + readContextualPosTableFormat2 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 3 ) { + readContextualPosTableFormat3 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported contextual positioning subtable format: " + sf ); + } + return sf; + } + + private void readChainedContextualPosTableFormat1(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read rule set count + int nrs = in.readTTFUShort(); + // read rule set offsets + int[] rsoa = new int [ nrs ]; + for ( int i = 0; i < nrs; i++ ) { + rsoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual positioning subtable format: " + subtableFormat + " (glyphs)" ); + log.debug(tableTag + " chained contextual positioning coverage table offset: " + co ); + log.debug(tableTag + " chained contextual positioning rule set count: " + nrs ); + for ( int i = 0; i < nrs; i++ ) { + log.debug(tableTag + " chained contextual positioning rule set offset[" + i + "]: " + rsoa[i] ); + } + } + // read coverage table + GlyphCoverageTable ct; + if ( co > 0 ) { + ct = readCoverageTable ( tableTag + " chained contextual positioning coverage", subtableOffset + co ); + } else { + ct = null; + } + // read rule sets + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ nrs ]; + String header = null; + for ( int i = 0; i < nrs; i++ ) { + GlyphTable.RuleSet rs; + int rso = rsoa [ i ]; + if ( rso > 0 ) { + // seek to rule set [ i ] + in.seekSet ( subtableOffset + rso ); + // read rule count + int nr = in.readTTFUShort(); + // read rule offsets + int[] roa = new int [ nr ]; + GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ]; + for ( int j = 0; j < nr; j++ ) { + roa [ j ] = in.readTTFUShort(); + } + // read glyph sequence rules + for ( int j = 0; j < nr; j++ ) { + GlyphTable.ChainedGlyphSequenceRule r; + int ro = roa [ j ]; + if ( ro > 0 ) { + // seek to rule [ j ] + in.seekSet ( subtableOffset + rso + ro ); + // read backtrack glyph count + int nbg = in.readTTFUShort(); + // read backtrack glyphs + int[] backtrackGlyphs = new int [ nbg ]; + for ( int k = 0, nk = backtrackGlyphs.length; k < nk; k++ ) { + backtrackGlyphs [ k ] = in.readTTFUShort(); + } + // read input glyph count + int nig = in.readTTFUShort(); + // read glyphs + int[] glyphs = new int [ nig - 1 ]; + for ( int k = 0, nk = glyphs.length; k < nk; k++ ) { + glyphs [ k ] = in.readTTFUShort(); + } + // read lookahead glyph count + int nlg = in.readTTFUShort(); + // read lookahead glyphs + int[] lookaheadGlyphs = new int [ nlg ]; + for ( int k = 0, nk = lookaheadGlyphs.length; k < nk; k++ ) { + lookaheadGlyphs [ k ] = in.readTTFUShort(); + } + // read rule lookup count + int nl = in.readTTFUShort(); + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + r = new GlyphTable.ChainedGlyphSequenceRule ( lookups, nig, glyphs, backtrackGlyphs, lookaheadGlyphs ); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new GlyphTable.HomogeneousRuleSet ( ra ); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add ( rsa ); + } + + private void readChainedContextualPosTableFormat2(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read coverage offset + int co = in.readTTFUShort(); + // read backtrack class def table offset + int bcdo = in.readTTFUShort(); + // read input class def table offset + int icdo = in.readTTFUShort(); + // read lookahead class def table offset + int lcdo = in.readTTFUShort(); + // read class set count + int ngc = in.readTTFUShort(); + // read class set offsets + int[] csoa = new int [ ngc ]; + for ( int i = 0; i < ngc; i++ ) { + csoa [ i ] = in.readTTFUShort(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual positioning subtable format: " + subtableFormat + " (glyph classes)" ); + log.debug(tableTag + " chained contextual positioning coverage table offset: " + co ); + log.debug(tableTag + " chained contextual positioning class set count: " + ngc ); + for ( int i = 0; i < ngc; i++ ) { + log.debug(tableTag + " chained contextual positioning class set offset[" + i + "]: " + csoa[i] ); + } + } + // read coverage table + GlyphCoverageTable ct; + if ( co > 0 ) { + ct = readCoverageTable ( tableTag + " chained contextual positioning coverage", subtableOffset + co ); + } else { + ct = null; + } + // read backtrack class definition table + GlyphClassTable bcdt; + if ( bcdo > 0 ) { + bcdt = readClassDefTable ( tableTag + " contextual positioning backtrack class definition", subtableOffset + bcdo ); + } else { + bcdt = null; + } + // read input class definition table + GlyphClassTable icdt; + if ( icdo > 0 ) { + icdt = readClassDefTable ( tableTag + " contextual positioning input class definition", subtableOffset + icdo ); + } else { + icdt = null; + } + // read lookahead class definition table + GlyphClassTable lcdt; + if ( lcdo > 0 ) { + lcdt = readClassDefTable ( tableTag + " contextual positioning lookahead class definition", subtableOffset + lcdo ); + } else { + lcdt = null; + } + // read rule sets + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet [ ngc ]; + String header = null; + for ( int i = 0; i < ngc; i++ ) { + int cso = csoa [ i ]; + GlyphTable.RuleSet rs; + if ( cso > 0 ) { + // seek to rule set [ i ] + in.seekSet ( subtableOffset + cso ); + // read rule count + int nr = in.readTTFUShort(); + // read rule offsets + int[] roa = new int [ nr ]; + GlyphTable.Rule[] ra = new GlyphTable.Rule [ nr ]; + for ( int j = 0; j < nr; j++ ) { + roa [ j ] = in.readTTFUShort(); + } + // read glyph sequence rules + for ( int j = 0; j < nr; j++ ) { + GlyphTable.ChainedClassSequenceRule r; + int ro = roa [ j ]; + if ( ro > 0 ) { + // seek to rule [ j ] + in.seekSet ( subtableOffset + cso + ro ); + // read backtrack glyph class count + int nbc = in.readTTFUShort(); + // read backtrack glyph classes + int[] backtrackClasses = new int [ nbc ]; + for ( int k = 0, nk = backtrackClasses.length; k < nk; k++ ) { + backtrackClasses [ k ] = in.readTTFUShort(); + } + // read input glyph class count + int nic = in.readTTFUShort(); + // read input glyph classes + int[] classes = new int [ nic - 1 ]; + for ( int k = 0, nk = classes.length; k < nk; k++ ) { + classes [ k ] = in.readTTFUShort(); + } + // read lookahead glyph class count + int nlc = in.readTTFUShort(); + // read lookahead glyph classes + int[] lookaheadClasses = new int [ nlc ]; + for ( int k = 0, nk = lookaheadClasses.length; k < nk; k++ ) { + lookaheadClasses [ k ] = in.readTTFUShort(); + } + // read rule lookup count + int nl = in.readTTFUShort(); + // read rule lookups + if (log.isDebugEnabled()) { + header = tableTag + " contextual positioning lookups @rule[" + i + "][" + j + "]: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + r = new GlyphTable.ChainedClassSequenceRule ( lookups, nic, classes, backtrackClasses, lookaheadClasses ); + } else { + r = null; + } + ra [ j ] = r; + } + rs = new GlyphTable.HomogeneousRuleSet ( ra ); + } else { + rs = null; + } + rsa [ i ] = rs; + } + // store results + seMapping = ct; + seEntries.add ( icdt ); + seEntries.add ( bcdt ); + seEntries.add ( lcdt ); + seEntries.add ( Integer.valueOf ( ngc ) ); + seEntries.add ( rsa ); + } + + private void readChainedContextualPosTableFormat3(int lookupType, int lookupFlags, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read backtrack glyph count + int nbg = in.readTTFUShort(); + // read backtrack glyph coverage offsets + int[] bgcoa = new int [ nbg ]; + for ( int i = 0; i < nbg; i++ ) { + bgcoa [ i ] = in.readTTFUShort(); + } + // read input glyph count + int nig = in.readTTFUShort(); + // read backtrack glyph coverage offsets + int[] igcoa = new int [ nig ]; + for ( int i = 0; i < nig; i++ ) { + igcoa [ i ] = in.readTTFUShort(); + } + // read lookahead glyph count + int nlg = in.readTTFUShort(); + // read backtrack glyph coverage offsets + int[] lgcoa = new int [ nlg ]; + for ( int i = 0; i < nlg; i++ ) { + lgcoa [ i ] = in.readTTFUShort(); + } + // read positioning lookup count + int nl = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " chained contextual positioning subtable format: " + subtableFormat + " (glyph sets)" ); + log.debug(tableTag + " chained contextual positioning backtrack glyph count: " + nbg ); + for ( int i = 0; i < nbg; i++ ) { + log.debug(tableTag + " chained contextual positioning backtrack coverage table offset[" + i + "]: " + bgcoa[i] ); + } + log.debug(tableTag + " chained contextual positioning input glyph count: " + nig ); + for ( int i = 0; i < nig; i++ ) { + log.debug(tableTag + " chained contextual positioning input coverage table offset[" + i + "]: " + igcoa[i] ); + } + log.debug(tableTag + " chained contextual positioning lookahead glyph count: " + nlg ); + for ( int i = 0; i < nlg; i++ ) { + log.debug(tableTag + " chained contextual positioning lookahead coverage table offset[" + i + "]: " + lgcoa[i] ); + } + log.debug(tableTag + " chained contextual positioning lookup count: " + nl ); + } + // read backtrack coverage tables + GlyphCoverageTable[] bgca = new GlyphCoverageTable[nbg]; + for ( int i = 0; i < nbg; i++ ) { + int bgco = bgcoa [ i ]; + GlyphCoverageTable bgct; + if ( bgco > 0 ) { + bgct = readCoverageTable ( tableTag + " chained contextual positioning backtrack coverage[" + i + "]", subtableOffset + bgco ); + } else { + bgct = null; + } + bgca[i] = bgct; + } + // read input coverage tables + GlyphCoverageTable[] igca = new GlyphCoverageTable[nig]; + for ( int i = 0; i < nig; i++ ) { + int igco = igcoa [ i ]; + GlyphCoverageTable igct; + if ( igco > 0 ) { + igct = readCoverageTable ( tableTag + " chained contextual positioning input coverage[" + i + "]", subtableOffset + igco ); + } else { + igct = null; + } + igca[i] = igct; + } + // read lookahead coverage tables + GlyphCoverageTable[] lgca = new GlyphCoverageTable[nlg]; + for ( int i = 0; i < nlg; i++ ) { + int lgco = lgcoa [ i ]; + GlyphCoverageTable lgct; + if ( lgco > 0 ) { + lgct = readCoverageTable ( tableTag + " chained contextual positioning lookahead coverage[" + i + "]", subtableOffset + lgco ); + } else { + lgct = null; + } + lgca[i] = lgct; + } + // read rule lookups + String header = null; + if (log.isDebugEnabled()) { + header = tableTag + " chained contextual positioning lookups: "; + } + GlyphTable.RuleLookup[] lookups = readRuleLookups ( nl, header ); + // construct rule, rule set, and rule set array + GlyphTable.Rule r = new GlyphTable.ChainedCoverageSequenceRule ( lookups, nig, igca, bgca, lgca ); + GlyphTable.RuleSet rs = new GlyphTable.HomogeneousRuleSet ( new GlyphTable.Rule[] {r} ); + GlyphTable.RuleSet[] rsa = new GlyphTable.RuleSet[] {rs}; + // store results + assert ( igca != null ) && ( igca.length > 0 ); + seMapping = igca[0]; + seEntries.add ( rsa ); + } + + private int readChainedContextualPosTable(int lookupType, int lookupFlags, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read positioning subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readChainedContextualPosTableFormat1 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 2 ) { + readChainedContextualPosTableFormat2 ( lookupType, lookupFlags, subtableOffset, sf ); + } else if ( sf == 3 ) { + readChainedContextualPosTableFormat3 ( lookupType, lookupFlags, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported chained contextual positioning subtable format: " + sf ); + } + return sf; + } + + private void readExtensionPosTableFormat1(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset, int subtableFormat) throws IOException { + String tableTag = "GPOS"; + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read extension lookup type + int lt = in.readTTFUShort(); + // read extension offset + long eo = in.readTTFULong(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " extension positioning subtable format: " + subtableFormat ); + log.debug(tableTag + " extension positioning lookup type: " + lt ); + log.debug(tableTag + " extension positioning lookup table offset: " + eo ); + } + // read referenced subtable from extended offset + readGPOSSubtable ( lt, lookupFlags, lookupSequence, subtableSequence, subtableOffset + eo ); + } + + private int readExtensionPosTable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read positioning subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readExtensionPosTableFormat1 ( lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported extension positioning subtable format: " + sf ); + } + return sf; + } + + private void readGPOSSubtable(int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, long subtableOffset) throws IOException { + initATSubState(); + int subtableFormat = -1; + switch ( lookupType ) { + case GPOSLookupType.SINGLE: + subtableFormat = readSinglePosTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GPOSLookupType.PAIR: + subtableFormat = readPairPosTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GPOSLookupType.CURSIVE: + subtableFormat = readCursivePosTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GPOSLookupType.MARK_TO_BASE: + subtableFormat = readMarkToBasePosTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GPOSLookupType.MARK_TO_LIGATURE: + subtableFormat = readMarkToLigaturePosTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GPOSLookupType.MARK_TO_MARK: + subtableFormat = readMarkToMarkPosTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GPOSLookupType.CONTEXTUAL: + subtableFormat = readContextualPosTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GPOSLookupType.CHAINED_CONTEXTUAL: + subtableFormat = readChainedContextualPosTable ( lookupType, lookupFlags, subtableOffset ); + break; + case GPOSLookupType.EXTENSION: + subtableFormat = readExtensionPosTable ( lookupType, lookupFlags, lookupSequence, subtableSequence, subtableOffset ); + break; + default: + break; + } + extractSESubState ( GlyphTable.GLYPH_TABLE_TYPE_POSITIONING, lookupType, lookupFlags, lookupSequence, subtableSequence, subtableFormat ); + resetATSubState(); + } + + private void readLookupTable(String tableTag, int lookupSequence, long lookupTable) throws IOException { + boolean isGSUB = tableTag.equals ( "GSUB" ); + boolean isGPOS = tableTag.equals ( "GPOS" ); + in.seekSet(lookupTable); + // read lookup type + int lt = in.readTTFUShort(); + // read lookup flags + int lf = in.readTTFUShort(); + // read sub-table count + int ns = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + String lts; + if ( isGSUB ) { + lts = GSUBLookupType.toString ( lt ); + } else if ( isGPOS ) { + lts = GPOSLookupType.toString ( lt ); + } else { + lts = "?"; + } + log.debug(tableTag + " lookup table type: " + lt + " (" + lts + ")" ); + log.debug(tableTag + " lookup table flags: " + lf + " (" + LookupFlag.toString ( lf ) + ")" ); + log.debug(tableTag + " lookup table subtable count: " + ns ); + } + // read subtable offsets + int[] soa = new int[ns]; + for ( int i = 0; i < ns; i++ ) { + int so = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup table subtable offset: " + so ); + } + soa[i] = so; + } + // read mark filtering set + if ( ( lf & LookupFlag.USE_MARK_FILTERING_SET ) != 0 ) { + // read mark filtering set + int fs = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup table mark filter set: " + fs ); + } + } + // read subtables + for ( int i = 0; i < ns; i++ ) { + int so = soa[i]; + if ( isGSUB ) { + readGSUBSubtable ( lt, lf, lookupSequence, i, lookupTable + so ); + } else if ( isGPOS ) { + readGPOSSubtable ( lt, lf, lookupSequence, i, lookupTable + so ); + } + } + } + + private void readLookupList(String tableTag, long lookupList) throws IOException { + in.seekSet(lookupList); + // read lookup record count + int nl = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup list record count: " + nl ); + } + if ( nl > 0 ) { + int[] loa = new int[nl]; + // read lookup records + for ( int i = 0, n = nl; i < n; i++ ) { + int lo = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup table offset: " + lo ); + } + loa[i] = lo; + } + // read lookup tables + for ( int i = 0, n = nl; i < n; i++ ) { + if (log.isDebugEnabled()) { + log.debug(tableTag + " lookup index: " + i ); + } + readLookupTable ( tableTag, i, lookupList + loa [ i ] ); + } + } + } + + /** + * Read the common layout tables (used by GSUB and GPOS). + * @param tableTag tag of table being read + * @param scriptList offset to script list from beginning of font file + * @param featureList offset to feature list from beginning of font file + * @param lookupList offset to lookup list from beginning of font file + * @throws IOException In case of a I/O problem + */ + private void readCommonLayoutTables(String tableTag, long scriptList, long featureList, long lookupList) throws IOException { + if ( scriptList > 0 ) { + readScriptList ( tableTag, scriptList ); + } + if ( featureList > 0 ) { + readFeatureList ( tableTag, featureList ); + } + if ( lookupList > 0 ) { + readLookupList ( tableTag, lookupList ); + } + } + + private void readGDEFClassDefTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + initATSubState(); + in.seekSet(subtableOffset); + // subtable is a bare class definition table + GlyphClassTable ct = readClassDefTable ( tableTag + " glyph class definition table", subtableOffset ); + // store results + seMapping = ct; + // extract subtable + extractSESubState ( GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.GLYPH_CLASS, 0, lookupSequence, 0, 1 ); + resetATSubState(); + } + + private void readGDEFAttachmentTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + initATSubState(); + in.seekSet(subtableOffset); + // read coverage offset + int co = in.readTTFUShort(); + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " attachment point coverage table offset: " + co ); + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable ( tableTag + " attachment point coverage", subtableOffset + co ); + // store results + seMapping = ct; + // extract subtable + extractSESubState ( GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.ATTACHMENT_POINT, 0, lookupSequence, 0, 1 ); + resetATSubState(); + } + + private void readGDEFLigatureCaretTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + initATSubState(); + in.seekSet(subtableOffset); + // read coverage offset + int co = in.readTTFUShort(); + // read ligature glyph count + int nl = in.readTTFUShort(); + // read ligature glyph table offsets + int[] lgto = new int [ nl ]; + for ( int i = 0; i < nl; i++ ) { + lgto [ i ] = in.readTTFUShort(); + } + + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " ligature caret coverage table offset: " + co ); + log.debug(tableTag + " ligature caret ligature glyph count: " + nl ); + for ( int i = 0; i < nl; i++ ) { + log.debug(tableTag + " ligature glyph table offset[" + i + "]: " + lgto[i] ); + } + } + // read coverage table + GlyphCoverageTable ct = readCoverageTable ( tableTag + " ligature caret coverage", subtableOffset + co ); + // store results + seMapping = ct; + // extract subtable + extractSESubState ( GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.LIGATURE_CARET, 0, lookupSequence, 0, 1 ); + resetATSubState(); + } + + private void readGDEFMarkAttachmentTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + initATSubState(); + in.seekSet(subtableOffset); + // subtable is a bare class definition table + GlyphClassTable ct = readClassDefTable ( tableTag + " glyph class definition table", subtableOffset ); + // store results + seMapping = ct; + // extract subtable + extractSESubState ( GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.MARK_ATTACHMENT, 0, lookupSequence, 0, 1 ); + resetATSubState(); + } + + private void readGDEFMarkGlyphsTableFormat1(String tableTag, int lookupSequence, long subtableOffset, int subtableFormat) throws IOException { + initATSubState(); + in.seekSet(subtableOffset); + // skip over format (already known) + in.skip ( 2 ); + // read mark set class count + int nmc = in.readTTFUShort(); + long[] mso = new long [ nmc ]; + // read mark set coverage offsets + for ( int i = 0; i < nmc; i++ ) { + mso [ i ] = in.readTTFULong(); + } + // dump info if debugging + if (log.isDebugEnabled()) { + log.debug(tableTag + " mark set subtable format: " + subtableFormat + " (glyph sets)" ); + log.debug(tableTag + " mark set class count: " + nmc ); + for ( int i = 0; i < nmc; i++ ) { + log.debug(tableTag + " mark set coverage table offset[" + i + "]: " + mso[i] ); + } + } + // read mark set coverage tables, one per class + GlyphCoverageTable[] msca = new GlyphCoverageTable[nmc]; + for ( int i = 0; i < nmc; i++ ) { + msca[i] = readCoverageTable ( tableTag + " mark set coverage[" + i + "]", subtableOffset + mso[i] ); + } + // create combined class table from per-class coverage tables + GlyphClassTable ct = GlyphClassTable.createClassTable ( Arrays.asList ( msca ) ); + // store results + seMapping = ct; + // extract subtable + extractSESubState ( GlyphTable.GLYPH_TABLE_TYPE_DEFINITION, GDEFLookupType.MARK_ATTACHMENT, 0, lookupSequence, 0, 1 ); + resetATSubState(); + } + + private void readGDEFMarkGlyphsTable(String tableTag, int lookupSequence, long subtableOffset) throws IOException { + in.seekSet(subtableOffset); + // read mark set subtable format + int sf = in.readTTFUShort(); + if ( sf == 1 ) { + readGDEFMarkGlyphsTableFormat1 ( tableTag, lookupSequence, subtableOffset, sf ); + } else { + throw new AdvancedTypographicTableFormatException ( "unsupported mark glyph sets subtable format: " + sf ); + } + } + + /** + * Read the GDEF table. + * @throws IOException In case of a I/O problem + */ + private void readGDEF() throws IOException { + String tableTag = "GDEF"; + // Initialize temporary state + initATState(); + // Read glyph definition (GDEF) table + TTFDirTabEntry dirTab = ttf.getDirectoryEntry ( tableTag ); + if ( gdef != null ) { + if (log.isDebugEnabled()) { + log.debug(tableTag + ": ignoring duplicate table"); + } + } else if (dirTab != null) { + ttf.seekTab(in, tableTag, 0); + long version = in.readTTFULong(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " version: " + ( version / 65536 ) + "." + ( version % 65536 )); + } + // glyph class definition table offset (may be null) + int cdo = in.readTTFUShort(); + // attach point list offset (may be null) + int apo = in.readTTFUShort(); + // ligature caret list offset (may be null) + int lco = in.readTTFUShort(); + // mark attach class definition table offset (may be null) + int mao = in.readTTFUShort(); + // mark glyph sets definition table offset (may be null) + int mgo; + if ( version >= 0x00010002 ) { + mgo = in.readTTFUShort(); + } else { + mgo = 0; + } + if (log.isDebugEnabled()) { + log.debug(tableTag + " glyph class definition table offset: " + cdo ); + log.debug(tableTag + " attachment point list offset: " + apo ); + log.debug(tableTag + " ligature caret list offset: " + lco ); + log.debug(tableTag + " mark attachment class definition table offset: " + mao ); + log.debug(tableTag + " mark glyph set definitions table offset: " + mgo ); + } + // initialize subtable sequence number + int seqno = 0; + // obtain offset to start of gdef table + long to = dirTab.getOffset(); + // (optionally) read glyph class definition subtable + if ( cdo != 0 ) { + readGDEFClassDefTable ( tableTag, seqno++, to + cdo ); + } + // (optionally) read glyph attachment point subtable + if ( apo != 0 ) { + readGDEFAttachmentTable ( tableTag, seqno++, to + apo ); + } + // (optionally) read ligature caret subtable + if ( lco != 0 ) { + readGDEFLigatureCaretTable ( tableTag, seqno++, to + lco ); + } + // (optionally) read mark attachment class subtable + if ( mao != 0 ) { + readGDEFMarkAttachmentTable ( tableTag, seqno++, to + mao ); + } + // (optionally) read mark glyph sets subtable + if ( mgo != 0 ) { + readGDEFMarkGlyphsTable ( tableTag, seqno++, to + mgo ); + } + GlyphDefinitionTable gdef; + if ( ( gdef = constructGDEF() ) != null ) { + this.gdef = gdef; + } + } + } + + /** + * Read the GSUB table. + * @throws IOException In case of a I/O problem + */ + private void readGSUB() throws IOException { + String tableTag = "GSUB"; + // Initialize temporary state + initATState(); + // Read glyph substitution (GSUB) table + TTFDirTabEntry dirTab = ttf.getDirectoryEntry ( tableTag ); + if ( gpos != null ) { + if (log.isDebugEnabled()) { + log.debug(tableTag + ": ignoring duplicate table"); + } + } else if (dirTab != null) { + ttf.seekTab(in, tableTag, 0); + int version = in.readTTFLong(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " version: " + ( version / 65536 ) + "." + ( version % 65536 )); + } + int slo = in.readTTFUShort(); + int flo = in.readTTFUShort(); + int llo = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script list offset: " + slo ); + log.debug(tableTag + " feature list offset: " + flo ); + log.debug(tableTag + " lookup list offset: " + llo ); + } + long to = dirTab.getOffset(); + readCommonLayoutTables ( tableTag, to + slo, to + flo, to + llo ); + GlyphSubstitutionTable gsub; + if ( ( gsub = constructGSUB() ) != null ) { + this.gsub = gsub; + } + } + } + + /** + * Read the GPOS table. + * @throws IOException In case of a I/O problem + */ + private void readGPOS() throws IOException { + String tableTag = "GPOS"; + // Initialize temporary state + initATState(); + // Read glyph positioning (GPOS) table + TTFDirTabEntry dirTab = ttf.getDirectoryEntry ( tableTag ); + if ( gpos != null ) { + if (log.isDebugEnabled()) { + log.debug(tableTag + ": ignoring duplicate table"); + } + } else if (dirTab != null) { + ttf.seekTab(in, tableTag, 0); + int version = in.readTTFLong(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " version: " + ( version / 65536 ) + "." + ( version % 65536 )); + } + int slo = in.readTTFUShort(); + int flo = in.readTTFUShort(); + int llo = in.readTTFUShort(); + if (log.isDebugEnabled()) { + log.debug(tableTag + " script list offset: " + slo ); + log.debug(tableTag + " feature list offset: " + flo ); + log.debug(tableTag + " lookup list offset: " + llo ); + } + long to = dirTab.getOffset(); + readCommonLayoutTables ( tableTag, to + slo, to + flo, to + llo ); + GlyphPositioningTable gpos; + if ( ( gpos = constructGPOS() ) != null ) { + this.gpos = gpos; + } + } + } + + /** + * Construct the (internal representation of the) GDEF table based on previously + * parsed state. + * @returns glyph definition table or null if insufficient or invalid state + */ + private GlyphDefinitionTable constructGDEF() { + GlyphDefinitionTable gdef = null; + List subtables; + if ( ( subtables = constructGDEFSubtables() ) != null ) { + if ( subtables.size() > 0 ) { + gdef = new GlyphDefinitionTable ( subtables ); + } + } + resetATState(); + return gdef; + } + + /** + * Construct the (internal representation of the) GSUB table based on previously + * parsed state. + * @returns glyph substitution table or null if insufficient or invalid state + */ + private GlyphSubstitutionTable constructGSUB() { + GlyphSubstitutionTable gsub = null; + Map lookups; + if ( ( lookups = constructLookups() ) != null ) { + List subtables; + if ( ( subtables = constructGSUBSubtables() ) != null ) { + if ( ( lookups.size() > 0 ) && ( subtables.size() > 0 ) ) { + gsub = new GlyphSubstitutionTable ( gdef, lookups, subtables ); + } + } + } + resetATState(); + return gsub; + } + + /** + * Construct the (internal representation of the) GPOS table based on previously + * parsed state. + * @returns glyph positioning table or null if insufficient or invalid state + */ + private GlyphPositioningTable constructGPOS() { + GlyphPositioningTable gpos = null; + Map lookups; + if ( ( lookups = constructLookups() ) != null ) { + List subtables; + if ( ( subtables = constructGPOSSubtables() ) != null ) { + if ( ( lookups.size() > 0 ) && ( subtables.size() > 0 ) ) { + gpos = new GlyphPositioningTable ( gdef, lookups, subtables ); + } + } + } + resetATState(); + return gpos; + } + + private void constructLookupsFeature ( Map lookups, String st, String lt, String fid ) { + Object[] fp = (Object[]) seFeatures.get ( fid ); + if ( fp != null ) { + assert fp.length == 2; + String ft = (String) fp[0]; // feature tag + List/*<String>*/ lul = (List) fp[1]; // list of lookup table ids + if ( ( ft != null ) && ( lul != null ) && ( lul.size() > 0 ) ) { + GlyphTable.LookupSpec ls = new GlyphTable.LookupSpec ( st, lt, ft ); + lookups.put ( ls, lul ); + } + } + } + + private void constructLookupsFeatures ( Map lookups, String st, String lt, List/*<String>*/ fids ) { + for ( Iterator fit = fids.iterator(); fit.hasNext();) { + String fid = (String) fit.next(); + constructLookupsFeature ( lookups, st, lt, fid ); + } + } + + private void constructLookupsLanguage ( Map lookups, String st, String lt, Map/*<String,Object[2]>*/ languages ) { + Object[] lp = (Object[]) languages.get ( lt ); + if ( lp != null ) { + assert lp.length == 2; + if ( lp[0] != null ) { // required feature id + constructLookupsFeature ( lookups, st, lt, (String) lp[0] ); + } + if ( lp[1] != null ) { // non-required features ids + constructLookupsFeatures ( lookups, st, lt, (List) lp[1] ); + } + } + } + + private void constructLookupsLanguages ( Map lookups, String st, List/*<String>*/ ll, Map/*<String,Object[2]>*/ languages ) { + for ( Iterator lit = ll.iterator(); lit.hasNext();) { + String lt = (String) lit.next(); + constructLookupsLanguage ( lookups, st, lt, languages ); + } + } + + private Map constructLookups() { + Map/*<GlyphTable.LookupSpec,List<String>>*/ lookups = new java.util.LinkedHashMap(); + for ( Iterator sit = seScripts.keySet().iterator(); sit.hasNext();) { + String st = (String) sit.next(); + Object[] sp = (Object[]) seScripts.get ( st ); + if ( sp != null ) { + assert sp.length == 3; + Map/*<String,Object[2]>*/ languages = (Map) sp[2]; + if ( sp[0] != null ) { // default language + constructLookupsLanguage ( lookups, st, (String) sp[0], languages ); + } + if ( sp[1] != null ) { // non-default languages + constructLookupsLanguages ( lookups, st, (List) sp[1], languages ); + } + } + } + return lookups; + } + + private List constructGDEFSubtables() { + List/*<GlyphDefinitionSubtable>*/ subtables = new java.util.ArrayList(); + if ( seSubtables != null ) { + for ( Iterator it = seSubtables.iterator(); it.hasNext();) { + Object[] stp = (Object[]) it.next(); + GlyphSubtable st; + if ( ( st = constructGDEFSubtable ( stp ) ) != null ) { + subtables.add ( st ); + } + } + } + return subtables; + } + + private GlyphSubtable constructGDEFSubtable ( Object[] stp ) { + GlyphSubtable st = null; + assert ( stp != null ) && ( stp.length == 8 ); + Integer tt = (Integer) stp[0]; // table type + Integer lt = (Integer) stp[1]; // lookup type + Integer ln = (Integer) stp[2]; // lookup sequence number + Integer lf = (Integer) stp[3]; // lookup flags + Integer sn = (Integer) stp[4]; // subtable sequence number + Integer sf = (Integer) stp[5]; // subtable format + GlyphMappingTable mapping = (GlyphMappingTable) stp[6]; + List entries = (List) stp[7]; + if ( tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION ) { + int type = GDEFLookupType.getSubtableType ( lt.intValue() ); + String lid = "lu" + ln.intValue(); + int sequence = sn.intValue(); + int flags = lf.intValue(); + int format = sf.intValue(); + st = GlyphDefinitionTable.createSubtable ( type, lid, sequence, flags, format, mapping, entries ); + } + return st; + } + + private List constructGSUBSubtables() { + List/*<GlyphSubtable>*/ subtables = new java.util.ArrayList(); + if ( seSubtables != null ) { + for ( Iterator it = seSubtables.iterator(); it.hasNext();) { + Object[] stp = (Object[]) it.next(); + GlyphSubtable st; + if ( ( st = constructGSUBSubtable ( stp ) ) != null ) { + subtables.add ( st ); + } + } + } + return subtables; + } + + private GlyphSubtable constructGSUBSubtable ( Object[] stp ) { + GlyphSubtable st = null; + assert ( stp != null ) && ( stp.length == 8 ); + Integer tt = (Integer) stp[0]; // table type + Integer lt = (Integer) stp[1]; // lookup type + Integer ln = (Integer) stp[2]; // lookup sequence number + Integer lf = (Integer) stp[3]; // lookup flags + Integer sn = (Integer) stp[4]; // subtable sequence number + Integer sf = (Integer) stp[5]; // subtable format + GlyphCoverageTable coverage = (GlyphCoverageTable) stp[6]; + List entries = (List) stp[7]; + if ( tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION ) { + int type = GSUBLookupType.getSubtableType ( lt.intValue() ); + String lid = "lu" + ln.intValue(); + int sequence = sn.intValue(); + int flags = lf.intValue(); + int format = sf.intValue(); + st = GlyphSubstitutionTable.createSubtable ( type, lid, sequence, flags, format, coverage, entries ); + } + return st; + } + + private List constructGPOSSubtables() { + List/*<GlyphSubtable>*/ subtables = new java.util.ArrayList(); + if ( seSubtables != null ) { + for ( Iterator it = seSubtables.iterator(); it.hasNext();) { + Object[] stp = (Object[]) it.next(); + GlyphSubtable st; + if ( ( st = constructGPOSSubtable ( stp ) ) != null ) { + subtables.add ( st ); + } + } + } + return subtables; + } + + private GlyphSubtable constructGPOSSubtable ( Object[] stp ) { + GlyphSubtable st = null; + assert ( stp != null ) && ( stp.length == 8 ); + Integer tt = (Integer) stp[0]; // table type + Integer lt = (Integer) stp[1]; // lookup type + Integer ln = (Integer) stp[2]; // lookup sequence number + Integer lf = (Integer) stp[3]; // lookup flags + Integer sn = (Integer) stp[4]; // subtable sequence number + Integer sf = (Integer) stp[5]; // subtable format + GlyphCoverageTable coverage = (GlyphCoverageTable) stp[6]; + List entries = (List) stp[7]; + if ( tt.intValue() == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING ) { + int type = GSUBLookupType.getSubtableType ( lt.intValue() ); + String lid = "lu" + ln.intValue(); + int sequence = sn.intValue(); + int flags = lf.intValue(); + int format = sf.intValue(); + st = GlyphPositioningTable.createSubtable ( type, lid, sequence, flags, format, coverage, entries ); + } + return st; + } + + private void initATState() { + seScripts = new java.util.LinkedHashMap(); + seLanguages = new java.util.LinkedHashMap(); + seFeatures = new java.util.LinkedHashMap(); + seSubtables = new java.util.ArrayList(); + resetATSubState(); + } + + private void resetATState() { + seScripts = null; + seLanguages = null; + seFeatures = null; + seSubtables = null; + resetATSubState(); + } + + private void initATSubState() { + seMapping = null; + seEntries = new java.util.ArrayList(); + } + + private void extractSESubState ( int tableType, int lookupType, int lookupFlags, int lookupSequence, int subtableSequence, int subtableFormat ) { + if ( seEntries != null ) { + if ( ( tableType == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION ) || ( seEntries.size() > 0 ) ) { + if ( seSubtables != null ) { + Integer tt = Integer.valueOf ( tableType ); + Integer lt = Integer.valueOf ( lookupType ); + Integer ln = Integer.valueOf ( lookupSequence ); + Integer lf = Integer.valueOf ( lookupFlags ); + Integer sn = Integer.valueOf ( subtableSequence ); + Integer sf = Integer.valueOf ( subtableFormat ); + seSubtables.add ( new Object[] { tt, lt, ln, lf, sn, sf, seMapping, seEntries } ); + } + } + } + } + + private void resetATSubState() { + seMapping = null; + seEntries = null; + } + + private void resetATStateAll() { + resetATState(); + gdef = null; gsub = null; gpos = null; + } + + /** helper method for formatting an integer array for output */ + private String toString ( int[] ia ) { + StringBuffer sb = new StringBuffer(); + if ( ( ia == null ) || ( ia.length == 0 ) ) { + sb.append ( '-' ); + } else { + boolean first = true; + for ( int i = 0; i < ia.length; i++ ) { + if ( ! first ) { + sb.append ( ' ' ); + } else { + first = false; + } + sb.append ( ia[i] ); + } + } + return sb.toString(); + } + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/Positionable.java b/src/java/org/apache/fop/complexscripts/fonts/Positionable.java new file mode 100644 index 000000000..ce2da38c6 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/Positionable.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +// CSOFF: LineLengthCheck + +/** + * Optional interface which indicates that glyph positioning is supported and, if supported, + * can perform positioning. + * @author Glenn Adams + */ +public interface Positionable { + + /** + * Determines if font performs glyph positioning. + * @return true if performs positioning + */ + boolean performsPositioning(); + + /** + * Perform glyph positioning. + * @param cs character sequence to map to position offsets (advancement adjustments) + * @param script a script identifier + * @param language a language identifier + * @param fontSize font size + * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies + */ + int[][] performPositioning ( CharSequence cs, String script, String language, int fontSize ); + + /** + * Perform glyph positioning using an implied font size. + * @param cs character sequence to map to position offsets (advancement adjustments) + * @param script a script identifier + * @param language a language identifier + * @return array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence, or null if no non-zero adjustment applies + */ + int[][] performPositioning ( CharSequence cs, String script, String language ); + +} diff --git a/src/java/org/apache/fop/complexscripts/fonts/Substitutable.java b/src/java/org/apache/fop/complexscripts/fonts/Substitutable.java new file mode 100644 index 000000000..1ff970a5e --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/fonts/Substitutable.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.fonts; + +// CSOFF: LineLengthCheck + +/** + * Optional interface which indicates that glyph substitution is supported and, if supported, + * can perform substitution. + * @author Glenn Adams + */ +public interface Substitutable { + + /** + * Determines if font performs glyph substitution. + * @return true if performs substitution. + */ + boolean performsSubstitution(); + + /** + * Perform substitutions on characters to effect glyph substitution. If some substitution is performed, it + * entails mapping from one or more input characters denoting textual character information to one or more + * output character codes denoting glyphs in this font, where the output character codes may make use of + * private character code values that have significance only for this font. + * @param cs character sequence to map to output font encoding character sequence + * @param script a script identifier + * @param language a language identifier + * @return output sequence (represented as a character sequence, where each character in the returned sequence + * denotes "font characters", i.e., character codes that map directly (1-1) to their associated glyphs + */ + CharSequence performSubstitution ( CharSequence cs, String script, String language ); + + /** + * Reorder combining marks in character sequence so that they precede (within the sequence) the base + * character to which they are applied. N.B. In the case of LTR segments, marks are not reordered by this, + * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede + * their base character. + * @param cs character sequence within which combining marks to be reordered + * @param gpa associated glyph position adjustments (also reordered) + * @param script a script identifier + * @param language a language identifier + * @return output sequence containing reordered "font characters" + */ + CharSequence reorderCombiningMarks ( CharSequence cs, int[][] gpa, String script, String language ); + +} diff --git a/src/java/org/apache/fop/complexscripts/scripts/ArabicScriptProcessor.java b/src/java/org/apache/fop/complexscripts/scripts/ArabicScriptProcessor.java new file mode 100644 index 000000000..503ee2705 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/scripts/ArabicScriptProcessor.java @@ -0,0 +1,522 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.scripts; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.bidi.BidiClass; +import org.apache.fop.complexscripts.bidi.BidiConstants; +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; +import org.apache.fop.complexscripts.util.GlyphContextTester; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: AvoidNestedBlocksCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: SimplifyBooleanReturnCheck +// CSOFF: LineLengthCheck + +/** + * <p>The <code>ArabicScriptProcessor</code> class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Arabic script.</p> + * @author Glenn Adams + */ +public class ArabicScriptProcessor extends DefaultScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(ArabicScriptProcessor.class); // CSOK: ConstantNameCheck + + /** features to use for substitutions */ + private static final String[] gsubFeatures = // CSOK: ConstantNameCheck + { + "calt", // contextual alternates + "ccmp", // glyph composition/decomposition + "fina", // final (terminal) forms + "init", // initial forms + "isol", // isolated formas + "liga", // standard ligatures + "medi", // medial forms + "rlig" // required ligatures + }; + + /** features to use for positioning */ + private static final String[] gposFeatures = // CSOK: ConstantNameCheck + { + "curs", // cursive positioning + "kern", // kerning + "mark", // mark to base or ligature positioning + "mkmk" // mark to mark positioning + }; + + private static class SubstitutionScriptContextTester implements ScriptContextTester { + private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/(); + static { + testerMap.put ( "fina", new GlyphContextTester() { + public boolean test ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + return inFinalContext ( script, language, feature, gs, index, flags ); + } + } ); + testerMap.put ( "init", new GlyphContextTester() { + public boolean test ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + return inInitialContext ( script, language, feature, gs, index, flags ); + } + } ); + testerMap.put ( "isol", new GlyphContextTester() { + public boolean test ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + return inIsolateContext ( script, language, feature, gs, index, flags ); + } + } ); + testerMap.put ( "liga", new GlyphContextTester() { + public boolean test ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + return inLigatureContext ( script, language, feature, gs, index, flags ); + } + } ); + testerMap.put ( "medi", new GlyphContextTester() { + public boolean test ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + return inMedialContext ( script, language, feature, gs, index, flags ); + } + } ); + } + public GlyphContextTester getTester ( String feature ) { + return (GlyphContextTester) testerMap.get ( feature ); + } + } + + private static class PositioningScriptContextTester implements ScriptContextTester { + private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/(); + public GlyphContextTester getTester ( String feature ) { + return (GlyphContextTester) testerMap.get ( feature ); + } + } + + private final ScriptContextTester subContextTester; + private final ScriptContextTester posContextTester; + + ArabicScriptProcessor ( String script ) { + super ( script ); + this.subContextTester = new SubstitutionScriptContextTester(); + this.posContextTester = new PositioningScriptContextTester(); + } + + /** {@inheritDoc} */ + public String[] getSubstitutionFeatures() { + return gsubFeatures; + } + + /** {@inheritDoc} */ + public ScriptContextTester getSubstitutionContextTester() { + return subContextTester; + } + + /** {@inheritDoc} */ + public String[] getPositioningFeatures() { + return gposFeatures; + } + + /** {@inheritDoc} */ + public ScriptContextTester getPositioningContextTester() { + return posContextTester; + } + + /** {@inheritDoc} */ + @Override + public GlyphSequence reorderCombiningMarks ( GlyphDefinitionTable gdef, GlyphSequence gs, int[][] gpa, String script, String language ) { + // a side effect of BIDI reordering is to order combining marks before their base, so we need to override the default here to + // prevent double reordering + return gs; + } + + private static boolean inFinalContext ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( index ); + int[] ca = gs.getCharacterArray ( false ); + int nc = gs.getCharacterCount(); + if ( nc == 0 ) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if ( ! hasFinalPrecedingContext ( ca, nc, s, e ) ) { + return false; + } else if ( forcesFinalThisContext ( ca, nc, s, e ) ) { + return true; + } else if ( ! hasFinalFollowingContext ( ca, nc, s, e ) ) { + return false; + } else { + return true; + } + } + } + + private static boolean inInitialContext ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( index ); + int[] ca = gs.getCharacterArray ( false ); + int nc = gs.getCharacterCount(); + if ( nc == 0 ) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if ( ! hasInitialPrecedingContext ( ca, nc, s, e ) ) { + return false; + } else if ( ! hasInitialFollowingContext ( ca, nc, s, e ) ) { + return false; + } else { + return true; + } + } + } + + private static boolean inIsolateContext ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( index ); + int nc = gs.getCharacterCount(); + if ( nc == 0 ) { + return false; + } else if ( ( a.getStart() == 0 ) && ( a.getEnd() == nc ) ) { + return true; + } else { + return false; + } + } + + private static boolean inLigatureContext ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( index ); + int[] ca = gs.getCharacterArray ( false ); + int nc = gs.getCharacterCount(); + if ( nc == 0 ) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if ( ! hasLigaturePrecedingContext ( ca, nc, s, e ) ) { + return false; + } else if ( ! hasLigatureFollowingContext ( ca, nc, s, e ) ) { + return false; + } else { + return true; + } + } + } + + private static boolean inMedialContext ( String script, String language, String feature, GlyphSequence gs, int index, int flags ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( index ); + int[] ca = gs.getCharacterArray ( false ); + int nc = gs.getCharacterCount(); + if ( nc == 0 ) { + return false; + } else { + int s = a.getStart(); + int e = a.getEnd(); + if ( ! hasMedialPrecedingContext ( ca, nc, s, e ) ) { + return false; + } else if ( ! hasMedialThisContext ( ca, nc, s, e ) ) { + return false; + } else if ( ! hasMedialFollowingContext ( ca, nc, s, e ) ) { + return false; + } else { + return true; + } + } + } + + private static boolean hasFinalPrecedingContext ( int[] ca, int nc, int s, int e ) { + int chp = 0; + int clp = 0; + for ( int i = s; i > 0; i-- ) { + int k = i - 1; + if ( ( k >= 0 ) && ( k < nc ) ) { + chp = ca [ k ]; + clp = BidiClass.getBidiClass ( chp ); + if ( clp != BidiConstants.NSM ) { + break; + } + } + } + if ( clp != BidiConstants.AL ) { + return false; + } else if ( hasIsolateInitial ( chp ) ) { + return false; + } else { + return true; + } + } + + private static boolean forcesFinalThisContext ( int[] ca, int nc, int s, int e ) { + int chl = 0; + int cll = 0; + for ( int i = 0, n = e - s; i < n; i++ ) { + int k = n - i - 1; + int j = s + k; + if ( ( j >= 0 ) && ( j < nc ) ) { + chl = ca [ j ]; + cll = BidiClass.getBidiClass ( chl ); + if ( cll != BidiConstants.NSM ) { + break; + } + } + } + if ( cll != BidiConstants.AL ) { + return false; + } + if ( hasIsolateInitial ( chl ) ) { + return true; + } else { + return false; + } + } + + private static boolean hasFinalFollowingContext ( int[] ca, int nc, int s, int e ) { + int chf = 0; + int clf = 0; + for ( int i = e, n = nc; i < n; i++ ) { + chf = ca [ i ]; + clf = BidiClass.getBidiClass ( chf ); + if ( clf != BidiConstants.NSM ) { + break; + } + } + if ( clf != BidiConstants.AL ) { + return true; + } else if ( hasIsolateFinal ( chf ) ) { + return true; + } else { + return false; + } + } + + private static boolean hasInitialPrecedingContext ( int[] ca, int nc, int s, int e ) { + int chp = 0; + int clp = 0; + for ( int i = s; i > 0; i-- ) { + int k = i - 1; + if ( ( k >= 0 ) && ( k < nc ) ) { + chp = ca [ k ]; + clp = BidiClass.getBidiClass ( chp ); + if ( clp != BidiConstants.NSM ) { + break; + } + } + } + if ( clp != BidiConstants.AL ) { + return true; + } else if ( hasIsolateInitial ( chp ) ) { + return true; + } else { + return false; + } + } + + private static boolean hasInitialFollowingContext ( int[] ca, int nc, int s, int e ) { + int chf = 0; + int clf = 0; + for ( int i = e, n = nc; i < n; i++ ) { + chf = ca [ i ]; + clf = BidiClass.getBidiClass ( chf ); + if ( clf != BidiConstants.NSM ) { + break; + } + } + if ( clf != BidiConstants.AL ) { + return false; + } else if ( hasIsolateFinal ( chf ) ) { + return false; + } else { + return true; + } + } + + private static boolean hasMedialPrecedingContext ( int[] ca, int nc, int s, int e ) { + int chp = 0; + int clp = 0; + for ( int i = s; i > 0; i-- ) { + int k = i - 1; + if ( ( k >= 0 ) && ( k < nc ) ) { + chp = ca [ k ]; + clp = BidiClass.getBidiClass ( chp ); + if ( clp != BidiConstants.NSM ) { + break; + } + } + } + if ( clp != BidiConstants.AL ) { + return false; + } else if ( hasIsolateInitial ( chp ) ) { + return false; + } else { + return true; + } + } + + private static boolean hasMedialThisContext ( int[] ca, int nc, int s, int e ) { + int chf = 0; // first non-NSM char in [s,e) + int clf = 0; + for ( int i = 0, n = e - s; i < n; i++ ) { + int k = s + i; + if ( ( k >= 0 ) && ( k < nc ) ) { + chf = ca [ s + i ]; + clf = BidiClass.getBidiClass ( chf ); + if ( clf != BidiConstants.NSM ) { + break; + } + } + } + if ( clf != BidiConstants.AL ) { + return false; + } + int chl = 0; // last non-NSM char in [s,e) + int cll = 0; + for ( int i = 0, n = e - s; i < n; i++ ) { + int k = n - i - 1; + int j = s + k; + if ( ( j >= 0 ) && ( j < nc ) ) { + chl = ca [ j ]; + cll = BidiClass.getBidiClass ( chl ); + if ( cll != BidiConstants.NSM ) { + break; + } + } + } + if ( cll != BidiConstants.AL ) { + return false; + } + if ( hasIsolateFinal ( chf ) ) { + return false; + } else if ( hasIsolateInitial ( chl ) ) { + return false; + } else { + return true; + } + } + + private static boolean hasMedialFollowingContext ( int[] ca, int nc, int s, int e ) { + int chf = 0; + int clf = 0; + for ( int i = e, n = nc; i < n; i++ ) { + chf = ca [ i ]; + clf = BidiClass.getBidiClass ( chf ); + if ( clf != BidiConstants.NSM ) { + break; + } + } + if ( clf != BidiConstants.AL ) { + return false; + } else if ( hasIsolateFinal ( chf ) ) { + return false; + } else { + return true; + } + } + + private static boolean hasLigaturePrecedingContext ( int[] ca, int nc, int s, int e ) { + return true; + } + + private static boolean hasLigatureFollowingContext ( int[] ca, int nc, int s, int e ) { + int chf = 0; + int clf = 0; + for ( int i = e, n = nc; i < n; i++ ) { + chf = ca [ i ]; + clf = BidiClass.getBidiClass ( chf ); + if ( clf != BidiConstants.NSM ) { + break; + } + } + if ( clf == BidiConstants.AL ) { + return true; + } else { + return false; + } + } + + /** + * Ordered array of Unicode scalars designating those Arabic (Script) Letters + * which exhibit an isolated form in word initial position. + */ + private static int[] isolatedInitials = { + 0x0621, // HAMZA + 0x0622, // ALEF WITH MADDA ABOVE + 0x0623, // ALEF WITH HAMZA ABOVE + 0x0624, // WAW WITH HAMZA ABOVE + 0x0625, // ALEF WITH HAMZA BELOWW + 0x0627, // ALEF + 0x062F, // DAL + 0x0630, // THAL + 0x0631, // REH + 0x0632, // ZAIN + 0x0648, // WAW + 0x0671, // ALEF WASLA + 0x0672, // ALEF WITH WAVY HAMZA ABOVE + 0x0673, // ALEF WITH WAVY HAMZA BELOW + 0x0675, // HIGH HAMZA ALEF + 0x0676, // HIGH HAMZA WAW + 0x0677, // U WITH HAMZA ABOVE + 0x0688, // DDAL + 0x0689, // DAL WITH RING + 0x068A, // DAL WITH DOT BELOW + 0x068B, // DAL WITH DOT BELOW AND SMALL TAH + 0x068C, // DAHAL + 0x068D, // DDAHAL + 0x068E, // DUL + 0x068F, // DUL WITH THREE DOTS ABOVE DOWNWARDS + 0x0690, // DUL WITH FOUR DOTS ABOVE + 0x0691, // RREH + 0x0692, // REH WITH SMALL V + 0x0693, // REH WITH RING + 0x0694, // REH WITH DOT BELOW + 0x0695, // REH WITH SMALL V BELOW + 0x0696, // REH WITH DOT BELOW AND DOT ABOVE + 0x0697, // REH WITH TWO DOTS ABOVE + 0x0698, // JEH + 0x0699, // REH WITH FOUR DOTS ABOVE + 0x06C4, // WAW WITH RING + 0x06C5, // KIRGHIZ OE + 0x06C6, // OE + 0x06C7, // U + 0x06C8, // YU + 0x06C9, // KIRGHIZ YU + 0x06CA, // WAW WITH TWO DOTS ABOVE + 0x06CB, // VE + 0x06CF, // WAW WITH DOT ABOVE + 0x06EE, // DAL WITH INVERTED V + 0x06EF // REH WITH INVERTED V + }; + + private static boolean hasIsolateInitial ( int ch ) { + return Arrays.binarySearch ( isolatedInitials, ch ) >= 0; + } + + /** + * Ordered array of Unicode scalars designating those Arabic (Script) Letters + * which exhibit an isolated form in word final position. + */ + private static int[] isolatedFinals = { + 0x0621 // HAMZA + }; + + private static boolean hasIsolateFinal ( int ch ) { + return Arrays.binarySearch ( isolatedFinals, ch ) >= 0; + } + +} diff --git a/src/java/org/apache/fop/complexscripts/scripts/DefaultScriptProcessor.java b/src/java/org/apache/fop/complexscripts/scripts/DefaultScriptProcessor.java new file mode 100644 index 000000000..ff70e6a67 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/scripts/DefaultScriptProcessor.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.scripts; + +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: LineLengthCheck + +/** + * Default script processor, which enables default glyph composition/decomposition, common ligatures, localized forms + * and kerning. + * + * @author Glenn Adams + */ +public class DefaultScriptProcessor extends ScriptProcessor { + + /** features to use for substitutions */ + private static final String[] gsubFeatures = // CSOK: ConstantNameCheck + { + "ccmp", // glyph composition/decomposition + "liga", // common ligatures + "locl" // localized forms + }; + + /** features to use for positioning */ + private static final String[] gposFeatures = // CSOK: ConstantNameCheck + { + "kern", // kerning + "mark", // mark to base or ligature positioning + "mkmk" // mark to mark positioning + }; + + DefaultScriptProcessor ( String script ) { + super ( script ); + } + + @Override + /** {@inheritDoc} */ + public String[] getSubstitutionFeatures() { + return gsubFeatures; + } + + @Override + /** {@inheritDoc} */ + public ScriptContextTester getSubstitutionContextTester() { + return null; + } + + @Override + /** {@inheritDoc} */ + public String[] getPositioningFeatures() { + return gposFeatures; + } + + @Override + /** {@inheritDoc} */ + public ScriptContextTester getPositioningContextTester() { + return null; + } + + @Override + /** {@inheritDoc} */ + public GlyphSequence reorderCombiningMarks ( GlyphDefinitionTable gdef, GlyphSequence gs, int[][] gpa, String script, String language ) { + int ng = gs.getGlyphCount(); + int[] ga = gs.getGlyphArray ( false ); + int nm = 0; + // count combining marks + for ( int i = 0; i < ng; i++ ) { + int gid = ga [ i ]; + if ( gdef.isGlyphClass ( gid, GlyphDefinitionTable.GLYPH_CLASS_MARK ) ) { + nm++; + } + } + // only reorder if there is at least one mark and at least one non-mark glyph + if ( ( nm > 0 ) && ( ( ng - nm ) > 0 ) ) { + GlyphSequence.CharAssociation[] aa = gs.getAssociations ( 0, -1 ); + int[] nga = new int [ ng ]; + int[][] npa = ( gpa != null ) ? new int [ ng ][] : null; + GlyphSequence.CharAssociation[] naa = new GlyphSequence.CharAssociation [ ng ]; + int k = 0; + GlyphSequence.CharAssociation ba = null; + int bg = -1; + int[] bpa = null; + for ( int i = 0; i < ng; i++ ) { + int gid = ga [ i ]; + int[] pa = ( gpa != null ) ? gpa [ i ] : null; + GlyphSequence.CharAssociation ca = aa [ i ]; + if ( gdef.isGlyphClass ( gid, GlyphDefinitionTable.GLYPH_CLASS_MARK ) ) { + nga [ k ] = gid; naa [ k ] = ca; + if ( npa != null ) { + npa [ k ] = pa; + } + k++; + } else { + if ( bg != -1 ) { + nga [ k ] = bg; naa [ k ] = ba; + if ( npa != null ) { + npa [ k ] = bpa; + } + k++; + bg = -1; ba = null; bpa = null; + } + if ( bg == -1 ) { + bg = gid; ba = ca; bpa = pa; + } + } + } + if ( bg != -1 ) { + nga [ k ] = bg; naa [ k ] = ba; + if ( npa != null ) { + npa [ k ] = bpa; + } + k++; + } + assert k == ng; + if ( npa != null ) { + System.arraycopy ( npa, 0, gpa, 0, ng ); + } + return new GlyphSequence ( gs, null, nga, null, null, naa, null ); + } else { + return gs; + } + } + +} diff --git a/src/java/org/apache/fop/complexscripts/scripts/DevanagariScriptProcessor.java b/src/java/org/apache/fop/complexscripts/scripts/DevanagariScriptProcessor.java new file mode 100644 index 000000000..52f1d7284 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/scripts/DevanagariScriptProcessor.java @@ -0,0 +1,537 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.scripts; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: AvoidNestedBlocksCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: WhitespaceAfter +// CSOFF: InnerAssignmentCheck +// CSOFF: SimplifyBooleanReturnCheck +// CSOFF: LineLengthCheck + +/** + * <p>The <code>DevanagariScriptProcessor</code> class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Devanagari script.</p> + * @author Glenn Adams + */ +public class DevanagariScriptProcessor extends IndicScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(DevanagariScriptProcessor.class); // CSOK: ConstantNameCheck + + DevanagariScriptProcessor ( String script ) { + super ( script ); + } + + @Override + protected Class<? extends DevanagariSyllabizer> getSyllabizerClass() { + return DevanagariSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra ( GlyphSequence gs ) { + int ng = gs.getGlyphCount(); + int lk = -1; + for ( int i = ng; i > 0; i-- ) { + int k = i - 1; + if ( containsPreBaseMatra ( gs, k ) ) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget ( GlyphSequence gs, int source ) { + int ng = gs.getGlyphCount(); + int lk = -1; + for ( int i = ( source < ng ) ? source : ng; i > 0; i-- ) { + int k = i - 1; + if ( containsConsonant ( gs, k ) ) { + if ( containsHalfConsonant ( gs, k ) ) { + lk = k; + } else if ( lk == -1 ) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + if ( isPreM ( ca [ i ] ) ) { + return true; + } + } + return false; + } + + private static boolean containsConsonant ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + if ( isC ( ca [ i ] ) ) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant ( GlyphSequence gs, int k ) { + Boolean half = (Boolean) gs.getAssociation ( k ) . getPredication ( "half" ); + return ( half != null ) ? half.booleanValue() : false; + } + + @Override + protected int findReph ( GlyphSequence gs ) { + int ng = gs.getGlyphCount(); + int li = -1; + for ( int i = 0; i < ng; i++ ) { + if ( containsReph ( gs, i ) ) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget ( GlyphSequence gs, int source ) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for ( int i = 0; i < ng; i++ ) { + if ( ( i != source ) && containsConsonant ( gs, i ) ) { + if ( ! containsHalfConsonant ( gs, i ) ) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for ( int i = ( c1 >= 0 ) ? c1 : 0; i < ng; i++ ) { + if ( containsMatra ( gs, i ) && ! containsPreBaseMatra ( gs, i ) ) { + c2 = i + 1; + } else if ( containsOtherMark ( gs, i ) ) { + c2 = i; + break; + } + } + if ( c2 >= 0 ) { + return c2; + } else if ( c1 >= 0 ) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph ( GlyphSequence gs, int k ) { + Boolean rphf = (Boolean) gs.getAssociation ( k ) . getPredication ( "rphf" ); + return ( rphf != null ) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + if ( isM ( ca [ i ] ) ) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + switch ( typeOf ( ca [ i ] ) ) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class DevanagariSyllabizer extends DefaultSyllabizer { + DevanagariSyllabizer ( String script, String language ) { + super ( script, language ); + } + @Override + // | C ... + protected int findStartOfSyllable ( int[] ca, int s, int e ) { + if ( ( s < 0 ) || ( s >= e ) ) { + return -1; + } else { + while ( s < e ) { + int c = ca [ s ]; + if ( isC ( c ) ) { + break; + } else { + s++; + } + } + return s; + } + } + @Override + // D* L? | ... + protected int findEndOfSyllable ( int[] ca, int s, int e ) { + if ( ( s < 0 ) || ( s >= e ) ) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ( ( i = isDeadConsonant ( ca, s, e ) ) > s ) { + s = i; nd++; + } + // consume zero or one live consonant + if ( ( i = isLiveConsonant ( ca, s, e ) ) > s ) { + s = i; nl++; + } + return ( ( nd > 0 ) || ( nl > 0 ) ) ? s : -1; + } + } + // D := ( C N? H )? + private int isDeadConsonant ( int[] ca, int s, int e ) { + if ( s < 0 ) { + return -1; + } else { + int c, i = 0; + int nc = 0, nh = 0; + do { + // C + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isC ( c ) ) { + i++; + nc++; + } else { + break; + } + } + // N? + if ( ( s + i ) < e ) { + c = ca [ s + 1 ]; + if ( isN ( c ) ) { + i++; + } + } + // H + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isH ( c ) ) { + i++; + nh++; + } else { + break; + } + } + } while ( false ); + return ( nc > 0 ) && ( nh > 0 ) ? s + i : -1; + } + } + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant ( int[] ca, int s, int e ) { + if ( s < 0 ) { + return -1; + } else { + int c, i = 0; + int nc = 0, nv = 0, nx = 0; + do { + // C + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isC ( c ) ) { + i++; + nc++; + } else if ( isV ( c ) ) { + i++; + nv++; + } else { + break; + } + } + // N? + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isN ( c ) ) { + i++; + } + } + // X* + while ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isX ( c ) ) { + i++; + nx++; + } else { + break; + } + } + } while ( false ); + // if no X but has H, then ignore C|I + if ( nx == 0 ) { + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isH ( c ) ) { + if ( nc > 0 ) { + nc--; + } else if ( nv > 0 ) { + nv--; + } + } + } + } + return ( ( nc > 0 ) || ( nv > 0 ) ) ? s + i : -1; + } + } + } + + // devanagari character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // devanagari block range + static final int ccaStart = 0x0900; // first code point mapped by cca // CSOK: ConstantNameCheck + static final int ccaEnd = 0x0980; // last code point + 1 mapped by cca // CSOK: ConstantNameCheck + // devanagari character type lookups + static final short[] cca = { // CSOK: ConstantNameCheck + C_O, // 0x0900 // INVERTED CANDRABINDU + C_O, // 0x0901 // CANDRABINDU + C_O, // 0x0902 // ANUSVARA + C_O, // 0x0903 // VISARGA + C_V, // 0x0904 // SHORT A + C_V, // 0x0905 // A + C_V, // 0x0906 // AA + C_V, // 0x0907 // I + C_V, // 0x0908 // II + C_V, // 0x0909 // U + C_V, // 0x090A // UU + C_V, // 0x090B // VOCALIC R + C_V, // 0x090C // VOCALIC L + C_V, // 0x090D // CANDRA E + C_V, // 0x090E // SHORT E + C_V, // 0x090F // E + C_V, // 0x0910 // AI + C_V, // 0x0911 // CANDRA O + C_V, // 0x0912 // SHORT O + C_V, // 0x0913 // O + C_V, // 0x0914 // AU + C_C, // 0x0915 // KA + C_C, // 0x0916 // KHA + C_C, // 0x0917 // GA + C_C, // 0x0918 // GHA + C_C, // 0x0919 // NGA + C_C, // 0x091A // CA + C_C, // 0x091B // CHA + C_C, // 0x091C // JA + C_C, // 0x091D // JHA + C_C, // 0x091E // NYA + C_C, // 0x091F // TTA + C_C, // 0x0920 // TTHA + C_C, // 0x0921 // DDA + C_C, // 0x0922 // DDHA + C_C, // 0x0923 // NNA + C_C, // 0x0924 // TA + C_C, // 0x0925 // THA + C_C, // 0x0926 // DA + C_C, // 0x0927 // DHA + C_C, // 0x0928 // NA + C_C, // 0x0929 // NNNA + C_C, // 0x092A // PA + C_C, // 0x092B // PHA + C_C, // 0x092C // BA + C_C, // 0x092D // BHA + C_C, // 0x092E // MA + C_C, // 0x092F // YA + C_C|C_R, // 0x0930 // RA // CSOK: WhitespaceAround + C_C|C_R|C_N, // 0x0931 // RRA = 0930+093C // CSOK: WhitespaceAround + C_C, // 0x0932 // LA + C_C, // 0x0933 // LLA + C_C, // 0x0934 // LLLA + C_C, // 0x0935 // VA + C_C, // 0x0936 // SHA + C_C, // 0x0937 // SSA + C_C, // 0x0938 // SA + C_C, // 0x0939 // HA + C_M, // 0x093A // OE (KASHMIRI) + C_M, // 0x093B // OOE (KASHMIRI) + C_N, // 0x093C // NUKTA + C_S, // 0x093D // AVAGRAHA + C_M, // 0x093E // AA + C_M|C_PRE, // 0x093F // I // CSOK: WhitespaceAround + C_M, // 0x0940 // II + C_M, // 0x0941 // U + C_M, // 0x0942 // UU + C_M, // 0x0943 // VOCALIC R + C_M, // 0x0944 // VOCALIC RR + C_M, // 0x0945 // CANDRA E + C_M, // 0x0946 // SHORT E + C_M, // 0x0947 // E + C_M, // 0x0948 // AI + C_M, // 0x0949 // CANDRA O + C_M, // 0x094A // SHORT O + C_M, // 0x094B // O + C_M, // 0x094C // AU + C_H, // 0x094D // VIRAMA (HALANT) + C_M, // 0x094E // PRISHTHAMATRA E + C_M, // 0x094F // AW + C_S, // 0x0950 // OM + C_T, // 0x0951 // UDATTA + C_T, // 0x0952 // ANUDATTA + C_A, // 0x0953 // GRAVE + C_A, // 0x0954 // ACUTE + C_M, // 0x0955 // CANDRA LONG E + C_M, // 0x0956 // UE + C_M, // 0x0957 // UUE + C_C|C_N, // 0x0958 // QA // CSOK: WhitespaceAround + C_C|C_N, // 0x0959 // KHHA // CSOK: WhitespaceAround + C_C|C_N, // 0x095A // GHHA // CSOK: WhitespaceAround + C_C|C_N, // 0x095B // ZA // CSOK: WhitespaceAround + C_C|C_N, // 0x095C // DDDHA // CSOK: WhitespaceAround + C_C|C_N, // 0x095D // RHA // CSOK: WhitespaceAround + C_C|C_N, // 0x095E // FA // CSOK: WhitespaceAround + C_C|C_N, // 0x095F // YYA // CSOK: WhitespaceAround + C_V, // 0x0960 // VOCALIC RR + C_V, // 0x0961 // VOCALIC LL + C_M, // 0x0962 // VOCALIC RR + C_M, // 0x0963 // VOCALIC LL + C_P, // 0x0964 // DANDA + C_P, // 0x0965 // DOUBLE DANDA + C_D, // 0x0966 // ZERO + C_D, // 0x0967 // ONE + C_D, // 0x0968 // TWO + C_D, // 0x0969 // THREE + C_D, // 0x096A // FOUR + C_D, // 0x096B // FIVE + C_D, // 0x096C // SIX + C_D, // 0x096D // SEVEN + C_D, // 0x096E // EIGHT + C_D, // 0x096F // NINE + C_S, // 0x0970 // ABBREVIATION SIGN + C_S, // 0x0971 // HIGH SPACING DOT + C_V, // 0x0972 // CANDRA A (MARATHI) + C_V, // 0x0973 // OE (KASHMIRI) + C_V, // 0x0974 // OOE (KASHMIRI) + C_V, // 0x0975 // AW (KASHMIRI) + C_V, // 0x0976 // UE (KASHMIRI) + C_V, // 0x0977 // UUE (KASHMIRI) + C_U, // 0x0978 // UNASSIGNED + C_C, // 0x0979 // ZHA + C_C, // 0x097A // HEAVY YA + C_C, // 0x097B // GGAA (SINDHI) + C_C, // 0x097C // JJA (SINDHI) + C_C, // 0x097D // GLOTTAL STOP (LIMBU) + C_C, // 0x097E // DDDA (SINDHI) + C_C // 0x097F // BBA (SINDHI) + }; + static int typeOf(int c) { + if ( ( c >= ccaStart ) && ( c < ccaEnd ) ) { + return cca [ c - ccaStart ] & C_M_TYPE; + } else { + return C_U; + } + } + static boolean isType(int c, int t) { + return typeOf ( c ) == t; + } + static boolean hasFlag(int c, int f) { + if ( ( c >= ccaStart ) && ( c < ccaEnd ) ) { + return ( cca [ c - ccaStart ] & f ) == f; + } else { + return false; + } + } + static boolean isC(int c) { + return isType(c,C_C); + } + static boolean isR(int c) { + return isType(c,C_C) && hasR(c); + } + static boolean isV(int c) { + return isType(c,C_V); + } + static boolean isN(int c) { + return c == 0x093C; + } + static boolean isH(int c) { + return c == 0x094D; + } + static boolean isM(int c) { + return isType(c,C_M); + } + static boolean isPreM(int c) { + return isType(c,C_M) && hasFlag(c,C_PRE); + } + static boolean isX(int c) { + switch ( typeOf ( c ) ) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + static boolean hasR(int c) { + return hasFlag(c,C_R); + } + static boolean hasN(int c) { + return hasFlag(c,C_N); + } + +} diff --git a/src/java/org/apache/fop/complexscripts/scripts/GujaratiScriptProcessor.java b/src/java/org/apache/fop/complexscripts/scripts/GujaratiScriptProcessor.java new file mode 100644 index 000000000..4aec56a65 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/scripts/GujaratiScriptProcessor.java @@ -0,0 +1,537 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.scripts; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: AvoidNestedBlocksCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: WhitespaceAfter +// CSOFF: InnerAssignmentCheck +// CSOFF: SimplifyBooleanReturnCheck +// CSOFF: LineLengthCheck + +/** + * <p>The <code>GujaratiScriptProcessor</code> class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Gujarati script.</p> + * @author Glenn Adams + */ +public class GujaratiScriptProcessor extends IndicScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GujaratiScriptProcessor.class); // CSOK: ConstantNameCheck + + GujaratiScriptProcessor ( String script ) { + super ( script ); + } + + @Override + protected Class<? extends GujaratiSyllabizer> getSyllabizerClass() { + return GujaratiSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra ( GlyphSequence gs ) { + int ng = gs.getGlyphCount(); + int lk = -1; + for ( int i = ng; i > 0; i-- ) { + int k = i - 1; + if ( containsPreBaseMatra ( gs, k ) ) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget ( GlyphSequence gs, int source ) { + int ng = gs.getGlyphCount(); + int lk = -1; + for ( int i = ( source < ng ) ? source : ng; i > 0; i-- ) { + int k = i - 1; + if ( containsConsonant ( gs, k ) ) { + if ( containsHalfConsonant ( gs, k ) ) { + lk = k; + } else if ( lk == -1 ) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + if ( isPreM ( ca [ i ] ) ) { + return true; + } + } + return false; + } + + private static boolean containsConsonant ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + if ( isC ( ca [ i ] ) ) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant ( GlyphSequence gs, int k ) { + Boolean half = (Boolean) gs.getAssociation ( k ) . getPredication ( "half" ); + return ( half != null ) ? half.booleanValue() : false; + } + + @Override + protected int findReph ( GlyphSequence gs ) { + int ng = gs.getGlyphCount(); + int li = -1; + for ( int i = 0; i < ng; i++ ) { + if ( containsReph ( gs, i ) ) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget ( GlyphSequence gs, int source ) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for ( int i = 0; i < ng; i++ ) { + if ( ( i != source ) && containsConsonant ( gs, i ) ) { + if ( ! containsHalfConsonant ( gs, i ) ) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for ( int i = ( c1 >= 0 ) ? c1 : 0; i < ng; i++ ) { + if ( containsMatra ( gs, i ) && ! containsPreBaseMatra ( gs, i ) ) { + c2 = i + 1; + } else if ( containsOtherMark ( gs, i ) ) { + c2 = i; + break; + } + } + if ( c2 >= 0 ) { + return c2; + } else if ( c1 >= 0 ) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph ( GlyphSequence gs, int k ) { + Boolean rphf = (Boolean) gs.getAssociation ( k ) . getPredication ( "rphf" ); + return ( rphf != null ) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + if ( isM ( ca [ i ] ) ) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + switch ( typeOf ( ca [ i ] ) ) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class GujaratiSyllabizer extends DefaultSyllabizer { + GujaratiSyllabizer ( String script, String language ) { + super ( script, language ); + } + @Override + // | C ... + protected int findStartOfSyllable ( int[] ca, int s, int e ) { + if ( ( s < 0 ) || ( s >= e ) ) { + return -1; + } else { + while ( s < e ) { + int c = ca [ s ]; + if ( isC ( c ) ) { + break; + } else { + s++; + } + } + return s; + } + } + @Override + // D* L? | ... + protected int findEndOfSyllable ( int[] ca, int s, int e ) { + if ( ( s < 0 ) || ( s >= e ) ) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ( ( i = isDeadConsonant ( ca, s, e ) ) > s ) { + s = i; nd++; + } + // consume zero or one live consonant + if ( ( i = isLiveConsonant ( ca, s, e ) ) > s ) { + s = i; nl++; + } + return ( ( nd > 0 ) || ( nl > 0 ) ) ? s : -1; + } + } + // D := ( C N? H )? + private int isDeadConsonant ( int[] ca, int s, int e ) { + if ( s < 0 ) { + return -1; + } else { + int c, i = 0; + int nc = 0, nh = 0; + do { + // C + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isC ( c ) ) { + i++; + nc++; + } else { + break; + } + } + // N? + if ( ( s + i ) < e ) { + c = ca [ s + 1 ]; + if ( isN ( c ) ) { + i++; + } + } + // H + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isH ( c ) ) { + i++; + nh++; + } else { + break; + } + } + } while ( false ); + return ( nc > 0 ) && ( nh > 0 ) ? s + i : -1; + } + } + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant ( int[] ca, int s, int e ) { + if ( s < 0 ) { + return -1; + } else { + int c, i = 0; + int nc = 0, nv = 0, nx = 0; + do { + // C + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isC ( c ) ) { + i++; + nc++; + } else if ( isV ( c ) ) { + i++; + nv++; + } else { + break; + } + } + // N? + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isN ( c ) ) { + i++; + } + } + // X* + while ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isX ( c ) ) { + i++; + nx++; + } else { + break; + } + } + } while ( false ); + // if no X but has H, then ignore C|I + if ( nx == 0 ) { + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isH ( c ) ) { + if ( nc > 0 ) { + nc--; + } else if ( nv > 0 ) { + nv--; + } + } + } + } + return ( ( nc > 0 ) || ( nv > 0 ) ) ? s + i : -1; + } + } + } + + // gujarati character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // gujarati block range + static final int ccaStart = 0x0A80; // first code point mapped by cca // CSOK: ConstantNameCheck + static final int ccaEnd = 0x0B00; // last code point + 1 mapped by cca // CSOK: ConstantNameCheck + // gujarati character type lookups + static final short[] cca = { // CSOK: ConstantNameCheck + C_U, // 0x0A80 // UNASSIGNED + C_O, // 0x0A81 // CANDRABINDU + C_O, // 0x0A82 // ANUSVARA + C_O, // 0x0A83 // VISARGA + C_U, // 0x0A84 // UNASSIGNED + C_V, // 0x0A85 // A + C_V, // 0x0A86 // AA + C_V, // 0x0A87 // I + C_V, // 0x0A88 // II + C_V, // 0x0A89 // U + C_V, // 0x0A8A // UU + C_V, // 0x0A8B // VOCALIC R + C_V, // 0x0A8C // VOCALIC L + C_V, // 0x0A8D // CANDRA E + C_U, // 0x0A8E // UNASSIGNED + C_V, // 0x0A8F // E + C_V, // 0x0A90 // AI + C_V, // 0x0A91 // CANDRA O + C_U, // 0x0A92 // UNASSIGNED + C_V, // 0x0A93 // O + C_V, // 0x0A94 // AU + C_C, // 0x0A95 // KA + C_C, // 0x0A96 // KHA + C_C, // 0x0A97 // GA + C_C, // 0x0A98 // GHA + C_C, // 0x0A99 // NGA + C_C, // 0x0A9A // CA + C_C, // 0x0A9B // CHA + C_C, // 0x0A9C // JA + C_C, // 0x0A9D // JHA + C_C, // 0x0A9E // NYA + C_C, // 0x0A9F // TTA + C_C, // 0x0AA0 // TTHA + C_C, // 0x0AA1 // DDA + C_C, // 0x0AA2 // DDHA + C_C, // 0x0AA3 // NNA + C_C, // 0x0AA4 // TA + C_C, // 0x0AA5 // THA + C_C, // 0x0AA6 // DA + C_C, // 0x0AA7 // DHA + C_C, // 0x0AA8 // NA + C_U, // 0x0AA9 // UNASSIGNED + C_C, // 0x0AAA // PA + C_C, // 0x0AAB // PHA + C_C, // 0x0AAC // BA + C_C, // 0x0AAD // BHA + C_C, // 0x0AAE // MA + C_C, // 0x0AAF // YA + C_C|C_R, // 0x0AB0 // RA // CSOK: WhitespaceAround + C_U, // 0x0AB1 // UNASSIGNED + C_C, // 0x0AB2 // LA + C_C, // 0x0AB3 // LLA + C_U, // 0x0AB4 // UNASSIGNED + C_C, // 0x0AB5 // VA + C_C, // 0x0AB6 // SHA + C_C, // 0x0AB7 // SSA + C_C, // 0x0AB8 // SA + C_C, // 0x0AB9 // HA + C_U, // 0x0ABA // UNASSIGNED + C_U, // 0x0ABB // UNASSIGNED + C_N, // 0x0ABC // NUKTA + C_S, // 0x0ABD // AVAGRAHA + C_M, // 0x0ABE // AA + C_M|C_PRE, // 0x0ABF // I // CSOK: WhitespaceAround + C_M, // 0x0AC0 // II + C_M, // 0x0AC1 // U + C_M, // 0x0AC2 // UU + C_M, // 0x0AC3 // VOCALIC R + C_M, // 0x0AC4 // VOCALIC RR + C_M, // 0x0AC5 // CANDRA E + C_U, // 0x0AC6 // UNASSIGNED + C_M, // 0x0AC7 // E + C_M, // 0x0AC8 // AI + C_M, // 0x0AC9 // CANDRA O + C_U, // 0x0ACA // UNASSIGNED + C_M, // 0x0ACB // O + C_M, // 0x0ACC // AU + C_H, // 0x0ACD // VIRAMA (HALANT) + C_U, // 0x0ACE // UNASSIGNED + C_U, // 0x0ACF // UNASSIGNED + C_S, // 0x0AD0 // OM + C_U, // 0x0AD1 // UNASSIGNED + C_U, // 0x0AD2 // UNASSIGNED + C_U, // 0x0AD3 // UNASSIGNED + C_U, // 0x0AD4 // UNASSIGNED + C_U, // 0x0AD5 // UNASSIGNED + C_U, // 0x0AD6 // UNASSIGNED + C_U, // 0x0AD7 // UNASSIGNED + C_U, // 0x0AD8 // UNASSIGNED + C_U, // 0x0AD9 // UNASSIGNED + C_U, // 0x0ADA // UNASSIGNED + C_U, // 0x0ADB // UNASSIGNED + C_U, // 0x0ADC // UNASSIGNED + C_U, // 0x0ADD // UNASSIGNED + C_U, // 0x0ADE // UNASSIGNED + C_U, // 0x0ADF // UNASSIGNED + C_V, // 0x0AE0 // VOCALIC RR + C_V, // 0x0AE1 // VOCALIC LL + C_M, // 0x0AE2 // VOCALIC L + C_M, // 0x0AE3 // VOCALIC LL + C_U, // 0x0AE4 // UNASSIGNED + C_U, // 0x0AE5 // UNASSIGNED + C_D, // 0x0AE6 // ZERO + C_D, // 0x0AE7 // ONE + C_D, // 0x0AE8 // TWO + C_D, // 0x0AE9 // THREE + C_D, // 0x0AEA // FOUR + C_D, // 0x0AEB // FIVE + C_D, // 0x0AEC // SIX + C_D, // 0x0AED // SEVEN + C_D, // 0x0AEE // EIGHT + C_D, // 0x0AEF // NINE + C_U, // 0x0AF0 // UNASSIGNED + C_S, // 0x0AF1 // RUPEE SIGN + C_U, // 0x0AF2 // UNASSIGNED + C_U, // 0x0AF3 // UNASSIGNED + C_U, // 0x0AF4 // UNASSIGNED + C_U, // 0x0AF5 // UNASSIGNED + C_U, // 0x0AF6 // UNASSIGNED + C_U, // 0x0AF7 // UNASSIGNED + C_U, // 0x0AF8 // UNASSIGNED + C_U, // 0x0AF9 // UNASSIGNED + C_U, // 0x0AFA // UNASSIGNED + C_U, // 0x0AFB // UNASSIGNED + C_U, // 0x0AFC // UNASSIGNED + C_U, // 0x0AFD // UNASSIGNED + C_U, // 0x0AFE // UNASSIGNED + C_U // 0x0AFF // UNASSIGNED + }; + static int typeOf(int c) { + if ( ( c >= ccaStart ) && ( c < ccaEnd ) ) { + return cca [ c - ccaStart ] & C_M_TYPE; + } else { + return C_U; + } + } + static boolean isType(int c, int t) { + return typeOf ( c ) == t; + } + static boolean hasFlag(int c, int f) { + if ( ( c >= ccaStart ) && ( c < ccaEnd ) ) { + return ( cca [ c - ccaStart ] & f ) == f; + } else { + return false; + } + } + static boolean isC(int c) { + return isType(c,C_C); + } + static boolean isR(int c) { + return isType(c,C_C) && hasR(c); + } + static boolean isV(int c) { + return isType(c,C_V); + } + static boolean isN(int c) { + return c == 0x0ABC; + } + static boolean isH(int c) { + return c == 0x0ACD; + } + static boolean isM(int c) { + return isType(c,C_M); + } + static boolean isPreM(int c) { + return isType(c,C_M) && hasFlag(c,C_PRE); + } + static boolean isX(int c) { + switch ( typeOf ( c ) ) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + static boolean hasR(int c) { + return hasFlag(c,C_R); + } + static boolean hasN(int c) { + return hasFlag(c,C_N); + } + +} diff --git a/src/java/org/apache/fop/complexscripts/scripts/GurmukhiScriptProcessor.java b/src/java/org/apache/fop/complexscripts/scripts/GurmukhiScriptProcessor.java new file mode 100644 index 000000000..1f1b56738 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/scripts/GurmukhiScriptProcessor.java @@ -0,0 +1,543 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.scripts; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: AvoidNestedBlocksCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: WhitespaceAfter +// CSOFF: InnerAssignmentCheck +// CSOFF: SimplifyBooleanReturnCheck +// CSOFF: LineLengthCheck + +/** + * <p>The <code>GurmukhiScriptProcessor</code> class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Gurmukhi script.</p> + * @author Glenn Adams + */ +public class GurmukhiScriptProcessor extends IndicScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(GurmukhiScriptProcessor.class); // CSOK: ConstantNameCheck + + GurmukhiScriptProcessor ( String script ) { + super ( script ); + } + + @Override + protected Class<? extends GurmukhiSyllabizer> getSyllabizerClass() { + return GurmukhiSyllabizer.class; + } + + @Override + // find rightmost pre-base matra + protected int findPreBaseMatra ( GlyphSequence gs ) { + int ng = gs.getGlyphCount(); + int lk = -1; + for ( int i = ng; i > 0; i-- ) { + int k = i - 1; + if ( containsPreBaseMatra ( gs, k ) ) { + lk = k; + break; + } + } + return lk; + } + + @Override + // find leftmost pre-base matra target, starting from source + protected int findPreBaseMatraTarget ( GlyphSequence gs, int source ) { + int ng = gs.getGlyphCount(); + int lk = -1; + for ( int i = ( source < ng ) ? source : ng; i > 0; i-- ) { + int k = i - 1; + if ( containsConsonant ( gs, k ) ) { + if ( containsHalfConsonant ( gs, k ) ) { + lk = k; + } else if ( lk == -1 ) { + lk = k; + } else { + break; + } + } + } + return lk; + } + + private static boolean containsPreBaseMatra ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + if ( isPreM ( ca [ i ] ) ) { + return true; + } + } + return false; + } + + private static boolean containsConsonant ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + if ( isC ( ca [ i ] ) ) { + return true; + } + } + return false; + } + + private static boolean containsHalfConsonant ( GlyphSequence gs, int k ) { + Boolean half = (Boolean) gs.getAssociation ( k ) . getPredication ( "half" ); + return ( half != null ) ? half.booleanValue() : false; + } + + @Override + protected int findReph ( GlyphSequence gs ) { + int ng = gs.getGlyphCount(); + int li = -1; + for ( int i = 0; i < ng; i++ ) { + if ( containsReph ( gs, i ) ) { + li = i; + break; + } + } + return li; + } + + @Override + protected int findRephTarget ( GlyphSequence gs, int source ) { + int ng = gs.getGlyphCount(); + int c1 = -1; + int c2 = -1; + // first candidate target is after first non-half consonant + for ( int i = 0; i < ng; i++ ) { + if ( ( i != source ) && containsConsonant ( gs, i ) ) { + if ( ! containsHalfConsonant ( gs, i ) ) { + c1 = i + 1; + break; + } + } + } + // second candidate target is after last non-prebase matra after first candidate or before first syllable or vedic mark + for ( int i = ( c1 >= 0 ) ? c1 : 0; i < ng; i++ ) { + if ( containsMatra ( gs, i ) && ! containsPreBaseMatra ( gs, i ) ) { + c2 = i + 1; + } else if ( containsOtherMark ( gs, i ) ) { + c2 = i; + break; + } + } + if ( c2 >= 0 ) { + return c2; + } else if ( c1 >= 0 ) { + return c1; + } else { + return source; + } + } + + private static boolean containsReph ( GlyphSequence gs, int k ) { + Boolean rphf = (Boolean) gs.getAssociation ( k ) . getPredication ( "rphf" ); + return ( rphf != null ) ? rphf.booleanValue() : false; + } + + private static boolean containsMatra ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + if ( isM ( ca [ i ] ) ) { + return true; + } + } + return false; + } + + private static boolean containsOtherMark ( GlyphSequence gs, int k ) { + GlyphSequence.CharAssociation a = gs.getAssociation ( k ); + int[] ca = gs.getCharacterArray ( false ); + for ( int i = a.getStart(), e = a.getEnd(); i < e; i++ ) { + switch ( typeOf ( ca [ i ] ) ) { + case C_T: // tone (e.g., udatta, anudatta) + case C_A: // accent (e.g., acute, grave) + case C_O: // other (e.g., candrabindu, anusvara, visarga, etc) + return true; + default: + break; + } + } + return false; + } + + private static class GurmukhiSyllabizer extends DefaultSyllabizer { + GurmukhiSyllabizer ( String script, String language ) { + super ( script, language ); + } + @Override + // | C ... + protected int findStartOfSyllable ( int[] ca, int s, int e ) { + if ( ( s < 0 ) || ( s >= e ) ) { + return -1; + } else { + while ( s < e ) { + int c = ca [ s ]; + if ( isC ( c ) ) { + break; + } else { + s++; + } + } + return s; + } + } + @Override + // D* L? | ... + protected int findEndOfSyllable ( int[] ca, int s, int e ) { + if ( ( s < 0 ) || ( s >= e ) ) { + return -1; + } else { + int nd = 0; + int nl = 0; + int i; + // consume dead consonants + while ( ( i = isDeadConsonant ( ca, s, e ) ) > s ) { + s = i; nd++; + } + // consume zero or one live consonant + if ( ( i = isLiveConsonant ( ca, s, e ) ) > s ) { + s = i; nl++; + } + return ( ( nd > 0 ) || ( nl > 0 ) ) ? s : -1; + } + } + // D := ( C N? H )? + private int isDeadConsonant ( int[] ca, int s, int e ) { + if ( s < 0 ) { + return -1; + } else { + int c, i = 0; + int nc = 0, nh = 0; + do { + // C + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isC ( c ) ) { + i++; + nc++; + } else { + break; + } + } + // N? + if ( ( s + i ) < e ) { + c = ca [ s + 1 ]; + if ( isN ( c ) ) { + i++; + } + } + // H + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isH ( c ) ) { + i++; + nh++; + } else { + break; + } + } + } while ( false ); + return ( nc > 0 ) && ( nh > 0 ) ? s + i : -1; + } + } + // L := ( (C|V) N? X* )?; where X = ( MATRA | ACCENT MARK | TONE MARK | OTHER MARK ) + private int isLiveConsonant ( int[] ca, int s, int e ) { + if ( s < 0 ) { + return -1; + } else { + int c, i = 0; + int nc = 0, nv = 0, nx = 0; + do { + // C + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isC ( c ) ) { + i++; + nc++; + } else if ( isV ( c ) ) { + i++; + nv++; + } else { + break; + } + } + // N? + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isN ( c ) ) { + i++; + } + } + // X* + while ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isX ( c ) ) { + i++; + nx++; + } else { + break; + } + } + } while ( false ); + // if no X but has H, then ignore C|I + if ( nx == 0 ) { + if ( ( s + i ) < e ) { + c = ca [ s + i ]; + if ( isH ( c ) ) { + if ( nc > 0 ) { + nc--; + } else if ( nv > 0 ) { + nv--; + } + } + } + } + return ( ( nc > 0 ) || ( nv > 0 ) ) ? s + i : -1; + } + } + } + + // gurmukhi character types + static final short C_U = 0; // unassigned + static final short C_C = 1; // consonant + static final short C_V = 2; // vowel + static final short C_M = 3; // vowel sign (matra) + static final short C_S = 4; // symbol or sign + static final short C_T = 5; // tone mark + static final short C_A = 6; // accent mark + static final short C_P = 7; // punctuation + static final short C_D = 8; // digit + static final short C_H = 9; // halant (virama) + static final short C_O = 10; // other signs + static final short C_N = 0x0100; // nukta(ized) + static final short C_R = 0x0200; // reph(ized) + static final short C_PRE = 0x0400; // pre-base + static final short C_M_TYPE = 0x00FF; // type mask + static final short C_M_FLAGS = 0x7F00; // flag mask + // gurmukhi block range + static final int ccaStart = 0x0A00; // first code point mapped by cca // CSOK: ConstantNameCheck + static final int ccaEnd = 0x0A80; // last code point + 1 mapped by cca // CSOK: ConstantNameCheck + // gurmukhi character type lookups + static final short[] cca = { // CSOK: ConstantNameCheck + C_U, // 0x0A00 // UNASSIGNED + C_O, // 0x0A01 // ADAK BINDI + C_O, // 0x0A02 // BINDI + C_O, // 0x0A03 // VISARGA + C_U, // 0x0A04 // UNASSIGNED + C_V, // 0x0A05 // A + C_V, // 0x0A06 // AA + C_V, // 0x0A07 // I + C_V, // 0x0A08 // II + C_V, // 0x0A09 // U + C_V, // 0x0A0A // UU + C_U, // 0x0A0B // UNASSIGNED + C_U, // 0x0A0C // UNASSIGNED + C_U, // 0x0A0D // UNASSIGNED + C_U, // 0x0A0E // UNASSIGNED + C_V, // 0x0A0F // E + C_V, // 0x0A10 // AI + C_U, // 0x0A11 // UNASSIGNED + C_U, // 0x0A12 // UNASSIGNED + C_V, // 0x0A13 // O + C_V, // 0x0A14 // AU + C_C, // 0x0A15 // KA + C_C, // 0x0A16 // KHA + C_C, // 0x0A17 // GA + C_C, // 0x0A18 // GHA + C_C, // 0x0A19 // NGA + C_C, // 0x0A1A // CA + C_C, // 0x0A1B // CHA + C_C, // 0x0A1C // JA + C_C, // 0x0A1D // JHA + C_C, // 0x0A1E // NYA + C_C, // 0x0A1F // TTA + C_C, // 0x0A20 // TTHA + C_C, // 0x0A21 // DDA + C_C, // 0x0A22 // DDHA + C_C, // 0x0A23 // NNA + C_C, // 0x0A24 // TA + C_C, // 0x0A25 // THA + C_C, // 0x0A26 // DA + C_C, // 0x0A27 // DHA + C_C, // 0x0A28 // NA + C_U, // 0x0A29 // UNASSIGNED + C_C, // 0x0A2A // PA + C_C, // 0x0A2B // PHA + C_C, // 0x0A2C // BA + C_C, // 0x0A2D // BHA + C_C, // 0x0A2E // MA + C_C, // 0x0A2F // YA + C_C|C_R, // 0x0A30 // RA // CSOK: WhitespaceAround + C_U, // 0x0A31 // UNASSIGNED + C_C, // 0x0A32 // LA + C_C, // 0x0A33 // LLA + C_U, // 0x0A34 // UNASSIGNED + C_C, // 0x0A35 // VA + C_C, // 0x0A36 // SHA + C_U, // 0x0A37 // UNASSIGNED + C_C, // 0x0A38 // SA + C_C, // 0x0A39 // HA + C_U, // 0x0A3A // UNASSIGNED + C_U, // 0x0A3B // UNASSIGNED + C_N, // 0x0A3C // NUKTA + C_U, // 0x0A3D // UNASSIGNED + C_M, // 0x0A3E // AA + C_M|C_PRE, // 0x0A3F // I // CSOK: WhitespaceAround + C_M, // 0x0A40 // II + C_M, // 0x0A41 // U + C_M, // 0x0A42 // UU + C_U, // 0x0A43 // UNASSIGNED + C_U, // 0x0A44 // UNASSIGNED + C_U, // 0x0A45 // UNASSIGNED + C_U, // 0x0A46 // UNASSIGNED + C_M, // 0x0A47 // EE + C_M, // 0x0A48 // AI + C_U, // 0x0A49 // UNASSIGNED + C_U, // 0x0A4A // UNASSIGNED + C_M, // 0x0A4B // OO + C_M, // 0x0A4C // AU + C_H, // 0x0A4D // VIRAMA (HALANT) + C_U, // 0x0A4E // UNASSIGNED + C_U, // 0x0A4F // UNASSIGNED + C_U, // 0x0A50 // UNASSIGNED + C_T, // 0x0A51 // UDATTA + C_U, // 0x0A52 // UNASSIGNED + C_U, // 0x0A53 // UNASSIGNED + C_U, // 0x0A54 // UNASSIGNED + C_U, // 0x0A55 // UNASSIGNED + C_U, // 0x0A56 // UNASSIGNED + C_U, // 0x0A57 // UNASSIGNED + C_U, // 0x0A58 // UNASSIGNED + C_C|C_N, // 0x0A59 // KHHA // CSOK: WhitespaceAround + C_C|C_N, // 0x0A5A // GHHA // CSOK: WhitespaceAround + C_C|C_N, // 0x0A5B // ZA // CSOK: WhitespaceAround + C_C|C_N, // 0x0A5C // RRA // CSOK: WhitespaceAround + C_U, // 0x0A5D // UNASSIGNED + C_C|C_N, // 0x0A5E // FA // CSOK: WhitespaceAround + C_U, // 0x0A5F // UNASSIGNED + C_U, // 0x0A60 // UNASSIGNED + C_U, // 0x0A61 // UNASSIGNED + C_U, // 0x0A62 // UNASSIGNED + C_U, // 0x0A63 // UNASSIGNED + C_U, // 0x0A64 // UNASSIGNED + C_U, // 0x0A65 // UNASSIGNED + C_D, // 0x0A66 // ZERO + C_D, // 0x0A67 // ONE + C_D, // 0x0A68 // TWO + C_D, // 0x0A69 // THREE + C_D, // 0x0A6A // FOUR + C_D, // 0x0A6B // FIVE + C_D, // 0x0A6C // SIX + C_D, // 0x0A6D // SEVEN + C_D, // 0x0A6E // EIGHT + C_D, // 0x0A6F // NINE + C_O, // 0x0A70 // TIPPI + C_O, // 0x0A71 // ADDAK + C_V, // 0x0A72 // IRI + C_V, // 0x0A73 // URA + C_S, // 0x0A74 // EK ONKAR + C_O, // 0x0A75 // YAKASH + C_U, // 0x0A76 // UNASSIGNED + C_U, // 0x0A77 // UNASSIGNED + C_U, // 0x0A78 // UNASSIGNED + C_U, // 0x0A79 // UNASSIGNED + C_U, // 0x0A7A // UNASSIGNED + C_U, // 0x0A7B // UNASSIGNED + C_U, // 0x0A7C // UNASSIGNED + C_U, // 0x0A7D // UNASSIGNED + C_U, // 0x0A7E // UNASSIGNED + C_U // 0x0A7F // UNASSIGNED + }; + static int typeOf(int c) { + if ( ( c >= ccaStart ) && ( c < ccaEnd ) ) { + return cca [ c - ccaStart ] & C_M_TYPE; + } else { + return C_U; + } + } + static boolean isType(int c, int t) { + return typeOf ( c ) == t; + } + static boolean hasFlag(int c, int f) { + if ( ( c >= ccaStart ) && ( c < ccaEnd ) ) { + return ( cca [ c - ccaStart ] & f ) == f; + } else { + return false; + } + } + static boolean isC(int c) { + return isType(c,C_C); + } + static boolean isR(int c) { + return isType(c,C_C) && hasR(c); + } + static boolean isV(int c) { + return isType(c,C_V); + } + static boolean isN(int c) { + return c == 0x0A3C; + } + static boolean isH(int c) { + return c == 0x0A4D; + } + static boolean isM(int c) { + return isType(c,C_M); + } + static boolean isPreM(int c) { + return isType(c,C_M) && hasFlag(c,C_PRE); + } + static boolean isX(int c) { + switch ( typeOf ( c ) ) { + case C_M: // matra (combining vowel) + case C_A: // accent mark + case C_T: // tone mark + case C_O: // other (modifying) mark + return true; + default: + return false; + } + } + static boolean hasR(int c) { + return hasFlag(c,C_R); + } + static boolean hasN(int c) { + return hasFlag(c,C_N); + } + + @Override + public GlyphSequence reorderCombiningMarks ( GlyphDefinitionTable gdef, GlyphSequence gs, int[][] gpa, String script, String language ) { + return super.reorderCombiningMarks ( gdef, gs, gpa, script, language ); + } + +} diff --git a/src/java/org/apache/fop/complexscripts/scripts/IndicScriptProcessor.java b/src/java/org/apache/fop/complexscripts/scripts/IndicScriptProcessor.java new file mode 100644 index 000000000..92ed31501 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/scripts/IndicScriptProcessor.java @@ -0,0 +1,589 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.scripts; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.fonts.GlyphTable; +import org.apache.fop.complexscripts.util.CharScript; +import org.apache.fop.complexscripts.util.GlyphContextTester; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: AvoidNestedBlocksCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: SimplifyBooleanReturnCheck +// CSOFF: EmptyForIteratorPadCheck +// CSOFF: WhitespaceAfterCheck +// CSOFF: ParameterNumberCheck +// CSOFF: LineLengthCheck + +/** + * <p>The <code>IndicScriptProcessor</code> class implements a script processor for + * performing glyph substitution and positioning operations on content associated with the Indic script.</p> + * @author Glenn Adams + */ +public class IndicScriptProcessor extends DefaultScriptProcessor { + + /** logging instance */ + private static final Log log = LogFactory.getLog(IndicScriptProcessor.class); // CSOK: ConstantNameCheck + + /** required features to use for substitutions */ + private static final String[] gsubReqFeatures = // CSOK: ConstantNameCheck + { + "abvf", // above base forms + "abvs", // above base substitutions + "akhn", // akhand + "blwf", // below base forms + "blws", // below base substitutions + "ccmp", // glyph composition/decomposition + "cjct", // conjunct forms + "clig", // contextual ligatures + "half", // half forms + "haln", // halant forms + "locl", // localized forms + "nukt", // nukta forms + "pref", // pre-base forms + "pres", // pre-base substitutions + "pstf", // post-base forms + "psts", // post-base substitutions + "rkrf", // rakar forms + "rphf", // reph form + "vatu" // vattu variants + }; + + /** optional features to use for substitutions */ + private static final String[] gsubOptFeatures = // CSOK: ConstantNameCheck + { + "afrc", // alternative fractions + "calt", // contextual alternatives + "dlig" // discretionary ligatures + }; + + /** required features to use for positioning */ + private static final String[] gposReqFeatures = // CSOK: ConstantNameCheck + { + "abvm", // above base marks + "blwm", // below base marks + "dist", // distance (adjustment) + "kern" // kerning + }; + + /** required features to use for positioning */ + private static final String[] gposOptFeatures = // CSOK: ConstantNameCheck + { + }; + + private static class SubstitutionScriptContextTester implements ScriptContextTester { + private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/(); + public GlyphContextTester getTester ( String feature ) { + return (GlyphContextTester) testerMap.get ( feature ); + } + } + + private static class PositioningScriptContextTester implements ScriptContextTester { + private static Map/*<String,GlyphContextTester>*/ testerMap = new HashMap/*<String,GlyphContextTester>*/(); + public GlyphContextTester getTester ( String feature ) { + return (GlyphContextTester) testerMap.get ( feature ); + } + } + + /** + * Make script specific flavor of Indic script processor. + * @param script tag + * @return script processor instance + */ + public static ScriptProcessor makeProcessor ( String script ) { + switch ( CharScript.scriptCodeFromTag ( script ) ) { + case CharScript.SCRIPT_DEVANAGARI: + case CharScript.SCRIPT_DEVANAGARI_2: + return new DevanagariScriptProcessor ( script ); + case CharScript.SCRIPT_GUJARATI: + case CharScript.SCRIPT_GUJARATI_2: + return new GujaratiScriptProcessor ( script ); + case CharScript.SCRIPT_GURMUKHI: + case CharScript.SCRIPT_GURMUKHI_2: + return new GurmukhiScriptProcessor ( script ); + // [TBD] implement other script processors + default: + return new IndicScriptProcessor ( script ); + } + } + + private final ScriptContextTester subContextTester; + private final ScriptContextTester posContextTester; + + IndicScriptProcessor ( String script ) { + super ( script ); + this.subContextTester = new SubstitutionScriptContextTester(); + this.posContextTester = new PositioningScriptContextTester(); + } + + /** {@inheritDoc} */ + public String[] getSubstitutionFeatures() { + return gsubReqFeatures; + } + + /** {@inheritDoc} */ + public String[] getOptionalSubstitutionFeatures() { + return gsubOptFeatures; + } + + /** {@inheritDoc} */ + public ScriptContextTester getSubstitutionContextTester() { + return subContextTester; + } + + /** {@inheritDoc} */ + public String[] getPositioningFeatures() { + return gposReqFeatures; + } + + /** {@inheritDoc} */ + public String[] getOptionalPositioningFeatures() { + return gposOptFeatures; + } + + /** {@inheritDoc} */ + public ScriptContextTester getPositioningContextTester() { + return posContextTester; + } + + /** {@inheritDoc} */ + @Override + public GlyphSequence substitute ( GlyphSequence gs, String script, String language, GlyphTable.UseSpec[] usa, ScriptContextTester sct ) { + assert usa != null; + // 1. syllabize + GlyphSequence[] sa = syllabize ( gs, script, language ); + // 2. process each syllable + for ( int i = 0, n = sa.length; i < n; i++ ) { + GlyphSequence s = sa [ i ]; + // apply basic shaping subs + for ( int j = 0, m = usa.length; j < m; j++ ) { + GlyphTable.UseSpec us = usa [ j ]; + if ( isBasicShapingUse ( us ) ) { + s.setPredications ( true ); + s = us.substitute ( s, script, language, sct ); + } + } + // reorder pre-base matra + s = reorderPreBaseMatra ( s ); + // reorder reph + s = reorderReph ( s ); + // apply presentation subs + for ( int j = 0, m = usa.length; j < m; j++ ) { + GlyphTable.UseSpec us = usa [ j ]; + if ( isPresentationUse ( us ) ) { + s.setPredications ( true ); + s = us.substitute ( s, script, language, sct ); + } + } + // record result + sa [ i ] = s; + } + // 3. return reassembled substituted syllables + return unsyllabize ( gs, sa ); + } + + /** + * Get script specific syllabizer class. + * @return a syllabizer class object or null + */ + protected Class<? extends Syllabizer> getSyllabizerClass() { + return null; + } + + private GlyphSequence[] syllabize ( GlyphSequence gs, String script, String language ) { + return Syllabizer.getSyllabizer ( script, language, getSyllabizerClass() ) . syllabize ( gs ); + } + + private GlyphSequence unsyllabize ( GlyphSequence gs, GlyphSequence[] sa ) { + return GlyphSequence.join ( gs, sa ); + } + + private static Set<String> basicShapingFeatures; + private static final String[] basicShapingFeatureStrings = { // CSOK: ConstantNameCheck + "abvf", + "akhn", + "blwf", + "cjct", + "half", + "locl", + "nukt", + "pref", + "pstf", + "rkrf", + "rphf", + "vatu", + }; + static { + basicShapingFeatures = new HashSet<String>(); + for ( String s : basicShapingFeatureStrings ) { + basicShapingFeatures.add ( s ); + } + } + private boolean isBasicShapingUse ( GlyphTable.UseSpec us ) { + assert us != null; + if ( basicShapingFeatures != null ) { + return basicShapingFeatures.contains ( us.getFeature() ); + } else { + return false; + } + } + + private static Set<String> presentationFeatures; + private static final String[] presentationFeatureStrings = { // CSOK: ConstantNameCheck + "abvs", + "blws", + "calt", + "haln", + "pres", + "psts", + }; + static { + presentationFeatures = new HashSet<String>(); + for ( String s : presentationFeatureStrings ) { + presentationFeatures.add ( s ); + } + } + private boolean isPresentationUse ( GlyphTable.UseSpec us ) { + assert us != null; + if ( presentationFeatures != null ) { + return presentationFeatures.contains ( us.getFeature() ); + } else { + return false; + } + } + + private GlyphSequence reorderPreBaseMatra ( GlyphSequence gs ) { + int source; + if ( ( source = findPreBaseMatra ( gs ) ) >= 0 ) { + int target; + if ( ( target = findPreBaseMatraTarget ( gs, source ) ) >= 0 ) { + if ( target != source ) { + gs = reorder ( gs, source, target ); + } + } + } + return gs; + } + + /** + * Find pre-base matra in sequence. + * @param gs input sequence + * @return index of pre-base matra or -1 if not found + */ + protected int findPreBaseMatra ( GlyphSequence gs ) { + return -1; + } + + /** + * Find pre-base matra target in sequence. + * @param gs input sequence + * @param source index of pre-base matra + * @return index of pre-base matra target or -1 + */ + protected int findPreBaseMatraTarget ( GlyphSequence gs, int source ) { + return -1; + } + + private GlyphSequence reorderReph ( GlyphSequence gs ) { + int source; + if ( ( source = findReph ( gs ) ) >= 0 ) { + int target; + if ( ( target = findRephTarget ( gs, source ) ) >= 0 ) { + if ( target != source ) { + gs = reorder ( gs, source, target ); + } + } + } + return gs; + } + + /** + * Find reph in sequence. + * @param gs input sequence + * @return index of reph or -1 if not found + */ + protected int findReph ( GlyphSequence gs ) { + return -1; + } + + /** + * Find reph target in sequence. + * @param gs input sequence + * @param source index of reph + * @return index of reph target or -1 + */ + protected int findRephTarget ( GlyphSequence gs, int source ) { + return -1; + } + + private GlyphSequence reorder ( GlyphSequence gs, int source, int target ) { + return GlyphSequence.reorder ( gs, source, 1, target ); + } + + /** {@inheritDoc} */ + @Override + public boolean position ( GlyphSequence gs, String script, String language, int fontSize, GlyphTable.UseSpec[] usa, int[] widths, int[][] adjustments, ScriptContextTester sct ) { + boolean adjusted = super.position ( gs, script, language, fontSize, usa, widths, adjustments, sct ); + return adjusted; + } + + /** Abstract syllabizer. */ + protected abstract static class Syllabizer { + private String script; + private String language; + Syllabizer ( String script, String language ) { + this.script = script; + this.language = language; + } + /** + * Subdivide glyph sequence GS into syllabic segments each represented by a distinct + * output glyph sequence. + * @param gs input glyph sequence + * @return segmented syllabic glyph sequences + */ + abstract GlyphSequence[] syllabize ( GlyphSequence gs ); + /** {@inheritDoc} */ + public int hashCode() { + int hc = 0; + hc = 7 * hc + ( hc ^ script.hashCode() ); + hc = 11 * hc + ( hc ^ language.hashCode() ); + return hc; + } + /** {@inheritDoc} */ + public boolean equals ( Object o ) { + if ( o instanceof Syllabizer ) { + Syllabizer s = (Syllabizer) o; + if ( ! s.script.equals ( script ) ) { + return false; + } else if ( ! s.language.equals ( language ) ) { + return false; + } else { + return true; + } + } else { + return false; + } + } + /** {@inheritDoc} */ + public int compareTo ( Object o ) { + int d; + if ( o instanceof Syllabizer ) { + Syllabizer s = (Syllabizer) o; + if ( ( d = script.compareTo ( s.script ) ) == 0 ) { + d = language.compareTo ( s.language ); + } + } else { + d = -1; + } + return d; + } + private static Map<String,Syllabizer> syllabizers = new HashMap<String,Syllabizer>(); + static Syllabizer getSyllabizer ( String script, String language, Class<? extends Syllabizer> syllabizerClass ) { + String sid = makeSyllabizerId ( script, language ); + Syllabizer s = syllabizers.get ( sid ); + if ( s == null ) { + if ( ( s = makeSyllabizer ( script, language, syllabizerClass ) ) == null ) { + s = new DefaultSyllabizer ( script, language ); + } + syllabizers.put ( sid, s ); + } + return s; + } + static String makeSyllabizerId ( String script, String language ) { + return script + ":" + language; + } + static Syllabizer makeSyllabizer ( String script, String language, Class<? extends Syllabizer> syllabizerClass ) { + Syllabizer s; + try { + Constructor<? extends Syllabizer> cf = syllabizerClass.getDeclaredConstructor ( new Class[] { String.class, String.class } ); + s = (Syllabizer) cf.newInstance ( script, language ); + } catch ( NoSuchMethodException e ) { + s = null; + } catch ( InstantiationException e ) { + s = null; + } catch ( IllegalAccessException e ) { + s = null; + } catch ( InvocationTargetException e ) { + s = null; + } + return s; + } + } + + /** Default syllabizer. */ + protected static class DefaultSyllabizer extends Syllabizer { + DefaultSyllabizer ( String script, String language ) { + super ( script, language ); + } + /** {@inheritDoc} */ + @Override + GlyphSequence[] syllabize ( GlyphSequence gs ) { + int[] ca = gs.getCharacterArray ( false ); + int nc = gs.getCharacterCount(); + if ( nc == 0 ) { + return new GlyphSequence[] { gs }; + } else { + return segmentize ( gs, segmentize ( ca, nc ) ); + } + } + /** + * Construct array of segements from original character array (associated with original glyph sequence) + * @param ca input character sequence + * @param nc number of characters in sequence + * @return array of syllable segments + */ + protected Segment[] segmentize ( int[] ca, int nc ) { + Vector<Segment> sv = new Vector<Segment> ( nc ); + for ( int s = 0, e = nc; s < e; ) { + int i; + if ( ( i = findStartOfSyllable ( ca, s, e ) ) > s ) { + // from s to i is non-syllable segment + sv.add ( new Segment ( s, i, Segment.OTHER ) ); + s = i; // move s to start of syllable + } else if ( i > s ) { + // from s to e is non-syllable segment + sv.add ( new Segment ( s, e, Segment.OTHER ) ); + s = e; // move s to end of input sequence + } + if ( ( i = findEndOfSyllable ( ca, s, e ) ) > s ) { + // from s to i is syllable segment + sv.add ( new Segment ( s, i, Segment.SYLLABLE ) ); + s = i; // move s to end of syllable + } else { + // from s to e is non-syllable segment + sv.add ( new Segment ( s, e, Segment.OTHER ) ); + s = e; // move s to end of input sequence + } + } + return sv.toArray ( new Segment [ sv.size() ] ); + } + /** + * Construct array of glyph sequences from original glyph sequence and segment array. + * @param gs original input glyph sequence + * @param sa segment array + * @return array of glyph sequences each belonging to an (ordered) segment in SA + */ + protected GlyphSequence[] segmentize ( GlyphSequence gs, Segment[] sa ) { + int ng = gs.getGlyphCount(); + int[] ga = gs.getGlyphArray ( false ); + GlyphSequence.CharAssociation[] aa = gs.getAssociations ( 0, -1 ); + Vector<GlyphSequence> nsv = new Vector<GlyphSequence>(); + for ( int i = 0, ns = sa.length; i < ns; i++ ) { + Segment s = sa [ i ]; + Vector<Integer> ngv = new Vector<Integer> ( ng ); + Vector<GlyphSequence.CharAssociation> nav = new Vector<GlyphSequence.CharAssociation> ( ng ); + for ( int j = 0; j < ng; j++ ) { + GlyphSequence.CharAssociation ca = aa [ j ]; + if ( ca.contained ( s.getOffset(), s.getCount() ) ) { + ngv.add ( ga [ j ] ); + nav.add ( ca ); + } + } + if ( ngv.size() > 0 ) { + nsv.add ( new GlyphSequence ( gs, null, toIntArray ( ngv ), null, null, nav.toArray ( new GlyphSequence.CharAssociation [ nav.size() ] ), null ) ); + } + } + if ( nsv.size() > 0 ) { + return nsv.toArray ( new GlyphSequence [ nsv.size() ] ); + } else { + return new GlyphSequence[] { gs }; + } + } + /** + * Find start of syllable in character array, starting at S, ending at E. + * @param ca character array + * @param s start index + * @param e end index + * @return index of start or E if no start found + */ + protected int findStartOfSyllable ( int[] ca, int s, int e ) { + return e; + } + /** + * Find end of syllable in character array, starting at S, ending at E. + * @param ca character array + * @param s start index + * @param e end index + * @return index of start or S if no end found + */ + protected int findEndOfSyllable ( int[] ca, int s, int e ) { + return s; + } + private static int[] toIntArray ( Vector<Integer> iv ) { + int ni = iv.size(); + int[] ia = new int [ iv.size() ]; + for ( int i = 0, n = ni; i < n; i++ ) { + ia [ i ] = (int) iv.get ( i ); + } + return ia; + } + } + + /** Syllabic segment. */ + protected static class Segment { + + static final int OTHER = 0; // other (non-syllable) characters + static final int SYLLABLE = 1; // (orthographic) syllable + + private int start; + private int end; + private int type; + + Segment ( int start, int end, int type ) { + this.start = start; + this.end = end; + this.type = type; + } + + int getStart() { + return start; + } + + int getEnd() { + return end; + } + + int getOffset() { + return start; + } + + int getCount() { + return end - start; + } + + int getType() { + return type; + } + } +} diff --git a/src/java/org/apache/fop/complexscripts/scripts/ScriptProcessor.java b/src/java/org/apache/fop/complexscripts/scripts/ScriptProcessor.java new file mode 100644 index 000000000..4f6feffee --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/scripts/ScriptProcessor.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.scripts; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; +import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; +import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable; +import org.apache.fop.complexscripts.fonts.GlyphTable; +import org.apache.fop.complexscripts.util.CharScript; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.complexscripts.util.ScriptContextTester; + +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck +// CSOFF: ParameterNumberCheck + +/** + * Abstract script processor base class for which an implementation of the substitution and positioning methods + * must be supplied. + * @author Glenn Adams + */ +public abstract class ScriptProcessor { + + private final String script; + + private static Map<String, ScriptProcessor> processors = new HashMap<String, ScriptProcessor>(); + + /** + * Instantiate a script processor. + * @param script a script identifier + */ + protected ScriptProcessor ( String script ) { + if ( ( script == null ) || ( script.length() == 0 ) ) { + throw new IllegalArgumentException ( "script must be non-empty string" ); + } else { + this.script = script; + } + } + + /** @return script identifier */ + public final String getScript() { + return script; + } + + /** + * Obtain script specific required substitution features. + * @return array of suppported substitution features or null + */ + public abstract String[] getSubstitutionFeatures(); + + /** + * Obtain script specific optional substitution features. + * @return array of suppported substitution features or null + */ + public String[] getOptionalSubstitutionFeatures() { + return new String[0]; + } + + /** + * Obtain script specific substitution context tester. + * @return substitution context tester or null + */ + public abstract ScriptContextTester getSubstitutionContextTester(); + + /** + * Perform substitution processing using a specific set of lookup tables. + * @param gsub the glyph substitution table that applies + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param lookups a mapping from lookup specifications to glyph subtables to use for substitution processing + * @return the substituted (output) glyph sequence + */ + public final GlyphSequence substitute ( GlyphSubstitutionTable gsub, GlyphSequence gs, String script, String language, Map/*<LookupSpec,List<LookupTable>>>*/ lookups ) { + return substitute ( gs, script, language, assembleLookups ( gsub, getSubstitutionFeatures(), lookups ), getSubstitutionContextTester() ); + } + + /** + * Perform substitution processing using a specific set of ordered glyph table use specifications. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param usa an ordered array of glyph table use specs + * @param sct a script specific context tester (or null) + * @return the substituted (output) glyph sequence + */ + public GlyphSequence substitute ( GlyphSequence gs, String script, String language, GlyphTable.UseSpec[] usa, ScriptContextTester sct ) { + assert usa != null; + for ( int i = 0, n = usa.length; i < n; i++ ) { + GlyphTable.UseSpec us = usa [ i ]; + gs = us.substitute ( gs, script, language, sct ); + } + return gs; + } + + /** + * Reorder combining marks in glyph sequence so that they precede (within the sequence) the base + * character to which they are applied. N.B. In the case of RTL segments, marks are not reordered by this, + * method since when the segment is reversed by BIDI processing, marks are automatically reordered to precede + * their base glyph. + * @param gdef the glyph definition table that applies + * @param gs an input glyph sequence + * @param gpa associated glyph position adjustments (also reordered) + * @param script a script identifier + * @param language a language identifier + * @return the reordered (output) glyph sequence + */ + public GlyphSequence reorderCombiningMarks ( GlyphDefinitionTable gdef, GlyphSequence gs, int[][] gpa, String script, String language ) { + return gs; + } + + /** + * Obtain script specific required positioning features. + * @return array of suppported positioning features or null + */ + public abstract String[] getPositioningFeatures(); + + /** + * Obtain script specific optional positioning features. + * @return array of suppported positioning features or null + */ + public String[] getOptionalPositioningFeatures() { + return new String[0]; + } + + /** + * Obtain script specific positioning context tester. + * @return positioning context tester or null + */ + public abstract ScriptContextTester getPositioningContextTester(); + + /** + * Perform positioning processing using a specific set of lookup tables. + * @param gpos the glyph positioning table that applies + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param fontSize size in device units + * @param lookups a mapping from lookup specifications to glyph subtables to use for positioning processing + * @param widths array of default advancements for each glyph + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @return true if some adjustment is not zero; otherwise, false + */ + public final boolean position ( GlyphPositioningTable gpos, GlyphSequence gs, String script, String language, int fontSize, Map/*<LookupSpec,List<LookupTable>>*/ lookups, int[] widths, int[][] adjustments ) { + return position ( gs, script, language, fontSize, assembleLookups ( gpos, getPositioningFeatures(), lookups ), widths, adjustments, getPositioningContextTester() ); + } + + /** + * Perform positioning processing using a specific set of ordered glyph table use specifications. + * @param gs an input glyph sequence + * @param script a script identifier + * @param language a language identifier + * @param fontSize size in device units + * @param usa an ordered array of glyph table use specs + * @param widths array of default advancements for each glyph in font + * @param adjustments accumulated adjustments array (sequence) of 4-tuples of placement [PX,PY] and advance [AX,AY] adjustments, in that order, + * with one 4-tuple for each element of glyph sequence + * @param sct a script specific context tester (or null) + * @return true if some adjustment is not zero; otherwise, false + */ + public boolean position ( GlyphSequence gs, String script, String language, int fontSize, GlyphTable.UseSpec[] usa, int[] widths, int[][] adjustments, ScriptContextTester sct ) { + assert usa != null; + boolean adjusted = false; + for ( int i = 0, n = usa.length; i < n; i++ ) { + GlyphTable.UseSpec us = usa [ i ]; + if ( us.position ( gs, script, language, fontSize, widths, adjustments, sct ) ) { + adjusted = true; + } + } + return adjusted; + } + + /** + * Assemble ordered array of lookup table use specifications according to the specified features and candidate lookups, + * where the order of the array is in accordance to the order of the applicable lookup list. + * @param table the governing glyph table + * @param features array of feature identifiers to apply + * @param lookups a mapping from lookup specifications to lists of look tables from which to select lookup tables according to the specified features + * @return ordered array of assembled lookup table use specifications + */ + public final GlyphTable.UseSpec[] assembleLookups ( GlyphTable table, String[] features, Map/*<LookupSpec,List<LookupTable>>*/ lookups ) { + return table.assembleLookups ( features, lookups ); + } + + /** + * Obtain script processor instance associated with specified script. + * @param script a script identifier + * @return a script processor instance or null if none found + */ + public static synchronized ScriptProcessor getInstance ( String script ) { + ScriptProcessor sp = null; + assert processors != null; + if ( ( sp = processors.get ( script ) ) == null ) { + processors.put ( script, sp = createProcessor ( script ) ); + } + return sp; + } + + // [TBD] - rework to provide more configurable binding between script name and script processor constructor + private static ScriptProcessor createProcessor ( String script ) { + ScriptProcessor sp = null; + int sc = CharScript.scriptCodeFromTag ( script ); + if ( sc == CharScript.SCRIPT_ARABIC ) { + sp = new ArabicScriptProcessor ( script ); + } else if ( CharScript.isIndicScript ( sc ) ) { + sp = IndicScriptProcessor.makeProcessor ( script ); + } else { + sp = new DefaultScriptProcessor ( script ); + } + return sp; + } + +} diff --git a/src/java/org/apache/fop/complexscripts/util/CharMirror.java b/src/java/org/apache/fop/complexscripts/util/CharMirror.java new file mode 100644 index 000000000..bb1d1587f --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/util/CharMirror.java @@ -0,0 +1,715 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.util; + +import java.util.Arrays; + +/** + * Mirror related utilities. + * @author Glenn Adams + */ +public final class CharMirror { + + private CharMirror() { + } + + /** + * Mirror characters that are designated as having the bidi mirrorred property. + * @param s a string whose characters are to be mirrored + * @return the resulting string + */ + public static String mirror ( String s ) { + StringBuffer sb = new StringBuffer ( s ); + for ( int i = 0, n = sb.length(); i < n; i++ ) { + sb.setCharAt ( i, (char) mirror ( sb.charAt ( i ) ) ); + } + return sb.toString(); + } + + private static int[] mirroredCharacters = { + 0x0028, + 0x0029, + 0x003C, + 0x003E, + 0x005B, + 0x005D, + 0x007B, + 0x007D, + 0x00AB, + 0x00BB, + 0x0F3A, + 0x0F3B, + 0x0F3C, + 0x0F3D, + 0x169B, + 0x169C, + 0x2039, + 0x203A, + 0x2045, + 0x2046, + 0x207D, + 0x207E, + 0x208D, + 0x208E, + 0x2208, + 0x2209, + 0x220A, + 0x220B, + 0x220C, + 0x220D, + 0x2215, + 0x223C, + 0x223D, + 0x2243, + 0x2252, + 0x2253, + 0x2254, + 0x2255, + 0x2264, + 0x2265, + 0x2266, + 0x2267, + 0x2268, + 0x2269, + 0x226A, + 0x226B, + 0x226E, + 0x226F, + 0x2270, + 0x2271, + 0x2272, + 0x2273, + 0x2274, + 0x2275, + 0x2276, + 0x2277, + 0x2278, + 0x2279, + 0x227A, + 0x227B, + 0x227C, + 0x227D, + 0x227E, + 0x227F, + 0x2280, + 0x2281, + 0x2282, + 0x2283, + 0x2284, + 0x2285, + 0x2286, + 0x2287, + 0x2288, + 0x2289, + 0x228A, + 0x228B, + 0x228F, + 0x2290, + 0x2291, + 0x2292, + 0x2298, + 0x22A2, + 0x22A3, + 0x22A6, + 0x22A8, + 0x22A9, + 0x22AB, + 0x22B0, + 0x22B1, + 0x22B2, + 0x22B3, + 0x22B4, + 0x22B5, + 0x22B6, + 0x22B7, + 0x22C9, + 0x22CA, + 0x22CB, + 0x22CC, + 0x22CD, + 0x22D0, + 0x22D1, + 0x22D6, + 0x22D7, + 0x22D8, + 0x22D9, + 0x22DA, + 0x22DB, + 0x22DC, + 0x22DD, + 0x22DE, + 0x22DF, + 0x22E0, + 0x22E1, + 0x22E2, + 0x22E3, + 0x22E4, + 0x22E5, + 0x22E6, + 0x22E7, + 0x22E8, + 0x22E9, + 0x22EA, + 0x22EB, + 0x22EC, + 0x22ED, + 0x22F0, + 0x22F1, + 0x22F2, + 0x22F3, + 0x22F4, + 0x22F6, + 0x22F7, + 0x22FA, + 0x22FB, + 0x22FC, + 0x22FD, + 0x22FE, + 0x2308, + 0x2309, + 0x230A, + 0x230B, + 0x2329, + 0x232A, + 0x2768, + 0x2769, + 0x276A, + 0x276B, + 0x276C, + 0x276D, + 0x276E, + 0x276F, + 0x2770, + 0x2771, + 0x2772, + 0x2773, + 0x2774, + 0x2775, + 0x27C3, + 0x27C4, + 0x27C5, + 0x27C6, + 0x27C8, + 0x27C9, + 0x27D5, + 0x27D6, + 0x27DD, + 0x27DE, + 0x27E2, + 0x27E3, + 0x27E4, + 0x27E5, + 0x27E6, + 0x27E7, + 0x27E8, + 0x27E9, + 0x27EA, + 0x27EB, + 0x27EC, + 0x27ED, + 0x27EE, + 0x27EF, + 0x2983, + 0x2984, + 0x2985, + 0x2986, + 0x2987, + 0x2988, + 0x2989, + 0x298A, + 0x298B, + 0x298C, + 0x298D, + 0x298E, + 0x298F, + 0x2990, + 0x2991, + 0x2992, + 0x2993, + 0x2994, + 0x2995, + 0x2996, + 0x2997, + 0x2998, + 0x29B8, + 0x29C0, + 0x29C1, + 0x29C4, + 0x29C5, + 0x29CF, + 0x29D0, + 0x29D1, + 0x29D2, + 0x29D4, + 0x29D5, + 0x29D8, + 0x29D9, + 0x29DA, + 0x29DB, + 0x29F5, + 0x29F8, + 0x29F9, + 0x29FC, + 0x29FD, + 0x2A2B, + 0x2A2C, + 0x2A2D, + 0x2A2E, + 0x2A34, + 0x2A35, + 0x2A3C, + 0x2A3D, + 0x2A64, + 0x2A65, + 0x2A79, + 0x2A7A, + 0x2A7D, + 0x2A7E, + 0x2A7F, + 0x2A80, + 0x2A81, + 0x2A82, + 0x2A83, + 0x2A84, + 0x2A8B, + 0x2A8C, + 0x2A91, + 0x2A92, + 0x2A93, + 0x2A94, + 0x2A95, + 0x2A96, + 0x2A97, + 0x2A98, + 0x2A99, + 0x2A9A, + 0x2A9B, + 0x2A9C, + 0x2AA1, + 0x2AA2, + 0x2AA6, + 0x2AA7, + 0x2AA8, + 0x2AA9, + 0x2AAA, + 0x2AAB, + 0x2AAC, + 0x2AAD, + 0x2AAF, + 0x2AB0, + 0x2AB3, + 0x2AB4, + 0x2AC3, + 0x2AC4, + 0x2AC5, + 0x2AC6, + 0x2ACD, + 0x2ACE, + 0x2ACF, + 0x2AD0, + 0x2AD1, + 0x2AD2, + 0x2AD3, + 0x2AD4, + 0x2AD5, + 0x2AD6, + 0x2ADE, + 0x2AE3, + 0x2E02, + 0x2E03, + 0x2E04, + 0x2E05, + 0x2E09, + 0x2E0A, + 0x2E0C, + 0x2E0D, + 0x2E1C, + 0x2E1D, + 0x2E20, + 0x2E21, + 0x2E22, + 0x2E23, + 0x2E24, + 0x2E25, + 0x2E26, + 0x300E, + 0x300F, + 0x3010, + 0x3011, + 0x3014, + 0x3015, + 0x3016, + 0x3017, + 0x3018, + 0x3019, + 0x301A, + 0x301B, + 0xFE59, + 0xFE5A, + 0xFF3B, + 0xFF3D, + 0xFF5B, + 0xFF5D, + 0xFF5F, + 0xFF60, + 0xFF62, + 0xFF63 + }; + + private static int[] mirroredCharactersMapping = { + 0x0029, + 0x0028, + 0x003E, + 0x003C, + 0x005D, + 0x005B, + 0x007D, + 0x007B, + 0x00BB, + 0x00AB, + 0x0F3B, + 0x0F3A, + 0x0F3D, + 0x0F3C, + 0x169C, + 0x169B, + 0x203A, + 0x2039, + 0x2046, + 0x2045, + 0x207E, + 0x207D, + 0x208E, + 0x208D, + 0x220B, + 0x220C, + 0x220D, + 0x2208, + 0x2209, + 0x220A, + 0x29F5, + 0x223D, + 0x223C, + 0x22CD, + 0x2253, + 0x2252, + 0x2255, + 0x2254, + 0x2265, + 0x2264, + 0x2267, + 0x2266, + 0x2269, + 0x2268, + 0x226B, + 0x226A, + 0x226F, + 0x226E, + 0x2271, + 0x2270, + 0x2273, + 0x2272, + 0x2275, + 0x2274, + 0x2277, + 0x2276, + 0x2279, + 0x2278, + 0x227B, + 0x227A, + 0x227D, + 0x227C, + 0x227F, + 0x227E, + 0x2281, + 0x2280, + 0x2283, + 0x2282, + 0x2285, + 0x2284, + 0x2287, + 0x2286, + 0x2289, + 0x2288, + 0x228B, + 0x228A, + 0x2290, + 0x228F, + 0x2292, + 0x2291, + 0x29B8, + 0x22A3, + 0x22A2, + 0x2ADE, + 0x2AE4, + 0x2AE3, + 0x2AE5, + 0x22B1, + 0x22B0, + 0x22B3, + 0x22B2, + 0x22B5, + 0x22B4, + 0x22B7, + 0x22B6, + 0x22CA, + 0x22C9, + 0x22CC, + 0x22CB, + 0x2243, + 0x22D1, + 0x22D0, + 0x22D7, + 0x22D6, + 0x22D9, + 0x22D8, + 0x22DB, + 0x22DA, + 0x22DD, + 0x22DC, + 0x22DF, + 0x22DE, + 0x22E1, + 0x22E0, + 0x22E3, + 0x22E2, + 0x22E5, + 0x22E4, + 0x22E7, + 0x22E6, + 0x22E9, + 0x22E8, + 0x22EB, + 0x22EA, + 0x22ED, + 0x22EC, + 0x22F1, + 0x22F0, + 0x22FA, + 0x22FB, + 0x22FC, + 0x22FD, + 0x22FE, + 0x22F2, + 0x22F3, + 0x22F4, + 0x22F6, + 0x22F7, + 0x2309, + 0x2308, + 0x230B, + 0x230A, + 0x232A, + 0x2329, + 0x2769, + 0x2768, + 0x276B, + 0x276A, + 0x276D, + 0x276C, + 0x276F, + 0x276E, + 0x2771, + 0x2770, + 0x2773, + 0x2772, + 0x2775, + 0x2774, + 0x27C4, + 0x27C3, + 0x27C6, + 0x27C5, + 0x27C9, + 0x27C8, + 0x27D6, + 0x27D5, + 0x27DE, + 0x27DD, + 0x27E3, + 0x27E2, + 0x27E5, + 0x27E4, + 0x27E7, + 0x27E6, + 0x27E9, + 0x27E8, + 0x27EB, + 0x27EA, + 0x27ED, + 0x27EC, + 0x27EF, + 0x27EE, + 0x2984, + 0x2983, + 0x2986, + 0x2985, + 0x2988, + 0x2987, + 0x298A, + 0x2989, + 0x298C, + 0x298B, + 0x2990, + 0x298F, + 0x298E, + 0x298D, + 0x2992, + 0x2991, + 0x2994, + 0x2993, + 0x2996, + 0x2995, + 0x2998, + 0x2997, + 0x2298, + 0x29C1, + 0x29C0, + 0x29C5, + 0x29C4, + 0x29D0, + 0x29CF, + 0x29D2, + 0x29D1, + 0x29D5, + 0x29D4, + 0x29D9, + 0x29D8, + 0x29DB, + 0x29DA, + 0x2215, + 0x29F9, + 0x29F8, + 0x29FD, + 0x29FC, + 0x2A2C, + 0x2A2B, + 0x2A2E, + 0x2A2D, + 0x2A35, + 0x2A34, + 0x2A3D, + 0x2A3C, + 0x2A65, + 0x2A64, + 0x2A7A, + 0x2A79, + 0x2A7E, + 0x2A7D, + 0x2A80, + 0x2A7F, + 0x2A82, + 0x2A81, + 0x2A84, + 0x2A83, + 0x2A8C, + 0x2A8B, + 0x2A92, + 0x2A91, + 0x2A94, + 0x2A93, + 0x2A96, + 0x2A95, + 0x2A98, + 0x2A97, + 0x2A9A, + 0x2A99, + 0x2A9C, + 0x2A9B, + 0x2AA2, + 0x2AA1, + 0x2AA7, + 0x2AA6, + 0x2AA9, + 0x2AA8, + 0x2AAB, + 0x2AAA, + 0x2AAD, + 0x2AAC, + 0x2AB0, + 0x2AAF, + 0x2AB4, + 0x2AB3, + 0x2AC4, + 0x2AC3, + 0x2AC6, + 0x2AC5, + 0x2ACE, + 0x2ACD, + 0x2AD0, + 0x2ACF, + 0x2AD2, + 0x2AD1, + 0x2AD4, + 0x2AD3, + 0x2AD6, + 0x2AD5, + 0x22A6, + 0x22A9, + 0x2E03, + 0x2E02, + 0x2E05, + 0x2E04, + 0x2E0A, + 0x2E09, + 0x2E0D, + 0x2E0C, + 0x2E1D, + 0x2E1C, + 0x2E21, + 0x2E20, + 0x2E23, + 0x2E22, + 0x2E25, + 0x2E24, + 0x2E27, + 0x300F, + 0x300E, + 0x3011, + 0x3010, + 0x3015, + 0x3014, + 0x3017, + 0x3016, + 0x3019, + 0x3018, + 0x301B, + 0x301A, + 0xFE5A, + 0xFE59, + 0xFF3D, + 0xFF3B, + 0xFF5D, + 0xFF5B, + 0xFF60, + 0xFF5F, + 0xFF63, + 0xFF62 + }; + + private static int mirror ( int c ) { + int i = Arrays.binarySearch ( mirroredCharacters, c ); + if ( i < 0 ) { + return c; + } else { + return mirroredCharactersMapping [ i ]; + } + } + +} diff --git a/src/java/org/apache/fop/complexscripts/util/CharScript.java b/src/java/org/apache/fop/complexscripts/util/CharScript.java new file mode 100644 index 000000000..bcce31327 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/util/CharScript.java @@ -0,0 +1,930 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.util; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.fop.util.CharUtilities; + +// CSOFF: AvoidNestedBlocksCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck +// CSOFF: SimplifyBooleanReturnCheck +// CSOFF: WhitespaceAfterCheck + +/** + * Script related utilities. + * @author Glenn Adams + */ +public final class CharScript { + + // + // The following script codes are based on ISO 15924. Codes less than 1000 are + // official assignments from 15924; those equal to or greater than 1000 are FOP + // implementation specific. + // + /** hebrew script constant */ + public static final int SCRIPT_HEBREW = 125; // 'hebr' + /** mongolian script constant */ + public static final int SCRIPT_MONGOLIAN = 145; // 'mong' + /** arabic script constant */ + public static final int SCRIPT_ARABIC = 160; // 'arab' + /** greek script constant */ + public static final int SCRIPT_GREEK = 200; // 'grek' + /** latin script constant */ + public static final int SCRIPT_LATIN = 215; // 'latn' + /** cyrillic script constant */ + public static final int SCRIPT_CYRILLIC = 220; // 'cyrl' + /** georgian script constant */ + public static final int SCRIPT_GEORGIAN = 240; // 'geor' + /** bopomofo script constant */ + public static final int SCRIPT_BOPOMOFO = 285; // 'bopo' + /** hangul script constant */ + public static final int SCRIPT_HANGUL = 286; // 'hang' + /** gurmukhi script constant */ + public static final int SCRIPT_GURMUKHI = 310; // 'guru' + /** gurmukhi 2 script constant */ + public static final int SCRIPT_GURMUKHI_2 = 1310; // 'gur2' -- MSFT (pseudo) script tag for variant shaping semantics + /** devanagari script constant */ + public static final int SCRIPT_DEVANAGARI = 315; // 'deva' + /** devanagari 2 script constant */ + public static final int SCRIPT_DEVANAGARI_2 = 1315; // 'dev2' -- MSFT (pseudo) script tag for variant shaping semantics + /** gujarati script constant */ + public static final int SCRIPT_GUJARATI = 320; // 'gujr' + /** gujarati 2 script constant */ + public static final int SCRIPT_GUJARATI_2 = 1320; // 'gjr2' -- MSFT (pseudo) script tag for variant shaping semantics + /** bengali script constant */ + public static final int SCRIPT_BENGALI = 326; // 'beng' + /** bengali 2 script constant */ + public static final int SCRIPT_BENGALI_2 = 1326; // 'bng2' -- MSFT (pseudo) script tag for variant shaping semantics + /** oriya script constant */ + public static final int SCRIPT_ORIYA = 327; // 'orya' + /** oriya 2 script constant */ + public static final int SCRIPT_ORIYA_2 = 1327; // 'ory2' -- MSFT (pseudo) script tag for variant shaping semantics + /** tibetan script constant */ + public static final int SCRIPT_TIBETAN = 330; // 'tibt' + /** telugu script constant */ + public static final int SCRIPT_TELUGU = 340; // 'telu' + /** telugu 2 script constant */ + public static final int SCRIPT_TELUGU_2 = 1340; // 'tel2' -- MSFT (pseudo) script tag for variant shaping semantics + /** kannada script constant */ + public static final int SCRIPT_KANNADA = 345; // 'knda' + /** kannada 2 script constant */ + public static final int SCRIPT_KANNADA_2 = 1345; // 'knd2' -- MSFT (pseudo) script tag for variant shaping semantics + /** tamil script constant */ + public static final int SCRIPT_TAMIL = 346; // 'taml' + /** tamil 2 script constant */ + public static final int SCRIPT_TAMIL_2 = 1346; // 'tml2' -- MSFT (pseudo) script tag for variant shaping semantics + /** malayalam script constant */ + public static final int SCRIPT_MALAYALAM = 347; // 'mlym' + /** malayalam 2 script constant */ + public static final int SCRIPT_MALAYALAM_2 = 1347; // 'mlm2' -- MSFT (pseudo) script tag for variant shaping semantics + /** sinhalese script constant */ + public static final int SCRIPT_SINHALESE = 348; // 'sinh' + /** burmese script constant */ + public static final int SCRIPT_BURMESE = 350; // 'mymr' + /** thai script constant */ + public static final int SCRIPT_THAI = 352; // 'thai' + /** khmer script constant */ + public static final int SCRIPT_KHMER = 355; // 'khmr' + /** lao script constant */ + public static final int SCRIPT_LAO = 356; // 'laoo' + /** hiragana script constant */ + public static final int SCRIPT_HIRAGANA = 410; // 'hira' + /** ethiopic script constant */ + public static final int SCRIPT_ETHIOPIC = 430; // 'ethi' + /** han script constant */ + public static final int SCRIPT_HAN = 500; // 'hani' + /** katakana script constant */ + public static final int SCRIPT_KATAKANA = 410; // 'kana' + /** math script constant */ + public static final int SCRIPT_MATH = 995; // 'zmth' + /** symbol script constant */ + public static final int SCRIPT_SYMBOL = 996; // 'zsym' + /** undetermined script constant */ + public static final int SCRIPT_UNDETERMINED = 998; // 'zyyy' + /** uncoded script constant */ + public static final int SCRIPT_UNCODED = 999; // 'zzzz' + + /** + * A static (class) parameter indicating whether V2 indic shaping + * rules apply or not, with default being <code>true</code>. + */ + private static final boolean useV2Indic = true; // CSOK: ConstantNameCheck + + private CharScript() { + } + + /** + * Determine if character c is punctuation. + * @param c a character represented as a unicode scalar value + * @return true if character is punctuation + */ + public static boolean isPunctuation ( int c ) { + if ( ( c >= 0x0021 ) && ( c <= 0x002F ) ) { // basic latin punctuation + return true; + } else if ( ( c >= 0x003A ) && ( c <= 0x0040 ) ) { // basic latin punctuation + return true; + } else if ( ( c >= 0x005F ) && ( c <= 0x0060 ) ) { // basic latin punctuation + return true; + } else if ( ( c >= 0x007E ) && ( c <= 0x007E ) ) { // basic latin punctuation + return true; + } else if ( ( c >= 0x007E ) && ( c <= 0x007E ) ) { // basic latin punctuation + return true; + } else if ( ( c >= 0x00A1 ) && ( c <= 0x00BF ) ) { // latin supplement punctuation + return true; + } else if ( ( c >= 0x00D7 ) && ( c <= 0x00D7 ) ) { // latin supplement punctuation + return true; + } else if ( ( c >= 0x00F7 ) && ( c <= 0x00F7 ) ) { // latin supplement punctuation + return true; + } else if ( ( c >= 0x2000 ) && ( c <= 0x206F ) ) { // general punctuation + return true; + } else { // [TBD] - not complete + return false; + } + } + + /** + * Determine if character c is a digit. + * @param c a character represented as a unicode scalar value + * @return true if character is a digit + */ + public static boolean isDigit ( int c ) { + if ( ( c >= 0x0030 ) && ( c <= 0x0039 ) ) { // basic latin digits + return true; + } else { // [TBD] - not complete + return false; + } + } + + /** + * Determine if character c belong to the hebrew script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to hebrew script + */ + public static boolean isHebrew ( int c ) { + if ( ( c >= 0x0590 ) && ( c <= 0x05FF ) ) { // hebrew block + return true; + } else if ( ( c >= 0xFB00 ) && ( c <= 0xFB4F ) ) { // hebrew presentation forms block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the mongolian script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to mongolian script + */ + public static boolean isMongolian ( int c ) { + if ( ( c >= 0x1800 ) && ( c <= 0x18AF ) ) { // mongolian block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the arabic script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to arabic script + */ + public static boolean isArabic ( int c ) { + if ( ( c >= 0x0600 ) && ( c <= 0x06FF ) ) { // arabic block + return true; + } else if ( ( c >= 0x0750 ) && ( c <= 0x077F ) ) { // arabic supplement block + return true; + } else if ( ( c >= 0xFB50 ) && ( c <= 0xFDFF ) ) { // arabic presentation forms a block + return true; + } else if ( ( c >= 0xFE70 ) && ( c <= 0xFEFF ) ) { // arabic presentation forms b block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the greek script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to greek script + */ + public static boolean isGreek ( int c ) { + if ( ( c >= 0x0370 ) && ( c <= 0x03FF ) ) { // greek (and coptic) block + return true; + } else if ( ( c >= 0x1F00 ) && ( c <= 0x1FFF ) ) { // greek extended block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the latin script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to latin script + */ + public static boolean isLatin ( int c ) { + if ( ( c >= 0x0041 ) && ( c <= 0x005A ) ) { // basic latin upper case + return true; + } else if ( ( c >= 0x0061 ) && ( c <= 0x007A ) ) { // basic latin lower case + return true; + } else if ( ( c >= 0x00C0 ) && ( c <= 0x00D6 ) ) { // latin supplement upper case + return true; + } else if ( ( c >= 0x00D8 ) && ( c <= 0x00DF ) ) { // latin supplement upper case + return true; + } else if ( ( c >= 0x00E0 ) && ( c <= 0x00F6 ) ) { // latin supplement lower case + return true; + } else if ( ( c >= 0x00F8 ) && ( c <= 0x00FF ) ) { // latin supplement lower case + return true; + } else if ( ( c >= 0x0100 ) && ( c <= 0x017F ) ) { // latin extended a + return true; + } else if ( ( c >= 0x0180 ) && ( c <= 0x024F ) ) { // latin extended b + return true; + } else if ( ( c >= 0x1E00 ) && ( c <= 0x1EFF ) ) { // latin extended additional + return true; + } else if ( ( c >= 0x2C60 ) && ( c <= 0x2C7F ) ) { // latin extended c + return true; + } else if ( ( c >= 0xA720 ) && ( c <= 0xA7FF ) ) { // latin extended d + return true; + } else if ( ( c >= 0xFB00 ) && ( c <= 0xFB0F ) ) { // latin ligatures + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the cyrillic script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to cyrillic script + */ + public static boolean isCyrillic ( int c ) { + if ( ( c >= 0x0400 ) && ( c <= 0x04FF ) ) { // cyrillic block + return true; + } else if ( ( c >= 0x0500 ) && ( c <= 0x052F ) ) { // cyrillic supplement block + return true; + } else if ( ( c >= 0x2DE0 ) && ( c <= 0x2DFF ) ) { // cyrillic extended-a block + return true; + } else if ( ( c >= 0xA640 ) && ( c <= 0xA69F ) ) { // cyrillic extended-b block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the georgian script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to georgian script + */ + public static boolean isGeorgian ( int c ) { + if ( ( c >= 0x10A0 ) && ( c <= 0x10FF ) ) { // georgian block + return true; + } else if ( ( c >= 0x2D00 ) && ( c <= 0x2D2F ) ) { // georgian supplement block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the hangul script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to hangul script + */ + public static boolean isHangul ( int c ) { + if ( ( c >= 0x1100 ) && ( c <= 0x11FF ) ) { // hangul jamo + return true; + } else if ( ( c >= 0x3130 ) && ( c <= 0x318F ) ) { // hangul compatibility jamo + return true; + } else if ( ( c >= 0xA960 ) && ( c <= 0xA97F ) ) { // hangul jamo extended a + return true; + } else if ( ( c >= 0xAC00 ) && ( c <= 0xD7A3 ) ) { // hangul syllables + return true; + } else if ( ( c >= 0xD7B0 ) && ( c <= 0xD7FF ) ) { // hangul jamo extended a + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the gurmukhi script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to gurmukhi script + */ + public static boolean isGurmukhi ( int c ) { + if ( ( c >= 0x0A00 ) && ( c <= 0x0A7F ) ) { // gurmukhi block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the devanagari script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to devanagari script + */ + public static boolean isDevanagari ( int c ) { + if ( ( c >= 0x0900 ) && ( c <= 0x097F ) ) { // devangari block + return true; + } else if ( ( c >= 0xA8E0 ) && ( c <= 0xA8FF ) ) { // devangari extended block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the gujarati script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to gujarati script + */ + public static boolean isGujarati ( int c ) { + if ( ( c >= 0x0A80 ) && ( c <= 0x0AFF ) ) { // gujarati block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the bengali script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to bengali script + */ + public static boolean isBengali ( int c ) { + if ( ( c >= 0x0980 ) && ( c <= 0x09FF ) ) { // bengali block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the oriya script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to oriya script + */ + public static boolean isOriya ( int c ) { + if ( ( c >= 0x0B00 ) && ( c <= 0x0B7F ) ) { // oriya block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the tibetan script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to tibetan script + */ + public static boolean isTibetan ( int c ) { + if ( ( c >= 0x0F00 ) && ( c <= 0x0FFF ) ) { // tibetan block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the telugu script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to telugu script + */ + public static boolean isTelugu ( int c ) { + if ( ( c >= 0x0C00 ) && ( c <= 0x0C7F ) ) { // telugu block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the kannada script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to kannada script + */ + public static boolean isKannada ( int c ) { + if ( ( c >= 0x0C00 ) && ( c <= 0x0C7F ) ) { // kannada block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the tamil script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to tamil script + */ + public static boolean isTamil ( int c ) { + if ( ( c >= 0x0B80 ) && ( c <= 0x0BFF ) ) { // tamil block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the malayalam script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to malayalam script + */ + public static boolean isMalayalam ( int c ) { + if ( ( c >= 0x0D00 ) && ( c <= 0x0D7F ) ) { // malayalam block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the sinhalese script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to sinhalese script + */ + public static boolean isSinhalese ( int c ) { + if ( ( c >= 0x0D80 ) && ( c <= 0x0DFF ) ) { // sinhala block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the burmese script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to burmese script + */ + public static boolean isBurmese ( int c ) { + if ( ( c >= 0x1000 ) && ( c <= 0x109F ) ) { // burmese (myanmar) block + return true; + } else if ( ( c >= 0xAA60 ) && ( c <= 0xAA7F ) ) { // burmese (myanmar) extended block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the thai script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to thai script + */ + public static boolean isThai ( int c ) { + if ( ( c >= 0x0E00 ) && ( c <= 0x0E7F ) ) { // thai block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the khmer script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to khmer script + */ + public static boolean isKhmer ( int c ) { + if ( ( c >= 0x1780 ) && ( c <= 0x17FF ) ) { // khmer block + return true; + } else if ( ( c >= 0x19E0 ) && ( c <= 0x19FF ) ) { // khmer symbols block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the lao script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to lao script + */ + public static boolean isLao ( int c ) { + if ( ( c >= 0x0E80 ) && ( c <= 0x0EFF ) ) { // lao block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the ethiopic (amharic) script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to ethiopic (amharic) script + */ + public static boolean isEthiopic ( int c ) { + if ( ( c >= 0x1200 ) && ( c <= 0x137F ) ) { // ethiopic block + return true; + } else if ( ( c >= 0x1380 ) && ( c <= 0x139F ) ) { // ethoipic supplement block + return true; + } else if ( ( c >= 0x2D80 ) && ( c <= 0x2DDF ) ) { // ethoipic extended block + return true; + } else if ( ( c >= 0xAB00 ) && ( c <= 0xAB2F ) ) { // ethoipic extended-a block + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the han (unified cjk) script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to han (unified cjk) script + */ + public static boolean isHan ( int c ) { + if ( ( c >= 0x3400 ) && ( c <= 0x4DBF ) ) { + return true; // cjk unified ideographs extension a + } else if ( ( c >= 0x4E00 ) && ( c <= 0x9FFF ) ) { + return true; // cjk unified ideographs + } else if ( ( c >= 0xF900 ) && ( c <= 0xFAFF ) ) { + return true; // cjk compatibility ideographs + } else if ( ( c >= 0x20000 ) && ( c <= 0x2A6DF ) ) { + return true; // cjk unified ideographs extension b + } else if ( ( c >= 0x2A700 ) && ( c <= 0x2B73F ) ) { + return true; // cjk unified ideographs extension c + } else if ( ( c >= 0x2F800 ) && ( c <= 0x2FA1F ) ) { + return true; // cjk compatibility ideographs supplement + } else { + return false; + } + } + + /** + * Determine if character c belong to the bopomofo script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to bopomofo script + */ + public static boolean isBopomofo ( int c ) { + if ( ( c >= 0x3100 ) && ( c <= 0x312F ) ) { + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the hiragana script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to hiragana script + */ + public static boolean isHiragana ( int c ) { + if ( ( c >= 0x3040 ) && ( c <= 0x309F ) ) { + return true; + } else { + return false; + } + } + + /** + * Determine if character c belong to the katakana script. + * @param c a character represented as a unicode scalar value + * @return true if character belongs to katakana script + */ + public static boolean isKatakana ( int c ) { + if ( ( c >= 0x30A0 ) && ( c <= 0x30FF ) ) { + return true; + } else if ( ( c >= 0x31F0 ) && ( c <= 0x31FF ) ) { + return true; + } else { + return false; + } + } + + /** + * Obtain ISO15924 numeric script code of character. If script is not or cannot be determined, + * then the script code 998 ('zyyy') is returned. + * @param c the character to obtain script + * @return an ISO15924 script code + */ + public static int scriptOf ( int c ) { // [TBD] - needs optimization!!! + if ( CharUtilities.isAnySpace ( c ) ) { + return SCRIPT_UNDETERMINED; + } else if ( isPunctuation ( c ) ) { + return SCRIPT_UNDETERMINED; + } else if ( isDigit ( c ) ) { + return SCRIPT_UNDETERMINED; + } else if ( isLatin ( c ) ) { + return SCRIPT_LATIN; + } else if ( isCyrillic ( c ) ) { + return SCRIPT_CYRILLIC; + } else if ( isGreek ( c ) ) { + return SCRIPT_GREEK; + } else if ( isHan ( c ) ) { + return SCRIPT_HAN; + } else if ( isBopomofo ( c ) ) { + return SCRIPT_BOPOMOFO; + } else if ( isKatakana ( c ) ) { + return SCRIPT_KATAKANA; + } else if ( isHiragana ( c ) ) { + return SCRIPT_HIRAGANA; + } else if ( isHangul ( c ) ) { + return SCRIPT_HANGUL; + } else if ( isArabic ( c ) ) { + return SCRIPT_ARABIC; + } else if ( isHebrew ( c ) ) { + return SCRIPT_HEBREW; + } else if ( isMongolian ( c ) ) { + return SCRIPT_MONGOLIAN; + } else if ( isGeorgian ( c ) ) { + return SCRIPT_GEORGIAN; + } else if ( isGurmukhi ( c ) ) { + return useV2IndicRules ( SCRIPT_GURMUKHI ); + } else if ( isDevanagari ( c ) ) { + return useV2IndicRules ( SCRIPT_DEVANAGARI ); + } else if ( isGujarati ( c ) ) { + return useV2IndicRules ( SCRIPT_GUJARATI ); + } else if ( isBengali ( c ) ) { + return useV2IndicRules ( SCRIPT_BENGALI ); + } else if ( isOriya ( c ) ) { + return useV2IndicRules ( SCRIPT_ORIYA ); + } else if ( isTibetan ( c ) ) { + return SCRIPT_TIBETAN; + } else if ( isTelugu ( c ) ) { + return useV2IndicRules ( SCRIPT_TELUGU ); + } else if ( isKannada ( c ) ) { + return useV2IndicRules ( SCRIPT_KANNADA ); + } else if ( isTamil ( c ) ) { + return useV2IndicRules ( SCRIPT_TAMIL ); + } else if ( isMalayalam ( c ) ) { + return useV2IndicRules ( SCRIPT_MALAYALAM ); + } else if ( isSinhalese ( c ) ) { + return SCRIPT_SINHALESE; + } else if ( isBurmese ( c ) ) { + return SCRIPT_BURMESE; + } else if ( isThai ( c ) ) { + return SCRIPT_THAI; + } else if ( isKhmer ( c ) ) { + return SCRIPT_KHMER; + } else if ( isLao ( c ) ) { + return SCRIPT_LAO; + } else if ( isEthiopic ( c ) ) { + return SCRIPT_ETHIOPIC; + } else { + return SCRIPT_UNDETERMINED; + } + } + + /** + * Obtain the V2 indic script code corresponding to V1 indic script code SC if + * and only iff V2 indic rules apply; otherwise return SC. + * @param sc a V1 indic script code + * @return either SC or the V2 flavor of SC if V2 indic rules apply + */ + public static int useV2IndicRules ( int sc ) { + if ( useV2Indic ) { + return ( sc < 1000 ) ? ( sc + 1000 ) : sc; + } else { + return sc; + } + } + + /** + * Obtain the script codes of each character in a character sequence. If script + * is not or cannot be determined for some character, then the script code 998 + * ('zyyy') is returned. + * @param cs the character sequence + * @return a (possibly empty) array of script codes + */ + public static int[] scriptsOf ( CharSequence cs ) { + Set s = new HashSet(); + for ( int i = 0, n = cs.length(); i < n; i++ ) { + s.add ( Integer.valueOf ( scriptOf ( cs.charAt ( i ) ) ) ); + } + int[] sa = new int [ s.size() ]; + int ns = 0; + for ( Iterator it = s.iterator(); it.hasNext();) { + sa [ ns++ ] = ( (Integer) it.next() ) .intValue(); + } + Arrays.sort ( sa ); + return sa; + } + + /** + * Determine the dominant script of a character sequence. + * @param cs the character sequence + * @return the dominant script or SCRIPT_UNDETERMINED + */ + public static int dominantScript ( CharSequence cs ) { + Map m = new HashMap(); + for ( int i = 0, n = cs.length(); i < n; i++ ) { + int c = cs.charAt ( i ); + int s = scriptOf ( c ); + Integer k = Integer.valueOf ( s ); + Integer v = (Integer) m.get ( k ); + if ( v != null ) { + m.put ( k, Integer.valueOf ( v.intValue() + 1 ) ); + } else { + m.put ( k, Integer.valueOf ( 0 ) ); + } + } + int sMax = -1; + int cMax = -1; + for ( Iterator it = m.entrySet().iterator(); it.hasNext();) { + Map.Entry e = (Map.Entry) it.next(); + Integer k = (Integer) e.getKey(); + int s = k.intValue(); + switch ( s ) { + case SCRIPT_UNDETERMINED: + case SCRIPT_UNCODED: + break; + default: + { + Integer v = (Integer) e.getValue(); + assert v != null; + int c = v.intValue(); + if ( c > cMax ) { + cMax = c; sMax = s; + } + break; + } + } + } + if ( sMax < 0 ) { + sMax = SCRIPT_UNDETERMINED; + } + return sMax; + } + + /** + * Determine if script tag denotes an 'Indic' script, where a + * script is an 'Indic' script if it is intended to be processed by + * the generic 'Indic' Script Processor. + * @param script a script tag + * @return true if script tag is a designated 'Indic' script + */ + public static boolean isIndicScript ( String script ) { + return isIndicScript ( scriptCodeFromTag ( script ) ); + } + + /** + * Determine if script tag denotes an 'Indic' script, where a + * script is an 'Indic' script if it is intended to be processed by + * the generic 'Indic' Script Processor. + * @param script a script code + * @return true if script code is a designated 'Indic' script + */ + public static boolean isIndicScript ( int script ) { + switch ( script ) { + case SCRIPT_BENGALI: + case SCRIPT_BENGALI_2: + case SCRIPT_BURMESE: + case SCRIPT_DEVANAGARI: + case SCRIPT_DEVANAGARI_2: + case SCRIPT_GUJARATI: + case SCRIPT_GUJARATI_2: + case SCRIPT_GURMUKHI: + case SCRIPT_GURMUKHI_2: + case SCRIPT_KANNADA: + case SCRIPT_KANNADA_2: + case SCRIPT_MALAYALAM: + case SCRIPT_MALAYALAM_2: + case SCRIPT_ORIYA: + case SCRIPT_ORIYA_2: + case SCRIPT_TAMIL: + case SCRIPT_TAMIL_2: + case SCRIPT_TELUGU: + case SCRIPT_TELUGU_2: + return true; + default: + return false; + } + } + + /** + * Determine the script tag associated with an internal script code. + * @param code the script code + * @return a script tag + */ + public static String scriptTagFromCode ( int code ) { + Map<Integer,String> m = getScriptTagsMap(); + if ( m != null ) { + String tag; + if ( ( tag = m.get ( Integer.valueOf ( code ) ) ) != null ) { + return tag; + } else { + return ""; + } + } else { + return ""; + } + } + + /** + * Determine the internal script code associated with a script tag. + * @param tag the script tag + * @return a script code + */ + public static int scriptCodeFromTag ( String tag ) { + Map<String,Integer> m = getScriptCodeMap(); + if ( m != null ) { + Integer c; + if ( ( c = m.get ( tag ) ) != null ) { + return (int) c; + } else { + return SCRIPT_UNDETERMINED; + } + } else { + return SCRIPT_UNDETERMINED; + } + } + + private static Map<Integer,String> scriptTagsMap = null; + private static Map<String,Integer> scriptCodeMap = null; + + private static void putScriptTag ( Map tm, Map cm, int code, String tag ) { + assert tag != null; + assert tag.length() != 0; + assert code >= 0; + assert code < 2000; + tm.put ( Integer.valueOf ( code ), tag ); + cm.put ( tag, Integer.valueOf ( code ) ); + } + + private static void makeScriptMaps() { + HashMap<Integer,String> tm = new HashMap<Integer,String>(); + HashMap<String,Integer> cm = new HashMap<String,Integer>(); + putScriptTag ( tm, cm, SCRIPT_HEBREW, "hebr" ); + putScriptTag ( tm, cm, SCRIPT_MONGOLIAN, "mong" ); + putScriptTag ( tm, cm, SCRIPT_ARABIC, "arab" ); + putScriptTag ( tm, cm, SCRIPT_GREEK, "grek" ); + putScriptTag ( tm, cm, SCRIPT_LATIN, "latn" ); + putScriptTag ( tm, cm, SCRIPT_CYRILLIC, "cyrl" ); + putScriptTag ( tm, cm, SCRIPT_GEORGIAN, "geor" ); + putScriptTag ( tm, cm, SCRIPT_BOPOMOFO, "bopo" ); + putScriptTag ( tm, cm, SCRIPT_HANGUL, "hang" ); + putScriptTag ( tm, cm, SCRIPT_GURMUKHI, "guru" ); + putScriptTag ( tm, cm, SCRIPT_GURMUKHI_2, "gur2" ); + putScriptTag ( tm, cm, SCRIPT_DEVANAGARI, "deva" ); + putScriptTag ( tm, cm, SCRIPT_DEVANAGARI_2, "dev2" ); + putScriptTag ( tm, cm, SCRIPT_GUJARATI, "gujr" ); + putScriptTag ( tm, cm, SCRIPT_GUJARATI_2, "gjr2" ); + putScriptTag ( tm, cm, SCRIPT_BENGALI, "beng" ); + putScriptTag ( tm, cm, SCRIPT_BENGALI_2, "bng2" ); + putScriptTag ( tm, cm, SCRIPT_ORIYA, "orya" ); + putScriptTag ( tm, cm, SCRIPT_ORIYA_2, "ory2" ); + putScriptTag ( tm, cm, SCRIPT_TIBETAN, "tibt" ); + putScriptTag ( tm, cm, SCRIPT_TELUGU, "telu" ); + putScriptTag ( tm, cm, SCRIPT_TELUGU_2, "tel2" ); + putScriptTag ( tm, cm, SCRIPT_KANNADA, "knda" ); + putScriptTag ( tm, cm, SCRIPT_KANNADA_2, "knd2" ); + putScriptTag ( tm, cm, SCRIPT_TAMIL, "taml" ); + putScriptTag ( tm, cm, SCRIPT_TAMIL_2, "tml2" ); + putScriptTag ( tm, cm, SCRIPT_MALAYALAM, "mlym" ); + putScriptTag ( tm, cm, SCRIPT_MALAYALAM_2, "mlm2" ); + putScriptTag ( tm, cm, SCRIPT_SINHALESE, "sinh" ); + putScriptTag ( tm, cm, SCRIPT_BURMESE, "mymr" ); + putScriptTag ( tm, cm, SCRIPT_THAI, "thai" ); + putScriptTag ( tm, cm, SCRIPT_KHMER, "khmr" ); + putScriptTag ( tm, cm, SCRIPT_LAO, "laoo" ); + putScriptTag ( tm, cm, SCRIPT_HIRAGANA, "hira" ); + putScriptTag ( tm, cm, SCRIPT_ETHIOPIC, "ethi" ); + putScriptTag ( tm, cm, SCRIPT_HAN, "hani" ); + putScriptTag ( tm, cm, SCRIPT_KATAKANA, "kana" ); + putScriptTag ( tm, cm, SCRIPT_MATH, "zmth" ); + putScriptTag ( tm, cm, SCRIPT_SYMBOL, "zsym" ); + putScriptTag ( tm, cm, SCRIPT_UNDETERMINED, "zyyy" ); + putScriptTag ( tm, cm, SCRIPT_UNCODED, "zzzz" ); + scriptTagsMap = tm; + scriptCodeMap = cm; + } + + private static Map<Integer,String> getScriptTagsMap() { + if ( scriptTagsMap == null ) { + makeScriptMaps(); + } + return scriptTagsMap; + } + + private static Map<String,Integer> getScriptCodeMap() { + if ( scriptCodeMap == null ) { + makeScriptMaps(); + } + return scriptCodeMap; + } + +} diff --git a/src/java/org/apache/fop/complexscripts/util/DiscontinuousAssociationException.java b/src/java/org/apache/fop/complexscripts/util/DiscontinuousAssociationException.java new file mode 100644 index 000000000..daade8ca6 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/util/DiscontinuousAssociationException.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.util; + +/** + * Exception thrown during when attempting to map glyphs to associated characters + * in the case that the associated characters do not represent a compact interval. + * @author Glenn Adams + */ +public class DiscontinuousAssociationException extends RuntimeException { + /** + * Instantiate discontinuous association exception + */ + public DiscontinuousAssociationException() { + super(); + } + /** + * Instantiate discontinuous association exception + * @param message a message string + */ + public DiscontinuousAssociationException(String message) { + super(message); + } +} diff --git a/src/java/org/apache/fop/complexscripts/util/GlyphContextTester.java b/src/java/org/apache/fop/complexscripts/util/GlyphContextTester.java new file mode 100644 index 000000000..6bdeb2298 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/util/GlyphContextTester.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.util; + +// CSOFF: LineLengthCheck + +/** + * Interface for testing the originating (source) character context of a glyph sequence. + * @author Glenn Adams + */ +public interface GlyphContextTester { + + /** + * Perform a test on a glyph sequence in a specific (originating) character context. + * @param script governing script + * @param language governing language + * @param feature governing feature + * @param gs glyph sequence to test + * @param index index into glyph sequence to test + * @param flags that apply to lookup in scope + * @return true if test is satisfied + */ + boolean test ( String script, String language, String feature, GlyphSequence gs, int index, int flags ); + +} diff --git a/src/java/org/apache/fop/complexscripts/util/GlyphSequence.java b/src/java/org/apache/fop/complexscripts/util/GlyphSequence.java new file mode 100644 index 000000000..0e256241d --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/util/GlyphSequence.java @@ -0,0 +1,1075 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.util; + +import java.nio.IntBuffer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fop.util.CharUtilities; + +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck +// CSOFF: WhitespaceAfterCheck +// CSOFF: NoWhitespaceAfterCheck + +/** + * A GlyphSequence encapsulates a sequence of character codes, a sequence of glyph codes, + * and a sequence of character associations, where, for each glyph in the sequence of glyph + * codes, there is a corresponding character association. Character associations server to + * relate the glyph codes in a glyph sequence to the specific characters in an original + * character code sequence with which the glyph codes are associated. + * @author Glenn Adams + */ +public class GlyphSequence implements Cloneable { + + /** default character buffer capacity in case new character buffer is created */ + private static final int DEFAULT_CHARS_CAPACITY = 8; + + /** character buffer */ + private IntBuffer characters; + /** glyph buffer */ + private IntBuffer glyphs; + /** association list */ + private List associations; + /** predications flag */ + private boolean predications; + + /** + * Instantiate a glyph sequence, reusing (i.e., not copying) the referenced + * character and glyph buffers and associations. If characters is null, then + * an empty character buffer is created. If glyphs is null, then a glyph buffer + * is created whose capacity is that of the character buffer. If associations is + * null, then identity associations are created. + * @param characters a (possibly null) buffer of associated (originating) characters + * @param glyphs a (possibly null) buffer of glyphs + * @param associations a (possibly null) array of glyph to character associations + * @param predications true if predications are enabled + */ + public GlyphSequence ( IntBuffer characters, IntBuffer glyphs, List associations, boolean predications ) { + if ( characters == null ) { + characters = IntBuffer.allocate ( DEFAULT_CHARS_CAPACITY ); + } + if ( glyphs == null ) { + glyphs = IntBuffer.allocate ( characters.capacity() ); + } + if ( associations == null ) { + associations = makeIdentityAssociations ( characters.limit(), glyphs.limit() ); + } + this.characters = characters; + this.glyphs = glyphs; + this.associations = associations; + this.predications = predications; + } + + /** + * Instantiate a glyph sequence, reusing (i.e., not copying) the referenced + * character and glyph buffers and associations. If characters is null, then + * an empty character buffer is created. If glyphs is null, then a glyph buffer + * is created whose capacity is that of the character buffer. If associations is + * null, then identity associations are created. + * @param characters a (possibly null) buffer of associated (originating) characters + * @param glyphs a (possibly null) buffer of glyphs + * @param associations a (possibly null) array of glyph to character associations + */ + public GlyphSequence ( IntBuffer characters, IntBuffer glyphs, List associations ) { + this ( characters, glyphs, associations, false ); + } + + /** + * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares + * the character array of the existing sequence (but not the buffer object), and creates new copies + * of glyphs buffer and association list. + * @param gs an existing glyph sequence + */ + public GlyphSequence ( GlyphSequence gs ) { + this ( gs.characters.duplicate(), copyBuffer ( gs.glyphs ), copyAssociations ( gs.associations ), gs.predications ); + } + + /** + * Instantiate a glyph sequence using an existing glyph sequence, where the new glyph sequence shares + * the character array of the existing sequence (but not the buffer object), but uses the specified + * backtrack, input, and lookahead glyph arrays to populate the glyphs, and uses the specified + * of glyphs buffer and association list. + * backtrack, input, and lookahead association arrays to populate the associations. + * @param gs an existing glyph sequence + * @param bga backtrack glyph array + * @param iga input glyph array + * @param lga lookahead glyph array + * @param bal backtrack association list + * @param ial input association list + * @param lal lookahead association list + */ + public GlyphSequence ( GlyphSequence gs, int[] bga, int[] iga, int[] lga, CharAssociation[] bal, CharAssociation[] ial, CharAssociation[] lal ) { + this ( gs.characters.duplicate(), concatGlyphs ( bga, iga, lga ), concatAssociations ( bal, ial, lal ), gs.predications ); + } + + /** + * Obtain reference to underlying character buffer. + * @return character buffer reference + */ + public IntBuffer getCharacters() { + return characters; + } + + /** + * Obtain array of characters. If <code>copy</code> is true, then + * a newly instantiated array is returned, otherwise a reference to + * the underlying buffer's array is returned. N.B. in case a reference + * to the undelying buffer's array is returned, the length + * of the array is not necessarily the number of characters in array. + * To determine the number of characters, use {@link #getCharacterCount}. + * @param copy true if to return a newly instantiated array of characters + * @return array of characters + */ + public int[] getCharacterArray ( boolean copy ) { + if ( copy ) { + return toArray ( characters ); + } else { + return characters.array(); + } + } + + /** + * Obtain the number of characters in character array, where + * each character constitutes a unicode scalar value. + * @return number of characters available in character array + */ + public int getCharacterCount() { + return characters.limit(); + } + + /** + * Obtain glyph id at specified index. + * @param index to obtain glyph + * @return the glyph identifier of glyph at specified index + * @throws IndexOutOfBoundsException if index is less than zero + * or exceeds last valid position + */ + public int getGlyph ( int index ) throws IndexOutOfBoundsException { + return glyphs.get ( index ); + } + + /** + * Set glyph id at specified index. + * @param index to set glyph + * @param gi glyph index + * @throws IndexOutOfBoundsException if index is greater or equal to + * the limit of the underlying glyph buffer + */ + public void setGlyph ( int index, int gi ) throws IndexOutOfBoundsException { + if ( gi > 65535 ) { + gi = 65535; + } + glyphs.put ( index, gi ); + } + + /** + * Obtain reference to underlying glyph buffer. + * @return glyph buffer reference + */ + public IntBuffer getGlyphs() { + return glyphs; + } + + /** + * Obtain count glyphs starting at offset. If <code>count</code> is + * negative, then it is treated as if the number of available glyphs + * were specified. + * @param offset into glyph sequence + * @param count of glyphs to obtain starting at offset, or negative, + * indicating all avaialble glyphs starting at offset + * @return glyph array + */ + public int[] getGlyphs ( int offset, int count ) { + int ng = getGlyphCount(); + if ( offset < 0 ) { + offset = 0; + } else if ( offset > ng ) { + offset = ng; + } + if ( count < 0 ) { + count = ng - offset; + } + int[] ga = new int [ count ]; + for ( int i = offset, n = offset + count, k = 0; i < n; i++ ) { + if ( k < ga.length ) { + ga [ k++ ] = glyphs.get ( i ); + } + } + return ga; + } + + /** + * Obtain array of glyphs. If <code>copy</code> is true, then + * a newly instantiated array is returned, otherwise a reference to + * the underlying buffer's array is returned. N.B. in case a reference + * to the undelying buffer's array is returned, the length + * of the array is not necessarily the number of glyphs in array. + * To determine the number of glyphs, use {@link #getGlyphCount}. + * @param copy true if to return a newly instantiated array of glyphs + * @return array of glyphs + */ + public int[] getGlyphArray ( boolean copy ) { + if ( copy ) { + return toArray ( glyphs ); + } else { + return glyphs.array(); + } + } + + /** + * Obtain the number of glyphs in glyphs array, where + * each glyph constitutes a font specific glyph index. + * @return number of glyphs available in character array + */ + public int getGlyphCount() { + return glyphs.limit(); + } + + /** + * Obtain association at specified index. + * @param index into associations array + * @return glyph to character associations at specified index + * @throws IndexOutOfBoundsException if index is less than zero + * or exceeds last valid position + */ + public CharAssociation getAssociation ( int index ) throws IndexOutOfBoundsException { + return (CharAssociation) associations.get ( index ); + } + + /** + * Obtain reference to underlying associations list. + * @return associations list + */ + public List getAssociations() { + return associations; + } + + /** + * Obtain count associations starting at offset. + * @param offset into glyph sequence + * @param count of associations to obtain starting at offset, or negative, + * indicating all avaialble associations starting at offset + * @return associations + */ + public CharAssociation[] getAssociations ( int offset, int count ) { + int ng = getGlyphCount(); + if ( offset < 0 ) { + offset = 0; + } else if ( offset > ng ) { + offset = ng; + } + if ( count < 0 ) { + count = ng - offset; + } + CharAssociation[] aa = new CharAssociation [ count ]; + for ( int i = offset, n = offset + count, k = 0; i < n; i++ ) { + if ( k < aa.length ) { + aa [ k++ ] = (CharAssociation) associations.get ( i ); + } + } + return aa; + } + + /** + * Enable or disable predications. + * @param enable true if predications are to be enabled; otherwise false to disable + */ + public void setPredications ( boolean enable ) { + this.predications = enable; + } + + /** + * Obtain predications state. + * @return true if predications are enabled + */ + public boolean getPredications() { + return this.predications; + } + + /** + * Set predication <KEY,VALUE> at glyph sequence OFFSET. + * @param offset offset (index) into glyph sequence + * @param key predication key + * @param value predication value + */ + public void setPredication ( int offset, String key, Object value ) { + if ( predications ) { + CharAssociation[] aa = getAssociations ( offset, 1 ); + CharAssociation ca = aa[0]; + ca.setPredication ( key, value ); + } + } + + /** + * Get predication KEY at glyph sequence OFFSET. + * @param offset offset (index) into glyph sequence + * @param key predication key + * @return predication KEY at OFFSET or null if none exists + */ + public Object getPredication ( int offset, String key ) { + if ( predications ) { + CharAssociation[] aa = getAssociations ( offset, 1 ); + CharAssociation ca = aa[0]; + return ca.getPredication ( key ); + } else { + return null; + } + } + + /** + * Compare glyphs. + * @param gb buffer containing glyph indices with which this glyph sequence's glyphs are to be compared + * @return zero if glyphs are the same, otherwise returns 1 or -1 according to whether this glyph sequence's + * glyphs are lexicographically greater or lesser than the glyphs in the specified string buffer + */ + public int compareGlyphs ( IntBuffer gb ) { + int ng = getGlyphCount(); + for ( int i = 0, n = gb.limit(); i < n; i++ ) { + if ( i < ng ) { + int g1 = glyphs.get ( i ); + int g2 = gb.get ( i ); + if ( g1 > g2 ) { + return 1; + } else if ( g1 < g2 ) { + return -1; + } + } else { + return -1; // this gb is a proper prefix of specified gb + } + } + return 0; // same lengths with no difference + } + + /** {@inheritDoc} */ + public Object clone() { + try { + GlyphSequence gs = (GlyphSequence) super.clone(); + gs.characters = copyBuffer ( characters ); + gs.glyphs = copyBuffer ( glyphs ); + gs.associations = copyAssociations ( associations ); + return gs; + } catch ( CloneNotSupportedException e ) { + return null; + } + } + + /** {@inheritDoc} */ + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append ( '{' ); + sb.append ( "chars = [" ); + sb.append ( characters ); + sb.append ( "], glyphs = [" ); + sb.append ( glyphs ); + sb.append ( "], associations = [" ); + sb.append ( associations ); + sb.append ( "]" ); + sb.append ( '}' ); + return sb.toString(); + } + + /** + * Determine if two arrays of glyphs are identical. + * @param ga1 first glyph array + * @param ga2 second glyph array + * @return true if arrays are botth null or both non-null and have identical elements + */ + public static boolean sameGlyphs ( int[] ga1, int[] ga2 ) { + if ( ga1 == ga2 ) { + return true; + } else if ( ( ga1 == null ) || ( ga2 == null ) ) { + return false; + } else if ( ga1.length != ga2.length ) { + return false; + } else { + for ( int i = 0, n = ga1.length; i < n; i++ ) { + if ( ga1[i] != ga2[i] ) { + return false; + } + } + return true; + } + } + + /** + * Concatenante glyph arrays. + * @param bga backtrack glyph array + * @param iga input glyph array + * @param lga lookahead glyph array + * @return new integer buffer containing concatenated glyphs + */ + public static IntBuffer concatGlyphs ( int[] bga, int[] iga, int[] lga ) { + int ng = 0; + if ( bga != null ) { + ng += bga.length; + } + if ( iga != null ) { + ng += iga.length; + } + if ( lga != null ) { + ng += lga.length; + } + IntBuffer gb = IntBuffer.allocate ( ng ); + if ( bga != null ) { + gb.put ( bga ); + } + if ( iga != null ) { + gb.put ( iga ); + } + if ( lga != null ) { + gb.put ( lga ); + } + gb.flip(); + return gb; + } + + /** + * Concatenante association arrays. + * @param baa backtrack association array + * @param iaa input association array + * @param laa lookahead association array + * @return new list containing concatenated associations + */ + public static List concatAssociations ( CharAssociation[] baa, CharAssociation[] iaa, CharAssociation[] laa ) { + int na = 0; + if ( baa != null ) { + na += baa.length; + } + if ( iaa != null ) { + na += iaa.length; + } + if ( laa != null ) { + na += laa.length; + } + if ( na > 0 ) { + List gl = new ArrayList ( na ); + if ( baa != null ) { + for ( int i = 0; i < baa.length; i++ ) { + gl.add ( baa[i] ); + } + } + if ( iaa != null ) { + for ( int i = 0; i < iaa.length; i++ ) { + gl.add ( iaa[i] ); + } + } + if ( laa != null ) { + for ( int i = 0; i < laa.length; i++ ) { + gl.add ( laa[i] ); + } + } + return gl; + } else { + return null; + } + } + + /** + * Join (concatenate) glyph sequences. + * @param gs original glyph sequence from which to reuse character array reference + * @param sa array of glyph sequences, whose glyph arrays and association lists are to be concatenated + * @return new glyph sequence referring to character array of GS and concatenated glyphs and associations of SA + */ + public static GlyphSequence join ( GlyphSequence gs, GlyphSequence[] sa ) { + assert sa != null; + int tg = 0; + int ta = 0; + for ( int i = 0, n = sa.length; i < n; i++ ) { + GlyphSequence s = sa [ i ]; + IntBuffer ga = s.getGlyphs(); + assert ga != null; + int ng = ga.limit(); + List al = s.getAssociations(); + assert al != null; + int na = al.size(); + assert na == ng; + tg += ng; + ta += na; + } + IntBuffer uga = IntBuffer.allocate ( tg ); + ArrayList ual = new ArrayList ( ta ); + for ( int i = 0, n = sa.length; i < n; i++ ) { + GlyphSequence s = sa [ i ]; + uga.put ( s.getGlyphs() ); + ual.addAll ( s.getAssociations() ); + } + return new GlyphSequence ( gs.getCharacters(), uga, ual, gs.getPredications() ); + } + + /** + * Reorder sequence such that [SOURCE,SOURCE+COUNT) is moved just prior to TARGET. + * @param gs input sequence + * @param source index of sub-sequence to reorder + * @param count length of sub-sequence to reorder + * @param target index to which source sub-sequence is to be moved + * @return reordered sequence (or original if no reordering performed) + */ + public static GlyphSequence reorder ( GlyphSequence gs, int source, int count, int target ) { + if ( source != target ) { + int ng = gs.getGlyphCount(); + int[] ga = gs.getGlyphArray ( false ); + int[] nga = new int [ ng ]; + GlyphSequence.CharAssociation[] aa = gs.getAssociations ( 0, ng ); + GlyphSequence.CharAssociation[] naa = new GlyphSequence.CharAssociation [ ng ]; + if ( source < target ) { + int t = 0; + for ( int s = 0, e = source; s < e; s++, t++ ) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for ( int s = source + count, e = target; s < e; s++, t++ ) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for ( int s = source, e = source + count; s < e; s++, t++ ) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for ( int s = target, e = ng; s < e; s++, t++ ) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + } else { + int t = 0; + for ( int s = 0, e = target; s < e; s++, t++ ) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for ( int s = source, e = source + count; s < e; s++, t++ ) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for ( int s = target, e = source; s < e; s++, t++ ) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + for ( int s = source + count, e = ng; s < e; s++, t++ ) { + nga[t] = ga[s]; + naa[t] = aa[s]; + } + } + return new GlyphSequence ( gs, null, nga, null, null, naa, null ); + } else { + return gs; + } + } + + private static int[] toArray ( IntBuffer ib ) { + if ( ib != null ) { + int n = ib.limit(); + int[] ia = new int[n]; + ib.get ( ia, 0, n ); + return ia; + } else { + return new int[0]; + } + } + + private static List makeIdentityAssociations ( int numChars, int numGlyphs ) { + int nc = numChars; + int ng = numGlyphs; + List av = new ArrayList ( ng ); + for ( int i = 0, n = ng; i < n; i++ ) { + int k = ( i > nc ) ? nc : i; + av.add ( new CharAssociation ( i, ( k == nc ) ? 0 : 1 ) ); + } + return av; + } + + private static IntBuffer copyBuffer ( IntBuffer ib ) { + if ( ib != null ) { + int[] ia = new int [ ib.capacity() ]; + int p = ib.position(); + int l = ib.limit(); + System.arraycopy ( ib.array(), 0, ia, 0, ia.length ); + return IntBuffer.wrap ( ia, p, l - p ); + } else { + return null; + } + } + + private static List copyAssociations ( List ca ) { + if ( ca != null ) { + return new ArrayList ( ca ); + } else { + return ca; + } + } + + /** + * A structure class encapsulating an interval of characters + * expressed as an offset and count of Unicode scalar values (in + * an IntBuffer). A <code>CharAssociation</code> is used to + * maintain a backpointer from a glyph to one or more character + * intervals from which the glyph was derived. + * + * Each glyph in a glyph sequence is associated with a single + * <code>CharAssociation</code> instance. + * + * A <code>CharAssociation</code> instance is additionally (and + * optionally) used to record predication information about the + * glyph, such as whether the glyph was produced by the + * application of a specific substitution table or whether its + * position was adjusted by a specific poisitioning table. + */ + public static class CharAssociation implements Cloneable { + + // instance state + private final int offset; + private final int count; + private final int[] subIntervals; + private Map<String,Object> predications; + + // class state + private static volatile Map<String,PredicationMerger> predicationMergers; + + interface PredicationMerger { + Object merge ( String key, Object v1, Object v2 ); + } + + /** + * Instantiate a character association. + * @param offset into array of Unicode scalar values (in associated IntBuffer) + * @param count of Unicode scalar values (in associated IntBuffer) + * @param subIntervals if disjoint, then array of sub-intervals, otherwise null; even + * members of array are sub-interval starts, and odd members are sub-interval + * ends (exclusive) + */ + public CharAssociation ( int offset, int count, int[] subIntervals ) { + this.offset = offset; + this.count = count; + this.subIntervals = ( ( subIntervals != null ) && ( subIntervals.length > 2 ) ) ? subIntervals : null; + } + + /** + * Instantiate a non-disjoint character association. + * @param offset into array of UTF-16 code elements (in associated CharSequence) + * @param count of UTF-16 character code elements (in associated CharSequence) + */ + public CharAssociation ( int offset, int count ) { + this ( offset, count, null ); + } + + /** + * Instantiate a non-disjoint character association. + * @param subIntervals if disjoint, then array of sub-intervals, otherwise null; even + * members of array are sub-interval starts, and odd members are sub-interval + * ends (exclusive) + */ + public CharAssociation ( int[] subIntervals ) { + this ( getSubIntervalsStart ( subIntervals ), getSubIntervalsLength ( subIntervals ), subIntervals ); + } + + /** @return offset (start of association interval) */ + public int getOffset() { + return offset; + } + + /** @return count (number of characer codes in association) */ + public int getCount() { + return count; + } + + /** @return start of association interval */ + public int getStart() { + return getOffset(); + } + + /** @return end of association interval */ + public int getEnd() { + return getOffset() + getCount(); + } + + /** @return true if association is disjoint */ + public boolean isDisjoint() { + return subIntervals != null; + } + + /** @return subintervals of disjoint association */ + public int[] getSubIntervals() { + return subIntervals; + } + + /** @return count of subintervals of disjoint association */ + public int getSubIntervalCount() { + return ( subIntervals != null ) ? ( subIntervals.length / 2 ) : 0; + } + + /** + * @param offset of interval in sequence + * @param count length of interval + * @return true if this association is contained within [offset,offset+count) + */ + public boolean contained ( int offset, int count ) { + int s = offset; + int e = offset + count; + if ( ! isDisjoint() ) { + int s0 = getStart(); + int e0 = getEnd(); + return ( s0 >= s ) && ( e0 <= e ); + } else { + int ns = getSubIntervalCount(); + for ( int i = 0; i < ns; i++ ) { + int s0 = subIntervals [ 2 * i + 0 ]; + int e0 = subIntervals [ 2 * i + 1 ]; + if ( ( s0 >= s ) && ( e0 <= e ) ) { + return true; + } + } + return false; + } + } + + /** + * Set predication <KEY,VALUE>. + * @param key predication key + * @param value predication value + */ + public void setPredication ( String key, Object value ) { + if ( predications == null ) { + predications = new HashMap<String,Object>(); + } + if ( predications != null ) { + predications.put ( key, value ); + } + } + + /** + * Get predication KEY. + * @param key predication key + * @return predication KEY at OFFSET or null if none exists + */ + public Object getPredication ( String key ) { + if ( predications != null ) { + return predications.get ( key ); + } else { + return null; + } + } + + /** + * Merge predication <KEY,VALUE>. + * @param key predication key + * @param value predication value + */ + public void mergePredication ( String key, Object value ) { + if ( predications == null ) { + predications = new HashMap<String,Object>(); + } + if ( predications != null ) { + if ( predications.containsKey ( key ) ) { + Object v1 = predications.get ( key ); + Object v2 = value; + predications.put ( key, mergePredicationValues ( key, v1, v2 ) ); + } else { + predications.put ( key, value ); + } + } + } + + /** + * Merge predication values V1 and V2 on KEY. Uses registered <code>PredicationMerger</code> + * if one exists, otherwise uses V2 if non-null, otherwise uses V1. + * @param key predication key + * @param v1 first (original) predication value + * @param v2 second (to be merged) predication value + * @return merged value + */ + public static Object mergePredicationValues ( String key, Object v1, Object v2 ) { + PredicationMerger pm = getPredicationMerger ( key ); + if ( pm != null ) { + return pm.merge ( key, v1, v2 ); + } else if ( v2 != null ) { + return v2; + } else { + return v1; + } + } + + /** + * Merge predications from another CA. + * @param ca from which to merge + */ + public void mergePredications ( CharAssociation ca ) { + if ( ca.predications != null ) { + for ( Map.Entry<String,Object> e : ca.predications.entrySet() ) { + mergePredication ( e.getKey(), e.getValue() ); + } + } + } + + /** {@inheritDoc} */ + public Object clone() { + try { + CharAssociation ca = (CharAssociation) super.clone(); + if ( predications != null ) { + ca.predications = new HashMap<String,Object> ( predications ); + } + return ca; + } catch ( CloneNotSupportedException e ) { + return null; + } + } + + /** + * Register predication merger PM for KEY. + * @param key for predication merger + * @param pm predication merger + */ + public static void setPredicationMerger ( String key, PredicationMerger pm ) { + if ( predicationMergers == null ) { + predicationMergers = new HashMap<String,PredicationMerger>(); + } + if ( predicationMergers != null ) { + predicationMergers.put ( key, pm ); + } + } + + /** + * Obtain predication merger for KEY. + * @param key for predication merger + * @return predication merger or null if none exists + */ + public static PredicationMerger getPredicationMerger ( String key ) { + if ( predicationMergers != null ) { + return predicationMergers.get ( key ); + } else { + return null; + } + } + + /** + * Replicate association to form <code>repeat</code> new associations. + * @param a association to replicate + * @param repeat count + * @return array of replicated associations + */ + public static CharAssociation[] replicate ( CharAssociation a, int repeat ) { + CharAssociation[] aa = new CharAssociation [ repeat ]; + for ( int i = 0, n = aa.length; i < n; i++ ) { + aa [ i ] = (CharAssociation) a.clone(); + } + return aa; + } + + /** + * Join (merge) multiple associations into a single, potentially disjoint + * association. + * @param aa array of associations to join + * @return (possibly disjoint) association containing joined associations + */ + public static CharAssociation join ( CharAssociation[] aa ) { + CharAssociation ca; + // extract sorted intervals + int[] ia = extractIntervals ( aa ); + if ( ( ia == null ) || ( ia.length == 0 ) ) { + ca = new CharAssociation ( 0, 0 ); + } else if ( ia.length == 2 ) { + int s = ia[0]; + int e = ia[1]; + ca = new CharAssociation ( s, e - s ); + } else { + ca = new CharAssociation ( mergeIntervals ( ia ) ); + } + return mergePredicates ( ca, aa ); + } + + private static CharAssociation mergePredicates ( CharAssociation ca, CharAssociation[] aa ) { + for ( CharAssociation a : aa ) { + ca.mergePredications ( a ); + } + return ca; + } + + private static int getSubIntervalsStart ( int[] ia ) { + int us = Integer.MAX_VALUE; + int ue = Integer.MIN_VALUE; + if ( ia != null ) { + for ( int i = 0, n = ia.length; i < n; i += 2 ) { + int s = ia [ i + 0 ]; + int e = ia [ i + 1 ]; + if ( s < us ) { + us = s; + } + if ( e > ue ) { + ue = e; + } + } + if ( ue < 0 ) { + ue = 0; + } + if ( us > ue ) { + us = ue; + } + } + return us; + } + + private static int getSubIntervalsLength ( int[] ia ) { + int us = Integer.MAX_VALUE; + int ue = Integer.MIN_VALUE; + if ( ia != null ) { + for ( int i = 0, n = ia.length; i < n; i += 2 ) { + int s = ia [ i + 0 ]; + int e = ia [ i + 1 ]; + if ( s < us ) { + us = s; + } + if ( e > ue ) { + ue = e; + } + } + if ( ue < 0 ) { + ue = 0; + } + if ( us > ue ) { + us = ue; + } + } + return ue - us; + } + + /** + * Extract sorted sub-intervals. + */ + private static int[] extractIntervals ( CharAssociation[] aa ) { + int ni = 0; + for ( int i = 0, n = aa.length; i < n; i++ ) { + CharAssociation a = aa [ i ]; + if ( a.isDisjoint() ) { + ni += a.getSubIntervalCount(); + } else { + ni += 1; + } + } + int[] sa = new int [ ni ]; + int[] ea = new int [ ni ]; + for ( int i = 0, k = 0; i < aa.length; i++ ) { + CharAssociation a = aa [ i ]; + if ( a.isDisjoint() ) { + int[] da = a.getSubIntervals(); + for ( int j = 0; j < da.length; j += 2 ) { + sa [ k ] = da [ j + 0 ]; + ea [ k ] = da [ j + 1 ]; + k++; + } + } else { + sa [ k ] = a.getStart(); + ea [ k ] = a.getEnd(); + k++; + } + } + return sortIntervals ( sa, ea ); + } + + private static final int[] sortIncrements16 // CSOK: ConstantNameCheck + = { 1391376, 463792, 198768, 86961, 33936, 13776, 4592, 1968, 861, 336, 112, 48, 21, 7, 3, 1 }; + + private static final int[] sortIncrements03 // CSOK: ConstantNameCheck + = { 7, 3, 1 }; + + /** + * Sort sub-intervals using modified Shell Sort. + */ + private static int[] sortIntervals ( int[] sa, int[] ea ) { + assert sa != null; + assert ea != null; + assert sa.length == ea.length; + int ni = sa.length; + int[] incr = ( ni < 21 ) ? sortIncrements03 : sortIncrements16; + for ( int k = 0; k < incr.length; k++ ) { + for ( int h = incr [ k ], i = h, n = ni, j; i < n; i++ ) { + int s1 = sa [ i ]; + int e1 = ea [ i ]; + for ( j = i; j >= h; j -= h) { + int s2 = sa [ j - h ]; + int e2 = ea [ j - h ]; + if ( s2 > s1 ) { + sa [ j ] = s2; + ea [ j ] = e2; + } else if ( ( s2 == s1 ) && ( e2 > e1 ) ) { + sa [ j ] = s2; + ea [ j ] = e2; + } else { + break; + } + } + sa [ j ] = s1; + ea [ j ] = e1; + } + } + int[] ia = new int [ ni * 2 ]; + for ( int i = 0; i < ni; i++ ) { + ia [ ( i * 2 ) + 0 ] = sa [ i ]; + ia [ ( i * 2 ) + 1 ] = ea [ i ]; + } + return ia; + } + + /** + * Merge overlapping and abutting sub-intervals. + */ + private static int[] mergeIntervals ( int[] ia ) { + int ni = ia.length; + int i, n, nm, is, ie; + // count merged sub-intervals + for ( i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2 ) { + int s = ia [ i + 0 ]; + int e = ia [ i + 1 ]; + if ( ( ie < 0 ) || ( s > ie ) ) { + is = s; + ie = e; + nm++; + } else if ( s >= is ) { + if ( e > ie ) { + ie = e; + } + } + } + int[] mi = new int [ nm * 2 ]; + // populate merged sub-intervals + for ( i = 0, n = ni, nm = 0, is = ie = -1; i < n; i += 2 ) { + int s = ia [ i + 0 ]; + int e = ia [ i + 1 ]; + int k = nm * 2; + if ( ( ie < 0 ) || ( s > ie ) ) { + is = s; + ie = e; + mi [ k + 0 ] = is; + mi [ k + 1 ] = ie; + nm++; + } else if ( s >= is ) { + if ( e > ie ) { + ie = e; + } + mi [ k - 1 ] = ie; + } + } + return mi; + } + + } + +} diff --git a/src/java/org/apache/fop/complexscripts/util/GlyphTester.java b/src/java/org/apache/fop/complexscripts/util/GlyphTester.java new file mode 100644 index 000000000..48d0444a0 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/util/GlyphTester.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.util; + +/** + * Interface for testing glyph properties according to glyph identifier. + * @author Glenn Adams + */ +public interface GlyphTester { + + /** + * Perform a test on a glyph identifier. + * @param gi glyph identififer + * @param flags that apply to lookup in scope + * @return true if test is satisfied + */ + boolean test ( int gi, int flags ); + +} diff --git a/src/java/org/apache/fop/complexscripts/util/NumberConverter.java b/src/java/org/apache/fop/complexscripts/util/NumberConverter.java new file mode 100644 index 000000000..6d9831249 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/util/NumberConverter.java @@ -0,0 +1,1616 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.util; + +import java.util.ArrayList; +import java.util.List; + +// CSOFF: LineLengthCheck +// CSOFF: InnerAssignmentCheck +// CSOFF: NoWhitespaceAfterCheck +// CSOFF: AvoidNestedBlocksCheck + +/** + * Implementation of Number to String Conversion algorithm specified by + * XSL Transformations (XSLT) Version 2.0, W3C Recommendation, 23 January 2007. + * + * This algorithm differs from that specified in XSLT 1.0 in the following + * ways: + * <ul> + * <li>input numbers are greater than or equal to zero rather than greater than zero;</li> + * <li>introduces format tokens { w, W, Ww };</li> + * <li>introduces ordinal parameter to generate ordinal numbers;</li> + * </ul> + * + * Implementation Defaults and Limitations + * <ul> + * <li>If language parameter is unspecified (null or empty string), then the value + * of DEFAULT_LANGUAGE is used, which is defined below as "eng" (English).</li> + * <li>Only English, French, and Spanish word numerals are supported, and only if less than one trillion (1,000,000,000,000).</li> + * <li>Ordinal word numerals are supported for French and Spanish only when less than or equal to ten (10).</li> + * </ul> + * + * Implementation Notes + * <ul> + * <li>In order to handle format tokens outside the Unicode BMP, all processing is + * done in Unicode Scalar Values represented with Integer and Integer[] + * types. Without affecting behavior, this may be subsequently optimized to + * use int and int[] types.</li> + * <li>In order to communicate various sub-parameters, including ordinalization, a <em>features</em> + * is employed, which consists of comma separated name and optional value tokens, where name and value + * are separated by an equals '=' sign.</li> + * <li>Ordinal numbers are selected by specifying a word based format token in combination with a 'ordinal' feature with no value, in which case + * the features 'male' and 'female' may be used to specify gender for gender sensitive languages. For example, the feature string "ordinal,female" + * selects female ordinals.</li> + * </ul> + * + * @author Glenn Adams + */ +public class NumberConverter { + + /** alphabetical */ + public static final int LETTER_VALUE_ALPHABETIC = 1; + /** traditional */ + public static final int LETTER_VALUE_TRADITIONAL = 2; + + /** no token type */ + private static final int TOKEN_NONE = 0; + /** alhphanumeric token type */ + private static final int TOKEN_ALPHANUMERIC = 1; + /** nonalphanumeric token type */ + private static final int TOKEN_NONALPHANUMERIC = 2; + /** default token */ + private static final Integer[] DEFAULT_TOKEN = new Integer[] { (int) '1' }; + /** default separator */ + private static final Integer[] DEFAULT_SEPARATOR = new Integer[] { (int) '.' }; + /** default language */ + private static final String DEFAULT_LANGUAGE = "eng"; + + /** prefix token */ + private Integer[] prefix; + /** suffix token */ + private Integer[] suffix; + /** sequence of tokens, as parsed from format */ + private Integer[][] tokens; + /** sequence of separators, as parsed from format */ + private Integer[][] separators; + /** grouping separator */ + private int groupingSeparator; + /** grouping size */ + private int groupingSize; + /** letter value */ + private int letterValue; + /** letter value system */ + private String features; + /** language */ + private String language; + /** country */ + private String country; + + /** + * Construct parameterized number converter. + * @param format format for the page number (may be null or empty, which is treated as null) + * @param groupingSeparator grouping separator (if zero, then no grouping separator applies) + * @param groupingSize grouping size (if zero or negative, then no grouping size applies) + * @param letterValue letter value (must be one of the above letter value enumeration values) + * @param features features (feature sub-parameters) + * @param language (may be null or empty, which is treated as null) + * @param country (may be null or empty, which is treated as null) + * @throws IllegalArgumentException if format is not a valid UTF-16 string (e.g., has unpaired surrogate) + */ + public NumberConverter ( String format, int groupingSeparator, int groupingSize, int letterValue, String features, String language, String country ) + throws IllegalArgumentException { + this.groupingSeparator = groupingSeparator; + this.groupingSize = groupingSize; + this.letterValue = letterValue; + this.features = features; + this.language = ( language != null ) ? language.toLowerCase() : null; + this.country = ( country != null ) ? country.toLowerCase() : null; + parseFormatTokens ( format ); + } + + /** + * Convert a number to string according to conversion parameters. + * @param number number to conver + * @return string representing converted number + */ + public String convert ( long number ) { + List<Long> numbers = new ArrayList<Long>(); + numbers.add ( number ); + return convert ( numbers ); + } + + /** + * Convert list of numbers to string according to conversion parameters. + * @param numbers list of numbers to convert + * @return string representing converted list of numbers + */ + public String convert ( List<Long> numbers ) { + List<Integer> scalars = new ArrayList<Integer>(); + if ( prefix != null ) { + appendScalars ( scalars, prefix ); + } + convertNumbers ( scalars, numbers ); + if ( suffix != null ) { + appendScalars ( scalars, suffix ); + } + return scalarsToString ( scalars ); + } + + private void parseFormatTokens ( String format ) throws IllegalArgumentException { + List<Integer[]> tokens = new ArrayList<Integer[]>(); + List<Integer[]> separators = new ArrayList<Integer[]>(); + if ( ( format == null ) || ( format.length() == 0 ) ) { + format = "1"; + } + int tokenType = TOKEN_NONE; + List<Integer> token = new ArrayList<Integer>(); + Integer[] ca = UTF32.toUTF32 ( format, 0, true ); + for ( int i = 0, n = ca.length; i < n; i++ ) { + int c = ca[i]; + int tokenTypeNew = isAlphaNumeric ( c ) ? TOKEN_ALPHANUMERIC : TOKEN_NONALPHANUMERIC; + if ( tokenTypeNew != tokenType ) { + if ( token.size() > 0 ) { + if ( tokenType == TOKEN_ALPHANUMERIC ) { + tokens.add ( token.toArray ( new Integer [ token.size() ] ) ); + } else { + separators.add ( token.toArray ( new Integer [ token.size() ] ) ); + } + token.clear(); + } + tokenType = tokenTypeNew; + } + token.add ( c ); + } + if ( token.size() > 0 ) { + if ( tokenType == TOKEN_ALPHANUMERIC ) { + tokens.add ( token.toArray ( new Integer [ token.size() ] ) ); + } else { + separators.add ( token.toArray ( new Integer [ token.size() ] ) ); + } + } + if ( ! separators.isEmpty() ) { + this.prefix = separators.remove ( 0 ); + } + if ( ! separators.isEmpty() ) { + this.suffix = separators.remove ( separators.size() - 1 ); + } + this.separators = separators.toArray ( new Integer [ separators.size() ] [] ); + this.tokens = tokens.toArray ( new Integer [ tokens.size() ] [] ); + } + + private static boolean isAlphaNumeric ( int c ) { + switch ( Character.getType ( c ) ) { + case Character.DECIMAL_DIGIT_NUMBER: // Nd + case Character.LETTER_NUMBER: // Nl + case Character.OTHER_NUMBER: // No + case Character.UPPERCASE_LETTER: // Lu + case Character.LOWERCASE_LETTER: // Ll + case Character.TITLECASE_LETTER: // Lt + case Character.MODIFIER_LETTER: // Lm + case Character.OTHER_LETTER: // Lo + return true; + default: + return false; + } + } + + private void convertNumbers ( List<Integer> scalars, List<Long> numbers ) { + Integer[] tknLast = DEFAULT_TOKEN; + int tknIndex = 0; + int tknCount = tokens.length; + int sepIndex = 0; + int sepCount = separators.length; + int numIndex = 0; + for ( Long number : numbers ) { + Integer[] sep = null; + Integer[] tkn; + if ( tknIndex < tknCount ) { + if ( numIndex > 0 ) { + if ( sepIndex < sepCount ) { + sep = separators [ sepIndex++ ]; + } else { + sep = DEFAULT_SEPARATOR; + } + } + tkn = tokens [ tknIndex++ ]; + } else { + tkn = tknLast; + } + appendScalars ( scalars, convertNumber ( number, sep, tkn ) ); + tknLast = tkn; + numIndex++; + } + } + + private Integer[] convertNumber ( long number, Integer[] separator, Integer[] token ) { + List<Integer> sl = new ArrayList<Integer>(); + if ( separator != null ) { + appendScalars ( sl, separator ); + } + if ( token != null ) { + appendScalars ( sl, formatNumber ( number, token ) ); + } + return sl.toArray ( new Integer [ sl.size() ] ); + } + + private Integer[] formatNumber ( long number, Integer[] token ) { + Integer[] fn = null; + assert token.length > 0; + if ( number < 0 ) { + throw new IllegalArgumentException ( "number must be non-negative" ); + } else if ( token.length == 1 ) { + int s = token[0].intValue(); + switch ( s ) { + case (int) '1': + { + fn = formatNumberAsDecimal ( number, (int) '1', 1 ); + break; + } + case (int) 'W': + case (int) 'w': + { + fn = formatNumberAsWord ( number, ( s == (int) 'W' ) ? Character.UPPERCASE_LETTER : Character.LOWERCASE_LETTER ); + break; + } + case (int) 'A': // handled as numeric sequence + case (int) 'a': // handled as numeric sequence + case (int) 'I': // handled as numeric special + case (int) 'i': // handled as numeric special + default: + { + if ( isStartOfDecimalSequence ( s ) ) { + fn = formatNumberAsDecimal ( number, s, 1 ); + } else if ( isStartOfAlphabeticSequence ( s ) ) { + fn = formatNumberAsSequence ( number, s, getSequenceBase ( s ), null ); + } else if ( isStartOfNumericSpecial ( s ) ) { + fn = formatNumberAsSpecial ( number, s ); + } else { + fn = null; + } + break; + } + } + } else if ( ( token.length == 2 ) && ( token[0] == (int) 'W' ) && ( token[1] == (int) 'w' ) ) { + fn = formatNumberAsWord ( number, Character.TITLECASE_LETTER ); + } else if ( isPaddedOne ( token ) ) { + int s = token [ token.length - 1 ].intValue(); + fn = formatNumberAsDecimal ( number, s, token.length ); + } else { + throw new IllegalArgumentException ( "invalid format token: \"" + UTF32.fromUTF32 ( token ) + "\"" ); + } + if ( fn == null ) { + fn = formatNumber ( number, DEFAULT_TOKEN ); + } + assert fn != null; + return fn; + } + + /** + * Format NUMBER as decimal using characters denoting digits that start at ONE, + * adding one or more (zero) padding characters as needed to fill out field WIDTH. + * @param number to be formatted + * @param one unicode scalar value denoting numeric value 1 + * @param width non-negative integer denoting field width of number, possible including padding + * @return formatted number as array of unicode scalars + */ + private Integer[] formatNumberAsDecimal ( long number, int one, int width ) { + assert Character.getNumericValue ( one ) == 1; + assert Character.getNumericValue ( one - 1 ) == 0; + assert Character.getNumericValue ( one + 8 ) == 9; + List<Integer> sl = new ArrayList<Integer>(); + int zero = one - 1; + while ( number > 0 ) { + long digit = number % 10; + sl.add ( 0, zero + (int) digit ); + number = number / 10; + } + while ( width > sl.size() ) { + sl.add ( 0, zero ); + } + if ( ( groupingSize != 0 ) && ( groupingSeparator != 0 ) ) { + sl = performGrouping ( sl, groupingSize, groupingSeparator ); + } + return sl.toArray ( new Integer [ sl.size() ] ); + } + + private static List<Integer> performGrouping ( List<Integer> sl, int groupingSize, int groupingSeparator ) { + assert groupingSize > 0; + assert groupingSeparator != 0; + if ( sl.size() > groupingSize ) { + List<Integer> gl = new ArrayList<Integer>(); + for ( int i = 0, n = sl.size(), g = 0; i < n; i++ ) { + int k = n - i - 1; + if ( g == groupingSize ) { + gl.add ( 0, groupingSeparator ); + g = 1; + } else { + g++; + } + gl.add ( 0, sl.get ( k ) ); + } + return gl; + } else { + return sl; + } + } + + + /** + * Format NUMBER as using sequence of characters that start at ONE, and + * having BASE radix. + * @param number to be formatted + * @param one unicode scalar value denoting start of sequence (numeric value 1) + * @param base number of elements in sequence + * @param map if non-null, then maps sequences indices to unicode scalars + * @return formatted number as array of unicode scalars + */ + private Integer[] formatNumberAsSequence ( long number, int one, int base, int[] map ) { + assert base > 1; + assert ( map == null ) || ( map.length >= base ); + List<Integer> sl = new ArrayList<Integer>(); + if ( number == 0 ) { + return null; + } else { + long n = number; + while ( n > 0 ) { + int d = (int) ( ( n - 1 ) % (long) base ); + int s = ( map != null ) ? map [ d ] : ( one + d ); + sl.add ( 0, s ); + n = ( n - 1 ) / base; + } + return sl.toArray ( new Integer [ sl.size() ] ); + } + } + + /** + * Format NUMBER as using special system that starts at ONE. + * @param number to be formatted + * @param one unicode scalar value denoting start of system (numeric value 1) + * @return formatted number as array of unicode scalars + */ + private Integer[] formatNumberAsSpecial ( long number, int one ) { + SpecialNumberFormatter f = getSpecialFormatter ( one, letterValue, features, language, country ); + if ( f != null ) { + return f.format ( number, one, letterValue, features, language, country ); + } else { + return null; + } + } + + /** + * Format NUMBER as word according to TYPE, which must be either + * Character.UPPERCASE_LETTER, Character.LOWERCASE_LETTER, or + * Character.TITLECASE_LETTER. Makes use of this.language to + * determine language of word. + * @param number to be formatted + * @param caseType unicode character type for case conversion + * @return formatted number as array of unicode scalars + */ + private Integer[] formatNumberAsWord ( long number, int caseType ) { + SpecialNumberFormatter f = null; + if ( isLanguage ( "eng" ) ) { + f = new EnglishNumberAsWordFormatter ( caseType ); + } else if ( isLanguage ( "spa" ) ) { + f = new SpanishNumberAsWordFormatter ( caseType ); + } else if ( isLanguage ( "fra" ) ) { + f = new FrenchNumberAsWordFormatter ( caseType ); + } else { + f = new EnglishNumberAsWordFormatter ( caseType ); + } + return f.format ( number, 0, letterValue, features, language, country ); + } + + private boolean isLanguage ( String iso3Code ) { + if ( language == null ) { + return false; + } else if ( language.equals ( iso3Code ) ) { + return true; + } else { + return isSameLanguage ( iso3Code, language ); + } + } + + private static String[][] equivalentLanguages = { + { "eng", "en" }, + { "fra", "fre", "fr" }, + { "spa", "es" }, + }; + + private static boolean isSameLanguage ( String i3c, String lc ) { + for ( String[] el : equivalentLanguages ) { + assert el.length >= 2; + if ( el[0].equals ( i3c ) ) { + for ( int i = 0, n = el.length; i < n; i++ ) { + if ( el[i].equals ( lc ) ) { + return true; + } + } + return false; + } + } + return false; + } + + private static boolean hasFeature ( String features, String feature ) { + if ( features != null ) { + assert feature != null; + assert feature.length() != 0; + String[] fa = features.split(","); + for ( String f : fa ) { + String[] fp = f.split("="); + assert fp.length > 0; + String fn = fp[0]; + String fv = ( fp.length > 1 ) ? fp[1] : ""; + if ( fn.equals ( feature ) ) { + return true; + } + } + } + return false; + } + + /* not yet used + private static String getFeatureValue ( String features, String feature ) { + if ( features != null ) { + assert feature != null; + assert feature.length() != 0; + String[] fa = features.split(","); + for ( String f : fa ) { + String[] fp = f.split("="); + assert fp.length > 0; + String fn = fp[0]; + String fv = ( fp.length > 1 ) ? fp[1] : ""; + if ( fn.equals ( feature ) ) { + return fv; + } + } + } + return ""; + } + */ + + private static void appendScalars ( List<Integer> scalars, Integer[] sa ) { + for ( Integer s : sa ) { + scalars.add ( s ); + } + } + + private static String scalarsToString ( List<Integer> scalars ) { + Integer[] sa = scalars.toArray ( new Integer [ scalars.size() ] ); + return UTF32.fromUTF32 ( sa ); + } + + private static boolean isPaddedOne ( Integer[] token ) { + if ( getDecimalValue ( token [ token.length - 1 ] ) != 1 ) { + return false; + } else { + for ( int i = 0, n = token.length - 1; i < n; i++ ) { + if ( getDecimalValue ( token [ i ] ) != 0 ) { + return false; + } + } + return true; + } + } + + private static int getDecimalValue ( Integer scalar ) { + int s = scalar.intValue(); + if ( Character.getType ( s ) == Character.DECIMAL_DIGIT_NUMBER ) { + return Character.getNumericValue ( s ); + } else { + return -1; + } + } + + private static boolean isStartOfDecimalSequence ( int s ) { + return ( Character.getNumericValue ( s ) == 1 ) + && ( Character.getNumericValue ( s - 1 ) == 0 ) + && ( Character.getNumericValue ( s + 8 ) == 9 ); + } + + private static int[][] supportedAlphabeticSequences = { + { 'A', 26 }, // A...Z + { 'a', 26 }, // a...z + }; + + private static boolean isStartOfAlphabeticSequence ( int s ) { + for ( int[] ss : supportedAlphabeticSequences ) { + assert ss.length >= 2; + if ( ss[0] == s ) { + return true; + } + } + return false; + } + + private static int getSequenceBase ( int s ) { + for ( int[] ss : supportedAlphabeticSequences ) { + assert ss.length >= 2; + if ( ss[0] == s ) { + return ss[1]; + } + } + return 0; + } + + private static int[][] supportedSpecials = { + { 'I' }, // latin - uppercase roman numerals + { 'i' }, // latin - lowercase roman numerals + { '\u0391' }, // greek - uppercase isopsephry numerals + { '\u03B1' }, // greek - lowercase isopsephry numerals + { '\u05D0' }, // hebrew - gematria numerals + { '\u0623' }, // arabic - abjadi numberals + { '\u0627' }, // arabic - either abjadi or hijai alphabetic sequence + { '\u0E01' }, // thai - default alphabetic sequence + { '\u3042' }, // kana - hiragana (gojuon) - default alphabetic sequence + { '\u3044' }, // kana - hiragana (iroha) + { '\u30A2' }, // kana - katakana (gojuon) - default alphabetic sequence + { '\u30A4' }, // kana - katakana (iroha) + }; + + private static boolean isStartOfNumericSpecial ( int s ) { + for ( int[] ss : supportedSpecials ) { + assert ss.length >= 1; + if ( ss[0] == s ) { + return true; + } + } + return false; + } + + private SpecialNumberFormatter getSpecialFormatter ( int one, int letterValue, String features, String language, String country ) { + if ( one == (int) 'I' ) { + return new RomanNumeralsFormatter(); + } else if ( one == (int) 'i' ) { + return new RomanNumeralsFormatter(); + } else if ( one == (int) '\u0391' ) { + return new IsopsephryNumeralsFormatter(); + } else if ( one == (int) '\u03B1' ) { + return new IsopsephryNumeralsFormatter(); + } else if ( one == (int) '\u05D0' ) { + return new GematriaNumeralsFormatter(); + } else if ( one == (int) '\u0623' ) { + return new ArabicNumeralsFormatter(); + } else if ( one == (int) '\u0627' ) { + return new ArabicNumeralsFormatter(); + } else if ( one == (int) '\u0E01' ) { + return new ThaiNumeralsFormatter(); + } else if ( one == (int) '\u3042' ) { + return new KanaNumeralsFormatter(); + } else if ( one == (int) '\u3044' ) { + return new KanaNumeralsFormatter(); + } else if ( one == (int) '\u30A2' ) { + return new KanaNumeralsFormatter(); + } else if ( one == (int) '\u30A4' ) { + return new KanaNumeralsFormatter(); + } else { + return null; + } + } + + private static Integer[] toUpperCase ( Integer[] sa ) { + assert sa != null; + for ( int i = 0, n = sa.length; i < n; i++ ) { + Integer s = sa [ i ]; + sa [ i ] = Character.toUpperCase ( s ); + } + return sa; + } + + private static Integer[] toLowerCase ( Integer[] sa ) { + assert sa != null; + for ( int i = 0, n = sa.length; i < n; i++ ) { + Integer s = sa [ i ]; + sa [ i ] = Character.toLowerCase ( s ); + } + return sa; + } + + /* not yet used + private static Integer[] toTitleCase ( Integer[] sa ) { + assert sa != null; + if ( sa.length > 0 ) { + sa [ 0 ] = Character.toTitleCase ( sa [ 0 ] ); + } + return sa; + } + */ + + private static List<String> convertWordCase ( List<String> words, int caseType ) { + List<String> wl = new ArrayList<String>(); + for ( String w : words ) { + wl.add ( convertWordCase ( w, caseType ) ); + } + return wl; + } + + private static String convertWordCase ( String word, int caseType ) { + if ( caseType == Character.UPPERCASE_LETTER ) { + return word.toUpperCase(); + } else if ( caseType == Character.LOWERCASE_LETTER ) { + return word.toLowerCase(); + } else if ( caseType == Character.TITLECASE_LETTER ) { + StringBuffer sb = new StringBuffer(); + for ( int i = 0, n = word.length(); i < n; i++ ) { + String s = word.substring ( i, i + 1 ); + if ( i == 0 ) { + sb.append ( s.toUpperCase() ); + } else { + sb.append ( s.toLowerCase() ); + } + } + return sb.toString(); + } else { + return word; + } + } + + private static String joinWords ( List<String> words, String separator ) { + StringBuffer sb = new StringBuffer(); + for ( String w : words ) { + if ( sb.length() > 0 ) { + sb.append ( separator ); + } + sb.append ( w ); + } + return sb.toString(); + } + + /** + * Special number formatter. + */ + interface SpecialNumberFormatter { + /** + * Format number with special numeral system. + * @param number to be formatted + * @param one unicode scalar value denoting numeric value 1 + * @param letterValue letter value (must be one of the above letter value enumeration values) + * @param features features (feature sub-parameters) + * @param language denotes applicable language + * @param country denotes applicable country + * @return formatted number as array of unicode scalars + */ + Integer[] format ( long number, int one, int letterValue, String features, String language, String country ); + } + + /** + * English Word Numerals + */ + private static String[] englishWordOnes = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" }; + private static String[] englishWordTeens = { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" }; + private static String[] englishWordTens = { "", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety" }; + private static String[] englishWordOthers = { "hundred", "thousand", "million", "billion" }; + private static String[] englishWordOnesOrd = { "none", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth" }; + private static String[] englishWordTeensOrd = { "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth" }; + private static String[] englishWordTensOrd = { "", "tenth", "twentieth", "thirtieth", "fortieth", "fiftieth", "sixtieth", "seventieth", "eightieth", "ninetith" }; + private static String[] englishWordOthersOrd = { "hundredth", "thousandth", "millionth", "billionth" }; + private static class EnglishNumberAsWordFormatter implements SpecialNumberFormatter { + private int caseType = Character.UPPERCASE_LETTER; + EnglishNumberAsWordFormatter ( int caseType ) { + this.caseType = caseType; + } + @Override + public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) { + List<String> wl = new ArrayList<String>(); + if ( number >= 1000000000000L ) { + return null; + } else { + boolean ordinal = hasFeature ( features, "ordinal" ); + if ( number == 0 ) { + wl.add ( englishWordOnes [ 0 ] ); + } else if ( ordinal && ( number < 10 ) ) { + wl.add ( englishWordOnesOrd [ (int) number ] ); + } else { + int ones = (int) ( number % 1000 ); + int thousands = (int) ( ( number / 1000 ) % 1000 ); + int millions = (int) ( ( number / 1000000 ) % 1000 ); + int billions = (int) ( ( number / 1000000000 ) % 1000 ); + if ( billions > 0 ) { + wl = formatOnesInThousand ( wl, billions ); + if ( ordinal && ( ( number % 1000000000 ) == 0 ) ) { + wl.add ( englishWordOthersOrd[3] ); + } else { + wl.add ( englishWordOthers[3] ); + } + } + if ( millions > 0 ) { + wl = formatOnesInThousand ( wl, millions ); + if ( ordinal && ( ( number % 1000000 ) == 0 ) ) { + wl.add ( englishWordOthersOrd[2] ); + } else { + wl.add ( englishWordOthers[2] ); + } + } + if ( thousands > 0 ) { + wl = formatOnesInThousand ( wl, thousands ); + if ( ordinal && ( ( number % 1000 ) == 0 ) ) { + wl.add ( englishWordOthersOrd[1] ); + } else { + wl.add ( englishWordOthers[1] ); + } + } + if ( ones > 0 ) { + wl = formatOnesInThousand ( wl, ones, ordinal ); + } + } + wl = convertWordCase ( wl, caseType ); + return UTF32.toUTF32 ( joinWords ( wl, " " ), 0, true ); + } + } + private List<String> formatOnesInThousand ( List<String> wl, int number ) { + return formatOnesInThousand ( wl, number, false ); + } + private List<String> formatOnesInThousand ( List<String> wl, int number, boolean ordinal ) { + assert number < 1000; + int ones = number % 10; + int tens = ( number / 10 ) % 10; + int hundreds = ( number / 100 ) % 10; + if ( hundreds > 0 ) { + wl.add ( englishWordOnes [ hundreds ] ); + if ( ordinal && ( ( number % 100 ) == 0 ) ) { + wl.add ( englishWordOthersOrd[0] ); + } else { + wl.add ( englishWordOthers[0] ); + } + } + if ( tens > 0 ) { + if ( tens == 1 ) { + if ( ordinal ) { + wl.add ( englishWordTeensOrd [ ones ] ); + } else { + wl.add ( englishWordTeens [ ones ] ); + } + } else { + if ( ordinal && ( ones == 0 ) ) { + wl.add ( englishWordTensOrd [ tens ] ); + } else { + wl.add ( englishWordTens [ tens ] ); + } + if ( ones > 0 ) { + if ( ordinal ) { + wl.add ( englishWordOnesOrd [ ones ] ); + } else { + wl.add ( englishWordOnes [ ones ] ); + } + } + } + } else if ( ones > 0 ) { + if ( ordinal ) { + wl.add ( englishWordOnesOrd [ ones ] ); + } else { + wl.add ( englishWordOnes [ ones ] ); + } + } + return wl; + } + } + + /** + * French Word Numerals + */ + private static String[] frenchWordOnes = { "z\u00e9ro", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf" }; + private static String[] frenchWordTeens = { "dix", "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf" }; + private static String[] frenchWordTens = { "", "dix", "vingt", "trente", "quarante", "cinquante", "soixante", "soixante-dix", "quatre-vingt", "quatre-vingt-dix" }; + private static String[] frenchWordOthers = { "cent", "cents", "mille", "million", "millions", "milliard", "milliards" }; + private static String[] frenchWordOnesOrdMale = { "premier", "deuxi\u00e8me", "troisi\u00e8me", "quatri\u00e8me", "cinqui\u00e8me", "sixi\u00e8me", "septi\u00e8me", "huiti\u00e8me", "neuvi\u00e8me", "dixi\u00e8me" }; + private static String[] frenchWordOnesOrdFemale = { "premi\u00e8re", "deuxi\u00e8me", "troisi\u00e8me", "quatri\u00e8me", "cinqui\u00e8me", "sixi\u00e8me", "septi\u00e8me", "huiti\u00e8me", "neuvi\u00e8me", "dixi\u00e8me" }; + private static class FrenchNumberAsWordFormatter implements SpecialNumberFormatter { + private int caseType = Character.UPPERCASE_LETTER; + FrenchNumberAsWordFormatter ( int caseType ) { + this.caseType = caseType; + } + @Override + public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) { + List<String> wl = new ArrayList<String>(); + if ( number >= 1000000000000L ) { + return null; + } else { + boolean ordinal = hasFeature ( features, "ordinal" ); + if ( number == 0 ) { + wl.add ( frenchWordOnes [ 0 ] ); + } else if ( ordinal && ( number <= 10 ) ) { + boolean female = hasFeature ( features, "female" ); + if ( female ) { + wl.add ( frenchWordOnesOrdFemale [ (int) number ] ); + } else { + wl.add ( frenchWordOnesOrdMale [ (int) number ] ); + } + } else { + int ones = (int) ( number % 1000 ); + int thousands = (int) ( ( number / 1000 ) % 1000 ); + int millions = (int) ( ( number / 1000000 ) % 1000 ); + int billions = (int) ( ( number / 1000000000 ) % 1000 ); + if ( billions > 0 ) { + wl = formatOnesInThousand ( wl, billions ); + if ( billions == 1 ) { + wl.add ( frenchWordOthers[5] ); + } else { + wl.add ( frenchWordOthers[6] ); + } + } + if ( millions > 0 ) { + wl = formatOnesInThousand ( wl, millions ); + if ( millions == 1 ) { + wl.add ( frenchWordOthers[3] ); + } else { + wl.add ( frenchWordOthers[4] ); + } + } + if ( thousands > 0 ) { + if ( thousands > 1 ) { + wl = formatOnesInThousand ( wl, thousands ); + } + wl.add ( frenchWordOthers[2] ); + } + if ( ones > 0 ) { + wl = formatOnesInThousand ( wl, ones ); + } + } + wl = convertWordCase ( wl, caseType ); + return UTF32.toUTF32 ( joinWords ( wl, " " ), 0, true ); + } + } + private List<String> formatOnesInThousand ( List<String> wl, int number ) { + assert number < 1000; + int ones = number % 10; + int tens = ( number / 10 ) % 10; + int hundreds = ( number / 100 ) % 10; + if ( hundreds > 0 ) { + if ( hundreds > 1 ) { + wl.add ( frenchWordOnes [ hundreds ] ); + } + if ( ( hundreds > 1 ) && ( tens == 0 ) && ( ones == 0 ) ) { + wl.add ( frenchWordOthers[1] ); + } else { + wl.add ( frenchWordOthers[0] ); + } + } + if ( tens > 0 ) { + if ( tens == 1 ) { + wl.add ( frenchWordTeens [ ones ] ); + } else if ( tens < 7 ) { + if ( ones == 1 ) { + wl.add ( frenchWordTens [ tens ] ); + wl.add ( "et" ); + wl.add ( frenchWordOnes [ ones ] ); + } else { + StringBuffer sb = new StringBuffer(); + sb.append ( frenchWordTens [ tens ] ); + if ( ones > 0 ) { + sb.append ( '-' ); + sb.append ( frenchWordOnes [ ones ] ); + } + wl.add ( sb.toString() ); + } + } else if ( tens == 7 ) { + if ( ones == 1 ) { + wl.add ( frenchWordTens [ 6 ] ); + wl.add ( "et" ); + wl.add ( frenchWordTeens [ ones ] ); + } else { + StringBuffer sb = new StringBuffer(); + sb.append ( frenchWordTens [ 6 ] ); + sb.append ( '-' ); + sb.append ( frenchWordTeens [ ones ] ); + wl.add ( sb.toString() ); + } + } else if ( tens == 8 ) { + StringBuffer sb = new StringBuffer(); + sb.append ( frenchWordTens [ tens ] ); + if ( ones > 0 ) { + sb.append ( '-' ); + sb.append ( frenchWordOnes [ ones ] ); + } else { + sb.append ( 's' ); + } + wl.add ( sb.toString() ); + } else if ( tens == 9 ) { + StringBuffer sb = new StringBuffer(); + sb.append ( frenchWordTens [ 8 ] ); + sb.append ( '-' ); + sb.append ( frenchWordTeens [ ones ] ); + wl.add ( sb.toString() ); + } + } else if ( ones > 0 ) { + wl.add ( frenchWordOnes [ ones ] ); + } + return wl; + } + } + + /** + * Spanish Word Numerals + */ + private static String[] spanishWordOnes = { "cero", "uno", "dos", "tres", "cuatro", "cinco", "seise", "siete", "ocho", "nueve" }; + private static String[] spanishWordTeens = { "diez", "once", "doce", "trece", "catorce", "quince", "diecis\u00e9is", "diecisiete", "dieciocho", "diecinueve" }; + private static String[] spanishWordTweens = { "veinte", "veintiuno", "veintid\u00f3s", "veintitr\u00e9s", "veinticuatro", "veinticinco", "veintis\u00e9is", "veintisiete", "veintiocho", "veintinueve" }; + private static String[] spanishWordTens = { "", "diez", "veinte", "treinta", "cuarenta", "cincuenta", "sesenta", "setenta", "ochenta", "noventa" }; + private static String[] spanishWordHundreds = { "", "ciento", "doscientos", "trescientos", "cuatrocientos", "quinientos", "seiscientos", "setecientos", "ochocientos", "novecientos" }; + private static String[] spanishWordOthers = { "un", "cien", "mil", "mill\u00f3n", "millones" }; + private static String[] spanishWordOnesOrdMale = { "ninguno", "primero", "segundo", "tercero", "cuarto", "quinto", "sexto", "s\u00e9ptimo", "octavo", "novento", "d\u00e9cimo" }; + private static String[] spanishWordOnesOrdFemale = { "ninguna", "primera", "segunda", "tercera", "cuarta", "quinta", "sexta", "s\u00e9ptima", "octava", "noventa", "d\u00e9cima" }; + private static class SpanishNumberAsWordFormatter implements SpecialNumberFormatter { + private int caseType = Character.UPPERCASE_LETTER; + SpanishNumberAsWordFormatter ( int caseType ) { + this.caseType = caseType; + } + @Override + public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) { + List<String> wl = new ArrayList<String>(); + if ( number >= 1000000000000L ) { + return null; + } else { + boolean ordinal = hasFeature ( features, "ordinal" ); + if ( number == 0 ) { + wl.add ( spanishWordOnes [ 0 ] ); + } else if ( ordinal && ( number <= 10 ) ) { + boolean female = hasFeature ( features, "female" ); + if ( female ) { + wl.add ( spanishWordOnesOrdFemale [ (int) number ] ); + } else { + wl.add ( spanishWordOnesOrdMale [ (int) number ] ); + } + } else { + int ones = (int) ( number % 1000 ); + int thousands = (int) ( ( number / 1000 ) % 1000 ); + int millions = (int) ( ( number / 1000000 ) % 1000 ); + int billions = (int) ( ( number / 1000000000 ) % 1000 ); + if ( billions > 0 ) { + if ( billions > 1 ) { + wl = formatOnesInThousand ( wl, billions ); + } + wl.add ( spanishWordOthers[2] ); + wl.add ( spanishWordOthers[4] ); + } + if ( millions > 0 ) { + if ( millions == 1 ) { + wl.add ( spanishWordOthers[0] ); + } else { + wl = formatOnesInThousand ( wl, millions ); + } + if ( millions > 1 ) { + wl.add ( spanishWordOthers[4] ); + } else { + wl.add ( spanishWordOthers[3] ); + } + } + if ( thousands > 0 ) { + if ( thousands > 1 ) { + wl = formatOnesInThousand ( wl, thousands ); + } + wl.add ( spanishWordOthers[2] ); + } + if ( ones > 0 ) { + wl = formatOnesInThousand ( wl, ones ); + } + } + wl = convertWordCase ( wl, caseType ); + return UTF32.toUTF32 ( joinWords ( wl, " " ), 0, true ); + } + } + private List<String> formatOnesInThousand ( List<String> wl, int number ) { + assert number < 1000; + int ones = number % 10; + int tens = ( number / 10 ) % 10; + int hundreds = ( number / 100 ) % 10; + if ( hundreds > 0 ) { + if ( ( hundreds == 1 ) && ( tens == 0 ) && ( ones == 0 ) ) { + wl.add ( spanishWordOthers[1] ); + } else { + wl.add ( spanishWordHundreds [ hundreds ] ); + } + } + if ( tens > 0 ) { + if ( tens == 1 ) { + wl.add ( spanishWordTeens [ ones ] ); + } else if ( tens == 2 ) { + wl.add ( spanishWordTweens [ ones ] ); + } else { + wl.add ( spanishWordTens [ tens ] ); + if ( ones > 0 ) { + wl.add ( "y" ); + wl.add ( spanishWordOnes [ ones ] ); + } + } + } else if ( ones > 0 ) { + wl.add ( spanishWordOnes [ ones ] ); + } + return wl; + } + } + + /** + * Roman (Latin) Numerals + */ + private static int[] romanMapping = { + 100000, + 90000, + 50000, + 40000, + 10000, + 9000, + 5000, + 4000, + 1000, + 900, + 500, + 400, + 100, + 90, + 50, + 40, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1 + }; + private static String[] romanStandardForms = { + null, + null, + null, + null, + null, + null, + null, + null, + "m", + "cm", + "d", + "cd", + "c", + "xc", + "l", + "xl", + "x", + "ix", + null, + null, + null, + "v", + "iv", + null, + null, + "i" + }; + private static String[] romanLargeForms = { + "\u2188", + "\u2182\u2188", + "\u2187", + "\u2182\u2187", + "\u2182", + "\u2180\u2182", + "\u2181", + "\u2180\u2181", + "m", + "cm", + "d", + "cd", + "c", + "xc", + "l", + "xl", + "x", + "ix", + null, + null, + null, + "v", + "iv", + null, + null, + "i" + }; + private static String[] romanNumberForms = { + "\u2188", + "\u2182\u2188", + "\u2187", + "\u2182\u2187", + "\u2182", + "\u2180\u2182", + "\u2181", + "\u2180\u2181", + "\u216F", + "\u216D\u216F", + "\u216E", + "\u216D\u216E", + "\u216D", + "\u2169\u216D", + "\u216C", + "\u2169\u216C", + "\u2169", + "\u2168", + "\u2167", + "\u2166", + "\u2165", + "\u2164", + "\u2163", + "\u2162", + "\u2161", + "\u2160" + }; + private static class RomanNumeralsFormatter implements SpecialNumberFormatter { + @Override + public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) { + List<Integer> sl = new ArrayList<Integer>(); + if ( number == 0 ) { + return null; + } else { + String[] forms; + int maxNumber; + if ( hasFeature ( features, "unicode-number-forms" ) ) { + forms = romanNumberForms; + maxNumber = 199999; + } else if ( hasFeature ( features, "large" ) ) { + forms = romanLargeForms; + maxNumber = 199999; + } else { + forms = romanStandardForms; + maxNumber = 4999; + } + if ( number > maxNumber ) { + return null; + } else { + while ( number > 0 ) { + for ( int i = 0, n = romanMapping.length; i < n; i++ ) { + int d = romanMapping [ i ]; + if ( ( number >= d ) && ( forms [ i ] != null ) ) { + appendScalars ( sl, UTF32.toUTF32 ( forms [ i ], 0, true ) ); + number = number - d; + break; + } + } + } + if ( one == (int) 'I' ) { + return toUpperCase ( sl.toArray ( new Integer [ sl.size() ] ) ); + } else if ( one == (int) 'i' ) { + return toLowerCase ( sl.toArray ( new Integer [ sl.size() ] ) ); + } else { + return null; + } + } + } + } + } + + /** + * Isopsephry (Greek) Numerals + */ + private static class IsopsephryNumeralsFormatter implements SpecialNumberFormatter { + @Override + public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) { + return null; + } + } + + /** + * Gematria (Hebrew) Numerals + */ + private static int[] hebrewGematriaAlphabeticMap = { + // ones + 0x05D0, // ALEF + 0x05D1, // BET + 0x05D2, // GIMEL + 0x05D3, // DALET + 0x05D4, // HE + 0x05D5, // VAV + 0x05D6, // ZAYIN + 0x05D7, // HET + 0x05D8, // TET + // tens + 0x05D9, // YOD + 0x05DB, // KAF + 0x05DC, // LAMED + 0x05DE, // MEM + 0x05E0, // NUN + 0x05E1, // SAMEKH + 0x05E2, // AYIN + 0x05E4, // PE + 0x05E6, // TSADHI + // hundreds + 0x05E7, // QOF + 0x05E8, // RESH + 0x05E9, // SHIN + 0x05EA, // TAV + 0x05DA, // FINAL KAF + 0x05DD, // FINAL MEM + 0x05DF, // FINAL NUN + 0x05E3, // FINAL PE + 0x05E5, // FINAL TSADHI + }; + private class GematriaNumeralsFormatter implements SpecialNumberFormatter { + @Override + public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) { + if ( one == 0x05D0 ) { + if ( letterValue == LETTER_VALUE_ALPHABETIC ) { + return formatNumberAsSequence ( number, one, hebrewGematriaAlphabeticMap.length, hebrewGematriaAlphabeticMap ); + } else if ( letterValue == LETTER_VALUE_TRADITIONAL ) { + if ( ( number == 0 ) || ( number > 1999 ) ) { + return null; + } else { + return formatAsGematriaNumber ( number, features, language, country ); + } + } else { + return null; + } + } else { + return null; + } + } + private Integer[] formatAsGematriaNumber ( long number, String features, String language, String country ) { + List<Integer> sl = new ArrayList<Integer>(); + assert hebrewGematriaAlphabeticMap.length == 27; + assert hebrewGematriaAlphabeticMap[0] == 0x05D0; // ALEF + assert hebrewGematriaAlphabeticMap[21] == 0x05EA; // TAV + assert number != 0; + assert number < 2000; + int[] map = hebrewGematriaAlphabeticMap; + int thousands = (int) ( ( number / 1000 ) % 10 ); + int hundreds = (int) ( ( number / 100 ) % 10 ); + int tens = (int) ( ( number / 10 ) % 10 ); + int ones = (int) ( ( number / 1 ) % 10 ); + if ( thousands > 0 ) { + sl.add ( map [ 0 + ( thousands - 1 ) ] ); + sl.add ( 0x05F3 ); + } + if ( hundreds > 0 ) { + assert hundreds < 10; + if ( hundreds < 5 ) { + sl.add ( map [ 18 + ( hundreds - 1 ) ] ); + } else if ( hundreds < 9 ) { + sl.add ( map [ 18 + ( 4 - 1 ) ] ); + sl.add ( 0x05F4 ); + sl.add ( map [ 18 + ( hundreds - 5 ) ] ); + } else if ( hundreds == 9 ) { + sl.add ( map [ 18 + ( 4 - 1 ) ] ); + sl.add ( map [ 18 + ( 4 - 1 ) ] ); + sl.add ( 0x05F4 ); + sl.add ( map [ 18 + ( hundreds - 9 ) ] ); + } + } + if ( number == 15 ) { + sl.add ( map [ 9 - 1] ); + sl.add ( 0x05F4 ); + sl.add ( map [ 6 - 1] ); + } else if ( number == 16 ) { + sl.add ( map [ 9 - 1 ] ); + sl.add ( 0x05F4 ); + sl.add ( map [ 7 - 1 ] ); + } else { + if ( tens > 0 ) { + assert tens < 10; + sl.add ( map [ 9 + ( tens - 1 ) ] ); + } + if ( ones > 0 ) { + assert ones < 10; + sl.add ( map [ 0 + ( ones - 1 ) ] ); + } + } + return sl.toArray ( new Integer [ sl.size() ] ); + } + } + + /** + * Arabic Numerals + */ + private static int[] arabicAbjadiAlphabeticMap = { + // ones + 0x0623, // ALEF WITH HAMZA ABOVE + 0x0628, // BEH + 0x062C, // JEEM + 0x062F, // DAL + 0x0647, // HEH + 0x0648, // WAW + 0x0632, // ZAIN + 0x062D, // HAH + 0x0637, // TAH + // tens + 0x0649, // ALEF MAQSURA + 0x0643, // KAF + 0x0644, // LAM + 0x0645, // MEEM + 0x0646, // NOON + 0x0633, // SEEN + 0x0639, // AIN + 0x0641, // FEH + 0x0635, // SAD + // hundreds + 0x0642, // QAF + 0x0631, // REH + 0x0634, // SHEEN + 0x062A, // TEH + 0x062B, // THEH + 0x062E, // KHAH + 0x0630, // THAL + 0x0636, // DAD + 0x0638, // ZAH + // thousands + 0x063A, // GHAIN + }; + private static int[] arabicHijaiAlphabeticMap = { + 0x0623, // ALEF WITH HAMZA ABOVE + 0x0628, // BEH + 0x062A, // TEH + 0x062B, // THEH + 0x062C, // JEEM + 0x062D, // HAH + 0x062E, // KHAH + 0x062F, // DAL + 0x0630, // THAL + 0x0631, // REH + 0x0632, // ZAIN + 0x0633, // SEEN + 0x0634, // SHEEN + 0x0635, // SAD + 0x0636, // DAD + 0x0637, // TAH + 0x0638, // ZAH + 0x0639, // AIN + 0x063A, // GHAIN + 0x0641, // FEH + 0x0642, // QAF + 0x0643, // KAF + 0x0644, // LAM + 0x0645, // MEEM + 0x0646, // NOON + 0x0647, // HEH + 0x0648, // WAW + 0x0649, // ALEF MAQSURA + }; + private class ArabicNumeralsFormatter implements SpecialNumberFormatter { + @Override + public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) { + if ( one == 0x0627 ) { + int[] map; + if ( letterValue == LETTER_VALUE_TRADITIONAL ) { + map = arabicAbjadiAlphabeticMap; + } else if ( letterValue == LETTER_VALUE_ALPHABETIC ) { + map = arabicHijaiAlphabeticMap; + } else { + map = arabicAbjadiAlphabeticMap; + } + return formatNumberAsSequence ( number, one, map.length, map ); + } else if ( one == 0x0623 ) { + if ( ( number == 0 ) || ( number > 1999 ) ) { + return null; + } else { + return formatAsAbjadiNumber ( number, features, language, country ); + } + } else { + return null; + } + } + private Integer[] formatAsAbjadiNumber ( long number, String features, String language, String country ) { + List<Integer> sl = new ArrayList<Integer>(); + assert arabicAbjadiAlphabeticMap.length == 28; + assert arabicAbjadiAlphabeticMap[0] == 0x0623; // ALEF WITH HAMZA ABOVE + assert arabicAbjadiAlphabeticMap[27] == 0x063A; // GHAIN + assert number != 0; + assert number < 2000; + int[] map = arabicAbjadiAlphabeticMap; + int thousands = (int) ( ( number / 1000 ) % 10 ); + int hundreds = (int) ( ( number / 100 ) % 10 ); + int tens = (int) ( ( number / 10 ) % 10 ); + int ones = (int) ( ( number / 1 ) % 10 ); + if ( thousands > 0 ) { + assert thousands < 2; + sl.add ( map [ 27 + ( thousands - 1 ) ] ); + } + if ( hundreds > 0 ) { + assert thousands < 10; + sl.add ( map [ 18 + ( hundreds - 1 ) ] ); + } + if ( tens > 0 ) { + assert tens < 10; + sl.add ( map [ 9 + ( tens - 1 ) ] ); + } + if ( ones > 0 ) { + assert ones < 10; + sl.add ( map [ 0 + ( ones - 1 ) ] ); + } + return sl.toArray ( new Integer [ sl.size() ] ); + } + } + + /** + * Kana (Japanese) Numerals + */ + private static int[] hiraganaGojuonAlphabeticMap = { + 0x3042, // A + 0x3044, // I + 0x3046, // U + 0x3048, // E + 0x304A, // O + 0x304B, // KA + 0x304D, // KI + 0x304F, // KU + 0x3051, // KE + 0x3053, // KO + 0x3055, // SA + 0x3057, // SI + 0x3059, // SU + 0x305B, // SE + 0x305D, // SO + 0x305F, // TA + 0x3061, // TI + 0x3064, // TU + 0x3066, // TE + 0x3068, // TO + 0x306A, // NA + 0x306B, // NI + 0x306C, // NU + 0x306D, // NE + 0x306E, // NO + 0x306F, // HA + 0x3072, // HI + 0x3075, // HU + 0x3078, // HE + 0x307B, // HO + 0x307E, // MA + 0x307F, // MI + 0x3080, // MU + 0x3081, // ME + 0x3082, // MO + 0x3084, // YA + 0x3086, // YU + 0x3088, // YO + 0x3089, // RA + 0x308A, // RI + 0x308B, // RU + 0x308C, // RE + 0x308D, // RO + 0x308F, // WA + 0x3090, // WI + 0x3091, // WE + 0x3092, // WO + 0x3093, // N + }; + private static int[] katakanaGojuonAlphabeticMap = { + 0x30A2, // A + 0x30A4, // I + 0x30A6, // U + 0x30A8, // E + 0x30AA, // O + 0x30AB, // KA + 0x30AD, // KI + 0x30AF, // KU + 0x30B1, // KE + 0x30B3, // KO + 0x30B5, // SA + 0x30B7, // SI + 0x30B9, // SU + 0x30BB, // SE + 0x30BD, // SO + 0x30BF, // TA + 0x30C1, // TI + 0x30C4, // TU + 0x30C6, // TE + 0x30C8, // TO + 0x30CA, // NA + 0x30CB, // NI + 0x30CC, // NU + 0x30CD, // NE + 0x30CE, // NO + 0x30CF, // HA + 0x30D2, // HI + 0x30D5, // HU + 0x30D8, // HE + 0x30DB, // HO + 0x30DE, // MA + 0x30DF, // MI + 0x30E0, // MU + 0x30E1, // ME + 0x30E2, // MO + 0x30E4, // YA + 0x30E6, // YU + 0x30E8, // YO + 0x30E9, // RA + 0x30EA, // RI + 0x30EB, // RU + 0x30EC, // RE + 0x30ED, // RO + 0x30EF, // WA + 0x30F0, // WI + 0x30F1, // WE + 0x30F2, // WO + 0x30F3, // N + }; + private class KanaNumeralsFormatter implements SpecialNumberFormatter { + @Override + public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) { + if ( ( one == 0x3042 ) && ( letterValue == LETTER_VALUE_ALPHABETIC ) ) { + return formatNumberAsSequence ( number, one, hiraganaGojuonAlphabeticMap.length, hiraganaGojuonAlphabeticMap ); + } else if ( ( one == 0x30A2 ) && ( letterValue == LETTER_VALUE_ALPHABETIC ) ) { + return formatNumberAsSequence ( number, one, katakanaGojuonAlphabeticMap.length, katakanaGojuonAlphabeticMap ); + } else { + return null; + } + } + } + + /** + * Thai Numerals + */ + private static int[] thaiAlphabeticMap = { + 0x0E01, + 0x0E02, + 0x0E03, + 0x0E04, + 0x0E05, + 0x0E06, + 0x0E07, + 0x0E08, + 0x0E09, + 0x0E0A, + 0x0E0B, + 0x0E0C, + 0x0E0D, + 0x0E0E, + 0x0E0F, + 0x0E10, + 0x0E11, + 0x0E12, + 0x0E13, + 0x0E14, + 0x0E15, + 0x0E16, + 0x0E17, + 0x0E18, + 0x0E19, + 0x0E1A, + 0x0E1B, + 0x0E1C, + 0x0E1D, + 0x0E1E, + 0x0E1F, + 0x0E20, + 0x0E21, + 0x0E22, + 0x0E23, + // 0x0E24, // RU - not used in modern sequence + 0x0E25, + // 0x0E26, // LU - not used in modern sequence + 0x0E27, + 0x0E28, + 0x0E29, + 0x0E2A, + 0x0E2B, + 0x0E2C, + 0x0E2D, + 0x0E2E, + }; + private class ThaiNumeralsFormatter implements SpecialNumberFormatter { + @Override + public Integer[] format ( long number, int one, int letterValue, String features, String language, String country ) { + if ( ( one == 0x0E01 ) && ( letterValue == LETTER_VALUE_ALPHABETIC ) ) { + return formatNumberAsSequence ( number, one, thaiAlphabeticMap.length, thaiAlphabeticMap ); + } else { + return null; + } + } + } + +} diff --git a/src/java/org/apache/fop/complexscripts/util/ScriptContextTester.java b/src/java/org/apache/fop/complexscripts/util/ScriptContextTester.java new file mode 100644 index 000000000..3f68b00e2 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/util/ScriptContextTester.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.util; + +/** + * Interface for providing script specific context testers. + * @author Glenn Adams + */ +public interface ScriptContextTester { + + /** + * Obtain a glyph context tester for the specified feature. + * @param feature a feature identifier + * @return a glyph context tester or null if none available for the specified feature + */ + GlyphContextTester getTester ( String feature ); + +} diff --git a/src/java/org/apache/fop/complexscripts/util/UTF32.java b/src/java/org/apache/fop/complexscripts/util/UTF32.java new file mode 100644 index 000000000..9df2020f0 --- /dev/null +++ b/src/java/org/apache/fop/complexscripts/util/UTF32.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.util; + +import org.apache.fop.util.CharUtilities; + +// CSOFF: InnerAssignmentCheck + +/** + * UTF32 related utilities. + * @author Glenn Adams + */ +public final class UTF32 { + + private UTF32() { + } + + /** + * Convert Java string (UTF-16) to a Unicode scalar array (UTF-32). + * Note that if there are any non-BMP encoded characters present in the + * input, then the number of entries in the output array will be less + * than the number of elements in the input string. Any + * @param s input string + * @param substitution value to substitute for ill-formed surrogate + * @param errorOnSubstitution throw runtime exception (IllegalArgumentException) in + * case this argument is true and a substitution would be attempted + * @return output scalar array + * @throws IllegalArgumentException if substitution required and errorOnSubstitution + * is not false + */ + public static Integer[] toUTF32 ( String s, int substitution, boolean errorOnSubstitution ) + throws IllegalArgumentException { + int n; + if ( ( n = s.length() ) == 0 ) { + return new Integer[0]; + } else { + Integer[] sa = new Integer [ n ]; + int k = 0; + for ( int i = 0; i < n; i++ ) { + int c = (int) s.charAt(i); + if ( ( c >= 0xD800 ) && ( c < 0xE000 ) ) { + int s1 = c; + int s2 = ( ( i + 1 ) < n ) ? (int) s.charAt ( i + 1 ) : 0; + if ( s1 < 0xDC00 ) { + if ( ( s2 >= 0xDC00 ) && ( s2 < 0xE000 ) ) { + c = ( ( s1 - 0xD800 ) << 10 ) + ( s2 - 0xDC00 ) + 65536; + i++; + } else { + if ( errorOnSubstitution ) { + throw new IllegalArgumentException + ( "isolated high (leading) surrogate" ); + } else { + c = substitution; + } + } + } else { + if ( errorOnSubstitution ) { + throw new IllegalArgumentException + ( "isolated low (trailing) surrogate" ); + } else { + c = substitution; + } + } + } + sa[k++] = c; + } + if ( k == n ) { + return sa; + } else { + Integer[] na = new Integer [ k ]; + System.arraycopy ( sa, 0, na, 0, k ); + return na; + } + } + } + + /** + * Convert a Unicode scalar array (UTF-32) a Java string (UTF-16). + * @param sa input scalar array + * @return output (UTF-16) string + * @throws IllegalArgumentException if an input scalar value is illegal, + * e.g., a surrogate or out of range + */ + public static String fromUTF32 ( Integer[] sa ) throws IllegalArgumentException { + StringBuffer sb = new StringBuffer(); + for ( int s : sa ) { + if ( s < 65535 ) { + if ( ( s < 0xD800 ) || ( s > 0xDFFF ) ) { + sb.append ( (char) s ); + } else { + String ncr = CharUtilities.charToNCRef(s); + throw new IllegalArgumentException + ( "illegal scalar value 0x" + ncr.substring(2, ncr.length() - 1) + + "; cannot be UTF-16 surrogate" ); + } + } else if ( s < 1114112 ) { + int s1 = ( ( ( s - 65536 ) >> 10 ) & 0x3FF ) + 0xD800; + int s2 = ( ( ( s - 65536 ) >> 0 ) & 0x3FF ) + 0xDC00; + sb.append ( (char) s1 ); + sb.append ( (char) s2 ); + } else { + String ncr = CharUtilities.charToNCRef(s); + throw new IllegalArgumentException + ( "illegal scalar value 0x" + ncr.substring(2, ncr.length() - 1) + + "; out of range for UTF-16" ); + } + } + return sb.toString(); + } + +} diff --git a/src/java/org/apache/fop/fo/Constants.java b/src/java/org/apache/fop/fo/Constants.java index a2b982269..2d10dcdd9 100644 --- a/src/java/org/apache/fop/fo/Constants.java +++ b/src/java/org/apache/fop/fo/Constants.java @@ -26,7 +26,7 @@ package org.apache.fop.fo; * <li>Input and output formats</li> * <li>Formatting objects (<em>FO_XXX</em>)</li> * <li>Formatting properties (<em>PR_XXX</em>)</li> - * <li>Enumerated values used in formatting properties (<em>EN_XXX</em>)</li> + * <li>Enumerated values used in formatting properties and traits (<em>EN_XXX</em>)</li> * </ul> */ public interface Constants { @@ -778,9 +778,15 @@ public interface Constants { int PR_X_ALT_TEXT = 274; /** Property constant - FOP proprietary prototype (in XSL-FO 2.0 Requirements) */ int PR_X_XML_BASE = 275; + /** + * Property constant - FOP proprietary extension (see NumberConverter) used + * to perform additional control over number conversion when generating page + * numbers. + */ + int PR_X_NUMBER_CONVERSION_FEATURES = 276; /** Number of property constants defined */ - int PROPERTY_COUNT = 275; + int PROPERTY_COUNT = 276; // compound property constants @@ -1208,6 +1214,16 @@ public interface Constants { int EN_NO_LINK = 199; /** Enumeration constant -- XSL 1.1 */ int EN_ALTERNATE = 200; + /** Enumeration constant -- for *-direction traits */ + int EN_LR = 201; // left to right + /** Enumeration constant -- for *-direction traits */ + int EN_RL = 202; // right to left + /** Enumeration constant -- for *-direction traits */ + int EN_TB = 203; // top to bottom + /** Enumeration constant -- for *-direction traits */ + int EN_BT = 204; // bottom to top + /** Enumeration constant */ + int EN_TB_LR = 205; // for top-to-bottom, left-to-right writing mode /** Number of enumeration constants defined */ - int ENUM_COUNT = 200; + int ENUM_COUNT = 205; } diff --git a/src/java/org/apache/fop/fo/FONode.java b/src/java/org/apache/fop/fo/FONode.java index b63c49ae2..99d133f16 100644 --- a/src/java/org/apache/fop/fo/FONode.java +++ b/src/java/org/apache/fop/fo/FONode.java @@ -20,8 +20,10 @@ package org.apache.fop.fo; // Java +import java.util.Iterator; import java.util.ListIterator; import java.util.Map; +import java.util.Stack; import org.xml.sax.Attributes; import org.xml.sax.Locator; @@ -35,6 +37,7 @@ import org.apache.xmlgraphics.util.QName; import org.apache.fop.accessibility.StructureTreeElement; import org.apache.fop.apps.FOPException; import org.apache.fop.apps.FOUserAgent; +import org.apache.fop.complexscripts.bidi.DelimitedTextRange; import org.apache.fop.fo.extensions.ExtensionAttachment; import org.apache.fop.fo.extensions.ExtensionElementMapping; import org.apache.fop.fo.extensions.InternalElementMapping; @@ -865,6 +868,122 @@ public abstract class FONode implements Cloneable { } /** + * Determine if node has a delimited text range boundary. N.B. that we report + * this to be true by default, while specific subclasses override this method to report false. + * @param boundary one of {EN_BEFORE, EN_AFTER, or EN_BOTH} enumeration constants + * @return true if indicated boundary (or boundaries) constitute a delimited text range + * boundary. + */ + public boolean isDelimitedTextRangeBoundary ( int boundary ) { + return true; + } + + /** + * Collect the sequence of delimited text ranges, where each new + * range is pushed onto RANGES. + * @param ranges a stack of delimited text ranges + * @return the (possibly) updated stack of delimited text ranges + */ + public Stack collectDelimitedTextRanges ( Stack ranges ) { + // if boundary before, then push new range + if ( isRangeBoundaryBefore() ) { + maybeNewRange ( ranges ); + } + // get current range, if one exists + DelimitedTextRange currentRange; + if ( ranges.size() > 0 ) { + currentRange = (DelimitedTextRange) ranges.peek(); + } else { + currentRange = null; + } + // proceses this node + ranges = collectDelimitedTextRanges ( ranges, currentRange ); + // if boundary after, then push new range + if ( isRangeBoundaryAfter() ) { + maybeNewRange ( ranges ); + } + return ranges; + } + + /** + * Collect the sequence of delimited text ranges, where each new + * range is pushed onto RANGES, where default implementation collects ranges + * of child nodes. + * @param ranges a stack of delimited text ranges + * @param currentRange the current range or null (if none) + * @return the (possibly) updated stack of delimited text ranges + */ + protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) { + for ( Iterator it = getChildNodes(); ( it != null ) && it.hasNext();) { + ranges = ( (FONode) it.next() ).collectDelimitedTextRanges ( ranges ); + } + return ranges; + } + + /** + * Determine if this node is a new bidi RANGE block item. + * @return true if this node is a new bidi RANGE block item + */ + public boolean isBidiRangeBlockItem() { + return false; + } + + /** + * Conditionally add a new delimited text range to RANGES, where new range is + * associated with current FONode. A new text range is added unless all of the following are + * true: + * <ul> + * <li>there exists a current range RCUR in RANGES</li> + * <li>RCUR is empty</li> + * <li>the node of the RCUR is the same node as FN or a descendent node of FN</li> + * </ul> + * @param ranges stack of delimited text ranges + * @return new range (if constructed and pushed onto stack) or current range (if any) or null + */ + private DelimitedTextRange maybeNewRange ( Stack ranges ) { + DelimitedTextRange rCur = null; // current range (top of range stack) + DelimitedTextRange rNew = null; // new range to be pushed onto range stack + if ( ranges.empty() ) { + if ( isBidiRangeBlockItem() ) { + rNew = new DelimitedTextRange(this); + } + } else { + rCur = (DelimitedTextRange) ranges.peek(); + if ( rCur != null ) { + if ( !rCur.isEmpty() || !isSelfOrDescendent ( rCur.getNode(), this ) ) { + rNew = new DelimitedTextRange(this); + } + } + } + if ( rNew != null ) { + ranges.push ( rNew ); + } else { + rNew = rCur; + } + return rNew; + } + + private boolean isRangeBoundaryBefore() { + return isDelimitedTextRangeBoundary ( Constants.EN_BEFORE ); + } + + private boolean isRangeBoundaryAfter() { + return isDelimitedTextRangeBoundary ( Constants.EN_AFTER ); + } + + /** + * Determine if node N2 is the same or a descendent of node N1. + */ + private static boolean isSelfOrDescendent ( FONode n1, FONode n2 ) { + for ( FONode n = n2; n != null; n = n.getParent() ) { + if ( n == n1 ) { + return true; + } + } + return false; + } + + /** * Base iterator interface over a FO's children */ public interface FONodeIterator extends ListIterator { diff --git a/src/java/org/apache/fop/fo/FOPropertyMapping.java b/src/java/org/apache/fop/fo/FOPropertyMapping.java index 172aa5329..b29571b09 100644 --- a/src/java/org/apache/fop/fo/FOPropertyMapping.java +++ b/src/java/org/apache/fop/fo/FOPropertyMapping.java @@ -632,7 +632,7 @@ public final class FOPropertyMapping implements Constants { m.setDefault("black"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_TOP_COLOR, PR_BORDER_TOP_COLOR, - PR_BORDER_RIGHT_COLOR); + PR_BORDER_RIGHT_COLOR, PR_BORDER_LEFT_COLOR); corr.setRelative(true); addPropertyMaker("border-before-color", m); @@ -641,7 +641,7 @@ public final class FOPropertyMapping implements Constants { m.useGeneric(genericBorderStyle); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_TOP_STYLE, PR_BORDER_TOP_STYLE, - PR_BORDER_RIGHT_STYLE); + PR_BORDER_RIGHT_STYLE, PR_BORDER_LEFT_STYLE); corr.setRelative(true); addPropertyMaker("border-before-style", m); @@ -651,7 +651,7 @@ public final class FOPropertyMapping implements Constants { m.getSubpropMaker(CP_CONDITIONALITY).setDefault("discard"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_TOP_WIDTH, PR_BORDER_TOP_WIDTH, - PR_BORDER_RIGHT_WIDTH); + PR_BORDER_RIGHT_WIDTH, PR_BORDER_LEFT_WIDTH); corr.setRelative(true); addPropertyMaker("border-before-width", m); @@ -662,7 +662,7 @@ public final class FOPropertyMapping implements Constants { m.setDefault("black"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_BOTTOM_COLOR, PR_BORDER_BOTTOM_COLOR, - PR_BORDER_LEFT_COLOR); + PR_BORDER_LEFT_COLOR, PR_BORDER_RIGHT_COLOR); corr.setRelative(true); addPropertyMaker("border-after-color", m); @@ -671,7 +671,7 @@ public final class FOPropertyMapping implements Constants { m.useGeneric(genericBorderStyle); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_BOTTOM_STYLE, PR_BORDER_BOTTOM_STYLE, - PR_BORDER_LEFT_STYLE); + PR_BORDER_LEFT_STYLE, PR_BORDER_RIGHT_STYLE); corr.setRelative(true); addPropertyMaker("border-after-style", m); @@ -681,7 +681,7 @@ public final class FOPropertyMapping implements Constants { m.getSubpropMaker(CP_CONDITIONALITY).setDefault("discard"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_BOTTOM_WIDTH, PR_BORDER_BOTTOM_WIDTH, - PR_BORDER_LEFT_WIDTH); + PR_BORDER_LEFT_WIDTH, PR_BORDER_LEFT_WIDTH); corr.setRelative(true); addPropertyMaker("border-after-width", m); @@ -692,7 +692,7 @@ public final class FOPropertyMapping implements Constants { m.setDefault("black"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_LEFT_COLOR, PR_BORDER_RIGHT_COLOR, - PR_BORDER_TOP_COLOR); + PR_BORDER_TOP_COLOR, PR_BORDER_TOP_COLOR); corr.setRelative(true); addPropertyMaker("border-start-color", m); @@ -701,7 +701,7 @@ public final class FOPropertyMapping implements Constants { m.useGeneric(genericBorderStyle); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_LEFT_STYLE, PR_BORDER_RIGHT_STYLE, - PR_BORDER_TOP_STYLE); + PR_BORDER_TOP_STYLE, PR_BORDER_TOP_STYLE); corr.setRelative(true); addPropertyMaker("border-start-style", m); @@ -711,7 +711,7 @@ public final class FOPropertyMapping implements Constants { m.getSubpropMaker(CP_CONDITIONALITY).setDefault("discard"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_LEFT_WIDTH, PR_BORDER_RIGHT_WIDTH, - PR_BORDER_TOP_WIDTH); + PR_BORDER_TOP_WIDTH, PR_BORDER_TOP_WIDTH); corr.setRelative(true); addPropertyMaker("border-start-width", m); @@ -722,7 +722,7 @@ public final class FOPropertyMapping implements Constants { m.setDefault("black"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_RIGHT_COLOR, PR_BORDER_LEFT_COLOR, - PR_BORDER_BOTTOM_COLOR); + PR_BORDER_BOTTOM_COLOR, PR_BORDER_BOTTOM_COLOR); corr.setRelative(true); addPropertyMaker("border-end-color", m); @@ -731,7 +731,7 @@ public final class FOPropertyMapping implements Constants { m.useGeneric(genericBorderStyle); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_RIGHT_STYLE, PR_BORDER_LEFT_STYLE, - PR_BORDER_BOTTOM_STYLE); + PR_BORDER_BOTTOM_STYLE, PR_BORDER_BOTTOM_STYLE); corr.setRelative(true); addPropertyMaker("border-end-style", m); @@ -741,7 +741,7 @@ public final class FOPropertyMapping implements Constants { m.getSubpropMaker(CP_CONDITIONALITY).setDefault("discard"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_RIGHT_WIDTH, PR_BORDER_LEFT_WIDTH, - PR_BORDER_BOTTOM_WIDTH); + PR_BORDER_BOTTOM_WIDTH, PR_BORDER_BOTTOM_WIDTH); corr.setRelative(true); addPropertyMaker("border-end-width", m); @@ -755,7 +755,7 @@ public final class FOPropertyMapping implements Constants { m.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_BEFORE_COLOR, PR_BORDER_BEFORE_COLOR, - PR_BORDER_START_COLOR); + PR_BORDER_START_COLOR, PR_BORDER_START_COLOR); addPropertyMaker("border-top-color", m); // border-top-style @@ -766,7 +766,7 @@ public final class FOPropertyMapping implements Constants { m.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_BEFORE_STYLE, PR_BORDER_BEFORE_STYLE, - PR_BORDER_START_STYLE); + PR_BORDER_START_STYLE, PR_BORDER_START_STYLE); addPropertyMaker("border-top-style", m); // border-top-width @@ -778,7 +778,7 @@ public final class FOPropertyMapping implements Constants { bwm.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(bwm); corr.setCorresponding(PR_BORDER_BEFORE_WIDTH, PR_BORDER_BEFORE_WIDTH, - PR_BORDER_START_WIDTH); + PR_BORDER_START_WIDTH, PR_BORDER_START_WIDTH); addPropertyMaker("border-top-width", bwm); // border-bottom-color @@ -791,7 +791,7 @@ public final class FOPropertyMapping implements Constants { m.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_AFTER_COLOR, PR_BORDER_AFTER_COLOR, - PR_BORDER_END_COLOR); + PR_BORDER_END_COLOR, PR_BORDER_END_COLOR); addPropertyMaker("border-bottom-color", m); // border-bottom-style @@ -802,7 +802,7 @@ public final class FOPropertyMapping implements Constants { m.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_AFTER_STYLE, PR_BORDER_AFTER_STYLE, - PR_BORDER_END_STYLE); + PR_BORDER_END_STYLE, PR_BORDER_END_STYLE); addPropertyMaker("border-bottom-style", m); // border-bottom-width @@ -814,7 +814,7 @@ public final class FOPropertyMapping implements Constants { bwm.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(bwm); corr.setCorresponding(PR_BORDER_AFTER_WIDTH, PR_BORDER_AFTER_WIDTH, - PR_BORDER_END_WIDTH); + PR_BORDER_END_WIDTH, PR_BORDER_END_WIDTH); addPropertyMaker("border-bottom-width", bwm); // border-left-color @@ -827,7 +827,7 @@ public final class FOPropertyMapping implements Constants { m.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_START_COLOR, PR_BORDER_END_COLOR, - PR_BORDER_AFTER_COLOR); + PR_BORDER_AFTER_COLOR, PR_BORDER_BEFORE_COLOR); addPropertyMaker("border-left-color", m); // border-left-style @@ -838,7 +838,7 @@ public final class FOPropertyMapping implements Constants { m.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_START_STYLE, PR_BORDER_END_STYLE, - PR_BORDER_AFTER_STYLE); + PR_BORDER_AFTER_STYLE, PR_BORDER_BEFORE_STYLE); addPropertyMaker("border-left-style", m); // border-left-width @@ -850,7 +850,7 @@ public final class FOPropertyMapping implements Constants { bwm.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(bwm); corr.setCorresponding(PR_BORDER_START_WIDTH, PR_BORDER_END_WIDTH, - PR_BORDER_AFTER_WIDTH); + PR_BORDER_AFTER_WIDTH, PR_BORDER_BEFORE_WIDTH); addPropertyMaker("border-left-width", bwm); // border-right-color @@ -863,7 +863,7 @@ public final class FOPropertyMapping implements Constants { m.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_END_COLOR, PR_BORDER_START_COLOR, - PR_BORDER_BEFORE_COLOR); + PR_BORDER_BEFORE_COLOR, PR_BORDER_AFTER_COLOR); addPropertyMaker("border-right-color", m); // border-right-style @@ -874,7 +874,7 @@ public final class FOPropertyMapping implements Constants { m.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_BORDER_END_STYLE, PR_BORDER_START_STYLE, - PR_BORDER_BEFORE_STYLE); + PR_BORDER_BEFORE_STYLE, PR_BORDER_AFTER_STYLE); addPropertyMaker("border-right-style", m); // border-right-width @@ -886,7 +886,7 @@ public final class FOPropertyMapping implements Constants { bwm.addShorthand(generics[PR_BORDER]); corr = new CorrespondingPropertyMaker(bwm); corr.setCorresponding(PR_BORDER_END_WIDTH, PR_BORDER_START_WIDTH, - PR_BORDER_BEFORE_WIDTH); + PR_BORDER_BEFORE_WIDTH, PR_BORDER_AFTER_WIDTH); addPropertyMaker("border-right-width", bwm); // padding-before @@ -895,7 +895,7 @@ public final class FOPropertyMapping implements Constants { m.getSubpropMaker(CP_CONDITIONALITY).setDefault("discard"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_PADDING_TOP, PR_PADDING_TOP, - PR_PADDING_RIGHT); + PR_PADDING_RIGHT, PR_PADDING_LEFT); corr.setRelative(true); addPropertyMaker("padding-before", m); @@ -905,7 +905,7 @@ public final class FOPropertyMapping implements Constants { m.getSubpropMaker(CP_CONDITIONALITY).setDefault("discard"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_PADDING_BOTTOM, PR_PADDING_BOTTOM, - PR_PADDING_LEFT); + PR_PADDING_LEFT, PR_PADDING_RIGHT); corr.setRelative(true); addPropertyMaker("padding-after", m); @@ -915,7 +915,7 @@ public final class FOPropertyMapping implements Constants { m.getSubpropMaker(CP_CONDITIONALITY).setDefault("discard"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_PADDING_LEFT, PR_PADDING_RIGHT, - PR_PADDING_TOP); + PR_PADDING_TOP, PR_PADDING_TOP); corr.setRelative(true); addPropertyMaker("padding-start", m); @@ -925,7 +925,7 @@ public final class FOPropertyMapping implements Constants { m.getSubpropMaker(CP_CONDITIONALITY).setDefault("discard"); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_PADDING_RIGHT, PR_PADDING_LEFT, - PR_PADDING_BOTTOM); + PR_PADDING_BOTTOM, PR_PADDING_BOTTOM); corr.setRelative(true); addPropertyMaker("padding-end", m); @@ -934,7 +934,7 @@ public final class FOPropertyMapping implements Constants { m.useGeneric(genericPadding); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_PADDING_BEFORE, PR_PADDING_BEFORE, - PR_PADDING_START); + PR_PADDING_START, PR_PADDING_START); addPropertyMaker("padding-top", m); // padding-bottom @@ -942,7 +942,7 @@ public final class FOPropertyMapping implements Constants { m.useGeneric(genericPadding); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_PADDING_AFTER, PR_PADDING_AFTER, - PR_PADDING_END); + PR_PADDING_END, PR_PADDING_END); addPropertyMaker("padding-bottom", m); // padding-left @@ -950,7 +950,7 @@ public final class FOPropertyMapping implements Constants { m.useGeneric(genericPadding); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_PADDING_START, PR_PADDING_END, - PR_PADDING_AFTER); + PR_PADDING_AFTER, PR_PADDING_BEFORE); addPropertyMaker("padding-left", m); // padding-right @@ -958,7 +958,7 @@ public final class FOPropertyMapping implements Constants { m.useGeneric(genericPadding); corr = new CorrespondingPropertyMaker(m); corr.setCorresponding(PR_PADDING_END, PR_PADDING_START, - PR_PADDING_BEFORE); + PR_PADDING_BEFORE, PR_PADDING_AFTER); addPropertyMaker("padding-right", m); } @@ -1151,7 +1151,7 @@ public final class FOPropertyMapping implements Constants { m = new SpaceProperty.Maker(PR_SPACE_BEFORE); m.useGeneric(genericSpace); corr = new SpacePropertyMaker(m); - corr.setCorresponding(PR_MARGIN_TOP, PR_MARGIN_TOP, PR_MARGIN_RIGHT); + corr.setCorresponding(PR_MARGIN_TOP, PR_MARGIN_TOP, PR_MARGIN_RIGHT, PR_MARGIN_LEFT); corr.setUseParent(false); corr.setRelative(true); addPropertyMaker("space-before", m); @@ -1160,7 +1160,7 @@ public final class FOPropertyMapping implements Constants { m = new SpaceProperty.Maker(PR_SPACE_AFTER); m.useGeneric(genericSpace); corr = new SpacePropertyMaker(m); - corr.setCorresponding(PR_MARGIN_BOTTOM, PR_MARGIN_BOTTOM, PR_MARGIN_LEFT); + corr.setCorresponding(PR_MARGIN_BOTTOM, PR_MARGIN_BOTTOM, PR_MARGIN_LEFT, PR_MARGIN_RIGHT); corr.setUseParent(false); corr.setRelative(true); addPropertyMaker("space-after", m); @@ -1171,14 +1171,14 @@ public final class FOPropertyMapping implements Constants { m.setDefault("0pt"); m.setPercentBase(LengthBase.CONTAINING_REFAREA_WIDTH); IndentPropertyMaker sCorr = new IndentPropertyMaker(m); - sCorr.setCorresponding(PR_MARGIN_LEFT, PR_MARGIN_RIGHT, PR_MARGIN_TOP); + sCorr.setCorresponding(PR_MARGIN_LEFT, PR_MARGIN_RIGHT, PR_MARGIN_TOP, PR_MARGIN_TOP); sCorr.setUseParent(false); sCorr.setRelative(true); sCorr.setPaddingCorresponding(new int[] { - PR_PADDING_LEFT, PR_PADDING_RIGHT, PR_PADDING_TOP + PR_PADDING_LEFT, PR_PADDING_RIGHT, PR_PADDING_TOP, PR_PADDING_TOP }); sCorr.setBorderWidthCorresponding(new int[] { - PR_BORDER_LEFT_WIDTH, PR_BORDER_RIGHT_WIDTH, PR_BORDER_TOP_WIDTH + PR_BORDER_LEFT_WIDTH, PR_BORDER_RIGHT_WIDTH, PR_BORDER_TOP_WIDTH, PR_BORDER_TOP_WIDTH }); addPropertyMaker("start-indent", m); @@ -1188,14 +1188,16 @@ public final class FOPropertyMapping implements Constants { m.setDefault("0pt"); m.setPercentBase(LengthBase.CONTAINING_REFAREA_WIDTH); IndentPropertyMaker eCorr = new IndentPropertyMaker(m); - eCorr.setCorresponding(PR_MARGIN_RIGHT, PR_MARGIN_LEFT, PR_MARGIN_BOTTOM); + eCorr.setCorresponding(PR_MARGIN_RIGHT, PR_MARGIN_LEFT, + PR_MARGIN_BOTTOM, PR_MARGIN_BOTTOM); eCorr.setUseParent(false); eCorr.setRelative(true); eCorr.setPaddingCorresponding(new int[] { - PR_PADDING_RIGHT, PR_PADDING_LEFT, PR_PADDING_BOTTOM + PR_PADDING_RIGHT, PR_PADDING_LEFT, PR_PADDING_BOTTOM, PR_PADDING_BOTTOM }); eCorr.setBorderWidthCorresponding(new int[] { - PR_BORDER_RIGHT_WIDTH, PR_BORDER_LEFT_WIDTH, PR_BORDER_BOTTOM_WIDTH + PR_BORDER_RIGHT_WIDTH, PR_BORDER_LEFT_WIDTH, + PR_BORDER_BOTTOM_WIDTH, PR_BORDER_BOTTOM_WIDTH }); addPropertyMaker("end-indent", m); } @@ -1352,10 +1354,10 @@ public final class FOPropertyMapping implements Constants { m.addSubpropMaker(l); pdim = new DimensionPropertyMaker(m); - pdim.setCorresponding(PR_HEIGHT, PR_HEIGHT, PR_WIDTH); + pdim.setCorresponding(PR_HEIGHT, PR_HEIGHT, PR_WIDTH, PR_WIDTH); pdim.setExtraCorresponding(new int[][] { - {PR_MIN_HEIGHT, PR_MIN_HEIGHT, PR_MIN_WIDTH, }, - {PR_MAX_HEIGHT, PR_MAX_HEIGHT, PR_MAX_WIDTH, } + {PR_MIN_HEIGHT, PR_MIN_HEIGHT, PR_MIN_WIDTH, PR_MIN_WIDTH}, + {PR_MAX_HEIGHT, PR_MAX_HEIGHT, PR_MAX_WIDTH, PR_MAX_WIDTH} }); pdim.setRelative(true); m.setCorresponding(pdim); @@ -1419,10 +1421,10 @@ public final class FOPropertyMapping implements Constants { pdim = new DimensionPropertyMaker(m); pdim.setRelative(true); - pdim.setCorresponding(PR_WIDTH, PR_WIDTH, PR_HEIGHT); + pdim.setCorresponding(PR_WIDTH, PR_WIDTH, PR_HEIGHT, PR_HEIGHT); pdim.setExtraCorresponding(new int[][] { - {PR_MIN_WIDTH, PR_MIN_WIDTH, PR_MIN_HEIGHT, }, - {PR_MAX_WIDTH, PR_MAX_WIDTH, PR_MAX_HEIGHT, } + {PR_MIN_WIDTH, PR_MIN_WIDTH, PR_MIN_HEIGHT, PR_MIN_HEIGHT }, + {PR_MAX_WIDTH, PR_MAX_WIDTH, PR_MAX_HEIGHT, PR_MIN_HEIGHT } }); m.setCorresponding(pdim); addPropertyMaker("inline-progression-dimension", m); @@ -1491,6 +1493,37 @@ public final class FOPropertyMapping implements Constants { addPropertyMaker("fox:block-progression-unit", l); } + private Property calcWritingModeDependent ( int pv, int wm ) { + if ( pv == EN_LEFT ) { + if ( wm == Constants.EN_LR_TB ) { + pv = EN_START; + } else if ( wm == Constants.EN_RL_TB ) { + pv = EN_END; + } else { + pv = EN_START; + } + } else if ( pv == EN_RIGHT ) { + if ( wm == Constants.EN_LR_TB ) { + pv = EN_END; + } else if ( wm == Constants.EN_RL_TB ) { + pv = EN_START; + } else { + pv = EN_END; + } + } + return makeWritingModeDependentEnum ( pv ); + } + + private Property makeWritingModeDependentEnum ( int pv ) { + if ( pv == EN_START ) { + return getEnumProperty ( EN_START, "START" ); + } else if ( pv == EN_END ) { + return getEnumProperty ( EN_END, "END" ); + } else { + return null; + } + } + private void createBlockAndLineProperties() { // CSOK: MethodLength PropertyMaker m; @@ -1576,16 +1609,29 @@ public final class FOPropertyMapping implements Constants { addPropertyMaker("white-space-treatment", m); // text-align TODO: make it a StringProperty with enums. - m = new EnumProperty.Maker(PR_TEXT_ALIGN); + m = new EnumProperty.Maker(PR_TEXT_ALIGN) { + public Property get(int subpropId, PropertyList propertyList, + boolean bTryInherit, boolean bTryDefault) throws PropertyException { + Property p = super.get(subpropId, propertyList, bTryInherit, bTryDefault); + if ( p != null ) { + int pv = p.getEnum(); + if ( ( pv == EN_LEFT ) || ( pv == EN_RIGHT ) ) { + p = calcWritingModeDependent + ( pv, propertyList.get(Constants.PR_WRITING_MODE).getEnum() ); + } + } + return p; + } + }; m.setInherited(true); - // Note: both 'end', 'right' and 'outside' are mapped to END - // both 'start', 'left' and 'inside' are mapped to START m.addEnum("center", getEnumProperty(EN_CENTER, "CENTER")); m.addEnum("end", getEnumProperty(EN_END, "END")); - m.addEnum("right", getEnumProperty(EN_END, "END")); m.addEnum("start", getEnumProperty(EN_START, "START")); - m.addEnum("left", getEnumProperty(EN_START, "START")); m.addEnum("justify", getEnumProperty(EN_JUSTIFY, "JUSTIFY")); + // [GA] must defer writing-mode relative mapping of left/right + m.addEnum("left", getEnumProperty(EN_LEFT, "LEFT")); + m.addEnum("right", getEnumProperty(EN_RIGHT, "RIGHT")); + // [GA] inside and outside are not correctly implemented by the following mapping m.addEnum("inside", getEnumProperty(EN_START, "START")); m.addEnum("outside", getEnumProperty(EN_END, "END")); m.setDefault("start"); @@ -1605,7 +1651,6 @@ public final class FOPropertyMapping implements Constants { } return p; } - private Property calcRelative(PropertyList propertyList) throws PropertyException { Property corresponding = propertyList.get(PR_TEXT_ALIGN); if (corresponding == null) { @@ -1620,6 +1665,12 @@ public final class FOPropertyMapping implements Constants { return getEnumProperty(EN_START, "START"); } else if (correspondingValue == EN_CENTER) { return getEnumProperty(EN_CENTER, "CENTER"); + } else if (correspondingValue == EN_LEFT) { + return calcWritingModeDependent + ( EN_LEFT, propertyList.get(Constants.PR_WRITING_MODE).getEnum() ); + } else if (correspondingValue == EN_RIGHT) { + return calcWritingModeDependent + ( EN_RIGHT, propertyList.get(Constants.PR_WRITING_MODE).getEnum() ); } else { return null; } @@ -2171,6 +2222,12 @@ public final class FOPropertyMapping implements Constants { m.addEnum("auto", getEnumProperty(EN_AUTO, "AUTO")); m.setDefault("auto"); addPropertyMaker("letter-value", m); + + // fox:alt-text, used for accessibility + m = new StringProperty.Maker(PR_X_NUMBER_CONVERSION_FEATURES); + m.setInherited(false); + m.setDefault(""); + addPropertyMaker("fox:number-conversion-features", m); } private void createPaginationAndLayoutProperties() { @@ -2512,6 +2569,7 @@ public final class FOPropertyMapping implements Constants { m.addEnum("lr-tb", getEnumProperty(EN_LR_TB, "LR_TB")); m.addEnum("rl-tb", getEnumProperty(EN_RL_TB, "RL_TB")); m.addEnum("tb-rl", getEnumProperty(EN_TB_RL, "TB_RL")); + m.addEnum("tb-lr", getEnumProperty(EN_TB_LR, "TB_LR")); m.addKeyword("lr", "lr-tb"); m.addKeyword("rl", "rl-tb"); m.addKeyword("tb", "tb-rl"); diff --git a/src/java/org/apache/fop/fo/FOText.java b/src/java/org/apache/fop/fo/FOText.java index 43b58685f..5dfe0766b 100644 --- a/src/java/org/apache/fop/fo/FOText.java +++ b/src/java/org/apache/fop/fo/FOText.java @@ -21,12 +21,15 @@ package org.apache.fop.fo; import java.awt.Color; import java.nio.CharBuffer; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.Stack; import org.xml.sax.Locator; import org.apache.fop.accessibility.StructureTreeElement; import org.apache.fop.apps.FOPException; +import org.apache.fop.complexscripts.bidi.DelimitedTextRange; import org.apache.fop.datatypes.Length; import org.apache.fop.fo.flow.Block; import org.apache.fop.fo.properties.CommonFont; @@ -45,7 +48,8 @@ public class FOText extends FONode implements CharSequence { /** the <code>CharBuffer</code> containing the text */ private CharBuffer charBuffer; - /** properties relevant for #PCDATA */ + // The value of FO traits (refined properties) that apply to #PCDATA + // (aka implicit sequence of fo:character) private CommonFont commonFont; private CommonHyphenation commonHyphenation; private Color color; @@ -58,6 +62,10 @@ public class FOText extends FONode implements CharSequence { private Property wordSpacing; private int wrapOption; private Length baselineShift; + private String country; + private String language; + private String script; + // End of trait values /** * Points to the previous FOText object created within the current @@ -82,6 +90,12 @@ public class FOText extends FONode implements CharSequence { private StructureTreeElement structureTreeElement; + /* bidi levels */ + private int[] bidiLevels; + + /* advanced script processing state */ + private Map/*<MapRange,String>*/ mappings; + private static final int IS_WORD_CHAR_FALSE = 0; private static final int IS_WORD_CHAR_TRUE = 1; private static final int IS_WORD_CHAR_MAYBE = 2; @@ -98,21 +112,31 @@ public class FOText extends FONode implements CharSequence { /** {@inheritDoc} */ protected void characters(char[] data, int start, int length, PropertyList list, Locator locator) throws FOPException { - - if (this.charBuffer == null) { + if (charBuffer == null) { // buffer not yet initialized, do so now - this.charBuffer = CharBuffer.allocate(length); + int newLength = ( length < 16 ) ? 16 : length; + charBuffer = CharBuffer.allocate(newLength); } else { // allocate a larger buffer, and transfer contents - int newLength = this.charBuffer.limit() + length; - CharBuffer newBuffer = CharBuffer.allocate(newLength); - this.charBuffer.rewind(); - newBuffer.put(this.charBuffer); - this.charBuffer = newBuffer; + int requires = charBuffer.position() + length; + int capacity = charBuffer.capacity(); + if ( requires > capacity ) { + int newCapacity = capacity * 2; + if ( requires > newCapacity ) { + newCapacity = requires; + } + CharBuffer newBuffer = CharBuffer.allocate(newCapacity); + charBuffer.rewind(); + newBuffer.put(charBuffer); + charBuffer = newBuffer; + } } + // extend limit to capacity + charBuffer.limit(charBuffer.capacity()); // append characters - this.charBuffer.put(data, start, length); - + charBuffer.put(data, start, length); + // shrink limit to position + charBuffer.limit(charBuffer.position()); } /** @@ -135,10 +159,10 @@ public class FOText extends FONode implements CharSequence { if (removeChildren) { // not really removing, just make sure the char buffer // pointed to is really a different one - if (this.charBuffer != null) { - ft.charBuffer = CharBuffer.allocate(this.charBuffer.limit()); - this.charBuffer.rewind(); - ft.charBuffer.put(this.charBuffer); + if (charBuffer != null) { + ft.charBuffer = CharBuffer.allocate(charBuffer.limit()); + charBuffer.rewind(); + ft.charBuffer.put(charBuffer); ft.charBuffer.rewind(); } } @@ -163,10 +187,16 @@ public class FOText extends FONode implements CharSequence { this.wrapOption = pList.get(Constants.PR_WRAP_OPTION).getEnum(); this.textDecoration = pList.getTextDecorationProps(); this.baselineShift = pList.get(Constants.PR_BASELINE_SHIFT).getLength(); + this.country = pList.get(Constants.PR_COUNTRY).getString(); + this.language = pList.get(Constants.PR_LANGUAGE).getString(); + this.script = pList.get(Constants.PR_SCRIPT).getString(); } /** {@inheritDoc} */ protected void endOfNode() throws FOPException { + if ( charBuffer != null ) { + charBuffer.rewind(); + } super.endOfNode(); getFOEventHandler().characters(this); } @@ -187,20 +217,20 @@ public class FOText extends FONode implements CharSequence { */ public boolean willCreateArea() { if (whiteSpaceCollapse == Constants.EN_FALSE - && this.charBuffer.limit() > 0) { + && charBuffer.limit() > 0) { return true; } char ch; - this.charBuffer.rewind(); - while (this.charBuffer.hasRemaining()) { - ch = this.charBuffer.get(); + charBuffer.rewind(); + while (charBuffer.hasRemaining()) { + ch = charBuffer.get(); if (!((ch == CharUtilities.SPACE) || (ch == CharUtilities.LINEFEED_CHAR) || (ch == CharUtilities.CARRIAGE_RETURN) || (ch == CharUtilities.TAB))) { // not whitespace - this.charBuffer.rewind(); + charBuffer.rewind(); return true; } } @@ -243,13 +273,13 @@ public class FOText extends FONode implements CharSequence { return; } - this.charBuffer.rewind(); - CharBuffer tmp = this.charBuffer.slice(); + charBuffer.rewind(); + CharBuffer tmp = charBuffer.slice(); char c; - int lim = this.charBuffer.limit(); + int lim = charBuffer.limit(); int pos = -1; while (++pos < lim) { - c = this.charBuffer.get(); + c = charBuffer.get(); switch (textTransform) { case Constants.EN_UPPERCASE: tmp.put(Character.toUpperCase(c)); @@ -374,25 +404,25 @@ public class FOText extends FONode implements CharSequence { * @return The previous FOText node in this Block; null, if this is the * first FOText in this Block. */ - public FOText getPrevFOTextThisBlock () { - return prevFOTextThisBlock; - } + //public FOText getPrevFOTextThisBlock () { + // return prevFOTextThisBlock; + //} /** * @return The next FOText node in this Block; null if this is the last * FOText in this Block; null if subsequent FOText nodes have not yet been * processed. */ - public FOText getNextFOTextThisBlock () { - return nextFOTextThisBlock; - } + //public FOText getNextFOTextThisBlock () { + // return nextFOTextThisBlock; + //} /** * @return The nearest ancestor block object which contains this FOText. */ - public Block getAncestorBlock () { - return ancestorBlock; - } + //public Block getAncestorBlock () { + // return ancestorBlock; + //} /** * Determines whether the input char should be considered part of a @@ -485,6 +515,9 @@ public class FOText extends FONode implements CharSequence { private boolean canRemove = false; private boolean canReplace = false; + public TextCharIterator() { + } + /** {@inheritDoc} */ public boolean hasNext() { return (this.currentPosition < charBuffer.limit()); @@ -556,67 +589,88 @@ public class FOText extends FONode implements CharSequence { } /** - * @return the "color" property. + * @return the "color" trait. */ public Color getColor() { return color; } /** - * @return the "keep-together" property. + * @return the "keep-together" trait. */ public KeepProperty getKeepTogether() { return keepTogether; } /** - * @return the "letter-spacing" property. + * @return the "letter-spacing" trait. */ public Property getLetterSpacing() { return letterSpacing; } /** - * @return the "line-height" property. + * @return the "line-height" trait. */ public SpaceProperty getLineHeight() { return lineHeight; } /** - * @return the "white-space-treatment" property + * @return the "white-space-treatment" trait */ public int getWhitespaceTreatment() { return whiteSpaceTreatment; } /** - * @return the "word-spacing" property. + * @return the "word-spacing" trait. */ public Property getWordSpacing() { return wordSpacing; } /** - * @return the "wrap-option" property. + * @return the "wrap-option" trait. */ public int getWrapOption() { return wrapOption; } - /** @return the "text-decoration" property. */ + /** @return the "text-decoration" trait. */ public CommonTextDecoration getTextDecoration() { return textDecoration; } - /** @return the baseline-shift property */ + /** @return the baseline-shift trait */ public Length getBaseLineShift() { return baselineShift; } + /** @return the country trait */ + public String getCountry() { + return country; + } + + /** @return the language trait */ + public String getLanguage() { + return language; + } + + /** @return the script trait */ + public String getScript() { + return script; + } + /** {@inheritDoc} */ public String toString() { - return (this.charBuffer == null) ? "" : this.charBuffer.toString(); + if ( charBuffer == null ) { + return ""; + } else { + CharBuffer cb = charBuffer.duplicate(); + cb.rewind(); + return cb.toString(); + } } /** {@inheritDoc} */ @@ -640,29 +694,34 @@ public class FOText extends FONode implements CharSequence { /** {@inheritDoc} */ public char charAt(int position) { - return this.charBuffer.get(position); + return charBuffer.get(position); } /** {@inheritDoc} */ public CharSequence subSequence(int start, int end) { - return this.charBuffer.subSequence(start, end); + return charBuffer.subSequence(start, end); } /** {@inheritDoc} */ public int length() { - return this.charBuffer.limit(); + return charBuffer.limit(); } /** * Resets the backing <code>java.nio.CharBuffer</code> */ public void resetBuffer() { - if (this.charBuffer != null) { - this.charBuffer.rewind(); + if (charBuffer != null) { + charBuffer.rewind(); } } @Override + public boolean isDelimitedTextRangeBoundary ( int boundary ) { + return false; + } + + @Override public void setStructureTreeElement(StructureTreeElement structureTreeElement) { this.structureTreeElement = structureTreeElement; } @@ -672,4 +731,192 @@ public class FOText extends FONode implements CharSequence { return structureTreeElement; } + /** + * Set bidirectional level over interval [start,end). + * @param level the resolved level + * @param start the starting index of interval + * @param end the ending index of interval + */ + public void setBidiLevel ( int level, int start, int end ) { + if ( start < end ) { + if ( bidiLevels == null ) { + bidiLevels = new int [ length() ]; + } + for ( int i = start, n = end; i < n; i++ ) { + bidiLevels [ i ] = level; + } + if ( parent != null ) { + ( (FObj) parent ).setBidiLevel ( level ); + } + } else { + assert start < end; + } + } + + /** + * Obtain bidirectional level of each character + * represented by this FOText. + * @return a (possibly empty) array of bidi levels or null + * in case no bidi levels have been assigned + */ + public int[] getBidiLevels() { + return bidiLevels; + } + + /** + * Obtain bidirectional level of each character over + * interval [start,end). + * @param start the starting index of interval + * @param end the ending index of interval + * @return a (possibly empty) array of bidi levels or null + * in case no bidi levels have been assigned + */ + public int[] getBidiLevels ( int start, int end ) { + if ( this.bidiLevels != null ) { + assert start <= end; + int n = end - start; + int[] bidiLevels = new int [ n ]; + for ( int i = 0; i < n; i++ ) { + bidiLevels[i] = this.bidiLevels [ start + i ]; + } + return bidiLevels; + } else { + return null; + } + } + + /** + * Obtain bidirectional level of character at + * specified position, which must be a non-negative integer + * less than the length of this FO. + * @param position an offset position into FO's characters + * @return a resolved bidi level or -1 if default + * @throws IndexOutOfBoundsException if position is not non-negative integer + * or is greater than or equal to length + */ + public int bidiLevelAt ( int position ) throws IndexOutOfBoundsException { + if ( ( position < 0 ) || ( position >= length() ) ) { + throw new IndexOutOfBoundsException(); + } else if ( bidiLevels != null ) { + return bidiLevels [ position ]; + } else { + return -1; + } + } + + /** + * Add characters mapped by script substitution processing. + * @param start index in character buffer + * @param end index in character buffer + * @param mappedChars sequence of character codes denoting substituted characters + */ + public void addMapping ( int start, int end, CharSequence mappedChars ) { + if ( mappings == null ) { + mappings = new java.util.HashMap(); + } + mappings.put ( new MapRange ( start, end ), mappedChars.toString() ); + } + + /** + * Determine if characters over specific interval have a mapping. + * @param start index in character buffer + * @param end index in character buffer + * @return true if a mapping exist such that the mapping's interval is coincident to + * [start,end) + */ + public boolean hasMapping ( int start, int end ) { + return ( mappings != null ) && ( mappings.containsKey ( new MapRange ( start, end ) ) ); + } + + /** + * Obtain mapping of characters over specific interval. + * @param start index in character buffer + * @param end index in character buffer + * @return a string of characters representing the mapping over the interval + * [start,end) + */ + public String getMapping ( int start, int end ) { + if ( mappings != null ) { + return (String) mappings.get ( new MapRange ( start, end ) ); + } else { + return null; + } + } + + /** + * Obtain length of mapping of characters over specific interval. + * @param start index in character buffer + * @param end index in character buffer + * @return the length of the mapping (if present) or zero + */ + public int getMappingLength ( int start, int end ) { + if ( mappings != null ) { + return ( (String) mappings.get ( new MapRange ( start, end ) ) ) .length(); + } else { + return 0; + } + } + + /** + * Obtain bidirectional levels of mapping of characters over specific interval. + * @param start index in character buffer + * @param end index in character buffer + * @return a (possibly empty) array of bidi levels or null + * in case no bidi levels have been assigned + */ + public int[] getMappingBidiLevels ( int start, int end ) { + if ( hasMapping ( start, end ) ) { + int nc = end - start; + int nm = getMappingLength ( start, end ); + int[] la = getBidiLevels ( start, end ); + if ( la == null ) { + return null; + } else if ( nm == nc ) { // mapping is same length as mapped range + return la; + } else if ( nm > nc ) { // mapping is longer than mapped range + int[] ma = new int [ nm ]; + System.arraycopy ( la, 0, ma, 0, la.length ); + for ( int i = la.length, + n = ma.length, l = ( i > 0 ) ? la [ i - 1 ] : 0; i < n; i++ ) { + ma [ i ] = l; + } + return ma; + } else { // mapping is shorter than mapped range + int[] ma = new int [ nm ]; + System.arraycopy ( la, 0, ma, 0, ma.length ); + return ma; + } + } else { + return getBidiLevels ( start, end ); + } + } + + @Override + protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) { + if ( currentRange != null ) { + currentRange.append ( charIterator(), this ); + } + return ranges; + } + + private static class MapRange { + private int start; + private int end; + MapRange(int start, int end) { + this.start = start; + this.end = end; + } + public int hashCode() { + return ( start * 31 ) + end; + } + public boolean equals ( Object o ) { + if ( o instanceof MapRange ) { + MapRange r = (MapRange) o; + return ( r.start == start ) && ( r.end == end ); + } else { + return false; + } + } + } + } diff --git a/src/java/org/apache/fop/fo/FObj.java b/src/java/org/apache/fop/fo/FObj.java index 27d0711f2..f1d78acf1 100644 --- a/src/java/org/apache/fop/fo/FObj.java +++ b/src/java/org/apache/fop/fo/FObj.java @@ -67,6 +67,8 @@ public abstract class FObj extends FONode implements Constants { /** Markers added to this element. */ private Map markers = null; + private int bidiLevel = -1; + // The value of properties relevant for all fo objects private String id = null; // End of property values @@ -118,9 +120,7 @@ public abstract class FObj extends FONode implements Constants { throws FOPException { setLocator(locator); pList.addAttributesToList(attlist); - if (!inMarker() - || "marker".equals(elementName)) { - pList.setWritingMode(); + if (!inMarker() || "marker".equals(elementName)) { bind(pList); } } @@ -558,6 +558,63 @@ public abstract class FObj extends FONode implements Constants { return "fo"; } + /** {@inheritDoc} */ + public boolean isBidiRangeBlockItem() { + String ns = getNamespaceURI(); + String ln = getLocalName(); + return !isNeutralItem(ns, ln) && isBlockItem(ns, ln); + } + + /** + * Recursively set resolved bidirectional level of FO (and its ancestors) if + * and only if it is non-negative and if either the current value is reset (-1) + * or the new value is less than the current value. + * @param bidiLevel a non-negative bidi embedding level + */ + public void setBidiLevel(int bidiLevel) { + assert bidiLevel >= 0; + if ( bidiLevel >= 0 ) { + if ( ( this.bidiLevel < 0 ) || ( bidiLevel < this.bidiLevel ) ) { + this.bidiLevel = bidiLevel; + if ( parent != null ) { + FObj foParent = (FObj) parent; + int parentBidiLevel = foParent.getBidiLevel(); + if ( ( parentBidiLevel < 0 ) || ( bidiLevel < parentBidiLevel ) ) { + foParent.setBidiLevel ( bidiLevel ); + } + } + } + } + } + + /** + * Obtain resolved bidirectional level of FO. + * @return either a non-negative bidi embedding level or -1 + * in case no bidi levels have been assigned + */ + public int getBidiLevel() { + return bidiLevel; + } + + /** + * Obtain resolved bidirectional level of FO or nearest FO + * ancestor that has a resolved level. + * @return either a non-negative bidi embedding level or -1 + * in case no bidi levels have been assigned to this FO or + * any ancestor + */ + public int getBidiLevelRecursive() { + for ( FONode fn = this; fn != null; fn = fn.getParent() ) { + if ( fn instanceof FObj ) { + int level = ( (FObj) fn).getBidiLevel(); + if ( level >= 0 ) { + return level; + } + } + } + return -1; + } + /** * Add a new extension attachment to this FObj. * (see org.apache.fop.fo.FONode for details) diff --git a/src/java/org/apache/fop/fo/PropertyList.java b/src/java/org/apache/fop/fo/PropertyList.java index 65ebd40a2..0852c6e29 100644 --- a/src/java/org/apache/fop/fo/PropertyList.java +++ b/src/java/org/apache/fop/fo/PropertyList.java @@ -46,8 +46,6 @@ import org.apache.fop.fo.properties.PropertyMaker; */ public abstract class PropertyList { - // writing-mode index - private int writingMode; private static boolean[] inheritableProperty; @@ -220,58 +218,60 @@ public abstract class PropertyList { } /** - * Set writing mode for this FO. - * Use that from the nearest ancestor, including self, which generates - * reference areas, or from root FO if no ancestor found. - * @throws PropertyException ... - */ - public void setWritingMode() throws PropertyException { - FObj p = fobj.findNearestAncestorFObj(); - // If this is a reference area or the root, use the property value. - if (fobj.generatesReferenceAreas() || p == null) { - writingMode = get(Constants.PR_WRITING_MODE).getEnum(); - } else { - // Otherwise get the writing mode value from the parent. - writingMode = getParentPropertyList().getWritingMode(); - } - } - - /** - * Return the "writing-mode" property value. - * @return the "writing-mode" property value. - */ - public int getWritingMode() { - return writingMode; - } - - - /** - * Uses the stored writingMode. + * Select a writing mode dependent property ID based on value of writing mode property. * @param lrtb the property ID to return under lrtb writingmode. * @param rltb the property ID to return under rltb writingmode. * @param tbrl the property ID to return under tbrl writingmode. + * @param tblr the property ID to return under tblr writingmode. * @return one of the property IDs, depending on the writing mode. */ - public int getWritingMode(int lrtb, int rltb, int tbrl) { - switch (writingMode) { - case Constants.EN_LR_TB: return lrtb; - case Constants.EN_RL_TB: return rltb; - case Constants.EN_TB_RL: return tbrl; + public int selectFromWritingMode(int lrtb, int rltb, int tbrl, int tblr) { + int propID; + try { + switch (get(Constants.PR_WRITING_MODE).getEnum()) { + case Constants.EN_LR_TB: + propID = lrtb; + break; + case Constants.EN_RL_TB: + propID = rltb; + break; + case Constants.EN_TB_RL: + propID = tbrl; + break; + case Constants.EN_TB_LR: + propID = tblr; + break; default: - //nop + propID = -1; + break; + } + } catch ( PropertyException e ) { + propID = -1; } - return -1; + return propID; } private String addAttributeToList(Attributes attributes, String attributeName) throws ValidationException { String attributeValue = attributes.getValue(attributeName); - convertAttributeToProperty(attributes, attributeName, attributeValue); + if ( attributeValue != null ) { + convertAttributeToProperty(attributes, attributeName, attributeValue); + } return attributeValue; } /** - * Adds the attributes, passed in by the parser to the PropertyList + * <p>Adds the attributes, passed in by the parser to the PropertyList.</p> + * <p>Note that certain attributes are given priority in terms of order of + * processing due to conversion dependencies, where the order is as follows:</p> + * <ol> + * <li>writing-mode</li> + * <li>column-number</li> + * <li>number-columns-spanned</li> + * <li>font</li> + * <li>font-size</li> + * <li><emph>all others in order of appearance</emph></li> + * </ol> * * @param attributes Collection of attributes passed to us from the parser. * @throws ValidationException if there is an attribute that does not @@ -280,6 +280,11 @@ public abstract class PropertyList { public void addAttributesToList(Attributes attributes) throws ValidationException { /* + * Give writing-mode highest conversion priority. + */ + addAttributeToList(attributes, "writing-mode"); + + /* * If column-number/number-columns-spanned are specified, then we * need them before all others (possible from-table-column() on any * other property further in the list... @@ -300,9 +305,9 @@ public abstract class PropertyList { addAttributeToList(attributes, "font-size"); } + String attributeNS; String attributeName; String attributeValue; - String attributeNS; FopFactory factory = getFObj().getUserAgent().getFactory(); for (int i = 0; i < attributes.getLength(); i++) { /* convert all attributes with the same namespace as the fo element @@ -646,4 +651,3 @@ public abstract class PropertyList { return CommonTextDecoration.createFromPropertyList(this); } } - diff --git a/src/java/org/apache/fop/fo/flow/AbstractGraphics.java b/src/java/org/apache/fop/fo/flow/AbstractGraphics.java index d5fe2b500..3d93eaa8f 100644 --- a/src/java/org/apache/fop/fo/flow/AbstractGraphics.java +++ b/src/java/org/apache/fop/fo/flow/AbstractGraphics.java @@ -19,8 +19,11 @@ package org.apache.fop.fo.flow; +import java.util.Stack; + import org.apache.fop.accessibility.StructureTreeElement; import org.apache.fop.apps.FOPException; +import org.apache.fop.complexscripts.bidi.DelimitedTextRange; import org.apache.fop.datatypes.Length; import org.apache.fop.fo.FONode; import org.apache.fop.fo.FObj; @@ -33,6 +36,7 @@ import org.apache.fop.fo.properties.KeepProperty; import org.apache.fop.fo.properties.LengthRangeProperty; import org.apache.fop.fo.properties.SpaceProperty; import org.apache.fop.fo.properties.StructureTreeElementHolder; +import org.apache.fop.util.CharUtilities; /** * Common base class for the <a href="http://www.w3.org/TR/xsl/#fo_instream-foreign-object"> @@ -77,8 +81,6 @@ public abstract class AbstractGraphics extends FObj // private int scalingMethod; // End of property values - - /** * constructs an instream-foreign-object object (called by Maker). * @@ -250,4 +252,18 @@ public abstract class AbstractGraphics extends FObj /** @return the graphic's intrinsic alignment-adjust */ public abstract Length getIntrinsicAlignmentAdjust(); + + @Override + public boolean isDelimitedTextRangeBoundary ( int boundary ) { + return false; + } + + @Override + protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) { + if ( currentRange != null ) { + currentRange.append ( CharUtilities.OBJECT_REPLACEMENT_CHARACTER, this ); + } + return ranges; + } + } diff --git a/src/java/org/apache/fop/fo/flow/AbstractPageNumberCitation.java b/src/java/org/apache/fop/fo/flow/AbstractPageNumberCitation.java index 5a6893b55..ec3891632 100644 --- a/src/java/org/apache/fop/fo/flow/AbstractPageNumberCitation.java +++ b/src/java/org/apache/fop/fo/flow/AbstractPageNumberCitation.java @@ -20,12 +20,14 @@ package org.apache.fop.fo.flow; import java.awt.Color; +import java.util.Stack; import org.xml.sax.Attributes; import org.xml.sax.Locator; import org.apache.fop.accessibility.StructureTreeElement; import org.apache.fop.apps.FOPException; +import org.apache.fop.complexscripts.bidi.DelimitedTextRange; import org.apache.fop.datatypes.Length; import org.apache.fop.fo.Constants; import org.apache.fop.fo.FONode; @@ -39,6 +41,7 @@ import org.apache.fop.fo.properties.CommonFont; import org.apache.fop.fo.properties.CommonTextDecoration; import org.apache.fop.fo.properties.SpaceProperty; import org.apache.fop.fo.properties.StructureTreeElementHolder; +import org.apache.fop.util.CharUtilities; /** * Common base class for the <a href="http://www.w3.org/TR/xsl/#fo_page-number-citation"> @@ -197,4 +200,17 @@ public abstract class AbstractPageNumberCitation extends FObj return refId; } + @Override + public boolean isDelimitedTextRangeBoundary ( int boundary ) { + return false; + } + + @Override + protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) { + if ( currentRange != null ) { + currentRange.append ( CharUtilities.OBJECT_REPLACEMENT_CHARACTER, this ); + } + return ranges; + } + } diff --git a/src/java/org/apache/fop/fo/flow/BidiOverride.java b/src/java/org/apache/fop/fo/flow/BidiOverride.java index 6e4a74379..d38df90c7 100644 --- a/src/java/org/apache/fop/fo/flow/BidiOverride.java +++ b/src/java/org/apache/fop/fo/flow/BidiOverride.java @@ -19,38 +19,37 @@ package org.apache.fop.fo.flow; +import java.util.Iterator; +import java.util.Stack; + import org.xml.sax.Locator; import org.apache.fop.apps.FOPException; +import org.apache.fop.complexscripts.bidi.DelimitedTextRange; +import org.apache.fop.datatypes.Length; +import org.apache.fop.fo.Constants; import org.apache.fop.fo.FONode; import org.apache.fop.fo.FObjMixed; import org.apache.fop.fo.PropertyList; import org.apache.fop.fo.ValidationException; +import org.apache.fop.fo.properties.Property; import org.apache.fop.fo.properties.SpaceProperty; +import org.apache.fop.util.CharUtilities; /** * Class modelling the <a href="http://www.w3.org/TR/xsl/#fo_bidi-override"> * <code>fo:bidi-override</code></a> object. */ -public class BidiOverride extends FObjMixed { - - // used for FO validation - private boolean blockOrInlineItemFound = false; - private boolean canHaveBlockLevelChildren = true; - // The value of properties relevant for fo:bidi-override. - // private ToBeImplementedProperty prDirection; - // private ToBeImplementedProperty prLetterSpacing; - private SpaceProperty lineHeight; - // private ToBeImplementedProperty prScoreSpaces; - // private ToBeImplementedProperty prUnicodeBidi; - - // Unused but valid items, commented out for performance: - // private CommonAural commonAural; - // private CommonFont commonFont; - // private CommonRelativePosition commonRelativePosition; - // private Color prColor; - // private SpaceProperty prWordSpacing; - // End of property values +public class BidiOverride extends Inline { + + // The value of FO traits (refined properties) that apply to fo:bidi-override + // (that are not implemented by InlineLevel). + private Property letterSpacing; + private Property wordSpacing; + private int direction; + private int unicodeBidi; + // private int scoreSpaces; + // End of trait values /** * Base constructor @@ -59,66 +58,35 @@ public class BidiOverride extends FObjMixed { */ public BidiOverride(FONode parent) { super(parent); - - /* Check to see if this node can have block-level children. - * See validateChildNode() below. - */ - int lvlLeader = findAncestor(FO_LEADER); - int lvlInCntr = findAncestor(FO_INLINE_CONTAINER); - int lvlInline = findAncestor(FO_INLINE); - int lvlFootnote = findAncestor(FO_FOOTNOTE); - - if (lvlLeader > 0) { - if (lvlInCntr < 0 || (lvlInCntr > 0 && lvlInCntr > lvlLeader)) { - canHaveBlockLevelChildren = false; - } - } else if (lvlInline > 0 && lvlFootnote == (lvlInline + 1)) { - if (lvlInCntr < 0 || (lvlInCntr > 0 && lvlInCntr > lvlInline)) { - canHaveBlockLevelChildren = false; - } - } - } /** {@inheritDoc} */ public void bind(PropertyList pList) throws FOPException { - // prDirection = pList.get(PR_DIRECTION); - // prLetterSpacing = pList.get(PR_LETTER_SPACING); - lineHeight = pList.get(PR_LINE_HEIGHT).getSpace(); - // prScoreSpaces = pList.get(PR_SCORE_SPACES); - // prUnicodeBidi = pList.get(PR_UNICODE_BIDI); + super.bind(pList); + letterSpacing = pList.get(PR_LETTER_SPACING); + wordSpacing = pList.get(PR_WORD_SPACING); + direction = pList.get(PR_DIRECTION).getEnum(); + unicodeBidi = pList.get(PR_UNICODE_BIDI).getEnum(); } - /** - * {@inheritDoc} - * <br>XSL Content Model: marker* (#PCDATA|%inline;|%block;)* - * <br><i>Additionally: "An fo:bidi-override that is a descendant of an fo:leader - * or of the fo:inline child of an fo:footnote may not have block-level - * children, unless it has a nearer ancestor that is an - * fo:inline-container."</i> - */ - protected void validateChildNode(Locator loc, String nsURI, String localName) - throws ValidationException { - if (FO_URI.equals(nsURI)) { - if (localName.equals("marker")) { - if (blockOrInlineItemFound) { - nodesOutOfOrderError(loc, "fo:marker", - "(#PCDATA|%inline;|%block;)"); - } - } else if (!isBlockOrInlineItem(nsURI, localName)) { - invalidChildError(loc, nsURI, localName); - } else if (!canHaveBlockLevelChildren && isBlockItem(nsURI, localName)) { - invalidChildError(loc, getParent().getName(), nsURI, getName(), - "rule.bidiOverrideContent"); - } else { - blockOrInlineItemFound = true; - } - } + /** @return the "letter-spacing" trait */ + public Property getLetterSpacing() { + return letterSpacing; + } + + /** @return the "word-spacing" trait */ + public Property getWordSpacing() { + return wordSpacing; } - /** @return the "line-height" property */ - public SpaceProperty getLineHeight() { - return lineHeight; + /** @return the "direction" trait */ + public int getDirection() { + return direction; + } + + /** @return the "unicodeBidi" trait */ + public int getUnicodeBidi() { + return unicodeBidi; } /** {@inheritDoc} */ @@ -133,4 +101,32 @@ public class BidiOverride extends FObjMixed { public int getNameId() { return FO_BIDI_OVERRIDE; } + + @Override + protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) { + char pfx = 0; + char sfx = 0; + int unicodeBidi = getUnicodeBidi(); + int direction = getDirection(); + if ( unicodeBidi == Constants.EN_BIDI_OVERRIDE ) { + pfx = ( direction == Constants.EN_RTL ) ? CharUtilities.RLO : CharUtilities.LRO; + sfx = CharUtilities.PDF; + } else if ( unicodeBidi == Constants.EN_EMBED ) { + pfx = ( direction == Constants.EN_RTL ) ? CharUtilities.RLE : CharUtilities.LRE; + sfx = CharUtilities.PDF; + } + if ( currentRange != null ) { + if ( pfx != 0 ) { + currentRange.append ( pfx, this ); + } + for ( Iterator it = getChildNodes(); ( it != null ) && it.hasNext();) { + ranges = ( (FONode) it.next() ).collectDelimitedTextRanges ( ranges ); + } + if ( sfx != 0 ) { + currentRange.append ( sfx, this ); + } + } + return ranges; + } + } diff --git a/src/java/org/apache/fop/fo/flow/Block.java b/src/java/org/apache/fop/fo/flow/Block.java index 84f1abc1b..d93821ca6 100644 --- a/src/java/org/apache/fop/fo/flow/Block.java +++ b/src/java/org/apache/fop/fo/flow/Block.java @@ -54,7 +54,7 @@ public class Block extends FObjMixed implements BreakPropertySet, private boolean blockOrInlineItemFound = false; private boolean initialPropertySetFound = false; - // The value of properties relevant for fo:block. + // The value of FO traits (refined properties) that apply to fo:block. private CommonAccessibility commonAccessibility; private CommonBorderPaddingBackground commonBorderPaddingBackground; private CommonFont commonFont; @@ -91,7 +91,7 @@ public class Block extends FObjMixed implements BreakPropertySet, // private Length textDepth; // private Length textAltitude; // private int visibility; - // End of property values + // End of FO trait values /** * Base constructor @@ -177,87 +177,87 @@ public class Block extends FObjMixed implements BreakPropertySet, return commonHyphenation; } - /** @return the "break-after" property. */ + /** @return the "break-after" trait. */ public int getBreakAfter() { return breakAfter; } - /** @return the "break-before" property. */ + /** @return the "break-before" trait. */ public int getBreakBefore() { return breakBefore; } - /** @return the "hyphenation-ladder-count" property. */ + /** @return the "hyphenation-ladder-count" trait. */ public Numeric getHyphenationLadderCount() { return hyphenationLadderCount; } - /** @return the "keep-with-next" property. */ + /** @return the "keep-with-next" trait. */ public KeepProperty getKeepWithNext() { return keepWithNext; } - /** @return the "keep-with-previous" property. */ + /** @return the "keep-with-previous" trait. */ public KeepProperty getKeepWithPrevious() { return keepWithPrevious; } - /** @return the "keep-together" property. */ + /** @return the "keep-together" trait. */ public KeepProperty getKeepTogether() { return keepTogether; } - /** @return the "orphans" property. */ + /** @return the "orphans" trait. */ public int getOrphans() { return orphans.getValue(); } - /** @return the "widows" property. */ + /** @return the "widows" trait. */ public int getWidows() { return widows.getValue(); } - /** @return the "line-stacking-strategy" property. */ + /** @return the "line-stacking-strategy" trait. */ public int getLineStackingStrategy() { return lineStackingStrategy; } - /** @return the "color" property */ + /** @return the "color" trait */ public Color getColor() { return color; } - /** @return the "line-height" property */ + /** @return the "line-height" trait */ public SpaceProperty getLineHeight() { return lineHeight; } - /** @return the "span" property */ + /** @return the "span" trait */ public int getSpan() { return this.span; } - /** @return the "text-align" property */ + /** @return the "text-align" trait */ public int getTextAlign() { return textAlign; } - /** @return the "text-align-last" property */ + /** @return the "text-align-last" trait */ public int getTextAlignLast() { return textAlignLast; } - /** @return the "text-indent" property */ + /** @return the "text-indent" trait */ public Length getTextIndent() { return textIndent; } - /** @return the "last-line-end-indent" property */ + /** @return the "last-line-end-indent" trait */ public Length getLastLineEndIndent() { return lastLineEndIndent; } - /** @return the "wrap-option" property */ + /** @return the "wrap-option" trait */ public int getWrapOption() { return wrapOption; } @@ -295,17 +295,17 @@ public class Block extends FObjMixed implements BreakPropertySet, } } - /** @return the "linefeed-treatment" property */ + /** @return the "linefeed-treatment" trait */ public int getLinefeedTreatment() { return linefeedTreatment; } - /** @return the "white-space-treatment" property */ + /** @return the "white-space-treatment" trait */ public int getWhitespaceTreatment() { return whiteSpaceTreatment; } - /** @return the "white-space-collapse" property */ + /** @return the "white-space-collapse" trait */ public int getWhitespaceCollapse() { return whiteSpaceCollapse; } @@ -315,17 +315,17 @@ public class Block extends FObjMixed implements BreakPropertySet, return this.commonRelativePosition; } - /** @return the "hyphenation-keep" property */ + /** @return the "hyphenation-keep" trait */ public int getHyphenationKeep() { return this.hyphenationKeep; } - /** @return the "intrusion-displace" property */ + /** @return the "intrusion-displace" trait */ public int getIntrusionDisplace() { return this.intrusionDisplace; } - /** @return the "line-height-shift-adjustment" property */ + /** @return the "line-height-shift-adjustment" trait */ public int getLineHeightShiftAdjustment() { return this.lineHeightShiftAdjustment; } @@ -335,9 +335,9 @@ public class Block extends FObjMixed implements BreakPropertySet, * {@link org.apache.fop.fo.Constants#EN_TRUE}, * {@link org.apache.fop.fo.Constants#EN_FALSE} */ - public int getDisableColumnBalancing() { - return disableColumnBalancing; - } + public int getDisableColumnBalancing() { + return disableColumnBalancing; + } /** {@inheritDoc} */ @@ -350,10 +350,10 @@ public class Block extends FObjMixed implements BreakPropertySet, return "block"; } - /** - * {@inheritDoc} - * @return {@link org.apache.fop.fo.Constants#FO_BLOCK} - */ + /** + * {@inheritDoc} + * @return {@link org.apache.fop.fo.Constants#FO_BLOCK} + */ public int getNameId() { return FO_BLOCK; } diff --git a/src/java/org/apache/fop/fo/flow/BlockContainer.java b/src/java/org/apache/fop/fo/flow/BlockContainer.java index 57cb6e5c6..1dc43fd36 100644 --- a/src/java/org/apache/fop/fo/flow/BlockContainer.java +++ b/src/java/org/apache/fop/fo/flow/BlockContainer.java @@ -31,14 +31,18 @@ import org.apache.fop.fo.properties.CommonBorderPaddingBackground; import org.apache.fop.fo.properties.CommonMarginBlock; import org.apache.fop.fo.properties.KeepProperty; import org.apache.fop.fo.properties.LengthRangeProperty; +import org.apache.fop.traits.Direction; +import org.apache.fop.traits.WritingMode; +import org.apache.fop.traits.WritingModeTraits; +import org.apache.fop.traits.WritingModeTraitsGetter; import org.xml.sax.Locator; /** * Class modelling the <a href="http://www.w3.org/TR/xsl/#fo_block-container"> * <code>fo:block-container</code></a> object. */ -public class BlockContainer extends FObj implements BreakPropertySet { - // The value of properties relevant for fo:block-container. +public class BlockContainer extends FObj implements BreakPropertySet, WritingModeTraitsGetter { + // The value of FO traits (refined properties) that apply to fo:block-container. private CommonAbsolutePosition commonAbsolutePosition; private CommonBorderPaddingBackground commonBorderPaddingBackground; private CommonMarginBlock commonMarginBlock; @@ -55,11 +59,11 @@ public class BlockContainer extends FObj implements BreakPropertySet { private Numeric referenceOrientation; private int span; private int disableColumnBalancing; - private int writingMode; + private WritingModeTraits writingModeTraits; // Unused but valid items, commented out for performance: // private int intrusionDisplace; // private Numeric zIndex; - // End of property values + // End of FO trait values /** used for FO validation */ private boolean blockItemFound = false; @@ -92,7 +96,8 @@ public class BlockContainer extends FObj implements BreakPropertySet { overflow = pList.get(PR_OVERFLOW).getEnum(); referenceOrientation = pList.get(PR_REFERENCE_ORIENTATION).getNumeric(); span = pList.get(PR_SPAN).getEnum(); - writingMode = pList.get(PR_WRITING_MODE).getEnum(); + writingModeTraits = new WritingModeTraits + ( WritingMode.valueOf(pList.get(PR_WRITING_MODE).getEnum()) ); disableColumnBalancing = pList.get(PR_X_DISABLE_COLUMN_BALANCING).getEnum(); } @@ -161,58 +166,58 @@ public class BlockContainer extends FObj implements BreakPropertySet { } /** - * @return the "block-progression-dimension" property. + * @return the "block-progression-dimension" FO trait. */ public LengthRangeProperty getBlockProgressionDimension() { return blockProgressionDimension; } - /** @return the "display-align" property. */ + /** @return the "display-align" FO trait. */ public int getDisplayAlign() { return displayAlign; } - /** @return the "break-after" property. */ + /** @return the "break-after" FO trait. */ public int getBreakAfter() { return breakAfter; } - /** @return the "break-before" property. */ + /** @return the "break-before" FO trait. */ public int getBreakBefore() { return breakBefore; } - /** @return the "keep-with-next" property. */ + /** @return the "keep-with-next" FO trait. */ public KeepProperty getKeepWithNext() { return keepWithNext; } - /** @return the "keep-with-previous" property. */ + /** @return the "keep-with-previous" FO trait. */ public KeepProperty getKeepWithPrevious() { return keepWithPrevious; } - /** @return the "keep-together" property. */ + /** @return the "keep-together" FO trait. */ public KeepProperty getKeepTogether() { return keepTogether; } - /** @return the "inline-progression-dimension" property */ + /** @return the "inline-progression-dimension" FO trait */ public LengthRangeProperty getInlineProgressionDimension() { return inlineProgressionDimension; } - /** @return the "overflow" property */ + /** @return the "overflow" FO trait */ public int getOverflow() { return overflow; } - /** @return the "reference-orientation" property */ + /** @return the "reference-orientation" FO trait */ public int getReferenceOrientation() { return referenceOrientation.getValue(); } - /** @return the "span" property */ + /** @return the "span" FO trait */ public int getSpan() { return this.span; } @@ -226,10 +231,52 @@ public class BlockContainer extends FObj implements BreakPropertySet { return disableColumnBalancing; } + /** + * Obtain inline progression direction. + * @return the inline progression direction + */ + public Direction getInlineProgressionDirection() { + return writingModeTraits.getInlineProgressionDirection(); + } + + /** + * Obtain block progression direction. + * @return the block progression direction + */ + public Direction getBlockProgressionDirection() { + return writingModeTraits.getBlockProgressionDirection(); + } + + /** + * Obtain column progression direction. + * @return the column progression direction + */ + public Direction getColumnProgressionDirection() { + return writingModeTraits.getColumnProgressionDirection(); + } - /** @return the "writing-mode" property */ - public int getWritingMode() { - return writingMode; + /** + * Obtain row progression direction. + * @return the row progression direction + */ + public Direction getRowProgressionDirection() { + return writingModeTraits.getRowProgressionDirection(); + } + + /** + * Obtain (baseline) shift direction. + * @return the (baseline) shift direction + */ + public Direction getShiftDirection() { + return writingModeTraits.getShiftDirection(); + } + + /** + * Obtain writing mode. + * @return the writing mode + */ + public WritingMode getWritingMode() { + return writingModeTraits.getWritingMode(); } /** {@inheritDoc} */ diff --git a/src/java/org/apache/fop/fo/flow/Character.java b/src/java/org/apache/fop/fo/flow/Character.java index c4de9fb72..f48ff289e 100644 --- a/src/java/org/apache/fop/fo/flow/Character.java +++ b/src/java/org/apache/fop/fo/flow/Character.java @@ -21,11 +21,13 @@ package org.apache.fop.fo.flow; import java.awt.Color; import java.util.NoSuchElementException; +import java.util.Stack; import org.xml.sax.Locator; import org.apache.fop.accessibility.StructureTreeElement; import org.apache.fop.apps.FOPException; +import org.apache.fop.complexscripts.bidi.DelimitedTextRange; import org.apache.fop.datatypes.Length; import org.apache.fop.fo.CharIterator; import org.apache.fop.fo.FONode; @@ -233,6 +235,19 @@ public class Character extends FObj implements StructureTreeElementHolder { return FO_CHARACTER; } + @Override + public boolean isDelimitedTextRangeBoundary ( int boundary ) { + return false; + } + + @Override + protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) { + if ( currentRange != null ) { + currentRange.append ( charIterator(), this ); + } + return ranges; + } + private class FOCharIterator extends CharIterator { private boolean bFirst = true; diff --git a/src/java/org/apache/fop/fo/flow/InlineContainer.java b/src/java/org/apache/fop/fo/flow/InlineContainer.java index d2422a24b..a52801c3f 100644 --- a/src/java/org/apache/fop/fo/flow/InlineContainer.java +++ b/src/java/org/apache/fop/fo/flow/InlineContainer.java @@ -33,6 +33,10 @@ import org.apache.fop.fo.properties.CommonMarginInline; import org.apache.fop.fo.properties.KeepProperty; import org.apache.fop.fo.properties.LengthRangeProperty; import org.apache.fop.fo.properties.SpaceProperty; +import org.apache.fop.traits.Direction; +import org.apache.fop.traits.WritingMode; +import org.apache.fop.traits.WritingModeTraits; +import org.apache.fop.traits.WritingModeTraitsGetter; /** * Class modelling the <a href="http://www.w3.org/TR/xsl/#fo_inline-container"> @@ -40,7 +44,7 @@ import org.apache.fop.fo.properties.SpaceProperty; */ public class InlineContainer extends FObj { - // The value of properties relevant for fo:inline-container. + // The value of FO traits (refined properties) that apply to fo:inline-container. private Length alignmentAdjust; private int alignmentBaseline; private Length baselineShift; @@ -54,7 +58,7 @@ public class InlineContainer extends FObj { private SpaceProperty lineHeight; private int overflow; private Numeric referenceOrientation; - private int writingMode; + private WritingModeTraits writingModeTraits; // Unused but valid items, commented out for performance: // private CommonRelativePosition commonRelativePosition; // private int displayAlign; @@ -62,7 +66,7 @@ public class InlineContainer extends FObj { // private KeepProperty keepWithNext; // private KeepProperty keepWithPrevious; // private Length width; - // End of property values + // End of FO trait values /** used for FO validation */ private boolean blockItemFound = false; @@ -92,7 +96,8 @@ public class InlineContainer extends FObj { lineHeight = pList.get(PR_LINE_HEIGHT).getSpace(); overflow = pList.get(PR_OVERFLOW).getEnum(); referenceOrientation = pList.get(PR_REFERENCE_ORIENTATION).getNumeric(); - writingMode = pList.get(PR_WRITING_MODE).getEnum(); + writingModeTraits = new WritingModeTraits + ( WritingMode.valueOf(pList.get(PR_WRITING_MODE).getEnum()) ); } /** @@ -121,27 +126,27 @@ public class InlineContainer extends FObj { } } - /** @return the "alignment-adjust" property */ + /** @return the "alignment-adjust" FO trait */ public Length getAlignmentAdjust() { return alignmentAdjust; } - /** @return the "alignment-baseline" property */ + /** @return the "alignment-baseline" FO trait */ public int getAlignmentBaseline() { return alignmentBaseline; } - /** @return the "baseline-shift" property */ + /** @return the "baseline-shift" FO trait */ public Length getBaselineShift() { return baselineShift; } - /** @return the "block-progression-dimension" property */ + /** @return the "block-progression-dimension" FO trait */ public LengthRangeProperty getBlockProgressionDimension() { return blockProgressionDimension; } - /** @return the "clip" property */ + /** @return the "clip" FO trait */ public int getClip() { return clip; } @@ -156,39 +161,82 @@ public class InlineContainer extends FObj { return this.commonMarginInline; } - /** @return the "dominant-baseline" property */ + /** @return the "dominant-baseline" FO trait */ public int getDominantBaseline() { return dominantBaseline; } - /** @return the "keep-together" property */ + /** @return the "keep-together" FO trait */ public KeepProperty getKeepTogether() { return keepTogether; } - /** @return the "inline-progression-dimension" property */ + /** @return the "inline-progression-dimension" FO trait */ public LengthRangeProperty getInlineProgressionDimension() { return inlineProgressionDimension; } - /** @return the "line-height" property */ + /** @return the "line-height" FO trait */ public SpaceProperty getLineHeight() { return lineHeight; } - /** @return the "overflow" property */ + /** @return the "overflow" FO trait */ public int getOverflow() { return overflow; } - /** @return the "reference-orientation" property */ + /** @return the "reference-orientation" FO trait */ public int getReferenceOrientation() { return referenceOrientation.getValue(); } - /** @return the "writing-mode" property */ - public int getWritingMode() { - return writingMode; + /** + * Obtain inline progression direction. + * @return the inline progression direction + */ + public Direction getInlineProgressionDirection() { + return writingModeTraits.getInlineProgressionDirection(); + } + + /** + * Obtain block progression direction. + * @return the block progression direction + */ + public Direction getBlockProgressionDirection() { + return writingModeTraits.getBlockProgressionDirection(); + } + + /** + * Obtain column progression direction. + * @return the column progression direction + */ + public Direction getColumnProgressionDirection() { + return writingModeTraits.getColumnProgressionDirection(); + } + + /** + * Obtain row progression direction. + * @return the row progression direction + */ + public Direction getRowProgressionDirection() { + return writingModeTraits.getRowProgressionDirection(); + } + + /** + * Obtain (baseline) shift direction. + * @return the (baseline) shift direction + */ + public Direction getShiftDirection() { + return writingModeTraits.getShiftDirection(); + } + + /** + * Obtain writing mode. + * @return the writing mode + */ + public WritingMode getWritingMode() { + return writingModeTraits.getWritingMode(); } /** {@inheritDoc} */ @@ -204,4 +252,9 @@ public class InlineContainer extends FObj { return FO_INLINE_CONTAINER; } + @Override + public boolean isDelimitedTextRangeBoundary ( int boundary ) { + return false; + } + } diff --git a/src/java/org/apache/fop/fo/flow/InlineLevel.java b/src/java/org/apache/fop/fo/flow/InlineLevel.java index 0da769080..b0531f859 100644 --- a/src/java/org/apache/fop/fo/flow/InlineLevel.java +++ b/src/java/org/apache/fop/fo/flow/InlineLevel.java @@ -39,7 +39,7 @@ import org.apache.fop.fo.properties.SpaceProperty; */ public abstract class InlineLevel extends FObjMixed implements CommonAccessibilityHolder { - // The value of properties relevant for inline-level FOs. + // The value of FO traits (refined properties) that apply to inline level FOs. private CommonAccessibility commonAccessibility; private CommonBorderPaddingBackground commonBorderPaddingBackground; private CommonMarginInline commonMarginInline; @@ -48,7 +48,7 @@ public abstract class InlineLevel extends FObjMixed implements CommonAccessibili private KeepProperty keepWithNext; private KeepProperty keepWithPrevious; private SpaceProperty lineHeight; - // End of property values + // End of trait values /** * Base constructor @@ -92,24 +92,29 @@ public abstract class InlineLevel extends FObjMixed implements CommonAccessibili return commonFont; } - /** @return the "color" property */ + /** @return the "color" trait */ public Color getColor() { return color; } - /** @return the "line-height" property */ + /** @return the "line-height" trait */ public SpaceProperty getLineHeight() { return lineHeight; } - /** @return the "keep-with-next" property */ + /** @return the "keep-with-next" trait */ public KeepProperty getKeepWithNext() { return keepWithNext; } - /** @return the "keep-with-previous" property */ + /** @return the "keep-with-previous" trait */ public KeepProperty getKeepWithPrevious() { return keepWithPrevious; } + @Override + public boolean isDelimitedTextRangeBoundary ( int boundary ) { + return false; + } + } diff --git a/src/java/org/apache/fop/fo/flow/Leader.java b/src/java/org/apache/fop/fo/flow/Leader.java index f12930dde..ce166b114 100644 --- a/src/java/org/apache/fop/fo/flow/Leader.java +++ b/src/java/org/apache/fop/fo/flow/Leader.java @@ -19,18 +19,24 @@ package org.apache.fop.fo.flow; +import java.util.Stack; + +import org.xml.sax.Locator; + import org.apache.fop.apps.FOPException; +import org.apache.fop.complexscripts.bidi.DelimitedTextRange; import org.apache.fop.datatypes.Length; import org.apache.fop.fo.FONode; import org.apache.fop.fo.PropertyList; +import org.apache.fop.fo.ValidationException; import org.apache.fop.fo.properties.LengthRangeProperty; +import org.apache.fop.util.CharUtilities; /** * Class modelling the <a href="http://www.w3.org/TR/xsl/#fo_leader"> * <code>fo:leader</code></a> object. * The main property of <code>fo:leader</code> is leader-pattern. * The following patterns are treated: rule, space, dots and use-content. - * TODO implement validateChildNode() */ public class Leader extends InlineLevel { // The value of properties relevant for fo:leader. @@ -96,6 +102,28 @@ public class Leader extends InlineLevel { // textShadow = pList.get(PR_TEXT_SHADOW); } + /** + * {@inheritDoc} + * <br>XSL Content Model: (#PCDATA|%inline;)* + * <br><i>Additionally: "The content must not contain an + * fo:leader, fo:inline-container, fo:block-container, fo:float, + * fo:footnote, or fo:marker either as a direct child or as a + * descendant."</i> + */ + protected void validateChildNode(Locator loc, String nsURI, String localName) + throws ValidationException { + if (FO_URI.equals(nsURI)) { + if ( localName.equals("leader") + || localName.equals("inline-container") + || localName.equals("block-container") + || localName.equals("float") + || localName.equals("marker") + || !isInlineItem(nsURI, localName) ) { + invalidChildError(loc, nsURI, localName); + } + } + } + /** @return the "rule-style" property */ public int getRuleStyle() { return ruleStyle; @@ -170,4 +198,17 @@ public class Leader extends InlineLevel { super.endOfNode(); getFOEventHandler().endLeader(this); } + + @Override + protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) { + if ( currentRange != null ) { + if ( leaderPattern == EN_USECONTENT ) { + ranges = super.collectDelimitedTextRanges ( ranges, currentRange ); + } else { + currentRange.append ( CharUtilities.OBJECT_REPLACEMENT_CHARACTER, this ); + } + } + return ranges; + } + } diff --git a/src/java/org/apache/fop/fo/flow/ListItem.java b/src/java/org/apache/fop/fo/flow/ListItem.java index aa177777c..f61254874 100644 --- a/src/java/org/apache/fop/fo/flow/ListItem.java +++ b/src/java/org/apache/fop/fo/flow/ListItem.java @@ -19,9 +19,12 @@ package org.apache.fop.fo.flow; +import java.util.Stack; + import org.xml.sax.Locator; import org.apache.fop.apps.FOPException; +import org.apache.fop.complexscripts.bidi.DelimitedTextRange; import org.apache.fop.fo.FONode; import org.apache.fop.fo.FObj; import org.apache.fop.fo.PropertyList; @@ -201,5 +204,19 @@ public class ListItem extends FObj implements BreakPropertySet, CommonAccessibil public int getNameId() { return FO_LIST_ITEM; } + + @Override + protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) { + ListItemLabel label = getLabel(); + if ( label != null ) { + ranges = label.collectDelimitedTextRanges ( ranges ); + } + ListItemBody body = getBody(); + if ( body != null ) { + ranges = body.collectDelimitedTextRanges ( ranges ); + } + return ranges; + } + } diff --git a/src/java/org/apache/fop/fo/flow/PageNumber.java b/src/java/org/apache/fop/fo/flow/PageNumber.java index 59169c2d3..fe3e80a75 100644 --- a/src/java/org/apache/fop/fo/flow/PageNumber.java +++ b/src/java/org/apache/fop/fo/flow/PageNumber.java @@ -199,4 +199,10 @@ public class PageNumber extends FObj public int getNameId() { return FO_PAGE_NUMBER; } + + @Override + public boolean isDelimitedTextRangeBoundary ( int boundary ) { + return false; + } + } diff --git a/src/java/org/apache/fop/fo/flow/Wrapper.java b/src/java/org/apache/fop/fo/flow/Wrapper.java index 6c5619252..1302e3134 100644 --- a/src/java/org/apache/fop/fo/flow/Wrapper.java +++ b/src/java/org/apache/fop/fo/flow/Wrapper.java @@ -135,5 +135,11 @@ public class Wrapper extends FObjMixed { public int getNameId() { return FO_WRAPPER; } + + @Override + public boolean isDelimitedTextRangeBoundary ( int boundary ) { + return false; + } + } diff --git a/src/java/org/apache/fop/fo/flow/table/Table.java b/src/java/org/apache/fop/fo/flow/table/Table.java index c78f9f959..b14326af5 100644 --- a/src/java/org/apache/fop/fo/flow/table/Table.java +++ b/src/java/org/apache/fop/fo/flow/table/Table.java @@ -48,7 +48,7 @@ import org.apache.fop.fo.properties.TableColLength; public class Table extends TableFObj implements ColumnNumberManagerHolder, BreakPropertySet, CommonAccessibilityHolder { - /** properties */ + // The value of FO traits (refined properties) that apply to fo:table. private CommonAccessibility commonAccessibility; private CommonBorderPaddingBackground commonBorderPaddingBackground; private CommonMarginBlock commonMarginBlock; @@ -64,11 +64,12 @@ public class Table extends TableFObj implements ColumnNumberManagerHolder, Break private int tableLayout; private int tableOmitFooterAtBreak; private int tableOmitHeaderAtBreak; + private int writingMode; // Unused but valid items, commented out for performance: // private CommonAural commonAural; // private CommonRelativePosition commonRelativePosition; // private int intrusionDisplace; - // private int writingMode; + // End of FO trait values /** extension properties */ private Length widowContentLimit; @@ -130,6 +131,7 @@ public class Table extends TableFObj implements ColumnNumberManagerHolder, Break tableLayout = pList.get(PR_TABLE_LAYOUT).getEnum(); tableOmitFooterAtBreak = pList.get(PR_TABLE_OMIT_FOOTER_AT_BREAK).getEnum(); tableOmitHeaderAtBreak = pList.get(PR_TABLE_OMIT_HEADER_AT_BREAK).getEnum(); + writingMode = pList.get(PR_WRITING_MODE).getEnum(); //Bind extension properties widowContentLimit = pList.get(PR_X_WIDOW_CONTENT_LIMIT).getLength(); @@ -336,7 +338,6 @@ public class Table extends TableFObj implements ColumnNumberManagerHolder, Break TableColumn implicitColumn = new TableColumn(this, true); PropertyList pList = new StaticPropertyList( implicitColumn, this.propList); - pList.setWritingMode(); implicitColumn.bind(pList); implicitColumn.setColumnWidth(new TableColLength(1.0, implicitColumn)); implicitColumn.setColumnNumber(colNumber); @@ -433,14 +434,14 @@ public class Table extends TableFObj implements ColumnNumberManagerHolder, Break } /** - * @return the "inline-progression-dimension" property. + * @return the "inline-progression-dimension" FO trait. */ public LengthRangeProperty getInlineProgressionDimension() { return inlineProgressionDimension; } /** - * @return the "block-progression-dimension" property. + * @return the "block-progression-dimension" FO trait. */ public LengthRangeProperty getBlockProgressionDimension() { return blockProgressionDimension; @@ -460,27 +461,27 @@ public class Table extends TableFObj implements ColumnNumberManagerHolder, Break return commonBorderPaddingBackground; } - /** @return the "break-after" property. */ + /** @return the "break-after" FO trait. */ public int getBreakAfter() { return breakAfter; } - /** @return the "break-before" property. */ + /** @return the "break-before" FO trait. */ public int getBreakBefore() { return breakBefore; } - /** @return the "keep-with-next" property. */ + /** @return the "keep-with-next" FO trait. */ public KeepProperty getKeepWithNext() { return keepWithNext; } - /** @return the "keep-with-previous" property. */ + /** @return the "keep-with-previous" FO trait. */ public KeepProperty getKeepWithPrevious() { return keepWithPrevious; } - /** @return the "keep-together" property. */ + /** @return the "keep-together" FO trait. */ public KeepProperty getKeepTogether() { return keepTogether; } @@ -494,7 +495,7 @@ public class Table extends TableFObj implements ColumnNumberManagerHolder, Break || !getKeepTogether().getWithinColumn().isAuto(); } - /** @return the "border-collapse" property. */ + /** @return the "border-collapse" FO trait. */ public int getBorderCollapse() { return borderCollapse; } @@ -504,17 +505,22 @@ public class Table extends TableFObj implements ColumnNumberManagerHolder, Break return (getBorderCollapse() == EN_SEPARATE); } - /** @return the "border-separation" property. */ + /** @return the "border-separation" FO trait. */ public LengthPairProperty getBorderSeparation() { return borderSeparation; } - /** @return the "fox:widow-content-limit" extension property */ + /** @return the "writing-mode" FO trait */ + public int getWritingMode() { + return writingMode; + } + + /** @return the "fox:widow-content-limit" extension FO trait */ public Length getWidowContentLimit() { return widowContentLimit; } - /** @return the "fox:orphan-content-limit" extension property */ + /** @return the "fox:orphan-content-limit" extension FO trait */ public Length getOrphanContentLimit() { return orphanContentLimit; } diff --git a/src/java/org/apache/fop/fo/pagination/AbstractPageSequence.java b/src/java/org/apache/fop/fo/pagination/AbstractPageSequence.java index 51b24b314..8640ab19b 100644 --- a/src/java/org/apache/fop/fo/pagination/AbstractPageSequence.java +++ b/src/java/org/apache/fop/fo/pagination/AbstractPageSequence.java @@ -43,6 +43,9 @@ public abstract class AbstractPageSequence extends FObj { private char groupingSeparator; private int groupingSize; private Numeric referenceOrientation; //XSL 1.1 + private String language; + private String country; + private String numberConversionFeatures; // End of property values private PageNumberGenerator pageNumberGenerator; @@ -70,12 +73,16 @@ public abstract class AbstractPageSequence extends FObj { groupingSeparator = pList.get(PR_GROUPING_SEPARATOR).getCharacter(); groupingSize = pList.get(PR_GROUPING_SIZE).getNumber().intValue(); referenceOrientation = pList.get(PR_REFERENCE_ORIENTATION).getNumeric(); + language = pList.get(PR_LANGUAGE).getString(); + country = pList.get(PR_COUNTRY).getString(); + numberConversionFeatures = pList.get(PR_X_NUMBER_CONVERSION_FEATURES).getString(); } /** {@inheritDoc} */ protected void startOfNode() throws FOPException { this.pageNumberGenerator = new PageNumberGenerator( - format, groupingSeparator, groupingSize, letterValue); + format, groupingSeparator, groupingSize, letterValue, + numberConversionFeatures, language, country); } diff --git a/src/java/org/apache/fop/fo/pagination/PageNumberGenerator.java b/src/java/org/apache/fop/fo/pagination/PageNumberGenerator.java index 485fb67cc..ebd1919c4 100644 --- a/src/java/org/apache/fop/fo/pagination/PageNumberGenerator.java +++ b/src/java/org/apache/fop/fo/pagination/PageNumberGenerator.java @@ -19,84 +19,37 @@ package org.apache.fop.fo.pagination; +import org.apache.fop.complexscripts.util.NumberConverter; + +// CSOFF: LineLengthCheck + /** * This class uses the 'format', 'groupingSeparator', 'groupingSize', * and 'letterValue' properties on fo:page-sequence to return a String * corresponding to the supplied integer page number. + * + * In addition, (now) uses 'language' parameter and new 'fox:page-number-features' + * parameter to express applicable language and number conversion features. + * + * @author Glenn Adams (rewrite to use new NumberConverter utility) + * @see NumberConverter */ public class PageNumberGenerator { - private String format; - private char groupingSeparator; - private int groupingSize; - private int letterValue; - - // constants - private static final int DECIMAL = 1; // '0*1' - private static final int LOWERALPHA = 2; // 'a' - private static final int UPPERALPHA = 3; // 'A' - private static final int LOWERROMAN = 4; // 'i' - private static final int UPPERROMAN = 5; // 'I' - - // flags - private int formatType = DECIMAL; - private int minPadding = 0; // for decimal formats - - // preloaded strings of zeros - private String[] zeros = { - "", "0", "00", "000", "0000", "00000" - }; + private NumberConverter converter; /** - * Main constructor. For further information on the parameters see the XSLT - * specs (Number to String Conversion Attributes). - * @param format format for the page number - * @param groupingSeparator grouping separator - * @param groupingSize grouping size + * Main constructor. For further information on the parameters see {@link NumberConverter}. + * @param format format for the page number (may be null or empty, which is treated as null) + * @param groupingSeparator grouping separator (if zero, then no grouping separator applies) + * @param groupingSize grouping size (if zero or negative, then no grouping size applies) * @param letterValue letter value + * @param features features (feature sub-parameters) + * @param language (may be null or empty, which is treated as null) + * @param country (may be null or empty, which is treated as null) */ - public PageNumberGenerator(String format, char groupingSeparator, - int groupingSize, int letterValue) { - this.format = format; - this.groupingSeparator = groupingSeparator; - this.groupingSize = groupingSize; - this.letterValue = letterValue; - - // the only accepted format strings are currently '0*1' 'a', 'A', 'i' - // and 'I' - int fmtLen = format.length(); - if (fmtLen == 1) { - if (format.equals("1")) { - formatType = DECIMAL; - minPadding = 0; - } else if (format.equals("a")) { - formatType = LOWERALPHA; - } else if (format.equals("A")) { - formatType = UPPERALPHA; - } else if (format.equals("i")) { - formatType = LOWERROMAN; - } else if (format.equals("I")) { - formatType = UPPERROMAN; - } else { - // token not handled - //getLogger().debug("'format' token not recognized; using '1'"); - formatType = DECIMAL; - minPadding = 0; - } - } else { - // only accepted token is '0+1'at this stage. Because of the - // wonderful regular expression support in Java, we will resort to a - // loop - for (int i = 0; i < fmtLen - 1; i++) { - if (format.charAt(i) != '0') { - //getLogger().debug("'format' token not recognized; using '1'"); - formatType = DECIMAL; - minPadding = 0; - } else { - minPadding = fmtLen - 1; - } - } - } + public PageNumberGenerator ( String format, int groupingSeparator, int groupingSize, int letterValue, String features, String language, String country ) { + this.converter = new NumberConverter ( format, groupingSeparator, groupingSize, letterValue, features, language, country ); } /** @@ -104,70 +57,9 @@ public class PageNumberGenerator { * @param number page number to format * @return the formatted page number as a String */ - public String makeFormattedPageNumber(int number) { - String pn = null; - if (formatType == DECIMAL) { - pn = Integer.toString(number); - if (minPadding >= pn.length()) { - int nz = minPadding - pn.length() + 1; - pn = zeros[nz] + pn; - } - } else if ((formatType == LOWERROMAN) || (formatType == UPPERROMAN)) { - pn = makeRoman(number); - if (formatType == UPPERROMAN) { - pn = pn.toUpperCase(); - } - } else { - // alphabetic - pn = makeAlpha(number); - if (formatType == UPPERALPHA) { - pn = pn.toUpperCase(); - } - } - return pn; + public String makeFormattedPageNumber ( int number ) { + return converter.convert ( number ); } - private String makeRoman(int num) { - int[] arabic = { - 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 - }; - String[] roman = { - "m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", - "i" - }; - - int i = 0; - StringBuffer romanNumber = new StringBuffer(); - - while (num > 0) { - while (num >= arabic[i]) { - num = num - arabic[i]; - romanNumber.append(roman[i]); - } - i = i + 1; - } - return romanNumber.toString(); - } - - private String makeAlpha(int num) { - String letters = "abcdefghijklmnopqrstuvwxyz"; - StringBuffer alphaNumber = new StringBuffer(); - - int base = 26; - int rem = 0; - - num--; - if (num < base) { - alphaNumber.append(letters.charAt(num)); - } else { - while (num >= base) { - rem = num % base; - alphaNumber.append(letters.charAt(rem)); - num = num / base; - } - alphaNumber.append(letters.charAt(num - 1)); - } - return alphaNumber.reverse().toString(); - } } diff --git a/src/java/org/apache/fop/fo/pagination/PageSequence.java b/src/java/org/apache/fop/fo/pagination/PageSequence.java index 1c3b33fa7..63114b507 100644 --- a/src/java/org/apache/fop/fo/pagination/PageSequence.java +++ b/src/java/org/apache/fop/fo/pagination/PageSequence.java @@ -19,28 +19,35 @@ package org.apache.fop.fo.pagination; -// Java import java.util.Map; +import java.util.Stack; import org.xml.sax.Locator; import org.apache.fop.apps.FOPException; +import org.apache.fop.complexscripts.bidi.DelimitedTextRange; +import org.apache.fop.datatypes.Numeric; import org.apache.fop.fo.FONode; import org.apache.fop.fo.PropertyList; import org.apache.fop.fo.ValidationException; +import org.apache.fop.traits.Direction; +import org.apache.fop.traits.WritingMode; +import org.apache.fop.traits.WritingModeTraits; +import org.apache.fop.traits.WritingModeTraitsGetter; /** * Class modelling the <a href="http://www.w3.org/TR/xsl/#fo_page-sequence"> * <code>fo:page-sequence</code></a> object. */ -public class PageSequence extends AbstractPageSequence { +public class PageSequence extends AbstractPageSequence implements WritingModeTraitsGetter { - // The value of properties relevant for fo:page-sequence. + // The value of FO traits (refined properties) that apply to fo:page-sequence. private String country; private String language; private String masterReference; - //private int writingMode; //XSL 1.1 - // End of property values + private Numeric referenceOrientation; + private WritingModeTraits writingModeTraits; + // End of trait values // There doesn't seem to be anything in the spec requiring flows // to be in the order given, only that they map to the regions @@ -86,8 +93,9 @@ public class PageSequence extends AbstractPageSequence { country = pList.get(PR_COUNTRY).getString(); language = pList.get(PR_LANGUAGE).getString(); masterReference = pList.get(PR_MASTER_REFERENCE).getString(); - //writingMode = pList.getWritingMode(); - + referenceOrientation = pList.get(PR_REFERENCE_ORIENTATION).getNumeric(); + writingModeTraits = new WritingModeTraits + ( WritingMode.valueOf(pList.get(PR_WRITING_MODE).getEnum()) ); if (masterReference == null || masterReference.equals("")) { missingPropertyError("master-reference"); } @@ -292,8 +300,8 @@ public class PageSequence extends AbstractPageSequence { } /** - * Get the value of the <code>master-reference</code> property. - * @return the "master-reference" property + * Get the value of the <code>master-reference</code> trait. + * @return the "master-reference" trait */ public String getMasterReference() { return masterReference; @@ -313,22 +321,120 @@ public class PageSequence extends AbstractPageSequence { } /** - * Get the value of the <code>country</code> property. - * @return the country property value + * Get the value of the <code>country</code> trait. + * @return the country trait value */ public String getCountry() { return this.country; } /** - * Get the value of the <code>language</code> property. - * @return the language property value + * Get the value of the <code>language</code> trait. + * @return the language trait value */ public String getLanguage() { return this.language; } /** + * Get the value of the <code>reference-orientation</code> trait. + * @return the reference orientation trait value + */ + public int getReferenceOrientation() { + if ( referenceOrientation != null ) { + return referenceOrientation.getValue(); + } else { + return 0; + } + } + + /** + * {@inheritDoc} + */ + public Direction getInlineProgressionDirection() { + if ( writingModeTraits != null ) { + return writingModeTraits.getInlineProgressionDirection(); + } else { + return Direction.LR; + } + } + + /** + * {@inheritDoc} + */ + public Direction getBlockProgressionDirection() { + if ( writingModeTraits != null ) { + return writingModeTraits.getBlockProgressionDirection(); + } else { + return Direction.TB; + } + } + + /** + * {@inheritDoc} + */ + public Direction getColumnProgressionDirection() { + if ( writingModeTraits != null ) { + return writingModeTraits.getColumnProgressionDirection(); + } else { + return Direction.LR; + } + } + + /** + * {@inheritDoc} + */ + public Direction getRowProgressionDirection() { + if ( writingModeTraits != null ) { + return writingModeTraits.getRowProgressionDirection(); + } else { + return Direction.TB; + } + } + + /** + * {@inheritDoc} + */ + public Direction getShiftDirection() { + if ( writingModeTraits != null ) { + return writingModeTraits.getShiftDirection(); + } else { + return Direction.TB; + } + } + + /** + * {@inheritDoc} + */ + public WritingMode getWritingMode() { + if ( writingModeTraits != null ) { + return writingModeTraits.getWritingMode(); + } else { + return WritingMode.LR_TB; + } + } + + + @Override + protected Stack collectDelimitedTextRanges ( Stack ranges, DelimitedTextRange currentRange ) { + // collect ranges from static content flows + Map<String, FONode> flows = getFlowMap(); + if ( flows != null ) { + for ( FONode fn : flows.values() ) { + if ( fn instanceof StaticContent ) { + ranges = ( (StaticContent) fn ).collectDelimitedTextRanges ( ranges ); + } + } + } + // collect ranges in main flow + Flow main = getMainFlow(); + if ( main != null ) { + ranges = main.collectDelimitedTextRanges ( ranges ); + } + return ranges; + } + + /** * Releases a page-sequence's children after the page-sequence has been fully processed. */ public void releasePageSequence() { diff --git a/src/java/org/apache/fop/fo/pagination/Region.java b/src/java/org/apache/fop/fo/pagination/Region.java index a3c259aa7..16956b6dd 100644 --- a/src/java/org/apache/fop/fo/pagination/Region.java +++ b/src/java/org/apache/fop/fo/pagination/Region.java @@ -32,20 +32,21 @@ import org.apache.fop.fo.FObj; import org.apache.fop.fo.PropertyList; import org.apache.fop.fo.ValidationException; import org.apache.fop.fo.properties.CommonBorderPaddingBackground; +import org.apache.fop.traits.WritingMode; /** * This is an abstract base class for pagination regions. */ public abstract class Region extends FObj { - // The value of properties relevant for fo:region + // The value of FO traits (refined properties) that apply to fo:region private CommonBorderPaddingBackground commonBorderPaddingBackground; // private ToBeImplementedProperty clip private int displayAlign; private int overflow; private String regionName; private Numeric referenceOrientation; - private int writingMode; - // End of property values + private WritingMode writingMode; + // End of FO trait values /** the parent {@link SimplePageMaster} */ protected final SimplePageMaster layoutMaster; @@ -68,7 +69,7 @@ public abstract class Region extends FObj { overflow = pList.get(PR_OVERFLOW).getEnum(); regionName = pList.get(PR_REGION_NAME).getString(); referenceOrientation = pList.get(PR_REFERENCE_ORIENTATION).getNumeric(); - writingMode = pList.getWritingMode(); + writingMode = WritingMode.valueOf(pList.get(PR_WRITING_MODE).getEnum()); // regions may have name, or default if (regionName.equals("")) { @@ -169,28 +170,28 @@ public abstract class Region extends FObj { return commonBorderPaddingBackground; } - /** @return the "region-name" property. */ + /** @return the "region-name" FO trait. */ public String getRegionName() { return regionName; } - /** @return the "writing-mode" property. */ - public int getWritingMode() { - return writingMode; - } - - /** @return the "overflow" property. */ + /** @return the "overflow" FO trait. */ public int getOverflow() { return overflow; } - /** @return the display-align property. */ + /** @return the display-align FO trait. */ public int getDisplayAlign() { return displayAlign; } - /** @return the "reference-orientation" property. */ + /** @return the "reference-orientation" FO trait. */ public int getReferenceOrientation() { return referenceOrientation.getValue(); } + + /** @return the "writing-mode" FO trait. */ + public WritingMode getWritingMode() { + return writingMode; + } } diff --git a/src/java/org/apache/fop/fo/pagination/RegionAfter.java b/src/java/org/apache/fop/fo/pagination/RegionAfter.java index bab5d46b8..3541c54be 100644 --- a/src/java/org/apache/fop/fo/pagination/RegionAfter.java +++ b/src/java/org/apache/fop/fo/pagination/RegionAfter.java @@ -23,6 +23,7 @@ package org.apache.fop.fo.pagination; import java.awt.Rectangle; // FOP +import org.apache.fop.fo.Constants; import org.apache.fop.fo.FONode; import org.apache.fop.datatypes.FODimension; import org.apache.fop.datatypes.LengthBase; @@ -52,15 +53,22 @@ public class RegionAfter extends RegionBA { PercentBaseContext pageHeightContext = getPageHeightContext(LengthBase.CUSTOM_BASE); PercentBaseContext neighbourContext; Rectangle vpRect; - if (layoutMaster.getWritingMode() == EN_LR_TB - || layoutMaster.getWritingMode() == EN_RL_TB) { + + // [TBD] WRITING MODE ALERT + switch ( getWritingMode().getEnumValue() ) { + default: + case Constants.EN_LR_TB: + case Constants.EN_RL_TB: neighbourContext = pageWidthContext; vpRect = new Rectangle(0, reldims.bpd - getExtent().getValue(pageHeightContext) , reldims.ipd, getExtent().getValue(pageHeightContext)); - } else { + break; + case Constants.EN_TB_LR: + case Constants.EN_TB_RL: neighbourContext = pageHeightContext; vpRect = new Rectangle(0, reldims.bpd - getExtent().getValue(pageWidthContext) , getExtent().getValue(pageWidthContext), reldims.ipd); + break; } if (getPrecedence() == EN_FALSE) { adjustIPD(vpRect, layoutMaster.getWritingMode(), neighbourContext); diff --git a/src/java/org/apache/fop/fo/pagination/RegionBA.java b/src/java/org/apache/fop/fo/pagination/RegionBA.java index 149e470cf..eac7723f5 100644 --- a/src/java/org/apache/fop/fo/pagination/RegionBA.java +++ b/src/java/org/apache/fop/fo/pagination/RegionBA.java @@ -26,6 +26,7 @@ import org.apache.fop.apps.FOPException; import org.apache.fop.datatypes.PercentBaseContext; import org.apache.fop.fo.FONode; import org.apache.fop.fo.PropertyList; +import org.apache.fop.traits.WritingMode; /** * Abstract base class for <a href="http://www.w3.org/TR/xsl/#fo_region-before"> @@ -70,7 +71,8 @@ public abstract class RegionBA extends SideRegion { * @param wm writing mode * @param siblingContext the context to use to resolve extent on siblings */ - protected void adjustIPD(Rectangle vpRefRect, int wm, PercentBaseContext siblingContext) { + protected void adjustIPD + ( Rectangle vpRefRect, WritingMode wm, PercentBaseContext siblingContext ) { int offset = 0; RegionStart start = (RegionStart) getSiblingRegion(FO_REGION_START); if (start != null) { @@ -81,8 +83,9 @@ public abstract class RegionBA extends SideRegion { if (end != null) { offset += end.getExtent().getValue(siblingContext); } + // [TBD] WRITING MODE ALERT if (offset > 0) { - if (wm == EN_LR_TB || wm == EN_RL_TB) { + if (wm == WritingMode.LR_TB || wm == WritingMode.RL_TB) { vpRefRect.width -= offset; } else { vpRefRect.height -= offset; diff --git a/src/java/org/apache/fop/fo/pagination/RegionBefore.java b/src/java/org/apache/fop/fo/pagination/RegionBefore.java index 4aa29ec90..295f6dc1c 100644 --- a/src/java/org/apache/fop/fo/pagination/RegionBefore.java +++ b/src/java/org/apache/fop/fo/pagination/RegionBefore.java @@ -23,6 +23,7 @@ package org.apache.fop.fo.pagination; import java.awt.Rectangle; // FOP +import org.apache.fop.fo.Constants; import org.apache.fop.datatypes.FODimension; import org.apache.fop.datatypes.LengthBase; import org.apache.fop.datatypes.PercentBaseContext; @@ -57,13 +58,19 @@ public class RegionBefore extends RegionBA { PercentBaseContext pageHeightContext = getPageHeightContext(LengthBase.CUSTOM_BASE); PercentBaseContext neighbourContext; Rectangle vpRect; - if (layoutMaster.getWritingMode() == EN_LR_TB - || layoutMaster.getWritingMode() == EN_RL_TB) { + // [TBD] WRITING MODE ALERT + switch ( getWritingMode().getEnumValue() ) { + default: + case Constants.EN_LR_TB: + case Constants.EN_RL_TB: neighbourContext = pageWidthContext; vpRect = new Rectangle(0, 0, reldims.ipd, getExtent().getValue(pageHeightContext)); - } else { + break; + case Constants.EN_TB_LR: + case Constants.EN_TB_RL: neighbourContext = pageHeightContext; vpRect = new Rectangle(0, 0, getExtent().getValue(pageWidthContext), reldims.ipd); + break; } if (getPrecedence() == EN_FALSE) { adjustIPD(vpRect, layoutMaster.getWritingMode(), neighbourContext); diff --git a/src/java/org/apache/fop/fo/pagination/RegionBody.java b/src/java/org/apache/fop/fo/pagination/RegionBody.java index 5eed36061..9e6392337 100644 --- a/src/java/org/apache/fop/fo/pagination/RegionBody.java +++ b/src/java/org/apache/fop/fo/pagination/RegionBody.java @@ -28,9 +28,11 @@ import org.apache.fop.datatypes.Length; import org.apache.fop.datatypes.LengthBase; import org.apache.fop.datatypes.Numeric; import org.apache.fop.datatypes.PercentBaseContext; +import org.apache.fop.fo.Constants; import org.apache.fop.fo.FONode; import org.apache.fop.fo.PropertyList; import org.apache.fop.fo.properties.CommonMarginBlock; +import org.apache.fop.traits.WritingMode; /** * Class modelling the <a href="http://www.w3.org/TR/xsl/#fo_region-body"> @@ -113,12 +115,22 @@ public class RegionBody extends Region { int start; int end; - if (layoutMaster.getWritingMode() == EN_LR_TB) { // Left-to-right + // [TBD] WRITING MODE ALERT + switch ( getWritingMode().getEnumValue() ) { + default: + case Constants.EN_LR_TB: start = commonMarginBlock.marginLeft.getValue(pageWidthContext); end = commonMarginBlock.marginRight.getValue(pageWidthContext); - } else { // all other supported modes are right-to-left + break; + case Constants.EN_RL_TB: start = commonMarginBlock.marginRight.getValue(pageWidthContext); end = commonMarginBlock.marginLeft.getValue(pageWidthContext); + break; + case Constants.EN_TB_LR: + case Constants.EN_TB_RL: + start = commonMarginBlock.marginTop.getValue(pageWidthContext); + end = commonMarginBlock.marginBottom.getValue(pageWidthContext); + break; } int before = commonMarginBlock.spaceBefore.getOptimum(pageHeightContext) .getLength().getValue(pageHeightContext); diff --git a/src/java/org/apache/fop/fo/pagination/RegionEnd.java b/src/java/org/apache/fop/fo/pagination/RegionEnd.java index 2533763a5..ce0258524 100644 --- a/src/java/org/apache/fop/fo/pagination/RegionEnd.java +++ b/src/java/org/apache/fop/fo/pagination/RegionEnd.java @@ -23,6 +23,7 @@ package org.apache.fop.fo.pagination; import java.awt.Rectangle; // FOP +import org.apache.fop.fo.Constants; import org.apache.fop.fo.FONode; import org.apache.fop.datatypes.FODimension; import org.apache.fop.datatypes.LengthBase; @@ -52,18 +53,27 @@ public class RegionEnd extends RegionSE { PercentBaseContext pageHeightContext = getPageHeightContext(LengthBase.CUSTOM_BASE); PercentBaseContext neighbourContext; Rectangle vpRect; - if (layoutMaster.getWritingMode() == EN_LR_TB - || layoutMaster.getWritingMode() == EN_RL_TB) { + // [TBD] WRITING MODE ALERT + switch ( getWritingMode().getEnumValue() ) { + default: + case Constants.EN_LR_TB: neighbourContext = pageHeightContext; vpRect = new Rectangle(reldims.ipd - getExtent().getValue(pageWidthContext), 0, getExtent().getValue(pageWidthContext), reldims.bpd); - } else { + break; + case Constants.EN_RL_TB: + neighbourContext = pageHeightContext; + vpRect = new Rectangle(0, 0, getExtent().getValue(pageWidthContext), reldims.bpd); + break; + case Constants.EN_TB_LR: + case Constants.EN_TB_RL: // Rectangle: x , y (of top left point), width, height neighbourContext = pageWidthContext; vpRect = new Rectangle(reldims.ipd - getExtent().getValue(pageHeightContext), 0, reldims.bpd, getExtent().getValue(pageHeightContext)); + break; } - adjustIPD(vpRect, layoutMaster.getWritingMode(), neighbourContext); + adjustIPD(vpRect, getWritingMode(), neighbourContext); return vpRect; } diff --git a/src/java/org/apache/fop/fo/pagination/RegionSE.java b/src/java/org/apache/fop/fo/pagination/RegionSE.java index f106e8a36..9fc2bdc27 100644 --- a/src/java/org/apache/fop/fo/pagination/RegionSE.java +++ b/src/java/org/apache/fop/fo/pagination/RegionSE.java @@ -26,6 +26,7 @@ import org.apache.fop.apps.FOPException; import org.apache.fop.datatypes.PercentBaseContext; import org.apache.fop.fo.FONode; import org.apache.fop.fo.PropertyList; +import org.apache.fop.traits.WritingMode; /** * Abstract base class for <a href="http://www.w3.org/TR/xsl/#fo_region-start"> @@ -61,7 +62,8 @@ public abstract class RegionSE extends SideRegion { * @param wm writing mode * @param siblingContext the context to use to resolve extent on siblings */ - protected void adjustIPD(Rectangle vpRefRect, int wm, PercentBaseContext siblingContext) { + protected void adjustIPD + ( Rectangle vpRefRect, WritingMode wm, PercentBaseContext siblingContext ) { int offset = 0; RegionBefore before = (RegionBefore) getSiblingRegion(FO_REGION_BEFORE); if (before != null && before.getPrecedence() == EN_TRUE) { @@ -72,8 +74,9 @@ public abstract class RegionSE extends SideRegion { if (after != null && after.getPrecedence() == EN_TRUE) { offset += after.getExtent().getValue(siblingContext); } + // [TBD] WRITING MODE ALERT if (offset > 0) { - if (wm == EN_LR_TB || wm == EN_RL_TB) { + if (wm == WritingMode.LR_TB || wm == WritingMode.RL_TB) { vpRefRect.height -= offset; } else { vpRefRect.width -= offset; diff --git a/src/java/org/apache/fop/fo/pagination/RegionStart.java b/src/java/org/apache/fop/fo/pagination/RegionStart.java index fdb423c51..c2fc3fe17 100644 --- a/src/java/org/apache/fop/fo/pagination/RegionStart.java +++ b/src/java/org/apache/fop/fo/pagination/RegionStart.java @@ -23,6 +23,7 @@ package org.apache.fop.fo.pagination; import java.awt.Rectangle; // FOP +import org.apache.fop.fo.Constants; import org.apache.fop.fo.FONode; import org.apache.fop.datatypes.FODimension; import org.apache.fop.datatypes.LengthBase; @@ -52,13 +53,23 @@ public class RegionStart extends RegionSE { PercentBaseContext pageHeightContext = getPageHeightContext(LengthBase.CUSTOM_BASE); PercentBaseContext neighbourContext; Rectangle vpRect; - if (layoutMaster.getWritingMode() == EN_LR_TB - || layoutMaster.getWritingMode() == EN_RL_TB) { + // [TBD] WRITING MODE ALERT + switch ( getWritingMode().getEnumValue() ) { + default: + case Constants.EN_LR_TB: neighbourContext = pageHeightContext; vpRect = new Rectangle(0, 0, getExtent().getValue(pageWidthContext), reldims.bpd); - } else { + break; + case Constants.EN_RL_TB: + neighbourContext = pageHeightContext; + vpRect = new Rectangle(reldims.ipd - getExtent().getValue(pageWidthContext), 0, + getExtent().getValue(pageWidthContext), reldims.bpd); + break; + case Constants.EN_TB_LR: + case Constants.EN_TB_RL: neighbourContext = pageWidthContext; vpRect = new Rectangle(0, 0, reldims.bpd, getExtent().getValue(pageHeightContext)); + break; } adjustIPD(vpRect, layoutMaster.getWritingMode(), neighbourContext); return vpRect; diff --git a/src/java/org/apache/fop/fo/pagination/SimplePageMaster.java b/src/java/org/apache/fop/fo/pagination/SimplePageMaster.java index e38993487..79c8c3358 100644 --- a/src/java/org/apache/fop/fo/pagination/SimplePageMaster.java +++ b/src/java/org/apache/fop/fo/pagination/SimplePageMaster.java @@ -35,6 +35,7 @@ import org.apache.fop.fo.FObj; import org.apache.fop.fo.PropertyList; import org.apache.fop.fo.ValidationException; import org.apache.fop.fo.properties.CommonMarginBlock; +import org.apache.fop.traits.WritingMode; /** * Class modelling the <a href="http://www.w3.org/TR/xsl/#fo_simple-page-master"> @@ -43,14 +44,14 @@ import org.apache.fop.fo.properties.CommonMarginBlock; * and attributes. */ public class SimplePageMaster extends FObj { - // The value of properties relevant for fo:simple-page-master. + // The value of FO traits (refined properties) that apply to fo:simple-page-master. private CommonMarginBlock commonMarginBlock; private String masterName; private Length pageHeight; private Length pageWidth; private Numeric referenceOrientation; - private int writingMode; - // End of property values + private WritingMode writingMode; + // End of FO trait values /** * Page regions (regionClass, Region) @@ -80,7 +81,7 @@ public class SimplePageMaster extends FObj { pageHeight = pList.get(PR_PAGE_HEIGHT).getLength(); pageWidth = pList.get(PR_PAGE_WIDTH).getLength(); referenceOrientation = pList.get(PR_REFERENCE_ORIENTATION).getNumeric(); - writingMode = pList.getWritingMode(); + writingMode = WritingMode.valueOf(pList.get(PR_WRITING_MODE).getEnum()); if (masterName == null || masterName.equals("")) { missingPropertyError("master-name"); @@ -269,31 +270,31 @@ public class SimplePageMaster extends FObj { return commonMarginBlock; } - /** @return "master-name" property. */ + /** @return "master-name" FO trait. */ public String getMasterName() { return masterName; } - /** @return the "page-width" property. */ + /** @return the "page-width" FO trait. */ public Length getPageWidth() { return pageWidth; } - /** @return the "page-height" property. */ + /** @return the "page-height" FO trait. */ public Length getPageHeight() { return pageHeight; } - /** @return the "writing-mode" property. */ - public int getWritingMode() { - return writingMode; - } - - /** @return the "reference-orientation" property. */ + /** @return the "reference-orientation" FO trait. */ public int getReferenceOrientation() { return referenceOrientation.getValue(); } + /** @return the "writing-mode" FO trait. */ + public WritingMode getWritingMode() { + return writingMode; + } + /** {@inheritDoc} */ public String getLocalName() { return "simple-page-master"; diff --git a/src/java/org/apache/fop/fo/properties/CorrespondingPropertyMaker.java b/src/java/org/apache/fop/fo/properties/CorrespondingPropertyMaker.java index 6183b9e56..b74c9dcfb 100644 --- a/src/java/org/apache/fop/fo/properties/CorrespondingPropertyMaker.java +++ b/src/java/org/apache/fop/fo/properties/CorrespondingPropertyMaker.java @@ -35,6 +35,8 @@ public class CorrespondingPropertyMaker { protected int rltb; /** corresponding property for tb-rl writing mode */ protected int tbrl; + /** corresponding property for tb-lr writing mode */ + protected int tblr; /** user parent property list */ protected boolean useParent; private boolean relative; @@ -48,17 +50,26 @@ public class CorrespondingPropertyMaker { baseMaker.setCorresponding(this); } + /** + * Set corresponding property values. + * @param lrtb a corresponding value + * @param rltb a corresponding value + * @param tbrl a corresponding value + * @param tblr a corresponding value + */ /** * Set corresponding property identifiers. * @param lrtb the property that corresponds with lr-tb writing mode * @param rltb the property that corresponds with rl-tb writing mode * @param tbrl the property that corresponds with tb-lr writing mode + * @param tblr the property that corresponds with tb-lr writing mode */ - public void setCorresponding(int lrtb, int rltb, int tbrl) { + public void setCorresponding(int lrtb, int rltb, int tbrl, int tblr) { this.lrtb = lrtb; this.rltb = rltb; this.tbrl = tbrl; + this.tblr = tblr; } /** @@ -72,7 +83,7 @@ public class CorrespondingPropertyMaker { /** * Set relative flag. - * @param relative true if relative direction + * @param relative true if properties operate on a relative direction */ public void setRelative(boolean relative) { this.relative = relative; @@ -102,7 +113,7 @@ public class CorrespondingPropertyMaker { PropertyList pList = getWMPropertyList(propertyList); if (pList != null) { - int correspondingId = pList.getWritingMode(lrtb, rltb, tbrl); + int correspondingId = pList.selectFromWritingMode(lrtb, rltb, tbrl, tblr); if (pList.getExplicit(correspondingId) != null) { return true; @@ -126,7 +137,7 @@ public class CorrespondingPropertyMaker { if (pList == null) { return null; } - int correspondingId = pList.getWritingMode(lrtb, rltb, tbrl); + int correspondingId = pList.selectFromWritingMode(lrtb, rltb, tbrl, tblr); Property p = propertyList.getExplicitOrShorthand(correspondingId); if (p != null) { diff --git a/src/java/org/apache/fop/fo/properties/DimensionPropertyMaker.java b/src/java/org/apache/fop/fo/properties/DimensionPropertyMaker.java index 0bef5e916..13efa6f30 100644 --- a/src/java/org/apache/fop/fo/properties/DimensionPropertyMaker.java +++ b/src/java/org/apache/fop/fo/properties/DimensionPropertyMaker.java @@ -34,7 +34,7 @@ public class DimensionPropertyMaker extends CorrespondingPropertyMaker { private int[][] extraCorresponding = null; /** - * Construct a dimension property maker. + * Instantiate a dimension property maker. * @param baseMaker the base property maker */ public DimensionPropertyMaker(PropertyMaker baseMaker) { @@ -43,9 +43,18 @@ public class DimensionPropertyMaker extends CorrespondingPropertyMaker { /** * Set extra correspondences. - * @param extraCorresponding the extra correspondences + * @param extraCorresponding an array of four element integer arrays */ public void setExtraCorresponding(int[][] extraCorresponding) { + if ( extraCorresponding == null ) { + throw new NullPointerException(); + } + for ( int i = 0; i < extraCorresponding.length; i++ ) { + int[] eca = extraCorresponding[i]; + if ( ( eca == null ) || ( eca.length != 4 ) ) { + throw new IllegalArgumentException ( "bad sub-array @ [" + i + "]" ); + } + } this.extraCorresponding = extraCorresponding; } @@ -76,18 +85,20 @@ public class DimensionPropertyMaker extends CorrespondingPropertyMaker { } // Based on min-[width|height] - int wmcorr = propertyList.getWritingMode(extraCorresponding[0][0], + int wmcorr = propertyList.selectFromWritingMode(extraCorresponding[0][0], extraCorresponding[0][1], - extraCorresponding[0][2]); + extraCorresponding[0][2], + extraCorresponding[0][3]); Property subprop = propertyList.getExplicitOrShorthand(wmcorr); if (subprop != null) { baseMaker.setSubprop(p, Constants.CP_MINIMUM, subprop); } // Based on max-[width|height] - wmcorr = propertyList.getWritingMode(extraCorresponding[1][0], + wmcorr = propertyList.selectFromWritingMode(extraCorresponding[1][0], extraCorresponding[1][1], - extraCorresponding[1][2]); + extraCorresponding[1][2], + extraCorresponding[1][3]); subprop = propertyList.getExplicitOrShorthand(wmcorr); // TODO: Don't set when NONE. if (subprop != null) { diff --git a/src/java/org/apache/fop/fo/properties/IndentPropertyMaker.java b/src/java/org/apache/fop/fo/properties/IndentPropertyMaker.java index 19c4675ed..6b7c58a62 100644 --- a/src/java/org/apache/fop/fo/properties/IndentPropertyMaker.java +++ b/src/java/org/apache/fop/fo/properties/IndentPropertyMaker.java @@ -55,6 +55,9 @@ public class IndentPropertyMaker extends CorrespondingPropertyMaker { * @param paddingCorresponding the corresping propids. */ public void setPaddingCorresponding(int[] paddingCorresponding) { + if ( ( paddingCorresponding == null ) || ( paddingCorresponding.length != 4 ) ) { + throw new IllegalArgumentException(); + } this.paddingCorresponding = paddingCorresponding; } @@ -63,6 +66,9 @@ public class IndentPropertyMaker extends CorrespondingPropertyMaker { * @param borderWidthCorresponding the corresping propids. */ public void setBorderWidthCorresponding(int[] borderWidthCorresponding) { + if ( ( borderWidthCorresponding == null ) || ( borderWidthCorresponding.length != 4 ) ) { + throw new IllegalArgumentException(); + } this.borderWidthCorresponding = borderWidthCorresponding; } @@ -99,7 +105,7 @@ public class IndentPropertyMaker extends CorrespondingPropertyMaker { Numeric padding = getCorresponding(paddingCorresponding, propertyList).getNumeric(); Numeric border = getCorresponding(borderWidthCorresponding, propertyList).getNumeric(); - int marginProp = pList.getWritingMode(lrtb, rltb, tbrl); + int marginProp = pList.selectFromWritingMode(lrtb, rltb, tbrl, tblr); // Calculate the absolute margin. if (propertyList.getExplicitOrShorthand(marginProp) == null) { Property indent = propertyList.getExplicit(baseMaker.propId); @@ -158,7 +164,7 @@ public class IndentPropertyMaker extends CorrespondingPropertyMaker { Numeric padding = getCorresponding(paddingCorresponding, propertyList).getNumeric(); Numeric border = getCorresponding(borderWidthCorresponding, propertyList).getNumeric(); - int marginProp = pList.getWritingMode(lrtb, rltb, tbrl); + int marginProp = pList.selectFromWritingMode(lrtb, rltb, tbrl, tblr); //Determine whether the nearest anscestor indent was specified through //start-indent|end-indent or through a margin property. @@ -208,7 +214,8 @@ public class IndentPropertyMaker extends CorrespondingPropertyMaker { throws PropertyException { PropertyList pList = getWMPropertyList(propertyList); if (pList != null) { - int wmcorr = pList.getWritingMode(corresponding[0], corresponding[1], corresponding[2]); + int wmcorr = pList.selectFromWritingMode + ( corresponding[0], corresponding[1], corresponding[2], corresponding[3] ); return propertyList.get(wmcorr); } else { return null; diff --git a/src/java/org/apache/fop/fonts/BFEntry.java b/src/java/org/apache/fop/fonts/BFEntry.java index e04b021e4..d3c7956ba 100644 --- a/src/java/org/apache/fop/fonts/BFEntry.java +++ b/src/java/org/apache/fop/fonts/BFEntry.java @@ -68,9 +68,15 @@ public class BFEntry { @Override public String toString() { StringBuilder sb = new StringBuilder("BFEntry: "); - sb.append("Unicode ").append(getUnicodeStart()).append("..").append(getUnicodeEnd()); - sb.append(" --> ").append(getGlyphStartIndex()).append(".."); - sb.append(getGlyphStartIndex() + getUnicodeEnd() - getUnicodeStart()); + sb.append ( "{ UC[" ); + sb.append ( unicodeStart ); + sb.append ( ',' ); + sb.append ( unicodeEnd ); + sb.append ( "]: GC[" ); + sb.append ( glyphStartIndex ); + sb.append ( ',' ); + sb.append ( glyphStartIndex + ( unicodeEnd - unicodeStart ) ); + sb.append ( "] }" ); return sb.toString(); } diff --git a/src/java/org/apache/fop/fonts/CustomFont.java b/src/java/org/apache/fop/fonts/CustomFont.java index 4432fccef..c6b43fe98 100644 --- a/src/java/org/apache/fop/fonts/CustomFont.java +++ b/src/java/org/apache/fop/fonts/CustomFont.java @@ -60,6 +60,7 @@ public abstract class CustomFont extends Typeface private Map<Integer, Map<Integer, Integer>> kerning; private boolean useKerning = true; + private boolean useAdvanced = true; /** {@inheritDoc} */ public String getFontName() { @@ -285,6 +286,15 @@ public abstract class CustomFont extends Typeface } } + /** + * Used to determine if advanced typographic features are enabled. + * By default, this is false, but may be overridden by subclasses. + * @return true if enabled. + */ + public boolean isAdvancedEnabled() { + return useAdvanced; + } + /* ---- MutableFont interface ---- */ /** {@inheritDoc} */ @@ -428,6 +438,13 @@ public abstract class CustomFont extends Typeface } /** + * {@inheritDoc} + */ + public void setAdvancedEnabled(boolean enabled) { + this.useAdvanced = enabled; + } + + /** * Sets the font resolver. Needed for URI resolution. * @param resolver the font resolver */ diff --git a/src/java/org/apache/fop/fonts/CustomFontCollection.java b/src/java/org/apache/fop/fonts/CustomFontCollection.java index 9f98814a3..6e798a8f7 100644 --- a/src/java/org/apache/fop/fonts/CustomFontCollection.java +++ b/src/java/org/apache/fop/fonts/CustomFontCollection.java @@ -33,13 +33,14 @@ public class CustomFontCollection implements FontCollection { * Main constructor. * @param fontResolver a font resolver * @param customFonts the list of custom fonts + * @param useComplexScriptFeatures true if complex script features enabled */ public CustomFontCollection(FontResolver fontResolver, - List<EmbedFontInfo> customFonts) { + List<EmbedFontInfo> customFonts, boolean useComplexScriptFeatures) { this.fontResolver = fontResolver; if (this.fontResolver == null) { //Ensure that we have minimal font resolution capabilities - this.fontResolver = FontManager.createMinimalFontResolver(); + this.fontResolver = FontManager.createMinimalFontResolver(useComplexScriptFeatures); } this.embedFontInfoList = customFonts; } diff --git a/src/java/org/apache/fop/fonts/EmbedFontInfo.java b/src/java/org/apache/fop/fonts/EmbedFontInfo.java index b53cdfdd6..8848c0a87 100644 --- a/src/java/org/apache/fop/fonts/EmbedFontInfo.java +++ b/src/java/org/apache/fop/fonts/EmbedFontInfo.java @@ -29,7 +29,7 @@ import java.util.List; public class EmbedFontInfo implements Serializable { /** Serialization Version UID */ - private static final long serialVersionUID = 8755432068669997368L; + private static final long serialVersionUID = 8755432068669997369L; /** filename of the metrics file */ protected String metricsFile; @@ -37,6 +37,8 @@ public class EmbedFontInfo implements Serializable { protected String embedFile; /** false, to disable kerning */ protected boolean kerning; + /** false, to disable advanced typographic features */ + protected boolean advanced; /** the requested encoding mode for the font */ protected EncodingMode encodingMode = EncodingMode.AUTO; @@ -52,17 +54,19 @@ public class EmbedFontInfo implements Serializable { /** * Main constructor - * @param metricsFile Path to the xml file containing font metrics - * @param kerning True if kerning should be enabled - * @param fontTriplets List of font triplets to associate with this font - * @param embedFile Path to the embeddable font file (may be null) + * @param metricsFile path to the xml file containing font metrics + * @param kerning true if kerning should be enabled + * @param advanced true if advanced typography features should be enabled + * @param fontTriplets list of font triplets to associate with this font + * @param embedFile path to the embeddable font file (may be null) * @param subFontName the sub-fontname used for TrueType Collections (null otherwise) */ - public EmbedFontInfo(String metricsFile, boolean kerning, + public EmbedFontInfo(String metricsFile, boolean kerning, boolean advanced, List<FontTriplet> fontTriplets, String embedFile, String subFontName) { this.metricsFile = metricsFile; this.embedFile = embedFile; this.kerning = kerning; + this.advanced = advanced; this.fontTriplets = fontTriplets; this.subFontName = subFontName; } @@ -85,13 +89,21 @@ public class EmbedFontInfo implements Serializable { /** * Determines if kerning is enabled - * @return True if enabled + * @return true if enabled */ public boolean getKerning() { return kerning; } /** + * Determines if advanced typographic features are enabled + * @return true if enabled + */ + public boolean getAdvanced() { + return advanced; + } + + /** * Returns the sub-font name of the font. This is primarily used for TrueType Collections * to select one of the sub-fonts. For all other fonts, this is always null. * @return the sub-font name (or null) @@ -173,6 +185,7 @@ public class EmbedFontInfo implements Serializable { public String toString() { return "metrics-url=" + metricsFile + ", embed-url=" + embedFile + ", kerning=" + kerning + + ", advanced=" + advanced + ", enc-mode=" + encodingMode + ", font-triplet=" + fontTriplets + (getSubFontName() != null ? ", sub-font=" + getSubFontName() : "") diff --git a/src/java/org/apache/fop/fonts/Font.java b/src/java/org/apache/fop/fonts/Font.java index 8ca802234..ca387840a 100644 --- a/src/java/org/apache/fop/fonts/Font.java +++ b/src/java/org/apache/fop/fonts/Font.java @@ -25,11 +25,16 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.fop.complexscripts.fonts.Positionable; +import org.apache.fop.complexscripts.fonts.Substitutable; + +// CSOFF: LineLengthCheck + /** * This class holds font state information and provides access to the font * metrics. */ -public class Font { +public class Font implements Substitutable, Positionable { /** Extra Bold font weight */ public static final int WEIGHT_EXTRA_BOLD = 800; @@ -189,6 +194,30 @@ public class Font { } /** + * Returns the amount of kerning between two characters. + * + * The value returned measures in pt. So it is already adjusted for font size. + * + * @param ch1 first character + * @param ch2 second character + * @return the distance to adjust for kerning, 0 if there's no kerning + */ + public int getKernValue(int ch1, int ch2) { + // TODO !BMP + if ( ch1 > 0x10000 ) { + return 0; + } else if ( ( ch1 >= 0xD800 ) && ( ch1 <= 0xE000 ) ) { + return 0; + } else if ( ch2 > 0x10000 ) { + return 0; + } else if ( ( ch2 >= 0xD800 ) && ( ch2 <= 0xE000 ) ) { + return 0; + } else { + return getKernValue ( (char) ch1, (char) ch2 ); + } + } + + /** * Returns the width of a character * @param charnum character to look up * @return width of the character @@ -241,8 +270,8 @@ public class Font { */ @Override public String toString() { - StringBuffer sbuf = new StringBuffer(); - sbuf.append('('); + StringBuffer sbuf = new StringBuffer(super.toString()); + sbuf.append('{'); /* sbuf.append(fontFamily); sbuf.append(',');*/ @@ -254,7 +283,7 @@ public class Font { sbuf.append(fontStyle); sbuf.append(','); sbuf.append(fontWeight);*/ - sbuf.append(')'); + sbuf.append('}'); return sbuf.toString(); } @@ -264,7 +293,7 @@ public class Font { * This also performs some guessing on widths on various * versions of space that might not exists in the font. * @param c character to inspect - * @return the width of the character + * @return the width of the character or -1 if no width available */ public int getCharWidth(char c) { int width; @@ -329,6 +358,23 @@ public class Font { } /** + * Helper method for getting the width of a unicode char + * from the current fontstate. + * This also performs some guessing on widths on various + * versions of space that might not exists in the font. + * @param c character to inspect + * @return the width of the character or -1 if no width available + */ + public int getCharWidth(int c) { + if ( c < 0x10000 ) { + return getCharWidth ( (char) c ); + } else { + // TODO !BMP + return -1; + } + } + + /** * Calculates the word width. * @param word text to get width for * @return the width of the text @@ -347,6 +393,59 @@ public class Font { return width; } -} + /** {@inheritDoc} */ + public boolean performsSubstitution() { + if ( metric instanceof Substitutable ) { + Substitutable s = (Substitutable) metric; + return s.performsSubstitution(); + } else { + return false; + } + } + + /** {@inheritDoc} */ + public CharSequence performSubstitution ( CharSequence cs, String script, String language ) { + if ( metric instanceof Substitutable ) { + Substitutable s = (Substitutable) metric; + return s.performSubstitution ( cs, script, language ); + } else { + throw new UnsupportedOperationException(); + } + } + + /** {@inheritDoc} */ + public CharSequence reorderCombiningMarks ( CharSequence cs, int[][] gpa, String script, String language ) { + if ( metric instanceof Substitutable ) { + Substitutable s = (Substitutable) metric; + return s.reorderCombiningMarks ( cs, gpa, script, language ); + } else { + throw new UnsupportedOperationException(); + } + } + /** {@inheritDoc} */ + public boolean performsPositioning() { + if ( metric instanceof Positionable ) { + Positionable p = (Positionable) metric; + return p.performsPositioning(); + } else { + return false; + } + } + /** {@inheritDoc} */ + public int[][] performPositioning ( CharSequence cs, String script, String language, int fontSize ) { + if ( metric instanceof Positionable ) { + Positionable p = (Positionable) metric; + return p.performPositioning ( cs, script, language, fontSize ); + } else { + throw new UnsupportedOperationException(); + } + } + + /** {@inheritDoc} */ + public int[][] performPositioning ( CharSequence cs, String script, String language ) { + return performPositioning ( cs, script, language, fontSize ); + } + +} diff --git a/src/java/org/apache/fop/fonts/FontInfoConfigurator.java b/src/java/org/apache/fop/fonts/FontInfoConfigurator.java index 1f1116990..34b6ed1d0 100644 --- a/src/java/org/apache/fop/fonts/FontInfoConfigurator.java +++ b/src/java/org/apache/fop/fonts/FontInfoConfigurator.java @@ -253,20 +253,26 @@ public class FontInfoConfigurator { } boolean useKerning = fontCfg.getAttributeAsBoolean("kerning", true); + boolean useAdvanced = fontCfg.getAttributeAsBoolean("advanced", true); EncodingMode encodingMode = EncodingMode.getEncodingMode( fontCfg.getAttribute("encoding-mode", EncodingMode.AUTO.getName())); EmbedFontInfo embedFontInfo - = new EmbedFontInfo(metricsUrl, useKerning, tripletList, embedUrl, subFont); + = new EmbedFontInfo(metricsUrl, useKerning, useAdvanced, tripletList, embedUrl, + subFont); embedFontInfo.setEncodingMode(encodingMode); + boolean skipCachedFont = false; if (fontCache != null) { if (!fontCache.containsFont(embedFontInfo)) { fontCache.addFont(embedFontInfo); + } else { + skipCachedFont = true; } } if (log.isDebugEnabled()) { String embedFile = embedFontInfo.getEmbedFile(); - log.debug("Adding font " + (embedFile != null ? embedFile + ", " : "") + log.debug( ( skipCachedFont ? "Skipping (cached) font " : "Adding font " ) + + (embedFile != null ? embedFile + ", " : "") + "metric file " + embedFontInfo.getMetricsFile()); for (int j = 0; j < tripletList.size(); ++j) { FontTriplet triplet = tripletList.get(j); diff --git a/src/java/org/apache/fop/fonts/FontLoader.java b/src/java/org/apache/fop/fonts/FontLoader.java index 02c09a1a1..91b763939 100644 --- a/src/java/org/apache/fop/fonts/FontLoader.java +++ b/src/java/org/apache/fop/fonts/FontLoader.java @@ -43,31 +43,36 @@ public abstract class FontLoader { protected static final Log log = LogFactory.getLog(FontLoader.class); /** URI representing the font file */ - protected String fontFileURI = null; + protected String fontFileURI; /** the FontResolver to use for font URI resolution */ - protected FontResolver resolver = null; + protected FontResolver resolver; /** the loaded font */ - protected CustomFont returnFont = null; + protected CustomFont returnFont; /** true if the font has been loaded */ - protected boolean loaded = false; + protected boolean loaded; /** true if the font will be embedded, false if it will be referenced only. */ - protected boolean embedded = true; - /** true if kerning information shall be loaded if available. */ - protected boolean useKerning = true; + protected boolean embedded; + /** true if kerning information false be loaded if available. */ + protected boolean useKerning; + /** true if advanced typographic information shall be loaded if available. */ + protected boolean useAdvanced; /** * Default constructor. * @param fontFileURI the URI to the PFB file of a Type 1 font * @param embedded indicates whether the font is embedded or referenced * @param useKerning indicates whether kerning information shall be loaded if available + * @param useAdvanced indicates whether advanced typographic information shall be loaded if + * available * @param resolver the font resolver used to resolve URIs */ public FontLoader(String fontFileURI, boolean embedded, boolean useKerning, - FontResolver resolver) { + boolean useAdvanced, FontResolver resolver) { this.fontFileURI = fontFileURI; this.embedded = embedded; this.useKerning = useKerning; + this.useAdvanced = useAdvanced; this.resolver = resolver; } @@ -105,7 +110,7 @@ public abstract class FontLoader { boolean embedded, EncodingMode encodingMode, FontResolver resolver) throws IOException { return loadFont(fontUrl.toExternalForm(), subFontName, - embedded, encodingMode, true, + embedded, encodingMode, true, true, resolver); } @@ -116,13 +121,15 @@ public abstract class FontLoader { * @param embedded indicates whether the font is embedded or referenced * @param encodingMode the requested encoding mode * @param useKerning indicates whether kerning information should be loaded if available + * @param useAdvanced indicates whether advanced typographic information shall be loaded if + * available * @param resolver the font resolver to use when resolving URIs * @return the newly loaded font * @throws IOException In case of an I/O error */ public static CustomFont loadFont(String fontFileURI, String subFontName, boolean embedded, EncodingMode encodingMode, boolean useKerning, - FontResolver resolver) throws IOException { + boolean useAdvanced, FontResolver resolver) throws IOException { fontFileURI = fontFileURI.trim(); boolean type1 = isType1(fontFileURI); FontLoader loader; @@ -134,7 +141,7 @@ public abstract class FontLoader { loader = new Type1FontLoader(fontFileURI, embedded, useKerning, resolver); } else { loader = new TTFFontLoader(fontFileURI, subFontName, - embedded, encodingMode, useKerning, resolver); + embedded, encodingMode, useKerning, useAdvanced, resolver); } return loader.getFont(); } diff --git a/src/java/org/apache/fop/fonts/FontManager.java b/src/java/org/apache/fop/fonts/FontManager.java index 51516e231..9285c94be 100644 --- a/src/java/org/apache/fop/fonts/FontManager.java +++ b/src/java/org/apache/fop/fonts/FontManager.java @@ -224,16 +224,32 @@ public class FontManager { getFontSubstitutions().adjustFontInfo(fontInfo); } - /** @return a new FontResolver to be used by the font subsystem */ - public static FontResolver createMinimalFontResolver() { - return new FontResolver() { - - /** {@inheritDoc} */ - public Source resolve(String href) { - //Minimal functionality here - return new StreamSource(href); - } - }; + /** + * Minimum implemenation of FontResolver. + */ + public static class MinimalFontResolver implements FontResolver { + private boolean useComplexScriptFeatures; + MinimalFontResolver(boolean useComplexScriptFeatures) { + this.useComplexScriptFeatures = useComplexScriptFeatures; + } + /** {@inheritDoc} */ + public Source resolve(String href) { + //Minimal functionality here + return new StreamSource(href); + } + /** {@inheritDoc} */ + public boolean isComplexScriptFeaturesEnabled() { + return useComplexScriptFeatures; + } + } + + /** + * Create minimal font resolver. + * @param useComplexScriptFeatures true if complex script features enabled + * @return a new FontResolver to be used by the font subsystem + */ + public static FontResolver createMinimalFontResolver(boolean useComplexScriptFeatures) { + return new MinimalFontResolver ( useComplexScriptFeatures ); } /** diff --git a/src/java/org/apache/fop/fonts/FontManagerConfigurator.java b/src/java/org/apache/fop/fonts/FontManagerConfigurator.java index 6935df7e3..40e163d42 100644 --- a/src/java/org/apache/fop/fonts/FontManagerConfigurator.java +++ b/src/java/org/apache/fop/fonts/FontManagerConfigurator.java @@ -98,6 +98,18 @@ public class FontManagerConfigurator { } } + // [GA] permit configuration control over base14 kerning; without this, + // there is no way for a user to enable base14 kerning other than by + // programmatic API; + if (cfg.getChild("base14-kerning", false) != null) { + try { + fontManager + .setBase14KerningEnabled(cfg.getChild("base14-kerning").getValueAsBoolean()); + } catch (ConfigurationException e) { + LogUtil.handleException(log, e, true); + } + } + // global font configuration Configuration fontsCfg = cfg.getChild("fonts", false); if (fontsCfg != null) { diff --git a/src/java/org/apache/fop/fonts/FontReader.java b/src/java/org/apache/fop/fonts/FontReader.java index e9b88ec16..0f0348fc1 100644 --- a/src/java/org/apache/fop/fonts/FontReader.java +++ b/src/java/org/apache/fop/fonts/FontReader.java @@ -34,6 +34,7 @@ import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; @@ -52,7 +53,7 @@ import org.apache.fop.fonts.apps.TTFReader; */ public class FontReader extends DefaultHandler { - private Locator locator = null; + // private Locator locator = null; // not used at present private boolean isCID = false; private CustomFont returnFont = null; private MultiByteFont multiFont = null; @@ -117,6 +118,14 @@ public class FontReader extends DefaultHandler { } /** + * Enable/disable use of advanced typographic features for the font + * @param enabled true to enable, false to disable + */ + public void setAdvancedEnabled(boolean enabled) { + returnFont.setAdvancedEnabled(enabled); + } + + /** * Sets the font resolver. Needed for URI resolution. * @param resolver the font resolver */ @@ -153,7 +162,7 @@ public class FontReader extends DefaultHandler { * {@inheritDoc} */ public void setDocumentLocator(Locator locator) { - this.locator = locator; + // this.locator = locator; // not used at present } /** @@ -212,6 +221,7 @@ public class FontReader extends DefaultHandler { currentKerning.put(new Integer(attributes.getValue("kpx2")), new Integer(attributes.getValue("kern"))); } + } private int getInt(String str) throws SAXException { @@ -305,6 +315,5 @@ public class FontReader extends DefaultHandler { public void characters(char[] ch, int start, int length) { text.append(ch, start, length); } -} - +} diff --git a/src/java/org/apache/fop/fonts/FontResolver.java b/src/java/org/apache/fop/fonts/FontResolver.java index 1da2339f7..bd155482e 100644 --- a/src/java/org/apache/fop/fonts/FontResolver.java +++ b/src/java/org/apache/fop/fonts/FontResolver.java @@ -36,4 +36,10 @@ public interface FontResolver { */ Source resolve(String href); + /** + * Check whether complex script features are enabled. + * @return true if FOP is to use complex script features + */ + boolean isComplexScriptFeaturesEnabled(); + } diff --git a/src/java/org/apache/fop/fonts/FontSetup.java b/src/java/org/apache/fop/fonts/FontSetup.java index 935f695b1..ba31b2d50 100644 --- a/src/java/org/apache/fop/fonts/FontSetup.java +++ b/src/java/org/apache/fop/fonts/FontSetup.java @@ -57,9 +57,10 @@ public final class FontSetup { /** * Sets up a font info * @param fontInfo font info + * @param base14Kerning true if base14 kerning applies */ - public static void setup(FontInfo fontInfo) { - setup(fontInfo, null, null); + public static void setup(FontInfo fontInfo, boolean base14Kerning) { + setup(fontInfo, null, null, base14Kerning); } /** @@ -71,10 +72,10 @@ public final class FontSetup { * @param fontInfo the font info object to set up * @param embedFontInfoList a list of EmbedFontInfo objects * @param resolver the font resolver + * @param base14Kerning true if base14 kerning applies */ public static void setup(FontInfo fontInfo, List<EmbedFontInfo> embedFontInfoList, - FontResolver resolver) { - final boolean base14Kerning = false; + FontResolver resolver, boolean base14Kerning) { fontInfo.addMetrics("F1", new Helvetica(base14Kerning)); fontInfo.addMetrics("F2", new HelveticaOblique(base14Kerning)); fontInfo.addMetrics("F3", new HelveticaBold(base14Kerning)); @@ -180,7 +181,7 @@ public final class FontSetup { final int startNum = 15; /* Add configured fonts */ - addConfiguredFonts(fontInfo, embedFontInfoList, startNum, resolver); + addConfiguredFonts(fontInfo, embedFontInfoList, startNum, resolver, base14Kerning); } /** @@ -191,14 +192,17 @@ public final class FontSetup { * @param resolver the font resolver */ private static void addConfiguredFonts(FontInfo fontInfo, - List<EmbedFontInfo> embedFontInfoList, int num, FontResolver resolver) { + List<EmbedFontInfo> embedFontInfoList, int num, FontResolver resolver, + boolean base14Kerning) { if (embedFontInfoList == null) { return; //No fonts to process } if (resolver == null) { //Ensure that we have minimal font resolution capabilities - resolver = createMinimalFontResolver(); + //None of the built-in base14 fonts have advanced typographic data + boolean useAdvanced = false; + resolver = createMinimalFontResolver(useAdvanced); } String internalName = null; @@ -218,15 +222,31 @@ public final class FontSetup { } } - /** @return a new FontResolver to be used by the font subsystem */ - public static FontResolver createMinimalFontResolver() { - return new FontResolver() { + /** + * Minimum implemenation of FontResolver. + */ + public static class MinimalFontResolver implements FontResolver { + private boolean useComplexScriptFeatures; + MinimalFontResolver(boolean useComplexScriptFeatures) { + this.useComplexScriptFeatures = useComplexScriptFeatures; + } + /** {@inheritDoc} */ + public Source resolve(String href) { + //Minimal functionality here + return new StreamSource(href); + } + /** {@inheritDoc} */ + public boolean isComplexScriptFeaturesEnabled() { + return useComplexScriptFeatures; + } + } - /** {@inheritDoc} */ - public Source resolve(String href) { - //Minimal functionality here - return new StreamSource(href); - } - }; + /** + * Create minimal font resolver. + * @param useComplexScriptFeatures true if complex script features enabled + * @return a new FontResolver to be used by the font subsystem + */ + public static FontResolver createMinimalFontResolver(boolean useComplexScriptFeatures) { + return new MinimalFontResolver ( useComplexScriptFeatures ); } } diff --git a/src/java/org/apache/fop/fonts/LazyFont.java b/src/java/org/apache/fop/fonts/LazyFont.java index f331a331e..dfd2367b2 100644 --- a/src/java/org/apache/fop/fonts/LazyFont.java +++ b/src/java/org/apache/fop/fonts/LazyFont.java @@ -29,28 +29,34 @@ import javax.xml.transform.stream.StreamSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.apache.fop.apps.FOPException; +import org.apache.fop.complexscripts.fonts.Positionable; +import org.apache.fop.complexscripts.fonts.Substitutable; + + import org.xml.sax.InputSource; /** * This class is used to defer the loading of a font until it is really used. */ -public class LazyFont extends Typeface implements FontDescriptor { +public class LazyFont extends Typeface implements FontDescriptor, Substitutable, Positionable { private static Log log = LogFactory.getLog(LazyFont.class); - private String metricsFileName = null; - private String fontEmbedPath = null; - private boolean useKerning = false; + private String metricsFileName; + private String fontEmbedPath; + private boolean useKerning; + private boolean useAdvanced; private EncodingMode encodingMode = EncodingMode.AUTO; - private boolean embedded = true; - private String subFontName = null; + private boolean embedded; + private String subFontName; - private boolean isMetricsLoaded = false; - private Typeface realFont = null; - private FontDescriptor realFontDescriptor = null; + private boolean isMetricsLoaded; + private Typeface realFont; + private FontDescriptor realFontDescriptor; - private FontResolver resolver = null; + private FontResolver resolver; /** * Main constructor @@ -62,6 +68,11 @@ public class LazyFont extends Typeface implements FontDescriptor { this.metricsFileName = fontInfo.getMetricsFile(); this.fontEmbedPath = fontInfo.getEmbedFile(); this.useKerning = fontInfo.getKerning(); + if ( resolver != null ) { + this.useAdvanced = resolver.isComplexScriptFeaturesEnabled(); + } else { + this.useAdvanced = fontInfo.getAdvanced(); + } this.encodingMode = fontInfo.getEncodingMode(); this.subFontName = fontInfo.getSubFontName(); this.embedded = fontInfo.isEmbedded(); @@ -70,9 +81,15 @@ public class LazyFont extends Typeface implements FontDescriptor { /** {@inheritDoc} */ public String toString() { - return ( "metrics-url=" + metricsFileName + ", embed-url=" + fontEmbedPath - + ", kerning=" + useKerning ); - } + StringBuffer sbuf = new StringBuffer(super.toString()); + sbuf.append('{'); + sbuf.append("metrics-url=" + metricsFileName); + sbuf.append(",embed-url=" + fontEmbedPath); + sbuf.append(",kerning=" + useKerning); + sbuf.append(",advanced=" + useAdvanced); + sbuf.append('}'); + return sbuf.toString(); + } private void load(boolean fail) { if (!isMetricsLoaded) { @@ -120,6 +137,7 @@ public class LazyFont extends Typeface implements FontDescriptor { new URL(metricsFileName).openStream())); } reader.setKerningEnabled(useKerning); + reader.setAdvancedEnabled(useAdvanced); if (this.embedded) { reader.setFontEmbedPath(fontEmbedPath); } @@ -130,7 +148,7 @@ public class LazyFont extends Typeface implements FontDescriptor { throw new RuntimeException("Cannot load font. No font URIs available."); } realFont = FontLoader.loadFont(fontEmbedPath, this.subFontName, - this.embedded, this.encodingMode, useKerning, resolver); + this.embedded, this.encodingMode, useKerning, useAdvanced, resolver); } if (realFont instanceof FontDescriptor) { realFontDescriptor = (FontDescriptor) realFont; @@ -376,6 +394,84 @@ public class LazyFont extends Typeface implements FontDescriptor { /** * {@inheritDoc} */ + public boolean performsSubstitution() { + load(true); + if ( realFontDescriptor instanceof Substitutable ) { + return ((Substitutable)realFontDescriptor).performsSubstitution(); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public CharSequence performSubstitution ( CharSequence cs, String script, String language ) { + load(true); + if ( realFontDescriptor instanceof Substitutable ) { + return ((Substitutable)realFontDescriptor).performSubstitution(cs, script, language); + } else { + return cs; + } + } + + /** + * {@inheritDoc} + */ + public CharSequence reorderCombiningMarks + ( CharSequence cs, int[][] gpa, String script, String language ) { + load(true); + if ( realFontDescriptor instanceof Substitutable ) { + return ((Substitutable)realFontDescriptor). + reorderCombiningMarks(cs, gpa, script, language); + } else { + return cs; + } + } + + /** + * {@inheritDoc} + */ + public boolean performsPositioning() { + load(true); + if ( realFontDescriptor instanceof Positionable ) { + return ((Positionable)realFontDescriptor).performsPositioning(); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public int[][] + performPositioning ( CharSequence cs, String script, String language, int fontSize ) { + load(true); + if ( realFontDescriptor instanceof Positionable ) { + return ((Positionable)realFontDescriptor) + .performPositioning(cs, script, language, fontSize); + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ + public int[][] + performPositioning ( CharSequence cs, String script, String language ) { + load(true); + if ( realFontDescriptor instanceof Positionable ) { + return ((Positionable)realFontDescriptor) + .performPositioning(cs, script, language); + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ public boolean isSubsetEmbedded() { load(true); return realFont.isMultiByte(); diff --git a/src/java/org/apache/fop/fonts/MultiByteFont.java b/src/java/org/apache/fop/fonts/MultiByteFont.java index 0d9897268..3b15662dc 100644 --- a/src/java/org/apache/fop/fonts/MultiByteFont.java +++ b/src/java/org/apache/fop/fonts/MultiByteFont.java @@ -19,14 +19,29 @@ package org.apache.fop.fonts; -//Java +import java.nio.CharBuffer; +import java.nio.IntBuffer; import java.util.Map; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; +import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; +import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable; +import org.apache.fop.complexscripts.fonts.Positionable; +import org.apache.fop.complexscripts.fonts.Substitutable; +import org.apache.fop.complexscripts.util.GlyphSequence; +import org.apache.fop.util.CharUtilities; /** * Generic MultiByte (CID) font */ -public class MultiByteFont extends CIDFont { +public class MultiByteFont extends CIDFont implements Substitutable, Positionable { + + /** logging instance */ + private static final Log log // CSOK: ConstantNameCheck + = LogFactory.getLog(MultiByteFont.class); private String ttcName = null; private String encoding = "Identity-H"; @@ -36,9 +51,28 @@ public class MultiByteFont extends CIDFont { private CIDSubset subset = new CIDSubset(); - /** A map from Unicode indices to glyph indices */ + /** + * A map from Unicode indices to glyph indices. No assumption + * about ordering is made below. If lookup is changed to a binary + * search (from the current linear search), then addPrivateUseMapping() + * needs to be changed to perform ordered inserts. + */ private BFEntry[] bfentries = null; + /* advanced typographic support */ + private GlyphDefinitionTable gdef; + private GlyphSubstitutionTable gsub; + private GlyphPositioningTable gpos; + + /* dynamic private use (character) mappings */ + private int numMapped; + private int numUnmapped; + private int nextPrivateUse = 0xE000; + private int firstPrivate; + private int lastPrivate; + private int firstUnmapped; + private int lastUnmapped; + /** * Default constructor */ @@ -132,13 +166,14 @@ public class MultiByteFont extends CIDFont { * @param c the Unicode character index * @return the glyph index (or 0 if the glyph is not available) */ - private int findGlyphIndex(char c) { - int idx = (int)c; + // [TBD] - needs optimization, i.e., change from linear search to binary search + private int findGlyphIndex(int c) { + int idx = c; int retIdx = SingleByteEncoding.NOT_FOUND_CODE_POINT; for (int i = 0; (i < bfentries.length) && retIdx == 0; i++) { if (bfentries[i].getUnicodeStart() <= idx - && bfentries[i].getUnicodeEnd() >= idx) { + && bfentries[i].getUnicodeEnd() >= idx) { retIdx = bfentries[i].getGlyphStartIndex() + idx @@ -148,6 +183,95 @@ public class MultiByteFont extends CIDFont { return retIdx; } + /** + * Add a private use mapping {PU,GI} to the existing BFENTRIES map. + * N.B. Does not insert in order, merely appends to end of existing map. + */ + private synchronized void addPrivateUseMapping ( int pu, int gi ) { + assert findGlyphIndex ( pu ) == SingleByteEncoding.NOT_FOUND_CODE_POINT; + BFEntry[] bfeOld = bfentries; + int bfeCnt = bfeOld.length; + BFEntry[] bfeNew = new BFEntry [ bfeCnt + 1 ]; + System.arraycopy ( bfeOld, 0, bfeNew, 0, bfeCnt ); + bfeNew [ bfeCnt ] = new BFEntry ( pu, pu, gi ); + bfentries = bfeNew; + } + + /** + * Given a glyph index, create a new private use mapping, augmenting the bfentries + * table. This is needed to accommodate the presence of an (output) glyph index in a + * complex script glyph substitution that does not correspond to a character in the + * font's CMAP. The creation of such private use mappings is deferred until an + * attempt is actually made to perform the reverse lookup from the glyph index. This + * is necessary in order to avoid exhausting the private use space on fonts containing + * many such non-mapped glyph indices, if these mappings had been created statically + * at font load time. + * @param gi glyph index + * @returns unicode scalar value + */ + private int createPrivateUseMapping ( int gi ) { + while ( ( nextPrivateUse < 0xF900 ) + && ( findGlyphIndex(nextPrivateUse) != SingleByteEncoding.NOT_FOUND_CODE_POINT ) ) { + nextPrivateUse++; + } + if ( nextPrivateUse < 0xF900 ) { + int pu = nextPrivateUse; + addPrivateUseMapping ( pu, gi ); + if ( firstPrivate == 0 ) { + firstPrivate = pu; + } + lastPrivate = pu; + numMapped++; + if (log.isDebugEnabled()) { + log.debug ( "Create private use mapping from " + + CharUtilities.format ( pu ) + + " to glyph index " + gi + + " in font '" + getFullName() + "'" ); + } + return pu; + } else { + if ( firstUnmapped == 0 ) { + firstUnmapped = gi; + } + lastUnmapped = gi; + numUnmapped++; + log.warn ( "Exhausted private use area: unable to map " + + numUnmapped + " glyphs in glyph index range [" + + firstUnmapped + "," + lastUnmapped + + "] (inclusive) of font '" + getFullName() + "'" ); + return 0; + } + } + + /** + * Returns the Unicode scalar value that corresponds to the glyph index. If more than + * one correspondence exists, then the first one is returned (ordered by bfentries[]). + * @param gi glyph index + * @returns unicode scalar value + */ + // [TBD] - needs optimization, i.e., change from linear search to binary search + private int findCharacterFromGlyphIndex ( int gi, boolean augment ) { + int cc = 0; + for ( int i = 0, n = bfentries.length; i < n; i++ ) { + BFEntry be = bfentries [ i ]; + int s = be.getGlyphStartIndex(); + int e = s + ( be.getUnicodeEnd() - be.getUnicodeStart() ); + if ( ( gi >= s ) && ( gi <= e ) ) { + cc = be.getUnicodeStart() + ( gi - s ); + break; + } + } + if ( ( cc == 0 ) && augment ) { + cc = createPrivateUseMapping ( gi ); + } + return cc; + } + + private int findCharacterFromGlyphIndex ( int gi ) { + return findCharacterFromGlyphIndex ( gi, true ); + } + + /** {@inheritDoc} */ public char mapChar(char c) { notifyMapOperation(); @@ -223,5 +347,220 @@ public class MultiByteFont extends CIDFont { } return subset.getSubsetChars(); } + + /** + * Establishes the glyph definition table. + * @param gdef the glyph definition table to be used by this font + */ + public void setGDEF ( GlyphDefinitionTable gdef ) { + if ( ( this.gdef == null ) || ( gdef == null ) ) { + this.gdef = gdef; + } else { + throw new IllegalStateException ( "font already associated with GDEF table" ); + } + } + + /** + * Obtain glyph definition table. + * @return glyph definition table or null if none is associated with font + */ + public GlyphDefinitionTable getGDEF() { + return gdef; + } + + /** + * Establishes the glyph substitution table. + * @param gsub the glyph substitution table to be used by this font + */ + public void setGSUB ( GlyphSubstitutionTable gsub ) { + if ( ( this.gsub == null ) || ( gsub == null ) ) { + this.gsub = gsub; + } else { + throw new IllegalStateException ( "font already associated with GSUB table" ); + } + } + + /** + * Obtain glyph substitution table. + * @return glyph substitution table or null if none is associated with font + */ + public GlyphSubstitutionTable getGSUB() { + return gsub; + } + + /** + * Establishes the glyph positioning table. + * @param gpos the glyph positioning table to be used by this font + */ + public void setGPOS ( GlyphPositioningTable gpos ) { + if ( ( this.gpos == null ) || ( gpos == null ) ) { + this.gpos = gpos; + } else { + throw new IllegalStateException ( "font already associated with GPOS table" ); + } + } + + /** + * Obtain glyph positioning table. + * @return glyph positioning table or null if none is associated with font + */ + public GlyphPositioningTable getGPOS() { + return gpos; + } + + /** {@inheritDoc} */ + public boolean performsSubstitution() { + return gsub != null; + } + + /** {@inheritDoc} */ + public CharSequence performSubstitution ( CharSequence cs, String script, String language ) { + if ( gsub != null ) { + GlyphSequence igs = mapCharsToGlyphs ( cs ); + GlyphSequence ogs = gsub.substitute ( igs, script, language ); + CharSequence ocs = mapGlyphsToChars ( ogs ); + return ocs; + } else { + return cs; + } + } + + /** {@inheritDoc} */ + public CharSequence reorderCombiningMarks + ( CharSequence cs, int[][] gpa, String script, String language ) { + if ( gdef != null ) { + GlyphSequence igs = mapCharsToGlyphs ( cs ); + GlyphSequence ogs = gdef.reorderCombiningMarks ( igs, gpa, script, language ); + CharSequence ocs = mapGlyphsToChars ( ogs ); + return ocs; + } else { + return cs; + } + } + + /** {@inheritDoc} */ + public boolean performsPositioning() { + return gpos != null; + } + + /** {@inheritDoc} */ + public int[][] + performPositioning ( CharSequence cs, String script, String language, int fontSize ) { + if ( gpos != null ) { + GlyphSequence gs = mapCharsToGlyphs ( cs ); + int[][] adjustments = new int [ gs.getGlyphCount() ] [ 4 ]; + if ( gpos.position ( gs, script, language, fontSize, this.width, adjustments ) ) { + return scaleAdjustments ( adjustments, fontSize ); + } else { + return null; + } + } else { + return null; + } + } + + /** {@inheritDoc} */ + public int[][] performPositioning ( CharSequence cs, String script, String language ) { + throw new UnsupportedOperationException(); + } + + + private int[][] scaleAdjustments ( int[][] adjustments, int fontSize ) { + if ( adjustments != null ) { + for ( int i = 0, n = adjustments.length; i < n; i++ ) { + int[] gpa = adjustments [ i ]; + for ( int k = 0; k < 4; k++ ) { + gpa [ k ] = ( gpa [ k ] * fontSize ) / 1000; + } + } + return adjustments; + } else { + return null; + } + } + + /** + * Map sequence CS, comprising a sequence of UTF-16 encoded Unicode Code Points, to + * an output character sequence GS, comprising a sequence of Glyph Indices. N.B. Unlike + * mapChar(), this method does not make use of embedded subset encodings. + * @param cs a CharSequence containing UTF-16 encoded Unicode characters + * @returns a CharSequence containing glyph indices + */ + private GlyphSequence mapCharsToGlyphs ( CharSequence cs ) { + IntBuffer cb = IntBuffer.allocate ( cs.length() ); + IntBuffer gb = IntBuffer.allocate ( cs.length() ); + int gi, giMissing = findGlyphIndex ( Typeface.NOT_FOUND ); + for ( int i = 0, n = cs.length(); i < n; i++ ) { + int cc = cs.charAt ( i ); + if ( ( cc >= 0xD800 ) && ( cc < 0xDC00 ) ) { + if ( ( i + 1 ) < n ) { + int sh = cc; + int sl = cs.charAt ( ++i ); + if ( ( sl >= 0xDC00 ) && ( sl < 0xE000 ) ) { + cc = 0x10000 + ( ( sh - 0xD800 ) << 10 ) + ( ( sl - 0xDC00 ) << 0 ); + } else { + throw new IllegalArgumentException + ( "ill-formed UTF-16 sequence, " + + "contains isolated high surrogate at index " + i ); + } + } else { + throw new IllegalArgumentException + ( "ill-formed UTF-16 sequence, " + + "contains isolated high surrogate at end of sequence" ); + } + } else if ( ( cc >= 0xDC00 ) && ( cc < 0xE000 ) ) { + throw new IllegalArgumentException + ( "ill-formed UTF-16 sequence, " + + "contains isolated low surrogate at index " + i ); + } + notifyMapOperation(); + gi = findGlyphIndex ( cc ); + if ( gi == SingleByteEncoding.NOT_FOUND_CODE_POINT ) { + warnMissingGlyph ( (char) cc ); + gi = giMissing; + } + cb.put ( cc ); + gb.put ( gi ); + } + cb.flip(); + gb.flip(); + return new GlyphSequence ( cb, gb, null ); + } + + /** + * Map sequence GS, comprising a sequence of Glyph Indices, to output sequence CS, + * comprising a sequence of UTF-16 encoded Unicode Code Points. + * @param gs a GlyphSequence containing glyph indices + * @returns a CharSequence containing UTF-16 encoded Unicode characters + */ + private CharSequence mapGlyphsToChars ( GlyphSequence gs ) { + int ng = gs.getGlyphCount(); + CharBuffer cb = CharBuffer.allocate ( ng ); + int ccMissing = Typeface.NOT_FOUND; + for ( int i = 0, n = ng; i < n; i++ ) { + int gi = gs.getGlyph ( i ); + int cc = findCharacterFromGlyphIndex ( gi ); + if ( ( cc == 0 ) || ( cc > 0x10FFFF ) ) { + cc = ccMissing; + log.warn("Unable to map glyph index " + gi + + " to Unicode scalar in font '" + + getFullName() + "', substituting missing character '" + + (char) cc + "'"); + } + if ( cc > 0x00FFFF ) { + int sh, sl; + cc -= 0x10000; + sh = ( ( cc >> 10 ) & 0x3FF ) + 0xD800; + sl = ( ( cc >> 0 ) & 0x3FF ) + 0xDC00; + cb.put ( (char) sh ); + cb.put ( (char) sl ); + } else { + cb.put ( (char) cc ); + } + } + cb.flip(); + return (CharSequence) cb; + } + } diff --git a/src/java/org/apache/fop/fonts/MutableFont.java b/src/java/org/apache/fop/fonts/MutableFont.java index bcbcadbdc..41c552a0b 100644 --- a/src/java/org/apache/fop/fonts/MutableFont.java +++ b/src/java/org/apache/fop/fonts/MutableFont.java @@ -133,6 +133,12 @@ public interface MutableFont { void setKerningEnabled(boolean enabled); /** + * Enables/disabled advanced typographic features. + * @param enabled true if advanced typographic features should be enabled if available + */ + void setAdvancedEnabled(boolean enabled); + + /** * Adds an entry to the kerning table. * @param key Kerning key * @param value Kerning value diff --git a/src/java/org/apache/fop/fonts/Typeface.java b/src/java/org/apache/fop/fonts/Typeface.java index a0c1d99ec..e933781cd 100644 --- a/src/java/org/apache/fop/fonts/Typeface.java +++ b/src/java/org/apache/fop/fonts/Typeface.java @@ -143,6 +143,10 @@ public abstract class Typeface implements FontMetrics { /** {@inheritDoc} */ public String toString() { - return getFullName(); + StringBuffer sbuf = new StringBuffer(super.toString()); + sbuf.append('{'); + sbuf.append(getFullName()); + sbuf.append('}'); + return sbuf.toString(); } } diff --git a/src/java/org/apache/fop/fonts/apps/AbstractFontReader.java b/src/java/org/apache/fop/fonts/apps/AbstractFontReader.java index 1c973cd51..89c7890af 100644 --- a/src/java/org/apache/fop/fonts/apps/AbstractFontReader.java +++ b/src/java/org/apache/fop/fonts/apps/AbstractFontReader.java @@ -67,7 +67,7 @@ public abstract class AbstractFontReader { List arguments = new java.util.ArrayList(); for (int i = 0; i < args.length; i++) { if (args[i].startsWith("-")) { - if ("-d".equals(args[i]) || "-q".equals(args[i])) { + if ("-t".equals(args[i]) || "-d".equals(args[i]) || "-q".equals(args[i])) { options.put(args[i], ""); } else if ((i + 1) < args.length && !args[i + 1].startsWith("-")) { options.put(args[i], args[i + 1]); @@ -101,7 +101,9 @@ public abstract class AbstractFontReader { */ protected static void determineLogLevel(Map options) { //Determine log level - if (options.get("-d") != null) { + if (options.get("-t") != null) { + setLogLevel("trace"); + } else if (options.get("-d") != null) { setLogLevel("debug"); } else if (options.get("-q") != null) { setLogLevel("error"); diff --git a/src/java/org/apache/fop/fonts/apps/TTFReader.java b/src/java/org/apache/fop/fonts/apps/TTFReader.java index 6e64f9144..1804a8dd9 100644 --- a/src/java/org/apache/fop/fonts/apps/TTFReader.java +++ b/src/java/org/apache/fop/fonts/apps/TTFReader.java @@ -21,6 +21,7 @@ package org.apache.fop.fonts.apps; import java.io.IOException; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; @@ -38,6 +39,9 @@ import org.w3c.dom.Element; import org.xml.sax.Attributes; import org.xml.sax.SAXException; +// CSOFF: InnerAssignmentCheck +// CSOFF: LineLengthCheck + /** * A tool which reads TTF files and generates * XML font metrics file for use in FOP. @@ -61,6 +65,7 @@ public class TTFReader extends AbstractFontReader { "java " + TTFReader.class.getName() + " [options] fontfile.ttf xmlfile.xml"); System.out.println(); System.out.println("where options can be:"); + System.out.println("-t Trace mode"); System.out.println("-d Debug mode"); System.out.println("-q Quiet mode"); System.out.println("-enc ansi"); @@ -102,6 +107,8 @@ public class TTFReader extends AbstractFontReader { * you can also include the fontfile in the fop.jar file when building fop. * You can use both -ef and -er. The file specified in -ef will be searched first, * then the -er file. + * -nocs + * if complex script features are disabled */ public static void main(String[] args) { String embFile = null; @@ -155,13 +162,19 @@ public class TTFReader extends AbstractFontReader { className = (String)options.get("-cn"); } + boolean useKerning = true; + boolean useAdvanced = true; + if (options.get("-nocs") != null) { + useAdvanced = false; + } + if (arguments.length != 2 || options.get("-h") != null || options.get("-help") != null || options.get("--help") != null) { displayUsage(); } else { try { log.info("Parsing font..."); - TTFFile ttf = app.loadTTF(arguments[0], ttcName); + TTFFile ttf = app.loadTTF(arguments[0], ttcName, useKerning, useAdvanced); if (ttf != null) { org.w3c.dom.Document doc = app.constructFontXML(ttf, fontName, className, embResource, embFile, isCid, @@ -198,11 +211,13 @@ public class TTFReader extends AbstractFontReader { * * @param fileName The filename of the TTF file. * @param fontName The name of the font + * @param useKerning true if should load kerning data + * @param useAdvanced true if should load advanced typographic table data * @return The TTF as an object, null if the font is incompatible. * @throws IOException In case of an I/O problem */ - public TTFFile loadTTF(String fileName, String fontName) throws IOException { - TTFFile ttfFile = new TTFFile(); + public TTFFile loadTTF(String fileName, String fontName, boolean useKerning, boolean useAdvanced) throws IOException { + TTFFile ttfFile = new TTFFile(useKerning, useAdvanced); log.info("Reading " + fileName + "..."); FontFileReader reader = new FontFileReader(fileName); @@ -463,7 +478,6 @@ public class TTFReader extends AbstractFontReader { } } - /** * Bugzilla 40739, check that attr has a metrics-version attribute * compatible with ours. diff --git a/src/java/org/apache/fop/fonts/autodetect/FontInfoFinder.java b/src/java/org/apache/fop/fonts/autodetect/FontInfoFinder.java index 714af0e57..bf6f493cf 100644 --- a/src/java/org/apache/fop/fonts/autodetect/FontInfoFinder.java +++ b/src/java/org/apache/fop/fonts/autodetect/FontInfoFinder.java @@ -148,7 +148,7 @@ public class FontInfoFinder { subFontName = ((MultiByteFont)customFont).getTTCName(); } EmbedFontInfo fontInfo = new EmbedFontInfo(null, customFont.isKerningEnabled(), - fontTripletList, embedUrl, subFontName); + customFont.isAdvancedEnabled(), fontTripletList, embedUrl, subFontName); fontInfo.setPostScriptName(customFont.getFontName()); if (fontCache != null) { fontCache.addFont(fontInfo); @@ -168,6 +168,9 @@ public class FontInfoFinder { public EmbedFontInfo[] find(URL fontURL, FontResolver resolver, FontCache fontCache) { String embedURL = null; embedURL = fontURL.toExternalForm(); + boolean useKerning = true; + boolean useAdvanced = ( resolver != null ) + ? resolver.isComplexScriptFeaturesEnabled() : true; long fileLastModified = -1; if (fontCache != null) { @@ -190,14 +193,14 @@ public class FontInfoFinder { // try to determine triplet information from font file CustomFont customFont = null; - if (fontURL.toExternalForm().endsWith(".ttc")) { + if (fontURL.toExternalForm().toLowerCase().endsWith(".ttc")) { // Get a list of the TTC Font names List<String> ttcNames = null; String fontFileURL = fontURL.toExternalForm().trim(); InputStream in = null; try { in = FontLoader.openFontUri(resolver, fontFileURL); - TTFFile ttf = new TTFFile(); + TTFFile ttf = new TTFFile(false, false); FontFileReader reader = new FontFileReader(in); ttcNames = ttf.getTTCnames(reader); } catch (Exception e) { @@ -218,7 +221,8 @@ public class FontInfoFinder { } try { TTFFontLoader ttfLoader = new TTFFontLoader( - fontFileURL, fontName, true, EncodingMode.AUTO, true, resolver); + fontFileURL, fontName, true, EncodingMode.AUTO, + useKerning, useAdvanced, resolver); customFont = ttfLoader.getFont(); if (this.eventListener != null) { customFont.setEventListener(this.eventListener); diff --git a/src/java/org/apache/fop/fonts/truetype/TTFDirTabEntry.java b/src/java/org/apache/fop/fonts/truetype/TTFDirTabEntry.java index 405a71395..1f05ebfa1 100644 --- a/src/java/org/apache/fop/fonts/truetype/TTFDirTabEntry.java +++ b/src/java/org/apache/fop/fonts/truetype/TTFDirTabEntry.java @@ -26,7 +26,7 @@ import java.io.UnsupportedEncodingException; /** * This class represents an entry to a TrueType font's Dir Tab. */ -class TTFDirTabEntry { +public class TTFDirTabEntry { private byte[] tag = new byte[4]; private int checksum; @@ -34,7 +34,10 @@ class TTFDirTabEntry { private long length; /** - * Read Dir Tab, return tag name + * Read Dir Tab. + * @param in font file reader + * @return tag name + * @throws IOException upon I/O exception */ public String read(FontFileReader in) throws IOException { tag[0] = in.readTTFByte(); @@ -52,6 +55,7 @@ class TTFDirTabEntry { } + @Override public String toString() { return "Read dir tab [" + tag[0] + " " + tag[1] + " " + tag[2] + " " + tag[3] + "]" diff --git a/src/java/org/apache/fop/fonts/truetype/TTFFile.java b/src/java/org/apache/fop/fonts/truetype/TTFFile.java index f4806d95e..33d608540 100644 --- a/src/java/org/apache/fop/fonts/truetype/TTFFile.java +++ b/src/java/org/apache/fop/fonts/truetype/TTFFile.java @@ -28,9 +28,15 @@ import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.fop.fonts.FontUtil; import org.apache.xmlgraphics.fonts.Glyphs; +import org.apache.fop.complexscripts.fonts.AdvancedTypographicTableFormatException; +import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; +import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; +import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable; +import org.apache.fop.complexscripts.fonts.OTFAdvancedTypographicTableReader; +import org.apache.fop.fonts.FontUtil; + /** * Reads a TrueType file or a TrueType Collection. * The TrueType spec can be found at the Microsoft. @@ -43,12 +49,12 @@ public class TTFFile { static final int MAX_CHAR_CODE = 255; static final int ENC_BUF_SIZE = 1024; - /** Set to true to get even more debug output than with level DEBUG */ - public static final boolean TRACE_ENABLED = false; - private final String encoding = "WinAnsiEncoding"; // Default encoding private final short firstChar = 0; + + private boolean useKerning = false; + private boolean isEmbeddable = true; private boolean hasSerifs = true; /** @@ -58,7 +64,7 @@ public class TTFFile { private Map<Integer, Map<Integer, Integer>> kerningTab; // for CIDs private Map<Integer, Map<Integer, Integer>> ansiKerningTab; // For winAnsiEncoding private List cmaps; - private List unicodeMapping; + private Set unicodeMappings; private int upem; // unitsPerEm from "head" table private int nhmtx; // Number of horizontal metrics @@ -120,15 +126,29 @@ public class TTFFile { private boolean isCFF; + // advanced typographic table support + private boolean useAdvanced = false; + private OTFAdvancedTypographicTableReader advancedTableReader; + /** * logging instance */ protected Log log = LogFactory.getLog(TTFFile.class); /** + * Constructor + * @param useKerning true if kerning data should be loaded + * @param useAdvanced true if advanced typographic tables should be loaded + */ + public TTFFile ( boolean useKerning, boolean useAdvanced ) { + this.useKerning = useKerning; + this.useAdvanced = useAdvanced; + } + + /** * Key-value helper class */ - class UnicodeMapping { + class UnicodeMapping implements Comparable { private final int unicodeIndex; private final int glyphIndex; @@ -155,15 +175,67 @@ public class TTFFile { public int getUnicodeIndex() { return unicodeIndex; } + + + /** {@inheritDoc} */ + public int hashCode() { + int hc = unicodeIndex; + hc = 19 * hc + ( hc ^ glyphIndex ); + return hc; + } + + /** {@inheritDoc} */ + public boolean equals ( Object o ) { + if ( o instanceof UnicodeMapping ) { + UnicodeMapping m = (UnicodeMapping) o; + if ( unicodeIndex != m.unicodeIndex ) { + return false; + } else { + return ( glyphIndex == m.glyphIndex ); + } + } else { + return false; + } + } + + /** {@inheritDoc} */ + public int compareTo ( Object o ) { + if ( o instanceof UnicodeMapping ) { + UnicodeMapping m = (UnicodeMapping) o; + if ( unicodeIndex > m.unicodeIndex ) { + return 1; + } else if ( unicodeIndex < m.unicodeIndex ) { + return -1; + } else { + return 0; + } + } else { + return -1; + } + } + } + + /** + * Obtain directory table entry. + * @param name (tag) of entry + * @return a directory table entry or null if none found + */ + public TTFDirTabEntry getDirectoryEntry ( String name ) { + return (TTFDirTabEntry) dirTabs.get ( name ); } /** * Position inputstream to position indicated * in the dirtab offset + offset + * @param in font file reader + * @param name (tag) of table + * @param offset from start of table + * @return true if seek succeeded + * @throws IOException if I/O exception occurs during seek */ - boolean seekTab(FontFileReader in, String name, + public boolean seekTab(FontFileReader in, String name, long offset) throws IOException { - TTFDirTabEntry dt = (TTFDirTabEntry)dirTabs.get(name); + TTFDirTabEntry dt = getDirectoryEntry ( name ); if (dt == null) { log.error("Dirtab " + name + " not found."); return false; @@ -203,7 +275,7 @@ public class TTFFile { */ private boolean readCMAP(FontFileReader in) throws IOException { - unicodeMapping = new java.util.ArrayList(); + unicodeMappings = new java.util.TreeSet(); seekTab(in, "cmap", 2); int numCMap = in.readTTFUShort(); // Number of cmap subtables @@ -344,7 +416,7 @@ public class TTFFile { glyphIdx = (in.readTTFUShort() + cmapDeltas[i]) & 0xffff; - unicodeMapping.add(new UnicodeMapping(glyphIdx, j)); + unicodeMappings.add(new UnicodeMapping(glyphIdx, j)); mtxTab[glyphIdx].getUnicodeIndex().add(new Integer(j)); if (encodingID == 0 && j >= 0xF020 && j <= 0xF0FF) { @@ -354,7 +426,7 @@ public class TTFFile { int mapped = j - 0xF000; if (!eightBitGlyphs.get(mapped)) { //Only map if Unicode code point hasn't been mapped before - unicodeMapping.add(new UnicodeMapping(glyphIdx, mapped)); + unicodeMappings.add(new UnicodeMapping(glyphIdx, mapped)); mtxTab[glyphIdx].getUnicodeIndex().add(new Integer(mapped)); } } @@ -395,7 +467,7 @@ public class TTFFile { + mtxTab.length); } - unicodeMapping.add(new UnicodeMapping(glyphIdx, j)); + unicodeMappings.add(new UnicodeMapping(glyphIdx, j)); if (glyphIdx < mtxTab.length) { mtxTab[glyphIdx].getUnicodeIndex().add(new Integer(j)); } else { @@ -554,9 +626,27 @@ public class TTFFile { } // Create cmaps for bfentries createCMaps(); - // print_max_min(); - readKerning(in); + if ( useKerning ) { + readKerning(in); + } + + // Read advanced typographic tables. + if ( useAdvanced ) { + try { + OTFAdvancedTypographicTableReader atr + = new OTFAdvancedTypographicTableReader ( this, in ); + atr.readAll(); + this.advancedTableReader = atr; + } catch ( AdvancedTypographicTableFormatException e ) { + log.warn ( + "Encountered format constraint violation in advanced (typographic) table (AT) " + + "in font '" + getFullName() + "', ignoring AT data: " + + e.getMessage() + ); + } + } + guessVerticalMetricsFromGlyphBBox(); return true; } @@ -565,7 +655,7 @@ public class TTFFile { cmaps = new java.util.ArrayList(); TTFCmapEntry tce = new TTFCmapEntry(); - Iterator e = unicodeMapping.listIterator(); + Iterator e = unicodeMappings.iterator(); UnicodeMapping um = (UnicodeMapping)e.next(); UnicodeMapping lastMapping = um; @@ -927,8 +1017,8 @@ public class TTFFile { int mtxSize = Math.max(numberOfGlyphs, nhmtx); mtxTab = new TTFMtxEntry[mtxSize]; - if (TRACE_ENABLED) { - log.debug("*** Widths array: \n"); + if (log.isTraceEnabled()) { + log.trace("*** Widths array: \n"); } for (int i = 0; i < mtxSize; i++) { mtxTab[i] = new TTFMtxEntry(); @@ -937,11 +1027,9 @@ public class TTFFile { mtxTab[i].setWx(in.readTTFUShort()); mtxTab[i].setLsb(in.readTTFUShort()); - if (TRACE_ENABLED) { - if (log.isDebugEnabled()) { - log.debug(" width[" + i + "] = " - + convertTTFUnit2PDFUnit(mtxTab[i].getWx()) + ";"); - } + if (log.isTraceEnabled()) { + log.trace(" width[" + i + "] = " + + convertTTFUnit2PDFUnit(mtxTab[i].getWx()) + ";"); } } @@ -1045,7 +1133,7 @@ public class TTFFile { */ private void readOS2(FontFileReader in) throws IOException { // Check if font is embeddable - TTFDirTabEntry os2Entry = (TTFDirTabEntry)dirTabs.get("OS/2"); + TTFDirTabEntry os2Entry = getDirectoryEntry ( "OS/2" ); if (os2Entry != null) { seekTab(in, "OS/2", 0); int version = in.readTTFUShort(); @@ -1133,7 +1221,7 @@ public class TTFFile { * @throws IOException In case of a I/O problem */ private void readGlyf(FontFileReader in) throws IOException { - TTFDirTabEntry dirTab = (TTFDirTabEntry)dirTabs.get("glyf"); + TTFDirTabEntry dirTab = getDirectoryEntry ( "glyf" ); if (dirTab == null) { throw new IOException("glyf table not found, cannot continue"); } @@ -1153,7 +1241,7 @@ public class TTFFile { } - long n = ((TTFDirTabEntry)dirTabs.get("glyf")).getOffset(); + long n = dirTab.getOffset(); for (int i = 0; i < numberOfGlyphs; i++) { if ((i + 1) >= mtxTab.length || mtxTab[i].getOffset() != mtxTab[i + 1].getOffset()) { @@ -1259,7 +1347,7 @@ public class TTFFile { * @throws IOException In case of a I/O problem */ private boolean readPCLT(FontFileReader in) throws IOException { - TTFDirTabEntry dirTab = (TTFDirTabEntry)dirTabs.get("PCLT"); + TTFDirTabEntry dirTab = getDirectoryEntry ( "PCLT" ); if (dirTab != null) { in.seekSet(dirTab.getOffset() + 4 + 4 + 2); xHeight = in.readTTFUShort(); @@ -1380,7 +1468,7 @@ public class TTFFile { capHeight = os2CapHeight; } if (capHeight == 0) { - log.warn("capHeight value could not be determined." + log.debug("capHeight value could not be determined." + " The font may not work as expected."); } } @@ -1390,7 +1478,7 @@ public class TTFFile { xHeight = os2xHeight; } if (xHeight == 0) { - log.warn("xHeight value could not be determined." + log.debug("xHeight value could not be determined." + " The font may not work as expected."); } } @@ -1406,7 +1494,7 @@ public class TTFFile { // Read kerning kerningTab = new java.util.HashMap(); ansiKerningTab = new java.util.HashMap(); - TTFDirTabEntry dirTab = (TTFDirTabEntry)dirTabs.get("kern"); + TTFDirTabEntry dirTab = getDirectoryEntry ( "kern" ); if (dirTab != null) { seekTab(in, "kern", 2); for (int n = in.readTTFUShort(); n > 0; n--) { @@ -1584,11 +1672,9 @@ public class TTFFile { dirOffsets[i] = in.readTTFULong(); } - if (log.isDebugEnabled()) { - log.debug("This is a TrueType collection file with " - + numDirectories + " fonts"); - log.debug("Containing the following fonts: "); - } + log.info("This is a TrueType collection file with " + + numDirectories + " fonts"); + log.info("Containing the following fonts: "); for (int i = 0; (i < numDirectories); i++) { in.seekSet(dirOffsets[i]); @@ -1596,7 +1682,7 @@ public class TTFFile { readName(in); - log.debug(fullName); + log.info(fullName); fontNames.add(fullName); // Reset names @@ -1667,9 +1753,8 @@ public class TTFFile { * * @param glyphIndex * @return unicode code point - * @throws IOException if glyphIndex not found */ - private Integer glyphToUnicode(int glyphIndex) throws IOException { + private Integer glyphToUnicode(int glyphIndex) { return (Integer) glyphToUnicodeMap.get(new Integer(glyphIndex)); } @@ -1678,7 +1763,6 @@ public class TTFFile { * * @param unicodeIndex unicode code point * @return glyph index - * @throws IOException if unicodeIndex not found */ private Integer unicodeToGlyph(int unicodeIndex) throws IOException { final Integer result @@ -1691,12 +1775,62 @@ public class TTFFile { } /** + * Determine if advanced (typographic) table is present. + * @return true if advanced (typographic) table is present + */ + public boolean hasAdvancedTable() { + if ( advancedTableReader != null ) { + return advancedTableReader.hasAdvancedTable(); + } else { + return false; + } + } + + /** + * Returns the GDEF table or null if none present. + * @return the GDEF table + */ + public GlyphDefinitionTable getGDEF() { + if ( advancedTableReader != null ) { + return advancedTableReader.getGDEF(); + } else { + return null; + } + } + + /** + * Returns the GSUB table or null if none present. + * @return the GSUB table + */ + public GlyphSubstitutionTable getGSUB() { + if ( advancedTableReader != null ) { + return advancedTableReader.getGSUB(); + } else { + return null; + } + } + + /** + * Returns the GPOS table or null if none present. + * @return the GPOS table + */ + public GlyphPositioningTable getGPOS() { + if ( advancedTableReader != null ) { + return advancedTableReader.getGPOS(); + } else { + return null; + } + } + + /** * Static main method to get info about a TrueType font. * @param args The command line arguments */ public static void main(String[] args) { try { - TTFFile ttfFile = new TTFFile(); + boolean useKerning = true; + boolean useAdvanced = true; + TTFFile ttfFile = new TTFFile(useKerning, useAdvanced); FontFileReader reader = new FontFileReader(args[0]); @@ -1713,4 +1847,4 @@ public class TTFFile { ioe.printStackTrace(System.err); } } -}
\ No newline at end of file +} diff --git a/src/java/org/apache/fop/fonts/truetype/TTFFontLoader.java b/src/java/org/apache/fop/fonts/truetype/TTFFontLoader.java index f9be8e010..54324be52 100644 --- a/src/java/org/apache/fop/fonts/truetype/TTFFontLoader.java +++ b/src/java/org/apache/fop/fonts/truetype/TTFFontLoader.java @@ -54,7 +54,7 @@ public class TTFFontLoader extends FontLoader { * @param resolver the FontResolver for font URI resolution */ public TTFFontLoader(String fontFileURI, FontResolver resolver) { - this(fontFileURI, null, true, EncodingMode.AUTO, true, resolver); + this(fontFileURI, null, true, EncodingMode.AUTO, true, true, resolver); } /** @@ -65,12 +65,13 @@ public class TTFFontLoader extends FontLoader { * @param embedded indicates whether the font is embedded or referenced * @param encodingMode the requested encoding mode * @param useKerning true to enable loading kerning info if available, false to disable + * @param useAdvanced true to enable loading advanced info if available, false to disable * @param resolver the FontResolver for font URI resolution */ public TTFFontLoader(String fontFileURI, String subFontName, boolean embedded, EncodingMode encodingMode, boolean useKerning, - FontResolver resolver) { - super(fontFileURI, embedded, useKerning, resolver); + boolean useAdvanced, FontResolver resolver) { + super(fontFileURI, embedded, useKerning, useAdvanced, resolver); this.subFontName = subFontName; this.encodingMode = encodingMode; if (this.encodingMode == EncodingMode.AUTO) { @@ -93,7 +94,7 @@ public class TTFFontLoader extends FontLoader { private void read(String ttcFontName) throws IOException { InputStream in = openFontUri(resolver, this.fontFileURI); try { - TTFFile ttf = new TTFFile(); + TTFFile ttf = new TTFFile(useKerning, useAdvanced); FontFileReader reader = new FontFileReader(in); boolean supported = ttf.readFont(reader, ttcFontName); if (!supported) { @@ -169,6 +170,9 @@ public class TTFFontLoader extends FontLoader { if (useKerning) { copyKerning(ttf, isCid); } + if (useAdvanced) { + copyAdvanced(ttf); + } if (this.embedded) { if (ttf.isEmbeddable()) { returnFont.setEmbedFileName(this.fontFileURI); @@ -229,4 +233,17 @@ public class TTFFontLoader extends FontLoader { returnFont.putKerningEntry(kpx1, h2); } } + + /** + * Copy advanced typographic information. + */ + private void copyAdvanced ( TTFFile ttf ) { + if ( returnFont instanceof MultiByteFont ) { + MultiByteFont mbf = (MultiByteFont) returnFont; + mbf.setGDEF ( ttf.getGDEF() ); + mbf.setGSUB ( ttf.getGSUB() ); + mbf.setGPOS ( ttf.getGPOS() ); + } + } + } diff --git a/src/java/org/apache/fop/fonts/truetype/TTFSubSetFile.java b/src/java/org/apache/fop/fonts/truetype/TTFSubSetFile.java index 665cf289d..e081734be 100644 --- a/src/java/org/apache/fop/fonts/truetype/TTFSubSetFile.java +++ b/src/java/org/apache/fop/fonts/truetype/TTFSubSetFile.java @@ -57,6 +57,22 @@ public class TTFSubSetFile extends TTFFile { private int locaOffset = 0; /** + * Default Constructor + */ + public TTFSubSetFile() { + this(false, false); + } + + /** + * Constructor + * @param useKerning true if kerning data should be loaded + * @param useAdvanced true if advanced typographic tables should be loaded + */ + public TTFSubSetFile ( boolean useKerning, boolean useAdvanced ) { + super(useKerning, useAdvanced); + } + + /** * Initalize the output array */ private void init(int size) { diff --git a/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java b/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java index 1e93763e8..924f7b0b8 100644 --- a/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java +++ b/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java @@ -52,7 +52,7 @@ public class Type1FontLoader extends FontLoader { */ public Type1FontLoader(String fontFileURI, boolean embedded, boolean useKerning, FontResolver resolver) throws IOException { - super(fontFileURI, embedded, useKerning, resolver); + super(fontFileURI, embedded, useKerning, true, resolver); } private String getPFMURI(String pfbURI) { diff --git a/src/java/org/apache/fop/layoutmgr/AbstractLayoutManager.java b/src/java/org/apache/fop/layoutmgr/AbstractLayoutManager.java index 8e96f3eb1..dd9c34149 100644 --- a/src/java/org/apache/fop/layoutmgr/AbstractLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/AbstractLayoutManager.java @@ -448,7 +448,7 @@ public abstract class AbstractLayoutManager extends AbstractBaseLayoutManager /** {@inheritDoc} */ @Override public String toString() { - return (super.toString() + (fobj != null ? "[fobj=" + fobj.toString() + "]" : "")); + return (super.toString() + (fobj != null ? "{fobj = " + fobj.toString() + "}" : "")); } /** {@inheritDoc} */ diff --git a/src/java/org/apache/fop/layoutmgr/BlockContainerLayoutManager.java b/src/java/org/apache/fop/layoutmgr/BlockContainerLayoutManager.java index 95b7f8a39..205fac0f2 100644 --- a/src/java/org/apache/fop/layoutmgr/BlockContainerLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/BlockContainerLayoutManager.java @@ -349,7 +349,12 @@ public class BlockContainerLayoutManager extends BlockStackingLayoutManager contentRectOffsetX = 0; contentRectOffsetY = 0; - contentRectOffsetX += fo.getCommonMarginBlock().startIndent.getValue(this); + int level = fo.getBidiLevel(); + if ( ( level < 0 ) || ( ( level & 1 ) == 0 ) ) { + contentRectOffsetX += fo.getCommonMarginBlock().startIndent.getValue(this); + } else { + contentRectOffsetX += fo.getCommonMarginBlock().endIndent.getValue(this); + } contentRectOffsetY += fo.getCommonBorderPaddingBackground().getBorderBeforeWidth(false); contentRectOffsetY += fo.getCommonBorderPaddingBackground().getPaddingBefore(false, this); @@ -834,10 +839,13 @@ public class BlockContainerLayoutManager extends BlockStackingLayoutManager if (referenceArea == null) { boolean switchedProgressionDirection = blockProgressionDirectionChanges(); boolean allowBPDUpdate = autoHeight && !switchedProgressionDirection; + int level = getBlockContainerFO().getBidiLevel(); viewportBlockArea = new BlockViewport(allowBPDUpdate); viewportBlockArea.addTrait(Trait.IS_VIEWPORT_AREA, Boolean.TRUE); - + if ( level >= 0 ) { + viewportBlockArea.setBidiLevel ( level ); + } viewportBlockArea.setIPD(getContentAreaIPD()); if (allowBPDUpdate) { viewportBlockArea.setBPD(0); @@ -872,6 +880,9 @@ public class BlockContainerLayoutManager extends BlockStackingLayoutManager referenceArea = new Block(); referenceArea.addTrait(Trait.IS_REFERENCE_AREA, Boolean.TRUE); + if ( level >= 0 ) { + referenceArea.setBidiLevel ( level ); + } TraitSetter.setProducerID(referenceArea, getBlockContainerFO().getId()); if (abProps.absolutePosition == EN_ABSOLUTE) { diff --git a/src/java/org/apache/fop/layoutmgr/BlockLayoutManager.java b/src/java/org/apache/fop/layoutmgr/BlockLayoutManager.java index ed2ea4ee9..9c3639f93 100644 --- a/src/java/org/apache/fop/layoutmgr/BlockLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/BlockLayoutManager.java @@ -359,6 +359,8 @@ public class BlockLayoutManager extends BlockStackingLayoutManager curBlockArea.setIPD(super.getContentAreaIPD()); + curBlockArea.setBidiLevel ( getBlockFO().getBidiLevel() ); + TraitSetter.addBreaks(curBlockArea, getBlockFO().getBreakBefore(), getBlockFO().getBreakAfter()); @@ -503,4 +505,3 @@ public class BlockLayoutManager extends BlockStackingLayoutManager } } - diff --git a/src/java/org/apache/fop/layoutmgr/ExternalDocumentLayoutManager.java b/src/java/org/apache/fop/layoutmgr/ExternalDocumentLayoutManager.java index c4aa1a39b..a46b74e1a 100644 --- a/src/java/org/apache/fop/layoutmgr/ExternalDocumentLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/ExternalDocumentLayoutManager.java @@ -51,6 +51,7 @@ import org.apache.fop.datatypes.URISpecification; import org.apache.fop.fo.Constants; import org.apache.fop.fo.extensions.ExternalDocument; import org.apache.fop.layoutmgr.inline.ImageLayout; +import org.apache.fop.traits.WritingMode; /** * LayoutManager for an external-document extension element. This class is instantiated by @@ -184,12 +185,12 @@ public class ExternalDocumentLayoutManager extends AbstractPageSequenceLayoutMan TraitSetter.setProducerID(imageArea, fobj.getId()); transferForeignAttributes(imageArea); - InlineViewport vp = new InlineViewport(imageArea); + InlineViewport vp = new InlineViewport(imageArea, fobj.getBidiLevel()); TraitSetter.setProducerID(vp, fobj.getId()); vp.setIPD(imageSize.width); vp.setBPD(imageSize.height); vp.setContentPosition(imageLayout.getPlacement()); - vp.setOffset(0); + vp.setBlockProgressionOffset(0); //Link them all together... lineArea.addInlineArea(vp); @@ -230,8 +231,9 @@ public class ExternalDocumentLayoutManager extends AbstractPageSequenceLayoutMan referenceRect = new Rectangle(0, 0, imageSize.height, imageSize.width); } FODimension reldims = new FODimension(0, 0); + // [TBD] BIDI ALERT CTM pageCTM = CTM.getCTMandRelDims(pageSeq.getReferenceOrientation(), - Constants.EN_LR_TB, referenceRect, reldims); + WritingMode.LR_TB, referenceRect, reldims); Page page = new Page(referenceRect, pageNumber, pageNumberString, isBlank); diff --git a/src/java/org/apache/fop/layoutmgr/LayoutContext.java b/src/java/org/apache/fop/layoutmgr/LayoutContext.java index 18059c5ca..a2512c50b 100644 --- a/src/java/org/apache/fop/layoutmgr/LayoutContext.java +++ b/src/java/org/apache/fop/layoutmgr/LayoutContext.java @@ -26,6 +26,7 @@ import org.apache.fop.fo.Constants; import org.apache.fop.layoutmgr.inline.AlignmentContext; import org.apache.fop.layoutmgr.inline.HyphContext; import org.apache.fop.traits.MinOptMax; +import org.apache.fop.traits.WritingMode; /** @@ -92,7 +93,7 @@ public class LayoutContext { //overlap with refIPD. Need to investigate how best to refactor that. /** the writing mode established by the nearest ancestor reference area */ - private int writingMode = Constants.EN_LR_TB; + private WritingMode writingMode = WritingMode.LR_TB; /** Current pending space-after or space-end from preceding area */ private SpaceSpecifier trailingSpace; @@ -564,7 +565,7 @@ public class LayoutContext { * Get the writing mode of the relevant reference area. * @return the applicable writing mode */ - public int getWritingMode() { + public WritingMode getWritingMode() { return writingMode; } @@ -572,7 +573,7 @@ public class LayoutContext { * Set the writing mode. * @param writingMode the writing mode */ - public void setWritingMode(int writingMode) { + public void setWritingMode(WritingMode writingMode) { this.writingMode = writingMode; } diff --git a/src/java/org/apache/fop/layoutmgr/LayoutManagerMapping.java b/src/java/org/apache/fop/layoutmgr/LayoutManagerMapping.java index 2acf67c1f..76a1cb9e4 100644 --- a/src/java/org/apache/fop/layoutmgr/LayoutManagerMapping.java +++ b/src/java/org/apache/fop/layoutmgr/LayoutManagerMapping.java @@ -67,6 +67,7 @@ import org.apache.fop.fo.pagination.SideRegion; import org.apache.fop.fo.pagination.StaticContent; import org.apache.fop.fo.pagination.Title; import org.apache.fop.layoutmgr.inline.BasicLinkLayoutManager; +import org.apache.fop.layoutmgr.inline.BidiLayoutManager; import org.apache.fop.layoutmgr.inline.CharacterLayoutManager; import org.apache.fop.layoutmgr.inline.ContentLayoutManager; import org.apache.fop.layoutmgr.inline.ExternalGraphicLayoutManager; @@ -246,28 +247,9 @@ public class LayoutManagerMapping implements LayoutManagerMaker { public static class BidiOverrideLayoutManagerMaker extends Maker { /** {@inheritDoc} */ public void make(FONode node, List lms) { - /* [GA] remove broken code - if (false) { - // this is broken; it does nothing - // it should make something like an InlineStackingLM - super.make(node, lms); - } else { - ArrayList childList = new ArrayList(); - // this is broken; it does nothing - // it should make something like an InlineStackingLM - super.make(node, childList); - for (int count = childList.size() - 1; count >= 0; count--) { - LayoutManager lm = (LayoutManager) childList.get(count); - if (lm instanceof InlineLevelLayoutManager) { - LayoutManager blm = new BidiLayoutManager - ((BidiOverride) node, (InlineLayoutManager) lm); - lms.add(blm); - } else { - lms.add(lm); - } - } + if ( node instanceof BidiOverride ) { + lms.add(new BidiLayoutManager((BidiOverride) node)); } - */ } } diff --git a/src/java/org/apache/fop/layoutmgr/PageProvider.java b/src/java/org/apache/fop/layoutmgr/PageProvider.java index 364839f8a..a668b044b 100644 --- a/src/java/org/apache/fop/layoutmgr/PageProvider.java +++ b/src/java/org/apache/fop/layoutmgr/PageProvider.java @@ -335,6 +335,7 @@ public class PageProvider implements Constants { //Set unique key obtained from the AreaTreeHandler page.getPageViewport().setKey(areaTreeHandler.generatePageViewportKey()); page.getPageViewport().setForeignAttributes(spm.getForeignAttributes()); + page.getPageViewport().setWritingModeTraits(pageSeq); cachedPages.add(page); return page; } diff --git a/src/java/org/apache/fop/layoutmgr/PageSequenceLayoutManager.java b/src/java/org/apache/fop/layoutmgr/PageSequenceLayoutManager.java index 1b2b5d7c5..8ad124c27 100644 --- a/src/java/org/apache/fop/layoutmgr/PageSequenceLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/PageSequenceLayoutManager.java @@ -25,6 +25,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.fop.area.AreaTreeHandler; import org.apache.fop.area.AreaTreeModel; import org.apache.fop.area.LineArea; +import org.apache.fop.complexscripts.bidi.BidiResolver; import org.apache.fop.fo.Constants; import org.apache.fop.fo.pagination.PageSequence; import org.apache.fop.fo.pagination.PageSequenceMaster; @@ -78,8 +79,12 @@ public class PageSequenceLayoutManager extends AbstractPageSequenceLayoutManager public void activateLayout() { initialize(); - LineArea title = null; + // perform step 5.8 of refinement process (Unicode BIDI Processing) + if ( areaTreeHandler.isComplexScriptFeaturesEnabled() ) { + BidiResolver.resolveInlineDirectionality(getPageSequence()); + } + LineArea title = null; if (getPageSequence().getTitleFO() != null) { try { ContentLayoutManager clm = getLayoutManagerMaker(). diff --git a/src/java/org/apache/fop/layoutmgr/inline/AbstractGraphicsLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/AbstractGraphicsLayoutManager.java index 8c797a48c..a7bc78be8 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/AbstractGraphicsLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/AbstractGraphicsLayoutManager.java @@ -59,6 +59,7 @@ public abstract class AbstractGraphicsLayoutManager extends LeafNodeLayoutManage Dimension intrinsicSize = new Dimension( fobj.getIntrinsicWidth(), fobj.getIntrinsicHeight()); + int bidiLevel = fobj.getBidiLevel(); //TODO Investigate if the line-height property has to be taken into the calculation //somehow. There was some code here that hints in this direction but it was disabled. @@ -67,6 +68,7 @@ public abstract class AbstractGraphicsLayoutManager extends LeafNodeLayoutManage Rectangle placement = imageLayout.getPlacement(); CommonBorderPaddingBackground borderProps = fobj.getCommonBorderPaddingBackground(); + setCommonBorderPaddingBackground(borderProps); //Determine extra BPD from borders and padding int beforeBPD = borderProps.getPadding(CommonBorderPaddingBackground.BEFORE, false, this); @@ -75,30 +77,35 @@ public abstract class AbstractGraphicsLayoutManager extends LeafNodeLayoutManage placement.y += beforeBPD; //Determine extra IPD from borders and padding - int startIPD = borderProps.getPadding(CommonBorderPaddingBackground.START, false, this); - startIPD += borderProps.getBorderWidth(CommonBorderPaddingBackground.START, false); - - placement.x += startIPD; + if ( ( bidiLevel == -1 ) || ( ( bidiLevel & 1 ) == 0 ) ) { + int startIPD = borderProps.getPadding(CommonBorderPaddingBackground.START, false, this); + startIPD += borderProps.getBorderWidth(CommonBorderPaddingBackground.START, false); + placement.x += startIPD; + } else { + int endIPD = borderProps.getPadding(CommonBorderPaddingBackground.END, false, this); + endIPD += borderProps.getBorderWidth(CommonBorderPaddingBackground.END, false); + placement.x += endIPD; + } Area viewportArea = getChildArea(); TraitSetter.setProducerID(viewportArea, fobj.getId()); transferForeignAttributes(viewportArea); - InlineViewport vp = new InlineViewport(viewportArea); + InlineViewport vp = new InlineViewport(viewportArea, bidiLevel); TraitSetter.addStructureTreeElement(vp, fobj.getStructureTreeElement()); TraitSetter.setProducerID(vp, fobj.getId()); vp.setIPD(imageLayout.getViewportSize().width); vp.setBPD(imageLayout.getViewportSize().height); vp.setContentPosition(placement); vp.setClip(imageLayout.isClipped()); - vp.setOffset(0); + vp.setBlockProgressionOffset(0); // Common Border, Padding, and Background Properties - TraitSetter.addBorders(vp, fobj.getCommonBorderPaddingBackground() + TraitSetter.addBorders(vp, borderProps , false, false, false, false, this); - TraitSetter.addPadding(vp, fobj.getCommonBorderPaddingBackground() + TraitSetter.addPadding(vp, borderProps , false, false, false, false, this); - TraitSetter.addBackground(vp, fobj.getCommonBorderPaddingBackground(), this); + TraitSetter.addBackground(vp, borderProps, this); return vp; } diff --git a/src/java/org/apache/fop/layoutmgr/inline/AbstractPageNumberCitationLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/AbstractPageNumberCitationLayoutManager.java index 8c769924a..aba137115 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/AbstractPageNumberCitationLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/AbstractPageNumberCitationLayoutManager.java @@ -81,10 +81,7 @@ public abstract class AbstractPageNumberCitationLayoutManager extends LeafNodeLa } /** {@inheritDoc} */ - public InlineArea get(LayoutContext context) { - curArea = getPageNumberCitationInlineArea(); - return curArea; - } + public abstract InlineArea get(LayoutContext context); /** * {@inheritDoc} @@ -98,35 +95,6 @@ public abstract class AbstractPageNumberCitationLayoutManager extends LeafNodeLa } /** - * If id can be resolved then simply return a text area, otherwise - * return a resolvable area - * - * @return a corresponding InlineArea - */ - private InlineArea getPageNumberCitationInlineArea() { - PageViewport page = getPSLM().getFirstPVWithID(fobj.getRefId()); - TextArea text; - if (page != null) { - String str = page.getPageNumberString(); - // get page string from parent, build area - text = new TextArea(); - int width = getStringWidth(str); - text.addWord(str, 0); - text.setIPD(width); - resolved = true; - } else { - resolved = false; - text = new UnresolvedPageNumber(fobj.getRefId(), font); - String str = "MMM"; // reserve three spaces for page number - int width = getStringWidth(str); - text.setIPD(width); - } - updateTextAreaTraits(text); - - return text; - } - - /** * Updates the traits for the generated text area. * @param text the text area */ @@ -152,5 +120,12 @@ public abstract class AbstractPageNumberCitationLayoutManager extends LeafNodeLa return width; } + /** + * @return bidi level governing abstract page number citation + */ + protected int getBidiLevel() { + return fobj.getBidiLevel(); + } + } diff --git a/src/java/org/apache/fop/layoutmgr/inline/AlignmentContext.java b/src/java/org/apache/fop/layoutmgr/inline/AlignmentContext.java index daf7ce01d..eb199da9d 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/AlignmentContext.java +++ b/src/java/org/apache/fop/layoutmgr/inline/AlignmentContext.java @@ -24,6 +24,7 @@ import org.apache.fop.datatypes.LengthBase; import org.apache.fop.datatypes.SimplePercentBaseContext; import org.apache.fop.fo.Constants; import org.apache.fop.fonts.Font; +import org.apache.fop.traits.WritingMode; /** * The alignment context is carried within a LayoutContext and as @@ -173,7 +174,7 @@ public class AlignmentContext implements Constants { * @param lineHeight the computed value of the lineHeight property * @param writingMode the current writing mode */ - AlignmentContext(Font font, int lineHeight, int writingMode) { + AlignmentContext(Font font, int lineHeight, WritingMode writingMode) { this.areaHeight = font.getAscender() - font.getDescender(); this.lineHeight = lineHeight; this.xHeight = font.getXHeight(); @@ -299,6 +300,14 @@ public class AlignmentContext implements Constants { } /** + * Return the writing mode. + * @return the writing mode + */ +/* public WritingMode getWritingMode() { + return scaledBaselineTable.getWritingMode(); + }*/ + + /** * Calculates the baseline shift value based on the baseline-shift * property value. * @param baselineShift the baseline shift property value @@ -504,6 +513,10 @@ public class AlignmentContext implements Constants { && parentAlignmentContext.usesInitialBaselineTable()); } + /* private boolean isHorizontalWritingMode() { + return (getWritingMode() == WritingMode.LR_TB || getWritingMode() == WritingMode.RL_TB); + }*/ + /** {@inheritDoc} */ public String toString() { StringBuffer sb = new StringBuffer(64); diff --git a/src/java/org/apache/fop/layoutmgr/inline/BasicLinkLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/BasicLinkLayoutManager.java index 1390c04d8..593003d55 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/BasicLinkLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/BasicLinkLayoutManager.java @@ -69,6 +69,12 @@ public class BasicLinkLayoutManager extends InlineLayoutManager { res.resolveIDRef(idref, pslm.getFirstPVWithID(idref)); if (!res.isResolved()) { pslm.addUnresolvedArea(idref, res); + if ( area instanceof BasicLinkArea ) { + // establish back-pointer from BasicLinkArea to LinkResolver to + // handle inline area unflattening during line bidi reordering; + // needed to create internal link trait on synthesized basic link area + ((BasicLinkArea)area).setResolver(res); + } } } else if (fobj.hasExternalDestination()) { String url = URISpecification.getURL(fobj.getExternalDestination()); diff --git a/src/java/org/apache/fop/layoutmgr/inline/BidiLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/BidiLayoutManager.java index f342399fc..b6145f025 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/BidiLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/BidiLayoutManager.java @@ -19,56 +19,19 @@ package org.apache.fop.layoutmgr.inline; -import java.util.ArrayList; -import java.util.List; - -import org.apache.fop.area.inline.InlineArea; import org.apache.fop.fo.flow.BidiOverride; - /** - * If this bidi has a different writing mode direction - * ltr or rtl than its parent writing mode then this - * reverses the inline areas (at the character level). + * Layout manager for fo:bidi-override. */ -public class BidiLayoutManager extends LeafNodeLayoutManager { - - private List children; +public class BidiLayoutManager extends InlineLayoutManager { /** * Construct bidi layout manager. - * @param node bidi override FO - * @param cLM parent layout manager + * @param node an BidiOverride FONode */ - public BidiLayoutManager(BidiOverride node, InlineLayoutManager cLM) { + public BidiLayoutManager(BidiOverride node) { super(node); - setParent(cLM); - children = new ArrayList(); -/* - for (int count = cLM.size() - 1; count >= 0; count--) { - InlineArea ia = cLM.get(count); - if (ia instanceof Word) { - // reverse word - Word word = (Word) ia; - StringBuffer sb = new StringBuffer(word.getWord()); - word.setWord(sb.reverse().toString()); - } - children.add(ia); - } -*/ - } - - /** @return number of children */ - public int size() { - return children.size(); - } - - /** - * @param index of child inline area - * @return a child inline area - */ - public InlineArea get(int index) { - return (InlineArea) children.get(index); } } diff --git a/src/java/org/apache/fop/layoutmgr/inline/CharacterLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/CharacterLayoutManager.java index 4877ff9bd..32d4d1004 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/CharacterLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/CharacterLayoutManager.java @@ -77,13 +77,18 @@ public class CharacterLayoutManager extends LeafNodeLayoutManager { private TextArea getCharacterInlineArea(Character node) { TextArea text = new TextArea(); char ch = node.getCharacter(); + int ipd = font.getCharWidth(ch); + int blockProgressionOffset = 0; + int level = node.getBidiLevel(); if (CharUtilities.isAnySpace(ch)) { // add space unless it's zero-width: if (!CharUtilities.isZeroWidthSpace(ch)) { - text.addSpace(ch, 0, CharUtilities.isAdjustableSpace(ch)); + text.addSpace(ch, ipd, CharUtilities.isAdjustableSpace(ch), + blockProgressionOffset, level); } } else { - text.addWord(String.valueOf(ch), 0); + int[] levels = ( level >= 0 ) ? new int[] {level} : null; + text.addWord(String.valueOf(ch), ipd, null, levels, null, blockProgressionOffset); } TraitSetter.setProducerID(text, node.getId()); TraitSetter.addTextDecoration(text, node.getTextDecoration()); @@ -105,9 +110,8 @@ public class CharacterLayoutManager extends LeafNodeLayoutManager { Character fobj = (Character)this.fobj; - ipd = MinOptMax.getInstance(font.getCharWidth(fobj.getCharacter())); + ipd = MinOptMax.getInstance(curArea.getIPD()); - curArea.setIPD(ipd.getOpt()); curArea.setBPD(font.getAscender() - font.getDescender()); TraitSetter.addFontTraits(curArea, font); @@ -227,4 +231,3 @@ public class CharacterLayoutManager extends LeafNodeLayoutManager { } } - diff --git a/src/java/org/apache/fop/layoutmgr/inline/InlineLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/InlineLayoutManager.java index ede078066..1eb91e0ab 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/InlineLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/InlineLayoutManager.java @@ -207,7 +207,7 @@ public class InlineLayoutManager extends InlineStackingLayoutManager { InlineArea area; if (isInline) { area = createInlineParent(); - area.setOffset(0); + area.setBlockProgressionOffset(0); } else { area = new InlineBlockParent(); } @@ -482,12 +482,12 @@ public class InlineLayoutManager extends InlineStackingLayoutManager { || lastLM instanceof InlineLevelLayoutManager); parent.setBPD(alignmentContext.getHeight()); if (parent instanceof InlineParent) { - parent.setOffset(alignmentContext.getOffset()); + parent.setBlockProgressionOffset(alignmentContext.getOffset()); } else if (parent instanceof InlineBlockParent) { // All inline elements are positioned by the renderers relative to // the before edge of their content rectangle if (borderProps != null) { - parent.setOffset(borderProps.getPaddingBefore(false, this) + parent.setBlockProgressionOffset(borderProps.getPaddingBefore(false, this) + borderProps.getBorderBeforeWidth(false)); } } diff --git a/src/java/org/apache/fop/layoutmgr/inline/InlineStackingLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/InlineStackingLayoutManager.java index b15f0f8db..4a203d55e 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/InlineStackingLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/InlineStackingLayoutManager.java @@ -180,6 +180,10 @@ public abstract class InlineStackingLayoutManager extends AbstractLayoutManager //getLogger().debug("Add leading space: " + iAdjust); Space ls = new Space(); ls.setIPD(iAdjust); + int level = parentArea.getBidiLevel(); + if ( level >= 0 ) { + ls.setBidiLevel ( level ); + } parentArea.addChildArea(ls); } } diff --git a/src/java/org/apache/fop/layoutmgr/inline/LeaderLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/LeaderLayoutManager.java index 2a58b2018..b3d3218ab 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/LeaderLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/LeaderLayoutManager.java @@ -115,7 +115,7 @@ public class LeaderLayoutManager extends LeafNodeLayoutManager { private InlineArea getLeaderInlineArea(LayoutContext context) { InlineArea leaderArea = null; - + int level = fobj.getBidiLevel(); if (fobj.getLeaderPattern() == EN_RULE) { if (fobj.getRuleStyle() != EN_NONE) { org.apache.fop.area.inline.Leader leader @@ -125,28 +125,41 @@ public class LeaderLayoutManager extends LeafNodeLayoutManager { leaderArea = leader; } else { leaderArea = new Space(); + if ( level >= 0 ) { + leaderArea.setBidiLevel ( level ); + } } leaderArea.setBPD(fobj.getRuleThickness().getValue(this)); leaderArea.addTrait(Trait.COLOR, fobj.getColor()); + if ( level >= 0 ) { + leaderArea.setBidiLevel ( level ); + } } else if (fobj.getLeaderPattern() == EN_SPACE) { leaderArea = new Space(); leaderArea.setBPD(fobj.getRuleThickness().getValue(this)); + if ( level >= 0 ) { + leaderArea.setBidiLevel ( level ); + } } else if (fobj.getLeaderPattern() == EN_DOTS) { TextArea t = new TextArea(); char dot = '.'; // userAgent.getLeaderDotCharacter(); - int width = font.getCharWidth(dot); - t.addWord("" + dot, 0); + int[] levels = ( level < 0 ) ? null : new int[] {level}; + t.addWord("" + dot, width, null, levels, null, 0); t.setIPD(width); t.setBPD(width); t.setBaselineOffset(width); TraitSetter.addFontTraits(t, font); t.addTrait(Trait.COLOR, fobj.getColor()); Space spacer = null; - if (fobj.getLeaderPatternWidth().getValue(this) > width) { + int widthLeaderPattern = fobj.getLeaderPatternWidth().getValue(this); + if (widthLeaderPattern > width) { spacer = new Space(); - spacer.setIPD(fobj.getLeaderPatternWidth().getValue(this) - width); - width = fobj.getLeaderPatternWidth().getValue(this); + spacer.setIPD(widthLeaderPattern - width); + if ( level >= 0 ) { + spacer.setBidiLevel ( level ); + } + width = widthLeaderPattern; } FilledArea fa = new FilledArea(); fa.setUnitWidth(width); @@ -155,7 +168,6 @@ public class LeaderLayoutManager extends LeafNodeLayoutManager { fa.addChildArea(spacer); } fa.setBPD(t.getBPD()); - leaderArea = fa; } else if (fobj.getLeaderPattern() == EN_USECONTENT) { if (fobj.getChildNodes() == null) { @@ -188,6 +200,9 @@ public class LeaderLayoutManager extends LeafNodeLayoutManager { if (fobj.getLeaderPatternWidth().getValue(this) > width) { spacer = new Space(); spacer.setIPD(fobj.getLeaderPatternWidth().getValue(this) - width); + if ( level >= 0 ) { + spacer.setBidiLevel ( level ); + } width = fobj.getLeaderPatternWidth().getValue(this); } fa.setUnitWidth(width); @@ -199,6 +214,7 @@ public class LeaderLayoutManager extends LeafNodeLayoutManager { //Content collapsed to nothing, so use a space leaderArea = new Space(); leaderArea.setBPD(fobj.getRuleThickness().getValue(this)); + leaderArea.setBidiLevel ( fobj.getBidiLevelRecursive() ); } } TraitSetter.setProducerID(leaderArea, fobj.getId()); diff --git a/src/java/org/apache/fop/layoutmgr/inline/LeafNodeLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/LeafNodeLayoutManager.java index 5e3464da7..c7205905b 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/LeafNodeLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/LeafNodeLayoutManager.java @@ -224,7 +224,7 @@ public abstract class LeafNodeLayoutManager extends AbstractLayoutManager * @param context the layout context used for adding the area */ protected void offsetArea(InlineArea area, LayoutContext context) { - area.setOffset(alignmentContext.getOffset()); + area.setBlockProgressionOffset(alignmentContext.getOffset()); } /** diff --git a/src/java/org/apache/fop/layoutmgr/inline/LineLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/LineLayoutManager.java index db7d1edc8..f57b9367a 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/LineLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/LineLayoutManager.java @@ -32,6 +32,7 @@ import org.apache.fop.area.Area; import org.apache.fop.area.LineArea; import org.apache.fop.area.Trait; import org.apache.fop.area.inline.InlineArea; +import org.apache.fop.complexscripts.bidi.BidiResolver; import org.apache.fop.datatypes.Length; import org.apache.fop.datatypes.Numeric; import org.apache.fop.fo.Constants; @@ -111,6 +112,7 @@ public class LineLayoutManager extends InlineStackingLayoutManager private final double dAdjust; // Percentage to adjust (stretch or shrink) private final double ipdAdjust; // Percentage to adjust (stretch or shrink) private final int startIndent; + private final int endIndent; private final int lineHeight; private final int lineWidth; private final int spaceBefore; @@ -119,8 +121,8 @@ public class LineLayoutManager extends InlineStackingLayoutManager LineBreakPosition( // CSOK: ParameterNumber LayoutManager lm, int index, int startIndex, int breakIndex, - int shrink, int stretch, int diff, double ipdA, double adjust, int ind, - int lh, int lw, int sb, int sa, int bl) { + int shrink, int stretch, int diff, double ipdA, double adjust, int si, + int ei, int lh, int lw, int sb, int sa, int bl) { super(lm, breakIndex); availableShrink = shrink; availableStretch = stretch; @@ -129,7 +131,8 @@ public class LineLayoutManager extends InlineStackingLayoutManager this.startIndex = startIndex; ipdAdjust = ipdA; dAdjust = adjust; - startIndent = ind; + startIndent = si; + endIndent = ei; lineHeight = lh; lineWidth = lw; spaceBefore = sb; @@ -140,6 +143,7 @@ public class LineLayoutManager extends InlineStackingLayoutManager } + private int bidiLevel = -1; private int textAlignment = EN_JUSTIFY; private int textAlignmentLast; private int effectiveAlignment; @@ -333,13 +337,38 @@ public class LineLayoutManager extends InlineStackingLayoutManager int total) { // compute indent and adjustment ratio, according to // the value of text-align and text-align-last - int indent = 0; + int startIndent; + int endIndent; int difference = bestActiveNode.difference; int textAlign = (bestActiveNode.line < total) ? alignment : alignmentLast; - indent += (textAlign == Constants.EN_CENTER) + + switch ( textAlign ) { + case Constants.EN_START: + startIndent = 0; + endIndent = difference > 0 ? difference : 0; + break; + case Constants.EN_END: + startIndent = difference > 0 ? difference : 0; + endIndent = 0; + break; + case Constants.EN_CENTER: + startIndent = difference / 2; + endIndent = startIndent; + break; + default: + case Constants.EN_JUSTIFY: + startIndent = 0; + endIndent = 0; + break; + } + + /* + startIndent += (textAlign == Constants.EN_CENTER) ? difference / 2 : (textAlign == Constants.EN_END) ? difference : 0; - indent += (bestActiveNode.line == 1 && indentFirstPart && isFirstInBlock) + */ + startIndent += (bestActiveNode.line == 1 && indentFirstPart && isFirstInBlock) ? textIndent : 0; + double ratio = (textAlign == Constants.EN_JUSTIFY || difference < 0 && -difference <= bestActiveNode.availableShrink) ? bestActiveNode.adjustRatio : 0; @@ -377,7 +406,7 @@ public class LineLayoutManager extends InlineStackingLayoutManager bestActiveNode.availableShrink - (addedPositions > 0 ? 0 : ((Paragraph) par).lineFiller.getShrink()), bestActiveNode.availableStretch, - difference, ratio, indent), activePossibility); + difference, ratio, startIndent, endIndent), activePossibility); addedPositions++; } @@ -389,7 +418,8 @@ public class LineLayoutManager extends InlineStackingLayoutManager private LineBreakPosition makeLineBreakPosition( // CSOK: ParameterNumber KnuthSequence par, int firstElementIndex, int lastElementIndex, int availableShrink, - int availableStretch, int difference, double ratio, int indent) { + int availableStretch, int difference, double ratio, int startIndent, + int endIndent) { // line height calculation - spaceBefore may differ from spaceAfter // by 1mpt due to rounding int spaceBefore = (lineHeight - lead - follow) / 2; @@ -456,14 +486,14 @@ public class LineLayoutManager extends InlineStackingLayoutManager knuthParagraphs.indexOf(par), firstElementIndex, lastElementIndex, availableShrink, availableStretch, - difference, ratio, 0, indent, + difference, ratio, 0, startIndent, endIndent, 0, ipd, 0, 0, 0); } else { return new LineBreakPosition(thisLLM, knuthParagraphs.indexOf(par), firstElementIndex, lastElementIndex, availableShrink, availableStretch, - difference, ratio, 0, indent, + difference, ratio, 0, startIndent, endIndent, lineLead + lineFollow, ipd, spaceBefore, spaceAfter, lineLead); @@ -544,6 +574,7 @@ public class LineLayoutManager extends InlineStackingLayoutManager /** {@inheritDoc} */ @Override public void initialize() { + bidiLevel = fobj.getBidiLevel(); textAlignment = fobj.getTextAlign(); textAlignmentLast = fobj.getTextAlignLast(); textIndent = fobj.getTextIndent(); @@ -1429,8 +1460,12 @@ public class LineLayoutManager extends InlineStackingLayoutManager if (lbp.startIndent != 0) { lineArea.addTrait(Trait.START_INDENT, lbp.startIndent); } + if (lbp.endIndent != 0) { + lineArea.addTrait(Trait.END_INDENT, new Integer(lbp.endIndent)); + } lineArea.setBPD(lbp.lineHeight); lineArea.setIPD(lbp.lineWidth); + lineArea.setBidiLevel(bidiLevel); lineArea.addTrait(Trait.SPACE_BEFORE, lbp.spaceBefore); lineArea.addTrait(Trait.SPACE_AFTER, lbp.spaceAfter); alignmentContext.resizeLine(lbp.lineHeight, lbp.baseline); @@ -1506,7 +1541,10 @@ public class LineLayoutManager extends InlineStackingLayoutManager && (!context.isLastArea() || !isLastPosition)) { lineArea.setBPD(lineArea.getBPD() + context.getSpaceAfter()); } - lineArea.finalise(); + lineArea.finish(); + if ( lineArea.getBidiLevel() >= 0 ) { + BidiResolver.reorder ( lineArea ); + } parentLayoutManager.addChildArea(lineArea); } @@ -1556,6 +1594,9 @@ public class LineLayoutManager extends InlineStackingLayoutManager blocklc.setTrailingSpace(new SpaceSpecifier(false)); } lineArea.updateExtentsFromChildren(); + if ( lineArea.getBidiLevel() >= 0 ) { + BidiResolver.reorder ( lineArea ); + } parentLayoutManager.addChildArea(lineArea); } diff --git a/src/java/org/apache/fop/layoutmgr/inline/PageNumberCitationLastLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/PageNumberCitationLastLayoutManager.java index 56bcdaa3b..b1ba38565 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/PageNumberCitationLastLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/PageNumberCitationLastLayoutManager.java @@ -56,22 +56,24 @@ public class PageNumberCitationLastLayoutManager extends AbstractPageNumberCitat */ private InlineArea getPageNumberCitationLastInlineArea(LayoutManager parentLM) { TextArea text = null; - resolved = false; + int level = getBidiLevel(); if (!getPSLM().associateLayoutManagerID(fobj.getRefId())) { text = new UnresolvedPageNumber(fobj.getRefId(), font, UnresolvedPageNumber.LAST); getPSLM().addUnresolvedArea(fobj.getRefId(), (Resolvable)text); String str = "MMM"; // reserve three spaces for page number int width = getStringWidth(str); + text.setBidiLevel(level); text.setIPD(width); + resolved = false; } else { PageViewport page = getPSLM().getLastPVWithID(fobj.getRefId()); String str = page.getPageNumberString(); // get page string from parent, build area text = new TextArea(); int width = getStringWidth(str); - text.addWord(str, 0); + text.setBidiLevel(level); + text.addWord(str, 0, level); text.setIPD(width); - resolved = true; } diff --git a/src/java/org/apache/fop/layoutmgr/inline/PageNumberCitationLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/PageNumberCitationLayoutManager.java index 958a854ac..16ce09ec5 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/PageNumberCitationLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/PageNumberCitationLayoutManager.java @@ -50,27 +50,32 @@ public class PageNumberCitationLayoutManager extends AbstractPageNumberCitationL /** * if id can be resolved then simply return a word, otherwise * return a resolvable area + * + * TODO: [GA] May need to run bidi algorithm and script processor + * on resolved page number. */ private InlineArea getPageNumberCitationInlineArea() { PageViewport page = getPSLM().getFirstPVWithID(fobj.getRefId()); TextArea text = null; + int level = getBidiLevel(); if (page != null) { String str = page.getPageNumberString(); // get page string from parent, build area text = new TextArea(); - int width = getStringWidth(str); - text.addWord(str, 0); - text.setIPD(width); + int width = getStringWidth(str); // TODO: [GA] !I18N! + text.setBidiLevel(level); + text.addWord(str, 0, level); + text.setIPD(width); // TODO: [GA] !I18N! resolved = true; } else { - resolved = false; text = new UnresolvedPageNumber(fobj.getRefId(), font); String str = "MMM"; // reserve three spaces for page number - int width = getStringWidth(str); - text.setIPD(width); + int width = getStringWidth(str); // TODO: [GA] !I18N! + text.setBidiLevel(level); + text.setIPD(width); // TODO: [GA] !I18N! + resolved = false; } updateTextAreaTraits(text); - return text; } diff --git a/src/java/org/apache/fop/layoutmgr/inline/PageNumberLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/PageNumberLayoutManager.java index 4b7289b37..c3ee5af32 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/PageNumberLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/PageNumberLayoutManager.java @@ -101,7 +101,7 @@ public class PageNumberLayoutManager extends LeafNodeLayoutManager { TraitSetter.setProducerID(ta, fobj.getId()); ta.setIPD(baseArea.getIPD()); ta.setBPD(baseArea.getBPD()); - ta.setOffset(baseArea.getOffset()); + ta.setBlockProgressionOffset(baseArea.getBlockProgressionOffset()); ta.setBaselineOffset(baseArea.getBaselineOffset()); ta.addTrait(Trait.COLOR, fobj.getColor()); //only to initialize the trait map ta.getTraits().putAll(baseArea.getTraits()); diff --git a/src/java/org/apache/fop/layoutmgr/inline/ScaledBaselineTable.java b/src/java/org/apache/fop/layoutmgr/inline/ScaledBaselineTable.java index d5d3e5edb..1ccba23d7 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/ScaledBaselineTable.java +++ b/src/java/org/apache/fop/layoutmgr/inline/ScaledBaselineTable.java @@ -20,6 +20,7 @@ package org.apache.fop.layoutmgr.inline; import org.apache.fop.fo.Constants; +import org.apache.fop.traits.WritingMode; /** @@ -43,7 +44,7 @@ final class ScaledBaselineTable { private final int dominantBaselineIdentifier; - private final int writingMode; + private final WritingMode writingMode; private final int dominantBaselineOffset; @@ -66,7 +67,7 @@ final class ScaledBaselineTable { int depth, int xHeight, int dominantBaselineIdentifier, - int writingMode) { + WritingMode writingMode) { this.altitude = altitude; this.depth = depth; this.xHeight = xHeight; @@ -89,7 +90,7 @@ final class ScaledBaselineTable { * Return the writing mode for this baseline table. * @return the writing mode */ - int getWritingMode() { + WritingMode getWritingMode() { return this.writingMode; } @@ -139,7 +140,7 @@ final class ScaledBaselineTable { } private boolean isHorizontalWritingMode() { - return writingMode == Constants.EN_LR_TB || writingMode == Constants.EN_RL_TB; + return writingMode.isHorizontal(); } /** diff --git a/src/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java b/src/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java index 8767fe296..c63c65277 100644 --- a/src/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java @@ -19,6 +19,7 @@ package org.apache.fop.layoutmgr.inline; +import java.util.Arrays; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -29,6 +30,8 @@ import org.apache.commons.logging.LogFactory; import org.apache.fop.area.Trait; import org.apache.fop.area.inline.TextArea; +import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; +import org.apache.fop.complexscripts.util.CharScript; import org.apache.fop.fo.Constants; import org.apache.fop.fo.FOText; import org.apache.fop.fonts.Font; @@ -71,6 +74,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { private final int startIndex; private final int breakIndex; + private int wordCharLength; private final int wordSpaceCount; private int letterSpaceCount; private MinOptMax areaIPD; @@ -78,14 +82,17 @@ public class TextLayoutManager extends LeafNodeLayoutManager { private final boolean isSpace; private boolean breakOppAfter; private final Font font; + private final int level; + private final int[][] gposAdjustments; - AreaInfo( // CSOK: ParameterNumber - int startIndex, int breakIndex, int wordSpaceCount, int letterSpaceCount, - MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter, - Font font) { + AreaInfo // CSOK: ParameterNumber + (int startIndex, int breakIndex, int wordSpaceCount, int letterSpaceCount, + MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter, + Font font, int level, int[][] gposAdjustments) { assert startIndex <= breakIndex; this.startIndex = startIndex; this.breakIndex = breakIndex; + this.wordCharLength = -1; this.wordSpaceCount = wordSpaceCount; this.letterSpaceCount = letterSpaceCount; this.areaIPD = areaIPD; @@ -93,10 +100,26 @@ public class TextLayoutManager extends LeafNodeLayoutManager { this.isSpace = isSpace; this.breakOppAfter = breakOppAfter; this.font = font; + this.level = level; + this.gposAdjustments = gposAdjustments; } - private int getCharLength() { - return breakIndex - startIndex; + /** + * Obtain number of 'characters' contained in word. If word + * is mapped, then this number may be less than or greater than the + * original length (breakIndex - startIndex). We compute and + * memoize thius length upon first invocation of this method. + */ + private int getWordLength() { + if ( wordCharLength == -1 ) { + if ( foText.hasMapping ( startIndex, breakIndex ) ) { + wordCharLength = foText.getMapping ( startIndex, breakIndex ).length(); + } else { + assert breakIndex >= startIndex; + wordCharLength = breakIndex - startIndex; + } + } + return wordCharLength; } private void addToAreaIPD(MinOptMax idp) { @@ -104,16 +127,16 @@ public class TextLayoutManager extends LeafNodeLayoutManager { } public String toString() { - return "AreaInfo[" - + "letterSpaceCount = " + letterSpaceCount - + ", wordSpaceCount = " + wordSpaceCount + return super.toString() + "{" + + "interval = [" + startIndex + "," + breakIndex + "]" + + ", isSpace = " + isSpace + + ", level = " + level + ", areaIPD = " + areaIPD - + ", startIndex = " + startIndex - + ", breakIndex = " + breakIndex + + ", letterSpaceCount = " + letterSpaceCount + + ", wordSpaceCount = " + wordSpaceCount + ", isHyphenated = " + isHyphenated - + ", isSpace = " + isSpace + ", font = " + font - + "]"; + + "}"; } } @@ -149,7 +172,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { * be used to influence the start position of the first letter. The entry i+1 defines the * cursor advancement after the character i. A null entry means no special advancement. */ - private final MinOptMax[] letterAdjustArray; //size = textArray.length + 1 + private final MinOptMax[] letterSpaceAdjustArray; //size = textArray.length + 1 /** Font used for the space between words. */ private Font spaceFont = null; @@ -192,7 +215,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { */ public TextLayoutManager(FOText node) { foText = node; - letterAdjustArray = new MinOptMax[node.length() + 1]; + letterSpaceAdjustArray = new MinOptMax[node.length() + 1]; areaInfos = new ArrayList(); } @@ -269,7 +292,9 @@ public class TextLayoutManager extends LeafNodeLayoutManager { } if (tbpNext.getLeafPos() != -1) { areaInfo = (AreaInfo) areaInfos.get(tbpNext.getLeafPos()); - if (lastAreaInfo == null || areaInfo.font != lastAreaInfo.font) { + if (lastAreaInfo == null + || ( areaInfo.font != lastAreaInfo.font ) + || ( areaInfo.level != lastAreaInfo.level ) ) { if (lastAreaInfo != null) { addAreaInfoAreas(lastAreaInfo, wordSpaceCount, letterSpaceCount, firstAreaInfoIndex, @@ -291,6 +316,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { addAreaInfoAreas(lastAreaInfo, wordSpaceCount, letterSpaceCount, firstAreaInfoIndex, lastAreaInfoIndex, realWidth, context); } + } private void addAreaInfoAreas(AreaInfo areaInfo, int wordSpaceCount, int letterSpaceCount, @@ -301,7 +327,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { // changes. However, it seems as if they should use the AreaInfo from // firstAreaInfoIndex.. lastAreaInfoIndex rather than just the last areaInfo. // This needs to be checked. - int textLength = areaInfo.getCharLength(); + int textLength = areaInfo.getWordLength(); if (areaInfo.letterSpaceCount == textLength && !areaInfo.isHyphenated && context.isLastArea()) { // the line ends at a character like "/" or "-"; @@ -311,8 +337,8 @@ public class TextLayoutManager extends LeafNodeLayoutManager { } for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) { - MinOptMax letterAdjustment = letterAdjustArray[i + 1]; - if (letterAdjustment != null && letterAdjustment.isElastic()) { + MinOptMax letterSpaceAdjustment = letterSpaceAdjustArray[i + 1]; + if (letterSpaceAdjustment != null && letterSpaceAdjustment.isElastic()) { letterSpaceCount++; } } @@ -387,21 +413,27 @@ public class TextLayoutManager extends LeafNodeLayoutManager { private final class TextAreaBuilder { - private final MinOptMax width; - private final int adjust; - private final LayoutContext context; - private final int firstIndex; - private final int lastIndex; - private final boolean isLastArea; - private final Font font; - - private int blockProgressionDimension; - private AreaInfo areaInfo; - private StringBuffer wordChars; - private int[] letterAdjust; - private int letterAdjustIndex; - - private TextArea textArea; + // constructor initialized state + private final MinOptMax width; // content ipd + private final int adjust; // content ipd adjustment + private final LayoutContext context; // layout context + private final int firstIndex; // index of first AreaInfo + private final int lastIndex; // index of last AreaInfo + private final boolean isLastArea; // true if last inline area in line area + private final Font font; // applicable font + + // other, non-constructor state + private TextArea textArea; // text area being constructed + private int blockProgressionDimension; // calculated bpd + private AreaInfo areaInfo; // current area info when iterating over words + private StringBuffer wordChars; // current word's character buffer + private int[] letterSpaceAdjust; // current word's letter space adjustments + private int letterSpaceAdjustIndex; // last written letter space adjustment index + private int[] wordLevels; // current word's bidi levels + private int wordLevelsIndex; // last written bidi level index + private int wordIPD; // accumulated ipd of current word + private int[][] gposAdjustments; // current word's glyph position adjustments + private int gposAdjustmentsIndex; // last written glyph position adjustment index /** * Creates a new <code>TextAreaBuilder</code> which itself builds an inline word area. This @@ -432,7 +464,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { calcBlockProgressionDimension(); setBlockProgressionDimension(); setBaselineOffset(); - setOffset(); + setBlockProgressionOffset(); setText(); TraitSetter.addFontTraits(textArea, font); textArea.addTrait(Trait.COLOR, foText.getColor()); @@ -471,11 +503,11 @@ public class TextLayoutManager extends LeafNodeLayoutManager { textArea.setBaselineOffset(font.getAscender()); } - private void setOffset() { + private void setBlockProgressionOffset() { if (blockProgressionDimension == alignmentContext.getHeight()) { - textArea.setOffset(0); + textArea.setBlockProgressionOffset(0); } else { - textArea.setOffset(alignmentContext.getOffset()); + textArea.setBlockProgressionOffset(alignmentContext.getOffset()); } } @@ -483,7 +515,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { * Sets the text of the TextArea, split into words and spaces. */ private void setText() { - int wordStartIndex = -1; + int areaInfoIndex = -1; int wordCharLength = 0; for (int wordIndex = firstIndex; wordIndex <= lastIndex; wordIndex++) { areaInfo = getAreaInfo(wordIndex); @@ -491,15 +523,15 @@ public class TextLayoutManager extends LeafNodeLayoutManager { addSpaces(); } else { // areaInfo stores information about a word fragment - if (wordStartIndex == -1) { + if (areaInfoIndex == -1) { // here starts a new word - wordStartIndex = wordIndex; + areaInfoIndex = wordIndex; wordCharLength = 0; } - wordCharLength += areaInfo.getCharLength(); + wordCharLength += areaInfo.getWordLength(); if (isWordEnd(wordIndex)) { - addWord(wordStartIndex, wordIndex, wordCharLength); - wordStartIndex = -1; + addWord(areaInfoIndex, wordIndex, wordCharLength); + areaInfoIndex = -1; } } } @@ -509,26 +541,78 @@ public class TextLayoutManager extends LeafNodeLayoutManager { return areaInfoIndex == lastIndex || getAreaInfo(areaInfoIndex + 1).isSpace; } - private void addWord(int startIndex, int endIndex, int charLength) { + /** + * Add word with fragments from STARTINDEX to ENDINDEX, where + * total length of (possibly mapped) word is CHARLENGTH. + * A word is composed from one or more word fragments, where each + * fragment corresponds to distinct instance in a sequence of + * area info instances starting at STARTINDEX continuing through (and + * including) ENDINDEX. + * @param startIndex index of first area info of word to add + * @param endIndex index of last area info of word to add + * @param wordLength number of (mapped) characters in word + */ + private void addWord(int startIndex, int endIndex, int wordLength) { + int blockProgressionOffset = 0; + boolean gposAdjusted = false; if (isHyphenated(endIndex)) { - charLength++; + // TODO may be problematic in some I18N contexts [GA] + wordLength++; } - initWord(charLength); + initWord(wordLength); + // iterate over word's fragments, adding word chars (with bidi + // levels), letter space adjustments, and glyph position adjustments for (int i = startIndex; i <= endIndex; i++) { AreaInfo wordAreaInfo = getAreaInfo(i); addWordChars(wordAreaInfo); addLetterAdjust(wordAreaInfo); + if ( addGlyphPositionAdjustments(wordAreaInfo) ) { + gposAdjusted = true; + } } if (isHyphenated(endIndex)) { + // TODO may be problematic in some I18N contexts [GA] addHyphenationChar(); } - textArea.addWord(wordChars.toString(), 0, letterAdjust); + if ( !gposAdjusted ) { + gposAdjustments = null; + } + textArea.addWord(wordChars.toString(), wordIPD, letterSpaceAdjust, + getNonEmptyLevels(), gposAdjustments, blockProgressionOffset); } - private void initWord(int charLength) { - wordChars = new StringBuffer(charLength); - letterAdjust = new int[charLength]; - letterAdjustIndex = 0; + private int[] getNonEmptyLevels() { + if ( wordLevels != null ) { + assert wordLevelsIndex <= wordLevels.length; + boolean empty = true; + for ( int i = 0, n = wordLevelsIndex; i < n; i++ ) { + if ( wordLevels [ i ] >= 0 ) { + empty = false; + break; + } + } + return empty ? null : wordLevels; + } else { + return null; + } + } + + /** + * Fully allocate word character buffer, letter space adjustments + * array, bidi levels array, and glyph position adjustments array. + * based on full word length, including all (possibly mapped) fragments. + * @param wordLength length of word including all (possibly mapped) fragments + */ + private void initWord(int wordLength) { + wordChars = new StringBuffer(wordLength); + letterSpaceAdjust = new int[wordLength]; + letterSpaceAdjustIndex = 0; + wordLevels = new int[wordLength]; + wordLevelsIndex = 0; + Arrays.fill ( wordLevels, -1 ); + gposAdjustments = new int[wordLength][4]; + gposAdjustmentsIndex = 0; + wordIPD = 0; } private boolean isHyphenated(int endIndex) { @@ -537,27 +621,112 @@ public class TextLayoutManager extends LeafNodeLayoutManager { private void addHyphenationChar() { wordChars.append(foText.getCommonHyphenation().getHyphChar(font)); + // [TBD] expand bidi word levels, letter space adjusts, gpos adjusts + // [TBD] [GA] problematic in bidi context... what is level of hyphen? } + /** + * Given a word area info associated with a word fragment, + * (1) concatenate (possibly mapped) word characters to word character buffer; + * (2) concatenante (possibly mapped) word bidi levels to levels buffer; + * (3) update word's IPD with optimal IPD of fragment. + * @param wordAreaInfo fragment info + */ private void addWordChars(AreaInfo wordAreaInfo) { - for (int i = wordAreaInfo.startIndex; i < wordAreaInfo.breakIndex; i++) { - wordChars.append(foText.charAt(i)); + int s = wordAreaInfo.startIndex; + int e = wordAreaInfo.breakIndex; + if ( foText.hasMapping ( s, e ) ) { + wordChars.append ( foText.getMapping ( s, e ) ); + addWordLevels ( foText.getMappingBidiLevels ( s, e ) ); + } else { + for (int i = s; i < e; i++) { + wordChars.append(foText.charAt(i)); + } + addWordLevels ( foText.getBidiLevels ( s, e ) ); + } + wordIPD += wordAreaInfo.areaIPD.getOpt(); + } + + /** + * Given a (possibly null) bidi levels array associated with a word fragment, + * concatenante (possibly mapped) word bidi levels to levels buffer. + * @param levels bidi levels array or null + */ + private void addWordLevels ( int[] levels ) { + int numLevels = ( levels != null ) ? levels.length : 0; + if ( numLevels > 0 ) { + int need = wordLevelsIndex + numLevels; + if ( need <= wordLevels.length ) { + System.arraycopy ( levels, 0, wordLevels, wordLevelsIndex, numLevels ); + } else { + throw new IllegalStateException + ( "word levels array too short: expect at least " + + need + " entries, but has only " + wordLevels.length + " entries" ); + } } + wordLevelsIndex += numLevels; } + /** + * Given a word area info associated with a word fragment, + * concatenate letter space adjustments for each (possibly mapped) character. + * @param wordAreaInfo fragment info + */ private void addLetterAdjust(AreaInfo wordAreaInfo) { int letterSpaceCount = wordAreaInfo.letterSpaceCount; - for (int i = wordAreaInfo.startIndex; i < wordAreaInfo.breakIndex; i++) { - if (letterAdjustIndex > 0) { - MinOptMax adj = letterAdjustArray[i]; - letterAdjust[letterAdjustIndex] = adj == null ? 0 : adj.getOpt(); + int wordLength = wordAreaInfo.getWordLength(); + int taAdjust = textArea.getTextLetterSpaceAdjust(); + for ( int i = 0, n = wordLength; i < n; i++ ) { + int j = letterSpaceAdjustIndex + i; + if ( j > 0 ) { + int k = wordAreaInfo.startIndex + i; + MinOptMax adj = ( k < letterSpaceAdjustArray.length ) + ? letterSpaceAdjustArray [ k ] : null; + letterSpaceAdjust [ j ] = ( adj == null ) ? 0 : adj.getOpt(); } - if (letterSpaceCount > 0) { - letterAdjust[letterAdjustIndex] += textArea.getTextLetterSpaceAdjust(); + if ( letterSpaceCount > 0 ) { + letterSpaceAdjust [ j ] += taAdjust; letterSpaceCount--; } - letterAdjustIndex++; } + letterSpaceAdjustIndex += wordLength; + } + + /** + * Given a word area info associated with a word fragment, + * concatenate glyph position adjustments for each (possibly mapped) character. + * @param wordAreaInfo fragment info + * @return true if an adjustment was non-zero + */ + private boolean addGlyphPositionAdjustments(AreaInfo wordAreaInfo) { + boolean adjusted = false; + int[][] gpa = wordAreaInfo.gposAdjustments; + int numAdjusts = ( gpa != null ) ? gpa.length : 0; + int wordLength = wordAreaInfo.getWordLength(); + if ( numAdjusts > 0 ) { + int need = gposAdjustmentsIndex + numAdjusts; + if ( need <= gposAdjustments.length ) { + for ( int i = 0, n = wordLength, j = 0; i < n; i++ ) { + if ( i < numAdjusts ) { + int[] wpa1 = gposAdjustments [ gposAdjustmentsIndex + i ]; + int[] wpa2 = gpa [ j++ ]; + for ( int k = 0; k < 4; k++ ) { + int a = wpa2 [ k ]; + if ( a != 0 ) { + wpa1 [ k ] += a; adjusted = true; + } + } + } + } + } else { + throw new IllegalStateException + ( "gpos adjustments array too short: expect at least " + + need + " entries, but has only " + gposAdjustments.length + + " entries" ); + } + } + gposAdjustmentsIndex += wordLength; + return adjusted; } /** @@ -566,13 +735,44 @@ public class TextLayoutManager extends LeafNodeLayoutManager { * Add the spaces - except zero-width spaces - to the TextArea. */ private void addSpaces() { + int blockProgressionOffset = 0; + // [TBD] need to better handling of spaceIPD assignment, for now, + // divide the area info's allocated IPD evenly among the + // non-zero-width space characters + int numZeroWidthSpaces = 0; for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) { char spaceChar = foText.charAt(i); + if (CharUtilities.isZeroWidthSpace(spaceChar)) { + numZeroWidthSpaces++; + } + } + int numSpaces = areaInfo.breakIndex - areaInfo.startIndex - numZeroWidthSpaces; + int spaceIPD = areaInfo.areaIPD.getOpt() / ( ( numSpaces > 0 ) ? numSpaces : 1 ); + // add space area children, one for each non-zero-width space character + for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) { + char spaceChar = foText.charAt(i); + int level = foText.bidiLevelAt(i); if (!CharUtilities.isZeroWidthSpace(spaceChar)) { - textArea.addSpace(spaceChar, 0, CharUtilities.isAdjustableSpace(spaceChar)); + textArea.addSpace + ( spaceChar, spaceIPD, + CharUtilities.isAdjustableSpace(spaceChar), + blockProgressionOffset, level ); } } } + + } + + private void addAreaInfo ( AreaInfo ai ) { + addAreaInfo ( areaInfos.size(), ai ); + } + + private void addAreaInfo ( int index, AreaInfo ai ) { + areaInfos.add ( index, ai ); + } + + private void removeAreaInfo ( int index ) { + areaInfos.remove ( index ); } private AreaInfo getAreaInfo(int index) { @@ -580,10 +780,10 @@ public class TextLayoutManager extends LeafNodeLayoutManager { } private void addToLetterAdjust(int index, int width) { - if (letterAdjustArray[index] == null) { - letterAdjustArray[index] = MinOptMax.getInstance(width); + if (letterSpaceAdjustArray[index] == null) { + letterSpaceAdjustArray[index] = MinOptMax.getInstance(width); } else { - letterAdjustArray[index] = letterAdjustArray[index].plus(width); + letterSpaceAdjustArray[index] = letterSpaceAdjustArray[index].plus(width); } } @@ -600,6 +800,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { /** {@inheritDoc} */ public List getNextKnuthElements(final LayoutContext context, final int alignment) { + lineStartBAP = context.getLineStartBorderAndPaddingWidth(); lineEndBAP = context.getLineEndBorderAndPaddingWidth(); alignmentContext = context.getAlignmentContext(); @@ -610,13 +811,19 @@ public class TextLayoutManager extends LeafNodeLayoutManager { AreaInfo prevAreaInfo = null; returnList.add(sequence); + if (LOG.isDebugEnabled()) { + LOG.debug ( "GK: [" + nextStart + "," + foText.length() + "]" ); + } LineBreakStatus lineBreakStatus = new LineBreakStatus(); thisStart = nextStart; boolean inWord = false; boolean inWhitespace = false; char ch = 0; + int level = -1; + int prevLevel = -1; while (nextStart < foText.length()) { ch = foText.charAt(nextStart); + level = foText.bidiLevelAt(nextStart); boolean breakOpportunity = false; byte breakAction = keepTogether ? LineBreakStatus.PROHIBITED_BREAK @@ -635,17 +842,29 @@ public class TextLayoutManager extends LeafNodeLayoutManager { default: TextLayoutManager.LOG.error("Unexpected breakAction: " + breakAction); } + if (LOG.isDebugEnabled()) { + LOG.debug ( "GK: {" + + " index = " + nextStart + + ", char = " + CharUtilities.charToNCRef ( ch ) + + ", level = " + level + + ", levelPrev = " + prevLevel + + ", inWord = " + inWord + + ", inSpace = " + inWhitespace + + "}" ); + } if (inWord) { - if (breakOpportunity - || TextLayoutManager.isSpace(ch) - || CharUtilities.isExplicitBreak(ch)) { + if ( breakOpportunity + || TextLayoutManager.isSpace(ch) + || CharUtilities.isExplicitBreak(ch) + || ( ( prevLevel != -1 ) && ( level != prevLevel ) ) ) { // this.foText.charAt(lastIndex) == CharUtilities.SOFT_HYPHEN prevAreaInfo = processWord(alignment, sequence, prevAreaInfo, ch, - breakOpportunity, true); + breakOpportunity, true, prevLevel); } } else if (inWhitespace) { if (ch != CharUtilities.SPACE || breakOpportunity) { - prevAreaInfo = processWhitespace(alignment, sequence, breakOpportunity); + prevAreaInfo = processWhitespace(alignment, sequence, + breakOpportunity, prevLevel); } } else { if (areaInfo != null) { @@ -665,14 +884,14 @@ public class TextLayoutManager extends LeafNodeLayoutManager { // preserved space or non-breaking space: // create the AreaInfo object areaInfo = new AreaInfo(nextStart, nextStart + 1, 1, 0, wordSpaceIPD, false, true, - breakOpportunity, spaceFont); + breakOpportunity, spaceFont, level, null); thisStart = nextStart + 1; } else if (CharUtilities.isFixedWidthSpace(ch) || CharUtilities.isZeroWidthSpace(ch)) { // create the AreaInfo object Font font = FontSelector.selectFontForCharacterInText(ch, foText, this); MinOptMax ipd = MinOptMax.getInstance(font.getCharWidth(ch)); areaInfo = new AreaInfo(nextStart, nextStart + 1, 0, 0, ipd, false, true, - breakOpportunity, font); + breakOpportunity, font, level, null); thisStart = nextStart + 1; } else if (CharUtilities.isExplicitBreak(ch)) { //mandatory break-character: only advance index @@ -682,14 +901,15 @@ public class TextLayoutManager extends LeafNodeLayoutManager { inWord = !TextLayoutManager.isSpace(ch) && !CharUtilities.isExplicitBreak(ch); inWhitespace = ch == CharUtilities.SPACE && foText.getWhitespaceTreatment() != Constants.EN_PRESERVE; + prevLevel = level; nextStart++; } // Process any last elements if (inWord) { - processWord(alignment, sequence, prevAreaInfo, ch, false, false); + processWord(alignment, sequence, prevAreaInfo, ch, false, false, prevLevel); } else if (inWhitespace) { - processWhitespace(alignment, sequence, !keepTogether); + processWhitespace(alignment, sequence, !keepTogether, prevLevel); } else if (areaInfo != null) { processLeftoverAreaInfo(alignment, sequence, areaInfo, ch == CharUtilities.ZERO_WIDTH_SPACE); @@ -708,6 +928,8 @@ public class TextLayoutManager extends LeafNodeLayoutManager { } else { return returnList; } + + } private KnuthSequence processLinebreak(List returnList, KnuthSequence sequence) { @@ -723,20 +945,27 @@ public class TextLayoutManager extends LeafNodeLayoutManager { private void processLeftoverAreaInfo(int alignment, KnuthSequence sequence, AreaInfo areaInfo, boolean breakOpportunityAfter) { - areaInfos.add(areaInfo); + addAreaInfo(areaInfo); areaInfo.breakOppAfter = breakOpportunityAfter; addElementsForASpace(sequence, alignment, areaInfo, areaInfos.size() - 1); } private AreaInfo processWhitespace(final int alignment, - final KnuthSequence sequence, final boolean breakOpportunity) { + final KnuthSequence sequence, final boolean breakOpportunity, int level) { + + if (LOG.isDebugEnabled()) { + LOG.debug ( "PS: [" + thisStart + "," + nextStart + "]" ); + } + // End of whitespace // create the AreaInfo object assert nextStart >= thisStart; - AreaInfo areaInfo = new AreaInfo(thisStart, nextStart, nextStart - thisStart, 0, - wordSpaceIPD.mult(nextStart - thisStart), false, true, breakOpportunity, spaceFont); + AreaInfo areaInfo = new AreaInfo + ( thisStart, nextStart, nextStart - thisStart, 0, + wordSpaceIPD.mult(nextStart - thisStart), + false, true, breakOpportunity, spaceFont, level, null ); - areaInfos.add(areaInfo); + addAreaInfo(areaInfo); // create the elements addElementsForASpace(sequence, alignment, areaInfo, areaInfos.size() - 1); @@ -745,22 +974,136 @@ public class TextLayoutManager extends LeafNodeLayoutManager { return areaInfo; } - private AreaInfo processWord(final int alignment, final KnuthSequence sequence, - AreaInfo prevAreaInfo, final char ch, final boolean breakOpportunity, - final boolean checkEndsWithHyphen) { + private AreaInfo processWordMapping + ( int lastIndex, final Font font, AreaInfo prevAreaInfo, final char breakOpportunityChar, + final boolean endsWithHyphen, int level ) { + int s = this.thisStart; // start index of word in FOText character buffer + int e = lastIndex; // end index of word in FOText character buffer + int nLS = 0; // # of letter spaces + String script = foText.getScript(); + String language = foText.getLanguage(); + + if (LOG.isDebugEnabled()) { + LOG.debug ( "PW: [" + thisStart + "," + lastIndex + "]: {" + + " +M" + + ", level = " + level + + " }" ); + } - //Word boundary found, process widths and kerning - int lastIndex = nextStart; - while (lastIndex > 0 && foText.charAt(lastIndex - 1) == CharUtilities.SOFT_HYPHEN) { - lastIndex--; + // 1. extract unmapped character sequence + CharSequence ics = foText.subSequence ( s, e ); + + // 2. if script is not specified (by FO property) or it is specified as 'auto', + // then compute dominant script + if ( ( script == null ) || "auto".equals(script) ) { + script = CharScript.scriptTagFromCode ( CharScript.dominantScript ( ics ) ); + } + if ( ( language == null ) || "none".equals(language) ) { + language = "dflt"; } - Font font = FontSelector - .selectFontForCharactersInText(foText, thisStart, lastIndex, foText, this); + + // 3. perform mapping of chars to glyphs ... to glyphs ... to chars + CharSequence mcs = font.performSubstitution ( ics, script, language ); + + // 4. compute glyph position adjustments on (substituted) characters + int[][] gpa; + if ( font.performsPositioning() ) { + // handle GPOS adjustments + gpa = font.performPositioning ( mcs, script, language ); + } else if ( font.hasKerning() ) { + // handle standard (non-GPOS) kerning adjustments + gpa = getKerningAdjustments ( mcs, font ); + } else { + gpa = null; + } + + // 5. reorder combining marks so that they precede (within the mapped char sequence) the + // base to which they are applied; N.B. position adjustments (gpa) are reordered in place + mcs = font.reorderCombiningMarks ( mcs, gpa, script, language ); + + // 6. if mapped sequence differs from input sequence, then memoize mapped sequence + if ( !CharUtilities.isSameSequence ( mcs, ics ) ) { + foText.addMapping ( s, e, mcs ); + } + + // 7. compute word ipd based on final position adjustments + MinOptMax ipd = MinOptMax.ZERO; + for ( int i = 0, n = mcs.length(); i < n; i++ ) { + int c = mcs.charAt ( i ); + // TODO !BMP + int w = font.getCharWidth ( c ); + if ( w < 0 ) { + w = 0; + } + if ( gpa != null ) { + w += gpa [ i ] [ GlyphPositioningTable.Value.IDX_X_ADVANCE ]; + } + ipd = ipd.plus ( w ); + } + + // [TBD] - handle letter spacing + + return new AreaInfo + ( s, e, 0, nLS, ipd, endsWithHyphen, false, + breakOpportunityChar != 0, font, level, gpa ); + } + + /** + * Given a mapped character sequence MCS, obtain glyph position adjustments + * from the font's kerning data. + * @param mcs mapped character sequence + * @param font applicable font + * @return glyph position adjustments (or null if no kerning) + */ + private int[][] getKerningAdjustments ( CharSequence mcs, final Font font ) { + int nc = mcs.length(); + // extract kerning array + int[] ka = new int [ nc ]; // kerning array + for ( int i = 0, n = nc, cPrev = -1; i < n; i++ ) { + int c = mcs.charAt ( i ); + // TODO !BMP + if ( cPrev >= 0 ) { + ka[i] = font.getKernValue ( cPrev, c ); + } + cPrev = c; + } + // was there a non-zero kerning? + boolean hasKerning = false; + for ( int i = 0, n = nc; i < n; i++ ) { + if ( ka[i] != 0 ) { + hasKerning = true; + break; + } + } + // if non-zero kerning, then create and return glyph position adjustment array + if ( hasKerning ) { + int[][] gpa = new int [ nc ] [ 4 ]; + for ( int i = 0, n = nc; i < n; i++ ) { + if ( i > 0 ) { + gpa [ i - 1 ] [ GlyphPositioningTable.Value.IDX_X_ADVANCE ] = ka [ i ]; + } + } + return gpa; + } else { + return null; + } + } + + private AreaInfo processWordNoMapping(int lastIndex, final Font font, AreaInfo prevAreaInfo, + final char breakOpportunityChar, final boolean endsWithHyphen, int level) { boolean kerning = font.hasKerning(); MinOptMax wordIPD = MinOptMax.ZERO; + + if (LOG.isDebugEnabled()) { + LOG.debug ( "PW: [" + thisStart + "," + lastIndex + "]: {" + + " -M" + + ", level = " + level + + " }" ); + } + for (int i = thisStart; i < lastIndex; i++) { char currentChar = foText.charAt(i); - + //character width int charWidth = font.getCharWidth(currentChar); wordIPD = wordIPD.plus(charWidth); @@ -782,14 +1125,12 @@ public class TextLayoutManager extends LeafNodeLayoutManager { } } } - boolean endsWithHyphen = checkEndsWithHyphen - && foText.charAt(lastIndex) == CharUtilities.SOFT_HYPHEN; if (kerning - && breakOpportunity - && !TextLayoutManager.isSpace(ch) + && ( breakOpportunityChar != 0 ) + && !TextLayoutManager.isSpace(breakOpportunityChar) && lastIndex > 0 && endsWithHyphen) { - int kern = font.getKernValue(foText.charAt(lastIndex - 1), ch); + int kern = font.getKernValue(foText.charAt(lastIndex - 1), breakOpportunityChar); if (kern != 0) { addToLetterAdjust(lastIndex, kern); //TODO: add kern to wordIPD? @@ -801,23 +1142,46 @@ public class TextLayoutManager extends LeafNodeLayoutManager { int letterSpaces = 0; if (wordLength != 0) { letterSpaces = wordLength - 1; - // if there is a break opportunity and the next one + // if there is a break opportunity and the next one (break character) // is not a space, it could be used as a line end; // add one more letter space, in case other text follows - if (breakOpportunity && !TextLayoutManager.isSpace(ch)) { - letterSpaces++; + if (( breakOpportunityChar != 0 ) && !TextLayoutManager.isSpace(breakOpportunityChar)) { + letterSpaces++; } } assert letterSpaces >= 0; wordIPD = wordIPD.plus(letterSpaceIPD.mult(letterSpaces)); - // create the AreaInfo object - AreaInfo areaInfo = new AreaInfo(thisStart, lastIndex, 0, - letterSpaces, wordIPD, - endsWithHyphen, - false, breakOpportunity, font); + // create and return the AreaInfo object + return new AreaInfo(thisStart, lastIndex, 0, + letterSpaces, wordIPD, + endsWithHyphen, + false, breakOpportunityChar != 0, font, level, null); + } + + private AreaInfo processWord(final int alignment, final KnuthSequence sequence, + AreaInfo prevAreaInfo, final char ch, final boolean breakOpportunity, + final boolean checkEndsWithHyphen, int level) { + + //Word boundary found, process widths and kerning + int lastIndex = nextStart; + while (lastIndex > 0 && foText.charAt(lastIndex - 1) == CharUtilities.SOFT_HYPHEN) { + lastIndex--; + } + final boolean endsWithHyphen = checkEndsWithHyphen + && foText.charAt(lastIndex) == CharUtilities.SOFT_HYPHEN; + Font font = FontSelector.selectFontForCharactersInText + ( foText, thisStart, lastIndex, foText, this ); + AreaInfo areaInfo; + if ( font.performsSubstitution() || font.performsPositioning() ) { + areaInfo = processWordMapping + ( lastIndex, font, prevAreaInfo, breakOpportunity ? ch : 0, endsWithHyphen, level ); + } else { + areaInfo = processWordNoMapping + ( lastIndex, font, prevAreaInfo, breakOpportunity ? ch : 0, endsWithHyphen, level ); + } prevAreaInfo = areaInfo; - areaInfos.add(areaInfo); + addAreaInfo(areaInfo); tempStart = nextStart; //add the elements @@ -902,22 +1266,22 @@ public class TextLayoutManager extends LeafNodeLayoutManager { newIPD = newIPD.plus(font.getCharWidth(ch)); //if (i > startIndex) { if (i < stopIndex) { - MinOptMax letterAdjust = letterAdjustArray[i + 1]; + MinOptMax letterSpaceAdjust = letterSpaceAdjustArray[i + 1]; if (i == stopIndex - 1 && hyphenFollows) { //the letter adjust here needs to be handled further down during //element generation because it depends on hyph/no-hyph condition - letterAdjust = null; + letterSpaceAdjust = null; } - if (letterAdjust != null) { - newIPD = newIPD.plus(letterAdjust); + if (letterSpaceAdjust != null) { + newIPD = newIPD.plus(letterSpaceAdjust); } } } // add letter spaces boolean isWordEnd - = stopIndex == areaInfo.breakIndex - && areaInfo.letterSpaceCount < areaInfo.getCharLength(); + = (stopIndex == areaInfo.breakIndex) + && (areaInfo.letterSpaceCount < areaInfo.getWordLength()); int letterSpaceCount = isWordEnd ? stopIndex - startIndex - 1 : stopIndex - startIndex; assert letterSpaceCount >= 0; @@ -925,8 +1289,11 @@ public class TextLayoutManager extends LeafNodeLayoutManager { if (!(nothingChanged && stopIndex == areaInfo.breakIndex && !hyphenFollows)) { // the new AreaInfo object is not equal to the old one - changeList.add(new PendingChange(new AreaInfo(startIndex, stopIndex, 0, - letterSpaceCount, newIPD, hyphenFollows, false, false, font), + changeList.add + ( new PendingChange + ( new AreaInfo(startIndex, stopIndex, 0, + letterSpaceCount, newIPD, hyphenFollows, + false, false, font, -1, null), ((LeafPosition) pos).getLeafPos() + changeOffset)); nothingChanged = false; } @@ -992,9 +1359,9 @@ public class TextLayoutManager extends LeafNodeLayoutManager { areaInfosAdded++; oldIndex = currChange.index; changeIndex = currChange.index + areaInfosAdded - areaInfosRemoved; - areaInfos.remove(changeIndex); + removeAreaInfo(changeIndex); } - areaInfos.add(changeIndex, currChange.areaInfo); + addAreaInfo(changeIndex, currChange.areaInfo); } changeList.clear(); } @@ -1035,7 +1402,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { int leafValue = ((LeafPosition) pos).getLeafPos() + changeOffset; if (leafValue != -1) { AreaInfo areaInfo = getAreaInfo(leafValue); - StringBuffer buffer = new StringBuffer(areaInfo.getCharLength()); + StringBuffer buffer = new StringBuffer(areaInfo.getWordLength()); for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) { buffer.append(foText.charAt(i)); } @@ -1209,7 +1576,7 @@ public class TextLayoutManager extends LeafNodeLayoutManager { MinOptMax widthIfNoBreakOccurs = null; if (areaInfo.breakIndex < foText.length()) { //Add in kerning in no-break condition - widthIfNoBreakOccurs = letterAdjustArray[areaInfo.breakIndex]; + widthIfNoBreakOccurs = letterSpaceAdjustArray[areaInfo.breakIndex]; } //if (areaInfo.breakIndex) @@ -1325,4 +1692,14 @@ public class TextLayoutManager extends LeafNodeLayoutManager { } + /** {@inheritDoc} */ + public String toString() { + return super.toString() + "{" + + "chars = \'" + + CharUtilities.toNCRefs ( foText.getCharSequence().toString() ) + + "\'" + + ", len = " + foText.length() + + "}"; + } + } diff --git a/src/java/org/apache/fop/layoutmgr/list/ListBlockLayoutManager.java b/src/java/org/apache/fop/layoutmgr/list/ListBlockLayoutManager.java index 5772c33ff..5bd451ffb 100644 --- a/src/java/org/apache/fop/layoutmgr/list/ListBlockLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/list/ListBlockLayoutManager.java @@ -120,8 +120,8 @@ public class ListBlockLayoutManager extends BlockStackingLayoutManager } /** - * The table area is a reference area that contains areas for - * columns, bodies, rows and the contents are in cells. + * A list block generates one or more normal block areas whose child areas are + * normal block areas returned by the children of fo:list-block. See XSL-FO 1.1 6.8.2. * * @param parentIter the position iterator * @param layoutContext the layout context for adding areas diff --git a/src/java/org/apache/fop/layoutmgr/list/ListItemContentLayoutManager.java b/src/java/org/apache/fop/layoutmgr/list/ListItemContentLayoutManager.java index 8ef0f62cf..72e4e227a 100644 --- a/src/java/org/apache/fop/layoutmgr/list/ListItemContentLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/list/ListItemContentLayoutManager.java @@ -45,7 +45,7 @@ public class ListItemContentLayoutManager extends BlockStackingLayoutManager { private Block curBlockArea; - private int xoffset; + private int xOffset; private int itemIPD; /** @@ -80,7 +80,7 @@ public class ListItemContentLayoutManager extends BlockStackingLayoutManager { * @param off the x offset */ public void setXOffset(int off) { - xoffset = off; + xOffset = off; } /** @@ -175,7 +175,7 @@ public class ListItemContentLayoutManager extends BlockStackingLayoutManager { curBlockArea = new Block(); curBlockArea.setPositioning(Block.ABSOLUTE); // set position - curBlockArea.setXOffset(xoffset); + curBlockArea.setXOffset(xOffset); //TODO: Check - itemIPD never set? curBlockArea.setIPD(itemIPD); //curBlockArea.setHeight(); diff --git a/src/java/org/apache/fop/layoutmgr/list/ListItemLayoutManager.java b/src/java/org/apache/fop/layoutmgr/list/ListItemLayoutManager.java index 8b9282aea..bc7daa24f 100644 --- a/src/java/org/apache/fop/layoutmgr/list/ListItemLayoutManager.java +++ b/src/java/org/apache/fop/layoutmgr/list/ListItemLayoutManager.java @@ -465,7 +465,6 @@ public class ListItemLayoutManager extends BlockStackingLayoutManager /** * Add the areas for the break points. - * This sets the offset of each cell as it is added. * * @param parentIter the position iterator * @param layoutContext the layout context for adding areas @@ -507,12 +506,12 @@ public class ListItemLayoutManager extends BlockStackingLayoutManager int bodyFirstIndex = ((ListItemPosition) positionList.getFirst()).getBodyFirstIndex(); int bodyLastIndex = ((ListItemPosition) positionList.getLast()).getBodyLastIndex(); - //Determine previous break if any + //Determine previous break if any (in item label list) int previousBreak = ElementListUtils.determinePreviousBreak(labelList, labelFirstIndex); SpaceResolver.performConditionalsNotification(labelList, labelFirstIndex, labelLastIndex, previousBreak); - //Determine previous break if any + //Determine previous break if any (in item body list) previousBreak = ElementListUtils.determinePreviousBreak(bodyList, bodyFirstIndex); SpaceResolver.performConditionalsNotification(bodyList, bodyFirstIndex, bodyLastIndex, previousBreak); diff --git a/src/java/org/apache/fop/layoutmgr/table/ColumnSetup.java b/src/java/org/apache/fop/layoutmgr/table/ColumnSetup.java index c1fc19050..aaccbd0d3 100644 --- a/src/java/org/apache/fop/layoutmgr/table/ColumnSetup.java +++ b/src/java/org/apache/fop/layoutmgr/table/ColumnSetup.java @@ -33,6 +33,9 @@ import org.apache.fop.fo.expr.RelativeNumericProperty; import org.apache.fop.fo.flow.table.Table; import org.apache.fop.fo.flow.table.TableColumn; import org.apache.fop.fo.properties.TableColLength; +import org.apache.fop.traits.Direction; +import org.apache.fop.traits.WritingModeTraits; +import org.apache.fop.traits.WritingModeTraitsGetter; /** * Class holding a number of columns making up the column setup of a row. @@ -43,6 +46,7 @@ public class ColumnSetup { private static Log log = LogFactory.getLog(ColumnSetup.class); private Table table; + private WritingModeTraitsGetter wmTraits; private List columns = new java.util.ArrayList(); private List colWidths = new java.util.ArrayList(); @@ -53,7 +57,9 @@ public class ColumnSetup { * @param table the table to construct this column setup for */ public ColumnSetup(Table table) { + assert table != null; this.table = table; + this.wmTraits = WritingModeTraits.getWritingModeTraitsGetter ( table ); prepareColumns(); initializeColumnWidths(); } @@ -232,11 +238,47 @@ public class ColumnSetup { } /** + * Determine the X offset of the indicated column, where this + * offset denotes the left edge of the column irrespective of writing + * mode. If writing mode's column progression direction is right-to-left, + * then the first column is the right-most column and the last column is + * the left-most column; otherwise, the first column is the left-most + * column. * @param col column index (1 is first column) * @param context the context for percentage based calculations * @return the X offset of the requested column */ public int getXOffset(int col, PercentBaseContext context) { + // TODO handle vertical WMs [GA] + if ( (wmTraits != null) && (wmTraits.getColumnProgressionDirection() == Direction.RL) ) { + return getXOffsetRTL(col, context); + } else { + return getXOffsetLTR(col, context); + } + } + + /* + * Determine x offset by summing widths of columns to left of specified + * column; i.e., those columns whose column numbers are greater than the + * specified column number. + */ + private int getXOffsetRTL(int col, PercentBaseContext context) { + int xoffset = 0; + for (int i = col, nc = colWidths.size(); ++i < nc;) { + int effCol = i; + if (colWidths.get(effCol) != null) { + xoffset += ((Length) colWidths.get(effCol)).getValue(context); + } + } + return xoffset; + } + + /* + * Determine x offset by summing widths of columns to left of specified + * column; i.e., those columns whose column numbers are less than the + * specified column number. + */ + private int getXOffsetLTR(int col, PercentBaseContext context) { int xoffset = 0; for (int i = col; --i >= 0;) { int effCol; diff --git a/src/java/org/apache/fop/pdf/PDFTextUtil.java b/src/java/org/apache/fop/pdf/PDFTextUtil.java index 3768bb6ce..ed475fcb8 100644 --- a/src/java/org/apache/fop/pdf/PDFTextUtil.java +++ b/src/java/org/apache/fop/pdf/PDFTextUtil.java @@ -81,8 +81,8 @@ public abstract class PDFTextUtil { sb.append(PDFNumber.doubleOut(lt[5], DEC)); } - private void writeChar(char ch, StringBuffer sb) { - if (!useMultiByte) { + private static void writeChar(char ch, StringBuffer sb, boolean multibyte) { + if (!multibyte) { if (ch < 32 || ch > 127) { sb.append("\\").append(Integer.toOctalString(ch)); } else { @@ -101,6 +101,10 @@ public abstract class PDFTextUtil { } } + private void writeChar(char ch, StringBuffer sb) { + writeChar ( ch, sb, useMultiByte ); + } + private void checkInTextObject() { if (!inTextObject) { throw new IllegalStateException("Not in text object"); @@ -245,24 +249,38 @@ public abstract class PDFTextUtil { bufTJ = new StringBuffer(); } if (bufTJ.length() == 0) { - bufTJ.append("[").append(startText); + bufTJ.append("["); + bufTJ.append(startText); } writeChar(codepoint, bufTJ); } /** * Writes a glyph adjust value to the "TJ-Buffer". + + * <p>Assumes the following:</p> + * <ol> + * <li>if buffer is currently empty, then this is the start of the array object + * that encodes the adjustment and character values, and, therfore, a LEFT + * SQUARE BRACKET '[' must be prepended; and + * </li> + * <li>otherwise (the buffer is + * not empty), then the last element written to the buffer was a mapped + * character, and, therefore, a terminating '>' or ')' followed by a space + * must be appended to the buffer prior to appending the adjustment value. + * </li> + * </ol> * @param adjust the glyph adjust value in thousands of text unit space. */ public void adjustGlyphTJ(double adjust) { if (bufTJ == null) { bufTJ = new StringBuffer(); } - if (bufTJ.length() > 0) { - bufTJ.append(endText).append(" "); - } if (bufTJ.length() == 0) { bufTJ.append("["); + } else { + bufTJ.append(endText); + bufTJ.append(" "); } bufTJ.append(PDFNumber.doubleOut(adjust, DEC - 4)); bufTJ.append(" "); @@ -285,4 +303,31 @@ public abstract class PDFTextUtil { return bufTJ != null && bufTJ.length() > 0; } + /** + * Writes a "Td" command with specified x and y coordinates. + * @param x coordinate + * @param y coordinate + */ + public void writeTd ( double x, double y ) { + StringBuffer sb = new StringBuffer(); + sb.append(PDFNumber.doubleOut(x, DEC)); + sb.append(' '); + sb.append(PDFNumber.doubleOut(y, DEC)); + sb.append ( " Td\n" ); + write ( sb.toString() ); + } + + /** + * Writes a "Tj" command with specified character code. + * @param ch character code to write + */ + public void writeTj ( char ch ) { + StringBuffer sb = new StringBuffer(); + sb.append ( '<' ); + writeChar ( ch, sb, true ); + sb.append ( '>' ); + sb.append ( " Tj\n" ); + write ( sb.toString() ); + } + } diff --git a/src/java/org/apache/fop/render/AbstractPathOrientedRenderer.java b/src/java/org/apache/fop/render/AbstractPathOrientedRenderer.java index 333248f4d..7930e62ab 100644 --- a/src/java/org/apache/fop/render/AbstractPathOrientedRenderer.java +++ b/src/java/org/apache/fop/render/AbstractPathOrientedRenderer.java @@ -73,29 +73,31 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { * @param block the block to render the traits */ protected void handleBlockTraits(Block block) { - int borderPaddingStart = block.getBorderAndPaddingWidthStart(); - int borderPaddingBefore = block.getBorderAndPaddingWidthBefore(); + float borderPaddingStart = block.getBorderAndPaddingWidthStart() / 1000f; + float borderPaddingEnd = block.getBorderAndPaddingWidthEnd() / 1000f; + float borderPaddingBefore = block.getBorderAndPaddingWidthBefore() / 1000f; + float borderPaddingAfter = block.getBorderAndPaddingWidthAfter() / 1000f; float startx = currentIPPosition / 1000f; float starty = currentBPPosition / 1000f; float width = block.getIPD() / 1000f; float height = block.getBPD() / 1000f; - /* using start-indent now - Integer spaceStart = (Integer) block.getTrait(Trait.SPACE_START); - if (spaceStart != null) { - startx += spaceStart.floatValue() / 1000f; - }*/ - startx += block.getStartIndent() / 1000f; - startx -= block.getBorderAndPaddingWidthStart() / 1000f; - - width += borderPaddingStart / 1000f; - width += block.getBorderAndPaddingWidthEnd() / 1000f; - height += borderPaddingBefore / 1000f; - height += block.getBorderAndPaddingWidthAfter() / 1000f; - - drawBackAndBorders(block, startx, starty, - width, height); + int level = block.getBidiLevel(); + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + startx += block.getStartIndent() / 1000f; + startx -= borderPaddingStart; + } else { + startx += block.getEndIndent() / 1000f; + startx -= borderPaddingEnd; + } + + width += borderPaddingStart; + width += borderPaddingEnd; + height += borderPaddingBefore; + height += borderPaddingAfter; + + drawBackAndBorders(block, startx, starty, width, height); } /** @@ -114,7 +116,12 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { // adjust the current position according to region borders and padding currentBPPosition = referenceArea.getBorderAndPaddingWidthBefore(); - currentIPPosition = referenceArea.getBorderAndPaddingWidthStart(); + int level = region.getBidiLevel(); + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + currentIPPosition = referenceArea.getBorderAndPaddingWidthStart(); + } else { + currentIPPosition = referenceArea.getBorderAndPaddingWidthEnd(); + } // draw background (traits are in the RegionViewport) // and borders (traits are in the RegionReference) drawBackAndBorders(region, referenceArea, startx, starty, width, height); @@ -161,9 +168,9 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { drawBackground(startx, starty, width, height, (Trait.Background) backgroundArea.getTrait(Trait.BACKGROUND), - bpsBefore, bpsAfter, bpsStart, bpsEnd); + bpsBefore, bpsAfter, bpsStart, bpsEnd, backgroundArea.getBidiLevel()); drawBorders(startx, starty, width, height, - bpsBefore, bpsAfter, bpsStart, bpsEnd); + bpsBefore, bpsAfter, bpsStart, bpsEnd, borderArea.getBidiLevel()); } /** @@ -179,11 +186,44 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { * @param bpsAfter the border-after traits * @param bpsStart the border-start traits * @param bpsEnd the border-end traits + * @param level of bidirectional embedding */ protected void drawBackground( // CSOK: ParameterNumber float startx, float starty, float width, float height, Trait.Background back, BorderProps bpsBefore, BorderProps bpsAfter, - BorderProps bpsStart, BorderProps bpsEnd) { + BorderProps bpsStart, BorderProps bpsEnd, int level) { + BorderProps bpsTop = bpsBefore; + BorderProps bpsBottom = bpsAfter; + BorderProps bpsLeft; + BorderProps bpsRight; + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + bpsLeft = bpsStart; + bpsRight = bpsEnd; + } else { + bpsLeft = bpsEnd; + bpsRight = bpsStart; + } + drawBackground(startx, starty, width, height, back, bpsTop, bpsBottom, bpsLeft, bpsRight); + } + + /** + * Draw the background. + * This draws the background given the position and the traits. + * + * @param startx the start x position + * @param starty the start y position + * @param width the width of the area + * @param height the height of the area + * @param back the background traits + * @param bpsTop the border specification on the top edge + * @param bpsBottom the border traits associated with bottom edge + * @param bpsLeft the border specification on the left edge + * @param bpsRight the border specification on the right edge + */ + protected void drawBackground( // CSOK: ParameterNumber + float startx, float starty, float width, float height, Trait.Background back, + BorderProps bpsTop, BorderProps bpsBottom, + BorderProps bpsLeft, BorderProps bpsRight) { if (back != null) { endTextObject(); @@ -192,19 +232,19 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { float sy = starty; float paddRectWidth = width; float paddRectHeight = height; - if (bpsStart != null) { - sx += bpsStart.width / 1000f; - paddRectWidth -= bpsStart.width / 1000f; + if (bpsLeft != null) { + sx += bpsLeft.width / 1000f; + paddRectWidth -= bpsLeft.width / 1000f; } - if (bpsBefore != null) { - sy += bpsBefore.width / 1000f; - paddRectHeight -= bpsBefore.width / 1000f; + if (bpsTop != null) { + sy += bpsTop.width / 1000f; + paddRectHeight -= bpsTop.width / 1000f; } - if (bpsEnd != null) { - paddRectWidth -= bpsEnd.width / 1000f; + if (bpsRight != null) { + paddRectWidth -= bpsRight.width / 1000f; } - if (bpsAfter != null) { - paddRectHeight -= bpsAfter.width / 1000f; + if (bpsBottom != null) { + paddRectHeight -= bpsBottom.width / 1000f; } if (back.getColor() != null) { @@ -264,86 +304,99 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { * @param starty the start y position * @param width the width of the area * @param height the height of the area - * @param bpsBefore the border-before traits - * @param bpsAfter the border-after traits - * @param bpsStart the border-start traits - * @param bpsEnd the border-end traits + * @param bpsBefore the border traits associated with before edge + * @param bpsAfter the border traits associated with after edge + * @param bpsStart the border traits associated with start edge + * @param bpsEnd the border traits associated with end edge + * @param level of bidirectional embedding */ protected void drawBorders( // CSOK: ParameterNumber float startx, float starty, float width, float height, BorderProps bpsBefore, BorderProps bpsAfter, - BorderProps bpsStart, BorderProps bpsEnd) { + BorderProps bpsStart, BorderProps bpsEnd, int level) { Rectangle2D.Float borderRect = new Rectangle2D.Float(startx, starty, width, height); - drawBorders(borderRect, bpsBefore, bpsAfter, bpsStart, bpsEnd); + BorderProps bpsTop = bpsBefore; + BorderProps bpsBottom = bpsAfter; + BorderProps bpsLeft; + BorderProps bpsRight; + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + bpsLeft = bpsStart; + bpsRight = bpsEnd; + } else { + bpsLeft = bpsEnd; + bpsRight = bpsStart; + } + drawBorders(borderRect, bpsTop, bpsBottom, bpsLeft, bpsRight); } - private static final int BEFORE = 0; - private static final int END = 1; - private static final int AFTER = 2; - private static final int START = 3; + private static final int TOP = 0; + private static final int RIGHT = 1; + private static final int BOTTOM = 2; + private static final int LEFT = 3; /** * Draws borders. * @param borderRect the border rectangle - * @param bpsBefore the border specification on the before side - * @param bpsAfter the border specification on the after side - * @param bpsStart the border specification on the start side - * @param bpsEnd the border specification on the end side + * @param bpsTop the border specification on the top edge + * @param bpsBottom the border traits associated with bottom edge + * @param bpsLeft the border specification on the left edge + * @param bpsRight the border specification on the right edge */ protected void drawBorders( // CSOK: MethodLength Rectangle2D.Float borderRect, - BorderProps bpsBefore, BorderProps bpsAfter, BorderProps bpsStart, BorderProps bpsEnd) { + BorderProps bpsTop, BorderProps bpsBottom, BorderProps bpsLeft, BorderProps bpsRight) { //TODO generalize each of the four conditions into using a parameterized drawBorder() boolean[] border = new boolean[] { - (bpsBefore != null), (bpsEnd != null), - (bpsAfter != null), (bpsStart != null)}; + (bpsTop != null), (bpsRight != null), + (bpsBottom != null), (bpsLeft != null)}; float startx = borderRect.x; float starty = borderRect.y; float width = borderRect.width; float height = borderRect.height; float[] borderWidth = new float[] { - (border[BEFORE] ? bpsBefore.width / 1000f : 0.0f), - (border[END] ? bpsEnd.width / 1000f : 0.0f), - (border[AFTER] ? bpsAfter.width / 1000f : 0.0f), - (border[START] ? bpsStart.width / 1000f : 0.0f)}; + (border[TOP] ? bpsTop.width / 1000f : 0.0f), + (border[RIGHT] ? bpsRight.width / 1000f : 0.0f), + (border[BOTTOM] ? bpsBottom.width / 1000f : 0.0f), + (border[LEFT] ? bpsLeft.width / 1000f : 0.0f)}; float[] clipw = new float[] { - BorderProps.getClippedWidth(bpsBefore) / 1000f, - BorderProps.getClippedWidth(bpsEnd) / 1000f, - BorderProps.getClippedWidth(bpsAfter) / 1000f, - BorderProps.getClippedWidth(bpsStart) / 1000f}; - starty += clipw[BEFORE]; - height -= clipw[BEFORE]; - height -= clipw[AFTER]; - startx += clipw[START]; - width -= clipw[START]; - width -= clipw[END]; + BorderProps.getClippedWidth(bpsTop) / 1000f, + BorderProps.getClippedWidth(bpsRight) / 1000f, + BorderProps.getClippedWidth(bpsBottom) / 1000f, + BorderProps.getClippedWidth(bpsLeft) / 1000f}; + + starty += clipw[TOP]; + height -= clipw[TOP]; + height -= clipw[BOTTOM]; + startx += clipw[LEFT]; + width -= clipw[LEFT]; + width -= clipw[RIGHT]; boolean[] slant = new boolean[] { - (border[START] && border[BEFORE]), - (border[BEFORE] && border[END]), - (border[END] && border[AFTER]), - (border[AFTER] && border[START])}; - if (bpsBefore != null) { + (border[LEFT] && border[TOP]), + (border[TOP] && border[RIGHT]), + (border[RIGHT] && border[BOTTOM]), + (border[BOTTOM] && border[LEFT])}; + if (bpsTop != null) { endTextObject(); float sx1 = startx; - float sx2 = (slant[BEFORE] ? sx1 + borderWidth[START] - clipw[START] : sx1); + float sx2 = (slant[TOP] ? sx1 + borderWidth[LEFT] - clipw[LEFT] : sx1); float ex1 = startx + width; - float ex2 = (slant[END] ? ex1 - borderWidth[END] + clipw[END] : ex1); - float outery = starty - clipw[BEFORE]; - float clipy = outery + clipw[BEFORE]; - float innery = outery + borderWidth[BEFORE]; + float ex2 = (slant[RIGHT] ? ex1 - borderWidth[RIGHT] + clipw[RIGHT] : ex1); + float outery = starty - clipw[TOP]; + float clipy = outery + clipw[TOP]; + float innery = outery + borderWidth[TOP]; saveGraphicsState(); moveTo(sx1, clipy); float sx1a = sx1; float ex1a = ex1; - if (bpsBefore.mode == BorderProps.COLLAPSE_OUTER) { - if (bpsStart != null && bpsStart.mode == BorderProps.COLLAPSE_OUTER) { - sx1a -= clipw[START]; + if (bpsTop.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsLeft != null && bpsLeft.mode == BorderProps.COLLAPSE_OUTER) { + sx1a -= clipw[LEFT]; } - if (bpsEnd != null && bpsEnd.mode == BorderProps.COLLAPSE_OUTER) { - ex1a += clipw[END]; + if (bpsRight != null && bpsRight.mode == BorderProps.COLLAPSE_OUTER) { + ex1a += clipw[RIGHT]; } lineTo(sx1a, outery); lineTo(ex1a, outery); @@ -354,30 +407,30 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { closePath(); clip(); drawBorderLine(sx1a, outery, ex1a, innery, true, true, - bpsBefore.style, bpsBefore.color); + bpsTop.style, bpsTop.color); restoreGraphicsState(); } - if (bpsEnd != null) { + if (bpsRight != null) { endTextObject(); float sy1 = starty; - float sy2 = (slant[END] ? sy1 + borderWidth[BEFORE] - clipw[BEFORE] : sy1); + float sy2 = (slant[RIGHT] ? sy1 + borderWidth[TOP] - clipw[TOP] : sy1); float ey1 = starty + height; - float ey2 = (slant[AFTER] ? ey1 - borderWidth[AFTER] + clipw[AFTER] : ey1); - float outerx = startx + width + clipw[END]; - float clipx = outerx - clipw[END]; - float innerx = outerx - borderWidth[END]; + float ey2 = (slant[BOTTOM] ? ey1 - borderWidth[BOTTOM] + clipw[BOTTOM] : ey1); + float outerx = startx + width + clipw[RIGHT]; + float clipx = outerx - clipw[RIGHT]; + float innerx = outerx - borderWidth[RIGHT]; saveGraphicsState(); moveTo(clipx, sy1); float sy1a = sy1; float ey1a = ey1; - if (bpsEnd.mode == BorderProps.COLLAPSE_OUTER) { - if (bpsBefore != null && bpsBefore.mode == BorderProps.COLLAPSE_OUTER) { - sy1a -= clipw[BEFORE]; + if (bpsRight.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsTop != null && bpsTop.mode == BorderProps.COLLAPSE_OUTER) { + sy1a -= clipw[TOP]; } - if (bpsAfter != null && bpsAfter.mode == BorderProps.COLLAPSE_OUTER) { - ey1a += clipw[AFTER]; + if (bpsBottom != null && bpsBottom.mode == BorderProps.COLLAPSE_OUTER) { + ey1a += clipw[BOTTOM]; } lineTo(outerx, sy1a); lineTo(outerx, ey1a); @@ -387,30 +440,31 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { lineTo(innerx, sy2); closePath(); clip(); - drawBorderLine(innerx, sy1a, outerx, ey1a, false, false, bpsEnd.style, bpsEnd.color); + drawBorderLine(innerx, sy1a, outerx, ey1a, false, false, + bpsRight.style, bpsRight.color); restoreGraphicsState(); } - if (bpsAfter != null) { + if (bpsBottom != null) { endTextObject(); float sx1 = startx; - float sx2 = (slant[START] ? sx1 + borderWidth[START] - clipw[START] : sx1); + float sx2 = (slant[LEFT] ? sx1 + borderWidth[LEFT] - clipw[LEFT] : sx1); float ex1 = startx + width; - float ex2 = (slant[AFTER] ? ex1 - borderWidth[END] + clipw[END] : ex1); - float outery = starty + height + clipw[AFTER]; - float clipy = outery - clipw[AFTER]; - float innery = outery - borderWidth[AFTER]; + float ex2 = (slant[BOTTOM] ? ex1 - borderWidth[RIGHT] + clipw[RIGHT] : ex1); + float outery = starty + height + clipw[BOTTOM]; + float clipy = outery - clipw[BOTTOM]; + float innery = outery - borderWidth[BOTTOM]; saveGraphicsState(); moveTo(ex1, clipy); float sx1a = sx1; float ex1a = ex1; - if (bpsAfter.mode == BorderProps.COLLAPSE_OUTER) { - if (bpsStart != null && bpsStart.mode == BorderProps.COLLAPSE_OUTER) { - sx1a -= clipw[START]; + if (bpsBottom.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsLeft != null && bpsLeft.mode == BorderProps.COLLAPSE_OUTER) { + sx1a -= clipw[LEFT]; } - if (bpsEnd != null && bpsEnd.mode == BorderProps.COLLAPSE_OUTER) { - ex1a += clipw[END]; + if (bpsRight != null && bpsRight.mode == BorderProps.COLLAPSE_OUTER) { + ex1a += clipw[RIGHT]; } lineTo(ex1a, outery); lineTo(sx1a, outery); @@ -420,30 +474,31 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { lineTo(ex2, innery); closePath(); clip(); - drawBorderLine(sx1a, innery, ex1a, outery, true, false, bpsAfter.style, bpsAfter.color); + drawBorderLine(sx1a, innery, ex1a, outery, true, false, + bpsBottom.style, bpsBottom.color); restoreGraphicsState(); } - if (bpsStart != null) { + if (bpsLeft != null) { endTextObject(); float sy1 = starty; - float sy2 = (slant[BEFORE] ? sy1 + borderWidth[BEFORE] - clipw[BEFORE] : sy1); + float sy2 = (slant[TOP] ? sy1 + borderWidth[TOP] - clipw[TOP] : sy1); float ey1 = sy1 + height; - float ey2 = (slant[START] ? ey1 - borderWidth[AFTER] + clipw[AFTER] : ey1); - float outerx = startx - clipw[START]; - float clipx = outerx + clipw[START]; - float innerx = outerx + borderWidth[START]; + float ey2 = (slant[LEFT] ? ey1 - borderWidth[BOTTOM] + clipw[BOTTOM] : ey1); + float outerx = startx - clipw[LEFT]; + float clipx = outerx + clipw[LEFT]; + float innerx = outerx + borderWidth[LEFT]; saveGraphicsState(); moveTo(clipx, ey1); float sy1a = sy1; float ey1a = ey1; - if (bpsStart.mode == BorderProps.COLLAPSE_OUTER) { - if (bpsBefore != null && bpsBefore.mode == BorderProps.COLLAPSE_OUTER) { - sy1a -= clipw[BEFORE]; + if (bpsLeft.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsTop != null && bpsTop.mode == BorderProps.COLLAPSE_OUTER) { + sy1a -= clipw[TOP]; } - if (bpsAfter != null && bpsAfter.mode == BorderProps.COLLAPSE_OUTER) { - ey1a += clipw[AFTER]; + if (bpsBottom != null && bpsBottom.mode == BorderProps.COLLAPSE_OUTER) { + ey1a += clipw[BOTTOM]; } lineTo(outerx, ey1a); lineTo(outerx, sy1a); @@ -453,7 +508,7 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { lineTo(innerx, ey2); closePath(); clip(); - drawBorderLine(outerx, sy1a, innerx, ey1a, false, true, bpsStart.style, bpsStart.color); + drawBorderLine(outerx, sy1a, innerx, ey1a, false, true, bpsLeft.style, bpsLeft.color); restoreGraphicsState(); } } @@ -466,16 +521,16 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { */ protected void renderInlineAreaBackAndBorders(InlineArea area) { float borderPaddingStart = area.getBorderAndPaddingWidthStart() / 1000f; + float borderPaddingEnd = area.getBorderAndPaddingWidthEnd() / 1000f; float borderPaddingBefore = area.getBorderAndPaddingWidthBefore() / 1000f; - float bpwidth = borderPaddingStart - + (area.getBorderAndPaddingWidthEnd() / 1000f); - float bpheight = borderPaddingBefore - + (area.getBorderAndPaddingWidthAfter() / 1000f); + float borderPaddingAfter = area.getBorderAndPaddingWidthAfter() / 1000f; + float bpwidth = borderPaddingStart + borderPaddingEnd; + float bpheight = borderPaddingBefore + borderPaddingAfter; float height = area.getBPD() / 1000f; if (height != 0.0f || bpheight != 0.0f && bpwidth != 0.0f) { float x = currentIPPosition / 1000f; - float y = (currentBPPosition + area.getOffset()) / 1000f; + float y = (currentBPPosition + area.getBlockProgressionOffset()) / 1000f; float width = area.getIPD() / 1000f; drawBackAndBorders(area, x, y - borderPaddingBefore , width + bpwidth @@ -512,10 +567,16 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { AffineTransform positionTransform = new AffineTransform(); positionTransform.translate(bv.getXOffset(), bv.getYOffset()); + int level = bv.getBidiLevel(); int borderPaddingStart = bv.getBorderAndPaddingWidthStart(); + int borderPaddingEnd = bv.getBorderAndPaddingWidthEnd(); //"left/"top" (bv.getX/YOffset()) specify the position of the content rectangle - positionTransform.translate(-borderPaddingStart, -borderPaddingBefore); + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + positionTransform.translate(-borderPaddingStart, -borderPaddingBefore); + } else { + positionTransform.translate(-borderPaddingEnd, -borderPaddingBefore); + } //Free transformation for the block-container viewport String transf; @@ -536,14 +597,18 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { //Background and borders float borderPaddingWidth - = (borderPaddingStart + bv.getBorderAndPaddingWidthEnd()) / 1000f; + = (borderPaddingStart + borderPaddingEnd) / 1000f; float borderPaddingHeight = (borderPaddingBefore + bv.getBorderAndPaddingWidthAfter()) / 1000f; drawBackAndBorders(bv, 0, 0, width + borderPaddingWidth, height + borderPaddingHeight); //Shift to content rectangle after border painting AffineTransform contentRectTransform = new AffineTransform(); - contentRectTransform.translate(borderPaddingStart, borderPaddingBefore); + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + contentRectTransform.translate(borderPaddingStart, borderPaddingBefore); + } else { + contentRectTransform.translate(borderPaddingEnd, borderPaddingBefore); + } if (!contentRectTransform.isIdentity()) { establishTransformationMatrix(contentRectTransform); @@ -694,25 +759,28 @@ public abstract class AbstractPathOrientedRenderer extends PrintRenderer { * @param viewport the viewport to handle */ public void renderInlineViewport(InlineViewport viewport) { - + int level = viewport.getBidiLevel(); float x = currentIPPosition / 1000f; - float y = (currentBPPosition + viewport.getOffset()) / 1000f; + float y = (currentBPPosition + viewport.getBlockProgressionOffset()) / 1000f; float width = viewport.getIPD() / 1000f; float height = viewport.getBPD() / 1000f; // TODO: Calculate the border rect correctly. float borderPaddingStart = viewport.getBorderAndPaddingWidthStart() / 1000f; + float borderPaddingEnd = viewport.getBorderAndPaddingWidthEnd() / 1000f; float borderPaddingBefore = viewport.getBorderAndPaddingWidthBefore() / 1000f; - float bpwidth = borderPaddingStart - + (viewport.getBorderAndPaddingWidthEnd() / 1000f); - float bpheight = borderPaddingBefore - + (viewport.getBorderAndPaddingWidthAfter() / 1000f); + float borderPaddingAfter = viewport.getBorderAndPaddingWidthAfter() / 1000f; + float bpwidth = borderPaddingStart + borderPaddingEnd; + float bpheight = borderPaddingBefore + borderPaddingAfter; drawBackAndBorders(viewport, x, y, width + bpwidth, height + bpheight); if (viewport.hasClip()) { saveGraphicsState(); - - clipRect(x + borderPaddingStart, y + borderPaddingBefore, width, height); + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + clipRect(x + borderPaddingStart, y + borderPaddingBefore, width, height); + } else { + clipRect(x + borderPaddingEnd, y + borderPaddingBefore, width, height); + } } super.renderInlineViewport(viewport); diff --git a/src/java/org/apache/fop/render/AbstractRenderer.java b/src/java/org/apache/fop/render/AbstractRenderer.java index 3c07c0006..639f8590b 100644 --- a/src/java/org/apache/fop/render/AbstractRenderer.java +++ b/src/java/org/apache/fop/render/AbstractRenderer.java @@ -57,6 +57,7 @@ import org.apache.fop.area.RegionViewport; import org.apache.fop.area.Span; import org.apache.fop.area.Trait; import org.apache.fop.area.inline.Container; +import org.apache.fop.area.inline.FilledArea; import org.apache.fop.area.inline.ForeignObject; import org.apache.fop.area.inline.Image; import org.apache.fop.area.inline.InlineArea; @@ -402,22 +403,34 @@ public abstract class AbstractRenderer * @param mr The main reference area */ protected void renderMainReference(MainReference mr) { - int saveIPPos = currentIPPosition; - Span span = null; List spans = mr.getSpans(); int saveBPPos = currentBPPosition; int saveSpanBPPos = saveBPPos; + int saveIPPos = currentIPPosition; for (int count = 0; count < spans.size(); count++) { span = (Span) spans.get(count); + int level = span.getBidiLevel(); + if ( level < 0 ) { + level = 0; + } + if ( ( level & 1 ) == 1 ) { + currentIPPosition += span.getIPD(); + currentIPPosition += mr.getColumnGap(); + } for (int c = 0; c < span.getColumnCount(); c++) { NormalFlow flow = span.getNormalFlow(c); - if (flow != null) { currentBPPosition = saveSpanBPPos; + if ( ( level & 1 ) == 1 ) { + currentIPPosition -= flow.getIPD(); + currentIPPosition -= mr.getColumnGap(); + } renderFlow(flow); - currentIPPosition += flow.getIPD(); - currentIPPosition += mr.getColumnGap(); + if ( ( level & 1 ) == 0 ) { + currentIPPosition += flow.getIPD(); + currentIPPosition += mr.getColumnGap(); + } } } currentIPPosition = saveIPPos; @@ -512,17 +525,10 @@ public abstract class AbstractRenderer */ protected void renderBlocks(Block parent, List blocks) { int saveIP = currentIPPosition; -// int saveBP = currentBPPosition; // Calculate the position of the content rectangle. if (parent != null && !parent.getTraitAsBoolean(Trait.IS_VIEWPORT_AREA)) { currentBPPosition += parent.getBorderAndPaddingWidthBefore(); - /* This is unnecessary now as we're going to use the *-indent traits - currentIPPosition += parent.getBorderAndPaddingWidthStart(); - Integer spaceStart = (Integer) parent.getTrait(Trait.SPACE_START); - if (spaceStart != null) { - currentIPPosition += spaceStart.intValue(); - }*/ } // the position of the containing block is used for @@ -541,15 +547,19 @@ public abstract class AbstractRenderer renderBlock((Block) obj); containingBPPosition = contBP; containingIPPosition = contIP; - } else { + } else if (obj instanceof LineArea) { // a line area is rendered from the top left position // of the line, each inline object is offset from there LineArea line = (LineArea) obj; - currentIPPosition = contIP - + parent.getStartIndent() - + line.getStartIndent(); + if ( parent != null ) { + int level = parent.getBidiLevel(); + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + currentIPPosition += parent.getStartIndent(); + } else { + currentIPPosition += parent.getEndIndent(); + } + } renderLineArea(line); - //InlineArea child = (InlineArea) line.getInlineAreas().get(0); currentBPPosition += line.getAllocBPD(); } currentIPPosition = saveIP; @@ -562,6 +572,7 @@ public abstract class AbstractRenderer * @param block The block area */ protected void renderBlock(Block block) { + assert block != null; List children = block.getChildAreas(); if (block instanceof BlockViewport) { if (children != null) { @@ -611,13 +622,40 @@ public abstract class AbstractRenderer List children = line.getInlineAreas(); int saveBP = currentBPPosition; currentBPPosition += line.getSpaceBefore(); - for (int count = 0; count < children.size(); count++) { - InlineArea inline = (InlineArea) children.get(count); + int bl = line.getBidiLevel(); + if ( bl >= 0 ) { + if ( ( bl & 1 ) == 0 ) { + currentIPPosition += line.getStartIndent(); + } else { + currentIPPosition += line.getEndIndent(); + // if line's content overflows line area, then + // ensure that overflow is drawn (extends) + // outside of left side of line area + int overflow = computeInlinesOverflow ( line ); + if ( overflow > 0 ) { + currentIPPosition -= overflow; + } + } + } else { + currentIPPosition += line.getStartIndent(); + } + for (int i = 0, l = children.size(); i < l; i++) { + InlineArea inline = (InlineArea) children.get(i); renderInlineArea(inline); } currentBPPosition = saveBP; } + private int computeInlinesOverflow ( LineArea line ) { + List children = line.getInlineAreas(); + int ipdConsumed = 0; + for (int i = 0, l = children.size(); i < l; i++) { + InlineArea inline = (InlineArea) children.get(i); + ipdConsumed += inline.getIPD(); + } + return ipdConsumed - line.getIPD(); + } + /** * Render the given InlineArea. * @param inlineArea inline area text to render @@ -678,11 +716,12 @@ public abstract class AbstractRenderer * @param text the text to render */ protected void renderText(TextArea text) { + List children = text.getChildAreas(); int saveIP = currentIPPosition; int saveBP = currentBPPosition; - Iterator iter = text.getChildAreas().iterator(); - while (iter.hasNext()) { - renderInlineArea((InlineArea) iter.next()); + for (int i = 0, l = children.size(); i < l; i++) { + InlineArea inline = (InlineArea) children.get(i); + renderInlineArea(inline); } currentIPPosition = saveIP + text.getAllocIPD(); } @@ -708,14 +747,39 @@ public abstract class AbstractRenderer * @param ip the inline parent to render */ protected void renderInlineParent(InlineParent ip) { + int level = ip.getBidiLevel(); + List children = ip.getChildAreas(); renderInlineAreaBackAndBorders(ip); int saveIP = currentIPPosition; int saveBP = currentBPPosition; - currentIPPosition += ip.getBorderAndPaddingWidthStart(); - currentBPPosition += ip.getOffset(); - Iterator iter = ip.getChildAreas().iterator(); - while (iter.hasNext()) { - renderInlineArea((InlineArea) iter.next()); + // if inline parent is a filled area (generated by Leader), and if + // it is right-to-left, then adjust starting ip position in order to + // align children to starting (right) edge of filled area + int ipAdjust; + if ( ( ip instanceof FilledArea ) && ( ( level & 1 ) != 0 ) ) { + int ipdChildren = 0; + for (int i = 0, l = children.size(); i < l; i++) { + InlineArea inline = (InlineArea) children.get(i); + ipdChildren += inline.getAllocIPD(); + } + ipAdjust = ip.getAllocIPD() - ipdChildren; + } else { + ipAdjust = 0; + } + // perform inline position adjustments + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + currentIPPosition += ip.getBorderAndPaddingWidthStart(); + } else { + currentIPPosition += ip.getBorderAndPaddingWidthEnd(); + if ( ipAdjust > 0 ) { + currentIPPosition += ipAdjust; + } + } + currentBPPosition += ip.getBlockProgressionOffset(); + // render children inlines + for (int i = 0, l = children.size(); i < l; i++) { + InlineArea inline = (InlineArea) children.get(i); + renderInlineArea(inline); } currentIPPosition = saveIP + ip.getAllocIPD(); currentBPPosition = saveBP; @@ -726,11 +790,16 @@ public abstract class AbstractRenderer * @param ibp the inline block parent to render */ protected void renderInlineBlockParent(InlineBlockParent ibp) { + int level = ibp.getBidiLevel(); renderInlineAreaBackAndBorders(ibp); - currentIPPosition += ibp.getBorderAndPaddingWidthStart(); + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + currentIPPosition += ibp.getBorderAndPaddingWidthStart(); + } else { + currentIPPosition += ibp.getBorderAndPaddingWidthEnd(); + } // For inline content the BP position is updated by the enclosing line area int saveBP = currentBPPosition; - currentBPPosition += ibp.getOffset(); + currentBPPosition += ibp.getBlockProgressionOffset(); renderBlock(ibp.getChildArea()); currentBPPosition = saveBP; } @@ -742,7 +811,7 @@ public abstract class AbstractRenderer protected void renderInlineViewport(InlineViewport viewport) { Area content = viewport.getContent(); int saveBP = currentBPPosition; - currentBPPosition += viewport.getOffset(); + currentBPPosition += viewport.getBlockProgressionOffset(); Rectangle2D contpos = viewport.getContentPosition(); if (content instanceof Image) { renderImage((Image) content, contpos); diff --git a/src/java/org/apache/fop/render/DefaultFontResolver.java b/src/java/org/apache/fop/render/DefaultFontResolver.java index a4960c665..0642537c4 100644 --- a/src/java/org/apache/fop/render/DefaultFontResolver.java +++ b/src/java/org/apache/fop/render/DefaultFontResolver.java @@ -44,4 +44,9 @@ public class DefaultFontResolver implements FontResolver { return userAgent.resolveURI(href, userAgent.getFactory().getFontManager().getFontBaseURL()); } + /** {@inheritDoc} */ + public boolean isComplexScriptFeaturesEnabled() { + return userAgent.isComplexScriptFeaturesEnabled(); + } + } diff --git a/src/java/org/apache/fop/render/PrintRenderer.java b/src/java/org/apache/fop/render/PrintRenderer.java index 4e49adc25..96a62cb11 100644 --- a/src/java/org/apache/fop/render/PrintRenderer.java +++ b/src/java/org/apache/fop/render/PrintRenderer.java @@ -91,7 +91,8 @@ public abstract class PrintRenderer extends AbstractRenderer { FontManager fontManager = userAgent.getFactory().getFontManager(); FontCollection[] fontCollections = new FontCollection[] { new Base14FontCollection(fontManager.isBase14KerningEnabled()), - new CustomFontCollection(getFontResolver(), getFontList()) + new CustomFontCollection(getFontResolver(), getFontList(), + userAgent.isComplexScriptFeaturesEnabled()) }; fontManager.setup(getFontInfo(), fontCollections); } diff --git a/src/java/org/apache/fop/render/PrintRendererConfigurator.java b/src/java/org/apache/fop/render/PrintRendererConfigurator.java index 90305ae03..93678b4f8 100644 --- a/src/java/org/apache/fop/render/PrintRendererConfigurator.java +++ b/src/java/org/apache/fop/render/PrintRendererConfigurator.java @@ -95,7 +95,9 @@ public class PrintRendererConfigurator extends AbstractRendererConfigurator FontManager fontManager = factory.getFontManager(); if (fontResolver == null) { //Ensure that we have minimal font resolution capabilities - fontResolver = FontManager.createMinimalFontResolver(); + fontResolver + = FontManager.createMinimalFontResolver + ( userAgent.isComplexScriptFeaturesEnabled() ); } boolean strict = factory.validateUserConfigStrictly(); @@ -128,7 +130,8 @@ public class PrintRendererConfigurator extends AbstractRendererConfigurator FontEventListener listener = new FontEventAdapter( userAgent.getEventBroadcaster()); List<EmbedFontInfo> fontList = buildFontList(cfg, fontResolver, listener); - fontCollections.add(new CustomFontCollection(fontResolver, fontList)); + fontCollections.add(new CustomFontCollection(fontResolver, fontList, + userAgent.isComplexScriptFeaturesEnabled())); } fontManager.setup(fontInfo, diff --git a/src/java/org/apache/fop/render/afp/AFPPainter.java b/src/java/org/apache/fop/render/afp/AFPPainter.java index 28ca6c67f..30e4e7693 100644 --- a/src/java/org/apache/fop/render/afp/AFPPainter.java +++ b/src/java/org/apache/fop/render/afp/AFPPainter.java @@ -57,6 +57,7 @@ import org.apache.fop.render.intermediate.BorderPainter; import org.apache.fop.render.intermediate.IFContext; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFState; +import org.apache.fop.render.intermediate.IFUtil; import org.apache.fop.traits.BorderProps; import org.apache.fop.traits.RuleStyle; import org.apache.fop.util.CharUtilities; @@ -259,11 +260,11 @@ public class AFPPainter extends AbstractIFPainter { /** {@inheritDoc} */ @Override - public void drawBorderRect(Rectangle rect, BorderProps before, BorderProps after, - BorderProps start, BorderProps end) throws IFException { - if (before != null || after != null || start != null || end != null) { + public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom, + BorderProps left, BorderProps right) throws IFException { + if (top != null || bottom != null || left != null || right != null) { try { - this.borderPainter.drawBorders(rect, before, after, start, end); + this.borderPainter.drawBorders(rect, top, bottom, left, right); } catch (IOException ife) { throw new IFException("IO error while painting borders", ife); } @@ -354,8 +355,8 @@ public class AFPPainter extends AbstractIFPainter { /** {@inheritDoc} */ public void drawText( // CSOK: MethodLength - int x, int y, final int letterSpacing, final int wordSpacing, final int[] dx, - final String text) throws IFException { + int x, int y, final int letterSpacing, final int wordSpacing, + final int[][] dp, final String text) throws IFException { final int fontSize = this.state.getFontSize(); getPaintingState().setFontSize(fontSize); @@ -404,6 +405,7 @@ public class AFPPainter extends AbstractIFPainter { builder.setCodedFont((byte)fontReference); int l = text.length(); + int[] dx = IFUtil.convertDPToDX ( dp ); int dxl = (dx != null ? dx.length : 0); StringBuffer sb = new StringBuffer(); diff --git a/src/java/org/apache/fop/render/bitmap/BitmapRendererConfigurator.java b/src/java/org/apache/fop/render/bitmap/BitmapRendererConfigurator.java index 2d78b53c6..296d4628a 100644 --- a/src/java/org/apache/fop/render/bitmap/BitmapRendererConfigurator.java +++ b/src/java/org/apache/fop/render/bitmap/BitmapRendererConfigurator.java @@ -129,7 +129,8 @@ public class BitmapRendererConfigurator extends Java2DRendererConfigurator FontEventListener listener = new FontEventAdapter( userAgent.getEventBroadcaster()); List fontList = buildFontList(cfg, fontResolver, listener); - fontCollections.add(new ConfiguredFontCollection(fontResolver, fontList)); + fontCollections.add(new ConfiguredFontCollection(fontResolver, fontList, + userAgent.isComplexScriptFeaturesEnabled())); } fontManager.setup(fontInfo, diff --git a/src/java/org/apache/fop/render/intermediate/AbstractIFPainter.java b/src/java/org/apache/fop/render/intermediate/AbstractIFPainter.java index 2226274ab..c696e552d 100644 --- a/src/java/org/apache/fop/render/intermediate/AbstractIFPainter.java +++ b/src/java/org/apache/fop/render/intermediate/AbstractIFPainter.java @@ -315,55 +315,55 @@ public abstract class AbstractIFPainter implements IFPainter { } /** {@inheritDoc} */ - public void drawBorderRect(Rectangle rect, BorderProps before, BorderProps after, - BorderProps start, BorderProps end) throws IFException { - if (before != null) { + public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom, + BorderProps left, BorderProps right) throws IFException { + if (top != null) { Rectangle b = new Rectangle( rect.x, rect.y, - rect.width, before.width); - fillRect(b, before.color); + rect.width, top.width); + fillRect(b, top.color); } - if (end != null) { + if (right != null) { Rectangle b = new Rectangle( - rect.x + rect.width - end.width, rect.y, - end.width, rect.height); - fillRect(b, end.color); + rect.x + rect.width - right.width, rect.y, + right.width, rect.height); + fillRect(b, right.color); } - if (after != null) { + if (bottom != null) { Rectangle b = new Rectangle( - rect.x, rect.y + rect.height - after.width, - rect.width, after.width); - fillRect(b, after.color); + rect.x, rect.y + rect.height - bottom.width, + rect.width, bottom.width); + fillRect(b, bottom.color); } - if (start != null) { + if (left != null) { Rectangle b = new Rectangle( rect.x, rect.y, - start.width, rect.height); - fillRect(b, start.color); + left.width, rect.height); + fillRect(b, left.color); } } /** * Indicates whether the given border segments (if present) have only solid borders, i.e. * could be painted in a simplified fashion keeping the output file smaller. - * @param before the border segment on the before-side (top) - * @param after the border segment on the after-side (bottom) - * @param start the border segment on the start-side (left) - * @param end the border segment on the end-side (right) + * @param top the border segment on the top edge + * @param bottom the border segment on the bottom edge + * @param left the border segment on the left edge + * @param right the border segment on the right edge * @return true if any border segment has a non-solid border style */ - protected boolean hasOnlySolidBorders(BorderProps before, BorderProps after, - BorderProps start, BorderProps end) { - if (before != null && before.style != Constants.EN_SOLID) { + protected boolean hasOnlySolidBorders(BorderProps top, BorderProps bottom, + BorderProps left, BorderProps right) { + if (top != null && top.style != Constants.EN_SOLID) { return false; } - if (after != null && after.style != Constants.EN_SOLID) { + if (bottom != null && bottom.style != Constants.EN_SOLID) { return false; } - if (start != null && start.style != Constants.EN_SOLID) { + if (left != null && left.style != Constants.EN_SOLID) { return false; } - if (end != null && end.style != Constants.EN_SOLID) { + if (right != null && right.style != Constants.EN_SOLID) { return false; } return true; diff --git a/src/java/org/apache/fop/render/intermediate/BorderPainter.java b/src/java/org/apache/fop/render/intermediate/BorderPainter.java index 20402369a..cc28028ef 100644 --- a/src/java/org/apache/fop/render/intermediate/BorderPainter.java +++ b/src/java/org/apache/fop/render/intermediate/BorderPainter.java @@ -35,35 +35,35 @@ public abstract class BorderPainter { /** * Draws borders. * @param borderRect the border rectangle - * @param bpsBefore the border specification on the before side - * @param bpsAfter the border specification on the after side - * @param bpsStart the border specification on the start side - * @param bpsEnd the border specification on the end side + * @param bpsTop the border specification on the top side + * @param bpsBottom the border specification on the bottom side + * @param bpsLeft the border specification on the left side + * @param bpsRight the border specification on the end side * @throws IOException if an I/O error occurs while creating the borders */ public void drawBorders(Rectangle borderRect, // CSOK: MethodLength - BorderProps bpsBefore, BorderProps bpsAfter, - BorderProps bpsStart, BorderProps bpsEnd) throws IOException { + BorderProps bpsTop, BorderProps bpsBottom, + BorderProps bpsLeft, BorderProps bpsRight) throws IOException { int startx = borderRect.x; int starty = borderRect.y; int width = borderRect.width; int height = borderRect.height; boolean[] b = new boolean[] { - (bpsBefore != null), (bpsEnd != null), - (bpsAfter != null), (bpsStart != null)}; + (bpsTop != null), (bpsRight != null), + (bpsBottom != null), (bpsLeft != null)}; if (!b[0] && !b[1] && !b[2] && !b[3]) { return; } int[] bw = new int[] { - (b[0] ? bpsBefore.width : 0), - (b[1] ? bpsEnd.width : 0), - (b[2] ? bpsAfter.width : 0), - (b[3] ? bpsStart.width : 0)}; + (b[0] ? bpsTop.width : 0), + (b[1] ? bpsRight.width : 0), + (b[2] ? bpsBottom.width : 0), + (b[3] ? bpsLeft.width : 0)}; int[] clipw = new int[] { - BorderProps.getClippedWidth(bpsBefore), - BorderProps.getClippedWidth(bpsEnd), - BorderProps.getClippedWidth(bpsAfter), - BorderProps.getClippedWidth(bpsStart)}; + BorderProps.getClippedWidth(bpsTop), + BorderProps.getClippedWidth(bpsRight), + BorderProps.getClippedWidth(bpsBottom), + BorderProps.getClippedWidth(bpsLeft)}; starty += clipw[0]; height -= clipw[0]; height -= clipw[2]; @@ -73,7 +73,7 @@ public abstract class BorderPainter { boolean[] slant = new boolean[] { (b[3] && b[0]), (b[0] && b[1]), (b[1] && b[2]), (b[2] && b[3])}; - if (bpsBefore != null) { + if (bpsTop != null) { int sx1 = startx; int sx2 = (slant[0] ? sx1 + bw[3] - clipw[3] : sx1); int ex1 = startx + width; @@ -86,11 +86,11 @@ public abstract class BorderPainter { moveTo(sx1, clipy); int sx1a = sx1; int ex1a = ex1; - if (bpsBefore.mode == BorderProps.COLLAPSE_OUTER) { - if (bpsStart != null && bpsStart.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsTop.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsLeft != null && bpsLeft.mode == BorderProps.COLLAPSE_OUTER) { sx1a -= clipw[3]; } - if (bpsEnd != null && bpsEnd.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsRight != null && bpsRight.mode == BorderProps.COLLAPSE_OUTER) { ex1a += clipw[1]; } lineTo(sx1a, outery); @@ -102,10 +102,10 @@ public abstract class BorderPainter { closePath(); clip(); drawBorderLine(sx1a, outery, ex1a, innery, true, true, - bpsBefore.style, bpsBefore.color); + bpsTop.style, bpsTop.color); restoreGraphicsState(); } - if (bpsEnd != null) { + if (bpsRight != null) { int sy1 = starty; int sy2 = (slant[1] ? sy1 + bw[0] - clipw[0] : sy1); int ey1 = starty + height; @@ -118,11 +118,11 @@ public abstract class BorderPainter { moveTo(clipx, sy1); int sy1a = sy1; int ey1a = ey1; - if (bpsEnd.mode == BorderProps.COLLAPSE_OUTER) { - if (bpsBefore != null && bpsBefore.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsRight.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsTop != null && bpsTop.mode == BorderProps.COLLAPSE_OUTER) { sy1a -= clipw[0]; } - if (bpsAfter != null && bpsAfter.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsBottom != null && bpsBottom.mode == BorderProps.COLLAPSE_OUTER) { ey1a += clipw[2]; } lineTo(outerx, sy1a); @@ -133,10 +133,11 @@ public abstract class BorderPainter { lineTo(innerx, sy2); closePath(); clip(); - drawBorderLine(innerx, sy1a, outerx, ey1a, false, false, bpsEnd.style, bpsEnd.color); + drawBorderLine(innerx, sy1a, outerx, ey1a, false, false, + bpsRight.style, bpsRight.color); restoreGraphicsState(); } - if (bpsAfter != null) { + if (bpsBottom != null) { int sx1 = startx; int sx2 = (slant[3] ? sx1 + bw[3] - clipw[3] : sx1); int ex1 = startx + width; @@ -149,11 +150,11 @@ public abstract class BorderPainter { moveTo(ex1, clipy); int sx1a = sx1; int ex1a = ex1; - if (bpsAfter.mode == BorderProps.COLLAPSE_OUTER) { - if (bpsStart != null && bpsStart.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsBottom.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsLeft != null && bpsLeft.mode == BorderProps.COLLAPSE_OUTER) { sx1a -= clipw[3]; } - if (bpsEnd != null && bpsEnd.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsRight != null && bpsRight.mode == BorderProps.COLLAPSE_OUTER) { ex1a += clipw[1]; } lineTo(ex1a, outery); @@ -164,10 +165,11 @@ public abstract class BorderPainter { lineTo(ex2, innery); closePath(); clip(); - drawBorderLine(sx1a, innery, ex1a, outery, true, false, bpsAfter.style, bpsAfter.color); + drawBorderLine(sx1a, innery, ex1a, outery, true, false, + bpsBottom.style, bpsBottom.color); restoreGraphicsState(); } - if (bpsStart != null) { + if (bpsLeft != null) { int sy1 = starty; int sy2 = (slant[0] ? sy1 + bw[0] - clipw[0] : sy1); int ey1 = sy1 + height; @@ -180,11 +182,11 @@ public abstract class BorderPainter { moveTo(clipx, ey1); int sy1a = sy1; int ey1a = ey1; - if (bpsStart.mode == BorderProps.COLLAPSE_OUTER) { - if (bpsBefore != null && bpsBefore.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsLeft.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsTop != null && bpsTop.mode == BorderProps.COLLAPSE_OUTER) { sy1a -= clipw[0]; } - if (bpsAfter != null && bpsAfter.mode == BorderProps.COLLAPSE_OUTER) { + if (bpsBottom != null && bpsBottom.mode == BorderProps.COLLAPSE_OUTER) { ey1a += clipw[2]; } lineTo(outerx, ey1a); @@ -195,7 +197,7 @@ public abstract class BorderPainter { lineTo(innerx, ey2); closePath(); clip(); - drawBorderLine(outerx, sy1a, innerx, ey1a, false, true, bpsStart.style, bpsStart.color); + drawBorderLine(outerx, sy1a, innerx, ey1a, false, true, bpsLeft.style, bpsLeft.color); restoreGraphicsState(); } } diff --git a/src/java/org/apache/fop/render/intermediate/IFPainter.java b/src/java/org/apache/fop/render/intermediate/IFPainter.java index 00fd74209..06dfbd181 100644 --- a/src/java/org/apache/fop/render/intermediate/IFPainter.java +++ b/src/java/org/apache/fop/render/intermediate/IFPainter.java @@ -151,12 +151,14 @@ public interface IFPainter { * @param y Y-coordinate of the starting point of the text * @param letterSpacing additional spacing between characters (may be 0) * @param wordSpacing additional spacing between words (may be 0) - * @param dx an array of adjustment values for each character in X-direction (may be null) + * @param dp an array of 4-tuples, expressing [X,Y] placment + * adjustments and [X,Y] advancement adjustments, in that order (may be null); if + * not null, then adjustments.length must be the same as text.length() * @param text the text * @throws IFException if an error occurs while handling this event */ void drawText(int x, int y, int letterSpacing, int wordSpacing, - int[] dx, String text) throws IFException; + int[][] dp, String text) throws IFException; /** * Restricts the current clipping region with the given rectangle. @@ -178,15 +180,15 @@ public interface IFPainter { * Draws a border rectangle. The border segments are specified through {@link BorderProps} * instances. * @param rect the rectangle's coordinates and extent - * @param before the border segment on the before-side (top) - * @param after the border segment on the after-side (bottom) - * @param start the border segment on the start-side (left) - * @param end the border segment on the end-side (right) + * @param top the border segment on the top edge + * @param bottom the border segment on the bottom edge + * @param left the border segment on the left edge + * @param right the border segment on the right edge * @throws IFException if an error occurs while handling this event */ void drawBorderRect(Rectangle rect, - BorderProps before, BorderProps after, - BorderProps start, BorderProps end) throws IFException; + BorderProps top, BorderProps bottom, + BorderProps left, BorderProps right) throws IFException; /** * Draws a line. NOTE: Currently, only horizontal lines are implemented! diff --git a/src/java/org/apache/fop/render/intermediate/IFParser.java b/src/java/org/apache/fop/render/intermediate/IFParser.java index 36f1fd841..26d1b62ed 100644 --- a/src/java/org/apache/fop/render/intermediate/IFParser.java +++ b/src/java/org/apache/fop/render/intermediate/IFParser.java @@ -618,8 +618,14 @@ public class IFParser implements IFConstants { s = lastAttributes.getValue("word-spacing"); int wordSpacing = (s != null ? Integer.parseInt(s) : 0); int[] dx = XMLUtil.getAttributeAsIntArray(lastAttributes, "dx"); + int[][] dp = XMLUtil.getAttributeAsPositionAdjustments(lastAttributes, "dp"); + // if only DX present, then convert DX to DP; otherwise use only DP, + // effectively ignoring DX + if ( ( dp == null ) && ( dx != null ) ) { + dp = IFUtil.convertDXToDP ( dx ); + } establishStructureTreeElement(lastAttributes); - painter.drawText(x, y, letterSpacing, wordSpacing, dx, content.toString()); + painter.drawText(x, y, letterSpacing, wordSpacing, dp, content.toString()); resetStructureTreeElement(); } @@ -679,7 +685,7 @@ public class IFParser implements IFConstants { } - private static final String[] SIDES = new String[] {"before", "after", "start", "end"}; + private static final String[] SIDES = new String[] {"top", "bottom", "left", "right"}; private class BorderRectHandler extends AbstractElementHandler { diff --git a/src/java/org/apache/fop/render/intermediate/IFRenderer.java b/src/java/org/apache/fop/render/intermediate/IFRenderer.java index 4fec6c623..2987a4be8 100644 --- a/src/java/org/apache/fop/render/intermediate/IFRenderer.java +++ b/src/java/org/apache/fop/render/intermediate/IFRenderer.java @@ -61,6 +61,7 @@ import org.apache.fop.area.BlockViewport; import org.apache.fop.area.BookmarkData; import org.apache.fop.area.CTM; import org.apache.fop.area.DestinationData; +import org.apache.fop.area.LineArea; import org.apache.fop.area.OffDocumentExtensionAttachment; import org.apache.fop.area.OffDocumentItem; import org.apache.fop.area.PageSequence; @@ -488,7 +489,8 @@ public class IFRenderer extends AbstractPathOrientedRenderer { if (hasDocumentNavigation() && id != null) { int extraMarginBefore = 5000; // millipoints int ipp = currentIPPosition; - int bpp = currentBPPosition + inlineArea.getOffset() - extraMarginBefore; + int bpp = currentBPPosition + + inlineArea.getBlockProgressionOffset() - extraMarginBefore; saveAbsolutePosition(id, ipp, bpp); } } @@ -919,7 +921,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { AbstractAction action = null; // make sure the rect is determined *before* calling super! int ipp = currentIPPosition; - int bpp = currentBPPosition + ip.getOffset(); + int bpp = currentBPPosition + ip.getBlockProgressionOffset(); ipRect = new Rectangle(ipp, bpp, ip.getIPD(), ip.getBPD()); AffineTransform transform = graphicContext.getTransform(); ipRect = transform.createTransformedShape(ipRect).getBounds(); @@ -1031,7 +1033,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { } int rx = currentIPPosition + text.getBorderAndPaddingWidthStart(); - int bl = currentBPPosition + text.getOffset() + text.getBaselineOffset(); + int bl = currentBPPosition + text.getBlockProgressionOffset() + text.getBaselineOffset(); textUtil.flush(); textUtil.setStartPosition(rx, bl); textUtil.setSpacing(text.getTextLetterSpaceAdjust(), text.getTextWordSpaceAdjust()); @@ -1047,7 +1049,12 @@ public class IFRenderer extends AbstractPathOrientedRenderer { Font font = getFontFromArea(word.getParentArea()); String s = word.getWord(); - renderText(s, word.getLetterAdjustArray(), + int[][] dp = word.getGlyphPositionAdjustments(); + if ( dp == null ) { + dp = IFUtil.convertDXToDP ( word.getLetterAdjustArray() ); + } + + renderText(s, dp, word.isReversed(), font, (AbstractTextArea)word.getParentArea()); super.renderWord(word); @@ -1059,7 +1066,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { String s = space.getSpace(); AbstractTextArea textArea = (AbstractTextArea)space.getParentArea(); - renderText(s, null, font, textArea); + renderText(s, null, false, font, textArea); if (textUtil.combined && space.isAdjustable()) { //Used for justified text, for example @@ -1072,24 +1079,32 @@ public class IFRenderer extends AbstractPathOrientedRenderer { super.renderSpace(space); } + private void renderText(String s, + int[][] dp, boolean reversed, + Font font, AbstractTextArea parentArea) { + if ( ( dp == null ) || IFUtil.isDPOnlyDX ( dp ) ) { + int[] dx = IFUtil.convertDPToDX ( dp ); + renderTextWithAdjustments ( s, dx, reversed, font, parentArea ); + } else { + renderTextWithAdjustments ( s, dp, reversed, font, parentArea ); + } + } + /** - * Does low-level rendering of text. + * Does low-level rendering of text using DX only position adjustments. * @param s text to render - * @param letterAdjust an array of widths for letter adjustment (may be null) + * @param dx an array of widths for letter adjustment (may be null) + * @param reversed if true then text has been reversed (from logical order) * @param font to font in use * @param parentArea the parent text area to retrieve certain traits from */ - protected void renderText(String s, - int[] letterAdjust, - Font font, AbstractTextArea parentArea) { + private void renderTextWithAdjustments(String s, + int[] dx, boolean reversed, + Font font, AbstractTextArea parentArea) { int l = s.length(); if (l == 0) { return; } - - if (letterAdjust != null) { - textUtil.adjust(letterAdjust[0]); - } for (int i = 0; i < l; i++) { char ch = s.charAt(i); textUtil.addChar(ch); @@ -1098,18 +1113,38 @@ public class IFRenderer extends AbstractPathOrientedRenderer { int tls = (i < l - 1 ? parentArea.getTextLetterSpaceAdjust() : 0); glyphAdjust += tls; } - if (letterAdjust != null && i < l - 1) { - glyphAdjust += letterAdjust[i + 1]; + if (dx != null && i < l) { + glyphAdjust += dx[i]; } - textUtil.adjust(glyphAdjust); } } + /** + * Does low-level rendering of text using generalized position adjustments. + * @param s text to render + * @param dp an array of 4-tuples, expressing [X,Y] placment + * adjustments and [X,Y] advancement adjustments, in that order (may be null) + * @param reversed if true then text has been reversed (from logical order) + * @param font to font in use + * @param parentArea the parent text area to retrieve certain traits from + */ + private void renderTextWithAdjustments(String s, + int[][] dp, boolean reversed, + Font font, AbstractTextArea parentArea) { + assert !textUtil.combined; + for ( int i = 0, n = s.length(); i < n; i++ ) { + textUtil.addChar ( s.charAt ( i ) ); + if ( dp != null ) { + textUtil.adjust ( dp[i] ); + } + } + } + private class TextUtil { private static final int INITIAL_BUFFER_SIZE = 16; - private int[] dx = new int[INITIAL_BUFFER_SIZE]; - private int lastDXPos = 0; + private int[][] dp = new int[INITIAL_BUFFER_SIZE][4]; + // private int lastDPPos = 0; // TBD - not yet used private final StringBuffer text = new StringBuffer(); private int startx, starty; private int tls, tws; @@ -1119,25 +1154,41 @@ public class IFRenderer extends AbstractPathOrientedRenderer { text.append(ch); } - void adjust(int adjust) { - if (adjust != 0) { + void adjust(int dx) { + adjust ( new int[] { + dx, // xPlaAdjust + 0, // yPlaAdjust + dx, // xAdvAdjust + 0 // yAdvAdjust + } ); + } + + void adjust(int[] pa) { + if ( !IFUtil.isPAIdentity ( pa ) ) { int idx = text.length(); - if (idx > dx.length - 1) { - int newSize = Math.max(dx.length, idx + 1) + INITIAL_BUFFER_SIZE; - int[] newDX = new int[newSize]; - System.arraycopy(dx, 0, newDX, 0, dx.length); - dx = newDX; + if (idx > dp.length - 1) { + int newSize = Math.max(dp.length, idx + 1) + INITIAL_BUFFER_SIZE; + int[][] newDP = new int[newSize][]; + // reuse prior PA[0]...PA[dp.length-1] + System.arraycopy(dp, 0, newDP, 0, dp.length); + // populate new PA[dp.length]...PA[newDP.length-1] + for ( int i = dp.length, n = newDP.length; i < n; i++ ) { + newDP[i] = new int[4]; + } + dp = newDP; } - dx[idx] += adjust; - lastDXPos = idx; + IFUtil.adjustPA ( dp[idx - 1], pa ); + // lastDPPos = idx; } } void reset() { if (text.length() > 0) { text.setLength(0); - Arrays.fill(dx, 0); - lastDXPos = 0; + for ( int i = 0, n = dp.length; i < n; i++ ) { + Arrays.fill(dp[i], 0); + } + // lastDPPos = 0; } } @@ -1154,16 +1205,12 @@ public class IFRenderer extends AbstractPathOrientedRenderer { void flush() { if (text.length() > 0) { try { - int[] effDX = null; - if (lastDXPos > 0) { - int size = lastDXPos + 1; - effDX = new int[size]; - System.arraycopy(dx, 0, effDX, 0, size); - } if (combined) { - painter.drawText(startx, starty, 0, 0, effDX, text.toString()); + painter.drawText(startx, starty, 0, 0, + trimAdjustments ( dp, text.length() ), text.toString()); } else { - painter.drawText(startx, starty, tls, tws, effDX, text.toString()); + painter.drawText(startx, starty, tls, tws, + trimAdjustments ( dp, text.length() ), text.toString()); } } catch (IFException e) { handleIFException(e); @@ -1171,6 +1218,38 @@ public class IFRenderer extends AbstractPathOrientedRenderer { reset(); } } + + /** + * Trim adjustments array <code>dp</code> to be no greater length than + * text length, and where trailing all-zero entries are removed. + * @param dp a position adjustments array (or null) + * @param textLength the length of the associated text + * @return either the original value of <code>dp</code> or a copy + * of its first N significant adjustment entries, such that N is + * no greater than text length, and the last entry has a non-zero + * adjustment. + */ + private int[][] trimAdjustments ( int[][] dp, int textLength ) { + if ( dp != null ) { + int tl = textLength; + int pl = dp.length; + int i = ( tl < pl ) ? tl : pl; + while ( i > 0 ) { + int[] pa = dp [ i - 1 ]; + if ( !IFUtil.isPAIdentity ( pa ) ) { + break; + } else { + i--; + } + } + if ( i == 0 ) { + dp = null; + } else if ( i < pl ) { + dp = IFUtil.copyDP ( dp, 0, i ); + } + } + return dp; + } } /** {@inheritDoc} */ @@ -1220,7 +1299,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { int style = area.getRuleStyle(); int ruleThickness = area.getRuleThickness(); int startx = currentIPPosition + area.getBorderAndPaddingWidthStart(); - int starty = currentBPPosition + area.getOffset() + (ruleThickness / 2); + int starty = currentBPPosition + area.getBlockProgressionOffset() + (ruleThickness / 2); int endx = currentIPPosition + area.getBorderAndPaddingWidthStart() + area.getIPD(); @@ -1262,10 +1341,21 @@ public class IFRenderer extends AbstractPathOrientedRenderer { float startx, float starty, float width, float height, BorderProps bpsBefore, BorderProps bpsAfter, - BorderProps bpsStart, BorderProps bpsEnd) { + BorderProps bpsStart, BorderProps bpsEnd, int level) { Rectangle rect = toMillipointRectangle(startx, starty, width, height); try { - painter.drawBorderRect(rect, bpsBefore, bpsAfter, bpsStart, bpsEnd); + BorderProps bpsTop = bpsBefore; + BorderProps bpsBottom = bpsAfter; + BorderProps bpsLeft; + BorderProps bpsRight; + if ( ( level == -1 ) || ( ( level & 1 ) == 0 ) ) { + bpsLeft = bpsStart; + bpsRight = bpsEnd; + } else { + bpsLeft = bpsEnd; + bpsRight = bpsStart; + } + painter.drawBorderRect(rect, bpsTop, bpsBottom, bpsLeft, bpsRight); } catch (IFException ife) { handleIFException(ife); } diff --git a/src/java/org/apache/fop/render/intermediate/IFSerializer.java b/src/java/org/apache/fop/render/intermediate/IFSerializer.java index 7b9d95849..f8f286cb3 100644 --- a/src/java/org/apache/fop/render/intermediate/IFSerializer.java +++ b/src/java/org/apache/fop/render/intermediate/IFSerializer.java @@ -521,9 +521,9 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler } /** {@inheritDoc} */ - public void drawBorderRect(Rectangle rect, BorderProps before, BorderProps after, - BorderProps start, BorderProps end) throws IFException { - if (before == null && after == null && start == null && end == null) { + public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom, + BorderProps left, BorderProps right) throws IFException { + if (top == null && bottom == null && left == null && right == null) { return; } try { @@ -532,17 +532,17 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler addAttribute(atts, "y", Integer.toString(rect.y)); addAttribute(atts, "width", Integer.toString(rect.width)); addAttribute(atts, "height", Integer.toString(rect.height)); - if (before != null) { - addAttribute(atts, "before", before.toString()); + if (top != null) { + addAttribute(atts, "top", top.toString()); } - if (after != null) { - addAttribute(atts, "after", after.toString()); + if (bottom != null) { + addAttribute(atts, "bottom", bottom.toString()); } - if (start != null) { - addAttribute(atts, "start", start.toString()); + if (left != null) { + addAttribute(atts, "left", left.toString()); } - if (end != null) { - addAttribute(atts, "end", end.toString()); + if (right != null) { + addAttribute(atts, "right", right.toString()); } handler.element(EL_BORDER_RECT, atts); } catch (SAXException e) { @@ -571,7 +571,7 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler /** {@inheritDoc} */ public void drawText(int x, int y, int letterSpacing, int wordSpacing, - int[] dx, String text) throws IFException { + int[][] dp, String text) throws IFException { try { addID(); AttributesImpl atts = new AttributesImpl(); @@ -583,8 +583,17 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler if (wordSpacing != 0) { addAttribute(atts, "word-spacing", Integer.toString(wordSpacing)); } - if (dx != null) { - addAttribute(atts, "dx", IFUtil.toString(dx)); + if (dp != null) { + if ( IFUtil.isDPIdentity(dp) ) { + // don't add dx or dp attribute + } else if ( IFUtil.isDPOnlyDX(dp) ) { + // add dx attribute only + int[] dx = IFUtil.convertDPToDX(dp); + addAttribute(atts, "dx", IFUtil.toString(dx)); + } else { + // add dp attribute only + addAttribute(atts, "dp", XMLUtil.encodePositionAdjustments(dp)); + } } addStructureReference(atts); handler.startElement(EL_TEXT, atts); diff --git a/src/java/org/apache/fop/render/intermediate/IFUtil.java b/src/java/org/apache/fop/render/intermediate/IFUtil.java index 1867b0294..86991ecb1 100644 --- a/src/java/org/apache/fop/render/intermediate/IFUtil.java +++ b/src/java/org/apache/fop/render/intermediate/IFUtil.java @@ -199,4 +199,182 @@ public final class IFUtil { return documentHandler.getMimeType(); } + /** + * Convert the general gpos 'dp' adjustments to the older 'dx' adjustments. + * This utility method is used to provide backward compatibility in implementations + * of IFPainter that have not yet been upgraded to the general position adjustments format. + * @param dp an array of 4-tuples, expressing [X,Y] placment + * adjustments and [X,Y] advancement adjustments, in that order (may be null) + * @param count if <code>dp</code> is not null, then a count of dp values to convert + * @return if <code>dp</code> is not null, then an array of adjustments to the current + * x position prior to rendering individual glyphs; otherwise, null + */ + public static int[] convertDPToDX ( int[][] dp, int count ) { + int[] dx; + if ( dp != null ) { + dx = new int [ count ]; + for ( int i = 0, n = count; i < n; i++ ) { + dx [ i ] = dp [ i ] [ 0 ]; // xPlaAdjust[i] + } + } else { + dx = null; + } + return dx; + } + + /** + * Convert the general gpos 'dp' adjustments to the older 'dx' adjustments. + * This utility method is used to provide backward compatibility in implementations + * of IFPainter that have not yet been upgraded to the general position adjustments format. + * @param dp an array of 4-tuples, expressing [X,Y] placment + * adjustments and [X,Y] advancement adjustments, in that order (may be null) + * @return if <code>dp</code> is not null, then an array of adjustments to the current + * x position prior to rendering individual glyphs; otherwise, null + */ + public static int[] convertDPToDX ( int[][] dp ) { + return convertDPToDX ( dp, ( dp != null ) ? dp.length : 0 ); + } + + /** + * Convert the general gpos 'dp' adjustments to the older 'dx' adjustments. + * This utility method is used to provide backward compatibility in implementations + * of IFPainter that have not yet been upgraded to the general position adjustments format. + * @param dx an array of adjustments to the current x position prior to rendering + * individual glyphs or null + * @param count if <code>dx</code> is not null, then a count of dx values to convert + * @return if <code>dx</code> is not null, then an array of 4-tuples, expressing [X,Y] + * placment adjustments and [X,Y] advancement adjustments, in that order; otherwise, null + */ + public static int[][] convertDXToDP ( int[] dx, int count ) { + int[][] dp; + if ( dx != null ) { + dp = new int [ count ] [ 4 ]; + for ( int i = 0, n = count; i < n; i++ ) { + int[] pa = dp [ i ]; + int d = dx [ i ]; + pa [ 0 ] = d; // xPlaAdjust[i] + pa [ 2 ] = d; // xAdvAdjust[i] + } + } else { + dp = null; + } + return dp; + } + + /** + * Convert the general gpos 'dp' adjustments to the older 'dx' adjustments. + * This utility method is used to provide backward compatibility in implementations + * of IFPainter that have not yet been upgraded to the general position adjustments format. + * @param dx an array of adjustments to the current x position prior to rendering + * individual glyphs or null + * @return if <code>dx</code> is not null, then an array of 4-tuples, expressing [X,Y] + * placment adjustments and [X,Y] advancement adjustments, in that order; otherwise, null + */ + public static int[][] convertDXToDP ( int[] dx ) { + return convertDXToDP ( dx, ( dx != null ) ? dx.length : 0 ); + } + + /** + * Determine if position adjustment is the identity adjustment, i.e., no non-zero adjustment. + * @param pa a 4-tuple, expressing [X,Y] placment and [X,Y] advance adjuustments (may be null) + * @return true if <code>dp</code> is null or contains no non-zero adjustment + */ + public static boolean isPAIdentity ( int[] pa ) { + if ( pa == null ) { + return true; + } else { + for ( int k = 0; k < 4; k++ ) { + if ( pa[k] != 0 ) { + return false; + } + } + return true; + } + } + + /** + * Determine if position adjustments is the identity adjustment, i.e., no non-zero adjustment. + * @param dp an array of 4-tuples, expressing [X,Y] placment + * adjustments and [X,Y] advancement adjustments, in that order (may be null) + * @return true if <code>dp</code> is null or contains no non-zero adjustment + */ + public static boolean isDPIdentity ( int[][] dp ) { + if ( dp == null ) { + return true; + } else { + for ( int i = 0, n = dp.length; i < n; i++ ) { + if ( !isPAIdentity ( dp[i] ) ) { + return false; + } + } + return true; + } + } + + /** + * Determine if position adjustments comprises only DX adjustments as encoded by + * {@link #convertDPToDX}. Note that if given a set of all all zero position + * adjustments, both this method and {@link #isDPIdentity} will return true; + * however, this method may return true when {@link #isDPIdentity} returns false. + * @param dp an array of 4-tuples, expressing [X,Y] placment + * adjustments and [X,Y] advancement adjustments, in that order (may be null) + * @return true if <code>dp</code> is not null and contains only xPlaAdjust + * and xAdvAdjust values consistent with the output of {@link #convertDPToDX}. + */ + public static boolean isDPOnlyDX ( int[][] dp ) { + if ( dp == null ) { + return false; + } else { + for ( int i = 0, n = dp.length; i < n; i++ ) { + int[] pa = dp[i]; + if ( pa[0] != pa[2] ) { + return false; + } + } + return true; + } + } + + /** + * Adjust a position adjustments array. If both <code>paDst</code> and <code>paSrc</code> are + * non-null, then <code>paSrc[i]</code> is added to <code>paDst[i]</code>. + * @param paDst a 4-tuple, expressing [X,Y] placment + * and [X,Y] advance adjuustments (may be null) + * @param paSrc a 4-tuple, expressing [X,Y] placment + * and [X,Y] advance adjuustments (may be null) + */ + public static void adjustPA ( int[] paDst, int[] paSrc ) { + if ( ( paDst != null ) && ( paSrc != null ) ) { + assert paDst.length == 4; assert paSrc.length == 4; + for ( int i = 0; i < 4; i++ ) { + paDst[i] += paSrc[i]; + } + } + } + + /** + * Copy entries from position adjustments. + * @param dp an array of 4-tuples, expressing [X,Y] placment + * adjustments and [X,Y] advancement adjustments, in that order + * @param offset starting offset from which to copy + * @param count number of entries to copy + * @return a deep copy of the count position adjustment entries start at + * offset + */ + public static int[][] copyDP ( int[][] dp, int offset, int count ) { + if ( ( dp == null ) || ( offset > dp.length ) || ( ( offset + count ) > dp.length ) ) { + throw new IllegalArgumentException(); + } else { + int[][] dpNew = new int [ count ] [ 4 ]; + for ( int i = 0, n = count; i < n; i++ ) { + int[] paDst = dpNew [ i ]; + int[] paSrc = dp [ i + offset ]; + for ( int k = 0; k < 4; k++ ) { + paDst [ k ] = paSrc [ k ]; + } + } + return dpNew; + } + } + } diff --git a/src/java/org/apache/fop/render/java2d/ConfiguredFontCollection.java b/src/java/org/apache/fop/render/java2d/ConfiguredFontCollection.java index fb88b8bce..7ac350d5d 100644 --- a/src/java/org/apache/fop/render/java2d/ConfiguredFontCollection.java +++ b/src/java/org/apache/fop/render/java2d/ConfiguredFontCollection.java @@ -51,13 +51,14 @@ public class ConfiguredFontCollection implements FontCollection { * Main constructor * @param fontResolver a font resolver * @param customFonts the list of custom fonts + * @param useComplexScriptFeatures true if complex script features enabled */ public ConfiguredFontCollection(FontResolver fontResolver, - List/*<EmbedFontInfo>*/ customFonts) { + List/*<EmbedFontInfo>*/ customFonts, boolean useComplexScriptFeatures) { this.fontResolver = fontResolver; if (this.fontResolver == null) { //Ensure that we have minimal font resolution capabilities - this.fontResolver = FontManager.createMinimalFontResolver(); + this.fontResolver = FontManager.createMinimalFontResolver(useComplexScriptFeatures); } this.embedFontInfoList = customFonts; } @@ -89,7 +90,8 @@ public class ConfiguredFontCollection implements FontCollection { } else { CustomFont fontMetrics = FontLoader.loadFont( fontFile, null, true, EncodingMode.AUTO, - configFontInfo.getKerning(), fontResolver); + configFontInfo.getKerning(), + configFontInfo.getAdvanced(), fontResolver); font = new CustomFontMetricsMapper(fontMetrics); } diff --git a/src/java/org/apache/fop/render/java2d/Java2DPainter.java b/src/java/org/apache/fop/render/java2d/Java2DPainter.java index 396645768..c7fa1adc5 100644 --- a/src/java/org/apache/fop/render/java2d/Java2DPainter.java +++ b/src/java/org/apache/fop/render/java2d/Java2DPainter.java @@ -42,6 +42,7 @@ import org.apache.fop.render.intermediate.IFContext; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFPainter; import org.apache.fop.render.intermediate.IFState; +import org.apache.fop.render.intermediate.IFUtil; import org.apache.fop.traits.BorderProps; import org.apache.fop.traits.RuleStyle; import org.apache.fop.util.CharUtilities; @@ -184,11 +185,11 @@ public class Java2DPainter extends AbstractIFPainter { } /** {@inheritDoc} */ - public void drawBorderRect(Rectangle rect, BorderProps before, BorderProps after, - BorderProps start, BorderProps end) throws IFException { - if (before != null || after != null || start != null || end != null) { + public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom, + BorderProps left, BorderProps right) throws IFException { + if (top != null || bottom != null || left != null || right != null) { try { - this.borderPainter.drawBorders(rect, before, after, start, end); + this.borderPainter.drawBorders(rect, top, bottom, left, right); } catch (IOException e) { //Won't happen with Java2D throw new IllegalStateException("Unexpected I/O error"); @@ -203,7 +204,7 @@ public class Java2DPainter extends AbstractIFPainter { } /** {@inheritDoc} */ - public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text) + public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text) throws IFException { g2dState.updateColor(state.getTextColor()); FontTriplet triplet = new FontTriplet( @@ -220,6 +221,7 @@ public class Java2DPainter extends AbstractIFPainter { Point2D cursor = new Point2D.Float(0, 0); int l = text.length(); + int[] dx = IFUtil.convertDPToDX ( dp ); int dxl = (dx != null ? dx.length : 0); if (dx != null && dxl > 0 && dx[0] != 0) { diff --git a/src/java/org/apache/fop/render/java2d/Java2DRenderer.java b/src/java/org/apache/fop/render/java2d/Java2DRenderer.java index 1d1697607..45c91b2ee 100644 --- a/src/java/org/apache/fop/render/java2d/Java2DRenderer.java +++ b/src/java/org/apache/fop/render/java2d/Java2DRenderer.java @@ -178,7 +178,8 @@ public abstract class Java2DRenderer extends AbstractPathOrientedRenderer implem FontCollection[] fontCollections = new FontCollection[] { new Base14FontCollection(java2DFontMetrics), new InstalledFontCollection(java2DFontMetrics), - new ConfiguredFontCollection(getFontResolver(), getFontList()) + new ConfiguredFontCollection(getFontResolver(), getFontList(), + userAgent.isComplexScriptFeaturesEnabled()) }; userAgent.getFactory().getFontManager().setup( getFontInfo(), fontCollections); @@ -715,7 +716,7 @@ public abstract class Java2DRenderer extends AbstractPathOrientedRenderer implem renderInlineAreaBackAndBorders(text); int rx = currentIPPosition + text.getBorderAndPaddingWidthStart(); - int bl = currentBPPosition + text.getOffset() + text.getBaselineOffset(); + int bl = currentBPPosition + text.getBlockProgressionOffset() + text.getBaselineOffset(); int saveIP = currentIPPosition; Font font = getFontFromArea(text); @@ -827,7 +828,7 @@ public abstract class Java2DRenderer extends AbstractPathOrientedRenderer implem // TODO Colors do not work on Leaders yet float startx = (currentIPPosition + area.getBorderAndPaddingWidthStart()) / 1000f; - float starty = ((currentBPPosition + area.getOffset()) / 1000f); + float starty = ((currentBPPosition + area.getBlockProgressionOffset()) / 1000f); float endx = (currentIPPosition + area.getBorderAndPaddingWidthStart() + area.getIPD()) / 1000f; diff --git a/src/java/org/apache/fop/render/pcl/PCLPainter.java b/src/java/org/apache/fop/render/pcl/PCLPainter.java index afae8ac27..53e3d77da 100644 --- a/src/java/org/apache/fop/render/pcl/PCLPainter.java +++ b/src/java/org/apache/fop/render/pcl/PCLPainter.java @@ -51,6 +51,7 @@ import org.apache.fop.render.intermediate.IFContext; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFPainter; import org.apache.fop.render.intermediate.IFState; +import org.apache.fop.render.intermediate.IFUtil; import org.apache.fop.render.java2d.FontMetricsMapper; import org.apache.fop.render.java2d.Java2DPainter; import org.apache.fop.traits.BorderProps; @@ -206,13 +207,13 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { /** {@inheritDoc} */ public void drawBorderRect(final Rectangle rect, - final BorderProps before, final BorderProps after, - final BorderProps start, final BorderProps end) throws IFException { + final BorderProps top, final BorderProps bottom, + final BorderProps left, final BorderProps right) throws IFException { if (isSpeedOptimized()) { - super.drawBorderRect(rect, before, after, start, end); + super.drawBorderRect(rect, top, bottom, left, right); return; } - if (before != null || after != null || start != null || end != null) { + if (top != null || bottom != null || left != null || right != null) { final Rectangle boundingBox = rect; final Dimension dim = boundingBox.getSize(); @@ -224,7 +225,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { Java2DPainter painter = new Java2DPainter(g2d, getContext(), parent.getFontInfo(), state); try { - painter.drawBorderRect(rect, before, after, start, end); + painter.drawBorderRect(rect, top, bottom, left, right); } catch (IFException e) { //This should never happen with the Java2DPainter throw new RuntimeException("Unexpected error while painting borders", e); @@ -307,7 +308,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { } /** {@inheritDoc} */ - public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text) + public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text) throws IFException { try { FontTriplet triplet = new FontTriplet( @@ -319,13 +320,13 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { ? false : HardcodedFonts.setFont(gen, fontKey, state.getFontSize(), text); if (pclFont) { - drawTextNative(x, y, letterSpacing, wordSpacing, dx, text, triplet); + drawTextNative(x, y, letterSpacing, wordSpacing, dp, text, triplet); } else { - drawTextAsBitmap(x, y, letterSpacing, wordSpacing, dx, text, triplet); + drawTextAsBitmap(x, y, letterSpacing, wordSpacing, dp, text, triplet); if (DEBUG) { state.setTextColor(Color.GRAY); HardcodedFonts.setFont(gen, "F1", state.getFontSize(), text); - drawTextNative(x, y, letterSpacing, wordSpacing, dx, text, triplet); + drawTextNative(x, y, letterSpacing, wordSpacing, dp, text, triplet); } } } catch (IOException ioe) { @@ -333,7 +334,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { } } - private void drawTextNative(int x, int y, int letterSpacing, int wordSpacing, int[] dx, + private void drawTextNative(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text, FontTriplet triplet) throws IOException { Color textColor = state.getTextColor(); if (textColor != null) { @@ -347,6 +348,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { float fontSize = state.getFontSize() / 1000f; Font font = parent.getFontInfo().getFontInstance(triplet, state.getFontSize()); int l = text.length(); + int[] dx = IFUtil.convertDPToDX ( dp ); int dxl = (dx != null ? dx.length : 0); StringBuffer sb = new StringBuffer(Math.max(16, l)); @@ -392,7 +394,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { private Rectangle getTextBoundingBox( // CSOK: ParameterNumber int x, int y, - int letterSpacing, int wordSpacing, int[] dx, + int letterSpacing, int wordSpacing, int[][] dp, String text, Font font, FontMetricsMapper metrics) { int maxAscent = metrics.getMaxAscent(font.getFontSize()) / 1000; @@ -403,6 +405,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { 0, maxAscent - descent + 2 * safetyMargin); int l = text.length(); + int[] dx = IFUtil.convertDPToDX ( dp ); int dxl = (dx != null ? dx.length : 0); if (dx != null && dxl > 0 && dx[0] != 0) { @@ -432,7 +435,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { } private void drawTextAsBitmap(final int x, final int y, - final int letterSpacing, final int wordSpacing, final int[] dx, + final int letterSpacing, final int wordSpacing, final int[][] dp, final String text, FontTriplet triplet) throws IFException { //Use Java2D to paint different fonts via bitmap final Font font = parent.getFontInfo().getFontInstance(triplet, state.getFontSize()); @@ -447,7 +450,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { final int baselineOffset = maxAscent + safetyMargin; final Rectangle boundingBox = getTextBoundingBox(x, y, - letterSpacing, wordSpacing, dx, text, font, mapper); + letterSpacing, wordSpacing, dp, text, font, mapper); final Dimension dim = boundingBox.getSize(); Graphics2DImagePainter painter = new Graphics2DImagePainter() { @@ -470,7 +473,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { Java2DPainter painter = new Java2DPainter(g2d, getContext(), parent.getFontInfo(), state); try { - painter.drawText(x, y, letterSpacing, wordSpacing, dx, text); + painter.drawText(x, y, letterSpacing, wordSpacing, dp, text); } catch (IFException e) { //This should never happen with the Java2DPainter throw new RuntimeException("Unexpected error while painting text", e); diff --git a/src/java/org/apache/fop/render/pcl/PCLRendererConfigurator.java b/src/java/org/apache/fop/render/pcl/PCLRendererConfigurator.java index 605220ee2..84608b685 100644 --- a/src/java/org/apache/fop/render/pcl/PCLRendererConfigurator.java +++ b/src/java/org/apache/fop/render/pcl/PCLRendererConfigurator.java @@ -119,7 +119,8 @@ public class PCLRendererConfigurator extends PrintRendererConfigurator FontEventListener listener = new FontEventAdapter( userAgent.getEventBroadcaster()); List fontList = buildFontList(cfg, fontResolver, listener); - fontCollections.add(new ConfiguredFontCollection(fontResolver, fontList)); + fontCollections.add(new ConfiguredFontCollection(fontResolver, fontList, + userAgent.isComplexScriptFeaturesEnabled())); } fontManager.setup(fontInfo, diff --git a/src/java/org/apache/fop/render/pdf/PDFPainter.java b/src/java/org/apache/fop/render/pdf/PDFPainter.java index f2fbfd014..4928e7251 100644 --- a/src/java/org/apache/fop/render/pdf/PDFPainter.java +++ b/src/java/org/apache/fop/render/pdf/PDFPainter.java @@ -45,8 +45,10 @@ import org.apache.fop.render.intermediate.AbstractIFPainter; import org.apache.fop.render.intermediate.IFContext; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFState; +import org.apache.fop.render.intermediate.IFUtil; import org.apache.fop.render.pdf.PDFLogicalStructureHandler.MarkedContentInfo; import org.apache.fop.traits.BorderProps; +import org.apache.fop.traits.Direction; import org.apache.fop.traits.RuleStyle; import org.apache.fop.util.CharUtilities; @@ -259,12 +261,12 @@ public class PDFPainter extends AbstractIFPainter { /** {@inheritDoc} */ @Override - public void drawBorderRect(Rectangle rect, BorderProps before, BorderProps after, - BorderProps start, BorderProps end) throws IFException { - if (before != null || after != null || start != null || end != null) { + public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom, + BorderProps left, BorderProps right) throws IFException { + if (top != null || bottom != null || left != null || right != null) { generator.endTextObject(); try { - this.borderPainter.drawBorders(rect, before, after, start, end); + this.borderPainter.drawBorders(rect, top, bottom, left, right); } catch (IOException ioe) { throw new IFException("I/O error while drawing borders", ioe); } @@ -291,7 +293,7 @@ public class PDFPainter extends AbstractIFPainter { } /** {@inheritDoc} */ - public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, + public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text) throws IFException { if (accessEnabled) { @@ -309,6 +311,19 @@ public class PDFPainter extends AbstractIFPainter { FontTriplet triplet = new FontTriplet( state.getFontFamily(), state.getFontStyle(), state.getFontWeight()); + + if ( ( dp == null ) || IFUtil.isDPOnlyDX ( dp ) ) { + drawTextWithDX ( x, y, text, triplet, letterSpacing, + wordSpacing, IFUtil.convertDPToDX ( dp ) ); + } else { + drawTextWithDP ( x, y, text, triplet, letterSpacing, + wordSpacing, dp ); + } + } + + private void drawTextWithDX ( int x, int y, String text, FontTriplet triplet, + int letterSpacing, int wordSpacing, int[] dx ) { + //TODO Ignored: state.getFontVariant() //TODO Opportunity for font caching if font state is more heavily used String fontKey = getFontInfo().getInternalFontKey(triplet); @@ -375,6 +390,70 @@ public class PDFPainter extends AbstractIFPainter { textutil.writeTJ(); } + private static int[] paZero = new int[4]; + + private void drawTextWithDP ( int x, int y, String text, FontTriplet triplet, + int letterSpacing, int wordSpacing, int[][] dp ) { + assert text != null; + assert triplet != null; + assert dp != null; + String fk = getFontInfo().getInternalFontKey(triplet); + Typeface tf = getTypeface(fk); + if ( tf.isMultiByte() ) { + int fs = state.getFontSize(); + float fsPoints = fs / 1000f; + Font f = getFontInfo().getFontInstance(triplet, fs); + // String fn = f.getFontName(); + PDFTextUtil tu = generator.getTextUtil(); + double xc = 0f; + double yc = 0f; + double xoLast = 0f; + double yoLast = 0f; + double wox = wordSpacing; + tu.writeTextMatrix ( new AffineTransform ( 1, 0, 0, -1, x / 1000f, y / 1000f ) ); + tu.updateTf ( fk, fsPoints, true ); + generator.updateCharacterSpacing ( letterSpacing / 1000f ); + for ( int i = 0, n = text.length(); i < n; i++ ) { + char ch = text.charAt ( i ); + int[] pa = ( i < dp.length ) ? dp [ i ] : paZero; + double xo = xc + pa[0]; + double yo = yc + pa[1]; + double xa = f.getCharWidth(ch) + maybeWordOffsetX ( wox, ch, null ); + double ya = 0; + double xd = ( xo - xoLast ) / 1000f; + double yd = ( yo - yoLast ) / 1000f; + tu.writeTd ( xd, yd ); + tu.writeTj ( f.mapChar ( ch ) ); + xc += xa + pa[2]; + yc += ya + pa[3]; + xoLast = xo; + yoLast = yo; + } + } + } + + private double maybeWordOffsetX ( double wox, char ch, Direction dir ) { + if ( ( wox != 0 ) + && CharUtilities.isAdjustableSpace ( ch ) + && ( ( dir == null ) || dir.isHorizontal() ) ) { + return wox; + } else { + return 0; + } + } + + /* + private double maybeWordOffsetY ( double woy, char ch, Direction dir ) { + if ( ( woy != 0 ) + && CharUtilities.isAdjustableSpace ( ch ) && dir.isVertical() + && ( ( dir != null ) && dir.isVertical() ) ) { + return woy; + } else { + return 0; + } + } + */ + private char selectAndMapSingleByteFont(SingleByteFont singleByteFont, String fontName, float fontSize, PDFTextUtil textutil, char ch) { if (singleByteFont != null && singleByteFont.hasAdditionalEncodings()) { diff --git a/src/java/org/apache/fop/render/ps/AbstractPSTranscoder.java b/src/java/org/apache/fop/render/ps/AbstractPSTranscoder.java index 766c939f1..ad5090686 100644 --- a/src/java/org/apache/fop/render/ps/AbstractPSTranscoder.java +++ b/src/java/org/apache/fop/render/ps/AbstractPSTranscoder.java @@ -112,8 +112,9 @@ public abstract class AbstractPSTranscoder extends AbstractFOPTranscoder { graphics = createDocumentGraphics2D(); if (!isTextStroked()) { try { + boolean useComplexScriptFeatures = false; //TODO - FIX ME this.fontInfo = PDFDocumentGraphics2DConfigurator.createFontInfo( - getEffectiveConfiguration()); + getEffectiveConfiguration(), useComplexScriptFeatures); graphics.setCustomTextHandler(new NativeTextHandler(graphics, fontInfo)); } catch (FOPException fe) { throw new TranscoderException(fe); diff --git a/src/java/org/apache/fop/render/ps/NativeTextHandler.java b/src/java/org/apache/fop/render/ps/NativeTextHandler.java index 7cf59d519..e1171b3e1 100644 --- a/src/java/org/apache/fop/render/ps/NativeTextHandler.java +++ b/src/java/org/apache/fop/render/ps/NativeTextHandler.java @@ -73,7 +73,8 @@ public class NativeTextHandler implements PSTextHandler { private void setupFontInfo() { //Sets up a FontInfo with default fonts fontInfo = new FontInfo(); - FontSetup.setup(fontInfo); + boolean base14Kerning = false; + FontSetup.setup(fontInfo, base14Kerning); } /** diff --git a/src/java/org/apache/fop/render/ps/PSPainter.java b/src/java/org/apache/fop/render/ps/PSPainter.java index ef9239c2a..370472457 100644 --- a/src/java/org/apache/fop/render/ps/PSPainter.java +++ b/src/java/org/apache/fop/render/ps/PSPainter.java @@ -51,6 +51,7 @@ import org.apache.fop.render.intermediate.AbstractIFPainter; import org.apache.fop.render.intermediate.IFContext; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFState; +import org.apache.fop.render.intermediate.IFUtil; import org.apache.fop.traits.BorderProps; import org.apache.fop.traits.RuleStyle; import org.apache.fop.util.CharUtilities; @@ -234,16 +235,16 @@ public class PSPainter extends AbstractIFPainter { } /** {@inheritDoc} */ - public void drawBorderRect(Rectangle rect, BorderProps before, BorderProps after, - BorderProps start, BorderProps end) throws IFException { - if (before != null || after != null || start != null || end != null) { + public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom, + BorderProps left, BorderProps right) throws IFException { + if (top != null || bottom != null || left != null || right != null) { try { endTextObject(); if (getPSUtil().getRenderingMode() == PSRenderingMode.SIZE - && hasOnlySolidBorders(before, after, start, end)) { - super.drawBorderRect(rect, before, after, start, end); + && hasOnlySolidBorders(top, bottom, left, right)) { + super.drawBorderRect(rect, top, bottom, left, right); } else { - this.borderPainter.drawBorders(rect, before, after, start, end); + this.borderPainter.drawBorders(rect, top, bottom, left, right); } } catch (IOException ioe) { throw new IFException("I/O error in drawBorderRect()", ioe); @@ -343,9 +344,8 @@ public class PSPainter extends AbstractIFPainter { /** {@inheritDoc} */ public void drawText(int x, int y, int letterSpacing, int wordSpacing, - int[] dx, String text) throws IFException { + int[][] dp, String text) throws IFException { try { - //Note: dy is currently ignored PSGenerator generator = getGenerator(); generator.useColor(state.getTextColor()); beginTextObject(); @@ -383,8 +383,8 @@ public class PSPainter extends AbstractIFPainter { int encoding = mapped / 256; if (currentEncoding != encoding) { if (i > 0) { - writeText(text, start, i - start, letterSpacing, wordSpacing, dx, - font, tf); + writeText(text, start, i - start, + letterSpacing, wordSpacing, dp, font, tf); } if (encoding == 0) { useFont(fontKey, sizeMillipoints); @@ -399,7 +399,7 @@ public class PSPainter extends AbstractIFPainter { //Simple single-font painting useFont(fontKey, sizeMillipoints); } - writeText(text, start, textLen - start, letterSpacing, wordSpacing, dx, font, tf); + writeText(text, start, textLen - start, letterSpacing, wordSpacing, dp, font, tf); } catch (IOException ioe) { throw new IFException("I/O error in drawText()", ioe); } @@ -407,7 +407,7 @@ public class PSPainter extends AbstractIFPainter { private void writeText( // CSOK: ParameterNumber String text, int start, int len, - int letterSpacing, int wordSpacing, int[] dx, + int letterSpacing, int wordSpacing, int[][] dp, Font font, Typeface tf) throws IOException { PSGenerator generator = getGenerator(); int end = start + len; @@ -420,6 +420,7 @@ public class PSPainter extends AbstractIFPainter { int lineStart = 0; StringBuffer accText = new StringBuffer(initialSize); StringBuffer sb = new StringBuffer(initialSize); + int[] dx = IFUtil.convertDPToDX ( dp ); int dxl = (dx != null ? dx.length : 0); for (int i = start; i < end; i++) { char orgChar = text.charAt(i); diff --git a/src/java/org/apache/fop/render/rtf/RTFHandler.java b/src/java/org/apache/fop/render/rtf/RTFHandler.java index 22c0c8b76..95c4fec12 100644 --- a/src/java/org/apache/fop/render/rtf/RTFHandler.java +++ b/src/java/org/apache/fop/render/rtf/RTFHandler.java @@ -167,7 +167,8 @@ public class RTFHandler extends FOEventHandler { this.os = os; bDefer = true; - FontSetup.setup(fontInfo, null, new DefaultFontResolver(userAgent)); + boolean base14Kerning = false; + FontSetup.setup(fontInfo, null, new DefaultFontResolver(userAgent), base14Kerning); } /** diff --git a/src/java/org/apache/fop/render/xml/XMLRenderer.java b/src/java/org/apache/fop/render/xml/XMLRenderer.java index 504133c09..4ac650269 100644 --- a/src/java/org/apache/fop/render/xml/XMLRenderer.java +++ b/src/java/org/apache/fop/render/xml/XMLRenderer.java @@ -86,6 +86,7 @@ import org.apache.fop.render.Renderer; import org.apache.fop.render.RendererContext; import org.apache.fop.render.XMLHandler; import org.apache.fop.util.ColorUtil; +import org.apache.fop.util.XMLUtil; /** * Renderer that renders areas to XML for debugging purposes. @@ -157,6 +158,7 @@ public class XMLRenderer extends AbstractXMLRenderer { protected void addAreaAttributes(Area area) { addAttribute("ipd", area.getIPD()); addAttribute("bpd", area.getBPD()); + maybeAddLevelAttribute(area); if (isDetailedFormat()) { if (area.getIPD() != 0) { addAttribute("ipda", area.getAllocIPD()); @@ -708,7 +710,7 @@ public class XMLRenderer extends AbstractXMLRenderer { atts.clear(); addAreaAttributes(viewport); addTraitAttributes(viewport); - addAttribute("offset", viewport.getOffset()); + addAttribute("offset", viewport.getBlockProgressionOffset()); addAttribute("pos", viewport.getContentPosition()); if (viewport.hasClip()) { addAttribute("clip", "true"); @@ -770,7 +772,7 @@ public class XMLRenderer extends AbstractXMLRenderer { atts.clear(); addAreaAttributes(space); addTraitAttributes(space); - addAttribute("offset", space.getOffset()); + addAttribute("offset", space.getBlockProgressionOffset()); startElement("space", atts); endElement("space"); } @@ -787,7 +789,7 @@ public class XMLRenderer extends AbstractXMLRenderer { if (text.getTextLetterSpaceAdjust() != 0) { addAttribute("tlsadjust", text.getTextLetterSpaceAdjust()); } - addAttribute("offset", text.getOffset()); + addAttribute("offset", text.getBlockProgressionOffset()); addAttribute("baseline", text.getBaselineOffset()); addAreaAttributes(text); addTraitAttributes(text); @@ -802,7 +804,10 @@ public class XMLRenderer extends AbstractXMLRenderer { @Override protected void renderWord(WordArea word) { atts.clear(); - addAttribute("offset", word.getOffset()); + int offset = word.getBlockProgressionOffset(); + if ( offset != 0 ) { + addAttribute("offset", offset); + } int[] letterAdjust = word.getLetterAdjustArray(); if (letterAdjust != null) { StringBuffer sb = new StringBuffer(64); @@ -818,6 +823,11 @@ public class XMLRenderer extends AbstractXMLRenderer { addAttribute("letter-adjust", sb.toString()); } } + maybeAddLevelAttribute(word); + maybeAddPositionAdjustAttribute(word); + if ( word.isReversed() ) { + addAttribute("reversed", "true"); + } startElement("word", atts); characters(word.getWord()); endElement("word"); @@ -830,7 +840,11 @@ public class XMLRenderer extends AbstractXMLRenderer { @Override protected void renderSpace(SpaceArea space) { atts.clear(); - addAttribute("offset", space.getOffset()); + int offset = space.getBlockProgressionOffset(); + if ( offset != 0 ) { + addAttribute("offset", offset); + } + maybeAddLevelAttribute(space); if (!space.isAdjustable()) { addAttribute("adj", "false"); //default is true } @@ -848,7 +862,7 @@ public class XMLRenderer extends AbstractXMLRenderer { atts.clear(); addAreaAttributes(ip); addTraitAttributes(ip); - addAttribute("offset", ip.getOffset()); + addAttribute("offset", ip.getBlockProgressionOffset()); startElement("inlineparent", atts); super.renderInlineParent(ip); endElement("inlineparent"); @@ -862,7 +876,7 @@ public class XMLRenderer extends AbstractXMLRenderer { atts.clear(); addAreaAttributes(ibp); addTraitAttributes(ibp); - addAttribute("offset", ibp.getOffset()); + addAttribute("offset", ibp.getBlockProgressionOffset()); startElement("inlineblockparent", atts); super.renderInlineBlockParent(ibp); endElement("inlineblockparent"); @@ -876,7 +890,7 @@ public class XMLRenderer extends AbstractXMLRenderer { atts.clear(); addAreaAttributes(area); addTraitAttributes(area); - addAttribute("offset", area.getOffset()); + addAttribute("offset", area.getBlockProgressionOffset()); addAttribute("ruleStyle", area.getRuleStyleAsString()); addAttribute("ruleThickness", area.getRuleThickness()); startElement("leader", atts); @@ -889,5 +903,19 @@ public class XMLRenderer extends AbstractXMLRenderer { return XML_MIME_TYPE; } -} + private void maybeAddLevelAttribute ( Area a ) { + int level = a.getBidiLevel(); + if ( level >= 0 ) { + addAttribute ( "level", level ); + } + } + private void maybeAddPositionAdjustAttribute ( WordArea w ) { + int[][] adjustments = w.getGlyphPositionAdjustments(); + if ( adjustments != null ) { + addAttribute ( "position-adjust", XMLUtil.encodePositionAdjustments ( adjustments ) ); + } + } + + +} diff --git a/src/java/org/apache/fop/svg/PDFDocumentGraphics2D.java b/src/java/org/apache/fop/svg/PDFDocumentGraphics2D.java index 7ec9d7f2b..e91e2231c 100644 --- a/src/java/org/apache/fop/svg/PDFDocumentGraphics2D.java +++ b/src/java/org/apache/fop/svg/PDFDocumentGraphics2D.java @@ -159,7 +159,8 @@ public class PDFDocumentGraphics2D extends PDFGraphics2D { if (fontInfo == null) { //Default minimal fonts FontInfo fontInfo = new FontInfo(); - FontSetup.setup(fontInfo); + boolean base14Kerning = false; + FontSetup.setup(fontInfo, base14Kerning); setFontInfo(fontInfo); } } diff --git a/src/java/org/apache/fop/svg/PDFDocumentGraphics2DConfigurator.java b/src/java/org/apache/fop/svg/PDFDocumentGraphics2DConfigurator.java index 24974b01a..954291a57 100644 --- a/src/java/org/apache/fop/svg/PDFDocumentGraphics2DConfigurator.java +++ b/src/java/org/apache/fop/svg/PDFDocumentGraphics2DConfigurator.java @@ -46,9 +46,11 @@ public class PDFDocumentGraphics2DConfigurator { * Configures a PDFDocumentGraphics2D instance using an Avalon Configuration object. * @param graphics the PDFDocumentGraphics2D instance * @param cfg the configuration + * @param useComplexScriptFeatures true if complex script features enabled * @throws ConfigurationException if an error occurs while configuring the object */ - public void configure(PDFDocumentGraphics2D graphics, Configuration cfg) + public void configure(PDFDocumentGraphics2D graphics, Configuration cfg, + boolean useComplexScriptFeatures ) throws ConfigurationException { PDFDocument pdfDoc = graphics.getPDFDocument(); @@ -58,7 +60,7 @@ public class PDFDocumentGraphics2DConfigurator { //Fonts try { - FontInfo fontInfo = createFontInfo(cfg); + FontInfo fontInfo = createFontInfo(cfg, useComplexScriptFeatures); graphics.setFontInfo(fontInfo); } catch (FOPException e) { throw new ConfigurationException("Error while setting up fonts", e); @@ -68,13 +70,15 @@ public class PDFDocumentGraphics2DConfigurator { /** * Creates the {@link FontInfo} instance for the given configuration. * @param cfg the configuration + * @param useComplexScriptFeatures true if complex script features enabled * @return the font collection * @throws FOPException if an error occurs while setting up the fonts */ - public static FontInfo createFontInfo(Configuration cfg) throws FOPException { + public static FontInfo createFontInfo(Configuration cfg, boolean useComplexScriptFeatures) + throws FOPException { FontInfo fontInfo = new FontInfo(); final boolean strict = false; - FontResolver fontResolver = FontManager.createMinimalFontResolver(); + FontResolver fontResolver = FontManager.createMinimalFontResolver(useComplexScriptFeatures); //TODO The following could be optimized by retaining the FontManager somewhere FontManager fontManager = new FontManager(); if (cfg != null) { @@ -92,7 +96,8 @@ public class PDFDocumentGraphics2DConfigurator { = new FontInfoConfigurator(cfg, fontManager, fontResolver, listener, strict); List/*<EmbedFontInfo>*/ fontInfoList = new java.util.ArrayList/*<EmbedFontInfo>*/(); fontInfoConfigurator.configure(fontInfoList); - fontCollections.add(new CustomFontCollection(fontResolver, fontInfoList)); + fontCollections.add(new CustomFontCollection(fontResolver, fontInfoList, + fontResolver.isComplexScriptFeaturesEnabled())); } fontManager.setup(fontInfo, (FontCollection[])fontCollections.toArray( diff --git a/src/java/org/apache/fop/svg/PDFGraphics2D.java b/src/java/org/apache/fop/svg/PDFGraphics2D.java index 0d25e166c..429a1ea35 100644 --- a/src/java/org/apache/fop/svg/PDFGraphics2D.java +++ b/src/java/org/apache/fop/svg/PDFGraphics2D.java @@ -965,7 +965,8 @@ public class PDFGraphics2D extends AbstractGraphics2D implements NativeImageHand preparePainting(); FontInfo specialFontInfo = new FontInfo(); - FontSetup.setup(specialFontInfo); + boolean base14Kerning = false; + FontSetup.setup(specialFontInfo, base14Kerning); PDFResources res = pdfDoc.getFactory().makeResources(); PDFResourceContext context = new PDFResourceContext(res); diff --git a/src/java/org/apache/fop/svg/PDFTranscoder.java b/src/java/org/apache/fop/svg/PDFTranscoder.java index bf08b2fcf..8cf396ae5 100644 --- a/src/java/org/apache/fop/svg/PDFTranscoder.java +++ b/src/java/org/apache/fop/svg/PDFTranscoder.java @@ -126,7 +126,8 @@ public class PDFTranscoder extends AbstractFOPTranscoder { if (effCfg != null) { PDFDocumentGraphics2DConfigurator configurator = new PDFDocumentGraphics2DConfigurator(); - configurator.configure(graphics, effCfg); + boolean useComplexScriptFeatures = false; //TODO - FIX ME + configurator.configure(graphics, effCfg, useComplexScriptFeatures); } else { graphics.setupDefaultFontInfo(); } diff --git a/src/java/org/apache/fop/text/linebreak/LineBreakUtils.java b/src/java/org/apache/fop/text/linebreak/LineBreakUtils.java index 343191eea..d6e19a97a 100644 --- a/src/java/org/apache/fop/text/linebreak/LineBreakUtils.java +++ b/src/java/org/apache/fop/text/linebreak/LineBreakUtils.java @@ -100,35 +100,35 @@ public final class LineBreakUtils { lineBreakProperties[6] = new byte[] { 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 13, 13, 13, 13, 13, 13, 13, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 19, 0}; lineBreakProperties[7] = new byte[] { 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[9] = new byte[] { 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; - lineBreakProperties[10] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; + lineBreakProperties[10] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[11] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 0, 19, 4, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 4, 9, 2, 9, 9, 2, 9, 9, 12, 9, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - lineBreakProperties[12] = new byte[] { 2, 2, 2, 2, 0, 0, 2, 2, 2, 28, 28, 28, 19, 19, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 12, 0, 0, 12, 12, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 28, 26, 26, 2, 2, 2, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; - lineBreakProperties[13] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 12, 2, 9, 9, 9, 9, 9, 9, 9, 2, 9, 9, 9, 9, 9, 9, 9, 2, 2, 9, 9, 2, 9, 9, 9, 9, 2, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2}; + lineBreakProperties[12] = new byte[] { 2, 2, 2, 2, 0, 0, 2, 2, 2, 28, 28, 28, 19, 19, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 12, 0, 0, 12, 12, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 28, 26, 26, 2, 2, 2, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; + lineBreakProperties[13] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 12, 2, 9, 9, 9, 9, 9, 9, 9, 2, 2, 9, 9, 9, 9, 9, 9, 2, 2, 9, 9, 2, 9, 9, 9, 9, 2, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2}; lineBreakProperties[14] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[15] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 2, 19, 12, 2, 0, 0, 0, 0, 0}; - lineBreakProperties[16] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 9, 9, 9, 2, 9, 9, 9, 9, 9, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + lineBreakProperties[16] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 9, 9, 9, 2, 9, 9, 9, 9, 9, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[17] = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - lineBreakProperties[18] = new byte[] { 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 9, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 2, 9, 9, 9, 9, 9, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 4, 4, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2}; + lineBreakProperties[18] = new byte[] { 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 4, 4, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[19] = new byte[] { 0, 9, 9, 9, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 0, 0, 2, 2, 2, 2, 0, 0, 9, 2, 9, 9, 9, 9, 9, 9, 9, 0, 0, 9, 9, 0, 0, 9, 9, 9, 2, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 2, 2, 0, 2, 2, 2, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 28, 28, 2, 2, 2, 2, 2, 28, 2, 29, 0, 0, 0, 0}; lineBreakProperties[20] = new byte[] { 0, 9, 9, 9, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 0, 2, 2, 0, 2, 2, 0, 0, 9, 0, 9, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 9, 9, 9, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 9, 9, 2, 2, 2, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[21] = new byte[] { 0, 9, 9, 9, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 9, 2, 9, 9, 9, 9, 9, 9, 9, 9, 0, 9, 9, 9, 0, 9, 9, 9, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - lineBreakProperties[22] = new byte[] { 0, 9, 9, 9, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 9, 2, 9, 9, 9, 9, 9, 9, 9, 0, 0, 9, 9, 0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 2, 2, 0, 2, 2, 2, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + lineBreakProperties[22] = new byte[] { 0, 9, 9, 9, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 9, 2, 9, 9, 9, 9, 9, 9, 9, 0, 0, 9, 9, 0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 2, 2, 0, 2, 2, 2, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[23] = new byte[] { 0, 0, 9, 2, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 0, 2, 2, 0, 2, 0, 2, 2, 0, 0, 0, 2, 2, 0, 0, 0, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 9, 9, 9, 9, 9, 0, 0, 0, 9, 9, 9, 0, 9, 9, 9, 9, 0, 0, 2, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2, 2, 2, 2, 29, 2, 0, 0, 0, 0, 0}; lineBreakProperties[24] = new byte[] { 0, 9, 9, 9, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 0, 2, 9, 9, 9, 9, 9, 9, 9, 0, 9, 9, 9, 0, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 9, 9, 0, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[25] = new byte[] { 0, 0, 9, 9, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 0, 9, 2, 9, 9, 9, 9, 9, 9, 9, 0, 9, 9, 9, 0, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 2, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - lineBreakProperties[26] = new byte[] { 0, 0, 9, 9, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 9, 9, 9, 9, 9, 9, 9, 0, 9, 9, 9, 0, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2, 0, 0, 0, 28, 2, 2, 2, 2, 2, 2}; + lineBreakProperties[26] = new byte[] { 0, 0, 9, 9, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 9, 9, 9, 9, 9, 9, 9, 0, 9, 9, 9, 0, 9, 9, 9, 9, 2, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2, 0, 0, 0, 28, 2, 2, 2, 2, 2, 2}; lineBreakProperties[27] = new byte[] { 0, 0, 9, 9, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 9, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 0, 9, 0, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[28] = new byte[] { 0, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 29, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[29] = new byte[] { 0, 31, 31, 0, 31, 0, 0, 31, 31, 0, 31, 0, 0, 31, 0, 0, 0, 0, 0, 0, 31, 31, 31, 31, 0, 31, 31, 31, 31, 31, 31, 31, 0, 31, 31, 31, 0, 31, 0, 31, 0, 0, 31, 31, 0, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 31, 31, 31, 0, 0, 31, 31, 31, 31, 31, 0, 31, 0, 31, 31, 31, 31, 31, 31, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 31, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[30] = new byte[] { 2, 5, 5, 5, 5, 2, 5, 5, 13, 5, 5, 4, 13, 12, 12, 12, 12, 12, 13, 2, 12, 2, 2, 2, 9, 9, 2, 2, 2, 2, 2, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 9, 2, 9, 2, 9, 27, 8, 27, 8, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 4}; - lineBreakProperties[31] = new byte[] { 9, 9, 9, 9, 9, 4, 9, 9, 2, 2, 2, 2, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 4, 4, 2, 2, 2, 2, 2, 2, 9, 2, 2, 2, 2, 2, 2, 0, 2, 2, 5, 5, 4, 5, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + lineBreakProperties[31] = new byte[] { 9, 9, 9, 9, 9, 4, 9, 9, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 4, 4, 2, 2, 2, 2, 2, 2, 9, 2, 2, 2, 2, 2, 2, 0, 2, 2, 5, 5, 4, 5, 2, 2, 2, 2, 2, 13, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[32] = new byte[] { 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 4, 4, 2, 2, 2, 2, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31}; lineBreakProperties[33] = new byte[] { 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 31, 31, 31, 31, 31, 31, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0}; lineBreakProperties[34] = new byte[] { 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22}; lineBreakProperties[35] = new byte[] { 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21}; lineBreakProperties[36] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[37] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; - lineBreakProperties[38] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 9, 2, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0}; + lineBreakProperties[38] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 9, 9, 9, 2, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0}; lineBreakProperties[39] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[40] = new byte[] { 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[45] = new byte[] { 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; @@ -137,24 +137,24 @@ public final class LineBreakUtils { lineBreakProperties[48] = new byte[] { 2, 2, 12, 12, 4, 4, 5, 2, 12, 12, 2, 9, 9, 9, 13, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[49] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[50] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 2, 0, 0, 0, 12, 12, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 31, 31, 31, 31, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - lineBreakProperties[51] = new byte[] { 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 31, 31, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; + lineBreakProperties[51] = new byte[] { 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 31, 0, 0, 0, 31, 31, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[52] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 0, 0, 2, 2, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 9}; lineBreakProperties[53] = new byte[] { 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 0, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 0, 0, 0, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[54] = new byte[] { 9, 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 4, 4, 2, 4, 4, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0}; - lineBreakProperties[55] = new byte[] { 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 2, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + lineBreakProperties[55] = new byte[] { 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 2, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2}; lineBreakProperties[56] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 4, 4, 4, 4, 4, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 2, 2, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4}; lineBreakProperties[57] = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 2, 9, 2, 2, 2, 2, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - lineBreakProperties[59] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9}; + lineBreakProperties[59] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9}; lineBreakProperties[62] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 2, 0, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0}; lineBreakProperties[63] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 5, 2, 0}; lineBreakProperties[64] = new byte[] { 4, 4, 4, 4, 4, 4, 4, 13, 4, 4, 4, 37, 9, 9, 9, 9, 4, 13, 4, 4, 3, 1, 1, 2, 30, 30, 27, 30, 30, 30, 27, 30, 1, 1, 2, 2, 18, 18, 18, 4, 6, 6, 9, 9, 9, 9, 9, 13, 28, 28, 28, 28, 28, 28, 28, 28, 2, 30, 30, 1, 25, 25, 2, 2, 2, 2, 2, 2, 19, 27, 8, 25, 25, 25, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 2, 4, 4, 4, 4, 2, 4, 4, 4, 35, 2, 2, 2, 2, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 2, 2, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 1}; - lineBreakProperties[65] = new byte[] { 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 29, 29, 29, 29, 29, 29, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 28, 29, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + lineBreakProperties[65] = new byte[] { 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 29, 29, 29, 29, 29, 29, 29, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 28, 29, 29, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[66] = new byte[] { 2, 2, 2, 28, 2, 1, 2, 2, 2, 28, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 29, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 1, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2}; lineBreakProperties[67] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[68] = new byte[] { 1, 2, 1, 1, 2, 2, 2, 1, 1, 2, 2, 1, 2, 2, 2, 1, 2, 1, 29, 29, 2, 1, 2, 2, 2, 2, 1, 2, 2, 1, 1, 1, 1, 2, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 1, 1, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[69] = new byte[] { 2, 2, 1, 1, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[70] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; - lineBreakProperties[71] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + lineBreakProperties[71] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[72] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; lineBreakProperties[73] = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2}; } @@ -163,14 +163,14 @@ public final class LineBreakUtils { lineBreakProperties[74] = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[75] = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[76] = new byte[] { 2, 2, 2, 2, 2, 1, 1, 2, 2, 1, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; - lineBreakProperties[77] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - lineBreakProperties[78] = new byte[] { 0, 2, 2, 2, 2, 0, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 0, 0, 0, 2, 1, 2, 2, 2, 30, 30, 30, 30, 0, 0, 2, 12, 12, 2, 2, 2, 2, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - lineBreakProperties[79] = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 27, 8, 2, 2, 2, 2, 0, 2, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; + lineBreakProperties[77] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + lineBreakProperties[78] = new byte[] { 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 30, 30, 30, 30, 2, 2, 2, 12, 12, 2, 2, 2, 2, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + lineBreakProperties[79] = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 2, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[83] = new byte[] { 2, 2, 2, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 27, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 2, 2}; lineBreakProperties[86] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[88] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[89] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 12, 4, 4, 4, 2, 12, 4}; - lineBreakProperties[90] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + lineBreakProperties[90] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9}; lineBreakProperties[91] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9}; lineBreakProperties[92] = new byte[] { 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 4, 4, 4, 4, 4, 4, 4, 4, 2, 4, 27, 4, 2, 2, 30, 30, 2, 2, 30, 30, 27, 8, 27, 8, 27, 8, 27, 8, 4, 4, 4, 4, 12, 2, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[93] = new byte[] { 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; @@ -179,21 +179,22 @@ public final class LineBreakUtils { lineBreakProperties[96] = new byte[] { 17, 8, 8, 17, 17, 25, 17, 17, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 17, 17, 27, 8, 27, 8, 27, 8, 27, 8, 25, 27, 8, 8, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 9, 9, 9, 9, 9, 9, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 25, 17, 17, 17, 0, 25, 17, 25, 17, 25, 17, 25, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17}; lineBreakProperties[97] = new byte[] { 17, 17, 17, 25, 17, 25, 17, 25, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 25, 25, 0, 0, 9, 9, 25, 25, 25, 25, 17, 25, 25, 17, 25, 17, 25, 17, 25, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 25, 17, 25, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 25, 25, 17, 17, 17, 17, 25, 25, 25, 25, 17}; lineBreakProperties[98] = new byte[] { 0, 0, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17}; - lineBreakProperties[99] = new byte[] { 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25}; + lineBreakProperties[99] = new byte[] { 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25}; lineBreakProperties[100] = new byte[] { 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 1, 1, 1, 1, 1, 1, 1, 1, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17}; lineBreakProperties[101] = new byte[] { 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0}; lineBreakProperties[155] = new byte[] { 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[320] = new byte[] { 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 25, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17}; lineBreakProperties[329] = new byte[] { 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4}; - lineBreakProperties[332] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 12, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 2, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 2, 2}; + lineBreakProperties[332] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 12, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 2, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 2, 2}; lineBreakProperties[333] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 2, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0}; - lineBreakProperties[335] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2}; + lineBreakProperties[335] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2}; lineBreakProperties[336] = new byte[] { 2, 2, 9, 2, 2, 2, 9, 2, 2, 2, 2, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 2, 2, 2, 2, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 28, 2, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 5, 5, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[337] = new byte[] { 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0}; lineBreakProperties[338] = new byte[] { 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 0, 0, 0}; lineBreakProperties[339] = new byte[] { 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 2, 2, 2, 2, 2, 4, 4, 4, 2, 2, 2, 2, 0, 2, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[340] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 9, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 2, 4, 4, 4, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0}; lineBreakProperties[341] = new byte[] { 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 31, 31, 31, 31, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + lineBreakProperties[342] = new byte[] { 0, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; lineBreakProperties[343] = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 9, 9, 9, 9, 9, 9, 9, 9, 4, 9, 9, 0, 0, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 0, 0, 0, 0, 0, 0}; lineBreakProperties[344] = new byte[] { 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}; lineBreakProperties[345] = new byte[] { 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 15, 15, 15}; @@ -206,7 +207,7 @@ public final class LineBreakUtils { lineBreakProperties[432] = new byte[] { 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; lineBreakProperties[448] = new byte[] { 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36, 36}; lineBreakProperties[502] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 0, 2, 0, 2, 2, 0, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; - lineBreakProperties[503] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; + lineBreakProperties[503] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[506] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 27, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; lineBreakProperties[507] = new byte[] { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 28, 2, 0, 0}; lineBreakProperties[508] = new byte[] { 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 19, 8, 8, 19, 19, 12, 12, 27, 8, 18, 0, 0, 0, 0, 0, 0, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 17, 17, 17, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 27, 8, 17, 17, 27, 8, 17, 17, 17, 17, 17, 17, 17, 8, 17, 8, 0, 25, 25, 12, 12, 17, 27, 8, 27, 8, 27, 8, 17, 17, 17, 17, 17, 17, 17, 17, 0, 17, 29, 28, 17, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}; @@ -462,7 +463,6 @@ public final class LineBreakUtils { lineBreakProperties[330] = lineBreakProperties[2]; lineBreakProperties[331] = lineBreakProperties[2]; lineBreakProperties[334] = lineBreakProperties[2]; - lineBreakProperties[342] = lineBreakProperties[17]; lineBreakProperties[351] = lineBreakProperties[344]; lineBreakProperties[352] = lineBreakProperties[345]; lineBreakProperties[353] = lineBreakProperties[346]; diff --git a/src/java/org/apache/fop/traits/Direction.java b/src/java/org/apache/fop/traits/Direction.java new file mode 100644 index 000000000..5eb36058e --- /dev/null +++ b/src/java/org/apache/fop/traits/Direction.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.traits; + +import java.io.ObjectStreamException; + +import org.apache.fop.fo.Constants; + +/** + * Enumeration class for direction traits, namely {inline,block}-progression-direction + * and shift-direction. + */ +public final class Direction extends TraitEnum { + + private static final long serialVersionUID = 1L; + + private static final String[] DIRECTION_NAMES = new String[] + {"lr", "rl", "tb", "bt"}; + + private static final int[] DIRECTION_VALUES = new int[] + {Constants.EN_LR, Constants.EN_RL, Constants.EN_TB, Constants.EN_BT}; + + /** direction: left-to-right */ + public static final Direction LR = new Direction(0); + /** direction: right-to-left */ + public static final Direction RL = new Direction(1); + /** direction: top-to-bottom */ + public static final Direction TB = new Direction(2); + /** direction: bottom-to-top */ + public static final Direction BT = new Direction(3); + + private static final Direction[] DIRECTIONS = new Direction[] {LR, RL, TB, BT}; + + private Direction(int index) { + super(DIRECTION_NAMES[index], DIRECTION_VALUES[index]); + } + + /** + * Determine if direction is vertical or not. + * @return true if vertical + */ + public boolean isVertical() { + return ( getEnumValue() == Constants.EN_TB ) || ( getEnumValue() == Constants.EN_BT ); + } + + /** + * Determine if direction is horizontal or not. + * @return true if horizontal + */ + public boolean isHorizontal() { + return ( getEnumValue() == Constants.EN_LR ) || ( getEnumValue() == Constants.EN_RL ); + } + + /** + * Returns the enumeration/singleton object based on its name. + * @param name the name of the enumeration value + * @return the enumeration object + */ + public static Direction valueOf(String name) { + for (int i = 0; i < DIRECTIONS.length; i++) { + if (DIRECTIONS[i].getName().equalsIgnoreCase(name)) { + return DIRECTIONS[i]; + } + } + throw new IllegalArgumentException("Illegal direction: " + name); + } + + /** + * Returns the enumeration/singleton object based on its name. + * @param enumValue the enumeration value + * @return the enumeration object + */ + public static Direction valueOf(int enumValue) { + for (int i = 0; i < DIRECTIONS.length; i++) { + if (DIRECTIONS[i].getEnumValue() == enumValue) { + return DIRECTIONS[i]; + } + } + throw new IllegalArgumentException("Illegal direction: " + enumValue); + } + + private Object readResolve() throws ObjectStreamException { + return valueOf(getName()); + } + + /** {@inheritDoc} */ + public String toString() { + return getName(); + } + +} diff --git a/src/java/org/apache/fop/traits/WritingMode.java b/src/java/org/apache/fop/traits/WritingMode.java new file mode 100644 index 000000000..5b12bb127 --- /dev/null +++ b/src/java/org/apache/fop/traits/WritingMode.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.traits; + +import java.io.ObjectStreamException; + +import org.apache.fop.fo.Constants; + +/** Enumeration class for writing mode trait. */ +public final class WritingMode extends TraitEnum { + + private static final long serialVersionUID = 1L; + + private static final String[] WRITING_MODE_NAMES = new String[] + {"lr-tb", "rl-tb", "tb-lr", "tb-rl"}; + + private static final int[] WRITING_MODE_VALUES = new int[] + {Constants.EN_LR_TB, Constants.EN_RL_TB, Constants.EN_TB_LR, Constants.EN_TB_RL}; + + /** writing mode: lr-tb */ + public static final WritingMode LR_TB = new WritingMode(0); + /** writing mode: rl-tb */ + public static final WritingMode RL_TB = new WritingMode(1); + /** writing mode: tb-lr */ + public static final WritingMode TB_LR = new WritingMode(2); + /** writing mode: tb-rl */ + public static final WritingMode TB_RL = new WritingMode(3); + + private static final WritingMode[] WRITING_MODES + = new WritingMode[] {LR_TB, RL_TB, TB_LR, TB_RL}; + + private WritingMode(int index) { + super(WRITING_MODE_NAMES[index], WRITING_MODE_VALUES[index]); + } + + /** + * Assign writing mode traits from this trait to the specified + * writing mode traits setter. + * @param wms a writing mode traits setter + */ + public void assignWritingModeTraits ( WritingModeTraitsSetter wms ) { + Direction inlineProgressionDirection; + Direction blockProgressionDirection; + Direction columnProgressionDirection; + Direction rowProgressionDirection; + Direction shiftDirection; + switch ( getEnumValue() ) { + default: + case Constants.EN_LR_TB: + inlineProgressionDirection = Direction.LR; + blockProgressionDirection = Direction.TB; + columnProgressionDirection = Direction.LR; + rowProgressionDirection = Direction.TB; + shiftDirection = Direction.BT; + break; + case Constants.EN_RL_TB: + inlineProgressionDirection = Direction.RL; + blockProgressionDirection = Direction.TB; + columnProgressionDirection = Direction.RL; + rowProgressionDirection = Direction.TB; + shiftDirection = Direction.BT; + break; + case Constants.EN_TB_LR: + inlineProgressionDirection = Direction.TB; + blockProgressionDirection = Direction.LR; + columnProgressionDirection = Direction.TB; + rowProgressionDirection = Direction.LR; + shiftDirection = Direction.RL; + break; + case Constants.EN_TB_RL: + inlineProgressionDirection = Direction.TB; + blockProgressionDirection = Direction.RL; + columnProgressionDirection = Direction.TB; + rowProgressionDirection = Direction.RL; + shiftDirection = Direction.LR; + break; + } + wms.setInlineProgressionDirection ( inlineProgressionDirection ); + wms.setBlockProgressionDirection ( blockProgressionDirection ); + wms.setColumnProgressionDirection ( columnProgressionDirection ); + wms.setRowProgressionDirection ( rowProgressionDirection ); + wms.setShiftDirection ( shiftDirection ); + wms.setWritingMode ( this ); + } + + /** + * Determine if WM is horizontal or not. + * @return true if horizontal + */ + public boolean isHorizontal() { + switch ( getEnumValue() ) { + case Constants.EN_LR_TB: + case Constants.EN_RL_TB: + return true; + case Constants.EN_TB_LR: + case Constants.EN_TB_RL: + return false; + default: + assert false; + return true; + } + } + + /** + * Determine if WM is vertical or not. + * @return true if vertical + */ + public boolean isVertical() { + return !isHorizontal(); + } + + /** + * Returns the enumeration/singleton object based on its name. + * @param name the name of the enumeration value + * @return the enumeration object + */ + public static WritingMode valueOf(String name) { + for (int i = 0; i < WRITING_MODES.length; i++) { + if (WRITING_MODES[i].getName().equalsIgnoreCase(name)) { + return WRITING_MODES[i]; + } + } + throw new IllegalArgumentException("Illegal writing mode: " + name); + } + + /** + * Returns the enumeration/singleton object based on its name. + * @param enumValue the enumeration value + * @return the enumeration object + */ + public static WritingMode valueOf(int enumValue) { + for (int i = 0; i < WRITING_MODES.length; i++) { + if (WRITING_MODES[i].getEnumValue() == enumValue) { + return WRITING_MODES[i]; + } + } + throw new IllegalArgumentException("Illegal writing mode: " + enumValue); + } + + private Object readResolve() throws ObjectStreamException { + return valueOf(getName()); + } + + /** {@inheritDoc} */ + public String toString() { + return getName(); + } + +} diff --git a/src/java/org/apache/fop/traits/WritingModeTraits.java b/src/java/org/apache/fop/traits/WritingModeTraits.java new file mode 100644 index 000000000..c96cd73c3 --- /dev/null +++ b/src/java/org/apache/fop/traits/WritingModeTraits.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.traits; + +/** + * This class provides a reusable implementation of the WritingModeTraitsSetter + * interface. + */ +public class WritingModeTraits implements WritingModeTraitsSetter { + + private Direction inlineProgressionDirection; + private Direction blockProgressionDirection; + private Direction columnProgressionDirection; + private Direction rowProgressionDirection; + private Direction shiftDirection; + private WritingMode writingMode; + + /** + * Default writing mode traits constructor. + */ + public WritingModeTraits() { + this ( WritingMode.LR_TB ); + } + + /** + * Construct writing mode traits using the specified writing mode. + * @param writingMode a writing mode traits object + */ + public WritingModeTraits ( WritingMode writingMode ) { + assignWritingModeTraits ( writingMode ); + } + + /** + * @return the "inline-progression-direction" trait. + */ + public Direction getInlineProgressionDirection() { + return inlineProgressionDirection; + } + + /** + * @param direction the "inline-progression-direction" trait. + */ + public void setInlineProgressionDirection ( Direction direction ) { + this.inlineProgressionDirection = direction; + } + + /** + * @return the "block-progression-direction" trait. + */ + public Direction getBlockProgressionDirection() { + return blockProgressionDirection; + } + + /** + * @param direction the "block-progression-direction" trait. + */ + public void setBlockProgressionDirection ( Direction direction ) { + this.blockProgressionDirection = direction; + } + + /** + * @return the "column-progression-direction" trait. + */ + public Direction getColumnProgressionDirection() { + return columnProgressionDirection; + } + + /** + * @param direction the "column-progression-direction" trait. + */ + public void setColumnProgressionDirection ( Direction direction ) { + this.columnProgressionDirection = direction; + } + + /** + * @return the "row-progression-direction" trait. + */ + public Direction getRowProgressionDirection() { + return rowProgressionDirection; + } + + /** + * @param direction the "row-progression-direction" trait. + */ + public void setRowProgressionDirection ( Direction direction ) { + this.rowProgressionDirection = direction; + } + + /** + * @return the "shift-direction" trait. + */ + public Direction getShiftDirection() { + return shiftDirection; + } + + /** + * @param direction the "shift-direction" trait. + */ + public void setShiftDirection ( Direction direction ) { + this.shiftDirection = direction; + } + + /** + * @return the "writing-mode" trait. + */ + public WritingMode getWritingMode() { + return writingMode; + } + + /** + * @param writingMode the "writing-mode" trait. + */ + public void setWritingMode ( WritingMode writingMode ) { + this.writingMode = writingMode; + } + + /** + * @param writingMode the "writing-mode" trait. + */ + public void assignWritingModeTraits ( WritingMode writingMode ) { + writingMode.assignWritingModeTraits ( this ); + } + + /** + * Helper function to find the writing mode traits getter (if any) that applies for + * a given FO node. + * @param fn the node to start searching from + * @return the applicable writing mode traits getter, or null if none applies + */ + public static WritingModeTraitsGetter + getWritingModeTraitsGetter ( org.apache.fop.fo.FONode fn ) { + for ( org.apache.fop.fo.FONode n = fn; n != null; n = n.getParent() ) { + if ( n instanceof WritingModeTraitsGetter ) { + return (WritingModeTraitsGetter) n; + } + } + return null; + } + +} diff --git a/src/java/org/apache/fop/traits/WritingModeTraitsGetter.java b/src/java/org/apache/fop/traits/WritingModeTraitsGetter.java new file mode 100644 index 000000000..a67e437c9 --- /dev/null +++ b/src/java/org/apache/fop/traits/WritingModeTraitsGetter.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.traits; + +/** + * This interface provides read access to FO traits related to writing mode. + */ +public interface WritingModeTraitsGetter { + + /** + * @return the "inline-progression-direction" trait + */ + Direction getInlineProgressionDirection(); + + /** + * @return the "block-progression-direction" trait + */ + Direction getBlockProgressionDirection(); + + /** + * @return the "column-progression-direction" trait + */ + Direction getColumnProgressionDirection(); + + /** + * @return the "row-progression-direction" trait + */ + + Direction getRowProgressionDirection(); + + /** + * @return the "shift-direction" trait + */ + Direction getShiftDirection(); + + /** + * @return the "writing-mode" trait + */ + WritingMode getWritingMode(); + +} diff --git a/src/java/org/apache/fop/traits/WritingModeTraitsSetter.java b/src/java/org/apache/fop/traits/WritingModeTraitsSetter.java new file mode 100644 index 000000000..1b94e22de --- /dev/null +++ b/src/java/org/apache/fop/traits/WritingModeTraitsSetter.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.traits; + +/** + * This interface provides read and assignment access to FO traits related to writing mode. + */ +public interface WritingModeTraitsSetter extends WritingModeTraitsGetter { + + /** + * Set value of inline-progression-direction trait. + * @param direction the "inline-progression-direction" trait + */ + void setInlineProgressionDirection ( Direction direction ); + + /** + * Set value of block-progression-direction trait. + * @param direction the "block-progression-direction" trait + */ + void setBlockProgressionDirection ( Direction direction ); + + /** + * Set value of column-progression-direction trait. + * @param direction the "column-progression-direction" trait + */ + void setColumnProgressionDirection ( Direction direction ); + + /** + * Set value of row-progression-direction trait. + * @param direction the "row-progression-direction" trait + */ + void setRowProgressionDirection ( Direction direction ); + + /** + * Set value of shift-direction trait. + * @param direction the "shift-direction" trait + */ + void setShiftDirection ( Direction direction ); + + /** + * Set value of writing-mode trait. + * @param writingMode the "writing-mode" trait + */ + void setWritingMode ( WritingMode writingMode ); + + /** + * Collectivelly assign values to all writing mode traits based upon a specific + * writing mode. + * @param writingMode the "writing-mode" trait + */ + void assignWritingModeTraits ( WritingMode writingMode ); + +} diff --git a/src/java/org/apache/fop/util/CharUtilities.java b/src/java/org/apache/fop/util/CharUtilities.java index 7786552ff..8b90e3d25 100644 --- a/src/java/org/apache/fop/util/CharUtilities.java +++ b/src/java/org/apache/fop/util/CharUtilities.java @@ -74,6 +74,20 @@ public class CharUtilities { public static final char WORD_JOINER = '\u2060'; /** zero-width joiner */ public static final char ZERO_WIDTH_JOINER = '\u200D'; + /** left-to-right mark */ + public static final char LRM = '\u200E'; + /** right-to-left mark */ + public static final char RLM = '\u202F'; + /** left-to-right embedding */ + public static final char LRE = '\u202A'; + /** right-to-left embedding */ + public static final char RLE = '\u202B'; + /** pop directional formatting */ + public static final char PDF = '\u202C'; + /** left-to-right override */ + public static final char LRO = '\u202D'; + /** right-to-left override */ + public static final char RLO = '\u202E'; /** zero-width no-break space (= byte order mark) */ public static final char ZERO_WIDTH_NOBREAK_SPACE = '\uFEFF'; /** soft hyphen */ @@ -86,10 +100,11 @@ public class CharUtilities { public static final char MISSING_IDEOGRAPH = '\u25A1'; /** Ideogreaphic space */ public static final char IDEOGRAPHIC_SPACE = '\u3000'; + /** Object replacement character */ + public static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC'; /** Unicode value indicating the the character is "not a character". */ public static final char NOT_A_CHARACTER = '\uFFFF'; - /** * Utility class: Constructor prevents instantiating when subclassed. */ @@ -103,7 +118,7 @@ public class CharUtilities { * @param c character to inspect * @return the determined character class */ - public static int classOf(char c) { + public static int classOf ( int c ) { switch (c) { case CODE_EOT: return EOT; @@ -126,7 +141,7 @@ public class CharUtilities { * @param c character to inspect * @return True if the character is a normal space */ - public static boolean isBreakableSpace(char c) { + public static boolean isBreakableSpace ( int c ) { return (c == SPACE || isFixedWidthSpace(c)); } @@ -135,7 +150,7 @@ public class CharUtilities { * @param c the character to check * @return true if the character is a zero-width space */ - public static boolean isZeroWidthSpace(char c) { + public static boolean isZeroWidthSpace ( int c ) { return c == ZERO_WIDTH_SPACE // 200Bh || c == WORD_JOINER // 2060h || c == ZERO_WIDTH_NOBREAK_SPACE; // FEFFh (also used as BOM) @@ -146,7 +161,7 @@ public class CharUtilities { * @param c the character to check * @return true if the character has a fixed-width */ - public static boolean isFixedWidthSpace(char c) { + public static boolean isFixedWidthSpace ( int c ) { return (c >= '\u2000' && c <= '\u200B') || c == '\u3000'; // c == '\u2000' // en quad @@ -170,7 +185,7 @@ public class CharUtilities { * @param c character to check * @return True if the character is a nbsp */ - public static boolean isNonBreakableSpace(char c) { + public static boolean isNonBreakableSpace ( int c ) { return (c == NBSPACE // no-break space || c == '\u202F' // narrow no-break space @@ -185,7 +200,7 @@ public class CharUtilities { * @param c character to check * @return True if the character is adjustable */ - public static boolean isAdjustableSpace(char c) { + public static boolean isAdjustableSpace ( int c ) { //TODO: are there other kinds of adjustable spaces? return (c == '\u0020' // normal space @@ -197,19 +212,19 @@ public class CharUtilities { * @param c character to check * @return True if the character represents any kind of space */ - public static boolean isAnySpace(char c) { + public static boolean isAnySpace ( int c ) { return (isBreakableSpace(c) || isNonBreakableSpace(c)); } /** * Indicates whether a character is classified as "Alphabetic" by the Unicode standard. - * @param ch the character + * @param c the character * @return true if the character is "Alphabetic" */ - public static boolean isAlphabetic(char ch) { + public static boolean isAlphabetic ( int c ) { //http://www.unicode.org/Public/UNIDATA/UCD.html#Alphabetic //Generated from: Other_Alphabetic + Lu + Ll + Lt + Lm + Lo + Nl - int generalCategory = Character.getType(ch); + int generalCategory = Character.getType((char)c); switch (generalCategory) { case Character.UPPERCASE_LETTER: //Lu case Character.LOWERCASE_LETTER: //Ll @@ -227,15 +242,120 @@ public class CharUtilities { /** * Indicates whether the given character is an explicit break-character - * @param ch the character to check + * @param c the character to check * @return true if the character represents an explicit break */ - public static boolean isExplicitBreak(char ch) { - return (ch == LINEFEED_CHAR - || ch == CARRIAGE_RETURN - || ch == NEXT_LINE - || ch == LINE_SEPARATOR - || ch == PARAGRAPH_SEPARATOR); + public static boolean isExplicitBreak ( int c ) { + return (c == LINEFEED_CHAR + || c == CARRIAGE_RETURN + || c == NEXT_LINE + || c == LINE_SEPARATOR + || c == PARAGRAPH_SEPARATOR); + } + + /** + * Convert a single unicode scalar value to an XML numeric character + * reference. If in the BMP, four digits are used, otherwise 6 digits are used. + * @param c a unicode scalar value + * @return a string representing a numeric character reference + */ + public static String charToNCRef ( int c ) { + StringBuffer sb = new StringBuffer(); + for ( int i = 0, nDigits = ( c > 0xFFFF ) ? 6 : 4; i < nDigits; i++, c >>= 4 ) { + int d = c & 0xF; + char hd; + if ( d < 10 ) { + hd = (char) ( (int) '0' + d ); + } else { + hd = (char) ( (int) 'A' + ( d - 10 ) ); + } + sb.append ( hd ); + } + return "&#x" + sb.reverse() + ";"; + } + + /** + * Convert a string to a sequence of ASCII or XML numeric character references. + * @param s a java string (encoded in UTF-16) + * @return a string representing a sequence of numeric character reference or + * ASCII characters + * @author Glenn Adams + */ + public static String toNCRefs ( String s ) { + StringBuffer sb = new StringBuffer(); + if ( s != null ) { + for ( int i = 0; i < s.length(); i++ ) { + char c = s.charAt(i); + if ( ( c >= 32 ) && ( c < 127 ) ) { + if ( c == '<' ) { + sb.append ( "<" ); + } else if ( c == '>' ) { + sb.append ( ">" ); + } else if ( c == '&' ) { + sb.append ( "&" ); + } else { + sb.append ( c ); + } + } else { + sb.append ( charToNCRef ( c ) ); + } + } + } + return sb.toString(); + } + + /** + * Pad a string S on left out to width W using padding character PAD. + * @param s string to pad + * @param width width of field to add padding + * @param pad character to use for padding + * @return padded string + * @author Glenn Adams + */ + public static String padLeft ( String s, int width, char pad ) { + StringBuffer sb = new StringBuffer(); + for ( int i = s.length(); i < width; i++ ) { + sb.append(pad); + } + sb.append ( s ); + return sb.toString(); + } + + /** + * Format character for debugging output, which it is prefixed with "0x", padded left with '0' + * and either 4 or 6 hex characters in width according to whether it is in the BMP or not. + * @param c character code + * @return formatted character string + * @author Glenn Adams + */ + public static String format ( int c ) { + if ( c < 1114112 ) { + return "0x" + padLeft ( Integer.toString ( c, 16 ), ( c < 65536 ) ? 4 : 6, '0' ); + } else { + return "!NOT A CHARACTER!"; + } } -} + /** + * Determine if two character sequences contain the same characters. + * @param cs1 first character sequence + * @param cs2 second character sequence + * @return true if both sequences have same length and same character sequence + * @author Glenn Adams + */ + public static boolean isSameSequence ( CharSequence cs1, CharSequence cs2 ) { + assert cs1 != null; + assert cs2 != null; + if ( cs1.length() != cs2.length() ) { + return false; + } else { + for ( int i = 0, n = cs1.length(); i < n; i++ ) { + if ( cs1.charAt(i) != cs2.charAt(i) ) { + return false; + } + } + return true; + } + } + +} diff --git a/src/java/org/apache/fop/util/XMLUtil.java b/src/java/org/apache/fop/util/XMLUtil.java index d4397c2c7..e91b6eb5d 100644 --- a/src/java/org/apache/fop/util/XMLUtil.java +++ b/src/java/org/apache/fop/util/XMLUtil.java @@ -173,4 +173,126 @@ public final class XMLUtil implements XMLConstants { atts.addAttribute("", localName, localName, XMLUtil.CDATA, value); } + /** + * Encode a glyph position adjustments array as a string, where the string value + * adheres to the following syntax: + * + * count ( 'Z' repeat | number ) + * + * where each token is separated by whitespace, except that 'Z' followed by repeat + * are considered to be a single token with no intervening whitespace, and where + * 'Z' repeat encodes repeated zeroes. + * @param dp the adjustments array + * @param paCount the number of entries to encode from adjustments array + * @return the encoded value + */ + public static String encodePositionAdjustments ( int[][] dp, int paCount ) { + assert dp != null; + StringBuffer sb = new StringBuffer(); + int na = paCount; + int nz = 0; + sb.append ( na ); + for ( int i = 0; i < na; i++ ) { + int[] pa = dp [ i ]; + for ( int k = 0; k < 4; k++ ) { + int a = pa [ k ]; + if ( a != 0 ) { + encodeNextAdjustment ( sb, nz, a ); nz = 0; + } else { + nz++; + } + } + } + encodeNextAdjustment ( sb, nz, 0 ); + return sb.toString(); + } + + /** + * Encode a glyph position adjustments array as a string, where the string value + * adheres to the following syntax: + * + * count ( 'Z' repeat | number ) + * + * where each token is separated by whitespace, except that 'Z' followed by repeat + * are considered to be a single token with no intervening whitespace. + * @param dp the adjustments array + * @return the encoded value + */ + public static String encodePositionAdjustments ( int[][] dp ) { + assert dp != null; + return encodePositionAdjustments ( dp, dp.length ); + } + + private static void encodeNextAdjustment ( StringBuffer sb, int nz, int a ) { + encodeZeroes ( sb, nz ); + encodeAdjustment ( sb, a ); + } + + private static void encodeZeroes ( StringBuffer sb, int nz ) { + if ( nz > 0 ) { + sb.append ( ' ' ); + if ( nz == 1 ) { + sb.append ( '0' ); + } else { + sb.append ( 'Z' ); + sb.append ( nz ); + } + } + } + + private static void encodeAdjustment ( StringBuffer sb, int a ) { + if ( a != 0 ) { + sb.append ( ' ' ); + sb.append ( a ); + } + } + + /** + * Decode a string as a glyph position adjustments array, where the string + * shall adhere to the syntax specified by {@link #encodePositionAdjustments}. + * @param value the encoded value + * @return the position adjustments array + */ + public static int[][] decodePositionAdjustments ( String value ) { + int[][] dp = null; + if ( value != null ) { + String[] sa = value.split ( "\\s" ); + if ( sa != null ) { + if ( sa.length > 0 ) { + int na = Integer.parseInt ( sa[0] ); + dp = new int [ na ] [ 4 ]; + for ( int i = 1, n = sa.length, k = 0; i < n; i++ ) { + String s = sa [ i ]; + if ( s.charAt(0) == 'Z' ) { + int nz = Integer.parseInt ( s.substring ( 1 ) ); + k += nz; + } else { + dp [ k / 4 ] [ k % 4 ] = Integer.parseInt ( s ); + k += 1; + } + } + } + } + } + return dp; + } + + /** + * Returns an attribute value as a glyph position adjustments array. The string value + * is expected to be a non-empty sequence of either Z<repeat> or <number>, where the + * former encodes a repeat count (of zeroes) and the latter encodes a integer number, + * and where each item is separated by whitespace. + * @param attributes the Attributes object + * @param name the name of the attribute + * @return the position adjustments array + */ + public static int[][] getAttributeAsPositionAdjustments(Attributes attributes, String name) { + String s = attributes.getValue(name); + if (s == null) { + return null; + } else { + return decodePositionAdjustments(s.trim()); + } + } + } |