Bug 60656 - Emf image support in slideshows

- use Rectangle2D instead of Dimension2D for image bounds
- fix shearing transformation
- fix rendering of font attributes (bold/italic/...)
- emf+: needs its own object table and properties table
- emf+: add linear gradient handler
- emf+: handle brush data of pens
- wmf/emf/emf+: position right aligned text correctly
- emf+: use emf+ instead of emf records in dual-mode
- emf+: handle region data and operations correctly
- emf/+: map font weight to awt font weight correctly

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1870566 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andreas Beeker 2019-11-29 00:39:35 +00:00
parent 7aab19c3b5
commit 1562175343
34 changed files with 919 additions and 207 deletions

View File

@ -17,7 +17,6 @@
package org.apache.poi.sl.draw; package org.apache.poi.sl.draw;
import java.awt.Dimension;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Insets; import java.awt.Insets;
@ -255,10 +254,10 @@ public class BitmapImageRenderer implements ImageRenderer {
} }
@Override @Override
public Dimension getDimension() { public Rectangle2D getBounds() {
return (img == null) return (img == null)
? new Dimension(0,0) ? new Rectangle2D.Double()
: new Dimension(img.getWidth(),img.getHeight()); : new Rectangle2D.Double(0, 0, img.getWidth(), img.getHeight());
} }
@Override @Override
@ -316,7 +315,9 @@ public class BitmapImageRenderer implements ImageRenderer {
AffineTransform at = new AffineTransform(sx, 0, 0, sy, tx, ty) ; AffineTransform at = new AffineTransform(sx, 0, 0, sy, tx, ty) ;
Shape clipOld = graphics.getClip(); Shape clipOld = graphics.getClip();
if (isClipped) graphics.clip(anchor.getBounds2D()); if (isClipped) {
graphics.clip(anchor.getBounds2D());
}
graphics.drawRenderedImage(img, at); graphics.drawRenderedImage(img, at);
graphics.setClip(clipOld); graphics.setClip(clipOld);

View File

@ -583,8 +583,7 @@ public class DrawPaint {
* *
* @return an array containing the 3 HSL values. * @return an array containing the 3 HSL values.
*/ */
private static double[] RGB2HSL(Color color) public static double[] RGB2HSL(Color color) {
{
// Get RGB values in the range 0 - 1 // Get RGB values in the range 0 - 1
float[] rgb = color.getRGBColorComponents( null ); float[] rgb = color.getRGBColorComponents( null );

View File

@ -46,6 +46,8 @@ public interface Drawable {
case 9: return "FONT_MAP"; case 9: return "FONT_MAP";
case 10: return "GSAVE"; case 10: return "GSAVE";
case 11: return "GRESTORE"; case 11: return "GRESTORE";
case 12: return "CURRENT_SLIDE";
case 13: return "BUFFERED_IMAGE";
default: return "UNKNOWN_ID "+intKey(); default: return "UNKNOWN_ID "+intKey();
} }
} }

View File

@ -27,6 +27,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.util.Dimension2DDouble;
/** /**
* Classes can implement this interfaces to support other formats, for * Classes can implement this interfaces to support other formats, for
@ -105,10 +106,18 @@ public interface ImageRenderer {
*/ */
Rectangle2D getNativeBounds(); Rectangle2D getNativeBounds();
/**
* @return the bounds of the buffered image in pixel
*/
Rectangle2D getBounds();
/** /**
* @return the dimension of the buffered image in pixel * @return the dimension of the buffered image in pixel
*/ */
Dimension2D getDimension(); default Dimension2D getDimension() {
Rectangle2D r = getBounds();
return new Dimension2DDouble(Math.abs(r.getWidth()), Math.abs(r.getHeight()));
}
/** /**
* @param alpha the alpha [0..1] to be added to the image (possibly already containing an alpha channel) * @param alpha the alpha [0..1] to be added to the image (possibly already containing an alpha channel)

View File

@ -17,6 +17,7 @@
package org.apache.poi.util; package org.apache.poi.util;
import java.awt.geom.Dimension2D; import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
public class Units { public class Units {
/** /**
@ -158,6 +159,22 @@ public class Units {
return new Dimension2DDouble(width, height); return new Dimension2DDouble(width, height);
} }
public static Rectangle2D pointsToPixel(Rectangle2D pointsDim) {
double x = pointsDim.getX() * PIXEL_DPI / POINT_DPI;
double y = pointsDim.getY() * PIXEL_DPI / POINT_DPI;
double width = pointsDim.getWidth() * PIXEL_DPI / POINT_DPI;
double height = pointsDim.getHeight() * PIXEL_DPI / POINT_DPI;
return new Rectangle2D.Double(x, y, width, height);
}
public static Rectangle2D pixelToPoints(Rectangle2D pointsDim) {
double x = pointsDim.getX() * POINT_DPI / PIXEL_DPI;
double y = pointsDim.getY() * POINT_DPI / PIXEL_DPI;
double width = pointsDim.getWidth() * POINT_DPI / PIXEL_DPI;
double height = pointsDim.getHeight() * POINT_DPI / PIXEL_DPI;
return new Rectangle2D.Double(x, y, width, height);
}
public static int charactersToEMU(double characters) { public static int charactersToEMU(double characters) {
return (int) characters * EMU_PER_CHARACTER; return (int) characters * EMU_PER_CHARACTER;
} }

View File

@ -42,7 +42,6 @@ import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.poi.sl.draw.Drawable; import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.sl.draw.ImageRenderer; import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.sl.usermodel.PictureData; import org.apache.poi.sl.usermodel.PictureData;
import org.apache.poi.util.Dimension2DDouble;
import org.w3c.dom.Document; import org.w3c.dom.Document;
public class SVGImageRenderer implements ImageRenderer { public class SVGImageRenderer implements ImageRenderer {
@ -76,9 +75,8 @@ public class SVGImageRenderer implements ImageRenderer {
} }
@Override @Override
public Dimension2D getDimension() { public Rectangle2D getBounds() {
Rectangle2D r = svgRoot.getPrimitiveBounds(); return svgRoot.getPrimitiveBounds();
return new Dimension2DDouble(Math.ceil(r.getWidth()), Math.ceil(r.getHeight()));
} }
@Override @Override

View File

@ -17,10 +17,13 @@
package org.apache.poi.hemf.draw; package org.apache.poi.hemf.draw;
import java.awt.Color;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle; import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle;
@ -47,6 +50,10 @@ public class HemfDrawProperties extends HwmfDrawProperties {
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<>();
private Rectangle2D brushRect;
private List<? extends Map.Entry<Float,Color>> brushColorsV;
private List<? extends Map.Entry<Float,Color>> brushColorsH;
public HemfDrawProperties() { public HemfDrawProperties() {
} }
@ -60,6 +67,15 @@ public class HemfDrawProperties extends HwmfDrawProperties {
emfPlusImage = other.emfPlusImage; emfPlusImage = other.emfPlusImage;
transXForm.addAll(other.transXForm); transXForm.addAll(other.transXForm);
transOper.addAll(other.transOper); transOper.addAll(other.transOper);
if (other.brushRect != null) {
brushRect = (Rectangle2D)other.brushRect.clone();
}
if (other.brushColorsV != null) {
brushColorsV = new ArrayList<>(other.brushColorsV);
}
if (other.brushColorsH != null) {
brushColorsH = new ArrayList<>(other.brushColorsH);
}
} }
/** /**
@ -139,4 +155,28 @@ public class HemfDrawProperties extends HwmfDrawProperties {
List<TransOperand> getTransOper() { List<TransOperand> getTransOper() {
return transOper; return transOper;
} }
public Rectangle2D getBrushRect() {
return brushRect;
}
public void setBrushRect(Rectangle2D brushRect) {
this.brushRect = brushRect;
}
public List<? extends Map.Entry<Float, Color>> getBrushColorsV() {
return brushColorsV;
}
public void setBrushColorsV(List<? extends Map.Entry<Float, Color>> brushColorsV) {
this.brushColorsV = brushColorsV;
}
public List<? extends Map.Entry<Float, Color>> getBrushColorsH() {
return brushColorsH;
}
public void setBrushColorsH(List<? extends Map.Entry<Float, Color>> brushColorsH) {
this.brushColorsH = brushColorsH;
}
} }

View File

@ -20,16 +20,25 @@ package org.apache.poi.hemf.draw;
import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_NULL; import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_NULL;
import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID; import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID;
import java.awt.AlphaComposite;
import java.awt.Color; import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.LinearGradientPaint;
import java.awt.MultipleGradientPaint;
import java.awt.Paint; import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.AffineTransform; 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.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Stream;
import org.apache.poi.hemf.draw.HemfDrawProperties.TransOperand; import org.apache.poi.hemf.draw.HemfDrawProperties.TransOperand;
import org.apache.poi.hemf.record.emf.HemfComment.EmfComment; import org.apache.poi.hemf.record.emf.HemfComment.EmfComment;
@ -37,7 +46,9 @@ import org.apache.poi.hemf.record.emf.HemfRecord;
import org.apache.poi.hemf.record.emfplus.HemfPlusRecord; import org.apache.poi.hemf.record.emfplus.HemfPlusRecord;
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.HwmfBrushStyle;
import org.apache.poi.hwmf.record.HwmfColorRef; import org.apache.poi.hwmf.record.HwmfColorRef;
import org.apache.poi.hwmf.record.HwmfMisc;
import org.apache.poi.hwmf.record.HwmfObjectTableEntry; import org.apache.poi.hwmf.record.HwmfObjectTableEntry;
import org.apache.poi.hwmf.record.HwmfPenStyle; import org.apache.poi.hwmf.record.HwmfPenStyle;
import org.apache.poi.util.Internal; import org.apache.poi.util.Internal;
@ -58,11 +69,14 @@ public class HemfGraphics extends HwmfGraphics {
private static final HwmfColorRef BLACK = new HwmfColorRef(Color.BLACK); private static final HwmfColorRef BLACK = new HwmfColorRef(Color.BLACK);
private EmfRenderState renderState = EmfRenderState.INITIAL; private EmfRenderState renderState = EmfRenderState.INITIAL;
private final Map<Integer,HwmfObjectTableEntry> plusObjectTable = new HashMap<>();
private final Map<Integer,HemfDrawProperties> plusPropStack = new HashMap<>();
public HemfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) { public HemfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) {
super(graphicsCtx,bbox); super(graphicsCtx,bbox);
// add dummy entry for object ind ex 0, as emf is 1-based // add dummy entry for object ind ex 0, as emf is 1-based
objectIndexes.set(0); objectIndexes.set(0);
getProperties().setBkMode(HwmfMisc.WmfSetBkMode.HwmfBkMode.TRANSPARENT);
} }
@Override @Override
@ -87,14 +101,8 @@ public class HemfGraphics extends HwmfGraphics {
public void draw(HemfRecord r) { public void draw(HemfRecord r) {
switch (getRenderState()) { switch (getRenderState()) {
default:
case EMF_DCONTEXT: case EMF_DCONTEXT:
// 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;
case INITIAL: case INITIAL:
r.draw(this); r.draw(this);
break; break;
@ -108,8 +116,6 @@ public class HemfGraphics extends HwmfGraphics {
r.draw(this); r.draw(this);
} }
break; break;
default:
break;
} }
} }
@ -171,10 +177,7 @@ public class HemfGraphics extends HwmfGraphics {
/** /**
* Adds or sets an record of type {@link HwmfObjectTableEntry} to the object table. * Adds or sets an record of type {@link HwmfObjectTableEntry} to the object table.
* If the {@code index} is less than 1, the method acts the same as * The index must be &gt; 0
* {@link HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry)}, otherwise the
* index is used to access the object table.
* As the table is filled successively, the index must be between 1 and size+1
* *
* @param entry the record to be stored * @param entry the record to be stored
* @param index the index to be overwritten, regardless if its content was unset before * @param index the index to be overwritten, regardless if its content was unset before
@ -182,42 +185,74 @@ public class HemfGraphics extends HwmfGraphics {
* @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry) * @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry)
*/ */
public void addObjectTableEntry(HwmfObjectTableEntry entry, int index) { public void addObjectTableEntry(HwmfObjectTableEntry entry, int index) {
checkTableEntryIndex(index); // in EMF the index must > 0
if (index < 1) {
throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index);
}
objectIndexes.set(index); objectIndexes.set(index);
objectTable.put(index, entry); objectTable.put(index, entry);
} }
/** /**
* Gets a record which was registered earliser * Adds or sets an record of type {@link HwmfObjectTableEntry} to the plus object table.
* The index must be in the range [0..63]
*
* @param entry the record to be stored
* @param index the index to be overwritten, regardless if its content was unset before
*
* @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry)
*/
public void addPlusObjectTableEntry(HwmfObjectTableEntry entry, int index) {
// in EMF+ the index must be between 0 and 63
if (index < 0 || index > 63) {
throw new IndexOutOfBoundsException("Object table entry index in EMF+ must be [0..63] - invalid index: "+index);
}
plusObjectTable.put(index, entry);
}
/**
* Gets a record which was registered earlier
* @param index the record index * @param index the record index
* @return the record or {@code null} if it doesn't exist * @return the record or {@code null} if it doesn't exist
*/ */
public HwmfObjectTableEntry getObjectTableEntry(int index) { public HwmfObjectTableEntry getObjectTableEntry(int index) {
checkTableEntryIndex(index); // in EMF the index must > 0
if (index < 1) {
throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index);
}
return objectTable.get(index); return objectTable.get(index);
} }
private void checkTableEntryIndex(int index) { public HwmfObjectTableEntry getPlusObjectTableEntry(int index) {
if (renderState != EmfRenderState.EMFPLUS_ONLY && renderState != EmfRenderState.EMF_DCONTEXT) { // in EMF+ the index must be between 0 and 63
// in EMF the index must > 0 if (index < 0 || index > 63) {
if (index < 1) { throw new IndexOutOfBoundsException("Object table entry index in EMF+ must be [0..63] - invalid index: "+index);
throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index);
}
} else {
// in EMF+ the index must be between 0 and 63
if (index < 0 || index > 63) {
throw new IndexOutOfBoundsException("Object table entry index in EMF+ must be [0..63] - invalid index: "+index);
}
} }
return plusObjectTable.get(index);
} }
@Override @Override
public void applyObjectTableEntry(int index) { public void applyObjectTableEntry(int index) {
if ((index & 0x80000000) != 0) { if ((index & 0x80000000) != 0) {
selectStockObject(index); selectStockObject(index);
} else { } else {
super.applyObjectTableEntry(index); HwmfObjectTableEntry ote = objectTable.get(index);
if (ote == null) {
throw new NoSuchElementException("EMF reference exception - object table entry on index "+index+" was deleted before.");
}
ote.applyObject(this);
}
}
public void applyPlusObjectTableEntry(int index) {
if ((index & 0x80000000) != 0) {
selectStockObject(index);
} else {
HwmfObjectTableEntry ote = plusObjectTable.get(index);
if (ote == null) {
throw new NoSuchElementException("EMF+ reference exception - plus object table entry on index "+index+" was deleted before.");
}
ote.applyObject(this);
} }
} }
@ -351,4 +386,82 @@ public class HemfGraphics extends HwmfGraphics {
graphicsCtx.setTransform(tx); graphicsCtx.setTransform(tx);
} }
@Override
public void fill(Shape shape) {
HemfDrawProperties prop = getProperties();
Composite old = graphicsCtx.getComposite();
graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
if (prop.getBrushStyle() != HwmfBrushStyle.BS_NULL) {
if (prop.getBkMode() == HwmfMisc.WmfSetBkMode.HwmfBkMode.OPAQUE) {
graphicsCtx.setPaint(prop.getBackgroundColor().getColor());
graphicsCtx.fill(shape);
}
graphicsCtx.setPaint(getFill());
graphicsCtx.fill(shape);
}
graphicsCtx.setComposite(old);
}
@Override
protected Paint getLinearGradient() {
HemfDrawProperties prop = getProperties();
Rectangle2D rect = prop.getBrushRect();
List<? extends Map.Entry<Float, Color>> colorsH = prop.getBrushColorsH();
assert(rect != null && colorsH != null);
// TODO: handle ColorsV list with a custom GradientPaint
// for an idea on how to handle 2d-gradients google "bilinear color interpolation".
// basically use two linear interpolations for x/y or vertical/horizontal axis.
// the resulting two colors need to be interpolated by 50%.
return new LinearGradientPaint(
new Point2D.Double(rect.getMinX(),rect.getCenterY()),
new Point2D.Double(rect.getMaxX(),rect.getCenterY()),
toArray(colorsH.stream().map(Map.Entry::getKey), colorsH.size()),
colorsH.stream().map(Map.Entry::getValue).toArray(Color[]::new),
MultipleGradientPaint.CycleMethod.NO_CYCLE,
MultipleGradientPaint.ColorSpaceType.SRGB,
prop.getBrushTransform()
);
}
private static float[] toArray(Stream<? extends Number> numbers, int size) {
float[] arr = new float[size];
final int[] i = {0};
numbers.forEach(n -> arr[i[0]++] = n.floatValue());
return arr;
}
/**
* Saves the current properties to the plus stack
*/
public void savePlusProperties(int index) {
final HemfDrawProperties p = getProperties();
assert(p != null);
p.setTransform(graphicsCtx.getTransform());
p.setClip(graphicsCtx.getClip());
plusPropStack.put(index,p);
prop = newProperties(p);
}
/**
* Restores the properties from the plus stack
*
* @param index the index of the previously saved properties
*/
public void restorePlusProperties(int index) {
if (!plusPropStack.containsKey(index)) {
return;
}
prop = new HemfDrawProperties(plusPropStack.get(index));
graphicsCtx.setTransform(prop.getTransform());
graphicsCtx.setClip(prop.getClip());
}
} }

View File

@ -19,11 +19,9 @@ package org.apache.poi.hemf.draw;
import static org.apache.poi.hwmf.draw.HwmfImageRenderer.getOuterBounds; import static org.apache.poi.hwmf.draw.HwmfImageRenderer.getOuterBounds;
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;
@ -33,6 +31,7 @@ import java.io.InputStream;
import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.hemf.usermodel.HemfPicture; import org.apache.poi.hemf.usermodel.HemfPicture;
import org.apache.poi.hwmf.draw.HwmfGraphicsState;
import org.apache.poi.hwmf.draw.HwmfImageRenderer; import org.apache.poi.hwmf.draw.HwmfImageRenderer;
import org.apache.poi.sl.draw.BitmapImageRenderer; import org.apache.poi.sl.draw.BitmapImageRenderer;
import org.apache.poi.sl.draw.EmbeddedExtractor; import org.apache.poi.sl.draw.EmbeddedExtractor;
@ -66,11 +65,6 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor {
image = new HemfPicture(new ByteArrayInputStream(data)); image = new HemfPicture(new ByteArrayInputStream(data));
} }
@Override
public Dimension2D getDimension() {
return Units.pointsToPixel(image == null ? new Dimension() : image.getSize());
}
@Override @Override
public void setAlpha(double alpha) { public void setAlpha(double alpha) {
this.alpha = alpha; this.alpha = alpha;
@ -110,21 +104,21 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor {
return false; return false;
} }
boolean isClipped = true; HwmfGraphicsState graphicsState = new HwmfGraphicsState();
if (clip == null) { graphicsState.backup(graphics);
isClipped = false;
clip = new Insets(0,0,0,0); try {
if (clip != null) {
graphics.clip(anchor);
} else {
clip = new Insets(0, 0, 0, 0);
}
image.draw(graphics, getOuterBounds(anchor, clip));
} finally {
graphicsState.restore(graphics);
} }
Shape clipOld = graphics.getClip();
if (isClipped) {
graphics.clip(anchor);
}
image.draw(graphics, getOuterBounds(anchor, clip));
graphics.setClip(clipOld);
return true; return true;
} }
@ -142,4 +136,9 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor {
public Rectangle2D getNativeBounds() { public Rectangle2D getNativeBounds() {
return image.getBounds(); return image.getBounds();
} }
@Override
public Rectangle2D getBounds() {
return Units.pointsToPixel(image == null ? new Rectangle2D.Double() : image.getBoundsInPoints());
}
} }

View File

@ -29,6 +29,7 @@ import java.util.function.Supplier;
import org.apache.poi.common.usermodel.GenericRecord; 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.record.emfplus.HemfPlusRecord; import org.apache.poi.hemf.record.emfplus.HemfPlusRecord;
import org.apache.poi.hemf.record.emfplus.HemfPlusRecordIterator; import org.apache.poi.hemf.record.emfplus.HemfPlusRecordIterator;
import org.apache.poi.hwmf.usermodel.HwmfPicture; import org.apache.poi.hwmf.usermodel.HwmfPicture;
@ -300,6 +301,9 @@ public class HemfComment {
@Override @Override
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
// 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.
ctx.setRenderState(EmfRenderState.EMFPLUS_ONLY);
records.forEach(ctx::draw); records.forEach(ctx::draw);
} }

View File

@ -862,7 +862,9 @@ public class HemfFill {
// m12 (translateY) = eDy (The vertical translation component, in logical units.) // m12 (translateY) = eDy (The vertical translation component, in logical units.)
double m12 = leis.readFloat(); double m12 = leis.readFloat();
xform.setTransform(m00, m10, m01, m11, m02, m12); // TODO: not sure, why the shearing has to be inverted here,
// probably because of the different world/user space transformation
xform.setTransform(m00, -m10, -m01, m11, m02, m12);
if (xform.isIdentity()) { if (xform.isIdentity()) {
xform.setToIdentity(); xform.setToIdentity();

View File

@ -492,6 +492,30 @@ public class HemfFont extends HwmfFont {
); );
} }
public void setHeight(double height) {
this.height = height;
}
public void setWeight(int weight) {
this.weight = weight;
}
public void setItalic(boolean italic) {
this.italic = italic;
}
public void setUnderline(boolean underline) {
this.underline = underline;
}
public void setStrikeOut(boolean strikeOut) {
this.strikeOut = strikeOut;
}
public void setTypeface(String typeface) {
this.facename = typeface;
}
@Override @Override
protected int readString(LittleEndianInputStream leis, StringBuilder sb, int limit) throws IOException { protected int readString(LittleEndianInputStream leis, StringBuilder sb, int limit) throws IOException {
sb.setLength(0); sb.setLength(0);

View File

@ -559,6 +559,9 @@ public class HemfMisc {
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException {
final int startIdx = leis.getReadIndex(); final int startIdx = leis.getReadIndex();
// An unsigned integer that specifies the index of the extended logical pen object in
// the EMF object table. This index MUST be saved so that this object can be
// reused or modified.
penIndex = (int) leis.readUInt(); penIndex = (int) leis.readUInt();
// A 32-bit unsigned integer that specifies the offset from the start of this // A 32-bit unsigned integer that specifies the offset from the start of this

View File

@ -72,10 +72,12 @@ public class HemfRecordIterator implements Iterator<HemfRecord> {
final HemfRecord record = type.constructor.get(); final HemfRecord record = type.constructor.get();
try { try {
long remBytes = recordSize-HEADER_SIZE; long remBytes = recordSize - HEADER_SIZE;
long readBytes = record.init(stream, remBytes, recordId); long readBytes = record.init(stream, remBytes, recordId);
assert (readBytes <= remBytes); assert (readBytes <= remBytes);
stream.skipFully((int)(remBytes-readBytes)); stream.skipFully((int) (remBytes - readBytes));
} catch (RecordFormatException e) {
throw e;
} catch (IOException|RuntimeException e) { } catch (IOException|RuntimeException e) {
throw new RecordFormatException(e); throw new RecordFormatException(e);
} }

View File

@ -29,12 +29,18 @@ import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections; 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.BiFunction;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.hemf.draw.HemfDrawProperties; import org.apache.poi.hemf.draw.HemfDrawProperties;
@ -47,6 +53,7 @@ import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType;
import org.apache.poi.hemf.record.emfplus.HemfPlusPath.EmfPlusPath; import org.apache.poi.hemf.record.emfplus.HemfPlusPath.EmfPlusPath;
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.sl.draw.DrawPaint;
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;
@ -310,7 +317,19 @@ public class HemfPlusBrush {
long init(LittleEndianInputStream leis, long dataSize) throws IOException; long init(LittleEndianInputStream leis, long dataSize) throws IOException;
/**
* Apply brush data to graphics properties
* @param ctx the graphics context
* @param continuedObjectData the list continued object data
*/
void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData); void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData);
/**
* Apply brush data to pen properties
* @param ctx the graphics context
* @param continuedObjectData the list continued object data
*/
void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData);
} }
/** The EmfPlusBrush object specifies a graphics brush for filling regions. */ /** The EmfPlusBrush object specifies a graphics brush for filling regions. */
@ -347,6 +366,13 @@ public class HemfPlusBrush {
brushData.applyObject(ctx, continuedObjectData); brushData.applyObject(ctx, continuedObjectData);
} }
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
EmfPlusBrushData brushData = getBrushData(continuedObjectData);
brushData.applyPen(ctx, continuedObjectData);
}
@Override @Override
public EmfPlusGraphicsVersion getGraphicsVersion() { public EmfPlusGraphicsVersion getGraphicsVersion() {
return graphicsVersion; return graphicsVersion;
@ -372,7 +398,6 @@ public class HemfPlusBrush {
return brushData; return brushData;
} }
public byte[] getRawData(List<? extends EmfPlusObjectData> continuedObjectData) { public byte[] getRawData(List<? extends EmfPlusObjectData> continuedObjectData) {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
try { try {
@ -416,11 +441,17 @@ public class HemfPlusBrush {
@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();
prop.setBackgroundColor(new HwmfColorRef(solidColor)); prop.setBrushColor(new HwmfColorRef(solidColor));
prop.setBrushTransform(null); prop.setBrushTransform(null);
prop.setBrushStyle(HwmfBrushStyle.BS_SOLID); prop.setBrushStyle(HwmfBrushStyle.BS_SOLID);
} }
@Override
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
HemfDrawProperties prop = ctx.getProperties();
prop.setPenColor(new HwmfColorRef(solidColor));
}
@Override @Override
public String toString() { public String toString() {
return GenericRecordJsonWriter.marshal(this); return GenericRecordJsonWriter.marshal(this);
@ -457,6 +488,12 @@ public class HemfPlusBrush {
prop.setEmfPlusBrushHatch(style); prop.setEmfPlusBrushHatch(style);
} }
@Override
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
HemfDrawProperties prop = ctx.getProperties();
prop.setPenColor(new HwmfColorRef(foreColor));
}
@Override @Override
public String toString() { public String toString() {
return GenericRecordJsonWriter.marshal(this); return GenericRecordJsonWriter.marshal(this);
@ -484,12 +521,15 @@ public class HemfPlusBrush {
private Rectangle2D rect = new Rectangle2D.Double(); private Rectangle2D rect = new Rectangle2D.Double();
private Color startColor, endColor; private Color startColor, endColor;
private AffineTransform transform; private AffineTransform transform;
private double[] positions; private float[] positions;
private Color[] blendColors; private Color[] blendColors;
private double[] positionsV; private float[] positionsV;
private double[] blendFactorsV; private float[] blendFactorsV;
private double[] positionsH; private float[] positionsH;
private double[] blendFactorsH; private float[] blendFactorsH;
private static int[] FLAG_MASKS = { 0x02, 0x04, 0x08, 0x10, 0x80 };
private static String[] FLAG_NAMES = { "TRANSFORM", "PRESET_COLORS", "BLEND_FACTORS_H", "BLEND_FACTORS_V", "BRUSH_DATA_IS_GAMMA_CORRECTED" };
@Override @Override
public long init(LittleEndianInputStream leis, long dataSize) throws IOException { public long init(LittleEndianInputStream leis, long dataSize) throws IOException {
@ -518,16 +558,13 @@ public class HemfPlusBrush {
size += readXForm(leis, (transform = new AffineTransform())); size += readXForm(leis, (transform = new AffineTransform()));
} }
final boolean isPreset = PRESET_COLORS.isSet(dataFlags); if (isPreset() && (isBlendH() || isBlendV())) {
final boolean blendH = BLEND_FACTORS_H.isSet(dataFlags);
final boolean blendV = BLEND_FACTORS_V.isSet(dataFlags);
if (isPreset && (blendH || blendV)) {
throw new RuntimeException("invalid combination of preset colors and blend factors v/h"); throw new RuntimeException("invalid combination of preset colors and blend factors v/h");
} }
size += (isPreset) ? readColors(leis, d -> positions = d, c -> blendColors = c) : 0; size += (isPreset()) ? readColors(leis, d -> positions = d, c -> blendColors = c) : 0;
size += (blendV) ? readFactors(leis, d -> positionsV = d, f -> blendFactorsV = f) : 0; size += (isBlendV()) ? readFactors(leis, d -> positionsV = d, f -> blendFactorsV = f) : 0;
size += (blendH) ? readFactors(leis, d -> positionsH = d, f -> blendFactorsH = f) : 0; size += (isBlendH()) ? readFactors(leis, d -> positionsH = d, f -> blendFactorsH = f) : 0;
return size; return size;
} }
@ -535,7 +572,26 @@ public class HemfPlusBrush {
@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();
// TODO: implement prop.setBrushStyle(HwmfBrushStyle.BS_LINEAR_GRADIENT);
prop.setBrushRect(rect);
prop.setBrushTransform(transform);
// Preset colors and BlendH/V are mutual exclusive
if (isPreset()) {
setColorProps(prop::setBrushColorsH, positions, this::getBlendColorAt);
} else {
setColorProps(prop::setBrushColorsH, positionsH, this::getBlendHColorAt);
}
setColorProps(prop::setBrushColorsV, positionsV, this::getBlendVColorAt);
if (!(isPreset() || isBlendH() || isBlendV())) {
prop.setBrushColorsH(Arrays.asList(kv(0f,startColor), kv(1f,endColor)));
}
}
@Override
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
} }
@Override @Override
@ -551,7 +607,7 @@ public class HemfPlusBrush {
@Override @Override
public Map<String, Supplier<?>> getGenericProperties() { public Map<String, Supplier<?>> getGenericProperties() {
final Map<String,Supplier<?>> m = new LinkedHashMap<>(); final Map<String,Supplier<?>> m = new LinkedHashMap<>();
m.put("flags", () -> dataFlags); m.put("flags", GenericRecordUtil.getBitsAsString(() -> dataFlags, FLAG_MASKS, FLAG_NAMES));
m.put("wrapMode", () -> wrapMode); m.put("wrapMode", () -> wrapMode);
m.put("rect", () -> rect); m.put("rect", () -> rect);
m.put("startColor", () -> startColor); m.put("startColor", () -> startColor);
@ -565,6 +621,67 @@ public class HemfPlusBrush {
m.put("blendFactorsH", () -> blendFactorsH); m.put("blendFactorsH", () -> blendFactorsH);
return Collections.unmodifiableMap(m); return Collections.unmodifiableMap(m);
} }
private boolean isPreset() {
return PRESET_COLORS.isSet(dataFlags);
}
private boolean isBlendH() {
return BLEND_FACTORS_H.isSet(dataFlags);
}
private boolean isBlendV() {
return BLEND_FACTORS_V.isSet(dataFlags);
}
private Map.Entry<Float,Color> getBlendColorAt(int index) {
return kv(positions[index], blendColors[index]);
}
private Map.Entry<Float,Color> getBlendHColorAt(int index) {
return kv(positionsH[index],interpolateColors(blendFactorsH[index]));
}
private Map.Entry<Float,Color> getBlendVColorAt(int index) {
return kv(positionsV[index],interpolateColors(blendFactorsV[index]));
}
private static Map.Entry<Float,Color> kv(Float position, Color color) {
return new AbstractMap.SimpleEntry<>(position, color);
}
private static void setColorProps(
Consumer<List<? extends Map.Entry<Float, Color>>> setter, float[] positions, Function<Integer,? extends Map.Entry<Float, Color>> sup) {
if (positions == null) {
setter.accept(null);
} else {
setter.accept(IntStream.range(0, positions.length).boxed().map(sup).collect(Collectors.toList()));
}
}
private Color interpolateColors(final double factor) {
// https://stackoverflow.com/questions/1416560/hsl-interpolation
final double[] hslStart = DrawPaint.RGB2HSL(startColor);
final double[] hslStop = DrawPaint.RGB2HSL(endColor);
BiFunction<Number,Number,Double> linearInter = (start, stop) ->
start.doubleValue()+(stop.doubleValue()-start.doubleValue())*factor;
double alpha = linearInter.apply(startColor.getAlpha(),endColor.getAlpha());
double sat = linearInter.apply(hslStart[1],hslStop[1]);
double lum = linearInter.apply(hslStart[2],hslStop[2]);
double hue1 = (hslStart[0]+hslStop[0])/2.;
double hue2 = (hslStart[0]+hslStop[0]+360.)/2.;
Function<Double,Double> hueDelta = (hue) ->
Math.min(Math.abs(hslStart[0]-hue), Math.abs(hslStop[0]-hue));
double hue = hueDelta.apply(hue1) < hueDelta.apply(hue2) ? hue1 : hue2;
return DrawPaint.HSL2RGB(hue, sat, lum, alpha/255.);
}
} }
/** The EmfPlusPathGradientBrushData object specifies a path gradient for a graphics brush. */ /** The EmfPlusPathGradientBrushData object specifies a path gradient for a graphics brush. */
@ -577,9 +694,9 @@ public class HemfPlusBrush {
private EmfPlusPath boundaryPath; private EmfPlusPath boundaryPath;
private Point2D[] boundaryPoints; private Point2D[] boundaryPoints;
private AffineTransform transform; private AffineTransform transform;
private double[] positions; private float[] positions;
private Color[] blendColors; private Color[] blendColors;
private double[] blendFactorsH; private float[] blendFactorsH;
private Double focusScaleX, focusScaleY; private Double focusScaleX, focusScaleY;
@Override @Override
@ -597,8 +714,12 @@ public class HemfPlusBrush {
// that appears at the center point of the brush. The color of the brush changes gradually from the // that appears at the center point of the brush. The color of the brush changes gradually from the
// boundary color to the center color as it moves from the boundary to the center point. // boundary color to the center color as it moves from the boundary to the center point.
centerColor = readARGB(leis.readInt()); centerColor = readARGB(leis.readInt());
int size = 3*LittleEndianConsts.INT_SIZE; int size = 3*LittleEndianConsts.INT_SIZE;
if (wrapMode == null) {
return size;
}
size += readPointF(leis, centerPoint); size += readPointF(leis, centerPoint);
// An unsigned 32-bit integer that specifies the number of colors specified in the SurroundingColor field. // An unsigned 32-bit integer that specifies the number of colors specified in the SurroundingColor field.
@ -608,10 +729,10 @@ public class HemfPlusBrush {
// An array of SurroundingColorCount EmfPlusARGB objects that specify the colors for discrete points on the // An array of SurroundingColorCount EmfPlusARGB objects that specify the colors for discrete points on the
// boundary of the brush. // boundary of the brush.
surroundingColor = new Color[colorCount]; surroundingColor = new Color[colorCount];
for (int i=0; i<colorCount; i++) { for (int i = 0; i < colorCount; i++) {
surroundingColor[i] = readARGB(leis.readInt()); surroundingColor[i] = readARGB(leis.readInt());
} }
size += (colorCount+1) * LittleEndianConsts.INT_SIZE; size += (colorCount + 1) * LittleEndianConsts.INT_SIZE;
// The boundary of the path gradient brush, which is specified by either a path or a closed cardinal spline. // The boundary of the path gradient brush, which is specified by either a path or a closed cardinal spline.
// If the BrushDataPath flag is set in the BrushDataFlags field, this field MUST contain an // If the BrushDataPath flag is set in the BrushDataFlags field, this field MUST contain an
@ -677,6 +798,11 @@ public class HemfPlusBrush {
} }
@Override
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
}
@Override @Override
public String toString() { public String toString() {
return GenericRecordJsonWriter.marshal(this); return GenericRecordJsonWriter.marshal(this);
@ -747,6 +873,11 @@ public class HemfPlusBrush {
prop.setBrushTransform(transform); prop.setBrushTransform(transform);
} }
@Override
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
}
@Override @Override
public String toString() { public String toString() {
return GenericRecordJsonWriter.marshal(this); return GenericRecordJsonWriter.marshal(this);
@ -768,11 +899,11 @@ public class HemfPlusBrush {
} }
} }
private static int readPositions(LittleEndianInputStream leis, Consumer<double[]> pos) { private static int readPositions(LittleEndianInputStream leis, Consumer<float[]> pos) {
final int count = leis.readInt(); final int count = leis.readInt();
int size = LittleEndianConsts.INT_SIZE; int size = LittleEndianConsts.INT_SIZE;
double[] positions = new double[count]; float[] positions = new float[count];
for (int i=0; i<count; i++) { for (int i=0; i<count; i++) {
positions[i] = leis.readFloat(); positions[i] = leis.readFloat();
size += LittleEndianConsts.INT_SIZE; size += LittleEndianConsts.INT_SIZE;
@ -782,7 +913,7 @@ public class HemfPlusBrush {
return size; return size;
} }
private static int readColors(LittleEndianInputStream leis, Consumer<double[]> pos, Consumer<Color[]> cols) { private static int readColors(LittleEndianInputStream leis, Consumer<float[]> pos, Consumer<Color[]> cols) {
int[] count = { 0 }; int[] count = { 0 };
int size = readPositions(leis, p -> { count[0] = p.length; pos.accept(p); }); int size = readPositions(leis, p -> { count[0] = p.length; pos.accept(p); });
Color[] colors = new Color[count[0]]; Color[] colors = new Color[count[0]];
@ -793,10 +924,10 @@ public class HemfPlusBrush {
return size + colors.length * LittleEndianConsts.INT_SIZE; return size + colors.length * LittleEndianConsts.INT_SIZE;
} }
private static int readFactors(LittleEndianInputStream leis, Consumer<double[]> pos, Consumer<double[]> facs) { private static int readFactors(LittleEndianInputStream leis, Consumer<float[]> pos, Consumer<float[]> facs) {
int[] count = { 0 }; int[] count = { 0 };
int size = readPositions(leis, p -> { count[0] = p.length; pos.accept(p); }); int size = readPositions(leis, p -> { count[0] = p.length; pos.accept(p); });
double[] factors = new double[count[0]]; float[] factors = new float[count[0]];
for (int i=0; i<factors.length; i++) { for (int i=0; i<factors.length; i++) {
factors[i] = leis.readFloat(); factors[i] = leis.readFloat();
} }

View File

@ -22,7 +22,6 @@ 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;
@ -34,9 +33,11 @@ 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.PrimitiveIterator.OfInt;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.apache.commons.codec.Charsets;
import org.apache.commons.math3.linear.LUDecomposition; import org.apache.commons.math3.linear.LUDecomposition;
import org.apache.commons.math3.linear.MatrixUtils; import org.apache.commons.math3.linear.MatrixUtils;
import org.apache.commons.math3.linear.RealMatrix; import org.apache.commons.math3.linear.RealMatrix;
@ -47,7 +48,9 @@ import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId;
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.HwmfPenStyle;
import org.apache.poi.hwmf.record.HwmfTernaryRasterOp; import org.apache.poi.hwmf.record.HwmfTernaryRasterOp;
import org.apache.poi.hwmf.record.HwmfText;
import org.apache.poi.sl.draw.ImageRenderer; 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;
@ -165,7 +168,7 @@ public class HemfPlusDraw {
prop.setBrushStyle(HwmfBrushStyle.BS_SOLID); prop.setBrushStyle(HwmfBrushStyle.BS_SOLID);
prop.setBrushColor(new HwmfColorRef(getSolidColor())); prop.setBrushColor(new HwmfColorRef(getSolidColor()));
} else { } else {
ctx.applyObjectTableEntry(getBrushId()); ctx.applyPlusObjectTableEntry(getBrushId());
} }
} }
} }
@ -208,8 +211,8 @@ public class HemfPlusDraw {
@Override @Override
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
ctx.applyObjectTableEntry(penId); ctx.applyPlusObjectTableEntry(penId);
ctx.applyObjectTableEntry(getObjectId()); ctx.applyPlusObjectTableEntry(getObjectId());
HemfDrawProperties prop = ctx.getProperties(); HemfDrawProperties prop = ctx.getProperties();
final Path2D path = prop.getPath(); final Path2D path = prop.getPath();
@ -282,11 +285,18 @@ public class HemfPlusDraw {
@Override @Override
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
HemfDrawProperties prop = ctx.getProperties();
applyColor(ctx); applyColor(ctx);
Area area = new Area(); Area area = new Area();
rectData.stream().map(Area::new).forEach(area::add); rectData.stream().map(Area::new).forEach(area::add);
ctx.fill(area); HwmfPenStyle ps = prop.getPenStyle();
try {
prop.setPenStyle(null);
ctx.fill(area);
} finally {
prop.setPenStyle(ps);
}
} }
@Override @Override
@ -319,6 +329,7 @@ public class HemfPlusDraw {
} }
} }
/** The EmfPlusDrawImagePoints record specifies drawing a scaled image inside a parallelogram. */
public static class EmfPlusDrawImagePoints implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed, EmfPlusRelativePosition { public static class EmfPlusDrawImagePoints implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed, EmfPlusRelativePosition {
/** /**
* This bit indicates that the rendering of the image includes applying an effect. * This bit indicates that the rendering of the image includes applying an effect.
@ -426,39 +437,31 @@ public class HemfPlusDraw {
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
HemfDrawProperties prop = ctx.getProperties(); HemfDrawProperties prop = ctx.getProperties();
ctx.applyObjectTableEntry(imageAttributesID); ctx.applyPlusObjectTableEntry(imageAttributesID);
ctx.applyObjectTableEntry(getObjectId()); ctx.applyPlusObjectTableEntry(getObjectId());
final ImageRenderer ir = prop.getEmfPlusImage(); final ImageRenderer ir = prop.getEmfPlusImage();
if (ir == null) { if (ir == null) {
return; return;
} }
AffineTransform txSaved = ctx.getTransform(); AffineTransform txSaved = ctx.getTransform();
AffineTransform tx = new AffineTransform(txSaved); AffineTransform tx = (AffineTransform)txSaved.clone();
HwmfTernaryRasterOp oldOp = prop.getRasterOp();
HwmfBkMode oldBk = prop.getBkMode();
try { try {
tx.concatenate(trans); tx.concatenate(trans);
ctx.setTransform(tx); ctx.setTransform(tx);
final Rectangle2D srcBounds = ir.getNativeBounds();
final Dimension2D dim = ir.getDimension();
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 // transformation from srcRect to destRect was already applied,
// the image data from // therefore use srcRect as third parameter
final AffineTransform srcTx = new AffineTransform(); ctx.drawImage(ir, srcRect, srcRect);
srcTx.translate(-srcBounds.getX(), srcBounds.getY());
srcTx.scale(dim.getWidth()/srcBounds.getWidth(), dim.getHeight()/srcBounds.getHeight());
final Rectangle2D biRect = srcTx.createTransformedShape(srcRect).getBounds2D();
// TODO: handle srcUnit
Rectangle2D destRect = new Rectangle2D.Double(0, 0, biRect.getWidth(), biRect.getHeight());
ctx.drawImage(ir, srcRect, destRect);
} finally { } finally {
prop.setBkMode(oldBk);
prop.setRasterOp(oldOp);
ctx.setTransform(txSaved); ctx.setTransform(txSaved);
} }
} }
@ -529,8 +532,8 @@ public class HemfPlusDraw {
@Override @Override
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
ctx.applyObjectTableEntry(imageAttributesID); ctx.applyPlusObjectTableEntry(imageAttributesID);
ctx.applyObjectTableEntry(getObjectId()); ctx.applyPlusObjectTableEntry(getObjectId());
HemfDrawProperties prop = ctx.getProperties(); HemfDrawProperties prop = ctx.getProperties();
prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY); prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY);
@ -592,7 +595,7 @@ public class HemfPlusDraw {
@Override @Override
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
applyColor(ctx); applyColor(ctx);
ctx.applyObjectTableEntry(getObjectId()); ctx.applyPlusObjectTableEntry(getObjectId());
HemfDrawProperties prop = ctx.getProperties(); HemfDrawProperties prop = ctx.getProperties();
ctx.fill(prop.getPath()); ctx.fill(prop.getPath());
} }
@ -657,12 +660,12 @@ public class HemfPlusDraw {
private int brushId; private int brushId;
private int optionsFlags; private int optionsFlags;
private String glyphs; private String glyphs;
private final List<Point2D> glpyhPos = new ArrayList<>(); private final List<Point2D> glyphPos = new ArrayList<>();
private final AffineTransform transformMatrix = new AffineTransform(); private final AffineTransform transformMatrix = new AffineTransform();
@Override @Override
public HemfPlusRecordType getEmfPlusRecordType() { public HemfPlusRecordType getEmfPlusRecordType() {
return HemfPlusRecordType.drawDriverstring; return HemfPlusRecordType.drawDriverString;
} }
@Override @Override
@ -689,7 +692,7 @@ public class HemfPlusDraw {
// A 32-bit unsigned integer that specifies whether a transform matrix is present in the // A 32-bit unsigned integer that specifies whether a transform matrix is present in the
// TransformMatrix field. // TransformMatrix field.
boolean hasMatrix = leis.readInt() == 1; int matrixPresent = leis.readInt();
// A 32-bit unsigned integer that specifies number of glyphs in the string. // A 32-bit unsigned integer that specifies number of glyphs in the string.
int glyphCount = leis.readInt(); int glyphCount = leis.readInt();
@ -716,10 +719,10 @@ public class HemfPlusDraw {
for (int i=0; i<glyphCount; i++) { for (int i=0; i<glyphCount; i++) {
Point2D p = new Point2D.Double(); Point2D p = new Point2D.Double();
size += readPointF(leis, p); size += readPointF(leis, p);
glpyhPos.add(p); glyphPos.add(p);
} }
if (hasMatrix) { if (matrixPresent != 0) {
size += HemfFill.readXForm(leis, transformMatrix); size += HemfFill.readXForm(leis, transformMatrix);
} }
@ -727,6 +730,31 @@ public class HemfPlusDraw {
return size; return size;
} }
@Override
public void draw(HemfGraphics ctx) {
HemfDrawProperties prop = ctx.getProperties();
prop.setTextAlignLatin(HwmfText.HwmfTextAlignment.LEFT);
prop.setTextVAlignLatin(HwmfText.HwmfTextVerticalAlignment.BASELINE);
ctx.applyPlusObjectTableEntry(getObjectId());
if (isSolidColor()) {
prop.setTextColor(new HwmfColorRef(getSolidColor()));
} else {
ctx.applyPlusObjectTableEntry(getBrushId());
}
if (REALIZED_ADVANCE.isSet(optionsFlags)) {
byte[] buf = glyphs.getBytes(Charsets.UTF_16LE);
ctx.drawString(buf, buf.length, glyphPos.get(0), null, null, null, null, true);
} else {
final OfInt glyphIter = glyphs.codePoints().iterator();
glyphPos.forEach(p -> {
byte[] buf = new String(new int[]{glyphIter.next()}, 0, 1).getBytes(Charsets.UTF_16LE);
ctx.drawString(buf, buf.length, p, null, null, null, null, true);
});
}
}
@Override @Override
public String toString() { public String toString() {
return GenericRecordJsonWriter.marshal(this); return GenericRecordJsonWriter.marshal(this);
@ -739,7 +767,7 @@ public class HemfPlusDraw {
"brushId", this::getBrushId, "brushId", this::getBrushId,
"optionsFlags", getBitsAsString(() -> optionsFlags, OPTIONS_MASK, OPTIONS_NAMES), "optionsFlags", getBitsAsString(() -> optionsFlags, OPTIONS_MASK, OPTIONS_NAMES),
"glyphs", () -> glyphs, "glyphs", () -> glyphs,
"glyphPos", () -> glpyhPos, "glyphPos", () -> glyphPos,
"transform", () -> transformMatrix "transform", () -> transformMatrix
); );
} }

View File

@ -22,7 +22,10 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.apache.poi.common.usermodel.fonts.FontHeader;
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.HemfFont;
import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusUnitType; import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusUnitType;
import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion; import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion;
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData;
@ -106,7 +109,17 @@ public class HemfPlusFont {
@Override @Override
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
HemfDrawProperties prop = ctx.getProperties();
HemfFont font = new HemfFont();
font.initDefaults();
font.setTypeface(family);
// TODO: check how to calculate the font size
font.setHeight(emSize);
font.setStrikeOut(STRIKEOUT.isSet(styleFlags));
font.setUnderline(UNDERLINE.isSet(styleFlags));
font.setWeight(BOLD.isSet(styleFlags) ? 700 : FontHeader.REGULAR_WEIGHT);
font.setItalic(ITALIC.isSet(styleFlags));
prop.setFont(font);
} }
@Override @Override

View File

@ -18,7 +18,7 @@
package org.apache.poi.hemf.record.emfplus; package org.apache.poi.hemf.record.emfplus;
import static org.apache.poi.util.GenericRecordUtil.getBitsAsString; import static org.apache.poi.util.GenericRecordUtil.getEnumBitsAsString;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
@ -60,8 +60,13 @@ public class HemfPlusHeader implements HemfPlusRecord {
} }
} }
private static final int[] FLAGS_MASK = { 0x0001 }; private static final int[] FLAGS_MASK = { 0x0000, 0x0001 };
private static final String[] FLAGS_NAMES = { "DUAL_MODE" }; private static final String[] FLAGS_NAMES = { "EMF_PLUS_MODE", "DUAL_MODE" };
private static final int[] EMFFLAGS_MASK = { 0x0000, 0x0001 };
private static final String[] EMFFLAGS_NAMES = { "CONTEXT_PRINTER", "CONTEXT_VIDEO" };
private int flags; private int flags;
private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion();
@ -125,7 +130,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.EMF_DCONTEXT); ctx.setRenderState(EmfRenderState.EMF_DCONTEXT);
} }
@Override @Override
@ -136,9 +141,9 @@ public class HemfPlusHeader implements HemfPlusRecord {
@Override @Override
public Map<String, Supplier<?>> getGenericProperties() { public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties( return GenericRecordUtil.getGenericProperties(
"flags", this::getFlags, "flags", getEnumBitsAsString(this::getFlags, FLAGS_MASK, FLAGS_NAMES),
"version", this::getVersion, "version", this::getVersion,
"emfPlusFlags", getBitsAsString(this::getEmfPlusFlags, FLAGS_MASK, FLAGS_NAMES), "emfPlusFlags", getEnumBitsAsString(this::getEmfPlusFlags, EMFFLAGS_MASK, EMFFLAGS_NAMES),
"logicalDpiX", this::getLogicalDpiX, "logicalDpiX", this::getLogicalDpiX,
"logicalDpiY", this::getLogicalDpiY "logicalDpiY", this::getLogicalDpiY
); );

View File

@ -20,6 +20,7 @@ package org.apache.poi.hemf.record.emfplus;
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF;
import static org.apache.poi.util.GenericRecordUtil.getBitsAsString; import static org.apache.poi.util.GenericRecordUtil.getBitsAsString;
import java.awt.Shape;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
@ -30,6 +31,7 @@ import java.util.function.Supplier;
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.hwmf.record.HwmfRegionMode;
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.GenericRecordUtil; import org.apache.poi.util.GenericRecordUtil;
@ -53,18 +55,20 @@ public class HemfPlusMisc {
} }
public enum CombineMode { public enum CombineMode {
CombineModeReplace(0x00000000), REPLACE(0x00000000, HwmfRegionMode.RGN_COPY),
CombineModeIntersect(0x00000001), INTERSECT(0x00000001, HwmfRegionMode.RGN_AND),
CombineModeUnion(0x00000002), UNION(0x00000002, HwmfRegionMode.RGN_OR),
CombineModeXOR(0x00000003), XOR(0x00000003, HwmfRegionMode.RGN_XOR),
CombineModeExclude(0x00000004), EXCLUDE(0x00000004, HwmfRegionMode.RGN_DIFF),
CombineModeComplement(0x00000005) COMPLEMENT(0x00000005, HwmfRegionMode.RGN_COMPLEMENT)
; ;
public final int id; public final int id;
public final HwmfRegionMode regionMode;
CombineMode(int id) { CombineMode(int id, HwmfRegionMode regionMode) {
this.id = id; this.id = id;
this.regionMode = regionMode;
} }
public static CombineMode valueOf(int id) { public static CombineMode valueOf(int id) {
@ -303,6 +307,14 @@ public class HemfPlusMisc {
public CombineMode getCombineMode() { public CombineMode getCombineMode() {
return CombineMode.valueOf(COMBINE_MODE.getValue(getFlags())); return CombineMode.valueOf(COMBINE_MODE.getValue(getFlags()));
} }
@Override
public void draw(HemfGraphics ctx) {
HemfDrawProperties prop = ctx.getProperties();
ctx.applyPlusObjectTableEntry(getObjectId());
Shape clip = prop.getPath();
ctx.setClip(clip, clip == null ? HwmfRegionMode.RGN_COPY : getCombineMode().regionMode, false);
}
} }
/** The EmfPlusSetClipRect record combines the current clipping region with a rectangle. */ /** The EmfPlusSetClipRect record combines the current clipping region with a rectangle. */
@ -389,6 +401,11 @@ public class HemfPlusMisc {
return LittleEndianConsts.INT_SIZE; return LittleEndianConsts.INT_SIZE;
} }
@Override
public void draw(HemfGraphics ctx) {
ctx.savePlusProperties(getStackIndex());
}
@Override @Override
public Map<String, Supplier<?>> getGenericProperties() { public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties( return GenericRecordUtil.getGenericProperties(
@ -407,6 +424,11 @@ public class HemfPlusMisc {
public HemfPlusRecordType getEmfPlusRecordType() { public HemfPlusRecordType getEmfPlusRecordType() {
return HemfPlusRecordType.restore; return HemfPlusRecordType.restore;
} }
@Override
public void draw(HemfGraphics ctx) {
ctx.restorePlusProperties(getStackIndex());
}
} }
/** The EmfPlusSetRenderingOrigin record specifies the rendering origin for graphics output. */ /** The EmfPlusSetRenderingOrigin record specifies the rendering origin for graphics output. */
@ -432,10 +454,11 @@ public class HemfPlusMisc {
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;
// A 32-bit unsigned integer that defines the horizontal coordinate value of the rendering origin. // error in the MS-EMFPLUS docs - its a signed integer instead of an unsigned
double x = leis.readUInt(); // A 32-bit signed integer that defines the horizontal coordinate value of the rendering origin.
// A 32-bit unsigned integer that defines the vertical coordinate value of the rendering origin. int x = leis.readInt();
double y = leis.readUInt(); // A 32-bit signed integer that defines the vertical coordinate value of the rendering origin.
int y = leis.readInt();
origin.setLocation(x,y); origin.setLocation(x,y);

View File

@ -196,7 +196,7 @@ public class HemfPlusObject {
public void draw(HemfGraphics ctx) { public void draw(HemfGraphics ctx) {
if (objectData.isContinuedRecord()) { if (objectData.isContinuedRecord()) {
EmfPlusObject other; EmfPlusObject other;
HwmfObjectTableEntry entry = ctx.getObjectTableEntry(getObjectId()); HwmfObjectTableEntry entry = ctx.getPlusObjectTableEntry(getObjectId());
if (entry instanceof EmfPlusObject && if (entry instanceof EmfPlusObject &&
objectData.getClass().isInstance((other = (EmfPlusObject)entry).getObjectData()) objectData.getClass().isInstance((other = (EmfPlusObject)entry).getObjectData())
) { ) {
@ -205,7 +205,7 @@ public class HemfPlusObject {
throw new RuntimeException("can't find previous record for continued record"); throw new RuntimeException("can't find previous record for continued record");
} }
} else { } else {
ctx.addObjectTableEntry(this, getObjectId()); ctx.addPlusObjectTableEntry(this, getObjectId());
} }
} }

View File

@ -35,6 +35,7 @@ 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.emfplus.HemfPlusDraw.EmfPlusCompressed; import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusCompressed;
import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusRelativePosition; import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusRelativePosition;
import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion;
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; 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.util.BitField; import org.apache.poi.util.BitField;
@ -87,7 +88,7 @@ public class HemfPlusPath {
private static final int[] TYPE_MASKS = { 0x10, 0x20, 0x80 }; private static final int[] TYPE_MASKS = { 0x10, 0x20, 0x80 };
private static final String[] TYPE_NAMES = { "DASHED", "MARKER", "CLOSE" }; private static final String[] TYPE_NAMES = { "DASHED", "MARKER", "CLOSE" };
private final HemfPlusHeader.EmfPlusGraphicsVersion graphicsVersion = new HemfPlusHeader.EmfPlusGraphicsVersion(); private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion();
private int pointFlags; private int pointFlags;
private Point2D[] pathPoints; private Point2D[] pathPoints;
private byte[] pointTypes; private byte[] pointTypes;
@ -143,7 +144,7 @@ public class HemfPlusPath {
} }
@Override @Override
public HemfPlusHeader.EmfPlusGraphicsVersion getGraphicsVersion() { public EmfPlusGraphicsVersion getGraphicsVersion() {
return graphicsVersion; return graphicsVersion;
} }
@ -175,9 +176,15 @@ public class HemfPlusPath {
@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();
Path2D path = new Path2D.Double(Path2D.WIND_NON_ZERO); prop.setPath(getPath());
prop.setPath(path); }
public Path2D getPath() {
return getPath(Path2D.WIND_NON_ZERO);
}
public Path2D getPath(int windingRule) {
Path2D path = new Path2D.Double(windingRule);
for (int idx=0; idx < pathPoints.length; idx++) { for (int idx=0; idx < pathPoints.length; idx++) {
Point2D p1 = pathPoints[idx]; Point2D p1 = pathPoints[idx];
switch (getPointType(idx)) { switch (getPointType(idx)) {
@ -198,10 +205,11 @@ public class HemfPlusPath {
path.closePath(); path.closePath();
} }
} }
return path;
} }
@Override @Override
public EmfPlusObjectType getGenericRecordType() { public Enum getGenericRecordType() {
return EmfPlusObjectType.PATH; return EmfPlusObjectType.PATH;
} }

View File

@ -34,6 +34,7 @@ import java.util.function.Supplier;
import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.common.usermodel.GenericRecord;
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.emfplus.HemfPlusBrush.EmfPlusBrush;
import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusUnitType; import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusUnitType;
import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion; import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion;
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData;
@ -330,6 +331,8 @@ public class HemfPlusPen {
private EmfPlusCustomLineCap customStartCap; private EmfPlusCustomLineCap customStartCap;
private EmfPlusCustomLineCap customEndCap; private EmfPlusCustomLineCap customEndCap;
private final EmfPlusBrush brush = new EmfPlusBrush();
@Override @Override
public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException { public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException {
// An EmfPlusGraphicsVersion object that specifies the version of operating system graphics that // An EmfPlusGraphicsVersion object that specifies the version of operating system graphics that
@ -463,6 +466,8 @@ public class HemfPlusPen {
size += initCustomCap(c -> customEndCap = c, leis); size += initCustomCap(c -> customEndCap = c, leis);
} }
size += brush.init(leis, dataSize-size, EmfPlusObjectType.BRUSH, 0);
return size; return size;
} }
@ -472,8 +477,12 @@ public class HemfPlusPen {
} }
private long initCustomCap(Consumer<EmfPlusCustomLineCap> setter, LittleEndianInputStream leis) throws IOException { private long initCustomCap(Consumer<EmfPlusCustomLineCap> setter, LittleEndianInputStream leis) throws IOException {
int CustomStartCapSize = leis.readInt();
int size = LittleEndianConsts.INT_SIZE;
EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion();
long size = version.init(leis); size += version.init(leis);
assert(version.getGraphicsVersion() != null);
boolean adjustableArrow = (leis.readInt() != 0); boolean adjustableArrow = (leis.readInt() != 0);
size += LittleEndianConsts.INT_SIZE; size += LittleEndianConsts.INT_SIZE;
@ -492,11 +501,11 @@ public class HemfPlusPen {
// TOOD: // TOOD:
// - set width according unit type // - set width according unit type
// - provide logic for different start and end cap // - provide logic for different start and end cap
// - provide standard caps like diamondd // - provide standard caps like diamond
// - support custom caps // - support custom caps
// workaround for too wide pens ... just arbitrary reduce high values ... brush.applyPen(ctx, continuedObjectData);
prop.setPenWidth(penWidth > 20 ? 1 : penWidth); prop.setPenWidth(penWidth);
prop.setPenStyle(new HwmfPenStyle(){ prop.setPenStyle(new HwmfPenStyle(){
@Override @Override
public HwmfLineCap getLineCap() { public HwmfLineCap getLineCap() {
@ -573,6 +582,7 @@ public class HemfPlusPen {
m.put("compoundLineData", () -> compoundLineData); m.put("compoundLineData", () -> compoundLineData);
m.put("customStartCap", () -> customStartCap); m.put("customStartCap", () -> customStartCap);
m.put("customEndCap", () -> customEndCap); m.put("customEndCap", () -> customEndCap);
m.put("brush", () -> brush);
return Collections.unmodifiableMap(m); return Collections.unmodifiableMap(m);
} }
} }
@ -645,13 +655,17 @@ public class HemfPlusPen {
size += readPointF(leis, lineHotSpot); size += readPointF(leis, lineHotSpot);
if (FILL_PATH.isSet(dataFlags)) { if (FILL_PATH.isSet(dataFlags)) {
int fillSize = leis.readInt();
size += LittleEndianConsts.INT_SIZE;
fillPath = new EmfPlusPath(); fillPath = new EmfPlusPath();
size += fillPath.init(leis, -1, null, -1); size += fillPath.init(leis, fillSize, EmfPlusObjectType.PATH, -1);
} }
if (LINE_PATH.isSet(dataFlags)) { if (LINE_PATH.isSet(dataFlags)) {
int pathSize = leis.readInt();
size += LittleEndianConsts.INT_SIZE;
outlinePath = new EmfPlusPath(); outlinePath = new EmfPlusPath();
size += outlinePath.init(leis, -1, null, -1); size += outlinePath.init(leis, pathSize, EmfPlusObjectType.PATH, -1);
} }
return size; return size;

View File

@ -55,4 +55,8 @@ public interface HemfPlusRecord extends GenericRecord {
default void draw(HemfGraphics ctx) { default void draw(HemfGraphics ctx) {
} }
@Override
default Enum getGenericRecordType() {
return getEmfPlusRecordType();
}
} }

View File

@ -76,7 +76,7 @@ public enum HemfPlusRecordType {
setClipRegion(0x4033, HemfPlusMisc.EmfPlusSetClipRegion::new), setClipRegion(0x4033, HemfPlusMisc.EmfPlusSetClipRegion::new),
setClipPath(0x4034, HemfPlusMisc.EmfPlusSetClipPath::new), setClipPath(0x4034, HemfPlusMisc.EmfPlusSetClipPath::new),
offsetClip(0x4035, UnimplementedHemfPlusRecord::new), offsetClip(0x4035, UnimplementedHemfPlusRecord::new),
drawDriverstring(0x4036, HemfPlusDraw.EmfPlusDrawDriverString::new), drawDriverString(0x4036, HemfPlusDraw.EmfPlusDrawDriverString::new),
strokeFillPath(0x4037, UnimplementedHemfPlusRecord::new), strokeFillPath(0x4037, UnimplementedHemfPlusRecord::new),
serializableObject(0x4038, UnimplementedHemfPlusRecord::new), serializableObject(0x4038, UnimplementedHemfPlusRecord::new),
setTSGraphics(0x4039, UnimplementedHemfPlusRecord::new), setTSGraphics(0x4039, UnimplementedHemfPlusRecord::new),

View File

@ -19,14 +19,19 @@ package org.apache.poi.hemf.record.emfplus;
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.io.IOException; import java.io.IOException;
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.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.common.usermodel.GenericRecord;
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.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion; import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion;
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData;
@ -42,49 +47,51 @@ public class HemfPlusRegion {
* Specifies a region node with child nodes. A Boolean AND operation SHOULD be applied to the left and right * Specifies a region node with child nodes. A Boolean AND operation SHOULD be applied to the left and right
* child nodes specified by an EmfPlusRegionNodeChildNodes object * child nodes specified by an EmfPlusRegionNodeChildNodes object
*/ */
AND(0X00000001, EmfPlusRegionNode::new), AND(0X00000001, EmfPlusRegionNode::new, Area::intersect),
/** /**
* Specifies a region node with child nodes. A Boolean OR operation SHOULD be applied to the left and right * Specifies a region node with child nodes. A Boolean OR operation SHOULD be applied to the left and right
* child nodes specified by an EmfPlusRegionNodeChildNodes object. * child nodes specified by an EmfPlusRegionNodeChildNodes object.
*/ */
OR(0X00000002, EmfPlusRegionNode::new), OR(0X00000002, EmfPlusRegionNode::new, Area::add),
/** /**
* Specifies a region node with child nodes. A Boolean XOR operation SHOULD be applied to the left and right * Specifies a region node with child nodes. A Boolean XOR operation SHOULD be applied to the left and right
* child nodes specified by an EmfPlusRegionNodeChildNodes object. * child nodes specified by an EmfPlusRegionNodeChildNodes object.
*/ */
XOR(0X00000003, EmfPlusRegionNode::new), XOR(0X00000003, EmfPlusRegionNode::new, Area::exclusiveOr),
/** /**
* Specifies a region node with child nodes. A Boolean operation, defined as "the part of region 1 that is excluded * Specifies a region node with child nodes. A Boolean operation, defined as "the part of region 1 that is excluded
* from region 2", SHOULD be applied to the left and right child nodes specified by an EmfPlusRegionNodeChildNodes object. * from region 2", SHOULD be applied to the left and right child nodes specified by an EmfPlusRegionNodeChildNodes object.
*/ */
EXCLUDE(0X00000004, EmfPlusRegionNode::new), EXCLUDE(0X00000004, EmfPlusRegionNode::new, Area::subtract),
/** /**
* Specifies a region node with child nodes. A Boolean operation, defined as "the part of region 2 that is excluded * Specifies a region node with child nodes. A Boolean operation, defined as "the part of region 2 that is excluded
* from region 1", SHOULD be applied to the left and right child nodes specified by an EmfPlusRegionNodeChildNodes object. * from region 1", SHOULD be applied to the left and right child nodes specified by an EmfPlusRegionNodeChildNodes object.
*/ */
COMPLEMENT(0X00000005, EmfPlusRegionNode::new), COMPLEMENT(0X00000005, EmfPlusRegionNode::new, Area::subtract),
/** /**
* Specifies a region node with no child nodes. * Specifies a region node with no child nodes.
* The RegionNodeData field SHOULD specify a boundary with an EmfPlusRectF object. * The RegionNodeData field SHOULD specify a boundary with an EmfPlusRectF object.
*/ */
RECT(0X10000000, EmfPlusRegionRect::new), RECT(0X10000000, EmfPlusRegionRect::new, null),
/** /**
* Specifies a region node with no child nodes. * Specifies a region node with no child nodes.
* The RegionNodeData field SHOULD specify a boundary with an EmfPlusRegionNodePath object * The RegionNodeData field SHOULD specify a boundary with an EmfPlusRegionNodePath object
*/ */
PATH(0X10000001, EmfPlusRegionPath::new), PATH(0X10000001, EmfPlusRegionPath::new, null),
/** Specifies a region node with no child nodes. The RegionNodeData field SHOULD NOT be present. */ /** Specifies a region node with no child nodes. The RegionNodeData field SHOULD NOT be present. */
EMPTY(0X10000002, EmfPlusRegionEmpty::new), EMPTY(0X10000002, EmfPlusRegionEmpty::new, null),
/** Specifies a region node with no child nodes, and its bounds are not defined. */ /** Specifies a region node with no child nodes, and its bounds are not defined. */
INFINITE(0X10000003, EmfPlusRegionInfinite::new) INFINITE(0X10000003, EmfPlusRegionInfinite::new, null)
; ;
public final int id; public final int id;
public final Supplier<EmfPlusRegionNodeData> constructor; public final Supplier<EmfPlusRegionNodeData> constructor;
public final BiConsumer<Area,Area> operation;
EmfPlusRegionNodeDataType(int id, Supplier<EmfPlusRegionNodeData> constructor) { EmfPlusRegionNodeDataType(int id, Supplier<EmfPlusRegionNodeData> constructor, BiConsumer<Area,Area> operation) {
this.id = id; this.id = id;
this.constructor = constructor; this.constructor = constructor;
this.operation = operation;
} }
public static EmfPlusRegionNodeDataType valueOf(int id) { public static EmfPlusRegionNodeDataType valueOf(int id) {
@ -95,6 +102,7 @@ public class HemfPlusRegion {
} }
} }
/** The EmfPlusRegion object specifies line and curve segments that define a nonrectilinear shape. */
public static class EmfPlusRegion implements EmfPlusObjectData { public static class EmfPlusRegion implements EmfPlusObjectData {
private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion();
@ -111,14 +119,16 @@ public class HemfPlusRegion {
// An array of RegionNodeCount+1 EmfPlusRegionNode objects. Regions are specified as a binary tree of // An array of RegionNodeCount+1 EmfPlusRegionNode objects. Regions are specified as a binary tree of
// region nodes, and each node MUST either be a terminal node or specify one or two child nodes. // region nodes, and each node MUST either be a terminal node or specify one or two child nodes.
// RegionNode MUST contain at least one element. // RegionNode MUST contain at least one element.
size += readNode(leis, d -> regionNode = d); size += readNode(leis, this::setRegionNode);
return size; return size;
} }
@Override @Override
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
HemfDrawProperties prop = ctx.getProperties();
Shape shape = regionNode.getShape();
prop.setPath(shape == null ? null : new Path2D.Double(shape));
} }
@Override @Override
@ -131,11 +141,19 @@ public class HemfPlusRegion {
return EmfPlusObjectType.REGION; return EmfPlusObjectType.REGION;
} }
private void setRegionNode(EmfPlusRegionNodeData regionNode) {
this.regionNode = regionNode;
}
public EmfPlusRegionNodeData getRegionNode() {
return regionNode;
}
@Override @Override
public Map<String, Supplier<?>> getGenericProperties() { public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties( return GenericRecordUtil.getGenericProperties(
"graphicsVersion", this::getGraphicsVersion, "graphicsVersion", this::getGraphicsVersion,
"regionNode", () -> regionNode "regionNode", this::getRegionNode
); );
} }
} }
@ -143,6 +161,8 @@ public class HemfPlusRegion {
public interface EmfPlusRegionNodeData extends GenericRecord { public interface EmfPlusRegionNodeData extends GenericRecord {
long init(LittleEndianInputStream leis) throws IOException; long init(LittleEndianInputStream leis) throws IOException;
Shape getShape();
default void setNodeType(EmfPlusRegionNodeDataType type) {}
} }
public static class EmfPlusRegionPath extends EmfPlusPath implements EmfPlusRegionNodeData { public static class EmfPlusRegionPath extends EmfPlusPath implements EmfPlusRegionNodeData {
@ -150,6 +170,16 @@ public class HemfPlusRegion {
int dataSize = leis.readInt(); int dataSize = leis.readInt();
return super.init(leis, dataSize, EmfPlusObjectType.PATH, 0) + LittleEndianConsts.INT_SIZE; return super.init(leis, dataSize, EmfPlusObjectType.PATH, 0) + LittleEndianConsts.INT_SIZE;
} }
@Override
public Shape getShape() {
return getPath();
}
@Override
public EmfPlusRegionNodeDataType getGenericRecordType() {
return EmfPlusRegionNodeDataType.PATH;
}
} }
public static class EmfPlusRegionInfinite implements EmfPlusRegionNodeData { public static class EmfPlusRegionInfinite implements EmfPlusRegionNodeData {
@ -162,6 +192,16 @@ public class HemfPlusRegion {
public Map<String, Supplier<?>> getGenericProperties() { public Map<String, Supplier<?>> getGenericProperties() {
return null; return null;
} }
@Override
public Shape getShape() {
return null;
}
@Override
public EmfPlusRegionNodeDataType getGenericRecordType() {
return EmfPlusRegionNodeDataType.INFINITE;
}
} }
public static class EmfPlusRegionEmpty implements EmfPlusRegionNodeData { public static class EmfPlusRegionEmpty implements EmfPlusRegionNodeData {
@ -174,6 +214,16 @@ public class HemfPlusRegion {
public Map<String, Supplier<?>> getGenericProperties() { public Map<String, Supplier<?>> getGenericProperties() {
return null; return null;
} }
@Override
public Shape getShape() {
return new Rectangle2D.Double(0,0,0,0);
}
@Override
public EmfPlusRegionNodeDataType getGenericRecordType() {
return EmfPlusRegionNodeDataType.EMPTY;
}
} }
public static class EmfPlusRegionRect implements EmfPlusRegionNodeData { public static class EmfPlusRegionRect implements EmfPlusRegionNodeData {
@ -188,25 +238,90 @@ public class HemfPlusRegion {
public Map<String, Supplier<?>> getGenericProperties() { public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties("rect", () -> rect); return GenericRecordUtil.getGenericProperties("rect", () -> rect);
} }
@Override
public Shape getShape() {
return rect;
}
@Override
public EmfPlusRegionNodeDataType getGenericRecordType() {
return EmfPlusRegionNodeDataType.RECT;
}
} }
/** The EmfPlusRegionNode object specifies nodes of a graphics region. */
public static class EmfPlusRegionNode implements EmfPlusRegionNodeData { public static class EmfPlusRegionNode implements EmfPlusRegionNodeData {
private EmfPlusRegionNodeData left, right; private EmfPlusRegionNodeData left, right;
private EmfPlusRegionNodeDataType nodeType;
@Override @Override
public long init(LittleEndianInputStream leis) throws IOException { public long init(LittleEndianInputStream leis) throws IOException {
long size = readNode(leis, n -> left = n); long size = readNode(leis, this::setLeft);
size += readNode(leis, n -> right = n); size += readNode(leis, this::setRight);
return size; return size;
} }
private void setLeft(EmfPlusRegionNodeData left) {
this.left = left;
}
private void setRight(EmfPlusRegionNodeData right) {
this.right = right;
}
public EmfPlusRegionNodeData getLeft() {
return left;
}
public EmfPlusRegionNodeData getRight() {
return right;
}
public EmfPlusRegionNodeDataType getNodeType() {
return nodeType;
}
@Override
public void setNodeType(EmfPlusRegionNodeDataType nodeType) {
this.nodeType = nodeType;
}
@Override @Override
public Map<String, Supplier<?>> getGenericProperties() { public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties( return GenericRecordUtil.getGenericProperties(
"left", () -> left, "nodeType", this::getNodeType,
"right", () -> right "left", this::getLeft,
"right", this::getRight
); );
} }
@Override
public Shape getShape() {
boolean com = (nodeType == EmfPlusRegionNodeDataType.COMPLEMENT);
final Shape leftShape = (com ? right : left).getShape();
final Shape rightShape = (com ? left : right).getShape();
if (leftShape == null) {
return rightShape;
} else if (rightShape == null) {
return leftShape;
}
// TODO: check Area vs. Path manipulation
Area leftArea = new Area(leftShape);
Area rightArea = new Area(rightShape);
assert(nodeType.operation != null);
nodeType.operation.accept(leftArea, rightArea);
return leftArea;
}
@Override
public EmfPlusRegionNodeDataType getGenericRecordType() {
return nodeType;
}
} }
private static long readNode(LittleEndianInputStream leis, Consumer<EmfPlusRegionNodeData> con) throws IOException { private static long readNode(LittleEndianInputStream leis, Consumer<EmfPlusRegionNodeData> con) throws IOException {
@ -216,6 +331,7 @@ public class HemfPlusRegion {
assert(type != null); assert(type != null);
EmfPlusRegionNodeData nd = type.constructor.get(); EmfPlusRegionNodeData nd = type.constructor.get();
con.accept(nd); con.accept(nd);
nd.setNodeType(type);
return LittleEndianConsts.INT_SIZE + nd.init(leis); return LittleEndianConsts.INT_SIZE + nd.init(leis);
} }
} }

View File

@ -130,14 +130,23 @@ public class HemfPicture implements Iterable<HemfRecord>, GenericRecord {
return new Rectangle2D.Double(x, y, width, height); return new Rectangle2D.Double(x, y, width, height);
} }
/**
* Return the image bounds in points
*
* @return the image bounds in points
*/
public Rectangle2D getBoundsInPoints() {
return Units.pixelToPoints(getHeader().getBoundsRectangle());
}
/** /**
* Return the image size in points * Return the image size in points
* *
* @return the image size in points * @return the image size in points
*/ */
public Dimension2D getSize() { public Dimension2D getSize() {
final Rectangle2D b = getHeader().getBoundsRectangle(); final Rectangle2D b = getBoundsInPoints();
return Units.pixelToPoints(new Dimension2DDouble(abs(b.getWidth()), abs(b.getHeight()))); return new Dimension2DDouble(abs(b.getWidth()), abs(b.getHeight()));
} }
private static double minX(Rectangle2D bounds) { private static double minX(Rectangle2D bounds) {

View File

@ -93,12 +93,27 @@ public class HwmfGraphics {
} }
} }
protected final List<HwmfDrawProperties> propStack = new LinkedList<>(); private static final Float[] WEIGHT_MAP = {
900f, TextAttribute.WEIGHT_ULTRABOLD,
800f, TextAttribute.WEIGHT_EXTRABOLD,
750f, TextAttribute.WEIGHT_HEAVY,
700f, TextAttribute.WEIGHT_BOLD,
600f, TextAttribute.WEIGHT_DEMIBOLD,
500f, TextAttribute.WEIGHT_MEDIUM,
450f, TextAttribute.WEIGHT_SEMIBOLD,
400f, TextAttribute.WEIGHT_REGULAR,
300f, TextAttribute.WEIGHT_DEMILIGHT,
200f, TextAttribute.WEIGHT_LIGHT,
1f, TextAttribute.WEIGHT_EXTRA_LIGHT
};
private final List<HwmfDrawProperties> propStack = new LinkedList<>();
protected HwmfDrawProperties prop; protected HwmfDrawProperties prop;
protected final Graphics2D graphicsCtx; protected final Graphics2D graphicsCtx;
protected final BitSet objectIndexes = new BitSet(); protected final BitSet objectIndexes = new BitSet();
protected final TreeMap<Integer,HwmfObjectTableEntry> objectTable = new TreeMap<>(); protected final TreeMap<Integer,HwmfObjectTableEntry> objectTable = new TreeMap<>();
protected final AffineTransform initialAT = new AffineTransform(); private final AffineTransform initialAT = new AffineTransform();
private static final Charset DEFAULT_CHARSET = LocaleUtil.CHARSET_1252; private static final Charset DEFAULT_CHARSET = LocaleUtil.CHARSET_1252;
@ -207,9 +222,14 @@ public class HwmfGraphics {
case BS_DIBPATTERNPT: return getPatternPaint(); case BS_DIBPATTERNPT: return getPatternPaint();
case BS_SOLID: return getSolidFill(); case BS_SOLID: return getSolidFill();
case BS_HATCHED: return getHatchedFill(); case BS_HATCHED: return getHatchedFill();
case BS_LINEAR_GRADIENT: return getLinearGradient();
} }
} }
protected Paint getLinearGradient() {
return null;
}
protected Paint getSolidFill() { protected Paint getSolidFill() {
return getProperties().getBrushColor().getColor(); return getProperties().getBrushColor().getColor();
} }
@ -440,8 +460,13 @@ public class HwmfGraphics {
} }
int trimLen; int trimLen;
for (trimLen=0; trimLen<text.length-1; trimLen+=2) { for (trimLen=0; trimLen<text.length; trimLen+=2) {
if ((text[trimLen] == -1 && text[trimLen+1] == -1) || if (trimLen == text.length-1) {
if (text[trimLen] != 0) {
trimLen++;
}
break;
} else if ((text[trimLen] == -1 && text[trimLen+1] == -1) ||
((text[trimLen] & 0xE0) == 0 && text[trimLen+1] == 0)) { ((text[trimLen] & 0xE0) == 0 && text[trimLen+1] == 0)) {
break; break;
} }
@ -519,7 +544,7 @@ public class HwmfGraphics {
tx.translate(-pixelBounds.getWidth() / 2., 0); tx.translate(-pixelBounds.getWidth() / 2., 0);
break; break;
case RIGHT: case RIGHT:
tx.translate(-pixelBounds.getWidth(), 0); tx.translate(-layout.getAdvance(), 0);
break; break;
} }
@ -581,7 +606,16 @@ public class HwmfGraphics {
if (font.isItalic()) { if (font.isItalic()) {
as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE); as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
} }
as.addAttribute(TextAttribute.WEIGHT, font.getWeight()); // convert font weight to awt font weight - usually a font weight of 400 is regarded as regular
final int fw = font.getWeight();
Float awtFW = TextAttribute.WEIGHT_REGULAR;
for (int i=0; i<WEIGHT_MAP.length; i+=2) {
if (fw >= WEIGHT_MAP[i]) {
awtFW = WEIGHT_MAP[i+1];
break;
}
}
as.addAttribute(TextAttribute.WEIGHT, awtFW);
} }
private double getFontHeight(HwmfFont font) { private double getFontHeight(HwmfFont font) {
@ -661,7 +695,11 @@ public class HwmfGraphics {
// of the referenced image and can be also negative // of the referenced image and can be also negative
Composite old = graphicsCtx.getComposite(); Composite old = graphicsCtx.getComposite();
graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
img.drawImage(graphicsCtx, normBounds, getSubImageInsets(srcBounds, img.getNativeBounds()));
boolean useDeviceBounds = (img instanceof HwmfImageRenderer);
img.drawImage(graphicsCtx, normBounds,
getSubImageInsets(srcBounds, useDeviceBounds ? img.getNativeBounds() : img.getBounds()));
graphicsCtx.setComposite(old); graphicsCtx.setComposite(old);
graphicsCtx.setTransform(oldTrans); graphicsCtx.setTransform(oldTrans);
@ -683,9 +721,9 @@ public class HwmfGraphics {
// Todo: check if we need to normalize srcBounds x/y, in case of flipped images // Todo: check if we need to normalize srcBounds x/y, in case of flipped images
// for now we assume the width/height is positive // for now we assume the width/height is positive
int left = (int)Math.round((srcBounds.getX()-nativeBounds.getX())/nativeBounds.getWidth()*100_000.); 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 top = (int)Math.round((srcBounds.getY()-nativeBounds.getY())/nativeBounds.getHeight()*100_000.);
int right = (int)Math.round((nativeBounds.getMaxX()-srcBounds.getMaxX())/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.); int bottom = (int)Math.round((nativeBounds.getMaxY()-srcBounds.getMaxY())/nativeBounds.getHeight()*100_000.);
return new Insets(top, left, bottom, right); return new Insets(top, left, bottom, right);
} }

View File

@ -0,0 +1,72 @@
/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.hwmf.draw;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import org.apache.poi.util.Internal;
/**
* An util class for saving the state of a {@link java.awt.Graphics2D} object
*/
@Internal
public class HwmfGraphicsState {
private Color background;
private Shape clip;
private Color color;
private Composite composite;
private Font font;
private Paint paint;
private Stroke stroke;
private AffineTransform trans;
/**
* Saves the state of the graphics2D object
*/
public void backup(Graphics2D graphics2D) {
background = graphics2D.getBackground();
clip = graphics2D.getClip();
color = graphics2D.getColor();
composite = graphics2D.getComposite();
font = graphics2D.getFont();
paint = graphics2D.getPaint();
stroke = graphics2D.getStroke();
trans = graphics2D.getTransform();
}
/**
* Retrieves the state into the graphics2D object
*/
public void restore(Graphics2D graphics2D) {
graphics2D.setBackground(background);
graphics2D.setClip(clip);
graphics2D.setColor(color);
graphics2D.setComposite(composite);
graphics2D.setFont(font);
graphics2D.setPaint(paint);
graphics2D.setStroke(stroke);
graphics2D.setTransform(trans);
}
}

View File

@ -21,7 +21,6 @@ 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;
@ -114,20 +113,22 @@ public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor {
return false; return false;
} }
HwmfGraphicsState graphicsState = new HwmfGraphicsState();
graphicsState.backup(graphics);
boolean isClipped = true; boolean isClipped = true;
if (clip == null) { if (clip == null) {
isClipped = false; isClipped = false;
clip = new Insets(0,0,0,0); clip = new Insets(0,0,0,0);
} }
Shape clipOld = graphics.getClip();
if (isClipped) { if (isClipped) {
graphics.clip(anchor); graphics.clip(anchor);
} }
image.draw(graphics, getOuterBounds(anchor, clip)); image.draw(graphics, getOuterBounds(anchor, clip));
graphics.setClip(clipOld); graphicsState.restore(graphics);
return true; return true;
} }
@ -179,4 +180,9 @@ public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor {
public Rectangle2D getNativeBounds() { public Rectangle2D getNativeBounds() {
return image.getBounds(); return image.getBounds();
} }
@Override
public Rectangle2D getBounds() {
return Units.pointsToPixel(image == null ? new Rectangle2D.Double() : image.getBoundsInPoints());
}
} }

View File

@ -64,7 +64,11 @@ public enum HwmfBrushStyle {
/** /**
* Not supported * Not supported
*/ */
BS_MONOPATTERN(0x0009); BS_MONOPATTERN(0x0009),
/**
* (POI arbitrary:) EMF/EMF+ specific value for linear gradient paint
*/
BS_LINEAR_GRADIENT(0x0100);
int flag; int flag;
HwmfBrushStyle(int flag) { HwmfBrushStyle(int flag) {

View File

@ -288,7 +288,7 @@ public class HwmfFont implements FontInfo, GenericRecord {
* For all height comparisons, the font mapper SHOULD find the largest physical * For all height comparisons, the font mapper SHOULD find the largest physical
* font that does not exceed the requested size. * font that does not exceed the requested size.
*/ */
protected int height; protected double height;
/** /**
* A 16-bit signed integer that defines the average width, in logical units, of * A 16-bit signed integer that defines the average width, in logical units, of
@ -433,7 +433,7 @@ public class HwmfFont implements FontInfo, GenericRecord {
facename = "SansSerif"; facename = "SansSerif";
} }
public int getHeight() { public double getHeight() {
return height; return height;
} }

View File

@ -45,7 +45,11 @@ public enum HwmfRegionMode {
/** /**
* The new clipping region is the current path (or the new region). * The new clipping region is the current path (or the new region).
*/ */
RGN_COPY(0x05, HwmfRegionMode::copyOp); RGN_COPY(0x05, HwmfRegionMode::copyOp),
/**
* This is the opposite of {@link #RGN_DIFF}, and only made-up for compatibility with EMF+
*/
RGN_COMPLEMENT(-1, HwmfRegionMode::complementOp);
private final int flag; private final int flag;
private final BiFunction<Shape,Shape,Shape> op; private final BiFunction<Shape,Shape,Shape> op;
@ -125,4 +129,17 @@ public enum HwmfRegionMode {
private static Shape copyOp(final Shape oldClip, final Shape newClip) { private static Shape copyOp(final Shape oldClip, final Shape newClip) {
return (newClip == null || newClip.getBounds2D().isEmpty()) ? null : newClip; return (newClip == null || newClip.getBounds2D().isEmpty()) ? null : newClip;
} }
private static Shape complementOp(final Shape oldClip, final Shape newClip) {
assert(newClip != null);
if (newClip.getBounds2D().isEmpty()) {
return oldClip;
} else if (oldClip == null) {
return newClip;
} else {
Area newArea = new Area(newClip);
newArea.subtract(new Area(oldClip));
return newArea;
}
}
} }

View File

@ -232,7 +232,10 @@ public class HwmfText {
@Override @Override
public Map<String, Supplier<?>> getGenericProperties() { public Map<String, Supplier<?>> getGenericProperties() {
return GenericRecordUtil.getGenericProperties("text", () -> getText(StandardCharsets.US_ASCII)); return GenericRecordUtil.getGenericProperties(
"text", () -> getText(StandardCharsets.US_ASCII),
"reference", () -> reference
);
} }
} }

View File

@ -36,6 +36,7 @@ import java.util.function.Supplier;
import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.common.usermodel.GenericRecord;
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.draw.HwmfGraphicsState;
import org.apache.poi.hwmf.record.HwmfHeader; import org.apache.poi.hwmf.record.HwmfHeader;
import org.apache.poi.hwmf.record.HwmfPlaceableHeader; import org.apache.poi.hwmf.record.HwmfPlaceableHeader;
import org.apache.poi.hwmf.record.HwmfRecord; import org.apache.poi.hwmf.record.HwmfRecord;
@ -127,8 +128,8 @@ public class HwmfPicture implements Iterable<HwmfRecord>, GenericRecord {
} }
public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) { public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) {
final Shape clip = ctx.getClip(); HwmfGraphicsState state = new HwmfGraphicsState();
final AffineTransform at = ctx.getTransform(); state.backup(ctx);
try { try {
Rectangle2D wmfBounds = getBounds(); Rectangle2D wmfBounds = getBounds();
Rectangle2D innerBounds = getInnnerBounds(); Rectangle2D innerBounds = getInnnerBounds();
@ -137,13 +138,10 @@ public class HwmfPicture implements Iterable<HwmfRecord>, GenericRecord {
} }
// scale output bounds to image bounds // scale output bounds to image bounds
ctx.translate(graphicsBounds.getX(), graphicsBounds.getY()); ctx.translate(graphicsBounds.getCenterX(), graphicsBounds.getCenterY());
ctx.scale(graphicsBounds.getWidth()/wmfBounds.getWidth(), graphicsBounds.getHeight()/wmfBounds.getHeight()); ctx.scale(graphicsBounds.getWidth()/innerBounds.getWidth(), graphicsBounds.getHeight()/innerBounds.getHeight());
ctx.translate(-innerBounds.getCenterX(), -innerBounds.getCenterY());
ctx.translate(-wmfBounds.getX(), -wmfBounds.getY());
ctx.translate(innerBounds.getCenterX(), innerBounds.getCenterY());
ctx.scale(wmfBounds.getWidth()/innerBounds.getWidth(), wmfBounds.getHeight()/innerBounds.getHeight());
ctx.translate(-wmfBounds.getCenterX(), -wmfBounds.getCenterY());
HwmfGraphics g = new HwmfGraphics(ctx, innerBounds); HwmfGraphics g = new HwmfGraphics(ctx, innerBounds);
HwmfDrawProperties prop = g.getProperties(); HwmfDrawProperties prop = g.getProperties();
@ -162,8 +160,7 @@ public class HwmfPicture implements Iterable<HwmfRecord>, GenericRecord {
idx++; idx++;
} }
} finally { } finally {
ctx.setTransform(at); state.restore(ctx);
ctx.setClip(clip);
} }
} }
@ -216,17 +213,28 @@ public class HwmfPicture implements Iterable<HwmfRecord>, GenericRecord {
} }
/** /**
* Return the image size in points * Return the image bound in points
* *
* @return the image size in points * @return the image bound in points
*/ */
public Dimension2D getSize() { public Rectangle2D getBoundsInPoints() {
double inch = (placeableHeader == null) ? 1440 : placeableHeader.getUnitsPerInch(); double inch = (placeableHeader == null) ? 1440 : placeableHeader.getUnitsPerInch();
Rectangle2D bounds = getBounds(); Rectangle2D bounds = getBounds();
//coefficient to translate from WMF dpi to 72dpi //coefficient to translate from WMF dpi to 72dpi
double coeff = Units.POINT_DPI/inch; double coeff = Units.POINT_DPI/inch;
return new Dimension2DDouble(bounds.getWidth()*coeff, bounds.getHeight()*coeff); return AffineTransform.getScaleInstance(coeff, coeff).createTransformedShape(bounds).getBounds2D();
}
/**
* Return the image size in points
*
* @return the image size in points
*/
public Dimension2D getSize() {
Rectangle2D bounds = getBoundsInPoints();
return new Dimension2DDouble(bounds.getWidth(), bounds.getHeight());
} }
public Iterable<HwmfEmbedded> getEmbeddings() { public Iterable<HwmfEmbedded> getEmbeddings() {