package org.apache.poi.hemf.draw;
-import java.awt.Shape;
import java.awt.geom.Path2D;
+import java.awt.image.BufferedImage;
+import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle;
import org.apache.poi.hwmf.draw.HwmfDrawProperties;
public class HemfDrawProperties extends HwmfDrawProperties {
/** Path for path bracket operations */
protected Path2D path = null;
protected boolean usePathBracket = false;
+ private EmfPlusHatchStyle emfPlusBrushHatch;
+ private BufferedImage emfPlusImage;
public HemfDrawProperties() {
public HemfDrawProperties(HemfDrawProperties other) {
super(other);
path = (other.path != null) ? (Path2D)other.path.clone() : null;
+ usePathBracket = other.usePathBracket;
+ emfPlusBrushHatch = other.emfPlusBrushHatch;
// TODO: check how to clone
clip = other.clip;
+ emfPlusImage = other.emfPlusImage;
}
/**
public void setUsePathBracket(boolean usePathBracket) {
this.usePathBracket = usePathBracket;
}
+
+ public EmfPlusHatchStyle getEmfPlusBrushHatch() {
+ return emfPlusBrushHatch;
+ }
+
+ public void setEmfPlusBrushHatch(EmfPlusHatchStyle emfPlusBrushHatch) {
+ this.emfPlusBrushHatch = emfPlusBrushHatch;
+ }
+
+ public BufferedImage getEmfPlusImage() {
+ return emfPlusImage;
+ }
+
+ public void setEmfPlusImage(BufferedImage emfPlusImage) {
+ this.emfPlusImage = emfPlusImage;
+ }
}
import java.awt.Color;
import java.awt.Graphics2D;
+import java.awt.Paint;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.function.Consumer;
+import org.apache.poi.hemf.record.emf.HemfComment.EmfComment;
import org.apache.poi.hemf.record.emf.HemfRecord;
+import org.apache.poi.hemf.record.emfplus.HemfPlusRecord;
import org.apache.poi.hwmf.draw.HwmfDrawProperties;
import org.apache.poi.hwmf.draw.HwmfGraphics;
import org.apache.poi.hwmf.record.HwmfColorRef;
public class HemfGraphics extends HwmfGraphics {
+ public enum EmfRenderState {
+ INITIAL,
+ EMF_ONLY,
+ EMFPLUS_ONLY,
+ EMF_DCONTEXT
+ }
+
private static final HwmfColorRef WHITE = new HwmfColorRef(Color.WHITE);
private static final HwmfColorRef LTGRAY = new HwmfColorRef(new Color(0x00C0C0C0));
private static final HwmfColorRef GRAY = new HwmfColorRef(new Color(0x00808080));
private static final HwmfColorRef DKGRAY = new HwmfColorRef(new Color(0x00404040));
private static final HwmfColorRef BLACK = new HwmfColorRef(Color.BLACK);
+ private EmfRenderState renderState = EmfRenderState.INITIAL;
public HemfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) {
super(graphicsCtx,bbox);
: new HemfDrawProperties((HemfDrawProperties)oldProps);
}
+ public EmfRenderState getRenderState() {
+ return renderState;
+ }
+
+ public void setRenderState(EmfRenderState renderState) {
+ this.renderState = renderState;
+ }
+
public void draw(HemfRecord r) {
- r.draw(this);
+ switch (renderState) {
+ case EMF_DCONTEXT:
+ // keep the dcontext state, if the next record is an EMF+ record
+ // only reset it, when we are processing EMF records again
+ if (!(r instanceof EmfComment)) {
+ renderState = EmfRenderState.INITIAL;
+ }
+ r.draw(this);
+ break;
+ case INITIAL:
+ r.draw(this);
+ break;
+ case EMF_ONLY:
+ case EMFPLUS_ONLY:
+ if ((r instanceof EmfComment) == (renderState == EmfRenderState.EMFPLUS_ONLY)) {
+ r.draw(this);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ public void draw(HemfPlusRecord r) {
+ switch (renderState) {
+ case EMFPLUS_ONLY:
+ case EMF_DCONTEXT:
+ case INITIAL:
+ r.draw(this);
+ break;
+ case EMF_ONLY:
+ default:
+ break;
+ }
}
@Internal
* @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry)
*/
public void addObjectTableEntry(HwmfObjectTableEntry entry, int index) {
- if (index < 1) {
- throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index);
- }
-
+ checkTableEntryIndex(index);
objectIndexes.set(index);
objectTable.put(index, entry);
}
+ /**
+ * Gets a record which was registered earliser
+ * @param index the record index
+ * @return the record or {@code null} if it doesn't exist
+ */
+ public HwmfObjectTableEntry getObjectTableEntry(int index) {
+ checkTableEntryIndex(index);
+ return objectTable.get(index);
+ }
+
+ private void checkTableEntryIndex(int index) {
+ if (renderState != EmfRenderState.EMFPLUS_ONLY) {
+ // in EMF the index must > 0
+ if (index < 1) {
+ 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);
+ }
+ }
+ }
+
+
@Override
public void applyObjectTableEntry(int index) {
if ((index & 0x80000000) != 0) {
break;
}
}
+
+ @Override
+ protected Paint getHatchedFill() {
+ // TODO: use EmfPlusHatchBrushData
+ return super.getHatchedFill();
+ }
}
import java.util.List;
import java.util.function.Supplier;
+import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.record.emfplus.HemfPlusRecord;
import org.apache.poi.hemf.record.emfplus.HemfPlusRecordIterator;
import org.apache.poi.hwmf.usermodel.HwmfPicture;
return data;
}
+ @Override
+ public void draw(HemfGraphics ctx) {
+ if (data instanceof EmfCommentDataPlus) {
+ if (ctx.getRenderState() == HemfGraphics.EmfRenderState.INITIAL) {
+ ctx.setRenderState(HemfGraphics.EmfRenderState.EMFPLUS_ONLY);
+ }
+
+ ((EmfCommentDataPlus)data).draw(ctx);
+ }
+ }
+
@Override
public String toString() {
return "{ data: "+data+" }";
public List<HemfPlusRecord> getRecords() {
return Collections.unmodifiableList(records);
}
+
+ public void draw(HemfGraphics ctx) {
+ records.forEach(ctx::draw);
+ }
}
public static class EmfCommentDataBeginGroup implements EmfCommentData {
import static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds;
import java.awt.Shape;
+import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Dimension2D;
import java.awt.geom.Path2D;
import org.apache.poi.hwmf.draw.HwmfGraphics.FillDrawStyle;
import org.apache.poi.hwmf.record.HwmfDraw;
import org.apache.poi.hwmf.record.HwmfDraw.WmfSelectObject;
+import org.apache.poi.util.Internal;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
ctx.draw((path) -> path.append(pi, true), fillDrawStyle);
}
+
+
+ @Internal
+ public static String xformToString(AffineTransform xForm) {
+ return (xForm == null) ? "null" :
+ "{ scaleX: "+xForm.getScaleX()+
+ ", shearX: "+xForm.getShearX()+
+ ", transX: "+xForm.getTranslateX()+
+ ", scaleY: "+xForm.getScaleY()+
+ ", shearY: "+xForm.getShearY()+
+ ", transY: "+xForm.getTranslateY()+" }";
+ }
}
import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL;
import static org.apache.poi.hemf.record.emf.HemfDraw.readRectL;
+import static org.apache.poi.hemf.record.emf.HemfDraw.xformToString;
import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE;
import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString;
import static org.apache.poi.hwmf.record.HwmfDraw.pointToString;
public String toString() {
return
"{ bounds: "+boundsToString(bounds)+
- ", xFormSrc: { scaleX: "+xFormSrc.getScaleX()+", shearX: "+xFormSrc.getShearX()+", transX: "+xFormSrc.getTranslateX()+", scaleY: "+xFormSrc.getScaleY()+", shearY: "+xFormSrc.getShearY()+", transY: "+xFormSrc.getTranslateY()+" }"+
+ ", xFormSrc: " + xformToString(xFormSrc) +
", bkColorSrc: "+bkColorSrc+
","+super.toString().substring(1);
}
xform.setTransform(m00, m10, m01, m11, m02, m12);
+ if (xform.isIdentity()) {
+ xform.setToIdentity();
+ }
+
return 6 * LittleEndian.INT_SIZE;
}
package org.apache.poi.hemf.record.emf;
import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL;
+import static org.apache.poi.hemf.record.emf.HemfDraw.xformToString;
import static org.apache.poi.hemf.record.emf.HemfFill.readBitmap;
import static org.apache.poi.hemf.record.emf.HemfFill.readXForm;
import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE;
@Override
public String toString() {
- return
- "{ xForm: " +
- "{ scaleX: "+xForm.getScaleX()+
- ", shearX: "+xForm.getShearX()+
- ", transX: "+xForm.getTranslateX()+
- ", scaleY: "+xForm.getScaleY()+
- ", shearY: "+xForm.getShearY()+
- ", transY: "+xForm.getTranslateY()+" } }";
+ return "{ xForm: " + xformToString(xForm)+" }";
}
}
wsTrans.translate(-emfBounds.getHeight(), emfBounds.getHeight());
}
} else {
- wsTrans = adaptXForm(ctx.getTransform());
+ wsTrans = adaptXForm(xForm, ctx.getTransform());
}
tx = ctx.getTransform();
break;
case MWT_RIGHTMULTIPLY:
tx = ctx.getTransform();
- tx.preConcatenate(adaptXForm(tx));
+ tx.preConcatenate(adaptXForm(xForm, tx));
break;
case MWT_IDENTITY:
ctx.updateWindowMapMode();
case MWT_SET:
ctx.updateWindowMapMode();
tx = ctx.getTransform();
- tx.concatenate(adaptXForm(tx));
+ tx.concatenate(adaptXForm(xForm, tx));
break;
}
ctx.setTransform(tx);
}
- /**
- * adapt xform depending on the base transformation (... experimental ...)
- */
- private AffineTransform adaptXForm(AffineTransform other) {
- // normalize signed zero
- Function<Double,Double> nn = (d) -> (d == 0. ? 0. : d);
- double yDiff = Math.signum(nn.apply(xForm.getTranslateY())) == Math.signum(nn.apply(other.getTranslateY())) ? 1. : -1.;
- double xDiff = Math.signum(nn.apply(xForm.getTranslateX())) == Math.signum(nn.apply(other.getTranslateX())) ? 1. : -1.;
- return new AffineTransform(
- xForm.getScaleX() == 0 ? 1. : xForm.getScaleX(),
- yDiff * xForm.getShearY(),
- xDiff * xForm.getShearX(),
- xForm.getScaleY() == 0. ? 1. : xForm.getScaleY(),
- xForm.getTranslateX(),
- xForm.getTranslateY()
- );
- }
-
@Override
public String toString() {
return
- "{ xForm: " +
- "{ scaleX: "+xForm.getScaleX()+
- ", shearX: "+xForm.getShearX()+
- ", transX: "+xForm.getTranslateX()+
- ", scaleY: "+xForm.getScaleY()+
- ", shearY: "+xForm.getShearY()+
- ", transY: "+xForm.getTranslateY()+" }"+
+ "{ xForm: " + xformToString(xForm) +
", modifyWorldTransformMode: '"+modifyWorldTransformMode+"' }";
}
}
"}";
}
}
+
+
+ /**
+ * adapt xform depending on the base transformation (... experimental ...)
+ */
+ public static AffineTransform adaptXForm(AffineTransform xForm, AffineTransform other) {
+ // normalize signed zero
+ Function<Double,Double> nn = (d) -> (d == 0. ? 0. : d);
+ double yDiff = Math.signum(nn.apply(xForm.getTranslateY())) == Math.signum(nn.apply(other.getTranslateY())) ? 1. : -1.;
+ double xDiff = Math.signum(nn.apply(xForm.getTranslateX())) == Math.signum(nn.apply(other.getTranslateX())) ? 1. : -1.;
+ return new AffineTransform(
+ xForm.getScaleX() == 0 ? 1. : xForm.getScaleX(),
+ yDiff * xForm.getShearY(),
+ xDiff * xForm.getShearX(),
+ xForm.getScaleY() == 0. ? 1. : xForm.getScaleY(),
+ xForm.getTranslateX(),
+ xForm.getTranslateY()
+ );
+ }
}
package org.apache.poi.hemf.record.emfplus;
+import static java.util.stream.Collectors.joining;
+import static org.apache.poi.hemf.record.emf.HemfDraw.xformToString;
import static org.apache.poi.hemf.record.emf.HemfFill.readXForm;
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readARGB;
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readPointF;
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF;
+import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString;
+import static org.apache.poi.hwmf.record.HwmfDraw.pointToString;
import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
+import java.util.stream.Stream;
+import org.apache.poi.hemf.draw.HemfDrawProperties;
+import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion;
import org.apache.poi.hemf.record.emfplus.HemfPlusImage.EmfPlusImage;
import org.apache.poi.hemf.record.emfplus.HemfPlusImage.EmfPlusWrapMode;
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.HemfPlusPath.EmfPlusPath;
+import org.apache.poi.hwmf.record.HwmfBrushStyle;
+import org.apache.poi.hwmf.record.HwmfColorRef;
+import org.apache.poi.hwmf.record.HwmfDraw;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
+import org.apache.poi.util.Internal;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
}
public enum EmfPlusHatchStyle {
+ /** Specifies equally spaced horizontal lines. */
HORIZONTAL(0X00000000),
+ /** Specifies equally spaced vertical lines. */
VERTICAL(0X00000001),
+ /** Specifies lines on a diagonal from upper left to lower right. */
FORWARD_DIAGONAL(0X00000002),
+ /** Specifies lines on a diagonal from upper right to lower left. */
BACKWARD_DIAGONAL(0X00000003),
+ /** Specifies crossing horizontal and vertical lines. */
LARGE_GRID(0X00000004),
+ /** Specifies crossing forward diagonal and backward diagonal lines with anti-aliasing. */
DIAGONAL_CROSS(0X00000005),
+ /** Specifies a 5-percent hatch, which is the ratio of foreground color to background color equal to 5:100. */
PERCENT_05(0X00000006),
+ /** Specifies a 10-percent hatch, which is the ratio of foreground color to background color equal to 10:100. */
PERCENT_10(0X00000007),
+ /** Specifies a 20-percent hatch, which is the ratio of foreground color to background color equal to 20:100. */
PERCENT_20(0X00000008),
+ /** Specifies a 25-percent hatch, which is the ratio of foreground color to background color equal to 25:100. */
PERCENT_25(0X00000009),
+ /** Specifies a 30-percent hatch, which is the ratio of foreground color to background color equal to 30:100. */
PERCENT_30(0X0000000A),
+ /** Specifies a 40-percent hatch, which is the ratio of foreground color to background color equal to 40:100. */
PERCENT_40(0X0000000B),
+ /** Specifies a 50-percent hatch, which is the ratio of foreground color to background color equal to 50:100. */
PERCENT_50(0X0000000C),
+ /** Specifies a 60-percent hatch, which is the ratio of foreground color to background color equal to 60:100. */
PERCENT_60(0X0000000D),
+ /** Specifies a 70-percent hatch, which is the ratio of foreground color to background color equal to 70:100. */
PERCENT_70(0X0000000E),
+ /** Specifies a 75-percent hatch, which is the ratio of foreground color to background color equal to 75:100. */
PERCENT_75(0X0000000F),
+ /** Specifies an 80-percent hatch, which is the ratio of foreground color to background color equal to 80:100. */
PERCENT_80(0X00000010),
+ /** Specifies a 90-percent hatch, which is the ratio of foreground color to background color equal to 90:100. */
PERCENT_90(0X00000011),
+ /**
+ * Specifies diagonal lines that slant to the right from top to bottom points with no anti-aliasing.
+ * They are spaced 50 percent further apart than lines in the FORWARD_DIAGONAL pattern
+ */
LIGHT_DOWNWARD_DIAGONAL(0X00000012),
+ /**
+ * Specifies diagonal lines that slant to the left from top to bottom points with no anti-aliasing.
+ * They are spaced 50 percent further apart than lines in the BACKWARD_DIAGONAL pattern.
+ */
LIGHT_UPWARD_DIAGONAL(0X00000013),
+ /**
+ * Specifies diagonal lines that slant to the right from top to bottom points with no anti-aliasing.
+ * They are spaced 50 percent closer and are twice the width of lines in the FORWARD_DIAGONAL pattern.
+ */
DARK_DOWNWARD_DIAGONAL(0X00000014),
+ /**
+ * Specifies diagonal lines that slant to the left from top to bottom points with no anti-aliasing.
+ * They are spaced 50 percent closer and are twice the width of lines in the BACKWARD_DIAGONAL pattern.
+ */
DARK_UPWARD_DIAGONAL(0X00000015),
+ /**
+ * Specifies diagonal lines that slant to the right from top to bottom points with no anti-aliasing.
+ * They have the same spacing between lines in WIDE_DOWNWARD_DIAGONAL pattern and FORWARD_DIAGONAL pattern,
+ * but WIDE_DOWNWARD_DIAGONAL has the triple line width of FORWARD_DIAGONAL.
+ */
WIDE_DOWNWARD_DIAGONAL(0X00000016),
+ /**
+ * Specifies diagonal lines that slant to the left from top to bottom points with no anti-aliasing.
+ * They have the same spacing between lines in WIDE_UPWARD_DIAGONAL pattern and BACKWARD_DIAGONAL pattern,
+ * but WIDE_UPWARD_DIAGONAL has the triple line width of WIDE_UPWARD_DIAGONAL.
+ */
WIDE_UPWARD_DIAGONAL(0X00000017),
+ /** Specifies vertical lines that are spaced 50 percent closer together than lines in the VERTICAL pattern. */
LIGHT_VERTICAL(0X00000018),
+ /** Specifies horizontal lines that are spaced 50 percent closer than lines in the HORIZONTAL pattern. */
LIGHT_HORIZONTAL(0X00000019),
+ /**
+ * Specifies vertical lines that are spaced 75 percent closer than lines in the VERTICAL pattern;
+ * or 25 percent closer than lines in the LIGHT_VERTICAL pattern.
+ */
NARROW_VERTICAL(0X0000001A),
+ /**
+ * Specifies horizontal lines that are spaced 75 percent closer than lines in the HORIZONTAL pattern;
+ * or 25 percent closer than lines in the LIGHT_HORIZONTAL pattern.
+ */
NARROW_HORIZONTAL(0X0000001B),
+ /** Specifies lines that are spaced 50 percent closer than lines in the VERTICAL pattern. */
DARK_VERTICAL(0X0000001C),
+ /** Specifies lines that are spaced 50 percent closer than lines in the HORIZONTAL pattern. */
DARK_HORIZONTAL(0X0000001D),
+ /** Specifies dashed diagonal lines that slant to the right from top to bottom points. */
DASHED_DOWNWARD_DIAGONAL(0X0000001E),
+ /** Specifies dashed diagonal lines that slant to the left from top to bottom points. */
DASHED_UPWARD_DIAGONAL(0X0000001F),
+ /** Specifies dashed horizontal lines. */
DASHED_HORIZONTAL(0X00000020),
+ /** Specifies dashed vertical lines. */
DASHED_VERTICAL(0X00000021),
+ /** Specifies a pattern of lines that has the appearance of confetti. */
SMALL_CONFETTI(0X00000022),
+ /**
+ * Specifies a pattern of lines that has the appearance of confetti, and is composed of larger pieces
+ * than the SMALL_CONFETTI pattern.
+ */
LARGE_CONFETTI(0X00000023),
+ /** Specifies horizontal lines that are composed of zigzags. */
ZIGZAG(0X00000024),
+ /** Specifies horizontal lines that are composed of tildes. */
WAVE(0X00000025),
+ /**
+ * Specifies a pattern of lines that has the appearance of layered bricks that slant to the left from
+ * top to bottom points.
+ */
DIAGONAL_BRICK(0X00000026),
+ /** Specifies a pattern of lines that has the appearance of horizontally layered bricks. */
HORIZONTAL_BRICK(0X00000027),
+ /** Specifies a pattern of lines that has the appearance of a woven material. */
WEAVE(0X00000028),
+ /** Specifies a pattern of lines that has the appearance of a plaid material. */
PLAID(0X00000029),
+ /** Specifies a pattern of lines that has the appearance of divots. */
DIVOT(0X0000002A),
+ /** Specifies crossing horizontal and vertical lines, each of which is composed of dots. */
DOTTED_GRID(0X0000002B),
+ /** Specifies crossing forward and backward diagonal lines, each of which is composed of dots. */
DOTTED_DIAMOND(0X0000002C),
+ /**
+ * Specifies a pattern of lines that has the appearance of diagonally layered
+ * shingles that slant to the right from top to bottom points.
+ */
SHINGLE(0X0000002D),
+ /** Specifies a pattern of lines that has the appearance of a trellis. */
TRELLIS(0X0000002E),
+ /** Specifies a pattern of lines that has the appearance of spheres laid adjacent to each other. */
SPHERE(0X0000002F),
+ /** Specifies crossing horizontal and vertical lines that are spaced 50 percent closer together than LARGE_GRID. */
SMALL_GRID(0X00000030),
+ /** Specifies a pattern of lines that has the appearance of a checkerboard. */
SMALL_CHECKER_BOARD(0X00000031),
+ /**
+ * Specifies a pattern of lines that has the appearance of a checkerboard, with squares that are twice the
+ * size of the squares in the SMALL_CHECKER_BOARD pattern.
+ */
LARGE_CHECKER_BOARD(0X00000032),
+ /** Specifies crossing forward and backward diagonal lines; the lines are not anti-aliased. */
OUTLINED_DIAMOND(0X00000033),
+ /** Specifies a pattern of lines that has the appearance of a checkerboard placed diagonally. */
SOLID_DIAMOND(0X00000034)
;
BitField DO_NOT_TRANSFORM = BitFieldFactory.getInstance(0x00000100);
long init(LittleEndianInputStream leis, long dataSize) throws IOException;
+
+ void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData);
}
/** The EmfPlusBrush object specifies a graphics brush for filling regions. */
public static class EmfPlusBrush implements EmfPlusObjectData {
- private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion();
+ private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion();
private EmfPlusBrushType brushType;
private EmfPlusBrushData brushData;
@Override
public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException {
- long size = version.init(leis);
+ long size = graphicsVersion.init(leis);
brushType = EmfPlusBrushType.valueOf(leis.readInt());
size += LittleEndianConsts.INT_SIZE;
return size;
}
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+ brushData.applyObject(ctx, continuedObjectData);
+ }
+
+ @Override
+ public EmfPlusGraphicsVersion getGraphicsVersion() {
+ return graphicsVersion;
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ brushType: '"+brushType+"'" +
+ ", brushData: "+brushData+" }";
+ }
}
/** The EmfPlusSolidBrushData object specifies a solid color for a graphics brush. */
solidColor = readARGB(leis.readInt());
return LittleEndianConsts.INT_SIZE;
}
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+ HemfDrawProperties prop = ctx.getProperties();
+ prop.setBackgroundColor(new HwmfColorRef(solidColor));
+ }
+
+ @Override
+ public String toString() {
+ return "{ solidColor: "+new HwmfColorRef(solidColor)+" }";
+ }
}
backColor = readARGB(leis.readInt());
return 3*LittleEndianConsts.INT_SIZE;
}
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+ HemfDrawProperties prop = ctx.getProperties();
+ prop.setBrushColor(new HwmfColorRef(foreColor));
+ prop.setBackgroundColor(new HwmfColorRef(backColor));
+ prop.setEmfPlusBrushHatch(style);
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ style: '"+style+"'" +
+ ", foreColor: "+new HwmfColorRef(foreColor) +
+ ", backColor: "+new HwmfColorRef(backColor) + " }";
+ }
}
/** The EmfPlusLinearGradientBrushData object specifies a linear gradient for a graphics brush. */
return size;
}
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+ HemfDrawProperties prop = ctx.getProperties();
+ // TODO: implement
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ flags: "+dataFlags+
+ ", wrapMode: '"+wrapMode+"'"+
+ ", rect: "+boundsToString(rect)+
+ ", startColor: "+new HwmfColorRef(startColor)+
+ ", endColor: "+new HwmfColorRef(endColor)+
+ ", transform: "+xformToString(transform)+
+ ", positions: "+ Arrays.toString(positions)+
+ ", blendColors: "+ colorsToString(blendColors)+
+ ", positionsV: "+ Arrays.toString(positionsV)+
+ ", blendFactorsV: "+ Arrays.toString(blendFactorsV)+
+ ", positionsH: "+ Arrays.toString(positionsH)+
+ ", blendFactorsH: "+ Arrays.toString(blendFactorsH)+
+ "}";
+ }
+
+
}
/** The EmfPlusPathGradientBrushData object specifies a path gradient for a graphics brush. */
return size;
}
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ flags: "+dataFlags+
+ ", wrapMode: '"+wrapMode+"'"+
+ ", centerColor: "+new HwmfColorRef(centerColor)+
+ ", centerPoint: "+pointToString(centerPoint)+
+ ", surroundingColor: "+colorsToString(surroundingColor)+
+ ", boundaryPath: "+(boundaryPath == null ? "null" : boundaryPath)+
+ ", boundaryPoints: "+pointsToString(boundaryPoints)+
+ ", transform: "+xformToString(transform)+
+ ", positions: "+Arrays.toString(positions)+
+ ", blendColors: "+colorsToString(blendColors)+
+ ", blendFactorsH: "+Arrays.toString(blendFactorsH)+
+ ", focusScaleX: "+focusScaleX+
+ ", focusScaleY: "+focusScaleY+
+ "}"
+ ;
+ }
}
/** The EmfPlusTextureBrushData object specifies a texture image for a graphics brush. */
return size;
}
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+ image.applyObject(ctx, continuedObjectData);
+ HemfDrawProperties prop = ctx.getProperties();
+ prop.setBrushBitmap(prop.getEmfPlusImage());
+ prop.setBrushStyle(HwmfBrushStyle.BS_PATTERN);
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ flags: "+dataFlags+
+ ", wrapMode: '"+wrapMode+"'"+
+ ", transform: "+xformToString(transform)+
+ ", image: "+image+
+ "]";
+ }
}
private static int readPositions(LittleEndianInputStream leis, Consumer<double[]> pos) {
facs.accept(factors);
return size + factors.length * LittleEndianConsts.INT_SIZE;
}
+
+ @Internal
+ public static String colorsToString(Color[] colors) {
+ return (colors == null ? "null" :
+ Stream.of(colors).map(HwmfColorRef::new).map(Object::toString).
+ collect(joining(",", "{", "}")));
+ }
+
+ @Internal
+ public static String pointsToString(Point2D[] points) {
+ return (points == null ? "null" :
+ Stream.of(points).map(HwmfDraw::pointToString).
+ collect(joining(",", "{", "}")));
+ }
}
package org.apache.poi.hemf.record.emfplus;
+import static java.util.stream.Collectors.joining;
+import static org.apache.poi.hemf.record.emf.HemfDraw.xformToString;
+import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString;
+import static org.apache.poi.hwmf.record.HwmfDraw.pointToString;
+
import java.awt.Color;
import java.awt.geom.AffineTransform;
+import java.awt.geom.Area;
+import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import org.apache.commons.math3.linear.LUDecomposition;
import org.apache.commons.math3.linear.MatrixUtils;
import org.apache.commons.math3.linear.RealMatrix;
+import org.apache.poi.hemf.draw.HemfDrawProperties;
+import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.record.emf.HemfFill;
+import org.apache.poi.hemf.record.emfplus.HemfPlusImage.EmfPlusImage;
import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId;
+import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObject;
+import org.apache.poi.hwmf.record.HwmfBrushStyle;
+import org.apache.poi.hwmf.record.HwmfColorRef;
+import org.apache.poi.hwmf.record.HwmfDraw;
+import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode;
+import org.apache.poi.hwmf.record.HwmfTernaryRasterOp;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.IOUtils;
}
}
+ public interface EmfPlusSolidColor {
+ /**
+ * If set, brushId specifies a color as an EmfPlusARGB object.
+ * If clear, brushId contains the index of an EmfPlusBrush object in the EMF+ Object Table.
+ */
+ BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000);
+
+ int getFlags();
+
+ int getBrushIdValue();
+
+ default boolean isSolidColor() {
+ return SOLID_COLOR.isSet(getFlags());
+ }
+
+ default int getBrushId() {
+ return (isSolidColor()) ? -1 : getBrushIdValue();
+ }
+
+ default Color getSolidColor() {
+ return (isSolidColor()) ? readARGB(getBrushIdValue()) : null;
+ }
+
+ default void applyColor(HemfGraphics ctx) {
+ HemfDrawProperties prop = ctx.getProperties();
+ if (isSolidColor()) {
+ prop.setBrushStyle(HwmfBrushStyle.BS_SOLID);
+ prop.setBrushColor(new HwmfColorRef(getSolidColor()));
+ } else {
+ ctx.applyObjectTableEntry(getBrushId());
+ }
+ }
+ }
/**
* The EmfPlusDrawPath record specifies drawing a graphics path
*/
- public static class EmfPlusDrawPath implements HemfPlusRecord {
+ public static class EmfPlusDrawPath implements HemfPlusRecord, EmfPlusObjectId {
private int flags;
private int penId;
return LittleEndianConsts.INT_SIZE;
}
+
+ @Override
+ public void draw(HemfGraphics ctx) {
+ ctx.applyObjectTableEntry(penId);
+ ctx.applyObjectTableEntry(getObjectId());
+
+ HemfDrawProperties prop = ctx.getProperties();
+ final Path2D path = prop.getPath();
+ if (path != null) {
+ ctx.draw(path);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ flags: "+flags+
+ ", penId: "+penId+" }";
+ }
}
/**
* The EmfPlusFillRects record specifies filling the interiors of a series of rectangles.
*/
- public static class EmfPlusFillRects implements HemfPlusRecord, EmfPlusCompressed {
- /**
- * If set, brushId specifies a color as an EmfPlusARGB object.
- * If clear, brushId contains the index of an EmfPlusBrush object in the EMF+ Object Table.
- */
- private static final BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000);
-
+ public static class EmfPlusFillRects implements HemfPlusRecord, EmfPlusCompressed, EmfPlusSolidColor {
private int flags;
private int brushId;
private final ArrayList<Rectangle2D> rectData = new ArrayList<>();
return size;
}
+
+ @Override
+ public void draw(HemfGraphics ctx) {
+ applyColor(ctx);
+
+ Area area = new Area();
+ rectData.stream().map(Area::new).forEach(area::add);
+ ctx.fill(area);
+ }
+
+ @Override
+ public int getBrushIdValue() {
+ return brushId;
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ flags: "+flags+
+ ", brushId: "+brushId+
+ ", rectData: "+rectData.stream().map(HwmfDraw::boundsToString).collect(joining(",", "{", "}"))+
+ "}";
+ }
}
public static class EmfPlusDrawImagePoints implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed, EmfPlusRelativePosition {
return size;
}
+
+ @Override
+ public void draw(HemfGraphics ctx) {
+ HemfDrawProperties prop = ctx.getProperties();
+
+ ctx.applyObjectTableEntry(imageAttributesID);
+ ctx.applyObjectTableEntry(getObjectId());
+
+ AffineTransform txSaved = ctx.getTransform(), tx = new AffineTransform(txSaved);
+ try {
+ tx.concatenate(trans);
+ ctx.setTransform(tx);
+
+ EmfPlusObject imgObj = (EmfPlusObject)ctx.getObjectTableEntry(getObjectId());
+ EmfPlusImage img = imgObj.getObjectData();
+ Rectangle2D srcBounds = img.getBounds(imgObj.getContinuedObject());
+ BufferedImage bi = prop.getEmfPlusImage();
+
+ prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY);
+ prop.setBkMode(HwmfBkMode.TRANSPARENT);
+
+ // the buffered image might be rescaled, so we need to calculate a new src rect to take
+ // the image data from
+ AffineTransform srcTx = new AffineTransform();
+ srcTx.translate(-srcBounds.getX(), srcBounds.getY());
+ srcTx.scale(bi.getWidth()/srcBounds.getWidth(), bi.getHeight()/srcBounds.getHeight());
+ srcTx.translate(bi.getMinX(), bi.getMinY());
+
+ Rectangle2D biRect = srcTx.createTransformedShape(srcRect).getBounds2D();
+
+ // TODO: handle srcUnit
+ Rectangle2D destRect = new Rectangle2D.Double(0, 0, biRect.getWidth(), biRect.getHeight());
+ ctx.drawImage(bi, srcRect, destRect);
+ } finally {
+ ctx.setTransform(txSaved);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ flags: "+flags+
+ ", imageAttributesID: "+imageAttributesID+
+ ", srcUnit: '"+srcUnit+"'"+
+ ", srcRect: "+boundsToString(srcRect)+
+ ", upperLeft: "+pointToString(upperLeft)+
+ ", lowerLeft: "+pointToString(lowerLeft)+
+ ", lowerRight: "+pointToString(lowerRight)+
+ ", transform: "+xformToString(trans)+
+ "}"
+ ;
+ }
}
/** The EmfPlusDrawImage record specifies drawing a scaled image. */
return size;
}
+
+ @Override
+ public void draw(HemfGraphics ctx) {
+ ctx.applyObjectTableEntry(imageAttributesID);
+ ctx.applyObjectTableEntry(getObjectId());
+
+ HemfDrawProperties prop = ctx.getProperties();
+ prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY);
+ prop.setBkMode(HwmfBkMode.TRANSPARENT);
+
+ ctx.drawImage(prop.getEmfPlusImage(), srcRect, rectData);
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ flags: "+flags+
+ ", imageAttributesID: "+imageAttributesID+
+ ", srcUnit: '"+srcUnit+"'"+
+ ", srcRect: "+boundsToString(srcRect)+
+ ", rectData: "+boundsToString(rectData)+
+ "}"
+ ;
+ }
}
/** The EmfPlusFillRegion record specifies filling the interior of a graphics region. */
- public static class EmfPlusFillRegion implements HemfPlusRecord {
- private static final BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000);
-
+ public static class EmfPlusFillRegion implements HemfPlusRecord, EmfPlusSolidColor, EmfPlusObjectId {
private int flags;
private int brushId;
return flags;
}
- public boolean isSolidColor() {
- return SOLID_COLOR.isSet(getFlags());
- }
-
- public int getBrushId() {
- return (isSolidColor()) ? -1 : brushId;
- }
-
- public Color getSolidColor() {
- return (isSolidColor()) ? readARGB(brushId) : null;
+ @Override
+ public int getBrushIdValue() {
+ return brushId;
}
@Override
return LittleEndianConsts.INT_SIZE;
}
+
+ @Override
+ public void draw(HemfGraphics ctx) {
+ applyColor(ctx);
+ ctx.applyObjectTableEntry(getObjectId());
+ HemfDrawProperties prop = ctx.getProperties();
+ ctx.fill(prop.getPath());
+ }
+
+ @Override
+ public String toString() {
+ return
+ "{ flags: "+flags+
+ ", brushId: "+brushId+" }";
+ }
}
/** The EmfPlusFillPath record specifies filling the interior of a graphics path. */
}
/** The EmfPlusDrawDriverString record specifies text output with character positions. */
- public static class EmfPlusDrawDriverString implements HemfPlusRecord, EmfPlusObjectId {
- /**
- * If set, brushId specifies a color as an EmfPlusARGB object.
- * If clear, brushId contains the index of an EmfPlusBrush object in the EMF+ Object Table.
- */
- private static final BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000);
-
+ public static class EmfPlusDrawDriverString implements HemfPlusRecord, EmfPlusObjectId, EmfPlusSolidColor {
/**
* If set, the positions of character glyphs SHOULD be specified in a character map lookup table.
* If clear, the glyph positions SHOULD be obtained from an array of coordinates.
return flags;
}
+ @Override
+ public int getBrushIdValue() {
+ return brushId;
+ }
+
@Override
public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
this.flags = flags;
}
static Color readARGB(int argb) {
- return new Color( (argb >>> 8) & 0xFF, (argb >>> 16) & 0xFF, (argb >>> 24) & 0xFF, argb & 0xFF);
+ return new Color((argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, (argb >>> 24) & 0xFF);
}
}
package org.apache.poi.hemf.record.emfplus;
import java.io.IOException;
+import java.util.List;
+import org.apache.poi.hemf.draw.HemfGraphics;
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.HemfPlusObject.EmfPlusObjectData;
private static final BitField STRIKEOUT = BitFieldFactory.getInstance(0x00000008);
- private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion();
+ private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion();
private double emSize;
private EmfPlusUnitType sizeUnit;
private int styleFlags;
public long init(LittleEndianInputStream leis, long dataSize, HemfPlusObject.EmfPlusObjectType objectType, int flags) throws IOException {
// An EmfPlusGraphicsVersion object that specifies the version of operating system graphics that was used
// to create this object.
- long size = version.init(leis);
+ long size = graphicsVersion.init(leis);
// A 32-bit floating-point value that specifies the em size of the font in units specified by the SizeUnit field.
emSize = leis.readFloat();
return size;
}
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+
+ }
+
+ @Override
+ public EmfPlusGraphicsVersion getGraphicsVersion() {
+ return graphicsVersion;
+ }
}
}
import java.io.IOException;
+import org.apache.poi.hemf.draw.HemfGraphics;
+import org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.Internal;
return version;
}
+ /**
+ * If set, this flag indicates that this metafile is "dual-mode", which means that it contains two sets of records,
+ * each of which completely specifies the graphics content. If clear, the graphics content is specified by EMF+
+ * records, and possibly EMF records that are preceded by an EmfPlusGetDC record. If this flag is set, EMF records
+ * alone SHOULD suffice to define the graphics content. Note that whether the "dual-mode" flag is set or not, some
+ * EMF records are always present, namely EMF control records and the EMF records that contain EMF+ records.
+ *
+ * @return {@code true} if dual-mode is enabled
+ */
+ public boolean isEmfPlusDualMode() {
+ return (emfPlusFlags & 1) == 1;
+ }
+
public long getEmfPlusFlags() {
return emfPlusFlags;
}
return logicalDpiY;
}
+ @Override
+ public void draw(HemfGraphics ctx) {
+ // currently EMF is better supported than EMF+ ... so if there's a complete set of EMF records available,
+ // disable EMF+ rendering for now
+ ctx.setRenderState(isEmfPlusDualMode() ? EmfRenderState.EMF_ONLY : EmfRenderState.EMFPLUS_ONLY);
+ }
+
@Override
public String toString() {
return "HemfPlusHeader{" +
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readARGB;
import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.Transparency;
+import java.awt.color.ColorSpace;
+import java.awt.geom.Dimension2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.ComponentColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.PixelInterleavedSampleModel;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.util.List;
+import java.util.function.BiConsumer;
+import javax.imageio.ImageIO;
+
+import org.apache.poi.hemf.draw.HemfDrawProperties;
+import org.apache.poi.hemf.draw.HemfGraphics;
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.EmfPlusObjectType;
+import org.apache.poi.hemf.usermodel.HemfPicture;
+import org.apache.poi.hwmf.usermodel.HwmfPicture;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
+import org.apache.poi.util.Units;
public class HemfPlusImage {
+ /** Maximum image dimension for converting embedded metafiles */
+ private static final int MAX_IMAGE_SIZE = 1500;
+
/** The ImageDataType enumeration defines types of image data formats. */
public enum EmfPlusImageDataType {
/** The type of image is not known. */
leis.mark(LittleEndianConsts.INT_SIZE);
long size = graphicsVersion.init(leis);
- if (graphicsVersion.getGraphicsVersion() == null || graphicsVersion.getMetafileSignature() != 0xDBC01) {
+ if (isContinuedRecord()) {
// CONTINUABLE is not always correctly set, so we check the version field if this record is continued
imageDataType = EmfPlusImageDataType.CONTINUED;
leis.reset();
return size + fileSize;
}
+
+ @Override
+ public EmfPlusGraphicsVersion getGraphicsVersion() {
+ return graphicsVersion;
+ }
+
+ public Rectangle2D getBounds(List<? extends EmfPlusObjectData> continuedObjectData) {
+ try {
+ switch (getImageDataType()) {
+ case BITMAP:
+ if (getBitmapType() == EmfPlusBitmapDataType.PIXEL) {
+ return new Rectangle2D.Double(0, 0, bitmapWidth, bitmapHeight);
+ } else {
+ BufferedImage bi = ImageIO.read(new ByteArrayInputStream(getRawData(continuedObjectData)));
+ return new Rectangle2D.Double(bi.getMinX(), bi.getMinY(), bi.getWidth(), bi.getHeight());
+ }
+ case METAFILE:
+ ByteArrayInputStream bis = new ByteArrayInputStream(getRawData(continuedObjectData));
+ switch (getMetafileType()) {
+ case Wmf:
+ case WmfPlaceable:
+ HwmfPicture wmf = new HwmfPicture(bis);
+ return wmf.getBounds();
+ case Emf:
+ case EmfPlusDual:
+ case EmfPlusOnly:
+ HemfPicture emf = new HemfPicture(bis);
+ return emf.getBounds();
+ }
+ break;
+ default:
+ break;
+ }
+ } catch (Exception ignored) {
+ }
+ return new Rectangle2D.Double(1,1,1,1);
+ }
+
+ public byte[] getRawData(List<? extends EmfPlusObjectData> continuedObjectData) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try {
+ bos.write(getImageData());
+ if (continuedObjectData != null) {
+ for (EmfPlusObjectData od : continuedObjectData) {
+ bos.write(((EmfPlusImage)od).getImageData());
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return bos.toByteArray();
+ }
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+ HemfDrawProperties prop = ctx.getProperties();
+ BufferedImage bi = readImage(getRawData(continuedObjectData));
+ prop.setEmfPlusImage(bi);
+ }
+
+ /**
+ * Converts the gdi pixel data to a buffered image
+ * @param data the image data of all EmfPlusImage parts
+ * @return the BufferedImage
+ */
+ public BufferedImage readGDIImage(final byte[] data) {
+ if (getImageDataType() != EmfPlusImageDataType.BITMAP || getBitmapType() != EmfPlusBitmapDataType.PIXEL) {
+ throw new RuntimeException("image data is not a GDI image");
+ }
+
+ final int width = getBitmapWidth();
+ final int height = getBitmapHeight();
+ final int stride = getBitmapStride();
+ final EmfPlusPixelFormat pf = getPixelFormat();
+
+ int[] nBits, bOffs;
+ switch (pf) {
+ case ARGB_32BPP:
+ nBits = new int[]{8, 8, 8, 8};
+ bOffs = new int[]{2, 1, 0, 3};
+ break;
+ case RGB_24BPP:
+ nBits = new int[]{8, 8, 8};
+ bOffs = new int[]{2, 1, 0};
+ break;
+ default:
+ throw new RuntimeException("not yet implemented");
+ }
+
+ ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
+ ComponentColorModel cm = new ComponentColorModel
+ (cs, nBits, pf.isAlpha(), pf.isPreMultiplied(), Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
+ PixelInterleavedSampleModel csm =
+ new PixelInterleavedSampleModel(cm.getTransferType(), width, height, cm.getNumComponents(), stride, bOffs);
+
+ DataBufferByte dbb = new DataBufferByte(data, data.length);
+ WritableRaster raster = (WritableRaster) Raster.createRaster(csm, dbb, null);
+
+ return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
+ }
+
+ private BufferedImage readImage(final byte[] data) {
+ // TODO: instead of returning a BufferedImage, we might return a pair of raw data + image renderer
+ // instead, so metafiles aren't pixelated, but directly written to the output graphics context
+ try {
+ switch (getImageDataType()) {
+ case BITMAP: {
+ BufferedImage bi = (getBitmapType() == EmfPlusBitmapDataType.PIXEL)
+ ? readGDIImage(data)
+ : ImageIO.read(new ByteArrayInputStream(data));
+
+// final int w = bi.getWidth();
+// final int h = bi.getHeight();
+//
+// int[] line = new int[w];
+//
+// WritableRaster wr = bi.getRaster();
+// for (int row=0; row<h; row++) {
+// wr.get
+// for (int x=0; x<w; x++) {
+// // TODO: use clamp color here
+// if ((line[x] & 0xFFFFFF) == 0) {
+// // make it transparent
+// line[x] &= 0xFFFFFF;
+// }
+// }
+// wr.setPixels(0, row, w, 1, line);
+// }
+
+
+ return bi;
+ }
+ case METAFILE:
+ assert (getMetafileType() != null);
+ switch (getMetafileType()) {
+ case Wmf:
+ case WmfPlaceable:
+ HwmfPicture wmf = new HwmfPicture(new ByteArrayInputStream(data));
+ return readImage(wmf.getSize(), wmf::draw);
+
+ case Emf:
+ case EmfPlusDual:
+ case EmfPlusOnly:
+ HemfPicture emf = new HemfPicture(new ByteArrayInputStream(data));
+ return readImage(emf.getSize(), emf::draw);
+
+ default:
+ break;
+ }
+ default:
+ break;
+ }
+ } catch (IOException ignored) {
+ }
+
+ // fallback to empty image
+ return new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
+ }
+
+ private BufferedImage readImage(final Dimension2D dim, final BiConsumer<Graphics2D,Rectangle2D> draw) {
+ int width = Units.pointsToPixel(dim.getWidth());
+ // keep aspect ratio for height
+ int height = Units.pointsToPixel(dim.getHeight());
+ double longSide = Math.max(width,height);
+ if (longSide > MAX_IMAGE_SIZE) {
+ double scale = MAX_IMAGE_SIZE / longSide;
+ width *= scale;
+ height *= scale;
+ }
+
+ BufferedImage bufImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g = bufImg.createGraphics();
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+ g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
+
+ draw.accept(g, new Rectangle2D.Double(0, 0, width, height));
+
+ g.dispose();
+
+ return bufImg;
+ }
}
+
+
public static class EmfPlusImageAttributes implements EmfPlusObjectData {
private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion();
private EmfPlusWrapMode wrapMode;
public EmfPlusObjectClamp getObjectClamp() {
return objectClamp;
}
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+ }
}
}
package org.apache.poi.hemf.record.emfplus;
+import static org.apache.poi.hemf.record.emf.HemfMisc.adaptXForm;
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
+import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.record.emf.HemfFill;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
-import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
* SHOULD be processed.
*/
public static class EmfPlusGetDC extends EmfPlusFlagOnly {
+ @Override
+ public void draw(HemfGraphics ctx) {
+ if (ctx.getRenderState() == HemfGraphics.EmfRenderState.EMFPLUS_ONLY) {
+ ctx.setRenderState(HemfGraphics.EmfRenderState.EMF_DCONTEXT);
+ }
+ }
}
/**
return HemfFill.readXForm(leis, matrixData);
}
+
+ public AffineTransform getMatrixData() {
+ return matrixData;
+ }
+
+ @Override
+ public void draw(HemfGraphics ctx) {
+ ctx.updateWindowMapMode();
+ AffineTransform tx = ctx.getTransform();
+ tx.concatenate(getMatrixData());
+ ctx.setTransform(tx);
+ }
}
/**
public HemfPlusRecordType getEmfPlusRecordType() {
return HemfPlusRecordType.multiplyWorldTransform;
}
+
+ @Override
+ public void draw(HemfGraphics ctx) {
+ ctx.updateWindowMapMode();
+ AffineTransform tx = ctx.getTransform();
+ tx.preConcatenate(adaptXForm(getMatrixData(), tx));
+ tx.concatenate(getMatrixData());
+ ctx.setTransform(tx);
+ }
}
/**
package org.apache.poi.hemf.record.emfplus;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.function.Supplier;
+import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusBrush;
import org.apache.poi.hemf.record.emfplus.HemfPlusFont.EmfPlusFont;
import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion;
import org.apache.poi.hemf.record.emfplus.HemfPlusPath.EmfPlusPath;
import org.apache.poi.hemf.record.emfplus.HemfPlusPen.EmfPlusPen;
import org.apache.poi.hemf.record.emfplus.HemfPlusRegion.EmfPlusRegion;
+import org.apache.poi.hwmf.draw.HwmfGraphics;
+import org.apache.poi.hwmf.record.HwmfObjectTableEntry;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.IOUtils;
* The EmfPlusObject record specifies an object for use in graphics operations. The object definition
* can span multiple records), which is indicated by the value of the Flags field.
*/
- public static class EmfPlusObject implements HemfPlusRecord, EmfPlusObjectId {
+ public static class EmfPlusObject implements HemfPlusRecord, EmfPlusObjectId, HwmfObjectTableEntry {
/**
// for debugging
private int objectId;
private EmfPlusObjectData objectData;
+ private List<EmfPlusObjectData> continuedObjectData;
private int totalObjectSize;
@Override
return size;
}
+
+ @Override
+ public void draw(HemfGraphics ctx) {
+ HwmfObjectTableEntry entry = ctx.getObjectTableEntry(getObjectId());
+ if (objectData.isContinuedRecord()) {
+ EmfPlusObject other;
+ if (entry instanceof EmfPlusObject && objectData.getClass().isInstance((other = (EmfPlusObject)entry).getObjectData())) {
+ other.linkContinuedObject(objectData);
+ return;
+ } else {
+ throw new RuntimeException("can't find previous record for continued record");
+ }
+ }
+ ctx.addObjectTableEntry(this, getObjectId());
+ }
+
+ @Override
+ public void applyObject(HwmfGraphics ctx) {
+ objectData.applyObject((HemfGraphics)ctx, continuedObjectData);
+ }
+
+ void linkContinuedObject(EmfPlusObjectData continueObject) {
+ if (continuedObjectData == null) {
+ continuedObjectData = new ArrayList<>();
+ }
+ continuedObjectData.add(continueObject);
+ }
+
+ List<EmfPlusObjectData> getContinuedObject() {
+ return continuedObjectData;
+ }
}
public interface EmfPlusObjectData {
long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException;
+
+ void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData);
+
+ EmfPlusGraphicsVersion getGraphicsVersion();
+
+ default boolean isContinuedRecord() {
+ EmfPlusGraphicsVersion gv = getGraphicsVersion();
+ return (gv.getGraphicsVersion() == null || gv.getMetafileSignature() != 0xDBC01);
+ }
}
public static class EmfPlusUnknownData implements EmfPlusObjectData {
return dataSize;
}
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+
+ }
+
+ @Override
+ public EmfPlusGraphicsVersion getGraphicsVersion() {
+ return graphicsVersion;
+ }
}
}
package org.apache.poi.hemf.record.emfplus;
+import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.Arrays;
+import java.util.List;
import java.util.function.BiFunction;
+import org.apache.poi.hemf.draw.HemfDrawProperties;
+import org.apache.poi.hemf.draw.HemfGraphics;
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.HemfPlusObject.EmfPlusObjectData;
private static final BitField POINT_RLE_COUNT = BitFieldFactory.getInstance(0x3F);
- private final HemfPlusHeader.EmfPlusGraphicsVersion version = new HemfPlusHeader.EmfPlusGraphicsVersion();
+ private final HemfPlusHeader.EmfPlusGraphicsVersion graphicsVersion = new HemfPlusHeader.EmfPlusGraphicsVersion();
private int pointFlags;
private Point2D[] pathPoints;
private byte[] pointTypes;
@Override
public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException {
- long size = version.init(leis);
+ long size = graphicsVersion.init(leis);
// A 32-bit unsigned integer that specifies the number of points and associated point types that
// are defined by this object.
return size;
}
+ @Override
+ public HemfPlusHeader.EmfPlusGraphicsVersion getGraphicsVersion() {
+ return graphicsVersion;
+ }
+
public boolean isPointDashed(int index) {
return POINT_TYPE_DASHED.isSet(pointTypes[index]);
}
public int getFlags() {
return pointFlags;
}
+
+
+
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+ HemfDrawProperties prop = ctx.getProperties();
+ Path2D path = new Path2D.Double(Path2D.WIND_NON_ZERO);
+ prop.setPath(path);
+
+ for (int idx=0; idx < pathPoints.length; idx++) {
+ Point2D p1 = pathPoints[idx];
+ switch (getPointType(idx)) {
+ case START:
+ path.moveTo(p1.getX(), p1.getY());
+ break;
+ case LINE:
+ path.lineTo(p1.getX(), p1.getY());
+ break;
+ case BEZIER: {
+ Point2D p2 = pathPoints[++idx];
+ Point2D p3 = pathPoints[++idx];
+ path.curveTo(p1.getX(), p1.getY(), p2.getX(), p2.getY(), p3.getX(), p3.getY());
+ break;
+ }
+ }
+ if (isPointClosed(idx)) {
+ path.closePath();
+ }
+ }
+ }
+
+
}
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.io.IOException;
+import java.util.List;
import java.util.function.Consumer;
+import org.apache.poi.hemf.draw.HemfDrawProperties;
+import org.apache.poi.hemf.draw.HemfGraphics;
+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.HemfPlusObject.EmfPlusObjectData;
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType;
import org.apache.poi.hemf.record.emfplus.HemfPlusPath.EmfPlusPath;
+import org.apache.poi.hwmf.record.HwmfPenStyle;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.Internal;
}
}
+
+ @Internal
+ public interface EmfPlusCustomLineCap {
+ long init(LittleEndianInputStream leis) throws IOException;
+ }
+
+
public static class EmfPlusPen implements EmfPlusObjectData {
private int type;
private int penDataFlags;
- private HemfPlusDraw.EmfPlusUnitType unitType;
+ private EmfPlusUnitType unitType;
private double penWidth;
- private AffineTransform trans;
- private EmfPlusLineCapType startCap, endCap;
- private EmfPlusLineJoin join;
- private Double mitterLimit;
- private EmfPlusLineStyle style;
- EmfPlusDashedLineCapType dashedLineCapType;
+ private final AffineTransform trans = new AffineTransform();
+ private EmfPlusLineCapType startCap = EmfPlusLineCapType.FLAT;
+ private EmfPlusLineCapType endCap = startCap;
+ private EmfPlusLineJoin join = EmfPlusLineJoin.ROUND;
+ private Double miterLimit = 1.;
+ private EmfPlusLineStyle style = EmfPlusLineStyle.SOLID;
+ private EmfPlusDashedLineCapType dashedLineCapType;
private Double dashOffset;
- private double[] dashedLineData;
+ private float[] dashedLineData;
private EmfPlusPenAlignment penAlignment;
private double[] compoundLineData;
private EmfPlusCustomLineCap customStartCap;
penDataFlags = leis.readInt();
// A 32-bit unsigned integer that specifies the measuring units for the pen.
// The value MUST be from the UnitType enumeration
- unitType = HemfPlusDraw.EmfPlusUnitType.valueOf(leis.readInt());
+ unitType = EmfPlusUnitType.valueOf(leis.readInt());
// A 32-bit floating-point value that specifies the width of the line drawn by the pen in the units specified
// by the PenUnit field. If a zero width is specified, a minimum value is used, which is determined by the units.
penWidth = leis.readFloat();
- size += 4* LittleEndianConsts.INT_SIZE;
+ size += 4*LittleEndianConsts.INT_SIZE;
if (TRANSFORM.isSet(penDataFlags)) {
// An optional EmfPlusTransformMatrix object that specifies a world space to device space transform for
// the pen. This field MUST be present if the PenDataTransform flag is set in the PenDataFlags field of
// the EmfPlusPenData object.
- trans = new AffineTransform();
size += readXForm(leis, trans);
}
// line walls on the inside the join to the intersection of the line walls outside the join. The miter
// length can be large when the angle between two lines is small. This field MUST be present if the
// PenDataMiterLimit flag is set in the PenDataFlags field of the EmfPlusPenData object.
- mitterLimit = (double)leis.readFloat();
+ miterLimit = (double)leis.readFloat();
size += LittleEndianConsts.INT_SIZE;
}
}
// An array of DashedLineDataSize floating-point values that specify the lengths of the dashes and spaces in a dashed line.
- dashedLineData = new double[dashesSize];
+ dashedLineData = new float[dashesSize];
for (int i=0; i<dashesSize; i++) {
dashedLineData[i] = leis.readFloat();
}
return size;
}
+ @Override
+ public EmfPlusGraphicsVersion getGraphicsVersion() {
+ return graphicsVersion;
+ }
+
private long initCustomCap(Consumer<EmfPlusCustomLineCap> setter, LittleEndianInputStream leis) throws IOException {
EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion();
long size = version.init(leis);
return size;
}
- @Internal
- public interface EmfPlusCustomLineCap {
- long init(LittleEndianInputStream leis) throws IOException;
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+ final HemfDrawProperties prop = ctx.getProperties();
+ // TOOD:
+ // - set width according unit type
+ // - provide logic for different start and end cap
+ // - provide standard caps like diamondd
+ // - support custom caps
+
+ // workaround for too wide pens ... just arbitrary reduce high values ...
+ prop.setPenWidth(penWidth > 20 ? 1 : penWidth);
+ prop.setPenStyle(new HwmfPenStyle(){
+ @Override
+ public HwmfLineCap getLineCap() {
+ // ignore endCap for now
+ switch(startCap) {
+ default:
+ case FLAT:
+ return HwmfLineCap.FLAT;
+ case ROUND:
+ return HwmfLineCap.ROUND;
+ case SQUARE:
+ return HwmfLineCap.SQUARE;
+ }
+ }
+
+ @Override
+ public HwmfLineJoin getLineJoin() {
+ switch (join) {
+ default:
+ case BEVEL:
+ return HwmfLineJoin.BEVEL;
+ case ROUND:
+ return HwmfLineJoin.ROUND;
+ case MITER_CLIPPED:
+ case MITER:
+ return HwmfLineJoin.MITER;
+ }
+ }
+
+ @Override
+ public HwmfLineDash getLineDash() {
+ return dashedLineData == null ? HwmfLineDash.SOLID : HwmfLineDash.USERSTYLE;
+ }
+
+ @Override
+ public float[] getLineDashes() {
+ return dashedLineData;
+ }
+
+ @Override
+ public boolean isAlternateDash() {
+ return (getLineDash() != HwmfLineDash.SOLID && dashOffset != null && dashOffset == 0);
+ }
+
+ @Override
+ public boolean isGeometric() {
+ return (unitType == EmfPlusUnitType.World || unitType == EmfPlusUnitType.Display);
+ }
+ });
}
+ }
- public static class EmfPlusPathArrowCap implements EmfPlusCustomLineCap {
- /**
- * If set, an EmfPlusFillPath object MUST be specified in the OptionalData field of the
- * EmfPlusCustomLineCapData object for filling the custom line cap.
- */
- private static final BitField FILL_PATH = BitFieldFactory.getInstance(0x00000001);
- /**
- * If set, an EmfPlusLinePath object MUST be specified in the OptionalData field of the
- * EmfPlusCustomLineCapData object for outlining the custom line cap.
- */
- private static final BitField LINE_PATH = BitFieldFactory.getInstance(0x00000002);
-
-
- private int dataFlags;
- private EmfPlusLineCapType baseCap;
- private double baseInset;
- private EmfPlusLineCapType startCap;
- private EmfPlusLineCapType endCap;
- private EmfPlusLineJoin join;
- private double mitterLimit;
- private double widthScale;
- private final Point2D fillHotSpot = new Point2D.Double();
- private final Point2D lineHotSpot = new Point2D.Double();
- private EmfPlusPath fillPath;
- private EmfPlusPath outlinePath;
-
- @Override
- public long init(LittleEndianInputStream leis) throws IOException {
- // A 32-bit unsigned integer that specifies the data in the OptionalData field.
- // This value MUST be composed of CustomLineCapData flags
- dataFlags = leis.readInt();
-
- // A 32-bit unsigned integer that specifies the value from the LineCap enumeration on which
- // the custom line cap is based.
- baseCap = EmfPlusLineCapType.valueOf(leis.readInt());
-
- // A 32-bit floating-point value that specifies the distance between the
- // beginning of the line cap and the end of the line.
- baseInset = leis.readFloat();
-
- // A 32-bit unsigned integer that specifies the value in the LineCap enumeration that indicates the line
- // cap used at the start/end of the line to be drawn.
- startCap = EmfPlusLineCapType.valueOf(leis.readInt());
- endCap = EmfPlusLineCapType.valueOf(leis.readInt());
+ public static class EmfPlusPathArrowCap implements EmfPlusCustomLineCap {
+ /**
+ * If set, an EmfPlusFillPath object MUST be specified in the OptionalData field of the
+ * EmfPlusCustomLineCapData object for filling the custom line cap.
+ */
+ private static final BitField FILL_PATH = BitFieldFactory.getInstance(0x00000001);
+ /**
+ * If set, an EmfPlusLinePath object MUST be specified in the OptionalData field of the
+ * EmfPlusCustomLineCapData object for outlining the custom line cap.
+ */
+ private static final BitField LINE_PATH = BitFieldFactory.getInstance(0x00000002);
- // A 32-bit unsigned integer that specifies the value in the LineJoin enumeration, which specifies how
- // to join two lines that are drawn by the same pen and whose ends meet. At the intersection of the two
- // line ends, a line join makes the connection look more continuous.
- join = EmfPlusLineJoin.valueOf(leis.readInt());
- // A 32-bit floating-point value that contains the limit of the thickness of the join on a mitered corner
- // by setting the maximum allowed ratio of miter length to line width.
- mitterLimit = leis.readFloat();
+ private int dataFlags;
+ private EmfPlusLineCapType baseCap;
+ private double baseInset;
+ private EmfPlusLineCapType startCap;
+ private EmfPlusLineCapType endCap;
+ private EmfPlusLineJoin join;
+ private double mitterLimit;
+ private double widthScale;
+ private final Point2D fillHotSpot = new Point2D.Double();
+ private final Point2D lineHotSpot = new Point2D.Double();
+ private EmfPlusPath fillPath;
+ private EmfPlusPath outlinePath;
- // A 32-bit floating-point value that specifies the amount by which to scale the custom line cap with
- // respect to the width of the EmfPlusPen object that is used to draw the lines.
- widthScale = leis.readFloat();
+ @Override
+ public long init(LittleEndianInputStream leis) throws IOException {
+ // A 32-bit unsigned integer that specifies the data in the OptionalData field.
+ // This value MUST be composed of CustomLineCapData flags
+ dataFlags = leis.readInt();
- int size = 8* LittleEndianConsts.INT_SIZE;
+ // A 32-bit unsigned integer that specifies the value from the LineCap enumeration on which
+ // the custom line cap is based.
+ baseCap = EmfPlusLineCapType.valueOf(leis.readInt());
- // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}.
- size += readPointF(leis, fillHotSpot);
- size += readPointF(leis, lineHotSpot);
+ // A 32-bit floating-point value that specifies the distance between the
+ // beginning of the line cap and the end of the line.
+ baseInset = leis.readFloat();
- if (FILL_PATH.isSet(dataFlags)) {
- fillPath = new EmfPlusPath();
- size += fillPath.init(leis, -1, null, -1);
- }
+ // A 32-bit unsigned integer that specifies the value in the LineCap enumeration that indicates the line
+ // cap used at the start/end of the line to be drawn.
+ startCap = EmfPlusLineCapType.valueOf(leis.readInt());
+ endCap = EmfPlusLineCapType.valueOf(leis.readInt());
- if (LINE_PATH.isSet(dataFlags)) {
- outlinePath = new EmfPlusPath();
- size += outlinePath.init(leis, -1, null, -1);
- }
+ // A 32-bit unsigned integer that specifies the value in the LineJoin enumeration, which specifies how
+ // to join two lines that are drawn by the same pen and whose ends meet. At the intersection of the two
+ // line ends, a line join makes the connection look more continuous.
+ join = EmfPlusLineJoin.valueOf(leis.readInt());
+
+ // A 32-bit floating-point value that contains the limit of the thickness of the join on a mitered corner
+ // by setting the maximum allowed ratio of miter length to line width.
+ mitterLimit = leis.readFloat();
+
+ // A 32-bit floating-point value that specifies the amount by which to scale the custom line cap with
+ // respect to the width of the EmfPlusPen object that is used to draw the lines.
+ widthScale = leis.readFloat();
- return size;
+ int size = 8* LittleEndianConsts.INT_SIZE;
+
+ // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}.
+ size += readPointF(leis, fillHotSpot);
+ size += readPointF(leis, lineHotSpot);
+
+ if (FILL_PATH.isSet(dataFlags)) {
+ fillPath = new EmfPlusPath();
+ size += fillPath.init(leis, -1, null, -1);
+ }
+
+ if (LINE_PATH.isSet(dataFlags)) {
+ outlinePath = new EmfPlusPath();
+ size += outlinePath.init(leis, -1, null, -1);
}
+
+ return size;
}
+ }
- public static class EmfPlusAdjustableArrowCap implements EmfPlusCustomLineCap {
- private double width;
- private double height;
- private double middleInset;
- private boolean isFilled;
- private EmfPlusLineCapType startCap;
- private EmfPlusLineCapType endCap;
- private EmfPlusLineJoin join;
- private double mitterLimit;
- private double widthScale;
- private final Point2D fillHotSpot = new Point2D.Double();
- private final Point2D lineHotSpot = new Point2D.Double();
-
- @Override
- public long init(LittleEndianInputStream leis) throws IOException {
- // A 32-bit floating-point value that specifies the width of the arrow cap.
- // The width of the arrow cap is scaled by the width of the EmfPlusPen object that is used to draw the
- // line being capped. For example, when drawing a capped line with a pen that has a width of 5 pixels,
- // and the adjustable arrow cap object has a width of 3, the actual arrow cap is drawn 15 pixels wide.
- width = leis.readFloat();
-
- // A 32-bit floating-point value that specifies the height of the arrow cap.
- // The height of the arrow cap is scaled by the width of the EmfPlusPen object that is used to draw the
- // line being capped. For example, when drawing a capped line with a pen that has a width of 5 pixels,
- // and the adjustable arrow cap object has a height of 3, the actual arrow cap is drawn 15 pixels high.
- height = leis.readFloat();
-
- // A 32-bit floating-point value that specifies the number of pixels between the outline of the arrow
- // cap and the fill of the arrow cap.
- middleInset = leis.readFloat();
-
- // A 32-bit Boolean value that specifies whether the arrow cap is filled.
- // If the arrow cap is not filled, only the outline is drawn.
- isFilled = (leis.readInt() != 0);
-
- // A 32-bit unsigned integer that specifies the value in the LineCap enumeration that indicates
- // the line cap to be used at the start/end of the line to be drawn.
- startCap = EmfPlusLineCapType.valueOf(leis.readInt());
- endCap = EmfPlusLineCapType.valueOf(leis.readInt());
+ public static class EmfPlusAdjustableArrowCap implements EmfPlusCustomLineCap {
+ private double width;
+ private double height;
+ private double middleInset;
+ private boolean isFilled;
+ private EmfPlusLineCapType startCap;
+ private EmfPlusLineCapType endCap;
+ private EmfPlusLineJoin join;
+ private double mitterLimit;
+ private double widthScale;
+ private final Point2D fillHotSpot = new Point2D.Double();
+ private final Point2D lineHotSpot = new Point2D.Double();
- // 32-bit unsigned integer that specifies the value in the LineJoin enumeration that specifies how to
- // join two lines that are drawn by the same pen and whose ends meet. At the intersection of the two
- // line ends, a line join makes the connection look more continuous.
- join = EmfPlusLineJoin.valueOf(leis.readInt());
+ @Override
+ public long init(LittleEndianInputStream leis) throws IOException {
+ // A 32-bit floating-point value that specifies the width of the arrow cap.
+ // The width of the arrow cap is scaled by the width of the EmfPlusPen object that is used to draw the
+ // line being capped. For example, when drawing a capped line with a pen that has a width of 5 pixels,
+ // and the adjustable arrow cap object has a width of 3, the actual arrow cap is drawn 15 pixels wide.
+ width = leis.readFloat();
- // A 32-bit floating-point value that specifies the limit of the thickness of the join on a mitered
- // corner by setting the maximum allowed ratio of miter length to line width.
- mitterLimit = leis.readFloat();
+ // A 32-bit floating-point value that specifies the height of the arrow cap.
+ // The height of the arrow cap is scaled by the width of the EmfPlusPen object that is used to draw the
+ // line being capped. For example, when drawing a capped line with a pen that has a width of 5 pixels,
+ // and the adjustable arrow cap object has a height of 3, the actual arrow cap is drawn 15 pixels high.
+ height = leis.readFloat();
- // A 32-bit floating-point value that specifies the amount by which to scale an EmfPlusCustomLineCap
- // object with respect to the width of the graphics pen that is used to draw the lines.
- widthScale = leis.readFloat();
+ // A 32-bit floating-point value that specifies the number of pixels between the outline of the arrow
+ // cap and the fill of the arrow cap.
+ middleInset = leis.readFloat();
- int size = 9 * LittleEndianConsts.INT_SIZE;
+ // A 32-bit Boolean value that specifies whether the arrow cap is filled.
+ // If the arrow cap is not filled, only the outline is drawn.
+ isFilled = (leis.readInt() != 0);
- // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}.
- size += readPointF(leis, fillHotSpot);
+ // A 32-bit unsigned integer that specifies the value in the LineCap enumeration that indicates
+ // the line cap to be used at the start/end of the line to be drawn.
+ startCap = EmfPlusLineCapType.valueOf(leis.readInt());
+ endCap = EmfPlusLineCapType.valueOf(leis.readInt());
- // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}.
- size += readPointF(leis, lineHotSpot);
+ // 32-bit unsigned integer that specifies the value in the LineJoin enumeration that specifies how to
+ // join two lines that are drawn by the same pen and whose ends meet. At the intersection of the two
+ // line ends, a line join makes the connection look more continuous.
+ join = EmfPlusLineJoin.valueOf(leis.readInt());
- return size;
- }
- }
+ // A 32-bit floating-point value that specifies the limit of the thickness of the join on a mitered
+ // corner by setting the maximum allowed ratio of miter length to line width.
+ mitterLimit = leis.readFloat();
+ // A 32-bit floating-point value that specifies the amount by which to scale an EmfPlusCustomLineCap
+ // object with respect to the width of the graphics pen that is used to draw the lines.
+ widthScale = leis.readFloat();
+
+ int size = 9 * LittleEndianConsts.INT_SIZE;
+
+ // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}.
+ size += readPointF(leis, fillHotSpot);
+
+ // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}.
+ size += readPointF(leis, lineHotSpot);
+
+ return size;
+ }
}
}
import java.io.IOException;
-import org.apache.poi.hemf.record.emf.HemfRecordType;
+import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.util.Internal;
import org.apache.poi.util.LittleEndianInputStream;
*/
long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException;
+
+
+ /**
+ * Draws the record, the default redirects to the parent WMF record drawing
+ * @param ctx the drawing context
+ */
+ default void draw(HemfGraphics ctx) {
+ }
+
}
import java.awt.geom.Rectangle2D;
import java.io.IOException;
+import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
+import org.apache.poi.hemf.draw.HemfGraphics;
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.EmfPlusObjectType;
public static class EmfPlusRegion implements EmfPlusObjectData {
- private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion();
+ private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion();
private EmfPlusRegionNodeData regionNode;
@Override
public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException {
- long size = version.init(leis);
+ long size = graphicsVersion.init(leis);
// A 32-bit unsigned integer that specifies the number of child nodes in the RegionNode field.
int nodeCount = leis.readInt();
return size;
}
+ @Override
+ public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) {
+
+ }
+ @Override
+ public EmfPlusGraphicsVersion getGraphicsVersion() {
+ return graphicsVersion;
+ }
}
* Compress GDIs internal format to something useful
*/
private void compressGDIBitmap(EmfPlusImage img, HwmfEmbedded emb, HwmfEmbeddedType et) {
- final int width = img.getBitmapWidth();
- final int height = img.getBitmapHeight();
- final int stride = img.getBitmapStride();
- final EmfPlusPixelFormat pf = img.getPixelFormat();
-
- int[] nBits, bOffs;
- switch (pf) {
- case ARGB_32BPP:
- nBits = new int[]{8, 8, 8, 8};
- bOffs = new int[]{2, 1, 0, 3};
- break;
- case RGB_24BPP:
- nBits = new int[]{8, 8, 8};
- bOffs = new int[]{2, 1, 0};
- break;
- default:
- throw new RuntimeException("not yet implemented");
- }
-
- ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
- ComponentColorModel cm = new ComponentColorModel
- (cs, nBits, pf.isAlpha(), pf.isPreMultiplied(), Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
- PixelInterleavedSampleModel csm =
- new PixelInterleavedSampleModel(cm.getTransferType(), width, height, cm.getNumColorComponents(), stride, bOffs);
-
- byte d[] = emb.getRawData();
- WritableRaster raster = (WritableRaster) Raster.createRaster(csm, new DataBufferByte(d, d.length), null);
-
- BufferedImage bi = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null);
-
+ BufferedImage bi = img.readGDIImage(emb.getRawData());
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// use HwmfEmbeddedType literal for conversion
*/
@Internal
public class HemfPicture implements Iterable<HemfRecord> {
-
private final LittleEndianInputStream stream;
private final List<HemfRecord> records = new ArrayList<>();
private boolean isParsed = false;
}
/**
- * Return the image size in points
+ * Returns the bounding box in device-independent units. Usually this is taken from the placeable header.
*
- * @return the image size in points
+ * @return the bounding box
*/
- public Dimension2D getSize() {
+ public Rectangle2D getBounds() {
HemfHeader header = (HemfHeader)getRecords().get(0);
- final double coeff = (double) Units.EMU_PER_CENTIMETER / Units.EMU_PER_POINT / 10.;
Rectangle2D dim = header.getFrameRectangle();
+ double x = dim.getX(), y = dim.getY();
double width = dim.getWidth(), height = dim.getHeight();
- if (dim.isEmpty() || Math.rint(width*coeff) == 0 || Math.rint(height*coeff) == 0) {
+ if (dim.isEmpty() || Math.rint(width) == 0 || Math.rint(height) == 0) {
for (HemfRecord r : getRecords()) {
if (r instanceof HemfWindowing.EmfSetWindowExtEx) {
- Dimension2D d = ((HemfWindowing.EmfSetWindowExtEx)r).getSize();
+ HemfWindowing.EmfSetWindowExtEx extEx = (HemfWindowing.EmfSetWindowExtEx)r;
+ Dimension2D d = extEx.getSize();
width = d.getWidth();
height = d.getHeight();
// keep searching - sometimes there's another record
}
+ if (r instanceof HemfWindowing.EmfSetWindowOrgEx) {
+ HemfWindowing.EmfSetWindowOrgEx orgEx = (HemfWindowing.EmfSetWindowOrgEx)r;
+ x = orgEx.getX();
+ y = orgEx.getY();
+ }
}
}
- if (Math.rint(width*coeff) == 0 || Math.rint(height*coeff) == 0) {
- width = 100;
- height = 100;
+ return new Rectangle2D.Double(x, y, width, height);
+ }
+
+ /**
+ * Return the image size in points
+ *
+ * @return the image size in points
+ */
+ public Dimension2D getSize() {
+ final Rectangle2D bounds = getBounds();
+
+ if (bounds.isEmpty()) {
+ return new Dimension2DDouble(100,100);
}
- return new Dimension2DDouble(Math.abs(width*coeff), Math.abs(height*coeff));
+ final double coeff = (double) Units.EMU_PER_CENTIMETER / Units.EMU_PER_POINT / 10.;
+ double width = Math.abs(bounds.getWidth()*coeff);
+ double height = Math.abs(bounds.getHeight()*coeff);
+
+ return new Dimension2DDouble(width, height);
}
private static double minX(Rectangle2D bounds) {
package org.apache.poi.hwmf.draw;
+import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
+import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Paint;
public void fill(Shape shape) {
HwmfDrawProperties prop = getProperties();
+
+ Composite old = graphicsCtx.getComposite();
+ graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
if (prop.getBrushStyle() != HwmfBrushStyle.BS_NULL) {
if (prop.getBkMode() == HwmfBkMode.OPAQUE) {
graphicsCtx.setPaint(prop.getBackgroundColor().getColor());
graphicsCtx.setPaint(getFill());
graphicsCtx.fill(shape);
}
+ graphicsCtx.setComposite(old);
draw(shape);
}
protected BasicStroke getStroke() {
+ HwmfDrawProperties prop = getProperties();
+ HwmfPenStyle ps = prop.getPenStyle();
// TODO: fix line width calculation
- float width = (float)getProperties().getPenWidth();
+ float width = (float)prop.getPenWidth();
if (width == 0) {
width = 1;
}
- HwmfPenStyle ps = getProperties().getPenStyle();
int cap = ps.getLineCap().awtFlag;
int join = ps.getLineJoin().awtFlag;
- float miterLimit = (float)getProperties().getPenMiterLimit();
+ float miterLimit = (float)prop.getPenMiterLimit();
float[] dashes = ps.getLineDashes();
boolean dashAlt = ps.isAlternateDash();
// This value is not an integer index into the dash pattern array.
graphicsCtx.scale(dstBounds.getWidth()/srcBounds2.getWidth(), dstBounds.getHeight()/srcBounds2.getHeight());
graphicsCtx.translate(-srcBounds2.getX(), -srcBounds2.getY());
- graphicsCtx.drawImage(img, 0, 0, prop.getBackgroundColor().getColor(), null);
+ if (prop.getBkMode() == HwmfBkMode.OPAQUE) {
+ graphicsCtx.drawImage(img, 0, 0, prop.getBackgroundColor().getColor(), null);
+ } else {
+ Composite old = graphicsCtx.getComposite();
+ graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
+ graphicsCtx.drawImage(img, 0, 0, null);
+ graphicsCtx.setComposite(old);
+ }
graphicsCtx.setTransform(at);
graphicsCtx.setClip(clip);
package org.apache.poi.hwmf.record;
import java.awt.Shape;
+import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Dimension2D;
@Internal
public static String pointToString(Point2D point) {
- return "{ x: "+point.getX()+", y: "+point.getY()+" }";
+ return (point == null) ? "null" : "{ x: "+point.getX()+", y: "+point.getY()+" }";
}
@Internal
public static String boundsToString(Rectangle2D bounds) {
- return "{ x: "+bounds.getX()+", y: "+bounds.getY()+", w: "+bounds.getWidth()+", h: "+bounds.getHeight()+" }";
+ return (bounds == null) ? "null" : "{ x: "+bounds.getX()+", y: "+bounds.getY()+", w: "+bounds.getWidth()+", h: "+bounds.getHeight()+" }";
}
@Internal
public static String dimToString(Dimension2D dim) {
- return "{ w: "+dim.getWidth()+", h: "+dim.getHeight()+" }";
+ return (dim == null) ? "null" : "{ w: "+dim.getWidth()+", h: "+dim.getHeight()+" }";
}
@Internal
Math.abs(bounds.getHeight())
);
}
-
}
import static org.junit.Assert.assertTrue;
import java.awt.geom.Point2D;
-import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.FileWriter;
-import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.stream.Stream;
import org.apache.poi.POIDataSamples;
import org.apache.poi.hemf.record.emf.HemfComment;
private static final POIDataSamples ss_samples = POIDataSamples.getSpreadSheetInstance();
private static final POIDataSamples sl_samples = POIDataSamples.getSlideShowInstance();
- /*
- @Test
+/* @Test
@Ignore("Only for manual tests - need to add org.tukaani:xz:1.8 for this to work")
public void paint() throws IOException {
- byte buf[] = new byte[50_000_000];
+ final byte buf[] = new byte[50_000_000];
// good test samples to validate rendering:
// emfs/commoncrawl2/NB/NBWN2YH5VFCLZRFDQU7PB7IDD4UKY7DN_2.emf
final boolean writeLog = false;
final boolean dumpRecords = false;
- final boolean savePng = false;
- final boolean dumpEmbedded = true;
+ final boolean savePng = true;
+ final boolean dumpEmbedded = false;
Set<String> passed = new HashSet<>();
- try (BufferedWriter sucWrite = parseEmfLog(passed, "emf-success.txt");
- BufferedWriter parseError = parseEmfLog(passed, "emf-parse.txt");
- BufferedWriter renderError = parseEmfLog(passed, "emf-render.txt");
- SevenZFile sevenZFile = new SevenZFile(new File("tmp/plus_emf.7z"))) {
+ try (BufferedWriter sucWrite = parseEmfLog(passed, "emf-success.txt")
+ ;BufferedWriter parseError = parseEmfLog(passed, "emf-parse.txt")
+ ;BufferedWriter renderError = parseEmfLog(passed, "emf-render.txt")
+ ;SevenZFile sevenZFile = new SevenZFile(new File("tmp/plus_emf.7z"))
+ ) {
for (int idx=0;;idx++) {
SevenZArchiveEntry entry = sevenZFile.getNextEntry();
if (entry == null) break;
if (entry.isDirectory() || !etName.endsWith(".emf") || passed.contains(etName)) continue;
- // if (!etName.equals("emfs/commoncrawl2/2S/2SYMYPLNJURGCXJKLNZCJQGIBHVMQTRS_0.emf")) continue;
- // emfs/commoncrawl2/ZJ/ZJT2BZPLQR7DKSKYLYL6GRDEUM2KIO5F_4.emf
- // emfs/govdocs1/005/005203.ppt_3.emf
+ // KEEDHN6XES4EKK52E3AJHKCARNTQF7PO_0.emf takes ages, time is spent while drawing paths
+// if (!etName.contains("KEEDHN6XES4EKK52E3AJHKCARNTQF7PO_0.emf")) continue;
+
+ // F7GK5XOLERFURVTQALOCX3GJ6FH45LNQ strange colors
+ // ISS3ANIX2PL4PXR7SZSJSPBZI7YQQE3U stroke wrong
+// if (!etName.contains("ISS3ANIX2PL4PXR7SZSJSPBZI7YQQE3U")) continue;
+
System.out.println(etName);
int embIdx = 0;
for (HwmfEmbedded emb : emf.getEmbeddings()) {
final File embName = new File("build/tmp", "emb_"+etName.replaceFirst(".+/", "").replace(".emf", "_"+embIdx + emb.getEmbeddedType().extension) );
-// try (FileOutputStream fos = new FileOutputStream(embName)) {
-// fos.write(emb.getRawData());
-// }
+ try (FileOutputStream fos = new FileOutputStream(embName)) {
+ fos.write(emb.getRawData());
+ }
embIdx++;
}
}
BufferedImage bufImg = new BufferedImage((int)width, (int)height, BufferedImage.TYPE_INT_ARGB);
g = bufImg.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
- g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
- g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+ g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
+ g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g.setComposite(AlphaComposite.Clear);
g.fillRect(0, 0, (int)width, (int)height);
g.setComposite(AlphaComposite.Src);
+ final File pngName = new File("build/tmp", etName.replaceFirst(".+/", "").replace(".emf", ".png"));
+
emf.draw(g, new Rectangle2D.Double(0, 0, width, height));
- final File pngName = new File("build/tmp", etName.replaceFirst(".+/", "").replace(".emf", ".png"));
if (savePng) {
ImageIO.write(bufImg, "PNG", pngName);
}
}
}
}
- } */
+ }
private static int hashException(Throwable e) {
StringBuilder sb = new StringBuilder();
}
return Files.newBufferedWriter(log, StandardCharsets.UTF_8, soo);
- }
+ }*/
@Test
public void testBasicWindows() throws Exception {