diff --git a/build.gradle b/build.gradle index 63e1d34e8d..9c4afa814b 100644 --- a/build.gradle +++ b/build.gradle @@ -91,7 +91,7 @@ subprojects { // See https://github.com/melix/japicmp-gradle-plugin apply plugin: 'me.champeau.gradle.japicmp' - version = '4.0.2-SNAPSHOT' + version = '4.1.0-SNAPSHOT' ext { japicmpversion = '4.0.0' } diff --git a/build.xml b/build.xml index d7032dc1c2..e512230ac7 100644 --- a/build.xml +++ b/build.xml @@ -42,7 +42,7 @@ under the License. The Apache POI project Ant build. - + diff --git a/sonar/examples/pom.xml b/sonar/examples/pom.xml index 22c576a20d..b6854c9f7b 100644 --- a/sonar/examples/pom.xml +++ b/sonar/examples/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 4.0.2-SNAPSHOT + 4.1.0-SNAPSHOT poi-examples jar diff --git a/sonar/excelant/pom.xml b/sonar/excelant/pom.xml index 220d4eb1e9..2953351f8e 100644 --- a/sonar/excelant/pom.xml +++ b/sonar/excelant/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 4.0.2-SNAPSHOT + 4.1.0-SNAPSHOT poi-excelant jar diff --git a/sonar/main/pom.xml b/sonar/main/pom.xml index 9bd0af96a2..2c4eed2720 100644 --- a/sonar/main/pom.xml +++ b/sonar/main/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 4.0.2-SNAPSHOT + 4.1.0-SNAPSHOT poi-main jar diff --git a/sonar/ooxml-schema-encryption/pom.xml b/sonar/ooxml-schema-encryption/pom.xml index d768a5f4b3..9a0b19d190 100644 --- a/sonar/ooxml-schema-encryption/pom.xml +++ b/sonar/ooxml-schema-encryption/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 4.0.2-SNAPSHOT + 4.1.0-SNAPSHOT .. poi-ooxml-schema-encryption diff --git a/sonar/ooxml-schema-security/pom.xml b/sonar/ooxml-schema-security/pom.xml index 06318b2be9..a9d7418bbf 100644 --- a/sonar/ooxml-schema-security/pom.xml +++ b/sonar/ooxml-schema-security/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 4.0.2-SNAPSHOT + 4.1.0-SNAPSHOT .. poi-ooxml-schema-security diff --git a/sonar/ooxml-schema/pom.xml b/sonar/ooxml-schema/pom.xml index 7bb69433da..3b5f5c28f4 100644 --- a/sonar/ooxml-schema/pom.xml +++ b/sonar/ooxml-schema/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 4.0.2-SNAPSHOT + 4.1.0-SNAPSHOT .. poi-ooxml-schema diff --git a/sonar/ooxml/pom.xml b/sonar/ooxml/pom.xml index 6a6232acaa..7de88404cc 100644 --- a/sonar/ooxml/pom.xml +++ b/sonar/ooxml/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 4.0.2-SNAPSHOT + 4.1.0-SNAPSHOT poi-ooxml jar diff --git a/sonar/pom.xml b/sonar/pom.xml index c008c3a1c4..5fce9d97c5 100644 --- a/sonar/pom.xml +++ b/sonar/pom.xml @@ -4,7 +4,7 @@ org.apache.poi poi-parent pom - 4.0.2-SNAPSHOT + 4.1.0-SNAPSHOT Apache POI - the Java API for Microsoft Documents Maven build of Apache POI for Sonar checks http://poi.apache.org/ diff --git a/sonar/scratchpad/pom.xml b/sonar/scratchpad/pom.xml index a980e15568..5af72fdee3 100644 --- a/sonar/scratchpad/pom.xml +++ b/sonar/scratchpad/pom.xml @@ -6,7 +6,7 @@ org.apache.poi poi-parent - 4.0.2-SNAPSHOT + 4.1.0-SNAPSHOT poi-scratchpad jar diff --git a/src/integrationtest/org/apache/poi/stress/SlideShowHandler.java b/src/integrationtest/org/apache/poi/stress/SlideShowHandler.java index 63ffdbd58c..20460cc2db 100644 --- a/src/integrationtest/org/apache/poi/stress/SlideShowHandler.java +++ b/src/integrationtest/org/apache/poi/stress/SlideShowHandler.java @@ -105,7 +105,6 @@ public abstract class SlideShowHandler extends POIFSFileHandler { for (Slide s : ss.getSlides()) { BufferedImage img = new BufferedImage(pgsize.width, pgsize.height, BufferedImage.TYPE_INT_ARGB); Graphics2D graphics = img.createGraphics(); - DrawFactory.getInstance(graphics).fixFonts(graphics); // default rendering options graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); diff --git a/src/java/org/apache/poi/hssf/dev/BiffViewer.java b/src/java/org/apache/poi/hssf/dev/BiffViewer.java index cb17d84b56..631be8fd2d 100644 --- a/src/java/org/apache/poi/hssf/dev/BiffViewer.java +++ b/src/java/org/apache/poi/hssf/dev/BiffViewer.java @@ -527,20 +527,16 @@ public final class BiffViewer { } @Override public int read(byte[] b, int off, int len) throws IOException { + if (b == null || off < 0 || len < 0 || b.length < off+len) { + throw new IllegalArgumentException(); + } if (_currentPos >= _currentSize) { fillNextBuffer(); } if (_currentPos >= _currentSize) { return -1; } - int availSize = _currentSize - _currentPos; - int result; - if (len > availSize) { - System.err.println("Unexpected request to read past end of current biff record"); - result = availSize; - } else { - result = len; - } + final int result = Math.min(len, _currentSize - _currentPos); System.arraycopy(_data, _currentPos, b, off, result); _currentPos += result; _overallStreamPos += result; diff --git a/src/java/org/apache/poi/hssf/record/RecordInputStream.java b/src/java/org/apache/poi/hssf/record/RecordInputStream.java index 8347655a81..400553721e 100644 --- a/src/java/org/apache/poi/hssf/record/RecordInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordInputStream.java @@ -106,8 +106,8 @@ public final class RecordInputStream implements LittleEndianInput { private final LittleEndianInput _lei; - public SimpleHeaderInput(InputStream in) { - _lei = getLEI(in); + private SimpleHeaderInput(LittleEndianInput lei) { + _lei = lei; } @Override public int available() { @@ -129,8 +129,12 @@ public final class RecordInputStream implements LittleEndianInput { public RecordInputStream(InputStream in, EncryptionInfo key, int initialOffset) throws RecordFormatException { if (key == null) { - _dataInput = getLEI(in); - _bhi = new SimpleHeaderInput(in); + _dataInput = (in instanceof LittleEndianInput) + // accessing directly is an optimisation + ? (LittleEndianInput)in + // less optimal, but should work OK just the same. Often occurs in junit tests. + : new LittleEndianInputStream(in); + _bhi = new SimpleHeaderInput(_dataInput); } else { Biff8DecryptingStream bds = new Biff8DecryptingStream(in, initialOffset, key); _dataInput = bds; @@ -195,11 +199,9 @@ public final class RecordInputStream implements LittleEndianInput { private int readNextSid() { int nAvailable = _bhi.available(); if (nAvailable < EOFRecord.ENCODED_SIZE) { - /*if (nAvailable > 0) { - // some scrap left over? - // ex45582-22397.xls has one extra byte after the last record - // Excel reads that file OK - }*/ + // some scrap left over, if nAvailable > 0? + // ex45582-22397.xls has one extra byte after the last record + // Excel reads that file OK return INVALID_SID_VALUE; } int result = _bhi.readRecordSID(); @@ -305,14 +307,8 @@ public final class RecordInputStream implements LittleEndianInput { @Override public double readDouble() { - long valueLongBits = readLong(); - /*if (Double.isNaN(result)) { - // YK: Excel doesn't write NaN but instead converts the cell type into {@link CellType#ERROR}. - // HSSF prior to version 3.7 had a bug: it could write Double.NaN but could not read such a file back. - // This behavior was fixed in POI-3.7. - //throw new RuntimeException("Did not expect to read NaN"); // (Because Excel typically doesn't write NaN) - }*/ - return Double.longBitsToDouble(valueLongBits); + // YK: Excel doesn't write NaN but instead converts the cell type into {@link CellType#ERROR}. + return Double.longBitsToDouble(readLong()); } public void readPlain(byte[] buf, int off, int len) { @@ -329,7 +325,7 @@ public final class RecordInputStream implements LittleEndianInput { readFully(buf, off, len, false); } - protected void readFully(byte[] buf, int off, int len, boolean isPlain) { + private void readFully(byte[] buf, int off, int len, boolean isPlain) { int origLen = len; if (buf == null) { throw new NullPointerException(); diff --git a/src/java/org/apache/poi/sl/draw/BitmapImageRenderer.java b/src/java/org/apache/poi/sl/draw/BitmapImageRenderer.java index a92c8dc590..c37ce006a0 100644 --- a/src/java/org/apache/poi/sl/draw/BitmapImageRenderer.java +++ b/src/java/org/apache/poi/sl/draw/BitmapImageRenderer.java @@ -40,6 +40,7 @@ import javax.imageio.ImageTypeSpecifier; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.MemoryCacheImageInputStream; +import org.apache.poi.sl.usermodel.PictureData.PictureType; import org.apache.poi.util.IOUtils; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -48,10 +49,23 @@ import org.apache.poi.util.POILogger; * For now this class renders only images supported by the javax.imageio.ImageIO framework. **/ public class BitmapImageRenderer implements ImageRenderer { - private final static POILogger LOG = POILogFactory.getLogger(ImageRenderer.class); + private final static POILogger LOG = POILogFactory.getLogger(BitmapImageRenderer.class); protected BufferedImage img; + @Override + public boolean canRender(String contentType) { + PictureType[] pts = { + PictureType.JPEG, PictureType.PNG, PictureType.BMP, PictureType.GIF + }; + for (PictureType pt : pts) { + if (pt.contentType.equalsIgnoreCase(contentType)) { + return true; + } + } + return false; + } + @Override public void loadImage(InputStream data, String contentType) throws IOException { img = readImage(data, contentType); diff --git a/src/java/org/apache/poi/sl/draw/DrawFactory.java b/src/java/org/apache/poi/sl/draw/DrawFactory.java index 98c41ed993..99c9942b67 100644 --- a/src/java/org/apache/poi/sl/draw/DrawFactory.java +++ b/src/java/org/apache/poi/sl/draw/DrawFactory.java @@ -22,8 +22,6 @@ import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.text.AttributedString; -import java.util.HashMap; -import java.util.Map; import org.apache.poi.sl.usermodel.Background; import org.apache.poi.sl.usermodel.ConnectorShape; @@ -40,18 +38,18 @@ import org.apache.poi.sl.usermodel.TableShape; import org.apache.poi.sl.usermodel.TextBox; import org.apache.poi.sl.usermodel.TextParagraph; import org.apache.poi.sl.usermodel.TextShape; -import org.apache.poi.util.JvmBugs; public class DrawFactory { - protected static final ThreadLocal defaultFactory = new ThreadLocal<>(); + private static final ThreadLocal defaultFactory = new ThreadLocal<>(); /** * Set a custom draw factory for the current thread. * This is a fallback, for operations where usercode can't set a graphics context. * Preferably use the rendering hint {@link Drawable#DRAW_FACTORY} to set the factory. * - * @param factory + * @param factory the custom factory */ + @SuppressWarnings("unused") public static void setDefaultFactory(DrawFactory factory) { defaultFactory.set(factory); } @@ -170,6 +168,7 @@ public class DrawFactory { return new DrawBackground(shape); } + @SuppressWarnings("WeakerAccess") public DrawTextFragment getTextFragment(TextLayout layout, AttributedString str) { return new DrawTextFragment(layout, str); } @@ -213,35 +212,6 @@ public class DrawFactory { } - /** - * Replace font families for Windows JVM 6, which contains a font rendering error. - * This is likely to be removed, when POI upgrades to JDK 7 - * - * @param graphics the graphics context which will contain the font mapping - */ - public void fixFonts(Graphics2D graphics) { - if (!JvmBugs.hasLineBreakMeasurerBug()) return; - @SuppressWarnings("unchecked") - Map fontMap = (Map)graphics.getRenderingHint(Drawable.FONT_MAP); - if (fontMap == null) { - fontMap = new HashMap<>(); - graphics.setRenderingHint(Drawable.FONT_MAP, fontMap); - } - - String fonts[][] = { - { "Calibri", "Lucida Sans" }, - { "Cambria", "Lucida Bright" }, - { "Times New Roman", "Lucida Bright" }, - { "serif", "Lucida Bright" } - }; - - for (String f[] : fonts) { - if (!fontMap.containsKey(f[0])) { - fontMap.put(f[0], f[1]); - } - } - } - /** * Return a FontManager, either registered beforehand or a default implementation * diff --git a/src/java/org/apache/poi/sl/draw/DrawFontManagerDefault.java b/src/java/org/apache/poi/sl/draw/DrawFontManagerDefault.java index c439fc926f..a428943699 100644 --- a/src/java/org/apache/poi/sl/draw/DrawFontManagerDefault.java +++ b/src/java/org/apache/poi/sl/draw/DrawFontManagerDefault.java @@ -22,6 +22,8 @@ package org.apache.poi.sl.draw; import java.awt.Font; import java.awt.Graphics2D; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import org.apache.poi.common.usermodel.fonts.FontInfo; import org.apache.poi.sl.draw.Drawable.DrawableHint; @@ -33,6 +35,13 @@ import org.apache.poi.sl.draw.Drawable.DrawableHint; */ public class DrawFontManagerDefault implements DrawFontManager { + protected final Set knownSymbolFonts = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + + public DrawFontManagerDefault() { + knownSymbolFonts.add("Wingdings"); + knownSymbolFonts.add("Symbol"); + } + @Override public FontInfo getMappedFont(Graphics2D graphics, FontInfo fontInfo) { return getFontWithFallback(graphics, Drawable.FONT_MAP, fontInfo); @@ -49,25 +58,35 @@ public class DrawFontManagerDefault implements DrawFontManager { public String mapFontCharset(Graphics2D graphics, FontInfo fontInfo, String text) { // TODO: find a real charset mapping solution instead of hard coding for Wingdings - String attStr = text; - if (fontInfo != null && "Wingdings".equalsIgnoreCase(fontInfo.getTypeface())) { - // wingdings doesn't contain high-surrogates, so chars are ok - boolean changed = false; - char chrs[] = attStr.toCharArray(); - for (int i=0; i shape) { super(shape); } @@ -59,29 +62,47 @@ public class DrawPictureShape extends DrawSimpleShape { /** * Returns an ImageRenderer for the PictureData * - * @param graphics + * @param graphics the graphics context * @return the image renderer */ + @SuppressWarnings({"WeakerAccess", "unchecked"}) public static ImageRenderer getImageRenderer(Graphics2D graphics, String contentType) { - ImageRenderer renderer = (ImageRenderer)graphics.getRenderingHint(Drawable.IMAGE_RENDERER); - if (renderer != null) { + final ImageRenderer renderer = (ImageRenderer)graphics.getRenderingHint(Drawable.IMAGE_RENDERER); + if (renderer != null && renderer.canRender(contentType)) { return renderer; } - - if (PictureType.WMF.contentType.equals(contentType)) { + + // first try with our default image renderer + final BitmapImageRenderer bir = new BitmapImageRenderer(); + if (bir.canRender(contentType)) { + return bir; + } + + // then iterate through the scratchpad renderers + // + // this could be nicely implemented via a j.u.ServiceLoader, but OSGi makes things complicated ... + // https://blog.osgi.org/2013/02/javautilserviceloader-in-osgi.html + // ... therefore falling back to classloading attempts + ClassLoader cl = ImageRenderer.class.getClassLoader(); + for (String kr : KNOWN_RENDERER) { + final ImageRenderer ir; try { - @SuppressWarnings("unchecked") - Class irc = (Class) - DrawPictureShape.class.getClassLoader().loadClass(WMF_IMAGE_RENDERER); - return irc.newInstance(); - } catch (Exception e) { - // WMF image renderer is not on the classpath, continuing with BitmapRenderer - // although this doesn't make much sense ... - LOG.log(POILogger.ERROR, "WMF image renderer is not on the classpath - include poi-scratchpad jar!", e); + ir = ((Class)cl.loadClass(kr)).newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + // scratchpad was not on the path, ignore and continue + LOG.log(POILogger.INFO, "Known image renderer '"+kr+" not found/loaded - include poi-scratchpad jar!", e); + continue; + } + if (ir.canRender(contentType)) { + return ir; } } - - return new BitmapImageRenderer(); + + LOG.log(POILogger.WARN, "No suiteable image renderer found for content-type '"+ + contentType+"' - include poi-scratchpad jar!"); + + // falling back to BitmapImageRenderer, at least it gracefully handles invalid images + return bir; } @Override diff --git a/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java b/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java index 938d46230a..1ab4af3a1e 100644 --- a/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java +++ b/src/java/org/apache/poi/sl/draw/DrawTextParagraph.java @@ -254,7 +254,6 @@ public class DrawTextParagraph implements Drawable { lines.clear(); DrawFactory fact = DrawFactory.getInstance(graphics); - fact.fixFonts(graphics); StringBuilder text = new StringBuilder(); AttributedString at = getAttributedString(graphics, text); boolean emptyParagraph = text.toString().trim().isEmpty(); @@ -635,13 +634,6 @@ public class DrawTextParagraph implements Drawable { *
  • determine the font group - a text run can have different font groups. Depending on the chars, * the correct font group needs to be used * - * @param graphics - * @param dfm - * @param attList - * @param beginIndex - * @param run - * @param runText - * * @see Office Open XML Themes, Schemes, and Fonts */ private void processGlyphs(Graphics2D graphics, DrawFontManager dfm, List attList, final int beginIndex, TextRun run, String runText) { diff --git a/src/java/org/apache/poi/sl/draw/DrawTextShape.java b/src/java/org/apache/poi/sl/draw/DrawTextShape.java index 413ab218c9..c4dd65bb75 100644 --- a/src/java/org/apache/poi/sl/draw/DrawTextShape.java +++ b/src/java/org/apache/poi/sl/draw/DrawTextShape.java @@ -40,8 +40,6 @@ public class DrawTextShape extends DrawSimpleShape { @Override public void drawContent(Graphics2D graphics) { - DrawFactory.getInstance(graphics).fixFonts(graphics); - TextShape s = getShape(); Rectangle2D anchor = DrawShape.getAnchor(graphics, s); @@ -219,10 +217,9 @@ public class DrawTextShape extends DrawSimpleShape { graphics.addRenderingHints(oldGraphics.getRenderingHints()); graphics.setTransform(oldGraphics.getTransform()); } - DrawFactory.getInstance(graphics).fixFonts(graphics); return drawParagraphs(graphics, 0, 0); } - + @Override protected TextShape> getShape() { return (TextShape>)shape; diff --git a/src/java/org/apache/poi/sl/draw/ImageRenderer.java b/src/java/org/apache/poi/sl/draw/ImageRenderer.java index 7ecc96a967..b2355b3012 100644 --- a/src/java/org/apache/poi/sl/draw/ImageRenderer.java +++ b/src/java/org/apache/poi/sl/draw/ImageRenderer.java @@ -75,6 +75,13 @@ import java.io.InputStream; * */ public interface ImageRenderer { + /** + * Determines if this image renderer implementation supports the given contentType + * @param contentType the image content type + * @return if the content type is supported + */ + boolean canRender(String contentType); + /** * Load and buffer the image * diff --git a/src/java/org/apache/poi/util/Dimension2DDouble.java b/src/java/org/apache/poi/util/Dimension2DDouble.java new file mode 100644 index 0000000000..227c0cddd5 --- /dev/null +++ b/src/java/org/apache/poi/util/Dimension2DDouble.java @@ -0,0 +1,76 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.util; + +import java.awt.geom.Dimension2D; + +/** + * @since 4.1.0 + */ +public class Dimension2DDouble extends Dimension2D { + + double width; + double height; + + public Dimension2DDouble() { + width = 0d; + height = 0d; + } + + public Dimension2DDouble(double width, double height) { + this.width = width; + this.height = height; + } + + @Override + public double getWidth() { + return width; + } + + @Override + public double getHeight() { + return height; + } + + @Override + public void setSize(double width, double height) { + this.width = width; + this.height = height; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Dimension2DDouble) { + Dimension2DDouble other = (Dimension2DDouble) obj; + return width == other.width && height == other.height; + } + + return false; + } + + @Override + public int hashCode() { + double sum = width + height; + return (int) Math.ceil(sum * (sum + 1) / 2 + width); + } + + @Override + public String toString() { + return "Dimension2DDouble[" + width + ", " + height + "]"; + } +} diff --git a/src/java/org/apache/poi/util/IOUtils.java b/src/java/org/apache/poi/util/IOUtils.java index 009510ffc6..3dd4cb0d57 100644 --- a/src/java/org/apache/poi/util/IOUtils.java +++ b/src/java/org/apache/poi/util/IOUtils.java @@ -50,6 +50,7 @@ public final class IOUtils { * @param maxOverride The number of bytes that should be possible to be allocated in one step. * @since 4.0.0 */ + @SuppressWarnings("unused") public static void setByteArrayMaxOverride(int maxOverride) { BYTE_ARRAY_MAX_OVERRIDE = maxOverride; } @@ -395,13 +396,35 @@ public final class IOUtils { * @throws IOException If copying the data fails. */ public static long copy(InputStream inp, OutputStream out) throws IOException { + return copy(inp, out, -1); + } + + /** + * Copies all the data from the given InputStream to the OutputStream. It + * leaves both streams open, so you will still need to close them once done. + * + * @param inp The {@link InputStream} which provides the data + * @param out The {@link OutputStream} to write the data to + * @param limit limit the copied bytes - use {@code -1} for no limit + * @return the amount of bytes copied + * + * @throws IOException If copying the data fails. + */ + public static long copy(InputStream inp, OutputStream out, long limit) throws IOException { final byte[] buff = new byte[4096]; long totalCount = 0; - for (int count; (count = inp.read(buff)) != -1; totalCount += count) { - if (count > 0) { - out.write(buff, 0, count); + int readBytes = -1; + do { + int todoBytes = (int)((limit < 0) ? buff.length : Math.min(limit-totalCount, buff.length)); + if (todoBytes > 0) { + readBytes = inp.read(buff, 0, todoBytes); + if (readBytes > 0) { + out.write(buff, 0, readBytes); + totalCount += readBytes; + } } - } + } while (readBytes >= 0 && (limit == -1 || totalCount < limit)); + return totalCount; } diff --git a/src/java/org/apache/poi/util/LittleEndianInputStream.java b/src/java/org/apache/poi/util/LittleEndianInputStream.java index 886720f3f1..6199c4dd12 100644 --- a/src/java/org/apache/poi/util/LittleEndianInputStream.java +++ b/src/java/org/apache/poi/util/LittleEndianInputStream.java @@ -17,6 +17,7 @@ package org.apache.poi.util; +import java.io.BufferedInputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; @@ -29,12 +30,16 @@ import java.io.InputStream; */ public class LittleEndianInputStream extends FilterInputStream implements LittleEndianInput { + private static final int BUFFERED_SIZE = 8096; + private static final int EOF = -1; + private int readIndex = 0; + private int markIndex = -1; public LittleEndianInputStream(InputStream is) { - super(is); + super(is.markSupported() ? is : new BufferedInputStream(is, BUFFERED_SIZE)); } - + @Override @SuppressForbidden("just delegating") public int available() { @@ -60,7 +65,18 @@ public class LittleEndianInputStream extends FilterInputStream implements Little } return LittleEndian.getUByte(buf); } - + + /** + * get a float value, reads it in little endian format + * then converts the resulting revolting IEEE 754 (curse them) floating + * point number to a happy java float + * + * @return the float (32-bit) value + */ + public float readFloat() { + return Float.intBitsToFloat( readInt() ); + } + @Override public double readDouble() { return Double.longBitsToDouble(readLong()); @@ -137,14 +153,42 @@ public class LittleEndianInputStream extends FilterInputStream implements Little } } - //Makes repeated calls to super.read() until length is read or EOF is reached + @Override + public int read(byte[] b, int off, int len) throws IOException { + int readBytes = super.read(b, off, len); + readIndex += readBytes; + return readBytes; + } + + @Override + public synchronized void mark(int readlimit) { + super.mark(readlimit); + markIndex = readIndex; + } + + @Override + public synchronized void reset() throws IOException { + super.reset(); + if (markIndex > -1) { + readIndex = markIndex; + markIndex = -1; + } + } + + public int getReadIndex() { + return readIndex; + } + + + + //Makes repeated calls to super.read() until length is read or EOF is reached private int _read(byte[] buffer, int offset, int length) throws IOException { //lifted directly from org.apache.commons.io.IOUtils 2.4 int remaining = length; while (remaining > 0) { int location = length - remaining; int count = read(buffer, offset + location, remaining); - if (EOF == count) { // EOF + if (EOF == count) { break; } remaining -= count; @@ -157,4 +201,9 @@ public class LittleEndianInputStream extends FilterInputStream implements Little public void readPlain(byte[] buf, int off, int len) { readFully(buf, off, len); } + + + public void skipFully(int len) throws IOException { + IOUtils.skipFully(this, len); + } } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java index 71a42ef9e1..0977a403a1 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java @@ -177,7 +177,7 @@ public class SignatureConfig { /** * if true, the signature is added to the existing signatures * - * @since POI 4.0.2 + * @since POI 4.1.0 */ private boolean allowMultipleSignatures = false; @@ -1019,7 +1019,7 @@ public class SignatureConfig { /** * @return true, if multiple signatures can be attached * - * @since POI 4.0.2 + * @since POI 4.1.0 */ public boolean isAllowMultipleSignatures() { return allowMultipleSignatures; @@ -1031,7 +1031,7 @@ public class SignatureConfig { * @param allowMultipleSignatures if true, the signature will be added, * otherwise all existing signatures will be replaced by the current * - * @since POI 4.0.2 + * @since POI 4.1.0 */ public void setAllowMultipleSignatures(boolean allowMultipleSignatures) { this.allowMultipleSignatures = allowMultipleSignatures; diff --git a/src/ooxml/java/org/apache/poi/xdgf/geom/Dimension2dDouble.java b/src/ooxml/java/org/apache/poi/xdgf/geom/Dimension2dDouble.java index 94b887e04e..2363abb939 100644 --- a/src/ooxml/java/org/apache/poi/xdgf/geom/Dimension2dDouble.java +++ b/src/ooxml/java/org/apache/poi/xdgf/geom/Dimension2dDouble.java @@ -17,57 +17,12 @@ package org.apache.poi.xdgf.geom; -import java.awt.geom.Dimension2D; +import org.apache.poi.util.Removal; -public class Dimension2dDouble extends Dimension2D { - - double width; - double height; - - public Dimension2dDouble() { - width = 0d; - height = 0d; - } - - public Dimension2dDouble(double width, double height) { - this.width = width; - this.height = height; - } - - @Override - public double getWidth() { - return width; - } - - @Override - public double getHeight() { - return height; - } - - @Override - public void setSize(double width, double height) { - this.width = width; - this.height = height; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof Dimension2dDouble) { - Dimension2dDouble other = (Dimension2dDouble) obj; - return width == other.width && height == other.height; - } - - return false; - } - - @Override - public int hashCode() { - double sum = width + height; - return (int) Math.ceil(sum * (sum + 1) / 2 + width); - } - - @Override - public String toString() { - return "Dimension2dDouble[" + width + ", " + height + "]"; - } +/** + * @deprecated in 4.1.0 - use org.apache.poi.util.Dimension2DDouble + */ +@Deprecated +@Removal(version = "5.0.0") +public class Dimension2dDouble extends org.apache.poi.util.Dimension2DDouble { } diff --git a/src/ooxml/java/org/apache/poi/xslf/draw/SVGImageRenderer.java b/src/ooxml/java/org/apache/poi/xslf/draw/SVGImageRenderer.java index 2a03dda691..a40c169516 100644 --- a/src/ooxml/java/org/apache/poi/xslf/draw/SVGImageRenderer.java +++ b/src/ooxml/java/org/apache/poi/xslf/draw/SVGImageRenderer.java @@ -39,6 +39,7 @@ import org.apache.batik.ext.awt.image.renderable.ClipRable8Bit; import org.apache.batik.gvt.GraphicsNode; import org.apache.batik.util.XMLResourceDescriptor; import org.apache.poi.sl.draw.ImageRenderer; +import org.apache.poi.sl.usermodel.PictureData; import org.w3c.dom.Document; public class SVGImageRenderer implements ImageRenderer { @@ -133,4 +134,9 @@ public class SVGImageRenderer implements ImageRenderer { return true; } + + @Override + public boolean canRender(String contentType) { + return PictureData.PictureType.SVG.contentType.equalsIgnoreCase(contentType); + } } diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java index b0cbc59dd1..c5e47415df 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XMLSlideShow.java @@ -310,7 +310,7 @@ public class XMLSlideShow extends POIXMLDocument /** * This method is used to create template for chart XML. * @return Xslf chart object - * @since POI 4.0.2 + * @since POI 4.1.0 */ public XSLFChart createChart() { int chartIdx = findNextAvailableFileNameIndex(XSLFRelation.CHART, _charts.size() + 1); diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFChart.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFChart.java index 5e8d73cd06..97f3ec23ee 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFChart.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFChart.java @@ -112,7 +112,7 @@ public final class XSLFChart extends XDDFChart { * @param rID relation id * @param anchor size and location of chart * @return graphic frame object - * @since POI 4.0.2 + * @since POI 4.1.0 */ static CTGraphicalObjectFrame prototype(int shapeId, String rID, Rectangle2D anchor) { CTGraphicalObjectFrame frame = CTGraphicalObjectFrame.Factory.newInstance(); diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFDrawing.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFDrawing.java index ba93988d0b..dca6fed131 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFDrawing.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFDrawing.java @@ -112,7 +112,7 @@ public class XSLFDrawing { * * @param rID relation id of chart * @param rect2D Chart Bounding values - * @since POI 4.0.2 + * @since POI 4.1.0 */ public void addChart(String rID, Rectangle2D rect2D) { CTGraphicalObjectFrame sp = _spTree.addNewGraphicFrame(); diff --git a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java index 2bcf063990..d76142a4c4 100644 --- a/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java +++ b/src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFSheet.java @@ -724,7 +724,7 @@ implements XSLFShapeContainer, Sheet { * this method will add chart into slide * with default height, width, x and y * @param chart xslf chart object - * @since POI 4.0.2 + * @since POI 4.1.0 */ public void addChart(XSLFChart chart) { Rectangle2D rect2D = new java.awt.Rectangle(XDDFChart.DEFAULT_X, XDDFChart.DEFAULT_Y, @@ -737,7 +737,7 @@ implements XSLFShapeContainer, Sheet { * this method will add chart into slide * with given height, width, x and y * @param chart xslf chart object - * @since POI 4.0.2 + * @since POI 4.1.0 */ public void addChart(XSLFChart chart, Rectangle2D rect2D) { RelationPart rp = addRelation(null, XSLFRelation.CHART, chart); 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 b3288d46f3..2ae1293113 100644 --- a/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java +++ b/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java @@ -139,7 +139,6 @@ public class PPTX2PNG { BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D graphics = img.createGraphics(); - DrawFactory.getInstance(graphics).fixFonts(graphics); // default rendering options graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); diff --git a/src/ooxml/testcases/org/apache/poi/sl/TestFonts.java b/src/ooxml/testcases/org/apache/poi/sl/TestFonts.java index c7e950d236..39de26a073 100644 --- a/src/ooxml/testcases/org/apache/poi/sl/TestFonts.java +++ b/src/ooxml/testcases/org/apache/poi/sl/TestFonts.java @@ -130,8 +130,6 @@ public class TestFonts { graphics.setRenderingHint(Drawable.FONT_FALLBACK, fallbackMap); graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); - DrawFactory.getInstance(graphics).fixFonts(graphics); - tb.resizeToFitText(graphics); graphics.dispose(); diff --git a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFSimpleShape.java b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFSimpleShape.java index e35ed35a29..ddbfe26652 100644 --- a/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFSimpleShape.java +++ b/src/ooxml/testcases/org/apache/poi/xslf/usermodel/TestXSLFSimpleShape.java @@ -361,7 +361,6 @@ public class TestXSLFSimpleShape { BufferedImage img = new BufferedImage(pgsize.width, pgsize.height, BufferedImage.TYPE_INT_ARGB); Graphics2D graphics = img.createGraphics(); - DrawFactory.getInstance(graphics).fixFonts(graphics); // default rendering options graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); diff --git a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java new file mode 100644 index 0000000000..98b07d4c6b --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java @@ -0,0 +1,69 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.draw; + +import java.awt.Shape; +import java.awt.geom.Path2D; + +import org.apache.poi.hwmf.draw.HwmfDrawProperties; + +public class HemfDrawProperties extends HwmfDrawProperties { + + /** Path for path bracket operations */ + protected Path2D path = null; + protected boolean usePathBracket = false; + + + public HemfDrawProperties() { + } + + public HemfDrawProperties(HemfDrawProperties other) { + super(other); + path = (other.path != null) ? (Path2D)other.path.clone() : null; + // TODO: check how to clone + clip = other.clip; + } + + /** + * @return the current path used for bracket operations + */ + public Path2D getPath() { + return path; + } + + /** + * Un-/Sets the bracket path + * @param path the bracket path + */ + public void setPath(Path2D path) { + this.path = path; + } + + /** + * Use path (bracket) or graphics context for drawing operations + * @return {@code true}, if the drawing should go to the path bracket, + * if {@code false} draw directly to the graphics context + */ + public boolean getUsePathBracket() { + return usePathBracket; + } + + public void setUsePathBracket(boolean usePathBracket) { + this.usePathBracket = usePathBracket; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java new file mode 100644 index 0000000000..c7e99965f5 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java @@ -0,0 +1,259 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.draw; + +import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_NULL; +import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.function.Consumer; + +import org.apache.poi.hemf.record.emf.HemfRecord; +import org.apache.poi.hwmf.draw.HwmfDrawProperties; +import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.hwmf.record.HwmfColorRef; +import org.apache.poi.hwmf.record.HwmfObjectTableEntry; +import org.apache.poi.hwmf.record.HwmfPenStyle; +import org.apache.poi.util.Internal; + +public class HemfGraphics extends HwmfGraphics { + + private static final HwmfColorRef WHITE = new HwmfColorRef(Color.WHITE); + private static final HwmfColorRef LTGRAY = new HwmfColorRef(new Color(0x00C0C0C0)); + private static final HwmfColorRef GRAY = new HwmfColorRef(new Color(0x00808080)); + private static final HwmfColorRef DKGRAY = new HwmfColorRef(new Color(0x00404040)); + private static final HwmfColorRef BLACK = new HwmfColorRef(Color.BLACK); + + + public HemfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) { + super(graphicsCtx,bbox); + // add dummy entry for object ind ex 0, as emf is 1-based + objectIndexes.set(0); + } + + @Override + public HemfDrawProperties getProperties() { + return (HemfDrawProperties)super.getProperties(); + } + + @Override + protected HemfDrawProperties newProperties(HwmfDrawProperties oldProps) { + return (oldProps == null) + ? new HemfDrawProperties() + : new HemfDrawProperties((HemfDrawProperties)oldProps); + } + + public void draw(HemfRecord r) { + r.draw(this); + } + + @Internal + public void draw(Consumer pathConsumer, FillDrawStyle fillDraw) { + final HemfDrawProperties prop = getProperties(); + final boolean useBracket = prop.getUsePathBracket(); + + final Path2D path; + if (useBracket) { + path = prop.getPath(); + } else { + path = new Path2D.Double(); + path.setWindingRule(prop.getWindingRule()); + } + + // add dummy move-to at start, to handle invalid emfs not containing that move-to + if (path.getCurrentPoint() == null) { + Point2D pnt = prop.getLocation(); + path.moveTo(pnt.getX(), pnt.getY()); + } + + try { + pathConsumer.accept(path); + } catch (Exception e) { + // workaround if a path has been started and no MoveTo command + // has been specified before the first lineTo/splineTo + final Point2D loc = prop.getLocation(); + path.moveTo(loc.getX(), loc.getY()); + pathConsumer.accept(path); + } + + Point2D curPnt = path.getCurrentPoint(); + if (curPnt == null) { + return; + } + + prop.setLocation(curPnt); + if (!useBracket) { + switch (fillDraw) { + case FILL: + super.fill(path); + break; + case DRAW: + super.draw(path); + break; + case FILL_DRAW: + super.fill(path); + super.draw(path); + break; + } + } + + } + + /** + * Adds or sets an record of type {@link HwmfObjectTableEntry} to the object table. + * If the {@code index} is less than 1, the method acts the same as + * {@link HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry)}, otherwise the + * index is used to access the object table. + * As the table is filled successively, the index must be between 1 and size+1 + * + * @param entry the record to be stored + * @param index the index to be overwritten, regardless if its content was unset before + * + * @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry) + */ + public void addObjectTableEntry(HwmfObjectTableEntry entry, int index) { + if (index < 1) { + throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index); + } + + objectIndexes.set(index); + objectTable.put(index, entry); + } + + @Override + public void applyObjectTableEntry(int index) { + if ((index & 0x80000000) != 0) { + selectStockObject(index); + } else { + super.applyObjectTableEntry(index); + } + } + + private void selectStockObject(int objectIndex) { + final HemfDrawProperties prop = getProperties(); + switch (objectIndex) { + case 0x80000000: + // WHITE_BRUSH - A white, solid-color brush + // BrushStyle: BS_SOLID + // Color: 0x00FFFFFF + prop.setBrushColor(WHITE); + prop.setBrushStyle(BS_SOLID); + break; + case 0x80000001: + // LTGRAY_BRUSH - A light gray, solid-color brush + // BrushStyle: BS_SOLID + // Color: 0x00C0C0C0 + prop.setBrushColor(LTGRAY); + prop.setBrushStyle(BS_SOLID); + break; + case 0x80000002: + // GRAY_BRUSH - A gray, solid-color brush + // BrushStyle: BS_SOLID + // Color: 0x00808080 + prop.setBrushColor(GRAY); + prop.setBrushStyle(BS_SOLID); + break; + case 0x80000003: + // DKGRAY_BRUSH - A dark gray, solid color brush + // BrushStyle: BS_SOLID + // Color: 0x00404040 + prop.setBrushColor(DKGRAY); + prop.setBrushStyle(BS_SOLID); + break; + case 0x80000004: + // BLACK_BRUSH - A black, solid color brush + // BrushStyle: BS_SOLID + // Color: 0x00000000 + prop.setBrushColor(BLACK); + prop.setBrushStyle(BS_SOLID); + break; + case 0x80000005: + // NULL_BRUSH - A null brush + // BrushStyle: BS_NULL + prop.setBrushStyle(BS_NULL); + break; + case 0x80000006: + // WHITE_PEN - A white, solid-color pen + // PenStyle: PS_COSMETIC + PS_SOLID + // ColorRef: 0x00FFFFFF + prop.setPenStyle(HwmfPenStyle.valueOf(0)); + prop.setPenWidth(1); + prop.setPenColor(WHITE); + break; + case 0x80000007: + // BLACK_PEN - A black, solid-color pen + // PenStyle: PS_COSMETIC + PS_SOLID + // ColorRef: 0x00000000 + prop.setPenStyle(HwmfPenStyle.valueOf(0)); + prop.setPenWidth(1); + prop.setPenColor(BLACK); + break; + case 0x80000008: + // NULL_PEN - A null pen + // PenStyle: PS_NULL + prop.setPenStyle(HwmfPenStyle.valueOf(HwmfPenStyle.HwmfLineDash.NULL.wmfFlag)); + break; + case 0x8000000A: + // OEM_FIXED_FONT - A fixed-width, OEM character set + // Charset: OEM_CHARSET + // PitchAndFamily: FF_DONTCARE + FIXED_PITCH + break; + case 0x8000000B: + // ANSI_FIXED_FONT - A fixed-width font + // Charset: ANSI_CHARSET + // PitchAndFamily: FF_DONTCARE + FIXED_PITCH + break; + case 0x8000000C: + // ANSI_VAR_FONT - A variable-width font + // Charset: ANSI_CHARSET + // PitchAndFamily: FF_DONTCARE + VARIABLE_PITCH + break; + case 0x8000000D: + // SYSTEM_FONT - A font that is guaranteed to be available in the operating system + break; + case 0x8000000E: + // DEVICE_DEFAULT_FONT + // The default font that is provided by the graphics device driver for the current output device + break; + case 0x8000000F: + // DEFAULT_PALETTE + // The default palette that is defined for the current output device. + break; + case 0x80000010: + // SYSTEM_FIXED_FONT + // A fixed-width font that is guaranteed to be available in the operating system. + break; + case 0x80000011: + // DEFAULT_GUI_FONT + // The default font that is used for user interface objects such as menus and dialog boxes. + break; + case 0x80000012: + // DC_BRUSH + // The solid-color brush that is currently selected in the playback device context. + break; + case 0x80000013: + // DC_PEN + // The solid-color pen that is currently selected in the playback device context. + break; + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfImageRenderer.java b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfImageRenderer.java new file mode 100644 index 0000000000..712c2e6c0e --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfImageRenderer.java @@ -0,0 +1,126 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.draw; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.RenderingHints; +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.RescaleOp; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.poi.hemf.usermodel.HemfPicture; +import org.apache.poi.sl.draw.ImageRenderer; +import org.apache.poi.sl.usermodel.PictureData; +import org.apache.poi.util.Units; + +public class HemfImageRenderer implements ImageRenderer { + HemfPicture image; + double alpha; + + @Override + public boolean canRender(String contentType) { + return PictureData.PictureType.EMF.contentType.equalsIgnoreCase(contentType); + } + + @Override + public void loadImage(InputStream data, String contentType) throws IOException { + if (!PictureData.PictureType.EMF.contentType.equals(contentType)) { + throw new IOException("Invalid picture type"); + } + image = new HemfPicture(data); + } + + @Override + public void loadImage(byte[] data, String contentType) throws IOException { + if (!PictureData.PictureType.EMF.contentType.equals(contentType)) { + throw new IOException("Invalid picture type"); + } + image = new HemfPicture(new ByteArrayInputStream(data)); + } + + @Override + public Dimension getDimension() { + int width = 0, height = 0; + if (image != null) { + Dimension2D dim = image.getSize(); + width = Units.pointsToPixel(dim.getWidth()); + // keep aspect ratio for height + height = Units.pointsToPixel(dim.getHeight()); + } + return new Dimension(width, height); + } + + @Override + public void setAlpha(double alpha) { + this.alpha = alpha; + } + + @Override + public BufferedImage getImage() { + return getImage(getDimension()); + } + + @Override + public BufferedImage getImage(Dimension dim) { + if (image == null) { + return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + } + + BufferedImage bufImg = new BufferedImage((int)dim.getWidth(), (int)dim.getHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics2D g = bufImg.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + image.draw(g, new Rectangle2D.Double(0,0,dim.getWidth(),dim.getHeight())); + g.dispose(); + + if (alpha != 0) { + BufferedImage newImg = new BufferedImage((int)dim.getWidth(), (int)dim.getHeight(), BufferedImage.TYPE_INT_ARGB); + g = newImg.createGraphics(); + RescaleOp op = new RescaleOp(new float[]{1.0f, 1.0f, 1.0f, (float)alpha}, new float[]{0,0,0,0}, null); + g.drawImage(bufImg, op, 0, 0); + g.dispose(); + bufImg = newImg; + } + + return bufImg; + } + + @Override + public boolean drawImage(Graphics2D graphics, Rectangle2D anchor) { + return drawImage(graphics, anchor, null); + } + + @Override + public boolean drawImage(Graphics2D graphics, Rectangle2D anchor, Insets clip) { + if (image == null) { + return false; + } else { + image.draw(graphics, anchor); + return true; + } + } + +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/extractor/HemfExtractor.java b/src/scratchpad/src/org/apache/poi/hemf/extractor/HemfExtractor.java deleted file mode 100644 index ab4add4eba..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/extractor/HemfExtractor.java +++ /dev/null @@ -1,115 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.extractor; - - -import java.io.IOException; -import java.io.InputStream; -import java.util.Iterator; - -import org.apache.poi.hemf.record.HemfHeader; -import org.apache.poi.hemf.record.HemfRecord; -import org.apache.poi.hemf.record.HemfRecordType; -import org.apache.poi.util.Internal; -import org.apache.poi.util.LittleEndianInputStream; -import org.apache.poi.util.RecordFormatException; - -/** - * Read-only EMF extractor. Lots remain - */ -@Internal -public class HemfExtractor implements Iterable { - - private HemfHeader header; - private final LittleEndianInputStream stream; - - public HemfExtractor(InputStream is) throws IOException { - stream = new LittleEndianInputStream(is); - header = new HemfHeader(); - long recordId = stream.readUInt(); - long recordSize = stream.readUInt(); - - header = new HemfHeader(); - header.init(stream, recordId, recordSize-8); - } - - @Override - public Iterator iterator() { - return new HemfRecordIterator(); - } - - public HemfHeader getHeader() { - return header; - } - - private class HemfRecordIterator implements Iterator { - - private HemfRecord currentRecord; - - HemfRecordIterator() { - //queue the first non-header record - currentRecord = _next(); - } - - @Override - public boolean hasNext() { - return currentRecord != null; - } - - @Override - public HemfRecord next() { - HemfRecord toReturn = currentRecord; - currentRecord = _next(); - return toReturn; - } - - private HemfRecord _next() { - if (currentRecord != null && currentRecord.getRecordType().equals(HemfRecordType.eof)) { - return null; - } - long recordId = stream.readUInt(); - long recordSize = stream.readUInt(); - - HemfRecord record = null; - HemfRecordType type = HemfRecordType.getById(recordId); - if (type == null) { - throw new RuntimeException("Undefined record of type:"+recordId); - } - try { - record = type.clazz.newInstance(); - } catch (InstantiationException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - try { - record.init(stream, recordId, recordSize-8); - } catch (IOException e) { - throw new RecordFormatException(e); - } - - return record; - } - - @Override - public void remove() { - throw new UnsupportedOperationException("Remove not supported"); - } - - } -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecord.java b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecord.java deleted file mode 100644 index 09af3d5cf2..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecord.java +++ /dev/null @@ -1,44 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.hemfplus.record; - - -import java.io.IOException; - -import org.apache.poi.util.Internal; - -@Internal -public interface HemfPlusRecord { - - HemfPlusRecordType getRecordType(); - - int getFlags(); - - /** - * - * @param dataBytes these are the bytes that start after the id, flags, record size - * and go to the end of the record; they do not include any required padding - * at the end. - * @param recordId record type id - * @param flags flags - * @throws IOException, RecordFormatException - */ - void init(byte[] dataBytes, int recordId, int flags) throws IOException; - - -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecordType.java deleted file mode 100644 index 70837628e1..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusRecordType.java +++ /dev/null @@ -1,97 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.hemfplus.record; - -import org.apache.poi.util.Internal; - -@Internal -public enum HemfPlusRecordType { - header(0x4001, HemfPlusHeader.class), - endOfFile(0x4002, UnimplementedHemfPlusRecord.class), - comment(0x4003, UnimplementedHemfPlusRecord.class), - getDC(0x4004, UnimplementedHemfPlusRecord.class), - multiFormatStart(0x4005, UnimplementedHemfPlusRecord.class), - multiFormatSection(0x4006, UnimplementedHemfPlusRecord.class), - multiFormatEnd(0x4007, UnimplementedHemfPlusRecord.class), - object(0x4008, UnimplementedHemfPlusRecord.class), - clear(0x4009, UnimplementedHemfPlusRecord.class), - fillRects(0x400A, UnimplementedHemfPlusRecord.class), - drawRects(0x400B, UnimplementedHemfPlusRecord.class), - fillPolygon(0x400C, UnimplementedHemfPlusRecord.class), - drawLines(0x400D, UnimplementedHemfPlusRecord.class), - fillEllipse(0x400E, UnimplementedHemfPlusRecord.class), - drawEllipse(0x400F, UnimplementedHemfPlusRecord.class), - fillPie(0x4010, UnimplementedHemfPlusRecord.class), - drawPie(0x4011, UnimplementedHemfPlusRecord.class), - drawArc(0x4012, UnimplementedHemfPlusRecord.class), - fillRegion(0x4013, UnimplementedHemfPlusRecord.class), - fillPath(0x4014, UnimplementedHemfPlusRecord.class), - drawPath(0x4015, UnimplementedHemfPlusRecord.class), - fillClosedCurve(0x4016, UnimplementedHemfPlusRecord.class), - drawClosedCurve(0x4017, UnimplementedHemfPlusRecord.class), - drawCurve(0x4018, UnimplementedHemfPlusRecord.class), - drawBeziers(0x4019, UnimplementedHemfPlusRecord.class), - drawImage(0x401A, UnimplementedHemfPlusRecord.class), - drawImagePoints(0x401B, UnimplementedHemfPlusRecord.class), - drawString(0x401C, UnimplementedHemfPlusRecord.class), - setRenderingOrigin(0x401D, UnimplementedHemfPlusRecord.class), - setAntiAliasMode(0x401E, UnimplementedHemfPlusRecord.class), - setTextRenderingHint(0x401F, UnimplementedHemfPlusRecord.class), - setTextContrast(0x4020, UnimplementedHemfPlusRecord.class), - setInterpolationMode(0x4021, UnimplementedHemfPlusRecord.class), - setPixelOffsetMode(0x4022, UnimplementedHemfPlusRecord.class), - setComositingMode(0x4023, UnimplementedHemfPlusRecord.class), - setCompositingQuality(0x4024, UnimplementedHemfPlusRecord.class), - save(0x4025, UnimplementedHemfPlusRecord.class), - restore(0x4026, UnimplementedHemfPlusRecord.class), - beginContainer(0x4027, UnimplementedHemfPlusRecord.class), - beginContainerNoParams(0x428, UnimplementedHemfPlusRecord.class), - endContainer(0x4029, UnimplementedHemfPlusRecord.class), - setWorldTransform(0x402A, UnimplementedHemfPlusRecord.class), - resetWorldTransform(0x402B, UnimplementedHemfPlusRecord.class), - multiplyWorldTransform(0x402C, UnimplementedHemfPlusRecord.class), - translateWorldTransform(0x402D, UnimplementedHemfPlusRecord.class), - scaleWorldTransform(0x402E, UnimplementedHemfPlusRecord.class), - rotateWorldTransform(0x402F, UnimplementedHemfPlusRecord.class), - setPageTransform(0x4030, UnimplementedHemfPlusRecord.class), - resetClip(0x4031, UnimplementedHemfPlusRecord.class), - setClipRect(0x4032, UnimplementedHemfPlusRecord.class), - setClipRegion(0x4033, UnimplementedHemfPlusRecord.class), - setClipPath(0x4034, UnimplementedHemfPlusRecord.class), - offsetClip(0x4035, UnimplementedHemfPlusRecord.class), - drawDriverstring(0x4036, UnimplementedHemfPlusRecord.class), - strokeFillPath(0x4037, UnimplementedHemfPlusRecord.class), - serializableObject(0x4038, UnimplementedHemfPlusRecord.class), - setTSGraphics(0x4039, UnimplementedHemfPlusRecord.class), - setTSClip(0x403A, UnimplementedHemfPlusRecord.class); - - public final long id; - public final Class clazz; - - HemfPlusRecordType(long id, Class clazz) { - this.id = id; - this.clazz = clazz; - } - - public static HemfPlusRecordType getById(long id) { - for (HemfPlusRecordType wrt : values()) { - if (wrt.id == id) return wrt; - } - return null; - } -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfComment.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfComment.java deleted file mode 100644 index 5d45927bb9..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/record/HemfComment.java +++ /dev/null @@ -1,31 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.record; - -import org.apache.poi.util.Internal; - -/** - * Contains arbitrary data - */ -@Internal -public class HemfComment extends AbstractHemfComment { - - public HemfComment(byte[] rawBytes) { - super(rawBytes); - } -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFPlus.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFPlus.java deleted file mode 100644 index d5d4c10e84..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFPlus.java +++ /dev/null @@ -1,111 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.record; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.apache.poi.hemf.hemfplus.record.HemfPlusRecord; -import org.apache.poi.hemf.hemfplus.record.HemfPlusRecordType; -import org.apache.poi.util.IOUtils; -import org.apache.poi.util.Internal; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.RecordFormatException; - -/** - * An HemfCommentEMFPlus may contain one or more EMFPlus records - */ -@Internal -public class HemfCommentEMFPlus extends AbstractHemfComment { - - private static final int MAX_RECORD_LENGTH = 1_000_000; - - - long dataSize; - public HemfCommentEMFPlus(byte[] rawBytes) { - //these rawBytes contain only the EMFPlusRecord(s?) - //the EmfComment type, size, datasize and comment identifier have all been stripped. - //The EmfPlus type, flags, size, data size should start at rawBytes[0] - super(rawBytes); - - } - - public List getRecords() { - return HemfPlusParser.parse(getRawBytes()); - } - - private static class HemfPlusParser { - - public static List parse(byte[] bytes) { - List records = new ArrayList<>(); - int offset = 0; - while (offset < bytes.length) { - if (offset + 12 > bytes.length) { - //if header will go beyond bytes, stop now - //TODO: log or throw - break; - } - int type = LittleEndian.getUShort(bytes, offset); offset += LittleEndian.SHORT_SIZE; - int flags = LittleEndian.getUShort(bytes, offset); offset += LittleEndian.SHORT_SIZE; - long sizeLong = LittleEndian.getUInt(bytes, offset); offset += LittleEndian.INT_SIZE; - if (sizeLong >= Integer.MAX_VALUE) { - throw new RecordFormatException("size of emf record >= Integer.MAX_VALUE"); - } - int size = (int)sizeLong; - long dataSizeLong = LittleEndian.getUInt(bytes, offset); offset += LittleEndian.INT_SIZE; - if (dataSizeLong >= Integer.MAX_VALUE) { - throw new RuntimeException("data size of emfplus record cannot be >= Integer.MAX_VALUE"); - } - int dataSize = (int)dataSizeLong; - if (dataSize + offset > bytes.length) { - //TODO: log or throw? - break; - } - HemfPlusRecord record = buildRecord(type, flags, dataSize, offset, bytes); - records.add(record); - offset += dataSize; - } - return records; - } - - private static HemfPlusRecord buildRecord(int recordId, int flags, int size, int offset, byte[] bytes) { - HemfPlusRecord record = null; - HemfPlusRecordType type = HemfPlusRecordType.getById(recordId); - if (type == null) { - throw new RuntimeException("Undefined record of type:"+recordId); - } - try { - record = type.clazz.newInstance(); - } catch (InstantiationException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - byte[] dataBytes = IOUtils.safelyAllocate(size, MAX_RECORD_LENGTH); - System.arraycopy(bytes, offset, dataBytes, 0, size); - try { - record.init(dataBytes, recordId, flags); - } catch (IOException e) { - throw new RuntimeException(e); - } - return record; - - } - } -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFSpool.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFSpool.java deleted file mode 100644 index 009974d10b..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentEMFSpool.java +++ /dev/null @@ -1,31 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.record; - -import org.apache.poi.util.Internal; - -/** - * Not yet implemented - */ -@Internal -public class HemfCommentEMFSpool extends AbstractHemfComment { - - public HemfCommentEMFSpool(byte[] rawBytes) { - super(rawBytes); - } -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentPublic.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentPublic.java deleted file mode 100644 index 5ca5c375ce..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentPublic.java +++ /dev/null @@ -1,177 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.record; - - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -import org.apache.poi.util.IOUtils; -import org.apache.poi.util.Internal; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianConsts; -import org.apache.poi.util.RecordFormatException; - -/** - * Container class for four subtypes of HemfCommentPublic: BeginGroup, EndGroup, MultiFormats - * and Windows Metafile. - */ -@Internal -public class HemfCommentPublic { - - private static final int MAX_RECORD_LENGTH = 1_000_000; - - - /** - * Stub, to be implemented - */ - public static class BeginGroup extends AbstractHemfComment { - - public BeginGroup(byte[] rawBytes) { - super(rawBytes); - } - - } - - /** - * Stub, to be implemented - */ - public static class EndGroup extends AbstractHemfComment { - - public EndGroup(byte[] rawBytes) { - super(rawBytes); - } - } - - public static class MultiFormats extends AbstractHemfComment { - - public MultiFormats(byte[] rawBytes) { - super(rawBytes); - } - - /** - * - * @return a list of HemfMultFormatsData - */ - public List getData() { - - byte[] rawBytes = getRawBytes(); - //note that raw bytes includes the public comment identifier - int currentOffset = 4 + 16;//4 public comment identifier, 16 for outputrect - long countFormats = LittleEndian.getUInt(rawBytes, currentOffset); - currentOffset += LittleEndianConsts.INT_SIZE; - List emrFormatList = new ArrayList<>(); - for (long i = 0; i < countFormats; i++) { - emrFormatList.add(new EmrFormat(rawBytes, currentOffset)); - currentOffset += 4 * LittleEndianConsts.INT_SIZE; - } - List list = new ArrayList<>(); - for (EmrFormat emrFormat : emrFormatList) { - byte[] data = IOUtils.safelyAllocate(emrFormat.size, MAX_RECORD_LENGTH); - System.arraycopy(rawBytes, emrFormat.offset-4, data, 0, emrFormat.size); - list.add(new HemfMultiFormatsData(emrFormat.signature, emrFormat.version, data)); - } - return list; - } - - private static class EmrFormat { - long signature; - long version; - int size; - int offset; - - public EmrFormat(byte[] rawBytes, int currentOffset) { - signature = LittleEndian.getUInt(rawBytes, currentOffset); currentOffset += LittleEndianConsts.INT_SIZE; - version = LittleEndian.getUInt(rawBytes, currentOffset); currentOffset += LittleEndianConsts.INT_SIZE; - //spec says this must be a 32bit "aligned" typo for "signed"? - //realistically, this has to be an int... - size = LittleEndian.getInt(rawBytes, currentOffset); currentOffset += LittleEndianConsts.INT_SIZE; - //y, can be long, but realistically? - offset = LittleEndian.getInt(rawBytes, currentOffset); currentOffset += LittleEndianConsts.INT_SIZE; - if (size < 0) { - throw new RecordFormatException("size for emrformat must be > 0"); - } - if (offset < 0) { - throw new RecordFormatException("offset for emrformat must be > 0"); - } - } - } - } - - /** - * Stub, to be implemented - */ - public static class WindowsMetafile extends AbstractHemfComment { - - private final byte[] wmfBytes; - public WindowsMetafile(byte[] rawBytes) { - super(rawBytes); - int offset = LittleEndianConsts.INT_SIZE;//public comment identifier - int version = LittleEndian.getUShort(rawBytes, offset); offset += LittleEndianConsts.SHORT_SIZE; - int reserved = LittleEndian.getUShort(rawBytes, offset); offset += LittleEndianConsts.SHORT_SIZE; - offset += LittleEndianConsts.INT_SIZE; //checksum - offset += LittleEndianConsts.INT_SIZE; //flags - long winMetafileSizeLong = LittleEndian.getUInt(rawBytes, offset); offset += LittleEndianConsts.INT_SIZE; - if (winMetafileSizeLong == 0L) { - wmfBytes = new byte[0]; - return; - } - wmfBytes = IOUtils.safelyAllocate(winMetafileSizeLong, MAX_RECORD_LENGTH); - System.arraycopy(rawBytes, offset, wmfBytes, 0, wmfBytes.length); - } - - /** - * - * @return an InputStream for the embedded WMF file - */ - public InputStream getWmfInputStream() { - return new ByteArrayInputStream(wmfBytes); - } - } - - /** - * This encapulates a single record stored within - * a HemfCommentPublic.MultiFormats record. - */ - public static class HemfMultiFormatsData { - - long signature; - long version; - byte[] data; - - public HemfMultiFormatsData(long signature, long version, byte[] data) { - this.signature = signature; - this.version = version; - this.data = data; - } - - public long getSignature() { - return signature; - } - - public long getVersion() { - return version; - } - - public byte[] getData() { - return data; - } - } -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentRecord.java deleted file mode 100644 index af8aa28572..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/record/HemfCommentRecord.java +++ /dev/null @@ -1,154 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.record; - - -import java.io.IOException; - -import org.apache.poi.util.IOUtils; -import org.apache.poi.util.Internal; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianInputStream; -import org.apache.poi.util.RecordFormatException; - -/** - * This is the outer comment record that is recognized - * by the initial parse by {@link HemfRecordType#comment}. - * However, there are four types of comment: EMR_COMMENT, - * EMR_COMMENT_EMFPLUS, EMR_COMMENT_EMFSPOOL, and EMF_COMMENT_PUBLIC. - * To get the underlying comment, call {@link #getComment()}. - * - */ -@Internal -public class HemfCommentRecord implements HemfRecord { - private static final int MAX_RECORD_LENGTH = 1_000_000; - - public final static long COMMENT_EMFSPOOL = 0x00000000; - public final static long COMMENT_EMFPLUS = 0x2B464D45; - public final static long COMMENT_PUBLIC = 0x43494447; - - - private AbstractHemfComment comment; - @Override - public HemfRecordType getRecordType() { - return HemfRecordType.comment; - } - - @Override - public long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException { - long dataSize = leis.readUInt(); recordSize -= LittleEndian.INT_SIZE; - - byte[] optionalCommentIndentifierBuffer = new byte[4]; - leis.readFully(optionalCommentIndentifierBuffer); - dataSize = dataSize-LittleEndian.INT_SIZE; //size minus the first int which could be a comment identifier - recordSize -= LittleEndian.INT_SIZE; - long optionalCommentIdentifier = LittleEndian.getInt(optionalCommentIndentifierBuffer) & 0x00FFFFFFFFL; - if (optionalCommentIdentifier == COMMENT_EMFSPOOL) { - comment = new HemfCommentEMFSpool(readToByteArray(leis, dataSize, recordSize)); - } else if (optionalCommentIdentifier == COMMENT_EMFPLUS) { - comment = new HemfCommentEMFPlus(readToByteArray(leis, dataSize, recordSize)); - } else if (optionalCommentIdentifier == COMMENT_PUBLIC) { - comment = CommentPublicParser.parse(readToByteArray(leis, dataSize, recordSize)); - } else { - comment = new HemfComment(readToByteArray(optionalCommentIndentifierBuffer, leis, dataSize, recordSize)); - } - - return recordSize; - } - - //this prepends the initial "int" which turned out NOT to be - //a signifier of emfplus, spool, public. - private byte[] readToByteArray(byte[] initialBytes, LittleEndianInputStream leis, - long remainingDataSize, long remainingRecordSize) throws IOException { - if (remainingDataSize > Integer.MAX_VALUE) { - throw new RecordFormatException("Data size can't be > Integer.MAX_VALUE"); - } - - if (remainingRecordSize > Integer.MAX_VALUE) { - throw new RecordFormatException("Record size can't be > Integer.MAX_VALUE"); - } - if (remainingRecordSize == 0) { - return new byte[0]; - } - - int dataSize = (int)remainingDataSize; - int recordSize = (int)remainingRecordSize; - byte[] arr = IOUtils.safelyAllocate(dataSize+initialBytes.length, MAX_RECORD_LENGTH); - System.arraycopy(initialBytes,0,arr, 0, initialBytes.length); - long read = IOUtils.readFully(leis, arr, initialBytes.length, dataSize); - if (read != dataSize) { - throw new RecordFormatException("InputStream ended before full record could be read"); - } - long toSkip = recordSize-dataSize; - long skipped = IOUtils.skipFully(leis, toSkip); - if (toSkip != skipped) { - throw new RecordFormatException("InputStream ended before full record could be read"); - } - - return arr; - } - - private byte[] readToByteArray(LittleEndianInputStream leis, long dataSize, long recordSize) throws IOException { - - if (recordSize == 0) { - return new byte[0]; - } - - byte[] arr = IOUtils.safelyAllocate(dataSize, MAX_RECORD_LENGTH); - - long read = IOUtils.readFully(leis, arr); - if (read != dataSize) { - throw new RecordFormatException("InputStream ended before full record could be read"); - } - long toSkip = recordSize-dataSize; - long skipped = IOUtils.skipFully(leis, recordSize-dataSize); - if (toSkip != skipped) { - throw new RecordFormatException("InputStream ended before full record could be read"); - } - return arr; - } - - public AbstractHemfComment getComment() { - return comment; - } - - private static class CommentPublicParser { - private static final long WINDOWS_METAFILE = 0x80000001L; //wmf - private static final long BEGINGROUP = 0x00000002; //beginning of a group of drawing records - private static final long ENDGROUP = 0x00000003; //end of a group of drawing records - private static final long MULTIFORMATS = 0x40000004; //allows multiple definitions of an image, including encapsulated postscript - private static final long UNICODE_STRING = 0x00000040; //reserved. must not be used - private static final long UNICODE_END = 0x00000080; //reserved, must not be used - - private static AbstractHemfComment parse(byte[] bytes) { - long publicCommentIdentifier = LittleEndian.getUInt(bytes, 0); - if (publicCommentIdentifier == WINDOWS_METAFILE) { - return new HemfCommentPublic.WindowsMetafile(bytes); - } else if (publicCommentIdentifier == BEGINGROUP) { - return new HemfCommentPublic.BeginGroup(bytes); - } else if (publicCommentIdentifier == ENDGROUP) { - return new HemfCommentPublic.EndGroup(bytes); - } else if (publicCommentIdentifier == MULTIFORMATS) { - return new HemfCommentPublic.MultiFormats(bytes); - } else if (publicCommentIdentifier == UNICODE_STRING || publicCommentIdentifier == UNICODE_END) { - throw new RuntimeException("UNICODE_STRING/UNICODE_END values are reserved in CommentPublic records"); - } - throw new RuntimeException("Unrecognized public comment type:" +publicCommentIdentifier + " ; " + WINDOWS_METAFILE); - } - } -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfHeader.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfHeader.java deleted file mode 100644 index 7a6f876400..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/record/HemfHeader.java +++ /dev/null @@ -1,201 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.record; - -import java.awt.Rectangle; -import java.io.IOException; - -import org.apache.poi.util.IOUtils; -import org.apache.poi.util.Internal; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianInputStream; - -/** - * Extracts the full header from EMF files. - * @see org.apache.poi.sl.image.ImageHeaderEMF - */ -@Internal -public class HemfHeader implements HemfRecord { - - private static final int MAX_RECORD_LENGTH = 1_000_000; - - - private Rectangle boundsRectangle; - private Rectangle frameRectangle; - private long bytes; - private long records; - private int handles; - private long nDescription; - private long offDescription; - private long nPalEntries; - private boolean hasExtension1; - private long cbPixelFormat; - private long offPixelFormat; - private long bOpenGL; - private boolean hasExtension2; - private long micrometersX; - private long micrometersY; - - public Rectangle getBoundsRectangle() { - return boundsRectangle; - } - - public Rectangle getFrameRectangle() { - return frameRectangle; - } - - public long getBytes() { - return bytes; - } - - public long getRecords() { - return records; - } - - public int getHandles() { - return handles; - } - - public long getnDescription() { - return nDescription; - } - - public long getOffDescription() { - return offDescription; - } - - public long getnPalEntries() { - return nPalEntries; - } - - public boolean isHasExtension1() { - return hasExtension1; - } - - public long getCbPixelFormat() { - return cbPixelFormat; - } - - public long getOffPixelFormat() { - return offPixelFormat; - } - - public long getbOpenGL() { - return bOpenGL; - } - - public boolean isHasExtension2() { - return hasExtension2; - } - - public long getMicrometersX() { - return micrometersX; - } - - public long getMicrometersY() { - return micrometersY; - } - - @Override - public String toString() { - return "HemfHeader{" + - "boundsRectangle=" + boundsRectangle + - ", frameRectangle=" + frameRectangle + - ", bytes=" + bytes + - ", records=" + records + - ", handles=" + handles + - ", nDescription=" + nDescription + - ", offDescription=" + offDescription + - ", nPalEntries=" + nPalEntries + - ", hasExtension1=" + hasExtension1 + - ", cbPixelFormat=" + cbPixelFormat + - ", offPixelFormat=" + offPixelFormat + - ", bOpenGL=" + bOpenGL + - ", hasExtension2=" + hasExtension2 + - ", micrometersX=" + micrometersX + - ", micrometersY=" + micrometersY + - '}'; - } - - @Override - public HemfRecordType getRecordType() { - return HemfRecordType.header; - } - - @Override - public long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException { - if (recordId != 1L) { - throw new IOException("Not a valid EMF header. Record type:"+recordId); - } - //read the record--id and size (2 bytes) have already been read - byte[] data = IOUtils.safelyAllocate(recordSize, MAX_RECORD_LENGTH); - IOUtils.readFully(leis, data); - - int offset = 0; - - //bounds - int boundsLeft = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - int boundsTop = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - int boundsRight = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - int boundsBottom = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - boundsRectangle = new Rectangle(boundsLeft, boundsTop, - boundsRight - boundsLeft, boundsBottom - boundsTop); - - int frameLeft = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - int frameTop = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - int frameRight = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - int frameBottom = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - frameRectangle = new Rectangle(frameLeft, frameTop, - frameRight - frameLeft, frameBottom - frameTop); - - long recordSignature = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - if (recordSignature != 0x464D4520) { - throw new IOException("bad record signature: " + recordSignature); - } - - long version = LittleEndian.getInt(data, offset); offset += LittleEndian.INT_SIZE; - //According to the spec, MSOffice doesn't pay attention to this value. - //It _should_ be 0x00010000 - bytes = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - records = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - handles = LittleEndian.getUShort(data, offset);offset += LittleEndian.SHORT_SIZE; - offset += LittleEndian.SHORT_SIZE;//reserved - nDescription = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - offDescription = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - nPalEntries = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - - //should be skips - offset += 8;//device - offset += 8;//millimeters - - - if (recordSize+8 >= 100) { - hasExtension1 = true; - cbPixelFormat = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - offPixelFormat = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - bOpenGL= LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - } - - if (recordSize+8 >= 108) { - hasExtension2 = true; - micrometersX = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - micrometersY = LittleEndian.getUInt(data, offset); offset += LittleEndian.INT_SIZE; - } - return recordSize; - } -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecordType.java deleted file mode 100644 index b1c5857114..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecordType.java +++ /dev/null @@ -1,159 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.record; - -import org.apache.poi.util.Internal; - -@Internal -public enum HemfRecordType { - - header(0x00000001, UnimplementedHemfRecord.class), - polybeizer(0x00000002, UnimplementedHemfRecord.class), - polygon(0x00000003, UnimplementedHemfRecord.class), - polyline(0x00000004, UnimplementedHemfRecord.class), - polybezierto(0x00000005, UnimplementedHemfRecord.class), - polylineto(0x00000006, UnimplementedHemfRecord.class), - polypolyline(0x00000007, UnimplementedHemfRecord.class), - polypolygon(0x00000008, UnimplementedHemfRecord.class), - setwindowextex(0x00000009, UnimplementedHemfRecord.class), - setwindoworgex(0x0000000A, UnimplementedHemfRecord.class), - setviewportextex(0x0000000B, UnimplementedHemfRecord.class), - setviewportorgex(0x0000000C, UnimplementedHemfRecord.class), - setbrushorgex(0x0000000D, UnimplementedHemfRecord.class), - eof(0x0000000E, UnimplementedHemfRecord.class), - setpixelv(0x0000000F, UnimplementedHemfRecord.class), - setmapperflags(0x00000010, UnimplementedHemfRecord.class), - setmapmode(0x00000011, UnimplementedHemfRecord.class), - setbkmode(0x00000012, UnimplementedHemfRecord.class), - setpolyfillmode(0x00000013, UnimplementedHemfRecord.class), - setrop2(0x00000014, UnimplementedHemfRecord.class), - setstretchbltmode(0x00000015, UnimplementedHemfRecord.class), - settextalign(0x00000016, HemfText.SetTextAlign.class), - setcoloradjustment(0x00000017, UnimplementedHemfRecord.class), - settextcolor(0x00000018, HemfText.SetTextColor.class), - setbkcolor(0x00000019, UnimplementedHemfRecord.class), - setoffsetcliprgn(0x0000001A, UnimplementedHemfRecord.class), - setmovetoex(0x0000001B, UnimplementedHemfRecord.class), - setmetargn(0x0000001C, UnimplementedHemfRecord.class), - setexcludecliprect(0x0000001D, UnimplementedHemfRecord.class), - setintersectcliprect(0x0000001E, UnimplementedHemfRecord.class), - scaleviewportextex(0x0000001F, UnimplementedHemfRecord.class), - scalewindowextex(0x00000020, UnimplementedHemfRecord.class), - savedc(0x00000021, UnimplementedHemfRecord.class), - restoredc(0x00000022, UnimplementedHemfRecord.class), - setworldtransform(0x00000023, UnimplementedHemfRecord.class), - modifyworldtransform(0x00000024, UnimplementedHemfRecord.class), - selectobject(0x00000025, UnimplementedHemfRecord.class), - createpen(0x00000026, UnimplementedHemfRecord.class), - createbrushindirect(0x00000027, UnimplementedHemfRecord.class), - deleteobject(0x00000028, UnimplementedHemfRecord.class), - anglearc(0x00000029, UnimplementedHemfRecord.class), - ellipse(0x0000002A, UnimplementedHemfRecord.class), - rectangle(0x0000002B, UnimplementedHemfRecord.class), - roundirect(0x0000002C, UnimplementedHemfRecord.class), - arc(0x0000002D, UnimplementedHemfRecord.class), - chord(0x0000002E, UnimplementedHemfRecord.class), - pie(0x0000002F, UnimplementedHemfRecord.class), - selectpalette(0x00000030, UnimplementedHemfRecord.class), - createpalette(0x00000031, UnimplementedHemfRecord.class), - setpaletteentries(0x00000032, UnimplementedHemfRecord.class), - resizepalette(0x00000033, UnimplementedHemfRecord.class), - realizepalette(0x0000034, UnimplementedHemfRecord.class), - extfloodfill(0x00000035, UnimplementedHemfRecord.class), - lineto(0x00000036, UnimplementedHemfRecord.class), - arcto(0x00000037, UnimplementedHemfRecord.class), - polydraw(0x00000038, UnimplementedHemfRecord.class), - setarcdirection(0x00000039, UnimplementedHemfRecord.class), - setmiterlimit(0x0000003A, UnimplementedHemfRecord.class), - beginpath(0x0000003B, UnimplementedHemfRecord.class), - endpath(0x0000003C, UnimplementedHemfRecord.class), - closefigure(0x0000003D, UnimplementedHemfRecord.class), - fillpath(0x0000003E, UnimplementedHemfRecord.class), - strokeandfillpath(0x0000003F, UnimplementedHemfRecord.class), - strokepath(0x00000040, UnimplementedHemfRecord.class), - flattenpath(0x00000041, UnimplementedHemfRecord.class), - widenpath(0x00000042, UnimplementedHemfRecord.class), - selectclippath(0x00000043, UnimplementedHemfRecord.class), - abortpath(0x00000044, UnimplementedHemfRecord.class), //no 45?! - comment(0x00000046, HemfCommentRecord.class), - fillrgn(0x00000047, UnimplementedHemfRecord.class), - framergn(0x00000048, UnimplementedHemfRecord.class), - invertrgn(0x00000049, UnimplementedHemfRecord.class), - paintrgn(0x0000004A, UnimplementedHemfRecord.class), - extselectciprrgn(0x0000004B, UnimplementedHemfRecord.class), - bitblt(0x0000004C, UnimplementedHemfRecord.class), - stretchblt(0x0000004D, UnimplementedHemfRecord.class), - maskblt(0x0000004E, UnimplementedHemfRecord.class), - plgblt(0x0000004F, UnimplementedHemfRecord.class), - setbitstodevice(0x00000050, UnimplementedHemfRecord.class), - stretchdibits(0x00000051, UnimplementedHemfRecord.class), - extcreatefontindirectw(0x00000052, HemfText.ExtCreateFontIndirectW.class), - exttextouta(0x00000053, HemfText.ExtTextOutA.class), - exttextoutw(0x00000054, HemfText.ExtTextOutW.class), - polybezier16(0x00000055, UnimplementedHemfRecord.class), - polygon16(0x00000056, UnimplementedHemfRecord.class), - polyline16(0x00000057, UnimplementedHemfRecord.class), - polybezierto16(0x00000058, UnimplementedHemfRecord.class), - polylineto16(0x00000059, UnimplementedHemfRecord.class), - polypolyline16(0x0000005A, UnimplementedHemfRecord.class), - polypolygon16(0x0000005B, UnimplementedHemfRecord.class), - polydraw16(0x0000005C, UnimplementedHemfRecord.class), - createmonobrush16(0x0000005D, UnimplementedHemfRecord.class), - createdibpatternbrushpt(0x0000005E, UnimplementedHemfRecord.class), - extcreatepen(0x0000005F, UnimplementedHemfRecord.class), - polytextouta(0x00000060, HemfText.PolyTextOutA.class), - polytextoutw(0x00000061, HemfText.PolyTextOutW.class), - seticmmode(0x00000062, UnimplementedHemfRecord.class), - createcolorspace(0x00000063, UnimplementedHemfRecord.class), - setcolorspace(0x00000064, UnimplementedHemfRecord.class), - deletecolorspace(0x00000065, UnimplementedHemfRecord.class), - glsrecord(0x00000066, UnimplementedHemfRecord.class), - glsboundedrecord(0x00000067, UnimplementedHemfRecord.class), - pixelformat(0x00000068, UnimplementedHemfRecord.class), - drawescape(0x00000069, UnimplementedHemfRecord.class), - extescape(0x0000006A, UnimplementedHemfRecord.class),//no 6b?! - smalltextout(0x0000006C, UnimplementedHemfRecord.class), - forceufimapping(0x0000006D, UnimplementedHemfRecord.class), - namedescape(0x0000006E, UnimplementedHemfRecord.class), - colorcorrectpalette(0x0000006F, UnimplementedHemfRecord.class), - seticmprofilea(0x00000070, UnimplementedHemfRecord.class), - seticmprofilew(0x00000071, UnimplementedHemfRecord.class), - alphablend(0x00000072, UnimplementedHemfRecord.class), - setlayout(0x00000073, UnimplementedHemfRecord.class), - transparentblt(0x00000074, UnimplementedHemfRecord.class), - gradientfill(0x00000076, UnimplementedHemfRecord.class), //no 75?! - setlinkdufis(0x00000077, UnimplementedHemfRecord.class), - settextjustification(0x00000078, HemfText.SetTextJustification.class), - colormatchtargetw(0x00000079, UnimplementedHemfRecord.class), - createcolorspacew(0x0000007A, UnimplementedHemfRecord.class); - - public final long id; - public final Class clazz; - - HemfRecordType(long id, Class clazz) { - this.id = id; - this.clazz = clazz; - } - - public static HemfRecordType getById(long id) { - for (HemfRecordType wrt : values()) { - if (wrt.id == id) return wrt; - } - return null; - } -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfText.java b/src/scratchpad/src/org/apache/poi/hemf/record/HemfText.java deleted file mode 100644 index 74b8212846..0000000000 --- a/src/scratchpad/src/org/apache/poi/hemf/record/HemfText.java +++ /dev/null @@ -1,262 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - -package org.apache.poi.hemf.record; - -import static java.nio.charset.StandardCharsets.UTF_16LE; - -import java.io.ByteArrayInputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.Charset; - -import org.apache.poi.util.IOUtils; -import org.apache.poi.util.Internal; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.LittleEndianInputStream; -import org.apache.poi.util.RecordFormatException; - -/** - * Container class to gather all text-related commands - * This is starting out as read only, and very little is actually - * implemented at this point! - */ -@Internal -public class HemfText { - - private static final int MAX_RECORD_LENGTH = 1_000_000; - - public static class ExtCreateFontIndirectW extends UnimplementedHemfRecord { - } - - public static class ExtTextOutA implements HemfRecord { - - private long left,top,right,bottom; - - //TODO: translate this to a graphicsmode enum - private long graphicsMode; - - private long exScale; - private long eyScale; - EmrTextObject textObject; - - @Override - public HemfRecordType getRecordType() { - return HemfRecordType.exttextouta; - } - - @Override - public long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException { - //note that the first 2 uInts have been read off and the recordsize has - //been decreased by 8 - left = leis.readInt(); - top = leis.readInt(); - right = leis.readInt(); - bottom = leis.readInt(); - graphicsMode = leis.readUInt(); - exScale = leis.readUInt(); - eyScale = leis.readUInt(); - - int recordSizeInt = -1; - if (recordSize < Integer.MAX_VALUE) { - recordSizeInt = (int)recordSize; - } else { - throw new RecordFormatException("can't have text length > Integer.MAX_VALUE"); - } - //guarantee to read the rest of the EMRTextObjectRecord - //emrtextbytes start after 7*4 bytes read above - byte[] emrTextBytes = IOUtils.safelyAllocate(recordSizeInt-(7*LittleEndian.INT_SIZE), MAX_RECORD_LENGTH); - IOUtils.readFully(leis, emrTextBytes); - textObject = new EmrTextObject(emrTextBytes, getEncodingHint(), 20);//should be 28, but recordSizeInt has already subtracted 8 - return recordSize; - } - - protected Charset getEncodingHint() { - return null; - } - - /** - * - * To be implemented! We need to get the current character set - * from the current font for {@link ExtTextOutA}, - * which has to be tracked in the playback device. - * - * For {@link ExtTextOutW}, the charset is "UTF-16LE" - * - * @param charset the charset to be used to decode the character bytes - * @return text from this text element - * @throws IOException - */ - public String getText(Charset charset) throws IOException { - return textObject.getText(charset); - } - - /** - * - * @return the x offset for the EmrTextObject - */ - public long getX() { - return textObject.x; - } - - /** - * - * @return the y offset for the EmrTextObject - */ - public long getY() { - return textObject.y; - } - - public long getLeft() { - return left; - } - - public long getTop() { - return top; - } - - public long getRight() { - return right; - } - - public long getBottom() { - return bottom; - } - - public long getGraphicsMode() { - return graphicsMode; - } - - public long getExScale() { - return exScale; - } - - public long getEyScale() { - return eyScale; - } - - } - - public static class ExtTextOutW extends ExtTextOutA { - - @Override - public HemfRecordType getRecordType() { - return HemfRecordType.exttextoutw; - } - - @Override - protected Charset getEncodingHint() { - return UTF_16LE; - } - - public String getText() throws IOException { - return getText(UTF_16LE); - } - } - - /** - * Needs to be implemented. Couldn't find example. - */ - public static class PolyTextOutA extends UnimplementedHemfRecord { - - } - - /** - * Needs to be implemented. Couldn't find example. - */ - public static class PolyTextOutW extends UnimplementedHemfRecord { - - } - - public static class SetTextAlign extends UnimplementedHemfRecord { - } - - public static class SetTextColor extends UnimplementedHemfRecord { - } - - - public static class SetTextJustification extends UnimplementedHemfRecord { - - } - - private static class EmrTextObject { - long x; - long y; - int numChars; - byte[] rawTextBytes;//this stores _all_ of the bytes to the end of the EMRTextObject record. - //Because of potential variable length encodings, must - //carefully read only the numChars from this byte array. - - EmrTextObject(byte[] emrTextObjBytes, Charset charsetHint, int readSoFar) throws IOException { - - int offset = 0; - x = LittleEndian.getUInt(emrTextObjBytes, offset); offset+= LittleEndian.INT_SIZE; - y = LittleEndian.getUInt(emrTextObjBytes, offset); offset+= LittleEndian.INT_SIZE; - long numCharsLong = LittleEndian.getUInt(emrTextObjBytes, offset); offset += LittleEndian.INT_SIZE; - long offString = LittleEndian.getUInt(emrTextObjBytes, offset); offset += LittleEndian.INT_SIZE; - int start = (int)offString-offset-readSoFar; - - if (numCharsLong == 0) { - rawTextBytes = new byte[0]; - numChars = 0; - return; - } - if (numCharsLong > Integer.MAX_VALUE) { - throw new RecordFormatException("Number of characters can't be > Integer.MAX_VALUE"); - } else if (numCharsLong < 0) { - throw new RecordFormatException("Number of characters can't be < 0"); - } - - numChars = (int)numCharsLong; - rawTextBytes = IOUtils.safelyAllocate(emrTextObjBytes.length-start, MAX_RECORD_LENGTH); - System.arraycopy(emrTextObjBytes, start, rawTextBytes, 0, emrTextObjBytes.length-start); - } - - String getText(Charset charset) throws IOException { - StringBuilder sb = new StringBuilder(); - try (Reader r = new InputStreamReader(new ByteArrayInputStream(rawTextBytes), charset)) { - for (int i = 0; i < numChars; i++) { - sb.appendCodePoint(readCodePoint(r)); - } - } - return sb.toString(); - } - - //TODO: move this to IOUtils? - private int readCodePoint(Reader r) throws IOException { - int c1 = r.read(); - if (c1 == -1) { - throw new EOFException("Tried to read beyond byte array"); - } - if (!Character.isHighSurrogate((char)c1)) { - return c1; - } - int c2 = r.read(); - if (c2 == -1) { - throw new EOFException("Tried to read beyond byte array"); - } - if (!Character.isLowSurrogate((char)c2)) { - throw new RecordFormatException("Expected low surrogate after high surrogate"); - } - return Character.toCodePoint((char)c1, (char)c2); - } - } - - -} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java new file mode 100644 index 0000000000..6b8bd13df9 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java @@ -0,0 +1,465 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; + +import org.apache.poi.hemf.record.emfplus.HemfPlusRecord; +import org.apache.poi.hemf.record.emfplus.HemfPlusRecordIterator; +import org.apache.poi.hwmf.usermodel.HwmfPicture; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.LocaleUtil; +import org.apache.poi.util.RecordFormatException; + +/** + * Contains arbitrary data + */ +@Internal +public class HemfComment { + private static final int MAX_RECORD_LENGTH = HwmfPicture.MAX_RECORD_LENGTH; + + public enum HemfCommentRecordType { + emfGeneric(-1, EmfCommentDataGeneric::new, false), + emfSpool(0x00000000, EmfCommentDataGeneric::new, false), + emfPlus(0x2B464D45, EmfCommentDataPlus::new, false), + emfPublic(0x43494447, null, false), + emfBeginGroup(0x00000002, EmfCommentDataBeginGroup::new, true), + emfEndGroup(0x00000003, EmfCommentDataEndGroup::new, true), + emfMultiFormats(0x40000004, EmfCommentDataMultiformats::new, true), + emfWMF(0x80000001, EmfCommentDataWMF::new, true), + emfUnicodeString(0x00000040, EmfCommentDataUnicode::new, true), + emfUnicodeEnd(0x00000080, EmfCommentDataUnicode::new, true) + ; + + + public final long id; + public final Supplier constructor; + public final boolean isEmfPublic; + + HemfCommentRecordType(long id, Supplier constructor, boolean isEmfPublic) { + this.id = id; + this.constructor = constructor; + this.isEmfPublic = isEmfPublic; + } + + public static HemfCommentRecordType getById(long id, boolean isEmfPublic) { + for (HemfCommentRecordType wrt : values()) { + if (wrt.id == id && wrt.isEmfPublic == isEmfPublic) return wrt; + } + return emfGeneric; + } + } + + public interface EmfCommentData { + HemfCommentRecordType getCommentRecordType(); + + long init(LittleEndianInputStream leis, long dataSize) throws IOException; + } + + public static class EmfComment implements HemfRecord { + private EmfCommentData data; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.comment; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + int startIdx = leis.getReadIndex(); + data = new EmfCommentDataIterator(leis, (int)recordSize, true).next(); + return leis.getReadIndex()-startIdx; + } + + public EmfCommentData getCommentData() { + return data; + } + + @Override + public String toString() { + return "{ data: "+data+" }"; + } + } + + public static class EmfCommentDataIterator implements Iterator { + + private final LittleEndianInputStream leis; + private final int startIdx; + private final int limit; + private EmfCommentData currentRecord; + /** is the caller the EmfComment */ + private final boolean emfParent; + + public EmfCommentDataIterator(LittleEndianInputStream leis, int limit, boolean emfParent) { + this.leis = leis; + this.limit = limit; + this.emfParent = emfParent; + startIdx = leis.getReadIndex(); + //queue the first non-header record + currentRecord = _next(); + } + + @Override + public boolean hasNext() { + return currentRecord != null; + } + + @Override + public EmfCommentData next() { + EmfCommentData toReturn = currentRecord; + final boolean isEOF = (limit == -1 || leis.getReadIndex() >= startIdx+limit); + // (currentRecord instanceof HemfPlusMisc.EmfEof) + currentRecord = isEOF ? null : _next(); + return toReturn; + } + + private EmfCommentData _next() { + long type, recordSize; + if (currentRecord == null && emfParent) { + type = HemfRecordType.comment.id; + recordSize = limit; + } else { + // A 32-bit unsigned integer from the RecordType enumeration that identifies this record + // as a comment record. This value MUST be 0x00000046. + try { + type = leis.readUInt(); + } catch (RuntimeException e) { + // EOF + return null; + } + assert(type == HemfRecordType.comment.id); + // A 32-bit unsigned integer that specifies the size in bytes of this record in the + // metafile. This value MUST be a multiple of 4 bytes. + recordSize = leis.readUInt(); + } + + // A 32-bit unsigned integer that specifies the size, in bytes, of the CommentIdentifier and + // CommentRecordParm fields in the RecordBuffer field that follows. + // It MUST NOT include the size of itself or the size of the AlignmentPadding field, if present. + long dataSize = leis.readUInt(); + + try { + leis.mark(2*LittleEndianConsts.INT_SIZE); + // An optional, 32-bit unsigned integer that identifies the type of comment record. + // See the preceding table for descriptions of these record types. + // Valid comment identifier values are listed in the following table. + // + // If this field contains any other value, the comment record MUST be an EMR_COMMENT record + final int commentIdentifier = (int)leis.readUInt(); + // A 32-bit unsigned integer that identifies the type of public comment record. + final int publicCommentIdentifier = (int)leis.readUInt(); + + final boolean isEmfPublic = (commentIdentifier == HemfCommentRecordType.emfPublic.id); + leis.reset(); + + final HemfCommentRecordType commentType = HemfCommentRecordType.getById + (isEmfPublic ? publicCommentIdentifier : commentIdentifier, isEmfPublic); + assert(commentType != null); + final EmfCommentData record = commentType.constructor.get(); + + long readBytes = record.init(leis, dataSize); + final int skipBytes = (int)(recordSize-4-readBytes); + assert (skipBytes >= 0); + leis.skipFully(skipBytes); + + return record; + } catch (IOException e) { + throw new RecordFormatException(e); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + + } + + + + /** + * Private data is unknown to EMF; it is meaningful only to applications that know the format of the + * data and how to use it. EMR_COMMENT private data records MAY be ignored. + */ + public static class EmfCommentDataGeneric implements EmfCommentData { + private byte[] privateData; + + @Override + public HemfCommentRecordType getCommentRecordType() { + return HemfCommentRecordType.emfGeneric; + } + + @Override + public long init(LittleEndianInputStream leis, long dataSize) throws IOException { + privateData = IOUtils.safelyAllocate(dataSize, MAX_RECORD_LENGTH); + leis.readFully(privateData); + return privateData.length; + } + + @Override + public String toString() { + return "\""+new String(privateData, LocaleUtil.CHARSET_1252).replaceAll("\\p{Cntrl}", ".")+"\""; + } + } + + /** The EMR_COMMENT_EMFPLUS record contains embedded EMF+ records. */ + public static class EmfCommentDataPlus implements EmfCommentData { + private final List records = new ArrayList<>(); + + @Override + public HemfCommentRecordType getCommentRecordType() { + return HemfCommentRecordType.emfPlus; + } + + @Override + public long init(final LittleEndianInputStream leis, final long dataSize) + throws IOException { + long startIdx = leis.getReadIndex(); + int commentIdentifier = leis.readInt(); + assert (commentIdentifier == HemfCommentRecordType.emfPlus.id); + new HemfPlusRecordIterator(leis, (int)dataSize-LittleEndianConsts.INT_SIZE).forEachRemaining(records::add); + return leis.getReadIndex()-startIdx; + } + + public List getRecords() { + return Collections.unmodifiableList(records); + } + } + + public static class EmfCommentDataBeginGroup implements EmfCommentData { + private final Rectangle2D bounds = new Rectangle2D.Double(); + private String description; + + @Override + public HemfCommentRecordType getCommentRecordType() { + return HemfCommentRecordType.emfBeginGroup; + } + + @Override + public long init(final LittleEndianInputStream leis, final long dataSize) + throws IOException { + final int startIdx = leis.getReadIndex(); + final int commentIdentifier = (int)leis.readUInt(); + assert(commentIdentifier == HemfCommentRecordType.emfPublic.id); + final int publicCommentIdentifier = (int)leis.readUInt(); + assert(publicCommentIdentifier == HemfCommentRecordType.emfBeginGroup.id); + HemfDraw.readRectL(leis, bounds); + + // The number of Unicode characters in the optional description string that follows. + int nDescription = (int)leis.readUInt(); + + byte[] buf = IOUtils.safelyAllocate(nDescription*2, MAX_RECORD_LENGTH); + leis.readFully(buf); + description = new String(buf, StandardCharsets.UTF_16LE); + + return leis.getReadIndex()-startIdx; + } + } + + public static class EmfCommentDataEndGroup implements EmfCommentData { + @Override + public HemfCommentRecordType getCommentRecordType() { + return HemfCommentRecordType.emfEndGroup; + } + + @Override + public long init(final LittleEndianInputStream leis, final long dataSize) + throws IOException { + final int startIdx = leis.getReadIndex(); + final int commentIdentifier = (int)leis.readUInt(); + assert(commentIdentifier == HemfCommentRecordType.emfPublic.id); + final int publicCommentIdentifier = (int)leis.readUInt(); + assert(publicCommentIdentifier == HemfCommentRecordType.emfEndGroup.id); + return leis.getReadIndex()-startIdx; + } + } + + public static class EmfCommentDataMultiformats implements EmfCommentData { + private final Rectangle2D bounds = new Rectangle2D.Double(); + private final List formats = new ArrayList<>(); + + @Override + public HemfCommentRecordType getCommentRecordType() { + return HemfCommentRecordType.emfMultiFormats; + } + + @Override + public long init(final LittleEndianInputStream leis, final long dataSize) + throws IOException { + final int startIdx = leis.getReadIndex(); + final int commentIdentifier = (int)leis.readUInt(); + assert(commentIdentifier == HemfCommentRecordType.emfPublic.id); + final int publicCommentIdentifier = (int)leis.readUInt(); + assert(publicCommentIdentifier == HemfCommentRecordType.emfMultiFormats.id); + HemfDraw.readRectL(leis, bounds); + + // A 32-bit unsigned integer that specifies the number of graphics formats contained in this record. + int countFormats = (int)leis.readUInt(); + for (int i=0; i getFormats() { + return Collections.unmodifiableList(formats); + } + } + + public enum EmfFormatSignature { + ENHMETA_SIGNATURE(0x464D4520), + EPS_SIGNATURE(0x46535045); + + int id; + + EmfFormatSignature(int flag) { + this.id = id; + } + + public static EmfFormatSignature getById(int id) { + for (EmfFormatSignature wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + + } + + public static class EmfCommentDataFormat { + private EmfFormatSignature signature; + private int version; + private int sizeData; + private int offData; + private byte[] rawData; + + public long init(final LittleEndianInputStream leis, final long dataSize, long startIdx) throws IOException { + // A 32-bit unsigned integer that specifies the format of the image data. + signature = EmfFormatSignature.getById(leis.readInt()); + + // A 32-bit unsigned integer that specifies the format version number. + // If the Signature field specifies encapsulated PostScript (EPS), this value MUST be 0x00000001; + // otherwise, this value MUST be ignored. + version = leis.readInt(); + + // A 32-bit unsigned integer that specifies the size of the data in bytes. + sizeData = leis.readInt(); + + // A 32-bit unsigned integer that specifies the offset to the data from the start + // of the identifier field in an EMR_COMMENT_PUBLIC record. The offset MUST be 32-bit aligned. + offData = leis.readInt(); + if (sizeData < 0) { + throw new RecordFormatException("size for emrformat must be > 0"); + } + if (offData < 0) { + throw new RecordFormatException("offset for emrformat must be > 0"); + } + + return 4*LittleEndianConsts.INT_SIZE; + } + + public byte[] getRawData() { + return rawData; + } + } + + public static class EmfCommentDataWMF implements EmfCommentData { + private final Rectangle2D bounds = new Rectangle2D.Double(); + private final List formats = new ArrayList<>(); + + @Override + public HemfCommentRecordType getCommentRecordType() { + return HemfCommentRecordType.emfWMF; + } + + @Override + public long init(final LittleEndianInputStream leis, final long dataSize) throws IOException { + final int startIdx = leis.getReadIndex(); + final int commentIdentifier = (int)leis.readUInt(); + assert(commentIdentifier == HemfCommentRecordType.emfPublic.id); + final int publicCommentIdentifier = (int)leis.readUInt(); + assert(publicCommentIdentifier == HemfCommentRecordType.emfWMF.id); + + // A 16-bit unsigned integer that specifies the WMF metafile version in terms + //of support for device-independent bitmaps (DIBs) + int version = leis.readUShort(); + + // A 16-bit value that MUST be 0x0000 and MUST be ignored. + leis.skipFully(LittleEndianConsts.SHORT_SIZE); + + // A 32-bit unsigned integer that specifies the checksum for this record. + int checksum = leis.readInt(); + + // A 32-bit value that MUST be 0x00000000 and MUST be ignored. + int flags = leis.readInt(); + + // A 32-bit unsigned integer that specifies the size, in bytes, of the + // WMF metafile in the WinMetafile field. + int winMetafileSize = (int)leis.readUInt(); + + byte[] winMetafile = IOUtils.safelyAllocate(winMetafileSize, MAX_RECORD_LENGTH); + // some emf comments are truncated, so we don't use readFully here + leis.read(winMetafile); + + return leis.getReadIndex()-startIdx; + } + } + + public static class EmfCommentDataUnicode implements EmfCommentData { + private final Rectangle2D bounds = new Rectangle2D.Double(); + private final List formats = new ArrayList<>(); + + @Override + public HemfCommentRecordType getCommentRecordType() { + return HemfCommentRecordType.emfUnicodeString; + } + + @Override + public long init(final LittleEndianInputStream leis, final long dataSize) + throws IOException { + throw new RecordFormatException("UNICODE_STRING/UNICODE_END values are reserved in CommentPublic records"); + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java new file mode 100644 index 0000000000..7e495e2868 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java @@ -0,0 +1,1153 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; +import static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds; + +import java.awt.Shape; +import java.awt.geom.Arc2D; +import java.awt.geom.Dimension2D; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; + +import org.apache.poi.hemf.draw.HemfDrawProperties; +import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hwmf.draw.HwmfGraphics.FillDrawStyle; +import org.apache.poi.hwmf.record.HwmfDraw; +import org.apache.poi.hwmf.record.HwmfDraw.WmfSelectObject; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; + +public class HemfDraw { + /** + * The EMR_SELECTOBJECT record adds a graphics object to the current metafile playback device + * context. The object is specified either by its index in the EMF Object Table or by its + * value from the StockObject enumeration. + */ + public static class EmfSelectObject extends WmfSelectObject implements HemfRecord { + + private static final String[] STOCK_IDS = { + "0x80000000 /* WHITE_BRUSH */", + "0x80000001 /* LTGRAY_BRUSH */", + "0x80000002 /* GRAY_BRUSH */", + "0x80000003 /* DKGRAY_BRUSH */", + "0x80000004 /* BLACK_BRUSH */", + "0x80000005 /* NULL_BRUSH */", + "0x80000006 /* WHITE_PEN */", + "0x80000007 /* BLACK_PEN */", + "0x80000008 /* NULL_PEN */", + "0x8000000A /* OEM_FIXED_FONT */", + "0x8000000B /* ANSI_FIXED_FONT */", + "0x8000000C /* ANSI_VAR_FONT */", + "0x8000000D /* SYSTEM_FONT */", + "0x8000000E /* DEVICE_DEFAULT_FONT */", + "0x8000000F /* DEFAULT_PALETTE */", + "0x80000010 /* SYSTEM_FIXED_FONT */", + "0x80000011 /* DEFAULT_GUI_FONT */", + "0x80000012 /* DC_BRUSH */", + "0x80000013 /* DC_PEN */" + }; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.selectObject; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer that specifies either the index of a graphics object in the + // EMF Object Table or the index of a stock object from the StockObject enumeration. + objectIndex = leis.readInt(); + return LittleEndianConsts.INT_SIZE; + } + + @Override + public String toString() { + return "{ index: "+ + (((objectIndex & 0x80000000) != 0 && (objectIndex & 0x3FFFFFFF) <= 13 ) + ? STOCK_IDS[objectIndex & 0x3FFFFFFF] + : objectIndex)+" }"; + } + + } + + + /** The EMR_POLYBEZIER record specifies one or more Bezier curves. */ + public static class EmfPolyBezier extends HwmfDraw.WmfPolygon implements HemfRecord { + private final Rectangle2D bounds = new Rectangle2D.Double(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyBezier; + } + + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointL(leis, point); + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + + /* A 32-bit unsigned integer that specifies the number of points in the points + * array. This value MUST be one more than three times the number of curves to + * be drawn, because each Bezier curve requires two control points and an + * endpoint, and the initial curve requires an additional starting point. + * + * Line width | Device supports wideline | Maximum points allowed + * 1 | n/a | 16K + * > 1 | yes | 16K + * > 1 | no | 1360 + * + * Any extra points MUST be ignored. + */ + final int count = (int)leis.readUInt(); + final int points = Math.min(count, 16384); + size += LittleEndianConsts.INT_SIZE; + + poly = new Path2D.Double(Path2D.WIND_EVEN_ODD, points+2); + + /* Cubic Bezier curves are defined using the endpoints and control points + * specified by the points field. The first curve is drawn from the first + * point to the fourth point, using the second and third points as control + * points. Each subsequent curve in the sequence needs exactly three more points: + * the ending point of the previous curve is used as the starting point, + * the next two points in the sequence are control points, + * and the third is the ending point. + * The cubic Bezier curves SHOULD be drawn using the current pen. + */ + + Point2D pnt[] = { new Point2D.Double(), new Point2D.Double(), new Point2D.Double() }; + + int i=0; + if (hasStartPoint()) { + if (i < points) { + size += readPoint(leis, pnt[0]); + poly.moveTo(pnt[0].getX(), pnt[0].getY()); + i++; + } + } else { + poly.moveTo(0, 0); + } + + for (; i+2 path.append(poly, !hasStartPoint()), getFillDrawStyle()); + } + } + + /** + * The EMR_POLYBEZIER16 record specifies one or more Bezier curves. + * The curves are drawn using the current pen. + */ + public static class EmfPolyBezier16 extends EmfPolyBezier { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyBezier16; + } + + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointS(leis, point); + } + } + + + /** + * The EMR_POLYGON record specifies a polygon consisting of two or more vertexes connected by + * straight lines. + */ + public static class EmfPolygon extends HwmfDraw.WmfPolygon implements HemfRecord { + private final Rectangle2D bounds = new Rectangle2D.Double(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polygon; + } + + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointL(leis, point); + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + + // see PolyBezier about limits + final int count = (int)leis.readUInt(); + final int points = Math.min(count, 16384); + size += LittleEndianConsts.INT_SIZE; + + poly = new Path2D.Double(Path2D.WIND_EVEN_ODD, points); + + Point2D pnt = new Point2D.Double(); + for (int i=0; i path.append(poly, false), getFillDrawStyle()); + } + } + + /** + * The EMR_POLYGON16 record specifies a polygon consisting of two or more vertexes connected by straight lines. + * The polygon is outlined by using the current pen and filled by using the current brush and polygon fill mode. + * The polygon is closed automatically by drawing a line from the last vertex to the first + */ + public static class EmfPolygon16 extends EmfPolygon { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polygon16; + } + + @Override + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointS(leis, point); + } + } + + /** + * The EMR_POLYLINE record specifies a series of line segments by connecting the points in the + * specified array. + */ + public static class EmfPolyline extends EmfPolygon { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyline; + } + + @Override + protected FillDrawStyle getFillDrawStyle() { + // The line segments SHOULD be drawn using the current pen. + return FillDrawStyle.DRAW; + } + } + + /** + * The EMR_POLYLINE16 record specifies a series of line segments by connecting the points in the + * specified array. + */ + public static class EmfPolyline16 extends EmfPolyline { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyline16; + } + + @Override + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointS(leis, point); + } + } + + /** + * The EMR_POLYBEZIERTO record specifies one or more Bezier curves based upon the current + * position. + */ + public static class EmfPolyBezierTo extends EmfPolyBezier { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyBezierTo; + } + + @Override + protected boolean hasStartPoint() { + return false; + } + + @Override + public void draw(HemfGraphics ctx) { + polyTo(ctx, poly, getFillDrawStyle()); + } + } + + /** + * The EMR_POLYBEZIERTO16 record specifies one or more Bezier curves based on the current + * position. + */ + public static class EmfPolyBezierTo16 extends EmfPolyBezierTo { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyBezierTo16; + } + + @Override + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointS(leis, point); + } + } + + /** The EMR_POLYLINETO record specifies one or more straight lines based upon the current position. */ + public static class EmfPolylineTo extends EmfPolyline { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polylineTo; + } + + @Override + protected boolean hasStartPoint() { + return false; + } + + @Override + public void draw(HemfGraphics ctx) { + polyTo(ctx, poly, getFillDrawStyle()); + } + } + + /** + * The EMR_POLYLINETO16 record specifies one or more straight lines based upon the current position. + * A line is drawn from the current position to the first point specified by the points field by using the + * current pen. For each additional line, drawing is performed from the ending point of the previous + * line to the next point specified by points. + */ + public static class EmfPolylineTo16 extends EmfPolylineTo { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polylineTo16; + } + + @Override + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointS(leis, point); + } + } + + /** + * The EMR_POLYPOLYGON record specifies a series of closed polygons. + */ + public static class EmfPolyPolygon extends HwmfDraw.WmfPolyPolygon implements HemfRecord { + private final Rectangle2D bounds = new Rectangle2D.Double(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyPolygon; + } + + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointL(leis, point); + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + + // A 32-bit unsigned integer that specifies the number of polygons. + long numberOfPolygons = leis.readUInt(); + // A 32-bit unsigned integer that specifies the total number of points in all polygons. + long count = Math.min(16384, leis.readUInt()); + + size += 2 * LittleEndianConsts.INT_SIZE; + + // An array of 32-bit unsigned integers that specifies the point count for each polygon. + long[] polygonPointCount = new long[(int)numberOfPolygons]; + + size += numberOfPolygons * LittleEndianConsts.INT_SIZE; + + for (int i=0; i path.append(shape, false), getFillDrawStyle()); + } + } + + /** + * The EMR_POLYPOLYGON16 record specifies a series of closed polygons. Each polygon is outlined + * using the current pen, and filled using the current brush and polygon fill mode. + * The polygons drawn by this record can overlap. + */ + public static class EmfPolyPolygon16 extends EmfPolyPolygon { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyPolygon16; + } + + @Override + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointS(leis, point); + } + } + + /** + * The EMR_POLYPOLYLINE record specifies multiple series of connected line segments. + */ + public static class EmfPolyPolyline extends EmfPolyPolygon { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyPolyline; + } + + @Override + protected boolean isClosed() { + return false; + } + + @Override + protected FillDrawStyle getFillDrawStyle() { + return FillDrawStyle.DRAW; + } + } + + /** The EMR_POLYPOLYLINE16 record specifies multiple series of connected line segments. */ + public static class EmfPolyPolyline16 extends EmfPolyPolyline { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyPolyline16; + } + + @Override + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointS(leis, point); + } + } + + /** + * The EMR_SETPIXELV record defines the color of the pixel at the specified logical coordinates. + */ + public static class EmfSetPixelV extends HwmfDraw.WmfSetPixel implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setPixelV; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readPointL(leis, point); + size += colorRef.init(leis); + return size; + } + } + + /** + * The EMR_MOVETOEX record specifies coordinates of the new current position, in logical units. + */ + public static class EmfSetMoveToEx extends HwmfDraw.WmfMoveTo implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setMoveToEx; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readPointL(leis, point); + } + + @Override + public void draw(final HemfGraphics ctx) { + ctx.draw((path) -> path.moveTo(point.getX(), point.getY()), FillDrawStyle.NONE); + } + } + + /** + * The EMR_ARC record specifies an elliptical arc. + * It resets the current position to the end point of the arc. + */ + public static class EmfArc extends HwmfDraw.WmfArc implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.arc; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + size += readPointL(leis, startPoint); + size += readPointL(leis, endPoint); + return size; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.draw(path -> path.append(getShape(), false), getFillDrawStyle()); + } + } + + /** + * The EMR_CHORD record specifies a chord, which is a region bounded by the intersection of an + * ellipse and a line segment, called a secant. The chord is outlined by using the current pen + * and filled by using the current brush. + */ + public static class EmfChord extends HwmfDraw.WmfChord implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.chord; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + size += readPointL(leis, startPoint); + size += readPointL(leis, endPoint); + return size; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.draw(path -> path.append(getShape(), false), getFillDrawStyle()); + } + } + + /** + * The EMR_PIE record specifies a pie-shaped wedge bounded by the intersection of an ellipse and two + * radials. The pie is outlined by using the current pen and filled by using the current brush. + */ + public static class EmfPie extends HwmfDraw.WmfPie implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.pie; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + size += readPointL(leis, startPoint); + size += readPointL(leis, endPoint); + return size; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.draw(path -> path.append(getShape(), false), getFillDrawStyle()); + } + } + + /** + * The EMR_ELLIPSE record specifies an ellipse. The center of the ellipse is the center of the specified + * bounding rectangle. The ellipse is outlined by using the current pen and is filled by using the current + * brush. + */ + public static class EmfEllipse extends HwmfDraw.WmfEllipse implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.ellipse; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readRectL(leis, bounds); + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.draw(path -> path.append(getShape(), false), FillDrawStyle.FILL_DRAW); + } + } + + /** + * The EMR_RECTANGLE record draws a rectangle. The rectangle is outlined by using the current pen + * and filled by using the current brush. + */ + public static class EmfRectangle extends HwmfDraw.WmfRectangle implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.rectangle; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readRectL(leis, bounds); + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.draw(path -> path.append(normalizeBounds(bounds), false), FillDrawStyle.FILL_DRAW); + } + } + + /** + * The EMR_ROUNDRECT record specifies a rectangle with rounded corners. The rectangle is outlined + * by using the current pen and filled by using the current brush. + */ + public static class EmfRoundRect extends HwmfDraw.WmfRoundRect implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.roundRect; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + + // A 32-bit unsigned integer that defines the x-coordinate of the point. + width = (int)leis.readUInt(); + height = (int)leis.readUInt(); + + return size + 2*LittleEndianConsts.INT_SIZE; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.draw(path -> path.append(getShape(), false), FillDrawStyle.FILL_DRAW); + } + } + + /** + * The EMR_LINETO record specifies a line from the current position up to, but not including, the + * specified point. It resets the current position to the specified point. + */ + public static class EmfLineTo extends HwmfDraw.WmfLineTo implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.lineTo; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readPointL(leis, point); + } + + @Override + public void draw(final HemfGraphics ctx) { + ctx.draw((path) -> path.lineTo(point.getX(), point.getY()), FillDrawStyle.DRAW); + } + } + + /** + * The EMR_ARCTO record specifies an elliptical arc. + * It resets the current position to the end point of the arc. + */ + public static class EmfArcTo extends HwmfDraw.WmfArc implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.arcTo; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + size += readPointL(leis, startPoint); + size += readPointL(leis, endPoint); + return size; + } + + @Override + public void draw(final HemfGraphics ctx) { + final Arc2D arc = getShape(); + ctx.draw((path) -> path.append(arc, true), getFillDrawStyle()); + } + } + + /** The EMR_POLYDRAW record specifies a set of line segments and Bezier curves. */ + public static class EmfPolyDraw extends HwmfDraw.WmfPolygon implements HemfRecord { + private final Rectangle2D bounds = new Rectangle2D.Double(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyDraw; + } + + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointL(leis, point); + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + int count = (int)leis.readUInt(); + size += LittleEndianConsts.INT_SIZE; + Point2D points[] = new Point2D[count]; + for (int i=0; i path.append(poly, false), getFillDrawStyle()); + } + } + + public static class EmfPolyDraw16 extends EmfPolyDraw { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.polyDraw16; + } + + protected long readPoint(LittleEndianInputStream leis, Point2D point) { + return readPointS(leis, point); + } + } + + /** + * This record opens a path bracket in the current playback device context. + * + * After a path bracket is open, an application can begin processing records to define + * the points that lie in the path. An application MUST close an open path bracket by + * processing the EMR_ENDPATH record. + * + * When an application processes the EMR_BEGINPATH record, all previous paths + * MUST be discarded from the playback device context. + */ + public static class EmfBeginPath implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.beginPath; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return 0; + } + + @Override + public void draw(HemfGraphics ctx) { + final HemfDrawProperties prop = ctx.getProperties(); + prop.setPath(new Path2D.Double()); + prop.setUsePathBracket(true); + } + + @Override + public String toString() { + return "{}"; + } + } + + /** + * This record closes a path bracket and selects the path defined by the bracket into + * the playback device context. + */ + public static class EmfEndPath implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.endPath; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return 0; + } + + @Override + public void draw(HemfGraphics ctx) { + final HemfDrawProperties prop = ctx.getProperties(); + prop.setUsePathBracket(false); + } + + @Override + public String toString() { + return "{}"; + } + } + + /** + * This record aborts a path bracket or discards the path from a closed path bracket. + */ + public static class EmfAbortPath implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.abortPath; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return 0; + } + + @Override + public void draw(HemfGraphics ctx) { + final HemfDrawProperties prop = ctx.getProperties(); + prop.setPath(null); + prop.setUsePathBracket(false); + } + + @Override + public String toString() { + return "{}"; + } + } + + /** + * This record closes an open figure in a path. + * + * Processing the EMR_CLOSEFIGURE record MUST close the figure by drawing a line + * from the current position to the first point of the figure, and then it MUST connect + * the lines by using the line join style. If a figure is closed by processing the + * EMR_LINETO record instead of the EMR_CLOSEFIGURE record, end caps are + * used to create the corner instead of a join. + * + * The EMR_CLOSEFIGURE record SHOULD only be used if there is an open path + * bracket in the playback device context. + * + * A figure in a path is open unless it is explicitly closed by processing this record. + * Note: A figure can be open even if the current point and the starting point of the + * figure are the same. + * + * After processing the EMR_CLOSEFIGURE record, adding a line or curve to the path + * MUST start a new figure. + */ + public static class EmfCloseFigure implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.closeFigure; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return 0; + } + + @Override + public void draw(HemfGraphics ctx) { + final HemfDrawProperties prop = ctx.getProperties(); + final Path2D path = prop.getPath(); + if (path != null && path.getCurrentPoint() != null) { + path.closePath(); + prop.setLocation(path.getCurrentPoint()); + } + } + + @Override + public String toString() { + return "{}"; + } + } + + /** + * This record transforms any curves in the selected path into the playback device + * context; each curve MUST be turned into a sequence of lines. + */ + public static class EmfFlattenPath implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.flattenPath; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return 0; + } + } + + /** + * This record redefines the current path as the area that would be painted if the path + * were drawn using the pen currently selected into the playback device context. + */ + public static class EmfWidenPath implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.widenPath; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return 0; + } + + @Override + public String toString() { + return "{}"; + } + } + + /** + * The EMR_STROKEPATH record renders the specified path by using the current pen. + */ + public static class EmfStrokePath implements HemfRecord { + protected final Rectangle2D bounds = new Rectangle2D.Double(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.strokePath; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 128-bit WMF RectL object, which specifies bounding rectangle, in device units + return (recordSize == 0) ? 0 : readRectL(leis, bounds); + } + + @Override + public void draw(HemfGraphics ctx) { + HemfDrawProperties props = ctx.getProperties(); + Path2D path = props.getPath(); + path.setWindingRule(ctx.getProperties().getWindingRule()); + ctx.draw(path); + } + + @Override + public String toString() { + return boundsToString(bounds); + } + } + + + /** + * The EMR_FILLPATH record closes any open figures in the current path and fills the path's interior by + * using the current brush and polygon-filling mode. + */ + public static class EmfFillPath extends EmfStrokePath { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.fillPath; + } + + @Override + public void draw(HemfGraphics ctx) { + final HemfDrawProperties prop = ctx.getProperties(); + final Path2D origPath = prop.getPath(); + if (origPath.getCurrentPoint() == null) { + return; + } + final Path2D path = (Path2D)origPath.clone(); + path.closePath(); + path.setWindingRule(ctx.getProperties().getWindingRule()); + ctx.fill(path); + } + } + + /** + * The EMR_STROKEANDFILLPATH record closes any open figures in a path, strokes the outline of the + * path by using the current pen, and fills its interior by using the current brush. + */ + public static class EmfStrokeAndFillPath extends EmfStrokePath { + protected final Rectangle2D bounds = new Rectangle2D.Double(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.strokeAndFillPath; + } + + @Override + public void draw(HemfGraphics ctx) { + HemfDrawProperties props = ctx.getProperties(); + Path2D path = props.getPath(); + path.closePath(); + path.setWindingRule(ctx.getProperties().getWindingRule()); + ctx.fill(path); + ctx.draw(path); + } + } + + static long readRectL(LittleEndianInputStream leis, Rectangle2D bounds) { + /* A 32-bit signed integer that defines the x coordinate, in logical coordinates, + * of the ... corner of the rectangle. + */ + final int left = leis.readInt(); + final int top = leis.readInt(); + final int right = leis.readInt(); + final int bottom = leis.readInt(); + bounds.setRect(left, top, right-left, bottom-top); + + return 4 * LittleEndianConsts.INT_SIZE; + } + + static long readPointS(LittleEndianInputStream leis, Point2D point) { + // x (2 bytes): A 16-bit signed integer that defines the horizontal (x) coordinate of the point. + final int x = leis.readShort(); + // y (2 bytes): A 16-bit signed integer that defines the vertical (y) coordinate of the point. + final int y = leis.readShort(); + point.setLocation(x, y); + + return 2*LittleEndianConsts.SHORT_SIZE; + + } + static long readPointL(LittleEndianInputStream leis, Point2D point) { + // x (4 bytes): A 32-bit signed integer that defines the horizontal (x) coordinate of the point. + final int x = leis.readInt(); + // y (4 bytes): A 32-bit signed integer that defines the vertical (y) coordinate of the point. + final int y = leis.readInt(); + point.setLocation(x, y); + + return 2*LittleEndianConsts.INT_SIZE; + + } + + static long readDimensionFloat(LittleEndianInputStream leis, Dimension2D dimension) { + final double width = leis.readFloat(); + final double height = leis.readFloat(); + dimension.setSize(width, height); + return 2*LittleEndianConsts.INT_SIZE; + } + + static long readDimensionInt(LittleEndianInputStream leis, Dimension2D dimension) { + // although the spec says "use unsigned ints", there are examples out there using signed ints + final double width = leis.readInt(); + final double height = leis.readInt(); + dimension.setSize(width, height); + return 2*LittleEndianConsts.INT_SIZE; + } + + private static void polyTo(final HemfGraphics ctx, final Path2D poly, FillDrawStyle fillDrawStyle) { + if (poly.getCurrentPoint() == null) { + return; + } + + final PathIterator pi = poly.getPathIterator(null); + // ignore empty polys and dummy start point (moveTo) + pi.next(); + if (pi.isDone()) { + return; + } + + ctx.draw((path) -> path.append(pi, true), fillDrawStyle); + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java new file mode 100644 index 0000000000..8e19e87116 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java @@ -0,0 +1,719 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL; +import static org.apache.poi.hemf.record.emf.HemfDraw.readRectL; +import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE; +import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; +import static org.apache.poi.hwmf.record.HwmfDraw.pointToString; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.hemf.draw.HemfDrawProperties; +import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.hwmf.record.HwmfBitmapDib; +import org.apache.poi.hwmf.record.HwmfColorRef; +import org.apache.poi.hwmf.record.HwmfDraw; +import org.apache.poi.hwmf.record.HwmfFill; +import org.apache.poi.hwmf.record.HwmfFill.ColorUsage; +import org.apache.poi.hwmf.record.HwmfRegionMode; +import org.apache.poi.hwmf.record.HwmfTernaryRasterOp; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; + +public class HemfFill { + private static final int MAX_RECORD_LENGTH = 10_000_000; + + /** + * The EMR_SETPOLYFILLMODE record defines polygon fill mode. + */ + public static class EmfSetPolyfillMode extends HwmfFill.WmfSetPolyfillMode implements HemfRecord { + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setPolyfillMode; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer that specifies the polygon fill mode and + // MUST be in the PolygonFillMode enumeration. + polyFillMode = HwmfPolyfillMode.valueOf((int)leis.readUInt()); + return LittleEndianConsts.INT_SIZE; + } + } + + public static class EmfExtFloodFill extends HwmfFill.WmfExtFloodFill implements HemfRecord { + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.extFloodFill; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readPointL(leis, start); + size += colorRef.init(leis); + // A 32-bit unsigned integer that specifies how to use the Color value to determine the area for + // the flood fill operation. The value MUST be in the FloodFill enumeration + mode = (int)leis.readUInt(); + return size + LittleEndianConsts.INT_SIZE; + } + } + + /** + * The EMR_STRETCHBLT record specifies a block transfer of pixels from a source bitmap to a destination rectangle, + * optionally in combination with a brush pattern, according to a specified raster operation, stretching or + * compressing the output to fit the dimensions of the destination, if necessary. + */ + public static class EmfStretchBlt extends HwmfFill.WmfStretchDib implements HemfRecord { + protected final Rectangle2D bounds = new Rectangle2D.Double(); + + /** An XForm object that specifies a world-space to page-space transform to apply to the source bitmap. */ + protected final AffineTransform xFormSrc = new AffineTransform(); + + /** A WMF ColorRef object that specifies the background color of the source bitmap. */ + protected final HwmfColorRef bkColorSrc = new HwmfColorRef(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.stretchBlt; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + int startIdx = leis.getReadIndex(); + + long size = readRectL(leis, bounds); + + size += readBounds2(leis, this.dstBounds); + + // A 32-bit unsigned integer that specifies the raster operation code. This code defines how the + // color data of the source rectangle is to be combined with the color data of the destination + // rectangle and optionally a brush pattern, to achieve the final color. + int rasterOpIndex = (int)leis.readUInt(); + + rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex >>> 16); + + size += LittleEndianConsts.INT_SIZE; + + final Point2D srcPnt = new Point2D.Double(); + size += readPointL(leis, srcPnt); + + size += readXForm(leis, xFormSrc); + + size += bkColorSrc.init(leis); + + colorUsage = ColorUsage.valueOf((int)leis.readUInt()); + + // A 32-bit unsigned integer that specifies the offset, in bytes, from the + // start of this record to the source bitmap header in the BitmapBuffer field. + final int offBmiSrc = (int)leis.readUInt(); + + // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap header. + final int cbBmiSrc = (int)leis.readUInt(); + size += 3*LittleEndianConsts.INT_SIZE; + if (size >= recordSize) { + return size; + } + + // A 32-bit unsigned integer that specifies the offset, in bytes, from the + // start of this record to the source bitmap bits in the BitmapBuffer field. + final int offBitsSrc = (int)leis.readUInt(); + + // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap bits. + final int cbBitsSrc = (int)leis.readUInt(); + size += 2*LittleEndianConsts.INT_SIZE; + + if (size >= recordSize) { + return size; + } + + if (srcEqualsDstDimension()) { + srcBounds.setRect(srcPnt.getX(), srcPnt.getY(), dstBounds.getWidth(), dstBounds.getHeight()); + } else { + int srcWidth = leis.readInt(); + int srcHeight = leis.readInt(); + size += 2 * LittleEndianConsts.INT_SIZE; + srcBounds.setRect(srcPnt.getX(), srcPnt.getY(), srcWidth, srcHeight); + } + + size += readBitmap(leis, bitmap, startIdx, offBmiSrc, cbBmiSrc, offBitsSrc, cbBitsSrc); + + return size; + } + + protected boolean srcEqualsDstDimension() { + return false; + } + + @Override + public void draw(HemfGraphics ctx) { + HemfDrawProperties prop = ctx.getProperties(); + prop.setBackgroundColor(this.bkColorSrc); + super.draw(ctx); + } + + @Override + public String toString() { + return + "{ bounds: "+boundsToString(bounds)+ + ", xFormSrc: { scaleX: "+xFormSrc.getScaleX()+", shearX: "+xFormSrc.getShearX()+", transX: "+xFormSrc.getTranslateX()+", scaleY: "+xFormSrc.getScaleY()+", shearY: "+xFormSrc.getShearY()+", transY: "+xFormSrc.getTranslateY()+" }"+ + ", bkColorSrc: "+bkColorSrc+ + ","+super.toString().substring(1); + } + } + + /** + * The EMR_STRETCHDIBITS record specifies a block transfer of pixels from a source bitmap to a + * destination rectangle, optionally in combination with a brush pattern, according to a specified raster + * operation, stretching or compressing the output to fit the dimensions of the destination, if necessary. + */ + public static class EmfStretchDiBits extends HwmfFill.WmfStretchDib implements HemfRecord { + protected final Rectangle2D bounds = new Rectangle2D.Double(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.stretchDiBits; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + final int startIdx = leis.getReadIndex(); + + long size = readRectL(leis, bounds); + + // A 32-bit signed integer that specifies the logical x-coordinate of the upper-left + // corner of the destination rectangle. + int xDest = leis.readInt(); + int yDest = leis.readInt(); + size += 2*LittleEndianConsts.INT_SIZE; + + size += readBounds2(leis, srcBounds); + + // A 32-bit unsigned integer that specifies the offset, in bytes from the start + // of this record to the source bitmap header. + int offBmiSrc = (int)leis.readUInt(); + + // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap header. + int cbBmiSrc = (int)leis.readUInt(); + + // A 32-bit unsigned integer that specifies the offset, in bytes, from the + // start of this record to the source bitmap bits. + int offBitsSrc = (int)leis.readUInt(); + + // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap bits. + int cbBitsSrc = (int)leis.readUInt(); + + // A 32-bit unsigned integer that specifies how to interpret values in the color table + // in the source bitmap header. This value MUST be in the DIBColors enumeration + colorUsage = ColorUsage.valueOf(leis.readInt()); + + // A 32-bit unsigned integer that specifies a raster operation code. + // These codes define how the color data of the source rectangle is to be combined with the color data + // of the destination rectangle and optionally a brush pattern, to achieve the final color. + // The value MUST be in the WMF Ternary Raster Operation enumeration + int rasterOpIndex = (int)leis.readUInt(); + rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex >>> 16); + + // A 32-bit signed integer that specifies the logical width of the destination rectangle. + int cxDest = leis.readInt(); + + // A 32-bit signed integer that specifies the logical height of the destination rectangle. + int cyDest = leis.readInt(); + + dstBounds.setRect(xDest, yDest, cxDest, cyDest); + + size += 8*LittleEndianConsts.INT_SIZE; + + size += readBitmap(leis, bitmap, startIdx, offBmiSrc, cbBmiSrc, offBitsSrc, cbBitsSrc); + + return size; + } + } + + /** + * The EMR_BITBLT record specifies a block transfer of pixels from a source bitmap to a destination rectangle, + * optionally in combination with a brush pattern, according to a specified raster operation. + */ + public static class EmfBitBlt extends EmfStretchBlt { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.bitBlt; + } + + @Override + protected boolean srcEqualsDstDimension() { + return false; + } + } + + + /** The EMR_FRAMERGN record draws a border around the specified region using the specified brush. */ + public static class EmfFrameRgn extends HwmfDraw.WmfFrameRegion implements HemfRecord { + private final Rectangle2D bounds = new Rectangle2D.Double(); + private final List rgnRects = new ArrayList<>(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.frameRgn; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + // A 32-bit unsigned integer that specifies the size of region data, in bytes. + long rgnDataSize = leis.readUInt(); + // A 32-bit unsigned integer that specifies the brush EMF Object Table index. + brushIndex = (int)leis.readUInt(); + // A 32-bit signed integer that specifies the width of the vertical brush stroke, in logical units. + width = leis.readInt(); + // A 32-bit signed integer that specifies the height of the horizontal brush stroke, in logical units. + height = leis.readInt(); + size += 4*LittleEndianConsts.INT_SIZE; + size += readRgnData(leis, rgnRects); + return size; + } + + @Override + public void draw(HwmfGraphics ctx) { + ctx.applyObjectTableEntry(brushIndex); + ctx.fill(getShape()); + } + + protected Shape getShape() { + return getRgnShape(rgnRects); + } + } + + /** The EMR_INVERTRGN record inverts the colors in the specified region. */ + public static class EmfInvertRgn implements HemfRecord { + protected final Rectangle2D bounds = new Rectangle2D.Double(); + protected final List rgnRects = new ArrayList<>(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.invertRgn; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + // A 32-bit unsigned integer that specifies the size of region data, in bytes. + long rgnDataSize = leis.readUInt(); + size += LittleEndianConsts.INT_SIZE; + size += readRgnData(leis, rgnRects); + return size; + } + + protected Shape getShape() { + return getRgnShape(rgnRects); + } + } + + /** + * The EMR_PAINTRGN record paints the specified region by using the brush currently selected into the + * playback device context. + */ + public static class EmfPaintRgn extends EmfInvertRgn { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.paintRgn; + } + } + + /** The EMR_FILLRGN record fills the specified region by using the specified brush. */ + public static class EmfFillRgn extends HwmfFill.WmfFillRegion implements HemfRecord { + protected final Rectangle2D bounds = new Rectangle2D.Double(); + protected final List rgnRects = new ArrayList<>(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.fillRgn; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + long size = readRectL(leis, bounds); + // A 32-bit unsigned integer that specifies the size of region data, in bytes. + long rgnDataSize = leis.readUInt(); + brushIndex = (int)leis.readUInt(); + size += 2*LittleEndianConsts.INT_SIZE; + size += readRgnData(leis, rgnRects); + return size; + } + + protected Shape getShape() { + return getRgnShape(rgnRects); + } + } + + public static class EmfExtSelectClipRgn implements HemfRecord { + protected HwmfRegionMode regionMode; + protected final List rgnRects = new ArrayList<>(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.extSelectClipRgn; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer that specifies the size of region data in bytes + long rgnDataSize = leis.readUInt(); + // A 32-bit unsigned integer that specifies the way to use the region. + regionMode = HwmfRegionMode.valueOf((int)leis.readUInt()); + long size = 2* LittleEndianConsts.INT_SIZE; + + // If RegionMode is RGN_COPY, this data can be omitted and the clip region + // SHOULD be set to the default (NULL) clip region. + if (regionMode != HwmfRegionMode.RGN_COPY) { + size += readRgnData(leis, rgnRects); + } + return size; + } + + protected Shape getShape() { + return getRgnShape(rgnRects); + } + + @Override + public void draw(HemfGraphics ctx) { + HemfDrawProperties prop = ctx.getProperties(); + ctx.setClip(getShape(), regionMode, true); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ regionMode: '"+regionMode+"'"); + sb.append(", regions: ["); + boolean isFirst = true; + for (Rectangle2D r : rgnRects) { + if (!isFirst) { + sb.append(","); + } + isFirst = false; + sb.append(boundsToString(r)); + } + sb.append("]}"); + return sb.toString(); + } + } + + public static class EmfAlphaBlend implements HemfRecord { + /** the destination bounding rectangle in device units */ + protected final Rectangle2D bounds = new Rectangle2D.Double(); + /** the destination rectangle */ + protected final Rectangle2D destRect = new Rectangle2D.Double(); + /** the source rectangle */ + protected final Rectangle2D srcRect = new Rectangle2D.Double(); + /** + * The blend operation code. The only source and destination blend operation that has been defined + * is 0x00, which specifies that the source bitmap MUST be combined with the destination bitmap based + * on the alpha transparency values of the source pixels. + */ + protected byte blendOperation; + /** This value MUST be 0x00 and MUST be ignored. */ + protected byte blendFlags; + /** + * An 8-bit unsigned integer that specifies alpha transparency, which determines the blend of the source + * and destination bitmaps. This value MUST be used on the entire source bitmap. The minimum alpha + * transparency value, zero, corresponds to completely transparent; the maximum value, 0xFF, corresponds + * to completely opaque. In effect, a value of 0xFF specifies that the per-pixel alpha values determine + * the blend of the source and destination bitmaps. + */ + protected int srcConstantAlpha; + /** + * A byte that specifies how source and destination pixels are interpreted with respect to alpha transparency. + * + * 0x00: + * The pixels in the source bitmap do not specify alpha transparency. + * In this case, the SrcConstantAlpha value determines the blend of the source and destination bitmaps. + * Note that in the following equations SrcConstantAlpha is divided by 255, + * which produces a value in the range 0 to 1. + * + * 0x01: "AC_SRC_ALPHA" + * Indicates that the source bitmap is 32 bits-per-pixel and specifies an alpha transparency value + * for each pixel. + */ + protected byte alphaFormat; + /** a world-space to page-space transform to apply to the source bitmap. */ + protected final AffineTransform xFormSrc = new AffineTransform(); + /** the background color of the source bitmap. */ + protected final HwmfColorRef bkColorSrc = new HwmfColorRef(); + /** + * A 32-bit unsigned integer that specifies how to interpret values in the + * color table in the source bitmap header. + */ + protected ColorUsage usageSrc; + + protected final HwmfBitmapDib bitmap = new HwmfBitmapDib(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.alphaBlend; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + final int startIdx = leis.getReadIndex(); + + long size = readRectL(leis, bounds); + size += readBounds2(leis, destRect); + + blendOperation = leis.readByte(); + assert (blendOperation == 0); + blendFlags = leis.readByte(); + assert (blendOperation == 0); + srcConstantAlpha = leis.readUByte(); + alphaFormat = leis.readByte(); + + // A 32-bit signed integer that specifies the logical x-coordinate of the upper-left + // corner of the source rectangle. + final int xSrc = leis.readInt(); + // A 32-bit signed integer that specifies the logical y-coordinate of the upper-left + // corner of the source rectangle. + final int ySrc = leis.readInt(); + + size += 3*LittleEndianConsts.INT_SIZE; + size += readXForm(leis, xFormSrc); + size += bkColorSrc.init(leis); + + usageSrc = ColorUsage.valueOf((int)leis.readUInt()); + + + // A 32-bit unsigned integer that specifies the offset, in bytes, from the + // start of this record to the source bitmap header in the BitmapBuffer field. + final int offBmiSrc = (int)leis.readUInt(); + + // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap header. + final int cbBmiSrc = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the offset, in bytes, from the + // start of this record to the source bitmap bits in the BitmapBuffer field. + final int offBitsSrc = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap bits. + final int cbBitsSrc = (int)leis.readUInt(); + + // A 32-bit signed integer that specifies the logical width of the source rectangle. + // This value MUST be greater than zero. + final int cxSrc = leis.readInt(); + // A 32-bit signed integer that specifies the logical height of the source rectangle. + // This value MUST be greater than zero. + final int cySrc = leis.readInt(); + + srcRect.setRect(xSrc, ySrc, cxSrc, cySrc); + + size += 7 * LittleEndianConsts.INT_SIZE; + + size += readBitmap(leis, bitmap, startIdx, offBmiSrc, cbBmiSrc, offBitsSrc, cbBitsSrc); + + return size; + } + } + + /** + * The EMR_SETDIBITSTODEVICE record specifies a block transfer of pixels from specified scanlines of + * a source bitmap to a destination rectangle. + */ + public static class EmfSetDiBitsToDevice implements HemfRecord { + protected final Rectangle2D bounds = new Rectangle2D.Double(); + protected final Point2D dest = new Point2D.Double(); + protected final Rectangle2D src = new Rectangle2D.Double(); + protected ColorUsage usageSrc; + protected final HwmfBitmapDib bitmap = new HwmfBitmapDib(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setDiBitsToDevice; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + int startIdx = leis.getReadIndex(); + + // A WMF RectL object that defines the destination bounding rectangle in device units. + long size = readRectL(leis, bounds); + // the logical x/y-coordinate of the upper-left corner of the destination rectangle. + size += readPointL(leis, dest); + // the source rectangle + size += readBounds2(leis, src); + // A 32-bit unsigned integer that specifies the offset, in bytes, from the + // start of this record to the source bitmap header in the BitmapBuffer field. + final int offBmiSrc = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap header. + final int cbBmiSrc = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the offset, in bytes, from the + // start of this record to the source bitmap bits in the BitmapBuffer field. + final int offBitsSrc = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap bits. + final int cbBitsSrc = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies how to interpret values in the color table + // in the source bitmap header. This value MUST be in the DIBColors enumeration + usageSrc = ColorUsage.valueOf((int)leis.readUInt()); + // A 32-bit unsigned integer that specifies the first scan line in the array. + final int iStartScan = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the number of scan lines. + final int cScans = (int)leis.readUInt(); + size += 7*LittleEndianConsts.INT_SIZE; + + size += readBitmap(leis, bitmap, startIdx, offBmiSrc, cbBmiSrc, offBitsSrc, cbBitsSrc); + + return size; + } + + @Override + public String toString() { + return + "{ bounds: " + boundsToString(bounds) + + ", dest: " + pointToString(dest) + + ", src: " + boundsToString(src) + + ", usageSrc: '" + usageSrc + "'" + + ", bitmap: " + bitmap + + "}"; + } + } + + static long readBitmap(final LittleEndianInputStream leis, final HwmfBitmapDib bitmap, + final int startIdx, final int offBmi, final int cbBmi, final int offBits, int cbBits) + throws IOException { + if (offBmi == 0) { + return 0; + } + + final int offCurr = leis.getReadIndex()-(startIdx-HEADER_SIZE); + final int undefinedSpace1 = offBmi-offCurr; + if (undefinedSpace1 < 0) { + return 0; + } + + final int undefinedSpace2 = offBits-offCurr-cbBmi-undefinedSpace1; + assert(undefinedSpace2 >= 0); + + leis.skipFully(undefinedSpace1); + + if (cbBmi == 0 || cbBits == 0) { + return undefinedSpace1; + } + + final int dibSize = cbBmi+cbBits; + if (undefinedSpace2 == 0) { + return undefinedSpace1 + bitmap.init(leis, dibSize); + } + + final ByteArrayOutputStream bos = new ByteArrayOutputStream(cbBmi+cbBits); + final long cbBmiSrcAct = IOUtils.copy(leis, bos, cbBmi); + assert (cbBmiSrcAct == cbBmi); + leis.skipFully(undefinedSpace2); + final long cbBitsSrcAct = IOUtils.copy(leis, bos, cbBits); + assert (cbBitsSrcAct == cbBits); + + final LittleEndianInputStream leisDib = new LittleEndianInputStream(new ByteArrayInputStream(bos.toByteArray())); + final int dibSizeAct = bitmap.init(leisDib, dibSize); + assert (dibSizeAct <= dibSize); + return undefinedSpace1 + cbBmi + undefinedSpace2 + cbBits; + } + + + static long readRgnData(final LittleEndianInputStream leis, final List rgnRects) { + // *** RegionDataHeader *** + // A 32-bit unsigned integer that specifies the size of this object in bytes. This MUST be 0x00000020. + long rgnHdrSize = leis.readUInt(); + assert(rgnHdrSize == 0x20); + // A 32-bit unsigned integer that specifies the region type. This SHOULD be RDH_RECTANGLES (0x00000001) + long rgnHdrType = leis.readUInt(); + assert(rgnHdrType == 1); + // A 32-bit unsigned integer that specifies the number of rectangles in this region. + long rgnCntRect = leis.readUInt(); + // A 32-bit unsigned integer that specifies the size of the buffer of rectangles in bytes. + long rgnCntBytes = leis.readUInt(); + long size = 4*LittleEndianConsts.INT_SIZE; + // A 128-bit WMF RectL object, which specifies the bounds of the region. + Rectangle2D rgnBounds = new Rectangle2D.Double(); + size += readRectL(leis, rgnBounds); + for (int i=0; i = + + // m00 (scaleX) = eM11 (Horizontal scaling component) + double m00 = leis.readFloat(); + + // m01 (shearX) = eM12 (Horizontal proportionality constant) + double m01 = leis.readFloat(); + + // m10 (shearY) = eM21 (Vertical proportionality constant) + double m10 = leis.readFloat(); + + // m11 (scaleY) = eM22 (Vertical scaling component) + double m11 = leis.readFloat(); + + // m02 (translateX) = eDx (The horizontal translation component, in logical units.) + double m02 = leis.readFloat(); + + // m12 (translateY) = eDy (The vertical translation component, in logical units.) + double m12 = leis.readFloat(); + + xform.setTransform(m00, m10, m01, m11, m02, m12); + + return 6 * LittleEndian.INT_SIZE; + } + + protected static Shape getRgnShape(List rgnRects) { + if (rgnRects.size() == 1) { + return rgnRects.get(0); + } + final Area frame = new Area(); + rgnRects.forEach((rct) -> frame.add(new Area(rct))); + return frame; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFont.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFont.java new file mode 100644 index 0000000000..a7eb4c8b1c --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFont.java @@ -0,0 +1,496 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.apache.poi.common.usermodel.fonts.FontCharset; +import org.apache.poi.hwmf.record.HwmfFont; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; + +public class HemfFont extends HwmfFont { + private static final int LOGFONT_SIZE = 92; + private static final int LOGFONTPANOSE_SIZE = 320; + + protected interface LogFontDetails {} + + protected static class LogFontExDv implements LogFontDetails { + protected int[] designVector; + + @Override + public String toString() { + return "{ designVectorLen: " + (designVector == null ? 0 : designVector.length) + " }"; + } + } + + protected static class LogFontPanose implements LogFontDetails { + enum FamilyType { + PAN_ANY, + PAN_NO_FIT, + PAN_FAMILY_TEXT_DISPLAY, + PAN_FAMILY_SCRIPT, + PAN_FAMILY_DECORATIVE, + PAN_FAMILY_PICTORIAL + } + + enum SerifType { + PAN_ANY, + PAN_NO_FIT, + PAN_SERIF_COVE, + PAN_SERIF_OBTUSE_COVE, + PAN_SERIF_SQUARE_COVE, + PAN_SERIF_OBTUSE_SQUARE_COVE, + PAN_SERIF_SQUARE, + PAN_SERIF_THIN, + PAN_SERIF_BONE, + PAN_SERIF_EXAGGERATED, + PAN_SERIF_TRIANGLE, + PAN_SERIF_NORMAL_SANS, + PAN_SERIF_OBTUSE_SANS, + PAN_SERIF_PERP_SANS, + PAN_SERIF_FLARED, + PAN_SERIF_ROUNDED + } + + enum FontWeight { + PAN_ANY, + PAN_NO_FIT, + PAN_WEIGHT_VERY_LIGHT, + PAN_WEIGHT_LIGHT, + PAN_WEIGHT_THIN, + PAN_WEIGHT_BOOK, + PAN_WEIGHT_MEDIUM, + PAN_WEIGHT_DEMI, + PAN_WEIGHT_BOLD, + PAN_WEIGHT_HEAVY, + PAN_WEIGHT_BLACK, + PAN_WEIGHT_NORD + } + + enum Proportion { + PAN_ANY, + PAN_NO_FIT, + PAN_PROP_OLD_STYLE, + PAN_PROP_MODERN, + PAN_PROP_EVEN_WIDTH, + PAN_PROP_EXPANDED, + PAN_PROP_CONDENSED, + PAN_PROP_VERY_EXPANDED, + PAN_PROP_VERY_CONDENSED, + PAN_PROP_MONOSPACED + } + + enum Contrast { + PAN_ANY, + PAN_NO_FIT, + PAN_CONTRAST_NONE, + PAN_CONTRAST_VERY_LOW, + PAN_CONTRAST_LOW, + PAN_CONTRAST_MEDIUM_LOW, + PAN_CONTRAST_MEDIUM, + PAN_CONTRAST_MEDIUM_HIGH, + PAN_CONTRAST_HIGH, + PAN_CONTRAST_VERY_HIGH + } + + enum StrokeVariation { + PAN_ANY, + PAN_NO_FIT, + PAN_STROKE_GRADUAL_DIAG, + PAN_STROKE_GRADUAL_TRAN, + PAN_STROKE_GRADUAL_VERT, + PAN_STROKE_GRADUAL_HORZ, + PAN_STROKE_RAPID_VERT, + PAN_STROKE_RAPID_HORZ, + PAN_STROKE_INSTANT_VERT + } + + enum ArmStyle { + PAN_ANY, + PAN_NO_FIT, + PAN_STRAIGHT_ARMS_HORZ, + PAN_STRAIGHT_ARMS_WEDGE, + PAN_STRAIGHT_ARMS_VERT, + PAN_STRAIGHT_ARMS_SINGLE_SERIF, + PAN_STRAIGHT_ARMS_DOUBLE_SERIF, + PAN_BENT_ARMS_HORZ, + PAN_BENT_ARMS_WEDGE, + PAN_BENT_ARMS_VERT, + PAN_BENT_ARMS_SINGLE_SERIF, + PAN_BENT_ARMS_DOUBLE_SERIF + } + + enum Letterform { + PAN_ANY, + PAN_NO_FIT, + PAN_LETT_NORMAL_CONTACT, + PAN_LETT_NORMAL_WEIGHTED, + PAN_LETT_NORMAL_BOXED, + PAN_LETT_NORMAL_FLATTENED, + PAN_LETT_NORMAL_ROUNDED, + PAN_LETT_NORMAL_OFF_CENTER, + PAN_LETT_NORMAL_SQUARE, + PAN_LETT_OBLIQUE_CONTACT, + PAN_LETT_OBLIQUE_WEIGHTED, + PAN_LETT_OBLIQUE_BOXED, + PAN_LETT_OBLIQUE_FLATTENED, + PAN_LETT_OBLIQUE_ROUNDED, + PAN_LETT_OBLIQUE_OFF_CENTER, + PAN_LETT_OBLIQUE_SQUARE + } + + enum MidLine { + PAN_ANY, + PAN_NO_FIT, + PAN_MIDLINE_STANDARD_TRIMMED, + PAN_MIDLINE_STANDARD_POINTED, + PAN_MIDLINE_STANDARD_SERIFED, + PAN_MIDLINE_HIGH_TRIMMED, + PAN_MIDLINE_HIGH_POINTED, + PAN_MIDLINE_HIGH_SERIFED, + PAN_MIDLINE_CONSTANT_TRIMMED, + PAN_MIDLINE_CONSTANT_POINTED, + PAN_MIDLINE_CONSTANT_SERIFED, + PAN_MIDLINE_LOW_TRIMMED, + PAN_MIDLINE_LOW_POINTED, + PAN_MIDLINE_LOW_SERIFED + } + + enum XHeight { + PAN_ANY, + PAN_NO_FIT, + PAN_XHEIGHT_CONSTANT_SMALL, + PAN_XHEIGHT_CONSTANT_STD, + PAN_XHEIGHT_CONSTANT_LARGE, + PAN_XHEIGHT_DUCKING_SMALL, + PAN_XHEIGHT_DUCKING_STD, + PAN_XHEIGHT_DUCKING_LARGE + } + + protected int styleSize; + protected int vendorId; + protected int culture; + protected FamilyType familyType; + protected SerifType serifStyle; + protected FontWeight weight; + protected Proportion proportion; + protected Contrast contrast; + protected StrokeVariation strokeVariation; + protected ArmStyle armStyle; + protected Letterform letterform; + protected MidLine midLine; + protected XHeight xHeight; + + @Override + public String toString() { + return + "{ styleSize: " + styleSize + + ", vendorId: " + vendorId + + ", culture: " + culture + + ", familyType: '" + familyType + "'" + + ", serifStyle: '" + serifStyle + "'" + + ", weight: '" + weight + "'" + + ", proportion: '" + proportion + "'" + + ", contrast: '" + contrast + "'" + + ", strokeVariation: '" + strokeVariation + "'" + + ", armStyle: '" + armStyle + "'" + + ", letterform: '" + letterform + "'" + + ", midLine: '" + midLine + "'" + + ", xHeight: '" + xHeight + "'" + + "}"; + } + } + + protected String fullname; + protected String style; + protected String script; + + protected LogFontDetails details; + + @Override + public int init(LittleEndianInputStream leis, long recordSize) throws IOException { + // A 32-bit signed integer that specifies the height, in logical units, of the font's + // character cell or character. The character height value, also known as the em size, is the + // character cell height value minus the internal leading value. The font mapper SHOULD + // interpret the value specified in the Height field in the following manner. + // + // 0x00000000 < value: + // The font mapper transforms this value into device units and matches it against + // the cell height of the available fonts. + // + // 0x00000000 + // The font mapper uses a default height value when it searches for a match. + // + // value < 0x00000000: + // The font mapper transforms this value into device units and matches its + // absolute value against the character height of the available fonts. + // + // For all height comparisons, the font mapper SHOULD look for the largest font that does not + // exceed the requested size. + height = leis.readInt(); + + // A 32-bit signed integer that specifies the average width, in logical units, of + // characters in the font. If the Width field value is zero, an appropriate value SHOULD be + // calculated from other LogFont values to find a font that has the typographer's intended + // aspect ratio. + width = leis.readInt(); + + // A 32-bit signed integer that specifies the angle, in tenths of degrees, + // between the escapement vector and the x-axis of the device. The escapement vector is + // parallel to the baseline of a row of text. + // + // When the graphics mode is set to GM_ADVANCED, the escapement angle of the string can + // be specified independently of the orientation angle of the string's characters. + escapement = leis.readInt(); + + // A 32-bit signed integer that specifies the angle, in tenths of degrees, + // between each character's baseline and the x-axis of the device. + orientation = leis.readInt(); + + // A 32-bit signed integer that specifies the weight of the font in the range zero through 1000. + // For example, 400 is normal and 700 is bold. If this value is zero, a default weight can be used. + weight = leis.readInt(); + + // An 8-bit unsigned integer that specifies an italic font if set to 0x01; + // otherwise, it MUST be set to 0x00. + italic = (leis.readUByte() != 0x00); + + // An 8-bit unsigned integer that specifies an underlined font if set to 0x01; + // otherwise, it MUST be set to 0x00. + underline = (leis.readUByte() != 0x00); + + // An 8-bit unsigned integer that specifies a strikeout font if set to 0x01; + // otherwise, it MUST be set to 0x00. + strikeOut = (leis.readUByte() != 0x00); + + // An 8-bit unsigned integer that specifies the set of character glyphs. + // It MUST be a value in the WMF CharacterSet enumeration. + // If the character set is unknown, metafile processing SHOULD NOT attempt + // to translate or interpret strings that are rendered with that font. + // If a typeface name is specified in the Facename field, the CharSet field + // value MUST match the character set of that typeface. + charSet = FontCharset.valueOf(leis.readUByte()); + + // An 8-bit unsigned integer that specifies the output precision. + // The output precision defines how closely the font is required to match the requested height, width, + // character orientation, escapement, pitch, and font type. + // It MUST be a value from the WMF OutPrecision enumeration. + // Applications can use the output precision to control how the font mapper chooses a font when the + // operating system contains more than one font with a specified name. For example, if an operating + // system contains a font named Symbol in rasterized and TrueType forms, an output precision value + // of OUT_TT_PRECIS forces the font mapper to choose the TrueType version. + // A value of OUT_TT_ONLY_PRECIS forces the font mapper to choose a TrueType font, even if it is + // necessary to substitute a TrueType font with another name. + outPrecision = WmfOutPrecision.valueOf(leis.readUByte()); + + // An 8-bit unsigned integer that specifies the clipping precision. + // The clipping precision defines how to clip characters that are partially outside the clipping region. + // It can be one or more of the WMF ClipPrecision Flags + clipPrecision.init(leis); + + // An 8-bit unsigned integer that specifies the output quality. The output quality defines how closely + // to attempt to match the logical-font attributes to those of an actual physical font. + // It MUST be one of the values in the WMF FontQuality enumeration + quality = WmfFontQuality.valueOf(leis.readUByte()); + + // A WMF PitchAndFamily object that specifies the pitch and family of the font. + // Font families describe the look of a font in a general way. + // They are intended for specifying a font when the specified typeface is not available. + pitchAndFamily = leis.readUByte(); + + int size = 5* LittleEndianConsts.INT_SIZE+8*LittleEndianConsts.BYTE_SIZE; + + StringBuilder sb = new StringBuilder(); + + // A string of no more than 32 Unicode characters that specifies the typeface name of the font. + // If the length of this string is less than 32 characters, a terminating NULL MUST be present, + // after which the remainder of this field MUST be ignored. + int readBytes = readString(leis, sb, 32); + if (readBytes == -1) { + throw new IOException("Font facename can't be determined."); + } + facename = sb.toString(); + size += readBytes; + + if (recordSize <= LOGFONT_SIZE) { + return size; + } + + // A string of 64 Unicode characters that contains the font's full name. + // Ifthe length of this string is less than 64 characters, a terminating + // NULL MUST be present, after which the remainder of this field MUST be ignored. + readBytes = readString(leis, sb, 64); + if (readBytes == -1) { + throw new IOException("Font fullname can't be determined."); + } + fullname = sb.toString(); + size += readBytes; + + // A string of 32 Unicode characters that defines the font's style. If the length of + // this string is less than 32 characters, a terminating NULL MUST be present, + // after which the remainder of this field MUST be ignored. + readBytes = readString(leis, sb, 32); + if (readBytes == -1) { + throw new IOException("Font style can't be determined."); + } + style = sb.toString(); + size += readBytes; + + if (recordSize == LOGFONTPANOSE_SIZE) { + // LogFontPanose Object + + LogFontPanose logPan = new LogFontPanose(); + details = logPan; + + int version = leis.readInt(); + + // A 32-bit unsigned integer that specifies the point size at which font + //hinting is performed. If set to zero, font hinting is performed at the point size corresponding + //to the Height field in the LogFont object in the LogFont field. + logPan.styleSize = (int)leis.readUInt(); + + int match = leis.readInt(); + + int reserved = leis.readInt(); + + logPan.vendorId = leis.readInt(); + + logPan.culture = leis.readInt(); + + // An 8-bit unsigned integer that specifies the family type. + // The value MUST be in the FamilyType enumeration table. + logPan.familyType = LogFontPanose.FamilyType.values()[leis.readUByte()]; + + // An 8-bit unsigned integer that specifies the serif style. + // The value MUST be in the SerifType enumeration table. + logPan.serifStyle = LogFontPanose.SerifType.values()[leis.readUByte()]; + + // An 8-bit unsigned integer that specifies the weight of the font. + // The value MUST be in the Weight enumeration table. + logPan.weight = LogFontPanose.FontWeight.values()[leis.readUByte()]; + + // An 8-bit unsigned integer that specifies the proportion of the font. + // The value MUST be in the Proportion enumeration table. + logPan.proportion = LogFontPanose.Proportion.values()[leis.readUByte()]; + + // An 8-bit unsigned integer that specifies the proportion of the font. + // The value MUST be in the Proportion enumeration table. + logPan.contrast = LogFontPanose.Contrast.values()[leis.readUByte()]; + + // An 8-bit unsigned integer that specifies the stroke variation for the font. + // The value MUST be in the StrokeVariation enumeration table. + logPan.strokeVariation = LogFontPanose.StrokeVariation.values()[leis.readUByte()]; + + // An 8-bit unsigned integer that specifies the arm style of the font. + // The value MUST be in the ArmStyle enumeration table. + logPan.armStyle = LogFontPanose.ArmStyle.values()[leis.readUByte()]; + + // An 8-bit unsigned integer that specifies the letterform of the font. + // The value MUST be in the Letterform enumeration table. + logPan.letterform = LogFontPanose.Letterform.values()[leis.readUByte()]; + + // An 8-bit unsigned integer that specifies the midline of the font. + // The value MUST be in the MidLine enumeration table. + logPan.midLine = LogFontPanose.MidLine.values()[leis.readUByte()]; + + // An 8-bit unsigned integer that specifies the x height of the font. + // The value MUST be in the XHeight enumeration table. + logPan.xHeight = LogFontPanose.XHeight.values()[leis.readUByte()]; + + // skip 2 byte to ensure 32-bit alignment of this structure. + leis.skip(2); + + size += 6*LittleEndianConsts.INT_SIZE+10* LittleEndianConsts.BYTE_SIZE+2; + } else { + // LogFontExDv Object + + LogFontExDv logEx = new LogFontExDv(); + details = logEx; + + // A string of 32 Unicode characters that defines the character set of the font. + // If the length of this string is less than 32 characters, a terminating NULL MUST be present, + // after which the remainder of this field MUST be ignored. + readBytes = readString(leis, sb, 32); + if (readBytes == -1) { + throw new IOException("Font script can't be determined."); + } + script = sb.toString(); + size += readBytes; + + // Design Vector + + // A 32-bit unsigned integer that MUST be set to the value 0x08007664. + int signature = leis.readInt(); + // some non-conformant applications don't write the magic code in + // assert (signature == 0x08007664); + + // A 32-bit unsigned integer that specifies the number of elements in the + // Values array. It MUST be in the range 0 to 16, inclusive. + int numAxes = leis.readInt(); + assert (0 <= numAxes && numAxes <= 16); + + // An optional array of 32-bit signed integers that specify the values of the font axes of a + // multiple master, OpenType font. The maximum number of values in the array is 16. + if (numAxes > 0) { + logEx.designVector = new int[numAxes]; + for (int i=0; i 0 && offDescription > 0) { + int skip = (int)(offDescription - (size + HEADER_SIZE)); + leis.mark(skip+nDescription*2); + leis.skipFully(skip); + byte[] buf = new byte[(nDescription-1)*2]; + leis.readFully(buf); + description = new String(buf, StandardCharsets.UTF_16LE).replace((char)0, ' ').trim(); + leis.reset(); + } + + if (size+12 <= recordSize) { + hasExtension1 = true; + cbPixelFormat = leis.readUInt(); + offPixelFormat = leis.readUInt(); + bOpenGL = leis.readUInt(); + size += 3*LittleEndianConsts.INT_SIZE; + } + + if (size+8 <= recordSize) { + hasExtension2 = true; + size += readDimensionInt(leis, microDimension); + } + + return size; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java new file mode 100644 index 0000000000..5e04e932d6 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java @@ -0,0 +1,828 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL; +import static org.apache.poi.hemf.record.emf.HemfFill.readBitmap; +import static org.apache.poi.hemf.record.emf.HemfFill.readXForm; +import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE; + +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import org.apache.poi.hemf.draw.HemfDrawProperties; +import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hwmf.draw.HwmfDrawProperties; +import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.hwmf.record.HwmfBinaryRasterOp; +import org.apache.poi.hwmf.record.HwmfBitmapDib; +import org.apache.poi.hwmf.record.HwmfBrushStyle; +import org.apache.poi.hwmf.record.HwmfColorRef; +import org.apache.poi.hwmf.record.HwmfFill; +import org.apache.poi.hwmf.record.HwmfHatchStyle; +import org.apache.poi.hwmf.record.HwmfMapMode; +import org.apache.poi.hwmf.record.HwmfMisc; +import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode; +import org.apache.poi.hwmf.record.HwmfObjectTableEntry; +import org.apache.poi.hwmf.record.HwmfPalette.PaletteEntry; +import org.apache.poi.hwmf.record.HwmfPenStyle; +import org.apache.poi.hwmf.record.HwmfPenStyle.HwmfLineDash; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; + +public class HemfMisc { + + public enum HemfModifyWorldTransformMode { + /** + * Reset the current transform using the identity matrix. + * In this mode, the specified transform data is ignored. + */ + MWT_IDENTITY(1), + /** + * Multiply the current transform. In this mode, the specified transform data is the left multiplicand, + * and the transform that is currently defined in the playback device context is the right multiplicand. + */ + MWT_LEFTMULTIPLY(2), + /** + * Multiply the current transform. In this mode, the specified transform data is the right multiplicand, + * and the transform that is currently defined in the playback device context is the left multiplicand. + */ + MWT_RIGHTMULTIPLY(3), + /** + * Perform the function of an EMR_SETWORLDTRANSFORM record + */ + MWT_SET(4) + ; + + public final int id; + + HemfModifyWorldTransformMode(int id) { + this.id = id; + } + + public static HemfModifyWorldTransformMode valueOf(int id) { + for (HemfModifyWorldTransformMode wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + } + + + public static class EmfEof implements HemfRecord { + protected final List palette = new ArrayList<>(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.eof; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + final int startIdx = leis.getReadIndex(); + + // A 32-bit unsigned integer that specifies the number of palette entries. + final int nPalEntries = (int) leis.readUInt(); + // A 32-bit unsigned integer that specifies the offset to the palette entries from the start of this record. + final int offPalEntries = (int) leis.readUInt(); + + int size = 2 * LittleEndianConsts.INT_SIZE; + + if (nPalEntries > 0 && offPalEntries > 0) { + int undefinedSpace1 = (int) (offPalEntries - (size + HEADER_SIZE)); + assert (undefinedSpace1 >= 0); + leis.skipFully(undefinedSpace1); + size += undefinedSpace1; + + for (int i = 0; i < nPalEntries; i++) { + PaletteEntry pe = new PaletteEntry(); + size += pe.init(leis); + } + + int undefinedSpace2 = (int) (recordSize - size - LittleEndianConsts.INT_SIZE); + assert (undefinedSpace2 >= 0); + leis.skipFully(undefinedSpace2); + size += undefinedSpace2; + } + + // A 32-bit unsigned integer that MUST be the same as Size and MUST be the + // last field of the record and hence the metafile. + // LogPaletteEntry objects, if they exist, MUST precede this field. + long sizeLast = leis.readUInt(); + size += LittleEndianConsts.INT_SIZE; + // some files store the whole file size in sizeLast, other just the last record size + // assert (sizeLast == size+HEADER_SIZE); + assert (recordSize == size); + + return size; + } + } + + /** + * The EMF_SAVEDC record saves the playback device context for later retrieval. + */ + public static class EmfSaveDc extends HwmfMisc.WmfSaveDc implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.saveDc; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return 0; + } + } + + /** + * The EMF_RESTOREDC record restores the playback device context from a previously saved device + * context. + */ + public static class EmfRestoreDc extends HwmfMisc.WmfRestoreDc implements HemfRecord { + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.restoreDc; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit signed integer that specifies the saved state to restore relative to + // the current state. This value MUST be negative; –1 represents the state that was most + // recently saved on the stack, –2 the one before that, etc. + nSavedDC = leis.readInt(); + return LittleEndianConsts.INT_SIZE; + } + } + + /** + * The META_SETBKCOLOR record sets the background color in the playback device context to a + * specified color, or to the nearest physical color if the device cannot represent the specified color. + */ + public static class EmfSetBkColor extends HwmfMisc.WmfSetBkColor implements HemfRecord { + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setBkColor; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return colorRef.init(leis); + } + } + + + /** + * The EMR_SETBKMODE record specifies the background mix mode of the playback device context. + * The background mix mode is used with text, hatched brushes, and pen styles that are not solid + * lines. + */ + public static class EmfSetBkMode extends WmfSetBkMode implements HemfRecord { + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setBkMode; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + /* + * A 32-bit unsigned integer that specifies the background mode + * and MUST be in the BackgroundMode (section 2.1.4) enumeration + */ + bkMode = HwmfBkMode.valueOf((int) leis.readUInt()); + return LittleEndianConsts.INT_SIZE; + } + } + + /** + * The EMR_SETMAPPERFLAGS record specifies parameters of the process of matching logical fonts to + * physical fonts, which is performed by the font mapper. + */ + public static class EmfSetMapperFlags extends HwmfMisc.WmfSetMapperFlags implements HemfRecord { + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setMapperFlags; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return super.init(leis, recordSize, (int) recordId); + } + } + + /** + * The EMR_SETMAPMODE record specifies the mapping mode of the playback device context. The + * mapping mode specifies the unit of measure used to transform page space units into device space + * units, and also specifies the orientation of the device's x-axis and y-axis. + */ + public static class EmfSetMapMode extends HwmfMisc.WmfSetMapMode implements HemfRecord { + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setMapMode; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer whose definition MUST be in the MapMode enumeration + mapMode = HwmfMapMode.valueOf((int) leis.readUInt()); + return LittleEndianConsts.INT_SIZE; + } + } + + /** + * The EMR_SETROP2 record defines a binary raster operation mode. + */ + public static class EmfSetRop2 extends HwmfMisc.WmfSetRop2 implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setRop2; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer that specifies the raster operation mode and + // MUST be in the WMF Binary Raster Op enumeration + drawMode = HwmfBinaryRasterOp.valueOf((int) leis.readUInt()); + return LittleEndianConsts.INT_SIZE; + } + } + + + /** + * The EMR_SETSTRETCHBLTMODE record specifies bitmap stretch mode. + */ + public static class EmfSetStretchBltMode extends HwmfMisc.WmfSetStretchBltMode implements HemfRecord { + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setStretchBltMode; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer that specifies the stretch mode and MAY be + // in the StretchMode enumeration. + stretchBltMode = StretchBltMode.valueOf((int) leis.readUInt()); + return LittleEndianConsts.INT_SIZE; + } + } + + /** + * The EMR_CREATEBRUSHINDIRECT record defines a logical brush for graphics operations. + */ + public static class EmfCreateBrushIndirect extends HwmfMisc.WmfCreateBrushIndirect implements HemfRecord { + /** + * A 32-bit unsigned integer that specifies the index of the logical brush object in the + * EMF Object Table. This index MUST be saved so that this object can be reused or modified. + */ + private int brushIdx; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.createBrushIndirect; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + brushIdx = (int) leis.readUInt(); + + brushStyle = HwmfBrushStyle.valueOf((int) leis.readUInt()); + colorRef = new HwmfColorRef(); + int size = colorRef.init(leis); + brushHatch = HwmfHatchStyle.valueOf((int) leis.readUInt()); + return size + 3 * LittleEndianConsts.INT_SIZE; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.addObjectTableEntry(this, brushIdx); + } + + + @Override + public String toString() { + return + "{ brushIndex: "+brushIdx+ + ", brushStyle: '"+brushStyle+"'"+ + ", colorRef: "+colorRef+ + ", brushHatch: '"+brushHatch+"' }"; + } + } + + /** + * The EMR_CREATEDIBPATTERNBRUSHPT record defines a pattern brush for graphics operations. + * The pattern is specified by a DIB. + */ + public static class EmfCreateDibPatternBrushPt extends HwmfMisc.WmfDibCreatePatternBrush implements HemfRecord { + protected int brushIdx; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.createDibPatternBrushPt; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + final int startIdx = leis.getReadIndex(); + + style = HwmfBrushStyle.BS_DIBPATTERNPT; + + // A 32-bit unsigned integer that specifies the index of the pattern brush + // object in the EMF Object Table + brushIdx = (int)leis.readUInt(); + + // A 32-bit unsigned integer that specifies how to interpret values in the color + // table in the DIB header. This value MUST be in the DIBColors enumeration + colorUsage = HwmfFill.ColorUsage.valueOf((int)leis.readUInt()); + + // A 32-bit unsigned integer that specifies the offset from the start of this + // record to the DIB header. + final int offBmi = leis.readInt(); + + // A 32-bit unsigned integer that specifies the size of the DIB header. + final int cbBmi = leis.readInt(); + + // A 32-bit unsigned integer that specifies the offset from the start of this record to the DIB bits. + final int offBits = leis.readInt(); + + // A 32-bit unsigned integer that specifies the size of the DIB bits. + final int cbBits = leis.readInt(); + + int size = 6*LittleEndianConsts.INT_SIZE; + + patternDib = new HwmfBitmapDib(); + size += readBitmap(leis, patternDib, startIdx, offBmi, cbBmi, offBits, cbBits); + return size; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.addObjectTableEntry(this, brushIdx); + } + + } + + /** + * The EMR_DELETEOBJECT record deletes a graphics object, which is specified by its index + * in the EMF Object Table + */ + public static class EmfDeleteObject extends HwmfMisc.WmfDeleteObject implements HemfRecord { + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.deleteobject; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + objectIndex = (int) leis.readUInt(); + return LittleEndianConsts.INT_SIZE; + } + } + + /** + * The EMR_CREATEPEN record defines a logical pen for graphics operations. + */ + public static class EmfCreatePen extends HwmfMisc.WmfCreatePenIndirect implements HemfRecord { + /** + * A 32-bit unsigned integer that specifies the index of the logical palette object + * in the EMF Object Table. This index MUST be saved so that this object can be + * reused or modified. + */ + protected int penIndex; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.createPen; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + penIndex = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies the PenStyle. + // The value MUST be defined from the PenStyle enumeration table + penStyle = HwmfPenStyle.valueOf((int) leis.readUInt()); + + int widthX = leis.readInt(); + int widthY = leis.readInt(); + dimension.setSize(widthX, widthY); + + int size = colorRef.init(leis); + + return size + 4 * LittleEndianConsts.INT_SIZE; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.addObjectTableEntry(this, penIndex); + } + + @Override + public String toString() { + return super.toString().replaceFirst("\\{", "{ penIndex: "+penIndex+", "); + } + } + + public static class EmfExtCreatePen extends EmfCreatePen { + protected HwmfBrushStyle brushStyle; + protected HwmfHatchStyle hatchStyle; + + protected final HwmfBitmapDib bitmap = new HwmfBitmapDib(); + + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.extCreatePen; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + final int startIdx = leis.getReadIndex(); + + penIndex = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies the offset from the start of this + // record to the DIB header, if the record contains a DIB. + int offBmi = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies the size of the DIB header, if the + // record contains a DIB. + int cbBmi = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies the offset from the start of this + // record to the DIB bits, if the record contains a DIB. + int offBits = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies the size of the DIB bits, if the record + // contains a DIB. + int cbBits = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies the PenStyle. + // The value MUST be defined from the PenStyle enumeration table + final HemfPenStyle emfPS = HemfPenStyle.valueOf((int) leis.readUInt()); + penStyle = emfPS; + + // A 32-bit unsigned integer that specifies the width of the line drawn by the pen. + // If the pen type in the PenStyle field is PS_GEOMETRIC, this value is the width in logical + // units; otherwise, the width is specified in device units. If the pen type in the PenStyle field is + // PS_COSMETIC, this value MUST be 0x00000001. + long width = leis.readUInt(); + dimension.setSize(width, 0); + int size = 7 * LittleEndianConsts.INT_SIZE; + + // A 32-bit unsigned integer that specifies a brush style for the pen from the WMF BrushStyle enumeration + // + // If the pen type in the PenStyle field is PS_GEOMETRIC, this value MUST be either BS_SOLID or BS_HATCHED. + // The value of this field can be BS_NULL, but only if the line style specified in PenStyle is PS_NULL. + // The BS_NULL style SHOULD be used to specify a brush that has no effect + brushStyle = HwmfBrushStyle.valueOf((int) leis.readUInt()); + + size += LittleEndianConsts.INT_SIZE; + + size += colorRef.init(leis); + + hatchStyle = HwmfHatchStyle.valueOf(leis.readInt()); + size += LittleEndianConsts.INT_SIZE; + + // The number of elements in the array specified in the StyleEntry + // field. This value SHOULD be zero if PenStyle does not specify PS_USERSTYLE. + final int numStyleEntries = (int) leis.readUInt(); + size += LittleEndianConsts.INT_SIZE; + + assert (numStyleEntries == 0 || penStyle.getLineDash() == HwmfLineDash.USERSTYLE); + + // An optional array of 32-bit unsigned integers that defines the lengths of + // dashes and gaps in the line drawn by this pen, when the value of PenStyle is + // PS_USERSTYLE line style for the pen. The array contains a number of entries specified by + // NumStyleEntries, but it is used as if it repeated indefinitely. + // The first entry in the array specifies the length of the first dash. The second entry specifies + // the length of the first gap. Thereafter, lengths of dashes and gaps alternate. + // If the pen type in the PenStyle field is PS_GEOMETRIC, the lengths are specified in logical + // units; otherwise, the lengths are specified in device units. + + float[] dashPattern = new float[numStyleEntries]; + + for (int i = 0; i < numStyleEntries; i++) { + dashPattern[i] = (int) leis.readUInt(); + } + + if (penStyle.getLineDash() == HwmfLineDash.USERSTYLE) { + emfPS.setLineDashes(dashPattern); + } + + size += numStyleEntries * LittleEndianConsts.INT_SIZE; + + size += readBitmap(leis, bitmap, startIdx, offBmi, cbBmi, offBits, cbBits); + + return size; + } + + @Override + public String toString() { + // TODO: add style entries + bmp + return + "{ brushStyle: '"+brushStyle+"'"+ + ", hatchStyle: '"+hatchStyle+"'"+ + ", dashPattern: "+ Arrays.toString(penStyle.getLineDashes())+ + ", "+super.toString().substring(1); + } + } + + /** + * The EMR_SETMITERLIMIT record specifies the limit for the length of miter joins for the playback + * device context. + */ + public static class EmfSetMiterLimit implements HemfRecord { + protected int miterLimit; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setMiterLimit; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + miterLimit = (int) leis.readUInt(); + return LittleEndianConsts.INT_SIZE; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.getProperties().setPenMiterLimit(miterLimit); + } + + @Override + public String toString() { + return "{ miterLimit: "+miterLimit+" }"; + } + } + + + public static class EmfSetBrushOrgEx implements HemfRecord { + protected final Point2D origin = new Point2D.Double(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setBrushOrgEx; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readPointL(leis, origin); + } + + @Override + public String toString() { + return "{ x: "+origin.getX()+", y: "+origin.getY()+" }"; + } + } + + public static class EmfSetWorldTransform implements HemfRecord { + protected final AffineTransform xForm = new AffineTransform(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setWorldTransform; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readXForm(leis, xForm); + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.updateWindowMapMode(); + AffineTransform tx = ctx.getTransform(); + tx.concatenate(xForm); + ctx.setTransform(tx); + } + + @Override + public String toString() { + return + "{ xForm: " + + "{ scaleX: "+xForm.getScaleX()+ + ", shearX: "+xForm.getShearX()+ + ", transX: "+xForm.getTranslateX()+ + ", scaleY: "+xForm.getScaleY()+ + ", shearY: "+xForm.getShearY()+ + ", transY: "+xForm.getTranslateY()+" } }"; + } + } + + public static class EmfModifyWorldTransform implements HemfRecord { + protected final AffineTransform xForm = new AffineTransform(); + protected HemfModifyWorldTransformMode modifyWorldTransformMode; + protected HemfHeader header; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.modifyWorldTransform; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // An XForm object that defines a two-dimensional linear transform in logical units. + // This transform is used according to the ModifyWorldTransformMode to define a new value for + // the world-space to page-space transform in the playback device context. + int size = readXForm(leis, xForm); + + // A 32-bit unsigned integer that specifies how the transform specified in Xform is used. + // This value MUST be in the ModifyWorldTransformMode enumeration + modifyWorldTransformMode = HemfModifyWorldTransformMode.valueOf((int)leis.readUInt()); + + return size + LittleEndianConsts.INT_SIZE; + } + + @Override + public void setHeader(HemfHeader header) { + this.header = header; + } + + @Override + public void draw(HemfGraphics ctx) { + if (modifyWorldTransformMode == null) { + return; + } + + final HemfDrawProperties prop = ctx.getProperties(); + + final AffineTransform tx; + switch (modifyWorldTransformMode) { + case MWT_LEFTMULTIPLY: + + AffineTransform wsTrans; + final Rectangle2D win = prop.getWindow(); + boolean noSetWindowExYet = win.getWidth() == 1 && win.getHeight() == 1; + if (noSetWindowExYet) { + // TODO: understand world-space transformation [MSDN-WRLDPGSPC] + // experimental and horrible solved, because the world-space transformation behind it + // is not understood :( + // only found one example which had landscape bounds and transform of 90 degress + + try { + wsTrans = xForm.createInverse(); + } catch (NoninvertibleTransformException e) { + wsTrans = new AffineTransform(); + } + + Rectangle2D emfBounds = header.getBoundsRectangle(); + + if (xForm.getShearX() == -1.0 && xForm.getShearY() == 1.0) { + // rotate 90 deg + wsTrans.translate(-emfBounds.getHeight(), emfBounds.getHeight()); + } + } else { + wsTrans = adaptXForm(ctx.getTransform()); + } + + tx = ctx.getTransform(); + tx.concatenate(wsTrans); + break; + case MWT_RIGHTMULTIPLY: + tx = ctx.getTransform(); + tx.preConcatenate(adaptXForm(tx)); + break; + case MWT_IDENTITY: + ctx.updateWindowMapMode(); + tx = ctx.getTransform(); + break; + default: + case MWT_SET: + ctx.updateWindowMapMode(); + tx = ctx.getTransform(); + tx.concatenate(adaptXForm(tx)); + break; + } + ctx.setTransform(tx); + } + + /** + * adapt xform depending on the base transformation (... experimental ...) + */ + private AffineTransform adaptXForm(AffineTransform other) { + // normalize signed zero + Function nn = (d) -> (d == 0. ? 0. : d); + double yDiff = Math.signum(nn.apply(xForm.getTranslateY())) == Math.signum(nn.apply(other.getTranslateY())) ? 1. : -1.; + double xDiff = Math.signum(nn.apply(xForm.getTranslateX())) == Math.signum(nn.apply(other.getTranslateX())) ? 1. : -1.; + return new AffineTransform( + xForm.getScaleX() == 0 ? 1. : xForm.getScaleX(), + yDiff * xForm.getShearY(), + xDiff * xForm.getShearX(), + xForm.getScaleY() == 0. ? 1. : xForm.getScaleY(), + xForm.getTranslateX(), + xForm.getTranslateY() + ); + } + + @Override + public String toString() { + return + "{ xForm: " + + "{ scaleX: "+xForm.getScaleX()+ + ", shearX: "+xForm.getShearX()+ + ", transX: "+xForm.getTranslateX()+ + ", scaleY: "+xForm.getScaleY()+ + ", shearY: "+xForm.getShearY()+ + ", transY: "+xForm.getTranslateY()+" }"+ + ", modifyWorldTransformMode: '"+modifyWorldTransformMode+"' }"; + } + } + + public static class EmfCreateMonoBrush implements HemfRecord, HwmfObjectTableEntry { + /** + * A 32-bit unsigned integer that specifies the index of the logical palette object + * in the EMF Object Table. This index MUST be saved so that this object can be + * reused or modified. + */ + protected int penIndex; + + protected HwmfFill.ColorUsage colorUsage; + protected final HwmfBitmapDib bitmap = new HwmfBitmapDib(); + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.createMonoBrush; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + final int startIdx = leis.getReadIndex(); + + penIndex = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies how to interpret values in the color + // table in the DIB header. This value MUST be in the DIBColors enumeration + colorUsage = HwmfFill.ColorUsage.valueOf((int) leis.readUInt()); + + // A 32-bit unsigned integer that specifies the offset from the start of this + // record to the DIB header, if the record contains a DIB. + int offBmi = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies the size of the DIB header, if the + // record contains a DIB. + int cbBmi = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies the offset from the start of this + // record to the DIB bits, if the record contains a DIB. + int offBits = (int) leis.readUInt(); + + // A 32-bit unsigned integer that specifies the size of the DIB bits, if the record + // contains a DIB. + int cbBits = (int) leis.readUInt(); + + int size = 6 * LittleEndianConsts.INT_SIZE; + + size += readBitmap(leis, bitmap, startIdx, offBmi, cbBmi, offBits, cbBits); + + return size; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.addObjectTableEntry(this, penIndex); + } + + @Override + public void applyObject(HwmfGraphics ctx) { + if (!bitmap.isValid()) { + return; + } + HwmfDrawProperties props = ctx.getProperties(); + props.setBrushStyle(HwmfBrushStyle.BS_PATTERN); + BufferedImage bmp = bitmap.getImage(); + props.setBrushBitmap(bmp); + } + + @Override + public String toString() { + return + "{ penIndex: " + penIndex + + ", colorUsage: " + colorUsage + + ", bitmap: " + bitmap + + "}"; + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPalette.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPalette.java new file mode 100644 index 0000000000..9811cb2c3d --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPalette.java @@ -0,0 +1,154 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import java.io.IOException; + +import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hwmf.record.HwmfPalette; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; + +public class HemfPalette { + /** The EMR_SELECTPALETTE record specifies a logical palette for the playback device context. */ + public static class EmfSelectPalette extends HwmfPalette.WmfSelectPalette implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.selectPalette; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + /* + * A 32-bit unsigned integer that specifies either the index of a LogPalette object + * in the EMF Object Table or the value DEFAULT_PALETTE, which is the index + * of a stock object palette from the StockObject enumeration + */ + paletteIndex = (int)leis.readUInt(); + return LittleEndianConsts.INT_SIZE; + } + } + + /** The EMR_CREATEPALETTE record defines a logical palette for graphics operations. */ + public static class EmfCreatePalette extends HwmfPalette.WmfCreatePalette implements HemfRecord { + + protected int paletteIndex; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.createPalette; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + start = 0x0300; + /* A 32-bit unsigned integer that specifies the index of the logical palette object + * in the EMF Object Table. This index MUST be saved so that this object can be + * reused or modified. + */ + paletteIndex = (int)leis.readUInt(); + /* A 16-bit unsigned integer that specifies the version number of the system. This MUST be 0x0300. */ + int version = leis.readUShort(); + assert(version == 0x0300); + int size = readPaletteEntries(leis, -1); + return size + LittleEndianConsts.INT_SIZE + LittleEndianConsts.SHORT_SIZE; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.addObjectTableEntry(this, paletteIndex); + } + } + + /** + * The EMR_SETPALETTEENTRIES record defines RGB color values in a range of entries for an existing + * LogPalette object. + */ + public static class EmfSetPaletteEntries extends HwmfPalette.WmfSetPaletteEntries implements HemfRecord { + /** specifies the palette EMF Object Table index. */ + int paletteIndex; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setPaletteEntries; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer that specifies the palette EMF Object Table index. + paletteIndex = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the index of the first entry to set. + start = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the number of entries. + int nbrOfEntries = (int)leis.readUInt(); + int size = readPaletteEntries(leis, nbrOfEntries); + return size + 3*LittleEndianConsts.INT_SIZE; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.addObjectTableEntry(this, paletteIndex); + } + } + + /** + * The EMR_RESIZEPALETTE record increases or decreases the size of an existing LogPalette object + */ + public static class EmfResizePalette extends HwmfPalette.WmfResizePalette implements HemfRecord { + /** specifies the palette EMF Object Table index. */ + int paletteIndex; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.resizePalette; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer that specifies the index of the palette object in the EMF Object Table + paletteIndex = (int)leis.readUInt(); + + // A 32-bit unsigned integer that specifies the number of entries in the palette after resizing. + // The value MUST be less than or equal to 0x00000400 and greater than 0x00000000. + numberOfEntries = (int)leis.readUInt(); + + return 2*LittleEndianConsts.INT_SIZE; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.addObjectTableEntry(this, paletteIndex); + } + } + + /** + * This record maps palette entries from the current LogPalette object to the system_palette. + * This EMF record specifies no parameters. + */ + public static class EmfRealizePalette extends HwmfPalette.WmfRealizePalette implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.realizePalette; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return 0; + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/AbstractHemfComment.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPenStyle.java similarity index 57% rename from src/scratchpad/src/org/apache/poi/hemf/record/AbstractHemfComment.java rename to src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPenStyle.java index 7ffff6b016..74e9459923 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/AbstractHemfComment.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPenStyle.java @@ -15,25 +15,31 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hemf.record; +package org.apache.poi.hemf.record.emf; -import org.apache.poi.util.Internal; +import org.apache.poi.hwmf.record.HwmfPenStyle; -/** - * Syntactic utility to allow for four different - * comment classes - */ -@Internal -public abstract class AbstractHemfComment { +public class HemfPenStyle extends HwmfPenStyle { - private final byte[] rawBytes; + private float[] dashPattern; - public AbstractHemfComment(byte[] rawBytes) { - this.rawBytes = rawBytes; + public static HemfPenStyle valueOf(int flag) { + HemfPenStyle ps = new HemfPenStyle(); + ps.flag = flag; + return ps; } - public byte[] getRawBytes() { - return rawBytes; + @Override + public float[] getLineDashes() { + return (getLineDash() == HwmfLineDash.USERSTYLE) ? dashPattern : super.getLineDashes(); } + public void setLineDashes(float[] dashPattern) { + this.dashPattern = (dashPattern == null) ? null : dashPattern.clone(); + } + + @Override + public HemfPenStyle clone() { + return (HemfPenStyle)super.clone(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecord.java new file mode 100644 index 0000000000..41e9f30442 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecord.java @@ -0,0 +1,61 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + + +import java.io.IOException; + +import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hwmf.record.HwmfRecord; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianInputStream; + +@Internal +public interface HemfRecord { + + HemfRecordType getEmfRecordType(); + + /** + * Init record from stream + * + * @param leis the little endian input stream + * @param recordSize the size limit for this record + * @param recordId the id of the {@link HemfRecordType} + * + * @return count of processed bytes + * + * @throws IOException when the inputstream is malformed + */ + long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException; + + /** + * Draws the record, the default redirects to the parent WMF record drawing + * @param ctx the drawing context + */ + default void draw(HemfGraphics ctx) { + if (this instanceof HwmfRecord) { + ((HwmfRecord) this).draw(ctx); + } + } + + /** + * Sets the header reference, in case the record needs to refer to it + * @param header the emf header + */ + default void setHeader(HemfHeader header) {} +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java new file mode 100644 index 0000000000..dfa68670e1 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java @@ -0,0 +1,91 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import java.io.IOException; +import java.util.Iterator; + +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.RecordFormatException; + +public class HemfRecordIterator implements Iterator { + + static final int HEADER_SIZE = 2*LittleEndianConsts.INT_SIZE; + + private final LittleEndianInputStream stream; + private HemfRecord currentRecord; + + public HemfRecordIterator(LittleEndianInputStream leis) { + stream = leis; + //queue the first non-header record + currentRecord = _next(); + } + + @Override + public boolean hasNext() { + return currentRecord != null; + } + + @Override + public HemfRecord next() { + HemfRecord toReturn = currentRecord; + currentRecord = (currentRecord instanceof HemfMisc.EmfEof) ? null : _next(); + return toReturn; + } + + private HemfRecord _next() { + if (currentRecord != null && HemfRecordType.eof == currentRecord.getEmfRecordType()) { + return null; + } + + final int readIndex = stream.getReadIndex(); + + final long recordId, recordSize; + try { + recordId = stream.readUInt(); + recordSize = stream.readUInt(); + } catch (RuntimeException e) { + // EOF + return null; + } + + HemfRecordType type = HemfRecordType.getById(recordId); + if (type == null) { + throw new RecordFormatException("Undefined record of type: "+recordId+" at "+Integer.toHexString(readIndex)); + } + final HemfRecord record = type.constructor.get(); + + try { + long remBytes = recordSize-HEADER_SIZE; + long readBytes = record.init(stream, remBytes, recordId); + assert (readBytes <= remBytes); + stream.skipFully((int)(remBytes-readBytes)); + } catch (IOException|RuntimeException e) { + throw new RecordFormatException(e); + } + + return record; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + +} \ No newline at end of file diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java new file mode 100644 index 0000000000..9d25a9c145 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java @@ -0,0 +1,165 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import java.util.function.Supplier; + +import org.apache.poi.util.Internal; + +@Internal +public enum HemfRecordType { + + header(0x00000001, HemfHeader::new), + polyBezier(0x00000002, HemfDraw.EmfPolyBezier::new), + polygon(0x00000003, HemfDraw.EmfPolygon::new), + polyline(0x00000004, HemfDraw.EmfPolyline::new), + polyBezierTo(0x00000005, HemfDraw.EmfPolyBezierTo::new), + polylineTo(0x00000006, HemfDraw.EmfPolylineTo::new), + polyPolyline(0x00000007, HemfDraw.EmfPolyPolyline::new), + polyPolygon(0x00000008, HemfDraw.EmfPolyPolygon::new), + setWindowExtEx(0x00000009, HemfWindowing.EmfSetWindowExtEx::new), + setWindowOrgEx(0x0000000A, HemfWindowing.EmfSetWindowOrgEx::new), + setViewportExtEx(0x0000000B, HemfWindowing.EmfSetViewportExtEx::new), + setViewportOrgEx(0x0000000C, HemfWindowing.EmfSetViewportOrgEx::new), + setBrushOrgEx(0x0000000D, HemfMisc.EmfSetBrushOrgEx::new), + eof(0x0000000E, HemfMisc.EmfEof::new), + setPixelV(0x0000000F, HemfDraw.EmfSetPixelV::new), + setMapperFlags(0x00000010, HemfMisc.EmfSetMapperFlags::new), + setMapMode(0x00000011, HemfMisc.EmfSetMapMode::new), + setBkMode(0x00000012, HemfMisc.EmfSetBkMode::new), + setPolyfillMode(0x00000013, HemfFill.EmfSetPolyfillMode::new), + setRop2(0x00000014, HemfMisc.EmfSetRop2::new), + setStretchBltMode(0x00000015, HemfMisc.EmfSetStretchBltMode::new), + setTextAlign(0x00000016, HemfText.EmfSetTextAlign::new), + setcoloradjustment(0x00000017, UnimplementedHemfRecord::new), + setTextColor(0x00000018, HemfText.EmfSetTextColor::new), + setBkColor(0x00000019, HemfMisc.EmfSetBkColor::new), + setOffsetClipRgn(0x0000001A, HemfWindowing.EmfSetOffsetClipRgn::new), + setMoveToEx(0x0000001B, HemfDraw.EmfSetMoveToEx::new), + setmetargn(0x0000001C, UnimplementedHemfRecord::new), + setExcludeClipRect(0x0000001D, HemfWindowing.EmfSetExcludeClipRect::new), + setIntersectClipRect(0x0000001E, HemfWindowing.EmfSetIntersectClipRect::new), + scaleViewportExtEx(0x0000001F, HemfWindowing.EmfScaleViewportExtEx::new), + scaleWindowExtEx(0x00000020, HemfWindowing.EmfScaleWindowExtEx::new), + saveDc(0x00000021, HemfMisc.EmfSaveDc::new), + restoreDc(0x00000022, HemfMisc.EmfRestoreDc::new), + setWorldTransform(0x00000023, HemfMisc.EmfSetWorldTransform::new), + modifyWorldTransform(0x00000024, HemfMisc.EmfModifyWorldTransform::new), + selectObject(0x00000025, HemfDraw.EmfSelectObject::new), + createPen(0x00000026, HemfMisc.EmfCreatePen::new), + createBrushIndirect(0x00000027, HemfMisc.EmfCreateBrushIndirect::new), + deleteobject(0x00000028, HemfMisc.EmfDeleteObject::new), + anglearc(0x00000029, UnimplementedHemfRecord::new), + ellipse(0x0000002A, HemfDraw.EmfEllipse::new), + rectangle(0x0000002B, HemfDraw.EmfRectangle::new), + roundRect(0x0000002C, HemfDraw.EmfRoundRect::new), + arc(0x0000002D, HemfDraw.EmfArc::new), + chord(0x0000002E, HemfDraw.EmfChord::new), + pie(0x0000002F, HemfDraw.EmfPie::new), + selectPalette(0x00000030, HemfPalette.EmfSelectPalette::new), + createPalette(0x00000031, HemfPalette.EmfCreatePalette::new), + setPaletteEntries(0x00000032, HemfPalette.EmfSetPaletteEntries::new), + resizePalette(0x00000033, HemfPalette.EmfResizePalette::new), + realizePalette(0x0000034, HemfPalette.EmfRealizePalette::new), + extFloodFill(0x00000035, HemfFill.EmfExtFloodFill::new), + lineTo(0x00000036, HemfDraw.EmfLineTo::new), + arcTo(0x00000037, HemfDraw.EmfArcTo::new), + polyDraw(0x00000038, HemfDraw.EmfPolyDraw::new), + setarcdirection(0x00000039, UnimplementedHemfRecord::new), + setMiterLimit(0x0000003A, HemfMisc.EmfSetMiterLimit::new), + beginPath(0x0000003B, HemfDraw.EmfBeginPath::new), + endPath(0x0000003C, HemfDraw.EmfEndPath::new), + closeFigure(0x0000003D, HemfDraw.EmfCloseFigure::new), + fillPath(0x0000003E, HemfDraw.EmfFillPath::new), + strokeAndFillPath(0x0000003F, HemfDraw.EmfStrokeAndFillPath::new), + strokePath(0x00000040, HemfDraw.EmfStrokePath::new), + flattenPath(0x00000041, HemfDraw.EmfFlattenPath::new), + widenPath(0x00000042, HemfDraw.EmfWidenPath::new), + selectClipPath(0x00000043, HemfWindowing.EmfSelectClipPath::new), + abortPath(0x00000044, HemfDraw.EmfAbortPath::new), + // no 45 ?! + comment(0x00000046, HemfComment.EmfComment::new), + fillRgn(0x00000047, HemfFill.EmfFillRgn::new), + frameRgn(0x00000048, HemfFill.EmfFrameRgn::new), + invertRgn(0x00000049, HemfFill.EmfInvertRgn::new), + paintRgn(0x0000004A, HemfFill.EmfPaintRgn::new), + extSelectClipRgn(0x0000004B, HemfFill.EmfExtSelectClipRgn::new), + bitBlt(0x0000004C, HemfFill.EmfBitBlt::new), + stretchBlt(0x0000004D, HemfFill.EmfStretchBlt::new), + maskblt(0x0000004E, UnimplementedHemfRecord::new), + plgblt(0x0000004F, UnimplementedHemfRecord::new), + setDiBitsToDevice(0x00000050, HemfFill.EmfSetDiBitsToDevice::new), + stretchDiBits(0x00000051, HemfFill.EmfStretchDiBits::new), + extCreateFontIndirectW(0x00000052, HemfText.EmfExtCreateFontIndirectW::new), + extTextOutA(0x00000053, HemfText.EmfExtTextOutA::new), + extTextOutW(0x00000054, HemfText.EmfExtTextOutW::new), + polyBezier16(0x00000055, HemfDraw.EmfPolyBezier16::new), + polygon16(0x00000056, HemfDraw.EmfPolygon16::new), + polyline16(0x00000057, HemfDraw.EmfPolyline16::new), + polyBezierTo16(0x00000058, HemfDraw.EmfPolyBezierTo16::new), + polylineTo16(0x00000059, HemfDraw.EmfPolylineTo16::new), + polyPolyline16(0x0000005A, HemfDraw.EmfPolyPolyline16::new), + polyPolygon16(0x0000005B, HemfDraw.EmfPolyPolygon16::new), + polyDraw16(0x0000005C, HemfDraw.EmfPolyDraw16::new), + createMonoBrush(0x0000005D, HemfMisc.EmfCreateMonoBrush::new), + createDibPatternBrushPt(0x0000005E, HemfMisc.EmfCreateDibPatternBrushPt::new), + extCreatePen(0x0000005F, HemfMisc.EmfExtCreatePen::new), + polytextouta(0x00000060, HemfText.PolyTextOutA::new), + polytextoutw(0x00000061, HemfText.PolyTextOutW::new), + seticmmode(0x00000062, UnimplementedHemfRecord::new), + createcolorspace(0x00000063, UnimplementedHemfRecord::new), + setcolorspace(0x00000064, UnimplementedHemfRecord::new), + deletecolorspace(0x00000065, UnimplementedHemfRecord::new), + glsrecord(0x00000066, UnimplementedHemfRecord::new), + glsboundedrecord(0x00000067, UnimplementedHemfRecord::new), + pixelformat(0x00000068, UnimplementedHemfRecord::new), + drawescape(0x00000069, UnimplementedHemfRecord::new), + extescape(0x0000006A, UnimplementedHemfRecord::new), + // no 6b ?! + smalltextout(0x0000006C, UnimplementedHemfRecord::new), + forceufimapping(0x0000006D, UnimplementedHemfRecord::new), + namedescape(0x0000006E, UnimplementedHemfRecord::new), + colorcorrectpalette(0x0000006F, UnimplementedHemfRecord::new), + seticmprofilea(0x00000070, UnimplementedHemfRecord::new), + seticmprofilew(0x00000071, UnimplementedHemfRecord::new), + alphaBlend(0x00000072, HemfFill.EmfAlphaBlend::new), + setlayout(0x00000073, UnimplementedHemfRecord::new), + transparentblt(0x00000074, UnimplementedHemfRecord::new), + // no 75 ?! + gradientfill(0x00000076, UnimplementedHemfRecord::new), + setlinkdufis(0x00000077, UnimplementedHemfRecord::new), + settextjustification(0x00000078, HemfText.SetTextJustification::new), + colormatchtargetw(0x00000079, UnimplementedHemfRecord::new), + createcolorspacew(0x0000007A, UnimplementedHemfRecord::new); + + + public final long id; + public final Supplier constructor; + + HemfRecordType(long id, Supplier constructor) { + this.id = id; + this.constructor = constructor; + } + + public static HemfRecordType getById(long id) { + for (HemfRecordType wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java new file mode 100644 index 0000000000..7e785315c0 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java @@ -0,0 +1,332 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import static java.nio.charset.StandardCharsets.UTF_16LE; +import static org.apache.poi.hemf.record.emf.HemfDraw.readDimensionFloat; +import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL; +import static org.apache.poi.hemf.record.emf.HemfDraw.readRectL; +import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE; + +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.nio.charset.Charset; + +import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.hwmf.record.HwmfText; +import org.apache.poi.hwmf.record.HwmfText.WmfSetTextAlign; +import org.apache.poi.util.Dimension2DDouble; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.RecordFormatException; + +/** + * Container class to gather all text-related commands + * This is starting out as read only, and very little is actually + * implemented at this point! + */ +@Internal +public class HemfText { + + private static final int MAX_RECORD_LENGTH = 1_000_000; + + public enum EmfGraphicsMode { + GM_COMPATIBLE, GM_ADVANCED + } + + public static class EmfExtTextOutA extends HwmfText.WmfExtTextOut implements HemfRecord { + + protected Rectangle2D boundsIgnored = new Rectangle2D.Double(); + protected EmfGraphicsMode graphicsMode; + + /** + * The scale factor to apply along the X/Y axis to convert from page space units to .01mm units. + * This SHOULD be used only if the graphics mode specified by iGraphicsMode is GM_COMPATIBLE. + */ + protected final Dimension2D scale = new Dimension2DDouble(); + + public EmfExtTextOutA() { + super(new EmfExtTextOutOptions()); + } + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.extTextOutA; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + if (recordSize < 0 || Integer.MAX_VALUE <= recordSize) { + throw new RecordFormatException("recordSize must be a positive integer (0-0x7FFFFFFF)"); + } + + // A WMF RectL object. It is not used and MUST be ignored on receipt. + long size = readRectL(leis, boundsIgnored); + + // A 32-bit unsigned integer that specifies the graphics mode from the GraphicsMode enumeration + graphicsMode = EmfGraphicsMode.values()[leis.readInt()-1]; + size += LittleEndianConsts.INT_SIZE; + + size += readDimensionFloat(leis, scale); + + // A WMF PointL object that specifies the coordinates of the reference point used to position the string. + // The reference point is defined by the last EMR_SETTEXTALIGN record. + // If no such record has been set, the default alignment is TA_LEFT,TA_TOP. + size += readPointL(leis, reference); + // A 32-bit unsigned integer that specifies the number of characters in the string. + stringLength = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the offset to the output string, in bytes, + // from the start of the record in which this object is contained. + // This value MUST be 8- or 16-bit aligned, according to the character format. + int offString = (int)leis.readUInt(); + size += 2*LittleEndianConsts.INT_SIZE; + + size += options.init(leis); + // An optional WMF RectL object that defines a clipping and/or opaquing rectangle in logical units. + // This rectangle is applied to the text output performed by the containing record. + if (options.isClipped() || options.isOpaque()) { + size += readRectL(leis, bounds); + } + + // A 32-bit unsigned integer that specifies the offset to an intercharacter spacing array, in bytes, + // from the start of the record in which this object is contained. This value MUST be 32-bit aligned. + int offDx = (int)leis.readUInt(); + size += LittleEndianConsts.INT_SIZE; + + // handle dx before string and other way round + final String order = (offDx < offString) ? "ds" : "sd"; + // the next byte index after the string ends + int strEnd = (int)((offDx <= HEADER_SIZE) ? recordSize : offDx-HEADER_SIZE); + for (char op : order.toCharArray()) { + switch (op) { + case 'd': { + dx.clear(); + int undefinedSpace2 = (int) (offDx - (size + HEADER_SIZE)); + if (offDx > 0 && undefinedSpace2 >= 0 && offDx-HEADER_SIZE < recordSize) { + leis.skipFully(undefinedSpace2); + size += undefinedSpace2; + + // An array of 32-bit unsigned integers that specify the output spacing between the origins of adjacent + // character cells in logical units. The location of this field is specified by the value of offDx + // in bytes from the start of this record. If spacing is defined, this field contains the same number + // of values as characters in the output string. + // + // If the Options field of the EmrText object contains the ETO_PDY flag, then this buffer + // contains twice as many values as there are characters in the output string, one + // horizontal and one vertical offset for each, in that order. + // + // If ETO_RTLREADING is specified, characters are laid right to left instead of left to right. + // No other options affect the interpretation of this field. + final int maxSize = (int)Math.min((offDx < offString) ? (offString-HEADER_SIZE) : recordSize, recordSize); + while (size <= maxSize-LittleEndianConsts.INT_SIZE) { + dx.add((int) leis.readUInt()); + size += LittleEndianConsts.INT_SIZE; + } + } + if (dx.size() < stringLength) { + // invalid dx array + dx.clear(); + } + strEnd = (int)recordSize; + break; + } + default: + case 's': { + int undefinedSpace1 = (int)(offString - (size + HEADER_SIZE)); + if (offString > 0 && undefinedSpace1 >= 0 && offString-HEADER_SIZE < recordSize) { + leis.skipFully(undefinedSpace1); + size += undefinedSpace1; + + // read all available bytes and not just "stringLength * 1(ansi)/2(unicode)" + // in case we need to deal with surrogate pairs + final int maxSize = (int)(Math.min(recordSize, strEnd)-size); + rawTextBytes = IOUtils.safelyAllocate(maxSize, MAX_RECORD_LENGTH); + leis.readFully(rawTextBytes); + size += maxSize; + break; + } + } + } + } + + return size; + } + + /** + * + * To be implemented! We need to get the current character set + * from the current font for {@link EmfExtTextOutA}, + * which has to be tracked in the playback device. + * + * For {@link EmfExtTextOutW}, the charset is "UTF-16LE" + * + * @param charset the charset to be used to decode the character bytes + * @return text from this text element + * @throws IOException + */ + public String getText(Charset charset) throws IOException { + return super.getText(charset); + } + + public EmfGraphicsMode getGraphicsMode() { + return graphicsMode; + } + + public Dimension2D getScale() { + return scale; + } + + @Override + public void draw(HwmfGraphics ctx) { + // A 32-bit floating-point value that specifies the scale factor to apply along + // the axis to convert from page space units to .01mm units. + // This SHOULD be used only if the graphics mode specified by iGraphicsMode is GM_COMPATIBLE. + Dimension2D scl = graphicsMode == EmfGraphicsMode.GM_COMPATIBLE ? scale : null; + ctx.drawString(rawTextBytes, stringLength, reference, scl, bounds, options, dx, isUnicode()); + } + + @Override + public String toString() { + return + "{ graphicsMode: '"+graphicsMode+"'"+ + ", scale: { w: "+scale.getWidth()+", h: "+scale.getHeight()+" },"+ + super.toString().substring(1); + } + } + + public static class EmfExtTextOutW extends EmfExtTextOutA { + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.extTextOutW; + } + + public String getText() throws IOException { + return getText(UTF_16LE); + } + + protected boolean isUnicode() { + return true; + } + } + + /** + * The EMR_SETTEXTALIGN record specifies text alignment. + */ + public static class EmfSetTextAlign extends WmfSetTextAlign implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setTextAlign; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + /** + * A 32-bit unsigned integer that specifies text alignment by using a mask of text alignment flags. + * These are either WMF TextAlignmentMode Flags for text with a horizontal baseline, + * or WMF VerticalTextAlignmentMode Flags for text with a vertical baseline. + * Only one value can be chosen from those that affect horizontal and vertical alignment. + */ + textAlignmentMode = (int)leis.readUInt(); + return LittleEndianConsts.INT_SIZE; + } + } + + /** + * The EMR_SETTEXTCOLOR record defines the current text color. + */ + public static class EmfSetTextColor extends HwmfText.WmfSetTextColor implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setTextColor; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return colorRef.init(leis); + } + } + + + + public static class EmfExtCreateFontIndirectW extends HwmfText.WmfCreateFontIndirect + implements HemfRecord { + int fontIdx; + + public EmfExtCreateFontIndirectW() { + super(new HemfFont()); + } + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.extCreateFontIndirectW; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer that specifies the index of the logical font object + // in the EMF Object Table + fontIdx = (int)leis.readUInt(); + int size = font.init(leis, (int)(recordSize-LittleEndianConsts.INT_SIZE)); + return size+LittleEndianConsts.INT_SIZE; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.addObjectTableEntry(this, fontIdx); + } + + @Override + public String toString() { + return "{ index: "+fontIdx+", font: "+font+" } "; + } + } + + public static class EmfExtTextOutOptions extends HwmfText.WmfExtTextOutOptions { + @Override + public int init(LittleEndianInputStream leis) { + // A 32-bit unsigned integer that specifies how to use the rectangle specified in the Rectangle field. + // This field can be a combination of more than one ExtTextOutOptions enumeration + flag = (int)leis.readUInt(); + return LittleEndianConsts.INT_SIZE; + } + } + + public static class SetTextJustification extends UnimplementedHemfRecord { + + } + + /** + * Needs to be implemented. Couldn't find example. + */ + public static class PolyTextOutA extends UnimplementedHemfRecord { + + } + + /** + * Needs to be implemented. Couldn't find example. + */ + public static class PolyTextOutW extends UnimplementedHemfRecord { + + } + +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java new file mode 100644 index 0000000000..a312dc116e --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java @@ -0,0 +1,220 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emf; + +import static org.apache.poi.hemf.record.emf.HemfDraw.readDimensionInt; +import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL; +import static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds; + +import java.io.IOException; + +import org.apache.poi.hemf.draw.HemfDrawProperties; +import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hwmf.record.HwmfRegionMode; +import org.apache.poi.hwmf.record.HwmfWindowing; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; + +public class HemfWindowing { + + /** + * The EMR_SETWINDOWEXTEX record defines the window extent. + */ + public static class EmfSetWindowExtEx extends HwmfWindowing.WmfSetWindowExt implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setWindowExtEx; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readDimensionInt(leis, size); + } + } + + /** + * The EMR_SETWINDOWORGEX record defines the window origin. + */ + public static class EmfSetWindowOrgEx extends HwmfWindowing.WmfSetWindowOrg implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setWindowOrgEx; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readPointL(leis, origin); + } + } + + /** + * The EMR_SETVIEWPORTEXTEX record defines the viewport extent. + */ + public static class EmfSetViewportExtEx extends HwmfWindowing.WmfSetViewportExt implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setViewportExtEx; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readDimensionInt(leis, extents); + } + } + + /** + * The EMR_SETVIEWPORTORGEX record defines the viewport origin. + */ + public static class EmfSetViewportOrgEx extends HwmfWindowing.WmfSetViewportOrg implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setViewportOrgEx; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readPointL(leis, origin); + } + } + + /** + * The EMR_OFFSETCLIPRGN record moves the current clipping region in the playback device context + * by the specified offsets. + */ + public static class EmfSetOffsetClipRgn extends HwmfWindowing.WmfOffsetClipRgn implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setOffsetClipRgn; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return readPointL(leis, offset); + } + } + + /** + * The EMR_EXCLUDECLIPRECT record specifies a new clipping region that consists of the existing + * clipping region minus the specified rectangle. + */ + public static class EmfSetExcludeClipRect extends HwmfWindowing.WmfExcludeClipRect implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setExcludeClipRect; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return HemfDraw.readRectL(leis, bounds); + } + } + + /** + * The EMR_INTERSECTCLIPRECT record specifies a new clipping region from the intersection of the + * current clipping region and the specified rectangle. + */ + public static class EmfSetIntersectClipRect extends HwmfWindowing.WmfIntersectClipRect implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.setIntersectClipRect; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + return HemfDraw.readRectL(leis, normalizeBounds(bounds)); + } + } + + /** + * The EMR_SCALEVIEWPORTEXTEX record respecifies the viewport for a device context by using the + * ratios formed by the specified multiplicands and divisors. + */ + public static class EmfScaleViewportExtEx extends HwmfWindowing.WmfScaleViewportExt implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.scaleViewportExtEx; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + double xNum = leis.readInt(); + double xDenom = leis.readInt(); + double yNum = leis.readInt(); + double yDenom = leis.readInt(); + scale.setSize(xNum / xDenom, yNum / yDenom); + return 4*LittleEndianConsts.INT_SIZE; + } + } + + /** + * The EMR_SCALEWINDOWEXTEX record respecifies the window for a playback device context by + * using the ratios formed by the specified multiplicands and divisors. + */ + public static class EmfScaleWindowExtEx extends HwmfWindowing.WmfScaleWindowExt implements HemfRecord { + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.scaleWindowExtEx; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + double xNum = leis.readInt(); + double xDenom = leis.readInt(); + double yNum = leis.readInt(); + double yDenom = leis.readInt(); + + scale.setSize(xNum / xDenom, yNum / yDenom); + + return 4*LittleEndianConsts.INT_SIZE; + } + } + + /** + * The EMR_SELECTCLIPPATH record specifies the current path as a clipping region for a playback + * device context, combining the new region with any existing clipping region using the specified mode. + */ + public static class EmfSelectClipPath implements HemfRecord { + protected HwmfRegionMode regionMode; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.selectClipPath; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + // A 32-bit unsigned integer that specifies the way to use the path. + // The value MUST be in the RegionMode enumeration + regionMode = HwmfRegionMode.valueOf(leis.readInt()); + + return LittleEndianConsts.INT_SIZE; + } + + @Override + public void draw(HemfGraphics ctx) { + HemfDrawProperties prop = ctx.getProperties(); + ctx.setClip(prop.getPath(), regionMode, false); + } + + @Override + public String toString() { + return "{ regionMode: '"+regionMode+"' }"; + } + } + +} \ No newline at end of file diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/UnimplementedHemfRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/UnimplementedHemfRecord.java similarity index 80% rename from src/scratchpad/src/org/apache/poi/hemf/record/UnimplementedHemfRecord.java rename to src/scratchpad/src/org/apache/poi/hemf/record/emf/UnimplementedHemfRecord.java index 6f3ded48d5..12034376c4 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/UnimplementedHemfRecord.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/UnimplementedHemfRecord.java @@ -15,7 +15,7 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hemf.record; +package org.apache.poi.hemf.record.emf; import java.io.IOException; @@ -27,19 +27,16 @@ import org.apache.poi.util.LittleEndianInputStream; @Internal public class UnimplementedHemfRecord implements HemfRecord { - private long recordId; - public UnimplementedHemfRecord() { + private HemfRecordType recordType; + @Override + public HemfRecordType getEmfRecordType() { + return recordType; } @Override - public HemfRecordType getRecordType() { - return HemfRecordType.getById(recordId); - } - - @Override - public long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException { - this.recordId = recordId; + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + recordType = HemfRecordType.getById(recordId); long skipped = IOUtils.skipFully(leis, recordSize); if (skipped < recordSize) { throw new IOException("End of stream reached before record read"); diff --git a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusHeader.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java similarity index 73% rename from src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusHeader.java rename to src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java index 25947937b4..6264750a92 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/HemfPlusHeader.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java @@ -15,13 +15,14 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hemf.hemfplus.record; +package org.apache.poi.hemf.record.emfplus; import java.io.IOException; import org.apache.poi.util.Internal; -import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; @Internal public class HemfPlusHeader implements HemfPlusRecord { @@ -33,7 +34,7 @@ public class HemfPlusHeader implements HemfPlusRecord { private long logicalDpiY; @Override - public HemfPlusRecordType getRecordType() { + public HemfPlusRecordType getEmfPlusRecordType() { return HemfPlusRecordType.header; } @@ -42,15 +43,19 @@ public class HemfPlusHeader implements HemfPlusRecord { } @Override - public void init(byte[] dataBytes, int recordId, int flags) throws IOException { - //assert record id == header + public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { this.flags = flags; - int offset = 0; - this.version = LittleEndian.getUInt(dataBytes, offset); offset += LittleEndian.INT_SIZE; - this.emfPlusFlags = LittleEndian.getUInt(dataBytes, offset); offset += LittleEndian.INT_SIZE; - this.logicalDpiX = LittleEndian.getUInt(dataBytes, offset); offset += LittleEndian.INT_SIZE; - this.logicalDpiY = LittleEndian.getUInt(dataBytes, offset); + version = leis.readUInt(); + // verify MetafileSignature (20 bits) == 0xDBC01 and + // GraphicsVersion (12 bits) in (1 or 2) + assert((version & 0xFFFFFA00) == 0xDBC01000L && ((version & 0x3FF) == 1 || (version & 0x3FF) == 2)); + + emfPlusFlags = leis.readUInt(); + + logicalDpiX = leis.readUInt(); + logicalDpiY = leis.readUInt(); + return 4* LittleEndianConsts.INT_SIZE; } public long getVersion() { @@ -79,4 +84,4 @@ public class HemfPlusHeader implements HemfPlusRecord { ", logicalDpiY=" + logicalDpiY + '}'; } -} +} \ No newline at end of file diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecord.java similarity index 69% rename from src/scratchpad/src/org/apache/poi/hemf/record/HemfRecord.java rename to src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecord.java index de1271e696..808c166360 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/HemfRecord.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecord.java @@ -15,27 +15,34 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hemf.record; +package org.apache.poi.hemf.record.emfplus; import java.io.IOException; +import org.apache.poi.hemf.record.emf.HemfRecordType; import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianInputStream; @Internal -public interface HemfRecord { +public interface HemfPlusRecord { - HemfRecordType getRecordType(); + HemfPlusRecordType getEmfPlusRecordType(); + + int getFlags(); /** * Init record from stream * * @param leis the little endian input stream + * @param dataSize the size limit for this record + * @param recordId the id of the {@link HemfPlusRecordType} + * @param flags the record flags + * * @return count of processed bytes - * @throws IOException + * + * @throws IOException when the inputstream is malformed */ - long init(LittleEndianInputStream leis, long recordId, long recordSize) throws IOException; - + long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException; } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordIterator.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordIterator.java new file mode 100644 index 0000000000..4c6f0ad919 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordIterator.java @@ -0,0 +1,98 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emfplus; + +import java.io.IOException; +import java.util.Iterator; + +import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.RecordFormatException; + +public class HemfPlusRecordIterator implements Iterator { + + private final LittleEndianInputStream leis; + private final int startIdx; + private final int limit; + private HemfPlusRecord currentRecord; + + public HemfPlusRecordIterator(LittleEndianInputStream leis) { + this(leis, -1); + } + + public HemfPlusRecordIterator(LittleEndianInputStream leis, int limit) { + this.leis = leis; + this.limit = limit; + startIdx = leis.getReadIndex(); + //queue the first non-header record + currentRecord = _next(); + } + + @Override + public boolean hasNext() { + return currentRecord != null; + } + + @Override + public HemfPlusRecord next() { + HemfPlusRecord toReturn = currentRecord; + final boolean isEOF = (limit == -1 || leis.getReadIndex()-startIdx >= limit); + // (currentRecord instanceof HemfPlusMisc.EmfEof) + currentRecord = isEOF ? null : _next(); + return toReturn; + } + + private HemfPlusRecord _next() { + if (currentRecord != null && HemfPlusRecordType.eof == currentRecord.getEmfPlusRecordType()) { + return null; + } + // A 16-bit unsigned integer that identifies this record type + int recordId = leis.readUShort(); + // A 16-bit unsigned integer that provides information about how the operation is + // to be performed, and about the structure of the record. + int flags = leis.readUShort(); + // A 32-bit unsigned integer that specifies the 32-bit-aligned size of the entire + // record in bytes, including the 12-byte record header and record-specific data. + int recordSize = (int)leis.readUInt(); + // A 32-bit unsigned integer that specifies the 32-bit-aligned number of bytes of data + // in the record-specific data that follows. This number does not include the size of + // the invariant part of this record. + int dataSize = (int)leis.readUInt(); + + HemfPlusRecordType type = HemfPlusRecordType.getById(recordId); + if (type == null) { + throw new RecordFormatException("Undefined record of type:"+recordId); + } + final HemfPlusRecord record = type.constructor.get(); + + try { + long readBytes = record.init(leis, dataSize, recordId, flags); + assert (readBytes <= recordSize-12); + leis.skipFully((int)(recordSize-12-readBytes)); + } catch (IOException e) { + throw new RecordFormatException(e); + } + + return record; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java new file mode 100644 index 0000000000..2fc1926ff6 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java @@ -0,0 +1,100 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.record.emfplus; + +import java.util.function.Supplier; + +import org.apache.poi.util.Internal; + +@Internal +public enum HemfPlusRecordType { + header(0x4001, HemfPlusHeader::new), + eof(0x4002, UnimplementedHemfPlusRecord::new), + comment(0x4003, UnimplementedHemfPlusRecord::new), + getDC(0x4004, UnimplementedHemfPlusRecord::new), + multiFormatStart(0x4005, UnimplementedHemfPlusRecord::new), + multiFormatSection(0x4006, UnimplementedHemfPlusRecord::new), + multiFormatEnd(0x4007, UnimplementedHemfPlusRecord::new), + object(0x4008, UnimplementedHemfPlusRecord::new), + clear(0x4009, UnimplementedHemfPlusRecord::new), + fillRects(0x400A, UnimplementedHemfPlusRecord::new), + drawRects(0x400B, UnimplementedHemfPlusRecord::new), + fillPolygon(0x400C, UnimplementedHemfPlusRecord::new), + drawLines(0x400D, UnimplementedHemfPlusRecord::new), + fillEllipse(0x400E, UnimplementedHemfPlusRecord::new), + drawEllipse(0x400F, UnimplementedHemfPlusRecord::new), + fillPie(0x4010, UnimplementedHemfPlusRecord::new), + drawPie(0x4011, UnimplementedHemfPlusRecord::new), + drawArc(0x4012, UnimplementedHemfPlusRecord::new), + fillRegion(0x4013, UnimplementedHemfPlusRecord::new), + fillPath(0x4014, UnimplementedHemfPlusRecord::new), + drawPath(0x4015, UnimplementedHemfPlusRecord::new), + fillClosedCurve(0x4016, UnimplementedHemfPlusRecord::new), + drawClosedCurve(0x4017, UnimplementedHemfPlusRecord::new), + drawCurve(0x4018, UnimplementedHemfPlusRecord::new), + drawBeziers(0x4019, UnimplementedHemfPlusRecord::new), + drawImage(0x401A, UnimplementedHemfPlusRecord::new), + drawImagePoints(0x401B, UnimplementedHemfPlusRecord::new), + drawString(0x401C, UnimplementedHemfPlusRecord::new), + setRenderingOrigin(0x401D, UnimplementedHemfPlusRecord::new), + setAntiAliasMode(0x401E, UnimplementedHemfPlusRecord::new), + setTextRenderingHint(0x401F, UnimplementedHemfPlusRecord::new), + setTextContrast(0x4020, UnimplementedHemfPlusRecord::new), + setInterpolationMode(0x4021, UnimplementedHemfPlusRecord::new), + setPixelOffsetMode(0x4022, UnimplementedHemfPlusRecord::new), + setComositingMode(0x4023, UnimplementedHemfPlusRecord::new), + setCompositingQuality(0x4024, UnimplementedHemfPlusRecord::new), + save(0x4025, UnimplementedHemfPlusRecord::new), + restore(0x4026, UnimplementedHemfPlusRecord::new), + beginContainer(0x4027, UnimplementedHemfPlusRecord::new), + beginContainerNoParams(0x428, UnimplementedHemfPlusRecord::new), + endContainer(0x4029, UnimplementedHemfPlusRecord::new), + setWorldTransform(0x402A, UnimplementedHemfPlusRecord::new), + resetWorldTransform(0x402B, UnimplementedHemfPlusRecord::new), + multiplyWorldTransform(0x402C, UnimplementedHemfPlusRecord::new), + translateWorldTransform(0x402D, UnimplementedHemfPlusRecord::new), + scaleWorldTransform(0x402E, UnimplementedHemfPlusRecord::new), + rotateWorldTransform(0x402F, UnimplementedHemfPlusRecord::new), + setPageTransform(0x4030, UnimplementedHemfPlusRecord::new), + resetClip(0x4031, UnimplementedHemfPlusRecord::new), + setClipRect(0x4032, UnimplementedHemfPlusRecord::new), + setClipRegion(0x4033, UnimplementedHemfPlusRecord::new), + setClipPath(0x4034, UnimplementedHemfPlusRecord::new), + offsetClip(0x4035, UnimplementedHemfPlusRecord::new), + drawDriverstring(0x4036, UnimplementedHemfPlusRecord::new), + strokeFillPath(0x4037, UnimplementedHemfPlusRecord::new), + serializableObject(0x4038, UnimplementedHemfPlusRecord::new), + setTSGraphics(0x4039, UnimplementedHemfPlusRecord::new), + setTSClip(0x403A, UnimplementedHemfPlusRecord::new); + + + public final long id; + public final Supplier constructor; + + HemfPlusRecordType(long id, Supplier constructor) { + this.id = id; + this.constructor = constructor; + } + + public static HemfPlusRecordType getById(long id) { + for (HemfPlusRecordType wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/UnimplementedHemfPlusRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/UnimplementedHemfPlusRecord.java similarity index 68% rename from src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/UnimplementedHemfPlusRecord.java rename to src/scratchpad/src/org/apache/poi/hemf/record/emfplus/UnimplementedHemfPlusRecord.java index 7e3cbcff4c..bc7152cd7e 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/hemfplus/record/UnimplementedHemfPlusRecord.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/UnimplementedHemfPlusRecord.java @@ -15,23 +15,27 @@ limitations under the License. ==================================================================== */ -package org.apache.poi.hemf.hemfplus.record; +package org.apache.poi.hemf.record.emfplus; import java.io.IOException; +import org.apache.poi.util.IOUtils; import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianInputStream; @Internal public class UnimplementedHemfPlusRecord implements HemfPlusRecord { - private int recordId; + private static final int MAX_RECORD_LENGTH = 1_000_000; + + private HemfPlusRecordType recordType; private int flags; private byte[] recordBytes; @Override - public HemfPlusRecordType getRecordType() { - return HemfPlusRecordType.getById(recordId); + public HemfPlusRecordType getEmfPlusRecordType() { + return recordType; } @Override @@ -40,14 +44,16 @@ public class UnimplementedHemfPlusRecord implements HemfPlusRecord { } @Override - public void init(byte[] recordBytes, int recordId, int flags) throws IOException { - this.recordId = recordId; + public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { + recordType = HemfPlusRecordType.getById(recordId); this.flags = flags; - this.recordBytes = recordBytes; + recordBytes = IOUtils.safelyAllocate(dataSize, MAX_RECORD_LENGTH); + leis.readFully(recordBytes); + return recordBytes.length; } public byte[] getRecordBytes() { //should probably defensively return a copy. return recordBytes; } -} +} \ No newline at end of file diff --git a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java new file mode 100644 index 0000000000..6d53ae23ea --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java @@ -0,0 +1,161 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hemf.usermodel; + + +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.function.Consumer; + +import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hemf.record.emf.HemfHeader; +import org.apache.poi.hemf.record.emf.HemfRecord; +import org.apache.poi.hemf.record.emf.HemfRecordIterator; +import org.apache.poi.hemf.record.emf.HemfWindowing; +import org.apache.poi.util.Dimension2DDouble; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.Units; + +/** + * Read-only EMF extractor. Lots remain + */ +@Internal +public class HemfPicture implements Iterable { + + private final LittleEndianInputStream stream; + private final List records = new ArrayList<>(); + private boolean isParsed = false; + + public HemfPicture(InputStream is) throws IOException { + this(new LittleEndianInputStream(is)); + } + + public HemfPicture(LittleEndianInputStream is) throws IOException { + stream = is; + } + + public HemfHeader getHeader() { + return (HemfHeader)getRecords().get(0); + } + + public List getRecords() { + if (!isParsed) { + // in case the (first) parsing throws an exception, we can provide the + // records up to that point + isParsed = true; + HemfHeader[] header = new HemfHeader[1]; + new HemfRecordIterator(stream).forEachRemaining(r -> { + if (r instanceof HemfHeader) { + header[0] = (HemfHeader) r; + } + r.setHeader(header[0]); + records.add(r); + }); + } + return records; + } + + @Override + public Iterator iterator() { + return getRecords().iterator(); + } + + @Override + public Spliterator spliterator() { + return getRecords().spliterator(); + } + + @Override + public void forEach(Consumer action) { + getRecords().forEach(action); + } + + /** + * Return the image size in points + * + * @return the image size in points + */ + public Dimension2D getSize() { + HemfHeader header = (HemfHeader)getRecords().get(0); + final double coeff = (double) Units.EMU_PER_CENTIMETER / Units.EMU_PER_POINT / 10.; + Rectangle2D dim = header.getFrameRectangle(); + double width = dim.getWidth(), height = dim.getHeight(); + if (dim.isEmpty() || Math.rint(width*coeff) == 0 || Math.rint(height*coeff) == 0) { + for (HemfRecord r : getRecords()) { + if (r instanceof HemfWindowing.EmfSetWindowExtEx) { + Dimension2D d = ((HemfWindowing.EmfSetWindowExtEx)r).getSize(); + width = d.getWidth(); + height = d.getHeight(); + // keep searching - sometimes there's another record + } + } + } + + if (Math.rint(width*coeff) == 0 || Math.rint(height*coeff) == 0) { + width = 100; + height = 100; + } + + return new Dimension2DDouble(Math.abs(width*coeff), Math.abs(height*coeff)); + } + + private static double minX(Rectangle2D bounds) { + return Math.min(bounds.getMinX(), bounds.getMaxX()); + } + + private static double minY(Rectangle2D bounds) { + return Math.min(bounds.getMinY(), bounds.getMaxY()); + } + + public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) { + HemfHeader header = (HemfHeader)getRecords().get(0); + + AffineTransform at = ctx.getTransform(); + try { + Rectangle2D emfBounds = header.getBoundsRectangle(); + + // scale output bounds to image bounds + ctx.translate(minX(graphicsBounds), minY(graphicsBounds)); + ctx.scale(graphicsBounds.getWidth()/emfBounds.getWidth(), graphicsBounds.getHeight()/emfBounds.getHeight()); + ctx.translate(-minX(emfBounds), -minY(emfBounds)); + + int idx = 0; + HemfGraphics g = new HemfGraphics(ctx, emfBounds); + for (HemfRecord r : getRecords()) { + try { + g.draw(r); + } catch (RuntimeException ignored) { + + } + idx++; + } + } finally { + ctx.setTransform(at); + } + } + +} diff --git a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java index bd8c9ff9a9..8b8358c30f 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java @@ -19,7 +19,9 @@ package org.apache.poi.hwmf.draw; import java.awt.Color; import java.awt.Shape; +import java.awt.geom.AffineTransform; import java.awt.geom.Area; +import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; @@ -36,6 +38,7 @@ import org.apache.poi.hwmf.record.HwmfMapMode; import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode; import org.apache.poi.hwmf.record.HwmfPalette.PaletteEntry; import org.apache.poi.hwmf.record.HwmfPenStyle; +import org.apache.poi.hwmf.record.HwmfTernaryRasterOp; import org.apache.poi.hwmf.record.HwmfText.HwmfTextAlignment; import org.apache.poi.hwmf.record.HwmfText.HwmfTextVerticalAlignment; @@ -43,36 +46,59 @@ public class HwmfDrawProperties { private final Rectangle2D window; private Rectangle2D viewport; private final Point2D location; - private HwmfMapMode mapMode = HwmfMapMode.MM_ANISOTROPIC; - private HwmfColorRef backgroundColor = new HwmfColorRef(Color.BLACK); - private HwmfBrushStyle brushStyle = HwmfBrushStyle.BS_SOLID; - private HwmfColorRef brushColor = new HwmfColorRef(Color.BLACK); - private HwmfHatchStyle brushHatch = HwmfHatchStyle.HS_HORIZONTAL; + private HwmfMapMode mapMode; + private HwmfColorRef backgroundColor; + private HwmfBrushStyle brushStyle; + private HwmfColorRef brushColor; + private HwmfHatchStyle brushHatch; private BufferedImage brushBitmap; - private double penWidth = 1; - private HwmfPenStyle penStyle = HwmfPenStyle.valueOf(0); - private HwmfColorRef penColor = new HwmfColorRef(Color.BLACK); - private double penMiterLimit = 10; - private HwmfBkMode bkMode = HwmfBkMode.OPAQUE; - private HwmfPolyfillMode polyfillMode = HwmfPolyfillMode.WINDING; + private double penWidth; + private HwmfPenStyle penStyle; + private HwmfColorRef penColor; + private double penMiterLimit; + private HwmfBkMode bkMode; + private HwmfPolyfillMode polyfillMode; private Shape region; private List palette; private int paletteOffset; private HwmfFont font; - private HwmfColorRef textColor = new HwmfColorRef(Color.BLACK); - private HwmfTextAlignment textAlignLatin = HwmfTextAlignment.LEFT; - private HwmfTextVerticalAlignment textVAlignLatin = HwmfTextVerticalAlignment.TOP; - private HwmfTextAlignment textAlignAsian = HwmfTextAlignment.RIGHT; - private HwmfTextVerticalAlignment textVAlignAsian = HwmfTextVerticalAlignment.TOP; + private HwmfColorRef textColor; + private HwmfTextAlignment textAlignLatin; + private HwmfTextVerticalAlignment textVAlignLatin; + private HwmfTextAlignment textAlignAsian; + private HwmfTextVerticalAlignment textVAlignAsian; + private HwmfTernaryRasterOp rasterOp; + protected Shape clip; + protected final AffineTransform transform = new AffineTransform(); public HwmfDrawProperties() { window = new Rectangle2D.Double(0, 0, 1, 1); viewport = null; location = new Point2D.Double(0,0); + mapMode = HwmfMapMode.MM_ANISOTROPIC; + backgroundColor = new HwmfColorRef(Color.BLACK); + brushStyle = HwmfBrushStyle.BS_SOLID; + brushColor = new HwmfColorRef(Color.BLACK); + brushHatch = HwmfHatchStyle.HS_HORIZONTAL; + penWidth = 1; + penStyle = HwmfPenStyle.valueOf(0); + penColor = new HwmfColorRef(Color.BLACK); + penMiterLimit = 10; + bkMode = HwmfBkMode.OPAQUE; + polyfillMode = HwmfPolyfillMode.WINDING; + textColor = new HwmfColorRef(Color.BLACK); + textAlignLatin = HwmfTextAlignment.LEFT; + textVAlignLatin = HwmfTextVerticalAlignment.TOP; + textAlignAsian = HwmfTextAlignment.RIGHT; + textVAlignAsian = HwmfTextVerticalAlignment.TOP; + rasterOp = HwmfTernaryRasterOp.PATCOPY; + clip = null; + font = new HwmfFont(); + font.initDefaults(); } public HwmfDrawProperties(HwmfDrawProperties other) { - this.window = (Rectangle2D)other.window.clone(); + this.window = (other.window == null) ? null : (Rectangle2D)other.window.clone(); this.viewport = (other.viewport == null) ? null : (Rectangle2D)other.viewport.clone(); this.location = (Point2D)other.location.clone(); this.mapMode = other.mapMode; @@ -86,7 +112,7 @@ public class HwmfDrawProperties { WritableRaster raster = other.brushBitmap.copyData(null); this.brushBitmap = new BufferedImage(cm, raster, isAlphaPremultiplied, null); } - this.penWidth = 1; + this.penWidth = other.penWidth; this.penStyle = (other.penStyle == null) ? null : other.penStyle.clone(); this.penColor = (other.penColor == null) ? null : other.penColor.clone(); this.penMiterLimit = other.penMiterLimit; @@ -101,6 +127,13 @@ public class HwmfDrawProperties { this.paletteOffset = other.paletteOffset; this.font = other.font; this.textColor = (other.textColor == null) ? null : other.textColor.clone(); + this.textAlignLatin = other.textAlignLatin; + this.textVAlignLatin = other.textVAlignLatin; + this.textAlignAsian = other.textAlignAsian; + this.textVAlignAsian = other.textVAlignAsian; + this.rasterOp = other.rasterOp; + this.transform.setTransform(other.transform); + this.clip = other.clip; } public void setViewportExt(double width, double height) { @@ -149,6 +182,10 @@ public class HwmfDrawProperties { location.setLocation(x, y); } + public void setLocation(Point2D point) { + location.setLocation(point); + } + public Point2D getLocation() { return (Point2D)location.clone(); } @@ -343,4 +380,35 @@ public class HwmfDrawProperties { public void setTextVAlignAsian(HwmfTextVerticalAlignment textVAlignAsian) { this.textVAlignAsian = textVAlignAsian; } + + /** + * @return the current active winding rule ({@link Path2D#WIND_EVEN_ODD} or {@link Path2D#WIND_NON_ZERO}) + */ + public int getWindingRule() { + return getPolyfillMode().awtFlag; + } + + public HwmfTernaryRasterOp getRasterOp() { + return rasterOp; + } + + public void setRasterOp(HwmfTernaryRasterOp rasterOp) { + this.rasterOp = rasterOp; + } + + public AffineTransform getTransform() { + return transform; + } + + public void setTransform(AffineTransform transform) { + this.transform.setTransform(transform); + } + + public Shape getClip() { + return clip; + } + + public void setClip(Shape clip) { + this.clip = clip; + } } 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 5eccd5d7a9..2f9811fc63 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java @@ -25,18 +25,26 @@ import java.awt.Paint; import java.awt.Rectangle; import java.awt.Shape; import java.awt.TexturePaint; +import java.awt.font.FontRenderContext; import java.awt.font.TextAttribute; +import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Dimension2D; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; 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.LinkedList; import java.util.List; -import java.util.ListIterator; import java.util.NoSuchElementException; +import java.util.TreeMap; +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; @@ -46,20 +54,31 @@ import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode; 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.DrawFactory; import org.apache.poi.sl.draw.DrawFontManager; +import org.apache.poi.sl.draw.DrawFontManagerDefault; import org.apache.poi.util.LocaleUtil; public class HwmfGraphics { + public enum FillDrawStyle { + NONE, FILL, DRAW, FILL_DRAW + } + + protected final List propStack = new LinkedList<>(); + protected HwmfDrawProperties prop; + protected final Graphics2D graphicsCtx; + protected final BitSet objectIndexes = new BitSet(); + protected final TreeMap objectTable = new TreeMap<>(); + protected final AffineTransform initialAT = new AffineTransform(); + + private static final Charset DEFAULT_CHARSET = LocaleUtil.CHARSET_1252; - private final Graphics2D graphicsCtx; - private final List propStack = new LinkedList<>(); - private HwmfDrawProperties prop = new HwmfDrawProperties(); - private List objectTable = new ArrayList<>(); - /** Bounding box from the placeable header */ + /** Bounding box from the placeable header */ private final Rectangle2D bbox; - private final AffineTransform initialAT; /** * Initialize a graphics context for wmf rendering @@ -70,16 +89,26 @@ public class HwmfGraphics { public HwmfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) { this.graphicsCtx = graphicsCtx; this.bbox = (Rectangle2D)bbox.clone(); - this.initialAT = graphicsCtx.getTransform(); - DrawFactory.getInstance(graphicsCtx).fixFonts(graphicsCtx); + this.initialAT.setTransform(graphicsCtx.getTransform()); } public HwmfDrawProperties getProperties() { + if (prop == null) { + prop = newProperties(null); + } return prop; } + protected HwmfDrawProperties newProperties(HwmfDrawProperties oldProps) { + return (oldProps == null) ? new HwmfDrawProperties() : new HwmfDrawProperties(oldProps); + } + public void draw(Shape shape) { - HwmfLineDash lineDash = prop.getPenStyle().getLineDash(); + HwmfPenStyle ps = getProperties().getPenStyle(); + if (ps == null) { + return; + } + HwmfLineDash lineDash = ps.getLineDash(); if (lineDash == HwmfLineDash.NULL) { // line is not drawn return; @@ -89,40 +118,44 @@ public class HwmfGraphics { // first draw a solid background line (depending on bkmode) // only makes sense if the line is not solid - if (prop.getBkMode() == HwmfBkMode.OPAQUE && (lineDash != HwmfLineDash.SOLID && lineDash != HwmfLineDash.INSIDEFRAME)) { + if (getProperties().getBkMode() == HwmfBkMode.OPAQUE && (lineDash != HwmfLineDash.SOLID && lineDash != HwmfLineDash.INSIDEFRAME)) { graphicsCtx.setStroke(new BasicStroke(stroke.getLineWidth())); - graphicsCtx.setColor(prop.getBackgroundColor().getColor()); + graphicsCtx.setColor(getProperties().getBackgroundColor().getColor()); graphicsCtx.draw(shape); } // then draw the (dashed) line graphicsCtx.setStroke(stroke); - graphicsCtx.setColor(prop.getPenColor().getColor()); + graphicsCtx.setColor(getProperties().getPenColor().getColor()); graphicsCtx.draw(shape); } public void fill(Shape shape) { + HwmfDrawProperties prop = getProperties(); if (prop.getBrushStyle() != HwmfBrushStyle.BS_NULL) { -// GeneralPath gp = new GeneralPath(shape); -// gp.setWindingRule(prop.getPolyfillMode().awtFlag); + if (prop.getBkMode() == HwmfBkMode.OPAQUE) { + graphicsCtx.setPaint(prop.getBackgroundColor().getColor()); + graphicsCtx.fill(shape); + } + graphicsCtx.setPaint(getFill()); graphicsCtx.fill(shape); } - draw(shape); +// draw(shape); } protected BasicStroke getStroke() { // TODO: fix line width calculation - float width = (float)prop.getPenWidth(); + float width = (float)getProperties().getPenWidth(); if (width == 0) { width = 1; } - HwmfPenStyle ps = prop.getPenStyle(); + HwmfPenStyle ps = getProperties().getPenStyle(); int cap = ps.getLineCap().awtFlag; int join = ps.getLineJoin().awtFlag; - float miterLimit = (float)prop.getPenMiterLimit(); - float dashes[] = ps.getLineDash().dashes; + float miterLimit = (float)getProperties().getPenMiterLimit(); + float dashes[] = ps.getLineDashes(); boolean dashAlt = ps.isAlternateDash(); // This value is not an integer index into the dash pattern array. // Instead, it is a floating-point value that specifies a linear distance. @@ -132,7 +165,7 @@ public class HwmfGraphics { } protected Paint getFill() { - switch (prop.getBrushStyle()) { + switch (getProperties().getBrushStyle()) { default: case BS_INDEXED: case BS_PATTERN8X8: @@ -148,20 +181,20 @@ public class HwmfGraphics { } protected Paint getSolidFill() { - return prop.getBrushColor().getColor(); + return getProperties().getBrushColor().getColor(); } protected Paint getHatchedFill() { int dim = 7, mid = 3; BufferedImage bi = new BufferedImage(dim, dim, BufferedImage.TYPE_4BYTE_ABGR); Graphics2D g = bi.createGraphics(); - Color c = (prop.getBkMode() == HwmfBkMode.TRANSPARENT) + Color c = (getProperties().getBkMode() == HwmfBkMode.TRANSPARENT) ? new Color(0, true) - : prop.getBackgroundColor().getColor(); + : getProperties().getBackgroundColor().getColor(); g.setColor(c); g.fillRect(0, 0, dim, dim); - g.setColor(prop.getBrushColor().getColor()); - HwmfHatchStyle h = prop.getBrushHatch(); + g.setColor(getProperties().getBrushColor().getColor()); + HwmfHatchStyle h = getProperties().getBrushHatch(); if (h == HwmfHatchStyle.HS_HORIZONTAL || h == HwmfHatchStyle.HS_CROSS) { g.drawLine(0, mid, dim, mid); } @@ -174,12 +207,13 @@ public class HwmfGraphics { if (h == HwmfHatchStyle.HS_BDIAGONAL || h == HwmfHatchStyle.HS_DIAGCROSS) { g.drawLine(0, dim, dim, 0); } + // TODO: handle new HS_* enumeration values g.dispose(); return new TexturePaint(bi, new Rectangle(0,0,dim,dim)); } protected Paint getPatternPaint() { - BufferedImage bi = prop.getBrushBitmap(); + BufferedImage bi = getProperties().getBrushBitmap(); return (bi == null) ? null : new TexturePaint(bi, new Rectangle(0,0,bi.getWidth(),bi.getHeight())); } @@ -196,15 +230,9 @@ public class HwmfGraphics { * @param entry */ public void addObjectTableEntry(HwmfObjectTableEntry entry) { - ListIterator oIter = objectTable.listIterator(); - while (oIter.hasNext()) { - HwmfObjectTableEntry tableEntry = oIter.next(); - if (tableEntry == null) { - oIter.set(entry); - return; - } - } - objectTable.add(entry); + int objIdx = objectIndexes.nextClearBit(0); + objectIndexes.set(objIdx); + objectTable.put(objIdx, entry); } /** @@ -237,15 +265,25 @@ public class HwmfGraphics { * @throws IndexOutOfBoundsException if the index is out of range */ public void unsetObjectTableEntry(int index) { - objectTable.set(index, null); + if (index < 0) { + throw new IndexOutOfBoundsException("Invalid index: "+index); + } + // sometime emfs remove object table entries before they set them + // so ignore requests, if the table entry doesn't exist + objectTable.remove(index); + objectIndexes.clear(index); } /** * Saves the current properties to the stack */ public void saveProperties() { - propStack.add(prop); - prop = new HwmfDrawProperties(prop); + final HwmfDrawProperties p = getProperties(); + assert(p != null); + p.setTransform(graphicsCtx.getTransform()); + p.setClip(graphicsCtx.getClip()); + propStack.add(p); + prop = newProperties(p); } /** @@ -260,7 +298,7 @@ public class HwmfGraphics { } int stackIndex = index; if (stackIndex < 0) { - int curIdx = propStack.indexOf(prop); + int curIdx = propStack.indexOf(getProperties()); if (curIdx == -1) { // the current element is not pushed to the stacked, i.e. it's the last curIdx = propStack.size(); @@ -271,7 +309,16 @@ public class HwmfGraphics { // roll to last when curIdx == 0 stackIndex = propStack.size()-1; } - prop = propStack.get(stackIndex); + + // The playback device context is restored by popping state information off a stack that was created by + // prior SAVEDC records + // ... so because being a stack, we will remove all entries having a greater index than the stackIndex + for (int i=propStack.size()-1; i>=stackIndex; i--) { + prop = propStack.remove(i); + } + + graphicsCtx.setTransform(prop.getTransform()); + graphicsCtx.setClip(prop.getClip()); } /** @@ -280,16 +327,20 @@ public class HwmfGraphics { * This methods gathers and sets the corresponding graphics transformations. */ public void updateWindowMapMode() { - Rectangle2D win = prop.getWindow(); - HwmfMapMode mapMode = prop.getMapMode(); + Rectangle2D win = getProperties().getWindow(); + Rectangle2D view = getProperties().getViewport(); + HwmfMapMode mapMode = getProperties().getMapMode(); graphicsCtx.setTransform(initialAT); switch (mapMode) { default: case MM_ANISOTROPIC: // scale window bounds to output bounds - graphicsCtx.scale(bbox.getWidth()/win.getWidth(), bbox.getHeight()/win.getHeight()); - graphicsCtx.translate(-win.getX(), -win.getY()); + if (view != null) { + graphicsCtx.translate(view.getCenterX(), view.getCenterY()); + graphicsCtx.scale(view.getWidth() / win.getWidth(), view.getHeight() / win.getHeight()); + graphicsCtx.translate(-win.getCenterX(), -win.getCenterY()); + } break; case MM_ISOTROPIC: // TODO: to be validated ... @@ -315,11 +366,21 @@ public class HwmfGraphics { } } - public void drawString(byte[] text, Rectangle2D bounds) { - drawString(text, bounds, null); + public void drawString(byte[] text, int length, Point2D reference) { + drawString(text, length, reference, null, null, null, null, false); } - public void drawString(byte[] text, Rectangle2D bounds, int dx[]) { + public void drawString(byte[] text, int length, Point2D reference, Dimension2D scale, Rectangle2D clip, WmfExtTextOutOptions opts, List dx, boolean isUnicode) { + final HwmfDrawProperties prop = getProperties(); + + final AffineTransform at = graphicsCtx.getTransform(); + + try { + at.createInverse(); + } catch (NoninvertibleTransformException e) { + return; + } + HwmfFont font = prop.getFont(); if (font == null || text == null || text.length == 0) { return; @@ -329,14 +390,34 @@ public class HwmfGraphics { // TODO: another approx. ... double fontW = fontH/1.8; - Charset charset = (font.getCharset().getCharset() == null)? - DEFAULT_CHARSET : font.getCharset().getCharset(); - String textString = new String(text, charset); - AttributedString as = new AttributedString(textString); - if (dx == null || dx.length == 0) { - addAttributes(as, font); + Charset charset; + if (isUnicode) { + charset = Charsets.UTF_16LE; } else { - int[] dxNormed = dx; + charset = font.getCharset().getCharset(); + if (charset == null) { + charset = DEFAULT_CHARSET; + } + } + + String textString = new String(text, charset).substring(0,length).trim(); + + 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 @@ -351,60 +432,101 @@ public class HwmfGraphics { // needs to be remapped as: //dxNormed[0] = 13 textString.get(0) = U+30D7 //dxNormed[1] = 14 textString.get(1) = U+30ED - if (textString.length() != text.length) { - int codePoints = textString.codePointCount(0, textString.length()); - dxNormed = new int[codePoints]; - int dxPosition = 0; - for (int offset = 0; offset < textString.length(); ) { - dxNormed[offset] = dx[dxPosition]; - int[] chars = new int[1]; - int cp = textString.codePointAt(offset); - chars[0] = cp; - //now figure out how many bytes it takes to encode that - //code point in the charset - int byteLength = new String(chars, 0, chars.length).getBytes(charset).length; - dxPosition += byteLength; - offset += Character.charCount(cp); - } - } - for (int i = 0; i < dxNormed.length; i++) { - addAttributes(as, font); - // 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 - if (i < dxNormed.length - 1) { - as.addAttribute(TextAttribute.TRACKING, (dxNormed[i] - fontW) / fontH, i + 1, i + 2); + + 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 AffineTransform at = graphicsCtx.getTransform(); + + final HwmfText.HwmfTextAlignment align = prop.getTextAlignLatin(); + final HwmfText.HwmfTextVerticalAlignment valign = prop.getTextVAlignLatin(); + final FontRenderContext frc = graphicsCtx.getFontRenderContext(); + final TextLayout layout = new TextLayout(as.getIterator(), frc); + + final Rectangle2D pixelBounds = layout.getBounds(); + + AffineTransform tx = new AffineTransform(); + switch (align) { + default: + case LEFT: + break; + case CENTER: + tx.translate(-pixelBounds.getWidth() / 2., 0); + break; + case RIGHT: + tx.translate(-pixelBounds.getWidth(), 0); + break; + } + + // TODO: check min/max orientation + switch (valign) { + case TOP: + tx.translate(0, layout.getAscent()); + 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 Shape clipShape = graphicsCtx.getClip(); try { - graphicsCtx.translate(bounds.getX(), bounds.getY()+fontH); - graphicsCtx.rotate(angle); - if (prop.getBkMode() == HwmfBkMode.OPAQUE) { - // TODO: validate bounds - graphicsCtx.setBackground(prop.getBackgroundColor().getColor()); - graphicsCtx.fill(new Rectangle2D.Double(0, 0, bounds.getWidth(), bounds.getHeight())); + if (clip != null) { + 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); } + + graphicsCtx.translate(reference.getX(), reference.getY()); + graphicsCtx.rotate(angle); + if (scale != null) { + graphicsCtx.scale(scale.getWidth() < 0 ? -1 : 1, scale.getHeight() < 0 ? -1 : 1); + } + graphicsCtx.translate(dst.getX(), dst.getY()); graphicsCtx.setColor(prop.getTextColor().getColor()); - graphicsCtx.drawString(as.getIterator(), 0, 0); // (float)bounds.getX(), (float)bounds.getY()); + graphicsCtx.drawString(as.getIterator(), 0, 0); } finally { graphicsCtx.setTransform(at); + graphicsCtx.setClip(clipShape); } } - - private void addAttributes(AttributedString as, HwmfFont font) { - DrawFontManager fontHandler = DrawFactory.getInstance(graphicsCtx).getFontManager(graphicsCtx); - FontInfo fontInfo = fontHandler.getMappedFont(graphicsCtx, font); - - as.addAttribute(TextAttribute.FAMILY, fontInfo.getTypeface()); + + private void addAttributes(AttributedString as, HwmfFont font, String typeface) { + as.addAttribute(TextAttribute.FAMILY, typeface); as.addAttribute(TextAttribute.SIZE, getFontHeight(font)); - as.addAttribute(TextAttribute.STRIKETHROUGH, font.isStrikeOut()); + if (font.isStrikeOut()) { + as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); + } if (font.isUnderline()) { as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); } @@ -428,4 +550,138 @@ public class HwmfGraphics { return fontHeight*3/4; } } + + public void drawImage(BufferedImage img, Rectangle2D srcBounds, Rectangle2D dstBounds) { + HwmfDrawProperties prop = getProperties(); + + // handle raster op + // currently the raster op as described in https://docs.microsoft.com/en-us/windows/desktop/gdi/ternary-raster-operations + // are not supported, as we would need to extract the destination image area from the underlying buffered image + // and therefore would make it mandatory that the graphics context must be from a buffered image + // furthermore I doubt the purpose of bitwise image operations on non-black/white images + switch (prop.getRasterOp()) { + case D: + // keep destination, i.e. do nothing + break; + case PATCOPY: + graphicsCtx.setPaint(getFill()); + graphicsCtx.fill(dstBounds); + break; + case BLACKNESS: + graphicsCtx.setPaint(Color.BLACK); + graphicsCtx.fill(dstBounds); + break; + case WHITENESS: + graphicsCtx.setPaint(Color.WHITE); + graphicsCtx.fill(dstBounds); + break; + default: + case SRCCOPY: + final Shape clip = graphicsCtx.getClip(); + + // add clipping in case of a source subimage, i.e. a clipped source image + // some dstBounds are horizontal or vertical flipped, so we need to normalize the images + Rectangle2D normalized = new Rectangle2D.Double( + dstBounds.getWidth() >= 0 ? dstBounds.getMinX() : dstBounds.getMaxX(), + dstBounds.getHeight() >= 0 ? dstBounds.getMinY() : dstBounds.getMaxY(), + Math.abs(dstBounds.getWidth()), + Math.abs(dstBounds.getHeight())); + graphicsCtx.clip(normalized); + final AffineTransform at = graphicsCtx.getTransform(); + + final Rectangle2D imgBounds = new Rectangle2D.Double(0,0,img.getWidth(),img.getHeight()); + final boolean isImgBounds = (srcBounds.equals(new Rectangle2D.Double())); + final Rectangle2D srcBounds2 = isImgBounds ? imgBounds : srcBounds; + + // TODO: apply emf transform + graphicsCtx.translate(dstBounds.getX(), dstBounds.getY()); + graphicsCtx.scale(dstBounds.getWidth()/srcBounds2.getWidth(), dstBounds.getHeight()/srcBounds2.getHeight()); + graphicsCtx.translate(-srcBounds2.getX(), -srcBounds2.getY()); + + graphicsCtx.drawImage(img, 0, 0, prop.getBackgroundColor().getColor(), null); + + graphicsCtx.setTransform(at); + graphicsCtx.setClip(clip); + break; + } + + } + + /** + * @return the initial AffineTransform, when this graphics context was created + */ + public AffineTransform getInitTransform() { + return new AffineTransform(initialAT); + } + + /** + * @return the current AffineTransform + */ + public AffineTransform getTransform() { + return new AffineTransform(graphicsCtx.getTransform()); + } + + /** + * Set the current AffineTransform + * @param tx the current AffineTransform + */ + public void setTransform(AffineTransform tx) { + graphicsCtx.setTransform(tx); + } + + private static int clipCnt = 0; + + public void setClip(Shape clip, HwmfRegionMode regionMode, boolean useInitialAT) { + final AffineTransform at = graphicsCtx.getTransform(); + if (useInitialAT) { + graphicsCtx.setTransform(initialAT); + } + final Shape oldClip = graphicsCtx.getClip(); + final boolean isEmpty = clip.getBounds2D().isEmpty(); + switch (regionMode) { + case RGN_AND: + if (!isEmpty) { + graphicsCtx.clip(clip); + } + break; + case RGN_OR: + if (!isEmpty) { + if (oldClip == null) { + graphicsCtx.setClip(clip); + } else { + Area area = new Area(oldClip); + area.add(new Area(clip)); + graphicsCtx.setClip(area); + } + } + break; + case RGN_XOR: + if (!isEmpty) { + if (oldClip == null) { + graphicsCtx.setClip(clip); + } else { + Area area = new Area(oldClip); + area.exclusiveOr(new Area(clip)); + graphicsCtx.setClip(area); + } + } + break; + case RGN_DIFF: + if (!isEmpty) { + if (oldClip != null) { + Area area = new Area(oldClip); + area.subtract(new Area(clip)); + graphicsCtx.setClip(area); + } + } + break; + case RGN_COPY: { + graphicsCtx.setClip(isEmpty ? null : clip); + break; + } + } + if (useInitialAT) { + graphicsCtx.setTransform(at); + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfSLImageRenderer.java b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfImageRenderer.java similarity index 91% rename from src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfSLImageRenderer.java rename to src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfImageRenderer.java index e2601bc65e..a87db042d6 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfSLImageRenderer.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfImageRenderer.java @@ -31,20 +31,25 @@ import java.io.InputStream; import org.apache.poi.hwmf.usermodel.HwmfPicture; import org.apache.poi.sl.draw.DrawPictureShape; import org.apache.poi.sl.draw.ImageRenderer; -import org.apache.poi.sl.usermodel.PictureData; +import org.apache.poi.sl.usermodel.PictureData.PictureType; import org.apache.poi.util.Units; /** * Helper class which is instantiated by {@link DrawPictureShape} * via reflection */ -public class HwmfSLImageRenderer implements ImageRenderer { +public class HwmfImageRenderer implements ImageRenderer { HwmfPicture image; double alpha; - + + @Override + public boolean canRender(String contentType) { + return PictureType.WMF.contentType.equalsIgnoreCase(contentType); + } + @Override public void loadImage(InputStream data, String contentType) throws IOException { - if (!PictureData.PictureType.WMF.contentType.equals(contentType)) { + if (!PictureType.WMF.contentType.equals(contentType)) { throw new IOException("Invalid picture type"); } image = new HwmfPicture(data); @@ -52,7 +57,7 @@ public class HwmfSLImageRenderer implements ImageRenderer { @Override public void loadImage(byte[] data, String contentType) throws IOException { - if (!PictureData.PictureType.WMF.contentType.equals(contentType)) { + if (!PictureType.WMF.contentType.equals(contentType)) { throw new IOException("Invalid picture type"); } image = new HwmfPicture(new ByteArrayInputStream(data)); diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java index 82f593963e..a800bb0e05 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java @@ -17,14 +17,21 @@ package org.apache.poi.hwmf.record; -import javax.imageio.ImageIO; +import java.awt.AlphaComposite; +import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; +import java.awt.LinearGradientPaint; +import java.awt.MultipleGradientPaint; +import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import javax.imageio.ImageIO; + +import org.apache.poi.hwmf.usermodel.HwmfPicture; import org.apache.poi.util.IOUtils; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianConsts; @@ -38,9 +45,11 @@ import org.apache.poi.util.RecordFormatException; */ public class HwmfBitmapDib { - private static final int MAX_RECORD_LENGTH = 10000000; + private static final POILogger logger = POILogFactory.getLogger(HwmfBitmapDib.class); + private static final int BMP_HEADER_SIZE = 14; + private static final int MAX_RECORD_LENGTH = HwmfPicture.MAX_RECORD_LENGTH; - public static enum BitCount { + public enum BitCount { /** * The image SHOULD be in either JPEG or PNG format. <6> Neither of these formats includes * a color table, so this value specifies that no color table is present. See [JFIF] and [RFC2083] @@ -122,7 +131,7 @@ public class HwmfBitmapDib { } } - public static enum Compression { + public enum Compression { /** * The bitmap is in uncompressed red green blue (RGB) format that is not compressed * and does not use color masks. @@ -191,9 +200,7 @@ public class HwmfBitmapDib { } } - private final static POILogger logger = POILogFactory.getLogger(HwmfBitmapDib.class); - private static final int BMP_HEADER_SIZE = 14; - + private int headerSize; private int headerWidth; private int headerHeight; @@ -225,14 +232,36 @@ public class HwmfBitmapDib { introSize += readColors(leis); assert(introSize < 10000); - int fileSize = (headerImageSize < headerSize) ? recordSize : (int)Math.min(introSize+headerImageSize,recordSize); - leis.reset(); - imageData = IOUtils.toByteArray(leis, fileSize); - - assert( headerSize != 0x0C || ((((headerWidth * headerPlanes * headerBitCount.flag + 31) & ~31) / 8) * Math.abs(headerHeight)) == headerImageSize); - return fileSize; + // The size and format of this data is determined by information in the DIBHeaderInfo field. If + // it is a BitmapCoreHeader, the size in bytes MUST be calculated as follows: + + int bodySize = ((((headerWidth * headerPlanes * headerBitCount.flag + 31) & ~31) / 8) * Math.abs(headerHeight)); + + // This formula SHOULD also be used to calculate the size of aData when DIBHeaderInfo is a + // BitmapInfoHeader Object, using values from that object, but only if its Compression value is + // BI_RGB, BI_BITFIELDS, or BI_CMYK. + // Otherwise, the size of aData MUST be the BitmapInfoHeader Object value ImageSize. + + assert( headerSize != 0x0C || bodySize == headerImageSize); + + if (headerSize == 0x0C || + headerCompression == Compression.BI_RGB || + headerCompression == Compression.BI_BITFIELDS || + headerCompression == Compression.BI_CMYK) { + int fileSize = (int)Math.min(introSize+bodySize,recordSize); + imageData = IOUtils.safelyAllocate(fileSize, MAX_RECORD_LENGTH); + leis.readFully(imageData, 0, introSize); + leis.skipFully(recordSize-fileSize); + // emfs are sometimes truncated, read as much as possible + int readBytes = leis.read(imageData, introSize, fileSize-introSize); + return introSize+(recordSize-fileSize)+readBytes; + } else { + imageData = IOUtils.safelyAllocate(recordSize, MAX_RECORD_LENGTH); + leis.readFully(imageData); + return recordSize; + } } protected int readHeader(LittleEndianInputStream leis) throws IOException { @@ -262,6 +291,9 @@ public class HwmfBitmapDib { headerBitCount = BitCount.valueOf(leis.readUShort()); size += 4*LittleEndianConsts.SHORT_SIZE; } else { + // fix header size, sometimes this is invalid + headerSize = 40; + // BitmapInfoHeader // A 32-bit signed integer that defines the width of the DIB, in pixels. // This value MUST be positive. @@ -306,7 +338,6 @@ public class HwmfBitmapDib { headerColorImportant = leis.readUInt(); size += 8*LittleEndianConsts.INT_SIZE+2*LittleEndianConsts.SHORT_SIZE; } - assert(size == headerSize); return size; } @@ -374,11 +405,35 @@ public class HwmfBitmapDib { return size; } + public boolean isValid() { + // the recordsize ended before the image data + if (imageData == null) { + return false; + } + + // ignore all black mono-brushes + if (this.headerBitCount == BitCount.BI_BITCOUNT_1) { + if (colorTable == null) { + return false; + } + + for (Color c : colorTable) { + if (!Color.BLACK.equals(c)) { + return true; + } + } + + return false; + } + + return true; + } + public InputStream getBMPStream() { return new ByteArrayInputStream(getBMPData()); } - private byte[] getBMPData() { + public byte[] getBMPData() { if (imageData == null) { throw new RecordFormatException("bitmap not initialized ... need to call init() before"); } @@ -407,14 +462,56 @@ public class HwmfBitmapDib { public BufferedImage getImage() { try { return ImageIO.read(getBMPStream()); - } catch (IOException e) { - logger.log(POILogger.ERROR, "invalid bitmap data - returning black opaque image"); - BufferedImage bi = new BufferedImage(headerWidth, headerHeight, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = bi.createGraphics(); - g.setPaint(Color.black); - g.fillRect(0, 0, headerWidth, headerHeight); - g.dispose(); - return bi; + } catch (IOException|RuntimeException e) { + logger.log(POILogger.ERROR, "invalid bitmap data - returning placeholder image"); + return getPlaceholder(); } } + + @Override + public String toString() { + return + "{ headerSize: " + headerSize + + ", width: " + headerWidth + + ", height: " + headerHeight + + ", planes: " + headerPlanes + + ", bitCount: '" + headerBitCount + "'" + + ", compression: '" + headerCompression + "'" + + ", imageSize: " + headerImageSize + + ", xPelsPerMeter: " + headerXPelsPerMeter + + ", yPelsPerMeter: " + headerYPelsPerMeter + + ", colorUsed: " + headerColorUsed + + ", colorImportant: " + headerColorImportant + + ", imageSize: " + (imageData == null ? 0 : imageData.length) + + "}"; + } + + protected BufferedImage getPlaceholder() { + BufferedImage bi = new BufferedImage(headerWidth, headerHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = bi.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, headerWidth, headerHeight); + + final int arcs = Math.min(headerWidth, headerHeight) / 7; + + Color bg = Color.LIGHT_GRAY; + Color fg = Color.GRAY; + LinearGradientPaint lgp = new LinearGradientPaint(0f, 0f, 5, 5, + new float[] {0,.1f,.1001f}, new Color[] {fg,fg,bg}, MultipleGradientPaint.CycleMethod.REFLECT); + g.setComposite(AlphaComposite.SrcOver.derive(0.4f)); + g.setPaint(lgp); + g.fillRoundRect(0, 0, headerWidth-1, headerHeight-1, arcs, arcs); + + g.setColor(Color.DARK_GRAY); + g.setComposite(AlphaComposite.Src); + g.setStroke(new BasicStroke(2)); + g.drawRoundRect(0, 0, headerWidth-1, headerHeight-1, arcs, arcs); + g.dispose(); + return bi; + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBrushStyle.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBrushStyle.java index 2e8122fb15..903ba592ac 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBrushStyle.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBrushStyle.java @@ -71,7 +71,7 @@ public enum HwmfBrushStyle { this.flag = flag; } - static HwmfBrushStyle valueOf(int flag) { + public static HwmfBrushStyle valueOf(int flag) { for (HwmfBrushStyle bs : values()) { if (bs.flag == flag) return bs; } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfColorRef.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfColorRef.java index 5b24fbc7e9..a757617752 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfColorRef.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfColorRef.java @@ -19,6 +19,7 @@ package org.apache.poi.hwmf.record; import java.awt.Color; import java.io.IOException; +import java.util.Locale; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -53,6 +54,10 @@ public class HwmfColorRef implements Cloneable { return colorRef; } + public void setColor(Color color) { + colorRef = color; + } + /** * Creates a new object of the same class and with the * same contents as this object. @@ -69,4 +74,9 @@ public class HwmfColorRef implements Cloneable { throw new InternalError(); } } + + @Override + public String toString() { + return String.format(Locale.ROOT, "%#08X", colorRef.getRGB()&0xFFFFFF); + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java index 48bcc60b44..17cd3c83d8 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java @@ -20,9 +20,11 @@ package org.apache.poi.hwmf.record; import java.awt.Shape; import java.awt.geom.Arc2D; import java.awt.geom.Area; +import java.awt.geom.Dimension2D; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; @@ -31,6 +33,8 @@ import java.util.ArrayList; import java.util.List; import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.hwmf.draw.HwmfGraphics.FillDrawStyle; +import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -41,31 +45,26 @@ public class HwmfDraw { */ public static class WmfMoveTo implements HwmfRecord { - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units. - */ - private int y; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units. - */ - private int x; + protected final Point2D point = new Point2D.Double(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.moveTo; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - y = leis.readShort(); - x = leis.readShort(); - return 2*LittleEndianConsts.SHORT_SIZE; + return readPointS(leis, point); } @Override public void draw(HwmfGraphics ctx) { - ctx.getProperties().setLocation(x, y); + ctx.getProperties().setLocation(point); + } + + @Override + public String toString() { + return pointToString(point); } } @@ -75,36 +74,29 @@ public class HwmfDraw { */ public static class WmfLineTo implements HwmfRecord { - /** - * A 16-bit signed integer that defines the vertical component of the drawing - * destination position, in logical units. - */ - private int y; - - /** - * A 16-bit signed integer that defines the horizontal component of the drawing - * destination position, in logical units. - */ - private int x; + protected final Point2D point = new Point2D.Double(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.lineTo; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - y = leis.readShort(); - x = leis.readShort(); - return 2*LittleEndianConsts.SHORT_SIZE; + return readPointS(leis, point); } @Override public void draw(HwmfGraphics ctx) { Point2D start = ctx.getProperties().getLocation(); - Line2D line = new Line2D.Double(start.getX(), start.getY(), x, y); + Line2D line = new Line2D.Double(start, point); ctx.draw(line); - ctx.getProperties().setLocation(x, y); + ctx.getProperties().setLocation(point); + } + + @Override + public String toString() { + return pointToString(point); } } @@ -115,10 +107,10 @@ public class HwmfDraw { */ public static class WmfPolygon implements HwmfRecord { - private Path2D poly = new Path2D.Double(); + protected Path2D poly; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.polygon; } @@ -129,6 +121,7 @@ public class HwmfDraw { */ int numberofPoints = leis.readShort(); + poly = new Path2D.Double(Path2D.WIND_EVEN_ODD, numberofPoints); for (int i=0; i polyList = new ArrayList<>(); + protected final List polyList = new ArrayList<>(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.polyPolygon; } @@ -325,7 +314,7 @@ public class HwmfDraw { * An array of 16-bit signed integers that define the coordinates of the polygons. * (Note: MS-WMF wrongly says unsigned integers ...) */ - Path2D poly = new Path2D.Double(); + Path2D poly = new Path2D.Double(Path2D.WIND_EVEN_ODD, nPoints); for (int i=0; i 0 ? 0 : 360); if (startAngle < 0) { startAngle += 360; @@ -608,28 +579,32 @@ public class HwmfDraw { boolean fillShape; int arcClosure; - switch (getRecordType()) { + switch (getWmfRecordType()) { default: case arc: arcClosure = Arc2D.OPEN; - fillShape = false; break; case chord: arcClosure = Arc2D.CHORD; - fillShape = true; break; case pie: arcClosure = Arc2D.PIE; - fillShape = true; break; } - - Shape s = new Arc2D.Double(x, y, w, h, startAngle, arcAngle, arcClosure); - if (fillShape) { - ctx.fill(s); - } else { - ctx.draw(s); - } + + return new Arc2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), startAngle, arcAngle, arcClosure); + } + + @Override + public String toString() { + Arc2D arc = getShape(); + return + "{ startPoint: "+pointToString(startPoint)+ + ", endPoint: "+pointToString(endPoint)+ + ", startAngle: "+arc.getAngleStart()+ + ", extentAngle: "+arc.getAngleExtent()+ + ", bounds: "+boundsToString(bounds)+ + " }"; } } @@ -641,7 +616,7 @@ public class HwmfDraw { public static class WmfPie extends WmfArc { @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.pie; } } @@ -654,7 +629,7 @@ public class HwmfDraw { public static class WmfChord extends WmfArc { @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.chord; } } @@ -673,10 +648,10 @@ public class HwmfDraw { * A 16-bit unsigned integer used to index into the WMF Object Table to * get the object to be selected. */ - private int objectIndex; + protected int objectIndex; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.selectObject; } @@ -690,9 +665,113 @@ public class HwmfDraw { public void draw(HwmfGraphics ctx) { ctx.applyObjectTableEntry(objectIndex); } + + @Override + public String toString() { + return "{ index: "+objectIndex +" }"; + } } - private static int getWindingRule(HwmfGraphics ctx) { - return ctx.getProperties().getPolyfillMode().awtFlag; + static int readBounds(LittleEndianInputStream leis, Rectangle2D bounds) { + /** + * The 16-bit signed integers that defines the corners of the bounding rectangle. + */ + int bottom = leis.readShort(); + int right = leis.readShort(); + int top = leis.readShort(); + int left = leis.readShort(); + + int x = Math.min(left, right); + int y = Math.min(top, bottom); + int w = Math.abs(left - right - 1); + int h = Math.abs(top - bottom - 1); + + bounds.setRect(x, y, w, h); + + return 4 * LittleEndianConsts.SHORT_SIZE; } - } + + static int readRectS(LittleEndianInputStream leis, Rectangle2D bounds) { + /** + * The 16-bit signed integers that defines the corners of the bounding rectangle. + */ + int left = leis.readShort(); + int top = leis.readShort(); + int right = leis.readShort(); + int bottom = leis.readShort(); + + int x = Math.min(left, right); + int y = Math.min(top, bottom); + int w = Math.abs(left - right - 1); + int h = Math.abs(top - bottom - 1); + + bounds.setRect(x, y, w, h); + + return 4 * LittleEndianConsts.SHORT_SIZE; + } + + static int readPointS(LittleEndianInputStream leis, Point2D point) { + /** a signed integer that defines the x/y-coordinate, in logical units. */ + int y = leis.readShort(); + int x = leis.readShort(); + point.setLocation(x, y); + return 2*LittleEndianConsts.SHORT_SIZE; + } + + static String polyToString(Path2D poly) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + final PathIterator iter = poly.getPathIterator(null); + double[] pnts = new double[6]; + while (!iter.isDone()) { + int segType = iter.currentSegment(pnts); + switch (segType) { + case PathIterator.SEG_MOVETO: + sb.append("{ type: 'move', x: "+pnts[0]+", y: "+pnts[1]+" }, "); + break; + case PathIterator.SEG_LINETO: + sb.append("{ type: 'lineto', x: "+pnts[0]+", y: "+pnts[1]+" }, "); + break; + case PathIterator.SEG_QUADTO: + sb.append("{ type: 'quad', x1: "+pnts[0]+", y1: "+pnts[1]+", x2: "+pnts[2]+", y2: "+pnts[3]+" }, "); + break; + case PathIterator.SEG_CUBICTO: + sb.append("{ type: 'cubic', x1: "+pnts[0]+", y1: "+pnts[1]+", x2: "+pnts[2]+", y2: "+pnts[3]+", x3: "+pnts[4]+", y3: "+pnts[5]+" }, "); + break; + case PathIterator.SEG_CLOSE: + sb.append("{ type: 'close' }, "); + break; + } + iter.next(); + } + sb.append("]"); + return sb.toString(); + } + + @Internal + public static String pointToString(Point2D point) { + return "{ x: "+point.getX()+", y: "+point.getY()+" }"; + } + + @Internal + public static String boundsToString(Rectangle2D bounds) { + return "{ x: "+bounds.getX()+", y: "+bounds.getY()+", w: "+bounds.getWidth()+", h: "+bounds.getHeight()+" }"; + } + + @Internal + public static String dimToString(Dimension2D dim) { + return "{ w: "+dim.getWidth()+", h: "+dim.getHeight()+" }"; + } + + @Internal + public static Rectangle2D normalizeBounds(Rectangle2D bounds) { + return (bounds.getWidth() >= 0 && bounds.getHeight() >= 0) ? bounds + : new Rectangle2D.Double( + bounds.getWidth() >= 0 ? bounds.getMinX() : bounds.getMaxX(), + bounds.getHeight() >= 0 ? bounds.getMinY() : bounds.getMaxY(), + Math.abs(bounds.getWidth()), + Math.abs(bounds.getHeight()) + ); + } + +} diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfEscape.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfEscape.java index 6c7ef213b0..a1de4dca46 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfEscape.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfEscape.java @@ -185,7 +185,7 @@ public class HwmfEscape implements HwmfRecord { private byte escapeData[]; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.escape; } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java index ec4d070540..3b420e401d 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java @@ -17,11 +17,17 @@ package org.apache.poi.hwmf.record; +import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; +import static org.apache.poi.hwmf.record.HwmfDraw.readPointS; + import java.awt.Shape; import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; +import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -62,7 +68,7 @@ public class HwmfFill { this.flag = flag; } - static ColorUsage valueOf(int flag) { + public static ColorUsage valueOf(int flag) { for (ColorUsage bs : values()) { if (bs.flag == flag) return bs; } @@ -80,16 +86,16 @@ public class HwmfFill { * A 16-bit unsigned integer used to index into the WMF Object Table to get * the region to be filled. */ - private int regionIndex; + protected int regionIndex; /** * A 16-bit unsigned integer used to index into the WMF Object Table to get the * brush to use for filling the region. */ - private int brushIndex; + protected int brushIndex; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.fillRegion; } @@ -125,7 +131,7 @@ public class HwmfFill { */ int regionIndex; - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.paintRegion; } @@ -155,31 +161,21 @@ public class HwmfFill { /** * A 32-bit ColorRef Object that defines the color value. */ - private HwmfColorRef colorRef; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * point where filling is to start. - */ - private int yStart; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * point where filling is to start. - */ - private int xStart; - - + protected final HwmfColorRef colorRef = new HwmfColorRef(); + + /** the point where filling is to start. */ + protected final Point2D start = new Point2D.Double(); + @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.floodFill; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - colorRef = new HwmfColorRef(); int size = colorRef.init(leis); - yStart = leis.readShort(); - xStart = leis.readShort(); - return size+2*LittleEndianConsts.SHORT_SIZE; + size += readPointS(leis, start); + return size; } @Override @@ -215,34 +211,39 @@ public class HwmfFill { this.awtFlag = awtFlag; } - static HwmfPolyfillMode valueOf(int wmfFlag) { + public static HwmfPolyfillMode valueOf(int wmfFlag) { for (HwmfPolyfillMode pm : values()) { if (pm.wmfFlag == wmfFlag) return pm; } return null; } } - + /** - * A 16-bit unsigned integer that defines polygon fill mode. + * An unsigned integer that defines polygon fill mode. * This MUST be one of the values: ALTERNATE = 0x0001, WINDING = 0x0002 */ - private HwmfPolyfillMode polyfillMode; + protected HwmfPolyfillMode polyFillMode; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setPolyFillMode; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - polyfillMode = HwmfPolyfillMode.valueOf(leis.readUShort() & 3); + polyFillMode = HwmfPolyfillMode.valueOf(leis.readUShort() & 3); return LittleEndianConsts.SHORT_SIZE; } @Override public void draw(HwmfGraphics ctx) { - ctx.getProperties().setPolyfillMode(polyfillMode); + ctx.getProperties().setPolyfillMode(polyFillMode); + } + + @Override + public String toString() { + return "{ polyFillMode: '"+ polyFillMode +"' }"; } } @@ -251,7 +252,7 @@ public class HwmfFill { * The META_EXTFLOODFILL record fills an area with the brush that is defined in * the playback device context. */ - public static class WmfExtFloodFill implements HwmfRecord { + public static class WmfExtFloodFill extends WmfFloodFill { /** * A 16-bit unsigned integer that defines the fill operation to be performed. This @@ -266,38 +267,17 @@ public class HwmfFill { * Filling continues outward in all directions as long as the color is encountered. * This style is useful for filling areas with multicolored boundaries. */ - private int mode; - - /** - * A 32-bit ColorRef Object that defines the color value. - */ - private HwmfColorRef colorRef; - - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the point - * to be set. - */ - private int y; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the point - * to be set. - */ - private int x; + protected int mode; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.extFloodFill; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { mode = leis.readUShort(); - colorRef = new HwmfColorRef(); - int size = colorRef.init(leis); - y = leis.readShort(); - x = leis.readShort(); - return size+3*LittleEndianConsts.SHORT_SIZE; + return super.init(leis, recordSize, recordFunction)+LittleEndianConsts.SHORT_SIZE; } @Override @@ -318,7 +298,7 @@ public class HwmfFill { private int region; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.invertRegion; } @@ -348,30 +328,10 @@ public class HwmfFill { */ private HwmfTernaryRasterOp rasterOperation; - /** - * A 16-bit signed integer that defines the height, in logical units, of the rectangle. - */ - private int height; - - /** - * A 16-bit signed integer that defines the width, in logical units, of the rectangle. - */ - private int width; - - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * upper-left corner of the rectangle to be filled. - */ - private int yLeft; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * upper-left corner of the rectangle to be filled. - */ - private int xLeft; - + private final Rectangle2D bounds = new Rectangle2D.Double(); + @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.patBlt; } @@ -383,12 +343,7 @@ public class HwmfFill { rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex); assert(rasterOpCode == rasterOperation.opCode); - height = leis.readShort(); - width = leis.readShort(); - yLeft = leis.readShort(); - xLeft = leis.readShort(); - - return 6*LittleEndianConsts.SHORT_SIZE; + return readBounds2(leis, bounds)+2*LittleEndianConsts.SHORT_SIZE; } @Override @@ -414,53 +369,22 @@ public class HwmfFill { * in the playback device context, and the destination pixels are to be combined to form the new * image. This code MUST be one of the values in the Ternary Raster Operation Enumeration */ - private HwmfTernaryRasterOp rasterOperation; - - /** - * A 16-bit signed integer that defines the height, in logical units, of the source rectangle. - */ - private int srcHeight; - /** - * A 16-bit signed integer that defines the width, in logical units, of the source rectangle. - */ - private int srcWidth; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the upper-left corner - * of the source rectangle. - */ - private int ySrc; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the upper-left corner - * of the source rectangle. - */ - private int xSrc; - /** - * A 16-bit signed integer that defines the height, in logical units, of the destination rectangle. - */ - private int destHeight; - /** - * A 16-bit signed integer that defines the width, in logical units, of the destination rectangle. - */ - private int destWidth; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the upper-left - * corner of the destination rectangle. - */ - private int yDest; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the upper-left - * corner of the destination rectangle. - */ - private int xDest; - + protected HwmfTernaryRasterOp rasterOperation; + + /** the source rectangle */ + protected final Rectangle2D srcBounds = new Rectangle2D.Double(); + + /** the destination rectangle */ + protected final Rectangle2D dstBounds = new Rectangle2D.Double(); + /** * A variable-sized Bitmap16 Object that defines source image content. * This object MUST be specified, even if the raster operation does not require a source. */ - HwmfBitmap16 target; + protected HwmfBitmap16 target; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.stretchBlt; } @@ -469,27 +393,23 @@ public class HwmfFill { public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { boolean hasBitmap = (recordSize > ((recordFunction >> 8) + 3)); - int size = 0; int rasterOpCode = leis.readUShort(); int rasterOpIndex = leis.readUShort(); rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex); assert(rasterOpCode == rasterOperation.opCode); - srcHeight = leis.readShort(); - srcWidth = leis.readShort(); - ySrc = leis.readShort(); - xSrc = leis.readShort(); - size = 6*LittleEndianConsts.SHORT_SIZE; + int size = 2*LittleEndianConsts.SHORT_SIZE; + + size += readBounds2(leis, srcBounds); + if (!hasBitmap) { /*int reserved =*/ leis.readShort(); size += LittleEndianConsts.SHORT_SIZE; } - destHeight = leis.readShort(); - destWidth = leis.readShort(); - yDest = leis.readShort(); - xDest = leis.readShort(); - size += 4*LittleEndianConsts.SHORT_SIZE; + + size += readBounds2(leis, dstBounds); + if (hasBitmap) { target = new HwmfBitmap16(); size += target.init(leis); @@ -502,6 +422,15 @@ public class HwmfFill { public void draw(HwmfGraphics ctx) { } + + @Override + public String toString() { + return + "{ rasterOperation: '"+rasterOperation+"'"+ + ", srcBounds: "+boundsToString(srcBounds)+ + ", dstBounds: "+boundsToString(dstBounds)+ + "}"; + } } /** @@ -511,67 +440,34 @@ public class HwmfFill { * The source of the color data is a DIB, and the destination of the transfer is * the current output region in the playback device context. */ - public static class WmfStretchDib implements HwmfRecord, HwmfImageRecord, HwmfObjectTableEntry { + public static class WmfStretchDib implements HwmfRecord, HwmfImageRecord { /** * A 32-bit unsigned integer that defines how the source pixels, the current brush in * the playback device context, and the destination pixels are to be combined to * form the new image. */ - private HwmfTernaryRasterOp rasterOperation; + protected HwmfTernaryRasterOp rasterOperation; /** * A 16-bit unsigned integer that defines whether the Colors field of the * DIB contains explicit RGB values or indexes into a palette. */ - private ColorUsage colorUsage; - /** - * A 16-bit signed integer that defines the height, in logical units, of the - * source rectangle. - */ - private int srcHeight; - /** - * A 16-bit signed integer that defines the width, in logical units, of the - * source rectangle. - */ - private int srcWidth; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * source rectangle. - */ - private int ySrc; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * source rectangle. - */ - private int xSrc; - /** - * A 16-bit signed integer that defines the height, in logical units, of the - * destination rectangle. - */ - private int destHeight; - /** - * A 16-bit signed integer that defines the width, in logical units, of the - * destination rectangle. - */ - private int destWidth; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * upper-left corner of the destination rectangle. - */ - private int yDst; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * upper-left corner of the destination rectangle. - */ - private int xDst; + protected ColorUsage colorUsage; + + /** the source rectangle. */ + protected final Rectangle2D srcBounds = new Rectangle2D.Double(); + + /** the destination rectangle. */ + protected final Rectangle2D dstBounds = new Rectangle2D.Double(); + /** * A variable-sized DeviceIndependentBitmap Object (section 2.2.2.9) that is the * source of the color data. */ - private HwmfBitmapDib dib; + protected final HwmfBitmapDib bitmap = new HwmfBitmapDib(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.stretchDib; } @@ -585,85 +481,49 @@ public class HwmfFill { assert(rasterOpCode == rasterOperation.opCode); colorUsage = ColorUsage.valueOf(leis.readUShort()); - srcHeight = leis.readShort(); - srcWidth = leis.readShort(); - ySrc = leis.readShort(); - xSrc = leis.readShort(); - destHeight = leis.readShort(); - destWidth = leis.readShort(); - yDst = leis.readShort(); - xDst = leis.readShort(); - - int size = 11*LittleEndianConsts.SHORT_SIZE; - dib = new HwmfBitmapDib(); - size += dib.init(leis, (int)(recordSize-6-size)); + + int size = 3*LittleEndianConsts.SHORT_SIZE; + + size += readBounds2(leis, srcBounds); + size += readBounds2(leis, dstBounds); + + size += bitmap.init(leis, (int)(recordSize-6-size)); return size; } @Override public void draw(HwmfGraphics ctx) { - ctx.addObjectTableEntry(this); - } - - @Override - public void applyObject(HwmfGraphics ctx) { - + HwmfDrawProperties prop = ctx.getProperties(); + prop.setRasterOp(rasterOperation); + if (bitmap.isValid()) { + ctx.drawImage(getImage(), srcBounds, dstBounds); + } else if (!dstBounds.isEmpty()) { + BufferedImage bi = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + ctx.drawImage(bi, new Rectangle2D.Double(0,0,100,100), dstBounds); + } } @Override public BufferedImage getImage() { - return dib.getImage(); + return bitmap.getImage(); + } + + @Override + public String toString() { + return + "{ rasterOperation: '"+rasterOperation+"'"+ + ", colorUsage: '"+colorUsage+"'"+ + ", srcBounds: "+boundsToString(srcBounds)+ + ", dstBounds: "+boundsToString(dstBounds)+ + "}"; } } - public static class WmfBitBlt implements HwmfRecord { - - /** - * A 32-bit unsigned integer that defines how the source pixels, the current brush in the playback - * device context, and the destination pixels are to be combined to form the new image. - */ - private HwmfTernaryRasterOp rasterOperation; - - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the upper-left corner - of the source rectangle. - */ - private int ySrc; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the upper-left corner - of the source rectangle. - */ - private int xSrc; - /** - * A 16-bit signed integer that defines the height, in logical units, of the source and - destination rectangles. - */ - private int height; - /** - * A 16-bit signed integer that defines the width, in logical units, of the source and destination - rectangles. - */ - private int width; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the upper-left - corner of the destination rectangle. - */ - private int yDest; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the upper-left - corner of the destination rectangle. - */ - private int xDest; - - /** - * A variable-sized Bitmap16 Object that defines source image content. - * This object MUST be specified, even if the raster operation does not require a source. - */ - private HwmfBitmap16 target; + public static class WmfBitBlt extends WmfStretchBlt { @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.bitBlt; } @@ -671,41 +531,33 @@ public class HwmfFill { public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { boolean hasBitmap = (recordSize/2 != ((recordFunction >> 8) + 3)); - int size = 0; int rasterOpCode = leis.readUShort(); int rasterOpIndex = leis.readUShort(); rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex); assert(rasterOpCode == rasterOperation.opCode); - ySrc = leis.readShort(); - xSrc = leis.readShort(); + int size = 2*LittleEndianConsts.SHORT_SIZE; + + final Point2D srcPnt = new Point2D.Double(); + size += readPointS(leis, srcPnt); - size = 4*LittleEndianConsts.SHORT_SIZE; - if (!hasBitmap) { /*int reserved =*/ leis.readShort(); size += LittleEndianConsts.SHORT_SIZE; } - - height = leis.readShort(); - width = leis.readShort(); - yDest = leis.readShort(); - xDest = leis.readShort(); - size += 4*LittleEndianConsts.SHORT_SIZE; + size += readBounds2(leis, dstBounds); + if (hasBitmap) { target = new HwmfBitmap16(); size += target.init(leis); } + + srcBounds.setRect(srcPnt.getX(), srcPnt.getY(), dstBounds.getWidth(), dstBounds.getHeight()); return size; } - - @Override - public void draw(HwmfGraphics ctx) { - - } } @@ -729,36 +581,13 @@ public class HwmfFill { * A 16-bit unsigned integer that defines the starting scan line in the source. */ private int startScan; - /** - * A 16-bit unsigned integer that defines the y-coordinate, in logical units, of the - * source rectangle. - */ - private int yDib; - /** - * A 16-bit unsigned integer that defines the x-coordinate, in logical units, of the - * source rectangle. - */ - private int xDib; - /** - * A 16-bit unsigned integer that defines the height, in logical units, of the - * source and destination rectangles. - */ - private int height; - /** - * A 16-bit unsigned integer that defines the width, in logical units, of the - * source and destination rectangles. - */ - private int width; - /** - * A 16-bit unsigned integer that defines the y-coordinate, in logical units, of the - * upper-left corner of the destination rectangle. - */ - private int yDest; - /** - * A 16-bit unsigned integer that defines the x-coordinate, in logical units, of the - * upper-left corner of the destination rectangle. - */ - private int xDest; + + /** the source rectangle */ + protected final Rectangle2D srcBounds = new Rectangle2D.Double(); + + /** the destination rectangle, having the same dimension as the source rectangle */ + protected final Rectangle2D dstBounds = new Rectangle2D.Double(); + /** * A variable-sized DeviceIndependentBitmap Object that is the source of the color data. */ @@ -766,7 +595,7 @@ public class HwmfFill { @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setDibToDev; } @@ -775,17 +604,19 @@ public class HwmfFill { colorUsage = ColorUsage.valueOf(leis.readUShort()); scanCount = leis.readUShort(); startScan = leis.readUShort(); - yDib = leis.readUShort(); - xDib = leis.readUShort(); - height = leis.readUShort(); - width = leis.readUShort(); - yDest = leis.readUShort(); - xDest = leis.readUShort(); - - int size = 9*LittleEndianConsts.SHORT_SIZE; + + int size = 3*LittleEndianConsts.SHORT_SIZE; + + final Point2D srcPnt = new Point2D.Double(); + size += readPointS(leis, srcPnt); + + size += readBounds2(leis, dstBounds); + dib = new HwmfBitmapDib(); size += dib.init(leis, (int)(recordSize-6-size)); - + + srcBounds.setRect(srcPnt.getX(), srcPnt.getY(), dstBounds.getWidth(), dstBounds.getHeight()); + return size; } @@ -806,52 +637,9 @@ public class HwmfFill { } - public static class WmfDibBitBlt implements HwmfRecord, HwmfImageRecord, HwmfObjectTableEntry { - - /** - * A 32-bit unsigned integer that defines how the source pixels, the current brush - * in the playback device context, and the destination pixels are to be combined to form the - * new image. This code MUST be one of the values in the Ternary Raster Operation Enumeration. - */ - HwmfTernaryRasterOp rasterOperation; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the source rectangle. - */ - private int ySrc; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the source rectangle. - */ - private int xSrc; - /** - * A 16-bit signed integer that defines the height, in logical units, of the source and - * destination rectangles. - */ - private int height; - /** - * A 16-bit signed integer that defines the width, in logical units, of the source and destination - * rectangles. - */ - private int width; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the upper-left - * corner of the destination rectangle. - */ - private int yDest; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the upper-left - * corner of the destination rectangle. - */ - private int xDest; - - /** - * A variable-sized DeviceIndependentBitmap Object that defines image content. - * This object MUST be specified, even if the raster operation does not require a source. - */ - private HwmfBitmapDib target; - - + public static class WmfDibBitBlt extends WmfDibStretchBlt { @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.dibBitBlt; } @@ -859,48 +647,32 @@ public class HwmfFill { public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { boolean hasBitmap = (recordSize/2 != ((recordFunction >> 8) + 3)); - int size = 0; int rasterOpCode = leis.readUShort(); int rasterOpIndex = leis.readUShort(); rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex); assert(rasterOpCode == rasterOperation.opCode); - ySrc = leis.readShort(); - xSrc = leis.readShort(); - size = 4*LittleEndianConsts.SHORT_SIZE; + int size = 2*LittleEndianConsts.SHORT_SIZE; + + final Point2D srcPnt = new Point2D.Double(); + size += readPointS(leis, srcPnt); if (!hasBitmap) { /*int reserved =*/ leis.readShort(); size += LittleEndianConsts.SHORT_SIZE; } - height = leis.readShort(); - width = leis.readShort(); - yDest = leis.readShort(); - xDest = leis.readShort(); - - size += 4*LittleEndianConsts.SHORT_SIZE; + + size += readBounds2(leis, dstBounds); if (hasBitmap) { target = new HwmfBitmapDib(); size += target.init(leis, (int)(recordSize-6-size)); } - + + // the destination rectangle, having the same dimension as the source rectangle + srcBounds.setRect(srcPnt.getX(), srcPnt.getY(), dstBounds.getWidth(), dstBounds.getHeight()); + return size; } - - @Override - public void draw(HwmfGraphics ctx) { - ctx.addObjectTableEntry(this); - } - - @Override - public void applyObject(HwmfGraphics ctx) { - - } - - @Override - public BufferedImage getImage() { - return (target == null) ? null : target.getImage(); - } } public static class WmfDibStretchBlt implements HwmfRecord, HwmfImageRecord, HwmfObjectTableEntry { @@ -909,53 +681,22 @@ public class HwmfFill { * in the playback device context, and the destination pixels are to be combined to form the * new image. This code MUST be one of the values in the Ternary Raster Operation Enumeration. */ - private HwmfTernaryRasterOp rasterOperation; - /** - * A 16-bit signed integer that defines the height, in logical units, of the source rectangle. - */ - private int srcHeight; - /** - * A 16-bit signed integer that defines the width, in logical units, of the source rectangle. - */ - private int srcWidth; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * upper-left corner of the source rectangle. - */ - private int ySrc; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * upper-left corner of the source rectangle. - */ - private int xSrc; - /** - * A 16-bit signed integer that defines the height, in logical units, of the - * destination rectangle. - */ - private int destHeight; - /** - * A 16-bit signed integer that defines the width, in logical units, of the - * destination rectangle. - */ - private int destWidth; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, - * of the upper-left corner of the destination rectangle. - */ - private int yDest; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, - * of the upper-left corner of the destination rectangle. - */ - private int xDest; + protected HwmfTernaryRasterOp rasterOperation; + + /** the source rectangle */ + protected final Rectangle2D srcBounds = new Rectangle2D.Double(); + + /** the destination rectangle */ + protected final Rectangle2D dstBounds = new Rectangle2D.Double(); + /** * A variable-sized DeviceIndependentBitmap Object that defines image content. * This object MUST be specified, even if the raster operation does not require a source. */ - HwmfBitmapDib target; - + protected HwmfBitmapDib target; + @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.dibStretchBlt; } @@ -963,27 +704,21 @@ public class HwmfFill { public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { boolean hasBitmap = (recordSize > ((recordFunction >> 8) + 3)); - int size = 0; int rasterOpCode = leis.readUShort(); int rasterOpIndex = leis.readUShort(); rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex); assert(rasterOpCode == rasterOperation.opCode); - srcHeight = leis.readShort(); - srcWidth = leis.readShort(); - ySrc = leis.readShort(); - xSrc = leis.readShort(); - size = 6*LittleEndianConsts.SHORT_SIZE; + int size = 2*LittleEndianConsts.SHORT_SIZE; + + size += readBounds2(leis, srcBounds); if (!hasBitmap) { /*int reserved =*/ leis.readShort(); size += LittleEndianConsts.SHORT_SIZE; } - destHeight = leis.readShort(); - destWidth = leis.readShort(); - yDest = leis.readShort(); - xDest = leis.readShort(); - size += 4*LittleEndianConsts.SHORT_SIZE; + + size += readBounds2(leis, dstBounds); if (hasBitmap) { target = new HwmfBitmapDib(); size += target.init(leis, (int)(recordSize-6-size)); @@ -996,15 +731,30 @@ public class HwmfFill { public void draw(HwmfGraphics ctx) { ctx.addObjectTableEntry(this); } - + @Override public void applyObject(HwmfGraphics ctx) { - + } @Override public BufferedImage getImage() { - return target.getImage(); + return (target != null && target.isValid()) ? target.getImage() : null; } } + + static int readBounds2(LittleEndianInputStream leis, Rectangle2D bounds) { + /** + * The 16-bit signed integers that defines the corners of the bounding rectangle. + */ + int h = leis.readShort(); + int w = leis.readShort(); + int y = leis.readShort(); + int x = leis.readShort(); + + bounds.setRect(x, y, w, h); + + return 4 * LittleEndianConsts.SHORT_SIZE; + } + } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java index f094ae1959..4413983ef3 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java @@ -24,6 +24,8 @@ import org.apache.poi.common.usermodel.fonts.FontCharset; import org.apache.poi.common.usermodel.fonts.FontFamily; import org.apache.poi.common.usermodel.fonts.FontInfo; import org.apache.poi.common.usermodel.fonts.FontPitch; +import org.apache.poi.util.BitField; +import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -90,7 +92,7 @@ public class HwmfFont implements FontInfo { this.flag = flag; } - static WmfOutPrecision valueOf(int flag) { + public static WmfOutPrecision valueOf(int flag) { for (WmfOutPrecision op : values()) { if (op.flag == flag) { return op; @@ -104,22 +106,17 @@ public class HwmfFont implements FontInfo { * ClipPrecision Flags specify clipping precision, which defines how to clip characters that are * partially outside a clipping region. These flags can be combined to specify multiple options. */ - public enum WmfClipPrecision { + public static class WmfClipPrecision { - /** - * Specifies that default clipping MUST be used. - */ - CLIP_DEFAULT_PRECIS (0x00000000), + /** Specifies that default clipping MUST be used. */ + private static final BitField CLIP_DEFAULT_PRECIS = BitFieldFactory.getInstance(0x0000); - /** - * This value SHOULD NOT be used. - */ - CLIP_CHARACTER_PRECIS (0x00000001), - /** - * This value MAY be returned when enumerating rasterized, TrueType and vector fonts. - */ - CLIP_STROKE_PRECIS (0x00000002), + /** This value SHOULD NOT be used. */ + private static final BitField CLIP_CHARACTER_PRECIS = BitFieldFactory.getInstance(0x0001); + + /** This value MAY be returned when enumerating rasterized, TrueType and vector fonts. */ + private static final BitField CLIP_STROKE_PRECIS = BitFieldFactory.getInstance(0x0002); /** * This value is used to control font rotation, as follows: @@ -129,37 +126,39 @@ public class HwmfFont implements FontInfo { * If clear, device fonts SHOULD rotate counterclockwise, but the rotation of other fonts * SHOULD be determined by the orientation of the coordinate system. */ - CLIP_LH_ANGLES (0x00000010), + private static final BitField CLIP_LH_ANGLES = BitFieldFactory.getInstance(0x0010); - /** - * This value SHOULD NOT be used. - */ - CLIP_TT_ALWAYS (0x00000020), + /** This value SHOULD NOT be used. */ + private static final BitField CLIP_TT_ALWAYS = BitFieldFactory.getInstance(0x0020); - /** - * This value specifies that font association SHOULD< be turned off. - */ - CLIP_DFA_DISABLE (0x00000040), + /** This value specifies that font association SHOULD< be turned off. */ + private static final BitField CLIP_DFA_DISABLE = BitFieldFactory.getInstance(0x0040); /** * This value specifies that font embedding MUST be used to render document content; * embedded fonts are read-only. */ - CLIP_EMBEDDED (0x00000080); - + private static final BitField CLIP_EMBEDDED = BitFieldFactory.getInstance(0x0080); int flag; - WmfClipPrecision(int flag) { - this.flag = flag; + + public int init(LittleEndianInputStream leis) { + flag = leis.readUByte(); + return LittleEndianConsts.BYTE_SIZE; } - static WmfClipPrecision valueOf(int flag) { - for (WmfClipPrecision cp : values()) { - if (cp.flag == flag) { - return cp; - } - } - return null; + @Override + public String toString() { + return + (((flag&0x3) == 0 ? "default " : " ")+ + (CLIP_CHARACTER_PRECIS.isSet(flag) ? "char " : " ")+ + (CLIP_STROKE_PRECIS.isSet(flag) ? "stroke " : " ")+ + (CLIP_LH_ANGLES.isSet(flag) ? "angles " : " ")+ + (CLIP_TT_ALWAYS.isSet(flag) ? "tt_always " : " ")+ + (CLIP_DFA_DISABLE.isSet(flag) ? "dfa " : " ")+ + (CLIP_EMBEDDED.isSet(flag) ? "embedded " : " ") + ).trim() + ; } } @@ -210,7 +209,7 @@ public class HwmfFont implements FontInfo { this.flag = flag; } - static WmfFontQuality valueOf(int flag) { + public static WmfFontQuality valueOf(int flag) { for (WmfFontQuality fq : values()) { if (fq.flag == flag) { return fq; @@ -240,7 +239,7 @@ public class HwmfFont implements FontInfo { * For all height comparisons, the font mapper SHOULD find the largest physical * font that does not exceed the requested size. */ - int height; + protected int height; /** * A 16-bit signed integer that defines the average width, in logical units, of @@ -248,45 +247,45 @@ public class HwmfFont implements FontInfo { * against the digitization aspect ratio of the available fonts to find the closest match, * determined by the absolute value of the difference. */ - int width; + protected int width; /** * A 16-bit signed integer that defines the angle, in tenths of degrees, between the * escapement vector and the x-axis of the device. The escapement vector is parallel * to the base line of a row of text. */ - int escapement; + protected int escapement; /** * A 16-bit signed integer that defines the angle, in tenths of degrees, * between each character's base line and the x-axis of the device. */ - int orientation; + protected int orientation; /** * A 16-bit signed integer that defines the weight of the font in the range 0 * through 1000. For example, 400 is normal and 700 is bold. If this value is 0x0000, * a default weight SHOULD be used. */ - int weight; + protected int weight; /** * A 8-bit Boolean value that specifies the italic attribute of the font. * 0 = not italic / 1 = italic. */ - boolean italic; + protected boolean italic; /** * An 8-bit Boolean value that specifies the underline attribute of the font. * 0 = not underlined / 1 = underlined */ - boolean underline; + protected boolean underline; /** * An 8-bit Boolean value that specifies the strike out attribute of the font. * 0 = not striked out / 1 = striked out */ - boolean strikeOut; + protected boolean strikeOut; /** * An 8-bit unsigned integer that defines the character set. @@ -299,12 +298,12 @@ public class HwmfFont implements FontInfo { * If a typeface name in the FaceName field is specified, the CharSet value MUST match the * character set of that typeface. */ - FontCharset charSet; + protected FontCharset charSet; /** * An 8-bit unsigned integer that defines the output precision. */ - WmfOutPrecision outPrecision; + protected WmfOutPrecision outPrecision; /** * An 8-bit unsigned integer that defines the clipping precision. @@ -312,40 +311,40 @@ public class HwmfFont implements FontInfo { * * @see WmfClipPrecision */ - WmfClipPrecision clipPrecision; + protected final WmfClipPrecision clipPrecision = new WmfClipPrecision(); /** * An 8-bit unsigned integer that defines the output quality. */ - WmfFontQuality quality; + protected WmfFontQuality quality; /** * A PitchAndFamily object that defines the pitch and the family of the font. * Font families specify the look of fonts in a general way and are intended for * specifying fonts when the exact typeface wanted is not available. */ - int pitchAndFamily; + protected int pitchAndFamily; /** * Font families specify the look of fonts in a general way and are * intended for specifying fonts when the exact typeface wanted is not available. * (LSB 4 bits) */ - FontFamily family; + protected FontFamily family; /** * A property of a font that describes the pitch (MSB 2 bits) */ - FontPitch pitch; + protected FontPitch pitch; /** * A null-terminated string of 8-bit Latin-1 [ISO/IEC-8859-1] ANSI * characters that specifies the typeface name of the font. The length of this string MUST NOT * exceed 32 8-bit characters, including the terminating null. */ - String facename; + protected String facename; - public int init(LittleEndianInputStream leis) throws IOException { + public int init(LittleEndianInputStream leis, long recordSize) throws IOException { height = leis.readShort(); width = leis.readShort(); escapement = leis.readShort(); @@ -356,24 +355,35 @@ public class HwmfFont implements FontInfo { strikeOut = leis.readByte() != 0; charSet = FontCharset.valueOf(leis.readUByte()); outPrecision = WmfOutPrecision.valueOf(leis.readUByte()); - clipPrecision = WmfClipPrecision.valueOf(leis.readUByte()); + clipPrecision.init(leis); quality = WmfFontQuality.valueOf(leis.readUByte()); pitchAndFamily = leis.readUByte(); - - byte buf[] = new byte[32], b, readBytes = 0; - do { - if (readBytes == 32) { - throw new IOException("Font facename can't be determined."); - } - buf[readBytes++] = b = leis.readByte(); - } while (b != 0 && b != -1 && readBytes <= 32); - - facename = new String(buf, 0, readBytes-1, StandardCharsets.ISO_8859_1); - + StringBuilder sb = new StringBuilder(); + int readBytes = readString(leis, sb, 32); + if (readBytes == -1) { + throw new IOException("Font facename can't be determined."); + } + facename = sb.toString(); + return 5*LittleEndianConsts.SHORT_SIZE+8*LittleEndianConsts.BYTE_SIZE+readBytes; } + public void initDefaults() { + height = -12; + width = 0; + escapement = 0; + weight = 400; + italic = false; + underline = false; + strikeOut = false; + charSet = FontCharset.ANSI; + outPrecision = WmfOutPrecision.OUT_DEFAULT_PRECIS; + quality = WmfFontQuality.ANTIALIASED_QUALITY; + pitchAndFamily = FontFamily.FF_DONTCARE.getFlag() | (FontPitch.DEFAULT.getNativeId() << 6); + facename = "SansSerif"; + } + public int getHeight() { return height; } @@ -471,4 +481,38 @@ public class HwmfFont implements FontInfo { public void setCharset(FontCharset charset) { throw new UnsupportedOperationException("setCharset not supported by HwmfFont."); } + + @Override + public String toString() { + return "{ height: "+height+ + ", width: "+width+ + ", escapment: "+escapement+ + ", weight: "+weight+ + ", italic: "+italic+ + ", underline: "+underline+ + ", strikeOut: "+strikeOut+ + ", charset: '"+charSet+"'"+ + ", outPrecision: '"+outPrecision+"'"+ + ", clipPrecision: '"+clipPrecision+"'"+ + ", quality: '"+quality+"'"+ + ", pitch: '"+getPitch()+"'"+ + ", family: '"+getFamily()+"'"+ + ", facename: '"+facename+"'"+ + "}"; + } + + protected int readString(LittleEndianInputStream leis, StringBuilder sb, int limit) throws IOException { + byte buf[] = new byte[limit], b, readBytes = 0; + do { + if (readBytes == limit) { + return -1; + } + + buf[readBytes++] = b = leis.readByte(); + } while (b != 0 && b != -1 && readBytes <= limit); + + sb.append(new String(buf, 0, readBytes-1, StandardCharsets.ISO_8859_1)); + + return readBytes; + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfHatchStyle.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfHatchStyle.java index 02f896849a..6574deaed3 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfHatchStyle.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfHatchStyle.java @@ -32,14 +32,27 @@ public enum HwmfHatchStyle { /** +++++ - A horizontal and vertical cross-hatch. */ HS_CROSS(0x0004), /** xxxxx - A 45-degree crosshatch. */ - HS_DIAGCROSS(0x0005); + HS_DIAGCROSS(0x0005), + /** The hatch is not a pattern, but is a solid color. */ + HS_SOLIDCLR(0x0006), + /** The hatch is not a pattern, but is a dithered color. */ + HS_DITHEREDCLR(0x0007), + /** The hatch is not a pattern, but is a solid color, defined by the current text (foreground) color. */ + HS_SOLIDTEXTCLR(0x0008), + /** The hatch is not a pattern, but is a dithered color, defined by the current text (foreground) color. */ + HS_DITHEREDTEXTCLR(0x0009), + /** The hatch is not a pattern, but is a solid color, defined by the current background color. */ + HS_SOLIDBKCLR(0x000A), + /** The hatch is not a pattern, but is a dithered color, defined by the current background color. */ + HS_DITHEREDBKCLR(0x000B) + ; int flag; HwmfHatchStyle(int flag) { this.flag = flag; } - static HwmfHatchStyle valueOf(int flag) { + public static HwmfHatchStyle valueOf(int flag) { for (HwmfHatchStyle hs : values()) { if (hs.flag == flag) return hs; } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMapMode.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMapMode.java index f4f9a65c27..2e09606bdd 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMapMode.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMapMode.java @@ -105,7 +105,7 @@ public enum HwmfMapMode { this.scale = scale; } - static HwmfMapMode valueOf(int flag) { + public static HwmfMapMode valueOf(int flag) { for (HwmfMapMode mm : values()) { if (mm.flag == flag) return mm; } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java index f5ab077d22..eef15e30d7 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java @@ -17,6 +17,7 @@ package org.apache.poi.hwmf.record; +import java.awt.geom.Dimension2D; import java.awt.image.BufferedImage; import java.io.IOException; @@ -24,6 +25,7 @@ import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; import org.apache.poi.hwmf.record.HwmfFill.ColorUsage; import org.apache.poi.hwmf.record.HwmfFill.HwmfImageRecord; +import org.apache.poi.util.Dimension2DDouble; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -34,7 +36,7 @@ public class HwmfMisc { */ public static class WmfSaveDc implements HwmfRecord { @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.saveDc; } @@ -47,13 +49,18 @@ public class HwmfMisc { public void draw(HwmfGraphics ctx) { ctx.saveProperties(); } + + @Override + public String toString() { + return "{}"; + } } /** * The META_SETRELABS record is reserved and not supported. */ public static class WmfSetRelabs implements HwmfRecord { - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setRelabs; } @@ -78,10 +85,10 @@ public class HwmfMisc { * member is positive, nSavedDC represents a specific instance of the state to be restored. If * this member is negative, nSavedDC represents an instance relative to the current state. */ - private int nSavedDC; + protected int nSavedDC; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.restoreDc; } @@ -95,6 +102,11 @@ public class HwmfMisc { public void draw(HwmfGraphics ctx) { ctx.restoreProperties(nSavedDC); } + + @Override + public String toString() { + return "{ nSavedDC: "+nSavedDC+" }"; + } } /** @@ -103,16 +115,15 @@ public class HwmfMisc { */ public static class WmfSetBkColor implements HwmfRecord { - private HwmfColorRef colorRef; + protected final HwmfColorRef colorRef = new HwmfColorRef(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setBkColor; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - colorRef = new HwmfColorRef(); return colorRef.init(leis); } @@ -120,6 +131,11 @@ public class HwmfMisc { public void draw(HwmfGraphics ctx) { ctx.getProperties().setBackgroundColor(colorRef); } + + @Override + public String toString() { + return "{ colorRef: "+colorRef+" }"; + } } /** @@ -140,7 +156,7 @@ public class HwmfMisc { this.flag = flag; } - static HwmfBkMode valueOf(int flag) { + public static HwmfBkMode valueOf(int flag) { for (HwmfBkMode bs : values()) { if (bs.flag == flag) return bs; } @@ -148,9 +164,9 @@ public class HwmfMisc { } } - private HwmfBkMode bkMode; + protected HwmfBkMode bkMode; - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setBkMode; } @@ -163,6 +179,11 @@ public class HwmfMisc { public void draw(HwmfGraphics ctx) { ctx.getProperties().setBkMode(bkMode); } + + @Override + public String toString() { + return "{ bkMode: '"+bkMode+"' }"; + } } /** @@ -180,7 +201,7 @@ public class HwmfMisc { private int layout; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setLayout; } @@ -205,10 +226,10 @@ public class HwmfMisc { */ public static class WmfSetMapMode implements HwmfRecord { - private HwmfMapMode mapMode; + protected HwmfMapMode mapMode; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setMapMode; } @@ -223,6 +244,11 @@ public class HwmfMisc { ctx.getProperties().setMapMode(mapMode); ctx.updateWindowMapMode(); } + + @Override + public String toString() { + return "{ mapMode: '"+mapMode+"' }"; + } } /** @@ -234,12 +260,13 @@ public class HwmfMisc { /** * A 32-bit unsigned integer that defines whether the font mapper should attempt to * match a font's aspect ratio to the current device's aspect ratio. If bit 0 is - * set, the mapper selects only matching fonts. + * set, the font mapper SHOULD select only fonts that match the aspect ratio of the + * output device, as it is currently defined in the playback device context. */ private long mapperValues; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setMapperFlags; } @@ -253,6 +280,11 @@ public class HwmfMisc { public void draw(HwmfGraphics ctx) { } + + @Override + public String toString() { + return "{ mapperValues: "+mapperValues+" }"; + } } /** @@ -262,14 +294,11 @@ public class HwmfMisc { */ public static class WmfSetRop2 implements HwmfRecord { - /** - * A 16-bit unsigned integer that defines the foreground binary raster - * operation mixing mode - */ - private HwmfBinaryRasterOp drawMode; + /** An unsigned integer that defines the foreground binary raster operation mixing mode */ + protected HwmfBinaryRasterOp drawMode; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setRop2; } @@ -283,6 +312,11 @@ public class HwmfMisc { public void draw(HwmfGraphics ctx) { } + + @Override + public String toString() { + return "{ drawMode: '"+drawMode+"' }"; + } } /** @@ -291,24 +325,63 @@ public class HwmfMisc { */ public static class WmfSetStretchBltMode implements HwmfRecord { - /** - * A 16-bit unsigned integer that defines bitmap stretching mode. - * This MUST be one of the values: - * BLACKONWHITE = 0x0001, - * WHITEONBLACK = 0x0002, - * COLORONCOLOR = 0x0003, - * HALFTONE = 0x0004 - */ - private int setStretchBltMode; + public enum StretchBltMode { + /** + * Performs a Boolean AND operation by using the color values for the eliminated and existing pixels. + * If the bitmap is a monochrome bitmap, this mode preserves black pixels at the expense of white pixels. + * + * EMF name: STRETCH_ANDSCANS + */ + BLACKONWHITE(0x0001), + /** + * Performs a Boolean OR operation by using the color values for the eliminated and existing pixels. + * If the bitmap is a monochrome bitmap, this mode preserves white pixels at the expense of black pixels. + * + * EMF name: STRETCH_ORSCANS + */ + WHITEONBLACK(0x0002), + /** + * Deletes the pixels. This mode deletes all eliminated lines of pixels without trying + * to preserve their information. + * + * EMF name: STRETCH_DELETESCANS + */ + COLORONCOLOR(0x0003), + /** + * Maps pixels from the source rectangle into blocks of pixels in the destination rectangle. + * The average color over the destination block of pixels approximates the color of the source + * pixels. + * + * After setting the HALFTONE stretching mode, the brush origin MUST be set to avoid misalignment + * artifacts - in EMF this is done via EmfSetBrushOrgEx + * + * EMF name: STRETCH_HALFTONE + */ + HALFTONE(0x0004); + + public final int flag; + StretchBltMode(int flag) { + this.flag = flag; + } + + public static StretchBltMode valueOf(int flag) { + for (StretchBltMode bs : values()) { + if (bs.flag == flag) return bs; + } + return null; + } + } + + protected StretchBltMode stretchBltMode; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setStretchBltMode; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - setStretchBltMode = leis.readUShort(); + stretchBltMode = StretchBltMode.valueOf(leis.readUShort()); return LittleEndianConsts.SHORT_SIZE; } @@ -316,6 +389,11 @@ public class HwmfMisc { public void draw(HwmfGraphics ctx) { } + + @Override + public String toString() { + return "{ stretchBltMode: '"+stretchBltMode+"' }"; + } } /** @@ -324,7 +402,7 @@ public class HwmfMisc { */ public static class WmfDibCreatePatternBrush implements HwmfRecord, HwmfImageRecord, HwmfObjectTableEntry { - private HwmfBrushStyle style; + protected HwmfBrushStyle style; /** * A 16-bit unsigned integer that defines whether the Colors field of a DIB @@ -335,13 +413,13 @@ public class HwmfMisc { * * If the Style field specified anything but BS_PATTERN, this field MUST be one of the ColorUsage values. */ - private ColorUsage colorUsage; + protected ColorUsage colorUsage; - private HwmfBitmapDib patternDib; + protected HwmfBitmapDib patternDib; private HwmfBitmap16 pattern16; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.dibCreatePatternBrush; } @@ -376,6 +454,9 @@ public class HwmfMisc { @Override public void applyObject(HwmfGraphics ctx) { + if (patternDib != null && !patternDib.isValid()) { + return; + } HwmfDrawProperties prop = ctx.getProperties(); prop.setBrushStyle(style); prop.setBrushBitmap(getImage()); @@ -383,7 +464,7 @@ public class HwmfMisc { @Override public BufferedImage getImage() { - if (patternDib != null) { + if (patternDib != null && patternDib.isValid()) { return patternDib.getImage(); } else if (pattern16 != null) { return pattern16.getImage(); @@ -403,10 +484,10 @@ public class HwmfMisc { * A 16-bit unsigned integer used to index into the WMF Object Table to * get the object to be deleted. */ - private int objectIndex; + protected int objectIndex; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.deleteObject; } @@ -418,8 +499,19 @@ public class HwmfMisc { @Override public void draw(HwmfGraphics ctx) { + /* TODO: + * The object specified by this record MUST be deleted from the EMF Object Table. + * If the deleted object is currently selected in the playback device context, + * the default object for that graphics property MUST be restored. + */ + ctx.unsetObjectTableEntry(objectIndex); } + + @Override + public String toString() { + return "{ index: "+objectIndex+" }"; + } } public static class WmfCreatePatternBrush implements HwmfRecord, HwmfObjectTableEntry { @@ -427,7 +519,7 @@ public class HwmfMisc { private HwmfBitmap16 pattern; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.createPatternBrush; } @@ -452,30 +544,28 @@ public class HwmfMisc { public static class WmfCreatePenIndirect implements HwmfRecord, HwmfObjectTableEntry { - private HwmfPenStyle penStyle; - /** - * A 32-bit PointS Object that specifies a point for the object dimensions. - * The x-coordinate is the pen width. The y-coordinate is ignored. - */ - private int xWidth; - @SuppressWarnings("unused") - private int yWidth; + protected HwmfPenStyle penStyle; + + protected final Dimension2D dimension = new Dimension2DDouble(); /** * A 32-bit ColorRef Object that specifies the pen color value. */ - private HwmfColorRef colorRef; + protected final HwmfColorRef colorRef = new HwmfColorRef(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.createPenIndirect; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { penStyle = HwmfPenStyle.valueOf(leis.readUShort()); - xWidth = leis.readShort(); - yWidth = leis.readShort(); - colorRef = new HwmfColorRef(); + // A 32-bit PointS Object that specifies a point for the object dimensions. + // The x-coordinate is the pen width. The y-coordinate is ignored. + int xWidth = leis.readShort(); + int yWidth = leis.readShort(); + dimension.setSize(xWidth, yWidth); + int size = colorRef.init(leis); return size+3*LittleEndianConsts.SHORT_SIZE; } @@ -490,7 +580,15 @@ public class HwmfMisc { HwmfDrawProperties p = ctx.getProperties(); p.setPenStyle(penStyle); p.setPenColor(colorRef); - p.setPenWidth(xWidth); + p.setPenWidth(dimension.getWidth()); + } + + @Override + public String toString() { + return + "{ penStyle: "+penStyle+ + ", dimension: { width: "+dimension.getWidth()+", height: "+dimension.getHeight()+" }"+ + ", colorRef: "+colorRef+"}"; } } @@ -540,19 +638,14 @@ public class HwmfMisc { * */ public static class WmfCreateBrushIndirect implements HwmfRecord, HwmfObjectTableEntry { - private HwmfBrushStyle brushStyle; + protected HwmfBrushStyle brushStyle; - private HwmfColorRef colorRef; + protected HwmfColorRef colorRef; - /** - * A 16-bit field that specifies the brush hatch type. - * Its interpretation depends on the value of BrushStyle. - * - */ - private HwmfHatchStyle brushHatch; + protected HwmfHatchStyle brushHatch; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.createBrushIndirect; } @@ -577,5 +670,13 @@ public class HwmfMisc { p.setBrushColor(colorRef); p.setBrushHatch(brushHatch); } + + @Override + public String toString() { + return + "{ brushStyle: '"+brushStyle+"'"+ + ", colorRef: "+colorRef+ + ", brushHatch: '"+brushHatch+"' }"; + } } } \ No newline at end of file diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfPalette.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfPalette.java index cb80c454ee..cef75f698d 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfPalette.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfPalette.java @@ -39,12 +39,12 @@ public class HwmfPalette { private int values; private Color colorRef; - private PaletteEntry() { + public PaletteEntry() { this.values = PC_RESERVED.set(0); this.colorRef = Color.BLACK; } - private PaletteEntry(PaletteEntry other) { + public PaletteEntry(PaletteEntry other) { this.values = other.values; this.colorRef = other.colorRef; } @@ -100,19 +100,24 @@ public class HwmfPalette { * used with the META_SETPALENTRIES and META_ANIMATEPALETTE record types. * When used with META_CREATEPALETTE, it MUST be 0x0300 */ - private int start; + protected int start; - private List palette = new ArrayList<>(); + protected final List palette = new ArrayList<>(); @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { start = leis.readUShort(); + int size = readPaletteEntries(leis, -1); + return size + LittleEndianConsts.SHORT_SIZE; + } + + protected int readPaletteEntries(LittleEndianInputStream leis, int nbrOfEntries) throws IOException { /** * NumberOfEntries (2 bytes): A 16-bit unsigned integer that defines the number of objects in * aPaletteEntries. */ - int numberOfEntries = leis.readUShort(); - int size = 2*LittleEndianConsts.SHORT_SIZE; + final int numberOfEntries = (nbrOfEntries > -1) ? nbrOfEntries : leis.readUShort(); + int size = (nbrOfEntries > -1) ? 0 : LittleEndianConsts.SHORT_SIZE; for (int i=0; i clazz; + public final Supplier constructor; - HwmfRecordType(int id, Class clazz) { + HwmfRecordType(int id, Supplier constructor) { this.id = id; - this.clazz = clazz; + this.constructor = constructor; } public static HwmfRecordType getById(int id) { diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java new file mode 100644 index 0000000000..3a56e52e70 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java @@ -0,0 +1,59 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.hwmf.record; + +import org.apache.poi.hemf.record.emf.HemfFill; + +public enum HwmfRegionMode { + /** + * The new clipping region includes the intersection (overlapping areas) + * of the current clipping region and the current path (or new region). + */ + RGN_AND(0x01), + /** + * The new clipping region includes the union (combined areas) + * of the current clipping region and the current path (or new region). + */ + RGN_OR(0x02), + /** + * The new clipping region includes the union of the current clipping region + * and the current path (or new region) but without the overlapping areas + */ + RGN_XOR(0x03), + /** + * The new clipping region includes the areas of the current clipping region + * with those of the current path (or new region) excluded. + */ + RGN_DIFF(0x04), + /** + * The new clipping region is the current path (or the new region). + */ + RGN_COPY(0x05); + + int flag; + HwmfRegionMode(int flag) { + this.flag = flag; + } + + public static HwmfRegionMode valueOf(int flag) { + for (HwmfRegionMode rm : values()) { + if (rm.flag == flag) return rm; + } + return null; + } +} 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 9c6ed1e7f4..391215d657 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java @@ -17,10 +17,19 @@ package org.apache.poi.hwmf.record; +import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; +import static org.apache.poi.hwmf.record.HwmfDraw.pointToString; +import static org.apache.poi.hwmf.record.HwmfDraw.readPointS; +import static org.apache.poi.hwmf.record.HwmfDraw.readRectS; + +import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.codec.Charsets; import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; import org.apache.poi.hwmf.record.HwmfMisc.WmfSetMapMode; @@ -29,6 +38,7 @@ import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.IOUtils; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.LocaleUtil; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -52,7 +62,7 @@ public class HwmfText { private int charExtra; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setTextCharExtra; } @@ -73,16 +83,15 @@ public class HwmfText { */ public static class WmfSetTextColor implements HwmfRecord { - private HwmfColorRef colorRef; + protected final HwmfColorRef colorRef = new HwmfColorRef(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setTextColor; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - colorRef = new HwmfColorRef(); return colorRef.init(leis); } @@ -90,6 +99,11 @@ public class HwmfText { public void draw(HwmfGraphics ctx) { ctx.getProperties().setTextColor(colorRef); } + + @Override + public String toString() { + return "{ colorRef: "+colorRef+" }"; + } } /** @@ -112,7 +126,7 @@ public class HwmfText { private int breakExtra; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setBkColor; } @@ -147,19 +161,11 @@ public class HwmfText { * The string is written at the location specified by the XStart and YStart fields. */ private byte[] rawTextBytes; - /** - * A 16-bit signed integer that defines the vertical (y-axis) coordinate, in logical - * units, of the point where drawing is to start. - */ - private int yStart; - /** - * A 16-bit signed integer that defines the horizontal (x-axis) coordinate, in - * logical units, of the point where drawing is to start. - */ - private int xStart; - + + protected Point2D reference = new Point2D.Double(); + @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.textOut; } @@ -168,15 +174,19 @@ public class HwmfText { stringLength = leis.readShort(); rawTextBytes = IOUtils.safelyAllocate(stringLength+(stringLength&1), MAX_RECORD_LENGTH); leis.readFully(rawTextBytes); - yStart = leis.readShort(); - xStart = leis.readShort(); + // A 16-bit signed integer that defines the vertical (y-axis) coordinate, in logical + // units, of the point where drawing is to start. + int yStart = leis.readShort(); + // A 16-bit signed integer that defines the horizontal (x-axis) coordinate, in + // logical units, of the point where drawing is to start. + int xStart = leis.readShort(); + reference.setLocation(xStart, yStart); return 3*LittleEndianConsts.SHORT_SIZE+rawTextBytes.length; } @Override public void draw(HwmfGraphics ctx) { - Rectangle2D bounds = new Rectangle2D.Double(xStart, yStart, 0, 0); - ctx.drawString(getTextBytes(), bounds); + ctx.drawString(getTextBytes(), stringLength, reference); } public String getText(Charset charset) { @@ -195,40 +205,47 @@ public class HwmfText { return ret; } } - - /** - * The META_EXTTEXTOUT record outputs text by using the font, background color, and text color that - * are defined in the playback device context. Optionally, dimensions can be provided for clipping, - * opaquing, or both. - */ - public static class WmfExtTextOut implements HwmfRecord { + public static class WmfExtTextOutOptions { /** - * Indicates that the background color that is defined in the playback device context + * Indicates that the background color that is defined in the playback device context * SHOULD be used to fill the rectangle. - */ + */ private static final BitField ETO_OPAQUE = BitFieldFactory.getInstance(0x0002); - + /** * Indicates that the text SHOULD be clipped to the rectangle. */ private static final BitField ETO_CLIPPED = BitFieldFactory.getInstance(0x0004); /** - * Indicates that the string to be output SHOULD NOT require further processing - * with respect to the placement of the characters, and an array of character - * placement values SHOULD be provided. This character placement process is + * Indicates that the string to be output SHOULD NOT require further processing + * with respect to the placement of the characters, and an array of character + * placement values SHOULD be provided. This character placement process is * useful for fonts in which diacritical characters affect character spacing. */ private static final BitField ETO_GLYPH_INDEX = BitFieldFactory.getInstance(0x0010); /** - * Indicates that the text MUST be laid out in right-to-left reading order, instead of - * the default left-to-right order. This SHOULD be applied only when the font that is + * Indicates that the text MUST be laid out in right-to-left reading order, instead of + * the default left-to-right order. This SHOULD be applied only when the font that is * defined in the playback device context is either Hebrew or Arabic. */ private static final BitField ETO_RTLREADING = BitFieldFactory.getInstance(0x0080); + /** + * This bit indicates that the record does not specify a bounding rectangle for the + * text output. + */ + private static final BitField ETO_NO_RECT = BitFieldFactory.getInstance(0x0100); + + /** + * This bit indicates that the codes for characters in an output text string are 8 bits, + * derived from the low bytes of 16-bit Unicode UTF16-LE character codes, in which + * the high byte is assumed to be 0. + */ + private static final BitField ETO_SMALL_CHARS = BitFieldFactory.getInstance(0x0200); + /** * Indicates that to display numbers, digits appropriate to the locale SHOULD be used. */ @@ -240,32 +257,62 @@ public class HwmfText { private static final BitField ETO_NUMERICSLATIN = BitFieldFactory.getInstance(0x0800); /** - * Indicates that both horizontal and vertical character displacement values + * This bit indicates that no special operating system processing for glyph placement + * should be performed on right-to-left strings; that is, all glyph positioning + * SHOULD be taken care of by drawing and state records in the metafile + */ + private static final BitField ETO_IGNORELANGUAGE = BitFieldFactory.getInstance(0x1000); + + /** + * Indicates that both horizontal and vertical character displacement values * SHOULD be provided. */ private static final BitField ETO_PDY = BitFieldFactory.getInstance(0x2000); + /** This bit is reserved and SHOULD NOT be used. */ + private static final BitField ETO_REVERSE_INDEX_MAP = BitFieldFactory.getInstance(0x10000); + + protected int flag; + + public int init(LittleEndianInputStream leis) { + flag = leis.readUShort(); + return LittleEndianConsts.SHORT_SIZE; + } + + public boolean isOpaque() { + return ETO_OPAQUE.isSet(flag); + } + + public boolean isClipped() { + return ETO_CLIPPED.isSet(flag); + } + + public boolean isYDisplaced() { + return ETO_PDY.isSet(flag); + } + } + + /** + * The META_EXTTEXTOUT record outputs text by using the font, background color, and text color that + * are defined in the playback device context. Optionally, dimensions can be provided for clipping, + * opaquing, or both. + */ + public static class WmfExtTextOut implements HwmfRecord { /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, where the - text string is to be located. + * The location, in logical units, where the text string is to be placed. */ - private int y; - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, where the - text string is to be located. - */ - private int x; + protected final Point2D reference = new Point2D.Double(); + /** * A 16-bit signed integer that defines the length of the string. */ - private int stringLength; - - /** - * A 16-bit unsigned integer that defines the use of the application-defined - * rectangle. This member can be a combination of one or more values in the - * ExtTextOutOptions Flags (ETO_*) - */ - private int fwOpts; + protected int stringLength; + /** + * A 16-bit unsigned integer that defines the use of the application-defined + * rectangle. This member can be a combination of one or more values in the + * ExtTextOutOptions Flags (ETO_*) + */ + protected final WmfExtTextOutOptions options; /** * 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. @@ -274,24 +321,32 @@ public class HwmfText { * Each value is a 16-bit signed integer that defines the coordinate, in logical coordinates, of * the upper-left corner of the rectangle */ - private int left,top,right,bottom; + 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 * aligned on a 16-bit boundary. */ - private byte[] rawTextBytes; + protected byte[] rawTextBytes; /** * An optional array of 16-bit signed integers that 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. If this field is present, there MUST be the same * number of values as there are characters in the string. */ - private int dx[]; - + protected final List dx = new ArrayList<>(); + + public WmfExtTextOut() { + this(new WmfExtTextOutOptions()); + } + + protected WmfExtTextOut(WmfExtTextOutOptions options) { + this.options = options; + } + @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.extTextOut; } @@ -299,22 +354,17 @@ public class HwmfText { public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { // -6 bytes of record function and length header final int remainingRecordSize = (int)(recordSize-6); - - y = leis.readShort(); - x = leis.readShort(); + + int size = readPointS(leis, reference); + stringLength = leis.readShort(); - fwOpts = leis.readUShort(); - - int size = 4*LittleEndianConsts.SHORT_SIZE; - + size += LittleEndianConsts.SHORT_SIZE; + size += options.init(leis); + // Check if we have a rectangle - if ((ETO_OPAQUE.isSet(fwOpts) || ETO_CLIPPED.isSet(fwOpts)) && size+8<=remainingRecordSize) { - // the bounding rectangle is optional and only read when fwOpts are given - left = leis.readShort(); - top = leis.readShort(); - right = leis.readShort(); - bottom = leis.readShort(); - size += 4*LittleEndianConsts.SHORT_SIZE; + if ((options.isOpaque() || options.isClipped()) && size+8<=remainingRecordSize) { + // 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); @@ -331,9 +381,8 @@ public class HwmfText { logger.log(POILogger.WARN, "META_EXTTEXTOUT tracking info doesn't cover all characters"); } - dx = new int[stringLength]; for (int i=0; i + * + * If the flag is not set, the option TA_NOUPDATECP is active, i.e. the drawing position + * in the playback device context MUST NOT be updated after each text output call. + * The reference point MUST be passed to the text output function. */ @SuppressWarnings("unused") private static final BitField TA_UPDATECP = BitFieldFactory.getInstance(0x0001); - - /** - * The reference point MUST be on the right edge of the bounding rectangle. - */ - private static final BitField TA_RIGHT = BitFieldFactory.getInstance(0x0002); - - /** - * The reference point MUST be aligned horizontally with the center of the bounding - * rectangle. - */ - private static final BitField TA_CENTER = BitFieldFactory.getInstance(0x0006); - - /** - * The reference point MUST be on the bottom edge of the bounding rectangle. - */ - private static final BitField TA_BOTTOM = BitFieldFactory.getInstance(0x0008); - - /** - * The reference point MUST be on the baseline of the text. - */ - private static final BitField TA_BASELINE = BitFieldFactory.getInstance(0x0018); - + /** * The text MUST be laid out in right-to-left reading order, instead of the default * left-to-right order. This SHOULD be applied only when the font that is defined in the @@ -438,43 +461,64 @@ public class HwmfText { */ @SuppressWarnings("unused") private static final BitField TA_RTLREADING = BitFieldFactory.getInstance(0x0100); - - // *********************************************************************************** - // VerticalTextAlignmentMode Flags (e.g. for Kanji fonts) - // *********************************************************************************** - + + + private static final BitField ALIGN_MASK = BitFieldFactory.getInstance(0x0006); + /** - * The reference point MUST be on the top edge of the bounding rectangle. + * Flag TA_LEFT (0x0000): + * The reference point MUST be on the left edge of the bounding rectangle, + * if all bits of the align mask (latin mode) are unset. + * + * Flag VTA_TOP (0x0000): + * The reference point MUST be on the top edge of the bounding rectangle, + * if all bits of the valign mask are unset. */ - @SuppressWarnings("unused") - private static final BitField VTA_TOP = BitFieldFactory.getInstance(0x0000); - + private static final int ALIGN_LEFT = 0; + /** + * Flag TA_RIGHT (0x0002): * The reference point MUST be on the right edge of the bounding rectangle. - */ - @SuppressWarnings("unused") - private static final BitField VTA_RIGHT = BitFieldFactory.getInstance(0x0000); - - /** + * + * Flag VTA_BOTTOM (0x0002): * The reference point MUST be on the bottom edge of the bounding rectangle. */ - private static final BitField VTA_BOTTOM = BitFieldFactory.getInstance(0x0002); - + private static final int ALIGN_RIGHT = 1; + /** - * The reference point MUST be aligned vertically with the center of the bounding + * Flag TA_CENTER (0x0006) / VTA_CENTER (0x0006): + * The reference point MUST be aligned horizontally with the center of the bounding * rectangle. */ - private static final BitField VTA_CENTER = BitFieldFactory.getInstance(0x0006); - + private static final int ALIGN_CENTER = 3; + + private static final BitField VALIGN_MASK = BitFieldFactory.getInstance(0x0018); + /** + * Flag TA_TOP (0x0000): + * The reference point MUST be on the top edge of the bounding rectangle, + * if all bits of the valign mask are unset. + * + * Flag VTA_RIGHT (0x0000): + * The reference point MUST be on the right edge of the bounding rectangle, + * if all bits of the align mask (asian mode) are unset. + */ + private static final int VALIGN_TOP = 0; + + /** + * Flag TA_BOTTOM (0x0008): + * The reference point MUST be on the bottom edge of the bounding rectangle. + * + * Flag VTA_LEFT (0x0008): * The reference point MUST be on the left edge of the bounding rectangle. */ - private static final BitField VTA_LEFT = BitFieldFactory.getInstance(0x0008); + 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 BitField VTA_BASELINE = BitFieldFactory.getInstance(0x0018); + private static final int VALIGN_BASELINE = 3; /** * A 16-bit unsigned integer that defines text alignment. @@ -482,10 +526,10 @@ public class HwmfText { * for text with a horizontal baseline, and VerticalTextAlignmentMode Flags * for text with a vertical baseline. */ - private int textAlignmentMode; + protected int textAlignmentMode; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setTextAlign; } @@ -498,52 +542,90 @@ public class HwmfText { @Override public void draw(HwmfGraphics ctx) { HwmfDrawProperties props = ctx.getProperties(); - if (TA_CENTER.isSet(textAlignmentMode)) { - props.setTextAlignLatin(HwmfTextAlignment.CENTER); - } else if (TA_RIGHT.isSet(textAlignmentMode)) { - props.setTextAlignLatin(HwmfTextAlignment.RIGHT); - } else { - props.setTextAlignLatin(HwmfTextAlignment.LEFT); - } + props.setTextAlignLatin(getAlignLatin()); + props.setTextVAlignLatin(getVAlignLatin()); + props.setTextAlignAsian(getAlignAsian()); + props.setTextVAlignAsian(getVAlignAsian()); + } - if (VTA_CENTER.isSet(textAlignmentMode)) { - props.setTextAlignAsian(HwmfTextAlignment.CENTER); - } else if (VTA_LEFT.isSet(textAlignmentMode)) { - props.setTextAlignAsian(HwmfTextAlignment.LEFT); - } else { - props.setTextAlignAsian(HwmfTextAlignment.RIGHT); - } + @Override + public String toString() { + return + "{ align: '"+ getAlignLatin() + "'" + + ", valign: '"+ getVAlignLatin() + "'" + + ", alignAsian: '"+ getAlignAsian() + "'" + + ", valignAsian: '"+ getVAlignAsian() + "'" + + "}"; + } - if (TA_BASELINE.isSet(textAlignmentMode)) { - props.setTextVAlignLatin(HwmfTextVerticalAlignment.BASELINE); - } else if (TA_BOTTOM.isSet(textAlignmentMode)) { - props.setTextVAlignLatin(HwmfTextVerticalAlignment.BOTTOM); - } else { - props.setTextVAlignLatin(HwmfTextVerticalAlignment.TOP); + private HwmfTextAlignment getAlignLatin() { + switch (ALIGN_MASK.getValue(textAlignmentMode)) { + default: + case ALIGN_LEFT: + return HwmfTextAlignment.LEFT; + case ALIGN_CENTER: + return HwmfTextAlignment.CENTER; + case ALIGN_RIGHT: + return HwmfTextAlignment.RIGHT; } + } - if (VTA_BASELINE.isSet(textAlignmentMode)) { - props.setTextVAlignAsian(HwmfTextVerticalAlignment.BASELINE); - } else if (VTA_BOTTOM.isSet(textAlignmentMode)) { - props.setTextVAlignAsian(HwmfTextVerticalAlignment.BOTTOM); - } else { - props.setTextVAlignAsian(HwmfTextVerticalAlignment.TOP); + private HwmfTextVerticalAlignment getVAlignLatin() { + switch (VALIGN_MASK.getValue(textAlignmentMode)) { + default: + case VALIGN_TOP: + return HwmfTextVerticalAlignment.TOP; + case VALIGN_BASELINE: + return HwmfTextVerticalAlignment.BASELINE; + case VALIGN_BOTTOM: + return HwmfTextVerticalAlignment.BOTTOM; + } + } + + private HwmfTextAlignment getAlignAsian() { + switch (getVAlignLatin()) { + default: + case TOP: + return HwmfTextAlignment.RIGHT; + case BASELINE: + return HwmfTextAlignment.CENTER; + case BOTTOM: + return HwmfTextAlignment.LEFT; + } + } + + private HwmfTextVerticalAlignment getVAlignAsian() { + switch (getAlignLatin()) { + default: + case LEFT: + return HwmfTextVerticalAlignment.TOP; + case CENTER: + return HwmfTextVerticalAlignment.BASELINE; + case RIGHT: + return HwmfTextVerticalAlignment.BOTTOM; } } } public static class WmfCreateFontIndirect implements HwmfRecord, HwmfObjectTableEntry { - private HwmfFont font; - + protected final HwmfFont font; + + public WmfCreateFontIndirect() { + this(new HwmfFont()); + } + + protected WmfCreateFontIndirect(HwmfFont font) { + this.font = font; + } + @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.createFontIndirect; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - font = new HwmfFont(); - return font.init(leis); + return font.init(leis, recordSize); } @Override @@ -559,5 +641,10 @@ public class HwmfText { public HwmfFont getFont() { return font; } + + @Override + public String toString() { + return "{ font: "+font+" } "; + } } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java index 42913aad64..bbf5b9eb15 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java @@ -17,12 +17,23 @@ package org.apache.poi.hwmf.record; +import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; +import static org.apache.poi.hwmf.record.HwmfDraw.dimToString; +import static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds; +import static org.apache.poi.hwmf.record.HwmfDraw.pointToString; +import static org.apache.poi.hwmf.record.HwmfDraw.readBounds; +import static org.apache.poi.hwmf.record.HwmfDraw.readPointS; + import java.awt.Shape; import java.awt.geom.Area; +import java.awt.geom.Dimension2D; +import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; +import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.util.Dimension2DDouble; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -33,31 +44,33 @@ public class HwmfWindowing { */ public static class WmfSetViewportOrg implements HwmfRecord { - /** - * A 16-bit signed integer that defines the vertical offset, in device units. - */ - private int y; - - /** - * A 16-bit signed integer that defines the horizontal offset, in device units. - */ - private int x; + protected final Point2D origin = new Point2D.Double(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setViewportOrg; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - y = leis.readShort(); - x = leis.readShort(); - return 2*LittleEndianConsts.SHORT_SIZE; + return readPointS(leis, origin); } @Override public void draw(HwmfGraphics ctx) { - ctx.getProperties().setViewportOrg(x, y); + final HwmfDrawProperties prop = ctx.getProperties(); + Rectangle2D old = prop.getViewport(); + double oldX = (old == null ? 0 : old.getX()); + double oldY = (old == null ? 0 : old.getY()); + if (oldX != origin.getX() || oldY != origin.getY()) { + prop.setViewportOrg(origin.getX(), origin.getY()); + ctx.updateWindowMapMode(); + } + } + + @Override + public String toString() { + return pointToString(origin); } } @@ -67,33 +80,38 @@ public class HwmfWindowing { */ public static class WmfSetViewportExt implements HwmfRecord { - /** - * A 16-bit signed integer that defines the vertical extent - * of the viewport in device units. - */ - private int height; - - /** - * A 16-bit signed integer that defines the horizontal extent - * of the viewport in device units. - */ - private int width; + protected final Dimension2D extents = new Dimension2DDouble(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setViewportExt; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - height = leis.readShort(); - width = leis.readShort(); + // A signed integer that defines the vertical extent of the viewport in device units. + int height = leis.readShort(); + // A signed integer that defines the horizontal extent of the viewport in device units. + int width = leis.readShort(); + extents.setSize(width, height); return 2*LittleEndianConsts.SHORT_SIZE; } @Override public void draw(HwmfGraphics ctx) { - ctx.getProperties().setViewportExt(width, height); + final HwmfDrawProperties prop = ctx.getProperties(); + Rectangle2D old = prop.getViewport(); + double oldW = (old == null ? 0 : old.getWidth()); + double oldH = (old == null ? 0 : old.getHeight()); + if (oldW != extents.getWidth() || oldH != extents.getHeight()) { + prop.setViewportExt(extents.getWidth(), extents.getHeight()); + ctx.updateWindowMapMode(); + } + } + + @Override + public String toString() { + return dimToString(extents); } } @@ -103,34 +121,33 @@ public class HwmfWindowing { */ public static class WmfOffsetViewportOrg implements HwmfRecord { - /** - * A 16-bit signed integer that defines the vertical offset, in device units. - */ - private int yOffset; - - /** - * A 16-bit signed integer that defines the horizontal offset, in device units. - */ - private int xOffset; + protected final Point2D offset = new Point2D.Double(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.offsetViewportOrg; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - yOffset = leis.readShort(); - xOffset = leis.readShort(); - return 2*LittleEndianConsts.SHORT_SIZE; + return readPointS(leis, offset); } @Override public void draw(HwmfGraphics ctx) { - Rectangle2D viewport = ctx.getProperties().getViewport(); - double x = (viewport == null) ? 0 : viewport.getX(); - double y = (viewport == null) ? 0 : viewport.getY(); - ctx.getProperties().setViewportOrg(x+xOffset, y+yOffset); + final HwmfDrawProperties prop = ctx.getProperties(); + Rectangle2D viewport = prop.getViewport(); + if (offset.getX() != 0 || offset.getY() != 0) { + double x = (viewport == null) ? 0 : viewport.getX(); + double y = (viewport == null) ? 0 : viewport.getY(); + prop.setViewportOrg(x + offset.getX(), y + offset.getY()); + ctx.updateWindowMapMode(); + } + } + + @Override + public String toString() { + return pointToString(offset); } } @@ -139,40 +156,41 @@ public class HwmfWindowing { */ public static class WmfSetWindowOrg implements HwmfRecord { - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units. - */ - private int y; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units. - */ - private int x; + protected final Point2D origin = new Point2D.Double(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setWindowOrg; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - y = leis.readShort(); - x = leis.readShort(); - return 2*LittleEndianConsts.SHORT_SIZE; + return readPointS(leis, origin); } @Override public void draw(HwmfGraphics ctx) { - ctx.getProperties().setWindowOrg(x, y); - ctx.updateWindowMapMode(); + final HwmfDrawProperties prop = ctx.getProperties(); + final Rectangle2D old = prop.getWindow(); + double oldX = (old == null ? 0 : old.getX()); + double oldY = (old == null ? 0 : old.getY()); + if (oldX != getX() || oldY != getY()) { + prop.setWindowOrg(getX(), getY()); + ctx.updateWindowMapMode(); + } } - public int getY() { - return y; + public double getY() { + return origin.getY(); } - public int getX() { - return x; + public double getX() { + return origin.getX(); + } + + @Override + public String toString() { + return pointToString(origin); } } @@ -182,42 +200,42 @@ public class HwmfWindowing { */ public static class WmfSetWindowExt implements HwmfRecord { - /** - * A 16-bit signed integer that defines the vertical extent of - * the window in logical units. - */ - private int height; - - /** - * A 16-bit signed integer that defines the horizontal extent of - * the window in logical units. - */ - private int width; + protected final Dimension2D size = new Dimension2DDouble(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.setWindowExt; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - height = leis.readShort(); - width = leis.readShort(); + // A signed integer that defines the vertical extent of the window in logical units. + int height = leis.readShort(); + // A signed integer that defines the horizontal extent of the window in logical units. + int width = leis.readShort(); + size.setSize(width, height); return 2*LittleEndianConsts.SHORT_SIZE; } @Override public void draw(HwmfGraphics ctx) { - ctx.getProperties().setWindowExt(width, height); - ctx.updateWindowMapMode(); + final HwmfDrawProperties prop = ctx.getProperties(); + Rectangle2D old = prop.getWindow(); + double oldW = (old == null ? 0 : old.getWidth()); + double oldH = (old == null ? 0 : old.getHeight()); + if (oldW != size.getWidth() || oldH != size.getHeight()) { + prop.setWindowExt(size.getWidth(), size.getHeight()); + ctx.updateWindowMapMode(); + } } - public int getHeight() { - return height; + public Dimension2D getSize() { + return size; } - public int getWidth() { - return width; + @Override + public String toString() { + return dimToString(size); } } @@ -227,33 +245,31 @@ public class HwmfWindowing { */ public static class WmfOffsetWindowOrg implements HwmfRecord { - /** - * A 16-bit signed integer that defines the vertical offset, in device units. - */ - private int yOffset; - - /** - * A 16-bit signed integer that defines the horizontal offset, in device units. - */ - private int xOffset; + protected final Point2D offset = new Point2D.Double(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.offsetWindowOrg; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - yOffset = leis.readShort(); - xOffset = leis.readShort(); - return 2*LittleEndianConsts.SHORT_SIZE; + return readPointS(leis, offset); } @Override public void draw(HwmfGraphics ctx) { - Rectangle2D window = ctx.getProperties().getWindow(); - ctx.getProperties().setWindowOrg(window.getX()+xOffset, window.getY()+yOffset); - ctx.updateWindowMapMode(); + final HwmfDrawProperties prop = ctx.getProperties(); + Rectangle2D old = prop.getWindow(); + if (offset.getX() != 0 || offset.getY() != 0) { + prop.setWindowOrg(old.getX() + offset.getX(), old.getY() + offset.getY()); + ctx.updateWindowMapMode(); + } + } + + @Override + public String toString() { + return pointToString(offset); } } @@ -263,51 +279,48 @@ public class HwmfWindowing { */ public static class WmfScaleWindowExt implements HwmfRecord { - /** - * A 16-bit signed integer that defines the amount by which to divide the - * result of multiplying the current y-extent by the value of the yNum member. - */ - private int yDenom; - - /** - * A 16-bit signed integer that defines the amount by which to multiply the - * current y-extent. - */ - private int yNum; - - /** - * A 16-bit signed integer that defines the amount by which to divide the - * result of multiplying the current x-extent by the value of the xNum member. - */ - private int xDenom; - - /** - * A 16-bit signed integer that defines the amount by which to multiply the - * current x-extent. - */ - private int xNum; + protected final Dimension2D scale = new Dimension2DDouble(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.scaleWindowExt; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - yDenom = leis.readShort(); - yNum = leis.readShort(); - xDenom = leis.readShort(); - xNum = leis.readShort(); + // A signed integer that defines the amount by which to divide the + // result of multiplying the current y-extent by the value of the yNum member. + double yDenom = leis.readShort(); + // A signed integer that defines the amount by which to multiply the + // current y-extent. + double yNum = leis.readShort(); + // A signed integer that defines the amount by which to divide the + // result of multiplying the current x-extent by the value of the xNum member. + double xDenom = leis.readShort(); + // A signed integer that defines the amount by which to multiply the + // current x-extent. + double xNum = leis.readShort(); + + scale.setSize(xNum / xDenom, yNum / yDenom); + return 4*LittleEndianConsts.SHORT_SIZE; } @Override public void draw(HwmfGraphics ctx) { - Rectangle2D window = ctx.getProperties().getWindow(); - double width = window.getWidth() * xNum / xDenom; - double height = window.getHeight() * yNum / yDenom; - ctx.getProperties().setWindowExt(width, height); - ctx.updateWindowMapMode(); + final HwmfDrawProperties prop = ctx.getProperties(); + Rectangle2D old = prop.getWindow(); + if (scale.getWidth() != 1.0 || scale.getHeight() != 1.0) { + double width = old.getWidth() * scale.getWidth(); + double height = old.getHeight() * scale.getHeight(); + ctx.getProperties().setWindowExt(width, height); + ctx.updateWindowMapMode(); + } + } + + @Override + public String toString() { + return "{ scaleX: "+scale.getWidth()+", scaleY: "+scale.getHeight()+" }"; } } @@ -319,53 +332,49 @@ public class HwmfWindowing { */ public static class WmfScaleViewportExt implements HwmfRecord { - /** - * A 16-bit signed integer that defines the amount by which to divide the - * result of multiplying the current y-extent by the value of the yNum member. - */ - private int yDenom; - - /** - * A 16-bit signed integer that defines the amount by which to multiply the - * current y-extent. - */ - private int yNum; - - /** - * A 16-bit signed integer that defines the amount by which to divide the - * result of multiplying the current x-extent by the value of the xNum member. - */ - private int xDenom; - - /** - * A 16-bit signed integer that defines the amount by which to multiply the - * current x-extent. - */ - private int xNum; + protected final Dimension2D scale = new Dimension2DDouble(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.scaleViewportExt; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - yDenom = leis.readShort(); - yNum = leis.readShort(); - xDenom = leis.readShort(); - xNum = leis.readShort(); + // A signed integer that defines the amount by which to divide the + // result of multiplying the current y-extent by the value of the yNum member. + double yDenom = leis.readShort(); + // A signed integer that defines the amount by which to multiply the + // current y-extent. + double yNum = leis.readShort(); + // A signed integer that defines the amount by which to divide the + // result of multiplying the current x-extent by the value of the xNum member. + double xDenom = leis.readShort(); + // A signed integer that defines the amount by which to multiply the + // current x-extent. + double xNum = leis.readShort(); + + scale.setSize(xNum / xDenom, yNum / yDenom); + return 4*LittleEndianConsts.SHORT_SIZE; } @Override public void draw(HwmfGraphics ctx) { - Rectangle2D viewport = ctx.getProperties().getViewport(); - if (viewport == null) { - viewport = ctx.getProperties().getWindow(); + final HwmfDrawProperties prop = ctx.getProperties(); + final Rectangle2D old = prop.getViewport() == null ? prop.getWindow() : prop.getViewport(); + + if (scale.getWidth() != 1.0 || scale.getHeight() != 1.0) { + double width = old.getWidth() * scale.getWidth(); + double height = old.getHeight() * scale.getHeight(); + prop.setViewportExt(width, height); + ctx.updateWindowMapMode(); } - double width = viewport.getWidth() * xNum / xDenom; - double height = viewport.getHeight() * yNum / yDenom; - ctx.getProperties().setViewportExt(width, height); + } + + @Override + public String toString() { + return "{ scaleX: "+scale.getWidth()+", scaleY: "+scale.getHeight()+" }"; } } @@ -375,26 +384,16 @@ public class HwmfWindowing { */ public static class WmfOffsetClipRgn implements HwmfRecord, HwmfObjectTableEntry { - /** - * A 16-bit signed integer that defines the number of logical units to move up or down. - */ - private int yOffset; - - /** - * A 16-bit signed integer that defines the number of logical units to move left or right. - */ - private int xOffset; + protected final Point2D offset = new Point2D.Double(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.offsetClipRgn; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - yOffset = leis.readShort(); - xOffset = leis.readShort(); - return 2*LittleEndianConsts.SHORT_SIZE; + return readPointS(leis, offset); } @Override @@ -405,6 +404,11 @@ public class HwmfWindowing { @Override public void applyObject(HwmfGraphics ctx) { } + + @Override + public String toString() { + return pointToString(offset); + } } /** @@ -413,42 +417,17 @@ public class HwmfWindowing { */ public static class WmfExcludeClipRect implements HwmfRecord, HwmfObjectTableEntry { - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * lower-right corner of the rectangle. - */ - private int bottom; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * lower-right corner of the rectangle. - */ - private int right; - - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * upper-left corner of the rectangle. - */ - private int top; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * upper-left corner of the rectangle. - */ - private int left; + /** a rectangle in logical units */ + protected final Rectangle2D bounds = new Rectangle2D.Double(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.excludeClipRect; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - bottom = leis.readShort(); - right = leis.readShort(); - top = leis.readShort(); - left = leis.readShort(); - return 4*LittleEndianConsts.SHORT_SIZE; + return readBounds(leis, bounds); } @Override @@ -458,6 +437,12 @@ public class HwmfWindowing { @Override public void applyObject(HwmfGraphics ctx) { + ctx.setClip(normalizeBounds(bounds), HwmfRegionMode.RGN_DIFF, false); + } + + @Override + public String toString() { + return boundsToString(bounds); } } @@ -468,42 +453,17 @@ public class HwmfWindowing { */ public static class WmfIntersectClipRect implements HwmfRecord, HwmfObjectTableEntry { - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * lower-right corner of the rectangle. - */ - private int bottom; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * lower-right corner of the rectangle. - */ - private int right; - - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * upper-left corner of the rectangle. - */ - private int top; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * upper-left corner of the rectangle. - */ - private int left; + /** a rectangle in logical units */ + protected final Rectangle2D bounds = new Rectangle2D.Double(); @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.intersectClipRect; } @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - bottom = leis.readShort(); - right = leis.readShort(); - top = leis.readShort(); - left = leis.readShort(); - return 4*LittleEndianConsts.SHORT_SIZE; + return readBounds(leis, bounds); } @Override @@ -513,6 +473,12 @@ public class HwmfWindowing { @Override public void applyObject(HwmfGraphics ctx) { + ctx.setClip(bounds, HwmfRegionMode.RGN_AND, true); + } + + @Override + public String toString() { + return boundsToString(bounds); } } @@ -528,7 +494,7 @@ public class HwmfWindowing { private int region; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.selectClipRegion; } @@ -620,29 +586,7 @@ public class HwmfWindowing { */ private int maxScan; - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * lower-right corner of the rectangle. - */ - private int bottom; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * lower-right corner of the rectangle. - */ - private int right; - - /** - * A 16-bit signed integer that defines the y-coordinate, in logical units, of the - * upper-left corner of the rectangle. - */ - private int top; - - /** - * A 16-bit signed integer that defines the x-coordinate, in logical units, of the - * upper-left corner of the rectangle. - */ - private int left; + private Rectangle2D bounds = new Rectangle2D.Double(); /** * An array of Scan objects that define the scanlines in the region. @@ -650,7 +594,7 @@ public class HwmfWindowing { private WmfScanObject scanObjects[]; @Override - public HwmfRecordType getRecordType() { + public HwmfRecordType getWmfRecordType() { return HwmfRecordType.createRegion; } @@ -662,10 +606,19 @@ public class HwmfWindowing { regionSize = leis.readShort(); scanCount = leis.readShort(); maxScan = leis.readShort(); - left = leis.readShort(); - top = leis.readShort(); - right = leis.readShort(); - bottom = leis.readShort(); + // A 16-bit signed integer that defines the x-coordinate, in logical units, of the + // upper-left corner of the rectangle. + double left = leis.readShort(); + // A 16-bit signed integer that defines the y-coordinate, in logical units, of the + // upper-left corner of the rectangle. + double top = leis.readShort(); + // A 16-bit signed integer that defines the x-coordinate, in logical units, of the + // lower-right corner of the rectangle. + double right = leis.readShort(); + // A 16-bit signed integer that defines the y-coordinate, in logical units, of the + // lower-right corner of the rectangle. + double bottom = leis.readShort(); + bounds.setRect(left, top, right-left, bottom-top); int size = 9*LittleEndianConsts.SHORT_SIZE+LittleEndianConsts.INT_SIZE; diff --git a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java index fa59f4bc31..bf91dc864b 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java @@ -43,6 +43,9 @@ import org.apache.poi.util.RecordFormatException; import org.apache.poi.util.Units; public class HwmfPicture { + /** Max. record length - processing longer records will throw an exception */ + public static final int MAX_RECORD_LENGTH = 50_000_000; + private static final POILogger logger = POILogFactory.getLogger(HwmfPicture.class); final List records = new ArrayList<>(); @@ -51,8 +54,7 @@ public class HwmfPicture { public HwmfPicture(InputStream inputStream) throws IOException { - try (BufferedInputStream bis = new BufferedInputStream(inputStream, 10000); - LittleEndianInputStream leis = new LittleEndianInputStream(bis)) { + try (LittleEndianInputStream leis = new LittleEndianInputStream(inputStream)) { placeableHeader = HwmfPlaceableHeader.readHeader(leis); header = new HwmfHeader(leis); @@ -82,17 +84,12 @@ public class HwmfPicture { if (wrt == HwmfRecordType.eof) { break; } - if (wrt.clazz == null) { + if (wrt.constructor == null) { throw new IOException("unsupported record type: "+recordFunction); } - HwmfRecord wr; - try { - wr = wrt.clazz.newInstance(); - records.add(wr); - } catch (Exception e) { - throw (IOException)new IOException("can't create wmf record").initCause(e); - } + final HwmfRecord wr = wrt.constructor.get(); + records.add(wr); consumedSize += wr.init(leis, recordSize, recordFunction); int remainingSize = (int)(recordSize - consumedSize); @@ -162,7 +159,7 @@ public class HwmfPicture { if (wOrg == null || wExt == null) { throw new RuntimeException("invalid wmf file - window records are incomplete."); } - return new Rectangle2D.Double(wOrg.getX(), wOrg.getY(), wExt.getWidth(), wExt.getHeight()); + return new Rectangle2D.Double(wOrg.getX(), wOrg.getY(), wExt.getSize().getWidth(), wExt.getSize().getHeight()); } } diff --git a/src/scratchpad/testcases/org/apache/poi/hemf/extractor/HemfExtractorTest.java b/src/scratchpad/testcases/org/apache/poi/hemf/extractor/HemfExtractorTest.java deleted file mode 100644 index 8d32b21a5e..0000000000 --- a/src/scratchpad/testcases/org/apache/poi/hemf/extractor/HemfExtractorTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* ==================================================================== - 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. -==================================================================== */ - - -package org.apache.poi.hemf.extractor; - -import static org.apache.poi.POITestCase.assertContains; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.util.HashSet; -import java.util.Set; - -import org.apache.poi.POIDataSamples; -import org.apache.poi.hemf.record.AbstractHemfComment; -import org.apache.poi.hemf.record.HemfCommentPublic; -import org.apache.poi.hemf.record.HemfCommentRecord; -import org.apache.poi.hemf.record.HemfHeader; -import org.apache.poi.hemf.record.HemfRecord; -import org.apache.poi.hemf.record.HemfRecordType; -import org.apache.poi.hemf.record.HemfText; -import org.apache.poi.util.IOUtils; -import org.apache.poi.util.RecordFormatException; -import org.junit.Test; - -public class HemfExtractorTest { - - @Test - public void testBasicWindows() throws Exception { - InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleEMF_windows.emf"); - HemfExtractor ex = new HemfExtractor(is); - HemfHeader header = ex.getHeader(); - assertEquals(27864, header.getBytes()); - assertEquals(31, header.getRecords()); - assertEquals(3, header.getHandles()); - assertEquals(346000, header.getMicrometersX()); - assertEquals(194000, header.getMicrometersY()); - - int records = 0; - for (HemfRecord record : ex) { - records++; - } - - assertEquals(header.getRecords() - 1, records); - } - - @Test - public void testBasicMac() throws Exception { - InputStream is = - POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleEMF_mac.emf"); - HemfExtractor ex = new HemfExtractor(is); - HemfHeader header = ex.getHeader(); - - int records = 0; - boolean extractedData = false; - for (HemfRecord record : ex) { - if (record.getRecordType() == HemfRecordType.comment) { - AbstractHemfComment comment = ((HemfCommentRecord) record).getComment(); - if (comment instanceof HemfCommentPublic.MultiFormats) { - for (HemfCommentPublic.HemfMultiFormatsData d : ((HemfCommentPublic.MultiFormats) comment).getData()) { - byte[] data = d.getData(); - //make sure header starts at 0 - assertEquals('%', data[0]); - assertEquals('P', data[1]); - assertEquals('D', data[2]); - assertEquals('F', data[3]); - - //make sure byte array ends at EOF\n - assertEquals('E', data[data.length - 4]); - assertEquals('O', data[data.length - 3]); - assertEquals('F', data[data.length - 2]); - assertEquals('\n', data[data.length - 1]); - extractedData = true; - } - } - } - records++; - } - assertTrue(extractedData); - assertEquals(header.getRecords() - 1, records); - } - - @Test - public void testMacText() throws Exception { - InputStream is = - POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleEMF_mac.emf"); - HemfExtractor ex = new HemfExtractor(is); - - long lastY = -1; - long lastX = -1; - long fudgeFactorX = 1000;//derive this from the font information! - StringBuilder sb = new StringBuilder(); - for (HemfRecord record : ex) { - if (record.getRecordType().equals(HemfRecordType.exttextoutw)) { - HemfText.ExtTextOutW extTextOutW = (HemfText.ExtTextOutW) record; - if (lastY > -1 && lastY != extTextOutW.getY()) { - sb.append("\n"); - lastX = -1; - } - if (lastX > -1 && extTextOutW.getX() - lastX > fudgeFactorX) { - sb.append(" "); - } - sb.append(extTextOutW.getText()); - lastY = extTextOutW.getY(); - lastX = extTextOutW.getX(); - } - } - String txt = sb.toString(); - assertContains(txt, "Tika http://incubator.apache.org"); - assertContains(txt, "Latest News\n"); - } - - @Test - public void testWindowsText() throws Exception { - InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("SimpleEMF_windows.emf"); - HemfExtractor ex = new HemfExtractor(is); - long lastY = -1; - long lastX = -1; - long fudgeFactorX = 1000;//derive this from the font or frame/bounds information - StringBuilder sb = new StringBuilder(); - Set expectedParts = new HashSet<>(); - expectedParts.add("C:\\Users\\tallison\\"); - expectedParts.add("testPDF.pdf"); - int foundExpected = 0; - for (HemfRecord record : ex) { - if (record.getRecordType().equals(HemfRecordType.exttextoutw)) { - HemfText.ExtTextOutW extTextOutW = (HemfText.ExtTextOutW) record; - if (lastY > -1 && lastY != extTextOutW.getY()) { - sb.append("\n"); - lastX = -1; - } - if (lastX > -1 && extTextOutW.getX() - lastX > fudgeFactorX) { - sb.append(" "); - } - String txt = extTextOutW.getText(); - if (expectedParts.contains(txt)) { - foundExpected++; - } - sb.append(txt); - lastY = extTextOutW.getY(); - lastX = extTextOutW.getX(); - } - } - String txt = sb.toString(); - assertContains(txt, "C:\\Users\\tallison\\\n"); - assertContains(txt, "asf2-git-1.x\\tika-\n"); - assertEquals(expectedParts.size(), foundExpected); - } - - @Test(expected = RecordFormatException.class) - public void testInfiniteLoopOnFile() throws Exception { - InputStream is = null; - try { - is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("61294.emf"); - - HemfExtractor ex = new HemfExtractor(is); - for (HemfRecord record : ex) { - - } - } finally { - IOUtils.closeQuietly(is); - } - } - - @Test(expected = RecordFormatException.class) - public void testInfiniteLoopOnByteArray() throws Exception { - InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream("61294.emf"); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - IOUtils.copy(is, bos); - is.close(); - - HemfExtractor ex = new HemfExtractor(new ByteArrayInputStream(bos.toByteArray())); - for (HemfRecord record : ex) { - - } - } - - /* - govdocs1 064213.doc-0.emf contains an example of extextouta - */ -} \ No newline at end of file diff --git a/src/scratchpad/testcases/org/apache/poi/hemf/hemfplus/extractor/HemfPlusExtractorTest.java b/src/scratchpad/testcases/org/apache/poi/hemf/hemfplus/extractor/HemfPlusExtractorTest.java index f3bdbf7b4f..b42bac7d5d 100644 --- a/src/scratchpad/testcases/org/apache/poi/hemf/hemfplus/extractor/HemfPlusExtractorTest.java +++ b/src/scratchpad/testcases/org/apache/poi/hemf/hemfplus/extractor/HemfPlusExtractorTest.java @@ -25,13 +25,13 @@ import java.util.ArrayList; import java.util.List; import org.apache.poi.POIDataSamples; -import org.apache.poi.hemf.extractor.HemfExtractor; -import org.apache.poi.hemf.hemfplus.record.HemfPlusHeader; -import org.apache.poi.hemf.hemfplus.record.HemfPlusRecord; -import org.apache.poi.hemf.hemfplus.record.HemfPlusRecordType; -import org.apache.poi.hemf.record.HemfCommentEMFPlus; -import org.apache.poi.hemf.record.HemfCommentRecord; -import org.apache.poi.hemf.record.HemfRecord; +import org.apache.poi.hemf.record.emf.HemfComment.EmfComment; +import org.apache.poi.hemf.record.emf.HemfComment.EmfCommentDataPlus; +import org.apache.poi.hemf.record.emf.HemfRecord; +import org.apache.poi.hemf.record.emfplus.HemfPlusHeader; +import org.apache.poi.hemf.record.emfplus.HemfPlusRecord; +import org.apache.poi.hemf.record.emfplus.HemfPlusRecordType; +import org.apache.poi.hemf.usermodel.HemfPicture; import org.junit.Test; public class HemfPlusExtractorTest { @@ -39,10 +39,10 @@ public class HemfPlusExtractorTest { @Test public void testBasic() throws Exception { //test header - HemfCommentEMFPlus emfPlus = getCommentRecord("SimpleEMF_windows.emf", 0); + EmfCommentDataPlus emfPlus = getCommentRecord("SimpleEMF_windows.emf", 0); List records = emfPlus.getRecords(); assertEquals(1, records.size()); - assertEquals(HemfPlusRecordType.header, records.get(0).getRecordType()); + assertEquals(HemfPlusRecordType.header, records.get(0).getEmfPlusRecordType()); HemfPlusHeader header = (HemfPlusHeader)records.get(0); assertEquals(240, header.getLogicalDpiX()); @@ -67,29 +67,25 @@ public class HemfPlusExtractorTest { assertEquals(expected.size(), records.size()); for (int i = 0; i < expected.size(); i++) { - assertEquals(expected.get(i), records.get(i).getRecordType()); + assertEquals(expected.get(i), records.get(i).getEmfPlusRecordType()); } } - private HemfCommentEMFPlus getCommentRecord(String testFileName, int recordIndex) throws Exception { - InputStream is = null; - HemfCommentEMFPlus returnRecord = null; + private EmfCommentDataPlus getCommentRecord(String testFileName, int recordIndex) throws Exception { + EmfCommentDataPlus returnRecord = null; - try { - is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream(testFileName); - HemfExtractor ex = new HemfExtractor(is); + try (InputStream is = POIDataSamples.getSpreadSheetInstance().openResourceAsStream(testFileName)) { + HemfPicture ex = new HemfPicture(is); int i = 0; for (HemfRecord record : ex) { if (i == recordIndex) { - HemfCommentRecord commentRecord = ((HemfCommentRecord) record); - returnRecord = (HemfCommentEMFPlus) commentRecord.getComment(); + EmfComment commentRecord = ((EmfComment) record); + returnRecord = (EmfCommentDataPlus) commentRecord.getCommentData(); break; } i++; } - } finally { - is.close(); } return returnRecord; } diff --git a/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java b/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java new file mode 100644 index 0000000000..8ff4742be6 --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java @@ -0,0 +1,353 @@ +/* ==================================================================== + 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. +==================================================================== */ + + +package org.apache.poi.hemf.usermodel; + +import static org.apache.poi.POITestCase.assertContains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.awt.geom.Point2D; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.hemf.record.emf.HemfComment; +import org.apache.poi.hemf.record.emf.HemfComment.EmfComment; +import org.apache.poi.hemf.record.emf.HemfComment.EmfCommentDataFormat; +import org.apache.poi.hemf.record.emf.HemfComment.EmfCommentDataMultiformats; +import org.apache.poi.hemf.record.emf.HemfHeader; +import org.apache.poi.hemf.record.emf.HemfRecord; +import org.apache.poi.hemf.record.emf.HemfRecordType; +import org.apache.poi.hemf.record.emf.HemfText; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.RecordFormatException; +import org.junit.Test; + +public class HemfPictureTest { + + private static final POIDataSamples ss_samples = POIDataSamples.getSpreadSheetInstance(); + private static final POIDataSamples sl_samples = POIDataSamples.getSlideShowInstance(); + + /* + @Test + @Ignore("Only for manual tests - need to add org.tukaani:xz:1.8 for this to work") + public void paint() throws IOException { + byte buf[] = new byte[50_000_000]; + + // good test samples to validate rendering: + // emfs/commoncrawl2/NB/NBWN2YH5VFCLZRFDQU7PB7IDD4UKY7DN_2.emf + // emfs/govdocs1/777/777525.ppt_0.emf + // emfs/govdocs1/844/844795.ppt_2.emf + // emfs/commoncrawl2/TO/TOYZSTNUSW5OFCFUQ6T5FBLIDLCRF3NH_0.emf + + final boolean writeLog = true; + final boolean dumpRecords = false; + final boolean savePng = true; + + Set passed = new HashSet<>(); + + try (BufferedWriter sucWrite = parseEmfLog(passed, "emf-success.txt"); + BufferedWriter parseError = parseEmfLog(passed, "emf-parse.txt"); + BufferedWriter renderError = parseEmfLog(passed, "emf-render.txt"); + SevenZFile sevenZFile = new SevenZFile(new File("tmp/render_emf.7z"))) { + for (int idx=0;;idx++) { + SevenZArchiveEntry entry = sevenZFile.getNextEntry(); + if (entry == null) break; + final String etName = entry.getName(); + + if (entry.isDirectory() || !etName.endsWith(".emf") || passed.contains(etName)) continue; + + System.out.println(etName); + + int size = sevenZFile.read(buf); + + HemfPicture emf = null; + try { + emf = new HemfPicture(new ByteArrayInputStream(buf, 0, size)); + + // initialize parsing + emf.getRecords(); + } catch (Exception|AssertionError e) { + if (writeLog) { + parseError.write(etName+" "+hashException(e)+"\n"); + parseError.flush(); + } + System.out.println("parse error"); + // continue with the read records up to the error anyway + if (emf.getRecords().isEmpty()) { + continue; + } + } + + if (dumpRecords) { + dumpRecords(emf); + } + + Graphics2D g = null; + try { + Dimension2D dim = emf.getSize(); + double width = Units.pointsToPixel(dim.getWidth()); + // keep aspect ratio for height + double height = Units.pointsToPixel(dim.getHeight()); + double max = Math.max(width, height); + if (max > 1500.) { + width *= 1500. / max; + height *= 1500. / max; + } + width = Math.ceil(width); + height = Math.ceil(height); + + BufferedImage bufImg = new BufferedImage((int)width, (int)height, BufferedImage.TYPE_INT_ARGB); + g = bufImg.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, (int)width, (int)height); + g.setComposite(AlphaComposite.Src); + + emf.draw(g, new Rectangle2D.Double(0, 0, width, height)); + + final File pngName = new File("build/tmp", etName.replaceFirst(".+/", "").replace(".emf", ".png")); + if (savePng) { + ImageIO.write(bufImg, "PNG", pngName); + } + } catch (Exception|AssertionError e) { + System.out.println("render error"); + if (writeLog) { + // dumpRecords(emf.getRecords()); + renderError.write(etName+" "+hashException(e)+"\n"); + renderError.flush(); + } + continue; + } finally { + if (g != null) g.dispose(); + } + + if (writeLog) { + sucWrite.write(etName + "\n"); + sucWrite.flush(); + } + } + } + } */ + + private static int hashException(Throwable e) { + StringBuilder sb = new StringBuilder(); + for (StackTraceElement se : e.getStackTrace()) { + sb.append(se.getClassName()+":"+se.getLineNumber()); + } + return sb.toString().hashCode(); + } + + private static void dumpRecords(HemfPicture emf) throws IOException { + FileWriter fw = new FileWriter("record-list.txt"); + int i = 0; + for (HemfRecord r : emf.getRecords()) { + if (r.getEmfRecordType() != HemfRecordType.comment) { + fw.write(i + " " + r.getEmfRecordType() + " " + r.toString() + "\n"); + } + i++; + } + fw.close(); + } + + private static BufferedWriter parseEmfLog(Set passed, String logFile) throws IOException { + Path log = Paths.get(logFile); + + StandardOpenOption soo; + if (Files.exists(log)) { + soo = StandardOpenOption.APPEND; + try (Stream stream = Files.lines(log)) { + stream.forEach((s) -> passed.add(s.split("\\s")[0])); + } + } else { + soo = StandardOpenOption.CREATE; + } + + return Files.newBufferedWriter(log, StandardCharsets.UTF_8, soo); + } + + @Test + public void testBasicWindows() throws Exception { + try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_windows.emf")) { + HemfPicture pic = new HemfPicture(is); + HemfHeader header = pic.getHeader(); + assertEquals(27864, header.getBytes()); + assertEquals(31, header.getRecords()); + assertEquals(3, header.getHandles()); + assertEquals(346000, header.getMicroDimension().getWidth()); + assertEquals(194000, header.getMicroDimension().getHeight()); + + List records = pic.getRecords(); + + assertEquals(31, records.size()); + } + } + + @Test + public void testBasicMac() throws Exception { + try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_mac.emf")) { + HemfPicture pic = new HemfPicture(is); + HemfHeader header = pic.getHeader(); + + int records = 0; + boolean extractedData = false; + for (HemfRecord record : pic) { + if (record.getEmfRecordType() == HemfRecordType.comment) { + HemfComment.EmfCommentData comment = ((EmfComment) record).getCommentData(); + if (comment instanceof EmfCommentDataMultiformats) { + for (EmfCommentDataFormat d : ((EmfCommentDataMultiformats) comment).getFormats()) { + byte[] data = d.getRawData(); + //make sure header starts at 0 + assertEquals('%', data[0]); + assertEquals('P', data[1]); + assertEquals('D', data[2]); + assertEquals('F', data[3]); + + //make sure byte array ends at EOF\n + assertEquals('E', data[data.length - 4]); + assertEquals('O', data[data.length - 3]); + assertEquals('F', data[data.length - 2]); + assertEquals('\n', data[data.length - 1]); + extractedData = true; + } + } + } + records++; + } + assertTrue(extractedData); + assertEquals(header.getRecords(), records); + } + } + + @Test + public void testMacText() throws Exception { + try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_mac.emf")) { + HemfPicture pic = new HemfPicture(is); + + double lastY = -1; + double lastX = -1; + //derive this from the font information! + long fudgeFactorX = 1000; + StringBuilder sb = new StringBuilder(); + for (HemfRecord record : pic) { + if (record.getEmfRecordType().equals(HemfRecordType.extTextOutW)) { + HemfText.EmfExtTextOutW extTextOutW = (HemfText.EmfExtTextOutW) record; + Point2D reference = extTextOutW.getReference(); + if (lastY > -1 && lastY != reference.getY()) { + sb.append("\n"); + lastX = -1; + } + if (lastX > -1 && reference.getX() - lastX > fudgeFactorX) { + sb.append(" "); + } + sb.append(extTextOutW.getText()); + lastY = reference.getY(); + lastX = reference.getX(); + } + } + String txt = sb.toString(); + assertContains(txt, "Tika http://incubator.apache.org"); + assertContains(txt, "Latest News\n"); + } + } + + @Test + public void testWindowsText() throws Exception { + try (InputStream is = ss_samples.openResourceAsStream("SimpleEMF_windows.emf")) { + HemfPicture pic = new HemfPicture(is); + double lastY = -1; + double lastX = -1; + long fudgeFactorX = 1000;//derive this from the font or frame/bounds information + StringBuilder sb = new StringBuilder(); + Set expectedParts = new HashSet<>(); + expectedParts.add("C:\\Users\\tallison\\"); + expectedParts.add("testPDF.pdf"); + int foundExpected = 0; + for (HemfRecord record : pic) { + if (record.getEmfRecordType().equals(HemfRecordType.extTextOutW)) { + HemfText.EmfExtTextOutW extTextOutW = (HemfText.EmfExtTextOutW) record; + Point2D reference = extTextOutW.getReference(); + if (lastY > -1 && lastY != reference.getY()) { + sb.append("\n"); + lastX = -1; + } + if (lastX > -1 && reference.getX() - lastX > fudgeFactorX) { + sb.append(" "); + } + String txt = extTextOutW.getText(); + if (expectedParts.contains(txt)) { + foundExpected++; + } + sb.append(txt); + lastY = reference.getY(); + lastX = reference.getX(); + } + } + String txt = sb.toString(); + assertContains(txt, "C:\\Users\\tallison\\\n"); + assertContains(txt, "asf2-git-1.x\\tika-\n"); + assertEquals(expectedParts.size(), foundExpected); + } + } + + @Test(expected = RecordFormatException.class) + public void testInfiniteLoopOnFile() throws Exception { + try (InputStream is = ss_samples.openResourceAsStream("61294.emf")) { + HemfPicture pic = new HemfPicture(is); + for (HemfRecord record : pic) { + + } + } + } + + @Test(expected = RecordFormatException.class) + public void testInfiniteLoopOnByteArray() throws Exception { + try (InputStream is = ss_samples.openResourceAsStream("61294.emf")) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + IOUtils.copy(is, bos); + is.close(); + + HemfPicture pic = new HemfPicture(new ByteArrayInputStream(bos.toByteArray())); + for (HemfRecord record : pic) { + + } + } + } + + /* + govdocs1 064213.doc-0.emf contains an example of extextouta + */ +} \ No newline at end of file diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPicture.java b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPicture.java index 72eee287c2..d071f2dbdc 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPicture.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPicture.java @@ -210,7 +210,6 @@ public final class TestPicture { } else { BufferedImage img = new BufferedImage(pg.width, pg.height, BufferedImage.TYPE_INT_ARGB); Graphics2D graphics = img.createGraphics(); - DrawFactory.getInstance(graphics).fixFonts(graphics); slide.draw(graphics); graphics.setColor(Color.BLACK); graphics.setStroke(new BasicStroke(1)); diff --git a/src/scratchpad/testcases/org/apache/poi/hwmf/TestHwmfParsing.java b/src/scratchpad/testcases/org/apache/poi/hwmf/TestHwmfParsing.java index 1667b67b49..df084cdad3 100644 --- a/src/scratchpad/testcases/org/apache/poi/hwmf/TestHwmfParsing.java +++ b/src/scratchpad/testcases/org/apache/poi/hwmf/TestHwmfParsing.java @@ -222,11 +222,11 @@ public class TestHwmfParsing { //this happens to work on this test file, but you need to //do what Graphics does by maintaining the stack, etc.! for (HwmfRecord r : wmf.getRecords()) { - if (r.getRecordType().equals(HwmfRecordType.createFontIndirect)) { + if (r.getWmfRecordType().equals(HwmfRecordType.createFontIndirect)) { HwmfFont font = ((HwmfText.WmfCreateFontIndirect)r).getFont(); charset = (font.getCharset().getCharset() == null) ? LocaleUtil.CHARSET_1252 : font.getCharset().getCharset(); } - if (r.getRecordType().equals(HwmfRecordType.extTextOut)) { + if (r.getWmfRecordType().equals(HwmfRecordType.extTextOut)) { HwmfText.WmfExtTextOut textOut = (HwmfText.WmfExtTextOut)r; sb.append(textOut.getText(charset)).append("\n"); } @@ -250,11 +250,11 @@ public class TestHwmfParsing { //this happens to work on this test file, but you need to //do what Graphics does by maintaining the stack, etc.! for (HwmfRecord r : wmf.getRecords()) { - if (r.getRecordType().equals(HwmfRecordType.createFontIndirect)) { + if (r.getWmfRecordType().equals(HwmfRecordType.createFontIndirect)) { HwmfFont font = ((HwmfText.WmfCreateFontIndirect)r).getFont(); charset = (font.getCharset().getCharset() == null) ? LocaleUtil.CHARSET_1252 : font.getCharset().getCharset(); } - if (r.getRecordType().equals(HwmfRecordType.extTextOut)) { + if (r.getWmfRecordType().equals(HwmfRecordType.extTextOut)) { HwmfText.WmfExtTextOut textOut = (HwmfText.WmfExtTextOut)r; sb.append(textOut.getText(charset)).append("\n"); }