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'
}),
/** BMP image */
BMP(new byte[]{'B','M'}),
// keep UNKNOWN always as last enum!
/** UNKNOWN magic */
UNKNOWN(new byte[0]);

View File

@ -322,4 +322,9 @@ public class BitmapImageRenderer implements ImageRenderer {
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;
/**
* @return the format-specific / not-normalized bounds of the image
*/
Rectangle2D getNativeBounds();
/**
* @return the dimension of the buffered image in pixel
*/

View File

@ -17,7 +17,15 @@
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.channels.ReadableByteChannel;
import java.util.zip.CRC32;
@ -561,6 +569,11 @@ public final class IOUtils {
}
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) {
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);
}
checkLength(length, maxLength);
return new byte[(int)length];
}
/**

View File

@ -151,4 +151,9 @@ public class SVGImageRenderer implements ImageRenderer {
public boolean canRender(String 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 double lenSide = getDimensions(proxy, dim);
final int width = (int)Math.rint(dim.getWidth());
final int height = (int)Math.rint(dim.getHeight());
final int width = Math.max((int)Math.rint(dim.getWidth()),1);
final int height = Math.max((int)Math.rint(dim.getHeight()),1);
for (int slideNo : slidenum) {
proxy.setSlideNo(slideNo);
@ -309,7 +309,11 @@ public final class PPTX2PNG {
return;
}
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) {
fw.writeError(file.getName()+" doesn't support GenericRecord interface and can't be dumped to a file.");
} else {

View File

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

View File

@ -19,21 +19,30 @@ package org.apache.poi.hemf.draw;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle;
import org.apache.poi.hwmf.draw.HwmfDrawProperties;
import org.apache.poi.sl.draw.ImageRenderer;
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 */
protected Path2D path = null;
protected boolean usePathBracket = false;
private EmfPlusHatchStyle emfPlusBrushHatch;
private BufferedImage emfPlusImage;
private ImageRenderer emfPlusImage;
private final List<AffineTransform> transXForm = new ArrayList<>();
private final List<TransOperand> transOper = new ArrayList<>();
@ -89,22 +98,33 @@ public class HemfDrawProperties extends HwmfDrawProperties {
this.emfPlusBrushHatch = emfPlusBrushHatch;
}
public BufferedImage getEmfPlusImage() {
public ImageRenderer getEmfPlusImage() {
return emfPlusImage;
}
public void setEmfPlusImage(BufferedImage emfPlusImage) {
public void setEmfPlusImage(ImageRenderer emfPlusImage) {
this.emfPlusImage = emfPlusImage;
}
public void addLeftTransform(AffineTransform transform) {
transXForm.add(transform);
transOper.add(TransOperand.left);
addLRTransform(transform, TransOperand.left);
}
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);
transOper.add(TransOperand.right);
transOper.add(lr);
}
public void clearTransform() {

View File

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

View File

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

View File

@ -300,10 +300,6 @@ public class HemfComment {
@Override
public void draw(HemfGraphics ctx) {
if (ctx.getRenderState() == HemfGraphics.EmfRenderState.INITIAL) {
ctx.setRenderState(HemfGraphics.EmfRenderState.EMFPLUS_ONLY);
}
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.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -913,8 +912,7 @@ public class HemfMisc {
}
HwmfDrawProperties props = ctx.getProperties();
props.setBrushStyle(HwmfBrushStyle.BS_PATTERN);
BufferedImage bmp = bitmap.getImage();
props.setBrushBitmap(bmp);
props.setBrushBitmap(bitmap.getImage());
}
@Override

View File

@ -22,10 +22,10 @@ import static org.apache.poi.util.GenericRecordUtil.getBitsAsString;
import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Dimension2D;
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 java.math.BigDecimal;
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.HemfGraphics;
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.HemfPlusObject.EmfPlusObject;
import org.apache.poi.hwmf.record.HwmfBrushStyle;
import org.apache.poi.hwmf.record.HwmfColorRef;
import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode;
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.BitFieldFactory;
import org.apache.poi.util.GenericRecordJsonWriter;
@ -314,6 +313,7 @@ public class HemfPlusDraw {
return GenericRecordUtil.getGenericProperties(
"flags", this::getFlags,
"brushId", this::getBrushId,
"brushColor", this::getSolidColor,
"rectData", this::getRectData
);
}
@ -429,31 +429,35 @@ public class HemfPlusDraw {
ctx.applyObjectTableEntry(imageAttributesID);
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 {
tx.concatenate(trans);
ctx.setTransform(tx);
EmfPlusObject imgObj = (EmfPlusObject)ctx.getObjectTableEntry(getObjectId());
EmfPlusImage img = imgObj.getObjectData();
Rectangle2D srcBounds = img.getBounds(imgObj.getContinuedObject());
BufferedImage bi = prop.getEmfPlusImage();
final Rectangle2D srcBounds = ir.getNativeBounds();
final Dimension2D dim = ir.getDimension();
prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY);
prop.setBkMode(HwmfBkMode.TRANSPARENT);
// the buffered image might be rescaled, so we need to calculate a new src rect to take
// the image data from
AffineTransform srcTx = new AffineTransform();
final AffineTransform srcTx = new AffineTransform();
srcTx.translate(-srcBounds.getX(), srcBounds.getY());
srcTx.scale(bi.getWidth()/srcBounds.getWidth(), bi.getHeight()/srcBounds.getHeight());
srcTx.translate(bi.getMinX(), bi.getMinY());
srcTx.scale(dim.getWidth()/srcBounds.getWidth(), dim.getHeight()/srcBounds.getHeight());
Rectangle2D biRect = srcTx.createTransformedShape(srcRect).getBounds2D();
final Rectangle2D biRect = srcTx.createTransformedShape(srcRect).getBounds2D();
// TODO: handle srcUnit
Rectangle2D destRect = new Rectangle2D.Double(0, 0, biRect.getWidth(), biRect.getHeight());
ctx.drawImage(bi, srcRect, destRect);
ctx.drawImage(ir, srcRect, destRect);
} finally {
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.function.Supplier;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState;
import org.apache.poi.util.BitField;
@ -105,7 +106,7 @@ public class HemfPlusHeader implements HemfPlusRecord {
* @return {@code true} if dual-mode is enabled
*/
public boolean isEmfPlusDualMode() {
return (emfPlusFlags & 1) == 1;
return (flags & 1) == 1;
}
public long getEmfPlusFlags() {
@ -124,7 +125,7 @@ public class HemfPlusHeader implements HemfPlusRecord {
public void draw(HemfGraphics ctx) {
// currently EMF is better supported than EMF+ ... so if there's a complete set of EMF records available,
// disable EMF+ rendering for now
ctx.setRenderState(isEmfPlusDualMode() ? EmfRenderState.EMF_ONLY : EmfRenderState.EMFPLUS_ONLY);
ctx.setRenderState(isEmfPlusDualMode() ? EmfRenderState.EMF_ONLY : EmfRenderState.EMF_DCONTEXT);
}
@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 GRAPHICS_VERSION = BitFieldFactory.getInstance(0x00000FFF);
@ -172,8 +173,15 @@ public class HemfPlusHeader implements HemfPlusRecord {
}
public String toString() {
return "{ metafileSignature=0x"+Integer.toHexString(metafileSignature)+
" , graphicsVersion='"+graphicsVersion+"' }";
return GenericRecordJsonWriter.marshal(this);
}
@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 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.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.ByteArrayOutputStream;
import java.io.IOException;
@ -40,7 +29,6 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
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.usermodel.HemfPicture;
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.BitFieldFactory;
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.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
import org.apache.poi.util.Units;
public class HemfPlusImage {
/** Maximum image dimension for converting embedded metafiles */
@ -285,6 +275,7 @@ public class HemfPlusImage {
public static class EmfPlusImage implements EmfPlusObjectData {
private static final int MAX_OBJECT_SIZE = 50_000_000;
private static final String GDI_CONTENT = "GDI";
private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion();
private EmfPlusImageDataType imageDataType;
@ -473,8 +464,20 @@ public class HemfPlusImage {
@Override
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
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
*/
public BufferedImage readGDIImage(final byte[] data) {
return getGDIRenderer().readGDIImage(data);
}
private HemfPlusGDIImageRenderer getGDIRenderer() {
if (getImageDataType() != EmfPlusImageDataType.BITMAP || getBitmapType() != EmfPlusBitmapDataType.PIXEL) {
throw new RuntimeException("image data is not a GDI image");
}
final int width = getBitmapWidth();
final int height = getBitmapHeight();
final int stride = getBitmapStride();
final EmfPlusPixelFormat pf = getPixelFormat();
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);
HemfPlusGDIImageRenderer renderer = new HemfPlusGDIImageRenderer();
renderer.setWidth(getBitmapWidth());
renderer.setHeight(getBitmapHeight());
renderer.setStride(getBitmapStride());
renderer.setPixelFormat(getPixelFormat());
return renderer;
}
private BufferedImage readImage(final byte[] data) {
// TODO: instead of returning a BufferedImage, we might return a pair of raw data + image renderer
// instead, so metafiles aren't pixelated, but directly written to the output graphics context
try {
switch (getImageDataType()) {
case BITMAP: {
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;
private String getContentType(final byte[] data) {
PictureType pictureType = PictureType.UNKNOWN;
switch (getImageDataType()) {
case BITMAP:
if (getBitmapType() == EmfPlusBitmapDataType.PIXEL) {
return GDI_CONTENT;
}
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:
case EmfPlusDual:
case EmfPlusOnly:
HemfPicture emf = new HemfPicture(new ByteArrayInputStream(data));
return readImage(emf.getSize(), emf::draw);
switch (FileMagic.valueOf(data)) {
case GIF:
pictureType = PictureType.GIF;
break;
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:
break;
}
default:
break;
}
} catch (IOException ignored) {
case METAFILE:
assert (getMetafileType() != null);
switch (getMetafileType()) {
case Wmf:
case WmfPlaceable:
pictureType = PictureType.WMF;
break;
case Emf:
case EmfPlusDual:
case EmfPlusOnly:
pictureType = PictureType.EMF;
break;
}
break;
}
// fallback to empty image
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;
return pictureType.contentType;
}
@Override
@ -702,5 +649,4 @@ public class HemfPlusImage {
return EmfPlusObjectType.IMAGE_ATTRIBUTES;
}
}
}

View File

@ -157,9 +157,7 @@ public class HemfPlusMisc {
public static class EmfPlusGetDC extends EmfPlusFlagOnly {
@Override
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
public void draw(HemfGraphics ctx) {
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;
}
public int getTotalObjectSize() {
return totalObjectSize;
}
@Override
public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
this.flags = flags;
@ -190,17 +194,19 @@ public class HemfPlusObject {
@Override
public void draw(HemfGraphics ctx) {
HwmfObjectTableEntry entry = ctx.getObjectTableEntry(getObjectId());
if (objectData.isContinuedRecord()) {
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);
return;
} else {
throw new RuntimeException("can't find previous record for continued record");
}
} else {
ctx.addObjectTableEntry(this, getObjectId());
}
ctx.addObjectTableEntry(this, getObjectId());
}
@Override
@ -226,7 +232,7 @@ public class HemfPlusObject {
"objectId", this::getObjectId,
"objectData", () -> objectData.isContinuedRecord() ? null : getObjectData(),
"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.HwmfEmbeddedType;
import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.util.IOUtils;
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 Object current;
@ -282,10 +285,13 @@ public class HemfEmbeddedIterator implements Iterator<HwmfEmbedded> {
HwmfEmbedded emb = new HwmfEmbedded();
EmfPlusImage img = (EmfPlusImage)epo.getObjectData();
EmfPlusImage img = epo.getObjectData();
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 {
for (;;) {
bos.write(img.getImageData());
@ -294,9 +300,10 @@ public class HemfEmbeddedIterator implements Iterator<HwmfEmbedded> {
//noinspection ConstantConditions
if (hasNext() &&
(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 {
return emb;
}

View File

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

View File

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

View File

@ -23,6 +23,7 @@ import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.Rectangle;
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.HwmfText;
import org.apache.poi.hwmf.record.HwmfText.WmfExtTextOutOptions;
import org.apache.poi.sl.draw.BitmapImageRenderer;
import org.apache.poi.sl.draw.DrawFactory;
import org.apache.poi.sl.draw.DrawFontManager;
import org.apache.poi.sl.draw.DrawFontManagerDefault;
import org.apache.poi.sl.draw.DrawPictureShape;
import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.util.Internal;
import org.apache.poi.util.LocaleUtil;
public class HwmfGraphics {
@ -239,10 +244,16 @@ public class HwmfGraphics {
protected Paint getPatternPaint() {
HwmfDrawProperties prop = getProperties();
BufferedImage bi = prop.getBrushBitmap();
Rectangle2D rect = new Rectangle2D.Double(0, 0, bi.getWidth(), bi.getHeight());
ImageRenderer bb = prop.getBrushBitmap();
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();
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));
if (textString.isEmpty()) {
@ -581,6 +600,13 @@ public class HwmfGraphics {
}
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();
// handle raster op
@ -606,43 +632,64 @@ public class HwmfGraphics {
break;
default:
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
// 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());
Rectangle2D normBounds = normalizeRect(dstBounds);
// graphicsCtx.clip(normBounds);
if (prop.getBkMode() == HwmfBkMode.OPAQUE) {
graphicsCtx.drawImage(img, 0, 0, prop.getBackgroundColor().getColor(), null);
} else {
Composite old = graphicsCtx.getComposite();
graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
graphicsCtx.drawImage(img, 0, 0, null);
graphicsCtx.setComposite(old);
Paint oldPaint = graphicsCtx.getPaint();
graphicsCtx.setPaint(prop.getBackgroundColor().getColor());
graphicsCtx.fill(dstBounds);
graphicsCtx.setPaint(oldPaint);
}
graphicsCtx.setTransform(at);
graphicsCtx.setClip(clip);
graphicsCtx.translate(normBounds.getCenterX(), normBounds.getCenterY());
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;
}
}
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
*/
@ -689,4 +736,17 @@ public class HwmfGraphics {
}
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.Insets;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
@ -111,10 +112,33 @@ public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor {
public boolean drawImage(Graphics2D graphics, Rectangle2D anchor, Insets clip) {
if (image == null) {
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
@ -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.HwmfGraphics;
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.GenericRecordUtil;
import org.apache.poi.util.LittleEndianConsts;
@ -616,8 +617,7 @@ public class HwmfFill {
prop.getBkMode() == HwmfBkMode.TRANSPARENT);
ctx.drawImage(bi, 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);
ctx.drawImage((ImageRenderer)null, new Rectangle2D.Double(0,0,1,1), dstBounds);
}
}
@ -915,13 +915,13 @@ public class HwmfFill {
prop.setRasterOp(rasterOperation);
// TODO: implement second operation based on playback device context
if (target != null) {
HwmfBkMode mode = prop.getBkMode();
HwmfBkMode oldMode = prop.getBkMode();
prop.setBkMode(HwmfBkMode.TRANSPARENT);
Color fgColor = prop.getPenColor().getColor();
Color bgColor = prop.getBackgroundColor().getColor();
BufferedImage bi = target.getImage(fgColor, bgColor, true);
ctx.drawImage(bi, srcBounds, dstBounds);
prop.setBkMode(mode);
prop.setBkMode(oldMode);
}
}

View File

@ -540,8 +540,13 @@ public class HwmfMisc {
}
HwmfDrawProperties prop = ctx.getProperties();
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