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-ffa450edef68tags/before_ooxml_3rd_edition
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; | |||
}; |
@@ -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. |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
@@ -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; |
@@ -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 |
@@ -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()); | |||
} | |||
} | |||
/** |
@@ -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. */ |
@@ -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); |
@@ -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; | |||
} | |||
} | |||
/** |
@@ -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(); |
@@ -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; | |||
} | |||
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) { | |||
} | |||
return new Rectangle2D.Double(x, y, width, height); | |||
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); | |||
} | |||
} | |||
} |