Java tutorial
/* * Copyright 2017 Emmeran Seehuber * Licensed 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 de.rototor.pdfbox.graphics2d; import org.apache.fontbox.ttf.TrueTypeCollection; import org.apache.fontbox.ttf.TrueTypeFont; import org.apache.pdfbox.io.IOUtils; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.util.Matrix; import java.awt.*; import java.awt.font.LineMetrics; import java.awt.font.TextAttribute; import java.io.*; import java.text.AttributedCharacterIterator; import java.util.*; import java.util.List; /** * Default implementation to draw fonts. You can reuse instances of this class * within a PDDocument for more then one {@link PdfBoxGraphics2D}. * * Just ensure that you call close after you closed the PDDocument to free any * temporary files. */ public class PdfBoxGraphics2DFontTextDrawer implements IPdfBoxGraphics2DFontTextDrawer, Closeable { /** * Close / delete all resources associated with this drawer. This mainly means * deleting all temporary files. You can not use this object after a call to * close. * * Calling close multiple times does nothing. */ @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void close() { for (File tempFile : tempFiles) tempFile.delete(); tempFiles.clear(); fontFiles.clear(); fontMap.clear(); } private static class FontEntry { String overrideName; File file; } private final List<FontEntry> fontFiles = new ArrayList<FontEntry>(); private final List<File> tempFiles = new ArrayList<File>(); private final Map<String, PDFont> fontMap = new HashMap<String, PDFont>(); /** * Register a font. If possible, try to use a font file, i.e. * {@link #registerFont(String,File)}. This method will lead to the creation of * a temporary file which stores the font data. * * @param fontName * the name of the font to use. If null, the name is taken from the * font. * @param fontStream * the input stream of the font. This file must be a ttf/otf file! * You have to close the stream outside, this method will not close * the stream. * @throws IOException * when something goes wrong with reading the font or writing the * font to the content stream of the PDF: */ @SuppressWarnings("WeakerAccess") public void registerFont(String fontName, InputStream fontStream) throws IOException { File fontFile = File.createTempFile("pdfboxgfx2dfont", ".ttf"); FileOutputStream out = new FileOutputStream(fontFile); try { IOUtils.copy(fontStream, out); } finally { out.close(); } fontFile.deleteOnExit(); tempFiles.add(fontFile); registerFont(fontName, fontFile); } /** * Register a font. * * @param fontName * the name of the font to use. If null, the name is taken from the * font. * @param fontFile * the font file. This file must exist for the live time of this * object, as the font data will be read lazy on demand */ @SuppressWarnings("WeakerAccess") public void registerFont(String fontName, File fontFile) { if (!fontFile.exists()) throw new IllegalArgumentException("Font " + fontFile + " does not exist!"); FontEntry entry = new FontEntry(); entry.overrideName = fontName; entry.file = fontFile; fontFiles.add(entry); } /** * Override for registerFont(null,fontFile) * * @param fontFile * the font file */ @SuppressWarnings("WeakerAccess") public void registerFont(File fontFile) { registerFont(null, fontFile); } /** * Override for registerFont(null,fontStream) * * @param fontStream * the font file * @throws IOException * when something goes wrong with reading the font or writing the * font to the content stream of the PDF: */ @SuppressWarnings("WeakerAccess") public void registerFont(InputStream fontStream) throws IOException { registerFont(null, fontStream); } /** * Register a font which is already associated with the PDDocument * * @param name * the name of the font as returned by * {@link java.awt.Font#getFontName()}. This name is used for the * mapping the java.awt.Font to this PDFont. * @param font * the PDFont to use. This font must be loaded in the current * document. */ @SuppressWarnings("WeakerAccess") public void registerFont(String name, PDFont font) { fontMap.put(name, font); } /** * @return true if the font mapping is populated on demand. This is usually only * the case if this class has been derived. The default implementation * just checks for this. */ @SuppressWarnings("WeakerAccess") protected boolean hasDynamicFontMapping() { return getClass() != PdfBoxGraphics2DFontTextDrawer.class; } @Override public boolean canDrawText(AttributedCharacterIterator iterator, IFontTextDrawerEnv env) throws IOException, FontFormatException { /* * When no font is registered we can not display the text using a font... */ if (fontMap.size() == 0 && fontFiles.size() == 0 && !hasDynamicFontMapping()) return false; boolean run = true; StringBuilder sb = new StringBuilder(); while (run) { Font attributeFont = (Font) iterator.getAttribute(TextAttribute.FONT); if (attributeFont == null) attributeFont = env.getFont(); if (mapFont(attributeFont, env) == null) return false; /* * We can not do a Background on the text currently. */ if (iterator.getAttribute(TextAttribute.BACKGROUND) != null) return false; boolean isStrikeThrough = TextAttribute.STRIKETHROUGH_ON .equals(iterator.getAttribute(TextAttribute.STRIKETHROUGH)); boolean isUnderline = TextAttribute.UNDERLINE_ON.equals(iterator.getAttribute(TextAttribute.UNDERLINE)); boolean isLigatures = TextAttribute.LIGATURES_ON.equals(iterator.getAttribute(TextAttribute.LIGATURES)); if (isStrikeThrough || isUnderline || isLigatures) return false; run = iterateRun(iterator, sb); String s = sb.toString(); int l = s.length(); for (int i = 0; i < l;) { int codePoint = s.codePointAt(i); switch (Character.getDirectionality(codePoint)) { /* * We can handle normal LTR. */ case Character.DIRECTIONALITY_LEFT_TO_RIGHT: case Character.DIRECTIONALITY_EUROPEAN_NUMBER: case Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR: case Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR: case Character.DIRECTIONALITY_WHITESPACE: case Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR: case Character.DIRECTIONALITY_NONSPACING_MARK: case Character.DIRECTIONALITY_BOUNDARY_NEUTRAL: case Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR: case Character.DIRECTIONALITY_SEGMENT_SEPARATOR: case Character.DIRECTIONALITY_OTHER_NEUTRALS: case Character.DIRECTIONALITY_ARABIC_NUMBER: break; case Character.DIRECTIONALITY_RIGHT_TO_LEFT: case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: /* * We can not handle this */ return false; default: /* * Default: We can not handle this */ return false; } if (!attributeFont.canDisplay(codePoint)) return false; i += Character.charCount(codePoint); } } return true; } @Override public void drawText(AttributedCharacterIterator iterator, IFontTextDrawerEnv env) throws IOException, FontFormatException { PDPageContentStream contentStream = env.getContentStream(); contentStream.beginText(); Matrix textMatrix = new Matrix(); textMatrix.scale(1, -1); contentStream.setTextMatrix(textMatrix); StringBuilder sb = new StringBuilder(); boolean run = true; while (run) { Font attributeFont = (Font) iterator.getAttribute(TextAttribute.FONT); if (attributeFont == null) attributeFont = env.getFont(); Number fontSize = ((Number) iterator.getAttribute(TextAttribute.SIZE)); if (fontSize != null) attributeFont = attributeFont.deriveFont(fontSize.floatValue()); PDFont font = applyFont(attributeFont, env); Paint paint = (Paint) iterator.getAttribute(TextAttribute.FOREGROUND); if (paint == null) paint = env.getPaint(); /* * Apply the paint */ env.applyPaint(paint); boolean isStrikeThrough = TextAttribute.STRIKETHROUGH_ON .equals(iterator.getAttribute(TextAttribute.STRIKETHROUGH)); boolean isUnderline = TextAttribute.UNDERLINE_ON.equals(iterator.getAttribute(TextAttribute.UNDERLINE)); boolean isLigatures = TextAttribute.LIGATURES_ON.equals(iterator.getAttribute(TextAttribute.LIGATURES)); run = iterateRun(iterator, sb); String text = sb.toString(); /* * If we force the text write we may encounter situations where the font can not * display the characters. PDFBox will throw an exception in this case. We will * just silently ignore the text and not display it instead. */ try { showTextOnStream(env, contentStream, attributeFont, font, isStrikeThrough, isUnderline, isLigatures, text); } catch (IllegalArgumentException e) { if (font instanceof PDType1Font && !font.isEmbedded()) { /* * We tried to use a builtin default font, but it does not have the needed * characters. So we use a embedded font as fallback. */ try { if (fallbackFontUnknownEncodings == null) fallbackFontUnknownEncodings = findFallbackFont(env); if (fallbackFontUnknownEncodings != null) { env.getContentStream().setFont(fallbackFontUnknownEncodings, attributeFont.getSize2D()); showTextOnStream(env, contentStream, attributeFont, fallbackFontUnknownEncodings, isStrikeThrough, isUnderline, isLigatures, text); e = null; } } catch (IllegalArgumentException e1) { e = e1; } } if (e != null) System.err.println("PDFBoxGraphics: Can not map text " + text + " with font " + attributeFont.getFontName() + ": " + e.getMessage()); } } contentStream.endText(); } private PDFont fallbackFontUnknownEncodings; private PDFont findFallbackFont(IFontTextDrawerEnv env) throws IOException { /* * We search for the right font in the system folders... We try to use * LucidaSansRegular and if not found Arial, because this fonts often exists. We * use the Java default font as fallback. * * Normally this method is only used and called if a default font misses some * special characters, e.g. Hebrew or Arabic characters. */ String javaHome = System.getProperty("java.home", "."); String javaFontDir = javaHome + "/lib/fonts"; String windir = System.getenv("WINDIR"); if (windir == null) windir = javaFontDir; File[] paths = new File[] { new File(new File(windir), "fonts"), new File(System.getProperty("user.dir", ".")), new File("/Library/Fonts"), new File("/usr/share/fonts/truetype"), new File("/usr/share/fonts/truetype/dejavu"), new File("/usr/share/fonts/truetype/liberation"), new File("/usr/share/fonts/truetype/noto"), new File(javaFontDir) }; File foundFontFile = null; for (String fontFileName : new String[] { "LucidaSansRegular.ttf", "arial.ttf", "Arial.ttf", "DejaVuSans.ttf", "LiberationMono-Regular.ttf", "NotoSerif-Regular.ttf" }) { for (File path : paths) { File arialFile = new File(path, fontFileName); if (arialFile.exists()) { foundFontFile = arialFile; break; } } if (foundFontFile != null) break; } /* * If we did not find any font, we can't do anything :( */ if (foundFontFile == null) return null; return PDType0Font.load(env.getDocument(), foundFontFile); } private void showTextOnStream(IFontTextDrawerEnv env, PDPageContentStream contentStream, Font attributeFont, PDFont font, boolean isStrikeThrough, boolean isUnderline, boolean isLigatures, String text) throws IOException { if (isStrikeThrough || isUnderline) { // noinspection unused float stringWidth = font.getStringWidth(text); // noinspection unused LineMetrics lineMetrics = attributeFont.getLineMetrics(text, env.getFontRenderContext()); /* * TODO: We can not draw that yet, we must do that later. While in textmode its * not possible to draw lines... */ } // noinspection StatementWithEmptyBody if (isLigatures) { /* * No idea how to map this ... */ } contentStream.showText(text); } private PDFont applyFont(Font font, IFontTextDrawerEnv env) throws IOException, FontFormatException { PDFont fontToUse = mapFont(font, env); if (fontToUse == null) { /* * If we have no font but are forced to apply a font, we just use the default * builtin PDF font... */ fontToUse = PdfBoxGraphics2DFontTextDrawerDefaultFonts.chooseMatchingHelvetica(font); } env.getContentStream().setFont(fontToUse, font.getSize2D()); return fontToUse; } /** * Try to map the java.awt.Font to a PDFont. * * @param font * the java.awt.Font for which a mapping should be found * @param env * environment of the font mapper * @return the PDFont or null if none can be found. * @throws IOException * when the font can not be loaded * @throws FontFormatException * when the font file can not be loaded */ @SuppressWarnings("WeakerAccess") protected PDFont mapFont(final Font font, final IFontTextDrawerEnv env) throws IOException, FontFormatException { /* * If we have any font registering's, we must perform them now */ for (final FontEntry fontEntry : fontFiles) { if (fontEntry.overrideName == null) { Font javaFont = Font.createFont(Font.TRUETYPE_FONT, fontEntry.file); fontEntry.overrideName = javaFont.getFontName(); } if (fontEntry.file.getName().toLowerCase(Locale.US).endsWith(".ttc")) { TrueTypeCollection collection = new TrueTypeCollection(fontEntry.file); collection.processAllFonts(new TrueTypeCollection.TrueTypeFontProcessor() { @Override public void process(TrueTypeFont ttf) throws IOException { PDFont pdFont = PDType0Font.load(env.getDocument(), ttf, true); fontMap.put(fontEntry.overrideName, pdFont); fontMap.put(pdFont.getName(), pdFont); } }); } else { /* * We load the font using the file. */ PDFont pdFont = PDType0Font.load(env.getDocument(), fontEntry.file); fontMap.put(fontEntry.overrideName, pdFont); } } fontFiles.clear(); return fontMap.get(font.getFontName()); } private boolean iterateRun(AttributedCharacterIterator iterator, StringBuilder sb) { sb.setLength(0); int charCount = iterator.getRunLimit() - iterator.getRunStart(); while (charCount-- >= 0) { char c = iterator.current(); iterator.next(); if (c == AttributedCharacterIterator.DONE) { return false; } else { sb.append(c); } } return true; } }