/*
* Copyright 2013-16 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.ttx.transformer.isd;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.namespace.QName;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import com.skynav.ttv.model.imsc.IMSC1;
import com.skynav.ttv.model.smpte.ST20522010;
import com.skynav.ttv.util.PreVisitor;
import com.skynav.ttv.util.Traverse;
import com.skynav.ttv.util.Visitor;
import com.skynav.ttv.verifier.imsc.IMSC1SemanticsVerifier;
import com.skynav.ttv.verifier.ttml.TTML1StyleVerifier;
import com.skynav.ttx.transformer.TransformerContext;
import com.skynav.xml.helpers.Documents;
import com.skynav.xml.helpers.XML;
public class IMSC1Helper extends TTML2Helper {
public static final String NAMESPACE_IMSC_STYLING = IMSC1.Constants.NAMESPACE_IMSC_STYLING;
public static final String NAMESPACE_ST20522010 = ST20522010.Constants.NAMESPACE_2010;
public static final String NAMESPACE_EBUTT_STYLING = IMSC1.Constants.NAMESPACE_EBUTT_STYLING;
public static final QName backgroundImageAttributeName = new QName(NAMESPACE_ST20522010, "backgroundImage");
public static final QName forcedDisplayAttributeName = new QName(NAMESPACE_IMSC_STYLING, "forcedDisplay");
public static final QName linePaddingAlignAttributeName = new QName(NAMESPACE_EBUTT_STYLING, "linePadding");
public static final QName multiRowAlignAttributeName = new QName(NAMESPACE_EBUTT_STYLING, "multiRowAlign");
public static final String forcedParameterCondition = "parameter('forced')";
@Override
public void enactISDPostTransforms(Document doc, TransformerContext context) {
enactOrderDependentTransforms(doc, context);
enactOrderIndependentTransforms(doc, context);
maybeRecordTransformMetadata(doc, context);
}
private void enactOrderDependentTransforms(Document doc, TransformerContext context) {
// none at this time
}
private void enactOrderIndependentTransforms(Document doc, TransformerContext context) {
transformBackgroundImage(doc, context);
transformForcedDisplay(doc, context);
transformLinePadding(doc, context);
transformMultiRowAlign(doc, context);
}
private void transformBackgroundImage(final Document doc, final TransformerContext context) {
if (IMSC1SemanticsVerifier.isIMSCImageProfile(context)) {
try {
Traverse.traverseElements(doc, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (isDivisionElement(elt)) {
String source = Documents.getAttribute(elt, backgroundImageAttributeName);
if (source != null) {
source = source.trim();
if (!source.isEmpty()) {
Element image = Documents.createElement(doc, imageElementName);
Documents.setAttribute(image, sourceAttributeName, source);
// [TBD] ensure that no image child is already present
elt.appendChild(image);
// record uses of ttml2 feature
context.setResourceState(ResourceState.isdUsesTTML2Feature.name(), Boolean.TRUE);
}
Documents.removeAttribute(elt, backgroundImageAttributeName);
}
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
}
}
private void transformForcedDisplay(Document doc, TransformerContext context) {
final List<Element> styles = new java.util.ArrayList<Element>();
final List<Element> regions = new java.util.ArrayList<Element>();
final List<Element> forced = new java.util.ArrayList<Element>();
try {
Traverse.traverseElements(doc, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (isISDStyleElement(elt)) {
styles.add(elt);
} else if (isISDRegionElement(elt)) {
regions.add(elt);
} else if (isContentElement(elt)) {
if (Documents.hasAttribute(elt, forcedDisplayAttributeName)) {
if (Boolean.parseBoolean(Documents.getAttribute(elt, forcedDisplayAttributeName, "").trim()))
forced.add(elt);
Documents.removeAttribute(elt, forcedDisplayAttributeName);
}
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
if (!forced.isEmpty())
transformForcedDisplay(doc, styles, regions, forced, context);
}
private static final Pattern styleIdentifierPattern = Pattern.compile("s(\\d+)");
private void transformForcedDisplay(Document doc, List<Element> styles, List<Element> regions, List<Element> forced, TransformerContext context) {
// extract style index count (maxIndex) and existing conditional index (cssIndex) if already present
int maxIndex = -1;
int cssIndex = -1;
for (Element elt : styles) {
String id = getXmlIdentifier(elt);
Matcher m = styleIdentifierPattern.matcher(id);
if (m.matches()) {
String styleIndex = m.group(1);
if (styleIndex != null) {
try {
int index = Integer.parseInt(styleIndex);
if (index > maxIndex)
maxIndex = index;
String condition = getCondition(elt);
if ((condition != null) && isForcedParameterCondition(condition))
cssIndex = index;
} catch (NumberFormatException e) {
}
}
}
if (cssIndex >= 0)
break;
}
// create and insert conditional style if needed
String forcedStyleIdentifier;
if (cssIndex < 0) {
forcedStyleIdentifier = "s" + (maxIndex + 1);
Element css = Documents.createElement(doc, isdStyleElementName);
Documents.setAttribute(css, TTML1StyleVerifier.visibilityAttributeName, "visible");
Documents.setAttribute(css, XML.xmlIdentifierAttributeName, forcedStyleIdentifier);
Documents.setAttribute(css, conditionAttributeName, forcedParameterCondition);
if (!regions.isEmpty()) {
Element firstRegion = regions.get(0);
assert firstRegion != null;
Element isdParent = (Element) firstRegion.getParentNode();
isdParent.insertBefore(css, firstRegion);
} else {
context.getReporter().logError(new IllegalStateException("no content to conditionally style"));
}
} else {
forcedStyleIdentifier = "s" + cssIndex;
}
// add reference from forced content to conditional visibility style
for (Element elt : forced) {
StringBuffer sb = new StringBuffer();
sb.append(Documents.getAttribute(elt, isdStyleAttributeName, ""));
if (sb.length() > 0)
sb.append(' ');
sb.append(forcedStyleIdentifier);
Documents.setAttribute(elt, isdStyleAttributeName, sb.toString());
}
// record uses forced status
context.setResourceState(ResourceState.isdUsesForcedVisibility.name(), Boolean.TRUE);
// record uses of ttml2 feature
context.setResourceState(ResourceState.isdUsesTTML2Feature.name(), Boolean.TRUE);
}
private String getCondition(Element elt) {
return Documents.getAttribute(elt, conditionAttributeName, null);
}
private boolean isForcedParameterCondition(String condition) {
return (condition != null) && condition.trim().equals(forcedParameterCondition);
}
private void transformMultiRowAlign(final Document doc, final TransformerContext context) {
if (IMSC1SemanticsVerifier.isIMSCTextProfile(context)) {
final List<Element> styles = new java.util.ArrayList<Element>();
final List<Element> paragraphs = new java.util.ArrayList<Element>();
try {
Traverse.traverseElements(doc, new PreVisitor() {
public boolean visit(Object content, Object parent, Visitor.Order order) {
assert content instanceof Element;
Element elt = (Element) content;
if (isISDStyleElement(elt)) {
styles.add(elt);
} else if (isParagraphElement(elt)) {
paragraphs.add(elt);
}
return true;
}
});
} catch (Exception e) {
context.getReporter().logError(e);
}
if (!paragraphs.isEmpty())
transformMultiRowAlign(doc, styles, paragraphs, context);
}
}
private void transformMultiRowAlign(Document doc, List<Element> styles, List<Element> paragraphs, TransformerContext context) {
Map<String,Element> styleMap = new java.util.HashMap<String,Element>();
int maxStyleIndex = -1;
for (Element s : styles) {
String id = getXmlIdentifier(s);
if (id != null) {
Matcher m = styleIdentifierPattern.matcher(id);
if (m.matches()) {
String styleIndex = m.group(1);
if (styleIndex != null) {
try {
int index = Integer.parseInt(styleIndex);
if (index > maxStyleIndex)
maxStyleIndex = index;
} catch (NumberFormatException e) {
}
}
}
styleMap.put(id, s);
}
}
Element pFirst = null;
List<Element> newStyles = new java.util.ArrayList<Element>();
for (Element p : paragraphs) {
if (pFirst == null)
pFirst = p;
String ma = getStyleAttribute(p, styleMap, multiRowAlignAttributeName, "auto");
if (isMultiRowAlignValue(ma)) {
String ta = getStyleAttribute(p, styleMap, TTML1StyleVerifier.textAlignAttributeName, "start");
if (ma.equals("auto"))
ma = ta;
if (!ma.equals(ta)) {
Element span = Documents.createElement(doc, spanElementName);
// add new style (css) element to represent text alignment as required
StringBuffer sb = new StringBuffer();
sb.append('s');
sb.append(++maxStyleIndex);
String id = sb.toString();
Element s = Documents.createElement(doc, isdStyleElementName);
setXmlIdentifier(s, id);
copyStyleAttributes(p, styleMap, s);
Documents.setAttribute(s, TTML1StyleVerifier.textAlignAttributeName, ma);
newStyles.add(s);
Documents.setAttribute(span, isdStyleAttributeName, id);
// capture child nodes
List<Node> children = new java.util.ArrayList<Node>();
for (Node n = p.getFirstChild(); n != null; n = n.getNextSibling())
children.add(n);
// remove child nodes, reparenting to new span
for (Node n : children)
span.appendChild(p.removeChild(n));
assert p.getFirstChild() == null;
// add new span to paragraph
p.appendChild(span);
// record uses of ttml2 feature
context.setResourceState(ResourceState.isdUsesTTML2Feature.name(), Boolean.TRUE);
}
}
}
// remove ebutts:multiRowAlign from current styles
for (Element s : styles) {
Documents.removeAttribute(s, multiRowAlignAttributeName);
}
// augment styles with new text align styles as generated above
if (!newStyles.isEmpty()) {
Element isd = getISD(pFirst);
Element rFirst = getFirstRegion(isd);
for (Element s : newStyles)
isd.insertBefore(s, rFirst);
}
}
private String getStyleAttribute(Element e, Map<String,Element> styles, QName styleName, String defaultValue) {
String css = Documents.getAttribute(e, isdStyleAttributeName);
if (css != null) {
Element s = styles.get(css);
if (s != null)
return Documents.getAttribute(s, styleName, defaultValue);
}
return defaultValue;
}
private void copyStyleAttributes(Element e, Map<String,Element> styles, Element eTarget) {
String css = Documents.getAttribute(e, isdStyleAttributeName);
if (css != null) {
Element s = styles.get(css);
if (s != null) {
Map<QName,String> attributes = Documents.getAttributes(s);
for (Map.Entry<QName,String> a : attributes.entrySet()) {
QName n = a.getKey();
String v = a.getValue();
if (isStyleAttribute(n))
Documents.setAttribute(eTarget, n, v);
}
}
}
}
private boolean isMultiRowAlignValue(String s) {
if (s.equals("start"))
return true;
else if (s.equals("end"))
return true;
else if (s.equals("center"))
return true;
else if (s.equals("auto"))
return true;
else
return false;
}
private void transformLinePadding(Document doc, TransformerContext context) {
}
private void maybeRecordTransformMetadata(Document doc, TransformerContext context) {
maybeRecordUsesForcedVisibility(doc, context);
maybeRecordRequiresTTML2(doc, context);
}
private void maybeRecordUsesForcedVisibility(Document doc, TransformerContext context) {
Boolean usesForcedVisibility = (Boolean) context.getResourceState(ResourceState.isdUsesForcedVisibility.name());
if ((usesForcedVisibility != null) && usesForcedVisibility) {
Element ttm = Documents.createElement(doc, metadataElementName);
Element ttmi = Documents.createElement(doc, itemElementName);
Documents.setAttribute(ttmi, nameAttributeName, "usesForced");
ttm.appendChild(ttmi);
Element root = doc.getDocumentElement();
Documents.insertFirst(root, ttm);
}
}
@Override
public boolean hasUsableContent(Element elt) {
if (super.hasUsableContent(elt))
return true;
else if (isImageElement(elt))
return true;
else if (hasBackgroundImage(elt))
return true;
else
return false;
}
public static boolean isImageElement(Element elt) {
return isTimedTextElement(elt, "image");
}
public static boolean hasBackgroundImage(Element elt) {
if (Documents.hasAttribute(elt, backgroundImageAttributeName)) {
String backgroundImage = Documents.getAttribute(elt, backgroundImageAttributeName, "").trim();
return !backgroundImage.isEmpty();
} else
return false;
}
}