/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id: AFPResourceManager.java 1825861 2018-03-05 09:39:38Z ssteiner $ */

package org.apache.fop.afp;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.afp.AFPResourceLevel.ResourceType;
import org.apache.fop.afp.fonts.AFPFont;
import org.apache.fop.afp.fonts.CharacterSet;
import org.apache.fop.afp.modca.AbstractNamedAFPObject;
import org.apache.fop.afp.modca.AbstractPageObject;
import org.apache.fop.afp.modca.ActiveEnvironmentGroup;
import org.apache.fop.afp.modca.IncludeObject;
import org.apache.fop.afp.modca.IncludedResourceObject;
import org.apache.fop.afp.modca.ObjectContainer;
import org.apache.fop.afp.modca.PageSegment;
import org.apache.fop.afp.modca.Registry;
import org.apache.fop.afp.modca.ResourceGroup;
import org.apache.fop.afp.modca.ResourceObject;
import org.apache.fop.afp.modca.triplets.EncodingTriplet;
import org.apache.fop.afp.modca.triplets.FullyQualifiedNameTriplet;
import org.apache.fop.afp.util.AFPResourceAccessor;
import org.apache.fop.afp.util.AFPResourceUtil;
import org.apache.fop.apps.io.InternalResourceResolver;
import org.apache.fop.fonts.FontType;
import org.apache.fop.render.afp.AFPFontConfig;

Manages the creation and storage of document resources
/** * Manages the creation and storage of document resources */
public class AFPResourceManager {
logging instance
/** logging instance */
private static Log log = LogFactory.getLog(AFPResourceManager.class);
The AFP datastream (document tree)
/** The AFP datastream (document tree) */
private DataStream dataStream;
Resource creation factory
/** Resource creation factory */
private final Factory factory; private final AFPStreamer streamer; private final AFPDataObjectFactory dataObjectFactory;
Maintain a reference count of instream objects for referencing purposes
/** Maintain a reference count of instream objects for referencing purposes */
private int instreamObjectCount;
Mapping of resourceInfo to AbstractCachedObject
/** Mapping of resourceInfo to AbstractCachedObject */
private final Map<AFPResourceInfo, List<AbstractCachedObject>> includeObjectCache = new HashMap<AFPResourceInfo, List<AbstractCachedObject>>(); private AFPResourceLevelDefaults resourceLevelDefaults = new AFPResourceLevelDefaults(); protected boolean includeCached = true;
Main constructor
Params:
/** * Main constructor * * @param resourceResolver the associated {@link InternalResourceResolver} instance */
public AFPResourceManager(InternalResourceResolver resourceResolver) { this.factory = new Factory(); this.streamer = new AFPStreamer(factory, resourceResolver); this.dataObjectFactory = new AFPDataObjectFactory(factory); }
Sets the outputstream
Params:
  • paintingState – the AFP painting state
  • outputStream – the outputstream
Throws:
  • IOException – thrown if an I/O exception of some sort has occurred
Returns:a new AFP DataStream
/** * Sets the outputstream * * @param paintingState the AFP painting state * @param outputStream the outputstream * @return a new AFP DataStream * @throws IOException thrown if an I/O exception of some sort has occurred */
public DataStream createDataStream(AFPPaintingState paintingState, OutputStream outputStream) throws IOException { this.dataStream = streamer.createDataStream(paintingState); streamer.setOutputStream(outputStream); return this.dataStream; }
Returns the AFP DataStream
Returns:the AFP DataStream
/** * Returns the AFP DataStream * * @return the AFP DataStream */
public DataStream getDataStream() { return this.dataStream; }
Tells the streamer to write
Throws:
  • IOException – thrown if an I/O exception of some sort has occurred.
/** * Tells the streamer to write * * @throws IOException thrown if an I/O exception of some sort has occurred. */
public void writeToStream() throws IOException { streamer.close(); }
Sets the default resource group URI.
Params:
  • uri – the default resource group URI
/** * Sets the default resource group URI. * * @param uri the default resource group URI */
public void setDefaultResourceGroupUri(URI uri) { streamer.setDefaultResourceGroupUri(uri); }
Tries to create an include of a data object that has been previously added to the AFP data stream. If no such object was available, the method returns false which serves as a signal that the object has to be created.
Params:
  • dataObjectInfo – the data object info
Throws:
  • IOException – thrown if an I/O exception of some sort has occurred.
Returns:true if the inclusion succeeded, false if the object was not available
/** * Tries to create an include of a data object that has been previously added to the * AFP data stream. If no such object was available, the method returns false which serves * as a signal that the object has to be created. * @param dataObjectInfo the data object info * @return true if the inclusion succeeded, false if the object was not available * @throws IOException thrown if an I/O exception of some sort has occurred. */
public boolean tryIncludeObject(AFPDataObjectInfo dataObjectInfo) throws IOException { AFPResourceInfo resourceInfo = dataObjectInfo.getResourceInfo(); updateResourceInfoUri(resourceInfo); return includeCachedObject(resourceInfo, dataObjectInfo.getObjectAreaInfo()); }
Creates a new data object in the AFP datastream
Params:
  • dataObjectInfo – the data object info
Throws:
  • IOException – thrown if an I/O exception of some sort has occurred.
/** * Creates a new data object in the AFP datastream * * @param dataObjectInfo the data object info * * @throws IOException thrown if an I/O exception of some sort has occurred. */
public void createObject(AFPDataObjectInfo dataObjectInfo) throws IOException { if (tryIncludeObject(dataObjectInfo)) { //Object has already been produced and is available by inclusion, so return early. return; } AbstractNamedAFPObject namedObj = null; AFPResourceInfo resourceInfo = dataObjectInfo.getResourceInfo(); boolean useInclude = true; Registry.ObjectType objectType = null; // new resource so create if (dataObjectInfo instanceof AFPImageObjectInfo) { AFPImageObjectInfo imageObjectInfo = (AFPImageObjectInfo)dataObjectInfo; namedObj = dataObjectFactory.createImage(imageObjectInfo); } else if (dataObjectInfo instanceof AFPGraphicsObjectInfo) { AFPGraphicsObjectInfo graphicsObjectInfo = (AFPGraphicsObjectInfo)dataObjectInfo; namedObj = dataObjectFactory.createGraphic(graphicsObjectInfo); } else { // natively embedded data object namedObj = dataObjectFactory.createObjectContainer(dataObjectInfo); objectType = dataObjectInfo.getObjectType(); useInclude = objectType != null && objectType.isIncludable(); } AFPResourceLevel resourceLevel = resourceInfo.getLevel(); ResourceGroup resourceGroup = streamer.getResourceGroup(resourceLevel); useInclude &= resourceGroup != null; if (useInclude) { final boolean usePageSegment = dataObjectInfo.isCreatePageSegment(); // if it is to reside within a resource group at print-file or external level if (resourceLevel.isPrintFile() || resourceLevel.isExternal()) { if (usePageSegment) { String pageSegmentName = "S10" + namedObj.getName().substring(3); namedObj.setName(pageSegmentName); PageSegment seg = new PageSegment(pageSegmentName); seg.addObject(namedObj); namedObj = seg; } // wrap newly created data object in a resource object namedObj = dataObjectFactory.createResource(namedObj, resourceInfo, objectType); } // add data object into its resource group destination resourceGroup.addObject(namedObj); includeObject(namedObj, dataObjectInfo); } else { // not to be included so inline data object directly into the current page dataStream.getCurrentPage().addObject(namedObj); } } private abstract class AbstractCachedObject { protected String objectName; protected AFPDataObjectInfo dataObjectInfo; public AbstractCachedObject(String objectName, AFPDataObjectInfo dataObjectInfo) { this.objectName = objectName; this.dataObjectInfo = dataObjectInfo; } protected abstract void includeObject(); } private class CachedPageSegment extends AbstractCachedObject { public CachedPageSegment(String objectName, AFPDataObjectInfo dataObjectInfo) { super(objectName, dataObjectInfo); } protected void includeObject() { includePageSegment(dataObjectInfo, objectName); } } private class CachedObject extends AbstractCachedObject { public CachedObject(String objectName, AFPDataObjectInfo dataObjectInfo) { super(objectName, dataObjectInfo); } protected void includeObject() { AFPResourceManager.this.includeObject(dataObjectInfo, objectName); } } private void includeObject(AbstractNamedAFPObject namedObj, AFPDataObjectInfo dataObjectInfo) { // create the include object String objectName = namedObj.getName(); AbstractCachedObject cachedObject; if (dataObjectInfo.isCreatePageSegment()) { cachedObject = new CachedPageSegment(objectName, dataObjectInfo); } else { cachedObject = new CachedObject(objectName, dataObjectInfo); } cachedObject.includeObject(); addToCache(dataObjectInfo.getResourceInfo(), cachedObject); //The data field of dataObjectInfo is not further required // therefore we are safe to null the reference, saving memory dataObjectInfo.setData(null); } private void addToCache(AFPResourceInfo resourceInfo, AbstractCachedObject cachedObject) { List<AbstractCachedObject> objs = includeObjectCache.get(resourceInfo); if (objs == null) { objs = new ArrayList<AbstractCachedObject>(); includeObjectCache.put(resourceInfo, objs); } objs.add(cachedObject); }
Returns true if the passed AFPResourceInfo instance is already cached.
Params:
  • resourceInfo – the resource info to check
Returns: true if the object is cached
/** * Returns {@code true} if the passed {@link AFPResourceInfo} instance is already cached. * * @param resourceInfo the resource info to check * @return {@code true} if the object is cached */
public boolean isObjectCached(AFPResourceInfo resourceInfo) { return includeObjectCache.containsKey(resourceInfo); }
Params:
  • resourceInfo – the resource info to check
  • areaInfo – the area info to check
Returns: true if ...
/** * * @param resourceInfo the resource info to check * @param areaInfo the area info to check * @return {@code true} if ... */
public boolean includeCachedObject(AFPResourceInfo resourceInfo, AFPObjectAreaInfo areaInfo) { List<AbstractCachedObject> cachedObjectList = includeObjectCache.get(resourceInfo); if (cachedObjectList != null && includeCached) { for (AbstractCachedObject cachedObject : cachedObjectList) { if (areaInfo != null && cachedObjectList.size() == 1) { cachedObject.dataObjectInfo.setObjectAreaInfo(areaInfo); } cachedObject.includeObject(); } return true; } else { return false; } } private void updateResourceInfoUri(AFPResourceInfo resourceInfo) { String uri = resourceInfo.getUri(); if (uri == null) { uri = "/"; } // if this is an instream data object adjust the uri to ensure that its unique if (uri.endsWith("/")) { uri += "#" + (++instreamObjectCount); resourceInfo.setUri(uri); } } private void includeObject(AFPDataObjectInfo dataObjectInfo, String objectName) { IncludeObject includeObject = dataObjectFactory.createInclude(objectName, dataObjectInfo); dataStream.getCurrentPage().addObject(includeObject); }
Handles font embedding. If a font is embeddable and has not already been embedded it will be.
Params:
  • afpFont – the AFP font to be checked for embedding
  • charSet – the associated character set
Throws:
  • IOException – if there's a problem while embedding the external resources
/** * Handles font embedding. If a font is embeddable and has not already been embedded it will be. * @param afpFont the AFP font to be checked for embedding * @param charSet the associated character set * @throws IOException if there's a problem while embedding the external resources */
public void embedFont(AFPFont afpFont, CharacterSet charSet) throws IOException { if (afpFont.isEmbeddable()) { //Embed fonts (char sets and code pages) if (charSet.getResourceAccessor() != null) { AFPResourceAccessor accessor = charSet.getResourceAccessor(); if (afpFont.getFontType() == FontType.TRUETYPE) { createIncludedResource(afpFont.getFontName(), ((AFPFontConfig.AFPTrueTypeFont) afpFont).getUri(), accessor, ResourceObject.TYPE_OBJECT_CONTAINER, true, ((AFPFontConfig.AFPTrueTypeFont) afpFont).getTTC()); } else { createIncludedResource( charSet.getName(), accessor, ResourceObject.TYPE_FONT_CHARACTER_SET); createIncludedResource( charSet.getCodePage(), accessor, ResourceObject.TYPE_CODE_PAGE); } } } } private void includePageSegment(AFPDataObjectInfo dataObjectInfo, String pageSegmentName) { int x = dataObjectInfo.getObjectAreaInfo().getX(); int y = dataObjectInfo.getObjectAreaInfo().getY(); AbstractPageObject currentPage = dataStream.getCurrentPage(); boolean createHardPageSegments = true; currentPage.createIncludePageSegment(pageSegmentName, x, y, createHardPageSegments); }
Creates an included resource object by loading the contained object from a file.
Params:
  • resourceName – the name of the resource
  • accessor – resource accessor to access the resource with
  • resourceObjectType – the resource object type (ResourceObject.*)
Throws:
  • IOException – if an I/O error occurs while loading the resource
/** * Creates an included resource object by loading the contained object from a file. * @param resourceName the name of the resource * @param accessor resource accessor to access the resource with * @param resourceObjectType the resource object type ({@link ResourceObject}.*) * @throws IOException if an I/O error occurs while loading the resource */
public void createIncludedResource(String resourceName, AFPResourceAccessor accessor, byte resourceObjectType) throws IOException { URI uri; try { uri = new URI(resourceName.trim()); } catch (URISyntaxException e) { throw new IOException("Could not create URI from resource name: " + resourceName + " (" + e.getMessage() + ")"); } createIncludedResource(resourceName, uri, accessor, resourceObjectType, false, null); }
Creates an included resource object by loading the contained object from a file.
Params:
  • resourceName – the name of the resource
  • uri – the URI for the resource
  • accessor – resource accessor to access the resource with
  • resourceObjectType – the resource object type (ResourceObject.*)
Throws:
  • IOException – if an I/O error occurs while loading the resource
/** * Creates an included resource object by loading the contained object from a file. * @param resourceName the name of the resource * @param uri the URI for the resource * @param accessor resource accessor to access the resource with * @param resourceObjectType the resource object type ({@link ResourceObject}.*) * @throws IOException if an I/O error occurs while loading the resource */
public void createIncludedResource(String resourceName, URI uri, AFPResourceAccessor accessor, byte resourceObjectType, boolean truetype, String ttc) throws IOException { AFPResourceLevel resourceLevel = new AFPResourceLevel(ResourceType.PRINT_FILE); AFPResourceInfo resourceInfo = new AFPResourceInfo(); resourceInfo.setLevel(resourceLevel); resourceInfo.setName(resourceName); resourceInfo.setUri(uri.toASCIIString()); List<AbstractCachedObject> cachedObject = includeObjectCache.get(resourceInfo); if (cachedObject == null) { if (log.isDebugEnabled()) { log.debug("Adding included resource: " + resourceName); } ResourceGroup resourceGroup = streamer.getResourceGroup(resourceLevel); if (truetype) { ResourceObject res = factory.createResource(); res.setType(ResourceObject.TYPE_OBJECT_CONTAINER); ActiveEnvironmentGroup.setupTruetypeMDR(res, false); ObjectContainer oc = factory.createObjectContainer(); InputStream is = accessor.createInputStream(uri); if (ttc != null) { oc.setData(extractTTC(ttc, is)); } else { oc.setData(IOUtils.toByteArray(is)); } ActiveEnvironmentGroup.setupTruetypeMDR(oc, true); res.addTriplet(new EncodingTriplet(1200)); res.setFullyQualifiedName(FullyQualifiedNameTriplet.TYPE_REPLACE_FIRST_GID_NAME, FullyQualifiedNameTriplet.FORMAT_CHARSTR, resourceName, true); res.setDataObject(oc); resourceGroup.addObject(res); } else { ResourceObject resourceObject = factory.createResource(resourceName); IncludedResourceObject resourceContent = new IncludedResourceObject( resourceName, accessor, uri); resourceObject.setDataObject(resourceContent); resourceObject.setType(resourceObjectType); resourceGroup.addObject(resourceObject); } //TODO what is the data object? CachedObject newcachedObject = new CachedObject(resourceName, null); // record mapping of resource info to data object resource name addToCache(resourceInfo, newcachedObject); } else { //skip, already created } } private byte[] extractTTC(String ttc, InputStream is) throws IOException { // TrueTypeCollection trueTypeCollection = new TrueTypeCollection(is); // for (TrueTypeFont ttf : trueTypeCollection.getFonts()) { // String name = ttf.getNaming().getFontFamily(); // if (name.equals(ttc)) { // ByteArrayOutputStream bos = new ByteArrayOutputStream(); // TTFSubsetter s = new TTFSubsetter(ttf, null); // for (int i = 0; i < 256 * 256; i++) { // s.addCharCode(i); // } // s.writeToStream(bos); // return bos.toByteArray(); // } // } throw new IOException(ttc + " not supported"); }
Creates an included resource extracting the named resource from an external source.
Params:
  • resourceName – the name of the resource
  • uri – the URI for the resource
  • accessor – resource accessor to access the resource with
Throws:
  • IOException – if an I/O error occurs while loading the resource
/** * Creates an included resource extracting the named resource from an external source. * @param resourceName the name of the resource * @param uri the URI for the resource * @param accessor resource accessor to access the resource with * @throws IOException if an I/O error occurs while loading the resource */
public void createIncludedResourceFromExternal(final String resourceName, final URI uri, final AFPResourceAccessor accessor) throws IOException { AFPResourceLevel resourceLevel = new AFPResourceLevel(ResourceType.PRINT_FILE); AFPResourceInfo resourceInfo = new AFPResourceInfo(); resourceInfo.setLevel(resourceLevel); resourceInfo.setName(resourceName); resourceInfo.setUri(uri.toASCIIString()); List<AbstractCachedObject> resource = includeObjectCache.get(resourceInfo); if (resource == null) { ResourceGroup resourceGroup = streamer.getResourceGroup(resourceLevel); //resourceObject delegates write commands to copyNamedResource() //The included resource may already be wrapped in a resource object AbstractNamedAFPObject resourceObject = new AbstractNamedAFPObject(null) { @Override protected void writeContent(OutputStream os) throws IOException { InputStream inputStream = null; try { inputStream = accessor.createInputStream(uri); BufferedInputStream bin = new BufferedInputStream(inputStream); AFPResourceUtil.copyNamedResource(resourceName, bin, os); } finally { IOUtils.closeQuietly(inputStream); } } //bypass super.writeStart @Override protected void writeStart(OutputStream os) throws IOException { } //bypass super.writeEnd @Override protected void writeEnd(OutputStream os) throws IOException { } }; resourceGroup.addObject(resourceObject); CachedObject newresource = new CachedObject(resourceName, null); addToCache(resourceInfo, newresource); } }
Sets resource level defaults. The existing defaults over merged with the ones passed in as parameter.
Params:
  • defaults – the new defaults
/** * Sets resource level defaults. The existing defaults over merged with the ones passed in * as parameter. * @param defaults the new defaults */
public void setResourceLevelDefaults(AFPResourceLevelDefaults defaults) { this.resourceLevelDefaults.mergeFrom(defaults); }
Returns the resource level defaults in use with this resource manager.
Returns:the resource level defaults
/** * Returns the resource level defaults in use with this resource manager. * @return the resource level defaults */
public AFPResourceLevelDefaults getResourceLevelDefaults() { return this.resourceLevelDefaults; } }