| /* |
| * 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. |
| */ |
| package org.apache.fop.render.pdf.pdfbox; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.TreeMap; |
| |
| import org.apache.fop.fonts.truetype.FontFileReader; |
| import org.apache.fop.fonts.truetype.GlyfTable; |
| import org.apache.fop.fonts.truetype.OFDirTabEntry; |
| import org.apache.fop.fonts.truetype.OFMtxEntry; |
| import org.apache.fop.fonts.truetype.OFTableName; |
| import org.apache.fop.fonts.truetype.TTFSubSetFile; |
| |
| public class MergeTTFonts extends TTFSubSetFile implements MergeFonts { |
| private Map<Integer, Glyph> added = new TreeMap<Integer, Glyph>(); |
| private int origIndexesLen; |
| private int size; |
| protected MaximumProfileTable maxp = new MaximumProfileTable(); |
| private Integer nhmtxDiff = null; |
| private List<Cmap> cmap; |
| |
| static class Glyph { |
| final byte[] data; |
| final OFMtxEntry mtx; |
| Glyph(byte[] d, OFMtxEntry m) { |
| data = d; |
| mtx = m; |
| } |
| } |
| |
| public MergeTTFonts(List<Cmap> cmap) { |
| this.cmap = cmap; |
| } |
| |
| /** |
| * Create the glyf table and fill in loca table |
| * @param glyphs map of glyphs |
| * @param in fontfile |
| * @throws IOException on error |
| */ |
| private void readGlyf(Map<Integer, Integer> glyphs, FontFileReader in) throws IOException { |
| OFDirTabEntry entry = dirTabs.get(OFTableName.GLYF); |
| if (entry != null) { |
| int[] origIndexes = buildSubsetIndexToOrigIndexMap(glyphs); |
| for (int i = 0; i < origIndexes.length; i++) { |
| int nextOffset = 0; |
| int origGlyphIndex = origIndexes[i]; |
| if (origGlyphIndex >= (mtxTab.length - 1)) { |
| nextOffset = (int)lastLoca; |
| } else { |
| nextOffset = (int)mtxTab[origGlyphIndex + 1].getOffset(); |
| } |
| int glyphOffset = (int)mtxTab[origGlyphIndex].getOffset(); |
| int glyphLength = nextOffset - glyphOffset; |
| if (glyphLength < 0) { |
| continue; |
| } |
| byte[] glyphData = in.getBytes( |
| (int)entry.getOffset() + glyphOffset, |
| glyphLength); |
| |
| Glyph g = new Glyph(glyphData, mtxTab[origGlyphIndex]); |
| if (!cid && (origIndexesLen == 0 || (glyphLength > 0 && i > 0))) { |
| added.put(i, g); |
| } else if (cid) { |
| added.put(i + origIndexesLen, g); |
| } |
| } |
| if (!cid) { |
| origIndexesLen = origIndexes.length; |
| } else { |
| origIndexesLen += origIndexes.length; |
| } |
| } else { |
| throw new IOException("Can't find glyf table"); |
| } |
| } |
| |
| private void createGlyf() throws IOException { |
| OFDirTabEntry entry = dirTabs.get(OFTableName.GLYF); |
| int size = 0; |
| int startPos = 0; |
| int endOffset = 0; // Store this as the last loca |
| |
| if (entry != null) { |
| pad4(); |
| startPos = currentPos; |
| /* Loca table must be in order by glyph index, so build |
| * an array first and then write the glyph info and |
| * location offset. |
| */ |
| glyphOffsets = new int[origIndexesLen]; |
| for (Map.Entry<Integer, Glyph> gly : added.entrySet()) { |
| byte[] glyphData = gly.getValue().data; |
| int glyphLength = glyphData.length; |
| int i = gly.getKey(); |
| if (i >= origIndexesLen) { |
| continue; |
| } |
| int endOffset1 = endOffset; |
| // Copy glyph |
| writeBytes(glyphData); |
| // Update loca table |
| if (cid || locaFormat == 1) { |
| writeULong(locaOffset + i * 4, currentPos - startPos); |
| } |
| if ((currentPos - startPos + glyphLength) > endOffset1) { |
| endOffset1 = currentPos - startPos + glyphLength; |
| } |
| |
| // Store the glyph boundary positions relative to the start of the font |
| glyphOffsets[i] = currentPos; |
| currentPos += glyphLength; |
| realSize += glyphLength; |
| |
| endOffset = endOffset1; |
| } |
| |
| size = currentPos - startPos; |
| |
| currentPos += 12; |
| realSize += 12; |
| updateCheckSum(startPos, size + 12, OFTableName.GLYF); |
| |
| // Update loca checksum and last loca index |
| if (cid || locaFormat == 1) { |
| writeULong(locaOffset + added.size() * 4, endOffset); |
| } |
| int locaSize = added.size() * 4 + 4; |
| int checksum = getCheckSum(output, locaOffset, locaSize); |
| writeULong(offsets.get(OFTableName.LOCA), checksum); |
| int padSize = (locaOffset + locaSize) % 4; |
| newDirTabs.put(OFTableName.LOCA, |
| new OFDirTabEntry(locaOffset, locaSize + padSize)); |
| |
| if (!cid && locaFormat == 0) { |
| int i = 0; |
| int offset = 0; |
| for (Glyph e : added.values()) { |
| writeUShort(locaOffset + i * 2, offset / 2); |
| offset += e.data.length; |
| i++; |
| } |
| writeUShort(locaOffset + i * 2, offset / 2); |
| } |
| } else { |
| throw new IOException("Can't find glyf table"); |
| } |
| } |
| |
| /** |
| * Create the hmtx table by copying metrics from original |
| * font to subset font. The glyphs Map contains an |
| * Integer key and Integer value that maps the original |
| * metric (key) to the subset metric (value) |
| * @throws IOException on error |
| */ |
| protected void createHmtx() throws IOException { |
| OFTableName hmtx = OFTableName.HMTX; |
| OFDirTabEntry entry = dirTabs.get(hmtx); |
| if (entry != null) { |
| pad4(); |
| // int offset = (int)entry.offset; |
| |
| int longHorMetricSize = added.size() * 2; |
| int leftSideBearingSize = added.size() * 2; |
| int hmtxSize = longHorMetricSize + leftSideBearingSize; |
| |
| for (Map.Entry<Integer, Glyph> e : added.entrySet()) { |
| Integer subsetIndex = e.getKey(); |
| OFMtxEntry mtx = e.getValue().mtx; |
| writeUShort(currentPos + subsetIndex * 4, |
| mtx.getWx()); |
| writeUShort(currentPos + subsetIndex * 4 + 2, |
| mtx.getLsb()); |
| } |
| |
| updateCheckSum(currentPos, hmtxSize, hmtx); |
| currentPos += hmtxSize; |
| realSize += hmtxSize; |
| } else { |
| throw new IOException("Can't find hmtx table"); |
| } |
| } |
| |
| /** |
| * Returns a subset of the original font. |
| * |
| * @param is font file |
| * @param name name |
| * @param fontContainer fontContainer |
| * @param subsetGlyphs Map of glyphs (glyphs has old index as (Integer) key and |
| * new index as (Integer) value) |
| * @param cid is cid |
| * @throws IOException in case of an I/O problem |
| */ |
| public void readFont(InputStream is, String name, FontContainer fontContainer, |
| Map<Integer, Integer> subsetGlyphs, boolean cid) throws IOException { |
| this.cid = cid; |
| if (subsetGlyphs.isEmpty()) { |
| return; |
| } |
| this.fontFile = new FontFileReader(is); |
| size += fontFile.getAllBytes().length; |
| |
| readDirTabs(); |
| readFontHeader(); |
| getNumGlyphs(); |
| readHorizontalHeader(); |
| readHorizontalMetrics(); |
| readIndexToLocation(); |
| int sgsize = subsetGlyphs.size(); |
| if (!cid && subsetGlyphs.size() <= 1) { |
| for (int i = 0; i < mtxTab.length; i++) { |
| subsetGlyphs.put(i, i); |
| } |
| } |
| scanGlyphs(fontFile, subsetGlyphs); |
| readGlyf(subsetGlyphs, fontFile); |
| if (nhmtxDiff == null) { |
| nhmtxDiff = sgsize - nhmtx; |
| if (nhmtxDiff < 0) { |
| nhmtxDiff = 0; |
| } |
| } |
| } |
| |
| protected void scanGlyphs(FontFileReader in, Map<Integer, Integer> subsetGlyphs) throws IOException { |
| OFDirTabEntry glyfTableInfo = dirTabs.get(OFTableName.GLYF); |
| if (glyfTableInfo == null) { |
| throw new IOException("Glyf table could not be found"); |
| } |
| new MergeGlyfTable(in, mtxTab, glyfTableInfo, subsetGlyphs); |
| } |
| |
| static class MergeGlyfTable extends GlyfTable { |
| public MergeGlyfTable(FontFileReader in, OFMtxEntry[] metrics, OFDirTabEntry dirTableEntry, |
| Map<Integer, Integer> glyphs) throws IOException { |
| super(in, metrics, dirTableEntry, glyphs); |
| populateGlyphsWithComposites(); |
| } |
| |
| @Override |
| protected void addAllComposedGlyphsToSubset() { |
| int newIndex = -1; |
| for (int v : subset.values()) { |
| if (v > newIndex) { |
| newIndex = v; |
| } |
| } |
| for (int composedGlyph : composedGlyphs) { |
| subset.put(composedGlyph, ++newIndex); |
| } |
| } |
| } |
| |
| public byte[] getMergedFontSubset() throws IOException { |
| output = new byte[size * 2]; |
| createDirectory(); // Create the TrueType header and directory |
| int sgsize = added.size(); |
| if (!cid) { |
| writeCMAP(cmap); |
| // copyTable(fontFile, OFTableName.CMAP); |
| } |
| createHmtx(); // Create hmtx table |
| createLoca(sgsize); // create empty loca table |
| createHead(fontFile); |
| createOS2(fontFile); // copy the OS/2 table |
| if (!cid) { |
| createHhea(fontFile, sgsize - nhmtxDiff); // Create the hhea table |
| } else { |
| createHhea(fontFile, sgsize); // Create the hhea table |
| } |
| if (maxp.getVersion() == 0) { |
| createMaxp(fontFile, sgsize); // copy the maxp table |
| } else { |
| writeMaxp(); |
| } |
| createCvt(fontFile); // copy the cvt table |
| createFpgm(fontFile); // copy fpgm table |
| createPost(fontFile); // copy the post table |
| createPrep(fontFile); // copy prep table |
| createName(fontFile); // copy the name table |
| createGlyf(); //create glyf table and update loca table |
| pad4(); |
| createCheckSumAdjustment(); |
| return getFontSubset(); |
| } |
| |
| private void writeMaxp() { |
| int checksum = currentPos; |
| pad4(); |
| int startPos = currentPos; |
| writeUShort((int) maxp.getVersion()); //version |
| writeUShort(0); |
| writeUShort(added.size()); //numGlyphs |
| writeUShort(maxp.getMaxPoints()); //maxPoints |
| writeUShort(maxp.getMaxContours()); //maxContours |
| writeUShort(maxp.getMaxCompositePoints()); //maxCompositePoints |
| writeUShort(maxp.getMaxCompositeContours()); //maxCompositeContours |
| writeUShort(maxp.getMaxZones()); //maxZones |
| writeUShort(maxp.getMaxTwilightPoints()); //maxTwilightPoints |
| writeUShort(maxp.getMaxStorage()); //maxStorage |
| writeUShort(maxp.getMaxFunctionDefs()); //maxFunctionDefs |
| writeUShort(maxp.getMaxInstructionDefs()); //maxInstructionDefs |
| writeUShort(maxp.getMaxStackElements()); //maxStackElements |
| writeUShort(maxp.getMaxSizeOfInstructions()); //maxSizeOfInstructions |
| writeUShort(maxp.getMaxComponentElements()); //maxComponentElements |
| writeUShort(maxp.getMaxComponentDepth()); //maxComponentDepth |
| updateCheckSum(checksum, currentPos - startPos, OFTableName.MAXP); |
| realSize += currentPos - startPos; |
| } |
| |
| private void writeCMAP(List<Cmap> cmaps) { |
| mergeUniCmap(cmaps); |
| |
| int checksum = currentPos; |
| pad4(); |
| int cmapPos = currentPos; |
| writeUShort(0); //version |
| writeUShort(cmaps.size()); //number of tables |
| |
| int tablesSize = 8 * cmaps.size(); |
| for (int i = 0; i < cmaps.size(); i++) { |
| Cmap cmap = cmaps.get(i); |
| writeUShort(cmap.platformId); //platformid |
| writeUShort(cmap.platformEncodingId); //platformEncodingId |
| writeULong(currentPos, 4 + tablesSize + getCmapOffset(cmaps, i)); //subTableOffset |
| currentPos += 4; |
| } |
| |
| for (Cmap cmap : cmaps) { |
| writeUShort(12); //subtableFormat |
| writeUShort(0); |
| writeULong(currentPos, (cmap.glyphIdToCharacterCode.size() * 12) + 16); |
| currentPos += 4; |
| writeULong(currentPos, 0); |
| currentPos += 4; |
| writeULong(currentPos, cmap.glyphIdToCharacterCode.size()); |
| currentPos += 4; |
| |
| for (Map.Entry<Integer, Integer> g : cmap.glyphIdToCharacterCode.entrySet()) { |
| writeULong(currentPos, g.getKey()); |
| currentPos += 4; |
| writeULong(currentPos, g.getKey()); |
| currentPos += 4; |
| writeULong(currentPos, g.getValue()); |
| currentPos += 4; |
| } |
| } |
| |
| // writeUShort(6); //subtableFormat |
| // int firstCode = -1; |
| // int lastCode = -1; |
| // List<Integer> codes = new ArrayList<Integer>(); |
| // for (Map.Entry<Integer, Integer> g : glyphIdToCharacterCode.entrySet()) { |
| // if (firstCode < 0) { |
| // firstCode = g.getKey(); |
| // } |
| // while (lastCode > 0 && lastCode + 1 < g.getKey()) { |
| // codes.add(0); |
| // lastCode++; |
| // } |
| // codes.add(g.getValue()); |
| // |
| // lastCode = g.getKey(); |
| // } |
| // |
| // writeUShort((codes.size() * 2) + 6); //length |
| // writeUShort(0); //version |
| // |
| // writeUShort(firstCode); //firstCode |
| // writeUShort(codes.size()); //entryCount |
| // |
| // for (int i : codes) { |
| // writeUShort(i); |
| // } |
| |
| updateCheckSum(checksum, currentPos - cmapPos, OFTableName.CMAP); |
| realSize += currentPos - cmapPos; |
| } |
| |
| private void mergeUniCmap(List<Cmap> cmaps) { |
| Cmap uniCmap = null; |
| for (Cmap cmap : cmaps) { |
| if (cmap.platformId == 3 && cmap.platformEncodingId == 1) { |
| uniCmap = cmap; |
| } |
| } |
| if (uniCmap != null) { |
| for (Cmap cmap : cmaps) { |
| uniCmap.glyphIdToCharacterCode.putAll(cmap.glyphIdToCharacterCode); |
| } |
| } |
| } |
| |
| private int getCmapOffset(List<Cmap> cmaps, int index) { |
| int result = 0; |
| for (int i = 0; i < index; i++) { |
| Cmap curCmap = cmaps.get(i); |
| result += (curCmap.glyphIdToCharacterCode.size() * 12) + 16; |
| } |
| return result; |
| } |
| |
| /** |
| * Get index from starting at lowest glyph position |
| * @param glyphs map |
| * @return index map |
| */ |
| protected int[] buildSubsetIndexToOrigIndexMap(Map<Integer, Integer> glyphs) { |
| int[] origIndexes = new int[glyphs.size()]; |
| int minIndex = Integer.MAX_VALUE; |
| for (int glyph : glyphs.values()) { |
| if (minIndex > glyph) { |
| minIndex = glyph; |
| } |
| } |
| for (Map.Entry<Integer, Integer> glyph : glyphs.entrySet()) { |
| int origIndex = glyph.getKey(); |
| int subsetIndex = glyph.getValue() - minIndex; |
| origIndexes[subsetIndex] = origIndex; |
| } |
| return origIndexes; |
| } |
| |
| public static class Cmap { |
| int platformId; |
| int platformEncodingId; |
| Map<Integer, Integer> glyphIdToCharacterCode = new TreeMap<Integer, Integer>(); |
| |
| public Cmap(int platformID, int platformEncodingID) { |
| this.platformId = platformID; |
| this.platformEncodingId = platformEncodingID; |
| } |
| } |
| } |