public SVGPOIGraphics2D(Document document, boolean textAsShapes) {
super(getCtx(document), textAsShapes);
hints = getGeneratorContext().getGraphicContextDefaults().getRenderingHints();
+ ((SVGRenderExtension)getGeneratorContext().getExtensionHandler()).setSvgGraphics2D(this);
}
private static SVGGeneratorContext getCtx(Document document) {
package org.apache.poi.xslf.draw;
import static java.awt.MultipleGradientPaint.ColorSpaceType.LINEAR_RGB;
+import static org.apache.batik.svggen.SVGSyntax.ID_PREFIX_IMAGE;
+import static org.apache.batik.svggen.SVGSyntax.ID_PREFIX_PATTERN;
+import static org.apache.batik.svggen.SVGSyntax.SIGN_POUND;
+import static org.apache.batik.svggen.SVGSyntax.URL_PREFIX;
+import static org.apache.batik.svggen.SVGSyntax.URL_SUFFIX;
import static org.apache.batik.util.SVGConstants.*;
+import static org.apache.poi.sl.usermodel.PictureData.PictureType.GIF;
+import static org.apache.poi.sl.usermodel.PictureData.PictureType.JPEG;
+import static org.apache.poi.sl.usermodel.PictureData.PictureType.PNG;
import java.awt.Color;
import java.awt.LinearGradientPaint;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
+import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.CRC32;
+
+import javax.imageio.ImageIO;
import org.apache.batik.svggen.DefaultExtensionHandler;
import org.apache.batik.svggen.SVGColor;
import org.apache.batik.svggen.SVGGeneratorContext;
+import org.apache.batik.svggen.SVGGraphics2D;
import org.apache.batik.svggen.SVGPaintDescriptor;
import org.apache.batik.svggen.SVGTexturePaint;
+import org.apache.poi.sl.draw.BitmapImageRenderer;
+import org.apache.poi.sl.draw.DrawTexturePaint;
import org.apache.poi.sl.draw.Drawable;
+import org.apache.poi.sl.draw.ImageRenderer;
import org.apache.poi.sl.draw.PathGradientPaint;
import org.apache.poi.sl.draw.PathGradientPaint.PathGradientContext;
+import org.apache.poi.sl.usermodel.Insets2D;
+import org.apache.poi.sl.usermodel.PaintStyle;
+import org.apache.poi.sl.usermodel.SimpleShape;
+import org.apache.poi.util.Dimension2DDouble;
import org.apache.poi.util.Internal;
+import org.w3c.dom.Document;
import org.w3c.dom.Element;
*/
@Internal
public class SVGRenderExtension extends DefaultExtensionHandler {
+ private static final int LINE_LENGTH = 65;
+ private static final String XLINK_NS = "http://www.w3.org/1999/xlink";
+ private final Map<Long, String> imageMap = new HashMap<>();
+ private WeakReference<SVGGraphics2D> svgGraphics2D = null;
+
+
+ public SVGGraphics2D getSvgGraphics2D() {
+ return (svgGraphics2D != null) ? svgGraphics2D.get() : null;
+ }
+
+ public void setSvgGraphics2D(SVGGraphics2D svgGraphics2D) {
+ this.svgGraphics2D = new WeakReference<>(svgGraphics2D);
+ }
+
+
@Override
public SVGPaintDescriptor handlePaint(Paint paint, SVGGeneratorContext generatorContext) {
if (paint instanceof LinearGradientPaint) {
return getPathDescriptor((PathGradientPaint)paint, generatorContext);
}
+ if (paint instanceof DrawTexturePaint) {
+ return getDtpDescriptor((DrawTexturePaint)paint, generatorContext);
+ }
+
return super.handlePaint(paint, generatorContext);
}
}
// Convert gradient stops
- Color[] colors = gradient.getColors();
- float[] fracs = gradient.getFractions();
+ final Color[] colors = gradient.getColors();
+ final float[] fracs = gradient.getFractions();
for (int i = 0; i < colors.length; i++) {
Element stop = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG);
gradElem.setAttribute(x, Double.toString(point.getX()));
gradElem.setAttribute(y, Double.toString(point.getY()));
}
+
+ private SVGPaintDescriptor getDtpDescriptor(DrawTexturePaint tdp, SVGGeneratorContext genCtx) {
+ String imgID = getImageID(tdp, genCtx);
+ Document domFactory = genCtx.getDOMFactory();
+
+ Element patternDef = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_PATTERN_TAG);
+ String patID = genCtx.getIDGenerator().generateID(ID_PREFIX_PATTERN);
+
+ PaintStyle.TexturePaint fill = tdp.getFill();
+
+ Insets2D stretch = fill.getStretch();
+ if (stretch == null) {
+ stretch = new Insets2D(0,0,0,0);
+ }
+
+ Rectangle2D anchorRect = tdp.getAnchorRect();
+ String x = genCtx.doubleString(-stretch.left/100_000 * anchorRect.getWidth());
+ String y = genCtx.doubleString(-stretch.top/100_000 * anchorRect.getHeight());
+ String w = genCtx.doubleString((100_000+stretch.left+stretch.right)/100_000 * anchorRect.getWidth());
+ String h = genCtx.doubleString((100_000+stretch.top+stretch.bottom)/100_000 * anchorRect.getHeight());
+
+ Dimension2D scale = fill.getScale();
+ if (scale == null) {
+ scale = new Dimension2DDouble(1,1);
+ }
+ Point2D offset = fill.getOffset();
+ if (offset == null) {
+ offset = new Point2D.Double(0,0);
+ }
+
+ PaintStyle.FlipMode flipMode = fill.getFlipMode();
+ if (flipMode == null) {
+ flipMode = PaintStyle.FlipMode.NONE;
+ }
+
+ setAttribute(genCtx, patternDef,
+ null, SVG_PATTERN_UNITS_ATTRIBUTE, SVG_OBJECT_BOUNDING_BOX_VALUE,
+ null, SVG_ID_ATTRIBUTE, patID,
+ null, SVG_X_ATTRIBUTE, offset.getX(),
+ null, SVG_Y_ATTRIBUTE, offset.getY(),
+ null, SVG_WIDTH_ATTRIBUTE, genCtx.doubleString(scale.getWidth()*100)+"%",
+ null, SVG_HEIGHT_ATTRIBUTE, genCtx.doubleString(scale.getHeight()*100)+"%",
+ null, SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE, SVG_NONE_VALUE,
+ null, SVG_VIEW_BOX_ATTRIBUTE, x+" "+ y+" "+ w+" "+h
+ );
+
+ org.apache.poi.sl.usermodel.Shape slShape = fill.getShape();
+
+ // TODO: the rotation handling is incomplete and the scale handling is missing
+ // see DrawTexturePaint on how to do it for AWT
+ if (!fill.isRotatedWithShape() && slShape instanceof SimpleShape) {
+ double rot = ((SimpleShape)slShape).getRotation();
+ if (rot != 0) {
+ setAttribute(genCtx, patternDef,
+ null, SVG_PATTERN_TRANSFORM_ATTRIBUTE, "rotate(" + genCtx.doubleString(-rot) + ")");
+ }
+ }
+
+ Element useImageEl = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_USE_TAG);
+ useImageEl.setAttributeNS(null, "href", "#"+imgID);
+ patternDef.appendChild(useImageEl);
+
+ String patternAttrBuf = URL_PREFIX + SIGN_POUND + patID + URL_SUFFIX;
+ return new SVGPaintDescriptor(patternAttrBuf, SVG_OPAQUE_VALUE, patternDef);
+ }
+
+ private String getImageID(DrawTexturePaint tdp, SVGGeneratorContext genCtx) {
+ final ImageRenderer imgRdr = tdp.getImageRenderer();
+
+ byte[] imgData = null;
+ String contentType = null;
+ if (imgRdr instanceof BitmapImageRenderer) {
+ BitmapImageRenderer bir = (BitmapImageRenderer)imgRdr;
+ String ct = bir.getCachedContentType();
+ if (PNG.contentType.equals(ct) ||
+ JPEG.contentType.equals(ct) ||
+ GIF.contentType.equals(ct)) {
+ contentType = ct;
+ imgData = bir.getCachedImage();
+ }
+ }
+ if (imgData == null) {
+ BufferedImage bi = imgRdr.getImage();
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try {
+ ImageIO.write(bi, "PNG", bos);
+ } catch (IOException e) {
+ return null;
+ }
+ imgData = bos.toByteArray();
+ contentType = PNG.contentType;
+ }
+
+ CRC32 crc = new CRC32();
+ crc.update(imgData);
+ Long imageCrc = crc.getValue();
+
+ String imgID = imageMap.get(imageCrc);
+ if (imgID != null) {
+ return imgID;
+ }
+
+ Document domFactory = genCtx.getDOMFactory();
+ Rectangle2D anchorRect = tdp.getAnchorRect();
+
+ imgID = genCtx.getIDGenerator().generateID(ID_PREFIX_IMAGE);
+ imageMap.put(imageCrc, imgID);
+
+ // length of a base64 string
+ int sbLen = ((4 * imgData.length / 3) + 3) & ~3;
+ // add line breaks every 65 chars and a few more padding chars
+ sbLen += sbLen / LINE_LENGTH + 30;
+ StringBuilder sb = new StringBuilder(sbLen);
+ sb.append("data:");
+ sb.append(contentType);
+ sb.append(";base64,\n");
+ sb.append(Base64.getMimeEncoder(LINE_LENGTH, "\n".getBytes(StandardCharsets.US_ASCII)).encodeToString(imgData));
+
+ Element imageEl = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_IMAGE_TAG);
+ setAttribute(genCtx, imageEl,
+ null, SVG_ID_ATTRIBUTE, imgID,
+ null, SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE, SVG_NONE_VALUE,
+ null, SVG_X_ATTRIBUTE, anchorRect.getX(),
+ null, SVG_Y_ATTRIBUTE, anchorRect.getY(),
+ null, SVG_WIDTH_ATTRIBUTE, anchorRect.getWidth(),
+ null, SVG_HEIGHT_ATTRIBUTE, anchorRect.getHeight(),
+ XLINK_NS, "xlink:href", sb.toString()
+ );
+
+ getSvgGraphics2D().getDOMTreeManager().addOtherDef(imageEl);
+
+ return imgID;
+ }
+
+ private static void setAttribute(SVGGeneratorContext genCtx, Element el, Object... params) {
+ for (int i=0; i<params.length; i+=3) {
+ String ns = (String)params[i];
+ String name = (String)params[i+1];
+ Object oval = params[i+2];
+ String val;
+ if (oval instanceof String) {
+ val = (String)oval;
+ } else if (oval instanceof Number) {
+ val = genCtx.doubleString(((Number) oval).doubleValue());
+ } else if (oval == null) {
+ val = "";
+ } else {
+ val = oval.toString();
+ }
+ el.setAttributeNS(ns, name, val);
+ }
+ }
}
@SuppressWarnings("WeakerAccess")
protected PaintStyle selectPaint(final CTBlipFillProperties blipFill, final PackagePart parentPart, CTSchemeColor phClr, final XSLFTheme theme) {
- return new XSLFTexturePaint(blipFill, parentPart, phClr, theme, _sheet);
+ return new XSLFTexturePaint(this, blipFill, parentPart, phClr, theme, _sheet);
}
@SuppressWarnings("WeakerAccess")
import org.apache.poi.sl.usermodel.ColorStyle;
import org.apache.poi.sl.usermodel.Insets2D;
import org.apache.poi.sl.usermodel.PaintStyle;
+import org.apache.poi.sl.usermodel.Shape;
import org.apache.poi.util.Dimension2DDouble;
import org.apache.poi.util.Internal;
import org.apache.poi.util.Units;
@Internal
public class XSLFTexturePaint implements PaintStyle.TexturePaint {
+ private final XSLFShape shape;
private final CTBlipFillProperties blipFill;
private final PackagePart parentPart;
private final CTBlip blip;
private final XSLFTheme theme;
private final XSLFSheet sheet;
- public XSLFTexturePaint(final CTBlipFillProperties blipFill, final PackagePart parentPart, CTSchemeColor phClr, final XSLFTheme theme, final XSLFSheet sheet) {
+ public XSLFTexturePaint(final XSLFShape shape, final CTBlipFillProperties blipFill, final PackagePart parentPart, CTSchemeColor phClr, final XSLFTheme theme, final XSLFSheet sheet) {
+ this.shape = shape;
this.blipFill = blipFill;
this.parentPart = parentPart;
blip = blipFill.getBlip();
@Override
public boolean isRotatedWithShape() {
- return blipFill.isSetRotWithShape() && blipFill.getRotWithShape();
+ return !blipFill.isSetRotWithShape() || blipFill.getRotWithShape();
}
@Override
return colors;
}
+ @Override
+ public Shape getShape() {
+ return shape;
+ }
private static Insets2D getRectVal(CTRelativeRect rect) {
return rect == null ? null : new Insets2D(
import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2D;
+import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.util.Internal;
import org.apache.poi.xslf.draw.SVGPOIGraphics2D;
import org.w3c.dom.DOMImplementation;
Document document = domImpl.createDocument(svgNS, "svg", null);
svgGenerator = new SVGPOIGraphics2D(document, textAsShapes);
svgGenerator.setSVGCanvasSize(new Dimension((int)width, (int)height));
+ svgGenerator.setRenderingHint(Drawable.CACHE_IMAGE_SOURCE, true);
return svgGenerator;
}
import org.apache.poi.sl.usermodel.PaintStyle.GradientPaint.GradientType;
import org.apache.poi.sl.usermodel.PaintStyle.TexturePaint;
import org.apache.poi.sl.usermodel.PictureData;
+import org.apache.poi.sl.usermodel.Shape;
import org.apache.poi.util.BitField;
import org.apache.poi.util.BitFieldFactory;
import org.apache.poi.util.LittleEndian;
* The default value for this property is 0x0.
*/
private static final BitField FILL_USE_HIT_TEST_FILL = BitFieldFactory.getInstance(0x00080000);
-
+
/**
* A bit that specifies whether the fillShape bit is set.
* A value of 0x0 specifies that the fillShape MUST be ignored.
* The default value for this property is 0x0.
*/
private static final BitField FILL_USE_FILL_SHAPE = BitFieldFactory.getInstance(0x00040000);
-
+
/**
* A bit that specifies whether the fillUseRect bit is set.
* A value of 0x0 specifies that the fillUseRect MUST be ignored.
* The default value for this property is 0x0.
*/
private static final BitField FILL_USE_FILL_USE_RECT = BitFieldFactory.getInstance(0x00020000);
-
+
/**
* A bit that specifies whether the fNoFillHitTest bit is set.
* A value of 0x0 specifies that the fNoFillHitTest MUST be ignored.
* The default value for this property is 0x0.
*/
private static final BitField FILL_USE_NO_FILL_HIT_TEST = BitFieldFactory.getInstance(0x00010000);
-
+
/**
* A bit that specifies how to recolor a picture fill. If this bit is set to 0x1, the pictureFillCrMod
* property of the picture fill is used for recoloring. If this bit is set to 0x0, the fillCrMod property,
* The default value for this property is 0x0.
*/
private static final BitField FILL_RECOLOR_FILL_AS_PICTURE = BitFieldFactory.getInstance(0x00000040);
-
+
/**
* A bit that specifies whether the fill is rotated with the shape.
* If UseUseShapeAnchor equals 0x0, this value MUST be ignored.
* The default value for this property is 0x0.
*/
private static final BitField FILL_USE_SHAPE_ANCHOR = BitFieldFactory.getInstance(0x00000020);
-
+
/**
* A bit that specifies whether the fill is rendered if the shape is a 2-D shape.
* If this bit is set to 0x1, the fill of this shape is rendered based on the properties of the Fill Style
* If UseFilled is 0x0, this value MUST be ignored. The default value for this property is 0x1.
*/
private static final BitField FILL_FILLED = BitFieldFactory.getInstance(0x00000010);
-
+
/**
* A bit that specifies whether this fill will be hit tested.
* If UsefHitTestFill equals 0x0, this value MUST be ignored.
* The default value for this property is 0x1.
*/
private static final BitField FILL_HIT_TEST_FILL = BitFieldFactory.getInstance(0x00000008);
-
+
/**
* A bit that specifies how the fill is aligned. If this bit is set to 0x1, the fill is
* aligned relative to the shape so that it moves with the shape. If this bit is set to 0x0,
* The default value for this property is 0x1.
*/
private static final BitField FILL_FILL_SHAPE = BitFieldFactory.getInstance(0x00000004);
-
+
/**
* A bit that specifies whether to use the rectangle specified by the fillRectLeft, fillRectRight,
* fillRectTop, and fillRectBottom properties, rather than the bounding rectangle of the shape,
* The default value for this property is 0x0.
*/
private static final BitField FILL_FILL_USE_RECT = BitFieldFactory.getInstance(0x00000002);
-
+
/**
* A bit that specifies whether this shape will be hit tested as though it were filled.
* If UsefNoFillHitTest equals 0x0, this value MUST be ignored.
return null;
}
}
-
+
private boolean isRotatedWithShape() {
// NOFILLHITTEST can be in the normal escher opt record but also in the tertiary record
// the extended bit fields seem to be in the second
public ColorStyle[] getGradientColors() {
return colors.stream().map(this::wrapColor).toArray(ColorStyle[]::new);
}
-
+
private ColorStyle wrapColor(Color col) {
return (col == null) ? null : DrawPaint.createSolidPaint(col).getSolidColor();
}
-
+
@Override
public float[] getGradientFractions() {
float[] frc = new float[fractions.size()];
}
return frc;
}
-
+
@Override
public boolean isRotatedWithShape() {
return HSLFFill.this.isRotatedWithShape();
}
-
+
@Override
public GradientType getGradientType() {
return gradientType;
}
};
}
-
+
private TexturePaint getTexturePaint() {
final HSLFPictureData pd = getPictureData();
if (pd == null) {
public boolean isRotatedWithShape() {
return HSLFFill.this.isRotatedWithShape();
}
+
+ @Override
+ public Shape getShape() {
+ return shape;
+ }
};
}
HSLFShape.setEscherProperty(opt, EscherPropertyTypes.FILL__FILLOPACITY, alphaFP);
}
}
-
+
EscherSimpleProperty p = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__NOFILLHITTEST);
int propVal = (p == null) ? 0 : p.getPropertyValue();
propVal = FILL_FILLED.setBoolean(propVal, color != null);
private static final Logger LOG = LogManager.getLogger(BitmapImageRenderer.class);
protected BufferedImage img;
+ private boolean doCache;
+ private byte[] cachedImage;
+ private String cachedContentType;
@Override
public boolean canRender(String contentType) {
@Override
public void loadImage(InputStream data, String contentType) throws IOException {
- img = readImage(data, contentType);
+ InputStream in = data;
+ if (doCache) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ IOUtils.copy(data, bos);
+ cachedImage = bos.toByteArray();
+ cachedContentType = contentType;
+ in = new ByteArrayInputStream(cachedImage);
+ }
+ img = readImage(in, contentType);
}
@Override
public void loadImage(byte[] data, String contentType) throws IOException {
+ if (data == null) {
+ return;
+ }
+ if (doCache) {
+ cachedImage = data.clone();
+ cachedContentType = contentType;
+ }
img = readImage(new ByteArrayInputStream(data), contentType);
}
public Rectangle2D getNativeBounds() {
return new Rectangle2D.Double(0, 0, img.getWidth(), img.getHeight());
}
+
+ @Override
+ public void setCacheInput(boolean enable) {
+ doCache = enable;
+ if (!enable) {
+ cachedContentType = null;
+ cachedImage = null;
+ }
+ }
+
+ @Override
+ public byte[] getCachedImage() {
+ return cachedImage;
+ }
+
+ @Override
+ public String getCachedContentType() {
+ return cachedContentType;
+ }
}
return TRANSPARENT;
}
+ Boolean cacheImage = (Boolean)graphics.getRenderingHint(Drawable.CACHE_IMAGE_SOURCE);
+ renderer.setCacheInput(cacheImage != null && cacheImage);
renderer.loadImage(is, contentType);
int alpha = fill.getAlpha();
Shape s = (Shape)graphics.getRenderingHint(Drawable.GRADIENT_SHAPE);
// TODO: check why original bitmaps scale/behave differently to vector based images
- return new DrawTexturePaint(image, s, fill, flipX, flipY, renderer instanceof BitmapImageRenderer);
+ return new DrawTexturePaint(renderer, image, s, fill, flipX, flipY, renderer instanceof BitmapImageRenderer);
} catch (IOException e) {
LOG.atError().withThrowable(e).log("Can't load image data - using transparent color");
return TRANSPARENT;
import org.apache.poi.sl.usermodel.Insets2D;
import org.apache.poi.sl.usermodel.PaintStyle;
import org.apache.poi.util.Dimension2DDouble;
+import org.apache.poi.util.Internal;
-/* package */ class DrawTexturePaint extends java.awt.TexturePaint {
+@Internal
+public class DrawTexturePaint extends java.awt.TexturePaint {
+ private final ImageRenderer imgRdr;
private final PaintStyle.TexturePaint fill;
private final Shape shape;
private final double flipX, flipY;
private static final Insets2D INSETS_EMPTY = new Insets2D(0,0,0,0);
- DrawTexturePaint(BufferedImage txtr, Shape shape, PaintStyle.TexturePaint fill, double flipX, double flipY, boolean isBitmapSrc) {
+ DrawTexturePaint(ImageRenderer imgRdr, BufferedImage txtr, Shape shape, PaintStyle.TexturePaint fill, double flipX, double flipY, boolean isBitmapSrc) {
// deactivate scaling/translation in super class, by specifying the dimension of the texture
super(txtr, new Rectangle2D.Double(0,0,txtr.getWidth(),txtr.getHeight()));
+ this.imgRdr = imgRdr;
this.fill = fill;
this.shape = shape;
this.flipX = flipX;
return xform;
}
+
+ public ImageRenderer getImageRenderer() {
+ return imgRdr;
+ }
+
+ public PaintStyle.TexturePaint getFill() {
+ return fill;
+ }
+
+ public Shape getAwtShape() {
+ return shape;
+ }
}
case 13: return "BUFFERED_IMAGE";
case 14: return "DEFAULT_CHARSET";
case 15: return "EMF_FORCE_HEADER_BOUNDS";
+ case 16: return "CACHE_IMAGE_SOURCE";
default: return "UNKNOWN_ID "+intKey();
}
}
*/
DrawableHint EMF_FORCE_HEADER_BOUNDS = new DrawableHint(15);
+ /**
+ * A boolean value to instruct the bitmap image renderer to keep the original image bytes.
+ * Defaults to {@code false} if unset.
+ */
+ DrawableHint CACHE_IMAGE_SOURCE = new DrawableHint(16);
+
+
/**
* Apply 2-D transforms before drawing this shape. This includes rotation and flipping.
*
* @param defaultCharset the default charset
*/
default void setDefaultCharset(Charset defaultCharset) {}
+
+
+ /**
+ * Dis-/Enables caching of input data for later retrieval.
+ * Opposed to {@link #getImage()}, which returns a {@link BufferedImage}, the cached image can be later
+ * used to embedded the original, unmodified data
+ * @param enable dis-/enables caching - this is an optional operation. {@code false} removes already cached data
+ */
+ default void setCacheInput(boolean enable) {}
+
+ /**
+ * @return the cached image data
+ */
+ default byte[] getCachedImage() { return null; }
+
+ /**
+ * @return the cached content type
+ */
+ default String getCachedContentType() { return null; }
}
\ No newline at end of file
/**
* The stretch specifies the edges of a fill rectangle.<p>
*
- * Each edge of the fill rectangle is defined by a perentage offset from the corresponding edge
+ * Each edge of the fill rectangle is defined by a percentage offset from the corresponding edge
* of the picture's bounding box. A positive percentage specifies an inset and a negative percentage
* specifies an outset.<p>
*
return null;
}
+ /**
+ * @return the shape this texture paint is applied to
+ */
+ Shape getShape();
}
}