]> source.dussan.org Git - poi.git/commitdiff
Bug 60656 - Emf image support in slideshows
authorAndreas Beeker <kiwiwings@apache.org>
Sun, 5 Apr 2020 00:42:08 +0000 (00:42 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Sun, 5 Apr 2020 00:42:08 +0000 (00:42 +0000)
- 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

src/java/org/apache/poi/sl/draw/DrawFontManager.java
src/java/org/apache/poi/sl/draw/DrawFontManagerDefault.java
src/java/org/apache/poi/util/StringUtil.java
src/ooxml/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java
src/ooxml/java/org/apache/poi/xslf/util/OutputFormat.java
src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java
src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java
src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java
src/scratchpad/testcases/org/apache/poi/hemf/usermodel/TestHemfPicture.java

index 0c16dd994ad49ffa1052d1e4bd9cf6972f169e9d..72216b36bdd444647d3b65aa1fc4907a2717147b 100644 (file)
@@ -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);
index 7dcd704c4eec4e9adfc149048021da17c8ad6774..e5b5f5134963b0e4db7de028f731ad2df26db3dc 100644 (file)
@@ -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;
+    }
 }
index fb69fbd538f0abd19ef8d5d608a2500b62c9fa5b..b946d654303555219d6281ca1b741506ffd24a78 100644 (file)
@@ -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
     }
@@ -293,16 +289,6 @@ public class StringUtil {
         return false;
     }
 
-    /**
-     * 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.
      */
@@ -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 = {
index a057d6db7d88dee32319265729fc066267dd47d8..b2b40f9163933dc4a86de91cb1b8ab7a97ac33c8 100644 (file)
@@ -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();
     }
 
index 9f0a6f77420e39378326b71d7b6ca7167e77e1f6..23c71b0d76ae1172c51eb30a40a2ecd1ee913c99 100644 (file)
@@ -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;
         }
index 1e4c92a5b1360dafca761bb583d4bfb077768b7e..4b2a7a600bafce305207fe00fdcf17986434249a 100644 (file)
@@ -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();
index 37919c9d69b4b947744b0ac13ea967f78f478541..cf35b716ae6ed6243e3580c3f23bdd38153c97b1 100644 (file)
@@ -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);
     }
index f96e02199ad7e9b54736264a8ea2173ee50abdd1..bbeb4ac98b6becabaa977ec0cac3a893ef3142fb 100644 (file)
@@ -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);
index 375d8a8cf538745ef0efca27d2c50340a7ee1e7a..0cea2261d76fb0cecc76d8f163f20cd12bc4bc41 100644 (file)
@@ -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) {
 
             }
         }