blob: 3d7093bd35425c93e3a3fb3d9b79d641e5cb2b57 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id$ */
package org.apache.fop.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(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(String font, byte[] subsetFont)
throws IOException {
decodedSections = new ArrayList<byte[]>();
//Reinitialise the input stream as reset only supports 1000 bytes.
InputStream 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>();
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++);
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++);
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++);
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);
}
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>();
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));
}
operands.clear();
}
if (cur >= 32 && cur <= 246) {
operands.add(new BytesNumber(cur - 139, 1));
} else if (cur >= 247 && cur <= 250) {
operands.add(new BytesNumber((cur - 247) * 256 + (data[i + 1] & 0xFF) + 108, 2));
i++;
} else if (cur >= 251 && 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 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;
}
}