]> source.dussan.org Git - poi.git/commitdiff
#64867 - Provide PDF rendering with PPTX2PNG
authorAndreas Beeker <kiwiwings@apache.org>
Sun, 8 Nov 2020 21:30:54 +0000 (21:30 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Sun, 8 Nov 2020 21:30:54 +0000 (21:30 +0000)
render text as text, i.e. not as shapes

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1883212 13f79535-47bb-0310-9956-ffa450edef68

13 files changed:
src/java/org/apache/poi/sl/draw/DrawTextParagraph.java
src/multimodule/ooxml/java9/module-info.class
src/multimodule/ooxml/java9/module-info.java
src/multimodule/ooxml/test9/module-info.class
src/multimodule/ooxml/test9/module-info.java
src/ooxml/java/org/apache/poi/xslf/util/PDFFontMapper.java [new file with mode: 0644]
src/ooxml/java/org/apache/poi/xslf/util/PDFFontMapper2.java [new file with mode: 0644]
src/ooxml/java/org/apache/poi/xslf/util/PDFFormat.java
src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java
src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestPPTX2PNG.java
src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java
src/scratchpad/testcases/commons-logging.properties [new file with mode: 0644]
src/scratchpad/testcases/log4j.properties

index c3e2693213f9294656077a29fbd8e008f4a1c3f3..b3d70fff26156fe2f4f348f8e178e0059b5538e0 100644 (file)
@@ -32,8 +32,10 @@ import java.text.AttributedCharacterIterator.Attribute;
 import java.text.AttributedString;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 
 import org.apache.poi.common.usermodel.fonts.FontGroup;
 import org.apache.poi.common.usermodel.fonts.FontGroup.FontGroupRange;
@@ -257,9 +259,18 @@ public class DrawTextParagraph implements Drawable {
 
         DrawFactory fact = DrawFactory.getInstance(graphics);
         StringBuilder text = new StringBuilder();
-        AttributedString at = getAttributedString(graphics, text);
 
-        AttributedCharacterIterator it = at.getIterator();
+        List<AttributedStringData> attList = getAttributedString(graphics, text);
+        AttributedString as = new AttributedString(text.toString());
+        AttributedString asNoCR = new AttributedString(text.toString().replaceAll("[\\r\\n]", " "));
+
+        for (AttributedStringData asd : attList) {
+            as.addAttribute(asd.attribute, asd.value, asd.beginIndex, asd.endIndex);
+            asNoCR.addAttribute(asd.attribute, asd.value, asd.beginIndex, asd.endIndex);
+        }
+
+        AttributedCharacterIterator it = as.getIterator();
+        AttributedCharacterIterator itNoCR = asNoCR.getIterator();
         LineBreakMeasurer measurer = new LineBreakMeasurer(it, graphics.getFontRenderContext());
         for (;;) {
             int startIndex = measurer.getPosition();
@@ -308,7 +319,7 @@ public class DrawTextParagraph implements Drawable {
                 }
             }
 
-            AttributedString str = new AttributedString(it, startIndex, endIndex);
+            AttributedString str = new AttributedString(itNoCR, startIndex, endIndex);
             DrawTextFragment line = fact.getTextFragment(layout, str);
             lines.add(line);
 
@@ -369,10 +380,14 @@ public class DrawTextParagraph implements Drawable {
         // TODO: check font group defaulting to Symbol
         buFont = dfm.getMappedFont(graphics, buFont);
 
+        Map<TextAttribute,Object> att = new HashMap<>();
+        att.put(TextAttribute.FOREGROUND, fgPaint);
+        att.put(TextAttribute.FAMILY, buFont.getTypeface());
+        att.put(TextAttribute.SIZE, fontSize);
+        att.put(TextAttribute.FONT, new Font(att));
+
         AttributedString str = new AttributedString(dfm.mapFontCharset(graphics,buFont,buCharacter));
-        str.addAttribute(TextAttribute.FOREGROUND, fgPaint);
-        str.addAttribute(TextAttribute.FAMILY, buFont.getTypeface());
-        str.addAttribute(TextAttribute.SIZE, fontSize);
+        att.forEach(str::addAttribute);
 
         TextLayout layout = new TextLayout(str.getIterator(), graphics.getFontRenderContext());
         DrawFactory fact = DrawFactory.getInstance(graphics);
@@ -559,8 +574,7 @@ public class DrawTextParagraph implements Drawable {
         };
     }
 
-    protected AttributedString getAttributedString(Graphics2D graphics, StringBuilder text){
-        List<AttributedStringData> attList = new ArrayList<>();
+    protected List<AttributedStringData> getAttributedString(Graphics2D graphics, StringBuilder text) {
         if (text == null) {
             text = new StringBuilder();
         }
@@ -569,6 +583,9 @@ public class DrawTextParagraph implements Drawable {
         DrawFontManager dfm = DrawFactory.getInstance(graphics).getFontManager(graphics);
         assert(dfm != null);
 
+        final Map<Attribute,Object> att = new HashMap<>();
+        final List<AttributedStringData> attList = new ArrayList<>();
+
         for (TextRun run : paragraph){
             String runText = getRenderableText(graphics, run);
             // skip empty runs
@@ -576,66 +593,79 @@ public class DrawTextParagraph implements Drawable {
                 continue;
             }
 
-            // user can pass an custom object to convert fonts
+            att.clear();
 
-            runText = dfm.mapFontCharset(graphics, run.getFontInfo(null), runText);
-            int beginIndex = text.length();
+            // user can pass an custom object to convert fonts
+            FontInfo fontInfo = run.getFontInfo(null);
+            runText = dfm.mapFontCharset(graphics, fontInfo, runText);
+            final int beginIndex = text.length();
             text.append(runText);
-            int endIndex = text.length();
+            final int endIndex = text.length();
 
             PaintStyle fgPaintStyle = run.getFontColor();
             Paint fgPaint = dp.getPaint(graphics, fgPaintStyle);
-            attList.add(new AttributedStringData(TextAttribute.FOREGROUND, fgPaint, beginIndex, endIndex));
+
+            att.put(TextAttribute.FOREGROUND, fgPaint);
 
             Double fontSz = run.getFontSize();
             if (fontSz == null) {
                 fontSz = paragraph.getDefaultFontSize();
             }
-            attList.add(new AttributedStringData(TextAttribute.SIZE, fontSz.floatValue(), beginIndex, endIndex));
+            att.put(TextAttribute.SIZE, fontSz.floatValue());
 
             if(run.isBold()) {
-                attList.add(new AttributedStringData(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, beginIndex, endIndex));
+                att.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
             }
             if(run.isItalic()) {
-                attList.add(new AttributedStringData(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, beginIndex, endIndex));
+                att.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
             }
             if(run.isUnderlined()) {
-                attList.add(new AttributedStringData(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, beginIndex, endIndex));
-                attList.add(new AttributedStringData(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_TWO_PIXEL, beginIndex, endIndex));
+                att.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
+                att.put(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_TWO_PIXEL);
             }
             if(run.isStrikethrough()) {
-                attList.add(new AttributedStringData(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON, beginIndex, endIndex));
+                att.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
             }
             if(run.isSubscript()) {
-                attList.add(new AttributedStringData(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB, beginIndex, endIndex));
+                att.put(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB);
             }
             if(run.isSuperscript()) {
-                attList.add(new AttributedStringData(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER, beginIndex, endIndex));
+                att.put(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER);
             }
 
             Hyperlink<?,?> hl = run.getHyperlink();
             if (hl != null) {
-                attList.add(new AttributedStringData(HYPERLINK_HREF, hl.getAddress(), beginIndex, endIndex));
-                attList.add(new AttributedStringData(HYPERLINK_LABEL, hl.getLabel(), beginIndex, endIndex));
+                att.put(HYPERLINK_HREF, hl.getAddress());
+                att.put(HYPERLINK_LABEL, hl.getLabel());
             }
 
+            if (fontInfo != null) {
+                att.put(TextAttribute.FAMILY, fontInfo.getTypeface());
+            } else {
+                att.put(TextAttribute.FAMILY, paragraph.getDefaultFontFamily());
+            }
+
+            att.put(TextAttribute.FONT, new Font(att));
+
+            att.forEach((k,v) -> attList.add(new AttributedStringData(k,v,beginIndex,endIndex)));
+
             processGlyphs(graphics, dfm, attList, beginIndex, run, runText);
         }
 
         // ensure that the paragraph contains at least one character
         // We need this trick to correctly measure text
         if (text.length() == 0) {
-            Double fontSz = paragraph.getDefaultFontSize();
             text.append(" ");
-            attList.add(new AttributedStringData(TextAttribute.SIZE, fontSz.floatValue(), 0, 1));
-        }
 
-        AttributedString string = new AttributedString(text.toString());
-        for (AttributedStringData asd : attList) {
-            string.addAttribute(asd.attribute, asd.value, asd.beginIndex, asd.endIndex);
+            Double fontSz = paragraph.getDefaultFontSize();
+            att.put(TextAttribute.SIZE, fontSz.floatValue());
+            att.put(TextAttribute.FAMILY, paragraph.getDefaultFontFamily());
+            att.put(TextAttribute.FONT, new Font(att));
+
+            att.forEach((k,v) -> attList.add(new AttributedStringData(k,v,0,1)));
         }
 
-        return string;
+        return attList;
     }
 
     /**
index f88bd8cb64c275d22a4b88ce4865790f753791e9..51342739ec73c778add58fae3eb3132ebe62cee6 100644 (file)
Binary files a/src/multimodule/ooxml/java9/module-info.class and b/src/multimodule/ooxml/java9/module-info.class differ
index 35533ce078d56365844d186a926801582e9e30c4..2ae4916646d7591010325f0c18ff5e987f9aedd4 100644 (file)
@@ -87,4 +87,11 @@ module org.apache.poi.ooxml {
     requires static org.apache.santuario.xmlsec;
     requires static org.bouncycastle.provider;
     requires static org.bouncycastle.pkix;
+
+    /* optional dependencies for slideshow rendering via PPTX2PNG */
+    requires static batik.all;
+    requires static org.apache.pdfbox;
+    requires static org.apache.fontbox;
+    requires static de.rototor.pdfbox.graphics2d;
+    requires static xmlgraphics.commons;
 }
\ No newline at end of file
index c85ff172e4737cc27cac4b1cca66a8a4b9ab2c27..bbd2c51907425f845cb1c2f22db2c5483d59cb97 100644 (file)
Binary files a/src/multimodule/ooxml/test9/module-info.class and b/src/multimodule/ooxml/test9/module-info.class differ
index 6ae5b6cd84783fa87424650a59f0349fcb61a785..cd4efbca09821d8a04076fc265cdbf75759fc51b 100644 (file)
@@ -89,6 +89,14 @@ module org.apache.poi.ooxml {
     requires org.bouncycastle.pkix;
 
 
+    /* optional dependencies for slideshow rendering via PPTX2PNG */
+    requires batik.all;
+    requires org.apache.pdfbox;
+    requires org.apache.fontbox;
+    requires de.rototor.pdfbox.graphics2d;
+    requires xmlgraphics.commons;
+
+
     // test specific exports
     requires junit;
     requires com.google.common;
diff --git a/src/ooxml/java/org/apache/poi/xslf/util/PDFFontMapper.java b/src/ooxml/java/org/apache/poi/xslf/util/PDFFontMapper.java
new file mode 100644 (file)
index 0000000..6180ad1
--- /dev/null
@@ -0,0 +1,102 @@
+package org.apache.poi.xslf.util;
+
+import java.awt.Font;
+import java.awt.FontFormatException;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import org.apache.pdfbox.pdmodel.font.PDFont;
+
+public class PDFFontMapper extends PDFFontMapper2 /* PdfBoxGraphics2DFontTextDrawer */ {
+
+    private static final String DEFAULT_TTF_PATTERN = ".*\\.tt[fc]";
+
+    private static final String FONTDIRS_MAC =
+        "$HOME/Library/Fonts;" +
+        "/Library/Fonts;" +
+        "/Network/Library/Fonts;" +
+        "/System/Library/Fonts;" +
+        "/System Folder/Fonts";
+
+    private static final String FONTDIRS_WIN =
+        "C:\\Windows\\Fonts";
+
+    private static final String FONTDIRS_UNX =
+        "/usr/share/fonts;" +
+        "/usr/local/share/fonts;" +
+        "$HOME/.fonts";
+
+
+    private final Map<String,File> fonts = new HashMap<>();
+    private final Set<String> registered = new HashSet<>();
+
+    public PDFFontMapper(String fontDir, String fontTtf) {
+        registerFonts(fontDir, fontTtf);
+    }
+
+
+    private void registerFonts(String fontDir, String fontTtf) {
+        if (fontDir == null) {
+            String OS = System.getProperty("os.name", "generic").toLowerCase(Locale.ROOT);
+            if (OS.contains("mac") || OS.contains("darwin")) {
+                fontDir = FONTDIRS_MAC;
+            } else if (OS.contains("win")) {
+                fontDir = FONTDIRS_WIN;
+            } else {
+                fontDir = FONTDIRS_UNX;
+            }
+        }
+
+        String fd = fontDir.replace("$HOME", System.getProperty("user.home"));
+        final LinkedList<File> dirs = new LinkedList<>();
+        Stream.of(fd.split(";")).map(File::new).filter(File::isDirectory).forEach(dirs::add);
+
+        Pattern p = Pattern.compile(fontTtf == null ? DEFAULT_TTF_PATTERN : fontTtf);
+
+        while (!dirs.isEmpty()) {
+            File[] ttfs = dirs.removeFirst().listFiles((f, n) -> {
+                File f2 = new File(f, n);
+                if (f2.isDirectory()) {
+                    dirs.add(f2);
+                    return false;
+                } else {
+                    return p.matcher(n).matches();
+                }
+            });
+
+            if (ttfs == null) {
+                continue;
+            }
+
+            for (File f : ttfs) {
+                try {
+                    Font font = Font.createFont(Font.TRUETYPE_FONT, f);
+                    fonts.put(font.getFontName(Locale.ROOT), f);
+                } catch (IOException|FontFormatException ignored) {
+                }
+
+            }
+        }
+    }
+
+    @Override
+    protected PDFont mapFont(Font font, IFontTextDrawerEnv env) throws IOException, FontFormatException {
+        String name = font.getFontName(Locale.ROOT);
+        if (!registered.contains(name)) {
+            registered.add(name);
+            File f = fonts.get(name);
+            if (f != null) {
+                super.registerFont(name, f);
+            }
+        }
+        return super.mapFont(font, env);
+    }
+}
diff --git a/src/ooxml/java/org/apache/poi/xslf/util/PDFFontMapper2.java b/src/ooxml/java/org/apache/poi/xslf/util/PDFFontMapper2.java
new file mode 100644 (file)
index 0000000..58ac115
--- /dev/null
@@ -0,0 +1,681 @@
+package org.apache.poi.xslf.util;
+
+import java.awt.Font;
+import java.awt.FontFormatException;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Paint;
+import java.awt.font.FontRenderContext;
+import java.awt.font.LineMetrics;
+import java.awt.font.TextAttribute;
+import java.awt.geom.Rectangle2D;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.AttributedCharacterIterator;
+import java.text.CharacterIterator;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2DFontTextDrawer;
+import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2DFontTextDrawerDefaultFonts;
+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 org.apache.poi.util.Internal;
+
+/**
+ * Workaround class until PdfBoxGraphics2DFontTextDrawer is fixed
+ */
+@Internal
+public class PDFFontMapper2 extends PdfBoxGraphics2DFontTextDrawer
+{
+    /**
+     * 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.
+     * <p>
+     * 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 true;
+    }
+
+    @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();
+
+            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();
+
+            /*
+             * Apply the paint
+             */
+            env.applyPaint(paint, null);
+
+            /*
+             * 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();
+    }
+
+    @Override
+    public FontMetrics getFontMetrics(final Font f, IFontTextDrawerEnv env)
+            throws IOException, FontFormatException
+    {
+        final FontMetrics defaultMetrics = env.getCalculationGraphics().getFontMetrics(f);
+        final PDFont pdFont = mapFont(f, env);
+        /*
+         * By default we delegate to the buffered image based calculation. This is wrong
+         * as soon as we use the native PDF Box font, as those have sometimes different widths.
+         *
+         * But it is correct and fine as long as we use vector shapes.
+         */
+        if (pdFont == null)
+            return defaultMetrics;
+        return new FontMetrics(f)
+        {
+            public int getDescent()
+            {
+                return defaultMetrics.getDescent();
+            }
+
+            public int getHeight()
+            {
+                return defaultMetrics.getHeight();
+            }
+
+            public int getMaxAscent()
+            {
+                return defaultMetrics.getMaxAscent();
+            }
+
+            public int getMaxDescent()
+            {
+                return defaultMetrics.getMaxDescent();
+            }
+
+            public boolean hasUniformLineMetrics()
+            {
+                return defaultMetrics.hasUniformLineMetrics();
+            }
+
+            public LineMetrics getLineMetrics(String str, Graphics context)
+            {
+                return defaultMetrics.getLineMetrics(str, context);
+            }
+
+            public LineMetrics getLineMetrics(String str, int beginIndex, int limit,
+                                              Graphics context)
+            {
+                return defaultMetrics.getLineMetrics(str, beginIndex, limit, context);
+            }
+
+            public LineMetrics getLineMetrics(char[] chars, int beginIndex, int limit,
+                                              Graphics context)
+            {
+                return defaultMetrics.getLineMetrics(chars, beginIndex, limit, context);
+            }
+
+            public LineMetrics getLineMetrics(CharacterIterator ci, int beginIndex, int limit,
+                                              Graphics context)
+            {
+                return defaultMetrics.getLineMetrics(ci, beginIndex, limit, context);
+            }
+
+            public Rectangle2D getStringBounds(String str, Graphics context)
+            {
+                return defaultMetrics.getStringBounds(str, context);
+            }
+
+            public Rectangle2D getStringBounds(String str, int beginIndex, int limit,
+                                               Graphics context)
+            {
+                return defaultMetrics.getStringBounds(str, beginIndex, limit, context);
+            }
+
+            public Rectangle2D getStringBounds(char[] chars, int beginIndex, int limit,
+                                               Graphics context)
+            {
+                return defaultMetrics.getStringBounds(chars, beginIndex, limit, context);
+            }
+
+            public Rectangle2D getStringBounds(CharacterIterator ci, int beginIndex, int limit,
+                                               Graphics context)
+            {
+                return defaultMetrics.getStringBounds(ci, beginIndex, limit, context);
+            }
+
+            public Rectangle2D getMaxCharBounds(Graphics context)
+            {
+                return defaultMetrics.getMaxCharBounds(context);
+            }
+
+            @Override
+            public int getAscent()
+            {
+                return defaultMetrics.getAscent();
+            }
+
+            @Override
+            public int getMaxAdvance()
+            {
+                return defaultMetrics.getMaxAdvance();
+            }
+
+            @Override
+            public int getLeading()
+            {
+                return defaultMetrics.getLeading();
+            }
+
+            @Override
+            public FontRenderContext getFontRenderContext()
+            {
+                return defaultMetrics.getFontRenderContext();
+            }
+
+            @Override
+            public int charWidth(char ch)
+            {
+                char[] chars = { ch };
+                return charsWidth(chars, 0, chars.length);
+            }
+
+            @Override
+            public int charWidth(int codePoint)
+            {
+                char[] data = Character.toChars(codePoint);
+                return charsWidth(data, 0, data.length);
+            }
+
+            @Override
+            public int charsWidth(char[] data, int off, int len)
+            {
+                return stringWidth(new String(data, off, len));
+            }
+
+            @Override
+            public int stringWidth(String str)
+            {
+                try
+                {
+                    return (int) (pdFont.getStringWidth(str) / 1000 * f.getSize());
+                }
+                catch (IOException e)
+                {
+                    throw new RuntimeException(e);
+                }
+                catch (IllegalArgumentException e)
+                {
+                    /*
+                     * We let unknown chars be handled with
+                     */
+                    return defaultMetrics.stringWidth(str);
+                }
+            }
+
+            @Override
+            public int[] getWidths()
+            {
+                try
+                {
+                    int[] first256Widths = new int[256];
+                    for (int i = 0; i < first256Widths.length; i++)
+                        first256Widths[i] = (int) (pdFont.getWidth(i) / 1000 * f.getSize());
+                    return first256Widths;
+                }
+                catch (IOException e)
+                {
+                    throw new RuntimeException(e);
+                }
+            }
+
+        };
+    }
+
+    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 (iterator.getIndex() < iterator.getRunLimit());
+    }
+
+}
index 2ada1c738b4d724cfecfdc192b7b1c8a23796733..1b71fb3f7bee530ae34c2c04a876a34592a00004 100644 (file)
@@ -24,6 +24,7 @@ import java.io.File;
 import java.io.IOException;
 
 import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2D;
+import de.rototor.pdfbox.graphics2d.PdfBoxGraphics2DFontTextDrawer;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.pdmodel.PDPage;
 import org.apache.pdfbox.pdmodel.PDPageContentStream;
@@ -36,17 +37,25 @@ public class PDFFormat implements OutputFormat {
     private final PDDocument document;
     private PDPageContentStream contentStream;
     private PdfBoxGraphics2D pdfBoxGraphics2D;
+    private PdfBoxGraphics2DFontTextDrawer fontTextDrawer;
+
+    public PDFFormat(boolean textAsShapes, String fontDir, String fontTtf) {
+        if (!textAsShapes) {
+            fontTextDrawer = new PDFFontMapper(fontDir, fontTtf);
+        }
 
-    public PDFFormat() {
         document = new PDDocument();
     }
 
     @Override
-    public Graphics2D addSlide(double width, double height)  throws IOException {
+    public Graphics2D addSlide(double width, double height) throws IOException {
         PDPage page = new PDPage(new PDRectangle((float) width, (float) height));
         document.addPage(page);
         contentStream = new PDPageContentStream(document, page);
-        pdfBoxGraphics2D = new PdfBoxGraphics2D(document, (float)width, (float)height);
+        pdfBoxGraphics2D = new PdfBoxGraphics2D(document, (float) width, (float) height);
+        if (fontTextDrawer != null) {
+            pdfBoxGraphics2D.setFontTextDrawer(fontTextDrawer);
+        }
         return pdfBoxGraphics2D;
     }
 
@@ -67,5 +76,9 @@ public class PDFFormat implements OutputFormat {
     @Override
     public void close() throws IOException {
         document.close();
+        if (fontTextDrawer != null) {
+            fontTextDrawer.close();
+        }
     }
+
 }
index db034e8caa6d4766d817219cc2b43f112b9df5b0..232e29340dabf1cc4645072ee268000543273142 100644 (file)
@@ -74,7 +74,10 @@ public final class PPTX2PNG {
             "    -textAsShapes     text elements are saved as shapes in SVG, necessary for variable spacing\n" +
             "                      often found in math formulas\n" +
             "    -charset <cs>     sets the default charset to be used, defaults to Windows-1252\n" +
-            "    -emfHeaderBounds  force the usage of the emf header bounds to calculate the bounding box";
+            "    -emfHeaderBounds  force the usage of the emf header bounds to calculate the bounding box\n" +
+            "    -fontdir <dir>    (PDF only) font directories separated by \";\" - use $HOME for current users home dir\n" +
+            "                      defaults to the usual plattform directories\n" +
+            "    -fontTtf <regex>  (PDF only) regex to match the .ttf filenames";
 
         System.out.println(msg);
         // no System.exit here, as we also run in junit tests!
@@ -104,6 +107,8 @@ public final class PPTX2PNG {
     private boolean textAsShapes = false;
     private Charset charset = LocaleUtil.CHARSET_1252;
     private boolean emfHeaderBounds = false;
+    private String fontDir = null;
+    private String fontTtf = null;
 
     private PPTX2PNG() {
     }
@@ -192,6 +197,22 @@ public final class PPTX2PNG {
                 case "-emfheaderbounds":
                     emfHeaderBounds = true;
                     break;
+                case "-fontdir":
+                    if (opt != null) {
+                        fontDir = opt;
+                        i++;
+                    } else {
+                        fontDir = null;
+                    }
+                    break;
+                case "-fontttf":
+                    if (opt != null) {
+                        fontTtf = opt;
+                        i++;
+                    } else {
+                        fontTtf = null;
+                    }
+                    break;
                 default:
                     file = new File(args[i]);
                     break;
@@ -313,7 +334,7 @@ public final class PPTX2PNG {
             case "svg":
                 return new SVGFormat(textAsShapes);
             case "pdf":
-                return new PDFFormat();
+                return new PDFFormat(textAsShapes,fontDir,fontTtf);
             default:
                 return new BitmapFormat(format);
         }
index ab59877dac05a8453555c9c7683ac9c224a93fc9..7717c5062f12668bc3a4c3cbaded008b0c8a221d 100644 (file)
@@ -49,6 +49,7 @@ public class TestPPTX2PNG {
     private static boolean xslfOnly;
     private static final POIDataSamples samples = POIDataSamples.getSlideShowInstance();
     private static final File basedir = null;
+
     private static final String files =
         "bug64693.pptx, 53446.ppt, alterman_security.ppt, alterman_security.pptx, KEY02.pptx, themes.pptx, " +
         "backgrounds.pptx, layouts.pptx, sample.pptx, shapes.pptx, 54880_chinese.ppt, keyframes.pptx," +
@@ -74,6 +75,8 @@ public class TestPPTX2PNG {
     @Parameter
     public String pptFile;
 
+
+
     @Parameters(name="{0}")
     public static Collection<String> data() {
         Function<String, Stream<String>> fun = (basedir == null) ? Stream::of :
@@ -105,6 +108,7 @@ public class TestPPTX2PNG {
                 "-quiet",
                 // "-charset", "GBK",
                 // "-emfHeaderBounds",
+                // "-textAsShapes",
                 "-fixside", "long",
                 "-scale", "800"
         ));
index b849a55bfb3fb45757b445305b0fd2d2b376d61e..48790f8e0a7e35f1ac26545669e85f5b7545d06b 100644 (file)
@@ -575,16 +575,18 @@ public class HwmfGraphics implements HwmfCharsetAware {
     }
 
     private void addAttributes(BiConsumer<TextAttribute,Object> attributes, HwmfFont font, String typeface) {
-        attributes.accept(TextAttribute.FAMILY, typeface);
-        attributes.accept(TextAttribute.SIZE, getFontHeight(font));
+        Map<TextAttribute,Object> att = new HashMap<>();
+        att.put(TextAttribute.FAMILY, typeface);
+        att.put(TextAttribute.SIZE, getFontHeight(font));
+
         if (font.isStrikeOut()) {
-            attributes.accept(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
+            att.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
         }
         if (font.isUnderline()) {
-            attributes.accept(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
+            att.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
         }
         if (font.isItalic()) {
-            attributes.accept(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
+            att.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
         }
         // convert font weight to awt font weight - usually a font weight of 400 is regarded as regular
         final int fw = font.getWeight();
@@ -595,7 +597,10 @@ public class HwmfGraphics implements HwmfCharsetAware {
                 break;
             }
         }
-        attributes.accept(TextAttribute.WEIGHT, awtFW);
+        att.put(TextAttribute.WEIGHT, awtFW);
+        att.put(TextAttribute.FONT, new Font(att));
+
+        att.forEach(attributes);
     }
 
     private double getFontHeight(HwmfFont font) {
diff --git a/src/scratchpad/testcases/commons-logging.properties b/src/scratchpad/testcases/commons-logging.properties
new file mode 100644 (file)
index 0000000..3b4d40d
--- /dev/null
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more\r
+# contributor license agreements.  See the NOTICE file distributed with\r
+# this work for additional information regarding copyright ownership.\r
+# The ASF licenses this file to You under the Apache License, Version 2.0\r
+# (the "License"); you may not use this file except in compliance with\r
+# the License.  You may obtain a copy of the License at\r
+#\r
+#     http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+# Unless required by applicable law or agreed to in writing, software\r
+# distributed under the License is distributed on an "AS IS" BASIS,\r
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+# See the License for the specific language governing permissions and\r
+# limitations under the License.\r
+\r
+org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger\r
+log4j.configuration=log4j.properties
\ No newline at end of file
index ac2be68500a45079b4fc62e1d0ad7865dbfcf5ae..23d316d79c4568e9eaa7eadf0a2c7e83f4ccfd32 100644 (file)
@@ -18,4 +18,6 @@ log4j.rootLogger=ALL,CONSOLE
 log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
 log4j.appender.CONSOLE.target=System.out
 log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
-log4j.appender.CONSOLE.layout.ConversionPattern=%d{dd.MM HH:mm:ss} %-30.30c %5p %m%n
+log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %c %5p %m%n
+
+log4j.logger.org.apache.fontbox.ttf=INFO