diff --git a/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java b/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java index c3e2693213..b3d70fff26 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.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 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 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 attList = new ArrayList<>(); + protected List 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 att = new HashMap<>(); + final List 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)); + + 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 f88bd8cb64..51342739ec 100644 Binary files a/src/multimodule/ooxml/java9/module-info.class and b/src/multimodule/ooxml/java9/module-info.class differ diff --git a/src/multimodule/ooxml/java9/module-info.java b/src/multimodule/ooxml/java9/module-info.java index 35533ce078..2ae4916646 100644 --- a/src/multimodule/ooxml/java9/module-info.java +++ b/src/multimodule/ooxml/java9/module-info.java @@ -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 diff --git a/src/multimodule/ooxml/test9/module-info.class b/src/multimodule/ooxml/test9/module-info.class index c85ff172e4..bbd2c51907 100644 Binary files a/src/multimodule/ooxml/test9/module-info.class and b/src/multimodule/ooxml/test9/module-info.class differ diff --git a/src/multimodule/ooxml/test9/module-info.java b/src/multimodule/ooxml/test9/module-info.java index 6ae5b6cd84..cd4efbca09 100644 --- a/src/multimodule/ooxml/test9/module-info.java +++ b/src/multimodule/ooxml/test9/module-info.java @@ -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 index 0000000000..6180ad1783 --- /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 fonts = new HashMap<>(); + private final Set 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 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 0000000000..58ac115bf3 --- /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. + *

+ * 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 fontFiles = new ArrayList(); + private final List tempFiles = new ArrayList(); + private final Map fontMap = new HashMap(); + + /** + * 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 2ada1c738b..1b71fb3f7b 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.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(); + } } + } 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 db034e8caa..232e29340d 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 @@ 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 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

(PDF only) font directories separated by \";\" - use $HOME for current users home dir\n" + + " defaults to the usual plattform directories\n" + + " -fontTtf (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); } 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 ab59877dac..7717c5062f 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 @@ 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 data() { Function> fun = (basedir == null) ? Stream::of : @@ -105,6 +108,7 @@ public class TestPPTX2PNG { "-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 b849a55bfb..48790f8e0a 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 @@ public class HwmfGraphics implements HwmfCharsetAware { } private void addAttributes(BiConsumer attributes, HwmfFont font, String typeface) { - attributes.accept(TextAttribute.FAMILY, typeface); - attributes.accept(TextAttribute.SIZE, getFontHeight(font)); + Map 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 index 0000000000..3b4d40d05e --- /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 ac2be68500..23d316d79c 100644 --- a/src/scratchpad/testcases/log4j.properties +++ b/src/scratchpad/testcases/log4j.properties @@ -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