Bug 60656 - Emf image support in slideshows

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

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

View File

@ -17,7 +17,6 @@
package org.apache.poi.sl.draw;
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);

View File

@ -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 );

View File

@ -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();
}
}

View File

@ -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)

View File

@ -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;
}

View File

@ -42,7 +42,6 @@ import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.sl.draw.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

View File

@ -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;
}
}

View File

@ -20,16 +20,25 @@ package org.apache.poi.hemf.draw;
import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_NULL;
import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_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 &gt; 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());
}
}

View File

@ -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,21 +104,21 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor {
return false;
}
boolean isClipped = true;
if (clip == null) {
isClipped = false;
clip = new Insets(0,0,0,0);
HwmfGraphicsState graphicsState = new HwmfGraphicsState();
graphicsState.backup(graphics);
try {
if (clip != null) {
graphics.clip(anchor);
} else {
clip = new Insets(0, 0, 0, 0);
}
image.draw(graphics, getOuterBounds(anchor, clip));
} finally {
graphicsState.restore(graphics);
}
Shape clipOld = graphics.getClip();
if (isClipped) {
graphics.clip(anchor);
}
image.draw(graphics, getOuterBounds(anchor, clip));
graphics.setClip(clipOld);
return true;
}
@ -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());
}
}

View File

@ -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);
}

View File

@ -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();

View File

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

View File

@ -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

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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
);
}

View File

@ -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

View File

@ -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
);

View File

@ -20,6 +20,7 @@ package org.apache.poi.hemf.record.emfplus;
import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF;
import static org.apache.poi.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);

View File

@ -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());
}
}

View File

@ -35,6 +35,7 @@ import org.apache.poi.hemf.draw.HemfDrawProperties;
import org.apache.poi.hemf.draw.HemfGraphics;
import org.apache.poi.hemf.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;
}

View File

@ -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;

View File

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

View File

@ -76,7 +76,7 @@ public enum HemfPlusRecordType {
setClipRegion(0x4033, HemfPlusMisc.EmfPlusSetClipRegion::new),
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),

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -93,12 +93,27 @@ public class HwmfGraphics {
}
}
protected final List<HwmfDrawProperties> propStack = new LinkedList<>();
private static final Float[] WEIGHT_MAP = {
900f, TextAttribute.WEIGHT_ULTRABOLD,
800f, TextAttribute.WEIGHT_EXTRABOLD,
750f, TextAttribute.WEIGHT_HEAVY,
700f, TextAttribute.WEIGHT_BOLD,
600f, TextAttribute.WEIGHT_DEMIBOLD,
500f, TextAttribute.WEIGHT_MEDIUM,
450f, TextAttribute.WEIGHT_SEMIBOLD,
400f, TextAttribute.WEIGHT_REGULAR,
300f, TextAttribute.WEIGHT_DEMILIGHT,
200f, TextAttribute.WEIGHT_LIGHT,
1f, TextAttribute.WEIGHT_EXTRA_LIGHT
};
private final List<HwmfDrawProperties> propStack = new LinkedList<>();
protected HwmfDrawProperties prop;
protected 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);
}

View File

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

View File

@ -21,7 +21,6 @@ import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.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());
}
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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
);
}
}

View File

@ -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);
}
}
@ -216,17 +213,28 @@ public class HwmfPicture implements Iterable<HwmfRecord>, GenericRecord {
}
/**
* Return the image size in points
* Return the image bound in points
*
* @return the image size in points
* @return the image bound in points
*/
public Dimension2D getSize() {
public Rectangle2D getBoundsInPoints() {
double inch = (placeableHeader == null) ? 1440 : placeableHeader.getUnitsPerInch();
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() {