/* * 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; } }