import org.apache.fop.fo.FOEventHandler;
import org.apache.fop.fo.FONode;
import org.apache.fop.fo.FOText;
+import org.apache.fop.fo.FObj;
import org.apache.fop.fo.extensions.ExtensionElementMapping;
import org.apache.fop.fo.extensions.InternalElementMapping;
-import org.apache.fop.fo.flow.AbstractGraphics;
import org.apache.fop.fo.flow.AbstractRetrieveMarker;
import org.apache.fop.fo.flow.BasicLink;
import org.apache.fop.fo.flow.Block;
@Override
public void startLink(BasicLink basicLink) {
- startElementWithID(basicLink);
+ startElementWithIDAndAltText(basicLink, basicLink.getAltText());
}
@Override
@Override
public void image(ExternalGraphic eg) {
- startElementWithIDAndAltText(eg);
+ startElementWithIDAndAltText(eg, eg.getAltText());
endElement(eg);
}
@Override
public void startInstreamForeignObject(InstreamForeignObject ifo) {
- startElementWithIDAndAltText(ifo);
+ startElementWithIDAndAltText(ifo, ifo.getAltText());
}
@Override
node.getParent().getStructureTreeElement()));
}
- private void startElementWithIDAndAltText(AbstractGraphics node) {
+ private void startElementWithIDAndAltText(FObj node, String altText) {
AttributesImpl attributes = new AttributesImpl();
String localName = node.getLocalName();
- addRole(node, attributes);
+ addRole((CommonAccessibilityHolder)node, attributes);
addAttribute(attributes, ExtensionElementMapping.URI, "alt-text",
- ExtensionElementMapping.STANDARD_PREFIX, node.getAltText());
+ ExtensionElementMapping.STANDARD_PREFIX, altText);
node.setStructureTreeElement(
structureTreeEventHandler.startImageNode(localName, attributes,
node.getParent().getStructureTreeElement()));
private EventBroadcaster eventBroadcaster = new FOPEventBroadcaster();
private StructureTreeEventHandler structureTreeEventHandler
= DummyStructureTreeEventHandler.INSTANCE;
+ private boolean pdfUAEnabled;
/** Producer: Metadata element for the system/software that produces
* the document. (Some renderers can store this in the document.)
return this.eventBroadcaster;
}
+ public boolean isPdfUAEnabled() {
+ return pdfUAEnabled;
+ }
+
+ public void setPdfUAEnabled(boolean pdfUAEnabled) {
+ this.pdfUAEnabled = pdfUAEnabled;
+ }
+
private class FOPEventBroadcaster extends DefaultEventBroadcaster {
private EventListener rootListener;
// private ToBeImplementedProperty indicateDestination;
private String internalDestination;
private int showDestination;
+ private String altText;
// private ToBeImplementedProperty targetProcessingContext;
// private ToBeImplementedProperty targetPresentationContext;
// private ToBeImplementedProperty targetStylesheet;
// slightly stronger than spec "should be specified"
getFOValidationEventProducer().missingLinkDestination(this, getName(), locator);
}
+ if (getUserAgent().isAccessibilityEnabled()) {
+ altText = pList.get(PR_X_ALT_TEXT).getString();
+ if (altText.equals("") && getUserAgent().isPdfUAEnabled()) {
+ getFOValidationEventProducer().altTextMissing(this, getLocalName(), getLocator());
+ }
+ }
}
/** {@inheritDoc} */
public int getNameId() {
return FO_BASIC_LINK;
}
+
+ public String getAltText() {
+ return altText;
+ }
}
import org.apache.xmlgraphics.xmp.schemas.pdf.AdobePDFSchema;
import org.apache.xmlgraphics.xmp.schemas.pdf.PDFAAdapter;
import org.apache.xmlgraphics.xmp.schemas.pdf.PDFAXMPSchema;
+import org.apache.xmlgraphics.xmp.schemas.pdf.PDFUAAdapter;
+import org.apache.xmlgraphics.xmp.schemas.pdf.PDFUAXMPSchema;
import org.apache.xmlgraphics.xmp.schemas.pdf.PDFVTAdapter;
import org.apache.xmlgraphics.xmp.schemas.pdf.PDFVTXMPSchema;
import org.apache.xmlgraphics.xmp.schemas.pdf.PDFXAdapter;
//Somewhat redundant but some PDF/A checkers issue a warning without this.
dc.setFormat("application/pdf");
+ PDFUAMode pdfuaMode = pdfDoc.getProfile().getPDFUAMode();
+ if (pdfuaMode.isEnabled()) {
+ PDFUAAdapter pdfua = PDFUAXMPSchema.getAdapter(meta);
+ pdfua.setPart(pdfuaMode.getPart());
+ }
+
//PDF/A identification
PDFAMode pdfaMode = pdfDoc.getProfile().getPDFAMode();
if (pdfaMode.isEnabled()) {
*/
protected PDFAMode pdfAMode = PDFAMode.DISABLED;
+ protected PDFUAMode pdfUAMode = PDFUAMode.DISABLED;
+
/**
* Indicates the PDF/X mode currently active. Defaults to "no restrictions", i.e.
* PDF/X not active.
return this.pdfAMode;
}
+ public PDFUAMode getPDFUAMode() {
+ return this.pdfUAMode;
+ }
+
/** @return true if any PDF/A mode is active */
public boolean isPDFAActive() {
return getPDFAMode() != PDFAMode.DISABLED;
validateProfileCombination();
}
+ public void setPDFUAMode(PDFUAMode mode) {
+ if (mode == null) {
+ mode = PDFUAMode.DISABLED;
+ }
+ this.pdfUAMode = mode;
+ validateProfileCombination();
+ }
+
/** @return the PDF/X mode */
public PDFXMode getPDFXMode() {
return this.pdfXMode;
sb.append(getPDFAMode());
} else if (isPDFXActive()) {
sb.append(getPDFXMode());
+ } else if (getPDFUAMode().isEnabled()) {
+ sb.append(getPDFUAMode());
} else {
sb.append(super.toString());
}
* Checks a few things required for tagged PDF.
*/
public void verifyTaggedPDF() {
- if (getPDFAMode().isLevelA()) {
+ if (getPDFAMode().isLevelA() || getPDFUAMode().isEnabled()) {
final String err = "{0} requires the {1} dictionary entry to be set";
+ String mode = getPDFAMode().toString();
+ if (getPDFUAMode().isEnabled()) {
+ mode = getPDFUAMode().toString();
+ }
PDFDictionary markInfo = getDocument().getRoot().getMarkInfo();
if (markInfo == null) {
throw new PDFConformanceException(format(
- "{0} requires that the accessibility option in the configuration file be enabled",
- getPDFAMode()));
+ "{0} requires that the accessibility option in the configuration file be enabled", mode));
}
if (!Boolean.TRUE.equals(markInfo.get("Marked"))) {
throw new PDFConformanceException(format(err,
- new Object[] {getPDFAMode(), "Marked"}));
+ new Object[] {mode, "Marked"}));
}
if (getDocument().getRoot().getStructTreeRoot() == null) {
throw new PDFConformanceException(format(err,
- new Object[] {getPDFAMode(), "StructTreeRoot"}));
+ new Object[] {mode, "StructTreeRoot"}));
}
if (getDocument().getRoot().getLanguage() == null) {
throw new PDFConformanceException(format(err,
- new Object[] {getPDFAMode(), "Lang"}));
+ new Object[] {mode, "Lang"}));
}
}
}
/** @return true if all fonts need to be embedded. */
public boolean isFontEmbeddingRequired() {
- return isPDFAActive() || isPDFXActive();
+ return isPDFAActive() || isPDFXActive() || getPDFUAMode().isEnabled();
}
/** Checks if a title may be absent. */
public void verifyTitleAbsent() {
+ final String err = "{0} requires the title to be set.";
+ if (getPDFUAMode().isEnabled()) {
+ throw new PDFConformanceException(format(err, getPDFUAMode()));
+ }
if (isPDFXActive()) {
- final String err = "{0} requires the title to be set.";
throw new PDFConformanceException(format(err, getPDFXMode()));
}
}
/** {@inheritDoc} */
public int output(OutputStream stream) throws IOException {
+ if (document.getProfile().getPDFUAMode().isEnabled()) {
+ PDFDictionary d = new PDFDictionary();
+ d.put("DisplayDocTitle", true);
+ put("ViewerPreferences", d);
+ }
getDocument().getProfile().verifyTaggedPDF();
return super.output(stream);
}
return this.kids;
}
+ public int output(OutputStream stream) throws IOException {
+ if (getDocument().getProfile().getPDFUAMode().isEnabled()
+ && entries.containsKey("Alt") && "".equals(get("Alt"))) {
+ put("Alt", "No alternate text specified");
+ }
+ return super.output(stream);
+ }
+
/**
* Class representing a placeholder for a PDF Structure Element.
*/
--- /dev/null
+/*
+ * 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.pdf;
+
+/** Enum class for PDF/UA modes. */
+public enum PDFUAMode {
+
+ /** PDF/UA disabled. */
+ DISABLED("PDF/UA disabled"),
+ /** PDF/UA-1 enabled. */
+ PDFUA_1(1);
+
+ private final String name;
+
+ private final int part;
+
+ /**
+ * Constructor to add a new named item.
+ * @param name Name of the item.
+ */
+ private PDFUAMode(String name) {
+ this.name = name;
+ this.part = 0;
+ }
+
+ private PDFUAMode(int part) {
+ this.name = "PDF/UA-" + part;
+ this.part = part;
+ }
+
+ /** @return the name of the enum */
+ public String getName() {
+ return this.name;
+ }
+
+ public int getPart() {
+ return part;
+ }
+
+ /**
+ * Returns {@code true} if this enum corresponds to one of the available PDF/A modes.
+ *
+ * @return {@code true} if this is not DISABLED
+ */
+ public boolean isEnabled() {
+ return this != DISABLED;
+ }
+
+ /**
+ * Returns the mode enum object given a String.
+ * @param s the string
+ * @return the PDFAMode enum object (DISABLED will be returned if no match is found)
+ */
+ public static PDFUAMode getValueOf(String s) {
+ for (PDFUAMode mode : values()) {
+ if (mode.name.equalsIgnoreCase(s)) {
+ return mode;
+ }
+ }
+ return DISABLED;
+ }
+
+ /** {@inheritDoc} */
+ public String toString() {
+ return name;
+ }
+
+}
import org.apache.fop.fonts.LazyFont;
import org.apache.fop.fonts.SingleByteFont;
import org.apache.fop.fonts.Typeface;
+import org.apache.fop.pdf.PDFArray;
+import org.apache.fop.pdf.PDFDictionary;
+import org.apache.fop.pdf.PDFName;
import org.apache.fop.pdf.PDFNumber;
import org.apache.fop.pdf.PDFStructElem;
import org.apache.fop.pdf.PDFTextUtil;
placeImage(rect, xobject);
}
} else {
+ addStructTreeBBox(rect);
drawImageUsingURI(uri, rect);
if (!getDocumentHandler().getPDFDocument().isLinearizationEnabled()) {
flushPDFDoc();
}
}
+ private void addStructTreeBBox(Rectangle rect) {
+ if (accessEnabled && getDocumentHandler().getPDFDocument().getProfile().getPDFUAMode().isEnabled()) {
+ PDFStructElem structElem = (PDFStructElem) getContext().getStructureTreeElement();
+ PDFDictionary d = new PDFDictionary();
+ int x = rect.x / 1000;
+ int y = rect.y / 1000;
+ int w = rect.width / 1000;
+ int h = rect.height / 1000;
+ d.put("BBox", new PDFArray(x, y, w, h));
+ d.put("O", new PDFName("Layout"));
+ structElem.put("A", d);
+ }
+ }
+
@Override
protected void drawImageUsingURI(String uri, Rectangle rect) {
ImageManager manager = getUserAgent().getImageManager();
if (accessEnabled) {
PDFStructElem structElem = (PDFStructElem) getContext().getStructureTreeElement();
prepareImageMCID(structElem);
+ addStructTreeBBox(rect);
}
drawImageUsingDocument(doc, rect);
if (!getDocumentHandler().getPDFDocument().isLinearizationEnabled()) {
}
if (rect.width != 0 && rect.height != 0) {
generator.endTextObject();
+ if (accessEnabled && getUserAgent().isPdfUAEnabled()) {
+ generator.beginMarkedContentSequence(null, 0, null);
+ }
if (fill != null) {
if (fill instanceof Color) {
generator.updateColor((Color)fill, true, null);
}*/
sb.append('\n');
generator.add(sb.toString());
+ if (accessEnabled && getUserAgent().isPdfUAEnabled()) {
+ generator.endMarkedContentSequence();
+ }
}
}
BorderProps left, BorderProps right, Color innerBackgroundColor) throws IFException {
if (top != null || bottom != null || left != null || right != null) {
generator.endTextObject();
+ if (accessEnabled && getUserAgent().isPdfUAEnabled()) {
+ generator.beginMarkedContentSequence(null, 0, null);
+ }
this.borderPainter.drawBorders(rect, top, bottom, left, right, innerBackgroundColor);
+ if (accessEnabled && getUserAgent().isPdfUAEnabled()) {
+ generator.endMarkedContentSequence();
+ }
}
}
-
-
-
/** {@inheritDoc} */
@Override
public void drawLine(Point start, Point end, int width, Color color, RuleStyle style)
throws IFException {
generator.endTextObject();
+ if (accessEnabled && getUserAgent().isPdfUAEnabled()) {
+ generator.beginMarkedContentSequence(null, 0, null);
+ }
try {
this.graphicsPainter.drawLine(start, end, width, color, style);
} catch (IOException ioe) {
throw new IFException("Cannot draw line", ioe);
}
+ if (accessEnabled && getUserAgent().isPdfUAEnabled()) {
+ generator.endMarkedContentSequence();
+ }
}
private Typeface getTypeface(String fontName) {
import static org.apache.fop.render.pdf.PDFRendererOption.MERGE_FONTS;
import static org.apache.fop.render.pdf.PDFRendererOption.OUTPUT_PROFILE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_A_MODE;
+import static org.apache.fop.render.pdf.PDFRendererOption.PDF_UA_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_VT_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_X_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.VERSION;
try {
buildFilterMapFromConfiguration(cfg);
parseAndPut(PDF_A_MODE, cfg);
+ parseAndPut(PDF_UA_MODE, cfg);
parseAndPut(PDF_X_MODE, cfg);
parseAndPut(PDF_VT_MODE, cfg);
configureEncryptionParams(cfg, userAgent, strict);
import org.apache.fop.apps.io.InternalResourceResolver;
import org.apache.fop.pdf.PDFAMode;
+import org.apache.fop.pdf.PDFUAMode;
import org.apache.fop.pdf.PDFVTMode;
import org.apache.fop.pdf.PDFXMode;
import org.apache.fop.pdf.Version;
return PDFAMode.getValueOf(value);
}
},
+ PDF_UA_MODE("pdf-ua-mode", PDFUAMode.DISABLED) {
+ @Override
+ PDFUAMode deserialize(String value) {
+ return PDFUAMode.getValueOf(value);
+ }
+ },
/** Rendering Options key for the PDF/X mode, default: {@link PDFXMode#DISABLED} */
PDF_X_MODE("pdf-x-mode", PDFXMode.DISABLED) {
@Override
import org.apache.fop.pdf.PDFAMode;
import org.apache.fop.pdf.PDFEncryptionParams;
+import org.apache.fop.pdf.PDFUAMode;
import org.apache.fop.pdf.PDFVTMode;
import org.apache.fop.pdf.PDFXMode;
import org.apache.fop.pdf.Version;
import static org.apache.fop.render.pdf.PDFRendererOption.MERGE_FONTS;
import static org.apache.fop.render.pdf.PDFRendererOption.OUTPUT_PROFILE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_A_MODE;
+import static org.apache.fop.render.pdf.PDFRendererOption.PDF_UA_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_VT_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_X_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.VERSION;
return (PDFAMode) properties.get(PDF_A_MODE);
}
+ public PDFUAMode getPDFUAMode() {
+ return (PDFUAMode) properties.get(PDF_UA_MODE);
+ }
+
public PDFXMode getPDFXMode() {
return (PDFXMode) properties.get(PDF_X_MODE);
}
private void updatePDFProfiles() {
pdfDoc.getProfile().setPDFAMode(rendererConfig.getPDFAMode());
+ pdfDoc.getProfile().setPDFUAMode(rendererConfig.getPDFUAMode());
+ userAgent.setPdfUAEnabled(pdfDoc.getProfile().getPDFUAMode().isEnabled());
pdfDoc.getProfile().setPDFXMode(rendererConfig.getPDFXMode());
pdfDoc.getProfile().setPDFVTMode(rendererConfig.getPDFVTMode());
}
addBuilder("list-item-body", StandardStructureTypes.List.LBODY);
addBuilder("list-item-label", StandardStructureTypes.List.LBL);
// Dynamic Effects: Link and Multi Formatting Objects
- addBuilder("basic-link", StandardStructureTypes.InlineLevelStructure.LINK);
+ addBuilder("basic-link", new LinkBuilder());
// Out-of-Line Formatting Objects
addBuilder("float", StandardStructureTypes.Grouping.DIV);
addBuilder("footnote", StandardStructureTypes.InlineLevelStructure.NOTE);
}
+ private static class LinkBuilder extends DefaultStructureElementBuilder {
+ LinkBuilder() {
+ super(StandardStructureTypes.InlineLevelStructure.LINK);
+ }
+
+ @Override
+ protected void setAttributes(PDFStructElem structElem, Attributes attributes) {
+ super.setAttributes(structElem, attributes);
+ String altTextNode = attributes.getValue(ExtensionElementMapping.URI, "alt-text");
+ if (altTextNode == null) {
+ altTextNode = "No alternate text specified";
+ }
+ structElem.put("Alt", altTextNode);
+ }
+ }
+
private static class TableBuilder extends DefaultStructureElementBuilder {
TableBuilder() {
}
private boolean isPDFA1Safe(String name) {
- return !(pdfFactory.getDocument().getProfile().getPDFAMode().isPart1()
+ return !((pdfFactory.getDocument().getProfile().getPDFAMode().isPart1()
+ || pdfFactory.getDocument().getProfile().getPDFUAMode().isEnabled())
&& (name.equals("table-body")
|| name.equals("table-header")
|| name.equals("table-footer")));
</fo:static-content>
<fo:flow flow-name="xsl-region-body">
<fo:block id="4">This is a link to the <fo:wrapper id="5" color="blue"><fo:basic-link id="6"
- internal-destination="second-start">next page-sequence</fo:basic-link></fo:wrapper>
+ internal-destination="second-start" fox:alt-text="">next page-sequence</fo:basic-link></fo:wrapper>
(which starts on page <fo:page-number-citation id="7" ref-id="second-start"/> and ends on
page <fo:page-number-citation-last id="8" ref-id="second-end"/>).</fo:block>
<fo:block id="9" font-family="sans-serif" font-weight="bold" space-before="1em"
<fo:block id="11">This block of text contains a footnote<fo:footnote id="12"><fo:inline id="13"
baseline-shift="super" font-size="70%">1</fo:inline><fo:footnote-body id="14"><fo:block
id="15">A footnote with a link to the <fo:wrapper id="16" color="blue"><fo:basic-link
- id="17" external-destination="http://xmlgraphics.apache.org/fop/">FOP
+ id="17" external-destination="http://xmlgraphics.apache.org/fop/" fox:alt-text="">FOP
website</fo:basic-link></fo:wrapper></fo:block></fo:footnote-body></fo:footnote>
call.</fo:block>
<fo:table id="18" space-before="1em" width="100%" table-layout="fixed">
--- /dev/null
+/*
+ * 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.pdf;
+
+import java.awt.geom.Rectangle2D;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.xmlgraphics.util.QName;
+import org.apache.xmlgraphics.xmp.Metadata;
+
+import org.apache.fop.render.pdf.PDFContentGenerator;
+
+public class PDFUATestCase {
+ @Test
+ public void testXMP() throws IOException {
+ PDFDocument doc = new PDFDocument("");
+ doc.getProfile().setPDFUAMode(PDFUAMode.PDFUA_1);
+ Metadata metadata = PDFMetadata.createXMPFromPDFDocument(doc);
+ StringBuilder sb = new StringBuilder();
+ Iterator i = metadata.iterator();
+ while (i.hasNext()) {
+ QName k = (QName) i.next();
+ sb.append(k + ": " + metadata.getProperty(k).getValue() + "\n");
+ }
+ String s = sb.toString();
+ Assert.assertTrue(s, s.contains("pdfuaid:part: 1"));
+ }
+
+ @Test
+ public void testPDF() throws IOException {
+ PDFDocument doc = new PDFDocument("");
+ doc.getRoot().makeTagged();
+ doc.getRoot().setStructTreeRoot(new PDFStructTreeRoot(null));
+ doc.getInfo().setTitle("title");
+ doc.getProfile().setPDFUAMode(PDFUAMode.PDFUA_1);
+ PDFResources resources = new PDFResources(doc);
+ doc.addObject(resources);
+ PDFResourceContext context = new PDFResourceContext(resources);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ PDFContentGenerator gen = new PDFContentGenerator(doc, out, context);
+ Rectangle2D.Float f = new Rectangle2D.Float();
+ PDFPage page = new PDFPage(resources, 0, f, f, f, f);
+ doc.addImage(context, new BitmapImage("", 1, 1, new byte[0], null));
+ doc.registerObject(page);
+ gen.flushPDFDoc();
+ doc.outputTrailer(out);
+
+ Collection<StringBuilder> objs = PDFLinearizationTestCase.readObjs(
+ new ByteArrayInputStream(out.toByteArray())).values();
+ Assert.assertTrue(getObj(objs, "/Type /Catalog").contains("/ViewerPreferences << /DisplayDocTitle true >>"));
+ }
+
+ private String getObj(Collection<StringBuilder> objs, String x) {
+ for (StringBuilder s : objs) {
+ if (s.toString().contains(x)) {
+ return s.toString();
+ }
+ }
+ return null;
+ }
+}