blob: 7331178f521acc745e779f2a220e540cb2243559 [file] [log] [blame]
/*
* 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$ */
package org.apache.fop.afp.fonts;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlgraphics.image.loader.util.SoftMapCache;
import org.apache.fop.afp.AFPConstants;
import org.apache.fop.afp.AFPEventProducer;
import org.apache.fop.afp.util.AFPResourceAccessor;
import org.apache.fop.afp.util.StructuredFieldReader;
import org.apache.fop.apps.io.InternalResourceResolver;
import org.apache.fop.fonts.Typeface;
/**
* The CharacterSetBuilder is responsible building the a CharacterSet instance that holds
* the font metric data. The data is either read from disk and passed to a CharacterSet (*)
* or a FopCharacterSet is instantiated that is composed of a Typeface instance configured
* with this data.<p/>
* -*- For referenced fonts CharacterSetBuilder is responsible for reading the font attributes
* from binary code page files and the character set metric files. In IBM font structure, a
* code page maps each character of text to the characters in a character set.
* Each character is translated into a code point. When the character is
* printed, each code point is matched to a character ID on the code page
* specified. The character ID is then matched to the image (raster pattern or
* outline pattern) of the character in the character set specified. The image
* in the character set is the image that is printed in the document. To be a
* valid code page for a particular character set, all character IDs in the code
* page must be included in that character set. <p/>This class will read the
* font information from the binary code page files and character set metric
* files in order to determine the correct metrics to use when rendering the
* formatted object. <p/>
*
*/
public abstract class CharacterSetBuilder {
/**
* Static logging instance
*/
protected static final Log LOG = LogFactory.getLog(CharacterSetBuilder.class);
/**
* Template used to convert lists to arrays.
*/
private static final CharacterSetOrientation[] EMPTY_CSO_ARRAY = new CharacterSetOrientation[0];
/** Codepage MO:DCA structured field. */
private static final byte[] CODEPAGE_SF = new byte[] {
(byte) 0xD3, (byte) 0xA8, (byte) 0x87};
/** Character table MO:DCA structured field. */
private static final byte[] CHARACTER_TABLE_SF = new byte[] {
(byte) 0xD3, (byte) 0x8C, (byte) 0x87};
/** Font descriptor MO:DCA structured field. */
private static final byte[] FONT_DESCRIPTOR_SF = new byte[] {
(byte) 0xD3, (byte) 0xA6, (byte) 0x89 };
/** Font control MO:DCA structured field. */
private static final byte[] FONT_CONTROL_SF = new byte[] {
(byte) 0xD3, (byte) 0xA7, (byte) 0x89 };
/** Font orientation MO:DCA structured field. */
private static final byte[] FONT_ORIENTATION_SF = new byte[] {
(byte) 0xD3, (byte) 0xAE, (byte) 0x89 };
/** Font position MO:DCA structured field. */
private static final byte[] FONT_POSITION_SF = new byte[] {
(byte) 0xD3, (byte) 0xAC, (byte) 0x89 };
/** Font index MO:DCA structured field. */
private static final byte[] FONT_INDEX_SF = new byte[] {
(byte) 0xD3, (byte) 0x8C, (byte) 0x89 };
/**
* The collection of code pages
*/
private final Map<String, Map<String, String>> codePagesCache
= Collections.synchronizedMap(new WeakHashMap<String, Map<String, String>>());
/**
* Cache of charactersets
*/
private final SoftMapCache characterSetsCache = new SoftMapCache(true);
/** Default constructor. */
private CharacterSetBuilder() {
}
/**
* Factory method for the single-byte implementation of AFPFontReader.
* @return AFPFontReader
*/
public static CharacterSetBuilder getSingleByteInstance() {
return SingleByteLoader.getInstance();
}
/**
* Factory method for the double-byte (CID Keyed font (Type 0)) implementation of AFPFontReader.
* @return AFPFontReader
*/
public static CharacterSetBuilder getDoubleByteInstance() {
return DoubleByteLoader.getInstance();
}
/**
* Returns an InputStream to a given file path and filename
*
* * @param accessor the resource accessor
* @param uriStr the URI
* @param eventProducer for handling AFP related events
* @return an inputStream
* @throws IOException in the event that an I/O exception of some sort has occurred
*/
private InputStream openInputStream(AFPResourceAccessor accessor, String uriStr,
AFPEventProducer eventProducer)
throws IOException {
URI uri;
try {
uri = InternalResourceResolver.cleanURI(uriStr.trim());
} catch (URISyntaxException e) {
throw new MalformedURLException("Invalid uri: " + uriStr + " (" + e.getMessage() + ")");
}
if (LOG.isDebugEnabled()) {
LOG.debug("Opening " + uri);
}
return accessor.createInputStream(uri);
}
/**
* Closes the inputstream
*
* @param inputStream the inputstream to close
*/
private void closeInputStream(InputStream inputStream) {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception ex) {
// Lets log at least!
LOG.error(ex.getMessage());
}
}
/**
* Load the font details and metrics into the CharacterSetMetric object, this will use the
* actual afp code page and character set files to load the object with the necessary metrics.
*
* @param characterSetName name of the characterset
* @param codePageName name of the code page file
* @param encoding encoding name
* @param accessor used to load codepage and characterset
* @param eventProducer for handling AFP related events
* @return CharacterSet object
* @throws IOException if an I/O error occurs
*/
public CharacterSet buildSBCS(String characterSetName, String codePageName, String encoding,
AFPResourceAccessor accessor, AFPEventProducer eventProducer) throws IOException {
return processFont(characterSetName, codePageName, encoding, CharacterSetType.SINGLE_BYTE,
accessor, eventProducer);
}
/**
* Load the font details and metrics into the CharacterSetMetric object, this will use the
* actual afp code page and character set files to load the object with the necessary metrics.
* This method is to be used for double byte character sets (DBCS).
*
* @param characterSetName name of the characterset
* @param codePageName name of the code page file
* @param encoding encoding name
* @param charsetType the characterset type
* @param accessor used to load codepage and characterset
* @param eventProducer for handling AFP related events
* @return CharacterSet object
* @throws IOException if an I/O error occurs
*/
public CharacterSet buildDBCS(String characterSetName, String codePageName, String encoding,
CharacterSetType charsetType, AFPResourceAccessor accessor, AFPEventProducer eventProducer)
throws IOException {
return processFont(characterSetName, codePageName, encoding, charsetType, accessor,
eventProducer);
}
/**
* Load the font details and metrics into the CharacterSetMetric object, this will use the
* actual afp code page and character set files to load the object with the necessary metrics.
*
* @param characterSetName the CharacterSetMetric object to populate
* @param codePageName the name of the code page to use
* @param encoding name of the encoding in use
* @param typeface base14 font name
* @param eventProducer for handling AFP related events
* @return CharacterSet object
* @throws IOException if an I/O error occurs
*/
public CharacterSet build(String characterSetName, String codePageName, String encoding,
Typeface typeface, AFPEventProducer eventProducer) throws IOException {
return new FopCharacterSet(codePageName, encoding, characterSetName, typeface,
eventProducer);
}
private CharacterSet processFont(String characterSetName, String codePageName, String encoding,
CharacterSetType charsetType, AFPResourceAccessor accessor, AFPEventProducer eventProducer)
throws IOException {
// check for cached version of the characterset
String descriptor = characterSetName + "_" + encoding + "_" + codePageName;
CharacterSet characterSet = (CharacterSet) characterSetsCache.get(descriptor);
if (characterSet != null) {
return characterSet;
}
// characterset not in the cache, so recreating
characterSet = new CharacterSet(codePageName, encoding, charsetType, characterSetName,
accessor, eventProducer);
InputStream inputStream = null;
try {
/**
* Get the code page which contains the character mapping
* information to map the unicode character id to the graphic
* chracter global identifier.
*/
Map<String, String> codePage;
synchronized (codePagesCache) {
codePage = codePagesCache.get(codePageName);
if (codePage == null) {
codePage = loadCodePage(codePageName, encoding, accessor, eventProducer);
codePagesCache.put(codePageName, codePage);
}
}
inputStream = openInputStream(accessor, characterSetName, eventProducer);
StructuredFieldReader structuredFieldReader = new StructuredFieldReader(inputStream);
// Process D3A689 Font Descriptor
FontDescriptor fontDescriptor = processFontDescriptor(structuredFieldReader);
characterSet.setNominalVerticalSize(fontDescriptor.getNominalFontSizeInMillipoints());
// Process D3A789 Font Control
FontControl fontControl = processFontControl(structuredFieldReader);
if (fontControl != null) {
//process D3AE89 Font Orientation
CharacterSetOrientation[] characterSetOrientations
= processFontOrientation(structuredFieldReader);
double metricNormalizationFactor;
if (fontControl.isRelative()) {
metricNormalizationFactor = 1;
} else {
int dpi = fontControl.getDpi();
metricNormalizationFactor = 1000.0d * 72000.0d
/ fontDescriptor.getNominalFontSizeInMillipoints() / dpi;
}
//process D3AC89 Font Position
processFontPosition(structuredFieldReader, characterSetOrientations,
metricNormalizationFactor);
//process D38C89 Font Index (per orientation)
for (int i = 0; i < characterSetOrientations.length; i++) {
processFontIndex(structuredFieldReader,
characterSetOrientations[i], codePage, metricNormalizationFactor);
characterSet.addCharacterSetOrientation(characterSetOrientations[i]);
}
} else {
throw new IOException("Missing D3AE89 Font Control structured field.");
}
} finally {
closeInputStream(inputStream);
}
characterSetsCache.put(descriptor, characterSet);
return characterSet;
}
/**
* Load the code page information from the appropriate file. The file name
* to load is determined by the code page name and the file extension 'CDP'.
*
* @param codePage
* the code page identifier
* @param encoding
* the encoding to use for the character decoding
* @param accessor the resource accessor
* @param eventProducer for handling AFP related events
* @return a code page mapping (key: GCGID, value: Unicode character)
* @throws IOException if an I/O exception of some sort has occurred.
*/
protected Map<String, String> loadCodePage(String codePage, String encoding,
AFPResourceAccessor accessor, AFPEventProducer eventProducer) throws IOException {
// Create the HashMap to store code page information
Map<String, String> codePages = new HashMap<String, String>();
InputStream inputStream = null;
try {
inputStream = openInputStream(accessor, codePage.trim(), eventProducer);
} catch (IOException e) {
eventProducer.codePageNotFound(this, e);
throw e;
}
try {
StructuredFieldReader structuredFieldReader = new StructuredFieldReader(inputStream);
byte[] data = structuredFieldReader.getNext(CHARACTER_TABLE_SF);
int position = 0;
byte[] gcgiBytes = new byte[8];
byte[] charBytes = new byte[1];
// Read data, ignoring bytes 0 - 2
for (int index = 3; index < data.length; index++) {
if (position < 8) {
// Build the graphic character global identifier key
gcgiBytes[position] = data[index];
position++;
} else if (position == 9) {
position = 0;
// Set the character
charBytes[0] = data[index];
String gcgiString = new String(gcgiBytes,
AFPConstants.EBCIDIC_ENCODING);
//Use the 8-bit char index to find the Unicode character using the Java encoding
//given in the configuration. If the code page and the Java encoding don't
//match, a wrong Unicode character will be associated with the AFP GCGID.
//Idea: we could use IBM's GCGID to Unicode map and build code pages ourselves.
String charString = new String(charBytes, encoding);
codePages.put(gcgiString, charString);
} else {
position++;
}
}
} finally {
closeInputStream(inputStream);
}
return codePages;
}
/**
* Process the font descriptor details using the structured field reader.
*
* @param structuredFieldReader the structured field reader
* @return a class representing the font descriptor
* @throws IOException if an I/O exception of some sort has occurred.
*/
private static FontDescriptor processFontDescriptor(
StructuredFieldReader structuredFieldReader) throws IOException {
byte[] fndData = structuredFieldReader.getNext(FONT_DESCRIPTOR_SF);
return new FontDescriptor(fndData);
}
/**
* Process the font control details using the structured field reader.
*
* @param structuredFieldReader
* the structured field reader
* @return the FontControl
* @throws IOException if an I/O exception of some sort has occurred.
*/
private FontControl processFontControl(StructuredFieldReader structuredFieldReader)
throws IOException {
byte[] fncData = structuredFieldReader.getNext(FONT_CONTROL_SF);
FontControl fontControl = null;
if (fncData != null) {
fontControl = new FontControl();
if (fncData[7] == (byte) 0x02) {
fontControl.setRelative(true);
}
int metricResolution = getUBIN(fncData, 9);
if (metricResolution == 1000) {
//Special case: 1000 units per em (rather than dpi)
fontControl.setUnitsPerEm(1000);
} else {
fontControl.setDpi(metricResolution / 10);
}
}
return fontControl;
}
/**
* Process the font orientation details from using the structured field
* reader.
*
* @param structuredFieldReader
* the structured field reader
* @return CharacterSetOrientation array
* @throws IOException if an I/O exception of some sort has occurred.
*/
private CharacterSetOrientation[] processFontOrientation(
StructuredFieldReader structuredFieldReader) throws IOException {
byte[] data = structuredFieldReader.getNext(FONT_ORIENTATION_SF);
int position = 0;
byte[] fnoData = new byte[26];
List<CharacterSetOrientation> orientations = new ArrayList<CharacterSetOrientation>();
// Read data, ignoring bytes 0 - 2
for (int index = 3; index < data.length; index++) {
// Build the font orientation record
fnoData[position] = data[index];
position++;
if (position == 26) {
position = 0;
int orientation = determineOrientation(fnoData[2]);
// Space Increment
int space = ((fnoData[8] & 0xFF ) << 8) + (fnoData[9] & 0xFF);
// Em-Space Increment
int em = ((fnoData[14] & 0xFF ) << 8) + (fnoData[15] & 0xFF);
CharacterSetOrientation cso = new CharacterSetOrientation(orientation);
cso.setSpaceIncrement(space);
cso.setEmSpaceIncrement(em);
orientations.add(cso);
}
}
return orientations.toArray(EMPTY_CSO_ARRAY);
}
/**
* Populate the CharacterSetOrientation object in the suplied array with the
* font position details using the supplied structured field reader.
*
* @param structuredFieldReader
* the structured field reader
* @param characterSetOrientations
* the array of CharacterSetOrientation objects
* @param metricNormalizationFactor factor to apply to the metrics to get normalized
* font metric values
* @throws IOException if an I/O exception of some sort has occurred.
*/
private void processFontPosition(StructuredFieldReader structuredFieldReader,
CharacterSetOrientation[] characterSetOrientations, double metricNormalizationFactor)
throws IOException {
byte[] data = structuredFieldReader.getNext(FONT_POSITION_SF);
int position = 0;
byte[] fpData = new byte[26];
int characterSetOrientationIndex = 0;
// Read data, ignoring bytes 0 - 2
for (int index = 3; index < data.length; index++) {
if (position < 22) {
// Build the font orientation record
fpData[position] = data[index];
if (position == 9) {
CharacterSetOrientation characterSetOrientation
= characterSetOrientations[characterSetOrientationIndex];
int xHeight = getSBIN(fpData, 2);
int capHeight = getSBIN(fpData, 4);
int ascHeight = getSBIN(fpData, 6);
int dscHeight = getSBIN(fpData, 8);
dscHeight = dscHeight * -1;
characterSetOrientation.setXHeight(
(int)Math.round(xHeight * metricNormalizationFactor));
characterSetOrientation.setCapHeight(
(int)Math.round(capHeight * metricNormalizationFactor));
characterSetOrientation.setAscender(
(int)Math.round(ascHeight * metricNormalizationFactor));
characterSetOrientation.setDescender(
(int)Math.round(dscHeight * metricNormalizationFactor));
}
} else if (position == 22) {
position = 0;
characterSetOrientationIndex++;
fpData[position] = data[index];
}
position++;
}
}
/**
* Process the font index details for the character set orientation.
*
* @param structuredFieldReader the structured field reader
* @param cso the CharacterSetOrientation object to populate
* @param codepage the map of code pages
* @param metricNormalizationFactor factor to apply to the metrics to get normalized
* font metric values
* @throws IOException if an I/O exception of some sort has occurred.
*/
private void processFontIndex(StructuredFieldReader structuredFieldReader,
CharacterSetOrientation cso, Map<String, String> codepage,
double metricNormalizationFactor)
throws IOException {
byte[] data = structuredFieldReader.getNext(FONT_INDEX_SF);
int position = 0;
byte[] gcgid = new byte[8];
byte[] fiData = new byte[20];
char lowest = 255;
char highest = 0;
String firstABCMismatch = null;
// Read data, ignoring bytes 0 - 2
for (int index = 3; index < data.length; index++) {
if (position < 8) {
gcgid[position] = data[index];
position++;
} else if (position < 27) {
fiData[position - 8] = data[index];
position++;
} else if (position == 27) {
fiData[position - 8] = data[index];
position = 0;
String gcgiString = new String(gcgid, AFPConstants.EBCIDIC_ENCODING);
String idx = codepage.get(gcgiString);
if (idx != null) {
char cidx = idx.charAt(0);
int width = getUBIN(fiData, 0);
int a = getSBIN(fiData, 10);
int b = getUBIN(fiData, 12);
int c = getSBIN(fiData, 14);
int abc = a + b + c;
int diff = Math.abs(abc - width);
if (diff != 0 && width != 0) {
double diffPercent = 100 * diff / (double)width;
if (diffPercent > 2) {
if (LOG.isTraceEnabled()) {
LOG.trace(gcgiString + ": "
+ a + " + " + b + " + " + c + " = " + (a + b + c)
+ " but found: " + width);
}
if (firstABCMismatch == null) {
firstABCMismatch = gcgiString;
}
}
}
if (cidx < lowest) {
lowest = cidx;
}
if (cidx > highest) {
highest = cidx;
}
int normalizedWidth = (int)Math.round(width * metricNormalizationFactor);
cso.setWidth(cidx, normalizedWidth);
}
}
}
cso.setFirstChar(lowest);
cso.setLastChar(highest);
if (LOG.isDebugEnabled() && firstABCMismatch != null) {
//Debug level because it usually is no problem.
LOG.debug("Font has metrics inconsitencies where A+B+C doesn't equal the"
+ " character increment. The first such character found: "
+ firstABCMismatch);
}
}
private static int getUBIN(byte[] data, int start) {
return ((data[start] & 0xFF) << 8) + (data[start + 1] & 0xFF);
}
private static int getSBIN(byte[] data, int start) {
int ubin = ((data[start] & 0xFF) << 8) + (data[start + 1] & 0xFF);
if ((ubin & 0x8000) != 0) {
//extend sign
return ubin | 0xFFFF0000;
} else {
return ubin;
}
}
private class FontControl {
private int dpi;
private int unitsPerEm;
private boolean isRelative = false;
public int getDpi() {
return dpi;
}
public void setDpi(int i) {
dpi = i;
}
public int getUnitsPerEm() {
return this.unitsPerEm;
}
public void setUnitsPerEm(int value) {
this.unitsPerEm = value;
}
public boolean isRelative() {
return isRelative;
}
public void setRelative(boolean b) {
isRelative = b;
}
}
private static class FontDescriptor {
private byte[] data;
public FontDescriptor(byte[] data) {
this.data = data;
}
public int getNominalFontSizeInMillipoints() {
int nominalFontSize = 100 * getUBIN(data, 39);
return nominalFontSize;
}
}
private static final class SingleByteLoader extends CharacterSetBuilder {
private static final SingleByteLoader INSTANCE = new SingleByteLoader();
private SingleByteLoader() {
super();
}
private static SingleByteLoader getInstance() {
return INSTANCE;
}
}
/**
* Double-byte (CID Keyed font (Type 0)) implementation of AFPFontReader.
*/
private static final class DoubleByteLoader extends CharacterSetBuilder {
private static final DoubleByteLoader INSTANCE = new DoubleByteLoader();
private DoubleByteLoader() {
}
static DoubleByteLoader getInstance() {
return INSTANCE;
}
@Override
protected Map<String, String> loadCodePage(String codePage, String encoding,
AFPResourceAccessor accessor, AFPEventProducer eventProducer) throws IOException {
// Create the HashMap to store code page information
Map<String, String> codePages = new HashMap<String, String>();
InputStream inputStream = null;
try {
inputStream = super.openInputStream(accessor, codePage.trim(), eventProducer);
} catch (IOException e) {
eventProducer.codePageNotFound(this, e);
throw e;
}
try {
StructuredFieldReader structuredFieldReader = new StructuredFieldReader(inputStream);
byte[] data;
while ((data = structuredFieldReader.getNext(CHARACTER_TABLE_SF)) != null) {
int position = 0;
byte[] gcgiBytes = new byte[8];
byte[] charBytes = new byte[2];
// Read data, ignoring bytes 0 - 2
for (int index = 3; index < data.length; index++) {
if (position < 8) {
// Build the graphic character global identifier key
gcgiBytes[position] = data[index];
position++;
} else if (position == 9) {
// Set the character
charBytes[0] = data[index];
position++;
} else if (position == 10) {
position = 0;
// Set the character
charBytes[1] = data[index];
String gcgiString = new String(gcgiBytes,
AFPConstants.EBCIDIC_ENCODING);
String charString = new String(charBytes, encoding);
codePages.put(gcgiString, charString);
} else {
position++;
}
}
}
} finally {
super.closeInputStream(inputStream);
}
return codePages;
}
}
private static int determineOrientation(byte orientation) {
int degrees = 0;
switch (orientation) {
case 0x00:
degrees = 0;
break;
case 0x2D:
degrees = 90;
break;
case 0x5A:
degrees = 180;
break;
case (byte) 0x87:
degrees = 270;
break;
default:
throw new IllegalStateException("Invalid orientation: " + orientation);
}
return degrees;
}
}