/* * Copyright 2015 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.ttv.verifier.imsc; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.bind.JAXBElement; import javax.xml.namespace.QName; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.xml.sax.Locator; import com.skynav.ttv.model.Model; import com.skynav.ttv.model.imsc.IMSC1; import com.skynav.ttv.model.imsc1.ittm.AltText; import com.skynav.ttv.model.ttml.TTML1; import com.skynav.ttv.model.ttml.TTML1.TTML1Model; import com.skynav.ttv.model.ttml1.tt.Body; import com.skynav.ttv.model.ttml1.tt.Break; import com.skynav.ttv.model.ttml1.tt.Division; import com.skynav.ttv.model.ttml1.tt.Head; import com.skynav.ttv.model.ttml1.tt.Layout; import com.skynav.ttv.model.ttml1.tt.Paragraph; import com.skynav.ttv.model.ttml1.tt.Region; import com.skynav.ttv.model.ttml1.tt.Span; import com.skynav.ttv.model.ttml1.tt.TimedText; import com.skynav.ttv.model.value.Length; import com.skynav.ttv.model.value.TextOutline; import com.skynav.ttv.model.value.Time; import com.skynav.ttv.model.value.TimeParameters; import com.skynav.ttv.util.Location; import com.skynav.ttv.util.Message; import com.skynav.ttv.util.PreVisitor; import com.skynav.ttv.util.Reporter; import com.skynav.ttv.util.StyleSet; import com.skynav.ttv.util.StyleSpecification; import com.skynav.ttv.util.Traverse; import com.skynav.ttv.util.Visitor; import com.skynav.ttv.verifier.VerificationParameters; import com.skynav.ttv.verifier.VerifierContext; import com.skynav.ttv.verifier.smpte.ST20522010SemanticsVerifier; import com.skynav.ttv.verifier.ttml.TTML1ProfileVerifier; import com.skynav.ttv.verifier.ttml.timing.TimingVerificationParameters; import com.skynav.ttv.verifier.ttml.timing.TimingVerificationParameters1; import com.skynav.ttv.verifier.util.Integers; 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.Outline; import com.skynav.ttv.verifier.util.Timing; import com.skynav.ttv.verifier.util.ZeroTreatment; import com.skynav.xml.helpers.Documents; import static com.skynav.ttv.model.imsc.IMSC1.Constants.*; public class IMSC1SemanticsVerifier extends ST20522010SemanticsVerifier { public IMSC1SemanticsVerifier(Model model) { super(model); } @Override public boolean verify(Object root, VerifierContext context) { boolean failed = false; if (!super.verify(root, context)) failed = true; if (root instanceof TimedText) { TimedText tt = (TimedText) root; if (!verifyCharset(tt)) failed = true; if (!verifyEmUsage(tt)) failed = true; if (!verifyExtentIfPixelUnitUsed(tt)) failed = true; if (!verifyFrameRateIfFramesUsed(tt)) failed = true; if (!verifyRegionContainment(tt)) failed = true; if (!verifyTickRateIfTicksUsed(tt)) failed = true; if (!verifyTimeableContentIsTimed(tt)) failed = true; } else { QName rootName = context.getBindingElementName(root); Reporter reporter = context.getReporter(); reporter.logError(reporter.message(getLocator(root), "*KEY*", "Root element must be ''{0}'', got ''{1}''.", TTML1Model.timedTextElementName, rootName)); failed = true; } return !failed; } protected boolean verifyCharset(TimedText tt) { boolean failed = false; Reporter reporter = getContext().getReporter(); try { Charset charsetRequired = Charset.forName(CHARSET_REQUIRED); String charsetRequiredName = charsetRequired.name(); Charset charset = (Charset) getContext().getResourceState("encoding"); String charsetName = (charset != null) ? charset.name() : "unknown"; if (!charsetName.equals(charsetRequiredName)) { reporter.logError(reporter.message(getLocator(tt), "*KEY*", "Document encoding uses ''{0}'', but requires ''{1}''.", charsetName, charsetRequiredName)); failed = true; } } catch (Exception e) { reporter.logError(e); failed = true; } return !failed; } protected boolean verifyEmUsage(TimedText tt) { boolean failed = false; VerifierContext context = getContext(); if (isIMSCImageProfile(context)) { @SuppressWarnings("unchecked") Set<Locator> usage = (Set<Locator>) context.getResourceState("usageEm"); if ((usage != null) && (usage.size() > 0)) { Reporter reporter = context.getReporter(); for (Locator locator : usage) reporter.logError(reporter.message(locator, "*KEY*", "Use of ''em'' unit prohibited in image profile.")); failed = true; } } return !failed; } protected boolean verifyExtentIfPixelUnitUsed(TimedText tt) { boolean failed = false; @SuppressWarnings("unchecked") Set<Locator> usage = (Set<Locator>) getContext().getResourceState("usagePixel"); if ((usage != null) && (usage.size() > 0)) { String extent = tt.getExtent(); if ((extent == null) || (extent.length() == 0)) { Reporter reporter = getContext().getReporter(); for (Locator locator : usage) { reporter.logError(reporter.message(locator, "*KEY*", "Uses ''px'' unit, but does not specify ''{0}'' attribute on ''{1}'' element.", IMSC1StyleVerifier.extentAttributeName, TTML1Model.timedTextElementName)); } failed = true; } } return !failed; } protected boolean verifyFrameRateIfFramesUsed(TimedText tt) { boolean failed = false; @SuppressWarnings("unchecked") Set<Locator> usage = (Set<Locator>) getContext().getResourceState("usageFrames"); if ((usage != null) && (usage.size() > 0)) { BigInteger frameRate = tt.getFrameRate(); if ((frameRate == null) || IMSC1ParameterVerifier.isFrameRateDefaulted(tt)) { Reporter reporter = getContext().getReporter(); for (Locator locator : usage) { reporter.logError(reporter.message(locator, "*KEY*", "Uses frames (''f'') metric or frame component, but does not specify ''{0}'' attribute on ''{1}'' element.", IMSC1ParameterVerifier.frameRateAttributeName, TTML1Model.timedTextElementName)); } failed = true; } } return !failed; } protected boolean verifyTickRateIfTicksUsed(TimedText tt) { boolean failed = false; @SuppressWarnings("unchecked") Set<Locator> usage = (Set<Locator>) getContext().getResourceState("usageTicks"); if ((usage != null) && (usage.size() > 0)) { BigInteger tickRate = tt.getTickRate(); if ((tickRate == null) || IMSC1ParameterVerifier.isTickRateDefaulted(tt)) { Reporter reporter = getContext().getReporter(); for (Locator locator : usage) { reporter.logError(reporter.message(locator, "*KEY*", "Uses ticks (''t'') metric, but does not specify ''{0}'' attribute on ''{1}'' element.", IMSC1ParameterVerifier.tickRateAttributeName, TTML1Model.timedTextElementName)); } failed = true; } } return !failed; } protected boolean verifyRegionContainment(TimedText tt) { boolean failed = false; Head head = tt.getHead(); if (head != null) { Layout layout = head.getLayout(); if (layout != null) { double[] rootExtent = getRootExtent(tt); double[] cellResolution = getCellResolution(tt); for (Region r : layout.getRegion()) { if (!verifyRegionContainment(r, rootExtent, cellResolution)) failed = true; } } } return !failed; } private double[] getRootExtent(TimedText tt) { Location location = new Location(tt, getContext().getBindingElementName(tt), IMSC1StyleVerifier.extentAttributeName, getLocator(tt)); return getRootExtent(tt.getExtent(), location); } private double[] getRootExtent(String extent, Location location) { double[] externalExtent = (double[]) getContext().getResourceState("externalExtent"); if (extent != null) { extent = extent.trim(); if (extent.equals("auto")) return externalExtent; else { Length[] lengths = parseLengthPair(extent, location, getContext().getReporter(), true); return new double[] { getPixels(lengths[0], 0, 1, 1, 1), getPixels(lengths[1], 0, 1, 1, 1) }; } } else return externalExtent; } private double[] getCellResolution(TimedText tt) { Location location = new Location(tt, getContext().getBindingElementName(tt), IMSC1ParameterVerifier.cellResolutionAttributeName, getLocator(tt)); return getCellResolution(tt.getCellResolution(), location); } private double[] getCellResolution(String cellResolution, Location location) { if (cellResolution != null) { cellResolution = cellResolution.trim(); Integer[] integers = parseIntegerPair(cellResolution, location, getContext().getReporter()); return new double[] { integers[0], integers[1] }; } else return null; } protected boolean verifyRegionContainment(Region region, double[] rootExtent, double[] cellResolution) { boolean failed = false; Reporter reporter = getContext().getReporter(); Location location = new Location(region, getContext().getBindingElementName(region), IMSC1StyleVerifier.extentAttributeName, getLocator(region)); if (rootExtent == null) rootExtent = new double[] { -1, -1 }; if (cellResolution == null) cellResolution = new double[] { 1, 1 }; // extract root edges in fractional pixels double xRoot = 0; double yRoot = 0; double wRoot = rootExtent[0]; double hRoot = rootExtent[1]; // extract region origin in fractional pixels String origin = region.getOrigin(); double x = xRoot; double y = yRoot; if (origin != null) { origin = origin.trim(); if (!origin.equals("auto")) { Length[] lengths = parseLengthPair(origin, location, reporter, false); if (lengths != null) { x = getPixels(lengths[0], 0, wRoot, wRoot, cellResolution[0]); y = getPixels(lengths[1], 0, hRoot, wRoot, cellResolution[1]); } else failed = true; } } // extract region extent in fractional pixels String extent = region.getExtent(); double w = wRoot; double h = hRoot; if (extent != null) { extent = extent.trim(); if (!extent.equals("auto")) { Length[] lengths = parseLengthPair(extent, location, reporter, false); if (lengths != null) { w = getPixels(lengths[0], 0, wRoot, wRoot, cellResolution[0]); h = getPixels(lengths[1], 0, hRoot, hRoot, cellResolution[1]); } else failed = true; } } // check containment if (!failed) { Locator locator = location.getLocator(); if (x < xRoot) { reporter.logError(reporter.message(locator, "*KEY*", "Left edge at {0}px is outside root container.", x)); failed = true; } if (y < yRoot) { reporter.logError(reporter.message(locator, "*KEY*", "Top edge at {0}px is outside root container.", y)); failed = true; } if ((wRoot >= 0) && (hRoot >= 0)) { if ((x + w) > (xRoot + wRoot)) { reporter.logError(reporter.message(locator, "*KEY*", "Right edge at {0}px is outside root container.", x + w)); failed = true; } if ((y + h) > (yRoot + hRoot)) { reporter.logError(reporter.message(locator, "*KEY*", "Bottom edge at {0}px is outside root container.", y + h)); failed = true; } } } return !failed; } private Length[] parseLengthPair(String pair, Location location, Reporter reporter, boolean enforcePixelsOnly) { Integer[] minMax = new Integer[] { 2, 2 }; Object[] treatments = new Object[] { NegativeTreatment.Allow, MixedUnitsTreatment.Allow }; List<Length> lengths = new java.util.ArrayList<Length>(); Locator locator = location.getLocator(); if (Lengths.isLengths(pair, location, getContext(), minMax, treatments, lengths)) { if (enforcePixelsOnly) { for (Length l : lengths) { if (l.getUnits() != Length.Unit.Pixel) { if (reporter != null) reporter.logError(reporter.message(locator, "*KEY*", "Invalid length pair component ''{0}'', must use pixel (''px'') unit only.", l)); return null; } } } return lengths.toArray(new Length[2]); } else { if (reporter != null) reporter.logInfo(reporter.message(locator, "*KEY*", "Invalid length pair ''{0}''.", pair)); return null; } } private Integer[] parseIntegerPair(String pair, Location location, Reporter reporter) { Integer[] minMax = new Integer[] { 2, 2 }; Object[] treatments = new Object[] { NegativeTreatment.Error, ZeroTreatment.Error }; List<Integer> integers = new java.util.ArrayList<Integer>(); Locator locator = location.getLocator(); if (Integers.isIntegers(pair, location, null, minMax, treatments, integers)) { return integers.toArray(new Integer[2]); } else { if (reporter != null) reporter.logError(reporter.message(locator, "*KEY*", "Invalid integer pair ''{0}''.", pair)); return null; } } private double getPixels(Length length, double fSize, double pSize, double rSize, double cSize) { double value = length.getValue(); Length.Unit units = length.getUnits(); if (pSize < 0) pSize = 0; if (rSize < 0) rSize = 0; if (units == Length.Unit.Pixel) return value; else if (units == Length.Unit.Cell) return value * (rSize / cSize); else if (units == Length.Unit.Percentage) return value * (pSize / 100); else if (units == Length.Unit.Em) return value * fSize; else return 0; } protected boolean verifyTimeableContentIsTimed(TimedText tt) { boolean failed = false; VerifierContext context = getContext(); VerificationParameters verificationParameters = makeTimingVerificationParameters(tt, context); String timeablesKey = getModel().makeResourceStateName("timeables"); @SuppressWarnings("unchecked") List<Object> timeables = (List<Object>) context.getResourceState(timeablesKey); if (timeables != null) { for (Object timeable : timeables) { if (!verifyTimedContent(timeable, getLocator(timeable), context, verificationParameters)) failed = true; } } context.setResourceState(timeablesKey, null); return !failed; } private static TimingVerificationParameters makeTimingVerificationParameters(Object content, VerifierContext context) { return new TimingVerificationParameters1(content, context != null ? context.getExternalParameters() : null); } private boolean verifyTimedContent(Object content, Locator locator, VerifierContext context, VerificationParameters parameters) { boolean failed = false; if (hasTimeableContent(content)) { if (!isSelfOrAncestorExplicitlyTimed(content, locator, context, parameters)) { Reporter reporter = context.getReporter(); if (reporter.isWarningEnabled("missing-timing")) { Message message = reporter.message(locator, "*KEY*", "Timeable content ''{0}'' is missing explicit @begin and/or @end on self or ancestor.", context.getBindingElementName(content)); if (reporter.logWarning(message)) { reporter.logError(message); failed = true; } } } } return !failed; } private boolean hasTimeableContent(Object content) { if (content instanceof Division) { return hasTimeableContent((Division) content); } else { List<Serializable> children; if (content instanceof Paragraph) children = ((Paragraph) content).getContent(); else if (content instanceof Span) children = ((Span) content).getContent(); else children = null; return (children == null) ? false : hasTimeableContent(children); } } private boolean hasTimeableContent(Division div) { String smpteBackgroundImage = div.getOtherAttributes().get(getBackgroundImageAttributeName()); if ((smpteBackgroundImage == null) || smpteBackgroundImage.isEmpty()) return false; else { try { new URI(smpteBackgroundImage); return true; } catch (URISyntaxException e) { return false; } } } private boolean hasTimeableContent(List<Serializable> content) { for (Serializable s : content) { if (s instanceof JAXBElement<?>) { Object c = ((JAXBElement<?>)s).getValue(); if (c instanceof Break) return true; } else if (s instanceof String) return true; } return false; } private boolean isSelfOrAncestorExplicitlyTimed(Object content, Locator locator, VerifierContext context, VerificationParameters parameters) { while (content != null) { if (isExplicitlyTimed(content, locator, context, parameters)) return true; else if (content instanceof Body) break; else content = context.getBindingElementParent(content); } return false; } private boolean isExplicitlyTimed(Object content, Locator locator, VerifierContext context, VerificationParameters parameters) { assert parameters instanceof TimingVerificationParameters; TimeParameters timeParameters = ((TimingVerificationParameters) parameters).getTimeParameters(); Time b = getBegin(content, locator, context, timeParameters); Time e = getEnd(content, locator, context, timeParameters); return (b != null) && (e != null) && (b.getTime(timeParameters) <= e.getTime(timeParameters)); } private Time getBegin(Object content, Locator locator, VerifierContext context, TimeParameters timeParameters) { QName name = IMSC1TimingVerifier.beginAttributeName; Object value = getTimingValue(content, name); if (value instanceof String) { Location location = new Location(content, context.getBindingElementName(content), name, locator); return parseTimeCoordinate((String) value, location, context, timeParameters); } else return null; } private Time getEnd(Object content, Locator locator, VerifierContext context, TimeParameters timeParameters) { QName name = IMSC1TimingVerifier.endAttributeName; Object value = getTimingValue(content, name); if (value instanceof String) { Location location = new Location(content, context.getBindingElementName(content), name, locator); return parseTimeCoordinate((String) value, location, context, timeParameters); } else return null; } private Object getTimingValue(Object content, QName timingAttributeName) { try { Class<?> contentClass = content.getClass(); Method m = contentClass.getMethod(makeGetterName(timingAttributeName), new Class<?>[]{}); return m.invoke(content, new Object[]{}); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { return null; } catch (SecurityException e) { throw new RuntimeException(e); } } private static String makeGetterName(QName name) { StringBuffer sb = new StringBuffer(); sb.append("get"); String ln = name.getLocalPart(); sb.append(Character.toUpperCase(ln.charAt(0))); sb.append(ln.substring(1)); return sb.toString(); } private Time parseTimeCoordinate(String value, Location location, VerifierContext context, TimeParameters timeParameters) { Time[] time = new Time[1]; if (Timing.isCoordinate(value, location, context, timeParameters, time)) return time[0]; else return null; } @Override protected boolean verifyTimedText(Object root) { boolean failed = false; assert root instanceof TimedText; TimedText tt = (TimedText) root; String profile = tt.getProfile(); if (profile == null) { Reporter reporter = getContext().getReporter(); Message message = reporter.message(getLocator(tt), "*KEY*", "Root element ''{0}'' should have a ''{1}'' attribute, but it is missing.", TTML1Model.timedTextElementName, TTML1ProfileVerifier.profileAttributeName); if (reporter.logWarning(message)) { reporter.logError(message); failed = true; } } else getContext().setResourceState(getModel().makeResourceStateName("profile"), profile); if (!super.verifyTimedText(root)) failed = true; return !failed; } @Override protected boolean verifyRegion(Object region) { if (!super.verifyRegion(region)) return false; else { boolean failed = false; assert region instanceof Region; if (!verifyRegionExtent((Region) region, getLocator(region), getContext())) failed = true; return !failed; } } private boolean verifyRegionExtent(Region region, Locator locator, VerifierContext context) { boolean failed = false; String extent = region.getExtent(); if (extent == null) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(locator, "*KEY*", "Style attribute ''{0}'' required on ''{1}''.", IMSC1StyleVerifier.extentAttributeName, context.getBindingElementName(region))); failed = true; } return !failed; } @Override protected boolean verifyDivision(Object division) { VerifierContext context = getContext(); if (!super.verifyDivision(division)) { return false; } else if (isIMSCImageProfile(context) && isNestedDivision(division)) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(getLocator(division), "*KEY*", "Nested ''{0}'' prohibited in image profile.", context.getBindingElementName(division))); return false; } else { String timeablesKey = getModel().makeResourceStateName("timeables"); @SuppressWarnings("unchecked") List<Object> timeables = (List<Object>) getContext().getResourceState(timeablesKey); if (timeables == null) { timeables = new java.util.ArrayList<Object>(); getContext().setResourceState(timeablesKey, timeables); } timeables.add(division); return true; } } private boolean isNestedDivision(Object division) { VerifierContext context = getContext(); for (Object p = context.getBindingElementParent(division); p != null; p = context.getBindingElementParent(p)) { if (p instanceof Division) return true; } return false; } @Override protected boolean verifyParagraph(Object paragraph) { VerifierContext context = getContext(); if (!super.verifyParagraph(paragraph)) { return false; } else if (isIMSCImageProfile(context)) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(getLocator(paragraph), "*KEY*", "Element ''{0}'' prohibited in image profile.", context.getBindingElementName(paragraph))); return false; } else { String timeablesKey = getModel().makeResourceStateName("timeables"); @SuppressWarnings("unchecked") List<Object> timeables = (List<Object>) context.getResourceState(timeablesKey); if (timeables == null) { timeables = new java.util.ArrayList<Object>(); context.setResourceState(timeablesKey, timeables); } timeables.add(paragraph); return true; } } @Override protected boolean verifySpan(Object span) { VerifierContext context = getContext(); if (!super.verifySpan(span)) { return false; } else if (isIMSCImageProfile(context)) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(getLocator(span), "*KEY*", "Element ''{0}'' prohibited in image profile.", context.getBindingElementName(span))); if (isNestedSpan(span)) { reporter.logError(reporter.message(getLocator(span), "*KEY*", "Nested ''{0}'' prohibited in image profile.", context.getBindingElementName(span))); } return false; } else { String timeablesKey = getModel().makeResourceStateName("timeables"); @SuppressWarnings("unchecked") List<Object> timeables = (List<Object>) context.getResourceState(timeablesKey); if (timeables == null) { timeables = new java.util.ArrayList<Object>(); context.setResourceState(timeablesKey, timeables); } timeables.add(span); return true; } } private boolean isNestedSpan(Object span) { VerifierContext context = getContext(); for (Object p = context.getBindingElementParent(span); p != null; p = context.getBindingElementParent(p)) { if (p instanceof JAXBElement<?>) p = ((JAXBElement<?>)p).getValue(); if (p instanceof Span) return true; } return false; } @Override protected boolean verifyBreak(Object br) { VerifierContext context = getContext(); if (!super.verifyBreak(br)) { return false; } else if (isIMSCImageProfile(context)) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(getLocator(br), "*KEY*", "Element ''{0}'' prohibited in image profile.", context.getBindingElementName(br))); return false; } else { return true; } } public boolean inIMSCNamespace(QName name) { return IMSC1.inIMSCNamespace(name); } @Override public boolean verifyOtherElement(Object content, Locator locator, VerifierContext context) { if (!super.verifyOtherElement(content, locator, context)) return false; else return verifyIMSCOtherElement(content, locator, context); } private boolean verifyIMSCOtherElement(Object content, Locator locator, VerifierContext context) { boolean failed = false; assert context == getContext(); Node node = context.getXMLNode(content); if (node == null) { if (content instanceof Element) node = (Element) content; } if (node != null) { String nsUri = node.getNamespaceURI(); String localName = node.getLocalName(); if (localName == null) localName = node.getNodeName(); QName name = new QName(nsUri != null ? nsUri : "", localName); Model model = getModel(); if (inIMSCNamespace(name)) { if (!model.isElement(name)) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(locator, "*KEY*", "Unknown element in IMSC namespace ''{0}''.", name)); failed = true; } else if (isIMSCAltTextElement(content)) { failed = !verifyIMSCAltText(content, locator, context); } else { return unexpectedContent(content); } } } return !failed; } @Override protected boolean verifySMPTEImage(Object image, Locator locator, VerifierContext context) { if (!super.verifySMPTEImage(image, locator, context)) return false; else { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(locator, "*KEY*", "SMPTE element ''{0}'' prohibited.", context.getBindingElementName(image))); return false; } } protected boolean isIMSCAltTextElement(Object content) { return content instanceof AltText; } protected boolean verifyIMSCAltText(Object image, Locator locator, VerifierContext context) { boolean failed = false; if (!verifyOtherAttributes(image)) failed = true; if (!verifyAncestry(image, locator, context)) failed = true; return !failed; } @Override public boolean verifyOtherAttributes(Object content, Locator locator, VerifierContext context) { if (!super.verifyOtherAttributes(content, locator, context)) return false; else return verifyIMSCOtherAttributes(content, locator, context); } @Override protected boolean verifySMPTEBackgroundImage(Object content, QName name, Object valueObject, Locator locator, VerifierContext context) { if (!super.verifySMPTEBackgroundImage(content, name, valueObject, locator, context)) { return false; } else if (isIMSCImageProfile(context)) { return true; } else if (isIMSCTextProfile(context)) { Reporter reporter = context.getReporter(); reporter.logInfo(reporter.message(locator, "*KEY*", "SMPTE attribute ''{0}'' prohibited on ''{1}'' in {2} text profile.", name, context.getBindingElementName(content), getModel().getName())); return false; } else { Reporter reporter = context.getReporter(); reporter.logInfo(reporter.message(locator, "*KEY*", "SMPTE attribute ''{0}'' prohibited on ''{1}'' in {2} of indeterminate profile.", name, context.getBindingElementName(content), getModel().getName())); return false; } } @Override protected boolean verifySMPTEBackgroundImageHV(Object content, QName name, Object valueObject, Locator locator, VerifierContext context) { if (!super.verifySMPTEBackgroundImageHV(content, name, valueObject, locator, context)) return false; else { Reporter reporter = context.getReporter(); reporter.logInfo(reporter.message(locator, "*KEY*", "SMPTE attribute ''{0}'' prohibited on ''{1}''.", name, context.getBindingElementName(content))); return false; } } private boolean verifyIMSCOtherAttributes(Object content, Locator locator, VerifierContext context) { boolean failed = false; NamedNodeMap attributes = context.getXMLNode(content).getAttributes(); for (int i = 0, n = attributes.getLength(); i < n; ++i) { boolean failedAttribute = false; Node item = attributes.item(i); if (!(item instanceof Attr)) continue; Attr attribute = (Attr) item; String nsUri = attribute.getNamespaceURI(); String localName = attribute.getLocalName(); if (localName == null) localName = attribute.getName(); if (localName.indexOf("xmlns") == 0) continue; QName name = new QName(nsUri != null ? nsUri : "", localName); Model model = getModel(); if (model.isNamespace(name.getNamespaceURI())) { String nsLabel; if (name.getNamespaceURI().indexOf(NAMESPACE_IMSC_PREFIX) == 0) nsLabel = "IMSC"; else if (name.getNamespaceURI().indexOf(NAMESPACE_EBUTT_PREFIX) == 0) nsLabel = "EBUTT"; else nsLabel = null; if (nsLabel != null) { Reporter reporter = context.getReporter(); String value = attribute.getValue(); if (!model.isGlobalAttribute(name)) { reporter.logError(reporter.message(locator, "*KEY*", "Unknown attribute in {0} namespace ''{1}'' not permitted on ''{2}''.", nsLabel, name, context.getBindingElementName(content))); failedAttribute = true; } else if (!model.isGlobalAttributePermitted(name, context.getBindingElementName(content))) { reporter.logError(reporter.message(locator, "*KEY*", "{0} attribute ''{1}'' not permitted on ''{2}''.", nsLabel, name, context.getBindingElementName(content))); failedAttribute = true; } else if (!verifyNonEmptyOrPadded(content, name, value, locator, context)) { reporter.logError(reporter.message(locator, "*KEY*", "Invalid {0} value ''{1}''.", name, value)); failedAttribute = true; } else if (nsLabel.equals("IMSC")) { if (!verifyIMSCAttribute(content, locator, context, name, value)) { reporter.logError(reporter.message(locator, "*KEY*", "Invalid {0} value ''{1}''.", name, value)); failedAttribute = true; } } else if (nsLabel.equals("EBUTT")) { if (!verifyEBUTTAttribute(content, locator, context, name, value)) { reporter.logError(reporter.message(locator, "*KEY*", "Invalid {0} value ''{1}''.", name, value)); failedAttribute = true; } } } } if (failedAttribute) failed = failedAttribute; } return !failed; } protected boolean verifyIMSCAttribute(Object content, Locator locator, VerifierContext context, QName name, String value) { boolean failed = false; // [TBD] - IMPLEMENT ME return !failed; } protected boolean verifyEBUTTAttribute(Object content, Locator locator, VerifierContext context, QName name, String value) { boolean failed = false; // [TBD] - IMPLEMENT ME return !failed; } @Override protected boolean verifyPostTransform(Object root, Document isd, VerifierContext context) { if (!super.verifyPostTransform(root, isd, context)) return false; else { boolean failed = false; if (!verifyPostTransformStyleConstraints(root, isd, context)) failed = true; if (!verifyMaximumRegionCount(root, isd, context)) failed = true; return !failed; } } private boolean verifyPostTransformStyleConstraints(final Object root, final Document isd, final VerifierContext context) { final Map<String,StyleSet> styleSets = getISDStyleSets(isd); try { final boolean[] failed = new boolean[] { false }; Traverse.traverseElements(isd, new PreVisitor() { public boolean visit(Object content, Object parent, Visitor.Order order) { assert content instanceof Element; Element elt = (Element) content; if (!verifyPostTransformStyleConstraints(root, isd, elt, styleSets, context)) failed[0] = true; return true; } }); return !failed[0]; } catch (Exception e) { return false; } } private boolean verifyPostTransformStyleConstraints(Object root, Document isd, Element elt, Map<String,StyleSet> styleSets, VerifierContext context) { boolean failed = false; if (isTTParagraphElement(elt)) { if (!verifyLineHeight(root, isd, elt, styleSets, context)) failed = true; } else if (isTTSpanElement(elt)) { if (!verifyFontFamily(root, isd, elt, styleSets, context)) failed = true; if (!verifyTextOutlineThickness(root, isd, elt, styleSets, context)) failed = true; } return !failed; } private QName isdCSSAttributeName = new QName(TTML1.Constants.NAMESPACE_TT_ISD, "css"); private boolean verifyFontFamily(Object root, Document isd, Element elt, Map<String,StyleSet> styleSets, VerifierContext context) { boolean failed = false; String style = Documents.getAttribute(elt, isdCSSAttributeName, null); String av = null; if (style != null) { StyleSet css = styleSets.get(style); if (css != null) { StyleSpecification ss = css.get(IMSC1StyleVerifier.fontFamilyAttributeName); if (ss != null) av = ss.getValue(); } } if (av == null) av = "default"; if (!isRecommendedFontFamily(av)) { Reporter reporter = context.getReporter(); if (reporter.isWarningEnabled("uses-non-recommended-font-family")) { Message message = reporter.message(getLocator(elt), "*KEY*", "Computed value of font family is ''{0}''.", av); if (reporter.logWarning(message)) { reporter.logError(message); failed = true; } } } return !failed; } private boolean isRecommendedFontFamily(String family) { assert family != null; if (family.equals("default")) return true; else if (family.equals("monospaceSerif")) return true; else if (family.equals("proportionalSansSerif")) return true; else return false; } private boolean verifyLineHeight(Object root, Document isd, Element elt, Map<String,StyleSet> styleSets, VerifierContext context) { boolean failed = false; String style = Documents.getAttribute(elt, isdCSSAttributeName, null); String av = null; if (style != null) { StyleSet css = styleSets.get(style); if (css != null) { StyleSpecification ss = css.get(IMSC1StyleVerifier.lineHeightAttributeName); if (ss != null) av = ss.getValue(); } } if (av == null) av = "normal"; if (av.equals("normal")) { Reporter reporter = context.getReporter(); if (reporter.isWarningEnabled("uses-line-height-normal")) { Message message = reporter.message(getLocator(elt), "*KEY*", "Computed value of line height is ''{0}''.", av); if (reporter.logWarning(message)) { reporter.logError(message); failed = true; } } } return !failed; } private boolean verifyTextOutlineThickness(Object root, Document isd, Element elt, Map<String,StyleSet> styleSets, VerifierContext context) { boolean failed = false; String style = Documents.getAttribute(elt, isdCSSAttributeName, null); String fs = null; String to = null; if (style != null) { StyleSet css = styleSets.get(style); if (css != null) { StyleSpecification ss; if ((ss = css.get(IMSC1StyleVerifier.textOutlineAttributeName)) != null) to = ss.getValue(); if ((to != null) && ((ss = css.get(IMSC1StyleVerifier.fontSizeAttributeName)) != null)) fs = ss.getValue(); } } if ((to != null) && !to.equals("none")) { if (fs == null) fs = "1c"; if (root instanceof TimedText) { TimedText tt = (TimedText) root; Locator locator = getLocator(elt); QName eltName = Documents.getName(elt); double[] rootExtent = getRootExtent(tt); double[] cellResolution = getCellResolution(tt); double hRoot, hCell; assert rootExtent != null; if (rootExtent.length > 1) hRoot = rootExtent[1]; else hRoot = 1; assert cellResolution != null; if (cellResolution.length > 1) hCell = cellResolution[1]; else hCell = 1; Location fsLocation = new Location(elt, eltName, IMSC1StyleVerifier.fontSizeAttributeName, locator); double fsInPixels = getFontSizeInPixels(fs, fsLocation, hRoot, hCell, context); Location toLocation = new Location(elt, eltName, IMSC1StyleVerifier.textOutlineAttributeName, locator); double toInPixels = getTextOutlineThicknessInPixels(to, toLocation, fsInPixels, hRoot, hCell, context); if (toInPixels > 0.1 * fsInPixels) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(getLocator(elt), "*KEY*", "Computed value {0}px of text outline thickness must be less than or equal to 10% of computed value {1}px of font size.", toInPixels, fsInPixels)); } } } return !failed; } private double getFontSizeInPixels(String value, Location location, double hRoot, double hCell, VerifierContext context) { Integer[] minMax = new Integer[] { 1, 2 }; Object[] treatments = new Object[] { NegativeTreatment.Error, MixedUnitsTreatment.Error }; List<Length> lengths = new java.util.ArrayList<Length>(); if (Lengths.isLengths(value, location, context, minMax, treatments, lengths)) { Length fs; if (lengths.size() > 1) fs = lengths.get(1); else if (lengths.size() > 0) fs = lengths.get(0); else fs = null; if (fs != null) return getPixels(fs, 0, hRoot / hCell, hRoot, hCell); } return 1; } private double getTextOutlineThicknessInPixels(String value, Location location, double hFont, double hRoot, double hCell, VerifierContext context) { TextOutline[] outline = new TextOutline[1]; if (Outline.isOutline(value, location, context, outline)) { Length to = outline[0].getThickness(); if (to != null) return getPixels(to, hFont, hFont, hRoot, hCell); } return 0; } private boolean verifyMaximumRegionCount(final Object root, final Document isd, final VerifierContext context) { boolean failed = false; int maxRegions = MAX_REGIONS_PER_ISD; List<Element> regions = getISDRegionElements(isd); if (regions.size() > maxRegions) { Reporter reporter = context.getReporter(); reporter.logError(reporter.message(getLocator(root), "*KEY*", "Maximum number of regions exceeded in ISD instance, expected no more than {0}, got {1}.", maxRegions, regions.size())); failed = true; } return !failed; } public static boolean isIMSCTextProfile(VerifierContext context) { String profile = (String) context.getResourceState(context.getModel().makeResourceStateName("profile")); return (profile != null) && profile.equals(PROFILE_TEXT_ABSOLUTE); } public static boolean isIMSCImageProfile(VerifierContext context) { String profile = (String) context.getResourceState(context.getModel().makeResourceStateName("profile")); return (profile != null) && profile.equals(PROFILE_IMAGE_ABSOLUTE); } }