- use Rectangle2D instead of Dimension2D for image bounds - fix shearing transformation - fix rendering of font attributes (bold/italic/...) - emf+: needs its own object table and properties table - emf+: add linear gradient handler - emf+: handle brush data of pens - wmf/emf/emf+: position right aligned text correctly - emf+: use emf+ instead of emf records in dual-mode - emf+: handle region data and operations correctly - emf/+: map font weight to awt font weight correctly git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1870566 13f79535-47bb-0310-9956-ffa450edef68tags/REL_4_1_2
@@ -17,7 +17,6 @@ | |||
package org.apache.poi.sl.draw; | |||
import java.awt.Dimension; | |||
import java.awt.Graphics; | |||
import java.awt.Graphics2D; | |||
import java.awt.Insets; | |||
@@ -255,10 +254,10 @@ public class BitmapImageRenderer implements ImageRenderer { | |||
} | |||
@Override | |||
public Dimension getDimension() { | |||
public Rectangle2D getBounds() { | |||
return (img == null) | |||
? new Dimension(0,0) | |||
: new Dimension(img.getWidth(),img.getHeight()); | |||
? new Rectangle2D.Double() | |||
: new Rectangle2D.Double(0, 0, img.getWidth(), img.getHeight()); | |||
} | |||
@Override | |||
@@ -316,7 +315,9 @@ public class BitmapImageRenderer implements ImageRenderer { | |||
AffineTransform at = new AffineTransform(sx, 0, 0, sy, tx, ty) ; | |||
Shape clipOld = graphics.getClip(); | |||
if (isClipped) graphics.clip(anchor.getBounds2D()); | |||
if (isClipped) { | |||
graphics.clip(anchor.getBounds2D()); | |||
} | |||
graphics.drawRenderedImage(img, at); | |||
graphics.setClip(clipOld); | |||
@@ -583,8 +583,7 @@ public class DrawPaint { | |||
* | |||
* @return an array containing the 3 HSL values. | |||
*/ | |||
private static double[] RGB2HSL(Color color) | |||
{ | |||
public static double[] RGB2HSL(Color color) { | |||
// Get RGB values in the range 0 - 1 | |||
float[] rgb = color.getRGBColorComponents( null ); |
@@ -46,6 +46,8 @@ public interface Drawable { | |||
case 9: return "FONT_MAP"; | |||
case 10: return "GSAVE"; | |||
case 11: return "GRESTORE"; | |||
case 12: return "CURRENT_SLIDE"; | |||
case 13: return "BUFFERED_IMAGE"; | |||
default: return "UNKNOWN_ID "+intKey(); | |||
} | |||
} |
@@ -27,6 +27,7 @@ import java.io.IOException; | |||
import java.io.InputStream; | |||
import org.apache.poi.common.usermodel.GenericRecord; | |||
import org.apache.poi.util.Dimension2DDouble; | |||
/** | |||
* Classes can implement this interfaces to support other formats, for | |||
@@ -105,10 +106,18 @@ public interface ImageRenderer { | |||
*/ | |||
Rectangle2D getNativeBounds(); | |||
/** | |||
* @return the bounds of the buffered image in pixel | |||
*/ | |||
Rectangle2D getBounds(); | |||
/** | |||
* @return the dimension of the buffered image in pixel | |||
*/ | |||
Dimension2D getDimension(); | |||
default Dimension2D getDimension() { | |||
Rectangle2D r = getBounds(); | |||
return new Dimension2DDouble(Math.abs(r.getWidth()), Math.abs(r.getHeight())); | |||
} | |||
/** | |||
* @param alpha the alpha [0..1] to be added to the image (possibly already containing an alpha channel) |
@@ -17,6 +17,7 @@ | |||
package org.apache.poi.util; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Rectangle2D; | |||
public class Units { | |||
/** | |||
@@ -158,6 +159,22 @@ public class Units { | |||
return new Dimension2DDouble(width, height); | |||
} | |||
public static Rectangle2D pointsToPixel(Rectangle2D pointsDim) { | |||
double x = pointsDim.getX() * PIXEL_DPI / POINT_DPI; | |||
double y = pointsDim.getY() * PIXEL_DPI / POINT_DPI; | |||
double width = pointsDim.getWidth() * PIXEL_DPI / POINT_DPI; | |||
double height = pointsDim.getHeight() * PIXEL_DPI / POINT_DPI; | |||
return new Rectangle2D.Double(x, y, width, height); | |||
} | |||
public static Rectangle2D pixelToPoints(Rectangle2D pointsDim) { | |||
double x = pointsDim.getX() * POINT_DPI / PIXEL_DPI; | |||
double y = pointsDim.getY() * POINT_DPI / PIXEL_DPI; | |||
double width = pointsDim.getWidth() * POINT_DPI / PIXEL_DPI; | |||
double height = pointsDim.getHeight() * POINT_DPI / PIXEL_DPI; | |||
return new Rectangle2D.Double(x, y, width, height); | |||
} | |||
public static int charactersToEMU(double characters) { | |||
return (int) characters * EMU_PER_CHARACTER; | |||
} |
@@ -42,7 +42,6 @@ import org.apache.batik.util.XMLResourceDescriptor; | |||
import org.apache.poi.sl.draw.Drawable; | |||
import org.apache.poi.sl.draw.ImageRenderer; | |||
import org.apache.poi.sl.usermodel.PictureData; | |||
import org.apache.poi.util.Dimension2DDouble; | |||
import org.w3c.dom.Document; | |||
public class SVGImageRenderer implements ImageRenderer { | |||
@@ -76,9 +75,8 @@ public class SVGImageRenderer implements ImageRenderer { | |||
} | |||
@Override | |||
public Dimension2D getDimension() { | |||
Rectangle2D r = svgRoot.getPrimitiveBounds(); | |||
return new Dimension2DDouble(Math.ceil(r.getWidth()), Math.ceil(r.getHeight())); | |||
public Rectangle2D getBounds() { | |||
return svgRoot.getPrimitiveBounds(); | |||
} | |||
@Override |
@@ -17,10 +17,13 @@ | |||
package org.apache.poi.hemf.draw; | |||
import java.awt.Color; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Path2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.function.BiConsumer; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle; | |||
@@ -47,6 +50,10 @@ public class HemfDrawProperties extends HwmfDrawProperties { | |||
private final List<AffineTransform> transXForm = new ArrayList<>(); | |||
private final List<TransOperand> transOper = new ArrayList<>(); | |||
private Rectangle2D brushRect; | |||
private List<? extends Map.Entry<Float,Color>> brushColorsV; | |||
private List<? extends Map.Entry<Float,Color>> brushColorsH; | |||
public HemfDrawProperties() { | |||
} | |||
@@ -60,6 +67,15 @@ public class HemfDrawProperties extends HwmfDrawProperties { | |||
emfPlusImage = other.emfPlusImage; | |||
transXForm.addAll(other.transXForm); | |||
transOper.addAll(other.transOper); | |||
if (other.brushRect != null) { | |||
brushRect = (Rectangle2D)other.brushRect.clone(); | |||
} | |||
if (other.brushColorsV != null) { | |||
brushColorsV = new ArrayList<>(other.brushColorsV); | |||
} | |||
if (other.brushColorsH != null) { | |||
brushColorsH = new ArrayList<>(other.brushColorsH); | |||
} | |||
} | |||
/** | |||
@@ -139,4 +155,28 @@ public class HemfDrawProperties extends HwmfDrawProperties { | |||
List<TransOperand> getTransOper() { | |||
return transOper; | |||
} | |||
public Rectangle2D getBrushRect() { | |||
return brushRect; | |||
} | |||
public void setBrushRect(Rectangle2D brushRect) { | |||
this.brushRect = brushRect; | |||
} | |||
public List<? extends Map.Entry<Float, Color>> getBrushColorsV() { | |||
return brushColorsV; | |||
} | |||
public void setBrushColorsV(List<? extends Map.Entry<Float, Color>> brushColorsV) { | |||
this.brushColorsV = brushColorsV; | |||
} | |||
public List<? extends Map.Entry<Float, Color>> getBrushColorsH() { | |||
return brushColorsH; | |||
} | |||
public void setBrushColorsH(List<? extends Map.Entry<Float, Color>> brushColorsH) { | |||
this.brushColorsH = brushColorsH; | |||
} | |||
} |
@@ -20,16 +20,25 @@ package org.apache.poi.hemf.draw; | |||
import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_NULL; | |||
import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID; | |||
import java.awt.AlphaComposite; | |||
import java.awt.Color; | |||
import java.awt.Composite; | |||
import java.awt.Graphics2D; | |||
import java.awt.LinearGradientPaint; | |||
import java.awt.MultipleGradientPaint; | |||
import java.awt.Paint; | |||
import java.awt.Shape; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Path2D; | |||
import java.awt.geom.Point2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.util.HashMap; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.NoSuchElementException; | |||
import java.util.function.Consumer; | |||
import java.util.stream.Stream; | |||
import org.apache.poi.hemf.draw.HemfDrawProperties.TransOperand; | |||
import org.apache.poi.hemf.record.emf.HemfComment.EmfComment; | |||
@@ -37,7 +46,9 @@ import org.apache.poi.hemf.record.emf.HemfRecord; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusRecord; | |||
import org.apache.poi.hwmf.draw.HwmfDrawProperties; | |||
import org.apache.poi.hwmf.draw.HwmfGraphics; | |||
import org.apache.poi.hwmf.record.HwmfBrushStyle; | |||
import org.apache.poi.hwmf.record.HwmfColorRef; | |||
import org.apache.poi.hwmf.record.HwmfMisc; | |||
import org.apache.poi.hwmf.record.HwmfObjectTableEntry; | |||
import org.apache.poi.hwmf.record.HwmfPenStyle; | |||
import org.apache.poi.util.Internal; | |||
@@ -58,11 +69,14 @@ public class HemfGraphics extends HwmfGraphics { | |||
private static final HwmfColorRef BLACK = new HwmfColorRef(Color.BLACK); | |||
private EmfRenderState renderState = EmfRenderState.INITIAL; | |||
private final Map<Integer,HwmfObjectTableEntry> plusObjectTable = new HashMap<>(); | |||
private final Map<Integer,HemfDrawProperties> plusPropStack = new HashMap<>(); | |||
public HemfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) { | |||
super(graphicsCtx,bbox); | |||
// add dummy entry for object ind ex 0, as emf is 1-based | |||
objectIndexes.set(0); | |||
getProperties().setBkMode(HwmfMisc.WmfSetBkMode.HwmfBkMode.TRANSPARENT); | |||
} | |||
@Override | |||
@@ -87,14 +101,8 @@ public class HemfGraphics extends HwmfGraphics { | |||
public void draw(HemfRecord r) { | |||
switch (getRenderState()) { | |||
default: | |||
case EMF_DCONTEXT: | |||
// This state specifies that subsequent EMF records encountered in the metafile SHOULD be processed. | |||
// EMF records cease being processed when the next EMF+ record is encountered. | |||
if (r instanceof EmfComment) { | |||
setRenderState(EmfRenderState.EMFPLUS_ONLY); | |||
} | |||
r.draw(this); | |||
break; | |||
case INITIAL: | |||
r.draw(this); | |||
break; | |||
@@ -108,8 +116,6 @@ public class HemfGraphics extends HwmfGraphics { | |||
r.draw(this); | |||
} | |||
break; | |||
default: | |||
break; | |||
} | |||
} | |||
@@ -171,10 +177,7 @@ public class HemfGraphics extends HwmfGraphics { | |||
/** | |||
* Adds or sets an record of type {@link HwmfObjectTableEntry} to the object table. | |||
* If the {@code index} is less than 1, the method acts the same as | |||
* {@link HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry)}, otherwise the | |||
* index is used to access the object table. | |||
* As the table is filled successively, the index must be between 1 and size+1 | |||
* The index must be > 0 | |||
* | |||
* @param entry the record to be stored | |||
* @param index the index to be overwritten, regardless if its content was unset before | |||
@@ -182,42 +185,74 @@ public class HemfGraphics extends HwmfGraphics { | |||
* @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry) | |||
*/ | |||
public void addObjectTableEntry(HwmfObjectTableEntry entry, int index) { | |||
checkTableEntryIndex(index); | |||
// in EMF the index must > 0 | |||
if (index < 1) { | |||
throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index); | |||
} | |||
objectIndexes.set(index); | |||
objectTable.put(index, entry); | |||
} | |||
/** | |||
* Gets a record which was registered earliser | |||
* Adds or sets an record of type {@link HwmfObjectTableEntry} to the plus object table. | |||
* The index must be in the range [0..63] | |||
* | |||
* @param entry the record to be stored | |||
* @param index the index to be overwritten, regardless if its content was unset before | |||
* | |||
* @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry) | |||
*/ | |||
public void addPlusObjectTableEntry(HwmfObjectTableEntry entry, int index) { | |||
// in EMF+ the index must be between 0 and 63 | |||
if (index < 0 || index > 63) { | |||
throw new IndexOutOfBoundsException("Object table entry index in EMF+ must be [0..63] - invalid index: "+index); | |||
} | |||
plusObjectTable.put(index, entry); | |||
} | |||
/** | |||
* Gets a record which was registered earlier | |||
* @param index the record index | |||
* @return the record or {@code null} if it doesn't exist | |||
*/ | |||
public HwmfObjectTableEntry getObjectTableEntry(int index) { | |||
checkTableEntryIndex(index); | |||
// in EMF the index must > 0 | |||
if (index < 1) { | |||
throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index); | |||
} | |||
return objectTable.get(index); | |||
} | |||
private void checkTableEntryIndex(int index) { | |||
if (renderState != EmfRenderState.EMFPLUS_ONLY && renderState != EmfRenderState.EMF_DCONTEXT) { | |||
// in EMF the index must > 0 | |||
if (index < 1) { | |||
throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index); | |||
} | |||
} 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); | |||
} | |||
public HwmfObjectTableEntry getPlusObjectTableEntry(int index) { | |||
// in EMF+ the index must be between 0 and 63 | |||
if (index < 0 || index > 63) { | |||
throw new IndexOutOfBoundsException("Object table entry index in EMF+ must be [0..63] - invalid index: "+index); | |||
} | |||
return plusObjectTable.get(index); | |||
} | |||
@Override | |||
public void applyObjectTableEntry(int index) { | |||
if ((index & 0x80000000) != 0) { | |||
selectStockObject(index); | |||
} else { | |||
super.applyObjectTableEntry(index); | |||
HwmfObjectTableEntry ote = objectTable.get(index); | |||
if (ote == null) { | |||
throw new NoSuchElementException("EMF reference exception - object table entry on index "+index+" was deleted before."); | |||
} | |||
ote.applyObject(this); | |||
} | |||
} | |||
public void applyPlusObjectTableEntry(int index) { | |||
if ((index & 0x80000000) != 0) { | |||
selectStockObject(index); | |||
} else { | |||
HwmfObjectTableEntry ote = plusObjectTable.get(index); | |||
if (ote == null) { | |||
throw new NoSuchElementException("EMF+ reference exception - plus object table entry on index "+index+" was deleted before."); | |||
} | |||
ote.applyObject(this); | |||
} | |||
} | |||
@@ -351,4 +386,82 @@ public class HemfGraphics extends HwmfGraphics { | |||
graphicsCtx.setTransform(tx); | |||
} | |||
@Override | |||
public void fill(Shape shape) { | |||
HemfDrawProperties prop = getProperties(); | |||
Composite old = graphicsCtx.getComposite(); | |||
graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); | |||
if (prop.getBrushStyle() != HwmfBrushStyle.BS_NULL) { | |||
if (prop.getBkMode() == HwmfMisc.WmfSetBkMode.HwmfBkMode.OPAQUE) { | |||
graphicsCtx.setPaint(prop.getBackgroundColor().getColor()); | |||
graphicsCtx.fill(shape); | |||
} | |||
graphicsCtx.setPaint(getFill()); | |||
graphicsCtx.fill(shape); | |||
} | |||
graphicsCtx.setComposite(old); | |||
} | |||
@Override | |||
protected Paint getLinearGradient() { | |||
HemfDrawProperties prop = getProperties(); | |||
Rectangle2D rect = prop.getBrushRect(); | |||
List<? extends Map.Entry<Float, Color>> colorsH = prop.getBrushColorsH(); | |||
assert(rect != null && colorsH != null); | |||
// TODO: handle ColorsV list with a custom GradientPaint | |||
// for an idea on how to handle 2d-gradients google "bilinear color interpolation". | |||
// basically use two linear interpolations for x/y or vertical/horizontal axis. | |||
// the resulting two colors need to be interpolated by 50%. | |||
return new LinearGradientPaint( | |||
new Point2D.Double(rect.getMinX(),rect.getCenterY()), | |||
new Point2D.Double(rect.getMaxX(),rect.getCenterY()), | |||
toArray(colorsH.stream().map(Map.Entry::getKey), colorsH.size()), | |||
colorsH.stream().map(Map.Entry::getValue).toArray(Color[]::new), | |||
MultipleGradientPaint.CycleMethod.NO_CYCLE, | |||
MultipleGradientPaint.ColorSpaceType.SRGB, | |||
prop.getBrushTransform() | |||
); | |||
} | |||
private static float[] toArray(Stream<? extends Number> numbers, int size) { | |||
float[] arr = new float[size]; | |||
final int[] i = {0}; | |||
numbers.forEach(n -> arr[i[0]++] = n.floatValue()); | |||
return arr; | |||
} | |||
/** | |||
* Saves the current properties to the plus stack | |||
*/ | |||
public void savePlusProperties(int index) { | |||
final HemfDrawProperties p = getProperties(); | |||
assert(p != null); | |||
p.setTransform(graphicsCtx.getTransform()); | |||
p.setClip(graphicsCtx.getClip()); | |||
plusPropStack.put(index,p); | |||
prop = newProperties(p); | |||
} | |||
/** | |||
* Restores the properties from the plus stack | |||
* | |||
* @param index the index of the previously saved properties | |||
*/ | |||
public void restorePlusProperties(int index) { | |||
if (!plusPropStack.containsKey(index)) { | |||
return; | |||
} | |||
prop = new HemfDrawProperties(plusPropStack.get(index)); | |||
graphicsCtx.setTransform(prop.getTransform()); | |||
graphicsCtx.setClip(prop.getClip()); | |||
} | |||
} |
@@ -19,11 +19,9 @@ package org.apache.poi.hemf.draw; | |||
import static org.apache.poi.hwmf.draw.HwmfImageRenderer.getOuterBounds; | |||
import java.awt.Dimension; | |||
import java.awt.Graphics2D; | |||
import java.awt.Insets; | |||
import java.awt.RenderingHints; | |||
import java.awt.Shape; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.awt.image.BufferedImage; | |||
@@ -33,6 +31,7 @@ import java.io.InputStream; | |||
import org.apache.poi.common.usermodel.GenericRecord; | |||
import org.apache.poi.hemf.usermodel.HemfPicture; | |||
import org.apache.poi.hwmf.draw.HwmfGraphicsState; | |||
import org.apache.poi.hwmf.draw.HwmfImageRenderer; | |||
import org.apache.poi.sl.draw.BitmapImageRenderer; | |||
import org.apache.poi.sl.draw.EmbeddedExtractor; | |||
@@ -66,11 +65,6 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor { | |||
image = new HemfPicture(new ByteArrayInputStream(data)); | |||
} | |||
@Override | |||
public Dimension2D getDimension() { | |||
return Units.pointsToPixel(image == null ? new Dimension() : image.getSize()); | |||
} | |||
@Override | |||
public void setAlpha(double alpha) { | |||
this.alpha = alpha; | |||
@@ -110,20 +104,20 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor { | |||
return false; | |||
} | |||
boolean isClipped = true; | |||
if (clip == null) { | |||
isClipped = false; | |||
clip = new Insets(0,0,0,0); | |||
} | |||
Shape clipOld = graphics.getClip(); | |||
if (isClipped) { | |||
graphics.clip(anchor); | |||
} | |||
HwmfGraphicsState graphicsState = new HwmfGraphicsState(); | |||
graphicsState.backup(graphics); | |||
image.draw(graphics, getOuterBounds(anchor, clip)); | |||
try { | |||
if (clip != null) { | |||
graphics.clip(anchor); | |||
} else { | |||
clip = new Insets(0, 0, 0, 0); | |||
} | |||
graphics.setClip(clipOld); | |||
image.draw(graphics, getOuterBounds(anchor, clip)); | |||
} finally { | |||
graphicsState.restore(graphics); | |||
} | |||
return true; | |||
} | |||
@@ -142,4 +136,9 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor { | |||
public Rectangle2D getNativeBounds() { | |||
return image.getBounds(); | |||
} | |||
@Override | |||
public Rectangle2D getBounds() { | |||
return Units.pointsToPixel(image == null ? new Rectangle2D.Double() : image.getBoundsInPoints()); | |||
} | |||
} |
@@ -29,6 +29,7 @@ import java.util.function.Supplier; | |||
import org.apache.poi.common.usermodel.GenericRecord; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusRecord; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusRecordIterator; | |||
import org.apache.poi.hwmf.usermodel.HwmfPicture; | |||
@@ -300,6 +301,9 @@ public class HemfComment { | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
// This state specifies that subsequent EMF records encountered in the metafile SHOULD be processed. | |||
// EMF records cease being processed when the next EMF+ record is encountered. | |||
ctx.setRenderState(EmfRenderState.EMFPLUS_ONLY); | |||
records.forEach(ctx::draw); | |||
} | |||
@@ -862,7 +862,9 @@ public class HemfFill { | |||
// m12 (translateY) = eDy (The vertical translation component, in logical units.) | |||
double m12 = leis.readFloat(); | |||
xform.setTransform(m00, m10, m01, m11, m02, m12); | |||
// TODO: not sure, why the shearing has to be inverted here, | |||
// probably because of the different world/user space transformation | |||
xform.setTransform(m00, -m10, -m01, m11, m02, m12); | |||
if (xform.isIdentity()) { | |||
xform.setToIdentity(); |
@@ -492,6 +492,30 @@ public class HemfFont extends HwmfFont { | |||
); | |||
} | |||
public void setHeight(double height) { | |||
this.height = height; | |||
} | |||
public void setWeight(int weight) { | |||
this.weight = weight; | |||
} | |||
public void setItalic(boolean italic) { | |||
this.italic = italic; | |||
} | |||
public void setUnderline(boolean underline) { | |||
this.underline = underline; | |||
} | |||
public void setStrikeOut(boolean strikeOut) { | |||
this.strikeOut = strikeOut; | |||
} | |||
public void setTypeface(String typeface) { | |||
this.facename = typeface; | |||
} | |||
@Override | |||
protected int readString(LittleEndianInputStream leis, StringBuilder sb, int limit) throws IOException { | |||
sb.setLength(0); |
@@ -559,6 +559,9 @@ public class HemfMisc { | |||
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { | |||
final int startIdx = leis.getReadIndex(); | |||
// An unsigned integer that specifies the index of the extended logical pen object in | |||
// the EMF object table. This index MUST be saved so that this object can be | |||
// reused or modified. | |||
penIndex = (int) leis.readUInt(); | |||
// A 32-bit unsigned integer that specifies the offset from the start of this |
@@ -72,10 +72,12 @@ public class HemfRecordIterator implements Iterator<HemfRecord> { | |||
final HemfRecord record = type.constructor.get(); | |||
try { | |||
long remBytes = recordSize-HEADER_SIZE; | |||
long remBytes = recordSize - HEADER_SIZE; | |||
long readBytes = record.init(stream, remBytes, recordId); | |||
assert (readBytes <= remBytes); | |||
stream.skipFully((int)(remBytes-readBytes)); | |||
stream.skipFully((int) (remBytes - readBytes)); | |||
} catch (RecordFormatException e) { | |||
throw e; | |||
} catch (IOException|RuntimeException e) { | |||
throw new RecordFormatException(e); | |||
} |
@@ -29,12 +29,18 @@ import java.awt.geom.Rectangle2D; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.IOException; | |||
import java.util.AbstractMap; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.function.BiFunction; | |||
import java.util.function.Consumer; | |||
import java.util.function.Function; | |||
import java.util.function.Supplier; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.IntStream; | |||
import org.apache.poi.common.usermodel.GenericRecord; | |||
import org.apache.poi.hemf.draw.HemfDrawProperties; | |||
@@ -47,6 +53,7 @@ import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusPath.EmfPlusPath; | |||
import org.apache.poi.hwmf.record.HwmfBrushStyle; | |||
import org.apache.poi.hwmf.record.HwmfColorRef; | |||
import org.apache.poi.sl.draw.DrawPaint; | |||
import org.apache.poi.util.BitField; | |||
import org.apache.poi.util.BitFieldFactory; | |||
import org.apache.poi.util.GenericRecordJsonWriter; | |||
@@ -310,7 +317,19 @@ public class HemfPlusBrush { | |||
long init(LittleEndianInputStream leis, long dataSize) throws IOException; | |||
/** | |||
* Apply brush data to graphics properties | |||
* @param ctx the graphics context | |||
* @param continuedObjectData the list continued object data | |||
*/ | |||
void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData); | |||
/** | |||
* Apply brush data to pen properties | |||
* @param ctx the graphics context | |||
* @param continuedObjectData the list continued object data | |||
*/ | |||
void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData); | |||
} | |||
/** The EmfPlusBrush object specifies a graphics brush for filling regions. */ | |||
@@ -347,6 +366,13 @@ public class HemfPlusBrush { | |||
brushData.applyObject(ctx, continuedObjectData); | |||
} | |||
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
EmfPlusBrushData brushData = getBrushData(continuedObjectData); | |||
brushData.applyPen(ctx, continuedObjectData); | |||
} | |||
@Override | |||
public EmfPlusGraphicsVersion getGraphicsVersion() { | |||
return graphicsVersion; | |||
@@ -372,7 +398,6 @@ public class HemfPlusBrush { | |||
return brushData; | |||
} | |||
public byte[] getRawData(List<? extends EmfPlusObjectData> continuedObjectData) { | |||
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |||
try { | |||
@@ -416,11 +441,17 @@ public class HemfPlusBrush { | |||
@Override | |||
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
prop.setBackgroundColor(new HwmfColorRef(solidColor)); | |||
prop.setBrushColor(new HwmfColorRef(solidColor)); | |||
prop.setBrushTransform(null); | |||
prop.setBrushStyle(HwmfBrushStyle.BS_SOLID); | |||
} | |||
@Override | |||
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
prop.setPenColor(new HwmfColorRef(solidColor)); | |||
} | |||
@Override | |||
public String toString() { | |||
return GenericRecordJsonWriter.marshal(this); | |||
@@ -457,6 +488,12 @@ public class HemfPlusBrush { | |||
prop.setEmfPlusBrushHatch(style); | |||
} | |||
@Override | |||
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
prop.setPenColor(new HwmfColorRef(foreColor)); | |||
} | |||
@Override | |||
public String toString() { | |||
return GenericRecordJsonWriter.marshal(this); | |||
@@ -484,12 +521,15 @@ public class HemfPlusBrush { | |||
private Rectangle2D rect = new Rectangle2D.Double(); | |||
private Color startColor, endColor; | |||
private AffineTransform transform; | |||
private double[] positions; | |||
private float[] positions; | |||
private Color[] blendColors; | |||
private double[] positionsV; | |||
private double[] blendFactorsV; | |||
private double[] positionsH; | |||
private double[] blendFactorsH; | |||
private float[] positionsV; | |||
private float[] blendFactorsV; | |||
private float[] positionsH; | |||
private float[] blendFactorsH; | |||
private static int[] FLAG_MASKS = { 0x02, 0x04, 0x08, 0x10, 0x80 }; | |||
private static String[] FLAG_NAMES = { "TRANSFORM", "PRESET_COLORS", "BLEND_FACTORS_H", "BLEND_FACTORS_V", "BRUSH_DATA_IS_GAMMA_CORRECTED" }; | |||
@Override | |||
public long init(LittleEndianInputStream leis, long dataSize) throws IOException { | |||
@@ -518,16 +558,13 @@ public class HemfPlusBrush { | |||
size += readXForm(leis, (transform = new AffineTransform())); | |||
} | |||
final boolean isPreset = PRESET_COLORS.isSet(dataFlags); | |||
final boolean blendH = BLEND_FACTORS_H.isSet(dataFlags); | |||
final boolean blendV = BLEND_FACTORS_V.isSet(dataFlags); | |||
if (isPreset && (blendH || blendV)) { | |||
if (isPreset() && (isBlendH() || isBlendV())) { | |||
throw new RuntimeException("invalid combination of preset colors and blend factors v/h"); | |||
} | |||
size += (isPreset) ? readColors(leis, d -> positions = d, c -> blendColors = c) : 0; | |||
size += (blendV) ? readFactors(leis, d -> positionsV = d, f -> blendFactorsV = f) : 0; | |||
size += (blendH) ? readFactors(leis, d -> positionsH = d, f -> blendFactorsH = f) : 0; | |||
size += (isPreset()) ? readColors(leis, d -> positions = d, c -> blendColors = c) : 0; | |||
size += (isBlendV()) ? readFactors(leis, d -> positionsV = d, f -> blendFactorsV = f) : 0; | |||
size += (isBlendH()) ? readFactors(leis, d -> positionsH = d, f -> blendFactorsH = f) : 0; | |||
return size; | |||
} | |||
@@ -535,7 +572,26 @@ public class HemfPlusBrush { | |||
@Override | |||
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
// TODO: implement | |||
prop.setBrushStyle(HwmfBrushStyle.BS_LINEAR_GRADIENT); | |||
prop.setBrushRect(rect); | |||
prop.setBrushTransform(transform); | |||
// Preset colors and BlendH/V are mutual exclusive | |||
if (isPreset()) { | |||
setColorProps(prop::setBrushColorsH, positions, this::getBlendColorAt); | |||
} else { | |||
setColorProps(prop::setBrushColorsH, positionsH, this::getBlendHColorAt); | |||
} | |||
setColorProps(prop::setBrushColorsV, positionsV, this::getBlendVColorAt); | |||
if (!(isPreset() || isBlendH() || isBlendV())) { | |||
prop.setBrushColorsH(Arrays.asList(kv(0f,startColor), kv(1f,endColor))); | |||
} | |||
} | |||
@Override | |||
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
} | |||
@Override | |||
@@ -551,7 +607,7 @@ public class HemfPlusBrush { | |||
@Override | |||
public Map<String, Supplier<?>> getGenericProperties() { | |||
final Map<String,Supplier<?>> m = new LinkedHashMap<>(); | |||
m.put("flags", () -> dataFlags); | |||
m.put("flags", GenericRecordUtil.getBitsAsString(() -> dataFlags, FLAG_MASKS, FLAG_NAMES)); | |||
m.put("wrapMode", () -> wrapMode); | |||
m.put("rect", () -> rect); | |||
m.put("startColor", () -> startColor); | |||
@@ -565,6 +621,67 @@ public class HemfPlusBrush { | |||
m.put("blendFactorsH", () -> blendFactorsH); | |||
return Collections.unmodifiableMap(m); | |||
} | |||
private boolean isPreset() { | |||
return PRESET_COLORS.isSet(dataFlags); | |||
} | |||
private boolean isBlendH() { | |||
return BLEND_FACTORS_H.isSet(dataFlags); | |||
} | |||
private boolean isBlendV() { | |||
return BLEND_FACTORS_V.isSet(dataFlags); | |||
} | |||
private Map.Entry<Float,Color> getBlendColorAt(int index) { | |||
return kv(positions[index], blendColors[index]); | |||
} | |||
private Map.Entry<Float,Color> getBlendHColorAt(int index) { | |||
return kv(positionsH[index],interpolateColors(blendFactorsH[index])); | |||
} | |||
private Map.Entry<Float,Color> getBlendVColorAt(int index) { | |||
return kv(positionsV[index],interpolateColors(blendFactorsV[index])); | |||
} | |||
private static Map.Entry<Float,Color> kv(Float position, Color color) { | |||
return new AbstractMap.SimpleEntry<>(position, color); | |||
} | |||
private static void setColorProps( | |||
Consumer<List<? extends Map.Entry<Float, Color>>> setter, float[] positions, Function<Integer,? extends Map.Entry<Float, Color>> sup) { | |||
if (positions == null) { | |||
setter.accept(null); | |||
} else { | |||
setter.accept(IntStream.range(0, positions.length).boxed().map(sup).collect(Collectors.toList())); | |||
} | |||
} | |||
private Color interpolateColors(final double factor) { | |||
// https://stackoverflow.com/questions/1416560/hsl-interpolation | |||
final double[] hslStart = DrawPaint.RGB2HSL(startColor); | |||
final double[] hslStop = DrawPaint.RGB2HSL(endColor); | |||
BiFunction<Number,Number,Double> linearInter = (start, stop) -> | |||
start.doubleValue()+(stop.doubleValue()-start.doubleValue())*factor; | |||
double alpha = linearInter.apply(startColor.getAlpha(),endColor.getAlpha()); | |||
double sat = linearInter.apply(hslStart[1],hslStop[1]); | |||
double lum = linearInter.apply(hslStart[2],hslStop[2]); | |||
double hue1 = (hslStart[0]+hslStop[0])/2.; | |||
double hue2 = (hslStart[0]+hslStop[0]+360.)/2.; | |||
Function<Double,Double> hueDelta = (hue) -> | |||
Math.min(Math.abs(hslStart[0]-hue), Math.abs(hslStop[0]-hue)); | |||
double hue = hueDelta.apply(hue1) < hueDelta.apply(hue2) ? hue1 : hue2; | |||
return DrawPaint.HSL2RGB(hue, sat, lum, alpha/255.); | |||
} | |||
} | |||
/** The EmfPlusPathGradientBrushData object specifies a path gradient for a graphics brush. */ | |||
@@ -577,9 +694,9 @@ public class HemfPlusBrush { | |||
private EmfPlusPath boundaryPath; | |||
private Point2D[] boundaryPoints; | |||
private AffineTransform transform; | |||
private double[] positions; | |||
private float[] positions; | |||
private Color[] blendColors; | |||
private double[] blendFactorsH; | |||
private float[] blendFactorsH; | |||
private Double focusScaleX, focusScaleY; | |||
@Override | |||
@@ -597,8 +714,12 @@ public class HemfPlusBrush { | |||
// that appears at the center point of the brush. The color of the brush changes gradually from the | |||
// boundary color to the center color as it moves from the boundary to the center point. | |||
centerColor = readARGB(leis.readInt()); | |||
int size = 3*LittleEndianConsts.INT_SIZE; | |||
if (wrapMode == null) { | |||
return size; | |||
} | |||
size += readPointF(leis, centerPoint); | |||
// An unsigned 32-bit integer that specifies the number of colors specified in the SurroundingColor field. | |||
@@ -608,10 +729,10 @@ public class HemfPlusBrush { | |||
// An array of SurroundingColorCount EmfPlusARGB objects that specify the colors for discrete points on the | |||
// boundary of the brush. | |||
surroundingColor = new Color[colorCount]; | |||
for (int i=0; i<colorCount; i++) { | |||
for (int i = 0; i < colorCount; i++) { | |||
surroundingColor[i] = readARGB(leis.readInt()); | |||
} | |||
size += (colorCount+1) * LittleEndianConsts.INT_SIZE; | |||
size += (colorCount + 1) * LittleEndianConsts.INT_SIZE; | |||
// The boundary of the path gradient brush, which is specified by either a path or a closed cardinal spline. | |||
// If the BrushDataPath flag is set in the BrushDataFlags field, this field MUST contain an | |||
@@ -677,6 +798,11 @@ public class HemfPlusBrush { | |||
} | |||
@Override | |||
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
} | |||
@Override | |||
public String toString() { | |||
return GenericRecordJsonWriter.marshal(this); | |||
@@ -747,6 +873,11 @@ public class HemfPlusBrush { | |||
prop.setBrushTransform(transform); | |||
} | |||
@Override | |||
public void applyPen(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
} | |||
@Override | |||
public String toString() { | |||
return GenericRecordJsonWriter.marshal(this); | |||
@@ -768,11 +899,11 @@ public class HemfPlusBrush { | |||
} | |||
} | |||
private static int readPositions(LittleEndianInputStream leis, Consumer<double[]> pos) { | |||
private static int readPositions(LittleEndianInputStream leis, Consumer<float[]> pos) { | |||
final int count = leis.readInt(); | |||
int size = LittleEndianConsts.INT_SIZE; | |||
double[] positions = new double[count]; | |||
float[] positions = new float[count]; | |||
for (int i=0; i<count; i++) { | |||
positions[i] = leis.readFloat(); | |||
size += LittleEndianConsts.INT_SIZE; | |||
@@ -782,7 +913,7 @@ public class HemfPlusBrush { | |||
return size; | |||
} | |||
private static int readColors(LittleEndianInputStream leis, Consumer<double[]> pos, Consumer<Color[]> cols) { | |||
private static int readColors(LittleEndianInputStream leis, Consumer<float[]> pos, Consumer<Color[]> cols) { | |||
int[] count = { 0 }; | |||
int size = readPositions(leis, p -> { count[0] = p.length; pos.accept(p); }); | |||
Color[] colors = new Color[count[0]]; | |||
@@ -793,10 +924,10 @@ public class HemfPlusBrush { | |||
return size + colors.length * LittleEndianConsts.INT_SIZE; | |||
} | |||
private static int readFactors(LittleEndianInputStream leis, Consumer<double[]> pos, Consumer<double[]> facs) { | |||
private static int readFactors(LittleEndianInputStream leis, Consumer<float[]> pos, Consumer<float[]> facs) { | |||
int[] count = { 0 }; | |||
int size = readPositions(leis, p -> { count[0] = p.length; pos.accept(p); }); | |||
double[] factors = new double[count[0]]; | |||
float[] factors = new float[count[0]]; | |||
for (int i=0; i<factors.length; i++) { | |||
factors[i] = leis.readFloat(); | |||
} |
@@ -22,7 +22,6 @@ import static org.apache.poi.util.GenericRecordUtil.getBitsAsString; | |||
import java.awt.Color; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Area; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Path2D; | |||
import java.awt.geom.Point2D; | |||
import java.awt.geom.Rectangle2D; | |||
@@ -34,9 +33,11 @@ import java.util.Collections; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.PrimitiveIterator.OfInt; | |||
import java.util.function.BiFunction; | |||
import java.util.function.Supplier; | |||
import org.apache.commons.codec.Charsets; | |||
import org.apache.commons.math3.linear.LUDecomposition; | |||
import org.apache.commons.math3.linear.MatrixUtils; | |||
import org.apache.commons.math3.linear.RealMatrix; | |||
@@ -47,7 +48,9 @@ import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId; | |||
import org.apache.poi.hwmf.record.HwmfBrushStyle; | |||
import org.apache.poi.hwmf.record.HwmfColorRef; | |||
import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode; | |||
import org.apache.poi.hwmf.record.HwmfPenStyle; | |||
import org.apache.poi.hwmf.record.HwmfTernaryRasterOp; | |||
import org.apache.poi.hwmf.record.HwmfText; | |||
import org.apache.poi.sl.draw.ImageRenderer; | |||
import org.apache.poi.util.BitField; | |||
import org.apache.poi.util.BitFieldFactory; | |||
@@ -165,7 +168,7 @@ public class HemfPlusDraw { | |||
prop.setBrushStyle(HwmfBrushStyle.BS_SOLID); | |||
prop.setBrushColor(new HwmfColorRef(getSolidColor())); | |||
} else { | |||
ctx.applyObjectTableEntry(getBrushId()); | |||
ctx.applyPlusObjectTableEntry(getBrushId()); | |||
} | |||
} | |||
} | |||
@@ -208,8 +211,8 @@ public class HemfPlusDraw { | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
ctx.applyObjectTableEntry(penId); | |||
ctx.applyObjectTableEntry(getObjectId()); | |||
ctx.applyPlusObjectTableEntry(penId); | |||
ctx.applyPlusObjectTableEntry(getObjectId()); | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
final Path2D path = prop.getPath(); | |||
@@ -282,11 +285,18 @@ public class HemfPlusDraw { | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
applyColor(ctx); | |||
Area area = new Area(); | |||
rectData.stream().map(Area::new).forEach(area::add); | |||
ctx.fill(area); | |||
HwmfPenStyle ps = prop.getPenStyle(); | |||
try { | |||
prop.setPenStyle(null); | |||
ctx.fill(area); | |||
} finally { | |||
prop.setPenStyle(ps); | |||
} | |||
} | |||
@Override | |||
@@ -319,6 +329,7 @@ public class HemfPlusDraw { | |||
} | |||
} | |||
/** The EmfPlusDrawImagePoints record specifies drawing a scaled image inside a parallelogram. */ | |||
public static class EmfPlusDrawImagePoints implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed, EmfPlusRelativePosition { | |||
/** | |||
* This bit indicates that the rendering of the image includes applying an effect. | |||
@@ -426,39 +437,31 @@ public class HemfPlusDraw { | |||
public void draw(HemfGraphics ctx) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
ctx.applyObjectTableEntry(imageAttributesID); | |||
ctx.applyObjectTableEntry(getObjectId()); | |||
ctx.applyPlusObjectTableEntry(imageAttributesID); | |||
ctx.applyPlusObjectTableEntry(getObjectId()); | |||
final ImageRenderer ir = prop.getEmfPlusImage(); | |||
if (ir == null) { | |||
return; | |||
} | |||
AffineTransform txSaved = ctx.getTransform(); | |||
AffineTransform tx = new AffineTransform(txSaved); | |||
AffineTransform tx = (AffineTransform)txSaved.clone(); | |||
HwmfTernaryRasterOp oldOp = prop.getRasterOp(); | |||
HwmfBkMode oldBk = prop.getBkMode(); | |||
try { | |||
tx.concatenate(trans); | |||
ctx.setTransform(tx); | |||
final Rectangle2D srcBounds = ir.getNativeBounds(); | |||
final Dimension2D dim = ir.getDimension(); | |||
prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY); | |||
prop.setBkMode(HwmfBkMode.TRANSPARENT); | |||
// the buffered image might be rescaled, so we need to calculate a new src rect to take | |||
// the image data from | |||
final AffineTransform srcTx = new AffineTransform(); | |||
srcTx.translate(-srcBounds.getX(), srcBounds.getY()); | |||
srcTx.scale(dim.getWidth()/srcBounds.getWidth(), dim.getHeight()/srcBounds.getHeight()); | |||
final Rectangle2D biRect = srcTx.createTransformedShape(srcRect).getBounds2D(); | |||
// TODO: handle srcUnit | |||
Rectangle2D destRect = new Rectangle2D.Double(0, 0, biRect.getWidth(), biRect.getHeight()); | |||
ctx.drawImage(ir, srcRect, destRect); | |||
// transformation from srcRect to destRect was already applied, | |||
// therefore use srcRect as third parameter | |||
ctx.drawImage(ir, srcRect, srcRect); | |||
} finally { | |||
prop.setBkMode(oldBk); | |||
prop.setRasterOp(oldOp); | |||
ctx.setTransform(txSaved); | |||
} | |||
} | |||
@@ -529,8 +532,8 @@ public class HemfPlusDraw { | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
ctx.applyObjectTableEntry(imageAttributesID); | |||
ctx.applyObjectTableEntry(getObjectId()); | |||
ctx.applyPlusObjectTableEntry(imageAttributesID); | |||
ctx.applyPlusObjectTableEntry(getObjectId()); | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY); | |||
@@ -592,7 +595,7 @@ public class HemfPlusDraw { | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
applyColor(ctx); | |||
ctx.applyObjectTableEntry(getObjectId()); | |||
ctx.applyPlusObjectTableEntry(getObjectId()); | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
ctx.fill(prop.getPath()); | |||
} | |||
@@ -657,12 +660,12 @@ public class HemfPlusDraw { | |||
private int brushId; | |||
private int optionsFlags; | |||
private String glyphs; | |||
private final List<Point2D> glpyhPos = new ArrayList<>(); | |||
private final List<Point2D> glyphPos = new ArrayList<>(); | |||
private final AffineTransform transformMatrix = new AffineTransform(); | |||
@Override | |||
public HemfPlusRecordType getEmfPlusRecordType() { | |||
return HemfPlusRecordType.drawDriverstring; | |||
return HemfPlusRecordType.drawDriverString; | |||
} | |||
@Override | |||
@@ -689,7 +692,7 @@ public class HemfPlusDraw { | |||
// A 32-bit unsigned integer that specifies whether a transform matrix is present in the | |||
// TransformMatrix field. | |||
boolean hasMatrix = leis.readInt() == 1; | |||
int matrixPresent = leis.readInt(); | |||
// A 32-bit unsigned integer that specifies number of glyphs in the string. | |||
int glyphCount = leis.readInt(); | |||
@@ -716,10 +719,10 @@ public class HemfPlusDraw { | |||
for (int i=0; i<glyphCount; i++) { | |||
Point2D p = new Point2D.Double(); | |||
size += readPointF(leis, p); | |||
glpyhPos.add(p); | |||
glyphPos.add(p); | |||
} | |||
if (hasMatrix) { | |||
if (matrixPresent != 0) { | |||
size += HemfFill.readXForm(leis, transformMatrix); | |||
} | |||
@@ -727,6 +730,31 @@ public class HemfPlusDraw { | |||
return size; | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
prop.setTextAlignLatin(HwmfText.HwmfTextAlignment.LEFT); | |||
prop.setTextVAlignLatin(HwmfText.HwmfTextVerticalAlignment.BASELINE); | |||
ctx.applyPlusObjectTableEntry(getObjectId()); | |||
if (isSolidColor()) { | |||
prop.setTextColor(new HwmfColorRef(getSolidColor())); | |||
} else { | |||
ctx.applyPlusObjectTableEntry(getBrushId()); | |||
} | |||
if (REALIZED_ADVANCE.isSet(optionsFlags)) { | |||
byte[] buf = glyphs.getBytes(Charsets.UTF_16LE); | |||
ctx.drawString(buf, buf.length, glyphPos.get(0), null, null, null, null, true); | |||
} else { | |||
final OfInt glyphIter = glyphs.codePoints().iterator(); | |||
glyphPos.forEach(p -> { | |||
byte[] buf = new String(new int[]{glyphIter.next()}, 0, 1).getBytes(Charsets.UTF_16LE); | |||
ctx.drawString(buf, buf.length, p, null, null, null, null, true); | |||
}); | |||
} | |||
} | |||
@Override | |||
public String toString() { | |||
return GenericRecordJsonWriter.marshal(this); | |||
@@ -739,7 +767,7 @@ public class HemfPlusDraw { | |||
"brushId", this::getBrushId, | |||
"optionsFlags", getBitsAsString(() -> optionsFlags, OPTIONS_MASK, OPTIONS_NAMES), | |||
"glyphs", () -> glyphs, | |||
"glyphPos", () -> glpyhPos, | |||
"glyphPos", () -> glyphPos, | |||
"transform", () -> transformMatrix | |||
); | |||
} |
@@ -22,7 +22,10 @@ import java.util.List; | |||
import java.util.Map; | |||
import java.util.function.Supplier; | |||
import org.apache.poi.common.usermodel.fonts.FontHeader; | |||
import org.apache.poi.hemf.draw.HemfDrawProperties; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hemf.record.emf.HemfFont; | |||
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; | |||
@@ -106,7 +109,17 @@ public class HemfPlusFont { | |||
@Override | |||
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
HemfFont font = new HemfFont(); | |||
font.initDefaults(); | |||
font.setTypeface(family); | |||
// TODO: check how to calculate the font size | |||
font.setHeight(emSize); | |||
font.setStrikeOut(STRIKEOUT.isSet(styleFlags)); | |||
font.setUnderline(UNDERLINE.isSet(styleFlags)); | |||
font.setWeight(BOLD.isSet(styleFlags) ? 700 : FontHeader.REGULAR_WEIGHT); | |||
font.setItalic(ITALIC.isSet(styleFlags)); | |||
prop.setFont(font); | |||
} | |||
@Override |
@@ -18,7 +18,7 @@ | |||
package org.apache.poi.hemf.record.emfplus; | |||
import static org.apache.poi.util.GenericRecordUtil.getBitsAsString; | |||
import static org.apache.poi.util.GenericRecordUtil.getEnumBitsAsString; | |||
import java.io.IOException; | |||
import java.util.Map; | |||
@@ -60,8 +60,13 @@ public class HemfPlusHeader implements HemfPlusRecord { | |||
} | |||
} | |||
private static final int[] FLAGS_MASK = { 0x0001 }; | |||
private static final String[] FLAGS_NAMES = { "DUAL_MODE" }; | |||
private static final int[] FLAGS_MASK = { 0x0000, 0x0001 }; | |||
private static final String[] FLAGS_NAMES = { "EMF_PLUS_MODE", "DUAL_MODE" }; | |||
private static final int[] EMFFLAGS_MASK = { 0x0000, 0x0001 }; | |||
private static final String[] EMFFLAGS_NAMES = { "CONTEXT_PRINTER", "CONTEXT_VIDEO" }; | |||
private int flags; | |||
private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); | |||
@@ -125,7 +130,7 @@ public class HemfPlusHeader implements HemfPlusRecord { | |||
public void draw(HemfGraphics ctx) { | |||
// currently EMF is better supported than EMF+ ... so if there's a complete set of EMF records available, | |||
// disable EMF+ rendering for now | |||
ctx.setRenderState(isEmfPlusDualMode() ? EmfRenderState.EMF_ONLY : EmfRenderState.EMF_DCONTEXT); | |||
ctx.setRenderState(EmfRenderState.EMF_DCONTEXT); | |||
} | |||
@Override | |||
@@ -136,9 +141,9 @@ public class HemfPlusHeader implements HemfPlusRecord { | |||
@Override | |||
public Map<String, Supplier<?>> getGenericProperties() { | |||
return GenericRecordUtil.getGenericProperties( | |||
"flags", this::getFlags, | |||
"flags", getEnumBitsAsString(this::getFlags, FLAGS_MASK, FLAGS_NAMES), | |||
"version", this::getVersion, | |||
"emfPlusFlags", getBitsAsString(this::getEmfPlusFlags, FLAGS_MASK, FLAGS_NAMES), | |||
"emfPlusFlags", getEnumBitsAsString(this::getEmfPlusFlags, EMFFLAGS_MASK, EMFFLAGS_NAMES), | |||
"logicalDpiX", this::getLogicalDpiX, | |||
"logicalDpiY", this::getLogicalDpiY | |||
); |
@@ -20,6 +20,7 @@ package org.apache.poi.hemf.record.emfplus; | |||
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; | |||
import static org.apache.poi.util.GenericRecordUtil.getBitsAsString; | |||
import java.awt.Shape; | |||
import java.awt.geom.AffineTransform; | |||
import java.awt.geom.Point2D; | |||
import java.awt.geom.Rectangle2D; | |||
@@ -30,6 +31,7 @@ import java.util.function.Supplier; | |||
import org.apache.poi.hemf.draw.HemfDrawProperties; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hemf.record.emf.HemfFill; | |||
import org.apache.poi.hwmf.record.HwmfRegionMode; | |||
import org.apache.poi.util.BitField; | |||
import org.apache.poi.util.BitFieldFactory; | |||
import org.apache.poi.util.GenericRecordUtil; | |||
@@ -53,18 +55,20 @@ public class HemfPlusMisc { | |||
} | |||
public enum CombineMode { | |||
CombineModeReplace(0x00000000), | |||
CombineModeIntersect(0x00000001), | |||
CombineModeUnion(0x00000002), | |||
CombineModeXOR(0x00000003), | |||
CombineModeExclude(0x00000004), | |||
CombineModeComplement(0x00000005) | |||
REPLACE(0x00000000, HwmfRegionMode.RGN_COPY), | |||
INTERSECT(0x00000001, HwmfRegionMode.RGN_AND), | |||
UNION(0x00000002, HwmfRegionMode.RGN_OR), | |||
XOR(0x00000003, HwmfRegionMode.RGN_XOR), | |||
EXCLUDE(0x00000004, HwmfRegionMode.RGN_DIFF), | |||
COMPLEMENT(0x00000005, HwmfRegionMode.RGN_COMPLEMENT) | |||
; | |||
public final int id; | |||
public final HwmfRegionMode regionMode; | |||
CombineMode(int id) { | |||
CombineMode(int id, HwmfRegionMode regionMode) { | |||
this.id = id; | |||
this.regionMode = regionMode; | |||
} | |||
public static CombineMode valueOf(int id) { | |||
@@ -303,6 +307,14 @@ public class HemfPlusMisc { | |||
public CombineMode getCombineMode() { | |||
return CombineMode.valueOf(COMBINE_MODE.getValue(getFlags())); | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
ctx.applyPlusObjectTableEntry(getObjectId()); | |||
Shape clip = prop.getPath(); | |||
ctx.setClip(clip, clip == null ? HwmfRegionMode.RGN_COPY : getCombineMode().regionMode, false); | |||
} | |||
} | |||
/** The EmfPlusSetClipRect record combines the current clipping region with a rectangle. */ | |||
@@ -389,6 +401,11 @@ public class HemfPlusMisc { | |||
return LittleEndianConsts.INT_SIZE; | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
ctx.savePlusProperties(getStackIndex()); | |||
} | |||
@Override | |||
public Map<String, Supplier<?>> getGenericProperties() { | |||
return GenericRecordUtil.getGenericProperties( | |||
@@ -407,6 +424,11 @@ public class HemfPlusMisc { | |||
public HemfPlusRecordType getEmfPlusRecordType() { | |||
return HemfPlusRecordType.restore; | |||
} | |||
@Override | |||
public void draw(HemfGraphics ctx) { | |||
ctx.restorePlusProperties(getStackIndex()); | |||
} | |||
} | |||
/** The EmfPlusSetRenderingOrigin record specifies the rendering origin for graphics output. */ | |||
@@ -432,10 +454,11 @@ public class HemfPlusMisc { | |||
public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { | |||
this.flags = flags; | |||
// A 32-bit unsigned integer that defines the horizontal coordinate value of the rendering origin. | |||
double x = leis.readUInt(); | |||
// A 32-bit unsigned integer that defines the vertical coordinate value of the rendering origin. | |||
double y = leis.readUInt(); | |||
// error in the MS-EMFPLUS docs - its a signed integer instead of an unsigned | |||
// A 32-bit signed integer that defines the horizontal coordinate value of the rendering origin. | |||
int x = leis.readInt(); | |||
// A 32-bit signed integer that defines the vertical coordinate value of the rendering origin. | |||
int y = leis.readInt(); | |||
origin.setLocation(x,y); | |||
@@ -196,7 +196,7 @@ public class HemfPlusObject { | |||
public void draw(HemfGraphics ctx) { | |||
if (objectData.isContinuedRecord()) { | |||
EmfPlusObject other; | |||
HwmfObjectTableEntry entry = ctx.getObjectTableEntry(getObjectId()); | |||
HwmfObjectTableEntry entry = ctx.getPlusObjectTableEntry(getObjectId()); | |||
if (entry instanceof EmfPlusObject && | |||
objectData.getClass().isInstance((other = (EmfPlusObject)entry).getObjectData()) | |||
) { | |||
@@ -205,7 +205,7 @@ public class HemfPlusObject { | |||
throw new RuntimeException("can't find previous record for continued record"); | |||
} | |||
} else { | |||
ctx.addObjectTableEntry(this, getObjectId()); | |||
ctx.addPlusObjectTableEntry(this, getObjectId()); | |||
} | |||
} | |||
@@ -35,6 +35,7 @@ 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.HemfPlusHeader.EmfPlusGraphicsVersion; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType; | |||
import org.apache.poi.util.BitField; | |||
@@ -87,7 +88,7 @@ public class HemfPlusPath { | |||
private static final int[] TYPE_MASKS = { 0x10, 0x20, 0x80 }; | |||
private static final String[] TYPE_NAMES = { "DASHED", "MARKER", "CLOSE" }; | |||
private final HemfPlusHeader.EmfPlusGraphicsVersion graphicsVersion = new HemfPlusHeader.EmfPlusGraphicsVersion(); | |||
private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); | |||
private int pointFlags; | |||
private Point2D[] pathPoints; | |||
private byte[] pointTypes; | |||
@@ -143,7 +144,7 @@ public class HemfPlusPath { | |||
} | |||
@Override | |||
public HemfPlusHeader.EmfPlusGraphicsVersion getGraphicsVersion() { | |||
public EmfPlusGraphicsVersion getGraphicsVersion() { | |||
return graphicsVersion; | |||
} | |||
@@ -175,9 +176,15 @@ public class HemfPlusPath { | |||
@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); | |||
prop.setPath(getPath()); | |||
} | |||
public Path2D getPath() { | |||
return getPath(Path2D.WIND_NON_ZERO); | |||
} | |||
public Path2D getPath(int windingRule) { | |||
Path2D path = new Path2D.Double(windingRule); | |||
for (int idx=0; idx < pathPoints.length; idx++) { | |||
Point2D p1 = pathPoints[idx]; | |||
switch (getPointType(idx)) { | |||
@@ -198,10 +205,11 @@ public class HemfPlusPath { | |||
path.closePath(); | |||
} | |||
} | |||
return path; | |||
} | |||
@Override | |||
public EmfPlusObjectType getGenericRecordType() { | |||
public Enum getGenericRecordType() { | |||
return EmfPlusObjectType.PATH; | |||
} | |||
@@ -34,6 +34,7 @@ import java.util.function.Supplier; | |||
import org.apache.poi.common.usermodel.GenericRecord; | |||
import org.apache.poi.hemf.draw.HemfDrawProperties; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusBrush; | |||
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; | |||
@@ -330,6 +331,8 @@ public class HemfPlusPen { | |||
private EmfPlusCustomLineCap customStartCap; | |||
private EmfPlusCustomLineCap customEndCap; | |||
private final EmfPlusBrush brush = new EmfPlusBrush(); | |||
@Override | |||
public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException { | |||
// An EmfPlusGraphicsVersion object that specifies the version of operating system graphics that | |||
@@ -463,6 +466,8 @@ public class HemfPlusPen { | |||
size += initCustomCap(c -> customEndCap = c, leis); | |||
} | |||
size += brush.init(leis, dataSize-size, EmfPlusObjectType.BRUSH, 0); | |||
return size; | |||
} | |||
@@ -472,8 +477,12 @@ public class HemfPlusPen { | |||
} | |||
private long initCustomCap(Consumer<EmfPlusCustomLineCap> setter, LittleEndianInputStream leis) throws IOException { | |||
int CustomStartCapSize = leis.readInt(); | |||
int size = LittleEndianConsts.INT_SIZE; | |||
EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); | |||
long size = version.init(leis); | |||
size += version.init(leis); | |||
assert(version.getGraphicsVersion() != null); | |||
boolean adjustableArrow = (leis.readInt() != 0); | |||
size += LittleEndianConsts.INT_SIZE; | |||
@@ -492,11 +501,11 @@ public class HemfPlusPen { | |||
// TOOD: | |||
// - set width according unit type | |||
// - provide logic for different start and end cap | |||
// - provide standard caps like diamondd | |||
// - provide standard caps like diamond | |||
// - support custom caps | |||
// workaround for too wide pens ... just arbitrary reduce high values ... | |||
prop.setPenWidth(penWidth > 20 ? 1 : penWidth); | |||
brush.applyPen(ctx, continuedObjectData); | |||
prop.setPenWidth(penWidth); | |||
prop.setPenStyle(new HwmfPenStyle(){ | |||
@Override | |||
public HwmfLineCap getLineCap() { | |||
@@ -573,6 +582,7 @@ public class HemfPlusPen { | |||
m.put("compoundLineData", () -> compoundLineData); | |||
m.put("customStartCap", () -> customStartCap); | |||
m.put("customEndCap", () -> customEndCap); | |||
m.put("brush", () -> brush); | |||
return Collections.unmodifiableMap(m); | |||
} | |||
} | |||
@@ -645,13 +655,17 @@ public class HemfPlusPen { | |||
size += readPointF(leis, lineHotSpot); | |||
if (FILL_PATH.isSet(dataFlags)) { | |||
int fillSize = leis.readInt(); | |||
size += LittleEndianConsts.INT_SIZE; | |||
fillPath = new EmfPlusPath(); | |||
size += fillPath.init(leis, -1, null, -1); | |||
size += fillPath.init(leis, fillSize, EmfPlusObjectType.PATH, -1); | |||
} | |||
if (LINE_PATH.isSet(dataFlags)) { | |||
int pathSize = leis.readInt(); | |||
size += LittleEndianConsts.INT_SIZE; | |||
outlinePath = new EmfPlusPath(); | |||
size += outlinePath.init(leis, -1, null, -1); | |||
size += outlinePath.init(leis, pathSize, EmfPlusObjectType.PATH, -1); | |||
} | |||
return size; |
@@ -55,4 +55,8 @@ public interface HemfPlusRecord extends GenericRecord { | |||
default void draw(HemfGraphics ctx) { | |||
} | |||
@Override | |||
default Enum getGenericRecordType() { | |||
return getEmfPlusRecordType(); | |||
} | |||
} |
@@ -76,7 +76,7 @@ public enum HemfPlusRecordType { | |||
setClipRegion(0x4033, HemfPlusMisc.EmfPlusSetClipRegion::new), | |||
setClipPath(0x4034, HemfPlusMisc.EmfPlusSetClipPath::new), | |||
offsetClip(0x4035, UnimplementedHemfPlusRecord::new), | |||
drawDriverstring(0x4036, HemfPlusDraw.EmfPlusDrawDriverString::new), | |||
drawDriverString(0x4036, HemfPlusDraw.EmfPlusDrawDriverString::new), | |||
strokeFillPath(0x4037, UnimplementedHemfPlusRecord::new), | |||
serializableObject(0x4038, UnimplementedHemfPlusRecord::new), | |||
setTSGraphics(0x4039, UnimplementedHemfPlusRecord::new), |
@@ -19,14 +19,19 @@ package org.apache.poi.hemf.record.emfplus; | |||
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; | |||
import java.awt.Shape; | |||
import java.awt.geom.Area; | |||
import java.awt.geom.Path2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.io.IOException; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.function.BiConsumer; | |||
import java.util.function.Consumer; | |||
import java.util.function.Supplier; | |||
import org.apache.poi.common.usermodel.GenericRecord; | |||
import org.apache.poi.hemf.draw.HemfDrawProperties; | |||
import org.apache.poi.hemf.draw.HemfGraphics; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion; | |||
import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; | |||
@@ -42,49 +47,51 @@ public class HemfPlusRegion { | |||
* Specifies a region node with child nodes. A Boolean AND operation SHOULD be applied to the left and right | |||
* child nodes specified by an EmfPlusRegionNodeChildNodes object | |||
*/ | |||
AND(0X00000001, EmfPlusRegionNode::new), | |||
AND(0X00000001, EmfPlusRegionNode::new, Area::intersect), | |||
/** | |||
* Specifies a region node with child nodes. A Boolean OR operation SHOULD be applied to the left and right | |||
* child nodes specified by an EmfPlusRegionNodeChildNodes object. | |||
*/ | |||
OR(0X00000002, EmfPlusRegionNode::new), | |||
OR(0X00000002, EmfPlusRegionNode::new, Area::add), | |||
/** | |||
* Specifies a region node with child nodes. A Boolean XOR operation SHOULD be applied to the left and right | |||
* child nodes specified by an EmfPlusRegionNodeChildNodes object. | |||
*/ | |||
XOR(0X00000003, EmfPlusRegionNode::new), | |||
XOR(0X00000003, EmfPlusRegionNode::new, Area::exclusiveOr), | |||
/** | |||
* Specifies a region node with child nodes. A Boolean operation, defined as "the part of region 1 that is excluded | |||
* from region 2", SHOULD be applied to the left and right child nodes specified by an EmfPlusRegionNodeChildNodes object. | |||
*/ | |||
EXCLUDE(0X00000004, EmfPlusRegionNode::new), | |||
EXCLUDE(0X00000004, EmfPlusRegionNode::new, Area::subtract), | |||
/** | |||
* Specifies a region node with child nodes. A Boolean operation, defined as "the part of region 2 that is excluded | |||
* from region 1", SHOULD be applied to the left and right child nodes specified by an EmfPlusRegionNodeChildNodes object. | |||
*/ | |||
COMPLEMENT(0X00000005, EmfPlusRegionNode::new), | |||
COMPLEMENT(0X00000005, EmfPlusRegionNode::new, Area::subtract), | |||
/** | |||
* Specifies a region node with no child nodes. | |||
* The RegionNodeData field SHOULD specify a boundary with an EmfPlusRectF object. | |||
*/ | |||
RECT(0X10000000, EmfPlusRegionRect::new), | |||
RECT(0X10000000, EmfPlusRegionRect::new, null), | |||
/** | |||
* Specifies a region node with no child nodes. | |||
* The RegionNodeData field SHOULD specify a boundary with an EmfPlusRegionNodePath object | |||
*/ | |||
PATH(0X10000001, EmfPlusRegionPath::new), | |||
PATH(0X10000001, EmfPlusRegionPath::new, null), | |||
/** Specifies a region node with no child nodes. The RegionNodeData field SHOULD NOT be present. */ | |||
EMPTY(0X10000002, EmfPlusRegionEmpty::new), | |||
EMPTY(0X10000002, EmfPlusRegionEmpty::new, null), | |||
/** Specifies a region node with no child nodes, and its bounds are not defined. */ | |||
INFINITE(0X10000003, EmfPlusRegionInfinite::new) | |||
INFINITE(0X10000003, EmfPlusRegionInfinite::new, null) | |||
; | |||
public final int id; | |||
public final Supplier<EmfPlusRegionNodeData> constructor; | |||
public final BiConsumer<Area,Area> operation; | |||
EmfPlusRegionNodeDataType(int id, Supplier<EmfPlusRegionNodeData> constructor) { | |||
EmfPlusRegionNodeDataType(int id, Supplier<EmfPlusRegionNodeData> constructor, BiConsumer<Area,Area> operation) { | |||
this.id = id; | |||
this.constructor = constructor; | |||
this.operation = operation; | |||
} | |||
public static EmfPlusRegionNodeDataType valueOf(int id) { | |||
@@ -95,6 +102,7 @@ public class HemfPlusRegion { | |||
} | |||
} | |||
/** The EmfPlusRegion object specifies line and curve segments that define a nonrectilinear shape. */ | |||
public static class EmfPlusRegion implements EmfPlusObjectData { | |||
private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); | |||
@@ -111,14 +119,16 @@ public class HemfPlusRegion { | |||
// An array of RegionNodeCount+1 EmfPlusRegionNode objects. Regions are specified as a binary tree of | |||
// region nodes, and each node MUST either be a terminal node or specify one or two child nodes. | |||
// RegionNode MUST contain at least one element. | |||
size += readNode(leis, d -> regionNode = d); | |||
size += readNode(leis, this::setRegionNode); | |||
return size; | |||
} | |||
@Override | |||
public void applyObject(HemfGraphics ctx, List<? extends EmfPlusObjectData> continuedObjectData) { | |||
HemfDrawProperties prop = ctx.getProperties(); | |||
Shape shape = regionNode.getShape(); | |||
prop.setPath(shape == null ? null : new Path2D.Double(shape)); | |||
} | |||
@Override | |||
@@ -131,11 +141,19 @@ public class HemfPlusRegion { | |||
return EmfPlusObjectType.REGION; | |||
} | |||
private void setRegionNode(EmfPlusRegionNodeData regionNode) { | |||
this.regionNode = regionNode; | |||
} | |||
public EmfPlusRegionNodeData getRegionNode() { | |||
return regionNode; | |||
} | |||
@Override | |||
public Map<String, Supplier<?>> getGenericProperties() { | |||
return GenericRecordUtil.getGenericProperties( | |||
"graphicsVersion", this::getGraphicsVersion, | |||
"regionNode", () -> regionNode | |||
"regionNode", this::getRegionNode | |||
); | |||
} | |||
} | |||
@@ -143,6 +161,8 @@ public class HemfPlusRegion { | |||
public interface EmfPlusRegionNodeData extends GenericRecord { | |||
long init(LittleEndianInputStream leis) throws IOException; | |||
Shape getShape(); | |||
default void setNodeType(EmfPlusRegionNodeDataType type) {} | |||
} | |||
public static class EmfPlusRegionPath extends EmfPlusPath implements EmfPlusRegionNodeData { | |||
@@ -150,6 +170,16 @@ public class HemfPlusRegion { | |||
int dataSize = leis.readInt(); | |||
return super.init(leis, dataSize, EmfPlusObjectType.PATH, 0) + LittleEndianConsts.INT_SIZE; | |||
} | |||
@Override | |||
public Shape getShape() { | |||
return getPath(); | |||
} | |||
@Override | |||
public EmfPlusRegionNodeDataType getGenericRecordType() { | |||
return EmfPlusRegionNodeDataType.PATH; | |||
} | |||
} | |||
public static class EmfPlusRegionInfinite implements EmfPlusRegionNodeData { | |||
@@ -162,6 +192,16 @@ public class HemfPlusRegion { | |||
public Map<String, Supplier<?>> getGenericProperties() { | |||
return null; | |||
} | |||
@Override | |||
public Shape getShape() { | |||
return null; | |||
} | |||
@Override | |||
public EmfPlusRegionNodeDataType getGenericRecordType() { | |||
return EmfPlusRegionNodeDataType.INFINITE; | |||
} | |||
} | |||
public static class EmfPlusRegionEmpty implements EmfPlusRegionNodeData { | |||
@@ -174,6 +214,16 @@ public class HemfPlusRegion { | |||
public Map<String, Supplier<?>> getGenericProperties() { | |||
return null; | |||
} | |||
@Override | |||
public Shape getShape() { | |||
return new Rectangle2D.Double(0,0,0,0); | |||
} | |||
@Override | |||
public EmfPlusRegionNodeDataType getGenericRecordType() { | |||
return EmfPlusRegionNodeDataType.EMPTY; | |||
} | |||
} | |||
public static class EmfPlusRegionRect implements EmfPlusRegionNodeData { | |||
@@ -188,25 +238,90 @@ public class HemfPlusRegion { | |||
public Map<String, Supplier<?>> getGenericProperties() { | |||
return GenericRecordUtil.getGenericProperties("rect", () -> rect); | |||
} | |||
@Override | |||
public Shape getShape() { | |||
return rect; | |||
} | |||
@Override | |||
public EmfPlusRegionNodeDataType getGenericRecordType() { | |||
return EmfPlusRegionNodeDataType.RECT; | |||
} | |||
} | |||
/** The EmfPlusRegionNode object specifies nodes of a graphics region. */ | |||
public static class EmfPlusRegionNode implements EmfPlusRegionNodeData { | |||
private EmfPlusRegionNodeData left, right; | |||
private EmfPlusRegionNodeDataType nodeType; | |||
@Override | |||
public long init(LittleEndianInputStream leis) throws IOException { | |||
long size = readNode(leis, n -> left = n); | |||
size += readNode(leis, n -> right = n); | |||
long size = readNode(leis, this::setLeft); | |||
size += readNode(leis, this::setRight); | |||
return size; | |||
} | |||
private void setLeft(EmfPlusRegionNodeData left) { | |||
this.left = left; | |||
} | |||
private void setRight(EmfPlusRegionNodeData right) { | |||
this.right = right; | |||
} | |||
public EmfPlusRegionNodeData getLeft() { | |||
return left; | |||
} | |||
public EmfPlusRegionNodeData getRight() { | |||
return right; | |||
} | |||
public EmfPlusRegionNodeDataType getNodeType() { | |||
return nodeType; | |||
} | |||
@Override | |||
public void setNodeType(EmfPlusRegionNodeDataType nodeType) { | |||
this.nodeType = nodeType; | |||
} | |||
@Override | |||
public Map<String, Supplier<?>> getGenericProperties() { | |||
return GenericRecordUtil.getGenericProperties( | |||
"left", () -> left, | |||
"right", () -> right | |||
"nodeType", this::getNodeType, | |||
"left", this::getLeft, | |||
"right", this::getRight | |||
); | |||
} | |||
@Override | |||
public Shape getShape() { | |||
boolean com = (nodeType == EmfPlusRegionNodeDataType.COMPLEMENT); | |||
final Shape leftShape = (com ? right : left).getShape(); | |||
final Shape rightShape = (com ? left : right).getShape(); | |||
if (leftShape == null) { | |||
return rightShape; | |||
} else if (rightShape == null) { | |||
return leftShape; | |||
} | |||
// TODO: check Area vs. Path manipulation | |||
Area leftArea = new Area(leftShape); | |||
Area rightArea = new Area(rightShape); | |||
assert(nodeType.operation != null); | |||
nodeType.operation.accept(leftArea, rightArea); | |||
return leftArea; | |||
} | |||
@Override | |||
public EmfPlusRegionNodeDataType getGenericRecordType() { | |||
return nodeType; | |||
} | |||
} | |||
private static long readNode(LittleEndianInputStream leis, Consumer<EmfPlusRegionNodeData> con) throws IOException { | |||
@@ -216,6 +331,7 @@ public class HemfPlusRegion { | |||
assert(type != null); | |||
EmfPlusRegionNodeData nd = type.constructor.get(); | |||
con.accept(nd); | |||
nd.setNodeType(type); | |||
return LittleEndianConsts.INT_SIZE + nd.init(leis); | |||
} | |||
} |
@@ -130,14 +130,23 @@ public class HemfPicture implements Iterable<HemfRecord>, GenericRecord { | |||
return new Rectangle2D.Double(x, y, width, height); | |||
} | |||
/** | |||
* Return the image bounds in points | |||
* | |||
* @return the image bounds in points | |||
*/ | |||
public Rectangle2D getBoundsInPoints() { | |||
return Units.pixelToPoints(getHeader().getBoundsRectangle()); | |||
} | |||
/** | |||
* Return the image size in points | |||
* | |||
* @return the image size in points | |||
*/ | |||
public Dimension2D getSize() { | |||
final Rectangle2D b = getHeader().getBoundsRectangle(); | |||
return Units.pixelToPoints(new Dimension2DDouble(abs(b.getWidth()), abs(b.getHeight()))); | |||
final Rectangle2D b = getBoundsInPoints(); | |||
return new Dimension2DDouble(abs(b.getWidth()), abs(b.getHeight())); | |||
} | |||
private static double minX(Rectangle2D bounds) { |
@@ -93,12 +93,27 @@ public class HwmfGraphics { | |||
} | |||
} | |||
protected final List<HwmfDrawProperties> propStack = new LinkedList<>(); | |||
private static final Float[] WEIGHT_MAP = { | |||
900f, TextAttribute.WEIGHT_ULTRABOLD, | |||
800f, TextAttribute.WEIGHT_EXTRABOLD, | |||
750f, TextAttribute.WEIGHT_HEAVY, | |||
700f, TextAttribute.WEIGHT_BOLD, | |||
600f, TextAttribute.WEIGHT_DEMIBOLD, | |||
500f, TextAttribute.WEIGHT_MEDIUM, | |||
450f, TextAttribute.WEIGHT_SEMIBOLD, | |||
400f, TextAttribute.WEIGHT_REGULAR, | |||
300f, TextAttribute.WEIGHT_DEMILIGHT, | |||
200f, TextAttribute.WEIGHT_LIGHT, | |||
1f, TextAttribute.WEIGHT_EXTRA_LIGHT | |||
}; | |||
private final List<HwmfDrawProperties> propStack = new LinkedList<>(); | |||
protected HwmfDrawProperties prop; | |||
protected final Graphics2D graphicsCtx; | |||
protected final BitSet objectIndexes = new BitSet(); | |||
protected final TreeMap<Integer,HwmfObjectTableEntry> objectTable = new TreeMap<>(); | |||
protected final AffineTransform initialAT = new AffineTransform(); | |||
private final AffineTransform initialAT = new AffineTransform(); | |||
private static final Charset DEFAULT_CHARSET = LocaleUtil.CHARSET_1252; | |||
@@ -207,9 +222,14 @@ public class HwmfGraphics { | |||
case BS_DIBPATTERNPT: return getPatternPaint(); | |||
case BS_SOLID: return getSolidFill(); | |||
case BS_HATCHED: return getHatchedFill(); | |||
case BS_LINEAR_GRADIENT: return getLinearGradient(); | |||
} | |||
} | |||
protected Paint getLinearGradient() { | |||
return null; | |||
} | |||
protected Paint getSolidFill() { | |||
return getProperties().getBrushColor().getColor(); | |||
} | |||
@@ -440,8 +460,13 @@ public class HwmfGraphics { | |||
} | |||
int trimLen; | |||
for (trimLen=0; trimLen<text.length-1; trimLen+=2) { | |||
if ((text[trimLen] == -1 && text[trimLen+1] == -1) || | |||
for (trimLen=0; trimLen<text.length; trimLen+=2) { | |||
if (trimLen == text.length-1) { | |||
if (text[trimLen] != 0) { | |||
trimLen++; | |||
} | |||
break; | |||
} else if ((text[trimLen] == -1 && text[trimLen+1] == -1) || | |||
((text[trimLen] & 0xE0) == 0 && text[trimLen+1] == 0)) { | |||
break; | |||
} | |||
@@ -519,7 +544,7 @@ public class HwmfGraphics { | |||
tx.translate(-pixelBounds.getWidth() / 2., 0); | |||
break; | |||
case RIGHT: | |||
tx.translate(-pixelBounds.getWidth(), 0); | |||
tx.translate(-layout.getAdvance(), 0); | |||
break; | |||
} | |||
@@ -581,7 +606,16 @@ public class HwmfGraphics { | |||
if (font.isItalic()) { | |||
as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE); | |||
} | |||
as.addAttribute(TextAttribute.WEIGHT, font.getWeight()); | |||
// convert font weight to awt font weight - usually a font weight of 400 is regarded as regular | |||
final int fw = font.getWeight(); | |||
Float awtFW = TextAttribute.WEIGHT_REGULAR; | |||
for (int i=0; i<WEIGHT_MAP.length; i+=2) { | |||
if (fw >= WEIGHT_MAP[i]) { | |||
awtFW = WEIGHT_MAP[i+1]; | |||
break; | |||
} | |||
} | |||
as.addAttribute(TextAttribute.WEIGHT, awtFW); | |||
} | |||
private double getFontHeight(HwmfFont font) { | |||
@@ -661,7 +695,11 @@ public class HwmfGraphics { | |||
// of the referenced image and can be also negative | |||
Composite old = graphicsCtx.getComposite(); | |||
graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); | |||
img.drawImage(graphicsCtx, normBounds, getSubImageInsets(srcBounds, img.getNativeBounds())); | |||
boolean useDeviceBounds = (img instanceof HwmfImageRenderer); | |||
img.drawImage(graphicsCtx, normBounds, | |||
getSubImageInsets(srcBounds, useDeviceBounds ? img.getNativeBounds() : img.getBounds())); | |||
graphicsCtx.setComposite(old); | |||
graphicsCtx.setTransform(oldTrans); | |||
@@ -683,9 +721,9 @@ public class HwmfGraphics { | |||
// Todo: check if we need to normalize srcBounds x/y, in case of flipped images | |||
// for now we assume the width/height is positive | |||
int left = (int)Math.round((srcBounds.getX()-nativeBounds.getX())/nativeBounds.getWidth()*100_000.); | |||
int top = (int)Math.round((srcBounds.getY()-nativeBounds.getY())/nativeBounds.getWidth()*100_000.); | |||
int top = (int)Math.round((srcBounds.getY()-nativeBounds.getY())/nativeBounds.getHeight()*100_000.); | |||
int right = (int)Math.round((nativeBounds.getMaxX()-srcBounds.getMaxX())/nativeBounds.getWidth()*100_000.); | |||
int bottom = (int)Math.round((nativeBounds.getMaxY()-srcBounds.getMaxY())/nativeBounds.getWidth()*100_000.); | |||
int bottom = (int)Math.round((nativeBounds.getMaxY()-srcBounds.getMaxY())/nativeBounds.getHeight()*100_000.); | |||
return new Insets(top, left, bottom, right); | |||
} |
@@ -0,0 +1,72 @@ | |||
/* ==================================================================== | |||
Licensed to the Apache Software Foundation (ASF) under one or more | |||
contributor license agreements. See the NOTICE file distributed with | |||
this work for additional information regarding copyright ownership. | |||
The ASF licenses this file to You under the Apache License, Version 2.0 | |||
(the "License"); you may not use this file except in compliance with | |||
the License. You may obtain a copy of the License at | |||
http://www.apache.org/licenses/LICENSE-2.0 | |||
Unless required by applicable law or agreed to in writing, software | |||
distributed under the License is distributed on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
See the License for the specific language governing permissions and | |||
limitations under the License. | |||
==================================================================== */ | |||
package org.apache.poi.hwmf.draw; | |||
import java.awt.Color; | |||
import java.awt.Composite; | |||
import java.awt.Font; | |||
import java.awt.Graphics2D; | |||
import java.awt.Paint; | |||
import java.awt.Shape; | |||
import java.awt.Stroke; | |||
import java.awt.geom.AffineTransform; | |||
import org.apache.poi.util.Internal; | |||
/** | |||
* An util class for saving the state of a {@link java.awt.Graphics2D} object | |||
*/ | |||
@Internal | |||
public class HwmfGraphicsState { | |||
private Color background; | |||
private Shape clip; | |||
private Color color; | |||
private Composite composite; | |||
private Font font; | |||
private Paint paint; | |||
private Stroke stroke; | |||
private AffineTransform trans; | |||
/** | |||
* Saves the state of the graphics2D object | |||
*/ | |||
public void backup(Graphics2D graphics2D) { | |||
background = graphics2D.getBackground(); | |||
clip = graphics2D.getClip(); | |||
color = graphics2D.getColor(); | |||
composite = graphics2D.getComposite(); | |||
font = graphics2D.getFont(); | |||
paint = graphics2D.getPaint(); | |||
stroke = graphics2D.getStroke(); | |||
trans = graphics2D.getTransform(); | |||
} | |||
/** | |||
* Retrieves the state into the graphics2D object | |||
*/ | |||
public void restore(Graphics2D graphics2D) { | |||
graphics2D.setBackground(background); | |||
graphics2D.setClip(clip); | |||
graphics2D.setColor(color); | |||
graphics2D.setComposite(composite); | |||
graphics2D.setFont(font); | |||
graphics2D.setPaint(paint); | |||
graphics2D.setStroke(stroke); | |||
graphics2D.setTransform(trans); | |||
} | |||
} |
@@ -21,7 +21,6 @@ import java.awt.Dimension; | |||
import java.awt.Graphics2D; | |||
import java.awt.Insets; | |||
import java.awt.RenderingHints; | |||
import java.awt.Shape; | |||
import java.awt.geom.Dimension2D; | |||
import java.awt.geom.Rectangle2D; | |||
import java.awt.image.BufferedImage; | |||
@@ -114,20 +113,22 @@ public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor { | |||
return false; | |||
} | |||
HwmfGraphicsState graphicsState = new HwmfGraphicsState(); | |||
graphicsState.backup(graphics); | |||
boolean isClipped = true; | |||
if (clip == null) { | |||
isClipped = false; | |||
clip = new Insets(0,0,0,0); | |||
} | |||
Shape clipOld = graphics.getClip(); | |||
if (isClipped) { | |||
graphics.clip(anchor); | |||
} | |||
image.draw(graphics, getOuterBounds(anchor, clip)); | |||
graphics.setClip(clipOld); | |||
graphicsState.restore(graphics); | |||
return true; | |||
} | |||
@@ -179,4 +180,9 @@ public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor { | |||
public Rectangle2D getNativeBounds() { | |||
return image.getBounds(); | |||
} | |||
@Override | |||
public Rectangle2D getBounds() { | |||
return Units.pointsToPixel(image == null ? new Rectangle2D.Double() : image.getBoundsInPoints()); | |||
} | |||
} |
@@ -64,7 +64,11 @@ public enum HwmfBrushStyle { | |||
/** | |||
* Not supported | |||
*/ | |||
BS_MONOPATTERN(0x0009); | |||
BS_MONOPATTERN(0x0009), | |||
/** | |||
* (POI arbitrary:) EMF/EMF+ specific value for linear gradient paint | |||
*/ | |||
BS_LINEAR_GRADIENT(0x0100); | |||
int flag; | |||
HwmfBrushStyle(int flag) { |
@@ -288,7 +288,7 @@ public class HwmfFont implements FontInfo, GenericRecord { | |||
* For all height comparisons, the font mapper SHOULD find the largest physical | |||
* font that does not exceed the requested size. | |||
*/ | |||
protected int height; | |||
protected double height; | |||
/** | |||
* A 16-bit signed integer that defines the average width, in logical units, of | |||
@@ -433,7 +433,7 @@ public class HwmfFont implements FontInfo, GenericRecord { | |||
facename = "SansSerif"; | |||
} | |||
public int getHeight() { | |||
public double getHeight() { | |||
return height; | |||
} | |||
@@ -45,7 +45,11 @@ public enum HwmfRegionMode { | |||
/** | |||
* The new clipping region is the current path (or the new region). | |||
*/ | |||
RGN_COPY(0x05, HwmfRegionMode::copyOp); | |||
RGN_COPY(0x05, HwmfRegionMode::copyOp), | |||
/** | |||
* This is the opposite of {@link #RGN_DIFF}, and only made-up for compatibility with EMF+ | |||
*/ | |||
RGN_COMPLEMENT(-1, HwmfRegionMode::complementOp); | |||
private final int flag; | |||
private final BiFunction<Shape,Shape,Shape> op; | |||
@@ -125,4 +129,17 @@ public enum HwmfRegionMode { | |||
private static Shape copyOp(final Shape oldClip, final Shape newClip) { | |||
return (newClip == null || newClip.getBounds2D().isEmpty()) ? null : newClip; | |||
} | |||
private static Shape complementOp(final Shape oldClip, final Shape newClip) { | |||
assert(newClip != null); | |||
if (newClip.getBounds2D().isEmpty()) { | |||
return oldClip; | |||
} else if (oldClip == null) { | |||
return newClip; | |||
} else { | |||
Area newArea = new Area(newClip); | |||
newArea.subtract(new Area(oldClip)); | |||
return newArea; | |||
} | |||
} | |||
} |
@@ -232,7 +232,10 @@ public class HwmfText { | |||
@Override | |||
public Map<String, Supplier<?>> getGenericProperties() { | |||
return GenericRecordUtil.getGenericProperties("text", () -> getText(StandardCharsets.US_ASCII)); | |||
return GenericRecordUtil.getGenericProperties( | |||
"text", () -> getText(StandardCharsets.US_ASCII), | |||
"reference", () -> reference | |||
); | |||
} | |||
} | |||
@@ -36,6 +36,7 @@ import java.util.function.Supplier; | |||
import org.apache.poi.common.usermodel.GenericRecord; | |||
import org.apache.poi.hwmf.draw.HwmfDrawProperties; | |||
import org.apache.poi.hwmf.draw.HwmfGraphics; | |||
import org.apache.poi.hwmf.draw.HwmfGraphicsState; | |||
import org.apache.poi.hwmf.record.HwmfHeader; | |||
import org.apache.poi.hwmf.record.HwmfPlaceableHeader; | |||
import org.apache.poi.hwmf.record.HwmfRecord; | |||
@@ -127,8 +128,8 @@ public class HwmfPicture implements Iterable<HwmfRecord>, GenericRecord { | |||
} | |||
public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) { | |||
final Shape clip = ctx.getClip(); | |||
final AffineTransform at = ctx.getTransform(); | |||
HwmfGraphicsState state = new HwmfGraphicsState(); | |||
state.backup(ctx); | |||
try { | |||
Rectangle2D wmfBounds = getBounds(); | |||
Rectangle2D innerBounds = getInnnerBounds(); | |||
@@ -137,13 +138,10 @@ public class HwmfPicture implements Iterable<HwmfRecord>, GenericRecord { | |||
} | |||
// scale output bounds to image bounds | |||
ctx.translate(graphicsBounds.getX(), graphicsBounds.getY()); | |||
ctx.scale(graphicsBounds.getWidth()/wmfBounds.getWidth(), graphicsBounds.getHeight()/wmfBounds.getHeight()); | |||
ctx.translate(graphicsBounds.getCenterX(), graphicsBounds.getCenterY()); | |||
ctx.scale(graphicsBounds.getWidth()/innerBounds.getWidth(), graphicsBounds.getHeight()/innerBounds.getHeight()); | |||
ctx.translate(-innerBounds.getCenterX(), -innerBounds.getCenterY()); | |||
ctx.translate(-wmfBounds.getX(), -wmfBounds.getY()); | |||
ctx.translate(innerBounds.getCenterX(), innerBounds.getCenterY()); | |||
ctx.scale(wmfBounds.getWidth()/innerBounds.getWidth(), wmfBounds.getHeight()/innerBounds.getHeight()); | |||
ctx.translate(-wmfBounds.getCenterX(), -wmfBounds.getCenterY()); | |||
HwmfGraphics g = new HwmfGraphics(ctx, innerBounds); | |||
HwmfDrawProperties prop = g.getProperties(); | |||
@@ -162,8 +160,7 @@ public class HwmfPicture implements Iterable<HwmfRecord>, GenericRecord { | |||
idx++; | |||
} | |||
} finally { | |||
ctx.setTransform(at); | |||
ctx.setClip(clip); | |||
state.restore(ctx); | |||
} | |||
} | |||
@@ -214,19 +211,30 @@ public class HwmfPicture implements Iterable<HwmfRecord>, GenericRecord { | |||
public HwmfHeader getHeader() { | |||
return header; | |||
} | |||
/** | |||
* Return the image size in points | |||
* Return the image bound in points | |||
* | |||
* @return the image size in points | |||
* @return the image bound in points | |||
*/ | |||
public Dimension2D getSize() { | |||
public Rectangle2D getBoundsInPoints() { | |||
double inch = (placeableHeader == null) ? 1440 : placeableHeader.getUnitsPerInch(); | |||
Rectangle2D bounds = getBounds(); | |||
//coefficient to translate from WMF dpi to 72dpi | |||
double coeff = Units.POINT_DPI/inch; | |||
return new Dimension2DDouble(bounds.getWidth()*coeff, bounds.getHeight()*coeff); | |||
return AffineTransform.getScaleInstance(coeff, coeff).createTransformedShape(bounds).getBounds2D(); | |||
} | |||
/** | |||
* Return the image size in points | |||
* | |||
* @return the image size in points | |||
*/ | |||
public Dimension2D getSize() { | |||
Rectangle2D bounds = getBoundsInPoints(); | |||
return new Dimension2DDouble(bounds.getWidth(), bounds.getHeight()); | |||
} | |||
public Iterable<HwmfEmbedded> getEmbeddings() { |