| /* |
| * 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.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| import org.apache.fontbox.cff.CFFStandardString; |
| import org.apache.fontbox.cff.encoding.CFFEncoding; |
| |
| import org.apache.fop.fonts.MultiByteFont; |
| 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.cff.CFFDataReader.FDSelect; |
| import org.apache.fop.fonts.cff.CFFDataReader.FontDict; |
| import org.apache.fop.fonts.cff.CFFDataReader.Format0FDSelect; |
| import org.apache.fop.fonts.cff.CFFDataReader.Format3FDSelect; |
| |
| /** |
| * Reads an OpenType CFF file and generates a subset |
| * The OpenType specification can be found at the Microsoft |
| * Typography site: http://www.microsoft.com/typography/otspec/ |
| */ |
| public class OTFSubSetFile extends OTFFile { |
| |
| protected byte[] output; |
| protected int currentPos; |
| private int realSize; |
| |
| /** A map containing each glyph to be included in the subset |
| * with their existing and new GID's **/ |
| protected LinkedHashMap<Integer, Integer> subsetGlyphs = new LinkedHashMap<Integer, Integer>(); |
| |
| /** A map of the new GID to SID used to construct the charset table **/ |
| protected LinkedHashMap<Integer, Integer> gidToSID; |
| |
| protected CFFIndexData localIndexSubr; |
| protected CFFIndexData globalIndexSubr; |
| |
| /** List of subroutines to write to the local / global indexes in the subset font **/ |
| protected List<byte[]> subsetLocalIndexSubr; |
| protected List<byte[]> subsetGlobalIndexSubr; |
| |
| /** For fonts which have an FDSelect or ROS flag in Top Dict, this is used to store the |
| * local subroutine indexes for each group as opposed to the above subsetLocalIndexSubr */ |
| private ArrayList<List<byte[]>> fdSubrs; |
| |
| /** The subset FD Select table used to store the mappings between glyphs and their |
| * associated FDFont object which point to a private dict and local subroutines. */ |
| private LinkedHashMap<Integer, FDIndexReference> subsetFDSelect; |
| |
| /** A list of unique subroutines from the global / local subroutine indexes */ |
| protected List<Integer> localUniques; |
| protected List<Integer> globalUniques; |
| |
| /** A store of the number of subroutines each global / local subroutine will store **/ |
| protected int subsetLocalSubrCount; |
| protected int subsetGlobalSubrCount; |
| |
| /** A list of char string data for each glyph to be stored in the subset font **/ |
| protected List<byte[]> subsetCharStringsIndex; |
| |
| /** The embedded name to change in the name table **/ |
| protected String embeddedName; |
| |
| /** An array used to hold the string index data for the subset font **/ |
| protected List<byte[]> stringIndexData = new ArrayList<byte[]>(); |
| |
| /** The CFF reader object used to read data and offsets from the original font file */ |
| protected CFFDataReader cffReader; |
| |
| /** The class used to represent this font **/ |
| private MultiByteFont mbFont; |
| |
| /** The number of standard strings in CFF **/ |
| public static final int NUM_STANDARD_STRINGS = 391; |
| /** The operator used to identify a local subroutine reference */ |
| private static final int LOCAL_SUBROUTINE = 10; |
| /** The operator used to identify a global subroutine reference */ |
| private static final int GLOBAL_SUBROUTINE = 29; |
| |
| public OTFSubSetFile() throws IOException { |
| super(); |
| } |
| |
| public void readFont(FontFileReader in, String embeddedName, String header, |
| MultiByteFont mbFont) throws IOException { |
| this.mbFont = mbFont; |
| readFont(in, embeddedName, header, mbFont.getUsedGlyphs()); |
| } |
| |
| /** |
| * Reads and creates a subset of the font. |
| * |
| * @param in FontFileReader to read from |
| * @param name Name to be checked for in the font file |
| * @param header The header of the font file |
| * @param glyphs Map of glyphs (glyphs has old index as (Integer) key and |
| * new index as (Integer) value) |
| * @throws IOException in case of an I/O problem |
| */ |
| void readFont(FontFileReader in, String embeddedName, String header, |
| Map<Integer, Integer> usedGlyphs) throws IOException { |
| fontFile = in; |
| |
| currentPos = 0; |
| realSize = 0; |
| |
| this.embeddedName = embeddedName; |
| |
| //Sort by the new GID and store in a LinkedHashMap |
| subsetGlyphs = sortByValue(usedGlyphs); |
| |
| output = new byte[in.getFileSize()]; |
| |
| initializeFont(in); |
| |
| cffReader = new CFFDataReader(fontFile); |
| |
| //Create the CIDFontType0C data |
| createCFF(); |
| } |
| |
| private LinkedHashMap<Integer, Integer> sortByValue(Map<Integer, Integer> map) { |
| List<Entry<Integer, Integer>> list = new ArrayList<Entry<Integer, Integer>>(map.entrySet()); |
| Collections.sort(list, new Comparator<Entry<Integer, Integer>>() { |
| public int compare(Entry<Integer, Integer> o1, Entry<Integer, Integer> o2) { |
| return ((Comparable<Integer>) o1.getValue()).compareTo(o2.getValue()); |
| } |
| }); |
| |
| LinkedHashMap<Integer, Integer> result = new LinkedHashMap<Integer, Integer>(); |
| for (Entry<Integer, Integer> entry : list) { |
| result.put(entry.getKey(), entry.getValue()); |
| } |
| return result; |
| } |
| |
| protected void createCFF() throws IOException { |
| //Header |
| writeBytes(cffReader.getHeader()); |
| |
| //Name Index |
| writeIndex(Arrays.asList(embeddedName.getBytes())); |
| |
| //Keep offset of the topDICT so it can be updated once all data has been written |
| int topDictOffset = currentPos; |
| //Top DICT Index and Data |
| byte[] topDictIndex = cffReader.getTopDictIndex().getByteData(); |
| int offSize = topDictIndex[2]; |
| writeBytes(topDictIndex, 0, 3 + (offSize * 2)); |
| int topDictDataOffset = currentPos; |
| writeTopDICT(); |
| |
| //Create the char string index data and related local / global subroutines |
| if (cffReader.getFDSelect() == null) { |
| createCharStringData(); |
| } else { |
| createCharStringDataCID(); |
| } |
| |
| //If it is a CID-Keyed font, store each FD font and add each SID |
| List<Integer> fontNameSIDs = null; |
| List<Integer> subsetFDFonts = null; |
| if (cffReader.getFDSelect() != null) { |
| subsetFDFonts = getUsedFDFonts(); |
| fontNameSIDs = storeFDStrings(subsetFDFonts); |
| } |
| |
| //String index |
| writeStringIndex(); |
| |
| //Global subroutine index |
| writeIndex(subsetGlobalIndexSubr); |
| |
| //Encoding |
| int encodingOffset = currentPos; |
| writeEncoding(fileFont.getEncoding()); |
| |
| //Charset table |
| int charsetOffset = currentPos; |
| writeCharsetTable(cffReader.getFDSelect() != null); |
| |
| //FDSelect table |
| int fdSelectOffset = currentPos; |
| if (cffReader.getFDSelect() != null) { |
| writeFDSelect(); |
| } |
| |
| //Char Strings Index |
| int charStringOffset = currentPos; |
| writeIndex(subsetCharStringsIndex); |
| |
| if (cffReader.getFDSelect() == null) { |
| //Keep offset to modify later with the local subroutine index offset |
| int privateDictOffset = currentPos; |
| writePrivateDict(); |
| |
| //Local subroutine index |
| int localIndexOffset = currentPos; |
| writeIndex(subsetLocalIndexSubr); |
| |
| //Update the offsets |
| updateOffsets(topDictOffset, charsetOffset, charStringOffset, privateDictOffset, |
| localIndexOffset, encodingOffset); |
| } else { |
| List<Integer> privateDictOffsets = writeCIDDictsAndSubrs(subsetFDFonts); |
| int fdArrayOffset = writeFDArray(subsetFDFonts, privateDictOffsets, fontNameSIDs); |
| |
| updateCIDOffsets(topDictDataOffset, fdArrayOffset, fdSelectOffset, charsetOffset, |
| charStringOffset, encodingOffset); |
| } |
| } |
| |
| protected List<Integer> storeFDStrings(List<Integer> uniqueNewRefs) throws IOException { |
| ArrayList<Integer> fontNameSIDs = new ArrayList<Integer>(); |
| List<FontDict> fdFonts = cffReader.getFDFonts(); |
| for (int i = 0; i < uniqueNewRefs.size(); i++) { |
| FontDict fdFont = fdFonts.get(uniqueNewRefs.get(i)); |
| byte[] fdFontByteData = fdFont.getByteData(); |
| Map<String, DICTEntry> fdFontDict = cffReader.parseDictData(fdFontByteData); |
| fontNameSIDs.add(stringIndexData.size() + NUM_STANDARD_STRINGS); |
| stringIndexData.add(cffReader.getStringIndex().getValue(fdFontDict.get("FontName") |
| .getOperands().get(0).intValue() - NUM_STANDARD_STRINGS)); |
| } |
| return fontNameSIDs; |
| } |
| |
| protected void writeBytes(byte[] out) { |
| for (int i = 0; i < out.length; i++) { |
| writeByte(out[i]); |
| } |
| } |
| |
| protected void writeBytes(byte[] out, int offset, int length) { |
| for (int i = offset; i < offset + length; i++) { |
| output[currentPos++] = out[i]; |
| realSize++; |
| } |
| } |
| |
| private void writeEncoding(CFFEncoding encoding) throws IOException { |
| LinkedHashMap<String, DICTEntry> topDICT = cffReader.getTopDictEntries(); |
| DICTEntry encodingEntry = topDICT.get("Encoding"); |
| if (encodingEntry != null && encodingEntry.getOperands().get(0).intValue() != 0 |
| && encodingEntry.getOperands().get(0).intValue() != 1) { |
| writeByte(0); |
| writeByte(gidToSID.size()); |
| for (int gid : gidToSID.keySet()) { |
| int code = encoding.getCode(gidToSID.get(gid)); |
| writeByte(code); |
| } |
| } |
| } |
| |
| protected void writeTopDICT() throws IOException { |
| LinkedHashMap<String, DICTEntry> topDICT = cffReader.getTopDictEntries(); |
| List<String> topDictStringEntries = Arrays.asList("version", "Notice", "Copyright", |
| "FullName", "FamilyName", "Weight", "PostScript"); |
| for (Map.Entry<String, DICTEntry> dictEntry : topDICT.entrySet()) { |
| String dictKey = dictEntry.getKey(); |
| DICTEntry entry = dictEntry.getValue(); |
| //If the value is an SID, update the reference but keep the size the same |
| if (dictKey.equals("ROS")) { |
| writeROSEntry(entry); |
| } else if (dictKey.equals("CIDCount")) { |
| writeCIDCount(entry); |
| } else if (topDictStringEntries.contains(dictKey)) { |
| writeTopDictStringEntry(entry); |
| } else { |
| writeBytes(entry.getByteData()); |
| } |
| } |
| } |
| |
| private void writeROSEntry(DICTEntry dictEntry) throws IOException { |
| int sidA = dictEntry.getOperands().get(0).intValue(); |
| if (sidA > 390) { |
| stringIndexData.add(cffReader.getStringIndex().getValue(sidA - NUM_STANDARD_STRINGS)); |
| } |
| int sidAStringIndex = stringIndexData.size() + 390; |
| int sidB = dictEntry.getOperands().get(1).intValue(); |
| if (sidB > 390) { |
| stringIndexData.add("Identity".getBytes()); |
| } |
| int sidBStringIndex = stringIndexData.size() + 390; |
| byte[] cidEntryByteData = dictEntry.getByteData(); |
| cidEntryByteData = updateOffset(cidEntryByteData, 0, dictEntry.getOperandLengths().get(0), |
| sidAStringIndex); |
| cidEntryByteData = updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0), |
| dictEntry.getOperandLengths().get(1), sidBStringIndex); |
| cidEntryByteData = updateOffset(cidEntryByteData, dictEntry.getOperandLengths().get(0) |
| + dictEntry.getOperandLengths().get(1), dictEntry.getOperandLengths().get(2), 139); |
| writeBytes(cidEntryByteData); |
| } |
| |
| protected void writeCIDCount(DICTEntry dictEntry) throws IOException { |
| byte[] cidCountByteData = dictEntry.getByteData(); |
| cidCountByteData = updateOffset(cidCountByteData, 0, dictEntry.getOperandLengths().get(0), |
| subsetGlyphs.size()); |
| writeBytes(cidCountByteData); |
| } |
| |
| private void writeTopDictStringEntry(DICTEntry dictEntry) throws IOException { |
| int sid = dictEntry.getOperands().get(0).intValue(); |
| if (sid > 391) { |
| stringIndexData.add(cffReader.getStringIndex().getValue(sid - 391)); |
| } |
| |
| byte[] newDictEntry = createNewRef(stringIndexData.size() + 390, dictEntry.getOperator(), |
| dictEntry.getOperandLength()); |
| writeBytes(newDictEntry); |
| } |
| |
| private void writeStringIndex() throws IOException { |
| Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries(); |
| int charsetOffset = topDICT.get("charset").getOperands().get(0).intValue(); |
| |
| gidToSID = new LinkedHashMap<Integer, Integer>(); |
| |
| for (int gid : subsetGlyphs.keySet()) { |
| int sid = cffReader.getSIDFromGID(charsetOffset, gid); |
| //Check whether the SID falls into the standard string set |
| if (sid < NUM_STANDARD_STRINGS) { |
| gidToSID.put(subsetGlyphs.get(gid), sid); |
| if (mbFont != null) { |
| mbFont.mapUsedGlyphName(subsetGlyphs.get(gid), |
| CFFStandardString.getName(sid)); |
| } |
| } else { |
| int index = sid - NUM_STANDARD_STRINGS; |
| if (index <= cffReader.getStringIndex().getNumObjects()) { |
| if (mbFont != null) { |
| mbFont.mapUsedGlyphName(subsetGlyphs.get(gid), |
| new String(cffReader.getStringIndex().getValue(index))); |
| } |
| gidToSID.put(subsetGlyphs.get(gid), stringIndexData.size() + 391); |
| stringIndexData.add(cffReader.getStringIndex().getValue(index)); |
| } else { |
| if (mbFont != null) { |
| mbFont.mapUsedGlyphName(subsetGlyphs.get(gid), ".notdef"); |
| } |
| gidToSID.put(subsetGlyphs.get(gid), index); |
| } |
| } |
| } |
| //Write the String Index |
| writeIndex(stringIndexData); |
| } |
| |
| protected void createCharStringDataCID() throws IOException { |
| CFFIndexData charStringsIndex = cffReader.getCharStringIndex(); |
| |
| FDSelect fontDictionary = cffReader.getFDSelect(); |
| if (fontDictionary instanceof Format0FDSelect) { |
| throw new UnsupportedOperationException("OTF CFF CID Format0 currently not implemented"); |
| } else if (fontDictionary instanceof Format3FDSelect) { |
| Format3FDSelect fdSelect = (Format3FDSelect)fontDictionary; |
| Map<Integer, Integer> subsetGroups = new HashMap<Integer, Integer>(); |
| |
| List<Integer> uniqueGroups = new ArrayList<Integer>(); |
| for (int gid : subsetGlyphs.keySet()) { |
| Integer[] ranges = fdSelect.getRanges().keySet().toArray(new Integer[0]); |
| for (int i = 0; i < ranges.length; i++) { |
| int nextRange = -1; |
| if (i < ranges.length - 1) { |
| nextRange = ranges[i + 1]; |
| } else { |
| nextRange = fdSelect.getSentinelGID(); |
| } |
| if (gid >= ranges[i] && gid < nextRange) { |
| subsetGroups.put(gid, fdSelect.getRanges().get(ranges[i])); |
| if (!uniqueGroups.contains(fdSelect.getRanges().get(ranges[i]))) { |
| uniqueGroups.add(fdSelect.getRanges().get(ranges[i])); |
| } |
| } |
| } |
| } |
| |
| //Prepare resources |
| globalIndexSubr = cffReader.getGlobalIndexSubr(); |
| |
| //Create the new char string index |
| subsetCharStringsIndex = new ArrayList<byte[]>(); |
| |
| globalUniques = new ArrayList<Integer>(); |
| |
| subsetFDSelect = new LinkedHashMap<Integer, FDIndexReference>(); |
| |
| List<List<Integer>> foundLocalUniques = new ArrayList<List<Integer>>(); |
| for (int i = 0; i < uniqueGroups.size(); i++) { |
| foundLocalUniques.add(new ArrayList<Integer>()); |
| } |
| for (int gid : subsetGlyphs.keySet()) { |
| int group = subsetGroups.get(gid); |
| localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData(); |
| localUniques = foundLocalUniques.get(uniqueGroups.indexOf(subsetGroups.get(gid))); |
| |
| FDIndexReference newFDReference = new FDIndexReference( |
| uniqueGroups.indexOf(subsetGroups.get(gid)), subsetGroups.get(gid)); |
| subsetFDSelect.put(subsetGlyphs.get(gid), newFDReference); |
| byte[] data = charStringsIndex.getValue(gid); |
| preScanForSubsetIndexSize(data); |
| } |
| |
| //Create the two lists which are to store the local and global subroutines |
| subsetGlobalIndexSubr = new ArrayList<byte[]>(); |
| |
| fdSubrs = new ArrayList<List<byte[]>>(); |
| subsetGlobalSubrCount = globalUniques.size(); |
| globalUniques.clear(); |
| localUniques = null; |
| |
| for (int l = 0; l < foundLocalUniques.size(); l++) { |
| fdSubrs.add(new ArrayList<byte[]>()); |
| } |
| List<List<Integer>> foundLocalUniquesB = new ArrayList<List<Integer>>(); |
| for (int k = 0; k < uniqueGroups.size(); k++) { |
| foundLocalUniquesB.add(new ArrayList<Integer>()); |
| } |
| for (Integer gid : subsetGlyphs.keySet()) { |
| int group = subsetGroups.get(gid); |
| localIndexSubr = cffReader.getFDFonts().get(group).getLocalSubrData(); |
| localUniques = foundLocalUniquesB.get(subsetFDSelect.get(subsetGlyphs.get(gid)).getNewFDIndex()); |
| byte[] data = charStringsIndex.getValue(gid); |
| subsetLocalIndexSubr = fdSubrs.get(subsetFDSelect.get(subsetGlyphs.get(gid)).getNewFDIndex()); |
| subsetLocalSubrCount = foundLocalUniques.get(subsetFDSelect.get(subsetGlyphs.get(gid)) |
| .getNewFDIndex()).size(); |
| data = readCharStringData(data, subsetLocalSubrCount); |
| subsetCharStringsIndex.add(data); |
| } |
| } |
| } |
| |
| protected void writeFDSelect() { |
| writeByte(0); //Format |
| for (Integer gid : subsetFDSelect.keySet()) { |
| writeByte(subsetFDSelect.get(gid).getNewFDIndex()); |
| } |
| } |
| |
| protected List<Integer> getUsedFDFonts() { |
| List<Integer> uniqueNewRefs = new ArrayList<Integer>(); |
| for (int gid : subsetFDSelect.keySet()) { |
| int fdIndex = subsetFDSelect.get(gid).getOldFDIndex(); |
| if (!uniqueNewRefs.contains(fdIndex)) { |
| uniqueNewRefs.add(fdIndex); |
| } |
| } |
| return uniqueNewRefs; |
| } |
| |
| protected List<Integer> writeCIDDictsAndSubrs(List<Integer> uniqueNewRefs) |
| throws IOException { |
| List<Integer> privateDictOffsets = new ArrayList<Integer>(); |
| List<FontDict> fdFonts = cffReader.getFDFonts(); |
| for (int i = 0; i < uniqueNewRefs.size(); i++) { |
| FontDict curFDFont = fdFonts.get(uniqueNewRefs.get(i)); |
| HashMap<String, DICTEntry> fdPrivateDict = cffReader.parseDictData( |
| curFDFont.getPrivateDictData()); |
| int privateDictOffset = currentPos; |
| privateDictOffsets.add(privateDictOffset); |
| byte[] fdPrivateDictByteData = curFDFont.getPrivateDictData(); |
| if (fdPrivateDict.get("Subrs") != null) { |
| fdPrivateDictByteData = updateOffset(fdPrivateDictByteData, fdPrivateDict.get("Subrs").getOffset(), |
| fdPrivateDict.get("Subrs").getOperandLength(), |
| fdPrivateDictByteData.length); |
| } |
| writeBytes(fdPrivateDictByteData); |
| writeIndex(fdSubrs.get(i)); |
| } |
| return privateDictOffsets; |
| } |
| |
| protected int writeFDArray(List<Integer> uniqueNewRefs, List<Integer> privateDictOffsets, |
| List<Integer> fontNameSIDs) |
| throws IOException { |
| int offset = currentPos; |
| List<FontDict> fdFonts = cffReader.getFDFonts(); |
| |
| writeCard16(uniqueNewRefs.size()); |
| writeByte(1); //Offset size |
| writeByte(1); //First offset |
| |
| int count = 1; |
| for (int i = 0; i < uniqueNewRefs.size(); i++) { |
| FontDict fdFont = fdFonts.get(uniqueNewRefs.get(i)); |
| count += fdFont.getByteData().length; |
| writeByte(count); |
| } |
| |
| for (int i = 0; i < uniqueNewRefs.size(); i++) { |
| FontDict fdFont = fdFonts.get(uniqueNewRefs.get(i)); |
| byte[] fdFontByteData = fdFont.getByteData(); |
| Map<String, DICTEntry> fdFontDict = cffReader.parseDictData(fdFontByteData); |
| //Update the SID to the FontName |
| fdFontByteData = updateOffset(fdFontByteData, fdFontDict.get("FontName").getOffset() - 1, |
| fdFontDict.get("FontName").getOperandLengths().get(0), |
| fontNameSIDs.get(i)); |
| //Update the Private dict reference |
| fdFontByteData = updateOffset(fdFontByteData, fdFontDict.get("Private").getOffset() |
| + fdFontDict.get("Private").getOperandLengths().get(0), |
| fdFontDict.get("Private").getOperandLengths().get(1), |
| privateDictOffsets.get(i)); |
| writeBytes(fdFontByteData); |
| } |
| return offset; |
| } |
| |
| private class FDIndexReference { |
| private int newFDIndex; |
| private int oldFDIndex; |
| |
| public FDIndexReference(int newFDIndex, int oldFDIndex) { |
| this.newFDIndex = newFDIndex; |
| this.oldFDIndex = oldFDIndex; |
| } |
| |
| public int getNewFDIndex() { |
| return newFDIndex; |
| } |
| |
| public int getOldFDIndex() { |
| return oldFDIndex; |
| } |
| } |
| |
| private void createCharStringData() throws IOException { |
| Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries(); |
| |
| CFFIndexData charStringsIndex = cffReader.getCharStringIndex(); |
| |
| DICTEntry privateEntry = topDICT.get("Private"); |
| if (privateEntry != null) { |
| int privateOffset = privateEntry.getOperands().get(1).intValue(); |
| Map<String, DICTEntry> privateDICT = cffReader.getPrivateDict(privateEntry); |
| |
| if (privateDICT.get("Subrs") != null) { |
| int localSubrOffset = privateOffset + privateDICT.get("Subrs").getOperands().get(0).intValue(); |
| localIndexSubr = cffReader.readIndex(localSubrOffset); |
| } else { |
| localIndexSubr = cffReader.readIndex(null); |
| } |
| } |
| |
| globalIndexSubr = cffReader.getGlobalIndexSubr(); |
| |
| //Create the two lists which are to store the local and global subroutines |
| subsetLocalIndexSubr = new ArrayList<byte[]>(); |
| subsetGlobalIndexSubr = new ArrayList<byte[]>(); |
| |
| //Create the new char string index |
| subsetCharStringsIndex = new ArrayList<byte[]>(); |
| |
| localUniques = new ArrayList<Integer>(); |
| globalUniques = new ArrayList<Integer>(); |
| |
| for (int gid : subsetGlyphs.keySet()) { |
| byte[] data = charStringsIndex.getValue(gid); |
| preScanForSubsetIndexSize(data); |
| } |
| |
| //Store the size of each subset index and clear the unique arrays |
| subsetLocalSubrCount = localUniques.size(); |
| subsetGlobalSubrCount = globalUniques.size(); |
| localUniques.clear(); |
| globalUniques.clear(); |
| |
| for (int gid : subsetGlyphs.keySet()) { |
| byte[] data = charStringsIndex.getValue(gid); |
| //Retrieve modified char string data and fill local / global subroutine arrays |
| data = readCharStringData(data, subsetLocalSubrCount); |
| subsetCharStringsIndex.add(data); |
| } |
| } |
| |
| private void preScanForSubsetIndexSize(byte[] data) throws IOException { |
| boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0; |
| boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0; |
| BytesNumber operand = new BytesNumber(-1, -1); |
| for (int dataPos = 0; dataPos < data.length; dataPos++) { |
| int b0 = data[dataPos] & 0xff; |
| if (b0 == LOCAL_SUBROUTINE && hasLocalSubroutines) { |
| int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(), operand.getNumber()); |
| |
| if (!localUniques.contains(subrNumber) && subrNumber < localIndexSubr.getNumObjects()) { |
| localUniques.add(subrNumber); |
| byte[] subr = localIndexSubr.getValue(subrNumber); |
| preScanForSubsetIndexSize(subr); |
| } |
| operand.clearNumber(); |
| } else if (b0 == GLOBAL_SUBROUTINE && hasGlobalSubroutines) { |
| int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(), operand.getNumber()); |
| |
| if (!globalUniques.contains(subrNumber) && subrNumber < globalIndexSubr.getNumObjects()) { |
| globalUniques.add(subrNumber); |
| byte[] subr = globalIndexSubr.getValue(subrNumber); |
| preScanForSubsetIndexSize(subr); |
| } |
| operand.clearNumber(); |
| } else if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) { |
| operand.clearNumber(); |
| if (b0 == 19 || b0 == 20) { |
| dataPos += 1; |
| } |
| } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) { |
| operand = readNumber(b0, data, dataPos); |
| dataPos += operand.getNumBytes() - 1; |
| } |
| } |
| } |
| |
| private int getSubrNumber(int numSubroutines, int operand) { |
| int bias = getBias(numSubroutines); |
| return bias + operand; |
| } |
| |
| private byte[] readCharStringData(byte[] data, int subsetLocalSubrCount) throws IOException { |
| boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0; |
| boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0; |
| BytesNumber operand = new BytesNumber(-1, -1); |
| for (int dataPos = 0; dataPos < data.length; dataPos++) { |
| int b0 = data[dataPos] & 0xff; |
| if (b0 == 10 && hasLocalSubroutines) { |
| int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(), operand.getNumber()); |
| |
| int newRef = getNewRefForReference(subrNumber, localUniques, localIndexSubr, subsetLocalIndexSubr, |
| subsetLocalSubrCount); |
| |
| if (newRef != -1) { |
| byte[] newData = constructNewRefData(dataPos, data, operand, subsetLocalSubrCount, |
| newRef, new int[] {10}); |
| dataPos -= data.length - newData.length; |
| data = newData; |
| } |
| |
| operand.clearNumber(); |
| } else if (b0 == 29 && hasGlobalSubroutines) { |
| int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(), operand.getNumber()); |
| |
| int newRef = getNewRefForReference(subrNumber, globalUniques, globalIndexSubr, subsetGlobalIndexSubr, |
| subsetGlobalSubrCount); |
| |
| if (newRef != -1) { |
| byte[] newData = constructNewRefData(dataPos, data, operand, subsetGlobalSubrCount, |
| newRef, new int[] {29}); |
| dataPos -= (data.length - newData.length); |
| data = newData; |
| } |
| |
| operand.clearNumber(); |
| } else if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) { |
| operand.clearNumber(); |
| if (b0 == 19 || b0 == 20) { |
| dataPos += 1; |
| } |
| } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) { |
| operand = readNumber(b0, data, dataPos); |
| dataPos += operand.getNumBytes() - 1; |
| } |
| } |
| |
| //Return the data with the modified references to our arrays |
| return data; |
| } |
| |
| private int getNewRefForReference(int subrNumber, List<Integer> uniquesArray, |
| CFFIndexData indexSubr, List<byte[]> subsetIndexSubr, int subrCount) throws IOException { |
| int newRef = -1; |
| if (!uniquesArray.contains(subrNumber)) { |
| if (subrNumber < indexSubr.getNumObjects()) { |
| byte[] subr = indexSubr.getValue(subrNumber); |
| subr = readCharStringData(subr, subrCount); |
| if (!uniquesArray.contains(subrNumber)) { |
| uniquesArray.add(subrNumber); |
| subsetIndexSubr.add(subr); |
| newRef = subsetIndexSubr.size() - 1; |
| } else { |
| newRef = uniquesArray.indexOf(subrNumber); |
| } |
| } |
| } else { |
| newRef = uniquesArray.indexOf(subrNumber); |
| } |
| return newRef; |
| } |
| |
| private int getBias(int subrCount) { |
| if (subrCount < 1240) { |
| return 107; |
| } else if (subrCount < 33900) { |
| return 1131; |
| } else { |
| return 32768; |
| } |
| } |
| |
| private byte[] constructNewRefData(int curDataPos, byte[] currentData, BytesNumber operand, |
| int fullSubsetIndexSize, int curSubsetIndexSize, int[] operatorCode) { |
| //Create the new array with the modified reference |
| byte[] newData; |
| int startRef = curDataPos - operand.getNumBytes(); |
| int length = operand.getNumBytes() + 1; |
| byte[] preBytes = new byte[startRef]; |
| System.arraycopy(currentData, 0, preBytes, 0, startRef); |
| int newBias = getBias(fullSubsetIndexSize); |
| int newRef = curSubsetIndexSize - newBias; |
| byte[] newRefBytes = createNewRef(newRef, operatorCode, -1); |
| newData = concatArray(preBytes, newRefBytes); |
| byte[] postBytes = new byte[currentData.length - (startRef + length)]; |
| System.arraycopy(currentData, startRef + length, postBytes, 0, |
| currentData.length - (startRef + length)); |
| return concatArray(newData, postBytes); |
| } |
| |
| public static byte[] createNewRef(int newRef, int[] operatorCode, int forceLength) { |
| byte[] newRefBytes; |
| int sizeOfOperator = operatorCode.length; |
| if ((forceLength == -1 && newRef <= 107) || forceLength == 1) { |
| newRefBytes = new byte[1 + sizeOfOperator]; |
| //The index values are 0 indexed |
| newRefBytes[0] = (byte)(newRef + 139); |
| for (int i = 0; i < operatorCode.length; i++) { |
| newRefBytes[1 + i] = (byte)operatorCode[i]; |
| } |
| } else if ((forceLength == -1 && newRef <= 1131) || forceLength == 2) { |
| newRefBytes = new byte[2 + sizeOfOperator]; |
| if (newRef <= 363) { |
| newRefBytes[0] = (byte)247; |
| } else if (newRef <= 619) { |
| newRefBytes[0] = (byte)248; |
| } else if (newRef <= 875) { |
| newRefBytes[0] = (byte)249; |
| } else { |
| newRefBytes[0] = (byte)250; |
| } |
| newRefBytes[1] = (byte)(newRef - 108); |
| for (int i = 0; i < operatorCode.length; i++) { |
| newRefBytes[2 + i] = (byte)operatorCode[i]; |
| } |
| } else if ((forceLength == -1 && newRef <= 32767) || forceLength == 3) { |
| newRefBytes = new byte[3 + sizeOfOperator]; |
| newRefBytes[0] = 28; |
| newRefBytes[1] = (byte)(newRef >> 8); |
| newRefBytes[2] = (byte)newRef; |
| for (int i = 0; i < operatorCode.length; i++) { |
| newRefBytes[3 + i] = (byte)operatorCode[i]; |
| } |
| } else { |
| newRefBytes = new byte[5 + sizeOfOperator]; |
| newRefBytes[0] = 29; |
| newRefBytes[1] = (byte)(newRef >> 24); |
| newRefBytes[2] = (byte)(newRef >> 16); |
| newRefBytes[3] = (byte)(newRef >> 8); |
| newRefBytes[4] = (byte)newRef; |
| for (int i = 0; i < operatorCode.length; i++) { |
| newRefBytes[5 + i] = (byte)operatorCode[i]; |
| } |
| } |
| return newRefBytes; |
| } |
| |
| public static byte[] concatArray(byte[] a, byte[] b) { |
| int aLen = a.length; |
| int bLen = b.length; |
| byte[] c = new byte[aLen + bLen]; |
| System.arraycopy(a, 0, c, 0, aLen); |
| System.arraycopy(b, 0, c, aLen, bLen); |
| return c; |
| } |
| |
| protected int writeIndex(List<byte[]> dataArray) { |
| int hdrTotal = 3; |
| //2 byte number of items |
| this.writeCard16(dataArray.size()); |
| //Offset Size: 1 byte = 256, 2 bytes = 65536 etc. |
| int totLength = 0; |
| for (int i = 0; i < dataArray.size(); i++) { |
| totLength += dataArray.get(i).length; |
| } |
| int offSize = 1; |
| if (totLength <= (1 << 8)) { |
| offSize = 1; |
| } else if (totLength <= (1 << 16)) { |
| offSize = 2; |
| } else if (totLength <= (1 << 24)) { |
| offSize = 3; |
| } else { |
| offSize = 4; |
| } |
| this.writeByte(offSize); |
| //Count the first offset 1 |
| hdrTotal += offSize; |
| int total = 0; |
| for (int i = 0; i < dataArray.size(); i++) { |
| hdrTotal += offSize; |
| int length = dataArray.get(i).length; |
| switch (offSize) { |
| case 1: |
| if (i == 0) { |
| writeByte(1); |
| } |
| total += length; |
| writeByte(total + 1); |
| break; |
| case 2: |
| if (i == 0) { |
| writeCard16(1); |
| } |
| total += length; |
| writeCard16(total + 1); |
| break; |
| case 3: |
| if (i == 0) { |
| writeThreeByteNumber(1); |
| } |
| total += length; |
| writeThreeByteNumber(total + 1); |
| break; |
| case 4: |
| if (i == 0) { |
| writeULong(1); |
| } |
| total += length; |
| writeULong(total + 1); |
| break; |
| default: |
| throw new AssertionError("Offset Size was not an expected value."); |
| } |
| } |
| for (int i = 0; i < dataArray.size(); i++) { |
| writeBytes(dataArray.get(i)); |
| } |
| return hdrTotal + total; |
| } |
| |
| 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(); |
| } |
| } |
| |
| /** |
| * A class used to store the last number operand and also it's size in bytes |
| */ |
| static class BytesNumber { |
| private int number; |
| private int numBytes; |
| |
| public BytesNumber(int number, int numBytes) { |
| this.number = number; |
| this.numBytes = numBytes; |
| } |
| |
| public int getNumber() { |
| return this.number; |
| } |
| |
| public int getNumBytes() { |
| return this.numBytes; |
| } |
| |
| public void clearNumber() { |
| this.number = -1; |
| this.numBytes = -1; |
| } |
| |
| public String toString() { |
| return Integer.toString(number); |
| } |
| |
| @Override |
| public boolean equals(Object entry) { |
| assert entry instanceof BytesNumber; |
| BytesNumber bnEntry = (BytesNumber)entry; |
| return this.number == bnEntry.getNumber() |
| && this.numBytes == bnEntry.getNumBytes(); |
| } |
| |
| @Override |
| public int hashCode() { |
| int hash = 1; |
| hash = hash * 17 + number; |
| hash = hash * 31 + numBytes; |
| return hash; |
| } |
| } |
| |
| private void writeCharsetTable(boolean cidFont) throws IOException { |
| writeByte(0); |
| for (int gid : gidToSID.keySet()) { |
| if (cidFont && gid == 0) { |
| continue; |
| } |
| writeCard16((cidFont) ? gid : gidToSID.get(gid)); |
| } |
| } |
| |
| protected void writePrivateDict() throws IOException { |
| Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries(); |
| |
| DICTEntry privateEntry = topDICT.get("Private"); |
| if (privateEntry != null) { |
| writeBytes(cffReader.getPrivateDictBytes(privateEntry)); |
| } |
| } |
| |
| protected void updateOffsets(int topDictOffset, int charsetOffset, int charStringOffset, |
| int privateDictOffset, int localIndexOffset, int encodingOffset) |
| throws IOException { |
| Map<String, DICTEntry> topDICT = cffReader.getTopDictEntries(); |
| Map<String, DICTEntry> privateDICT = null; |
| |
| DICTEntry privateEntry = topDICT.get("Private"); |
| if (privateEntry != null) { |
| privateDICT = cffReader.getPrivateDict(privateEntry); |
| } |
| |
| int dataPos = 3 + (cffReader.getTopDictIndex().getOffSize() |
| * cffReader.getTopDictIndex().getOffsets().length); |
| int dataTopDictOffset = topDictOffset + dataPos; |
| |
| updateFixedOffsets(topDICT, dataTopDictOffset, charsetOffset, charStringOffset, encodingOffset); |
| |
| if (privateDICT != null) { |
| //Private index offset in the top dict |
| int oldPrivateOffset = dataTopDictOffset + privateEntry.getOffset(); |
| output = updateOffset(output, oldPrivateOffset + privateEntry.getOperandLengths().get(0), |
| privateEntry.getOperandLengths().get(1), privateDictOffset); |
| |
| //Update the local subroutine index offset in the private dict |
| DICTEntry subroutines = privateDICT.get("Subrs"); |
| if (subroutines != null) { |
| int oldLocalSubrOffset = privateDictOffset + subroutines.getOffset(); |
| //Value needs to be converted to -139 etc. |
| int encodeValue = 0; |
| if (subroutines.getOperandLength() == 1) { |
| encodeValue = 139; |
| } |
| output = updateOffset(output, oldLocalSubrOffset, subroutines.getOperandLength(), |
| (localIndexOffset - privateDictOffset) + encodeValue); |
| } |
| } |
| } |
| |
| protected void updateFixedOffsets(Map<String, DICTEntry> topDICT, int dataTopDictOffset, |
| int charsetOffset, int charStringOffset, int encodingOffset) { |
| //Charset offset in the top dict |
| DICTEntry charset = topDICT.get("charset"); |
| int oldCharsetOffset = dataTopDictOffset + charset.getOffset(); |
| output = updateOffset(output, oldCharsetOffset, charset.getOperandLength(), charsetOffset); |
| |
| //Char string index offset in the private dict |
| DICTEntry charString = topDICT.get("CharStrings"); |
| int oldCharStringOffset = dataTopDictOffset + charString.getOffset(); |
| output = updateOffset(output, oldCharStringOffset, charString.getOperandLength(), charStringOffset); |
| |
| DICTEntry encodingEntry = topDICT.get("Encoding"); |
| if (encodingEntry != null && encodingEntry.getOperands().get(0).intValue() != 0 |
| && encodingEntry.getOperands().get(0).intValue() != 1) { |
| int oldEncodingOffset = dataTopDictOffset + encodingEntry.getOffset(); |
| output = updateOffset(output, oldEncodingOffset, encodingEntry.getOperandLength(), encodingOffset); |
| } |
| } |
| |
| protected void updateCIDOffsets(int topDictDataOffset, int fdArrayOffset, int fdSelectOffset, |
| int charsetOffset, int charStringOffset, int encodingOffset) { |
| LinkedHashMap<String, DICTEntry> topDict = cffReader.getTopDictEntries(); |
| |
| DICTEntry fdArrayEntry = topDict.get("FDArray"); |
| if (fdArrayEntry != null) { |
| output = updateOffset(output, topDictDataOffset + fdArrayEntry.getOffset() - 1, |
| fdArrayEntry.getOperandLength(), fdArrayOffset); |
| } |
| |
| DICTEntry fdSelect = topDict.get("FDSelect"); |
| if (fdSelect != null) { |
| output = updateOffset(output, topDictDataOffset + fdSelect.getOffset() - 1, |
| fdSelect.getOperandLength(), fdSelectOffset); |
| } |
| |
| updateFixedOffsets(topDict, topDictDataOffset, charsetOffset, charStringOffset, encodingOffset); |
| } |
| |
| protected byte[] updateOffset(byte[] out, int position, int length, int replacement) { |
| switch (length) { |
| case 1: |
| out[position] = (byte)(replacement & 0xFF); |
| break; |
| case 2: |
| if (replacement <= 363) { |
| out[position] = (byte)247; |
| } else if (replacement <= 619) { |
| out[position] = (byte)248; |
| } else if (replacement <= 875) { |
| out[position] = (byte)249; |
| } else { |
| out[position] = (byte)250; |
| } |
| out[position + 1] = (byte)(replacement - 108); |
| break; |
| case 3: |
| out[position] = (byte)28; |
| out[position + 1] = (byte)((replacement >> 8) & 0xFF); |
| out[position + 2] = (byte)(replacement & 0xFF); |
| break; |
| case 5: |
| out[position] = (byte)29; |
| out[position + 1] = (byte)((replacement >> 24) & 0xFF); |
| out[position + 2] = (byte)((replacement >> 16) & 0xFF); |
| out[position + 3] = (byte)((replacement >> 8) & 0xFF); |
| out[position + 4] = (byte)(replacement & 0xFF); |
| break; |
| default: |
| } |
| return out; |
| } |
| |
| /** |
| * Appends a byte to the output array, |
| * updates currentPost but not realSize |
| */ |
| protected void writeByte(int b) { |
| output[currentPos++] = (byte)b; |
| realSize++; |
| } |
| |
| /** |
| * Appends a USHORT to the output array, |
| * updates currentPost but not realSize |
| */ |
| protected void writeCard16(int s) { |
| byte b1 = (byte)((s >> 8) & 0xff); |
| byte b2 = (byte)(s & 0xff); |
| writeByte(b1); |
| writeByte(b2); |
| } |
| |
| private void writeThreeByteNumber(int s) { |
| byte b1 = (byte)((s >> 16) & 0xFF); |
| byte b2 = (byte)((s >> 8) & 0xFF); |
| byte b3 = (byte)(s & 0xFF); |
| writeByte(b1); |
| writeByte(b2); |
| writeByte(b3); |
| } |
| |
| /** |
| * Appends a ULONG to the output array, |
| * at the given position |
| */ |
| private void writeULong(int s) { |
| byte b1 = (byte)((s >> 24) & 0xff); |
| byte b2 = (byte)((s >> 16) & 0xff); |
| byte b3 = (byte)((s >> 8) & 0xff); |
| byte b4 = (byte)(s & 0xff); |
| writeByte(b1); |
| writeByte(b2); |
| writeByte(b3); |
| writeByte(b4); |
| } |
| |
| /** |
| * Returns a subset of the fonts (readFont() MUST be called first in order to create the |
| * subset). |
| * @return byte array |
| */ |
| public byte[] getFontSubset() { |
| byte[] ret = new byte[realSize]; |
| System.arraycopy(output, 0, ret, 0, realSize); |
| return ret; |
| } |
| |
| /** |
| * Returns the parsed CFF data for the original font. |
| * @return The CFFDataReader contaiing the parsed data |
| */ |
| public CFFDataReader getCFFReader() { |
| return cffReader; |
| } |
| } |