Type1Font.java
/*
* $Id: Type1Font.java 4065 2009-09-16 23:09:11Z psoares33 $
*
* Copyright 2001-2006 Paulo Soares
*
* The contents of this file are subject to the Mozilla Public License Version 1.1
* (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the License.
*
* The Original Code is 'iText, a free JAVA-PDF library'.
*
* The Initial Developer of the Original Code is Bruno Lowagie. Portions created by
* the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie.
* All Rights Reserved.
* Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer
* are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved.
*
* Contributor(s): all the names of the contributors are added in the source code
* where applicable.
*
* Alternatively, the contents of this file may be used under the terms of the
* LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the
* provisions of LGPL are applicable instead of those above. If you wish to
* allow use of your version of this file only under the terms of the LGPL
* License and not to allow others to use your version of this file under
* the MPL, indicate your decision by deleting the provisions above and
* replace them with the notice and other provisions required by the LGPL.
* If you do not delete the provisions above, a recipient may use your version
* of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE.
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MPL as stated above or under the terms of the GNU
* Library General Public License as published by the Free Software Foundation;
* either version 2 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more
* details.
*
* If you didn't download this code from the following link, you should check if
* you aren't using an obsolete version:
* https://github.com/LibrePDF/OpenPDF
*/
package org.openpdf.text.pdf;
import org.openpdf.text.Document;
import org.openpdf.text.DocumentException;
import org.openpdf.text.error_messages.MessageLocalization;
import org.openpdf.text.pdf.fonts.FontsResourceAnchor;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
/**
* Reads a Type1 font
*
* @author Paulo Soares (psoares@consiste.pt)
*/
class Type1Font extends BaseFont {
/**
* Types of records in a PFB file. ASCII is 1 and BINARY is 2. They have to appear in the PFB file in this
* sequence.
*/
private static final int[] PFB_TYPES = {1, 2, 1};
private static FontsResourceAnchor resourceAnchor;
/**
* The PFB file if the input was made with a <CODE>byte</CODE> array.
*/
protected byte[] pfb;
/**
* The Postscript font name.
*/
private String FontName;
/**
* The full name of the font.
*/
private String FullName;
/**
* The family name of the font.
*/
private String FamilyName;
/**
* The weight of the font: normal, bold, etc.
*/
private String Weight = "";
/**
* The italic angle of the font, usually 0.0 or negative.
*/
private float ItalicAngle = 0.0f;
/**
* <CODE>true</CODE> if all the characters have the same
* width.
*/
private boolean IsFixedPitch = false;
/**
* The character set of the font.
*/
private String CharacterSet;
/**
* The llx of the FontBox.
*/
private int llx = -50;
/**
* The lly of the FontBox.
*/
private int lly = -200;
/**
* The lurx of the FontBox.
*/
private int urx = 1000;
/**
* The ury of the FontBox.
*/
private int ury = 900;
/**
* The underline position.
*/
private int UnderlinePosition = -100;
/**
* The underline thickness.
*/
private int UnderlineThickness = 50;
/**
* The font's encoding name. This encoding is 'StandardEncoding' or 'AdobeStandardEncoding' for a font that can be
* totally encoded according to the characters names. For all other names the font is treated as symbolic.
*/
private String EncodingScheme = "FontSpecific";
/**
* A variable.
*/
private int CapHeight = 700;
/**
* A variable.
*/
private int XHeight = 480;
/**
* A variable.
*/
private int Ascender = 800;
/**
* A variable.
*/
private int Descender = -200;
/**
* A variable.
*/
private int StdHW;
/**
* A variable.
*/
private int StdVW = 80;
/**
* Represents the section CharMetrics in the AFM file. Each value of this array contains a <CODE>Object[4]</CODE>
* with an Integer, Integer, String and int[]. This is the code, width, name and char bbox. The key is the name of
* the char and also an Integer with the char number.
*/
private Map<Object, Object[]> CharMetrics = new HashMap<>();
/**
* Represents the section KernPairs in the AFM file. The key is the name of the first character and the value is a
* <CODE>Object[]</CODE> with 2 elements for each kern pair. Position 0 is the name of the second character and
* position 1 is the kerning distance. This is repeated for all the pairs.
*/
private Map<String, Object[]> KernPairs = new HashMap<>();
/**
* The file in use.
*/
private String fileName;
/**
* <CODE>true</CODE> if this font is one of the 14 built in fonts.
*/
private boolean builtinFont = false;
/**
* Creates a new Type1 font.
*
* @param ttfAfm the AFM file if the input is made with a <CODE>byte</CODE> array
* @param pfb the PFB file if the input is made with a <CODE>byte</CODE> array
* @param afmFile the name of one of the 14 built-in fonts or the location of an AFM file. The file must end in
* '.afm'
* @param enc the encoding to be applied to this font
* @param emb true if the font is to be embedded in the PDF
* @throws DocumentException the AFM file is invalid
* @throws IOException the AFM file could not be read
* @since 2.1.5
*/
Type1Font(String afmFile, String enc, boolean emb, byte[] ttfAfm, byte[] pfb, boolean forceRead)
throws DocumentException, IOException {
if (emb && ttfAfm != null && pfb == null) {
throw new DocumentException(
MessageLocalization.getComposedMessage("two.byte.arrays.are.needed.if.the.type1.font.is.embedded"));
}
if (emb && ttfAfm != null) {
this.pfb = pfb;
}
encoding = enc;
embedded = emb;
fileName = afmFile;
fontType = FONT_TYPE_T1;
RandomAccessFileOrArray rf = null;
InputStream is = null;
if (BuiltinFonts14.containsKey(afmFile)) {
embedded = false;
builtinFont = true;
byte[] buf = new byte[1024];
try {
if (resourceAnchor == null) {
resourceAnchor = new FontsResourceAnchor();
}
is = getResourceStream(RESOURCE_PATH + afmFile + ".afm");
if (is == null) {
String msg = MessageLocalization.getComposedMessage("1.not.found.as.resource", afmFile);
System.err.println(msg);
throw new DocumentException(msg);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
while (true) {
int size = is.read(buf);
if (size < 0) {
break;
}
out.write(buf, 0, size);
}
buf = out.toByteArray();
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {
// empty on purpose
}
}
}
try {
rf = new RandomAccessFileOrArray(buf);
process(rf);
} finally {
if (rf != null) {
try {
rf.close();
} catch (Exception e) {
// empty on purpose
}
}
}
} else if (afmFile.toLowerCase().endsWith(".afm")) {
try {
if (ttfAfm == null) {
rf = new RandomAccessFileOrArray(afmFile, forceRead, Document.plainRandomAccess);
} else {
rf = new RandomAccessFileOrArray(ttfAfm);
}
process(rf);
} finally {
if (rf != null) {
try {
rf.close();
} catch (Exception e) {
// empty on purpose
}
}
}
} else {
throw new DocumentException(
MessageLocalization.getComposedMessage("1.is.not.an.afm.or.pfm.font.file", afmFile));
}
EncodingScheme = EncodingScheme.trim();
if (EncodingScheme.equals("AdobeStandardEncoding") || EncodingScheme.equals("StandardEncoding")) {
fontSpecific = false;
}
if (!encoding.startsWith("#")) {
PdfEncodings.convertToBytes(" ", enc); // check if the encoding exists
}
createEncoding();
}
/**
* Gets the width from the font according to the <CODE>name</CODE> or, if the <CODE>name</CODE> is null, meaning it
* is a symbolic font, the char <CODE>c</CODE>.
*
* @param c the char if the font is symbolic
* @param name the glyph name
* @return the width of the char
*/
int getRawWidth(int c, String name) {
Object[] metrics;
if (name == null) { // font specific
metrics = CharMetrics.get(c);
} else {
if (name.equals(".notdef")) {
return 0;
}
metrics = CharMetrics.get(name);
}
if (metrics != null) {
return (Integer) (metrics[1]);
}
return 0;
}
/**
* Reads the font metrics
*
* @param rf the AFM file
* @throws DocumentException the AFM file is invalid
* @throws IOException the AFM file could not be read
*/
public void process(RandomAccessFileOrArray rf) throws DocumentException, IOException {
String line;
boolean isMetrics = false;
label:
while ((line = rf.readLine()) != null) {
StringTokenizer tok = new StringTokenizer(line, " ,\n\r\t\f");
if (!tok.hasMoreTokens()) {
continue;
}
String ident = tok.nextToken();
switch (ident) {
case "FontName":
FontName = tok.nextToken("\u00ff").substring(1);
break;
case "FullName":
FullName = tok.nextToken("\u00ff").substring(1);
break;
case "FamilyName":
FamilyName = tok.nextToken("\u00ff").substring(1);
break;
case "Weight":
Weight = tok.nextToken("\u00ff").substring(1);
break;
case "ItalicAngle":
ItalicAngle = Float.parseFloat(tok.nextToken());
break;
case "IsFixedPitch":
IsFixedPitch = tok.nextToken().equals("true");
break;
case "CharacterSet":
CharacterSet = tok.nextToken("\u00ff").substring(1);
break;
case "FontBBox":
llx = (int) Float.parseFloat(tok.nextToken());
lly = (int) Float.parseFloat(tok.nextToken());
urx = (int) Float.parseFloat(tok.nextToken());
ury = (int) Float.parseFloat(tok.nextToken());
break;
case "UnderlinePosition":
UnderlinePosition = (int) Float.parseFloat(tok.nextToken());
break;
case "UnderlineThickness":
UnderlineThickness = (int) Float.parseFloat(tok.nextToken());
break;
case "EncodingScheme":
EncodingScheme = tok.nextToken("\u00ff").substring(1);
break;
case "CapHeight":
CapHeight = (int) Float.parseFloat(tok.nextToken());
break;
case "XHeight":
XHeight = (int) Float.parseFloat(tok.nextToken());
break;
case "Ascender":
Ascender = (int) Float.parseFloat(tok.nextToken());
break;
case "Descender":
Descender = (int) Float.parseFloat(tok.nextToken());
break;
case "StdHW":
StdHW = (int) Float.parseFloat(tok.nextToken());
break;
case "StdVW":
StdVW = (int) Float.parseFloat(tok.nextToken());
break;
case "StartCharMetrics":
isMetrics = true;
break label;
}
}
if (!isMetrics) {
throw new DocumentException(
MessageLocalization.getComposedMessage("missing.startcharmetrics.in.1", fileName));
}
while ((line = rf.readLine()) != null) {
StringTokenizer tok = new StringTokenizer(line);
if (!tok.hasMoreTokens()) {
continue;
}
String ident = tok.nextToken();
if (ident.equals("EndCharMetrics")) {
isMetrics = false;
break;
}
Integer C = -1;
int WX = 250;
String N = "";
int[] B = null;
tok = new StringTokenizer(line, ";");
while (tok.hasMoreTokens()) {
StringTokenizer tokc = new StringTokenizer(tok.nextToken());
if (!tokc.hasMoreTokens()) {
continue;
}
ident = tokc.nextToken();
switch (ident) {
case "C":
C = Integer.valueOf(tokc.nextToken());
break;
case "WX":
WX = (int) Float.parseFloat(tokc.nextToken());
break;
case "N":
N = tokc.nextToken();
break;
case "B":
B = new int[]{Integer.parseInt(tokc.nextToken()),
Integer.parseInt(tokc.nextToken()),
Integer.parseInt(tokc.nextToken()),
Integer.parseInt(tokc.nextToken())};
break;
}
}
Object[] metrics = new Object[]{C, WX, N, B};
if (C >= 0) {
CharMetrics.put(C, metrics);
}
CharMetrics.put(N, metrics);
}
if (isMetrics) {
throw new DocumentException(
MessageLocalization.getComposedMessage("missing.endcharmetrics.in.1", fileName));
}
if (!CharMetrics.containsKey("nonbreakingspace")) {
Object[] space = CharMetrics.get("space");
if (space != null) {
CharMetrics.put("nonbreakingspace", space);
}
}
while ((line = rf.readLine()) != null) {
StringTokenizer tok = new StringTokenizer(line);
if (!tok.hasMoreTokens()) {
continue;
}
String ident = tok.nextToken();
if (ident.equals("EndFontMetrics")) {
return;
}
if (ident.equals("StartKernPairs")) {
isMetrics = true;
break;
}
}
if (!isMetrics) {
throw new DocumentException(
MessageLocalization.getComposedMessage("missing.endfontmetrics.in.1", fileName));
}
while ((line = rf.readLine()) != null) {
StringTokenizer tok = new StringTokenizer(line);
if (!tok.hasMoreTokens()) {
continue;
}
String ident = tok.nextToken();
if (ident.equals("KPX")) {
String first = tok.nextToken();
String second = tok.nextToken();
int width = Integer.parseInt(tok.nextToken());
Object[] relates = KernPairs.get(first);
if (relates == null) {
KernPairs.put(first, new Object[]{second, width});
} else {
int n = relates.length;
Object[] relates2 = new Object[n + 2];
System.arraycopy(relates, 0, relates2, 0, n);
relates2[n] = second;
relates2[n + 1] = width;
KernPairs.put(first, relates2);
}
} else if (ident.equals("EndKernPairs")) {
isMetrics = false;
break;
}
}
if (isMetrics) {
throw new DocumentException(MessageLocalization.getComposedMessage("missing.endkernpairs.in.1", fileName));
}
rf.close();
}
/**
* If the embedded flag is <CODE>false</CODE> or if the font is one of the 14 built in types, it returns
* <CODE>null</CODE>, otherwise the font is read and output in a PdfStream object.
*
* @return the PdfStream containing the font or <CODE>null</CODE>
* @throws DocumentException if there is an error reading the font
* @since 2.1.3
*/
public PdfStream getFullFontStream() throws DocumentException {
if (builtinFont || !embedded) {
return null;
}
RandomAccessFileOrArray rf = null;
try {
String filePfb = fileName.substring(0, fileName.length() - 3) + "pfb";
if (pfb == null) {
rf = new RandomAccessFileOrArray(filePfb, true, Document.plainRandomAccess);
} else {
rf = new RandomAccessFileOrArray(pfb);
}
int fileLength = rf.length();
byte[] st = new byte[fileLength - 18];
int[] lengths = new int[3];
int bytePtr = 0;
for (int k = 0; k < 3; ++k) {
if (rf.read() != 0x80) {
throw new DocumentException(
MessageLocalization.getComposedMessage("start.marker.missing.in.1", filePfb));
}
if (rf.read() != PFB_TYPES[k]) {
throw new DocumentException(
MessageLocalization.getComposedMessage("incorrect.segment.type.in.1", filePfb));
}
int size = rf.read();
size += rf.read() << 8;
size += rf.read() << 16;
size += rf.read() << 24;
lengths[k] = size;
while (size != 0) {
int got = rf.read(st, bytePtr, size);
if (got < 0) {
throw new DocumentException(
MessageLocalization.getComposedMessage("premature.end.in.1", filePfb));
}
bytePtr += got;
size -= got;
}
}
return new StreamFont(st, lengths, compressionLevel);
} catch (Exception e) {
throw new DocumentException(e);
} finally {
if (rf != null) {
try {
rf.close();
} catch (Exception e) {
// empty on purpose
}
}
}
}
/**
* Generates the font descriptor for this font or <CODE>null</CODE> if it is one of the 14 built in fonts.
*
* @param fontStream the indirect reference to a PdfStream containing the font or <CODE>null</CODE>
* @return the PdfDictionary containing the font descriptor or <CODE>null</CODE>
*/
private PdfDictionary getFontDescriptor(PdfIndirectReference fontStream) {
if (builtinFont) {
return null;
}
PdfDictionary dic = new PdfDictionary(PdfName.FONTDESCRIPTOR);
dic.put(PdfName.ASCENT, new PdfNumber(Ascender));
dic.put(PdfName.CAPHEIGHT, new PdfNumber(CapHeight));
dic.put(PdfName.DESCENT, new PdfNumber(Descender));
dic.put(PdfName.FONTBBOX, new PdfRectangle(llx, lly, urx, ury));
dic.put(PdfName.FONTNAME, new PdfName(FontName));
dic.put(PdfName.ITALICANGLE, new PdfNumber(ItalicAngle));
dic.put(PdfName.STEMV, new PdfNumber(StdVW));
if (fontStream != null) {
dic.put(PdfName.FONTFILE, fontStream);
}
int flags = 0;
if (IsFixedPitch) {
flags |= 1;
}
flags |= fontSpecific ? 4 : 32;
if (ItalicAngle < 0) {
flags |= 64;
}
if (FontName.contains("Caps") || FontName.endsWith("SC")) {
flags |= 131072;
}
if (Weight.equals("Bold")) {
flags |= 262144;
}
dic.put(PdfName.FLAGS, new PdfNumber(flags));
return dic;
}
/**
* Generates the font dictionary for this font.
*
* @param firstChar the first valid character
* @param lastChar the last valid character
* @param shortTag a 256 bytes long <CODE>byte</CODE> array where each unused byte is represented by 0
* @param fontDescriptor the indirect reference to a PdfDictionary containing the font descriptor or
* <CODE>null</CODE>
* @return the PdfDictionary containing the font dictionary
*/
private PdfDictionary getFontBaseType(PdfIndirectReference fontDescriptor, int firstChar, int lastChar,
byte[] shortTag) {
PdfDictionary dic = new PdfDictionary(PdfName.FONT);
dic.put(PdfName.SUBTYPE, PdfName.TYPE1);
dic.put(PdfName.BASEFONT, new PdfName(FontName));
boolean stdEncoding = encoding.equals("Cp1252") || encoding.equals("MacRoman");
if (!fontSpecific || specialMap != null) {
for (int k = firstChar; k <= lastChar; ++k) {
if (!differences[k].equals(notdef)) {
firstChar = k;
break;
}
}
if (stdEncoding) {
dic.put(PdfName.ENCODING,
encoding.equals("Cp1252") ? PdfName.WIN_ANSI_ENCODING : PdfName.MAC_ROMAN_ENCODING);
} else {
PdfDictionary enc = new PdfDictionary(PdfName.ENCODING);
PdfArray dif = new PdfArray();
boolean gap = true;
for (int k = firstChar; k <= lastChar; ++k) {
if (shortTag[k] != 0) {
if (gap) {
dif.add(new PdfNumber(k));
gap = false;
}
dif.add(new PdfName(differences[k]));
} else {
gap = true;
}
}
enc.put(PdfName.DIFFERENCES, dif);
dic.put(PdfName.ENCODING, enc);
}
}
if (specialMap != null || forceWidthsOutput || !(builtinFont && (fontSpecific || stdEncoding))) {
dic.put(PdfName.FIRSTCHAR, new PdfNumber(firstChar));
dic.put(PdfName.LASTCHAR, new PdfNumber(lastChar));
PdfArray wd = new PdfArray();
for (int k = firstChar; k <= lastChar; ++k) {
if (shortTag[k] == 0) {
wd.add(new PdfNumber(0));
} else {
wd.add(new PdfNumber(widths[k]));
}
}
dic.put(PdfName.WIDTHS, wd);
}
if (!builtinFont && fontDescriptor != null) {
dic.put(PdfName.FONTDESCRIPTOR, fontDescriptor);
}
return dic;
}
/**
* Outputs to the writer the font dictionaries and streams.
*
* @param writer the writer for this document
* @param ref the font indirect reference
* @param params several parameters that depend on the font type
* @throws IOException on error
* @throws DocumentException error in generating the object
*/
void writeFont(PdfWriter writer, PdfIndirectReference ref, Object[] params) throws DocumentException, IOException {
int firstChar = (Integer) params[0];
int lastChar = (Integer) params[1];
byte[] shortTag = (byte[]) params[2];
boolean subsetp = (Boolean) params[3] && subset;
if (!subsetp) {
firstChar = 0;
lastChar = shortTag.length - 1;
for (int k = 0; k < shortTag.length; ++k) {
shortTag[k] = 1;
}
}
PdfIndirectReference ind_font = null;
PdfObject pobj = null;
PdfIndirectObject obj = null;
pobj = getFullFontStream();
if (pobj != null) {
obj = writer.addToBody(pobj);
ind_font = obj.getIndirectReference();
}
pobj = getFontDescriptor(ind_font);
if (pobj != null) {
obj = writer.addToBody(pobj);
ind_font = obj.getIndirectReference();
}
pobj = getFontBaseType(ind_font, firstChar, lastChar, shortTag);
writer.addToBody(pobj, ref);
}
/**
* Gets the font parameter identified by <CODE>key</CODE>. Valid values for <CODE>key</CODE> are
* <CODE>ASCENT</CODE>, <CODE>CAPHEIGHT</CODE>, <CODE>DESCENT</CODE>,
* <CODE>ITALICANGLE</CODE>, <CODE>BBOXLLX</CODE>, <CODE>BBOXLLY</CODE>, <CODE>BBOXURX</CODE>
* and <CODE>BBOXURY</CODE>.
*
* @param key the parameter to be extracted
* @param fontSize the font size in points
* @return the parameter in points
*/
public float getFontDescriptor(int key, float fontSize) {
switch (key) {
case AWT_ASCENT:
case ASCENT:
return Ascender * fontSize / 1000;
case CAPHEIGHT:
return CapHeight * fontSize / 1000;
case AWT_DESCENT:
case DESCENT:
return Descender * fontSize / 1000;
case ITALICANGLE:
return ItalicAngle;
case BBOXLLX:
return llx * fontSize / 1000;
case BBOXLLY:
return lly * fontSize / 1000;
case BBOXURX:
return urx * fontSize / 1000;
case BBOXURY:
return ury * fontSize / 1000;
case AWT_LEADING:
return 0;
case AWT_MAXADVANCE:
return (urx - llx) * fontSize / 1000;
case UNDERLINE_POSITION:
return UnderlinePosition * fontSize / 1000;
case UNDERLINE_THICKNESS:
return UnderlineThickness * fontSize / 1000;
}
return 0;
}
/**
* Gets the postscript font name.
*
* @return the postscript font name
*/
public String getPostscriptFontName() {
return FontName;
}
protected int[] getRawCharBBox(int c, String name) {
Object[] metrics;
if (name == null) { // font specific
metrics = CharMetrics.get(c);
} else {
if (name.equals(".notdef")) {
return null;
}
metrics = CharMetrics.get(name);
}
if (metrics != null) {
return ((int[]) (metrics[3]));
}
return null;
}
}