Browse Source

FOP-2354: Subset support for Type 1 fonts

git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@1597112 13f79535-47bb-0310-9956-ffa450edef68
tags/fop-2_0
Robert Meyer 10 years ago
parent
commit
764eba0b31

+ 4
- 0
src/java/org/apache/fop/fonts/AbstractCodePointMapping.java View File

@@ -179,6 +179,10 @@ public class AbstractCodePointMapping implements SingleByteEncoding {
return -1;
}

public String getNameFromCodePoint(int idx) {
return getCharNameMap()[idx];
}

/** {@inheritDoc} */
public String[] getCharNameMap() {
if (this.charNameMap != null) {

+ 2
- 5
src/java/org/apache/fop/fonts/FontLoader.java View File

@@ -99,11 +99,8 @@ public abstract class FontLoader {
throw new IllegalArgumentException(
"CID encoding mode not supported for Type 1 fonts");
}
if (embeddingMode == EmbeddingMode.SUBSET) {
throw new IllegalArgumentException(
"Subset embedding for Type 1 fonts is not supported");
}
loader = new Type1FontLoader(fontFileURI, embedded, useKerning, resourceResolver);
loader = new Type1FontLoader(fontFileURI, embedded, embeddingMode, useKerning,
resourceResolver);
} else {
loader = new OFFontLoader(fontFileURI, subFontName, embedded, embeddingMode,
encodingMode, useKerning, useAdvanced, resourceResolver);

+ 2
- 2
src/java/org/apache/fop/fonts/FontReader.java View File

@@ -162,13 +162,13 @@ public class FontReader extends DefaultHandler {
isCID = true;
TTFReader.checkMetricsVersion(attributes);
} else if ("TRUETYPE".equals(attributes.getValue("type"))) {
singleFont = new SingleByteFont(resourceResolver);
singleFont = new SingleByteFont(resourceResolver, EmbeddingMode.AUTO);
singleFont.setFontType(FontType.TRUETYPE);
returnFont = singleFont;
isCID = false;
TTFReader.checkMetricsVersion(attributes);
} else {
singleFont = new SingleByteFont(resourceResolver);
singleFont = new SingleByteFont(resourceResolver, EmbeddingMode.AUTO);
singleFont.setFontType(FontType.TYPE1);
returnFont = singleFont;
isCID = false;

+ 88
- 9
src/java/org/apache/fop/fonts/SingleByteFont.java View File

@@ -21,7 +21,9 @@ package org.apache.fop.fonts;

import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -34,6 +36,7 @@ import org.apache.xmlgraphics.fonts.Glyphs;

import org.apache.fop.apps.io.InternalResourceResolver;
import org.apache.fop.fonts.truetype.OpenFont.PostScriptVersion;
import org.apache.fop.util.CharUtilities;

/**
* Generic SingleByte font
@@ -56,14 +59,30 @@ public class SingleByteFont extends CustomFont {

private PostScriptVersion ttPostScriptVersion;

/**
* @param resourceResolver the URI resolver for controlling file access
*/
private int usedGlyphsCount;
private LinkedHashMap<Integer, String> usedGlyphNames;
private Map<Integer, Integer> usedGlyphs;
private Map<Integer, Character> usedCharsIndex;

public SingleByteFont(InternalResourceResolver resourceResolver) {
super(resourceResolver);
setEncoding(CodePointMapping.WIN_ANSI_ENCODING);
}

public SingleByteFont(InternalResourceResolver resourceResolver, EmbeddingMode embeddingMode) {
this(resourceResolver);
setEmbeddingMode(embeddingMode);
if (embeddingMode != EmbeddingMode.FULL) {
usedGlyphNames = new LinkedHashMap<Integer, String>();
usedGlyphs = new HashMap<Integer, Integer>();
usedCharsIndex = new HashMap<Integer, Character>();

// The zeroth value is reserved for .notdef
usedGlyphs.put(0, 0);
usedGlyphsCount++;
}
}

/** {@inheritDoc} */
public boolean isEmbeddable() {
return (!(getEmbedFileURI() == null
@@ -72,7 +91,7 @@ public class SingleByteFont extends CustomFont {

/** {@inheritDoc} */
public boolean isSubsetEmbedded() {
return false;
return getEmbeddingMode() != EmbeddingMode.FULL;
}

/** {@inheritDoc} */
@@ -182,22 +201,53 @@ public class SingleByteFont extends CustomFont {
return d;
}

private boolean isSubset() {
return getEmbeddingMode() == EmbeddingMode.SUBSET;
}

/** {@inheritDoc} */
@Override
public char mapChar(char c) {
notifyMapOperation();
char d = lookupChar(c);
if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
return d;
} else {
if (d == SingleByteEncoding.NOT_FOUND_CODE_POINT) {
// Check for alternative
d = findAlternative(c);
if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
return d;
} else {
this.warnMissingGlyph(c);
return Typeface.NOT_FOUND;
}
}
this.warnMissingGlyph(c);
return Typeface.NOT_FOUND;
if (isEmbeddable() && isSubset()) {
mapChar(d, c);
}
return d;
}

private int mapChar(int glyphIndex, char unicode) {
// Reencode to a new subset font or get the reencoded value
// IOW, accumulate the accessed characters and build a character map for them
Integer subsetCharSelector = usedGlyphs.get(glyphIndex);
if (subsetCharSelector == null) {
int selector = usedGlyphsCount;
usedGlyphs.put(glyphIndex, selector);
usedCharsIndex.put(selector, unicode);
usedGlyphsCount++;
return selector;
} else {
return subsetCharSelector;
}
}

private char getUnicode(int index) {
Character mapValue = usedCharsIndex.get(index);
if (mapValue != null) {
return mapValue.charValue();
} else {
return CharUtilities.NOT_A_CHARACTER;
}
}

private char mapUnencodedChar(char ch) {
@@ -457,5 +507,34 @@ public class SingleByteFont extends CustomFont {
return ttPostScriptVersion;
}

/**
* Returns a Map of used Glyphs.
* @return Map Map of used Glyphs
*/
public Map<Integer, Integer> getUsedGlyphs() {
return Collections.unmodifiableMap(usedGlyphs);
}

public char getUnicodeFromSelector(int selector) {
return getUnicode(selector);
}

public void mapUsedGlyphName(int gid, String value) {
usedGlyphNames.put(gid, value);
}

public Map<Integer, String> getUsedGlyphNames() {
return usedGlyphNames;
}

public String getGlyphName(int idx) {
if (idx < mapping.getCharNameMap().length) {
return mapping.getCharNameMap()[idx];
} else {
int selector = usedGlyphs.get(idx);
char theChar = usedCharsIndex.get(selector);
return unencodedCharacters.get(theChar).getCharacter().getName();
}
}
}


+ 1
- 1
src/java/org/apache/fop/fonts/truetype/OFFontLoader.java View File

@@ -137,7 +137,7 @@ public class OFFontLoader extends FontLoader {
returnFont = multiFont;
multiFont.setTTCName(ttcFontName);
} else {
singleFont = new SingleByteFont(resourceResolver);
singleFont = new SingleByteFont(resourceResolver, embeddingMode);
returnFont = singleFont;
}


+ 10
- 1
src/java/org/apache/fop/fonts/type1/AdobeStandardEncoding.java View File

@@ -26,7 +26,7 @@ import java.util.Map;
* Enumerates the {@linkplain http://unicode.org/Public/MAPPINGS/VENDORS/ADOBE/stdenc.txt} for
* characters found in a Type1 font.
*/
enum AdobeStandardEncoding {
public enum AdobeStandardEncoding {
/** space character */
space(0x0020, 0x20, "SPACE", "space"),
/** space character */
@@ -407,4 +407,13 @@ enum AdobeStandardEncoding {
AdobeStandardEncoding encoding = CACHE.get(adobeName);
return encoding != null ? encoding.getAdobeCodePoint() : -1;
}

public static String getCharFromCodePoint(int codePoint) {
for (AdobeStandardEncoding encoding : CACHE.values()) {
if (encoding.getAdobeCodePoint() == codePoint) {
return encoding.getAdobeName();
}
}
return "";
}
}

+ 24
- 0
src/java/org/apache/fop/fonts/type1/PFBData.java View File

@@ -84,6 +84,14 @@ public class PFBData {
this.headerSegment = headerSeg;
}

/**
* Gets the header segment of the font file
* @return Header segment as a byte array
*/
public byte[] getHeaderSegment() {
return this.headerSegment.clone();
}

/**
* Sets the encrypted segment of the font file.
* @param encryptedSeg the encrypted segment
@@ -92,6 +100,14 @@ public class PFBData {
this.encryptedSegment = encryptedSeg;
}

/**
* Gets the encrypted segment of the font file
* @return The encrypted segment as a byte array
*/
public byte[] getEncryptedSegment() {
return this.encryptedSegment.clone();
}

/**
* Sets the trailer segment of the font file.
* @param trailerSeg the trailer segment
@@ -100,6 +116,14 @@ public class PFBData {
this.trailerSegment = trailerSeg;
}

/**
* Gets the trailer segment of the font file
* @return The trailer segment as a byte array
*/
public byte[] getTrailerSegment() {
return this.trailerSegment.clone();
}

/**
* Returns the full length of the raw font file.
* @return int the raw file length

+ 655
- 0
src/java/org/apache/fop/fonts/type1/PostscriptParser.java View File

@@ -0,0 +1,655 @@
/*
* 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.fonts.type1;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Scanner;

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

class PostscriptParser {

protected static final Log LOG = LogFactory.getLog(PostscriptParser.class);
/* Patterns used to identify Postscript elements */
private static final String DICTIONARY = "dict";
private static final String FIXED_ARRAY = "array";
private static final String VARIABLE_ARRAY = "[";
private static final String SUBROUTINE = "{";
/* A list of parsed subroutines so if they are encountered during the parsing
* phase of another element, they can be read and pattern matched. */
private HashMap<String, PSSubroutine> subroutines = new HashMap<String, PSSubroutine>();

/**
* Parses the postscript document and returns a list of elements
* @param segment The byte array containing the postscript data
* @return A list of found Postscript elements
* @throws IOException
*/
public List<PSElement> parse(byte[] segment) throws IOException {
List<PSElement> parsedElements = new ArrayList<PSElement>();
/* Currently only scan and store the top level element. For deeper
* Postscript parsing you can push and pop elements from a stack */
PSElement foundElement = null;
String operator = null;
StringBuilder token = new StringBuilder();
List<String> tokens = new ArrayList<String>();
int startPoint = -1;
boolean specialDelimiter = false;
boolean lastWasSpecial = false;
for (int i = 0; i < segment.length; i++) {
byte cur = segment[i];
if (foundElement != null && foundElement.hasMore()) {
foundElement.parse(cur, i);
continue;
} else {
char c = (char)cur;
if (!lastWasSpecial) {
specialDelimiter = (c == '{' || c == '}' || c == '[' || c == ']'
|| (!token.toString().equals("") && c == '/'));
boolean isNotBreak = !(c == ' ' || c == '\r' || cur == 15 || cur == 12
|| cur == 10);
if (isNotBreak && !specialDelimiter) {
token.append(c);
continue;
}
} else {
lastWasSpecial = false;
token.append(c);
if (token.toString().equals("/")) {
continue;
}
}
}
try {
boolean setOp = false;
if ((foundElement == null || !foundElement.hasMore()) && token.length() > 1
&& token.charAt(0) == '/' && tokens.size() != 1 || hasEndToken(token.toString())) {
operator = token.toString();
setOp = true;
if (tokens.size() > 2 && tokens.get(tokens.size() - 1).equals("def")) {
PSVariable newVar = new PSVariable(tokens.get(0), startPoint);
newVar.setValue(tokens.get(1));
newVar.setEndPoint(i - operator.length());
parsedElements.add(newVar);
}
tokens.clear();
startPoint = i - token.length();
}
if (operator != null) {
if (foundElement instanceof PSSubroutine) {
PSSubroutine sub = (PSSubroutine)foundElement;
subroutines.put(sub.getOperator(), sub);
parsedElements.add(sub);
if (!setOp) {
operator = "";
}
} else {
if (foundElement != null) {
if (!hasMatch(foundElement.getOperator(), parsedElements)) {
parsedElements.add(foundElement);
} else {
LOG.warn("Duplicate " + foundElement.getOperator()
+ " in font file, Ignoring.");
}
}
}
//Compare token against patterns and create an element if matched
foundElement = createElement(operator, token.toString(), startPoint);
}
} finally {
tokens.add(token.toString());
token = new StringBuilder();
if (specialDelimiter) {
specialDelimiter = false;
lastWasSpecial = true;
//Retrace special postscript character so it can be processed separately
i--;
}
}
}
return parsedElements;
}

private boolean hasEndToken(String token) {
return token.equals("currentdict");
}

private boolean hasMatch(String operator, List<PSElement> elements) {
for (PSElement element : elements) {
if (element.getOperator().equals(operator)) {
return true;
}
}
return false;
}

public PSElement createElement(String operator, String elementID, int startPoint) {
if (operator.equals("")) {
return null;
}
if (elementID.equals(FIXED_ARRAY)) {
return new PSFixedArray(operator, startPoint);
} else if (elementID.equals(VARIABLE_ARRAY)) {
return new PSVariableArray(operator, startPoint);
} else if (elementID.equals(SUBROUTINE)) {
return new PSSubroutine(operator, startPoint);
} else if (!operator.equals("/Private") && elementID.equals(DICTIONARY)) {
return new PSDictionary(operator, startPoint);
}
return null;
}

/**
* A base Postscript element class
*/
public abstract class PSElement {
/* The identifying operator for this element */
protected String operator;
private List<Byte> token;
/* Determines whether there is any more data to be read whilst parsing */
protected boolean hasMore = true;
/* The locations of any entries containing binary data (e.g. arrays) */
protected LinkedHashMap<String, int[]> binaryEntries;
/* The tokens parsed from the current element */
protected List<String> tokens;
/* Determines whether binary data is currently being read / parsed */
protected boolean readBinary = false;
/* The location of the element within the binary data */
private int startPoint = -1;
protected int endPoint = -1;
/* A flag to determine if unexpected postscript has been found in the element */
private boolean foundUnexpected = false;

public PSElement(String operator, int startPoint) {
this.operator = operator;
this.startPoint = startPoint;
token = new ArrayList<Byte>();
binaryEntries = new LinkedHashMap<String, int[]>();
tokens = new ArrayList<String>();
}

/**
* Gets the Postscript element operator
* @return The operator returned as a string
*/
public String getOperator() {
return operator;
}

/**
* The start location of the element within the source binary data
* @return The start location returned as an integer
*/
public int getStartPoint() {
return startPoint;
}

/**
* The end location of the element within the source binary data
* @return The end location returned as an integer
*/
public int getEndPoint() {
return endPoint;
}

/**
* Takes over the task of tokenizing the byte data
* @param cur The current byte being read
*/
public void parse(byte cur, int pos) throws UnsupportedEncodingException {
if (!readBinary) {
char c = (char)cur;
boolean specialDelimiter = (c == '{' || c == '}' || c == '[' || c == ']'
|| c == '(' || c == ')');
boolean isNotValidBreak = !(c == ' ' || cur == 15 || cur == 12 || c == '\r'
|| c == 10);
if (isNotValidBreak && !specialDelimiter) {
token.add(cur);
} else {
parseToken(pos);
}
if (specialDelimiter) {
token.add(cur);
parseToken(pos);
}
} else {
parseByte(cur, pos);
}
}

private void parseToken(int pos) throws UnsupportedEncodingException {
byte[] bytesToken = new byte[token.size()];
for (int i = 0; i < token.size(); i++) {
bytesToken[i] = token.get(i).byteValue();
}
parseToken(new String(bytesToken, "ASCII"), pos);
token.clear();
}

/**
* Passes responsibility for processing the byte stream to the PostScript object
* @param cur The byte currently being read
* @param pos The position of the given byte
*/
public abstract void parseByte(byte cur, int pos);

/**
* Delegates the parse routine to a sub class
* @param token The token which to parse
*/
public abstract void parseToken(String token, int curPos);

protected boolean isInteger(String intValue) {
try {
Integer.parseInt(intValue);
return true;
} catch (NumberFormatException ex) {
return false;
}
}

public LinkedHashMap<String, int[]> getBinaryEntries() {
return binaryEntries;
}

/**
* Gets the binary entry location of a given index from the array
* @param index The index for which to retrieve the binary data location
* @return
*/
public int[] getBinaryEntryByIndex(int index) {
int count = 0;
for (Entry<String, int[]> entry : binaryEntries.entrySet()) {
if (count == index) {
return entry.getValue();
}
count++;
}
return new int[0];
}

/**
* Determines if more data is still to be parsed for the Postscript element.
* @return Returns true if more data exists
*/
public boolean hasMore() {
return hasMore;
}

/**
* Sets a value to be true if an expected entry postscript is found in the element.
* An example is where the encoding table may have a series of postscript operators
* altering the state of the array. In this case the only option will be to
* fully embed the font to avoid incorrect encoding in the resulting subset.
* @param foundUnexpected true if unexpected postscript is found.
*/
protected void setFoundUnexpected(boolean foundUnexpected) {
this.foundUnexpected = foundUnexpected;
}

/**
* Returns whether unexpected postscript has been found in the element
* @return true if unexpected postscript is found
*/
public boolean getFoundUnexpected() {
return this.foundUnexpected;
}
}

/**
* An object representing a Postscript array with a fixed number of entries
*/
public class PSFixedArray extends PSElement {

private String entry = "";
private String token = "";
private boolean finished = false;
protected int binaryLength = 0;
/* A list containing each entry and it's contents in the array */
private HashMap<Integer, String> entries;
private static final String READ_ONLY = "readonly";

public PSFixedArray(String operator, int startPoint) {
super(operator, startPoint);
entries = new HashMap<Integer, String>();
}

@Override
public void parseToken(String token, int curPos) {
if (!checkForEnd(token) || token.equals("def")) {
hasMore = false;
endPoint = curPos;
return;
}
if (token.equals("dup")) {
if (entry.startsWith("dup")) {
addEntry(entry);
}
entry = "";
tokens.clear();
}
if (!token.equals(READ_ONLY)) {
entry += token + " ";
}
if (!token.trim().equals("")) {
tokens.add(token);
}
if (tokens.size() == 4 && tokens.get(0).equals("dup") && isInteger(tokens.get(2))) {
binaryLength = Integer.parseInt(tokens.get(2));
readBinary = true;
}
}

private boolean checkForEnd(String checkToken) {
boolean subFound = false;
//Check for a subroutine matching that of an array end definition
PSSubroutine sub = subroutines.get("/" + checkToken);
if (sub != null && sub.getSubroutine().contains("def")) {
subFound = true;
}
if (!finished && (subFound || checkToken.equals("def"))) {
finished = true;
addEntry(entry);
return false;
} else {
return !finished;
}
}

/**
* Gets a map of array entries identified by index
* @return Returns the map of array entries
*/
public HashMap<Integer, String> getEntries() {
return entries;
}

private void addEntry(String entry) {
if (!entry.equals("")) {
if (entry.indexOf('/') != -1 && entry.charAt(entry.indexOf('/') - 1) != ' ') {
entry = entry.replace("/", " /");
}
int entryLen;
do {
entryLen = entry.length();
entry = entry.replace(" ", " ");
} while (entry.length() != entryLen);
Scanner s = new Scanner(entry).useDelimiter(" ");
boolean valid = false;
do {
s.next();
if (!s.hasNext()) {
break;
}
int id = s.nextInt();
entries.put(id, entry);
valid = true;
} while (false);
if (!valid) {
setFoundUnexpected(true);
}
}
}

@Override
public void parseByte(byte cur, int pos) {
if (binaryLength > 0) {
token += (char)cur;
binaryLength--;
} else {
if (readBinary) {
int bLength = Integer.parseInt(tokens.get(2));
int start = pos - bLength;
int end = start + bLength;
binaryEntries.put(tokens.get(1), new int[] {start, end});
token = "";
readBinary = false;
} else {
tokens.add(token);
parseToken(token, pos);
token = "";
}
}
}
}

/**
* An object representing a Postscript array with a variable number of entries
*/
public class PSVariableArray extends PSElement {
private int level = 0;
private List<String> arrayItems;
private String entry = "";

public PSVariableArray(String operator, int startPoint) {
super(operator, startPoint);
arrayItems = new ArrayList<String>();
}

@Override
public void parseToken(String token, int curPos) {
entry += token + " ";
if (level <= 0 && token.length() > 0 && token.charAt(0) == ']') {
hasMore = false;
endPoint = curPos;
return;
}
/* If the array item is a subroutine, the following keeps track of the current level
* of the tokens being parsed so that it can identify the finish */
if (token.equals("{")) {
level++;
} else if (token.equals("}")) {
level--;
if (!entry.equals("") && level == 0) {
arrayItems.add(entry);
entry = "";
}
}
}

/**
* Gets a list of found array entries within the variable array
* @return Returns the found array elements as a list
*/
public List<String> getEntries() {
return arrayItems;
}

@Override
public void parseByte(byte cur, int pos) {
//Not currently used
}
}

/**
* An object representing a Postscript subroutine element
*/
public class PSSubroutine extends PSElement {
private int level = 1;
private String entry = "";

public PSSubroutine(String operator, int startPoint) {
super(operator, startPoint);
}

@Override
public void parseToken(String token, int curPos) {
if (level == 0 && token.length() > 0 && (token.equals("def") || token.equals("ifelse")
|| token.charAt(0) == '}')) {
hasMore = false;
endPoint = curPos;
return;
}
if (token.equals("{")) {
level++;
} else if (token.equals("}")) {
level--;
}
entry += token + " ";
}

/**
* Gets the parsed subroutine element as unmodified string
* @return The subroutine as a string
*/
public String getSubroutine() {
return entry.trim();
}

@Override
public void parseByte(byte cur, int pos) {
//Not currently used
}
}

/**
* An object representing a Postscript dictionary
*/
public class PSDictionary extends PSElement {
/* A list of dictionary entries which they themselves could be variables,
* subroutines and arrays, This is currently left as parsed Strings as there is
* no need to delve deeper for our current purposes. */
private HashMap<String, String> entries;
private String entry = "";
private String token = "";
protected int binaryLength = 0;

public PSDictionary(String operator, int startPoint) {
super(operator, startPoint);
entries = new HashMap<String, String>();
}

@Override
public void parseToken(String token, int curPos) {
if (token.equals("end")) {
addEntry(entry);
hasMore = false;
endPoint = curPos;
return;
}
if (token.startsWith("/")) {
if (entry.trim().startsWith("/")) {
tokens.clear();
addEntry(entry);
}
entry = "";
}
if (tokens.size() >= 1 || token.startsWith("/")) {
tokens.add(token);
}
entry += token + " ";
if (tokens.size() == 3 && tokens.get(0).startsWith("/") && !tokens.get(2).equals("def")
&& isInteger(tokens.get(1))) {
binaryLength = Integer.parseInt(tokens.get(1));
readBinary = true;
}
}

/**
* Gets a map of dictionary entries identified by their name
* @return Returns the dictionary entries as a map
*/
public HashMap<String, String> getEntries() {
return entries;
}

private void addEntry(String entry) {
Scanner s = new Scanner(entry).useDelimiter(" ");
String id = s.next();
entries.put(id, entry);
}

@Override
public void parseByte(byte cur, int pos) {
if (binaryLength > 0) {
binaryLength--;
} else {
if (readBinary) {
int start = pos - Integer.parseInt(tokens.get(1));
int end = pos;
binaryEntries.put(tokens.get(0), new int[] {start, end});
readBinary = false;
} else {
tokens.add(token);
parseToken(token, pos);
}
}
}
}

/**
* An object representing a Postscript variable
*/
public class PSVariable extends PSElement {

/* The value of the parsed Postscript variable. */
private String value = "";

public PSVariable(String operator, int startPoint) {
super(operator, startPoint);
}

@Override
public void parseToken(String token, int curPos) {
if (token.equals("def")) {
hasMore = false;
endPoint = curPos;
return;
}
}

@Override
public void parseByte(byte cur, int pos) {
//Not currently used
}

/**
* Sets the value of the Postscript variable value
* @param value The value to set
*/
public void setValue(String value) {
this.value = value;
}

/**
* Gets the value of the Postscript variable
* @return Returns the value as a String
*/
public String getValue() {
return value;
}

/**
* Sets the end point location of the current Postscript variable.
* @param endPoint The end point location as an integer
*/
public void setEndPoint(int endPoint) {
this.endPoint = endPoint;
}

}
}

+ 7
- 3
src/java/org/apache/fop/fonts/type1/Type1FontLoader.java View File

@@ -34,6 +34,7 @@ import org.apache.commons.logging.LogFactory;

import org.apache.fop.apps.io.InternalResourceResolver;
import org.apache.fop.fonts.CodePointMapping;
import org.apache.fop.fonts.EmbeddingMode;
import org.apache.fop.fonts.FontLoader;
import org.apache.fop.fonts.FontType;
import org.apache.fop.fonts.SingleByteEncoding;
@@ -48,6 +49,8 @@ public class Type1FontLoader extends FontLoader {

private SingleByteFont singleFont;

private EmbeddingMode embeddingMode;

/**
* Constructs a new Type 1 font loader.
* @param fontFileURI the URI to the PFB file of a Type 1 font
@@ -56,9 +59,10 @@ public class Type1FontLoader extends FontLoader {
* @param resourceResolver the font resolver used to resolve URIs
* @throws IOException In case of an I/O error
*/
public Type1FontLoader(URI fontFileURI, boolean embedded, boolean useKerning,
InternalResourceResolver resourceResolver) throws IOException {
public Type1FontLoader(URI fontFileURI, boolean embedded, EmbeddingMode embeddingMode,
boolean useKerning, InternalResourceResolver resourceResolver) throws IOException {
super(fontFileURI, embedded, useKerning, true, resourceResolver);
this.embeddingMode = embeddingMode;
}

private String getPFMURI(String pfbURI) {
@@ -137,7 +141,7 @@ public class Type1FontLoader extends FontLoader {
if (afm == null && pfm == null) {
throw new IllegalArgumentException("Need at least an AFM or a PFM!");
}
singleFont = new SingleByteFont(resourceResolver);
singleFont = new SingleByteFont(resourceResolver, embeddingMode);
singleFont.setFontType(FontType.TYPE1);
if (this.embedded) {
singleFont.setEmbedURI(this.fontFileURI);

+ 772
- 0
src/java/org/apache/fop/fonts/type1/Type1SubsetFile.java View File

@@ -0,0 +1,772 @@
/*
* 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.fonts.type1;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.Set;

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

import org.apache.fop.fonts.SingleByteFont;
import org.apache.fop.fonts.type1.PostscriptParser.PSDictionary;
import org.apache.fop.fonts.type1.PostscriptParser.PSElement;
import org.apache.fop.fonts.type1.PostscriptParser.PSFixedArray;
import org.apache.fop.fonts.type1.PostscriptParser.PSSubroutine;
import org.apache.fop.fonts.type1.PostscriptParser.PSVariable;

public class Type1SubsetFile {

protected static final Log LOG = LogFactory.getLog(Type1SubsetFile.class);
/* The subset list of char strings */
private HashMap<String, byte[]> subsetCharStrings;
/* The list of character names in the subset font */
private List<String> charNames = null;
/* A list of unique subroutines references */
private LinkedHashMap<Integer, byte[]> uniqueSubs;
private SingleByteFont sbfont = null;
/* New line character */
private String eol = "\n";
/* An option to determine whether the subroutines are subset */
private boolean subsetSubroutines = true;
private byte[] fullFont;
//List of parsed Postscript elements
private List<PSElement> headerSection;
private List<PSElement> mainSection;
//Determines whether the current font uses standard encoding
private boolean standardEncoding = false;

//Type 1 operators
private static final int OP_SEAC = 6;
private static final int OP_CALLSUBR = 10;
private static final int OP_CALLOTHERSUBR = 16;

public byte[] createSubset(InputStream in, SingleByteFont sbfont,
String fontPrefix) throws IOException {
fullFont = IOUtils.toByteArray(in);
byte[] subsetFont = createSubset(sbfont, fontPrefix, true);
//This should never happen but ensure that subset is shorter than original font
return (subsetFont.length == 0 || subsetFont.length > fullFont.length)
? fullFont : subsetFont;
}

/**
* Creates a new subset from the given type 1 font input stream
* @param in The type 1 font to subset
* @param sbfont The font object containing information such as the
* characters from which to create the subset
* @param fontPrefix The prefix used in identifying the subset font
* @param allSubroutines This option will force the subset to include all
* subroutines.
* @return Returns the subset as a byte array
* @throws IOException
*/
private byte[] createSubset(SingleByteFont sbfont,
String fontPrefix, boolean subsetSubroutines) throws IOException {
this.subsetSubroutines = subsetSubroutines;
InputStream in = new ByteArrayInputStream(fullFont);
//Initialise resources used for the font creation
this.sbfont = sbfont;
PFBParser pfbParser = new PFBParser();
PFBData pfbData = pfbParser.parsePFB(in);

PostscriptParser psParser = new PostscriptParser();
charNames = new ArrayList<String>();

//Parse the header section of the font
if (headerSection == null) {
headerSection = psParser.parse(pfbData.getHeaderSegment());
}

//Read the encoding section
PSElement encoding = getElement("/Encoding", headerSection);
if (encoding.getFoundUnexpected()) {
//Fully embed the font as we're unable to interpret postscript on arrays
return new byte[0];
}
List<String> subsetEncodingEntries = readEncoding(encoding);

//Decode the main section in preparation for parsing
byte[] decoded = BinaryCoder.decodeBytes(pfbData.getEncryptedSegment(), 55665, 4);

//Initialise the resources used to hold the subset data
uniqueSubs = new LinkedHashMap<Integer, byte[]>();
subsetCharStrings = new HashMap<String, byte[]>();

//Parse the encoded main font section for elements
if (mainSection == null) {
mainSection = psParser.parse(decoded);
}

//Process and write the main section
PSElement charStrings = getElement("/CharStrings", mainSection);
int result = readMainSection(mainSection, decoded, subsetEncodingEntries, charStrings);
if (result == 0) {
/* This check handles the case where a font uses a postscript method to return a
* subroutine index. As there is currently no java postscript interpreter and writing
* one would be very difficult it prevents us from handling this eventuality. The way
* this issue is being handled is to restart the subset process and include all
* subroutines. */
uniqueSubs.clear();
subsetCharStrings.clear();
charNames.clear();
return createSubset(sbfont, fontPrefix, false);
}

//Write header section
ByteArrayOutputStream boasHeader = writeHeader(pfbData, encoding, subsetEncodingEntries);

ByteArrayOutputStream boasMain = writeMainSection(decoded, mainSection, charStrings);
byte[] mainSectionBytes = boasMain.toByteArray();
mainSectionBytes = BinaryCoder.encodeBytes(mainSectionBytes, 55665, 4);
boasMain = new ByteArrayOutputStream();
boasMain.write(mainSectionBytes);

ByteArrayOutputStream baosTrailer = new ByteArrayOutputStream();
baosTrailer.write(pfbData.getTrailerSegment(), 0, pfbData.getTrailerSegment().length);

return stitchFont(boasHeader, boasMain, baosTrailer);
}

byte[] stitchFont(ByteArrayOutputStream boasHeader, ByteArrayOutputStream boasMain,
ByteArrayOutputStream boasTrailer) throws IOException {
int headerLength = boasHeader.size();
int mainLength = boasMain.size();

boasMain.write(128);
boasMain.write(1);
updateSectionSize(boasTrailer.size()).writeTo(boasMain);
boasTrailer.write(128);
boasTrailer.write(3);

boasTrailer.writeTo(boasMain);

boasHeader.write(128);
boasHeader.write(2);
//You need to encode the main section first before getting it's size!!!
updateSectionSize(mainLength).writeTo(boasHeader);
boasMain.writeTo(boasHeader);

ByteArrayOutputStream fullFont = new ByteArrayOutputStream();
fullFont.write(128);
fullFont.write(1);
updateSectionSize(headerLength).writeTo(fullFont);
boasHeader.writeTo(fullFont);

return fullFont.toByteArray();
}

private List<String> readEncoding(PSElement encoding) {
Map<Integer, Integer> usedGlyphs = sbfont.getUsedGlyphs();
List<Integer> glyphs = new ArrayList<Integer>(usedGlyphs.keySet());
Collections.sort(glyphs);
List<String> subsetEncodingEntries = new ArrayList<String>();
//Handle custom encoding
if (encoding instanceof PSFixedArray) {
PSFixedArray encodingArray = (PSFixedArray)encoding;
for (int glyph : glyphs) {
/* Search for matching entries in the original font encoding table to add
* to the subset. As there may be more than one entry for a character (as
* was the case in a font where some glyphs were duplicated), a name search is
* performed and all matching entries are added. */
List<String> matches = searchEntries(encodingArray.getEntries(), glyph);
/* If no matches are found, create a new entry for the character so
* that it can be added even if it's not in the current encoding. */
if (matches.size() == 0) {
matches = new ArrayList<String>();
if (glyph == 0) {
matches.add("dup 0 /.notdef put");
} else {
matches.add(String.format("dup %d /%s put", glyph,
sbfont.getGlyphName(glyph)));
}
}
for (String match : matches) {
subsetEncodingEntries.add(match);
addToCharNames(match);
}
}
//Handle fixed encoding
} else if (encoding instanceof PSVariable) {
if (((PSVariable) encoding).getValue().equals("StandardEncoding")) {
standardEncoding = true;
sbfont.mapUsedGlyphName(0, "/.notdef");
for (int glyph : glyphs) {
//Retrieve the character name and alternates for the given glyph
String name = sbfont.getGlyphName(glyph);
if (glyph != 0 && name != null && !name.trim().equals("")) {
sbfont.mapUsedGlyphName(glyph, "/" + name);
}
}
} else {
LOG.warn("Only Custom or StandardEncoding is supported when creating a Type 1 subset.");
}
}
return subsetEncodingEntries;
}

private List<String> searchEntries(HashMap<Integer, String> encodingEntries, int glyph) {
List<String> matches = new ArrayList<String>();
for (Entry<Integer, String> entry : encodingEntries.entrySet()) {
String tag = getEntryPart(entry.getValue(), 3);
String name = sbfont.getGlyphName(sbfont.getUsedGlyphs().get(glyph));
if (name.equals(tag)) {
matches.add(entry.getValue());
}
}
return matches;
}

private ByteArrayOutputStream writeHeader(PFBData pfbData, PSElement encoding,
List<String> subsetEncodingEntries) throws UnsupportedEncodingException,
IOException {
ByteArrayOutputStream boasHeader = new ByteArrayOutputStream();
boasHeader.write(pfbData.getHeaderSegment(), 0, encoding.getStartPoint() - 1);

if (!standardEncoding) {
//Write out the new encoding table for the subset font
String encodingArray = eol + String.format("/Encoding %d array", 256) + eol
+ "0 1 255 {1 index exch /.notdef put } for" + eol;
byte[] encodingDefinition = encodingArray.getBytes("ASCII");
boasHeader.write(encodingDefinition, 0, encodingDefinition.length);
Set<Entry<Integer, String>> entrySet = sbfont.getUsedGlyphNames().entrySet();
for (Entry<Integer, String> entry : entrySet) {
String arrayEntry = String.format("dup %d %s put", entry.getKey(),
entry.getValue());
writeString(arrayEntry + eol, boasHeader);
}
writeString("readonly def" + eol, boasHeader);
} else {
String theEncoding = eol + "/Encoding StandardEncoding def" + eol;
boasHeader.write(theEncoding.getBytes("ASCII"));
}
boasHeader.write(pfbData.getHeaderSegment(), encoding.getEndPoint(),
pfbData.getHeaderSegment().length - encoding.getEndPoint());

return boasHeader;
}

ByteArrayOutputStream updateSectionSize(int size) throws IOException {
//Update the size in the header for the previous section
ByteArrayOutputStream boas = new ByteArrayOutputStream();
byte[] lowOrderSize = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(
size).array();
boas.write(lowOrderSize);
return boas;
}

private int readMainSection(List<PSElement> mainSection, byte[] decoded,
List<String> subsetEncodingEntries, PSElement charStrings) {
subsetEncodingEntries.add(0, "dup 0 /.notdef put");
/* Reads and parses the charStrings section to subset the charString
* and it's referenced subroutines found in the main section for each glyph. */
PSDictionary charStringsDict = (PSDictionary)charStrings;
for (String tag : sbfont.getUsedGlyphNames().values()) {
if (!tag.equals("/.notdef")) {
charNames.add(tag);
}

int[] location = charStringsDict.getBinaryEntries().get(tag);
if (location == null) {
continue;
}
byte[] charStringEntry = getBinaryEntry(location, decoded);

int skipBytes = 4;
PSElement element = getElement("lenIV", mainSection);
if (element != null && element instanceof PSVariable) {
PSVariable lenIV = (PSVariable)element;
try {
skipBytes = Integer.parseInt(lenIV.getValue());
} catch (NumberFormatException ex) {
LOG.warn(String.format("Invalid value `%s` for lenIV found in font %s", lenIV.getValue(),
sbfont.getEmbedFileURI().toString()));
}
}

charStringEntry = BinaryCoder.decodeBytes(charStringEntry, 4330, skipBytes);
PSFixedArray subroutines = (PSFixedArray)getElement("/Subrs", mainSection);
if (subsetSubroutines) {
/* Recursively scan the charString array for subroutines and if found, copy the
* entry to our subset entries and update any references. */
charStringEntry = createSubsetCharStrings(decoded, charStringEntry, subroutines,
subsetEncodingEntries, tag);
}
if (charStringEntry.length == 0) {
return 0;
}
charStringEntry = BinaryCoder.encodeBytes(charStringEntry, 4330, skipBytes);
subsetCharStrings.put(tag, charStringEntry);
}
return 1;
}

private byte[] createSubsetCharStrings(byte[] decoded, byte[] data, PSFixedArray subroutines,
List<String> subsetEncodingEntries, String glyphName) {
List<BytesNumber> operands = new ArrayList<BytesNumber>();
for (int i = 0; i < data.length; i++) {
int cur = data[i] & 0xFF;
if (cur <= 31) {
int dataLength = data.length;
if (cur == OP_CALLSUBR) {
//Found subroutine. Read subroutine and recursively scan and update references
if (operands.size() == 0) {
continue;
}
if (uniqueSubs.get(operands.get(operands.size() - 1).getNumber()) == null) {
uniqueSubs.put(operands.get(operands.size() - 1).getNumber(), new byte[0]);
data = addSubroutine(subroutines, operands, decoded, subsetEncodingEntries,
glyphName, data, i, 1, -1, operands.get(
operands.size() - 1).getNumber());
} else {
data = addSubroutine(subroutines, operands, decoded, subsetEncodingEntries,
glyphName, data, i, 1, getSubrIndex(operands.get(
operands.size() - 1).getNumber()), operands.get(
operands.size() - 1).getNumber());
}
} else if (cur == 12) {
int next = data[++i] & 0xFF;
if (next == OP_SEAC) {
/* This charString references two other glyphs which must also be included
* for this character to be displayed properly. */
int first = operands.get(operands.size() - 2).getNumber();
int second = operands.get(operands.size() - 1).getNumber();
String charFirst = AdobeStandardEncoding.getCharFromCodePoint(first);
String charSecond = AdobeStandardEncoding.getCharFromCodePoint(second);
subsetEncodingEntries.add(String.format("dup %d /%s put",
first, charFirst));
subsetEncodingEntries.add(String.format("dup %d /%s put",
second, charSecond));
sbfont.mapUsedGlyphName(first, "/" + charFirst);
sbfont.mapUsedGlyphName(second, "/" + charSecond);
} else if (next == OP_CALLOTHERSUBR) {
/* Search for a specific operator chain which results in a referenced
* subroutine being returned from a postscript method. If it's found then
* return null so the subset process can be restarted and all subroutines
* can be included. */
int[] pattern = {12, 17, 10};
int count = 0;
boolean matchesPattern = true;
if (data.length > i + 4) {
for (int pos = i + 1; pos < i + 4; pos++) {
if (data[pos] != pattern[count++]) {
matchesPattern = false;
}
}
}
if (matchesPattern) {
return new byte[0];
}
data = addSubroutine(subroutines, operands, decoded, subsetEncodingEntries,
glyphName, data, i, 2, -1, operands.get(0).getNumber());
}
}
if (data.length == 0) {
return new byte[0];
}
i -= dataLength - data.length;
operands.clear();
} else if (cur <= 246) {
operands.add(new BytesNumber(cur - 139, 1));
} else if (cur <= 250) {
operands.add(new BytesNumber((cur - 247) * 256 + (data[i + 1] & 0xFF) + 108, 2));
i++;
} else if (cur <= 254) {
operands.add(new BytesNumber(-(cur - 251) * 256 - (data[i + 1] & 0xFF) - 108, 2));
i++;
} else if (cur == 255) {
int b1 = data[i + 1] & 0xFF;
int b2 = data[i + 2] & 0xFF;
int b3 = data[i + 3] & 0xFF;
int b4 = data[i + 4] & 0xFF;
int value = b1 << 24 | b2 << 16 | b3 << 8 | b4;
operands.add(new BytesNumber(value, 5));
i += 4;
}
}
return data;
}

private int getSubrIndex(int subID) {
int count = 0;
for (Integer key : uniqueSubs.keySet()) {
if (key == subID) {
return count;
}
count++;
}
return -1;
}

private byte[] addSubroutine(PSFixedArray subroutines, List<BytesNumber> operands, byte[] decoded,
List<String> subsetEncodingEntries, String glyphName, byte[] data, int i, int opLength,
int existingSubrRef, int subrID) {
if (existingSubrRef == -1) {
int[] subrData = subroutines.getBinaryEntryByIndex(subrID);
byte[] subroutine = getBinaryEntry(subrData, decoded);
subroutine = BinaryCoder.decodeBytes(subroutine, 4330, 4);
subroutine = createSubsetCharStrings(decoded, subroutine, subroutines,
subsetEncodingEntries, glyphName);
if (subroutine.length == 0) {
return new byte[0];
}
//Encode data
subroutine = BinaryCoder.encodeBytes(subroutine, 4330, 4);
uniqueSubs.put(subrID, subroutine);
}
int subRef = (existingSubrRef != -1) ? existingSubrRef : uniqueSubs.size() - 1;
data = constructNewRefData(i, data, operands, 1, subRef, opLength);
return data;
}

private ByteArrayOutputStream writeMainSection(byte[] decoded, List<PSElement> mainSection,
PSElement charStrings) throws IOException {
ByteArrayOutputStream main = new ByteArrayOutputStream();
PSElement subrs = getElement("/Subrs", mainSection);

//Find the ID of the three most commonly subroutines defined in Type 1 fonts
String rd = findVariable(decoded, mainSection, new String[]
{"string currentfile exch readstring pop"}, "RD");
String nd = findVariable(decoded, mainSection, new String[]
{"def", "noaccess def"}, "noaccess def");
String np = findVariable(decoded, mainSection, new String[]
{"put", "noaccess put"}, "noaccess put");

main.write(decoded, 0, subrs.getStartPoint());
//Write either the subset or full list of subroutines
if (subsetSubroutines) {
writeString(eol + String.format("/Subrs %d array", uniqueSubs.size()), main);
int count = 0;
for (Entry<Integer, byte[]> entry : uniqueSubs.entrySet()) {
byte[] newSubrBytes = (eol + String.format("dup %d %d %s ", count++,
entry.getValue().length, rd)).getBytes("ASCII");
newSubrBytes = concatArray(newSubrBytes, entry.getValue());
newSubrBytes = concatArray(newSubrBytes, String.format(" %s", np).getBytes("ASCII"));
main.write(newSubrBytes);
}
writeString(eol + nd, main);
} else {
int fullSubrsLength = subrs.getEndPoint() - subrs.getStartPoint();
main.write(decoded, subrs.getStartPoint(), fullSubrsLength);
}
main.write(decoded, subrs.getEndPoint(), charStrings.getStartPoint() - subrs.getEndPoint());
//Write the subset charString array
writeString(eol + String.format("/CharStrings %d dict dup begin",
subsetCharStrings.size()), main);
for (Entry<String, byte[]> entry : subsetCharStrings.entrySet()) {
writeString(eol + String.format("%s %d %s ", entry.getKey(),
entry.getValue().length, rd),
main);
main.write(entry.getValue());
writeString(" " + nd, main);
}
writeString(eol + "end", main);
main.write(decoded, charStrings.getEndPoint(), decoded.length - charStrings.getEndPoint());

return main;
}

private String findVariable(byte[] decoded, List<PSElement> elements, String[] matches,
String fallback) throws UnsupportedEncodingException {
for (PSElement element : elements) {
if (element instanceof PSSubroutine) {
byte[] var = new byte[element.getEndPoint() - element.getStartPoint()];
System.arraycopy(decoded, element.getStartPoint(), var, 0, element.getEndPoint()
- element.getStartPoint());
String found = readVariableContents(new String(var, "ASCII")).trim();
for (String match : matches) {
if (match.equals(found)) {
return element.getOperator().substring(1, element.getOperator().length());
}
}
}
}
return fallback;
}

String readVariableContents(String variable) {
int level = 0;
String result = "";
int start = 0;
int end = 0;
boolean reading = false;
List<Integer> results = new ArrayList<Integer>();
for (int i = 0; i < variable.length(); i++) {
char curChar = variable.charAt(i);
boolean sectionEnd = false;
if (curChar == '{') {
level++;
sectionEnd = true;
} else if (curChar == '}') {
level--;
sectionEnd = true;
} else if (level == 1) {
if (!reading) {
reading = true;
start = i;
}
end = i;
}
if (sectionEnd && reading) {
results.add(start);
results.add(end);
reading = false;
}
}
for (int i = 0; i < results.size(); i += 2) {
result = result.concat(variable.substring(results.get(i), results.get(i + 1) + 1));
}
return result;
}

private void addToCharNames(String encodingEntry) {
int spaceCount = 0;
int lastSpaceIndex = 0;
int charIndex = 0;
String charName = "";
//Extract the character name from an encoding entry
for (int i = 0; i < encodingEntry.length(); i++) {
boolean isSpace = encodingEntry.charAt(i) == ' ';
if (isSpace) {
spaceCount++;
switch (spaceCount - 1) {
case 1: charIndex = Integer.parseInt(encodingEntry.substring(lastSpaceIndex + 1,
i)); break;
case 2: charName = encodingEntry.substring(lastSpaceIndex + 1, i); break;
default: break;
}
}
if (isSpace) {
lastSpaceIndex = i;
}
}
sbfont.mapUsedGlyphName(charIndex, charName);
}

private void writeString(String entry, ByteArrayOutputStream boas)
throws UnsupportedEncodingException, IOException {
byte[] byteEntry = entry.getBytes("ASCII");
boas.write(byteEntry);
}

/**
* A class used to store the last number operand and also it's size in bytes
*/
public static final class BytesNumber {
private int number;
private int numBytes;
private String name = null;

public BytesNumber(int number, int numBytes) {
this.number = number;
this.numBytes = numBytes;
}

public int getNumber() {
return this.number;
}

public int getNumBytes() {
return this.numBytes;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}

private byte[] constructNewRefData(int curDataPos, byte[] currentData,
List<BytesNumber> operands, int opNum, int curSubsetIndexSize, int operatorLength) {
//Create the new array with the modified reference
byte[] newData;
int operandsLenth = getOperandsLength(operands);
int startRef = curDataPos - operandsLenth + getOpPosition(opNum, operands)
+ (1 - operatorLength);
byte[] preBytes = new byte[startRef];
System.arraycopy(currentData, 0, preBytes, 0, startRef);
byte[] newRefBytes = createNewRef(curSubsetIndexSize, -1);
newData = concatArray(preBytes, newRefBytes);
byte[] postBytes = new byte[currentData.length - (startRef
+ operands.get(opNum - 1).getNumBytes())];
System.arraycopy(currentData, startRef + operands.get(opNum - 1).getNumBytes(), postBytes, 0,
currentData.length - (startRef + operands.get(opNum - 1).getNumBytes()));
return concatArray(newData, postBytes);
}

int getOpPosition(int opNum, List<BytesNumber> operands) {
int byteCount = 0;
for (int i = 0; i < opNum - 1; i++) {
byteCount += operands.get(i).getNumBytes();
}
return byteCount;
}

int getOperandsLength(List<BytesNumber> operands) {
int length = 0;
for (BytesNumber number : operands) {
length += number.getNumBytes();
}
return length;
}

private byte[] createNewRef(int newRef, int forceLength) {
byte[] newRefBytes;
if ((forceLength == -1 && newRef <= 107) || forceLength == 1) {
newRefBytes = new byte[1];
newRefBytes[0] = (byte)(newRef + 139);
} else if ((forceLength == -1 && newRef <= 1131) || forceLength == 2) {
newRefBytes = new byte[2];
if (newRef <= 363) {
newRefBytes[0] = (byte)247;
} else if (newRef <= 619) {
newRefBytes[0] = (byte)248;
} else if (newRef <= 875) {
newRefBytes[0] = (byte)249;
} else {
newRefBytes[0] = (byte)250;
}
newRefBytes[1] = (byte)(newRef - 108);
} else {
newRefBytes = new byte[5];
newRefBytes[0] = (byte)255;
newRefBytes[1] = (byte)(newRef >> 24);
newRefBytes[2] = (byte)(newRef >> 16);
newRefBytes[3] = (byte)(newRef >> 8);
newRefBytes[4] = (byte)newRef;
}
return newRefBytes;
}

/**
* Concatenate two byte arrays together
* @param a The first array
* @param b The second array
* @return The concatenated array
*/
byte[] concatArray(byte[] a, byte[] b) {
int aLen = a.length;
int bLen = b.length;
byte[] c = new byte[aLen + bLen];
System.arraycopy(a, 0, c, 0, aLen);
System.arraycopy(b, 0, c, aLen, bLen);
return c;
}

/**
* Returns a section of a byte array determined by it's start and
* end position.
* @param position An array containing both the start and end position
* of the section to copy.
* @param decoded The array from which to copy a section of data
* @return Returns the copy of the data section
*/
byte[] getBinaryEntry(int[] position, byte[] decoded) {
int start = position[0];
int finish = position[1];
byte[] line = new byte[finish - start];
System.arraycopy(decoded, start, line, 0, finish - start);
return line;
}

private String getEntryPart(String entry, int part) {
Scanner s = new Scanner(entry).useDelimiter(" ");
for (int i = 1; i < part; i++) {
s.next();
}
return s.next();
}

private PSElement getElement(String elementID, List<PSElement> elements) {
for (PSElement element : elements) {
if (element.getOperator().equals(elementID)) {
return element;
}
}
return null;
}

/**
* Gets the list of subset character names
* @return Returns the subset character names
*/
public List<String> getCharNames() {
return charNames;
}

/**
* A class to encode and decode sections of a type 1 font file. See Adobe
* Type 1 Font Format Section 7.2 for more details.
*/
public static class BinaryCoder {
public static byte[] decodeBytes(byte[] in, int inR, int n) {
byte[] out = new byte[in.length - n];
int r = inR;
int c1 = 52845;
int c2 = 22719;
for (int i = 0; i < in.length; i++) {
int cypher = in[i] & 0xFF;
int plain = cypher ^ (r >> 8);
if (i >= n) {
out[i - n] = (byte)plain;
}
r = (cypher + r) * c1 + c2 & 0xFFFF;
}
return out;
}

public static byte[] encodeBytes(byte[] in, int inR, int n) {
byte[] buffer = new byte[in.length + n];
for (int i = 0; i < n; i++) {
buffer[i] = 0;
}
int r = inR;
int c1 = 52845;
int c2 = 22719;
System.arraycopy(in, 0, buffer, n, buffer.length - n);
byte[] out = new byte[buffer.length];
for (int i = 0; i < buffer.length; i++) {
int plain = buffer[i] & 0xff;
int cipher = plain ^ r >> 8;
out[i] = (byte) cipher;
r = (cipher + r) * c1 + c2 & 0xffff;
}
return out;
}
}
}

+ 70
- 11
src/java/org/apache/fop/pdf/PDFFactory.java View File

@@ -23,6 +23,7 @@ package org.apache.fop.pdf;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;
@@ -32,6 +33,8 @@ import java.util.BitSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
@@ -61,6 +64,7 @@ import org.apache.fop.fonts.truetype.OTFSubSetFile;
import org.apache.fop.fonts.truetype.TTFSubSetFile;
import org.apache.fop.fonts.type1.PFBData;
import org.apache.fop.fonts.type1.PFBParser;
import org.apache.fop.fonts.type1.Type1SubsetFile;

/**
* This class provides method to create and register PDF objects.
@@ -1372,11 +1376,50 @@ public class PDFFactory {
} else {
singleByteFont = (SingleByteFont)metrics;
}
int firstChar = singleByteFont.getFirstChar();
int lastChar = singleByteFont.getLastChar();
nonBase14.setWidthMetrics(firstChar,
lastChar,
new PDFArray(null, metrics.getWidths()));

int firstChar = 0;
int lastChar = 0;
boolean defaultChars = false;
if (singleByteFont.getEmbeddingMode() == EmbeddingMode.SUBSET) {
Map<Integer, Integer> usedGlyphs = singleByteFont.getUsedGlyphs();
if (fonttype == FontType.TYPE1 && usedGlyphs.size() > 0) {
SortedSet<Integer> keys = new TreeSet<Integer>(usedGlyphs.keySet());
keys.remove(0);
if (keys.size() > 0) {
firstChar = keys.first();
lastChar = keys.last();
int[] newWidths = new int[(lastChar - firstChar) + 1];
for (int i = firstChar; i < lastChar + 1; i++) {
if (usedGlyphs.get(i) != null) {
if (i - singleByteFont.getFirstChar() < metrics.getWidths().length) {
newWidths[i - firstChar] = metrics.getWidths()[i
- singleByteFont.getFirstChar()];
} else {
defaultChars = true;
break;
}
} else {
newWidths[i - firstChar] = 0;
}
}
nonBase14.setWidthMetrics(firstChar,
lastChar,
new PDFArray(null, newWidths));
}
} else {
defaultChars = true;
}
} else {
defaultChars = true;
}

if (defaultChars) {
firstChar = singleByteFont.getFirstChar();
lastChar = singleByteFont.getLastChar();
nonBase14.setWidthMetrics(firstChar,
lastChar,
new PDFArray(null, metrics.getWidths()));
}

//Handle encoding
SingleByteEncoding mapping = singleByteFont.getEncoding();
@@ -1493,7 +1536,7 @@ public class PDFFactory {
desc.getStemV(), null);
} else {
// Create normal FontDescriptor
descriptor = new PDFFontDescriptor(desc.getEmbedFontName(),
descriptor = new PDFFontDescriptor(fontPrefix + desc.getEmbedFontName(),
desc.getAscender(),
desc.getDescender(),
desc.getCapHeight(),
@@ -1507,7 +1550,6 @@ public class PDFFactory {
// Check if the font is embeddable
if (desc.isEmbeddable()) {
AbstractPDFStream stream = makeFontFile(desc, fontPrefix);

if (stream != null) {
descriptor.setFontFile(desc.getFontType(), stream);
getDocument().registerObject(stream);
@@ -1586,10 +1628,19 @@ public class PDFFactory {
}
embeddedFont = getFontStream(font, fontBytes, isCFF);
} else if (desc.getFontType() == FontType.TYPE1) {
PFBParser parser = new PFBParser();
PFBData pfb = parser.parsePFB(in);
embeddedFont = new PDFT1Stream();
((PDFT1Stream) embeddedFont).setData(pfb);
if (font.getEmbeddingMode() != EmbeddingMode.SUBSET) {
embeddedFont = fullyEmbedType1Font(in);
} else {
assert font instanceof SingleByteFont;
SingleByteFont sbfont = (SingleByteFont)font;
Type1SubsetFile pfbFile = new Type1SubsetFile();
byte[] subsetData = pfbFile.createSubset(in, sbfont, fontPrefix);
InputStream subsetStream = new ByteArrayInputStream(subsetData);
PFBParser parser = new PFBParser();
PFBData pfb = parser.parsePFB(subsetStream);
embeddedFont = new PDFT1Stream();
((PDFT1Stream) embeddedFont).setData(pfb);
}
} else {
byte[] file = IOUtils.toByteArray(in);
embeddedFont = new PDFTTFStream(file.length);
@@ -1614,6 +1665,14 @@ public class PDFFactory {
}
}

private AbstractPDFStream fullyEmbedType1Font(InputStream in) throws IOException {
PFBParser parser = new PFBParser();
PFBData pfb = parser.parsePFB(in);
AbstractPDFStream embeddedFont = new PDFT1Stream();
((PDFT1Stream) embeddedFont).setData(pfb);
return embeddedFont;
}

private byte[] getFontSubsetBytes(FontFileReader reader, MultiByteFont mbfont, String header,
String fontPrefix, FontDescriptor desc, boolean isCFF) throws IOException {
if (isCFF) {

+ 70
- 2
src/java/org/apache/fop/render/ps/PSFontUtils.java View File

@@ -19,6 +19,7 @@

package org.apache.fop.render.ps;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
@@ -27,6 +28,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fontbox.cff.CFFStandardString;
@@ -61,6 +63,7 @@ import org.apache.fop.fonts.truetype.OpenFont.PostScriptVersion;
import org.apache.fop.fonts.truetype.TTFFile;
import org.apache.fop.fonts.truetype.TTFOutputStream;
import org.apache.fop.fonts.truetype.TTFSubSetFile;
import org.apache.fop.fonts.type1.Type1SubsetFile;
import org.apache.fop.render.ps.fonts.PSTTFOutputStream;
import org.apache.fop.util.HexEncoder;

@@ -272,13 +275,13 @@ public class PSFontUtils extends org.apache.xmlgraphics.ps.PSFontUtils {
}
gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, fontRes);
if (fontType == FontType.TYPE1) {
embedType1Font(gen, in);
embedType1Font(gen, (SingleByteFont) tf, in);
fontResource = PSFontResource.createFontResource(fontRes);
} else if (fontType == FontType.TRUETYPE) {
embedTrueTypeFont(gen, (SingleByteFont) tf, in);
fontResource = PSFontResource.createFontResource(fontRes);
} else {
composeType0Font(gen, (MultiByteFont) tf, in);
composeType0Font(gen, (MultiByteFont) tf, in);
}
gen.writeDSCComment(DSCConstants.END_RESOURCE);
gen.getResourceTracker().registerSuppliedResource(fontRes);
@@ -311,6 +314,71 @@ public class PSFontUtils extends org.apache.xmlgraphics.ps.PSFontUtils {
}
}

private static void embedType1Font(PSGenerator gen, SingleByteFont font,
InputStream fontStream) throws IOException {
if (font.getEmbeddingMode() == EmbeddingMode.AUTO) {
font.setEmbeddingMode(EmbeddingMode.FULL);
}
byte[] fullFont = IOUtils.toByteArray(fontStream);
fontStream = new ByteArrayInputStream(fullFont);
boolean embed = true;
if (font.getEmbeddingMode() == EmbeddingMode.SUBSET) {
Type1SubsetFile subset = new Type1SubsetFile();
byte[] byteSubset = subset.createSubset(fontStream, font, "");
fontStream = new ByteArrayInputStream(byteSubset);
}
embedType1Font(gen, fontStream);
if (font.getEmbeddingMode() == EmbeddingMode.SUBSET) {
writeEncoding(gen, font);
}
}

private static void writeEncoding(PSGenerator gen, SingleByteFont font) throws IOException {
String psName = font.getEmbedFontName();
gen.writeln("/" + psName + ".0.enc [ ");
int lengthCount = 0;
int charCount = 1;
int encodingCount = 0;
StringBuilder line = new StringBuilder();
int lastGid = 0;
Set<Integer> keySet = font.getUsedGlyphNames().keySet();
for (int gid : keySet) {
for (int i = lastGid; i < gid - 1; i++) {
line.append("/.notdef ");
lengthCount++;
if (lengthCount == 8) {
gen.writeln(line.toString());
line = new StringBuilder();
lengthCount = 0;
}
}
lastGid = gid;
line.append(font.getUsedGlyphNames().get(gid) + " ");
lengthCount++;
charCount++;
if (lengthCount == 8) {
gen.writeln(line.toString());
line = new StringBuilder();
lengthCount = 0;
}
if (charCount > 256) {
encodingCount++;
charCount = 1;
gen.writeln(line.toString());
line = new StringBuilder();
lengthCount = 0;
gen.writeln("] def");
gen.writeln(String.format("/%s.%d %s.%d.enc /%s RE", psName,
encodingCount - 1, psName, encodingCount - 1, psName));
gen.writeln("/" + psName + "." + encodingCount + ".enc [ ");
}
}
gen.writeln(line.toString());
gen.writeln("] def");
gen.writeln(String.format("/%s.%d %s.%d.enc /%s RE", psName, encodingCount,
psName, encodingCount, psName));
}

private static void embedTrueTypeFont(PSGenerator gen,
SingleByteFont font, InputStream fontStream) throws IOException {
/* See Adobe Technical Note #5012, "The Type 42 Font Format Specification" */

+ 93
- 0
test/java/org/apache/fop/fonts/type1/PostscriptParserTestCase.java View File

@@ -0,0 +1,93 @@
/*
* 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.fonts.type1;

import java.io.IOException;
import java.util.List;

import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import org.apache.fop.fonts.type1.PostscriptParser.PSDictionary;
import org.apache.fop.fonts.type1.PostscriptParser.PSElement;
import org.apache.fop.fonts.type1.PostscriptParser.PSFixedArray;
import org.apache.fop.fonts.type1.PostscriptParser.PSSubroutine;
import org.apache.fop.fonts.type1.PostscriptParser.PSVariable;
import org.apache.fop.fonts.type1.PostscriptParser.PSVariableArray;

public class PostscriptParserTestCase {
private PostscriptParser parser;
private String eol = new String(new byte[] {13});
private String postscriptElements =
"/myVariable 100 def" + eol
+ "/-| {def} executeonly def" + eol
+ "/myFixedArray 6 array" + eol
+ "0 1 5 {1 index exch /.notdef put } for" + eol
+ "dup 1 /a put" + eol
+ "dup 2 /b put" + eol
+ "dup 3 /c put" + eol
+ "dup 4 /d put" + eol
+ "readonly def" + eol
+ "/myVariableArray [ { this } { is } { a } { test } ] no access def" + eol
+ "/refVarSubr myValue -|";

@Before
public void setUp() {
parser = new PostscriptParser();
}

/**
* Tests parsing an example Postscript document and verifying what
* has been read.
* @throws IOException
*/
@Test
public void testPostscriptParsing() throws IOException {
List<PSElement> elements = parser.parse(postscriptElements.getBytes());
assertEquals(elements.size(), 5);
assertTrue(elements.get(0) instanceof PSVariable);
assertTrue(elements.get(2) instanceof PSFixedArray);
assertTrue(elements.get(3) instanceof PSVariableArray);
PSFixedArray fixedArray = (PSFixedArray)elements.get(2);
assertEquals(fixedArray.getEntries().size(), 4);
assertEquals(fixedArray.getEntries().get(2), "dup 2 /b put ");
PSVariableArray variableArray = (PSVariableArray)elements.get(3);
assertEquals(variableArray.getEntries().size(), 4);
/* Currently only variable arrays containing subroutines are supported, though
* this can be modified to support single values and also strip out unnecessary
* characters like the { } below. */
assertEquals(variableArray.getEntries().get(0).trim(), "{ this }");
}

/**
* Tests that the correct element is returned given the operator and element ID provided
*/
@Test
public void testCreateElement() {
assertTrue(parser.createElement("/custDictionary", "dict", -1) instanceof PSDictionary);
assertEquals(parser.createElement("/Private", "dict", -1), null);
assertTrue(parser.createElement("/aFixedArray", "array", -1) instanceof PSFixedArray);
assertTrue(parser.createElement("/aVariableArray", "[", -1) instanceof PSVariableArray);
assertTrue(parser.createElement("/aSubroutine", "{", -1) instanceof PSSubroutine);
}
}

+ 348
- 0
test/java/org/apache/fop/fonts/type1/Type1SubsetFileTestCase.java View File

@@ -0,0 +1,348 @@
/*
* 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.fonts.type1;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Test;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.apache.xmlgraphics.fonts.Glyphs;

import org.apache.fop.fonts.SingleByteFont;
import org.apache.fop.fonts.type1.PostscriptParser.PSDictionary;
import org.apache.fop.fonts.type1.PostscriptParser.PSElement;
import org.apache.fop.fonts.type1.PostscriptParser.PSFixedArray;
import org.apache.fop.fonts.type1.Type1SubsetFile.BinaryCoder;
import org.apache.fop.fonts.type1.Type1SubsetFile.BytesNumber;

public class Type1SubsetFileTestCase {

private List<byte[]> decodedSections;
private static final String TEST_FONT_A = "./test/resources/fonts/type1/c0419bt_.pfb";

@Test
public void test() throws IOException {
InputStream in = new FileInputStream(TEST_FONT_A);
compareCharStringData(in, TEST_FONT_A, createFontASubset(in, TEST_FONT_A));
}

@Test
public void testStitchFont() throws IOException {
ByteArrayOutputStream baosHeader = new ByteArrayOutputStream();
ByteArrayOutputStream baosMain = new ByteArrayOutputStream();
ByteArrayOutputStream baosTrailer = new ByteArrayOutputStream();

//Header
for (int i = 0; i < 10; i++) {
baosHeader.write(123);
baosMain.write(123);
}
for (int i = 0; i < 10; i++) {
baosTrailer.write(0);
}

Type1SubsetFile subset = new Type1SubsetFile();
byte[] result = subset.stitchFont(baosHeader, baosMain, baosTrailer);
ByteArrayInputStream bais = new ByteArrayInputStream(result);
assertEquals(result.length, 50);
PFBParser parser = new PFBParser();
parser.parsePFB(bais);
}

@Test
public void testUpdateSectionSize() throws IOException {
Type1SubsetFile subset = new Type1SubsetFile();
ByteArrayOutputStream baos = subset.updateSectionSize(456);
byte[] lowOrderSize = baos.toByteArray();
assertEquals(lowOrderSize[0], -56);
assertEquals(lowOrderSize[1], 1);
}

@Test
public void testVariableContents() {
Type1SubsetFile subset = new Type1SubsetFile();
String result = subset.readVariableContents("/myvariable {some variable contents}");
assertEquals(result, "some variable contents");
result = subset.readVariableContents("/myvariable {hello {some more text {test} and some more}test}");
//Should only reads one level deep
assertEquals(result, "hello test");
}

@Test
public void getOpPositionAndLength() {
Type1SubsetFile subset = new Type1SubsetFile();
ArrayList<BytesNumber> ops = new ArrayList<BytesNumber>();
ops.add(new BytesNumber(10, 1));
ops.add(new BytesNumber(255, 2));
ops.add(new BytesNumber(100, 1));
ops.add(new BytesNumber(97, 1));
ops.add(new BytesNumber(856, 2));
assertEquals(subset.getOpPosition(4, ops), 4);
assertEquals(subset.getOperandsLength(ops), 7);
}

@Test
public void testConcatArrays() {
byte[] arrayA = {(byte)1, (byte)2, (byte)3, (byte)4, (byte)5};
byte[] arrayB = {(byte)6, (byte)7, (byte)8, (byte)9, (byte)10};
Type1SubsetFile subset = new Type1SubsetFile();
byte[] concatArray = subset.concatArray(arrayA, arrayB);
assertEquals(concatArray.length, 10);
assertEquals(concatArray[5], 6);
assertEquals(concatArray[3], 4);
}

@Test
public void testGetBinaryEntry() {
byte[] decoded = {(byte)34, (byte)23, (byte)78, (byte)55, (byte)12,
(byte)2, (byte)65, (byte)49, (byte)90, (byte)10};
int[] section = {3, 7};
Type1SubsetFile subset = new Type1SubsetFile();
byte[] segment = subset.getBinaryEntry(section, decoded);
assertEquals(segment.length, 4);
assertEquals(segment[0], 55);
assertEquals(segment[3], 65);
}

private void compareCharStringData(InputStream in, String font, byte[] subsetFont)
throws IOException {
decodedSections = new ArrayList<byte[]>();

//Reinitialise the input stream as reset only supports 1000 bytes.
in = new FileInputStream(font);
List<PSElement> origElements = parseElements(in);
List<PSElement> subsetElements = parseElements(new ByteArrayInputStream(subsetFont));

PSFixedArray origSubs = (PSFixedArray)findElement(origElements, "/Subrs");
PSFixedArray subsetSubs = (PSFixedArray)findElement(subsetElements, "/Subrs");
PSDictionary origCharStrings = (PSDictionary)findElement(origElements, "/CharStrings");
PSDictionary subsetCharStrings = (PSDictionary)findElement(subsetElements, "/CharStrings");
for (String element : subsetCharStrings.getEntries().keySet()) {
if (element.equals("/.notdef")) {
continue;
}
int[] origBinaryCharLocation = origCharStrings.getBinaryEntries().get(element);
int[] subsetBinaryCharLocation = subsetCharStrings.getBinaryEntries().get(element);

int origLength = origBinaryCharLocation[1] - origBinaryCharLocation[0];
int subsetLength = subsetBinaryCharLocation[1] - subsetBinaryCharLocation[0];
byte[] origCharData = new byte[origLength];
byte[] subsetCharData = new byte[subsetLength];
System.arraycopy(decodedSections.get(0), origBinaryCharLocation[0], origCharData, 0, origLength);
System.arraycopy(decodedSections.get(1), subsetBinaryCharLocation[0], subsetCharData, 0, subsetLength);
origCharData = BinaryCoder.decodeBytes(origCharData, 4330, 4);
subsetCharData = BinaryCoder.decodeBytes(subsetCharData, 4330, 4);
byte[] origFullCharData = readFullCharString(decodedSections.get(0), origCharData, origSubs);
byte[] subsetFullCharData = readFullCharString(decodedSections.get(1), subsetCharData, subsetSubs);
assertArrayEquals(origFullCharData, subsetFullCharData);
}
}

private byte[] createFontASubset(InputStream in, String font) throws IOException {
SingleByteFont sbfont = mock(SingleByteFont.class);
//Glyph index & selector
Map<Integer, Integer> glyphs = new HashMap<Integer, Integer>();
//Selector & unicode
Map<Integer, Character> usedCharsIndex = new HashMap<Integer, Character>();
Map<Integer, String> usedCharNames = new HashMap<Integer, String>();
int count = 0;
for (int i = 32; i < 127; i++) {
glyphs.put(i, count++);
when(sbfont.getUnicodeFromSelector(count)).thenReturn((char)i);
usedCharNames.put(i, String.format("/%s", Glyphs.charToGlyphName((char)i)));
when(sbfont.getGlyphName(i)).thenReturn(AdobeStandardEncoding.getCharFromCodePoint(i));
}
for (int i = 161; i < 204; i++) {
glyphs.put(i, count++);
usedCharsIndex.put(count, (char)i);
when(sbfont.getUnicodeFromSelector(count)).thenReturn((char)i);
usedCharNames.put(i, String.format("/%s", Glyphs.charToGlyphName((char)i)));
when(sbfont.getGlyphName(i)).thenReturn(AdobeStandardEncoding.getCharFromCodePoint(i));
}
int[] randomGlyphs = {205, 206, 207, 208, 225, 227, 232, 233, 234, 235, 241, 245,
248, 249, 250, 251
};
for (int i = 0; i < randomGlyphs.length; i++) {
glyphs.put(randomGlyphs[i], count++);
usedCharsIndex.put(count, (char)randomGlyphs[i]);
when(sbfont.getUnicodeFromSelector(count)).thenReturn((char)randomGlyphs[i]);
usedCharNames.put(i, String.format("/%s", Glyphs.charToGlyphName((char)i)));
when(sbfont.getGlyphName(i)).thenReturn(AdobeStandardEncoding.getCharFromCodePoint(i));
}
for (int i = 256; i < 335; i++) {
glyphs.put(i, count++);
usedCharsIndex.put(count, (char)i);
when(sbfont.getUnicodeFromSelector(count)).thenReturn((char)i);
usedCharNames.put(i, String.format("/%s", Glyphs.charToGlyphName((char)i)));
when(sbfont.getGlyphName(i)).thenReturn(AdobeStandardEncoding.getCharFromCodePoint(i));
}
when(sbfont.getUsedGlyphNames()).thenReturn(usedCharNames);
when(sbfont.getUsedGlyphs()).thenReturn(glyphs);
when(sbfont.getEmbedFileURI()).thenReturn(URI.create(font));
Type1SubsetFile subset = new Type1SubsetFile();
return subset.createSubset(in, sbfont, "AAAAAA");
}

private List<PSElement> parseElements(InputStream in)
throws IOException {
PFBParser pfbParser = new PFBParser();
PFBData origData = pfbParser.parsePFB(in);
PostscriptParser parser = new PostscriptParser();
byte[] decoded = BinaryCoder.decodeBytes(origData.getEncryptedSegment(), 55665, 4);
decodedSections.add(decoded);
return parser.parse(decoded);
}

private PSElement findElement(List<PSElement> elements, String operator) {
for (PSElement element : elements) {
if (element.getOperator().equals(operator)) {
return element;
}
}
return null;
}

private byte[] readFullCharString(byte[] decoded, byte[] data, PSFixedArray subroutines) {
List<BytesNumber> operands = new ArrayList<BytesNumber>();
List<BytesNumber> fullList = new ArrayList<BytesNumber>();
for (int i = 0; i < data.length; i++) {
int cur = data[i] & 0xFF;
if (cur >= 0 && cur <= 31) {
//Found subroutine. Read subroutine, recursively scan and update references
if (cur == 10) {
if (operands.size() == 0) {
continue;
}
int[] subrData = subroutines.getBinaryEntryByIndex(operands.get(0).getNumber());
byte[] subroutine = getBinaryEntry(subrData, decoded);
subroutine = BinaryCoder.decodeBytes(subroutine, 4330, 4);
subroutine = readFullCharString(decoded, subroutine, subroutines);
data = replaceReference(data, subroutine, i - 1 + operands.get(0).getNumBytes(), i);
} else {
int next = -1;
if (cur == 12) {
next = data[++i] & 0xFF;
}
BytesNumber operand = new BytesNumber(cur, i);
operand.setName(getName(cur, next));
fullList.add(operand);
}
operands.clear();
}
if (cur >= 32 && cur <= 246) {
operands.add(new BytesNumber(cur - 139, 1));
fullList.add(operands.get(operands.size() - 1));
} else if (cur >= 247 && cur <= 250) {
operands.add(new BytesNumber((cur - 247) * 256 + (data[i + 1] & 0xFF) + 108, 2));
fullList.add(operands.get(operands.size() - 1));
i++;
} else if (cur >= 251 && cur <= 254) {
operands.add(new BytesNumber(-(cur - 251) * 256 - (data[i + 1] & 0xFF) - 108, 2));
fullList.add(operands.get(operands.size() - 1));
i++;
} else if (cur == 255) {
int b1 = data[i + 1] & 0xFF;
int b2 = data[i + 2] & 0xFF;
int b3 = data[i + 3] & 0xFF;
int b4 = data[i + 4] & 0xFF;
int value = b1 << 24 | b2 << 16 | b3 << 8 | b4;
operands.add(new BytesNumber(value, 5));
fullList.add(operands.get(operands.size() - 1));
i += 4;
}
}
return data;
}

private String getName(int operator, int next) {
switch (operator) {
case 14: return "endchar";
case 13: return "hsbw";
case 12:
switch (next) {
case 0: return "dotsection";
case 1: return "vstem3";
case 2: return "hstem3";
case 6: return "seac";
case 7: return "sbw";
case 16: return "callothersubr";
case 17: return "pop";
case 33: return "setcurrentpoint";
default: return "unknown";
}
case 9: return "closepath";
case 6: return "hlineto";
case 22: return "hmoveto";
case 31: return "hvcurveto";
case 5: return "rlineto";
case 21: return "rmoveto";
case 8: return "rrcurveto";
case 30: return "vhcurveto";
case 7: return "vlineto";
case 4: return "vmoveto";
case 1: return "hstem";
case 3: return "vstem";
case 10: return "callsubr";
case 11: return "return";
default: return "unknown";
}
}

private byte[] replaceReference(byte[] data, byte[] subroutine, int startRef, int endRef) {
byte[] preBytes = new byte[startRef - 1];
System.arraycopy(data, 0, preBytes, 0, startRef - 1);
byte[] postBytes = new byte[data.length - endRef - 1];
System.arraycopy(data, endRef + 1, postBytes, 0, data.length - endRef - 1);
data = concatArray(preBytes, subroutine, 1);
data = concatArray(data, postBytes, 0);
return data;
}

private byte[] getBinaryEntry(int[] position, byte[] decoded) {
int start = position[0];
int finish = position[1];
byte[] line = new byte[finish - start];
System.arraycopy(decoded, start, line, 0, finish - start);
return line;
}

private byte[] concatArray(byte[] a, byte[] b, int subtract) {
int aLen = a.length;
int bLen = b.length - subtract;
byte[] c = new byte[aLen + bLen];
System.arraycopy(a, 0, c, 0, aLen);
System.arraycopy(b, 0, c, aLen, bLen);
return c;
}
}

+ 264
- 0
test/resources/fonts/type1/c0419bt_.afm View File

@@ -0,0 +1,264 @@
StartFontMetrics 2.0
Comment Bitstream AFM Data
Comment Copyright 1987-1990 as an unpublished work by Bitstream Inc., Cambridge, MA.
Comment All rights reserved
Comment Confidential and proprietary to Bitstream Inc.
Comment Bitstream is a registered trademark of Bitstream Inc.
Comment bitsClassification Fixed Pitch 810
Comment bitsFontID 0419
Comment bitsManufacturingDate Mon Nov 5 16:16:55 1990
Comment bitsLayoutName clayout.adobe.text228.new
Comment UniqueID 15530419
FontName Courier10PitchBT-Roman
FullName Courier 10 Pitch
FamilyName Courier 10 Pitch
Weight Normal
ItalicAngle 0.00
IsFixedPitch true
FontBBox -44 -299 664 858
UnderlinePosition -97
UnderlineThickness 82
Version 1.0 [UFO]
Notice Copyright 1987-1990 as an unpublished work by Bitstream Inc. All rights reserved. Confidential.
EncodingScheme AdobeStandardEncoding
CapHeight 579
XHeight 452
Ascender 639
Descender -195
StartCharMetrics 228
C 32 ; WX 602 ; N space ; B 0 0 0 0 ;
C 33 ; WX 602 ; N exclam ; B 210 -11 392 613 ;
C 34 ; WX 602 ; N quotedbl ; B 154 337 448 579 ;
C 35 ; WX 602 ; N numbersign ; B 105 -58 497 674 ;
C 36 ; WX 602 ; N dollar ; B 98 -122 498 666 ;
C 37 ; WX 602 ; N percent ; B 101 -2 501 618 ;
C 38 ; WX 602 ; N ampersand ; B 88 -11 501 547 ;
C 39 ; WX 602 ; N quoteright ; B 215 317 396 579 ;
C 40 ; WX 602 ; N parenleft ; B 201 -196 401 597 ;
C 41 ; WX 602 ; N parenright ; B 201 -196 400 597 ;
C 42 ; WX 602 ; N asterisk ; B 103 201 499 579 ;
C 43 ; WX 602 ; N plus ; B 48 49 553 566 ;
C 44 ; WX 602 ; N comma ; B 178 -157 399 163 ;
C 45 ; WX 602 ; N hyphen ; B 76 178 526 273 ;
C 46 ; WX 602 ; N period ; B 204 -7 397 167 ;
C 47 ; WX 602 ; N slash ; B 114 -55 503 699 ;
C 48 ; WX 602 ; N zero ; B 101 -17 501 632 ;
C 49 ; WX 602 ; N one ; B 126 0 499 630 ;
C 50 ; WX 602 ; N two ; B 80 0 487 633 ;
C 51 ; WX 602 ; N three ; B 90 -16 497 633 ;
C 52 ; WX 602 ; N four ; B 88 0 493 631 ;
C 53 ; WX 602 ; N five ; B 84 -16 503 616 ;
C 54 ; WX 602 ; N six ; B 100 -17 499 633 ;
C 55 ; WX 602 ; N seven ; B 101 -10 491 616 ;
C 56 ; WX 602 ; N eight ; B 102 -17 500 633 ;
C 57 ; WX 602 ; N nine ; B 98 -17 497 633 ;
C 58 ; WX 602 ; N colon ; B 204 -7 397 438 ;
C 59 ; WX 602 ; N semicolon ; B 178 -157 395 438 ;
C 60 ; WX 602 ; N less ; B 34 68 543 547 ;
C 61 ; WX 602 ; N equal ; B 51 189 551 426 ;
C 62 ; WX 602 ; N greater ; B 59 68 569 547 ;
C 63 ; WX 602 ; N question ; B 122 -8 488 613 ;
C 64 ; WX 602 ; N at ; B 109 -53 476 668 ;
C 65 ; WX 602 ; N A ; B 0 0 607 579 ;
C 66 ; WX 602 ; N B ; B 42 0 558 579 ;
C 67 ; WX 602 ; N C ; B 39 -14 541 595 ;
C 68 ; WX 602 ; N D ; B 49 -2 558 579 ;
C 69 ; WX 602 ; N E ; B 33 0 530 579 ;
C 70 ; WX 602 ; N F ; B 45 0 547 579 ;
C 71 ; WX 602 ; N G ; B 34 -16 574 596 ;
C 72 ; WX 602 ; N H ; B 49 0 553 579 ;
C 73 ; WX 602 ; N I ; B 94 0 508 579 ;
C 74 ; WX 602 ; N J ; B 54 -14 574 579 ;
C 75 ; WX 602 ; N K ; B 35 0 573 579 ;
C 76 ; WX 602 ; N L ; B 28 0 555 579 ;
C 77 ; WX 602 ; N M ; B 7 0 595 579 ;
C 78 ; WX 602 ; N N ; B 16 -8 569 579 ;
C 79 ; WX 602 ; N O ; B 36 -16 566 595 ;
C 80 ; WX 602 ; N P ; B 43 0 547 579 ;
C 81 ; WX 602 ; N Q ; B 36 -128 566 595 ;
C 82 ; WX 602 ; N R ; B 29 0 588 579 ;
C 83 ; WX 602 ; N S ; B 70 -23 527 596 ;
C 84 ; WX 602 ; N T ; B 45 0 557 579 ;
C 85 ; WX 602 ; N U ; B 31 -16 570 579 ;
C 86 ; WX 602 ; N V ; B 8 -7 594 579 ;
C 87 ; WX 602 ; N W ; B 2 0 597 579 ;
C 88 ; WX 602 ; N X ; B 42 0 560 579 ;
C 89 ; WX 602 ; N Y ; B 38 0 562 579 ;
C 90 ; WX 602 ; N Z ; B 89 0 492 579 ;
C 91 ; WX 602 ; N bracketleft ; B 202 -181 400 579 ;
C 92 ; WX 602 ; N backslash ; B 114 -55 503 698 ;
C 93 ; WX 602 ; N bracketright ; B 202 -181 400 579 ;
C 94 ; WX 602 ; N asciicircum ; B 126 448 476 676 ;
C 95 ; WX 602 ; N underscore ; B -22 -299 626 -217 ;
C 96 ; WX 602 ; N quoteleft ; B 210 334 391 596 ;
C 97 ; WX 602 ; N a ; B 66 -12 568 464 ;
C 98 ; WX 602 ; N b ; B 33 -10 566 639 ;
C 99 ; WX 602 ; N c ; B 48 -12 523 463 ;
C 100 ; WX 602 ; N d ; B 44 -12 577 639 ;
C 101 ; WX 602 ; N e ; B 51 -12 546 465 ;
C 102 ; WX 602 ; N f ; B 93 0 530 640 ;
C 103 ; WX 602 ; N g ; B 66 -196 567 452 ;
C 104 ; WX 602 ; N h ; B 49 0 561 639 ;
C 105 ; WX 602 ; N i ; B 82 0 540 672 ;
C 106 ; WX 602 ; N j ; B 115 -193 452 672 ;
C 107 ; WX 602 ; N k ; B 51 0 575 639 ;
C 108 ; WX 602 ; N l ; B 91 0 531 639 ;
C 109 ; WX 602 ; N m ; B 0 0 614 464 ;
C 110 ; WX 602 ; N n ; B 51 0 563 464 ;
C 111 ; WX 602 ; N o ; B 45 -12 557 463 ;
C 112 ; WX 602 ; N p ; B 33 -195 566 452 ;
C 113 ; WX 602 ; N q ; B 39 -195 572 452 ;
C 114 ; WX 602 ; N r ; B 53 0 556 464 ;
C 115 ; WX 602 ; N s ; B 87 -12 512 464 ;
C 116 ; WX 602 ; N t ; B 43 -10 550 591 ;
C 117 ; WX 602 ; N u ; B 35 -10 558 452 ;
C 118 ; WX 602 ; N v ; B 21 -18 580 452 ;
C 119 ; WX 602 ; N w ; B -10 -18 603 452 ;
C 120 ; WX 602 ; N x ; B 40 0 568 452 ;
C 121 ; WX 602 ; N y ; B 28 -195 570 452 ;
C 122 ; WX 602 ; N z ; B 98 0 508 452 ;
C 123 ; WX 602 ; N braceleft ; B 143 -183 468 581 ;
C 124 ; WX 602 ; N bar ; B 259 -261 342 789 ;
C 125 ; WX 602 ; N braceright ; B 142 -183 467 581 ;
C 126 ; WX 602 ; N asciitilde ; B 64 249 538 366 ;
C 161 ; WX 602 ; N exclamdown ; B 210 -29 392 595 ;
C 162 ; WX 602 ; N cent ; B 93 -33 493 646 ;
C 163 ; WX 602 ; N sterling ; B 127 -4 501 625 ;
C 164 ; WX 602 ; N fraction ; B 114 -55 503 699 ;
C 165 ; WX 602 ; N yen ; B 38 0 562 579 ;
C 166 ; WX 602 ; N florin ; B 17 -139 541 632 ;
C 167 ; WX 602 ; N section ; B 75 -105 526 579 ;
C 168 ; WX 602 ; N currency ; B 60 188 548 675 ;
C 169 ; WX 602 ; N quotesingle ; B 243 330 358 579 ;
C 170 ; WX 602 ; N quotedblleft ; B 107 341 475 596 ;
C 171 ; WX 602 ; N guillemotleft ; B 149 44 453 407 ;
C 172 ; WX 602 ; N guilsinglleft ; B 232 44 370 407 ;
C 173 ; WX 602 ; N guilsinglright ; B 231 44 370 407 ;
C 174 ; WX 602 ; N fi ; B -1 0 602 672 ;
C 175 ; WX 602 ; N fl ; B -1 0 602 642 ;
C 177 ; WX 602 ; N endash ; B -22 184 626 266 ;
C 178 ; WX 602 ; N dagger ; B 121 -15 481 595 ;
C 179 ; WX 602 ; N daggerdbl ; B 121 -15 481 595 ;
C 180 ; WX 602 ; N periodcentered ; B 207 214 395 402 ;
C 182 ; WX 602 ; N paragraph ; B 57 -77 551 616 ;
C 183 ; WX 602 ; N bullet ; B 222 256 379 413 ;
C 184 ; WX 602 ; N quotesinglbase ; B 199 -147 380 116 ;
C 185 ; WX 602 ; N quotedblbase ; B 114 -139 481 116 ;
C 186 ; WX 602 ; N quotedblright ; B 122 325 489 579 ;
C 187 ; WX 602 ; N guillemotright ; B 148 44 453 407 ;
C 188 ; WX 602 ; N ellipsis ; B 31 -15 570 122 ;
C 189 ; WX 602 ; N perthousand ; B -44 -2 664 618 ;
C 191 ; WX 602 ; N questiondown ; B 118 -25 485 595 ;
C 193 ; WX 602 ; N grave ; B 124 504 407 670 ;
C 194 ; WX 602 ; N acute ; B 195 504 478 670 ;
C 195 ; WX 602 ; N circumflex ; B 132 509 470 657 ;
C 196 ; WX 602 ; N tilde ; B 123 537 478 636 ;
C 197 ; WX 602 ; N macron ; B 130 551 472 611 ;
C 198 ; WX 602 ; N breve ; B 113 528 488 661 ;
C 199 ; WX 602 ; N dotaccent ; B 239 527 363 652 ;
C 200 ; WX 602 ; N dieresis ; B 117 522 484 641 ;
C 202 ; WX 602 ; N ring ; B 177 499 425 747 ;
C 203 ; WX 602 ; N cedilla ; B 178 -233 431 -9 ;
C 205 ; WX 602 ; N hungarumlaut ; B 168 504 535 661 ;
C 206 ; WX 602 ; N ogonek ; B 248 -224 435 15 ;
C 207 ; WX 602 ; N caron ; B 132 511 469 659 ;
C 208 ; WX 602 ; N emdash ; B -22 184 626 266 ;
C 225 ; WX 602 ; N AE ; B 0 0 578 579 ;
C 227 ; WX 602 ; N ordfeminine ; B 117 110 518 596 ;
C 232 ; WX 602 ; N Lslash ; B 28 0 555 579 ;
C 233 ; WX 602 ; N Oslash ; B 36 -55 566 631 ;
C 234 ; WX 602 ; N OE ; B 34 0 578 579 ;
C 235 ; WX 602 ; N ordmasculine ; B 95 110 508 596 ;
C 241 ; WX 602 ; N ae ; B 18 -13 579 465 ;
C 245 ; WX 602 ; N dotlessi ; B 82 0 540 452 ;
C 248 ; WX 602 ; N lslash ; B 91 0 531 639 ;
C 249 ; WX 602 ; N oslash ; B 47 -22 557 475 ;
C 250 ; WX 602 ; N oe ; B 35 -13 579 466 ;
C 251 ; WX 602 ; N germandbls ; B 21 -11 550 640 ;
C -1 ; WX 602 ; N Aacute ; B 0 0 607 802 ;
C -1 ; WX 602 ; N Acircumflex ; B 0 0 607 789 ;
C -1 ; WX 602 ; N Adieresis ; B 0 0 607 773 ;
C -1 ; WX 602 ; N Agrave ; B 0 0 607 802 ;
C -1 ; WX 602 ; N Aring ; B 0 0 607 858 ;
C -1 ; WX 602 ; N Atilde ; B 0 0 607 768 ;
C -1 ; WX 602 ; N Ccedilla ; B 39 -233 541 595 ;
C -1 ; WX 602 ; N Eacute ; B 33 0 530 802 ;
C -1 ; WX 602 ; N Ecircumflex ; B 33 0 530 789 ;
C -1 ; WX 602 ; N Edieresis ; B 33 0 530 773 ;
C -1 ; WX 602 ; N Egrave ; B 33 0 530 802 ;
C -1 ; WX 602 ; N Iacute ; B 94 0 508 802 ;
C -1 ; WX 602 ; N Icircumflex ; B 94 0 508 789 ;
C -1 ; WX 602 ; N Idieresis ; B 94 0 508 773 ;
C -1 ; WX 602 ; N Igrave ; B 94 0 508 802 ;
C -1 ; WX 602 ; N Ntilde ; B 16 -8 569 768 ;
C -1 ; WX 602 ; N Oacute ; B 36 -16 566 802 ;
C -1 ; WX 602 ; N Ocircumflex ; B 36 -16 566 789 ;
C -1 ; WX 602 ; N Odieresis ; B 36 -16 566 773 ;
C -1 ; WX 602 ; N Ograve ; B 36 -16 566 802 ;
C -1 ; WX 602 ; N Otilde ; B 36 -16 566 768 ;
C -1 ; WX 602 ; N Scaron ; B 70 -23 527 791 ;
C -1 ; WX 602 ; N Uacute ; B 31 -16 570 802 ;
C -1 ; WX 602 ; N Ucircumflex ; B 31 -16 570 789 ;
C -1 ; WX 602 ; N Udieresis ; B 31 -16 570 773 ;
C -1 ; WX 602 ; N Ugrave ; B 31 -16 570 802 ;
C -1 ; WX 602 ; N Ydieresis ; B 38 0 562 773 ;
C -1 ; WX 602 ; N Zcaron ; B 89 0 492 791 ;
C -1 ; WX 602 ; N aacute ; B 66 -12 568 670 ;
C -1 ; WX 602 ; N acircumflex ; B 66 -12 568 657 ;
C -1 ; WX 602 ; N adieresis ; B 66 -12 568 641 ;
C -1 ; WX 602 ; N agrave ; B 66 -12 568 670 ;
C -1 ; WX 602 ; N aring ; B 66 -12 568 743 ;
C -1 ; WX 602 ; N atilde ; B 66 -12 568 636 ;
C -1 ; WX 602 ; N ccedilla ; B 48 -233 523 463 ;
C -1 ; WX 602 ; N eacute ; B 51 -12 546 670 ;
C -1 ; WX 602 ; N ecircumflex ; B 51 -12 546 657 ;
C -1 ; WX 602 ; N edieresis ; B 51 -12 546 641 ;
C -1 ; WX 602 ; N egrave ; B 51 -12 546 670 ;
C -1 ; WX 602 ; N iacute ; B 82 0 540 670 ;
C -1 ; WX 602 ; N icircumflex ; B 82 0 540 657 ;
C -1 ; WX 602 ; N idieresis ; B 82 0 540 641 ;
C -1 ; WX 602 ; N igrave ; B 82 0 540 670 ;
C -1 ; WX 602 ; N ntilde ; B 51 0 563 636 ;
C -1 ; WX 602 ; N oacute ; B 45 -12 557 670 ;
C -1 ; WX 602 ; N ocircumflex ; B 45 -12 557 657 ;
C -1 ; WX 602 ; N odieresis ; B 45 -12 557 641 ;
C -1 ; WX 602 ; N ograve ; B 45 -12 557 670 ;
C -1 ; WX 602 ; N otilde ; B 45 -12 557 636 ;
C -1 ; WX 602 ; N scaron ; B 87 -12 512 659 ;
C -1 ; WX 602 ; N uacute ; B 35 -10 558 670 ;
C -1 ; WX 602 ; N ucircumflex ; B 35 -10 558 657 ;
C -1 ; WX 602 ; N udieresis ; B 35 -10 558 641 ;
C -1 ; WX 602 ; N ugrave ; B 35 -10 558 670 ;
C -1 ; WX 602 ; N ydieresis ; B 28 -195 570 641 ;
C -1 ; WX 602 ; N zcaron ; B 98 0 508 659 ;
C -1 ; WX 602 ; N trademark ; B 56 337 547 616 ;
C -1 ; WX 602 ; N copyright ; B 11 45 591 625 ;
C -1 ; WX 602 ; N logicalnot ; B 48 170 553 445 ;
C -1 ; WX 602 ; N registered ; B 11 45 591 625 ;
C -1 ; WX 602 ; N minus ; B 76 262 526 354 ;
C -1 ; WX 602 ; N Eth ; B 20 -2 558 579 ;
C -1 ; WX 602 ; N Thorn ; B 55 0 518 579 ;
C -1 ; WX 602 ; N Yacute ; B 38 0 562 802 ;
C -1 ; WX 602 ; N brokenbar ; B 271 -172 331 699 ;
C -1 ; WX 602 ; N degree ; B 143 349 459 665 ;
C -1 ; WX 602 ; N divide ; B 51 99 551 516 ;
C -1 ; WX 602 ; N eth ; B 45 -12 557 640 ;
C -1 ; WX 602 ; N mu ; B 32 -201 534 452 ;
C -1 ; WX 602 ; N multiply ; B 105 125 493 511 ;
C -1 ; WX 602 ; N onehalf ; B 59 -89 539 680 ;
C -1 ; WX 602 ; N onequarter ; B 59 -94 539 680 ;
C -1 ; WX 602 ; N onesuperior ; B 164 240 462 633 ;
C -1 ; WX 602 ; N plusminus ; B 105 44 497 619 ;
C -1 ; WX 602 ; N thorn ; B 33 -195 566 639 ;
C -1 ; WX 602 ; N threequarters ; B 59 -94 539 680 ;
C -1 ; WX 602 ; N threesuperior ; B 140 222 461 633 ;
C -1 ; WX 602 ; N twosuperior ; B 145 240 451 632 ;
C -1 ; WX 602 ; N yacute ; B 28 -195 570 670 ;
EndCharMetrics
StartKernData
StartKernPairs 0
EndKernPairs
StartTrackKern 0
EndTrackKern
EndKernData
EndFontMetrics

BIN
test/resources/fonts/type1/c0419bt_.pfb View File


Loading…
Cancel
Save