blob: 47131a9ad19457fdb2351c6ce4679e779eac138a [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.
*/
package org.apache.fop.render.pdf.pdfbox;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.fontbox.encoding.Encoding;
import org.apache.fontbox.type1.Type1Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.fop.fonts.type1.PFBData;
import org.apache.fop.fonts.type1.PFBParser;
import org.apache.fop.fonts.type1.PostscriptParser;
import org.apache.fop.fonts.type1.Type1SubsetFile;
public class MergeType1Fonts extends Type1SubsetFile implements MergeFonts {
private Map<Integer, String> nameMap = new HashMap<Integer, String>();
private PostscriptParser.PSElement encoding;
private List<String> subsetEncodingEntries = new ArrayList<String>();
private PFBData pfbData = null;
private byte[] decoded;
private PostscriptParser.PSElement charStrings;
private ByteArrayOutputStream subrsBeforeStream = new ByteArrayOutputStream();
private ByteArrayOutputStream subrsEndStream = new ByteArrayOutputStream();
private Map<Integer, byte[]> subByteMap = new HashMap<Integer, byte[]>();
public MergeType1Fonts() {
uniqueSubs = new LinkedHashMap<Integer, byte[]>();
subsetCharStrings = new HashMap<String, byte[]>();
charNames = new ArrayList<String>();
subsetSubroutines = false;
subsetEncodingEntries.add("dup 0 /.notdef put");
}
public void readFont(InputStream fontFile, String name, FontContainer font,
Map<Integer, Integer> subsetGlyphs, boolean cid) throws IOException {
PFBParser pfbParser = new PFBParser();
pfbData = pfbParser.parsePFB(fontFile);
PostscriptParser psParser = new PostscriptParser();
List<Integer> glyphs = new ArrayList<Integer>();
Type1Font t1f = ((PDType1Font)font.font).getType1Font();
Encoding enc = t1f.getEncoding();
for (int i = font.getFirstChar(); i <= font.getLastChar(); i++) {
if (!enc.getName(i).equals(".notdef")) {
nameMap.put(i, enc.getName(i));
glyphs.add(i);
}
}
Collections.sort(glyphs);
headerSection = psParser.parse(pfbData.getHeaderSegment());
encoding = getElement("/Encoding", headerSection);
if (encoding.getFoundUnexpected()) {
throw new IOException("unable to interpret postscript on arrays");
}
List<String> encodingEntries = readEncoding(glyphs, encoding);
for (String e : encodingEntries) {
if (e != null && !subsetEncodingEntries.contains(e)) {
subsetEncodingEntries.add(e);
}
}
decoded = BinaryCoder.decodeBytes(pfbData.getEncryptedSegment(), 55665, 4);
mainSection = psParser.parse(decoded);
PostscriptParser.PSFixedArray subroutines = (PostscriptParser.PSFixedArray)getElement("/Subrs", mainSection);
charStrings = getElement("/CharStrings", mainSection);
if (subroutines != null) {
subrsBeforeStream.reset();
subrsBeforeStream.write(decoded, 0, subroutines.getStartPoint());
subrsEndStream.reset();
subrsEndStream.write(decoded, subroutines.getEndPoint(),
charStrings.getStartPoint() - subroutines.getEndPoint());
}
List<byte[]> subArray = t1f.getSubrsArray();
for (int i = 0; i < subArray.size(); i++) {
if (subByteMap.containsKey(i) && !Arrays.equals(subByteMap.get(i), subArray.get(i))) {
throw new IOException("Can't merge font subroutines " + font.font.getName());
}
subByteMap.put(i, subArray.get(i));
}
Map<String, byte[]> cs = t1f.getCharStringsDict();
int lenIV = 4;
PostscriptParser.PSElement element = getElement("/lenIV", mainSection);
if (element != null && element instanceof PostscriptParser.PSVariable) {
PostscriptParser.PSVariable lenIVVar = (PostscriptParser.PSVariable)element;
lenIV = Integer.parseInt(lenIVVar.getValue());
}
for (String e : cs.keySet()) {
int[] be = charStrings.getBinaryEntries().get("/" + e);
if (be != null) {
byte[] charStringEntry = getBinaryEntry(be, decoded);
if (lenIV != 4) {
charStringEntry = BinaryCoder.decodeBytes(charStringEntry, 4330, lenIV);
charStringEntry = BinaryCoder.encodeBytes(charStringEntry, 4330, 4);
}
subsetCharStrings.put("/" + e, charStringEntry);
}
}
}
public byte[] getMergedFontSubset() throws IOException {
ByteArrayOutputStream boasHeader = writeHeader(pfbData, encoding);
ByteArrayOutputStream boasMain = writeMainSection(decoded, mainSection, charStrings);
byte[] mainSectionBytes = BinaryCoder.encodeBytes(boasMain.toByteArray(), 55665, 4);
boasMain.reset();
boasMain.write(mainSectionBytes);
ByteArrayOutputStream baosTrailer = new ByteArrayOutputStream();
baosTrailer.write(pfbData.getTrailerSegment(), 0, pfbData.getTrailerSegment().length);
return stitchFont(boasHeader, boasMain, baosTrailer);
}
@Override
protected List<String> searchEntries(HashMap<Integer, String> encodingEntries, int glyph) {
List<String> matches = new ArrayList<String>();
for (Map.Entry<Integer, String> entry : encodingEntries.entrySet()) {
String tag = getEntryPart(entry.getValue(), 3);
String name = "/" + nameMap.get(glyph);
if (name.equals(tag)) {
matches.add(entry.getValue());
}
}
return matches;
}
protected List<String> readEncoding(List<Integer> glyphs,
PostscriptParser.PSElement encoding) {
List<String> subsetEncodingEntries = new ArrayList<String>();
//Handle custom encoding
if (encoding instanceof PostscriptParser.PSFixedArray) {
PostscriptParser.PSFixedArray encodingArray = (PostscriptParser.PSFixedArray)encoding;
for (int glyph : glyphs) {
if (glyph != 0) {
//Find matching entries in the encoding table from the given glyph
List<String> matches = searchEntries(encodingArray.getEntries(), glyph);
/* If no matches are found, perform a lookup using the glyph index.
* This isn't done by default as some symbol based type-1 fonts do not
* place their characters according to the glyph index. */
if (matches.isEmpty()) {
matches.clear();
matches.add(encodingArray.getEntries().get(glyph));
}
for (String match : matches) {
subsetEncodingEntries.add(match);
}
}
}
//Handle fixed encoding
} else if (encoding instanceof PostscriptParser.PSVariable) {
if (((PostscriptParser.PSVariable) encoding).getValue().equals("StandardEncoding")) {
standardEncoding = true;
for (Map.Entry<Integer, String> v : nameMap.entrySet()) {
subsetEncodingEntries.add(String.format("dup %d /%s put", v.getKey(), v.getValue()));
}
} else {
throw new RuntimeException(
"Only Custom or StandardEncoding is supported when creating a Type 1 subset.");
}
}
return subsetEncodingEntries;
}
@Override
protected ByteArrayOutputStream writeHeader(PFBData pfbData, PostscriptParser.PSElement encoding)
throws 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 + "/Encoding 256 array" + eol
+ "0 1 255 {1 index exch /.notdef put } for" + eol;
byte[] encodingDefinition = encodingArray.getBytes("ASCII");
boasHeader.write(encodingDefinition, 0, encodingDefinition.length);
for (Map.Entry<Integer, String> entry : nameMap.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;
}
@Override
protected ByteArrayOutputStream writeMainSection(byte[] decoded, List<PostscriptParser.PSElement> mainSection,
PostscriptParser.PSElement charStrings) throws IOException {
ByteArrayOutputStream main = new ByteArrayOutputStream();
//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(subrsBeforeStream.toByteArray());
writeString("/lenIV 4 def", main);
writeString("/Subrs " + subByteMap.size() + " array" + eol, main);
for (Map.Entry<Integer, byte[]> e : subByteMap.entrySet()) {
if (e.getValue() != null) {
byte[] encoded = BinaryCoder.encodeBytes(e.getValue(), 4330, 4);
writeString("dup " + e.getKey() + " " + encoded.length + " " + rd + " ", main);
main.write(encoded);
writeString(" " + np + eol, main);
}
}
writeString(nd + eol, main);
main.write(subrsEndStream.toByteArray());
//Write the subset charString array
writeString(eol + String.format("/CharStrings %d dict dup begin",
subsetCharStrings.size()), main);
for (Map.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;
}
}