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