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