You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PDFRenderingUtil.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /* $Id$ */
  18. package org.apache.fop.render.pdf;
  19. import java.awt.color.ICC_Profile;
  20. import java.io.FileNotFoundException;
  21. import java.io.IOException;
  22. import java.io.InputStream;
  23. import java.io.OutputStream;
  24. import java.net.URI;
  25. import java.net.URISyntaxException;
  26. import java.util.EnumMap;
  27. import java.util.Map;
  28. import org.apache.commons.io.IOUtils;
  29. import org.apache.commons.logging.Log;
  30. import org.apache.commons.logging.LogFactory;
  31. import org.apache.xmlgraphics.java2d.color.profile.ColorProfileUtil;
  32. import org.apache.xmlgraphics.xmp.Metadata;
  33. import org.apache.xmlgraphics.xmp.schemas.XMPBasicAdapter;
  34. import org.apache.xmlgraphics.xmp.schemas.XMPBasicSchema;
  35. import org.apache.fop.accessibility.Accessibility;
  36. import org.apache.fop.apps.FOUserAgent;
  37. import org.apache.fop.apps.io.InternalResourceResolver;
  38. import org.apache.fop.fo.extensions.xmp.XMPMetadata;
  39. import org.apache.fop.pdf.PDFAMode;
  40. import org.apache.fop.pdf.PDFArray;
  41. import org.apache.fop.pdf.PDFConformanceException;
  42. import org.apache.fop.pdf.PDFDictionary;
  43. import org.apache.fop.pdf.PDFDocument;
  44. import org.apache.fop.pdf.PDFEmbeddedFile;
  45. import org.apache.fop.pdf.PDFEmbeddedFiles;
  46. import org.apache.fop.pdf.PDFEncryptionManager;
  47. import org.apache.fop.pdf.PDFEncryptionParams;
  48. import org.apache.fop.pdf.PDFFileSpec;
  49. import org.apache.fop.pdf.PDFICCBasedColorSpace;
  50. import org.apache.fop.pdf.PDFICCStream;
  51. import org.apache.fop.pdf.PDFInfo;
  52. import org.apache.fop.pdf.PDFMetadata;
  53. import org.apache.fop.pdf.PDFNames;
  54. import org.apache.fop.pdf.PDFNumsArray;
  55. import org.apache.fop.pdf.PDFOutputIntent;
  56. import org.apache.fop.pdf.PDFPageLabels;
  57. import org.apache.fop.pdf.PDFReference;
  58. import org.apache.fop.pdf.PDFText;
  59. import org.apache.fop.pdf.PDFXMode;
  60. import org.apache.fop.pdf.Version;
  61. import org.apache.fop.pdf.VersionController;
  62. import org.apache.fop.render.pdf.extensions.PDFEmbeddedFileExtensionAttachment;
  63. import static org.apache.fop.render.pdf.PDFEncryptionOption.ENCRYPTION_PARAMS;
  64. import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ACCESSCONTENT;
  65. import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ANNOTATIONS;
  66. import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_ASSEMBLEDOC;
  67. import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_COPY_CONTENT;
  68. import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_EDIT_CONTENT;
  69. import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_FILLINFORMS;
  70. import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_PRINT;
  71. import static org.apache.fop.render.pdf.PDFEncryptionOption.NO_PRINTHQ;
  72. import static org.apache.fop.render.pdf.PDFEncryptionOption.OWNER_PASSWORD;
  73. import static org.apache.fop.render.pdf.PDFEncryptionOption.USER_PASSWORD;
  74. /**
  75. * Utility class which enables all sorts of features that are not directly connected to the
  76. * normal rendering process.
  77. */
  78. class PDFRenderingUtil {
  79. /** logging instance */
  80. private static Log log = LogFactory.getLog(PDFRenderingUtil.class);
  81. private FOUserAgent userAgent;
  82. /** the PDF Document being created */
  83. private PDFDocument pdfDoc;
  84. private PDFRendererOptionsConfig rendererConfig;
  85. /** the ICC stream used as output profile by this document for PDF/A and PDF/X functionality. */
  86. private PDFICCStream outputProfile;
  87. /** the default sRGB color space. */
  88. private PDFICCBasedColorSpace sRGBColorSpace;
  89. PDFRenderingUtil(FOUserAgent userAgent) {
  90. this.userAgent = userAgent;
  91. initialize();
  92. }
  93. private void initialize() {
  94. rendererConfig = PDFRendererOptionsConfig.DEFAULT.merge(createFromUserAgent(userAgent));
  95. if (rendererConfig.getPDFAMode().isPDFA1LevelA()) {
  96. //Enable accessibility if PDF/A-1a is enabled because it requires tagged PDF.
  97. userAgent.getRendererOptions().put(Accessibility.ACCESSIBILITY, Boolean.TRUE);
  98. }
  99. }
  100. private static PDFRendererOptionsConfig createFromUserAgent(FOUserAgent userAgent) {
  101. Map<PDFRendererOption, Object> properties
  102. = new EnumMap<PDFRendererOption, Object>(PDFRendererOption.class);
  103. for (PDFRendererOption option : PDFRendererOption.values()) {
  104. Object value = userAgent.getRendererOption(option);
  105. properties.put(option, option.parse(value));
  106. }
  107. PDFEncryptionParams encryptionConfig = new EncryptionParamsBuilder().createParams(userAgent);
  108. return new PDFRendererOptionsConfig(properties, encryptionConfig);
  109. }
  110. void mergeRendererOptionsConfig(PDFRendererOptionsConfig config) {
  111. rendererConfig = rendererConfig.merge(config);
  112. }
  113. private void updateInfo() {
  114. PDFInfo info = pdfDoc.getInfo();
  115. info.setCreator(userAgent.getCreator());
  116. info.setCreationDate(userAgent.getCreationDate());
  117. info.setAuthor(userAgent.getAuthor());
  118. info.setTitle(userAgent.getTitle());
  119. info.setSubject(userAgent.getSubject());
  120. info.setKeywords(userAgent.getKeywords());
  121. }
  122. private void updatePDFProfiles() {
  123. pdfDoc.getProfile().setPDFAMode(rendererConfig.getPDFAMode());
  124. pdfDoc.getProfile().setPDFXMode(rendererConfig.getPDFXMode());
  125. }
  126. private void addsRGBColorSpace() throws IOException {
  127. if (rendererConfig.getDisableSRGBColorSpace()) {
  128. if (rendererConfig.getPDFAMode() != PDFAMode.DISABLED
  129. || rendererConfig.getPDFXMode() != PDFXMode.DISABLED
  130. || rendererConfig.getOutputProfileURI() != null) {
  131. throw new IllegalStateException("It is not possible to disable the sRGB color"
  132. + " space if PDF/A or PDF/X functionality is enabled or an"
  133. + " output profile is set!");
  134. }
  135. } else {
  136. if (this.sRGBColorSpace != null) {
  137. return;
  138. }
  139. //Map sRGB as default RGB profile for DeviceRGB
  140. this.sRGBColorSpace = PDFICCBasedColorSpace.setupsRGBAsDefaultRGBColorSpace(pdfDoc);
  141. }
  142. }
  143. private void addDefaultOutputProfile() throws IOException {
  144. if (this.outputProfile != null) {
  145. return;
  146. }
  147. ICC_Profile profile;
  148. InputStream in = null;
  149. URI outputProfileUri = rendererConfig.getOutputProfileURI();
  150. if (outputProfileUri != null) {
  151. this.outputProfile = pdfDoc.getFactory().makePDFICCStream();
  152. in = userAgent.getResourceResolver().getResource(rendererConfig.getOutputProfileURI());
  153. try {
  154. profile = ColorProfileUtil.getICC_Profile(in);
  155. } finally {
  156. IOUtils.closeQuietly(in);
  157. }
  158. this.outputProfile.setColorSpace(profile, null);
  159. } else {
  160. //Fall back to sRGB profile
  161. outputProfile = sRGBColorSpace.getICCStream();
  162. }
  163. }
  164. /**
  165. * Adds an OutputIntent to the PDF as mandated by PDF/A-1 when uncalibrated color spaces
  166. * are used (which is true if we use DeviceRGB to represent sRGB colors).
  167. * @throws IOException in case of an I/O problem
  168. */
  169. private void addPDFA1OutputIntent() throws IOException {
  170. addDefaultOutputProfile();
  171. String desc = ColorProfileUtil.getICCProfileDescription(this.outputProfile.getICCProfile());
  172. PDFOutputIntent outputIntent = pdfDoc.getFactory().makeOutputIntent();
  173. outputIntent.setSubtype(PDFOutputIntent.GTS_PDFA1);
  174. outputIntent.setDestOutputProfile(this.outputProfile);
  175. outputIntent.setOutputConditionIdentifier(desc);
  176. outputIntent.setInfo(outputIntent.getOutputConditionIdentifier());
  177. pdfDoc.getRoot().addOutputIntent(outputIntent);
  178. }
  179. /**
  180. * Adds an OutputIntent to the PDF as mandated by PDF/X when uncalibrated color spaces
  181. * are used (which is true if we use DeviceRGB to represent sRGB colors).
  182. * @throws IOException in case of an I/O problem
  183. */
  184. private void addPDFXOutputIntent() throws IOException {
  185. addDefaultOutputProfile();
  186. String desc = ColorProfileUtil.getICCProfileDescription(this.outputProfile.getICCProfile());
  187. int deviceClass = this.outputProfile.getICCProfile().getProfileClass();
  188. if (deviceClass != ICC_Profile.CLASS_OUTPUT) {
  189. throw new PDFConformanceException(pdfDoc.getProfile().getPDFXMode() + " requires that"
  190. + " the DestOutputProfile be an Output Device Profile. "
  191. + desc + " does not match that requirement.");
  192. }
  193. PDFOutputIntent outputIntent = pdfDoc.getFactory().makeOutputIntent();
  194. outputIntent.setSubtype(PDFOutputIntent.GTS_PDFX);
  195. outputIntent.setDestOutputProfile(this.outputProfile);
  196. outputIntent.setOutputConditionIdentifier(desc);
  197. outputIntent.setInfo(outputIntent.getOutputConditionIdentifier());
  198. pdfDoc.getRoot().addOutputIntent(outputIntent);
  199. }
  200. public void renderXMPMetadata(XMPMetadata metadata) {
  201. Metadata docXMP = metadata.getMetadata();
  202. Metadata fopXMP = PDFMetadata.createXMPFromPDFDocument(pdfDoc);
  203. //Merge FOP's own metadata into the one from the XSL-FO document
  204. fopXMP.mergeInto(docXMP);
  205. XMPBasicAdapter xmpBasic = XMPBasicSchema.getAdapter(docXMP);
  206. //Metadata was changed so update metadata date
  207. xmpBasic.setMetadataDate(new java.util.Date());
  208. PDFMetadata.updateInfoFromMetadata(docXMP, pdfDoc.getInfo());
  209. PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(
  210. docXMP, metadata.isReadOnly());
  211. pdfDoc.getRoot().setMetadata(pdfMetadata);
  212. }
  213. public void generateDefaultXMPMetadata() {
  214. if (pdfDoc.getRoot().getMetadata() == null) {
  215. //If at this time no XMP metadata for the overall document has been set, create it
  216. //from the PDFInfo object.
  217. Metadata xmp = PDFMetadata.createXMPFromPDFDocument(pdfDoc);
  218. PDFMetadata pdfMetadata = pdfDoc.getFactory().makeMetadata(
  219. xmp, true);
  220. pdfDoc.getRoot().setMetadata(pdfMetadata);
  221. }
  222. }
  223. public PDFDocument setupPDFDocument(OutputStream out) throws IOException {
  224. if (this.pdfDoc != null) {
  225. throw new IllegalStateException("PDFDocument already set up");
  226. }
  227. String producer = userAgent.getProducer() != null ? userAgent.getProducer() : "";
  228. final Version maxPDFVersion = rendererConfig.getPDFVersion();
  229. if (maxPDFVersion == null) {
  230. this.pdfDoc = new PDFDocument(producer);
  231. } else {
  232. VersionController controller
  233. = VersionController.getFixedVersionController(maxPDFVersion);
  234. this.pdfDoc = new PDFDocument(producer, controller);
  235. }
  236. updateInfo();
  237. updatePDFProfiles();
  238. pdfDoc.setFilterMap(rendererConfig.getFilterMap());
  239. pdfDoc.outputHeader(out);
  240. //Setup encryption if necessary
  241. PDFEncryptionManager.setupPDFEncryption(rendererConfig.getEncryptionParameters(), pdfDoc);
  242. addsRGBColorSpace();
  243. if (rendererConfig.getOutputProfileURI() != null) {
  244. addDefaultOutputProfile();
  245. }
  246. PDFXMode pdfXMode = rendererConfig.getPDFXMode();
  247. if (pdfXMode != PDFXMode.DISABLED) {
  248. log.debug(pdfXMode + " is active.");
  249. log.warn("Note: " + pdfXMode
  250. + " support is work-in-progress and not fully implemented, yet!");
  251. addPDFXOutputIntent();
  252. }
  253. PDFAMode pdfAMode = rendererConfig.getPDFAMode();
  254. if (pdfAMode.isPDFA1LevelB()) {
  255. log.debug("PDF/A is active. Conformance Level: " + pdfAMode);
  256. addPDFA1OutputIntent();
  257. }
  258. this.pdfDoc.enableAccessibility(userAgent.isAccessibilityEnabled());
  259. return this.pdfDoc;
  260. }
  261. /**
  262. * Generates a page label in the PDF document.
  263. * @param pageIndex the index of the page
  264. * @param pageNumber the formatted page number
  265. */
  266. public void generatePageLabel(int pageIndex, String pageNumber) {
  267. //Produce page labels
  268. PDFPageLabels pageLabels = this.pdfDoc.getRoot().getPageLabels();
  269. if (pageLabels == null) {
  270. //Set up PageLabels
  271. pageLabels = this.pdfDoc.getFactory().makePageLabels();
  272. this.pdfDoc.getRoot().setPageLabels(pageLabels);
  273. }
  274. PDFNumsArray nums = pageLabels.getNums();
  275. PDFDictionary dict = new PDFDictionary(nums);
  276. dict.put("P", pageNumber);
  277. //TODO If the sequence of generated page numbers were inspected, this could be
  278. //expressed in a more space-efficient way
  279. nums.put(pageIndex, dict);
  280. }
  281. /**
  282. * Adds an embedded file to the PDF file.
  283. * @param embeddedFile the object representing the embedded file to be added
  284. * @throws IOException if an I/O error occurs
  285. */
  286. public void addEmbeddedFile(PDFEmbeddedFileExtensionAttachment embeddedFile)
  287. throws IOException {
  288. this.pdfDoc.getProfile().verifyEmbeddedFilesAllowed();
  289. PDFNames names = this.pdfDoc.getRoot().getNames();
  290. if (names == null) {
  291. //Add Names if not already present
  292. names = this.pdfDoc.getFactory().makeNames();
  293. this.pdfDoc.getRoot().setNames(names);
  294. }
  295. //Create embedded file
  296. PDFEmbeddedFile file = new PDFEmbeddedFile();
  297. this.pdfDoc.registerObject(file);
  298. URI srcURI;
  299. try {
  300. srcURI = InternalResourceResolver.cleanURI(embeddedFile.getSrc());
  301. } catch (URISyntaxException use) {
  302. throw new RuntimeException(use);
  303. }
  304. InputStream in = userAgent.getResourceResolver().getResource(srcURI);
  305. if (in == null) {
  306. throw new FileNotFoundException(embeddedFile.getSrc());
  307. }
  308. try {
  309. OutputStream out = file.getBufferOutputStream();
  310. IOUtils.copyLarge(in, out);
  311. } finally {
  312. IOUtils.closeQuietly(in);
  313. }
  314. PDFDictionary dict = new PDFDictionary();
  315. dict.put("F", file);
  316. String filename = PDFText.toPDFString(embeddedFile.getFilename(), '_');
  317. PDFFileSpec fileSpec = new PDFFileSpec(filename);
  318. fileSpec.setEmbeddedFile(dict);
  319. if (embeddedFile.getDesc() != null) {
  320. fileSpec.setDescription(embeddedFile.getDesc());
  321. }
  322. this.pdfDoc.registerObject(fileSpec);
  323. //Make sure there is an EmbeddedFiles in the Names dictionary
  324. PDFEmbeddedFiles embeddedFiles = names.getEmbeddedFiles();
  325. if (embeddedFiles == null) {
  326. embeddedFiles = new PDFEmbeddedFiles();
  327. this.pdfDoc.assignObjectNumber(embeddedFiles);
  328. this.pdfDoc.addTrailerObject(embeddedFiles);
  329. names.setEmbeddedFiles(embeddedFiles);
  330. }
  331. //Add to EmbeddedFiles in the Names dictionary
  332. PDFArray nameArray = embeddedFiles.getNames();
  333. if (nameArray == null) {
  334. nameArray = new PDFArray();
  335. embeddedFiles.setNames(nameArray);
  336. }
  337. String name = PDFText.toPDFString(filename);
  338. nameArray.add(name);
  339. nameArray.add(new PDFReference(fileSpec));
  340. }
  341. private static final class EncryptionParamsBuilder {
  342. private PDFEncryptionParams params;
  343. private EncryptionParamsBuilder() {
  344. }
  345. private PDFEncryptionParams createParams(FOUserAgent userAgent) {
  346. params = (PDFEncryptionParams) userAgent.getRendererOptions().get(ENCRYPTION_PARAMS);
  347. String userPassword = (String) userAgent.getRendererOption(USER_PASSWORD);
  348. if (userPassword != null) {
  349. getEncryptionParams().setUserPassword(userPassword);
  350. }
  351. String ownerPassword = (String) userAgent.getRendererOption(OWNER_PASSWORD);
  352. if (ownerPassword != null) {
  353. getEncryptionParams().setOwnerPassword(ownerPassword);
  354. }
  355. Object noPrint = userAgent.getRendererOption(NO_PRINT);
  356. if (noPrint != null) {
  357. getEncryptionParams().setAllowPrint(!booleanValueOf(noPrint));
  358. }
  359. Object noCopyContent = userAgent.getRendererOption(NO_COPY_CONTENT);
  360. if (noCopyContent != null) {
  361. getEncryptionParams().setAllowCopyContent(!booleanValueOf(noCopyContent));
  362. }
  363. Object noEditContent = userAgent.getRendererOption(NO_EDIT_CONTENT);
  364. if (noEditContent != null) {
  365. getEncryptionParams().setAllowEditContent(!booleanValueOf(noEditContent));
  366. }
  367. Object noAnnotations = userAgent.getRendererOption(NO_ANNOTATIONS);
  368. if (noAnnotations != null) {
  369. getEncryptionParams().setAllowEditAnnotations(!booleanValueOf(noAnnotations));
  370. }
  371. Object noFillInForms = userAgent.getRendererOption(NO_FILLINFORMS);
  372. if (noFillInForms != null) {
  373. getEncryptionParams().setAllowFillInForms(!booleanValueOf(noFillInForms));
  374. }
  375. Object noAccessContent = userAgent.getRendererOption(NO_ACCESSCONTENT);
  376. if (noAccessContent != null) {
  377. getEncryptionParams().setAllowAccessContent(!booleanValueOf(noAccessContent));
  378. }
  379. Object noAssembleDoc = userAgent.getRendererOption(NO_ASSEMBLEDOC);
  380. if (noAssembleDoc != null) {
  381. getEncryptionParams().setAllowAssembleDocument(!booleanValueOf(noAssembleDoc));
  382. }
  383. Object noPrintHQ = userAgent.getRendererOption(NO_PRINTHQ);
  384. if (noPrintHQ != null) {
  385. getEncryptionParams().setAllowPrintHq(!booleanValueOf(noPrintHQ));
  386. }
  387. return params;
  388. }
  389. private PDFEncryptionParams getEncryptionParams() {
  390. if (params == null) {
  391. params = new PDFEncryptionParams();
  392. }
  393. return params;
  394. }
  395. private static boolean booleanValueOf(Object obj) {
  396. if (obj instanceof Boolean) {
  397. return ((Boolean)obj).booleanValue();
  398. } else if (obj instanceof String) {
  399. return Boolean.valueOf((String)obj).booleanValue();
  400. } else {
  401. throw new IllegalArgumentException("Boolean or \"true\" or \"false\" expected.");
  402. }
  403. }
  404. }
  405. }