Bug 60656 - Emf image support in slideshows

- Use ImageRenderer instead of prerendered BufferedImage to achieve better rendering result
- Fix extraction of EMF embeddings
- Fix renderer state handling in HemfGraphics (but still not perfect)

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1869582 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andreas Beeker 2019-11-08 20:32:46 +00:00
parent f7c28ad08f
commit 34fc1a45fe
25 changed files with 478 additions and 255 deletions

View File

@ -103,6 +103,8 @@ public enum FileMagic {
'?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?',
' ', 'E', 'M', 'F' ' ', 'E', 'M', 'F'
}), }),
/** BMP image */
BMP(new byte[]{'B','M'}),
// keep UNKNOWN always as last enum! // keep UNKNOWN always as last enum!
/** UNKNOWN magic */ /** UNKNOWN magic */
UNKNOWN(new byte[0]); UNKNOWN(new byte[0]);

View File

@ -322,4 +322,9 @@ public class BitmapImageRenderer implements ImageRenderer {
return true; return true;
} }
@Override
public Rectangle2D getNativeBounds() {
return new Rectangle2D.Double(0, 0, img.getWidth(), img.getHeight());
}
} }

View File

@ -100,6 +100,11 @@ public interface ImageRenderer {
*/ */
void loadImage(byte[] data, String contentType) throws IOException; void loadImage(byte[] data, String contentType) throws IOException;
/**
* @return the format-specific / not-normalized bounds of the image
*/
Rectangle2D getNativeBounds();
/** /**
* @return the dimension of the buffered image in pixel * @return the dimension of the buffered image in pixel
*/ */

View File

@ -17,7 +17,15 @@
package org.apache.poi.util; package org.apache.poi.util;
import java.io.*; import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.util.zip.CRC32; import java.util.zip.CRC32;
@ -561,6 +569,11 @@ public final class IOUtils {
} }
public static byte[] safelyAllocate(long length, int maxLength) { public static byte[] safelyAllocate(long length, int maxLength) {
safelyAllocateCheck(length, maxLength);
return new byte[(int)length];
}
public static void safelyAllocateCheck(long length, int maxLength) {
if (length < 0L) { if (length < 0L) {
throw new RecordFormatException("Can't allocate an array of length < 0, but had " + length + " and " + maxLength); throw new RecordFormatException("Can't allocate an array of length < 0, but had " + length + " and " + maxLength);
} }
@ -568,7 +581,6 @@ public final class IOUtils {
throw new RecordFormatException("Can't allocate an array > "+Integer.MAX_VALUE); throw new RecordFormatException("Can't allocate an array > "+Integer.MAX_VALUE);
} }
checkLength(length, maxLength); checkLength(length, maxLength);
return new byte[(int)length];
} }
/** /**

View File

@ -151,4 +151,9 @@ public class SVGImageRenderer implements ImageRenderer {
public boolean canRender(String contentType) { public boolean canRender(String contentType) {
return PictureData.PictureType.SVG.contentType.equalsIgnoreCase(contentType); return PictureData.PictureType.SVG.contentType.equalsIgnoreCase(contentType);
} }
@Override
public Rectangle2D getNativeBounds() {
return svgRoot.getPrimitiveBounds();
}
} }

View File

@ -225,8 +225,8 @@ public final class PPTX2PNG {
final Dimension2D dim = new Dimension2DDouble(); final Dimension2D dim = new Dimension2DDouble();
final double lenSide = getDimensions(proxy, dim); final double lenSide = getDimensions(proxy, dim);
final int width = (int)Math.rint(dim.getWidth()); final int width = Math.max((int)Math.rint(dim.getWidth()),1);
final int height = (int)Math.rint(dim.getHeight()); final int height = Math.max((int)Math.rint(dim.getHeight()),1);
for (int slideNo : slidenum) { for (int slideNo : slidenum) {
proxy.setSlideNo(slideNo); proxy.setSlideNo(slideNo);
@ -309,7 +309,11 @@ public final class PPTX2PNG {
return; return;
} }
GenericRecord gr = proxy.getRoot(); GenericRecord gr = proxy.getRoot();
try (GenericRecordJsonWriter fw = new GenericRecordJsonWriter(dumpfile)) { try (GenericRecordJsonWriter fw = new GenericRecordJsonWriter(dumpfile) {
protected boolean printBytes(String name, Object o) {
return false;
}
}) {
if (gr == null) { if (gr == null) {
fw.writeError(file.getName()+" doesn't support GenericRecord interface and can't be dumped to a file."); fw.writeError(file.getName()+" doesn't support GenericRecord interface and can't be dumped to a file.");
} else { } else {

View File

@ -23,7 +23,6 @@ import static org.junit.Assume.assumeFalse;
import java.io.File; import java.io.File;
import java.util.Collection; import java.util.Collection;
import java.util.Locale;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -83,7 +82,7 @@ public class TestPPTX2PNG {
"-slide", "-1", // -1 for all "-slide", "-1", // -1 for all
"-outdir", new File("build/tmp/").getCanonicalPath(), "-outdir", new File("build/tmp/").getCanonicalPath(),
"-outpat", "${basename}-${slideno}-${ext}.${format}", "-outpat", "${basename}-${slideno}-${ext}.${format}",
// "-dump", new File("build/tmp/", pptFile+".dump").getCanonicalPath(), // "-dump", new File("build/tmp/", pptFile+".json").getCanonicalPath(),
"-dump", "null", "-dump", "null",
"-quiet", "-quiet",
"-fixside", "long", "-fixside", "long",

View File

@ -19,21 +19,30 @@ package org.apache.poi.hemf.draw;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.BiConsumer;
import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle; import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle;
import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfDrawProperties;
import org.apache.poi.sl.draw.ImageRenderer;
public class HemfDrawProperties extends HwmfDrawProperties { public class HemfDrawProperties extends HwmfDrawProperties {
enum TransOperand { left, right } enum TransOperand {
left(AffineTransform::concatenate),
right(AffineTransform::preConcatenate);
BiConsumer<AffineTransform,AffineTransform> fun;
TransOperand(BiConsumer<AffineTransform,AffineTransform> fun) {
this.fun = fun;
}
}
/** Path for path bracket operations */ /** Path for path bracket operations */
protected Path2D path = null; protected Path2D path = null;
protected boolean usePathBracket = false; protected boolean usePathBracket = false;
private EmfPlusHatchStyle emfPlusBrushHatch; private EmfPlusHatchStyle emfPlusBrushHatch;
private BufferedImage emfPlusImage; private ImageRenderer emfPlusImage;
private final List<AffineTransform> transXForm = new ArrayList<>(); private final List<AffineTransform> transXForm = new ArrayList<>();
private final List<TransOperand> transOper = new ArrayList<>(); private final List<TransOperand> transOper = new ArrayList<>();
@ -89,22 +98,33 @@ public class HemfDrawProperties extends HwmfDrawProperties {
this.emfPlusBrushHatch = emfPlusBrushHatch; this.emfPlusBrushHatch = emfPlusBrushHatch;
} }
public BufferedImage getEmfPlusImage() { public ImageRenderer getEmfPlusImage() {
return emfPlusImage; return emfPlusImage;
} }
public void setEmfPlusImage(BufferedImage emfPlusImage) { public void setEmfPlusImage(ImageRenderer emfPlusImage) {
this.emfPlusImage = emfPlusImage; this.emfPlusImage = emfPlusImage;
} }
public void addLeftTransform(AffineTransform transform) { public void addLeftTransform(AffineTransform transform) {
transXForm.add(transform); addLRTransform(transform, TransOperand.left);
transOper.add(TransOperand.left);
} }
public void addRightTransform(AffineTransform transform) { public void addRightTransform(AffineTransform transform) {
addLRTransform(transform, TransOperand.right);
}
private static <T> T last(List<T> list) {
return list.isEmpty() ? null : list.get(list.size()-1);
}
private void addLRTransform(AffineTransform transform, TransOperand lr) {
if (transform.isIdentity() || (transform.equals(last(transXForm)) && lr.equals(last(transOper)))) {
// some EMFs add duplicated transformations - ignore them
return;
}
transXForm.add(transform); transXForm.add(transform);
transOper.add(TransOperand.right); transOper.add(lr);
} }
public void clearTransform() { public void clearTransform() {

View File

@ -27,6 +27,7 @@ import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -85,12 +86,12 @@ public class HemfGraphics extends HwmfGraphics {
} }
public void draw(HemfRecord r) { public void draw(HemfRecord r) {
switch (renderState) { switch (getRenderState()) {
case EMF_DCONTEXT: case EMF_DCONTEXT:
// keep the dcontext state, if the next record is an EMF+ record // This state specifies that subsequent EMF records encountered in the metafile SHOULD be processed.
// only reset it, when we are processing EMF records again // EMF records cease being processed when the next EMF+ record is encountered.
if (!(r instanceof EmfComment)) { if (r instanceof EmfComment) {
renderState = EmfRenderState.INITIAL; setRenderState(EmfRenderState.EMFPLUS_ONLY);
} }
r.draw(this); r.draw(this);
break; break;
@ -98,8 +99,12 @@ public class HemfGraphics extends HwmfGraphics {
r.draw(this); r.draw(this);
break; break;
case EMF_ONLY: case EMF_ONLY:
if (!(r instanceof EmfComment)) {
r.draw(this);
}
break;
case EMFPLUS_ONLY: case EMFPLUS_ONLY:
if ((r instanceof EmfComment) == (renderState == EmfRenderState.EMFPLUS_ONLY)) { if (r instanceof EmfComment) {
r.draw(this); r.draw(this);
} }
break; break;
@ -109,16 +114,7 @@ public class HemfGraphics extends HwmfGraphics {
} }
public void draw(HemfPlusRecord r) { public void draw(HemfPlusRecord r) {
switch (renderState) { r.draw(this);
case EMFPLUS_ONLY:
case EMF_DCONTEXT:
case INITIAL:
r.draw(this);
break;
case EMF_ONLY:
default:
break;
}
} }
@Internal @Internal
@ -202,7 +198,7 @@ public class HemfGraphics extends HwmfGraphics {
} }
private void checkTableEntryIndex(int index) { private void checkTableEntryIndex(int index) {
if (renderState != EmfRenderState.EMFPLUS_ONLY) { if (renderState != EmfRenderState.EMFPLUS_ONLY && renderState != EmfRenderState.EMF_DCONTEXT) {
// in EMF the index must > 0 // in EMF the index must > 0
if (index < 1) { if (index < 1) {
throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index); throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index);
@ -350,15 +346,8 @@ public class HemfGraphics extends HwmfGraphics {
assert(transXform.size() == transOper.size()); assert(transXform.size() == transOper.size());
AffineTransform tx = graphicsCtx.getTransform(); AffineTransform tx = graphicsCtx.getTransform();
for (int i=0; i<transXform.size(); i++) { Iterator<AffineTransform> iter = transXform.iterator();
AffineTransform tx2 = transXform.get(i); transOper.forEach(to -> to.fun.accept(tx, iter.next()));
if (transOper.get(i) == TransOperand.left) {
tx.concatenate(tx2);
} else {
tx.preConcatenate(tx2);
}
}
graphicsCtx.setTransform(tx); graphicsCtx.setTransform(tx);
} }

View File

@ -17,10 +17,13 @@
package org.apache.poi.hemf.draw; package org.apache.poi.hemf.draw;
import static org.apache.poi.hwmf.draw.HwmfImageRenderer.getOuterBounds;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Insets; import java.awt.Insets;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Dimension2D; import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@ -105,10 +108,24 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor {
public boolean drawImage(Graphics2D graphics, Rectangle2D anchor, Insets clip) { public boolean drawImage(Graphics2D graphics, Rectangle2D anchor, Insets clip) {
if (image == null) { if (image == null) {
return false; return false;
} else {
image.draw(graphics, anchor);
return true;
} }
boolean isClipped = true;
if (clip == null) {
isClipped = false;
clip = new Insets(0,0,0,0);
}
Shape clipOld = graphics.getClip();
if (isClipped) {
graphics.clip(anchor);
}
image.draw(graphics, getOuterBounds(anchor, clip));
graphics.setClip(clipOld);
return true;
} }
@Override @Override
@ -120,4 +137,9 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor {
public Iterable<EmbeddedPart> getEmbeddings() { public Iterable<EmbeddedPart> getEmbeddings() {
return HwmfImageRenderer.getEmbeddings(image.getEmbeddings()); return HwmfImageRenderer.getEmbeddings(image.getEmbeddings());
} }
@Override
public Rectangle2D getNativeBounds() {
return image.getBounds();
}
} }

View File

@ -300,10 +300,6 @@ public class HemfComment {
@Override @Override
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
if (ctx.getRenderState() == HemfGraphics.EmfRenderState.INITIAL) {
ctx.setRenderState(HemfGraphics.EmfRenderState.EMFPLUS_ONLY);
}
records.forEach(ctx::draw); records.forEach(ctx::draw);
} }

View File

@ -24,7 +24,6 @@ import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -913,8 +912,7 @@ public class HemfMisc {
} }
HwmfDrawProperties props = ctx.getProperties(); HwmfDrawProperties props = ctx.getProperties();
props.setBrushStyle(HwmfBrushStyle.BS_PATTERN); props.setBrushStyle(HwmfBrushStyle.BS_PATTERN);
BufferedImage bmp = bitmap.getImage(); props.setBrushBitmap(bitmap.getImage());
props.setBrushBitmap(bmp);
} }
@Override @Override

View File

@ -22,10 +22,10 @@ import static org.apache.poi.util.GenericRecordUtil.getBitsAsString;
import java.awt.Color; import java.awt.Color;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Area; import java.awt.geom.Area;
import java.awt.geom.Dimension2D;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
@ -43,13 +43,12 @@ import org.apache.commons.math3.linear.RealMatrix;
import org.apache.poi.hemf.draw.HemfDrawProperties; import org.apache.poi.hemf.draw.HemfDrawProperties;
import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.record.emf.HemfFill; import org.apache.poi.hemf.record.emf.HemfFill;
import org.apache.poi.hemf.record.emfplus.HemfPlusImage.EmfPlusImage;
import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId; import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId;
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObject;
import org.apache.poi.hwmf.record.HwmfBrushStyle; import org.apache.poi.hwmf.record.HwmfBrushStyle;
import org.apache.poi.hwmf.record.HwmfColorRef; import org.apache.poi.hwmf.record.HwmfColorRef;
import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode; import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode;
import org.apache.poi.hwmf.record.HwmfTernaryRasterOp; import org.apache.poi.hwmf.record.HwmfTernaryRasterOp;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.util.BitField; import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.GenericRecordJsonWriter; import org.apache.poi.util.GenericRecordJsonWriter;
@ -314,6 +313,7 @@ public class HemfPlusDraw {
return GenericRecordUtil.getGenericProperties( return GenericRecordUtil.getGenericProperties(
"flags", this::getFlags, "flags", this::getFlags,
"brushId", this::getBrushId, "brushId", this::getBrushId,
"brushColor", this::getSolidColor,
"rectData", this::getRectData "rectData", this::getRectData
); );
} }
@ -429,31 +429,35 @@ public class HemfPlusDraw {
ctx.applyObjectTableEntry(imageAttributesID); ctx.applyObjectTableEntry(imageAttributesID);
ctx.applyObjectTableEntry(getObjectId()); ctx.applyObjectTableEntry(getObjectId());
AffineTransform txSaved = ctx.getTransform(), tx = new AffineTransform(txSaved); final ImageRenderer ir = prop.getEmfPlusImage();
if (ir == null) {
return;
}
AffineTransform txSaved = ctx.getTransform();
AffineTransform tx = new AffineTransform(txSaved);
try { try {
tx.concatenate(trans); tx.concatenate(trans);
ctx.setTransform(tx); ctx.setTransform(tx);
EmfPlusObject imgObj = (EmfPlusObject)ctx.getObjectTableEntry(getObjectId()); final Rectangle2D srcBounds = ir.getNativeBounds();
EmfPlusImage img = imgObj.getObjectData(); final Dimension2D dim = ir.getDimension();
Rectangle2D srcBounds = img.getBounds(imgObj.getContinuedObject());
BufferedImage bi = prop.getEmfPlusImage();
prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY); prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY);
prop.setBkMode(HwmfBkMode.TRANSPARENT); prop.setBkMode(HwmfBkMode.TRANSPARENT);
// the buffered image might be rescaled, so we need to calculate a new src rect to take // the buffered image might be rescaled, so we need to calculate a new src rect to take
// the image data from // the image data from
AffineTransform srcTx = new AffineTransform(); final AffineTransform srcTx = new AffineTransform();
srcTx.translate(-srcBounds.getX(), srcBounds.getY()); srcTx.translate(-srcBounds.getX(), srcBounds.getY());
srcTx.scale(bi.getWidth()/srcBounds.getWidth(), bi.getHeight()/srcBounds.getHeight()); srcTx.scale(dim.getWidth()/srcBounds.getWidth(), dim.getHeight()/srcBounds.getHeight());
srcTx.translate(bi.getMinX(), bi.getMinY());
Rectangle2D biRect = srcTx.createTransformedShape(srcRect).getBounds2D(); final Rectangle2D biRect = srcTx.createTransformedShape(srcRect).getBounds2D();
// TODO: handle srcUnit // TODO: handle srcUnit
Rectangle2D destRect = new Rectangle2D.Double(0, 0, biRect.getWidth(), biRect.getHeight()); Rectangle2D destRect = new Rectangle2D.Double(0, 0, biRect.getWidth(), biRect.getHeight());
ctx.drawImage(bi, srcRect, destRect); ctx.drawImage(ir, srcRect, destRect);
} finally { } finally {
ctx.setTransform(txSaved); ctx.setTransform(txSaved);
} }

View File

@ -0,0 +1,103 @@
package org.apache.poi.hemf.record.emfplus;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.InputStream;
import org.apache.poi.sl.draw.BitmapImageRenderer;
import org.apache.poi.util.IOUtils;
public class HemfPlusGDIImageRenderer extends BitmapImageRenderer {
private int width;
private int height;
private int stride;
private HemfPlusImage.EmfPlusPixelFormat pixelFormat;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getStride() {
return stride;
}
public void setStride(int stride) {
this.stride = stride;
}
public HemfPlusImage.EmfPlusPixelFormat getPixelFormat() {
return pixelFormat;
}
public void setPixelFormat(HemfPlusImage.EmfPlusPixelFormat pixelFormat) {
this.pixelFormat = pixelFormat;
}
@Override
public boolean canRender(String contentType) {
return true;
}
@Override
public void loadImage(InputStream data, String contentType) throws IOException {
img = readGDIImage(IOUtils.toByteArray(data));
}
@Override
public void loadImage(byte[] data, String contentType) throws IOException {
img = readGDIImage(data);
}
/**
* Converts the gdi pixel data to a buffered image
* @param data the image data of all EmfPlusImage parts
* @return the BufferedImage
*/
public BufferedImage readGDIImage(final byte[] data) {
int[] nBits, bOffs;
switch (pixelFormat) {
case ARGB_32BPP:
nBits = new int[]{8, 8, 8, 8};
bOffs = new int[]{2, 1, 0, 3};
break;
case RGB_24BPP:
nBits = new int[]{8, 8, 8};
bOffs = new int[]{2, 1, 0};
break;
default:
throw new RuntimeException("not yet implemented");
}
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ComponentColorModel cm = new ComponentColorModel
(cs, nBits, pixelFormat.isAlpha(), pixelFormat.isPreMultiplied(), Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
PixelInterleavedSampleModel csm =
new PixelInterleavedSampleModel(cm.getTransferType(), width, height, cm.getNumComponents(), stride, bOffs);
DataBufferByte dbb = new DataBufferByte(data, data.length);
WritableRaster raster = (WritableRaster) Raster.createRaster(csm, dbb, null);
return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
}
}

View File

@ -24,6 +24,7 @@ import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState; import org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState;
import org.apache.poi.util.BitField; import org.apache.poi.util.BitField;
@ -105,7 +106,7 @@ public class HemfPlusHeader implements HemfPlusRecord {
* @return {@code true} if dual-mode is enabled * @return {@code true} if dual-mode is enabled
*/ */
public boolean isEmfPlusDualMode() { public boolean isEmfPlusDualMode() {
return (emfPlusFlags & 1) == 1; return (flags & 1) == 1;
} }
public long getEmfPlusFlags() { public long getEmfPlusFlags() {
@ -124,7 +125,7 @@ public class HemfPlusHeader implements HemfPlusRecord {
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
// currently EMF is better supported than EMF+ ... so if there's a complete set of EMF records available, // currently EMF is better supported than EMF+ ... so if there's a complete set of EMF records available,
// disable EMF+ rendering for now // disable EMF+ rendering for now
ctx.setRenderState(isEmfPlusDualMode() ? EmfRenderState.EMF_ONLY : EmfRenderState.EMFPLUS_ONLY); ctx.setRenderState(isEmfPlusDualMode() ? EmfRenderState.EMF_ONLY : EmfRenderState.EMF_DCONTEXT);
} }
@Override @Override
@ -143,7 +144,7 @@ public class HemfPlusHeader implements HemfPlusRecord {
); );
} }
public static class EmfPlusGraphicsVersion { public static class EmfPlusGraphicsVersion implements GenericRecord {
private static final BitField METAFILE_SIGNATURE = BitFieldFactory.getInstance(0xFFFFF000); private static final BitField METAFILE_SIGNATURE = BitFieldFactory.getInstance(0xFFFFF000);
private static final BitField GRAPHICS_VERSION = BitFieldFactory.getInstance(0x00000FFF); private static final BitField GRAPHICS_VERSION = BitFieldFactory.getInstance(0x00000FFF);
@ -172,8 +173,15 @@ public class HemfPlusHeader implements HemfPlusRecord {
} }
public String toString() { public String toString() {
return "{ metafileSignature=0x"+Integer.toHexString(metafileSignature)+ return GenericRecordJsonWriter.marshal(this);
" , graphicsVersion='"+graphicsVersion+"' }"; }
@Override
public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties(
"metafileSignature", this::getMetafileSignature,
"graphicsVersion", this::getGraphicsVersion
);
} }
} }
} }

View File

@ -20,19 +20,8 @@ package org.apache.poi.hemf.record.emfplus;
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readARGB; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readARGB;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@ -40,7 +29,6 @@ import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@ -52,6 +40,9 @@ import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData;
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType;
import org.apache.poi.hemf.usermodel.HemfPicture; import org.apache.poi.hemf.usermodel.HemfPicture;
import org.apache.poi.hwmf.usermodel.HwmfPicture; import org.apache.poi.hwmf.usermodel.HwmfPicture;
import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.sl.usermodel.PictureData.PictureType;
import org.apache.poi.util.BitField; import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.GenericRecordJsonWriter; import org.apache.poi.util.GenericRecordJsonWriter;
@ -59,7 +50,6 @@ import org.apache.poi.util.GenericRecordUtil;
import org.apache.poi.util.IOUtils; import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream; import org.apache.poi.util.LittleEndianInputStream;
import org.apache.poi.util.Units;
public class HemfPlusImage { public class HemfPlusImage {
/** Maximum image dimension for converting embedded metafiles */ /** Maximum image dimension for converting embedded metafiles */
@ -285,6 +275,7 @@ public class HemfPlusImage {
public static class EmfPlusImage implements EmfPlusObjectData { public static class EmfPlusImage implements EmfPlusObjectData {
private static final int MAX_OBJECT_SIZE = 50_000_000; private static final int MAX_OBJECT_SIZE = 50_000_000;
private static final String GDI_CONTENT = "GDI";
private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion();
private EmfPlusImageDataType imageDataType; private EmfPlusImageDataType imageDataType;
@ -473,8 +464,20 @@ public class HemfPlusImage {
@Override @Override
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
HemfDrawProperties prop = ctx.getProperties(); HemfDrawProperties prop = ctx.getProperties();
BufferedImage bi = readImage(getRawData(continuedObjectData));
prop.setEmfPlusImage(bi); byte[] data = getRawData(continuedObjectData);
String contentType = getContentType(data);
ImageRenderer imgr = (GDI_CONTENT.equals(contentType))
? getGDIRenderer() : ctx.getImageRenderer(contentType);
try {
imgr.loadImage(data, contentType);
} catch (IOException ignored) {
imgr = null;
}
prop.setEmfPlusImage(imgr);
} }
/** /**
@ -483,122 +486,66 @@ public class HemfPlusImage {
* @return the BufferedImage * @return the BufferedImage
*/ */
public BufferedImage readGDIImage(final byte[] data) { public BufferedImage readGDIImage(final byte[] data) {
return getGDIRenderer().readGDIImage(data);
}
private HemfPlusGDIImageRenderer getGDIRenderer() {
if (getImageDataType() != EmfPlusImageDataType.BITMAP || getBitmapType() != EmfPlusBitmapDataType.PIXEL) { if (getImageDataType() != EmfPlusImageDataType.BITMAP || getBitmapType() != EmfPlusBitmapDataType.PIXEL) {
throw new RuntimeException("image data is not a GDI image"); throw new RuntimeException("image data is not a GDI image");
} }
HemfPlusGDIImageRenderer renderer = new HemfPlusGDIImageRenderer();
final int width = getBitmapWidth(); renderer.setWidth(getBitmapWidth());
final int height = getBitmapHeight(); renderer.setHeight(getBitmapHeight());
final int stride = getBitmapStride(); renderer.setStride(getBitmapStride());
final EmfPlusPixelFormat pf = getPixelFormat(); renderer.setPixelFormat(getPixelFormat());
return renderer;
int[] nBits, bOffs;
switch (pf) {
case ARGB_32BPP:
nBits = new int[]{8, 8, 8, 8};
bOffs = new int[]{2, 1, 0, 3};
break;
case RGB_24BPP:
nBits = new int[]{8, 8, 8};
bOffs = new int[]{2, 1, 0};
break;
default:
throw new RuntimeException("not yet implemented");
}
ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
ComponentColorModel cm = new ComponentColorModel
(cs, nBits, pf.isAlpha(), pf.isPreMultiplied(), Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
PixelInterleavedSampleModel csm =
new PixelInterleavedSampleModel(cm.getTransferType(), width, height, cm.getNumComponents(), stride, bOffs);
DataBufferByte dbb = new DataBufferByte(data, data.length);
WritableRaster raster = (WritableRaster) Raster.createRaster(csm, dbb, null);
return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
} }
private BufferedImage readImage(final byte[] data) { private String getContentType(final byte[] data) {
// TODO: instead of returning a BufferedImage, we might return a pair of raw data + image renderer PictureType pictureType = PictureType.UNKNOWN;
// instead, so metafiles aren't pixelated, but directly written to the output graphics context switch (getImageDataType()) {
try { case BITMAP:
switch (getImageDataType()) { if (getBitmapType() == EmfPlusBitmapDataType.PIXEL) {
case BITMAP: { return GDI_CONTENT;
BufferedImage bi = (getBitmapType() == EmfPlusBitmapDataType.PIXEL)
? readGDIImage(data)
: ImageIO.read(new ByteArrayInputStream(data));
// final int w = bi.getWidth();
// final int h = bi.getHeight();
//
// int[] line = new int[w];
//
// WritableRaster wr = bi.getRaster();
// for (int row=0; row<h; row++) {
// wr.get
// for (int x=0; x<w; x++) {
// // TODO: use clamp color here
// if ((line[x] & 0xFFFFFF) == 0) {
// // make it transparent
// line[x] &= 0xFFFFFF;
// }
// }
// wr.setPixels(0, row, w, 1, line);
// }
return bi;
} }
case METAFILE:
assert (getMetafileType() != null);
switch (getMetafileType()) {
case Wmf:
case WmfPlaceable:
HwmfPicture wmf = new HwmfPicture(new ByteArrayInputStream(data));
return readImage(wmf.getSize(), wmf::draw);
case Emf: switch (FileMagic.valueOf(data)) {
case EmfPlusDual: case GIF:
case EmfPlusOnly: pictureType = PictureType.GIF;
HemfPicture emf = new HemfPicture(new ByteArrayInputStream(data)); break;
return readImage(emf.getSize(), emf::draw); case TIFF:
pictureType = PictureType.TIFF;
break;
case PNG:
pictureType = PictureType.PNG;
break;
case JPEG:
pictureType = PictureType.JPEG;
break;
case BMP:
pictureType = PictureType.BMP;
break;
}
break;
default: case METAFILE:
break; assert (getMetafileType() != null);
} switch (getMetafileType()) {
default: case Wmf:
break; case WmfPlaceable:
} pictureType = PictureType.WMF;
} catch (IOException ignored) { break;
case Emf:
case EmfPlusDual:
case EmfPlusOnly:
pictureType = PictureType.EMF;
break;
}
break;
} }
// fallback to empty image return pictureType.contentType;
return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
}
private BufferedImage readImage(final Dimension2D dim, final BiConsumer<Graphics2D,Rectangle2D> draw) {
int width = Units.pointsToPixel(dim.getWidth());
// keep aspect ratio for height
int height = Units.pointsToPixel(dim.getHeight());
double longSide = Math.max(width,height);
if (longSide > MAX_IMAGE_SIZE) {
double scale = MAX_IMAGE_SIZE / longSide;
width *= scale;
height *= scale;
}
BufferedImage bufImg = new BufferedImage(width, height, 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);
draw.accept(g, new Rectangle2D.Double(0, 0, width, height));
g.dispose();
return bufImg;
} }
@Override @Override
@ -702,5 +649,4 @@ public class HemfPlusImage {
return EmfPlusObjectType.IMAGE_ATTRIBUTES; return EmfPlusObjectType.IMAGE_ATTRIBUTES;
} }
} }
} }

View File

@ -157,9 +157,7 @@ public class HemfPlusMisc {
public static class EmfPlusGetDC extends EmfPlusFlagOnly { public static class EmfPlusGetDC extends EmfPlusFlagOnly {
@Override @Override
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
if (ctx.getRenderState() == HemfGraphics.EmfRenderState.EMFPLUS_ONLY) { ctx.setRenderState(HemfGraphics.EmfRenderState.EMF_DCONTEXT);
ctx.setRenderState(HemfGraphics.EmfRenderState.EMF_DCONTEXT);
}
} }
} }
@ -242,8 +240,11 @@ public class HemfPlusMisc {
@Override @Override
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
HemfDrawProperties prop = ctx.getProperties(); HemfDrawProperties prop = ctx.getProperties();
prop.addLeftTransform(getMatrixData());
ctx.updateWindowMapMode(); AffineTransform tx = ctx.getInitTransform();
tx.concatenate(getMatrixData());
ctx.setTransform(tx);
// don't call ctx.updateWindowMapMode();
} }
} }

View File

@ -159,6 +159,10 @@ public class HemfPlusObject {
return (T)objectData; return (T)objectData;
} }
public int getTotalObjectSize() {
return totalObjectSize;
}
@Override @Override
public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
this.flags = flags; this.flags = flags;
@ -190,17 +194,19 @@ public class HemfPlusObject {
@Override @Override
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
HwmfObjectTableEntry entry = ctx.getObjectTableEntry(getObjectId());
if (objectData.isContinuedRecord()) { if (objectData.isContinuedRecord()) {
EmfPlusObject other; EmfPlusObject other;
if (entry instanceof EmfPlusObject && objectData.getClass().isInstance((other = (EmfPlusObject)entry).getObjectData())) { HwmfObjectTableEntry entry = ctx.getObjectTableEntry(getObjectId());
if (entry instanceof EmfPlusObject &&
objectData.getClass().isInstance((other = (EmfPlusObject)entry).getObjectData())
) {
other.linkContinuedObject(objectData); other.linkContinuedObject(objectData);
return;
} else { } else {
throw new RuntimeException("can't find previous record for continued record"); throw new RuntimeException("can't find previous record for continued record");
} }
} else {
ctx.addObjectTableEntry(this, getObjectId());
} }
ctx.addObjectTableEntry(this, getObjectId());
} }
@Override @Override
@ -226,7 +232,7 @@ public class HemfPlusObject {
"objectId", this::getObjectId, "objectId", this::getObjectId,
"objectData", () -> objectData.isContinuedRecord() ? null : getObjectData(), "objectData", () -> objectData.isContinuedRecord() ? null : getObjectData(),
"continuedObject", objectData::isContinuedRecord, "continuedObject", objectData::isContinuedRecord,
"totalObjectSize", () -> totalObjectSize "totalObjectSize", this::getTotalObjectSize
); );
} }

View File

@ -37,8 +37,11 @@ import org.apache.poi.hwmf.record.HwmfFill;
import org.apache.poi.hwmf.usermodel.HwmfEmbedded; import org.apache.poi.hwmf.usermodel.HwmfEmbedded;
import org.apache.poi.hwmf.usermodel.HwmfEmbeddedType; import org.apache.poi.hwmf.usermodel.HwmfEmbeddedType;
import org.apache.poi.poifs.filesystem.FileMagic; import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.util.IOUtils;
public class HemfEmbeddedIterator implements Iterator<HwmfEmbedded> { public class HemfEmbeddedIterator implements Iterator<HwmfEmbedded> {
//arbitrarily selected; may need to increase
private static final int MAX_RECORD_LENGTH = 100_000_000;
private final Deque<Iterator<?>> iterStack = new ArrayDeque<>(); private final Deque<Iterator<?>> iterStack = new ArrayDeque<>();
private Object current; private Object current;
@ -282,10 +285,13 @@ public class HemfEmbeddedIterator implements Iterator<HwmfEmbedded> {
HwmfEmbedded emb = new HwmfEmbedded(); HwmfEmbedded emb = new HwmfEmbedded();
EmfPlusImage img = (EmfPlusImage)epo.getObjectData(); EmfPlusImage img = epo.getObjectData();
assert(img.getImageDataType() != null); assert(img.getImageDataType() != null);
ByteArrayOutputStream bos = new ByteArrayOutputStream(); int totalSize = epo.getTotalObjectSize();
IOUtils.safelyAllocateCheck(totalSize, MAX_RECORD_LENGTH);
ByteArrayOutputStream bos = new ByteArrayOutputStream(epo.getTotalObjectSize());
try { try {
for (;;) { for (;;) {
bos.write(img.getImageData()); bos.write(img.getImageData());
@ -294,9 +300,10 @@ public class HemfEmbeddedIterator implements Iterator<HwmfEmbedded> {
//noinspection ConstantConditions //noinspection ConstantConditions
if (hasNext() && if (hasNext() &&
(current instanceof EmfPlusObject) && (current instanceof EmfPlusObject) &&
((epo = (EmfPlusObject) current).getObjectId() == objectId) ((epo = (EmfPlusObject) current).getObjectId() == objectId) &&
bos.size() < totalSize-16
) { ) {
img = (EmfPlusImage)epo.getObjectData(); img = epo.getObjectData();
} else { } else {
return emb; return emb;
} }

View File

@ -171,7 +171,6 @@ public class HemfPicture implements Iterable<HemfRecord>, GenericRecord {
try { try {
g.draw(r); g.draw(r);
} catch (RuntimeException ignored) { } catch (RuntimeException ignored) {
} }
idx++; idx++;
} }

View File

@ -25,10 +25,9 @@ import java.awt.geom.Path2D;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
import java.util.List; import java.util.List;
import org.apache.poi.hwmf.draw.HwmfGraphics.BufferedImageRenderer;
import org.apache.poi.hwmf.record.HwmfBrushStyle; import org.apache.poi.hwmf.record.HwmfBrushStyle;
import org.apache.poi.hwmf.record.HwmfColorRef; import org.apache.poi.hwmf.record.HwmfColorRef;
import org.apache.poi.hwmf.record.HwmfFill.WmfSetPolyfillMode.HwmfPolyfillMode; import org.apache.poi.hwmf.record.HwmfFill.WmfSetPolyfillMode.HwmfPolyfillMode;
@ -41,6 +40,7 @@ import org.apache.poi.hwmf.record.HwmfPenStyle;
import org.apache.poi.hwmf.record.HwmfTernaryRasterOp; import org.apache.poi.hwmf.record.HwmfTernaryRasterOp;
import org.apache.poi.hwmf.record.HwmfText.HwmfTextAlignment; import org.apache.poi.hwmf.record.HwmfText.HwmfTextAlignment;
import org.apache.poi.hwmf.record.HwmfText.HwmfTextVerticalAlignment; import org.apache.poi.hwmf.record.HwmfText.HwmfTextVerticalAlignment;
import org.apache.poi.sl.draw.ImageRenderer;
public class HwmfDrawProperties { public class HwmfDrawProperties {
private final Rectangle2D window; private final Rectangle2D window;
@ -51,7 +51,7 @@ public class HwmfDrawProperties {
private HwmfBrushStyle brushStyle; private HwmfBrushStyle brushStyle;
private HwmfColorRef brushColor; private HwmfColorRef brushColor;
private HwmfHatchStyle brushHatch; private HwmfHatchStyle brushHatch;
private BufferedImage brushBitmap; private ImageRenderer brushBitmap;
private final AffineTransform brushTransform = new AffineTransform(); private final AffineTransform brushTransform = new AffineTransform();
private double penWidth; private double penWidth;
private HwmfPenStyle penStyle; private HwmfPenStyle penStyle;
@ -107,12 +107,7 @@ public class HwmfDrawProperties {
this.brushStyle = other.brushStyle; this.brushStyle = other.brushStyle;
this.brushColor = other.brushColor.clone(); this.brushColor = other.brushColor.clone();
this.brushHatch = other.brushHatch; this.brushHatch = other.brushHatch;
if (other.brushBitmap != null) { this.brushBitmap = other.brushBitmap;
ColorModel cm = other.brushBitmap.getColorModel();
boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
WritableRaster raster = other.brushBitmap.copyData(null);
this.brushBitmap = new BufferedImage(cm, raster, isAlphaPremultiplied, null);
}
this.brushTransform.setTransform(other.brushTransform); this.brushTransform.setTransform(other.brushTransform);
this.penWidth = other.penWidth; this.penWidth = other.penWidth;
this.penStyle = (other.penStyle == null) ? null : other.penStyle.clone(); this.penStyle = (other.penStyle == null) ? null : other.penStyle.clone();
@ -280,14 +275,17 @@ public class HwmfDrawProperties {
this.polyfillMode = polyfillMode; this.polyfillMode = polyfillMode;
} }
public BufferedImage getBrushBitmap() { public ImageRenderer getBrushBitmap() {
return brushBitmap; return brushBitmap;
} }
public void setBrushBitmap(BufferedImage brushBitmap) { public void setBrushBitmap(ImageRenderer brushBitmap) {
this.brushBitmap = brushBitmap; this.brushBitmap = brushBitmap;
} }
public void setBrushBitmap(BufferedImage brushBitmap) {
this.brushBitmap = new BufferedImageRenderer(brushBitmap);
}
/** /**
* Gets the last stored region * Gets the last stored region

View File

@ -23,6 +23,7 @@ import java.awt.Color;
import java.awt.Composite; import java.awt.Composite;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration; import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Paint; import java.awt.Paint;
import java.awt.Rectangle; import java.awt.Rectangle;
import java.awt.Shape; import java.awt.Shape;
@ -60,9 +61,13 @@ import org.apache.poi.hwmf.record.HwmfPenStyle.HwmfLineDash;
import org.apache.poi.hwmf.record.HwmfRegionMode; import org.apache.poi.hwmf.record.HwmfRegionMode;
import org.apache.poi.hwmf.record.HwmfText; import org.apache.poi.hwmf.record.HwmfText;
import org.apache.poi.hwmf.record.HwmfText.WmfExtTextOutOptions; import org.apache.poi.hwmf.record.HwmfText.WmfExtTextOutOptions;
import org.apache.poi.sl.draw.BitmapImageRenderer;
import org.apache.poi.sl.draw.DrawFactory; import org.apache.poi.sl.draw.DrawFactory;
import org.apache.poi.sl.draw.DrawFontManager; import org.apache.poi.sl.draw.DrawFontManager;
import org.apache.poi.sl.draw.DrawFontManagerDefault; import org.apache.poi.sl.draw.DrawFontManagerDefault;
import org.apache.poi.sl.draw.DrawPictureShape;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.util.Internal;
import org.apache.poi.util.LocaleUtil; import org.apache.poi.util.LocaleUtil;
public class HwmfGraphics { public class HwmfGraphics {
@ -239,10 +244,16 @@ public class HwmfGraphics {
protected Paint getPatternPaint() { protected Paint getPatternPaint() {
HwmfDrawProperties prop = getProperties(); HwmfDrawProperties prop = getProperties();
BufferedImage bi = prop.getBrushBitmap(); ImageRenderer bb = prop.getBrushBitmap();
Rectangle2D rect = new Rectangle2D.Double(0, 0, bi.getWidth(), bi.getHeight()); if (bb == null) {
return null;
}
Dimension2D dim = bb.getDimension();
Rectangle2D rect = new Rectangle2D.Double(0, 0, dim.getWidth(), dim.getHeight());
rect = prop.getBrushTransform().createTransformedShape(rect).getBounds2D(); rect = prop.getBrushTransform().createTransformedShape(rect).getBounds2D();
return (bi == null) ? null : new TexturePaint(bi, rect);
return new TexturePaint(bb.getImage(), rect);
} }
/** /**
@ -428,7 +439,15 @@ public class HwmfGraphics {
} }
} }
String textString = new String(text, charset).trim(); int trimLen;
for (trimLen=0; trimLen<text.length-1; trimLen+=2) {
if ((text[trimLen] == -1 && text[trimLen+1] == -1) ||
((text[trimLen] & 0xE0) == 0 && text[trimLen+1] == 0)) {
break;
}
}
String textString = new String(text, 0, trimLen, charset);
textString = textString.substring(0, Math.min(textString.length(), length)); textString = textString.substring(0, Math.min(textString.length(), length));
if (textString.isEmpty()) { if (textString.isEmpty()) {
@ -581,6 +600,13 @@ public class HwmfGraphics {
} }
public void drawImage(BufferedImage img, Rectangle2D srcBounds, Rectangle2D dstBounds) { public void drawImage(BufferedImage img, Rectangle2D srcBounds, Rectangle2D dstBounds) {
drawImage(new BufferedImageRenderer(img), srcBounds, dstBounds);
}
public void drawImage(ImageRenderer img, Rectangle2D srcBounds, Rectangle2D dstBounds) {
if (srcBounds.isEmpty()) {
return;
}
HwmfDrawProperties prop = getProperties(); HwmfDrawProperties prop = getProperties();
// handle raster op // handle raster op
@ -606,43 +632,64 @@ public class HwmfGraphics {
break; break;
default: default:
case SRCCOPY: case SRCCOPY:
final Shape clip = graphicsCtx.getClip(); if (img == null) {
return;
}
final Shape oldClip = graphicsCtx.getClip();
final AffineTransform oldTrans = graphicsCtx.getTransform();
// add clipping in case of a source subimage, i.e. a clipped source image // 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 // some dstBounds are horizontal or vertical flipped, so we need to normalize the images
Rectangle2D normalized = new Rectangle2D.Double( Rectangle2D normBounds = normalizeRect(dstBounds);
dstBounds.getWidth() >= 0 ? dstBounds.getMinX() : dstBounds.getMaxX(), // graphicsCtx.clip(normBounds);
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());
if (prop.getBkMode() == HwmfBkMode.OPAQUE) { if (prop.getBkMode() == HwmfBkMode.OPAQUE) {
graphicsCtx.drawImage(img, 0, 0, prop.getBackgroundColor().getColor(), null); Paint oldPaint = graphicsCtx.getPaint();
} else { graphicsCtx.setPaint(prop.getBackgroundColor().getColor());
Composite old = graphicsCtx.getComposite(); graphicsCtx.fill(dstBounds);
graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); graphicsCtx.setPaint(oldPaint);
graphicsCtx.drawImage(img, 0, 0, null);
graphicsCtx.setComposite(old);
} }
graphicsCtx.setTransform(at); graphicsCtx.translate(normBounds.getCenterX(), normBounds.getCenterY());
graphicsCtx.setClip(clip); graphicsCtx.scale(Math.signum(dstBounds.getWidth()), Math.signum(dstBounds.getHeight()));
graphicsCtx.translate(-normBounds.getCenterX(), -normBounds.getCenterY());
// this is similar to drawing bitmaps with a clipping
// see {@link BitmapImageRenderer#drawImage(Graphics2D,Rectangle2D,Insets)}
// the difference is, that clippings are 0-based, whereas the srcBounds are absolute in the user-space
// of the referenced image and can be also negative
Composite old = graphicsCtx.getComposite();
graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
img.drawImage(graphicsCtx, normBounds, getSubImageInsets(srcBounds, img.getNativeBounds()));
graphicsCtx.setComposite(old);
graphicsCtx.setTransform(oldTrans);
graphicsCtx.setClip(oldClip);
break; break;
} }
} }
private static Rectangle2D normalizeRect(Rectangle2D dstBounds) {
return 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()));
}
private static Insets getSubImageInsets(Rectangle2D srcBounds, Rectangle2D nativeBounds) {
// Todo: check if we need to normalize srcBounds x/y, in case of flipped images
// for now we assume the width/height is positive
int left = (int)Math.round((srcBounds.getX()-nativeBounds.getX())/nativeBounds.getWidth()*100_000.);
int top = (int)Math.round((srcBounds.getY()-nativeBounds.getY())/nativeBounds.getWidth()*100_000.);
int right = (int)Math.round((nativeBounds.getMaxX()-srcBounds.getMaxX())/nativeBounds.getWidth()*100_000.);
int bottom = (int)Math.round((nativeBounds.getMaxY()-srcBounds.getMaxY())/nativeBounds.getWidth()*100_000.);
return new Insets(top, left, bottom, right);
}
/** /**
* @return the initial AffineTransform, when this graphics context was created * @return the initial AffineTransform, when this graphics context was created
*/ */
@ -689,4 +736,17 @@ public class HwmfGraphics {
} }
prop.setClip(graphicsCtx.getClip()); prop.setClip(graphicsCtx.getClip());
} }
public ImageRenderer getImageRenderer(String contentType) {
// TODO: refactor DrawPictureShape method to POI Common
return DrawPictureShape.getImageRenderer(graphicsCtx, contentType);
}
@Internal
static class BufferedImageRenderer extends BitmapImageRenderer {
public BufferedImageRenderer(BufferedImage img) {
this.img = img;
}
}
} }

View File

@ -21,6 +21,7 @@ import java.awt.Dimension;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Insets; import java.awt.Insets;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Dimension2D; import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@ -111,10 +112,33 @@ public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor {
public boolean drawImage(Graphics2D graphics, Rectangle2D anchor, Insets clip) { public boolean drawImage(Graphics2D graphics, Rectangle2D anchor, Insets clip) {
if (image == null) { if (image == null) {
return false; return false;
} else {
image.draw(graphics, anchor);
return true;
} }
boolean isClipped = true;
if (clip == null) {
isClipped = false;
clip = new Insets(0,0,0,0);
}
Shape clipOld = graphics.getClip();
if (isClipped) {
graphics.clip(anchor);
}
image.draw(graphics, getOuterBounds(anchor, clip));
graphics.setClip(clipOld);
return true;
}
@Internal
public static Rectangle2D getOuterBounds(Rectangle2D anchor, Insets clip) {
double outerWidth = anchor.getWidth() / ((100_000.-clip.left-clip.right)/100_000.);
double outerHeight = anchor.getHeight() / ((100_000.-clip.top-clip.bottom)/100_000.);
double outerX = anchor.getX() - (clip.left / 100_000.) * outerWidth;
double outerY = anchor.getY() - (clip.top / 100_000.) * outerHeight;
return new Rectangle2D.Double(outerX, outerY, outerWidth, outerHeight);
} }
@Override @Override
@ -150,4 +174,9 @@ public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor {
}; };
}; };
} }
@Override
public Rectangle2D getNativeBounds() {
return image.getBounds();
}
} }

View File

@ -32,6 +32,7 @@ import java.util.function.Supplier;
import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfDrawProperties;
import org.apache.poi.hwmf.draw.HwmfGraphics; import org.apache.poi.hwmf.draw.HwmfGraphics;
import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode; import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.util.GenericRecordJsonWriter; import org.apache.poi.util.GenericRecordJsonWriter;
import org.apache.poi.util.GenericRecordUtil; import org.apache.poi.util.GenericRecordUtil;
import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianConsts;
@ -616,8 +617,7 @@ public class HwmfFill {
prop.getBkMode() == HwmfBkMode.TRANSPARENT); prop.getBkMode() == HwmfBkMode.TRANSPARENT);
ctx.drawImage(bi, srcBounds, dstBounds); ctx.drawImage(bi, srcBounds, dstBounds);
} else if (!dstBounds.isEmpty()) { } else if (!dstBounds.isEmpty()) {
BufferedImage bi = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); ctx.drawImage((ImageRenderer)null, new Rectangle2D.Double(0,0,1,1), dstBounds);
ctx.drawImage(bi, new Rectangle2D.Double(0,0,100,100), dstBounds);
} }
} }
@ -915,13 +915,13 @@ public class HwmfFill {
prop.setRasterOp(rasterOperation); prop.setRasterOp(rasterOperation);
// TODO: implement second operation based on playback device context // TODO: implement second operation based on playback device context
if (target != null) { if (target != null) {
HwmfBkMode mode = prop.getBkMode(); HwmfBkMode oldMode = prop.getBkMode();
prop.setBkMode(HwmfBkMode.TRANSPARENT); prop.setBkMode(HwmfBkMode.TRANSPARENT);
Color fgColor = prop.getPenColor().getColor(); Color fgColor = prop.getPenColor().getColor();
Color bgColor = prop.getBackgroundColor().getColor(); Color bgColor = prop.getBackgroundColor().getColor();
BufferedImage bi = target.getImage(fgColor, bgColor, true); BufferedImage bi = target.getImage(fgColor, bgColor, true);
ctx.drawImage(bi, srcBounds, dstBounds); ctx.drawImage(bi, srcBounds, dstBounds);
prop.setBkMode(mode); prop.setBkMode(oldMode);
} }
} }

View File

@ -540,8 +540,13 @@ public class HwmfMisc {
} }
HwmfDrawProperties prop = ctx.getProperties(); HwmfDrawProperties prop = ctx.getProperties();
prop.setBrushStyle(style); prop.setBrushStyle(style);
prop.setBrushBitmap(getImage(prop.getBrushColor().getColor(), prop.getBackgroundColor().getColor(),
prop.getBkMode() == HwmfBkMode.TRANSPARENT)); BufferedImage bufImg = getImage(
prop.getBrushColor().getColor(),
prop.getBackgroundColor().getColor(),
prop.getBkMode() == HwmfBkMode.TRANSPARENT);
prop.setBrushBitmap(bufImg);
} }
@Override @Override