123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- /*
- * 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.truetype;
-
- import java.io.IOException;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
-
- import org.junit.Before;
- import org.junit.Test;
-
- import static org.junit.Assert.assertEquals;
- import static org.junit.Assert.assertTrue;
-
- import org.apache.fontbox.cff.CFFFont;
-
- import org.apache.fop.fonts.cff.CFFDataReader;
- import org.apache.fop.fonts.cff.CFFDataReader.CFFIndexData;
- import org.apache.fop.fonts.cff.CFFDataReader.DICTEntry;
- import org.apache.fop.fonts.truetype.OTFSubSetFile.BytesNumber;
-
- public class OTFSubSetFileTestCase extends OTFFileTestCase {
-
- CFFDataReader cffReaderSourceSans;
- private OTFSubSetFile sourceSansSubset;
- private byte[] sourceSansData;
- CFFDataReader cffReaderHeitiStd;
-
- /**
- * Initialises the test by creating the font subset. A CFFDataReader is
- * also created based on the subset data for use in the tests.
- * @throws IOException
- */
- @Before
- public void setUp() throws Exception {
- super.setUp();
-
- Map<Integer, Integer> glyphs = new HashMap<Integer, Integer>();
- for (int i = 0; i < 256; i++) {
- glyphs.put(i, i);
- }
-
- sourceSansSubset = new OTFSubSetFile();
- String sourceSansHeader = OFFontLoader.readHeader(sourceSansReader);
- sourceSansSubset.readFont(sourceSansReader, "SourceSansProBold", sourceSansHeader, glyphs);
- sourceSansData = sourceSansSubset.getFontSubset();
- cffReaderSourceSans = new CFFDataReader(sourceSansData);
- }
-
- /**
- * Validates the CharString data against the original font
- * @throws IOException
- */
- @Test
- public void testCharStringIndex() throws IOException {
- assertEquals(256, cffReaderSourceSans.getCharStringIndex().getNumObjects());
- assertTrue(checkCorrectOffsets(cffReaderSourceSans.getCharStringIndex()));
- validateCharStrings(cffReaderSourceSans, sourceSansSubset.getCFFReader());
- }
-
- /**
- * Checks the index data to ensure that the offsets are valid
- * @param indexData The index data to check
- * @return Returns true if it is found to be valid
- */
- private boolean checkCorrectOffsets(CFFIndexData indexData) {
- int last = 0;
- for (int i = 0; i < indexData.getOffsets().length; i++) {
- if (indexData.getOffsets()[i] < last) {
- return false;
- }
- last = indexData.getOffsets()[i];
- }
- return true;
- }
-
- /**
- * Validates the subset font CharString data by comparing it with the original.
- * @param subsetCFF The subset CFFDataReader containing the CharString data
- * @param origCFF The original CFFDataReader containing the CharString data
- * @throws IOException
- */
- private void validateCharStrings(CFFDataReader subsetCFF, CFFDataReader origCFF)
- throws IOException {
- CFFFont sourceSansOriginal = sourceSansProBold.fileFont;
- CFFIndexData charStrings = subsetCFF.getCharStringIndex();
- Map<String, byte[]> origCharStringData = sourceSansOriginal.getCharStringsDict();
- for (int i = 0; i < charStrings.getNumObjects(); i++) {
- byte[] origCharData = origCharStringData.get(origCharStringData.keySet().toArray(
- new String[0])[i]);
- byte[] charData = charStrings.getValue(i);
- List<BytesNumber> origOperands = getFullCharString(origCharData, origCFF);
- List<BytesNumber> subsetOperands = getFullCharString(charData, subsetCFF);
- for (int j = 0; j < origOperands.size(); j++) {
- assertTrue(origOperands.get(j).equals(subsetOperands.get(j)));
- }
- }
- }
-
- /**
- * Recursively reads and constructs the full CharString for comparison
- * @param data The original byte data of the CharString
- * @param cffData The CFFDataReader containing the subroutine indexes
- * @return Returns a list of parsed operands and operators
- * @throws IOException
- */
- private List<BytesNumber> getFullCharString(byte[] data, CFFDataReader cffData) throws IOException {
- CFFIndexData localIndexSubr = cffData.getLocalIndexSubr();
- CFFIndexData globalIndexSubr = cffData.getGlobalIndexSubr();
- boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0;
- boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0;
- ArrayList<BytesNumber> operands = new ArrayList<BytesNumber>();
- for (int dataPos = 0; dataPos < data.length; dataPos++) {
- int b0 = data[dataPos] & 0xff;
- if (b0 == 10 && hasLocalSubroutines) {
- int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(),
- operands.get(operands.size() - 1).getNumber());
- byte[] subr = localIndexSubr.getValue(subrNumber);
- List<BytesNumber> subrOperands = getFullCharString(subr, cffData);
- operands = mergeOperands(operands, subrOperands);
- } else if (b0 == 29 && hasGlobalSubroutines) {
- int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(),
- operands.get(operands.size() - 1).getNumber());
- byte[] subr = globalIndexSubr.getValue(subrNumber);
- ArrayList<BytesNumber> subrOperands = (ArrayList<BytesNumber>)getFullCharString(subr, cffData);
- operands = mergeOperands(operands, subrOperands);
- } else if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) {
- int size = 1;
- int b1 = -1;
- if (b0 == 12) {
- b1 = data[dataPos++] & 0xff;
- size = 2;
- }
- if (b0 == 19 || b0 == 20) {
- dataPos += 1;
- size = 2;
- }
- operands.add(new Operator(b0, size, getOperatorName(b0, b1)));
- } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) {
- operands.add(readNumber(b0, data, dataPos));
- dataPos += operands.get(operands.size() - 1).getNumBytes() - 1;
- }
- }
- return operands;
- }
-
- /**
- * Merges two lists of operands. This is typically used to merge the CharString
- * data with that of a parsed and referenced subroutine.
- * @param charString The parsed CharString data so far
- * @param subroutine The parsed elements from a subroutine
- * @return Returns a merged list of both CharString and subroutine elements.
- */
- private ArrayList<BytesNumber> mergeOperands(List<BytesNumber> charString,
- List<BytesNumber> subroutine) {
- BytesNumber[] charStringOperands = charString.toArray(new BytesNumber[0]);
- BytesNumber[] subroutineOperands = subroutine.toArray(new BytesNumber[0]);
- BytesNumber[] mergeData = new BytesNumber[charStringOperands.length - 1
- + subroutineOperands.length - 1];
- System.arraycopy(charStringOperands, 0, mergeData, 0, charStringOperands.length - 1);
- System.arraycopy(subroutineOperands, 0, mergeData, charStringOperands.length - 1,
- subroutineOperands.length - 1);
- ArrayList<BytesNumber> hello = new ArrayList<BytesNumber>();
- hello.addAll(Arrays.asList(mergeData));
- return hello;
- }
-
- /**
- * Parses a number from one or more bytes
- * @param b0 The first byte to identify how to interpret the number
- * @param input The original byte data containing the number
- * @param curPos The current position of the number
- * @return Returns the number
- * @throws IOException
- */
- private BytesNumber readNumber(int b0, byte[] input, int curPos) throws IOException {
- if (b0 == 28) {
- int b1 = input[curPos + 1] & 0xff;
- int b2 = input[curPos + 2] & 0xff;
- return new BytesNumber(Integer.valueOf((short) (b1 << 8 | b2)), 3);
- } else if (b0 >= 32 && b0 <= 246) {
- return new BytesNumber(Integer.valueOf(b0 - 139), 1);
- } else if (b0 >= 247 && b0 <= 250) {
- int b1 = input[curPos + 1] & 0xff;
- return new BytesNumber(Integer.valueOf((b0 - 247) * 256 + b1 + 108), 2);
- } else if (b0 >= 251 && b0 <= 254) {
- int b1 = input[curPos + 1] & 0xff;
- return new BytesNumber(Integer.valueOf(-(b0 - 251) * 256 - b1 - 108), 2);
- } else if (b0 == 255) {
- int b1 = input[curPos + 1] & 0xff;
- int b2 = input[curPos + 2] & 0xff;
- return new BytesNumber(Integer.valueOf((short)(b1 << 8 | b2)), 5);
- } else {
- throw new IllegalArgumentException();
- }
- }
-
- /**
- * Gets the subroutine number according to the number of subroutines
- * and the provided operand.
- * @param numSubroutines The number of subroutines used to calculate the
- * subroutine reference.
- * @param operand The operand for the subroutine
- * @return Returns the calculated subroutine number
- */
- private int getSubrNumber(int numSubroutines, int operand) {
- int bias = getBias(numSubroutines);
- return bias + operand;
- }
-
- /**
- * Gets the bias give the number of subroutines. This is used in the
- * calculation to determine a subroutine's number
- * @param subrCount The number of subroutines for a given index
- * @return Returns the bias value
- */
- private int getBias(int subrCount) {
- if (subrCount < 1240) {
- return 107;
- } else if (subrCount < 33900) {
- return 1131;
- } else {
- return 32768;
- }
- }
-
- /**
- * A class representing an operator from the CharString data
- */
- private class Operator extends BytesNumber {
- private String opName = "";
-
- public Operator(int number, int numBytes, String opName) {
- super(number, numBytes);
- this.opName = opName;
- }
- public String toString() {
- return String.format("[%s]", opName);
- }
- }
-
- /**
- * Gets the identifying name for the given operator. This is primarily
- * used for debugging purposes. See the Type 2 CharString Format specification
- * document (Technical Note #5177) Appendix A (Command Codes).
- * @param operator The operator code
- * @param codeb The second byte of the operator
- * @return Returns the operator name.
- */
- private String getOperatorName(int operator, int operatorB) {
- switch (operator) {
- case 0: return "Reserved";
- case 1: return "hstem";
- case 2: return "Reserved";
- case 3: return "vstem";
- case 4: return "vmoveto";
- case 5: return "rlineto";
- case 6: return "hlineto";
- case 7: return "vlineto";
- case 8: return "rrcurveto";
- case 9: return "Reserved";
- case 10: return "callsubr";
- case 11: return "return";
- case 12: return getDoubleOpName(operatorB);
- case 13: return "Reserved";
- case 14: return "enchar";
- case 15:
- case 16:
- case 17: return "Reserved";
- case 18: return "hstemhm";
- case 19: return "hintmask";
- case 20: return "cntrmask";
- case 21: return "rmoveto";
- case 22: return "hmoveto";
- case 23: return "vstemhm";
- case 24: return "rcurveline";
- case 25: return "rlinecurve";
- case 26: return "vvcurveto";
- case 27: return "hhcurveto";
- case 28: return "shortint";
- case 29: return "callgsubr";
- case 30: return "vhcurveto";
- case 31: return "hvcurveto";
- default: return "Unknown";
- }
- }
-
- /**
- * Gets the name of a double byte operator code
- * @param operator The second byte of the operator
- * @return Returns the name
- */
- private String getDoubleOpName(int operator) {
- switch (operator) {
- case 0:
- case 1:
- case 2: return "Reserved";
- case 3: return "and";
- case 4: return "or";
- case 5: return "not";
- case 6:
- case 7:
- case 8: return "Reserved";
- case 9: return "abs";
- case 10: return "add";
- case 11: return "sub";
- case 12: return "div";
- case 13: return "Reserved";
- case 14: return "neg";
- case 15: return "eq";
- case 16:
- case 17: return "Reserved";
- case 18: return "drop";
- case 19: return "Reserved";
- case 20: return "put";
- case 21: return "get";
- case 22: return "ifelse";
- case 23: return "random";
- case 24: return "mul";
- case 25: return "Reserved";
- case 26: return "sqrt";
- case 27: return "dup";
- case 28: return "exch";
- case 29: return "index";
- case 30: return "roll";
- case 31:
- case 32:
- case 33: return "Reserved";
- case 34: return "hflex";
- case 35: return "flex";
- case 36: return "hflex1";
- case 37: return "flex1";
- case 38: return "Reserved";
- default: return "Unknown";
- }
- }
-
- /**
- * Validates the String index data and size
- * @throws IOException
- */
- @Test
- public void testStringIndex() throws IOException {
- assertEquals(164, cffReaderSourceSans.getStringIndex().getNumObjects());
- assertTrue(checkCorrectOffsets(cffReaderSourceSans.getStringIndex()));
- assertEquals("Amacron", new String(cffReaderSourceSans.getStringIndex().getValue(5)));
- assertEquals("Edotaccent", new String(cffReaderSourceSans.getStringIndex().getValue(32)));
- assertEquals("uni0122", new String(cffReaderSourceSans.getStringIndex().getValue(45)));
- }
-
- /**
- * Validates the Top Dict data
- * @throws IOException
- */
- @Test
- public void testTopDictData() throws IOException {
- Map<String, DICTEntry> topDictEntries = cffReaderSourceSans.parseDictData(
- cffReaderSourceSans.getTopDictIndex().getData());
- assertEquals(10, topDictEntries.size());
- }
- }
|