Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions fop-core/src/main/java/org/apache/fop/pdf/PDFLink.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class PDFLink extends PDFObject {
private String color;
private PDFAction action;
private Integer structParent;
private String contents;

/**
* create objects associated with a link annotation (GoToR)
Expand Down Expand Up @@ -81,6 +82,18 @@ public void setStructParent(int structParent) {
this.structParent = structParent;
}

/**
* Sets the alternate description written to the /Contents entry of the link
* annotation dictionary. PDF/UA-1 (ISO 14289-1 §7.18.5) requires every link
* annotation to carry such a description. This is used for internal
* (GoTo) links, whose action does not otherwise convey alternate text.
*
* @param contents the alternate description, typically from fox:alt-text
*/
public void setContents(String contents) {
this.contents = contents;
}

/**
* {@inheritDoc}
*/
Expand All @@ -101,16 +114,21 @@ public String toPDFString() {
+ this.action.getAction() + "\n" + "/H /I\n"
+ (this.structParent != null
? "/StructParent " + this.structParent.toString() + "\n" : "");
if (action instanceof PDFUri) {
String altText = ((PDFUri) action).getAltText();
if (altText != null && !altText.isEmpty()) {
if (getDocumentSafely().isEncryptionActive()) {
altText = new String(encodeText(altText), StandardCharsets.ISO_8859_1);
} else {
altText = PDFText.escapeText(altText);
}
dict += "/Contents " + altText + "\n";
// PDF/UA-1 requires every link annotation to carry an alternate
// description in /Contents. An explicitly set value (used for internal
// GoTo links) takes precedence; otherwise fall back to the alternate
// text carried by a URI action for external links.
String altText = this.contents;
if ((altText == null || altText.isEmpty()) && action instanceof PDFUri) {
altText = ((PDFUri) action).getAltText();
}
if (altText != null && !altText.isEmpty()) {
if (getDocumentSafely().isEncryptionActive()) {
altText = new String(encodeText(altText), StandardCharsets.ISO_8859_1);
} else {
altText = PDFText.escapeText(altText);
}
dict += "/Contents " + altText + "\n";
}
dict += fFlag + "\n>>";
return dict;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ public void renderLink(Link link) throws IFException {
PDFStructElem structure = (PDFStructElem) link.getAction().getStructureTreeElement();
if (documentHandler.getUserAgent().isAccessibilityEnabled() && structure != null) {
documentHandler.getLogicalStructureHandler().addLinkContentItem(pdfLink, structure);
// Propagate the link's alternate text (fox:alt-text, stored as
// /Alt on the Link structure element) to the annotation's
// /Contents entry, as required by PDF/UA-1 (ISO 14289-1
// §7.18.5). External (URI) links also carry this via their
// action, but internal (GoTo) links otherwise would not.
Object altText = structure.get("Alt");
if (altText instanceof String) {
pdfLink.setContents((String) altText);
}
}
documentHandler.getCurrentPage().addAnnotation(pdfLink);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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 org.junit.Test;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/**
* Tests that {@link PDFLink} writes the {@code /Contents} alternate description
* required by PDF/UA-1 (ISO 14289-1 §7.18.5) for internal (GoTo) links, not
* just external (URI) links.
*/
public class PDFLinkContentsTestCase {

private PDFLink newRegisteredLink(PDFDocument doc, PDFAction action) {
PDFLink link = new PDFLink(new Rectangle2D.Double(0, 0, 100, 20));
link.setAction(action);
doc.registerObject(action);
doc.registerObject(link);
return link;
}

/**
* An internal link (PDFGoTo action) carries no alternate text of its own, so
* previously no {@code /Contents} was written. With the alternate text set
* explicitly (as the renderer now does from the Link structure element's
* /Alt), the annotation dictionary must contain it.
*/
@Test
public void testInternalLinkWritesContents() {
PDFDocument doc = new PDFDocument("test");
PDFLink link = newRegisteredLink(doc, new PDFGoTo("1 0 R"));
link.setContents("Jump to target section");

assertTrue(link.toPDFString().contains("/Contents (Jump to target section)"));
}

/**
* Without an explicit description, an internal link still emits no
* {@code /Contents} (unchanged behaviour for documents that are not
* accessibility-enabled).
*/
@Test
public void testInternalLinkWithoutContents() {
PDFDocument doc = new PDFDocument("test");
PDFLink link = newRegisteredLink(doc, new PDFGoTo("1 0 R"));

assertFalse(link.toPDFString().contains("/Contents"));
}

/**
* For external links the {@code /Contents} value still falls back to the URI
* action's alternate text when none is set explicitly.
*/
@Test
public void testExternalLinkFallsBackToActionAltText() {
PDFDocument doc = new PDFDocument("test");
PDFLink link = newRegisteredLink(doc, new PDFUri("https://example.com", "Example website"));

assertTrue(link.toPDFString().contains("/Contents (Example website)"));
}

/**
* An explicitly set description takes precedence over the URI action's
* alternate text and is written only once.
*/
@Test
public void testExplicitContentsTakesPrecedence() {
PDFDocument doc = new PDFDocument("test");
PDFLink link = newRegisteredLink(doc, new PDFUri("https://example.com", "Action alt text"));
link.setContents("Explicit contents");

String pdf = link.toPDFString();
assertTrue(pdf.contains("/Contents (Explicit contents)"));
assertFalse(pdf.contains("Action alt text"));
}
}