Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
338 changes: 282 additions & 56 deletions openpdf/src/main/java/com/lowagie/text/pdf/FontDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import com.lowagie.text.Utilities;
import java.awt.font.GlyphVector;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;

/**
Expand Down Expand Up @@ -172,73 +173,180 @@ BaseFont getBaseFont() {
* encoding and the characters used are stored.
*
* @param text the text to convert
* @param options rendering options
* @return the conversion
*/
byte[] convertToBytes(String text, TextRenderingOptions options) {
byte[] b = null;
switch (fontType) {
case BaseFont.FONT_TYPE_T3:
return baseFont.convertToBytes(text);
return convertType3Font(text);

case BaseFont.FONT_TYPE_T1:
case BaseFont.FONT_TYPE_TT: {
b = baseFont.convertToBytes(text);
int len = b.length;
for (byte b1 : b) {
shortTag[b1 & 0xff] = 1;
}
break;
case BaseFont.FONT_TYPE_TT:
return convertType1OrTrueTypeFont(text);

case BaseFont.FONT_TYPE_CJK:
return convertCjkFont(text);

case BaseFont.FONT_TYPE_DOCUMENT:
return convertDocumentFont(text);

case BaseFont.FONT_TYPE_TTUNI:
return convertTrueTypeUnicodeFont(text, options);

default:
return convertType3Font(text);
}
}

// Converts Type 3 font text to bytes
private byte[] convertType3Font(String text) {
return baseFont.convertToBytes(text);
}

// Converts Type 1 or TrueType font text to bytes
private byte[] convertType1OrTrueTypeFont(String text) {
byte[] bytes = baseFont.convertToBytes(text);
recordUsedCharacters(bytes);
return bytes;
}

// Records characters that have been used
private void recordUsedCharacters(byte[] bytes) {
for (byte b : bytes) {
shortTag[b & 0xff] = 1;
}
}

// Converts CJK font text to bytes
private byte[] convertCjkFont(String text) {
recordCjkCharacters(text);
return baseFont.convertToBytes(text);
}

// Records CJK characters that have been used
private void recordCjkCharacters(String text) {
for (int i = 0; i < text.length(); i++) {
int cidCode = cjkFont.getCidCode(text.charAt(i));
cjkTag.put(cidCode, 0);
}
}

// Converts document font text to bytes
private byte[] convertDocumentFont(String text) {
return baseFont.convertToBytes(text);
}

// Converts TrueType Unicode font text to bytes
private byte[] convertTrueTypeUnicodeFont(String text, TextRenderingOptions options) {
try {
if (symbolic) {
return convertSymbolicFont(text);
}
case BaseFont.FONT_TYPE_CJK: {
int len = text.length();
for (int k = 0; k < len; ++k) {
cjkTag.put(cjkFont.getCidCode(text.charAt(k)), 0);
}
b = baseFont.convertToBytes(text);
break;

// Handle IVS (Ideographic Variation Sequence) fonts
if (mayContainIVS(text)) {
return handleIvsText(text, text.length(), 0);
}
case BaseFont.FONT_TYPE_DOCUMENT: {
b = baseFont.convertToBytes(text);
break;

// Use Fop glyph processor if applicable
if (shouldUseFopGlyphProcessor(options)) {
String fileName = ((TrueTypeFontUnicode) getBaseFont()).fileName;
return FopGlyphProcessor.convertToBytesWithGlyphs(
ttu, text, fileName, longTag, options.getDocumentLanguage()
);
}
case BaseFont.FONT_TYPE_TTUNI: {
try {
int len = text.length();
int[] metrics = null;
char[] glyph = new char[len];
int i = 0;
if (symbolic) {
b = PdfEncodings.convertToBytes(text, "symboltt");
len = b.length;
for (int k = 0; k < len; ++k) {
metrics = ttu.getMetricsTT(b[k] & 0xff);
if (metrics == null) {
continue;
}
longTag.put(metrics[0],
new int[]{metrics[0], metrics[1], ttu.getUnicodeDifferences(b[k] & 0xff)});
glyph[i++] = (char) metrics[0];
}
String s = new String(glyph, 0, i);
b = s.getBytes(CJKFont.CJK_ENCODING);

} else {
String fileName = ((TrueTypeFontUnicode) getBaseFont()).fileName;
if (options.isGlyphSubstitutionEnabled() && FopGlyphProcessor.isFopSupported()
&& (fileName != null && fileName.length() > 0
&& (fileName.contains(".ttf") || fileName.contains(".TTF")))) {
return FopGlyphProcessor.convertToBytesWithGlyphs(ttu, text, fileName, longTag,
options.getDocumentLanguage());
} else {
return convertToBytesWithGlyphs(text);
}
}
} catch (UnsupportedEncodingException e) {
throw new ExceptionConverter(e);
}
break;

// Default glyph conversion
return convertToBytesWithGlyphs(text);
} catch (UnsupportedEncodingException e) {
throw new ExceptionConverter(e);
}
}

// Converts symbolic font text to bytes
private byte[] convertSymbolicFont(String text) throws UnsupportedEncodingException {
byte[] symbolBytes = PdfEncodings.convertToBytes(text, "symboltt");
char[] glyphCodes = extractGlyphCodes(symbolBytes);
String glyphString = new String(glyphCodes);
return glyphString.getBytes(CJKFont.CJK_ENCODING);
}

// Extracts glyph codes from symbol bytes and records metrics
private char[] extractGlyphCodes(byte[] symbolBytes) {
char[] glyphCodes = new char[symbolBytes.length];
int glyphCount = 0;

for (byte b : symbolBytes) {
int[] metrics = ttu.getMetricsTT(b & 0xff);
if (metrics == null) {
continue;
}

int glyphCode = metrics[0];
int width = metrics[1];
int unicodeDiff = ttu.getUnicodeDifferences(b & 0xff);

longTag.put(glyphCode, new int[]{glyphCode, width, unicodeDiff});
glyphCodes[glyphCount++] = (char) glyphCode;
}
return b;

return java.util.Arrays.copyOf(glyphCodes, glyphCount);
}

// Determines whether to use Fop glyph processor
private boolean shouldUseFopGlyphProcessor(TextRenderingOptions options) {
if (!options.isGlyphSubstitutionEnabled() || !FopGlyphProcessor.isFopSupported()) {
return false;
}

String fileName = ((TrueTypeFontUnicode) getBaseFont()).fileName;
return fileName != null
&& !fileName.isEmpty()
&& isTrueTypeFile(fileName);
}

// Checks if the file is a TrueType font file
private boolean isTrueTypeFile(String fileName) {
String lowerFileName = fileName.toLowerCase();
return lowerFileName.endsWith(".ttf");
}

private static boolean isVariationSelector(int codePoint) {
return (codePoint >= 0xFE00 && codePoint <= 0xFE0F)
|| (codePoint >= 0xE0100 && codePoint <= 0xE01EF);
}

/**
* Quickly determine whether the text may contain IVS (to decide whether to use the IVS dedicated path)
* Note: This means "may contain," not "must contain"—err on the side of caution to avoid omissions
*/
private static boolean mayContainIVS(String text) {
if (text == null) return false;

for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);

if (c >= '\uFE00' && c <= '\uFE0F') {
return true;
}

if (c >= '\udb40' && c <= '\udb43') {
return true;
}
}
return false;
}

private byte[] convertCharsToBytes(char[] chars) {
byte[] result = new byte[chars.length * 2];

for (int i = 0; i < chars.length; ++i) {
result[2 * i] = (byte) (chars[i] / 256);
result[2 * i + 1] = (byte) (chars[i] % 256);
}

return result;
}

private byte[] convertToBytesWithGlyphs(String text) throws UnsupportedEncodingException {
Expand Down Expand Up @@ -380,4 +488,122 @@ public boolean isSubset() {
public void setSubset(boolean subset) {
this.subset = subset;
}

/**
* handle ivs text
*/
private byte[] handleIvsText(String text, int len, int startIndex) {
char[] glyph = new char[len * 2];
int glyphIndex = startIndex;
int k = 0;

while (k < len) {
CodePointInfo baseChar = parseCodePoint(text, k, len);
CodePointInfo vsChar = parseVariationSelector(text, k + baseChar.charCount, len);
int skipCount = baseChar.charCount;
if (vsChar != null) {
glyphIndex = addIvsGlyph(baseChar.codePoint, vsChar.codePoint, glyph, glyphIndex);
skipCount += vsChar.charCount;
} else {
glyphIndex = addDefaultGlyph(baseChar.codePoint, glyph, glyphIndex);
}
k += skipCount;
}

glyph = Arrays.copyOfRange(glyph, 0, glyphIndex);
return convertCharsToBytes(glyph);
}

private CodePointInfo parseCodePoint(String text, int index, int len) {
if (index < len - 1
&& Character.isHighSurrogate(text.charAt(index))
&& Character.isLowSurrogate(text.charAt(index + 1))) {
// Surrogate pair
int codePoint = Character.toCodePoint(text.charAt(index), text.charAt(index + 1));
return new CodePointInfo(codePoint, 2);
} else {
// BMP
return new CodePointInfo(text.charAt(index), 1);
}
}

private CodePointInfo parseVariationSelector(String text, int index, int len) {
if (index >= len) {
return null;
}

char currentChar = text.charAt(index);

// single char IVS
if (isVariationSelector(currentChar)) {
return new CodePointInfo(currentChar, 1);
}

// surrogate pair IVS
if (index < len - 1
&& Character.isHighSurrogate(currentChar)
&& Character.isLowSurrogate(text.charAt(index + 1))) {
int codePoint = Character.toCodePoint(currentChar, text.charAt(index + 1));
if (isVariationSelector(codePoint)) {
return new CodePointInfo(codePoint, 2);
}
}

return null;
}

private int addIvsGlyph(int baseCp, int vsCp, char[] glyph, int glyphIndex) {
int[] format14Metrics = this.ttu.getFormat14MetricsTT(baseCp, vsCp);

if (format14Metrics != null) {
int glyphId = format14Metrics[0];
cacheGlyphMetrics(glyphId, format14Metrics[1], baseCp, vsCp);
glyph[glyphIndex] = (char) glyphId;
return glyphIndex + 1;
}

// fallback
return addDefaultGlyph(baseCp, glyph, glyphIndex);
}

private int addDefaultGlyph(int codePoint, char[] glyph, int glyphIndex) {
int[] metrics = this.ttu.getMetricsTT(codePoint);

if (metrics != null) {
int glyphId = metrics[0];
cacheGlyphMetrics(glyphId, metrics[1], codePoint);
glyph[glyphIndex] = (char) glyphId;
return glyphIndex + 1;
}

return glyphIndex;
}

/**
* cache IVS glyph metrics info
*/
private void cacheGlyphMetrics(int glyphId, int width, int baseCp) {
if (!this.longTag.containsKey(glyphId)) {
this.longTag.put(glyphId, new int[]{glyphId, width, baseCp});
}
}

/**
* cache IVS glyph metrics info
*/
private void cacheGlyphMetrics(int glyphId, int width, int baseCp, int vsCp) {
if (!this.longTag.containsKey(glyphId)) {
this.longTag.put(glyphId, new int[]{glyphId, width, baseCp, vsCp});
}
}

private static class CodePointInfo {
final int codePoint;
final int charCount;

CodePointInfo(int codePoint, int charCount) {
this.codePoint = codePoint;
this.charCount = charCount;
}
}
}
Loading