mirror of https://github.com/apache/poi.git
#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:
parent
d835bdef42
commit
38b415e26a
|
@ -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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue