TrueTypeEmbedder.java
/*
* 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.pdfbox.pdmodel.font;
import java.awt.geom.GeneralPath;
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;
import org.apache.fontbox.ttf.CmapLookup;
import org.apache.fontbox.ttf.HeaderTable;
import org.apache.fontbox.ttf.HorizontalHeaderTable;
import org.apache.fontbox.ttf.OS2WindowsMetricsTable;
import org.apache.fontbox.ttf.PostScriptTable;
import org.apache.fontbox.ttf.TTFParser;
import org.apache.fontbox.ttf.TTFSubsetter;
import org.apache.fontbox.ttf.TrueTypeFont;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
/**
* 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 = 512;
private static final String BASE25 = "BCDEFGHIJKLMNOPQRSTUVWXYZ";
private final PDDocument document;
protected TrueTypeFont ttf;
protected PDFontDescriptor fontDescriptor;
protected final CmapLookup cmapLookup;
private final Set<Integer> subsetCodePoints = new HashSet<>();
private final boolean embedSubset;
private final Set<Integer> allGlyphIds = new HashSet<>();
/**
* Creates a new TrueType font for embedding.
*/
TrueTypeEmbedder(PDDocument document, COSDictionary dict, TrueTypeFont ttf,
boolean embedSubset) throws IOException
{
this.document = document;
this.embedSubset = embedSubset;
this.ttf = ttf;
fontDescriptor = createFontDescriptor(ttf);
if (!isEmbeddingPermitted(ttf))
{
throw new IOException("This font does not permit embedding");
}
if (!embedSubset)
{
// full embedding
// TrueType collections are not supported
InputStream is = ttf.getOriginalData();
byte[] b = new byte[4];
is.mark(b.length);
if (is.read(b) == b.length && new String(b).equals("ttcf"))
{
is.close();
throw new IOException("Full embedding of TrueType font collections not supported");
}
if (is.markSupported())
{
is.reset();
}
else
{
is.close();
is = ttf.getOriginalData();
}
PDStream stream = new PDStream(document, is, COSName.FLATE_DECODE);
stream.getCOSObject().setLong(COSName.LENGTH1, ttf.getOriginalDataSize());
fontDescriptor.setFontFile2(stream);
}
dict.setName(COSName.BASE_FONT, ttf.getName());
// choose a Unicode "cmap"
cmapLookup = ttf.getUnicodeCmapLookup();
}
public final void buildFontFile2(InputStream ttfStream) throws IOException
{
PDStream stream = new PDStream(document, ttfStream, COSName.FLATE_DECODE);
// as the stream was closed within the PDStream constructor, we have to recreate it
try (InputStream 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);
}
}
stream.getCOSObject().setLong(COSName.LENGTH1, ttf.getOriginalDataSize());
fontDescriptor.setFontFile2(stream);
}
/**
* Returns true if the fsType in the OS/2 table permits embedding.
*/
boolean isEmbeddingPermitted(TrueTypeFont ttf) throws IOException
{
if (ttf.getOS2Windows() != null)
{
int fsType = ttf.getOS2Windows().getFsType();
int maskedFsType = fsType & 0x000F;
// PDFBOX-5191: don't check the bit because permissions are exclusive
if (maskedFsType == OS2WindowsMetricsTable.FSTYPE_RESTRICTED)
{
// restricted License embedding
return false;
}
else if ((fsType & 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
{
OS2WindowsMetricsTable os2 = ttf.getOS2Windows();
if (os2 == null)
{
throw new IOException("os2 table is missing in font " + ttf.getName());
}
PostScriptTable post = ttf.getPostScript();
if (post == null)
{
throw new IOException("post table is missing in font " + ttf.getName());
}
PDFontDescriptor fd = new PDFontDescriptor();
fd.setFontName(ttf.getName());
// Flags
fd.setFixedPitch(post.getIsFixedPitch() > 0 ||
ttf.getHorizontalHeader().getNumberOfHMetrics() == 1);
int fsSelection = os2.getFsSelection();
fd.setItalic(((fsSelection & (ITALIC | OBLIQUE)) != 0));
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;
default:
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
{
GeneralPath capHPath = ttf.getPath("H");
if (capHPath != null)
{
fd.setCapHeight(Math.round(capHPath.getBounds2D().getMaxY()) * scaling);
}
else
{
// estimate by summing the typographical +ve ascender and -ve descender
fd.setCapHeight((os2.getTypoAscender() + os2.getTypoDescender()) * scaling);
}
GeneralPath xPath = ttf.getPath("x");
if (xPath != null)
{
fd.setXHeight(Math.round(xPath.getBounds2D().getMaxY()) * scaling);
}
else
{
// 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 font descriptor.
*/
public PDFontDescriptor getFontDescriptor()
{
return fontDescriptor;
}
@Override
public void addToSubset(int codePoint)
{
subsetCodePoints.add(codePoint);
}
public void addGlyphIds(Set<Integer> glyphIds)
{
allGlyphIds.addAll(glyphIds);
}
@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<>();
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(ttf, tables);
subsetter.addAll(subsetCodePoints);
if (!allGlyphIds.isEmpty())
{
subsetter.addGlyphIds(allGlyphIds);
}
// 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);
ttf.close();
}
/**
* 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();
}
}