1 /* Copyright (C) 2005-2011 Fabio Riccardi */ 2 3 package com.lightcrafts.jai.opimage; 4 5 import com.lightcrafts.model.Region; 6 import com.lightcrafts.model.Contour; 7 import com.lightcrafts.jai.JAIContext; 8 import com.lightcrafts.jai.LCROIShape; 9 import com.lightcrafts.jai.utils.Functions; 10 import com.lightcrafts.utils.SoftValueHashMap; 11 12 import com.lightcrafts.mediax.jai.*; 13 import java.awt.image.*; 14 import java.awt.image.renderable.ParameterBlock; 15 import java.awt.color.ColorSpace; 16 import java.awt.*; 17 import java.awt.geom.*; 18 import java.util.*; 19 20 /** 21 * Created by IntelliJ IDEA. 22 * User: fabio 23 * Date: May 24, 2005 24 * Time: 4:20:31 PM 25 * To change this template use File | Settings | File Templates. 26 */ 27 public class ShapedMask extends PlanarImage { 28 private Region region; 29 private LCROIShape shape; 30 getOuterBounds(Region region, AffineTransform transform)31 public static Rectangle getOuterBounds(Region region, AffineTransform transform) { 32 Rectangle outerBounds = null; 33 for (Object o : region.getContours()) { 34 Contour c = (Contour) o; 35 36 AffineTransform combined = transform; 37 if (c.getTranslation() != null) { 38 combined = AffineTransform.getTranslateInstance(c.getTranslation().getX(), c.getTranslation().getY()); 39 combined.preConcatenate(transform); 40 } 41 42 Rectangle cBounds = new Rectangle(c.getOuterShape().getBounds()); 43 cBounds = combined.createTransformedShape(cBounds).getBounds(); 44 45 if (outerBounds == null) 46 outerBounds = cBounds; 47 else 48 outerBounds = outerBounds.union(cBounds); 49 } 50 51 return outerBounds; 52 } 53 createLayout(Region region, AffineTransform transform)54 private static ImageLayout createLayout(Region region, AffineTransform transform) { 55 Rectangle regionBounds = getOuterBounds(region, transform); 56 57 SampleModel graySm = RasterFactory.createPixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, regionBounds.width, regionBounds.height, 1); 58 ColorModel grayCm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); 59 60 return new ImageLayout(regionBounds.x, regionBounds.y, regionBounds.width, regionBounds.height, 61 regionBounds.x, regionBounds.y, JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT, 62 graySm, grayCm); 63 } 64 createLayout(Rectangle regionBounds)65 static ImageLayout createLayout(Rectangle regionBounds) { 66 SampleModel graySm = RasterFactory.createPixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, regionBounds.width, regionBounds.height, 1); 67 ColorModel grayCm = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); 68 69 return new ImageLayout(regionBounds.x, regionBounds.y, regionBounds.width, regionBounds.height, 70 regionBounds.x, regionBounds.y, JAIContext.TILE_WIDTH, JAIContext.TILE_HEIGHT, 71 graySm, grayCm); 72 } 73 ShapedMask(Region region, LCROIShape shape)74 public ShapedMask(Region region, LCROIShape shape) { 75 super(createLayout(region, shape.getTransform()), null, null); 76 77 this.region = region; 78 this.shape = shape; 79 } 80 createBlurs(Shape shape, int width)81 static private Shape[] createBlurs(Shape shape, int width) { 82 java.util.List<Shape> blurs = new LinkedList<Shape>(); 83 do { 84 Stroke stroke = new BasicStroke(2 * width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); 85 Shape blur = stroke.createStrokedShape(shape); 86 blurs.add(blur); 87 width /= 2; 88 } while (width > 0); 89 90 int size = blurs.size(); 91 Shape[] result = new Shape[size]; 92 Iterator it = blurs.iterator(); 93 int i = 0; 94 while (it.hasNext()) 95 result[size - 1 - i++] = (Shape) it.next(); 96 return result; 97 } 98 99 private static Map<Contour, ScaledImage> bitmaps = 100 Collections.synchronizedMap(new WeakHashMap<Contour, ScaledImage>()); 101 102 private static BorderExtender extender = BorderExtender.createInstance(BorderExtender.BORDER_COPY); 103 104 private static class ScaledImage { 105 float scale = 1; 106 float tx = 0, ty = 0; 107 PlanarImage image = null; 108 } 109 110 drawBlurs(Graphics2D g2d, Shape shape, float contourWidth)111 private static void drawBlurs(Graphics2D g2d, Shape shape, float contourWidth) { 112 g2d.setColor(Color.white); 113 g2d.fill(shape); 114 115 if (contourWidth <= 1) 116 return; 117 118 // Draw the blurs in shades of gray: 119 int width = Math.round(contourWidth); 120 Shape[] blurs = createBlurs(shape, width); 121 int count = blurs.length; 122 Area shapeArea = new Area(shape); 123 for (int n = count - 1; n >= 0; n--) { 124 Shape blur = blurs[n]; 125 Area semiBlur = new Area(blur); 126 semiBlur.intersect(shapeArea); 127 float value = n / (float) (count + 1); 128 Color color = new Color(value, value, value); 129 g2d.setColor(color); 130 g2d.fill(semiBlur); 131 } 132 } 133 createContourImage(Contour contour)134 private static ScaledImage createContourImage(Contour contour) { 135 Shape shape = contour.getOuterShape(); 136 137 // limit the blur radius to 7 pixels, use intelligent rescaling to emulate larger blurs 138 float contourWidth = contour.getWidth(); 139 float widthScale = 1; 140 int divideByTwo = 1; 141 while (contourWidth > 7 * 4) { 142 contourWidth /= 2; 143 widthScale /= 2; 144 divideByTwo *= 2; 145 } 146 147 if (widthScale < 1) 148 shape = AffineTransform.getScaleInstance(widthScale, widthScale).createTransformedShape(shape); 149 150 // We create the supporting BufferedImage contourWidth larger than 151 // the outer shape bounds to allow for proper tapering of the blur 152 153 Rectangle bounds = shape.getBounds(); 154 bounds.grow((int) contourWidth, (int) contourWidth); 155 156 RenderedImage supportImage = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_BYTE_GRAY); 157 158 Graphics2D g2d = ((BufferedImage) supportImage).createGraphics(); 159 160 g2d.setTransform(AffineTransform.getTranslateInstance(-bounds.x, -bounds.y)); 161 162 drawBlurs(g2d, shape, contourWidth); 163 164 g2d.dispose(); 165 166 supportImage = new TiledImage(supportImage, 167 Math.max(JAIContext.TILE_WIDTH / divideByTwo, 8), 168 Math.max(JAIContext.TILE_HEIGHT / divideByTwo, 8)); 169 170 ScaledImage contourImage = new ScaledImage(); 171 172 if (contourWidth > 1) { 173 KernelJAI kernel = Functions.getGaussKernel(contourWidth/4); 174 ParameterBlock pb = new ParameterBlock(); 175 pb.addSource(supportImage); 176 pb.add(kernel); 177 RenderingHints hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER, extender); 178 179 contourImage.image = JAI.create("LCSeparableConvolve", pb, hints); 180 } else { 181 contourImage.image = (PlanarImage) supportImage; 182 } 183 184 contourImage.scale = widthScale; 185 contourImage.tx = bounds.x; 186 contourImage.ty = bounds.y; 187 188 return contourImage; 189 } 190 getContourImage(Contour contour)191 private static synchronized ScaledImage getContourImage(Contour contour) { 192 ScaledImage contourImage = bitmaps.get(contour); 193 194 if (contourImage == null) { 195 if ((contourImage = bitmaps.get(contour)) == null) { 196 contourImage = createContourImage(contour); 197 bitmaps.put(contour, contourImage); 198 } 199 } 200 201 return contourImage; 202 } 203 204 static class RasterImage extends PlanarImage { 205 private Raster raster; 206 RasterImage(Raster r, ColorModel colorModel)207 RasterImage(Raster r, ColorModel colorModel) { 208 super(new ImageLayout(r.getMinX(), 209 r.getMinY(), 210 r.getWidth(), 211 r.getHeight(), 212 r.getMinX(), 213 r.getMinY(), 214 r.getWidth(), 215 r.getHeight(), 216 r.getSampleModel(), 217 colorModel), null, null); 218 raster = r; 219 } 220 getTile(int tileX, int tileY)221 public Raster getTile(int tileX, int tileY) { 222 return raster; 223 } 224 } 225 226 private final static Map<AffinedImage, PlanarImage> expandedMasks = 227 new SoftValueHashMap<AffinedImage, PlanarImage>(); 228 229 private static class AffinedImage { 230 PlanarImage image; 231 AffineTransform transform; 232 AffinedImage(PlanarImage image, AffineTransform transform)233 AffinedImage(PlanarImage image, AffineTransform transform) { 234 this.image = image; 235 this.transform = transform; 236 } 237 equals(Object o)238 public boolean equals(Object o) { 239 if (this == o) return true; 240 if (!(o instanceof AffinedImage)) 241 return false; 242 243 final AffinedImage affinedImage = (AffinedImage) o; 244 245 if (image != null ? !image.equals(affinedImage.image) : affinedImage.image != null) 246 return false; 247 if (transform != null ? !transform.equals(affinedImage.transform) : affinedImage.transform != null) 248 return false; 249 250 return true; 251 } 252 hashCode()253 public int hashCode() { 254 int result; 255 result = (image != null ? image.hashCode() : 0); 256 result = 29 * result + (transform != null ? transform.hashCode() : 0); 257 return result; 258 } 259 } 260 getData(Rectangle rect)261 public Raster getData(Rectangle rect) { 262 ColorModel colorModel = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), 263 false, false, 264 Transparency.OPAQUE, DataBuffer.TYPE_BYTE); 265 SampleModel sampleModel = colorModel.createCompatibleSampleModel(rect.width, rect.height); 266 267 TiledImage ti = new TiledImage(rect.x, rect.y, rect.width, rect.height, 268 rect.x, rect.y, sampleModel, colorModel); 269 270 WritableRaster result = (WritableRaster) ti.getData(); 271 272 boolean overlay = false; 273 274 for (final Contour c : region.getContours()) { 275 AffineTransform combined = shape.getTransform(); 276 if (c.getTranslation() != null) { 277 combined = AffineTransform.getTranslateInstance(c.getTranslation().getX(), 278 c.getTranslation().getY()); 279 combined.preConcatenate(shape.getTransform()); 280 } 281 282 // Take the blur tapering into account 283 Rectangle bounds = c.getOuterShape().getBounds(); 284 bounds.grow((int) c.getWidth(), (int) c.getWidth()); 285 286 if (!combined.createTransformedShape(bounds).intersects(rect)) 287 continue; 288 289 ScaledImage scaledImage = getContourImage(c); 290 PlanarImage maskImage = scaledImage.image; 291 292 if (!combined.isIdentity() || scaledImage.scale < 1 || scaledImage.tx != 0 || scaledImage.ty != 0) { 293 AffineTransform transform = new AffineTransform(combined); 294 295 if (scaledImage.scale < 1) { 296 float scaleX = (float) Math.floor(maskImage.getWidth() / scaledImage.scale) / (float) maskImage.getWidth(); 297 float scaleY = (float) Math.floor(maskImage.getHeight() / scaledImage.scale) / (float) maskImage.getHeight(); 298 299 transform.concatenate(AffineTransform.getScaleInstance(scaleX, scaleY)); 300 } 301 302 if (scaledImage.tx != 0 || scaledImage.ty != 0) 303 transform.concatenate(AffineTransform.getTranslateInstance(scaledImage.tx, scaledImage.ty)); 304 305 Rectangle scaledBounds = transform.createTransformedShape(maskImage.getBounds()).getBounds(); 306 307 // Avoid scaling underflows resulting into exeptions 308 if (scaledBounds.width < 3 || scaledBounds.height < 3) 309 continue; 310 311 synchronized (expandedMasks) { 312 AffinedImage key = new AffinedImage(maskImage, transform); 313 PlanarImage affinedImage = expandedMasks.get(key); 314 if (affinedImage == null) { 315 RenderingHints hints = new RenderingHints(JAI.KEY_BORDER_EXTENDER, 316 BorderExtender.createInstance(BorderExtender.BORDER_COPY)); 317 // hints.add(JAIContext.noCacheHint); 318 Interpolation interp = Interpolation.getInstance(Interpolation.INTERP_BILINEAR); 319 ParameterBlock params = new ParameterBlock(); 320 params.addSource(maskImage); 321 params.add(transform); 322 params.add(interp); 323 maskImage = JAI.create("Affine", params, hints); 324 expandedMasks.put(key, maskImage); 325 } else { 326 maskImage = affinedImage; 327 } 328 } 329 } 330 331 if (!maskImage.getBounds().intersects(rect)) 332 continue; 333 334 Rectangle itx = maskImage.getBounds().intersection(rect); 335 336 byte resultData[]; 337 if (!overlay) { 338 resultData = (byte[]) maskImage.getData(itx).getDataElements(itx.x, itx.y, itx.width, itx.height, null); 339 overlay = true; 340 } else { 341 resultData = (byte[]) result.getDataElements(itx.x, itx.y, itx.width, itx.height, null); 342 byte currentData[] = (byte[]) maskImage.getData(itx).getDataElements(itx.x, itx.y, itx.width, itx.height, null); 343 344 // blend overlapping regions using Porter-Duff alpha compositing: ar = a1 * (1 - a2) + a2 345 for (int i = 0; i < resultData.length; i++) { 346 int current = currentData[i] & 0xFF; 347 if (current != 0) { 348 int cumulative = resultData[i] & 0xFF; 349 if (cumulative != 0) { 350 resultData[i] = (byte) ((cumulative * (0xff - current)) / 0x100 + current); 351 } else 352 resultData[i] = (byte) current; 353 } 354 } 355 } 356 357 result.setDataElements(itx.x, itx.y, itx.width, itx.height, resultData); 358 } 359 360 return result; 361 } 362 getTile(int tileX, int tileY)363 public Raster getTile(int tileX, int tileY) { 364 return getData(new Rectangle(tileXToX(tileX), tileYToY(tileY), getTileWidth(), getTileHeight())); 365 } 366 } 367