/* * Copyright 2014-2016 Skynav, Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY SKYNAV, INC. AND ITS CONTRIBUTORS “AS IS” AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL SKYNAV, INC. OR ITS CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.skynav.ttpe.layout; import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Stack; import javax.xml.namespace.QName; import org.w3c.dom.Element; import org.xml.sax.Locator; import com.skynav.ttpe.area.Area; import com.skynav.ttpe.area.AreaNode; import com.skynav.ttpe.area.BlockArea; import com.skynav.ttpe.area.CanvasArea; import com.skynav.ttpe.area.LineArea; import com.skynav.ttpe.area.NonLeafAreaNode; import com.skynav.ttpe.area.ReferenceArea; import com.skynav.ttpe.area.ViewportArea; import com.skynav.ttpe.fonts.Font; import com.skynav.ttpe.fonts.FontCache; import com.skynav.ttpe.geometry.Axis; import com.skynav.ttpe.geometry.Dimension; import com.skynav.ttpe.geometry.Extent; import com.skynav.ttpe.geometry.Overflow; import com.skynav.ttpe.geometry.Point; import com.skynav.ttpe.geometry.TransformMatrix; import com.skynav.ttpe.geometry.WritingMode; import com.skynav.ttpe.style.BlockAlignment; import com.skynav.ttpe.style.Color; import com.skynav.ttpe.style.Defaults; import com.skynav.ttpe.style.Display; import com.skynav.ttpe.style.Helpers; import com.skynav.ttpe.style.Image; import com.skynav.ttpe.style.Visibility; import com.skynav.ttpe.style.Whitespace; import com.skynav.ttpe.text.LineBreakIterator; import com.skynav.ttv.model.value.Length; import com.skynav.ttv.util.Condition; import com.skynav.ttv.util.Location; import com.skynav.ttv.util.Locators; import com.skynav.ttv.util.StyleSet; import com.skynav.ttv.util.StyleSpecification; import com.skynav.ttv.verifier.util.Keywords; import com.skynav.ttv.verifier.util.Lengths; import com.skynav.ttv.verifier.util.MixedUnitsTreatment; import com.skynav.ttv.verifier.util.NegativeTreatment; import com.skynav.ttv.verifier.util.Positions; import com.skynav.ttx.transformer.TransformerContext; import com.skynav.xml.helpers.Documents; import static com.skynav.ttpe.style.Constants.*; import static com.skynav.ttpe.text.Constants.*; public class BasicLayoutState implements LayoutState { // initialized state private TransformerContext context; private FontCache fontCache; private LineBreakIterator breakIterator; private LineBreakIterator characterIterator; private Defaults defaults; private Stack<NonLeafAreaNode> areas; private Map<String, StyleSet> styles; private int[] counters; private Locator baseLocator; public BasicLayoutState(TransformerContext context) { this.context = context; this.counters = new int[Counter.values().length]; } public LayoutState initialize(FontCache fontCache, LineBreakIterator breakIterator, LineBreakIterator characterIterator, Defaults defaults) { this.fontCache = fontCache.maybeLoad(); this.breakIterator = breakIterator; this.characterIterator = characterIterator; this.defaults = defaults; this.areas = new java.util.Stack<NonLeafAreaNode>(); this.styles = new java.util.HashMap<String,StyleSet>(); this.baseLocator = makeBaseLocator(context); return this; } private static Locator makeBaseLocator(TransformerContext context) { URI uri = (URI) context.getResourceState("sysid"); return Locators.getLocator(uri != null ? uri.toString() : null); } public FontCache getFontCache() { return fontCache; } public LineBreakIterator getBreakIterator() { return breakIterator; } public LineBreakIterator getCharacterIterator() { return characterIterator; } public Defaults getDefaults() { return defaults; } public NonLeafAreaNode pushCanvas(Element e, double begin, double end, Extent cellResolution) { return push(new CanvasArea(e, begin, end, cellResolution)); } public NonLeafAreaNode pushViewport(Element e, double width, double height, boolean clip) { NonLeafAreaNode a = push(new ViewportArea(e, width, height, clip)); if (areas.size() > 2) { incrementCounters(CounterEvent.ADD_REGION, a); ((ViewportArea) a).setId(generateRegionIdentifier()); } else { updateDefaultFontSizeWithCellResolution(width, height); } return a; } private void updateDefaultFontSizeWithCellResolution(double width, double height) { double h = height / getCellResolution().getHeight(); defaults.setFontSize(new Extent(h, h)); } public NonLeafAreaNode pushReference(Element e, double x, double y, double width, double height, WritingMode wm, TransformMatrix ctm, Visibility visibility) { ReferenceArea ra = new ReferenceArea(e, x, y, width, height, wm, ctm, visibility); processBlockTraits(ra, e); return push(ra); } public NonLeafAreaNode pushBlock(Element e, Visibility visibility) { ReferenceArea ra = getReferenceArea(); if (ra != null) { BlockArea ba = new BlockArea(e, ra.getIPD(), ra.getBPD(), getBidiLevel(), visibility); processBlockTraits(ba, e); return push(ba); } else throw new IllegalStateException(); } public NonLeafAreaNode push(NonLeafAreaNode a) { NonLeafAreaNode p = !areas.empty() ? peek() : null; if (p != null) p.addChild(a); return (NonLeafAreaNode) areas.push(a); } public NonLeafAreaNode addLine(LineArea l) { NonLeafAreaNode p = !areas.empty() ? peek() : null; if ((p == null) || !(p instanceof BlockArea)) throw new IllegalStateException(); else p.addChild(l); incrementCounters(CounterEvent.ADD_LINE, l); return l; } public NonLeafAreaNode pop() { return pop(true); } public NonLeafAreaNode pop(boolean collapse) { NonLeafAreaNode a = areas.pop(); if (collapse) collapse(a, Dimension.BPD); return a; } public NonLeafAreaNode peek() { return areas.peek(); } public CanvasArea getCanvasArea() { if (!areas.empty()) { for (int i = 0, n = areas.size(); i < n; ++i) { int k = n - i - 1; NonLeafAreaNode a = areas.get(k); if (a instanceof CanvasArea) return (CanvasArea) a; } } return null; } public ReferenceArea getReferenceArea() { if (!areas.empty()) { for (int i = 0, n = areas.size(); i < n; ++i) { int k = n - i - 1; NonLeafAreaNode a = areas.get(k); if (a instanceof ReferenceArea) return (ReferenceArea) a; } } return null; } public String getLanguage() { String language = null; if (!areas.empty()) language = areas.peek().getLanguage(); if (language == null) language = defaults.getLanguage(); return language; } public Whitespace getWhitespace() { Whitespace ws = null; if (!areas.empty()) ws = areas.peek().getWhitespace(); if (ws == null) ws = defaults.getWhitespace(); return ws; } public WritingMode getWritingMode() { WritingMode wm = null; if (!areas.empty()) wm = areas.peek().getWritingMode(); if (wm == null) wm = defaults.getWritingMode(); return wm; } public int getBidiLevel() { return !areas.empty() ? areas.peek().getBidiLevel() : -1; } public Font getFont() { Font f = null; if (!areas.empty()) f = areas.peek().getFont(); if (f == null) f = fontCache.getDefaultFont(getWritingMode().getAxis(Dimension.IPD), defaults.getFontSize()); return f; } public Extent getFontSize() { Font f = getFont(); if (f != null) return f.getSize(); else return Extent.UNIT; } public double getAvailable(Dimension dimension) { if (!areas.empty()) return areas.peek().getAvailable(dimension); else return 0; } public Extent getCellResolution() { CanvasArea ca = getCanvasArea(); if (ca != null) return ca.getCellResolution(); else return defaults.getCellResolution(); } public Extent getReferenceExtent() { ReferenceArea ra = getReferenceArea(); if (ra != null) return ra.getExtent(); else return Extent.UNIT; } public BlockAlignment getReferenceAlignment() { ReferenceArea ra = getReferenceArea(); if (ra != null) return getDisplayAlign(ra.getElement()); else return defaults.getDisplayAlign(); } public Extent getExternalExtent() { Object value = context.getExternalParameters().getParameter("externalExtent"); if (value instanceof double[]) { double[] parsedExternalExtent = (double[]) value; return new Extent(parsedExternalExtent[0], parsedExternalExtent[1]); } else return defaults.getExternalExtent(); } public Point getExternalOrigin() { return Point.ZERO; // [TBD] get from context } public Overflow getExternalOverflow() { return Overflow.HIDDEN; // [TBD] get from context } public TransformMatrix getExternalTransform() { return TransformMatrix.IDENTITY; // [TBD] get from context } public WritingMode getExternalWritingMode() { return WritingMode.LRTB; // [TBD] get from context } public void saveStyles(Element e) { assert Documents.isElement(e, isdComputedStyleSetElementName); String id = Documents.getAttribute(e, xmlIdAttrName, null); if (id != null) { styles.put(id, parseStyle(e, id)); } } public Map<String,StyleSet> getStyles() { return styles; } public StyleSet getStyles(Element e) { String style = Documents.getAttribute(e, isdCSSAttrName, null); if (style != null) { StyleSet styles = this.styles.get(style); if (styles != null) return styles; } return StyleSet.EMPTY; } public Color getBackgroundColor(Element e) { StyleSpecification s = getStyles(e).get(ttsBackgroundColorAttrName); if (s != null) { String v = s.getValue(); com.skynav.ttv.model.value.Color[] retColor = new com.skynav.ttv.model.value.Color[1]; if (com.skynav.ttv.verifier.util.Colors.isColor(v, getLocation(e, ttsBackgroundColorAttrName), context, retColor)) return new Color(retColor[0].getRed(), retColor[0].getGreen(), retColor[0].getBlue(), retColor[0].getAlpha()); } return defaults.getBackgroundColor(); } public Image getBackgroundImage(Element e) { QName an = ttsBackgroundImageAttrName; StyleSpecification s = getStyles(e).get(an); if (s != null) { Image image; if ((image = getImage(e, an, s.getValue())) != null) return image; } return defaults.getBackgroundImage(); } public Image getForegroundImage(Element e) { // [TBD] - implement support for all source categories, at present only support 'src' attribute QName an = sourceAttrName; String s = Documents.getAttribute(e, an); if (s != null) { Image image; if ((image = getImage(e, an, s)) != null) return image; } return Image.NONE; } private Image getImage(Element e, QName attrName, String attrValue) { if ((attrValue != null) && !attrValue.isEmpty()) { com.skynav.ttv.model.value.Image[] retImage = new com.skynav.ttv.model.value.Image[1]; if (com.skynav.ttv.verifier.util.Images.isImage(attrValue, getLocation(e, attrName), context, retImage)) { com.skynav.ttv.model.value.Image i = retImage[0]; return new Image(i.getURI(), i.getVerifiedType(), i.getVerifiedFormat(), i.getWidth(), i.getHeight()); } } return null; } public Display getDisplay(Element e) { StyleSpecification s = getStyles(e).get(ttsDisplayAttrName); if (s != null) { String v = s.getValue(); return Display.valueOf(v.toUpperCase()); } else return defaults.getDisplay(); } public BlockAlignment getDisplayAlign(Element e) { StyleSpecification s = getStyles(e).get(ttsDisplayAlignAttrName); if (s != null) { String v = s.getValue(); return BlockAlignment.valueOf(v.toUpperCase()); } else return defaults.getDisplayAlign(); } public Extent getExtent(Element e) { StyleSpecification s = getStyles(e).get(ttsExtentAttrName); if (s != null) { String v = s.getValue(); if (Keywords.isAuto(v)) { return getExternalExtent(); } else { Integer[] minMax = new Integer[] { 2, 2 }; Object[] treatments = new Object[] { NegativeTreatment.Error, MixedUnitsTreatment.Allow }; List<Length> lengths = new java.util.ArrayList<Length>(); if (Lengths.isLengths(v, getLocation(e, ttsExtentAttrName), context, minMax, treatments, lengths)) { assert lengths.size() == 2; Extent cellResolution = getCellResolution(); Extent externalExtent = getExternalExtent(); Extent referenceExtent = externalExtent; Extent fontSize = getFontSize(); double w = Helpers.resolveLength(e, lengths.get(0), Axis.HORIZONTAL, externalExtent, referenceExtent, fontSize, cellResolution); double h = Helpers.resolveLength(e, lengths.get(1), Axis.VERTICAL, externalExtent, referenceExtent, fontSize, cellResolution); return new Extent(w, h); } } } return getExternalExtent(); } public Point getOrigin(Element e) { StyleSpecification s = getStyles(e).get(ttsOriginAttrName); if (s != null) { String v = s.getValue(); if (Keywords.isAuto(v)) { return getExternalOrigin(); } else { Integer[] minMax = new Integer[] { 2, 2 }; Object[] treatments = new Object[] { NegativeTreatment.Allow, MixedUnitsTreatment.Allow }; List<Length> lengths = new java.util.ArrayList<Length>(); if (Lengths.isLengths(v, getLocation(e, ttsOriginAttrName), context, minMax, treatments, lengths)) { assert lengths.size() == 2; Extent cellResolution = getCellResolution(); Extent externalExtent = getExternalExtent(); Extent referenceExtent = externalExtent; Extent fontSize = getFontSize(); double x = Helpers.resolveLength(e, lengths.get(0), Axis.HORIZONTAL, externalExtent, referenceExtent, fontSize, cellResolution); double y = Helpers.resolveLength(e, lengths.get(1), Axis.VERTICAL, externalExtent, referenceExtent, fontSize, cellResolution); return new Point(x, y); } } } return defaults.getOrigin(); } private static final double[] zeroPadding = new double[4]; public double[] getPadding(Element e) { StyleSpecification s = getStyles(e).get(ttsPaddingAttrName); if (s != null) { String v = s.getValue(); if (Keywords.isAuto(v)) { return Arrays.copyOf(zeroPadding, zeroPadding.length); } else { Integer[] minMax = new Integer[] { 1, 4 }; Object[] treatments = new Object[] { NegativeTreatment.Error, MixedUnitsTreatment.Allow }; List<Length> lengths = new java.util.ArrayList<Length>(); if (Lengths.isLengths(v, getLocation(e, ttsPaddingAttrName), context, minMax, treatments, lengths)) { Length[] la = lengths.toArray(new Length[lengths.size()]); Extent externalExtent = getExternalExtent(); Extent referenceExtent = getReferenceExtent(); if ((referenceExtent == null) || referenceExtent.isEmpty()) referenceExtent = externalExtent; return Helpers.resolvePadding(e, la, getWritingMode(e), externalExtent, referenceExtent, getFontSize(), getCellResolution()); } } } return defaults.getPadding(); } public Point getPosition(Element e, Extent extent) { StyleSpecification s = getStyles(e).get(ttsPositionAttrName); if ((s == null) && (getStyles(e).get(ttsOriginAttrName) == null) && (context.getModel().getTTMLVersion() >= 2)) s = new StyleSpecification(ttsPositionAttrName, defaults.getPositionComponents()); if (s != null) { String v = s.getValue(); String [] components = v.split("[ \t\r\n]+"); Length[] lengths = new Length[4]; if (Positions.isPosition(components, getLocation(e, ttsPositionAttrName), context, lengths)) { Extent cellResolution = getCellResolution(); return Helpers.resolvePosition(e, lengths, getExternalExtent(), extent, cellResolution); } } return getOrigin(e); } public Overflow getOverflow(Element e) { StyleSpecification s = getStyles(e).get(ttsOverflowAttrName); if (s != null) { String v = s.getValue(); return Overflow.valueOf(v.toUpperCase()); } else return defaults.getOverflow(); } public TransformMatrix getTransform(Element e) { return defaults.getTransform(); } public Visibility getVisibility(Element e) { StyleSpecification s = getStyles(e).get(ttsVisibilityAttrName); if (s != null) { String v = s.getValue(); return Visibility.valueOf(v.toUpperCase()); } else return defaults.getVisibility(); } public WritingMode getWritingMode(Element e) { StyleSpecification s = getStyles(e).get(ttsWritingModeAttrName); if (s != null) { String v = s.getValue(); return WritingMode.valueOf(v.toUpperCase()); } else return defaults.getWritingMode(); } public void incrementCounters(CounterEvent event, Area a) { if (event == CounterEvent.ADD_REGION) { updateCharCounters(a); updateLineCounters(a); int nr = 1; counters[Counter.REGIONS_IN_CANVAS.ordinal()] += nr; } else if (event == CounterEvent.ADD_LINE) { updateCharCounters(a); int nc = countSpacingGlyphs(a); counters[Counter.CHARS_IN_LINE.ordinal()] += nc; counters[Counter.CHARS_IN_REGION.ordinal()] += nc; counters[Counter.CHARS_IN_CANVAS.ordinal()] += nc; int nl = 1; counters[Counter.LINES_IN_REGION.ordinal()] += nl; counters[Counter.LINES_IN_CANVAS.ordinal()] += nl; } else if (event == CounterEvent.RESET) { Arrays.fill(counters, 0); } else { throw new UnsupportedOperationException(); } } public void finalizeCounters() { updateCharCounters(null); updateLineCounters(null); } public int getCounter(Counter counter) { return counters[counter.ordinal()]; } private void updateLineCounters(Area a) { int n, m; if ((a == null) || (a instanceof ViewportArea)) { n = counters[Counter.LINES_IN_REGION.ordinal()]; m = counters[Counter.MAX_LINES_IN_REGION.ordinal()]; if (n > m) counters[Counter.MAX_LINES_IN_REGION.ordinal()] = n; resetCounter(Counter.LINES_IN_REGION); } } private void updateCharCounters(Area a) { int n, m; if ((a == null) || (a instanceof LineArea)) { n = counters[Counter.CHARS_IN_LINE.ordinal()]; m = counters[Counter.MAX_CHARS_IN_LINE.ordinal()]; if (n > m) counters[Counter.MAX_CHARS_IN_LINE.ordinal()] = n; resetCounter(Counter.CHARS_IN_LINE); } if ((a == null) || (a instanceof ViewportArea)) { n = counters[Counter.CHARS_IN_REGION.ordinal()]; m = counters[Counter.MAX_CHARS_IN_REGION.ordinal()]; if (n > m) counters[Counter.MAX_CHARS_IN_REGION.ordinal()] = n; resetCounter(Counter.CHARS_IN_REGION); } } private int countSpacingGlyphs(Area a) { if (a instanceof LineArea) return ((LineArea) a).getSpacingGlyphsCount(); else return 0; } private void resetCounter(Counter counter) { counters[counter.ordinal()] = 0; } private String generateRegionIdentifier() { StringBuffer sb = new StringBuffer(); sb.append('r'); sb.append(Integer.toString(getCounter(Counter.REGIONS_IN_CANVAS))); return sb.toString(); } private StyleSet parseStyle(Element e, String id) { assert Documents.isElement(e, isdComputedStyleSetElementName); StyleSet styles = new StyleSet(id); for (Map.Entry<QName,String> a : Documents.getAttributes(e).entrySet()) { QName qn = a.getKey(); String ns = qn.getNamespaceURI(); if (ns != null) { if (ns.equals(ttsNamespace) || qn.equals(xmlLanguageAttrName)) styles.merge(new StyleSpecification(ns, qn.getLocalPart(), a.getValue())); } } String condition = Documents.getAttribute(e, conditionAttrName, null); if (condition != null) { try { styles.setCondition(Condition.valueOf(condition)); } catch (Condition.ParserException x) { } } return styles; } private void collapse(NonLeafAreaNode a, Dimension dimension) { if ((dimension == Dimension.BOTH) || (dimension == Dimension.BPD)) collapseBPD(a); if ((dimension == Dimension.BOTH) || (dimension == Dimension.IPD)) collapseIPD(a); } private void collapseIPD(NonLeafAreaNode a) { a.setIPD(computeConsumed(a, Dimension.IPD)); } private void collapseBPD(NonLeafAreaNode a) { a.setBPD(computeConsumed(a, Dimension.BPD)); } private double computeConsumed(NonLeafAreaNode a, Dimension dimension) { double consumed = 0; for (AreaNode c : a.getChildren()) { if (dimension == Dimension.IPD) consumed += c.getIPD(); else if (dimension == Dimension.BPD) consumed += c.getBPD(); } return consumed; } private Location getLocation(Element e, QName attributeName) { return new Location(e, Documents.getName(e), attributeName, baseLocator); } /** * Assign block traits to block area A from styles on element E * These include: * * 1. background color * 2. background image and related properties * 3. border properties * 4. padding properties */ private void processBlockTraits(BlockArea a, Element e) { // background color Color c = getBackgroundColor(e); if ((c != null) && !c.isTransparent()) a.setBackgroundColor(c); // background image Image i = getBackgroundImage(e); if ((i != null) && !i.isNone()) a.setBackgroundImage(i); // border [TBD] // padding [TBD] double[] p = getPadding(e); if (!Arrays.equals(p,zeroPadding)) a.setPadding(p); } }