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: AFPDocumentHandler.java 1866691 2019-09-09 13:20:08Z ssteiner $ */
19 
20 package org.apache.fop.render.afp;
21 
22 import java.awt.Color;
23 import java.awt.Dimension;
24 import java.awt.geom.AffineTransform;
25 import java.io.IOException;
26 import java.net.URI;
27 import java.util.HashMap;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 
33 import org.apache.fop.afp.AFPDitheredRectanglePainter;
34 import org.apache.fop.afp.AFPPaintingState;
35 import org.apache.fop.afp.AFPRectanglePainter;
36 import org.apache.fop.afp.AFPResourceLevelDefaults;
37 import org.apache.fop.afp.AFPResourceManager;
38 import org.apache.fop.afp.AFPUnitConverter;
39 import org.apache.fop.afp.AbstractAFPPainter;
40 import org.apache.fop.afp.DataStream;
41 import org.apache.fop.afp.fonts.AFPFontCollection;
42 import org.apache.fop.afp.fonts.AFPPageFonts;
43 import org.apache.fop.afp.modca.ResourceObject;
44 import org.apache.fop.afp.util.AFPResourceAccessor;
45 import org.apache.fop.apps.MimeConstants;
46 import org.apache.fop.fonts.FontCollection;
47 import org.apache.fop.fonts.FontEventAdapter;
48 import org.apache.fop.fonts.FontInfo;
49 import org.apache.fop.fonts.FontManager;
50 import org.apache.fop.render.afp.AFPRendererConfig.AFPRendererConfigParser;
51 import org.apache.fop.render.afp.extensions.AFPElementMapping;
52 import org.apache.fop.render.afp.extensions.AFPIncludeFormMap;
53 import org.apache.fop.render.afp.extensions.AFPInvokeMediumMap;
54 import org.apache.fop.render.afp.extensions.AFPPageOverlay;
55 import org.apache.fop.render.afp.extensions.AFPPageSegmentElement;
56 import org.apache.fop.render.afp.extensions.AFPPageSetup;
57 import org.apache.fop.render.afp.extensions.ExtensionPlacement;
58 import org.apache.fop.render.intermediate.AbstractBinaryWritingIFDocumentHandler;
59 import org.apache.fop.render.intermediate.IFContext;
60 import org.apache.fop.render.intermediate.IFDocumentHandlerConfigurator;
61 import org.apache.fop.render.intermediate.IFException;
62 import org.apache.fop.render.intermediate.IFPainter;
63 
64 /**
65  * {@link org.apache.fop.render.intermediate.IFDocumentHandler} implementation that produces AFP
66  * (MO:DCA).
67  */
68 public class AFPDocumentHandler extends AbstractBinaryWritingIFDocumentHandler
69             implements AFPCustomizable {
70 
71     //** logging instance */
72     //private static Log log = LogFactory.getLog(AFPDocumentHandler.class);
73 
74     /** the resource manager */
75     private AFPResourceManager resourceManager;
76 
77     /** the painting state */
78     private final AFPPaintingState paintingState;
79 
80     /** unit converter */
81     private final AFPUnitConverter unitConv;
82 
83     /** the AFP datastream */
84     private DataStream dataStream;
85 
86     /** the map of page segments */
87     private Map<String, PageSegmentDescriptor> pageSegmentMap
88         = new java.util.HashMap<String, PageSegmentDescriptor>();
89 
90 
91     // Rounded corners are cached at the document level
92     private Map<String, String> roundedCornerNameCache
93             = new HashMap<String, String>();
94 
95     private int roundedCornerCount;
96 
97     private static enum Location {
98         ELSEWHERE, IN_DOCUMENT_HEADER, FOLLOWING_PAGE_SEQUENCE, IN_PAGE_HEADER
99     }
100 
101     private Location location = Location.ELSEWHERE;
102 
103     /** temporary holds extensions that have to be deferred until the end of the page-sequence */
104     private List<AFPPageSetup> deferredPageSequenceExtensions
105         = new java.util.LinkedList<AFPPageSetup>();
106 
107     /** the shading mode for filled rectangles */
108     private AFPShadingMode shadingMode = AFPShadingMode.COLOR;
109 
110     /**
111      * Default constructor.
112      */
AFPDocumentHandler(IFContext context)113     public AFPDocumentHandler(IFContext context) {
114         super(context);
115         this.resourceManager = new AFPResourceManager(context.getUserAgent().getResourceResolver());
116         this.paintingState = new AFPPaintingState();
117         this.unitConv = paintingState.getUnitConverter();
118     }
119 
120     /** {@inheritDoc} */
supportsPagesOutOfOrder()121     public boolean supportsPagesOutOfOrder() {
122         return false;
123     }
124 
125     /** {@inheritDoc} */
getMimeType()126     public String getMimeType() {
127         return MimeConstants.MIME_AFP;
128     }
129 
130     /** {@inheritDoc} */
getConfigurator()131     public IFDocumentHandlerConfigurator getConfigurator() {
132         return new AFPRendererConfigurator(getUserAgent(), new AFPRendererConfigParser());
133     }
134 
135     /** {@inheritDoc} */
136     @Override
setDefaultFontInfo(FontInfo fontInfo)137     public void setDefaultFontInfo(FontInfo fontInfo) {
138         FontManager fontManager = getUserAgent().getFontManager();
139         FontCollection[] fontCollections = new FontCollection[] {
140             new AFPFontCollection(getUserAgent().getEventBroadcaster(), null)
141         };
142 
143         FontInfo fi = (fontInfo != null ? fontInfo : new FontInfo());
144         fi.setEventListener(new FontEventAdapter(getUserAgent().getEventBroadcaster()));
145         fontManager.setup(fi, fontCollections);
146         setFontInfo(fi);
147     }
148 
getPaintingState()149     AFPPaintingState getPaintingState() {
150         return this.paintingState;
151     }
152 
getDataStream()153     DataStream getDataStream() {
154         return this.dataStream;
155     }
156 
getResourceManager()157     AFPResourceManager getResourceManager() {
158         return this.resourceManager;
159     }
160 
createRectanglePainter()161     AbstractAFPPainter createRectanglePainter() {
162         if (AFPShadingMode.DITHERED.equals(this.shadingMode)) {
163             return new AFPDitheredRectanglePainter(
164                     getPaintingState(), getDataStream(), getResourceManager());
165         } else {
166             return new AFPRectanglePainter(
167                     getPaintingState(), getDataStream());
168         }
169     }
170 
171     /** {@inheritDoc} */
172     @Override
startDocument()173     public void startDocument() throws IFException {
174         super.startDocument();
175         try {
176             paintingState.setColor(Color.WHITE);
177 
178             this.dataStream = resourceManager.createDataStream(paintingState, outputStream);
179 
180             this.dataStream.startDocument();
181         } catch (IOException e) {
182             throw new IFException("I/O error in startDocument()", e);
183         }
184     }
185 
186 
187     /** {@inheritDoc} */
188     @Override
startDocumentHeader()189     public void startDocumentHeader() throws IFException {
190         super.startDocumentHeader();
191         this.location = Location.IN_DOCUMENT_HEADER;
192     }
193 
194     /** {@inheritDoc} */
195     @Override
endDocumentHeader()196     public void endDocumentHeader() throws IFException {
197         super.endDocumentHeader();
198         this.location = Location.ELSEWHERE;
199     }
200 
201     /** {@inheritDoc} */
202     @Override
endDocument()203     public void endDocument() throws IFException {
204         try {
205             this.dataStream.endDocument();
206             this.dataStream = null;
207             this.resourceManager.writeToStream();
208             this.resourceManager = null;
209         } catch (IOException ioe) {
210             throw new IFException("I/O error in endDocument()", ioe);
211         }
212         super.endDocument();
213     }
214 
215     /** {@inheritDoc} */
startPageSequence(String id)216     public void startPageSequence(String id) throws IFException {
217         try {
218             dataStream.startPageGroup();
219         } catch (IOException ioe) {
220             throw new IFException("I/O error in startPageSequence()", ioe);
221         }
222         this.location = Location.FOLLOWING_PAGE_SEQUENCE;
223     }
224 
225     /** {@inheritDoc} */
endPageSequence()226     public void endPageSequence() throws IFException {
227         try {
228             //Process deferred page-sequence-level extensions
229             Iterator<AFPPageSetup> iter = this.deferredPageSequenceExtensions.iterator();
230             while (iter.hasNext()) {
231                 AFPPageSetup aps = iter.next();
232                 iter.remove();
233                 if (AFPElementMapping.NO_OPERATION.equals(aps.getElementName())) {
234                     handleNOP(aps);
235                 } else {
236                     throw new UnsupportedOperationException("Don't know how to handle " + aps);
237                 }
238             }
239 
240             //End page sequence
241             dataStream.endPageGroup();
242         } catch (IOException ioe) {
243             throw new IFException("I/O error in endPageSequence()", ioe);
244         }
245         this.location = Location.ELSEWHERE;
246     }
247 
248     /**
249      * Returns the base AFP transform
250      *
251      * @return the base AFP transform
252      */
getBaseTransform()253     private AffineTransform getBaseTransform() {
254         AffineTransform baseTransform = new AffineTransform();
255         double scale = unitConv.mpt2units(1);
256         baseTransform.scale(scale, scale);
257         return baseTransform;
258     }
259 
260     /** {@inheritDoc} */
startPage(int index, String name, String pageMasterName, Dimension size)261     public void startPage(int index, String name, String pageMasterName, Dimension size)
262                 throws IFException {
263         this.location = Location.ELSEWHERE;
264         paintingState.clear();
265 
266         AffineTransform baseTransform = getBaseTransform();
267         paintingState.concatenate(baseTransform);
268 
269         int pageWidth = Math.round(unitConv.mpt2units(size.width));
270         paintingState.setPageWidth(pageWidth);
271 
272         int pageHeight = Math.round(unitConv.mpt2units(size.height));
273         paintingState.setPageHeight(pageHeight);
274 
275         int pageRotation = paintingState.getPageRotation();
276         int resolution = paintingState.getResolution();
277 
278         dataStream.startPage(pageWidth, pageHeight, pageRotation,
279                 resolution, resolution);
280     }
281 
282     /** {@inheritDoc} */
283     @Override
startPageHeader()284     public void startPageHeader() throws IFException {
285         super.startPageHeader();
286         this.location = Location.IN_PAGE_HEADER;
287     }
288 
289     /** {@inheritDoc} */
290     @Override
endPageHeader()291     public void endPageHeader() throws IFException {
292         this.location = Location.ELSEWHERE;
293         super.endPageHeader();
294     }
295 
296     /** {@inheritDoc} */
startPageContent()297     public IFPainter startPageContent() throws IFException {
298         return new AFPPainter(this);
299     }
300 
301     /** {@inheritDoc} */
endPageContent()302     public void endPageContent() throws IFException {
303     }
304 
305     /** {@inheritDoc} */
endPage()306     public void endPage() throws IFException {
307         try {
308             AFPPageFonts pageFonts = paintingState.getPageFonts();
309             if (pageFonts != null && !pageFonts.isEmpty()) {
310                 dataStream.addFontsToCurrentPage(pageFonts);
311             }
312 
313             dataStream.endPage();
314         } catch (IOException ioe) {
315             throw new IFException("I/O error in endPage()", ioe);
316         }
317     }
318 
319     /** {@inheritDoc} */
handleExtensionObject(Object extension)320     public void handleExtensionObject(Object extension) throws IFException {
321         if (extension instanceof AFPPageSetup) {
322             AFPPageSetup aps = (AFPPageSetup)extension;
323             String element = aps.getElementName();
324             if (AFPElementMapping.TAG_LOGICAL_ELEMENT.equals(element)) {
325                 switch (this.location) {
326                 case FOLLOWING_PAGE_SEQUENCE:
327                 case IN_PAGE_HEADER:
328                     String name = aps.getName();
329                     String value = aps.getValue();
330                     int encoding = aps.getEncoding();
331                     dataStream.createTagLogicalElement(name, value, encoding);
332                     break;
333                 default:
334                     throw new IFException(
335                         "TLE extension must be in the page header or between page-sequence"
336                             + " and the first page: " + aps, null);
337                 }
338             } else if (AFPElementMapping.NO_OPERATION.equals(element)) {
339                 switch (this.location) {
340                 case FOLLOWING_PAGE_SEQUENCE:
341                     if (aps.getPlacement() == ExtensionPlacement.BEFORE_END) {
342                         this.deferredPageSequenceExtensions.add(aps);
343                         break;
344                     }
345                 case IN_DOCUMENT_HEADER:
346                 case IN_PAGE_HEADER:
347                     handleNOP(aps);
348                     break;
349                 default:
350                     throw new IFException(
351                             "NOP extension must be in the document header, the page header"
352                                 + " or between page-sequence"
353                                 + " and the first page: " + aps, null);
354                 }
355             } else {
356                 if (this.location != Location.IN_PAGE_HEADER) {
357                     throw new IFException(
358                         "AFP page setup extension encountered outside the page header: " + aps,
359                         null);
360                 }
361                 if (AFPElementMapping.INCLUDE_PAGE_SEGMENT.equals(element)) {
362                     AFPPageSegmentElement.AFPPageSegmentSetup apse
363                         = (AFPPageSegmentElement.AFPPageSegmentSetup)aps;
364                     String name = apse.getName();
365                     String source = apse.getValue();
366                     String uri = apse.getResourceSrc();
367                     pageSegmentMap.put(source, new PageSegmentDescriptor(name, uri));
368                 }
369             }
370         } else if (extension instanceof AFPPageOverlay) {
371             AFPPageOverlay ipo = (AFPPageOverlay)extension;
372             if (this.location != Location.IN_PAGE_HEADER) {
373                     throw new IFException(
374                         "AFP page overlay extension encountered outside the page header: " + ipo,
375                         null);
376             }
377             String overlay = ipo.getName();
378             if (overlay != null) {
379                 dataStream.createIncludePageOverlay(overlay, ipo.getX(), ipo.getY());
380             }
381         } else if (extension instanceof AFPInvokeMediumMap) {
382             if (this.location != Location.FOLLOWING_PAGE_SEQUENCE
383                     && this.location != Location.IN_PAGE_HEADER) {
384 
385                 throw new IFException(
386                     "AFP IMM extension must be between page-sequence"
387                     + " and the first page or child of page-header: "
388                     + extension, null);
389             }
390             AFPInvokeMediumMap imm = (AFPInvokeMediumMap)extension;
391             String mediumMap = imm.getName();
392             if (mediumMap != null) {
393                 dataStream.createInvokeMediumMap(mediumMap);
394             }
395         } else if (extension instanceof AFPIncludeFormMap) {
396             AFPIncludeFormMap formMap = (AFPIncludeFormMap)extension;
397             AFPResourceAccessor accessor = new AFPResourceAccessor(
398                     getUserAgent().getResourceResolver());
399             try {
400                 getResourceManager().createIncludedResource(formMap.getName(),
401                         formMap.getSrc(), accessor,
402                         ResourceObject.TYPE_FORMDEF, false, null);
403             } catch (IOException ioe) {
404                 throw new IFException(
405                         "I/O error while embedding form map resource: " + formMap.getName(), ioe);
406             }
407         }
408     }
409 
410     /**
411      * Corner images can be reused by storing at the document level in the AFP
412      * The cache is used to map cahced images to caller generated descriptions of the corner
413      * @param cornerKey caller's identifier for the corner
414      * @return document id of the corner image
415      */
cacheRoundedCorner(String cornerKey)416     public String cacheRoundedCorner(String cornerKey) {
417 
418         // Make a unique id
419         StringBuffer idBuilder = new StringBuffer("RC");
420 
421         String tmp = Integer.toHexString(roundedCornerCount).toUpperCase(Locale.ENGLISH);
422         if (tmp.length() > 6) {
423             //Will never happen
424             //log.error("Rounded corners cache capacity exceeded");
425             //We should get a visual clue
426             roundedCornerCount = 0;
427             tmp = "000000";
428         } else if (tmp.length() < 6) {
429             for (int i = 0; i < 6 - tmp.length(); i++) {
430                 idBuilder.append("0");
431             }
432             idBuilder.append(tmp);
433         }
434 
435        roundedCornerCount++;
436 
437        String id =  idBuilder.toString();
438 
439        //cache the corner id
440        roundedCornerNameCache.put(cornerKey, id);
441        return id;
442     }
443     /**
444      * This method returns the an id that identifies a cached corner or null if non existent
445      * @param cornerKey caller's identifier for the corner
446      * @return document id of the corner image
447      */
getCachedRoundedCorner(String cornerKey)448     public String getCachedRoundedCorner(String cornerKey) {
449         return roundedCornerNameCache.get(cornerKey);
450     }
451 
452 
handleNOP(AFPPageSetup nop)453     private void handleNOP(AFPPageSetup nop) {
454         String content = nop.getContent();
455         if (content != null) {
456             dataStream.createNoOperation(content);
457         }
458     }
459 
460     // ---=== AFPCustomizable ===---
461 
462     /** {@inheritDoc} */
setBitsPerPixel(int bitsPerPixel)463     public void setBitsPerPixel(int bitsPerPixel) {
464         paintingState.setBitsPerPixel(bitsPerPixel);
465     }
466 
467     /** {@inheritDoc} */
setColorImages(boolean colorImages)468     public void setColorImages(boolean colorImages) {
469         paintingState.setColorImages(colorImages);
470     }
471 
472     /** {@inheritDoc} */
setNativeImagesSupported(boolean nativeImages)473     public void setNativeImagesSupported(boolean nativeImages) {
474         paintingState.setNativeImagesSupported(nativeImages);
475     }
476 
477     /** {@inheritDoc} */
setCMYKImagesSupported(boolean value)478     public void setCMYKImagesSupported(boolean value) {
479         paintingState.setCMYKImagesSupported(value);
480     }
481 
482     /** {@inheritDoc} */
setDitheringQuality(float quality)483     public void setDitheringQuality(float quality) {
484         this.paintingState.setDitheringQuality(quality);
485     }
486 
487     /** {@inheritDoc} */
setBitmapEncodingQuality(float quality)488     public void setBitmapEncodingQuality(float quality) {
489         this.paintingState.setBitmapEncodingQuality(quality);
490     }
491 
492     /** {@inheritDoc} */
setShadingMode(AFPShadingMode shadingMode)493     public void setShadingMode(AFPShadingMode shadingMode) {
494         this.shadingMode = shadingMode;
495     }
496 
497     /** {@inheritDoc} */
setResolution(int resolution)498     public void setResolution(int resolution) {
499         paintingState.setResolution(resolution);
500     }
501 
502     /** {@inheritDoc} */
setLineWidthCorrection(float correction)503     public void setLineWidthCorrection(float correction) {
504         paintingState.setLineWidthCorrection(correction);
505     }
506 
507     /** {@inheritDoc} */
getResolution()508     public int getResolution() {
509         return paintingState.getResolution();
510     }
511 
512     /** {@inheritDoc} */
setGOCAEnabled(boolean enabled)513     public void setGOCAEnabled(boolean enabled) {
514         this.paintingState.setGOCAEnabled(enabled);
515     }
516 
517     /** {@inheritDoc} */
isGOCAEnabled()518     public boolean isGOCAEnabled() {
519         return this.paintingState.isGOCAEnabled();
520     }
521 
522     /** {@inheritDoc} */
setStrokeGOCAText(boolean stroke)523     public void setStrokeGOCAText(boolean stroke) {
524         this.paintingState.setStrokeGOCAText(stroke);
525     }
526 
527     /** {@inheritDoc} */
isStrokeGOCAText()528     public boolean isStrokeGOCAText() {
529         return this.paintingState.isStrokeGOCAText();
530     }
531 
532     /** {@inheritDoc} */
setWrapPSeg(boolean pSeg)533     public void setWrapPSeg(boolean pSeg) {
534         paintingState.setWrapPSeg(pSeg);
535     }
536 
setWrapGocaPSeg(boolean pSeg)537     public void setWrapGocaPSeg(boolean pSeg) {
538         paintingState.setWrapGocaPSeg(pSeg);
539     }
540 
541     /** {@inheritDoc} */
setFS45(boolean fs45)542     public void setFS45(boolean fs45) {
543         paintingState.setFS45(fs45);
544     }
545 
546     /** {@inheritDoc} */
getWrapPSeg()547     public boolean getWrapPSeg() {
548         return  paintingState.getWrapPSeg();
549     }
550 
551     /** {@inheritDoc} */
getFS45()552     public boolean getFS45() {
553         return  paintingState.getFS45();
554     }
555 
setDefaultResourceGroupUri(URI uri)556     public void setDefaultResourceGroupUri(URI uri) {
557         resourceManager.setDefaultResourceGroupUri(uri);
558     }
559 
560     /** {@inheritDoc} */
setResourceLevelDefaults(AFPResourceLevelDefaults defaults)561     public void setResourceLevelDefaults(AFPResourceLevelDefaults defaults) {
562         resourceManager.setResourceLevelDefaults(defaults);
563     }
564 
565     /**
566      * Returns the page segment descriptor for a given URI if it actually represents a page segment.
567      * Otherwise, it just returns null.
568      * @param uri the URI that identifies the page segment
569      * @return the page segment descriptor or null if there's no page segment for the given URI
570      */
getPageSegmentNameFor(String uri)571     PageSegmentDescriptor getPageSegmentNameFor(String uri) {
572         return pageSegmentMap.get(uri);
573     }
574 
575     /** {@inheritDoc} */
canEmbedJpeg(boolean canEmbed)576     public void canEmbedJpeg(boolean canEmbed) {
577         paintingState.setCanEmbedJpeg(canEmbed);
578     }
579 
580 }
581