package com.tom_roush.pdfbox.pdmodel.font;
import com.tom_roush.fontbox.ttf.CmapSubtable;
import com.tom_roush.fontbox.ttf.HeaderTable;
import com.tom_roush.fontbox.ttf.HorizontalHeaderTable;
import com.tom_roush.fontbox.ttf.OS2WindowsMetricsTable;
import com.tom_roush.fontbox.ttf.PostScriptTable;
import com.tom_roush.fontbox.ttf.TTFParser;
import com.tom_roush.fontbox.ttf.TTFSubsetter;
import com.tom_roush.fontbox.ttf.TrueTypeFont;
import com.tom_roush.pdfbox.cos.COSDictionary;
import com.tom_roush.pdfbox.cos.COSName;
import com.tom_roush.pdfbox.io.IOUtils;
import com.tom_roush.pdfbox.pdmodel.PDDocument;
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle;
import com.tom_roush.pdfbox.pdmodel.common.PDStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Common functionality for embedding TrueType fonts.
*
* @author Ben Litchfield
* @author John Hewson
*/
abstract class TrueTypeEmbedder implements Subsetter
{
private static final int ITALIC = 1;
private static final int OBLIQUE = 256;
private static final String BASE25 = "BCDEFGHIJKLMNOPQRSTUVWXYZ";
private final PDDocument document;
protected TrueTypeFont ttf;
protected PDFontDescriptor fontDescriptor;
protected final CmapSubtable cmap;
private final Set<Integer> subsetCodePoints = new HashSet<Integer>();
private final boolean embedSubset;
/**
* Creates a new TrueType font for embedding.
*/
TrueTypeEmbedder(PDDocument document, COSDictionary dict, InputStream ttfStream,
boolean embedSubset) throws IOException
{
this.document = document;
this.embedSubset = embedSubset;
buildFontFile2(ttfStream);
dict.setName(COSName.BASE_FONT, ttf.getName());
// choose a Unicode "cmap"
cmap = ttf.getUnicodeCmap();
}
public void buildFontFile2(InputStream ttfStream) throws IOException
{
PDStream stream = new PDStream(document, ttfStream, COSName.FLATE_DECODE);
stream.getStream().setInt(COSName.LENGTH1, stream.toByteArray().length);
// as the stream was closed within the PDStream constructor, we have to recreate it
InputStream input = null;
try
{
input = stream.createInputStream();
ttf = new TTFParser().parseEmbedded(input);
if (!isEmbeddingPermitted(ttf))
{
throw new IOException("This font does not permit embedding");
}
if (fontDescriptor == null)
{
fontDescriptor = createFontDescriptor(ttf);
}
}
finally
{
IOUtils.closeQuietly(input);
}
fontDescriptor.setFontFile2(stream);
}
/**
* Returns true if the fsType in the OS/2 table permits embedding.
*/
private boolean isEmbeddingPermitted(TrueTypeFont ttf) throws IOException
{
if (ttf.getOS2Windows() != null)
{
int fsType = ttf.getOS2Windows().getFsType();
int exclusive = fsType & 0x8; // bits 0-3 are a set of exclusive bits
if ((exclusive & OS2WindowsMetricsTable.FSTYPE_RESTRICTED) ==
OS2WindowsMetricsTable.FSTYPE_RESTRICTED)
{
// restricted License embedding
return false;
}
else if ((exclusive & OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY) ==
OS2WindowsMetricsTable.FSTYPE_BITMAP_ONLY)
{
// bitmap embedding only
return false;
}
}
return true;
}
/**
* Returns true if the fsType in the OS/2 table permits subsetting.
*/
private boolean isSubsettingPermitted(TrueTypeFont ttf) throws IOException
{
if (ttf.getOS2Windows() != null)
{
int fsType = ttf.getOS2Windows().getFsType();
if ((fsType & OS2WindowsMetricsTable.FSTYPE_NO_SUBSETTING) ==
OS2WindowsMetricsTable.FSTYPE_NO_SUBSETTING)
{
return false;
}
}
return true;
}
/**
* Creates a new font descriptor dictionary for the given TTF.
*/
private PDFontDescriptor createFontDescriptor(TrueTypeFont ttf) throws IOException
{
PDFontDescriptor fd = new PDFontDescriptor();
fd.setFontName(ttf.getName());
OS2WindowsMetricsTable os2 = ttf.getOS2Windows();
PostScriptTable post = ttf.getPostScript();
// Flags
fd.setFixedPitch(post.getIsFixedPitch() > 0 ||
ttf.getHorizontalHeader().getNumberOfHMetrics() == 1);
int fsSelection = os2.getFsSelection();
fd.setItalic((fsSelection & ITALIC) == fsSelection ||
(fsSelection & OBLIQUE) == fsSelection);
switch (os2.getFamilyClass())
{
case OS2WindowsMetricsTable.FAMILY_CLASS_CLAREDON_SERIFS:
case OS2WindowsMetricsTable.FAMILY_CLASS_FREEFORM_SERIFS:
case OS2WindowsMetricsTable.FAMILY_CLASS_MODERN_SERIFS:
case OS2WindowsMetricsTable.FAMILY_CLASS_OLDSTYLE_SERIFS:
case OS2WindowsMetricsTable.FAMILY_CLASS_SLAB_SERIFS:
fd.setSerif(true);
break;
case OS2WindowsMetricsTable.FAMILY_CLASS_SCRIPTS:
fd.setScript(true);
break;
}
fd.setFontWeight(os2.getWeightClass());
fd.setSymbolic(true);
fd.setNonSymbolic(false);
// ItalicAngle
fd.setItalicAngle(post.getItalicAngle());
// FontBBox
HeaderTable header = ttf.getHeader();
PDRectangle rect = new PDRectangle();
float scaling = 1000f / header.getUnitsPerEm();
rect.setLowerLeftX(header.getXMin() * scaling);
rect.setLowerLeftY(header.getYMin() * scaling);
rect.setUpperRightX(header.getXMax() * scaling);
rect.setUpperRightY(header.getYMax() * scaling);
fd.setFontBoundingBox(rect);
// Ascent, Descent
HorizontalHeaderTable hHeader = ttf.getHorizontalHeader();
fd.setAscent(hHeader.getAscender() * scaling);
fd.setDescent(hHeader.getDescender() * scaling);
// CapHeight, XHeight
if (os2.getVersion() >= 1.2)
{
fd.setCapHeight(os2.getCapHeight() * scaling);
fd.setXHeight(os2.getHeight() * scaling);
}
else
{
// estimate by summing the typographical +ve ascender and -ve descender
fd.setCapHeight((os2.getTypoAscender() + os2.getTypoDescender()) * scaling);
// estimate by halving the typographical ascender
fd.setXHeight(os2.getTypoAscender() / 2.0f * scaling);
}
// StemV - there's no true TTF equivalent of this, so we estimate it
fd.setStemV(fd.getFontBoundingBox().getWidth() * .13f);
return fd;
}
/**
* Returns the FontBox font.
*/
public TrueTypeFont getTrueTypeFont()
{
return ttf;
}
/**
* Returns the font descriptor.
*/
public PDFontDescriptor getFontDescriptor()
{
return fontDescriptor;
}
@Override
public void addToSubset(int codePoint)
{
subsetCodePoints.add(codePoint);
}
@Override
public void subset() throws IOException
{
if (!isSubsettingPermitted(ttf))
{
throw new IOException("This font does not permit subsetting");
}
if (!embedSubset)
{
throw new IllegalStateException("Subsetting is disabled");
}
// PDF spec required tables (if present), all others will be removed
List<String> tables = new ArrayList<String>();
tables.add("head");
tables.add("hhea");
tables.add("loca");
tables.add("maxp");
tables.add("cvt ");
tables.add("prep");
tables.add("glyf");
tables.add("hmtx");
tables.add("fpgm");
// Windows ClearType
tables.add("gasp");
// set the GIDs to subset
TTFSubsetter subsetter = new TTFSubsetter(getTrueTypeFont(), tables);
subsetter.addAll(subsetCodePoints);
// calculate deterministic tag based on the chosen subset
Map<Integer, Integer> gidToCid = subsetter.getGIDMap();
String tag = getTag(gidToCid);
subsetter.setPrefix(tag);
// save the subset font
ByteArrayOutputStream out = new ByteArrayOutputStream();
subsetter.writeToStream(out);
// re-build the embedded font
buildSubset(new ByteArrayInputStream(out.toByteArray()), tag, gidToCid);
}
/**
* Returns true if the font needs to be subset.
*/
public boolean needsSubset()
{
return embedSubset;
}
/**
* Rebuild a font subset.
*/
protected abstract void buildSubset(InputStream ttfSubset, String tag,
Map<Integer, Integer> gidToCid) throws IOException;
/**
* Returns an uppercase 6-character unique tag for the given subset.
*/
public String getTag(Map<Integer, Integer> gidToCid)
{
// deterministic
long num = gidToCid.hashCode();
// base25 encode
StringBuilder sb = new StringBuilder();
do
{
long div = num / 25;
int mod = (int)(num % 25);
sb.append(BASE25.charAt(mod));
num = div;
} while (num != 0 && sb.length() < 6);
// pad
while (sb.length() < 6)
{
sb.insert(0, 'A');
}
sb.append('+');
return sb.toString();
}
}