git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1860732 13f79535-47bb-0310-9956-ffa450edef68tags/REL_4_1_1
@@ -17,9 +17,10 @@ | |||
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 { | |||
@@ -27,6 +28,8 @@ 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() { | |||
@@ -35,8 +38,11 @@ public class HemfDrawProperties extends HwmfDrawProperties { | |||
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; | |||
} | |||
/** | |||
@@ -66,4 +72,20 @@ public class HemfDrawProperties extends HwmfDrawProperties { | |||
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; | |||
} | |||
} |
@@ -22,12 +22,15 @@ import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID; | |||
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; | |||
@@ -37,12 +40,20 @@ import org.apache.poi.util.Internal; | |||
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); | |||
@@ -62,8 +73,49 @@ public class HemfGraphics extends HwmfGraphics { | |||
: 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 | |||
@@ -131,14 +183,36 @@ public class HemfGraphics extends HwmfGraphics { | |||
* @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) { | |||
@@ -256,4 +330,10 @@ public class HemfGraphics extends HwmfGraphics { | |||
break; | |||
} | |||
} | |||
@Override | |||
protected Paint getHatchedFill() { | |||
// TODO: use EmfPlusHatchBrushData | |||
return super.getHatchedFill(); | |||
} | |||
} |
@@ -26,6 +26,7 @@ import java.util.Iterator; | |||
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; | |||
@@ -102,6 +103,17 @@ public class HemfComment { | |||
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+" }"; | |||
@@ -255,6 +267,10 @@ public class HemfComment { | |||
public List<HemfPlusRecord> getRecords() { | |||
return Collections.unmodifiableList(records); | |||
} | |||
public void draw(HemfGraphics ctx) { | |||
records.forEach(ctx::draw); | |||
} | |||
} | |||
public static class EmfCommentDataBeginGroup implements EmfCommentData { |
@@ -21,6 +21,7 @@ import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; | |||
import static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds; | |||
import java.awt.Shape; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Arc2D; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Path2D; | |||
@@ -34,6 +35,7 @@ import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hwmf.draw.HwmfGraphics.FillDrawStyle; | |||
import org.apache.poi.hwmf.record.HwmfDraw; | |||
import org.apache.poi.hwmf.record.HwmfDraw.WmfSelectObject; | |||
import org.apache.poi.util.Internal; | |||
import org.apache.poi.util.LittleEndianConsts; | |||
import org.apache.poi.util.LittleEndianInputStream; | |||
@@ -1153,4 +1155,16 @@ public class HemfDraw { | |||
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()+" }"; | |||
} | |||
} |
@@ -19,6 +19,7 @@ package org.apache.poi.hemf.record.emf; | |||
import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL; | |||
import static org.apache.poi.hemf.record.emf.HemfDraw.readRectL; | |||
import static org.apache.poi.hemf.record.emf.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; | |||
@@ -186,7 +187,7 @@ public class HemfFill { | |||
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); | |||
} | |||
@@ -705,6 +706,10 @@ public class HemfFill { | |||
xform.setTransform(m00, m10, m01, m11, m02, m12); | |||
if (xform.isIdentity()) { | |||
xform.setToIdentity(); | |||
} | |||
return 6 * LittleEndian.INT_SIZE; | |||
} | |||
@@ -18,6 +18,7 @@ | |||
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; | |||
@@ -621,14 +622,7 @@ public class HemfMisc { | |||
@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)+" }"; | |||
} | |||
} | |||
@@ -695,7 +689,7 @@ public class HemfMisc { | |||
wsTrans.translate(-emfBounds.getHeight(), emfBounds.getHeight()); | |||
} | |||
} else { | |||
wsTrans = adaptXForm(ctx.getTransform()); | |||
wsTrans = adaptXForm(xForm, ctx.getTransform()); | |||
} | |||
tx = ctx.getTransform(); | |||
@@ -703,7 +697,7 @@ public class HemfMisc { | |||
break; | |||
case MWT_RIGHTMULTIPLY: | |||
tx = ctx.getTransform(); | |||
tx.preConcatenate(adaptXForm(tx)); | |||
tx.preConcatenate(adaptXForm(xForm, tx)); | |||
break; | |||
case MWT_IDENTITY: | |||
ctx.updateWindowMapMode(); | |||
@@ -713,40 +707,16 @@ public class HemfMisc { | |||
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+"' }"; | |||
} | |||
} | |||
@@ -825,4 +795,23 @@ public class HemfMisc { | |||
"}"; | |||
} | |||
} | |||
/** | |||
* 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() | |||
); | |||
} | |||
} |
@@ -17,27 +17,40 @@ | |||
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; | |||
@@ -68,58 +81,149 @@ public class HemfPlusBrush { | |||
} | |||
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) | |||
; | |||
@@ -204,17 +308,19 @@ public class HemfPlusBrush { | |||
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; | |||
@@ -224,6 +330,23 @@ public class HemfPlusBrush { | |||
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. */ | |||
@@ -234,6 +357,17 @@ public class HemfPlusBrush { | |||
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)+" }"; | |||
} | |||
} | |||
@@ -247,6 +381,22 @@ public class HemfPlusBrush { | |||
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. */ | |||
@@ -303,6 +453,32 @@ public class HemfPlusBrush { | |||
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. */ | |||
@@ -409,6 +585,31 @@ public class HemfPlusBrush { | |||
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. */ | |||
@@ -440,6 +641,24 @@ public class HemfPlusBrush { | |||
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) { | |||
@@ -477,4 +696,18 @@ public class HemfPlusBrush { | |||
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(",", "{", "}"))); | |||
} | |||
} |
@@ -17,10 +17,18 @@ | |||
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; | |||
@@ -30,8 +38,17 @@ import java.util.function.BiFunction; | |||
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; | |||
@@ -116,12 +133,45 @@ public class HemfPlusDraw { | |||
} | |||
} | |||
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; | |||
@@ -148,18 +198,31 @@ public class HemfPlusDraw { | |||
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<>(); | |||
@@ -198,6 +261,29 @@ public class HemfPlusDraw { | |||
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 { | |||
@@ -302,6 +388,58 @@ public class HemfPlusDraw { | |||
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. */ | |||
@@ -347,12 +485,34 @@ public class HemfPlusDraw { | |||
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; | |||
@@ -366,16 +526,9 @@ public class HemfPlusDraw { | |||
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 | |||
@@ -390,6 +543,21 @@ public class HemfPlusDraw { | |||
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. */ | |||
@@ -403,13 +571,7 @@ public class HemfPlusDraw { | |||
} | |||
/** 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. | |||
@@ -453,6 +615,11 @@ public class HemfPlusDraw { | |||
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; | |||
@@ -615,7 +782,7 @@ public class HemfPlusDraw { | |||
} | |||
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); | |||
} | |||
} |
@@ -18,7 +18,9 @@ | |||
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; | |||
@@ -60,7 +62,7 @@ public class HemfPlusFont { | |||
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; | |||
@@ -70,7 +72,7 @@ public class HemfPlusFont { | |||
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(); | |||
@@ -96,5 +98,15 @@ public class HemfPlusFont { | |||
return size; | |||
} | |||
@Override | |||
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
} | |||
@Override | |||
public EmfPlusGraphicsVersion getGraphicsVersion() { | |||
return graphicsVersion; | |||
} | |||
} | |||
} |
@@ -20,6 +20,8 @@ package org.apache.poi.hemf.record.emfplus; | |||
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; | |||
@@ -85,6 +87,19 @@ public class HemfPlusHeader implements HemfPlusRecord { | |||
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; | |||
} | |||
@@ -97,6 +112,13 @@ public class HemfPlusHeader implements HemfPlusRecord { | |||
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{" + |
@@ -20,18 +20,45 @@ package org.apache.poi.hemf.record.emfplus; | |||
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readARGB; | |||
import java.awt.Color; | |||
import java.awt.Graphics2D; | |||
import java.awt.RenderingHints; | |||
import java.awt.Transparency; | |||
import java.awt.color.ColorSpace; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.awt.image.BufferedImage; | |||
import java.awt.image.ComponentColorModel; | |||
import java.awt.image.DataBuffer; | |||
import java.awt.image.DataBufferByte; | |||
import java.awt.image.PixelInterleavedSampleModel; | |||
import java.awt.image.Raster; | |||
import java.awt.image.WritableRaster; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.IOException; | |||
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. */ | |||
@@ -302,7 +329,7 @@ public class HemfPlusImage { | |||
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(); | |||
@@ -385,8 +412,193 @@ public class HemfPlusImage { | |||
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; | |||
@@ -434,6 +646,10 @@ public class HemfPlusImage { | |||
public EmfPlusObjectClamp getObjectClamp() { | |||
return objectClamp; | |||
} | |||
@Override | |||
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
} | |||
} | |||
} |
@@ -17,6 +17,7 @@ | |||
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; | |||
@@ -24,10 +25,10 @@ import java.awt.geom.Point2D; | |||
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; | |||
@@ -134,6 +135,12 @@ public class HemfPlusMisc { | |||
* 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); | |||
} | |||
} | |||
} | |||
/** | |||
@@ -174,6 +181,18 @@ public class HemfPlusMisc { | |||
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); | |||
} | |||
} | |||
/** | |||
@@ -185,6 +204,15 @@ public class HemfPlusMisc { | |||
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); | |||
} | |||
} | |||
/** |
@@ -18,8 +18,11 @@ | |||
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; | |||
@@ -29,6 +32,8 @@ import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId; | |||
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; | |||
@@ -107,7 +112,7 @@ public class HemfPlusObject { | |||
* 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 { | |||
/** | |||
@@ -126,6 +131,7 @@ public class HemfPlusObject { | |||
// for debugging | |||
private int objectId; | |||
private EmfPlusObjectData objectData; | |||
private List<EmfPlusObjectData> continuedObjectData; | |||
private int totalObjectSize; | |||
@Override | |||
@@ -174,10 +180,50 @@ public class HemfPlusObject { | |||
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 { | |||
@@ -195,5 +241,15 @@ public class HemfPlusObject { | |||
return dataSize; | |||
} | |||
@Override | |||
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
} | |||
@Override | |||
public EmfPlusGraphicsVersion getGraphicsVersion() { | |||
return graphicsVersion; | |||
} | |||
} | |||
} |
@@ -17,11 +17,15 @@ | |||
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; | |||
@@ -69,14 +73,14 @@ public class HemfPlusPath { | |||
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. | |||
@@ -124,6 +128,11 @@ public class HemfPlusPath { | |||
return size; | |||
} | |||
@Override | |||
public HemfPlusHeader.EmfPlusGraphicsVersion getGraphicsVersion() { | |||
return graphicsVersion; | |||
} | |||
public boolean isPointDashed(int index) { | |||
return POINT_TYPE_DASHED.isSet(pointTypes[index]); | |||
} | |||
@@ -144,6 +153,38 @@ public class HemfPlusPath { | |||
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(); | |||
} | |||
} | |||
} | |||
} | |||
@@ -23,12 +23,17 @@ import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readPointF; | |||
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; | |||
@@ -215,6 +220,13 @@ public class HemfPlusPen { | |||
} | |||
} | |||
@Internal | |||
public interface EmfPlusCustomLineCap { | |||
long init(LittleEndianInputStream leis) throws IOException; | |||
} | |||
public static class EmfPlusPen implements EmfPlusObjectData { | |||
@@ -283,16 +295,17 @@ public class HemfPlusPen { | |||
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; | |||
@@ -310,17 +323,16 @@ public class HemfPlusPen { | |||
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); | |||
} | |||
@@ -354,7 +366,7 @@ public class HemfPlusPen { | |||
// 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; | |||
} | |||
@@ -390,7 +402,7 @@ public class HemfPlusPen { | |||
} | |||
// 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(); | |||
} | |||
@@ -435,6 +447,11 @@ public class HemfPlusPen { | |||
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); | |||
@@ -450,153 +467,209 @@ public class HemfPlusPen { | |||
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; | |||
} | |||
} | |||
} |
@@ -20,7 +20,7 @@ package org.apache.poi.hemf.record.emfplus; | |||
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; | |||
@@ -45,4 +45,13 @@ public interface HemfPlusRecord { | |||
*/ | |||
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) { | |||
} | |||
} |
@@ -21,9 +21,11 @@ import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; | |||
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; | |||
@@ -92,12 +94,12 @@ public class HemfPlusRegion { | |||
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(); | |||
@@ -111,7 +113,15 @@ public class HemfPlusRegion { | |||
return size; | |||
} | |||
@Override | |||
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
} | |||
@Override | |||
public EmfPlusGraphicsVersion getGraphicsVersion() { | |||
return graphicsVersion; | |||
} | |||
} | |||
@@ -267,36 +267,7 @@ public class HemfEmbeddedIterator implements Iterator<HwmfEmbedded> { | |||
* 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 |
@@ -46,7 +46,6 @@ import org.apache.poi.util.Units; | |||
*/ | |||
@Internal | |||
public class HemfPicture implements Iterable<HemfRecord> { | |||
private final LittleEndianInputStream stream; | |||
private final List<HemfRecord> records = new ArrayList<>(); | |||
private boolean isParsed = false; | |||
@@ -96,32 +95,52 @@ public class HemfPicture implements Iterable<HemfRecord> { | |||
} | |||
/** | |||
* 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) { |
@@ -17,8 +17,10 @@ | |||
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; | |||
@@ -132,6 +134,9 @@ public class HwmfGraphics { | |||
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()); | |||
@@ -141,20 +146,22 @@ public class HwmfGraphics { | |||
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. | |||
@@ -602,7 +609,14 @@ public class HwmfGraphics { | |||
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); |
@@ -18,6 +18,7 @@ | |||
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; | |||
@@ -749,17 +750,17 @@ public class HwmfDraw { | |||
@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 | |||
@@ -772,5 +773,4 @@ public class HwmfDraw { | |||
Math.abs(bounds.getHeight()) | |||
); | |||
} | |||
} |
@@ -24,22 +24,14 @@ import static org.junit.Assert.assertNotNull; | |||
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; | |||
@@ -64,11 +56,10 @@ public class HemfPictureTest { | |||
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 | |||
@@ -78,15 +69,16 @@ public class HemfPictureTest { | |||
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; | |||
@@ -94,10 +86,14 @@ public class HemfPictureTest { | |||
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); | |||
@@ -129,9 +125,9 @@ public class HemfPictureTest { | |||
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++; | |||
} | |||
} | |||
@@ -154,17 +150,19 @@ public class HemfPictureTest { | |||
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); | |||
} | |||
@@ -186,7 +184,7 @@ public class HemfPictureTest { | |||
} | |||
} | |||
} | |||
} */ | |||
} | |||
private static int hashException(Throwable e) { | |||
StringBuilder sb = new StringBuilder(); | |||
@@ -222,7 +220,7 @@ public class HemfPictureTest { | |||
} | |||
return Files.newBufferedWriter(log, StandardCharsets.UTF_8, soo); | |||
} | |||
}*/ | |||
@Test | |||
public void testBasicWindows() throws Exception { |