diff options
author | Robert Meyer <rmeyer@apache.org> | 2014-05-23 15:05:19 +0000 |
---|---|---|
committer | Robert Meyer <rmeyer@apache.org> | 2014-05-23 15:05:19 +0000 |
commit | 764eba0b31076d7220082ac167f18cc75c427e27 (patch) | |
tree | 3e209ccb9e5e1a376b94e9e5ea29a811b45e3129 /test/java/org/apache | |
parent | c4e5885007f3c61837a12ab330658b131d978247 (diff) | |
download | xmlgraphics-fop-764eba0b31076d7220082ac167f18cc75c427e27.tar.gz xmlgraphics-fop-764eba0b31076d7220082ac167f18cc75c427e27.zip |
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
Diffstat (limited to 'test/java/org/apache')
-rw-r--r-- | test/java/org/apache/fop/fonts/type1/PostscriptParserTestCase.java | 93 | ||||
-rw-r--r-- | test/java/org/apache/fop/fonts/type1/Type1SubsetFileTestCase.java | 348 |
2 files changed, 441 insertions, 0 deletions
diff --git a/test/java/org/apache/fop/fonts/type1/PostscriptParserTestCase.java b/test/java/org/apache/fop/fonts/type1/PostscriptParserTestCase.java new file mode 100644 index 000000000..8686dc048 --- /dev/null +++ b/test/java/org/apache/fop/fonts/type1/PostscriptParserTestCase.java @@ -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); + } +} diff --git a/test/java/org/apache/fop/fonts/type1/Type1SubsetFileTestCase.java b/test/java/org/apache/fop/fonts/type1/Type1SubsetFileTestCase.java new file mode 100644 index 000000000..e18173a6b --- /dev/null +++ b/test/java/org/apache/fop/fonts/type1/Type1SubsetFileTestCase.java @@ -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; + } +} |