#64716 - wmf display error

EMF: workaround for invalid EMF header bounds
EMF: add option to PPTX2PNG / DrawableHint to fallback to force EMF header bounds 
EMF: use RGB instead of HSL gradiants

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1883051 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andreas Beeker 2020-11-01 15:37:20 +00:00
parent d835bdef42
commit 38b415e26a
13 changed files with 296 additions and 120 deletions

View File

@ -17,6 +17,8 @@
package org.apache.poi.common.usermodel;
import org.apache.poi.poifs.filesystem.FileMagic;
/**
* General enum class to define a picture format/type
*
@ -65,4 +67,37 @@ public enum PictureType {
this.contentType = contentType;
this.extension = extension;
}
public String getContentType() {
return contentType;
}
public String getExtension() {
return extension;
}
public static PictureType valueOf(FileMagic fm) {
switch (fm) {
case BMP:
return PictureType.BMP;
case GIF:
return PictureType.GIF;
case JPEG:
return PictureType.JPEG;
case PNG:
return PictureType.PNG;
case XML:
// this is quite fuzzy, to suppose all XMLs are SVGs when handling pictures ...
return PictureType.SVG;
case WMF:
return PictureType.WMF;
case EMF:
return PictureType.EMF;
case TIFF:
return PictureType.TIFF;
default:
case UNKNOWN:
return PictureType.UNKNOWN;
}
}
}

View File

@ -27,6 +27,8 @@ import java.util.ServiceLoader;
import java.util.function.Supplier;
import java.util.stream.StreamSupport;
import org.apache.poi.common.usermodel.PictureType;
import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.sl.usermodel.PictureData;
import org.apache.poi.sl.usermodel.PictureShape;
import org.apache.poi.sl.usermodel.RectAlign;
@ -36,11 +38,6 @@ import org.apache.poi.util.POILogger;
public class DrawPictureShape extends DrawSimpleShape {
private static final POILogger LOG = POILogFactory.getLogger(DrawPictureShape.class);
private static final String[] KNOWN_RENDERER = {
"org.apache.poi.hwmf.draw.HwmfImageRenderer",
"org.apache.poi.hemf.draw.HemfImageRenderer",
"org.apache.poi.xslf.draw.SVGImageRenderer"
};
public DrawPictureShape(PictureShape<?,?> shape) {
super(shape);
@ -60,10 +57,14 @@ public class DrawPictureShape extends DrawSimpleShape {
}
try {
String ct = data.getContentType();
byte[] dataBytes = data.getData();
PictureType type = PictureType.valueOf(FileMagic.valueOf(dataBytes));
String ct = (type == PictureType.UNKNOWN) ? data.getContentType() : type.getContentType();
ImageRenderer renderer = getImageRenderer(graphics, ct);
if (renderer.canRender(ct)) {
renderer.loadImage(data.getData(), ct);
renderer.loadImage(dataBytes, ct);
renderer.drawImage(graphics, anchor, insets);
return;
}
@ -92,7 +93,7 @@ public class DrawPictureShape extends DrawSimpleShape {
// the fallback is the BitmapImageRenderer, at least it gracefully handles invalid images
final Supplier<ImageRenderer> getFallback = () -> {
LOG.log(POILogger.WARN, "No suiteable image renderer found for content-type '"+
LOG.log(POILogger.WARN, "No suitable image renderer found for content-type '"+
contentType+"' - include poi-scratchpad (for wmf/emf) or poi-ooxml (for svg) jars!");
return fallback;
};

View File

@ -49,6 +49,7 @@ public interface Drawable {
case 12: return "CURRENT_SLIDE";
case 13: return "BUFFERED_IMAGE";
case 14: return "DEFAULT_CHARSET";
case 15: return "EMF_FORCE_HEADER_BOUNDS";
default: return "UNKNOWN_ID "+intKey();
}
}
@ -153,6 +154,17 @@ public interface Drawable {
*/
DrawableHint DEFAULT_CHARSET = new DrawableHint(14);
/**
* A boolean value to force the usage of the bounding box, which is specified in the EMF header.
* Defaults to {@code FALSE} - in this case the records are scanned for window and
* viewport records to determine the initial bounding box by using the following
* condition: {@code isValid(viewport) ? viewport : isValid(window) ? window : headerBounds }
* <p>
* This is a workaround switch, which might be removed in future releases, when the bounding box
* determination for the special cases is fixed.
* In most cases it's recommended to leave the default value.
*/
DrawableHint EMF_FORCE_HEADER_BOUNDS = new DrawableHint(15);
/**
* Apply 2-D transforms before drawing this shape. This includes rotation and flipping.

View File

@ -321,34 +321,11 @@ public abstract class SignatureLine {
*/
protected byte[] plainPng() throws IOException {
byte[] plain = getPlainSignature();
PictureType pictureType;
switch (FileMagic.valueOf(plain)) {
case PNG:
return plain;
case BMP:
pictureType = PictureType.BMP;
break;
case EMF:
pictureType = PictureType.EMF;
break;
case GIF:
pictureType = PictureType.GIF;
break;
case JPEG:
pictureType = PictureType.JPEG;
break;
case XML:
pictureType = PictureType.SVG;
break;
case TIFF:
pictureType = PictureType.TIFF;
break;
default:
throw new IllegalArgumentException("Unsupported picture format");
PictureType pictureType = PictureType.valueOf(FileMagic.valueOf(plain));
if (pictureType == PictureType.UNKNOWN) {
throw new IllegalArgumentException("Unsupported picture format");
}
ImageRenderer rnd = DrawPictureShape.getImageRenderer(null, pictureType.contentType);
if (rnd == null) {
throw new UnsupportedOperationException(pictureType + " can't be rendered - did you provide poi-scratchpad and its dependencies (batik et al.)");
@ -375,11 +352,8 @@ public abstract class SignatureLine {
/**
* Generate the image for a signature line
* @param caption three lines separated by "\n" - usually something like "First name Last name\nRole\nname of the key"
* @param inputImage the plain signature - supported formats are PNG,GIF,JPEG,(SVG),EMF,WMF.
* for SVG,EMF,WMF poi-scratchpad needs to be in the class-/modulepath
* if {@code null}, the inputImage is not rendered
* @param invalidText for invalid signature images, use the given text
* @param showSignature show signature image - use {@code false} for placeholder images in to-be-signed documents
* @param showInvalidStamp print invalid stamp over the signature
* @return the signature image in PNG format as byte array
*/
protected byte[] generateImage(boolean showSignature, boolean showInvalidStamp) throws IOException {
@ -462,28 +436,11 @@ public abstract class SignatureLine {
private void determineContentType() {
FileMagic fm = FileMagic.valueOf(plainSignature);
switch (fm) {
case GIF:
contentType = PictureType.GIF.contentType;
break;
case PNG:
contentType = PictureType.PNG.contentType;
break;
case JPEG:
contentType = PictureType.JPEG.contentType;
break;
case XML:
contentType = PictureType.SVG.contentType;
break;
case EMF:
contentType = PictureType.EMF.contentType;
break;
case WMF:
contentType = PictureType.WMF.contentType;
break;
default:
throw new IllegalArgumentException("unknown image type");
PictureType type = PictureType.valueOf(fm);
if (type == PictureType.UNKNOWN) {
throw new IllegalArgumentException("unknown image type");
}
contentType = type.contentType;
}
}

View File

@ -75,7 +75,8 @@ public final class PPTX2PNG {
" some files (usually wmf) don't have a header, i.e. an identifiable file magic\n" +
" -textAsShapes text elements are saved as shapes in SVG, necessary for variable spacing\n" +
" often found in math formulas\n" +
" -charset <cs> sets the default charset to be used, defaults to Windows-1252";
" -charset <cs> sets the default charset to be used, defaults to Windows-1252\n" +
" -emfHeaderBounds force the usage of the emf header bounds to calculate the bounding box";
System.out.println(msg);
// no System.exit here, as we also run in junit tests!
@ -104,6 +105,7 @@ public final class PPTX2PNG {
private FileMagic defaultFileType = FileMagic.OLE2;
private boolean textAsShapes = false;
private Charset charset = LocaleUtil.CHARSET_1252;
private boolean emfHeaderBounds = false;
private PPTX2PNG() {
}
@ -189,7 +191,9 @@ public final class PPTX2PNG {
charset = LocaleUtil.CHARSET_1252;
}
break;
case "-emfheaderbounds":
emfHeaderBounds = true;
break;
default:
file = new File(args[i]);
break;
@ -279,6 +283,7 @@ public final class PPTX2PNG {
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
graphics.setRenderingHint(Drawable.DEFAULT_CHARSET, getDefaultCharset());
graphics.setRenderingHint(Drawable.EMF_FORCE_HEADER_BOUNDS, emfHeaderBounds);
graphics.scale(scale / lenSide, scale / lenSide);

View File

@ -101,6 +101,9 @@ public class HemfComment {
*/
default void draw(HemfGraphics ctx) {}
default void calcBounds(Rectangle2D bounds, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { }
@Override
default HemfCommentRecordType getGenericRecordType() {
return getCommentRecordType();
@ -131,6 +134,11 @@ public class HemfComment {
data.draw(ctx);
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
data.calcBounds(window, viewport, renderState);
}
@Override
public String toString() {
return GenericRecordJsonWriter.marshal(this);
@ -332,6 +340,17 @@ public class HemfComment {
records.forEach(ctx::draw);
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, EmfRenderState[] renderState) {
renderState[0] = EmfRenderState.EMFPLUS_ONLY;
for (HemfPlusRecord r : records) {
r.calcBounds(window, viewport, renderState);
if (!window.isEmpty() && !viewport.isEmpty()) {
break;
}
}
}
@Override
public Map<String, Supplier<?>> getGenericProperties() {
return null;

View File

@ -18,6 +18,7 @@
package org.apache.poi.hemf.record.emf;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.Map;
import java.util.function.Supplier;
@ -56,6 +57,9 @@ public interface HemfRecord extends GenericRecord {
}
}
default void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
}
/**
* Sets the header reference, in case the record needs to refer to it
* @param header the emf header

View File

@ -22,6 +22,7 @@ import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL;
import static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.Map;
import java.util.function.Supplier;
@ -56,6 +57,13 @@ public class HemfWindowing {
public HemfRecordType getGenericRecordType() {
return getEmfRecordType();
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
double x = window.getX();
double y = window.getY();
window.setRect(x,y,size.getWidth(),size.getHeight());
}
}
/**
@ -76,6 +84,13 @@ public class HemfWindowing {
public HemfRecordType getGenericRecordType() {
return getEmfRecordType();
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
double w = window.getWidth();
double h = window.getHeight();
window.setRect(origin.getX(),origin.getY(),w,h);
}
}
/**
@ -96,6 +111,13 @@ public class HemfWindowing {
public HemfRecordType getGenericRecordType() {
return getEmfRecordType();
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
double x = viewport.getX();
double y = viewport.getY();
viewport.setRect(x,y,extents.getWidth(),extents.getHeight());
}
}
/**
@ -116,6 +138,13 @@ public class HemfWindowing {
public HemfRecordType getGenericRecordType() {
return getEmfRecordType();
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
double w = viewport.getWidth();
double h = viewport.getHeight();
viewport.setRect(origin.getX(), origin.getY(), w, h);
}
}
/**
@ -200,6 +229,16 @@ public class HemfWindowing {
public HemfRecordType getGenericRecordType() {
return getEmfRecordType();
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
double x = viewport.getX();
double y = viewport.getY();
double w = viewport.getWidth();
double h = viewport.getHeight();
viewport.setRect(x,y,w * scale.getWidth(),h * scale.getHeight());
}
}
/**
@ -221,6 +260,15 @@ public class HemfWindowing {
public HemfRecordType getGenericRecordType() {
return getEmfRecordType();
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
double x = window.getX();
double y = window.getY();
double w = window.getWidth();
double h = window.getHeight();
window.setRect(x,y,w * scale.getWidth(),h * scale.getHeight());
}
}
/**

View File

@ -35,7 +35,6 @@ 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;
@ -519,7 +518,7 @@ public class HemfPlusBrush {
public static class EmfPlusLinearGradientBrushData implements EmfPlusBrushData {
private int dataFlags;
private EmfPlusWrapMode wrapMode;
private Rectangle2D rect = new Rectangle2D.Double();
private final Rectangle2D rect = new Rectangle2D.Double();
private Color startColor, endColor;
private AffineTransform blendTransform;
private float[] positions;
@ -529,8 +528,8 @@ public class HemfPlusBrush {
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" };
private static final int[] FLAG_MASKS = {0x02, 0x04, 0x08, 0x10, 0x80};
private static final 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 {
@ -543,7 +542,7 @@ public class HemfPlusBrush {
// gradient is repeated.
wrapMode = EmfPlusWrapMode.valueOf(leis.readInt());
int size = 2*LittleEndianConsts.INT_SIZE;
int size = 2 * LittleEndianConsts.INT_SIZE;
size += readRectF(leis, rect);
// An EmfPlusARGB object that specifies the color at the starting/ending boundary point of the linear gradient brush.
@ -551,9 +550,9 @@ public class HemfPlusBrush {
endColor = readARGB(leis.readInt());
// skip reserved1/2 fields
leis.skipFully(2*LittleEndianConsts.INT_SIZE);
leis.skipFully(2 * LittleEndianConsts.INT_SIZE);
size += 4*LittleEndianConsts.INT_SIZE;
size += 4 * LittleEndianConsts.INT_SIZE;
if (TRANSFORM.isSet(dataFlags)) {
size += readXForm(leis, (blendTransform = new AffineTransform()));
@ -586,7 +585,7 @@ public class HemfPlusBrush {
setColorProps(prop::setBrushColorsV, positionsV, this::getBlendVColorAt);
if (!(isPreset() || isBlendH() || isBlendV())) {
prop.setBrushColorsH(Arrays.asList(kv(0f,startColor), kv(1f,endColor)));
prop.setBrushColorsH(Arrays.asList(kv(0f, startColor), kv(1f, endColor)));
}
}
@ -607,7 +606,7 @@ public class HemfPlusBrush {
@Override
public Map<String, Supplier<?>> getGenericProperties() {
final Map<String,Supplier<?>> m = new LinkedHashMap<>();
final Map<String, Supplier<?>> m = new LinkedHashMap<>();
m.put("flags", GenericRecordUtil.getBitsAsString(() -> dataFlags, FLAG_MASKS, FLAG_NAMES));
m.put("wrapMode", () -> wrapMode);
m.put("rect", () -> rect);
@ -635,24 +634,24 @@ public class HemfPlusBrush {
return BLEND_FACTORS_V.isSet(dataFlags);
}
private Map.Entry<Float,Color> getBlendColorAt(int index) {
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> 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 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) {
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) {
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 {
@ -661,8 +660,26 @@ public class HemfPlusBrush {
}
private Color interpolateColors(final double factor) {
// https://stackoverflow.com/questions/1416560/hsl-interpolation
return interpolateColorsRGB(factor);
}
private Color interpolateColorsRGB(final double factor) {
// TODO: check IS_GAMMA_CORRECTED flag and maybe don't convert into scRGB
double[] start = DrawPaint.RGB2SCRGB(startColor);
double[] end = DrawPaint.RGB2SCRGB(endColor);
// compute the interpolated color in linear space
int a = (int)Math.round(startColor.getAlpha() + factor * (endColor.getAlpha() - startColor.getAlpha()));
double r = start[0] + factor * (end[0] - start[0]);
double g = start[1] + factor * (end[1] - start[1]);
double b = start[2] + factor * (end[2] - start[2]);
Color inter = DrawPaint.SCRGB2RGB(r,g,b);
return new Color(inter.getRed(), inter.getGreen(), inter.getBlue(), a);
}
/*
private Color interpolateColorsHSL(final double factor) {
final double[] hslStart = DrawPaint.RGB2HSL(startColor);
final double[] hslStop = DrawPaint.RGB2HSL(endColor);
@ -673,16 +690,24 @@ public class HemfPlusBrush {
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.;
// find closest match - decide if need to go clockwise or counter-clockwise
// https://stackoverflow.com/questions/1416560/hsl-interpolation
double hueMidCW = (hslStart[0]+hslStop[0])/2.;
double hueMidCCW = (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;
double hslDiff;
if (hueDelta.apply(hueMidCW) > hueDelta.apply(hueMidCCW)) {
hslDiff = (hslStart[0] < hslStop[0]) ? hslStop[0]-hslStart[0] : (360-hslStart[0])+hslStop[0];
} else {
hslDiff = (hslStart[0] < hslStop[0]) ? -hslStart[0]-(360-hslStop[0]) : -(hslStart[0]-hslStop[0]);
}
double hue = (hslStart[0]+hslDiff*factor)%360.;
return DrawPaint.HSL2RGB(hue, sat, lum, alpha/255.);
}
} */
}
/** The EmfPlusPathGradientBrushData object specifies a path gradient for a graphics brush. */

View File

@ -20,6 +20,7 @@ package org.apache.poi.hemf.record.emfplus;
import static org.apache.poi.util.GenericRecordUtil.getEnumBitsAsString;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.Map;
import java.util.function.Supplier;
@ -134,6 +135,11 @@ public class HemfPlusHeader implements HemfPlusRecord {
ctx.setRenderState(EmfRenderState.EMF_DCONTEXT);
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, EmfRenderState[] renderState) {
renderState[0] = EmfRenderState.EMF_DCONTEXT;
}
@Override
public String toString() {
return GenericRecordJsonWriter.marshal(this);

View File

@ -163,6 +163,11 @@ public class HemfPlusMisc {
public void draw(HemfGraphics ctx) {
ctx.setRenderState(HemfGraphics.EmfRenderState.EMF_DCONTEXT);
}
@Override
public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
renderState[0] = HemfGraphics.EmfRenderState.EMF_DCONTEXT;
}
}
/**

View File

@ -18,6 +18,7 @@
package org.apache.poi.hemf.record.emfplus;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import org.apache.poi.common.usermodel.GenericRecord;
@ -55,6 +56,9 @@ public interface HemfPlusRecord extends GenericRecord {
default void draw(HemfGraphics ctx) {
}
default void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) {
}
@Override
default HemfPlusRecordType getGenericRecordType() {
return getEmfPlusRecordType();

View File

@ -19,6 +19,8 @@ package org.apache.poi.hemf.usermodel;
import static java.lang.Math.abs;
import static org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState.EMFPLUS_ONLY;
import static org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState.EMF_ONLY;
import java.awt.Graphics2D;
import java.awt.Shape;
@ -36,14 +38,14 @@ 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.emf.HemfComment;
import org.apache.poi.hemf.record.emf.HemfHeader;
import org.apache.poi.hemf.record.emf.HemfRecord;
import org.apache.poi.hemf.record.emf.HemfRecordIterator;
import org.apache.poi.hemf.record.emf.HemfWindowing;
import org.apache.poi.hwmf.usermodel.HwmfCharsetAware;
import org.apache.poi.hwmf.usermodel.HwmfEmbedded;
import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.util.Dimension2DDouble;
import org.apache.poi.util.Internal;
import org.apache.poi.util.LittleEndianInputStream;
@ -114,26 +116,36 @@ public class HemfPicture implements Iterable<HemfRecord>, GenericRecord {
*/
public Rectangle2D getBounds() {
Rectangle2D dim = getHeader().getFrameRectangle();
double x = dim.getX(), y = dim.getY();
double width = dim.getWidth(), height = dim.getHeight();
if (dim.isEmpty() || Math.rint(width) == 0 || Math.rint(height) == 0) {
for (HemfRecord r : getRecords()) {
if (r instanceof HemfWindowing.EmfSetWindowExtEx) {
HemfWindowing.EmfSetWindowExtEx extEx = (HemfWindowing.EmfSetWindowExtEx)r;
Dimension2D d = extEx.getSize();
width = d.getWidth();
height = d.getHeight();
// keep searching - sometimes there's another record
}
if (r instanceof HemfWindowing.EmfSetWindowOrgEx) {
HemfWindowing.EmfSetWindowOrgEx orgEx = (HemfWindowing.EmfSetWindowOrgEx)r;
x = orgEx.getX();
y = orgEx.getY();
}
boolean isInvalid = ReluctantRectangle2D.isEmpty(dim);
if (isInvalid) {
Rectangle2D lastDim = new ReluctantRectangle2D();
getInnerBounds(lastDim, new ReluctantRectangle2D());
if (!lastDim.isEmpty()) {
return lastDim;
}
}
return dim;
}
return new Rectangle2D.Double(x, y, width, height);
public void getInnerBounds(Rectangle2D window, Rectangle2D viewport) {
HemfGraphics.EmfRenderState[] renderState = { HemfGraphics.EmfRenderState.INITIAL };
for (HemfRecord r : getRecords()) {
if (
(renderState[0] == EMF_ONLY && r instanceof HemfComment.EmfComment) ||
(renderState[0] == EMFPLUS_ONLY && !(r instanceof HemfComment.EmfComment))
) {
continue;
}
try {
r.calcBounds(window, viewport, renderState);
} catch (RuntimeException ignored) {
}
if (!window.isEmpty() && !viewport.isEmpty()) {
break;
}
}
}
/**
@ -154,32 +166,36 @@ public class HemfPicture implements Iterable<HemfRecord>, GenericRecord {
final Rectangle2D b = getBoundsInPoints();
return new Dimension2DDouble(abs(b.getWidth()), abs(b.getHeight()));
}
private static double minX(Rectangle2D bounds) {
return Math.min(bounds.getMinX(), bounds.getMaxX());
}
private static double minY(Rectangle2D bounds) {
return Math.min(bounds.getMinY(), bounds.getMaxY());
}
public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) {
final Shape clip = ctx.getClip();
final AffineTransform at = ctx.getTransform();
try {
Rectangle2D emfBounds = getHeader().getBoundsRectangle();
Rectangle2D winBounds = new ReluctantRectangle2D();
Rectangle2D viewBounds = new ReluctantRectangle2D();
getInnerBounds(winBounds, viewBounds);
Boolean forceHeader = (Boolean)ctx.getRenderingHint(Drawable.EMF_FORCE_HEADER_BOUNDS);
if (forceHeader == null) {
forceHeader = false;
}
// this is a compromise ... sometimes winBounds are totally off :(
// but mostly they fit better than the header bounds
Rectangle2D b =
!viewBounds.isEmpty() && !forceHeader
? viewBounds
: !winBounds.isEmpty() && !forceHeader
? winBounds
: emfBounds;
// scale output bounds to image bounds
ctx.translate(graphicsBounds.getCenterX(), graphicsBounds.getCenterY());
ctx.scale(graphicsBounds.getWidth()/emfBounds.getWidth(), graphicsBounds.getHeight()/emfBounds.getHeight());
ctx.translate(-emfBounds.getCenterX(), -emfBounds.getCenterY());
ctx.scale(
graphicsBounds.getWidth()/b.getWidth(),
graphicsBounds.getHeight()/b.getHeight()
);
ctx.translate(-b.getCenterX(),-b.getCenterY());
HemfGraphics g = new HemfGraphics(ctx, emfBounds);
HemfDrawProperties prop = g.getProperties();
prop.setWindowOrg(emfBounds.getX(), emfBounds.getY());
prop.setWindowExt(emfBounds.getWidth(), emfBounds.getHeight());
prop.setViewportOrg(emfBounds.getX(), emfBounds.getY());
prop.setViewportExt(emfBounds.getWidth(), emfBounds.getHeight());
HemfGraphics g = new HemfGraphics(ctx, b);
for (HemfRecord r : getRecords()) {
try {
@ -214,4 +230,43 @@ public class HemfPicture implements Iterable<HemfRecord>, GenericRecord {
public Charset getDefaultCharset() {
return defaultCharset;
}
private static class ReluctantRectangle2D extends Rectangle2D.Double {
private boolean offsetSet = false;
private boolean rangeSet = false;
public ReluctantRectangle2D() {
super(-1,-1,0,0);
}
@Override
public void setRect(double x, double y, double w, double h) {
if (offsetSet && rangeSet) {
return;
}
super.setRect(
offsetSet ? this.x : x,
offsetSet ? this.y : y,
rangeSet ? this.width : w,
rangeSet ? this.height : h);
offsetSet |= (x != -1 || y != -1);
rangeSet |= (w != 0 || h != 0);
}
@Override
public boolean isEmpty() {
return isEmpty(this);
}
public static boolean isEmpty(Rectangle2D r) {
double w = Math.rint(r.getWidth());
double h = Math.rint(r.getHeight());
return
(w <= 0.0) || (h <= 0.0) ||
(r.getX() == -1 && r.getY() == -1) ||
// invalid emf bound have sometimes 1,1 as dimension
(w == 1 && h == 1);
}
}
}