]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/utils/MipMapBufferedImage.java
Render elements using custom color filters
[simantics/platform.git] / bundles / org.simantics.scenegraph / src / org / simantics / scenegraph / utils / MipMapBufferedImage.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.scenegraph.utils;
13
14 import java.awt.Color;
15 import java.awt.Graphics2D;
16 import java.awt.Point;
17 import java.awt.RenderingHints;
18 import java.awt.geom.AffineTransform;
19 import java.awt.geom.Rectangle2D;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Objects;
26
27 import org.simantics.scenegraph.g2d.G2DRenderingHints;
28 import org.simantics.scenegraph.g2d.color.ColorFilter;
29 import org.simantics.scenegraph.g2d.color.Graphics2DWithColorFilter;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 import com.kitfox.svg.SVGDiagram;
34
35 /**
36  * Video-ram cache suitable for rasterized PaintableSymbols scalable vector graphics.
37  * <p>
38  * This implementation rasterizes the same symbol from different mip map levels.
39  * 
40  * @see VRamImage
41  * @author Toni Kalajainen
42  */
43 public class MipMapBufferedImage extends BufferedImage {
44
45     private static final Logger LOGGER = LoggerFactory.getLogger(MipMapBufferedImage.class);
46
47     /** Extra margin to the bounds reported by batik */
48     public static final double MARGIN_PERCENT = 3;
49
50     // Was 800 in VRam.. ?
51     public static final double MAX_DIMENSION  = 600;
52     public static final double MIN_DIMENSION  = 4;
53
54     //Shape outline;
55
56     Map<Double, IRaster>       rasters        = new HashMap<Double, IRaster>();
57     double[]                   resolutions;
58     double                     minResolution, maxResolution;
59
60     /**
61      * @param original
62      * @param imageBounds
63      * @param referenceSize
64      * 
65      * FIXME: shouldn't be SVG dependent
66      */
67     public MipMapBufferedImage(SVGDiagram original, Rectangle2D imageBounds, Point referenceSize) {
68         super(original, imageBounds, referenceSize);
69         initializeRasters();
70     }
71
72     private void initializeRasters() {
73         if (LOGGER.isDebugEnabled())
74             LOGGER.debug("initializeRasters({}): diagram={}, referenceSize={}, imageBounds={}, maxres={}, minres={}", this.toString(), source, referenceSize, imageBounds, maxResolution(), minResolution());
75         List<Double> resolutions = new ArrayList<Double>();
76
77         if (referenceSize != null && !imageBounds.isEmpty()) {
78             // Init rasters - they are built on-demand
79             double maxResolution = maxResolution();
80             double minResolution = minResolution();
81             double fitResolution = fitResolution(referenceSize);
82             double resolution = fitResolution;
83             while (true) {
84                 double next = resolution * 2;
85                 if (next > maxResolution)
86                     break;
87                 resolution = next;
88             }
89             while (resolution > minResolution) {
90                 IRaster r = createRaster(resolution);
91                 rasters.put(resolution, r);
92                 resolutions.add(resolution);
93                 resolution /= 2;
94             }
95         } else {
96             // Init rasters - they are built on-demand
97             double maxResolution = maxResolution();
98             double minResolution = minResolution();
99             double resolution = maxResolution;
100             while (resolution > minResolution) {
101                 IRaster r = createRaster(resolution);
102                 rasters.put(resolution, r);
103                 resolutions.add(resolution);
104                 resolution /= 2;
105             }
106         }
107
108         if (LOGGER.isDebugEnabled())
109             LOGGER.debug("initializeRasters({}): resolutions={}", this.toString(), resolutions);
110
111         // arraylist -> array
112         this.resolutions = new double[resolutions.size()];
113         for (int i=0; i<resolutions.size(); i++)
114             this.resolutions[i] = resolutions.get(resolutions.size()-1-i);
115         this.minResolution = this.resolutions[0];
116         this.maxResolution = this.resolutions[this.resolutions.length-1];
117         //System.out.println("RESOLUTIONS: " + Arrays.toString(this.resolutions));
118     }
119
120     protected IRaster createRaster(double resolution) {
121         return new BufferedRaster(resolution);
122     }
123
124     private double fitResolution(Point p)
125     {
126         double wid = imageBounds.getWidth();
127         double hei = imageBounds.getHeight();
128         double rx = p.x / wid;
129         double ry = p.y / hei;
130         return Math.min(rx, ry);
131     }
132
133     private double maxResolution()
134     {
135         double wid = imageBounds.getWidth();
136         double hei = imageBounds.getHeight();
137         return MAX_DIMENSION/Math.sqrt(wid*hei);
138     }
139
140     private double minResolution()
141     {
142         double wid = imageBounds.getWidth();
143         double hei = imageBounds.getHeight();
144         return MIN_DIMENSION/Math.sqrt(wid*hei);
145     }
146
147     protected double requiredResolution(AffineTransform at)
148     {
149         double m00 = at.getScaleX();
150         double m11 = at.getScaleY();
151         double m10 = at.getShearY();
152         double m01 = at.getShearX();
153         // Project unit vector to canvas
154         double sx = Math.sqrt( m00*m00 + m10*m10 );
155         double sy = Math.sqrt( m01*m01 + m11*m11 );
156         return Math.max(sx, sy);
157         //return Math.sqrt(sx*sx+sy*sy);
158     }
159
160     protected double findClosestResolution(double resolution)
161     {
162         int index = Arrays.binarySearch(resolutions, resolution);
163         if (index>=0) return resolutions[index];
164         index  = -(index+1);
165
166         if (index>=resolutions.length) index = resolutions.length-1;
167         if (index<0) index = 0;
168         return resolutions[index];
169     }
170
171     @Override
172     public void paint(Graphics2D g) {
173         ColorFilter colorFilter = (ColorFilter) g.getRenderingHint(G2DRenderingHints.KEY_COLOR_FILTER);
174
175         // Quality rendering requested, do not render from cache
176         //QualityHints.HIGH_QUALITY_HINTS.setQuality(g);
177         if (g.getRenderingHint(RenderingHints.KEY_RENDERING) == RenderingHints.VALUE_RENDER_QUALITY)
178         {
179             try {
180                 if (colorFilter != null) {
181                     source.render(new Graphics2DWithColorFilter(g, colorFilter));
182                 } else {
183                     source.render(g);
184                 }
185             } catch (Exception e) {
186                 // NOTE: Catching Exception instead of SVGException due to an
187                 // NPE when encountering invalid color SVG definitions (e.g.
188                 // rgb(256,-1,0))
189                 e.printStackTrace();
190             }
191             return;
192         }
193
194         double requiredResolution = requiredResolution(g.getTransform());
195         // This scale makes the mipmapped painting use a mipmap that is smaller
196         // than the requested image pixel size in cases where the required
197         // resolution only slightly exceeds the size of an available mipmap.
198         requiredResolution *= 0.95;
199         //System.out.println("required resolution: " + requiredResolution);
200
201         if (requiredResolution > getRasterRenderingThresholdResolution()) {
202             Graphics2D g2d = (Graphics2D) g.create();
203             setupSourceRender(g2d);
204             try {
205                 if (colorFilter != null) {
206                     source.render(new Graphics2DWithColorFilter(g, colorFilter));
207                 } else {
208                     source.render(g);
209                 }
210             } catch (Exception e) {
211                 // NOTE: Catching Exception instead of SVGException due to an
212                 // NPE when encountering invalid color SVG definitions (e.g.
213                 // rgb(256,-1,0))
214                 e.printStackTrace();
215             } finally {
216                 g2d.dispose();
217             }
218         } else {
219             Object origInterpolationHint = g.getRenderingHint(RenderingHints.KEY_INTERPOLATION);
220             if (origInterpolationHint==null)
221                 origInterpolationHint = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
222
223             g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
224
225             double closestResolution = findClosestResolution(requiredResolution);
226             //System.out.println("  resolutions: " + Arrays.toString(resolutions));
227             //System.out.println("  closest resolution: " + closestResolution);
228             IRaster raster = rasters.get(closestResolution);
229             try {
230                 raster.paint( g );
231             } finally {
232                 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, origInterpolationHint);
233             }
234         }
235     }
236
237     protected void setupSourceRender(Graphics2D g2d) {
238         g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
239         g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
240         g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
241         g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
242         g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
243         g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
244     }
245
246     protected double getRasterRenderingThresholdResolution() {
247         return maxResolution;
248     }
249
250     @Override
251     public synchronized void releaseRaster() {
252         for (IRaster r : rasters.values())
253             r.release();
254     }
255
256     static interface IRaster extends Comparable<IRaster> {
257         double getResolution();
258         void paint(Graphics2D g);
259         void release();
260     }
261
262     static abstract class Raster implements IRaster {
263         protected final double resolution;
264
265         public Raster(double resolution) {
266             this.resolution = resolution;
267         }
268
269         public double getResolution() {
270             return resolution;
271         }
272
273         public int compareTo(IRaster o) {
274             double r = getResolution();
275             double or = o.getResolution();
276             if (or < r)
277                 return -1;
278             if (or > r)
279                 return 1;
280             return 0;
281         }
282     }
283
284     class BufferedRaster extends Raster {
285         java.awt.image.BufferedImage image;
286         //int widMargin, heiMargin;
287         int wid, hei;
288         private ColorFilter previousColorFilter = null;
289
290         BufferedRaster(double resolution) {
291             super(resolution);
292             double wid = imageBounds.getWidth();
293             double hei = imageBounds.getHeight();
294             this.wid = (int) (wid * resolution);
295             this.hei = (int) (hei * resolution);
296 //            widMargin = (int) (wid * resolution * (MARGIN_PERCENT/100)) +1;
297 //            heiMargin = (int) (hei * resolution * (MARGIN_PERCENT/100)) +1;
298         }
299
300         synchronized java.awt.image.BufferedImage getOrCreate(ColorFilter colorFilter)
301         {
302             if (!Objects.equals(colorFilter, previousColorFilter)) {
303                 previousColorFilter = colorFilter;
304                 image = null;
305             }
306             if (image!=null) return image;
307             image = new java.awt.image.BufferedImage(
308                     (wid+0*2+1),
309                     (hei+0*2+1),
310                     java.awt.image.BufferedImage.TYPE_INT_ARGB);
311
312             Graphics2D target = image.createGraphics();
313             target.setBackground(new Color(255,255,255,0));
314             target.clearRect(0, 0, image.getWidth(), image.getHeight());
315
316             target.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
317             target.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
318             target.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
319             target.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
320
321 //            target.translate(widMargin, heiMargin);
322             target.scale(resolution, resolution);
323             target.translate(-imageBounds.getMinX(), -imageBounds.getMinY());
324             try {
325                 if (colorFilter != null) {
326                     source.render(new Graphics2DWithColorFilter(target, colorFilter));
327                 } else {
328                     source.render(target);
329                 }
330             } catch (Exception e) {
331                 // TODO Auto-generated catch block
332                 // NOTE: Catching Exception instead of SVGException due to an
333                 // NPE when encountering invalid color SVG definitions (e.g.
334                 // rgb(256,-1,0))
335                 e.printStackTrace();
336             }
337 //            source.paint(
338 //                    new GraphicsContextImpl(target, new Rectangle2D.Double(0,0, image.getWidth(), image.getHeight()), null)
339 //            );
340             target.dispose();
341
342             return image;
343         }
344
345         public void paint(Graphics2D g) {
346             ColorFilter colorFilter = (ColorFilter) g.getRenderingHint(G2DRenderingHints.KEY_COLOR_FILTER); 
347             java.awt.image.BufferedImage image = getOrCreate(colorFilter);
348             if (image==null)
349             {
350                 try {
351                     source.render(new Graphics2DWithColorFilter(g, colorFilter));
352                 } catch (Exception e) {
353                     // TODO Auto-generated catch block
354                     // NOTE: Catching Exception instead of SVGException due to an
355                     // NPE when encountering invalid color SVG definitions (e.g.
356                     // rgb(256,-1,0))
357                     e.printStackTrace();
358                 }
359                 return;
360             }
361             AffineTransform af = g.getTransform();
362             Object rh = g.getRenderingHint(RenderingHints.KEY_INTERPOLATION);
363             try {
364                 /// Bicubic interpolation is very slow with opengl pipeline
365                 if (rh == RenderingHints.VALUE_INTERPOLATION_BICUBIC)
366                     g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
367                             RenderingHints.VALUE_INTERPOLATION_BILINEAR);
368                 g.translate(imageBounds.getMinX(), imageBounds.getMinY());
369                 g.scale(1/resolution, 1/resolution);
370 //                g.translate(-widMargin, -heiMargin);
371                 g.drawImage(image, 0, 0, null);
372             } finally {
373                 g.setTransform(af);
374                 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, rh);
375             }
376         }
377
378         public void release() {
379             image = null;
380         }
381     }
382
383 }