#64867 - Provide PDF rendering with PPTX2PNG
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
diff --git a/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java b/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java
index c3e2693..b3d70ff 100644
--- a/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java
+++ b/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java
@@ -32,8 +32,10 @@
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 @@
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 @@
}
}
- 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 @@
// 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 @@
};
}
- 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 @@
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 @@
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));
+
+ 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)));
}
- AttributedString string = new AttributedString(text.toString());
- for (AttributedStringData asd : attList) {
- string.addAttribute(asd.attribute, asd.value, asd.beginIndex, asd.endIndex);
- }
-
- return string;
+ return attList;
}
/**
diff --git a/src/multimodule/ooxml/java9/module-info.class b/src/multimodule/ooxml/java9/module-info.class
index f88bd8c..5134273 100644
--- a/src/multimodule/ooxml/java9/module-info.class
+++ b/src/multimodule/ooxml/java9/module-info.class
Binary files differ
diff --git a/src/multimodule/ooxml/java9/module-info.java b/src/multimodule/ooxml/java9/module-info.java
index 35533ce..2ae4916 100644
--- a/src/multimodule/ooxml/java9/module-info.java
+++ b/src/multimodule/ooxml/java9/module-info.java
@@ -87,4 +87,11 @@
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
diff --git a/src/multimodule/ooxml/test9/module-info.class b/src/multimodule/ooxml/test9/module-info.class
index c85ff17..bbd2c51 100644
--- a/src/multimodule/ooxml/test9/module-info.class
+++ b/src/multimodule/ooxml/test9/module-info.class
Binary files differ
diff --git a/src/multimodule/ooxml/test9/module-info.java b/src/multimodule/ooxml/test9/module-info.java
index 6ae5b6c..cd4efbc 100644
--- a/src/multimodule/ooxml/test9/module-info.java
+++ b/src/multimodule/ooxml/test9/module-info.java
@@ -89,6 +89,14 @@
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
index 0000000..6180ad1
--- /dev/null
+++ b/src/ooxml/java/org/apache/poi/xslf/util/PDFFontMapper.java
@@ -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
index 0000000..58ac115
--- /dev/null
+++ b/src/ooxml/java/org/apache/poi/xslf/util/PDFFontMapper2.java
@@ -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());
+ }
+
+}
diff --git a/src/ooxml/java/org/apache/poi/xslf/util/PDFFormat.java b/src/ooxml/java/org/apache/poi/xslf/util/PDFFormat.java
index 2ada1c7..1b71fb3 100644
--- a/src/ooxml/java/org/apache/poi/xslf/util/PDFFormat.java
+++ b/src/ooxml/java/org/apache/poi/xslf/util/PDFFormat.java
@@ -24,6 +24,7 @@
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 @@
private final PDDocument document;
private PDPageContentStream contentStream;
private PdfBoxGraphics2D pdfBoxGraphics2D;
+ private PdfBoxGraphics2DFontTextDrawer fontTextDrawer;
- public PDFFormat() {
+ public PDFFormat(boolean textAsShapes, String fontDir, String fontTtf) {
+ if (!textAsShapes) {
+ fontTextDrawer = new PDFFontMapper(fontDir, fontTtf);
+ }
+
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 @@
@Override
public void close() throws IOException {
document.close();
+ if (fontTextDrawer != null) {
+ fontTextDrawer.close();
+ }
}
+
}
diff --git a/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java b/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java
index db034e8..232e293 100644
--- a/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java
+++ b/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java
@@ -74,7 +74,10 @@
" -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 @@
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 @@
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 @@
case "svg":
return new SVGFormat(textAsShapes);
case "pdf":
- return new PDFFormat();
+ return new PDFFormat(textAsShapes,fontDir,fontTtf);
default:
return new BitmapFormat(format);
}
diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestPPTX2PNG.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestPPTX2PNG.java
index ab59877..7717c50 100644
--- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestPPTX2PNG.java
+++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestPPTX2PNG.java
@@ -49,6 +49,7 @@
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 @@
@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 @@
"-quiet",
// "-charset", "GBK",
// "-emfHeaderBounds",
+ // "-textAsShapes",
"-fixside", "long",
"-scale", "800"
));
diff --git a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java
index b849a55..48790f8 100644
--- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java
+++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java
@@ -575,16 +575,18 @@
}
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 @@
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
index 0000000..3b4d40d
--- /dev/null
+++ b/src/scratchpad/testcases/commons-logging.properties
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger
+log4j.configuration=log4j.properties
\ No newline at end of file
diff --git a/src/scratchpad/testcases/log4j.properties b/src/scratchpad/testcases/log4j.properties
index ac2be68..23d316d 100644
--- a/src/scratchpad/testcases/log4j.properties
+++ b/src/scratchpad/testcases/log4j.properties
@@ -18,4 +18,6 @@
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