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