Java tutorial
/* * 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.fontbox.ttf; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import org.apache.fontbox.encoding.Encoding; import org.apache.fontbox.encoding.MacRomanEncoding; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * A font, which is comprised of a subset of characters of a TrueType font. * Based on code developed by Wolfgang Glas * http://svn.clazzes.org/svn/sketch/trunk/pdf/pdf-entities/src/main/java/org/clazzes/sketch/pdf/entities/impl/TTFSubFont.java */ public class TTFSubFont { private static final Log LOG = LogFactory.getLog(TTFSubFont.class); private static final byte[] PAD_BUF = new byte[] { 0, 0, 0 }; private final TrueTypeFont baseTTF; private final String nameSuffix; private final CMAPEncodingEntry baseCmap; // A map of unicode char codes to glyph IDs of the original font. private final SortedMap<Integer, Integer> characters; // A sorted version of this set will comprise the generated glyph IDs // for the written truetype font. private final SortedSet<Integer> glyphIds; /** * Constructs a subfont based on the given font using the given suffix. * * @param baseFont the base font of the subfont * @param suffix suffix used for the naming * */ public TTFSubFont(TrueTypeFont baseFont, String suffix) { baseTTF = baseFont; nameSuffix = suffix; characters = new TreeMap<Integer, Integer>(); glyphIds = new TreeSet<Integer>(); CMAPEncodingEntry[] cmaps = this.baseTTF.getCMAP().getCmaps(); CMAPEncodingEntry unicodeCmap = null; for (CMAPEncodingEntry cmap : cmaps) { // take first unicode map. if (cmap.getPlatformId() == 0 || (cmap.getPlatformId() == 3 && cmap.getPlatformEncodingId() == 1)) { unicodeCmap = cmap; break; } } baseCmap = unicodeCmap; // add notdef character. addCharCode(0); } /** * Add the given charcode to the subfpont. * * @param charCode the charCode to be added * */ public void addCharCode(int charCode) { Integer gid = Integer.valueOf(baseCmap.getGlyphId(charCode)); if (charCode == 0 || gid != 0) { characters.put(charCode, gid); glyphIds.add(gid); } } private static int log2i(int i) { int ret = -1; if ((i & 0xffff0000) != 0) { i >>>= 16; ret += 16; } if ((i & 0xff00) != 0) { i >>>= 8; ret += 8; } if ((i & 0xf0) != 0) { i >>>= 4; ret += 4; } if ((i & 0xc) != 0) { i >>>= 2; ret += 2; } if ((i & 0x2) != 0) { i >>>= 1; ++ret; } if (i != 0) { ++ret; } return ret; } private static long buildUint32(int high, int low) { return ((((long) high) & 0xffffL) << 16) | (((long) low) & 0xffffL); } private static long buildUint32(byte[] bytes) { return ((((long) bytes[0]) & 0xffL) << 24) | ((((long) bytes[1]) & 0xffL) << 16) | ((((long) bytes[2]) & 0xffL) << 8) | (((long) bytes[3]) & 0xffL); } /** * @param dos The data output stream. * @param nTables The number of table. * @return The file offset of the first TTF table to write. * @throws IOException Upon errors. */ private static long writeFileHeader(DataOutputStream dos, int nTables) throws IOException { dos.writeInt(0x00010000); dos.writeShort(nTables); int mask = Integer.highestOneBit(nTables); int searchRange = mask * 16; dos.writeShort(searchRange); int entrySelector = log2i(mask); dos.writeShort(entrySelector); // numTables * 16 - searchRange int last = 16 * nTables - searchRange; dos.writeShort(last); return 0x00010000L + buildUint32(nTables, searchRange) + buildUint32(entrySelector, last); } private static long writeTableHeader(DataOutputStream dos, String tag, long offset, byte[] bytes) throws IOException { int n = bytes.length; int nup; long checksum = 0L; for (nup = 0; nup < n; ++nup) { checksum += (((long) bytes[nup]) & 0xffL) << (24 - (nup % 4) * 8); } checksum &= 0xffffffffL; LOG.debug(String.format("Writing table header [%s,%08x,%08x,%08x]", tag, checksum, offset, bytes.length)); byte[] tagbytes = tag.getBytes("US-ASCII"); dos.write(tagbytes, 0, 4); dos.writeInt((int) checksum); dos.writeInt((int) offset); dos.writeInt(bytes.length); // account for the checksum twice, one time for the header field, on time for the content itself. return buildUint32(tagbytes) + checksum + checksum + offset + bytes.length; } private static void writeTableBody(OutputStream os, byte[] bytes) throws IOException { int n = bytes.length; os.write(bytes); if ((n % 4) != 0) { os.write(PAD_BUF, 0, 4 - n % 4); } } private static void writeFixed(DataOutputStream dos, double f) throws IOException { double ip = Math.floor(f); double fp = (f - ip) * 65536.0; dos.writeShort((int) ip); dos.writeShort((int) fp); } private static void writeUint32(DataOutputStream dos, long l) throws IOException { dos.writeInt((int) l); } private static void writeUint16(DataOutputStream dos, int i) throws IOException { dos.writeShort(i); } private static void writeSint16(DataOutputStream dos, short i) throws IOException { dos.writeShort(i); } private static void writeUint8(DataOutputStream dos, int i) throws IOException { dos.writeByte(i); } private static void writeLongDateTime(DataOutputStream dos, Calendar calendar) throws IOException { // inverse operation of TTFDataStream.readInternationalDate() GregorianCalendar cal = new GregorianCalendar(1904, 0, 1); long millisFor1904 = cal.getTimeInMillis(); long secondsSince1904 = (calendar.getTimeInMillis() - millisFor1904) / 1000L; dos.writeLong(secondsSince1904); } private byte[] buildHeadTable() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); LOG.debug("Building table [head]..."); HeaderTable h = this.baseTTF.getHeader(); writeFixed(dos, h.getVersion()); writeFixed(dos, h.getFontRevision()); writeUint32(dos, 0 /* h.getCheckSumAdjustment() */); writeUint32(dos, h.getMagicNumber()); writeUint16(dos, h.getFlags()); writeUint16(dos, h.getUnitsPerEm()); writeLongDateTime(dos, h.getCreated()); writeLongDateTime(dos, h.getModified()); writeSint16(dos, h.getXMin()); writeSint16(dos, h.getYMin()); writeSint16(dos, h.getXMax()); writeSint16(dos, h.getYMax()); writeUint16(dos, h.getMacStyle()); writeUint16(dos, h.getLowestRecPPEM()); writeSint16(dos, h.getFontDirectionHint()); // force long format of 'loca' table. writeSint16(dos, (short) 1 /* h.getIndexToLocFormat() */); writeSint16(dos, h.getGlyphDataFormat()); dos.flush(); LOG.debug("Finished table [head]."); return bos.toByteArray(); } private byte[] buildHheaTable() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); LOG.debug("Building table [hhea]..."); HorizontalHeaderTable h = this.baseTTF.getHorizontalHeader(); writeFixed(dos, h.getVersion()); writeSint16(dos, h.getAscender()); writeSint16(dos, h.getDescender()); writeSint16(dos, h.getLineGap()); writeUint16(dos, h.getAdvanceWidthMax()); writeSint16(dos, h.getMinLeftSideBearing()); writeSint16(dos, h.getMinRightSideBearing()); writeSint16(dos, h.getXMaxExtent()); writeSint16(dos, h.getCaretSlopeRise()); writeSint16(dos, h.getCaretSlopeRun()); writeSint16(dos, h.getReserved1()); // caretOffset writeSint16(dos, h.getReserved2()); writeSint16(dos, h.getReserved3()); writeSint16(dos, h.getReserved4()); writeSint16(dos, h.getReserved5()); writeSint16(dos, h.getMetricDataFormat()); writeUint16(dos, glyphIds.subSet(0, h.getNumberOfHMetrics()).size()); dos.flush(); LOG.debug("Finished table [hhea]."); return bos.toByteArray(); } private static boolean replicateNameRecord(NameRecord nr) { return nr.getPlatformId() == NameRecord.PLATFORM_WINDOWS && nr.getPlatformEncodingId() == NameRecord.PLATFORM_ENCODING_WINDOWS_UNICODE && nr.getLanguageId() == 0 && nr.getNameId() >= 0 && nr.getNameId() < 7; } private byte[] buildNameTable() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); LOG.debug("Building table [name]..."); NamingTable n = this.baseTTF.getNaming(); List<NameRecord> nameRecords = null; if (n != null) { nameRecords = n.getNameRecords(); } else { // sometimes there is no naming table in an embedded subfonts // create some dummies nameRecords = new ArrayList<NameRecord>(); NameRecord nr = new NameRecord(); nr.setPlatformId(NameRecord.PLATFORM_WINDOWS); nr.setPlatformEncodingId(NameRecord.PLATFORM_ENCODING_WINDOWS_UNICODE); nr.setLanguageId(0); nr.setNameId(NameRecord.NAME_FONT_FAMILY_NAME); nr.setString("PDFBox-Dummy-Familyname"); nameRecords.add(nr); nr = new NameRecord(); nr.setPlatformId(NameRecord.PLATFORM_WINDOWS); nr.setPlatformEncodingId(NameRecord.PLATFORM_ENCODING_WINDOWS_UNICODE); nr.setLanguageId(0); nr.setNameId(NameRecord.NAME_FULL_FONT_NAME); nr.setString("PDFBox-Dummy-Fullname"); nameRecords.add(nr); } int numberOfRecords = nameRecords.size(); int nrep = 0; for (int i = 0; i < numberOfRecords; ++i) { NameRecord nr = nameRecords.get(i); if (replicateNameRecord(nr)) { LOG.debug("Writing name record [" + nr.getNameId() + "], [" + nr.getString() + "],"); ++nrep; } } writeUint16(dos, 0); writeUint16(dos, nrep); writeUint16(dos, 2 * 3 + (2 * 6) * nrep); byte[][] names = new byte[nrep][]; int j = 0; for (int i = 0; i < numberOfRecords; ++i) { NameRecord nr = nameRecords.get(i); if (replicateNameRecord(nr)) { int platform = nr.getPlatformId(); int encoding = nr.getPlatformEncodingId(); String charset = "ISO-8859-1"; if (platform == 3 && encoding == 1) { charset = "UTF-16BE"; } else if (platform == 2) { if (encoding == 0) { charset = "US-ASCII"; } else if (encoding == 1) { //not sure is this is correct?? charset = "UTF16-BE"; } else if (encoding == 2) { charset = "ISO-8859-1"; } } String value = nr.getString(); if (nr.getNameId() == 6 && this.nameSuffix != null) { value += this.nameSuffix; } names[j] = value.getBytes(charset); ++j; } } int offset = 0; j = 0; for (int i = 0; i < numberOfRecords; ++i) { NameRecord nr = nameRecords.get(i); if (replicateNameRecord(nr)) { writeUint16(dos, nr.getPlatformId()); writeUint16(dos, nr.getPlatformEncodingId()); writeUint16(dos, nr.getLanguageId()); writeUint16(dos, nr.getNameId()); writeUint16(dos, names[j].length); writeUint16(dos, offset); offset += names[j].length; ++j; } } for (int i = 0; i < nrep; ++i) { dos.write(names[i]); } dos.flush(); LOG.debug("Finished table [name]."); return bos.toByteArray(); } private byte[] buildMaxpTable() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); LOG.debug("Building table [maxp]..."); MaximumProfileTable p = this.baseTTF.getMaximumProfile(); writeFixed(dos, 1.0); writeUint16(dos, glyphIds.size()); writeUint16(dos, p.getMaxPoints()); writeUint16(dos, p.getMaxContours()); writeUint16(dos, p.getMaxCompositePoints()); writeUint16(dos, p.getMaxCompositeContours()); writeUint16(dos, p.getMaxZones()); writeUint16(dos, p.getMaxTwilightPoints()); writeUint16(dos, p.getMaxStorage()); writeUint16(dos, p.getMaxFunctionDefs()); writeUint16(dos, 0); writeUint16(dos, p.getMaxStackElements()); writeUint16(dos, 0); writeUint16(dos, p.getMaxComponentElements()); writeUint16(dos, p.getMaxComponentDepth()); dos.flush(); LOG.debug("Finished table [maxp]."); return bos.toByteArray(); } private byte[] buildOS2Table() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); OS2WindowsMetricsTable os2 = this.baseTTF.getOS2Windows(); if (os2 == null) { // sometimes there is no OS2 table in an embedded subfonts // create a dummy os2 = new OS2WindowsMetricsTable(); } LOG.debug("Building table [OS/2]..."); writeUint16(dos, 0); writeSint16(dos, os2.getAverageCharWidth()); writeUint16(dos, os2.getWeightClass()); writeUint16(dos, os2.getWidthClass()); writeSint16(dos, os2.getFsType()); writeSint16(dos, os2.getSubscriptXSize()); writeSint16(dos, os2.getSubscriptYSize()); writeSint16(dos, os2.getSubscriptXOffset()); writeSint16(dos, os2.getSubscriptYOffset()); writeSint16(dos, os2.getSuperscriptXSize()); writeSint16(dos, os2.getSuperscriptYSize()); writeSint16(dos, os2.getSuperscriptXOffset()); writeSint16(dos, os2.getSuperscriptYOffset()); writeSint16(dos, os2.getStrikeoutSize()); writeSint16(dos, os2.getStrikeoutPosition()); writeUint8(dos, os2.getFamilyClass()); writeUint8(dos, os2.getFamilySubClass()); dos.write(os2.getPanose()); writeUint32(dos, 0); writeUint32(dos, 0); writeUint32(dos, 0); writeUint32(dos, 0); dos.write(os2.getAchVendId().getBytes("ISO-8859-1")); Iterator<Entry<Integer, Integer>> it = characters.entrySet().iterator(); it.next(); Entry<Integer, Integer> first = it.next(); writeUint16(dos, os2.getFsSelection()); writeUint16(dos, first.getKey()); writeUint16(dos, characters.lastKey()); /* * The mysterious Microsoft additions. * * SHORT sTypoAscender * SHORT sTypoDescender * SHORT sTypoLineGap * USHORT usWinAscent * USHORT usWinDescent */ writeUint16(dos, os2.getTypoAscender()); writeUint16(dos, os2.getTypoDescender()); writeUint16(dos, os2.getTypeLineGap()); writeUint16(dos, os2.getWinAscent()); writeUint16(dos, os2.getWinDescent()); dos.flush(); LOG.debug("Finished table [OS/2]."); return bos.toByteArray(); } private byte[] buildLocaTable(long[] newOffsets) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); LOG.debug("Building table [loca]..."); for (long newOff : newOffsets) { writeUint32(dos, newOff); } dos.flush(); LOG.debug("Finished table [loca]."); return bos.toByteArray(); } private boolean addCompoundReferences() throws IOException { GlyphTable g = this.baseTTF.getGlyph(); long[] offsets = this.baseTTF.getIndexToLocation().getOffsets(); InputStream is = this.baseTTF.getOriginalData(); Set<Integer> glyphIdsToAdd = null; try { is.skip(g.getOffset()); long lastOff = 0L; for (Integer glyphId : this.glyphIds) { long offset = offsets[glyphId.intValue()]; long len = offsets[glyphId.intValue() + 1] - offset; is.skip(offset - lastOff); byte[] buf = new byte[(int) len]; is.read(buf); // rewrite glyphIds for compound glyphs if (buf.length >= 2 && buf[0] == -1 && buf[1] == -1) { int off = 2 * 5; int flags = 0; do { flags = ((((int) buf[off]) & 0xff) << 8) | (buf[off + 1] & 0xff); off += 2; int ogid = ((((int) buf[off]) & 0xff) << 8) | (buf[off + 1] & 0xff); if (!this.glyphIds.contains(ogid)) { LOG.debug("Adding referenced glyph " + ogid + " of compound glyph " + glyphId); if (glyphIdsToAdd == null) { glyphIdsToAdd = new TreeSet<Integer>(); } glyphIdsToAdd.add(ogid); } off += 2; // ARG_1_AND_2_ARE_WORDS if ((flags & (1 << 0)) != 0) { off += 2 * 2; } else { off += 2; } // WE_HAVE_A_TWO_BY_TWO if ((flags & (1 << 7)) != 0) { off += 2 * 4; } // WE_HAVE_AN_X_AND_Y_SCALE else if ((flags & (1 << 6)) != 0) { off += 2 * 2; } // WE_HAVE_A_SCALE else if ((flags & (1 << 3)) != 0) { off += 2; } // MORE_COMPONENTS } while ((flags & (1 << 5)) != 0); } lastOff = offsets[glyphId.intValue() + 1]; } } finally { is.close(); } if (glyphIdsToAdd != null) { this.glyphIds.addAll(glyphIdsToAdd); } return glyphIdsToAdd == null; } private byte[] buildGlyfTable(long[] newOffsets) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); LOG.debug("Building table [glyf]..."); GlyphTable g = this.baseTTF.getGlyph(); long[] offsets = this.baseTTF.getIndexToLocation().getOffsets(); InputStream is = this.baseTTF.getOriginalData(); try { is.skip(g.getOffset()); long lastOff = 0L; long newOff = 0L; int ioff = 0; for (Integer glyphId : this.glyphIds) { long offset = offsets[glyphId.intValue()]; long len = offsets[glyphId.intValue() + 1] - offset; newOffsets[ioff++] = newOff; is.skip(offset - lastOff); byte[] buf = new byte[(int) len]; is.read(buf); // rewrite glyphIds for compound glyphs if (buf.length >= 2 && buf[0] == -1 && buf[1] == -1) { LOG.debug("Compound glyph " + glyphId); int off = 2 * 5; int flags = 0; do { // clear the WE_HAVE_INSTRUCTIONS bit. (bit 8 is lsb of the first byte) buf[off] &= 0xfe; flags = ((((int) buf[off]) & 0xff) << 8) | ((int) buf[off + 1] & 0xff); off += 2; int ogid = ((((int) buf[off]) & 0xff) << 8) | ((int) buf[off + 1] & 0xff); if (!this.glyphIds.contains(ogid)) { this.glyphIds.add(ogid); } int ngid = this.getNewGlyphId(ogid); if (LOG.isDebugEnabled()) { LOG.debug(String.format("mapped glyph %d to %d in compound reference (flags=%04x)", ogid, ngid, flags)); } buf[off] = (byte) (ngid >>> 8); buf[off + 1] = (byte) ngid; off += 2; // ARG_1_AND_2_ARE_WORDS if ((flags & (1 << 0)) != 0) { off += 2 * 2; } else { off += 2; } // WE_HAVE_A_TWO_BY_TWO if ((flags & (1 << 7)) != 0) { off += 2 * 4; } // WE_HAVE_AN_X_AND_Y_SCALE else if ((flags & (1 << 6)) != 0) { off += 2 * 2; } // WE_HAVE_A_SCALE else if ((flags & (1 << 3)) != 0) { off += 2; } // MORE_COMPONENTS } while ((flags & (1 << 5)) != 0); // write the compound glyph bos.write(buf, 0, off); newOff += off; } else if (buf.length > 0) { /* * bail out instructions for simple glyphs, an excerpt from the specs is given below: * * int16 numberOfContours If the number of contours is positive or zero, it is a single glyph; * If the number of contours is -1, the glyph is compound * FWord xMin Minimum x for coordinate data * FWord yMin Minimum y for coordinate data * FWord xMax Maximum x for coordinate data * FWord yMax Maximum y for coordinate data * (here follow the data for the simple or compound glyph) * * Table 15: Simple glyph definition * Type Name Description * uint16 endPtsOfContours[n] Array of last points of each contour; n is the number of contours; * array entries are point indices * uint16 instructionLength Total number of bytes needed for instructions * uint8 instructions[instructionLength] Array of instructions for this glyph * uint8 flags[variable] Array of flags * uint8 or int16 xCoordinates[] Array of x-coordinates; the first is relative to (0,0), * others are relative to previous point * uint8 or int16 yCoordinates[] Array of y-coordinates; the first is relative to (0,0), * others are relative to previous point */ int numberOfContours = (((int) buf[0] & 0xff) << 8) | ((int) buf[1] & 0xff); // offset of instructionLength int off = 2 * 5 + numberOfContours * 2; // write numberOfContours, xMin, yMin, xMax, yMax, endPtsOfContours[n] bos.write(buf, 0, off); newOff += off; int instructionLength = ((((int) buf[off]) & 0xff) << 8) | ((int) buf[off + 1] & 0xff); // zarro instructions. bos.write(0); bos.write(0); newOff += 2; off += 2 + instructionLength; // flags and coordinates bos.write(buf, off, buf.length - off); newOff += buf.length - off; } if ((newOff % 4L) != 0L) { int np = (int) (4 - newOff % 4L); bos.write(PAD_BUF, 0, np); newOff += np; } lastOff = offsets[glyphId.intValue() + 1]; } newOffsets[ioff++] = newOff; } finally { is.close(); } LOG.debug("Finished table [glyf]."); return bos.toByteArray(); } private int getNewGlyphId(Integer oldGid) { return this.glyphIds.headSet(oldGid).size(); } private byte[] buildCmapTable() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); LOG.debug("Building table [cmap]..."); /* * UInt16 version Version number (Set to zero) * UInt16 numberSubtables Number of encoding subtables */ writeUint16(dos, 0); writeUint16(dos, 1); /* * UInt16 platformID Platform identifier * UInt16 platformSpecificID Platform-specific encoding identifier * UInt32 offset Offset of the mapping table */ writeUint16(dos, 3); // unicode writeUint16(dos, 1); // Default Semantics writeUint32(dos, 4 * 2 + 4); // mapping of type 4. Iterator<Entry<Integer, Integer>> it = this.characters.entrySet().iterator(); it.next(); Entry<Integer, Integer> lastChar = it.next(); Entry<Integer, Integer> prevChar = lastChar; int lastGid = this.getNewGlyphId(lastChar.getValue()); int[] startCode = new int[this.characters.size()]; int[] endCode = new int[this.characters.size()]; int[] idDelta = new int[this.characters.size()]; int nseg = 0; while (it.hasNext()) { Entry<Integer, Integer> curChar = it.next(); int curGid = this.getNewGlyphId(curChar.getValue()); if (curChar.getKey() != prevChar.getKey() + 1 || curGid - lastGid != curChar.getKey() - lastChar.getKey()) { // Don't emit ranges, which map to the undef glyph, the // undef glyph is emitted a the very last segment. if (lastGid != 0) { startCode[nseg] = lastChar.getKey(); endCode[nseg] = prevChar.getKey(); idDelta[nseg] = lastGid - lastChar.getKey(); ++nseg; } // shorten ranges which start with undef by one. else if (!lastChar.getKey().equals(prevChar.getKey())) { startCode[nseg] = lastChar.getKey() + 1; endCode[nseg] = prevChar.getKey(); idDelta[nseg] = lastGid - lastChar.getKey(); ++nseg; } lastGid = curGid; lastChar = curChar; } prevChar = curChar; } // trailing segment startCode[nseg] = lastChar.getKey(); endCode[nseg] = prevChar.getKey(); idDelta[nseg] = lastGid - lastChar.getKey(); ++nseg; // notdef character. startCode[nseg] = 0xffff; endCode[nseg] = 0xffff; idDelta[nseg] = 1; ++nseg; /* * UInt16 format Format number is set to 4 * UInt16 length Length of subtable in bytes * UInt16 language Language code for this encoding subtable, or zero if language-independent * UInt16 segCountX2 2 * segCount * UInt16 searchRange 2 * (2**FLOOR(log2(segCount))) * UInt16 entrySelector log2(searchRange/2) * UInt16 rangeShift (2 * segCount) - searchRange * UInt16 endCode[segCount] Ending character code for each segment, last = 0xFFFF. * UInt16 reservedPad This value should be zero * UInt16 startCode[segCount] Starting character code for each segment * UInt16 idDelta[segCount] Delta for all character codes in segment * UInt16 idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 * UInt16 glyphIndexArray[variable] Glyph index array */ writeUint16(dos, 4); writeUint16(dos, 8 * 2 + nseg * (4 * 2)); writeUint16(dos, 0); writeUint16(dos, nseg * 2); int nsegHigh = Integer.highestOneBit(nseg); writeUint16(dos, nsegHigh * 2); writeUint16(dos, log2i(nsegHigh)); writeUint16(dos, 2 * (nseg - nsegHigh)); for (int i = 0; i < nseg; ++i) { writeUint16(dos, endCode[i]); } writeUint16(dos, 0); for (int i = 0; i < nseg; ++i) { writeUint16(dos, startCode[i]); } for (int i = 0; i < nseg; ++i) { writeUint16(dos, idDelta[i]); } for (int i = 0; i < nseg; ++i) { writeUint16(dos, 0); } LOG.debug("Finished table [cmap]."); return bos.toByteArray(); } private byte[] buildPostTable() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); LOG.debug("Building table [post]..."); PostScriptTable p = this.baseTTF.getPostScript(); if (p == null) { // sometimes there is no post table in an embedded subfonts // create a dummy p = new PostScriptTable(); } String[] glyphNames = p.getGlyphNames(); /* Fixed format Format of this table Fixed italicAngle Italic angle in degrees FWord underlinePosition Underline position FWord underlineThickness Underline thickness uint32 isFixedPitch Font is monospaced; set to 1 if the font is monospaced and 0 otherwise (N.B., to maintain compatibility with older versions of the TrueType spec, accept any non-zero value as meaning that the font is monospaced) uint32 minMemType42 Minimum memory usage when a TrueType font is downloaded as a Type 42 font uint32 maxMemType42 Maximum memory usage when a TrueType font is downloaded as a Type 42 font uint32 minMemType1 Minimum memory usage when a TrueType font is downloaded as a Type 1 font uint32 maxMemType1 Maximum memory usage when a TrueType font is downloaded as a Type 1 font uint16 numberOfGlyphs number of glyphs uint16 glyphNameIndex[numberOfGlyphs] Ordinal number of this glyph in 'post' string tables. This is not an offset. Pascal string names[numberNewGlyphs] glyph names with length bytes [variable] (a Pascal string) */ writeFixed(dos, 2.0); writeFixed(dos, p.getItalicAngle()); writeSint16(dos, p.getUnderlinePosition()); writeSint16(dos, p.getUnderlineThickness()); writeUint32(dos, p.getIsFixedPitch()); writeUint32(dos, p.getMinMemType42()); writeUint32(dos, p.getMaxMemType42()); writeUint32(dos, p.getMimMemType1()); writeUint32(dos, p.getMaxMemType1()); writeUint16(dos, baseTTF.getHorizontalHeader().getNumberOfHMetrics()); List<String> additionalNames = new ArrayList<String>(); Map<String, Integer> additionalNamesIndices = new HashMap<String, Integer>(); if (glyphNames == null) { Encoding enc = MacRomanEncoding.INSTANCE; int[] gidToUC = this.baseCmap.getGlyphIdToCharacterCode(); for (Integer glyphId : this.glyphIds) { int uc = gidToUC[glyphId.intValue()]; String name = null; if (uc < 0x8000) { try { name = enc.getNameFromCharacter((char) uc); } catch (IOException e) { // TODO } } if (name == null) { name = String.format(Locale.ENGLISH, "uni%04X", uc); } Integer macId = Encoding.MAC_GLYPH_NAMES_INDICES.get(name); if (macId == null) { Integer idx = additionalNamesIndices.get(name); if (idx == null) { idx = additionalNames.size(); additionalNames.add(name); additionalNamesIndices.put(name, idx); } writeUint16(dos, idx.intValue() + 258); } else { writeUint16(dos, macId.intValue()); } } } else { for (Integer glyphId : this.glyphIds) { String name = glyphNames[glyphId.intValue()]; Integer macId = Encoding.MAC_GLYPH_NAMES_INDICES.get(name); if (macId == null) { Integer idx = additionalNamesIndices.get(name); if (idx == null) { idx = additionalNames.size(); additionalNames.add(name); additionalNamesIndices.put(name, idx); } writeUint16(dos, idx.intValue() + 258); } else { writeUint16(dos, macId.intValue()); } } } for (String name : additionalNames) { LOG.debug("additionalName=[" + name + "]."); byte[] buf = name.getBytes("US-ASCII"); writeUint8(dos, buf.length); dos.write(buf); } dos.flush(); LOG.debug("Finished table [post]."); return bos.toByteArray(); } private byte[] buildHmtxTable() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); LOG.debug("Building table [hmtx]..."); HorizontalHeaderTable h = this.baseTTF.getHorizontalHeader(); HorizontalMetricsTable hm = this.baseTTF.getHorizontalMetrics(); byte[] buf = new byte[4]; InputStream is = this.baseTTF.getOriginalData(); try { is.skip(hm.getOffset()); long lastOff = 0; for (Integer glyphId : this.glyphIds) { // offset in original file. long off; if (glyphId < h.getNumberOfHMetrics()) { off = glyphId * 4; } else { off = h.getNumberOfHMetrics() * 4 + (glyphId - h.getNumberOfHMetrics()) * 2; } // skip over from last original offset. if (off != lastOff) { long nskip = off - lastOff; if (nskip != is.skip(nskip)) { throw new EOFException("Unexpected EOF exception parsing glyphId of hmtx table."); } } // read left side bearings only, if we are beyond numOfHMetrics. int n = glyphId < h.getNumberOfHMetrics() ? 4 : 2; if (n != is.read(buf, 0, n)) { throw new EOFException("Unexpected EOF exception parsing glyphId of hmtx table."); } bos.write(buf, 0, n); lastOff = off + n; } LOG.debug("Finished table [hmtx]."); return bos.toByteArray(); } finally { is.close(); } } /** * Write the subfont to the given output stream. * * @param os the stream used for writing * @throws IOException if something went wrong. */ public void writeToStream(OutputStream os) throws IOException { LOG.debug("glyphIds=[" + glyphIds + "]"); LOG.debug("numGlyphs=[" + glyphIds.size() + "]"); while (!addCompoundReferences()) { } DataOutputStream dos = new DataOutputStream(os); try { /* 'cmap' character to glyph mapping 'glyf' glyph data 'head' font header 'hhea' horizontal header 'OS/2' OS/2 compatibility table. 'hmtx' horizontal metrics 'loca' index to location 'maxp' maximum profile 'name' naming 'post' PostScript */ String[] tableNames = { "OS/2", "cmap", "glyf", "head", "hhea", "hmtx", "loca", "maxp", "name", "post" }; byte[][] tables = new byte[tableNames.length][]; long[] newOffsets = new long[this.glyphIds.size() + 1]; tables[3] = this.buildHeadTable(); tables[4] = this.buildHheaTable(); tables[7] = this.buildMaxpTable(); tables[8] = this.buildNameTable(); tables[0] = this.buildOS2Table(); tables[2] = this.buildGlyfTable(newOffsets); tables[6] = this.buildLocaTable(newOffsets); tables[1] = this.buildCmapTable(); tables[5] = this.buildHmtxTable(); tables[9] = this.buildPostTable(); long checksum = writeFileHeader(dos, tableNames.length); long offset = 12L + 16L * tableNames.length; for (int i = 0; i < tableNames.length; ++i) { checksum += writeTableHeader(dos, tableNames[i], offset, tables[i]); offset += ((tables[i].length + 3) / 4) * 4; } checksum = 0xB1B0AFBAL - (checksum & 0xffffffffL); // correct checksumAdjustment of 'head' table. tables[3][8] = (byte) (checksum >>> 24); tables[3][9] = (byte) (checksum >>> 16); tables[3][10] = (byte) (checksum >>> 8); tables[3][11] = (byte) (checksum); for (int i = 0; i < tableNames.length; ++i) { writeTableBody(dos, tables[i]); } } finally { dos.close(); } } }