/*******************************************************************************
* Copyright (c) 2009, Adobe Systems Incorporated
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* · Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* · Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* · Neither the name of Adobe Systems Incorporated nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/
package com.adobe.dp.epub.otf;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;
import java.util.Vector;
import com.adobe.dp.css.BaseRule;
import com.adobe.dp.css.CSSName;
import com.adobe.dp.css.CSSNumber;
import com.adobe.dp.css.CSSQuotedString;
import com.adobe.dp.css.CSSValue;
import com.adobe.dp.css.CSSValueList;
import com.adobe.dp.css.CascadeResult;
import com.adobe.dp.css.FontFaceRule;
import com.adobe.dp.css.InlineRule;
import com.adobe.dp.epub.io.BufferedDataSource;
import com.adobe.dp.epub.opf.FontResource;
import com.adobe.dp.epub.opf.Publication;
import com.adobe.dp.epub.opf.StyleResource;
import com.adobe.dp.epub.ops.Element;
import com.adobe.dp.otf.FontInputStream;
import com.adobe.dp.otf.FontLocator;
import com.adobe.dp.otf.FontProperties;
import com.adobe.dp.otf.FontPropertyConstants;
import com.adobe.dp.otf.OpenTypeFont;
public class FontSubsetter implements FontEmbeddingReport {
class SubsetterEntry {
OpenTypeFont font;
boolean used;
}
class FontEntry {
SubsetterEntry subsetter;
String familyName;
int weight = FontPropertyConstants.WEIGHT_NORMAL;
int style = FontPropertyConstants.STYLE_REGULAR;
FontEntry cloneEntry() {
FontEntry entry = new FontEntry();
entry.familyName = familyName;
entry.weight = weight;
entry.style = style;
return entry;
}
public int hashCode() {
return familyName.hashCode() + weight + style;
}
public boolean equals(Object other) {
if (other.getClass() != getClass())
return false;
FontEntry o = (FontEntry) other;
return o.familyName.equals(familyName) && o.weight == weight && o.style == style;
}
}
static class FontListEntry {
SubsetterEntry[] subsetterList;
String[] familyName;
int weight = FontPropertyConstants.WEIGHT_NORMAL;
int style = FontPropertyConstants.STYLE_REGULAR;
FontListEntry cloneEntry() {
FontListEntry entry = new FontListEntry();
entry.familyName = familyName;
entry.weight = weight;
entry.style = style;
return entry;
}
public int hashCode() {
return familyName.hashCode() + weight + style;
}
public boolean equals(Object other) {
if (other.getClass() != getClass())
return false;
FontListEntry o = (FontListEntry) other;
return o.familyName.equals(familyName) && o.weight == weight && o.style == style;
}
}
Publication epub;
Stack entryStack = new Stack();
FontListEntry currentEntry;
StyleResource styleResource;
Vector styles;
Hashtable subsetters = new Hashtable();
Hashtable subsetterLists = new Hashtable();
FontLocator fontLocator;
Set missingFonts = new TreeSet();
Set prohibitedFonts = new TreeSet();
long totalPlay;
public FontSubsetter(Publication epub, StyleResource styleResource, FontLocator locator) {
this.styleResource = styleResource;
this.fontLocator = locator;
}
public Iterator missingFonts() {
return missingFonts.iterator();
}
public Iterator prohibitedFonts() {
return prohibitedFonts.iterator();
}
public Iterator usedFonts() {
Iterator keys = subsetters.keySet().iterator();
Set usedFonts = new TreeSet();
while (keys.hasNext()) {
FontEntry entry = (FontEntry) keys.next();
if (entry.subsetter.used) {
FontProperties prop = new FontProperties(entry.familyName, entry.weight, entry.style);
usedFonts.add(prop);
}
}
return usedFonts.iterator();
}
public void setStyles(Vector styles) {
this.styles = styles;
}
private String[] parseFamily(Object family) {
if (family instanceof CSSValueList) {
CSSValueList list = (CSSValueList) family;
int len = list.getSeparator() == ',' ? list.length() : 1;
String[] result = new String[len];
for (int i = 0; i < len; i++) {
CSSValue v = list.getSeparator() == ',' ? list.item(i) : list;
result[i] = v.toString();
}
return result;
} else if ((family instanceof CSSQuotedString) || (family instanceof CSSName)) {
String[] result = { family.toString() };
return result;
}
String[] result = { "serif" };
return result;
}
private int parseWeight(Object weight) {
if (weight instanceof CSSNumber) {
try {
return ((CSSNumber) weight).getNumber().intValue();
} catch (Exception e) {
}
} else if (weight.toString().toLowerCase().equals("bold")) {
return FontProperties.WEIGHT_BOLD;
}
return FontProperties.WEIGHT_NORMAL;
}
private int parseStyle(Object style) {
style = style.toString().toLowerCase();
if (style.equals("italic")) {
return FontProperties.STYLE_ITALIC;
}
if (style.equals("oblique")) {
return FontProperties.STYLE_OBLIQUE;
}
return FontProperties.STYLE_REGULAR;
}
private void processCascadeResult(CascadeResult cascade) {
// TODO: other media?
if (cascade != null) {
InlineRule rule = cascade.getProperties().getPropertySet();
processRule(rule);
}
}
private void processRule(BaseRule rule) {
if (rule == null)
return;
Object family = rule.get("font-family");
if (family != null)
currentEntry.familyName = parseFamily(family);
Object weight = rule.get("font-weight");
if (weight != null)
currentEntry.weight = parseWeight(weight);
Object style = rule.get("font-style");
if (style != null)
currentEntry.style = parseStyle(style);
}
private void processBuiltInStyles(String name) {
if (name.equals("h1") || name.equals("h2") || name.equals("h3") || name.equals("h4") || name.equals("h5")
|| name.equals("h6") || name.equals("b") || name.equals("strong"))
currentEntry.weight = FontProperties.WEIGHT_BOLD;
else if (name.equals("i") || name.equals("em"))
currentEntry.style = FontProperties.STYLE_ITALIC;
}
public void push(Element e) {
if (currentEntry == null) {
currentEntry = new FontListEntry();
} else {
entryStack.push(currentEntry);
currentEntry = currentEntry.cloneEntry();
}
// built-in stylesheet
processBuiltInStyles(e.getElementName());
// document stylesheets
processCascadeResult(e.getCascadeResult());
// style attribute: highest specificity
processRule(e.getStyle());
if (currentEntry.familyName != null) {
currentEntry.subsetterList = (SubsetterEntry[]) subsetterLists.get(currentEntry);
if (currentEntry.subsetterList == null) {
Vector subsetterList = new Vector();
for (int i = 0; i < currentEntry.familyName.length; i++) {
String family = currentEntry.familyName[i];
if (family.equals("serif") || family.equals("sans-serif") || family.equals("monospace"))
continue; // built-in
FontEntry entry = new FontEntry();
entry.familyName = family;
entry.style = currentEntry.style;
entry.weight = currentEntry.weight;
SubsetterEntry subsetter = (SubsetterEntry) subsetters.get(entry);
if (subsetter == null) {
try {
FontProperties prop = new FontProperties(entry.familyName, entry.weight, entry.style);
FontInputStream stream = fontLocator.locateFont(prop);
if (stream != null) {
OpenTypeFont font = new OpenTypeFont(stream);
if (font.canEmbedForReading() && font.canSubset()) {
subsetter = new SubsetterEntry();
subsetter.font = font;
subsetters.put(entry, subsetter);
entry.subsetter = subsetter;
} else {
prohibitedFonts.add(prop);
}
} else {
missingFonts.add(prop);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (subsetter != null)
subsetterList.add(subsetter);
}
currentEntry.subsetterList = new SubsetterEntry[subsetterList.size()];
subsetterList.copyInto(currentEntry.subsetterList);
subsetterLists.put(currentEntry, currentEntry.subsetterList);
}
}
}
public void pop(Element e) {
if (entryStack.isEmpty())
currentEntry = null;
else
currentEntry = (FontListEntry) entryStack.pop();
}
public void play(String text) {
if (currentEntry != null && currentEntry.subsetterList != null) {
long t0 = System.currentTimeMillis();
int subsetterListLen = currentEntry.subsetterList.length;
int stringLength = text.length();
for (int i = 0; i < stringLength; i++) {
char c = text.charAt(i);
for (int j = 0; j < subsetterListLen; j++) {
SubsetterEntry subsetter = currentEntry.subsetterList[j];
if (subsetter.font != null) {
if (subsetter.font.play(c)) {
subsetter.used = true;
break;
}
}
}
}
long t1 = System.currentTimeMillis();
totalPlay += (t1 - t0);
}
}
public void addFonts(Publication epub) {
// System.out.println("\t\tplaying text for subsetting: " + totalPlay
// /1000.0 + " seconds");
// long t0 = System.currentTimeMillis();
Enumeration list = subsetters.keys();
while (list.hasMoreElements()) {
FontEntry entry = (FontEntry) list.nextElement();
if (!entry.subsetter.used)
continue;
BufferedDataSource bds = new BufferedDataSource();
try {
OpenTypeFont font = entry.subsetter.font;
String resName = entry.familyName.replaceAll(" ", "-") + "-" + entry.weight;
switch (entry.style) {
case FontPropertyConstants.STYLE_ITALIC:
resName += "-Italic";
break;
case FontPropertyConstants.STYLE_OBLIQUE:
resName += "-Oblique";
break;
}
String primaryUUID = epub.getPrimaryIdentifier();
if (primaryUUID != null && primaryUUID.startsWith("urn:uuid:")) {
resName = resName + "-" + primaryUUID.substring(9);
}
bds.getOutputStream().write(font.getSubsettedFont());
String folder = epub.getContentFolder() == null ? "" : epub.getContentFolder() + "/";
FontResource fontResource = epub.createFontResource(folder + "fonts/" + resName + ".otf", bds);
FontFaceRule face = styleResource.getStylesheet().createFontFace(fontResource);
face.set("font-family", new CSSQuotedString(entry.familyName));
switch (entry.weight) {
case FontPropertyConstants.WEIGHT_NORMAL:
face.set("font-weight", new CSSName("normal"));
break;
case FontPropertyConstants.WEIGHT_BOLD:
face.set("font-weight", new CSSName("bold"));
break;
default:
face.set("font-weight", new CSSNumber(new Integer(entry.weight)));
break;
}
switch (entry.style) {
case FontPropertyConstants.STYLE_ITALIC:
face.set("font-style", new CSSName("italic"));
break;
case FontPropertyConstants.STYLE_OBLIQUE:
face.set("font-style", new CSSName("oblique"));
break;
default:
face.set("font-style", new CSSName("normal"));
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
// long t1 = System.currentTimeMillis();
// System.out.println("\t\twriting font subset: " + (t1 - t0) /1000.0 +
// " seconds");
}
}