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 18 /* $Id: ResourceHandler.java 1809628 2017-09-25 13:42:23Z ssteiner $ */ 19 20 package org.apache.fop.render.ps; 21 22 import java.awt.geom.Rectangle2D; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.io.OutputStream; 26 import java.util.Map; 27 import java.util.Set; 28 29 import org.apache.commons.logging.Log; 30 import org.apache.commons.logging.LogFactory; 31 32 import org.apache.xmlgraphics.image.loader.ImageException; 33 import org.apache.xmlgraphics.image.loader.ImageFlavor; 34 import org.apache.xmlgraphics.image.loader.ImageInfo; 35 import org.apache.xmlgraphics.image.loader.ImageManager; 36 import org.apache.xmlgraphics.image.loader.ImageSessionContext; 37 import org.apache.xmlgraphics.image.loader.util.ImageUtil; 38 import org.apache.xmlgraphics.ps.DSCConstants; 39 import org.apache.xmlgraphics.ps.PSGenerator; 40 import org.apache.xmlgraphics.ps.PSResource; 41 import org.apache.xmlgraphics.ps.dsc.DSCException; 42 import org.apache.xmlgraphics.ps.dsc.DSCFilter; 43 import org.apache.xmlgraphics.ps.dsc.DSCListener; 44 import org.apache.xmlgraphics.ps.dsc.DSCParser; 45 import org.apache.xmlgraphics.ps.dsc.DSCParserConstants; 46 import org.apache.xmlgraphics.ps.dsc.DefaultNestedDocumentHandler; 47 import org.apache.xmlgraphics.ps.dsc.ResourceTracker; 48 import org.apache.xmlgraphics.ps.dsc.events.DSCComment; 49 import org.apache.xmlgraphics.ps.dsc.events.DSCCommentBoundingBox; 50 import org.apache.xmlgraphics.ps.dsc.events.DSCCommentDocumentNeededResources; 51 import org.apache.xmlgraphics.ps.dsc.events.DSCCommentDocumentSuppliedResources; 52 import org.apache.xmlgraphics.ps.dsc.events.DSCCommentHiResBoundingBox; 53 import org.apache.xmlgraphics.ps.dsc.events.DSCCommentIncludeResource; 54 import org.apache.xmlgraphics.ps.dsc.events.DSCCommentLanguageLevel; 55 import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPage; 56 import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPages; 57 import org.apache.xmlgraphics.ps.dsc.events.DSCEvent; 58 import org.apache.xmlgraphics.ps.dsc.events.DSCHeaderComment; 59 import org.apache.xmlgraphics.ps.dsc.events.PostScriptComment; 60 import org.apache.xmlgraphics.ps.dsc.events.PostScriptLine; 61 import org.apache.xmlgraphics.ps.dsc.tools.DSCTools; 62 63 import org.apache.fop.ResourceEventProducer; 64 import org.apache.fop.apps.FOUserAgent; 65 import org.apache.fop.fonts.FontInfo; 66 import org.apache.fop.render.ImageHandler; 67 import org.apache.fop.render.ImageHandlerRegistry; 68 69 /** 70 * This class is used when two-pass production is used to generate the PostScript file (setting 71 * "optimize-resources"). It uses the DSC parser from XML Graphics Commons to go over the 72 * temporary file generated by the PSRenderer and adds all used fonts and images as resources 73 * to the PostScript file. 74 */ 75 public class ResourceHandler implements DSCParserConstants, PSSupportedFlavors { 76 77 /** logging instance */ 78 private static Log log = LogFactory.getLog(ResourceHandler.class); 79 80 private FOUserAgent userAgent; 81 private FontInfo fontInfo; 82 83 private PSEventProducer eventProducer; 84 85 private ResourceTracker resTracker; 86 87 //key: URI, values PSImageFormResource 88 private Map globalFormResources = new java.util.HashMap(); 89 //key: PSResource, values PSImageFormResource 90 private Map inlineFormResources = new java.util.HashMap(); 91 92 /** 93 * Main constructor. 94 * @param userAgent the FO user agent 95 * @param eventProducer the event producer 96 * @param fontInfo the font information 97 * @param resTracker the resource tracker to use 98 * @param formResources Contains all forms used by this document (maintained by PSRenderer) 99 */ ResourceHandler(FOUserAgent userAgent, PSEventProducer eventProducer, FontInfo fontInfo, ResourceTracker resTracker, Map formResources)100 public ResourceHandler(FOUserAgent userAgent, PSEventProducer eventProducer, 101 FontInfo fontInfo, ResourceTracker resTracker, Map formResources) { 102 this.userAgent = userAgent; 103 this.eventProducer = eventProducer; 104 this.fontInfo = fontInfo; 105 this.resTracker = resTracker; 106 determineInlineForms(formResources); 107 } 108 109 /** 110 * This method splits up the form resources map into two. One for global forms which 111 * have been referenced more than once, and one for inline forms which have only been 112 * used once. The latter is to conserve memory in the PostScript interpreter. 113 * @param formResources the original form resources map 114 */ determineInlineForms(Map formResources)115 private void determineInlineForms(Map formResources) { 116 if (formResources == null) { 117 return; 118 } 119 for (Object o : formResources.entrySet()) { 120 Map.Entry entry = (Map.Entry) o; 121 PSResource res = (PSResource) entry.getValue(); 122 long count = resTracker.getUsageCount(res); 123 if (count > 1) { 124 //Make global form 125 this.globalFormResources.put(entry.getKey(), res); 126 } else { 127 //Inline resource 128 this.inlineFormResources.put(res, res); 129 resTracker.declareInlined(res); 130 } 131 } 132 } 133 134 /** 135 * Rewrites the temporary PostScript file generated by PSRenderer adding all needed resources 136 * (fonts and images). 137 * @param in the InputStream for the temporary PostScript file 138 * @param out the OutputStream to write the finished file to 139 * @param pageCount the number of pages (given here because PSRenderer writes an "(atend)") 140 * @param documentBoundingBox the document's bounding box 141 * (given here because PSRenderer writes an "(atend)") 142 * @param psUtil 143 * @throws DSCException If there's an error in the DSC structure of the PS file 144 * @throws IOException In case of an I/O error 145 */ process(InputStream in, OutputStream out, int pageCount, Rectangle2D documentBoundingBox, PSRenderingUtil psUtil)146 public void process(InputStream in, OutputStream out, 147 int pageCount, Rectangle2D documentBoundingBox, PSRenderingUtil psUtil) 148 throws DSCException, IOException { 149 DSCParser parser = new DSCParser(in); 150 parser.setCheckEOF(false); 151 152 PSGenerator gen = new PSGenerator(out); 153 gen.setAcrobatDownsample(psUtil.isAcrobatDownsample()); 154 parser.addListener(new DefaultNestedDocumentHandler(gen)); 155 parser.addListener(new IncludeResourceListener(gen)); 156 157 //Skip DSC header 158 DSCHeaderComment header = DSCTools.checkAndSkipDSC30Header(parser); 159 header.generate(gen); 160 161 parser.setFilter(new DSCFilter() { 162 private final Set filtered = new java.util.HashSet(); 163 { 164 //We rewrite those as part of the processing 165 filtered.add(DSCConstants.PAGES); 166 filtered.add(DSCConstants.BBOX); 167 filtered.add(DSCConstants.HIRES_BBOX); 168 filtered.add(DSCConstants.DOCUMENT_NEEDED_RESOURCES); 169 filtered.add(DSCConstants.DOCUMENT_SUPPLIED_RESOURCES); 170 } 171 public boolean accept(DSCEvent event) { 172 if (event.isDSCComment()) { 173 //Filter %%Pages which we add manually from a parameter 174 return !(filtered.contains(event.asDSCComment().getName())); 175 } else { 176 return true; 177 } 178 } 179 }); 180 181 //Get PostScript language level (may be missing) 182 while (true) { 183 DSCEvent event = parser.nextEvent(); 184 if (event == null) { 185 reportInvalidDSC(); 186 } 187 if (DSCTools.headerCommentsEndHere(event)) { 188 //Set number of pages 189 DSCCommentPages pages = new DSCCommentPages(pageCount); 190 pages.generate(gen); 191 new DSCCommentBoundingBox(documentBoundingBox).generate(gen); 192 new DSCCommentHiResBoundingBox(documentBoundingBox).generate(gen); 193 194 PSFontUtils.determineSuppliedFonts(resTracker, fontInfo, fontInfo.getUsedFonts()); 195 registerSuppliedForms(resTracker, globalFormResources); 196 197 //Supplied Resources 198 DSCCommentDocumentSuppliedResources supplied 199 = new DSCCommentDocumentSuppliedResources( 200 resTracker.getDocumentSuppliedResources()); 201 supplied.generate(gen); 202 203 //Needed Resources 204 DSCCommentDocumentNeededResources needed 205 = new DSCCommentDocumentNeededResources( 206 resTracker.getDocumentNeededResources()); 207 needed.generate(gen); 208 209 //Write original comment that ends the header comments 210 event.generate(gen); 211 break; 212 } 213 if (event.isDSCComment()) { 214 DSCComment comment = event.asDSCComment(); 215 if (DSCConstants.LANGUAGE_LEVEL.equals(comment.getName())) { 216 DSCCommentLanguageLevel level = (DSCCommentLanguageLevel)comment; 217 gen.setPSLevel(level.getLanguageLevel()); 218 } 219 } 220 event.generate(gen); 221 } 222 223 //Skip to the FOPFontSetup 224 PostScriptComment fontSetupPlaceholder = parser.nextPSComment("FOPFontSetup", gen); 225 if (fontSetupPlaceholder == null) { 226 throw new DSCException("Didn't find %FOPFontSetup comment in stream"); 227 } 228 PSFontUtils.writeFontDict(gen, fontInfo, fontInfo.getUsedFonts(), eventProducer); 229 generateForms(globalFormResources, gen); 230 231 //Skip the prolog and to the first page 232 DSCComment pageOrTrailer = parser.nextDSCComment(DSCConstants.PAGE, gen); 233 if (pageOrTrailer == null) { 234 throw new DSCException("Page expected, but none found"); 235 } 236 237 //Process individual pages (and skip as necessary) 238 while (true) { 239 DSCCommentPage page = (DSCCommentPage)pageOrTrailer; 240 page.generate(gen); 241 pageOrTrailer = DSCTools.nextPageOrTrailer(parser, gen); 242 if (pageOrTrailer == null) { 243 reportInvalidDSC(); 244 } else if (!DSCConstants.PAGE.equals(pageOrTrailer.getName())) { 245 pageOrTrailer.generate(gen); 246 break; 247 } 248 } 249 250 //Write the rest 251 while (parser.hasNext()) { 252 DSCEvent event = parser.nextEvent(); 253 event.generate(gen); 254 } 255 gen.flush(); 256 } 257 reportInvalidDSC()258 private static void reportInvalidDSC() throws DSCException { 259 throw new DSCException("File is not DSC-compliant: Unexpected end of file"); 260 } 261 registerSuppliedForms(ResourceTracker resTracker, Map formResources)262 private static void registerSuppliedForms(ResourceTracker resTracker, Map formResources) 263 throws IOException { 264 if (formResources == null) { 265 return; 266 } 267 for (Object o : formResources.values()) { 268 PSImageFormResource form = (PSImageFormResource) o; 269 resTracker.registerSuppliedResource(form); 270 } 271 } 272 generateForms(Map formResources, PSGenerator gen)273 private void generateForms(Map formResources, PSGenerator gen) throws IOException { 274 if (formResources == null) { 275 return; 276 } 277 for (Object o : formResources.values()) { 278 PSImageFormResource form = (PSImageFormResource) o; 279 generateFormForImage(gen, form); 280 } 281 } 282 generateFormForImage(PSGenerator gen, PSImageFormResource form)283 private void generateFormForImage(PSGenerator gen, PSImageFormResource form) 284 throws IOException { 285 final String uri = form.getImageURI(); 286 287 ImageManager manager = userAgent.getImageManager(); 288 ImageInfo info = null; 289 try { 290 ImageSessionContext sessionContext = userAgent.getImageSessionContext(); 291 info = manager.getImageInfo(uri, sessionContext); 292 293 //Create a rendering context for form creation 294 PSRenderingContext formContext = new PSRenderingContext( 295 userAgent, gen, fontInfo, true); 296 297 ImageFlavor[] flavors; 298 ImageHandlerRegistry imageHandlerRegistry 299 = userAgent.getImageHandlerRegistry(); 300 flavors = imageHandlerRegistry.getSupportedFlavors(formContext); 301 302 Map hints = ImageUtil.getDefaultHints(sessionContext); 303 org.apache.xmlgraphics.image.loader.Image img = manager.getImage( 304 info, flavors, hints, sessionContext); 305 306 ImageHandler basicHandler = imageHandlerRegistry.getHandler(formContext, img); 307 if (basicHandler == null) { 308 throw new UnsupportedOperationException( 309 "No ImageHandler available for image: " 310 + img.getInfo() + " (" + img.getClass().getName() + ")"); 311 } 312 313 if (!(basicHandler instanceof PSImageHandler)) { 314 throw new IllegalStateException( 315 "ImageHandler implementation doesn't behave properly." 316 + " It should have returned false in isCompatible(). Class: " 317 + basicHandler.getClass().getName()); 318 } 319 PSImageHandler handler = (PSImageHandler)basicHandler; 320 if (log.isTraceEnabled()) { 321 log.trace("Using ImageHandler: " + handler.getClass().getName()); 322 } 323 handler.generateForm(formContext, img, form); 324 325 } catch (ImageException ie) { 326 ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get( 327 userAgent.getEventBroadcaster()); 328 eventProducer.imageError(resTracker, (info != null ? info.toString() : uri), 329 ie, null); 330 } 331 } 332 333 /* not used 334 private static FormGenerator createMissingForm(String formName, final Dimension2D dimensions) { 335 FormGenerator formGen = new FormGenerator(formName, null, dimensions) { 336 337 protected void generatePaintProc(PSGenerator gen) throws IOException { 338 gen.writeln("0 setgray"); 339 gen.writeln("0 setlinewidth"); 340 String w = gen.formatDouble(dimensions.getWidth()); 341 String h = gen.formatDouble(dimensions.getHeight()); 342 gen.writeln(w + " " + h + " scale"); 343 gen.writeln("0 0 1 1 rectstroke"); 344 gen.writeln("newpath"); 345 gen.writeln("0 0 moveto"); 346 gen.writeln("1 1 lineto"); 347 gen.writeln("stroke"); 348 gen.writeln("newpath"); 349 gen.writeln("0 1 moveto"); 350 gen.writeln("1 0 lineto"); 351 gen.writeln("stroke"); 352 } 353 354 }; 355 return formGen; 356 } 357 */ 358 359 private class IncludeResourceListener implements DSCListener { 360 361 private PSGenerator gen; 362 IncludeResourceListener(PSGenerator gen)363 public IncludeResourceListener(PSGenerator gen) { 364 this.gen = gen; 365 } 366 367 /** {@inheritDoc} */ processEvent(DSCEvent event, DSCParser parser)368 public void processEvent(DSCEvent event, DSCParser parser) 369 throws IOException, DSCException { 370 if (event.isDSCComment() && event instanceof DSCCommentIncludeResource) { 371 DSCCommentIncludeResource include = (DSCCommentIncludeResource)event; 372 PSResource res = include.getResource(); 373 if (res.getType().equals(PSResource.TYPE_FORM)) { 374 if (inlineFormResources.containsValue(res)) { 375 PSImageFormResource form = (PSImageFormResource) 376 inlineFormResources.get(res); 377 //Create an inline form 378 //Wrap in save/restore pair to release memory 379 gen.writeln("save"); 380 generateFormForImage(gen, form); 381 boolean execformFound = false; 382 DSCEvent next = parser.nextEvent(); 383 if (next.isLine()) { 384 PostScriptLine line = next.asLine(); 385 if (line.getLine().endsWith(" execform")) { 386 line.generate(gen); 387 execformFound = true; 388 } 389 } 390 if (!execformFound) { 391 throw new IOException( 392 "Expected a PostScript line in the form: <form> execform"); 393 } 394 gen.writeln("restore"); 395 } else { 396 //Do nothing 397 } 398 parser.next(); 399 } 400 } 401 } 402 403 } 404 405 } 406