diff options
author | Andreas Beeker <kiwiwings@apache.org> | 2020-04-05 00:42:08 +0000 |
---|---|---|
committer | Andreas Beeker <kiwiwings@apache.org> | 2020-04-05 00:42:08 +0000 |
commit | 81400a3ee211fa3c9b8609fd02bf3dfdbced714e (patch) | |
tree | 30429e975360dbc2e916960b844cea952bdbccd4 | |
parent | 1dc771394bedaf456b85e9e15d90f7429908919b (diff) | |
download | poi-81400a3ee211fa3c9b8609fd02bf3dfdbced714e.tar.gz poi-81400a3ee211fa3c9b8609fd02bf3dfdbced714e.zip |
Bug 60656 - Emf image support in slideshows
- fixed WmfExtTextOut dx handling for variable text spacing
- fixed WmfExtTextOut text position for (0,0) references based on the current/last path location
- fixed WmfExtTextOut handling of symbol/wingdings charset (move ascii to unicode private area, because Java font loader maps the glyphs there) - and use existing workaround if the fonts aren't installed, i.e. use corresponding unicode characters of the logcial font then
- provide option in PPTX2PNG to use given file input type, if the file magic is unknown
- provide option in PPTX2PNG to render text as shapes in SVG, as dx handling (above) implemented via TextAttribute.TRACKING is not supported by batik
source of the sample.wmf, which I've used:
https://stackoverflow.com/questions/58726194/svg-rendering-with-batik-produce-broken-image
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1876136 13f79535-47bb-0310-9956-ffa450edef68
9 files changed, 328 insertions, 265 deletions
diff --git a/src/java/org/apache/poi/sl/draw/DrawFontManager.java b/src/java/org/apache/poi/sl/draw/DrawFontManager.java index 0c16dd994a..72216b36bd 100644 --- a/src/java/org/apache/poi/sl/draw/DrawFontManager.java +++ b/src/java/org/apache/poi/sl/draw/DrawFontManager.java @@ -48,20 +48,25 @@ public interface DrawFontManager { * * @param graphics the graphics context to request additional rendering hints * @param fontInfo the font info object corresponding to the text run font - * + * * @return the font to be used as a fallback for the original typeface */ FontInfo getFallbackFont(Graphics2D graphics, FontInfo fontInfo); /** - * Map text charset depending on font family.<p> - * - * Currently this only maps for wingdings font (into unicode private use area) + * Map text charset depending on font family. + * <p> + * Currently this only maps for wingdings and symbol font (into unicode private use area) + * <p> + * Depending if the requested font is installed in the system, tbe mapped string varies:<br> + * If the font is registered into the graphics environment the characters are mapped to the + * private use area. If the font is missing (and hence a AWT logical font is used), the + * characters are mapped to the corresponding unicode characters * * @param graphics the graphics context to request additional rendering hints * @param fontInfo the font info object corresponding to the text run font * @param text the raw text - * + * * @return String with mapped codepoints * * @see <a href="http://stackoverflow.com/questions/8692095">Drawing exotic fonts in a java applet</a> @@ -77,7 +82,7 @@ public interface DrawFontManager { * @param size the font size in points * @param bold {@code true} if the font is bold * @param italic {@code true} if the font is italic - * + * * @return the AWT font object */ Font createAWTFont(Graphics2D graphics, FontInfo fontInfo, double size, boolean bold, boolean italic); diff --git a/src/java/org/apache/poi/sl/draw/DrawFontManagerDefault.java b/src/java/org/apache/poi/sl/draw/DrawFontManagerDefault.java index 7dcd704c4e..e5b5f51349 100644 --- a/src/java/org/apache/poi/sl/draw/DrawFontManagerDefault.java +++ b/src/java/org/apache/poi/sl/draw/DrawFontManagerDefault.java @@ -21,12 +21,16 @@ package org.apache.poi.sl.draw; import java.awt.Font; import java.awt.Graphics2D; +import java.awt.GraphicsEnvironment; +import java.util.Arrays; import java.util.Map; import java.util.Set; import java.util.TreeSet; +import org.apache.poi.common.usermodel.fonts.FontCharset; import org.apache.poi.common.usermodel.fonts.FontInfo; import org.apache.poi.sl.draw.Drawable.DrawableHint; +import org.apache.poi.util.StringUtil; /** * Manages fonts when rendering slides. @@ -56,37 +60,36 @@ public class DrawFontManagerDefault implements DrawFontManager { return fi; } - public String mapFontCharset(Graphics2D graphics, FontInfo fontInfo, String text) { - // TODO: find a real charset mapping solution instead of hard coding for Wingdings - return (fontInfo != null && knownSymbolFonts.contains(fontInfo.getTypeface())) - ? mapSymbolChars(text) - : text; - } - /** * Symbol fonts like "Wingdings" or "Symbol" have glyphs mapped to a Unicode private use range via the Java font loader, * although a system font viewer might show you the glyphs in the ASCII range. - * This helper function maps the chars of the text string to the corresponding private use range chars. + * This maps the chars of the text string to the corresponding private use range chars. + * + * @param graphics the used graphics context + * @param fontInfo the font info + * @param text the input string * - * @param text the input string, typically consists of ASCII chars * @return the mapped string, typically consists of chars in the range of 0xf000 to 0xf0ff * * @since POI 4.0.0 */ - public static String mapSymbolChars(String text) { - // wingdings doesn't contain high-surrogates, so chars are ok - boolean changed = false; - char[] chrs = text.toCharArray(); - for (int i=0; i<chrs.length; i++) { - // only change valid chars - if ((0x20 <= chrs[i] && chrs[i] <= 0x7f) || - (0xa0 <= chrs[i] && chrs[i] <= 0xff)) { - chrs[i] |= 0xf000; - changed = true; - } + @Override + public String mapFontCharset(Graphics2D graphics, FontInfo fontInfo, String text) { + if (fontInfo == null || text == null || text.isEmpty()) { + return text; } - return changed ? new String(chrs) : text; + String typeface = fontInfo.getTypeface(); + if (fontInfo.getCharset() == FontCharset.SYMBOL || knownSymbolFonts.contains(typeface)) { + int[] cps = text.codePoints().map(DrawFontManagerDefault::mapSymbolChar).toArray(); + String ret = new String(cps, 0, cps.length); + + String[] allFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); + boolean hasFont = Arrays.asList(allFonts).contains(typeface); + return hasFont ? ret : StringUtil.mapMsCodepointString(ret); + } + + return text; } @Override @@ -106,7 +109,7 @@ public class DrawFontManagerDefault implements DrawFontManager { if (fontMap == null) { return fontInfo; } - + String f = (fontInfo != null) ? fontInfo.getTypeface() : null; String mappedTypeface = null; if (fontMap.containsKey(f)) { @@ -117,4 +120,9 @@ public class DrawFontManagerDefault implements DrawFontManager { return (mappedTypeface != null) ? new DrawFontInfo(mappedTypeface) : fontInfo; } + + + private static int mapSymbolChar(int cp) { + return ((0x20 <= cp && cp <= 0x7f) || (0xa0 <= cp && cp <= 0xff)) ? cp | 0xf000 : cp; + } } diff --git a/src/java/org/apache/poi/util/StringUtil.java b/src/java/org/apache/poi/util/StringUtil.java index fb69fbd538..b946d65430 100644 --- a/src/java/org/apache/poi/util/StringUtil.java +++ b/src/java/org/apache/poi/util/StringUtil.java @@ -19,17 +19,15 @@ package org.apache.poi.util; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.Iterator; import java.util.Locale; -import java.util.Map; /** * Collection of string handling utilities */ @Internal -public class StringUtil { - protected static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1; +public final class StringUtil { + private static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1; //arbitrarily selected; may need to increase private static final int MAX_RECORD_LENGTH = 10000000; @@ -38,8 +36,6 @@ public class StringUtil { public static final Charset WIN_1252 = Charset.forName("cp1252"); public static final Charset BIG5 = Charset.forName("Big5"); - private static Map<Integer, Integer> msCodepointToUnicode; - private StringUtil() { // no instances of this class } @@ -294,16 +290,6 @@ public class StringUtil { } /** - * Checks to see if a given String needs to be represented as Unicode - * - * @param value The string to look at. - * @return true if string needs Unicode to be represented. - */ - public static boolean isUnicodeString(final String value) { - return !value.equals(new String(value.getBytes(ISO_8859_1), ISO_8859_1)); - } - - /** * Tests if the string starts with the specified prefix, ignoring case consideration. */ public static boolean startsWithIgnoreCase(String haystack, String prefix) { @@ -381,38 +367,18 @@ public class StringUtil { if (string == null || string.isEmpty()) { return string; } - initMsCodepointMap(); - - StringBuilder sb = new StringBuilder(); - final int length = string.length(); - for (int offset = 0; offset < length; ) { - int msCodepoint = string.codePointAt(offset); - Integer uniCodepoint = msCodepointToUnicode.get(msCodepoint); - sb.appendCodePoint(uniCodepoint == null ? msCodepoint : uniCodepoint); - offset += Character.charCount(msCodepoint); - } - return sb.toString(); + int[] cps = string.codePoints().map(StringUtil::mapMsCodepoint).toArray(); + return new String(cps, 0, cps.length); } - public static synchronized void mapMsCodepoint(int msCodepoint, int unicodeCodepoint) { - initMsCodepointMap(); - msCodepointToUnicode.put(msCodepoint, unicodeCodepoint); - } - - private static synchronized void initMsCodepointMap() { - if (msCodepointToUnicode != null) { - return; - } - msCodepointToUnicode = new HashMap<>(); - int i = 0xF020; - for (int ch : symbolMap_f020) { - msCodepointToUnicode.put(i++, ch); - } - i = 0xf0a0; - for (int ch : symbolMap_f0a0) { - msCodepointToUnicode.put(i++, ch); + private static int mapMsCodepoint(int cp) { + if (0xf020 <= cp && cp <= 0xf07f) { + return symbolMap_f020[cp - 0xf020]; + } else if (0xf0a0 <= cp && cp <= 0xf0ff) { + return symbolMap_f0a0[cp - 0xf0a0]; } + return cp; } private static final int[] symbolMap_f020 = { diff --git a/src/ooxml/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java b/src/ooxml/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java index a057d6db7d..b2b40f9163 100644 --- a/src/ooxml/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java +++ b/src/ooxml/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java @@ -37,8 +37,8 @@ public class SVGPOIGraphics2D extends SVGGraphics2D { private final RenderingHints hints; - public SVGPOIGraphics2D(Document document) { - super(getCtx(document), false); + public SVGPOIGraphics2D(Document document, boolean textAsShapes) { + super(getCtx(document), textAsShapes); hints = getGeneratorContext().getGraphicContextDefaults().getRenderingHints(); } diff --git a/src/ooxml/java/org/apache/poi/xslf/util/OutputFormat.java b/src/ooxml/java/org/apache/poi/xslf/util/OutputFormat.java index 9f0a6f7742..23c71b0d76 100644 --- a/src/ooxml/java/org/apache/poi/xslf/util/OutputFormat.java +++ b/src/ooxml/java/org/apache/poi/xslf/util/OutputFormat.java @@ -50,6 +50,11 @@ interface OutputFormat extends Closeable { class SVGFormat implements OutputFormat { static final String svgNS = "http://www.w3.org/2000/svg"; private SVGGraphics2D svgGenerator; + private final boolean textAsShapes; + + SVGFormat(boolean textAsShapes) { + this.textAsShapes = textAsShapes; + } @Override public Graphics2D getGraphics2D(double width, double height) { @@ -58,7 +63,7 @@ interface OutputFormat extends Closeable { // Create an instance of org.w3c.dom.Document. Document document = domImpl.createDocument(svgNS, "svg", null); - svgGenerator = new SVGPOIGraphics2D(document); + svgGenerator = new SVGPOIGraphics2D(document, textAsShapes); svgGenerator.setSVGCanvasSize(new Dimension((int)width, (int)height)); return svgGenerator; } 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 1e4c92a5b1..4b2a7a600b 100644 --- a/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java +++ b/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java @@ -67,7 +67,11 @@ public final class PPTX2PNG { " -dump <file> dump the annotated records to a file\n" + " -quiet do not write to console (for normal processing)\n" + " -ignoreParse ignore parsing error and continue with the records read until the error\n" + - " -extractEmbedded extract embedded parts"; + " -extractEmbedded extract embedded parts\n" + + " -inputType <type> default input file type (OLE2,WMF,EMF), default is OLE2 = Powerpoint\n" + + " some files (usually wmf) don't have a header, i.e. an identifiable file magic\n" + + " -textAsShapes text elements are saved as shapes in SVG, necessary for variable spacing\n" + + " often found in math formulas"; System.out.println(msg); // no System.exit here, as we also run in junit tests! @@ -93,6 +97,8 @@ public final class PPTX2PNG { private String fixSide = "scale"; private boolean ignoreParse = false; private boolean extractEmbedded = false; + private FileMagic defaultFileType = FileMagic.OLE2; + private boolean textAsShapes = false; private PPTX2PNG() { } @@ -153,6 +159,17 @@ public final class PPTX2PNG { fixSide = "long"; } break; + case "-inputType": + if (opt != null) { + defaultFileType = FileMagic.valueOf(opt); + i++; + } else { + defaultFileType = FileMagic.OLE2; + } + break; + case "-textAsShapes": + textAsShapes = true; + break; case "-ignoreParse": ignoreParse = true; break; @@ -238,7 +255,7 @@ public final class PPTX2PNG { extractEmbedded(proxy, slideNo); - try (OutputFormat outputFormat = ("svg".equals(format)) ? new SVGFormat() : new BitmapFormat(format)) { + try (OutputFormat outputFormat = ("svg".equals(format)) ? new SVGFormat(textAsShapes) : new BitmapFormat(format)) { Graphics2D graphics = outputFormat.getGraphics2D(width, height); // default rendering options @@ -337,6 +354,9 @@ public final class PPTX2PNG { if ("stdin".equals(fileName)) { InputStream bis = FileMagic.prepareToCheckMagic(System.in); FileMagic fm = FileMagic.valueOf(bis); + if (fm == FileMagic.UNKNOWN) { + fm = defaultFileType; + } switch (fm) { case EMF: proxy = new EMFHandler(); 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 37919c9d69..cf35b716ae 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java @@ -21,6 +21,7 @@ import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; +import java.awt.Font; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.Insets; @@ -29,6 +30,7 @@ import java.awt.Rectangle; import java.awt.Shape; import java.awt.TexturePaint; import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; import java.awt.font.TextAttribute; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; @@ -39,16 +41,18 @@ import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.nio.charset.Charset; import java.text.AttributedString; +import java.util.ArrayList; import java.util.BitSet; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.TreeMap; import java.util.function.BiConsumer; import org.apache.commons.codec.Charsets; -import org.apache.poi.common.usermodel.fonts.FontCharset; import org.apache.poi.common.usermodel.fonts.FontInfo; import org.apache.poi.hwmf.record.HwmfBrushStyle; import org.apache.poi.hwmf.record.HwmfFont; @@ -59,12 +63,10 @@ import org.apache.poi.hwmf.record.HwmfObjectTableEntry; import org.apache.poi.hwmf.record.HwmfPenStyle; import org.apache.poi.hwmf.record.HwmfPenStyle.HwmfLineDash; import org.apache.poi.hwmf.record.HwmfRegionMode; -import org.apache.poi.hwmf.record.HwmfText; import org.apache.poi.hwmf.record.HwmfText.WmfExtTextOutOptions; import org.apache.poi.sl.draw.BitmapImageRenderer; import org.apache.poi.sl.draw.DrawFactory; import org.apache.poi.sl.draw.DrawFontManager; -import org.apache.poi.sl.draw.DrawFontManagerDefault; import org.apache.poi.sl.draw.DrawPictureShape; import org.apache.poi.sl.draw.ImageRenderer; import org.apache.poi.util.Internal; @@ -107,6 +109,16 @@ public class HwmfGraphics { 1f, TextAttribute.WEIGHT_EXTRA_LIGHT }; + private static class DxLayout { + double dx; + // Spacing at default tracking value of 0 + double pos0; + // Spacing at second tracking value + double pos1; + int beginIndex; + int endIndex; + } + private final List<HwmfDrawProperties> propStack = new LinkedList<>(); protected HwmfDrawProperties prop; @@ -189,6 +201,7 @@ public class HwmfGraphics { draw(shape); } + @SuppressWarnings("MagicConstant") protected BasicStroke getStroke() { HwmfDrawProperties prop = getProperties(); HwmfPenStyle ps = prop.getPenStyle(); @@ -285,7 +298,7 @@ public class HwmfGraphics { * Moreover, each object table index uniquely refers to an object. * Indexes in the WMF Object Table always start at 0. * - * @param entry + * @param entry the object table entry */ public void addObjectTableEntry(HwmfObjectTableEntry entry) { int objIdx = objectIndexes.nextClearBit(0); @@ -433,155 +446,44 @@ public class HwmfGraphics { final HwmfDrawProperties prop = getProperties(); final AffineTransform at = graphicsCtx.getTransform(); - try { at.createInverse(); } catch (NoninvertibleTransformException e) { return; } - HwmfFont font = prop.getFont(); + final HwmfFont font = prop.getFont(); if (font == null || text == null || text.length == 0) { return; } - double fontH = getFontHeight(font); - // TODO: another approx. ... - double fontW = fontH/1.8; - - Charset charset; - if (isUnicode) { - charset = Charsets.UTF_16LE; - } else { - charset = font.getCharset().getCharset(); - if (charset == null) { - charset = DEFAULT_CHARSET; - } - } - - int trimLen; - for (trimLen=0; trimLen<text.length; trimLen+=2) { - if (trimLen == text.length-1) { - if (text[trimLen] != 0) { - trimLen++; - } - break; - } else if ((text[trimLen] == -1 && text[trimLen+1] == -1) || - ((text[trimLen] & 0xE0) == 0 && text[trimLen+1] == 0)) { - break; - } - } - - String textString = new String(text, 0, trimLen, charset); - textString = textString.substring(0, Math.min(textString.length(), length)); - + String textString = trimText(font, isUnicode, text, length); if (textString.isEmpty()) { return; } - DrawFontManager fontHandler = DrawFactory.getInstance(graphicsCtx).getFontManager(graphicsCtx); - FontInfo fontInfo = fontHandler.getMappedFont(graphicsCtx, font); - if (fontInfo.getCharset() == FontCharset.SYMBOL) { - textString = DrawFontManagerDefault.mapSymbolChars(textString); - } - - AttributedString as = new AttributedString(textString); - addAttributes(as, font, fontInfo.getTypeface()); - - // disabled for the time being, as the results aren't promising - /* - if (dx != null && !dx.isEmpty()) { - //for multi-byte encodings (e.g. Shift_JIS), the byte length - //might not equal the string length(). - //The x information is stored in dx[], an array parallel to the - //byte array text[]. dx[] stores the x info in the - //first byte of a multibyte character, but dx[] stores 0 - //for the other bytes in that character. - //We need to map this information to the String offsets - //dx[0] = 13 text[0] = -125 - //dx[1] = 0 text[1] = 118 - //dx[2] = 14 text[2] = -125 - //dx[3] = 0 text[3] = -115 - // needs to be remapped as: - //dxNormed[0] = 13 textString.get(0) = U+30D7 - //dxNormed[1] = 14 textString.get(1) = U+30ED - - final int cps = textString.codePointCount(0, textString.length()); - final int unicodeSteps = Math.max(dx.size()/cps, 1); - int dxPosition = 0, lastDxPosition = 0; - int beginIndex = 0; - while (beginIndex < textString.length() && dxPosition < dx.size()) { - int endIndex = textString.offsetByCodePoints(beginIndex, 1); - if (beginIndex > 0) { - // Tracking works as a prefix/advance space on characters whereas - // dx[...] is the complete width of the current char - // therefore we need to add the additional/suffix width to the next char - - as.addAttribute(TextAttribute.TRACKING, (float)((dx.get(lastDxPosition) - fontW) / fontH), beginIndex, endIndex); - } - lastDxPosition = dxPosition; - dxPosition += (isUnicode) ? unicodeSteps : (endIndex-beginIndex); - beginIndex = endIndex; - } - } - */ - - double angle = Math.toRadians(-font.getEscapement()/10.); + final DrawFontManager fontHandler = DrawFactory.getInstance(graphicsCtx).getFontManager(graphicsCtx); + final FontInfo fontInfo = fontHandler.getMappedFont(graphicsCtx, font); + textString = fontHandler.mapFontCharset(graphicsCtx, fontInfo, textString); - final HwmfText.HwmfTextAlignment align = prop.getTextAlignLatin(); - final HwmfText.HwmfTextVerticalAlignment valign = prop.getTextVAlignLatin(); + final AttributedString as = new AttributedString(textString); + addAttributes(as::addAttribute, font, fontInfo.getTypeface()); final FontRenderContext frc = graphicsCtx.getFontRenderContext(); - final TextLayout layout = new TextLayout(as.getIterator(), frc); - final Rectangle2D pixelBounds = layout.getBounds(); + calculateDx(textString, dx, font, fontInfo, frc, as); - AffineTransform tx = new AffineTransform(); - switch (align) { - default: - case LEFT: - break; - case CENTER: - tx.translate(-pixelBounds.getWidth() / 2., 0); - break; - case RIGHT: - tx.translate(-layout.getAdvance(), 0); - break; - } - - // TODO: check min/max orientation - switch (valign) { - case TOP: - tx.translate(0, layout.getAscent()); - break; - default: - case BASELINE: - break; - case BOTTOM: - tx.translate(0, -(pixelBounds.getHeight()-layout.getDescent())); - break; - } - tx.rotate(angle); - Point2D src = new Point2D.Double(); - Point2D dst = new Point2D.Double(); - tx.transform(src, dst); + final double angle = Math.toRadians(-font.getEscapement()/10.); + final Point2D dst = getRotatedOffset(angle, frc, as); final Shape clipShape = graphicsCtx.getClip(); + try { - if (clip != null && !clip.getBounds2D().isEmpty()) { - graphicsCtx.translate(-clip.getCenterX(), -clip.getCenterY()); - graphicsCtx.rotate(angle); - graphicsCtx.translate(clip.getCenterX(), clip.getCenterY()); - if (prop.getBkMode() == HwmfBkMode.OPAQUE && opts.isOpaque()) { - graphicsCtx.setPaint(prop.getBackgroundColor().getColor()); - graphicsCtx.fill(clip); - } - if (opts.isClipped()) { - graphicsCtx.setClip(clip); - } - graphicsCtx.setTransform(at); - } + updateClipping(graphicsCtx, clip, angle, opts); + + // TODO: Check: certain images don't use the reference of the extTextOut, but rely on a moveto issued beforehand + Point2D moveTo = (reference.distance(0,0) == 0) ? prop.getLocation() : reference; + graphicsCtx.translate(moveTo.getX(), moveTo.getY()); - graphicsCtx.translate(reference.getX(), reference.getY()); graphicsCtx.rotate(angle); if (scale != null) { graphicsCtx.scale(scale.getWidth() < 0 ? -1 : 1, scale.getHeight() < 0 ? -1 : 1); @@ -595,17 +497,74 @@ public class HwmfGraphics { } } - private void addAttributes(AttributedString as, HwmfFont font, String typeface) { - as.addAttribute(TextAttribute.FAMILY, typeface); - as.addAttribute(TextAttribute.SIZE, getFontHeight(font)); + /** + * The dx array indicate the distance between origins of adjacent character cells. + * For example, dx[i] logical units separate the origins of character cell i and character cell i + 1. + * So dx{i] is the complete width of the current char + space to the next character + * + * In AWT we have the {@link TextAttribute#TRACKING} attribute, which works very similar. + * As we don't know (yet) the calculation based on the font size/height, we interpolate + * between the default tracking and a tracking value of 1 + */ + private void calculateDx(String textString, List<Integer> dx, HwmfFont font, FontInfo fontInfo, FontRenderContext frc, AttributedString as) { + if (dx == null || dx.isEmpty()) { + return; + } + final List<DxLayout> dxList = new ArrayList<>(); + + Map<TextAttribute,Object> fontAtt = new HashMap<>(); + // Font tracking default (= 0) + addAttributes(fontAtt::put, font, fontInfo.getTypeface()); + final GlyphVector gv0 = new Font(fontAtt).createGlyphVector(frc, textString); + // Font tracking = 1 + fontAtt.put(TextAttribute.TRACKING, 1); + final GlyphVector gv1 = new Font(fontAtt).createGlyphVector(frc, textString); + + int beginIndex = 0; + for (int offset = 0; offset < dx.size(); offset++) { + if (beginIndex >= textString.length()) { + break; + } + DxLayout dxLayout = new DxLayout(); + dxLayout.dx = dx.get(offset); + dxLayout.pos0 = gv0.getGlyphPosition(offset).getX(); + dxLayout.pos1 = gv1.getGlyphPosition(offset).getX(); + dxLayout.beginIndex = beginIndex; + dxLayout.endIndex = textString.offsetByCodePoints(beginIndex, 1); + dxList.add(dxLayout); + + beginIndex = dxLayout.endIndex; + } + + // Calculate the linear (y ~= Tracking setting / x ~= character spacing / target value) + // y = m * x + n + // y = ((y2-y1)/(x2-x1))x + ((y1x2-y2x1)/(x2-x1)) + + DxLayout dx0 = null; + for (DxLayout dx1 : dxList) { + if (dx0 != null) { + // Default Tracking = 0 (y1) + double y1 = 0, x1 = dx1.pos0-dx0.pos0; + // Second Tracking = 1 (y2) + double y2 = 1, x2 = dx1.pos1-dx0.pos1; + double track = ((y2-y1)/(x2-x1))*dx0.dx + ((y1*x2-y2*x1)/(x2-x1)); + as.addAttribute(TextAttribute.TRACKING, (float)track, dx0.beginIndex, dx0.endIndex); + } + dx0 = dx1; + } + } + + private void addAttributes(BiConsumer<TextAttribute,Object> attributes, HwmfFont font, String typeface) { + attributes.accept(TextAttribute.FAMILY, typeface); + attributes.accept(TextAttribute.SIZE, getFontHeight(font)); if (font.isStrikeOut()) { - as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); + attributes.accept(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); } if (font.isUnderline()) { - as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); + attributes.accept(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); } if (font.isItalic()) { - as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE); + attributes.accept(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(); @@ -616,7 +575,7 @@ public class HwmfGraphics { break; } } - as.addAttribute(TextAttribute.WEIGHT, awtFW); + attributes.accept(TextAttribute.WEIGHT, awtFW); } private double getFontHeight(HwmfFont font) { @@ -630,10 +589,102 @@ public class HwmfGraphics { // TODO: fix font height calculation // the height is given as font size + ascent + descent // as an approximation we reduce the height by a static factor + // + // see https://stackoverflow.com/a/26564924/2066598 on to get the font size from the cell height return fontHeight*3/4; } } + private static Charset getCharset(HwmfFont font, boolean isUnicode) { + if (isUnicode) { + return Charsets.UTF_16LE; + } + + Charset charset = font.getCharset().getCharset(); + return (charset == null) ? DEFAULT_CHARSET : charset; + } + + private static String trimText(HwmfFont font, boolean isUnicode, byte[] text, int length) { + final Charset charset = getCharset(font, isUnicode); + + int trimLen; + for (trimLen=0; trimLen<text.length; trimLen+=2) { + if (trimLen == text.length-1) { + if (text[trimLen] != 0) { + trimLen++; + } + break; + } else if ((text[trimLen] == -1 && text[trimLen+1] == -1) || + ((text[trimLen] & 0xE0) == 0 && text[trimLen+1] == 0)) { + break; + } + } + + String textString = new String(text, 0, trimLen, charset); + return textString.substring(0, Math.min(textString.length(), length)); + } + + private void updateHorizontalAlign(AffineTransform tx, TextLayout layout) { + switch (prop.getTextAlignLatin()) { + default: + case LEFT: + break; + case CENTER: + tx.translate(-layout.getBounds().getWidth() / 2., 0); + break; + case RIGHT: + tx.translate(-layout.getAdvance(), 0); + break; + } + } + + private void updateVerticalAlign(AffineTransform tx, TextLayout layout) { + // TODO: check min/max orientation + switch (prop.getTextVAlignLatin()) { + case TOP: + tx.translate(0, layout.getAscent()); + break; + default: + case BASELINE: + break; + case BOTTOM: + tx.translate(0, -(layout.getBounds().getHeight()-layout.getDescent())); + break; + } + } + + private void updateClipping(Graphics2D graphicsCtx, Rectangle2D clip, double angle, WmfExtTextOutOptions opts) { + if (clip == null || clip.getBounds2D().isEmpty()) { + return; + } + + final AffineTransform at = graphicsCtx.getTransform(); + + graphicsCtx.translate(-clip.getCenterX(), -clip.getCenterY()); + graphicsCtx.rotate(angle); + graphicsCtx.translate(clip.getCenterX(), clip.getCenterY()); + if (prop.getBkMode() == HwmfBkMode.OPAQUE && opts.isOpaque()) { + graphicsCtx.setPaint(prop.getBackgroundColor().getColor()); + graphicsCtx.fill(clip); + } + if (opts.isClipped()) { + graphicsCtx.setClip(clip); + } + + graphicsCtx.setTransform(at); + } + + private Point2D getRotatedOffset(double angle, FontRenderContext frc, AttributedString as) { + final TextLayout layout = new TextLayout(as.getIterator(), frc); + final AffineTransform tx = new AffineTransform(); + updateHorizontalAlign(tx, layout); + updateVerticalAlign(tx, layout); + + tx.rotate(angle); + Point2D src = new Point2D.Double(); + return tx.transform(src, null); + } + public void drawImage(BufferedImage img, Rectangle2D srcBounds, Rectangle2D dstBounds) { drawImage(new BufferedImageRenderer(img), srcBounds, dstBounds); } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java index f96e02199a..bbeb4ac98b 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java @@ -50,12 +50,12 @@ public class HwmfText { private static final int MAX_RECORD_LENGTH = 1_000_000; /** - * The META_SETTEXTCHAREXTRA record defines inter-character spacing for text justification in the + * The META_SETTEXTCHAREXTRA record defines inter-character spacing for text justification in the * playback device context. Spacing is added to the white space between each character, including * break characters, when a line of justified text is output. */ public static class WmfSetTextCharExtra implements HwmfRecord { - + /** * A 16-bit unsigned integer that defines the amount of extra space, in * logical units, to be added to each character. If the current mapping mode is not MM_TEXT, @@ -63,12 +63,12 @@ public class HwmfText { * mapping mode, see META_SETMAPMODE */ private int charExtra; - + @Override public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setTextCharExtra; } - + @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { charExtra = leis.readUShort(); @@ -85,19 +85,19 @@ public class HwmfText { return GenericRecordUtil.getGenericProperties("charExtra", () -> charExtra); } } - + /** * The META_SETTEXTCOLOR record defines the text foreground color in the playback device context. */ public static class WmfSetTextColor implements HwmfRecord { - + protected final HwmfColorRef colorRef = new HwmfColorRef(); - + @Override public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setTextColor; } - + @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { return colorRef.init(leis); @@ -122,18 +122,18 @@ public class HwmfText { return GenericRecordUtil.getGenericProperties("colorRef", this::getColorRef); } } - + /** * The META_SETTEXTJUSTIFICATION record defines the amount of space to add to break characters * in a string of justified text. */ public static class WmfSetTextJustification implements HwmfRecord { - + /** * A 16-bit unsigned integer that specifies the number of space characters in the line. */ private int breakCount; - + /** * A 16-bit unsigned integer that specifies the total extra space, in logical * units, to be added to the line of text. If the current mapping mode is not MM_TEXT, the value @@ -141,12 +141,12 @@ public class HwmfText { * details about setting the mapping mode, see {@link WmfSetMapMode}. */ private int breakExtra; - + @Override public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setBkColor; } - + @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { breakCount = leis.readUShort(); @@ -167,7 +167,7 @@ public class HwmfText { ); } } - + /** * The META_TEXTOUT record outputs a character string at the specified location by using the font, * background color, and text color that are defined in the playback device context. @@ -193,7 +193,7 @@ public class HwmfText { public HwmfRecordType getWmfRecordType() { return HwmfRecordType.textOut; } - + @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { stringLength = leis.readShort(); @@ -239,6 +239,7 @@ public class HwmfText { } } + @SuppressWarnings("unused") public static class WmfExtTextOutOptions implements GenericRecord { /** * Indicates that the background color that is defined in the playback device context @@ -361,18 +362,18 @@ public class HwmfText { */ protected final WmfExtTextOutOptions options; /** - * An optional 8-byte Rect Object (section 2.2.2.18) that defines the + * An optional 8-byte Rect Object (section 2.2.2.18) that defines the * dimensions, in logical coordinates, of a rectangle that is used for clipping, opaquing, or both. - * + * * The corners are given in the order left, top, right, bottom. - * Each value is a 16-bit signed integer that defines the coordinate, in logical coordinates, of + * Each value is a 16-bit signed integer that defines the coordinate, in logical coordinates, of * the upper-left corner of the rectangle */ protected final Rectangle2D bounds = new Rectangle2D.Double(); /** - * A variable-length string that specifies the text to be drawn. The string does - * not need to be null-terminated, because StringLength specifies the length of the string. If - * the length is odd, an extra byte is placed after it so that the following member (optional Dx) is + * A variable-length string that specifies the text to be drawn. The string does + * not need to be null-terminated, because StringLength specifies the length of the string. If + * the length is odd, an extra byte is placed after it so that the following member (optional Dx) is * aligned on a 16-bit boundary. */ protected byte[] rawTextBytes; @@ -396,7 +397,7 @@ public class HwmfText { public HwmfRecordType getWmfRecordType() { return HwmfRecordType.extTextOut; } - + @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { // -6 bytes of record function and length header @@ -413,16 +414,16 @@ public class HwmfText { // the bounding rectangle is optional and only read when options are given size += readRectS(leis, bounds); } - + rawTextBytes = IOUtils.safelyAllocate(stringLength+(stringLength&1), MAX_RECORD_LENGTH); leis.readFully(rawTextBytes); size += rawTextBytes.length; - + if (size >= remainingRecordSize) { logger.log(POILogger.INFO, "META_EXTTEXTOUT doesn't contain character tracking info"); return size; } - + int dxLen = Math.min(stringLength, (remainingRecordSize-size)/LittleEndianConsts.SHORT_SIZE); if (dxLen < stringLength) { logger.log(POILogger.WARN, "META_EXTTEXTOUT tracking info doesn't cover all characters"); @@ -432,7 +433,7 @@ public class HwmfText { dx.add((int)leis.readShort()); size += LittleEndianConsts.SHORT_SIZE; } - + return size; } @@ -480,23 +481,24 @@ public class HwmfText { return GenericRecordUtil.getGenericProperties( "reference", this::getReference, "bounds", this::getBounds, - "text", this::getGenericText + "text", this::getGenericText, + "dx", () -> dx ); } } - + public enum HwmfTextAlignment { LEFT, RIGHT, CENTER } - + public enum HwmfTextVerticalAlignment { TOP, BOTTOM, BASELINE } - + /** * The META_SETTEXTALIGN record defines text-alignment values in the playback device context. */ @@ -572,13 +574,13 @@ public class HwmfText { * The reference point MUST be on the left edge of the bounding rectangle. */ private static final int VALIGN_BOTTOM = 1; - + /** * Flag TA_BASELINE (0x0018) / VTA_BASELINE (0x0018): * The reference point MUST be on the baseline of the text. */ private static final int VALIGN_BASELINE = 3; - + /** * A 16-bit unsigned integer that defines text alignment. * This value MUST be a combination of one or more TextAlignmentMode Flags @@ -586,12 +588,12 @@ public class HwmfText { * for text with a vertical baseline. */ protected int textAlignmentMode; - + @Override public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setTextAlign; } - + @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { textAlignmentMode = leis.readUShort(); @@ -670,7 +672,7 @@ public class HwmfText { } } } - + public static class WmfCreateFontIndirect implements HwmfRecord, HwmfObjectTableEntry { protected final HwmfFont font; @@ -686,7 +688,7 @@ public class HwmfText { public HwmfRecordType getWmfRecordType() { return HwmfRecordType.createFontIndirect; } - + @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { return font.init(leis, recordSize); @@ -696,7 +698,7 @@ public class HwmfText { public void draw(HwmfGraphics ctx) { ctx.addObjectTableEntry(this); } - + @Override public void applyObject(HwmfGraphics ctx) { ctx.getProperties().setFont(font); diff --git a/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/TestHemfPicture.java b/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/TestHemfPicture.java index 375d8a8cf5..0cea2261d7 100644 --- a/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/TestHemfPicture.java +++ b/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/TestHemfPicture.java @@ -51,6 +51,7 @@ import org.apache.poi.util.IOUtils; import org.apache.poi.util.RecordFormatException; import org.junit.Test; +@SuppressWarnings("StatementWithEmptyBody") public class TestHemfPicture { private static final POIDataSamples ss_samples = POIDataSamples.getSpreadSheetInstance(); @@ -77,44 +78,49 @@ public class TestHemfPicture { PPTX2PNG.main(args); } */ + /* @Test @Ignore("Only for manual tests - need to add org.tukaani:xz:1.8 for this to work") public void paintMultiple() throws Exception { - final byte buf[] = new byte[50_000_000]; + Pattern fileExt = Pattern.compile("(?i)^(.+/)*(.+)\\.(emf|wmf)$"); + final byte[] buf = new byte[50_000_000]; try (SevenZFile sevenZFile = new SevenZFile(new File("tmp/plus_emf.7z")) ) { SevenZArchiveEntry entry; while ((entry = sevenZFile.getNextEntry()) != null) { - final String etName = entry.getName(); - - if (entry.isDirectory() || !etName.endsWith(".emf")) continue; + if (entry.isDirectory() || entry.getSize() == 0) continue; + Matcher m = fileExt.matcher(entry.getName()); + if (!m.matches()) continue; int size = sevenZFile.read(buf); ByteArrayInputStream bis = new ByteArrayInputStream(buf, 0, size); System.setIn(bis); - String lastName = etName.replaceFirst(".+/", ""); - String[] args = { "-format", "png", // png,gif,jpg or null for test "-outdir", new File("build/tmp/").getCanonicalPath(), - "-outfile", lastName.replace(".emf", ".png"), + "-outfile", m.replaceAll("$2.png"), "-fixside", "long", "-scale", "800", "-ignoreParse", + "-inputtype", m.replaceAll("$3").toUpperCase(), // "-dump", new File("build/tmp/", lastName.replace(".emf",".json")).getCanonicalPath(), - // "-quiet", + "-quiet", // "-extractEmbedded", "stdin" }; - PPTX2PNG.main(args); + try { + PPTX2PNG.main(args); + System.out.println("Processing "+entry.getName()+" ok"); + } catch (Exception e) { + System.out.println("Processing "+entry.getName()+" failed"); + } } } } - */ - +*/ @Test public void testBasicWindows() throws Exception { try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_windows.emf")) { @@ -272,7 +278,7 @@ public class TestHemfPicture { public void testInfiniteLoopOnFile() throws Exception { try (InputStream is = ss_samples.openResourceAsStream("61294.emf")) { HemfPicture pic = new HemfPicture(is); - for (HemfRecord record : pic) { + for (HemfRecord ignored : pic) { } } @@ -286,7 +292,7 @@ public class TestHemfPicture { is.close(); HemfPicture pic = new HemfPicture(new ByteArrayInputStream(bos.toByteArray())); - for (HemfRecord record : pic) { + for (HemfRecord ignored : pic) { } } |