MaskPainter.java
package io.brunoborges.jairosvg.draw;
import java.awt.Graphics2D;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import io.brunoborges.jairosvg.dom.Node;
import io.brunoborges.jairosvg.surface.Surface;
/**
* Mask rendering and luminance-to-alpha compositing.
*/
public final class MaskPainter {
// SVG mask luminance coefficients (BT.709, scaled to ×256 for integer math)
private static final int LUMINANCE_RED_COEFF_256 = 54; // 0.2126 × 256
private static final int LUMINANCE_GREEN_COEFF_256 = 183; // 0.7152 × 256
private static final int LUMINANCE_BLUE_COEFF_256 = 19; // 0.0722 × 256
private MaskPainter() {
}
/** Render and apply luminance mask to an off-screen source image. */
public static BufferedImage paintMask(Surface surface, Node node, String name, BufferedImage sourceImage,
java.awt.geom.AffineTransform subRegionTransform) {
Node maskNode = surface.masks.get(name);
if (maskNode == null) {
return sourceImage;
}
int w = sourceImage.getWidth();
int h = sourceImage.getHeight();
// TYPE_INT_ARGB mask buffer — reuse full-canvas buffer; sub-region buffers are
// temporary and not cached to avoid evicting the reusable full-canvas instance.
boolean isSubRegion = subRegionTransform != null;
BufferedImage maskImage = surface.maskBuffer;
if (!isSubRegion && maskImage != null && maskImage.getWidth() == w && maskImage.getHeight() == h) {
java.util.Arrays.fill(((DataBufferInt) maskImage.getRaster().getDataBuffer()).getData(), 0);
} else {
maskImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
if (!isSubRegion) {
surface.maskBuffer = maskImage;
}
}
Graphics2D maskG2d = maskImage.createGraphics();
maskG2d.setRenderingHints(surface.context.getRenderingHints());
if (subRegionTransform != null) {
maskG2d.setTransform(subRegionTransform);
}
Graphics2D savedContext = surface.context;
GeneralPath savedPath = surface.path;
double savedWidth = surface.contextWidth;
double savedHeight = surface.contextHeight;
surface.context = maskG2d;
surface.path = new GeneralPath();
surface.contextWidth = savedWidth;
surface.contextHeight = savedHeight;
try {
for (Node child : maskNode.children) {
surface.draw(child);
}
} finally {
surface.context = savedContext;
surface.path = savedPath;
surface.contextWidth = savedWidth;
surface.contextHeight = savedHeight;
maskG2d.dispose();
}
// Single combined pass: apply BT.709 luminance of mask pixel to source alpha.
// Operates on the sub-region buffers — typically far smaller than the full
// image.
int[] sourcePixels = ((DataBufferInt) sourceImage.getRaster().getDataBuffer()).getData();
int[] maskPixels = ((DataBufferInt) maskImage.getRaster().getDataBuffer()).getData();
for (int i = 0; i < sourcePixels.length; i++) {
int src = sourcePixels[i];
int srcA = src >>> 24;
if (srcA == 0)
continue;
int m = maskPixels[i];
int ma = m >>> 24;
if (ma == 0) {
sourcePixels[i] = 0;
continue;
}
int mr = (m >> 16) & 0xFF;
int mg = (m >> 8) & 0xFF;
int mb = m & 0xFF;
int luminance256 = LUMINANCE_RED_COEFF_256 * mr + LUMINANCE_GREEN_COEFF_256 * mg
+ LUMINANCE_BLUE_COEFF_256 * mb;
if (luminance256 == 0) {
sourcePixels[i] = 0;
continue;
}
int luminance = (luminance256 + 128) >> 8;
if (luminance == 255 && ma == 255)
continue; // fast path: fully opaque white mask
int maskAlpha = ma * luminance;
maskAlpha = (maskAlpha + 1 + (maskAlpha >> 8)) >> 8;
int combined = srcA * maskAlpha;
int outA = (combined + 1 + (combined >> 8)) >> 8;
sourcePixels[i] = (outA << 24) | (src & 0x00FFFFFF);
}
return sourceImage;
}
}