< prev index next >

openjfx9/modules/javafx.graphics/src/main/java/com/sun/marlin/Renderer.java

Print this page

        

@@ -21,50 +21,38 @@
  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 
-package sun.java2d.marlin;
+package com.sun.marlin;
 
-import java.util.Arrays;
-import sun.awt.geom.PathConsumer2D;
-import static sun.java2d.marlin.OffHeapArray.SIZE_INT;
+import static com.sun.marlin.OffHeapArray.SIZE_INT;
 import jdk.internal.misc.Unsafe;
 
-final class Renderer implements PathConsumer2D, MarlinConst {
+public final class Renderer implements MarlinRenderer, MarlinConst {
 
     static final boolean DISABLE_RENDER = false;
 
-    static final boolean ENABLE_BLOCK_FLAGS = MarlinProperties.isUseTileFlags();
-    static final boolean ENABLE_BLOCK_FLAGS_HEURISTICS = MarlinProperties.isUseTileFlagsWithHeuristics();
-
     private static final int ALL_BUT_LSB = 0xfffffffe;
     private static final int ERR_STEP_MAX = 0x7fffffff; // = 2^31 - 1
 
     private static final double POWER_2_TO_32 = 0x1.0p32;
 
     // use float to make tosubpix methods faster (no int to float conversion)
-    public static final float F_SUBPIXEL_POSITIONS_X
+    static final float F_SUBPIXEL_POSITIONS_X
         = (float) SUBPIXEL_POSITIONS_X;
-    public static final float F_SUBPIXEL_POSITIONS_Y
+    static final float F_SUBPIXEL_POSITIONS_Y
         = (float) SUBPIXEL_POSITIONS_Y;
-    public static final int SUBPIXEL_MASK_X = SUBPIXEL_POSITIONS_X - 1;
-    public static final int SUBPIXEL_MASK_Y = SUBPIXEL_POSITIONS_Y - 1;
-
-    // number of subpixels corresponding to a tile line
-    private static final int SUBPIXEL_TILE
-        = TILE_SIZE << SUBPIXEL_LG_POSITIONS_Y;
+    static final int SUBPIXEL_MASK_X = SUBPIXEL_POSITIONS_X - 1;
+    static final int SUBPIXEL_MASK_Y = SUBPIXEL_POSITIONS_Y - 1;
 
     // 2048 (pixelSize) pixels (height) x 8 subpixels = 64K
     static final int INITIAL_BUCKET_ARRAY
         = INITIAL_PIXEL_DIM * SUBPIXEL_POSITIONS_Y;
 
-    // crossing capacity = edges count / 8 ~ 512
-    static final int INITIAL_CROSSING_COUNT = INITIAL_EDGES_COUNT >> 3;
-
-    public static final int WIND_EVEN_ODD = 0;
-    public static final int WIND_NON_ZERO = 1;
+    // crossing capacity = edges count / 4 ~ 1024
+    static final int INITIAL_CROSSING_COUNT = INITIAL_EDGES_COUNT >> 2;
 
     // common to all types of input path segments.
     // OFFSET as bytes
     // only integer values:
     public static final long OFF_CURX_OR  = 0;

@@ -78,24 +66,22 @@
     public static final int SIZEOF_EDGE_BYTES = (int)(OFF_YMAX + SIZE_INT);
 
     // curve break into lines
     // cubic error in subpixels to decrement step
     private static final float CUB_DEC_ERR_SUBPIX
-        = 2.5f * (NORM_SUBPIXELS / 8f); // 2.5 subpixel for typical 8x8 subpixels
+        = 1f * (NORM_SUBPIXELS / 8f); // 1 subpixel for typical 8x8 subpixels
     // cubic error in subpixels to increment step
     private static final float CUB_INC_ERR_SUBPIX
-        = 1f * (NORM_SUBPIXELS / 8f); // 1 subpixel for typical 8x8 subpixels
+        = 0.4f * (NORM_SUBPIXELS / 8f); // 0.4 subpixel for typical 8x8 subpixels
 
     // cubic bind length to decrement step = 8 * error in subpixels
-    // pisces: 20 / 8
-    // openjfx pisces: 8 / 3.2
     // multiply by 8 = error scale factor:
     public static final float CUB_DEC_BND
-        = 8f * CUB_DEC_ERR_SUBPIX; // 20f means 2.5 subpixel error
+        = 8f * CUB_DEC_ERR_SUBPIX;
     // cubic bind length to increment step = 8 * error in subpixels
     public static final float CUB_INC_BND
-        = 8f * CUB_INC_ERR_SUBPIX; // 8f means 1 subpixel error
+        = 8f * CUB_INC_ERR_SUBPIX;
 
     // cubic countlg
     public static final int CUB_COUNT_LG = 2;
     // cubic count = 2^countlg
     private static final int CUB_COUNT = 1 << CUB_COUNT_LG;

@@ -114,13 +100,12 @@
     // quadratic error in subpixels
     private static final float QUAD_DEC_ERR_SUBPIX
         = 1f * (NORM_SUBPIXELS / 8f); // 1 subpixel for typical 8x8 subpixels
 
     // quadratic bind length to decrement step = 8 * error in subpixels
-    // pisces and openjfx pisces: 32
     public static final float QUAD_DEC_BND
-        = 8f * QUAD_DEC_ERR_SUBPIX; // 8f means 1 subpixel error
+        = 8f * QUAD_DEC_ERR_SUBPIX;
 
 //////////////////////////////////////////////////////////////////////////////
 //  SCAN LINE
 //////////////////////////////////////////////////////////////////////////////
     // crossings ie subpixel edge x coordinates

@@ -163,18 +148,18 @@
     private int[] edgeBuckets;
     private int[] edgeBucketCounts; // 2*newedges + (1 if pruning needed)
     // used range for edgeBuckets / edgeBucketCounts
     private int buckets_minY;
     private int buckets_maxY;
-    // sum of each edge delta Y (subpixels)
-    private int edgeSumDeltaY;
 
     // edgeBuckets ref (clean)
     private final IntArrayCache.Reference edgeBuckets_ref;
     // edgeBucketCounts ref (clean)
     private final IntArrayCache.Reference edgeBucketCounts_ref;
 
+    boolean useRLE = false;
+
     // Flattens using adaptive forward differencing. This only carries out
     // one iteration of the AFD loop. All it does is update AFD variables (i.e.
     // X0, Y0, D*[X|Y], COUNT; not variables used for computing scanline crossings).
     private void quadBreakIntoLinesAndAdd(float x0, float y0,
                                           final Curve c,

@@ -482,13 +467,10 @@
         _edgeBuckets[bucketIdx]       = edgePtr;
         _edgeBucketCounts[bucketIdx] += 2; // 1 << 1
         // last bit means edge end
         _edgeBucketCounts[lastCrossing - _boundsMinY] |= 0x1;
 
-        // update sum of delta Y (subpixels):
-        edgeSumDeltaY += (lastCrossing - firstCrossing);
-
         // update free pointer (ie length in bytes)
         _edges.used += _SIZEOF_EDGE_BYTES;
 
         if (DO_MONITORS) {
             rdrCtx.stats.mon_rdr_addLine.stop();

@@ -496,13 +478,10 @@
     }
 
 // END EDGE LIST
 //////////////////////////////////////////////////////////////////////////////
 
-    // Cache to store RLE-encoded coverage mask of the current primitive
-    final MarlinCache cache;
-
     // Bounds of the drawing region, at subpixel precision.
     private int boundsMinX, boundsMinY, boundsMaxX, boundsMaxY;
 
     // Current winding rule
     private int windingRule;

@@ -548,12 +527,10 @@
 
         // 2048 (pixelsize) pixel large
         alphaLine_ref = rdrCtx.newCleanIntArrayRef(INITIAL_AA_ARRAY); // 8K
         alphaLine     = alphaLine_ref.initial;
 
-        this.cache = rdrCtx.cache;
-
         crossings_ref     = rdrCtx.newDirtyIntArrayRef(INITIAL_CROSSING_COUNT); // 2K
         aux_crossings_ref = rdrCtx.newDirtyIntArrayRef(INITIAL_CROSSING_COUNT); // 2K
         edgePtrs_ref      = rdrCtx.newDirtyIntArrayRef(INITIAL_CROSSING_COUNT); // 2K
         aux_edgePtrs_ref  = rdrCtx.newDirtyIntArrayRef(INITIAL_CROSSING_COUNT); // 2K
 

@@ -564,14 +541,14 @@
 
         blkFlags_ref = rdrCtx.newCleanIntArrayRef(INITIAL_ARRAY); // 1K = 1 tile line
         blkFlags     = blkFlags_ref.initial;
     }
 
-    Renderer init(final int pix_boundsX, final int pix_boundsY,
+    public Renderer init(final int pix_boundsX, final int pix_boundsY,
                   final int pix_boundsWidth, final int pix_boundsHeight,
-                  final int windingRule) {
-
+                  final int windingRule)
+    {
         this.windingRule = windingRule;
 
         // bounds as half-open intervals: minX <= x < maxX and minY <= y < maxY
         this.boundsMinX =  pix_boundsX << SUBPIXEL_LG_POSITIONS_X;
         this.boundsMaxX =

@@ -609,19 +586,21 @@
         // reset used mark:
         edgeCount = 0;
         activeEdgeMaxUsed = 0;
         edges.used = 0;
 
-        edgeSumDeltaY = 0;
+        // reset bbox:
+        bboxX0 = 0;
+        bboxX1 = 0;
 
         return this; // fluent API
     }
 
     /**
      * Disposes this renderer and recycle it clean up before reusing this instance
      */
-    void dispose() {
+    public void dispose() {
         if (DO_STATS) {
             rdrCtx.stats.stat_rdr_activeEdges.add(activeEdgeMaxUsed);
             rdrCtx.stats.stat_rdr_edges.add(edges.used);
             rdrCtx.stats.stat_rdr_edges_count.add(edges.used / SIZEOF_EDGE_BYTES);
             rdrCtx.stats.hist_rdr_edges_count.add(edges.used / SIZEOF_EDGE_BYTES);

@@ -732,18 +711,18 @@
     }
 
     @Override
     public void pathDone() {
         closePath();
-    }
 
-    @Override
-    public long getNativeConsumer() {
-        throw new InternalError("Renderer does not use a native consumer.");
+        // call endRendering() to determine the boundaries:
+        endRendering();
     }
 
-    private void _endRendering(final int ymin, final int ymax) {
+    private void _endRendering(final int ymin, final int ymax,
+                               final MarlinAlphaConsumer ac)
+    {
         if (DISABLE_RENDER) {
             return;
         }
 
         // Get X bounds as true pixel boundaries to compute correct pixel coverage:

@@ -754,11 +733,10 @@
 
         // Useful when processing tile line by tile line
         final int[] _alpha = alphaLine;
 
         // local vars (performance):
-        final MarlinCache _cache = cache;
         final OffHeapArray _edges = edges;
         final int[] _edgeBuckets = edgeBuckets;
         final int[] _edgeBucketCounts = edgeBucketCounts;
 
         int[] _crossings = this.crossings;

@@ -1174,11 +1152,18 @@
 
                         if ((sum & 0x1) != 0) {
                             // TODO: perform line clipping on left-right sides
                             // to avoid such bound checks:
                             x0 = (prev > bboxx0) ? prev : bboxx0;
-                            x1 = (curx < bboxx1) ? curx : bboxx1;
+
+                            if (curx < bboxx1) {
+                                x1 = curx;
+                            } else {
+                                x1 = bboxx1;
+                                // skip right side (fast exit loop):
+                                i = numCrossings;
+                            }
 
                             if (x0 < x1) {
                                 x0 -= bboxx0; // turn x0, x1 from coords to indices
                                 x1 -= bboxx0; // in the alpha array.
 

@@ -1191,11 +1176,12 @@
                                     _alpha[pix_x    ] += tmp;
                                     _alpha[pix_x + 1] -= tmp;
 
                                     if (useBlkFlags) {
                                         // flag used blocks:
-                                        _blkFlags[pix_x >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[pix_x       >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[(pix_x + 1) >> _BLK_SIZE_LG] = 1;
                                     }
                                 } else {
                                     tmp = (x0 & _SUBPIXEL_MASK_X);
                                     _alpha[pix_x    ]
                                         += (_SUBPIXEL_POSITIONS_X - tmp);

@@ -1210,12 +1196,14 @@
                                     _alpha[pix_xmax + 1]
                                         -= tmp;
 
                                     if (useBlkFlags) {
                                         // flag used blocks:
-                                        _blkFlags[pix_x    >> _BLK_SIZE_LG] = 1;
-                                        _blkFlags[pix_xmax >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[ pix_x         >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[(pix_x + 1)    >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[pix_xmax       >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[(pix_xmax + 1) >> _BLK_SIZE_LG] = 1;
                                     }
                                 }
                             }
                         }
 

@@ -1235,11 +1223,18 @@
                             }
                         } else {
                             // TODO: perform line clipping on left-right sides
                             // to avoid such bound checks:
                             x0 = (prev > bboxx0) ? prev : bboxx0;
-                            x1 = (curx < bboxx1) ? curx : bboxx1;
+
+                            if (curx < bboxx1) {
+                                x1 = curx;
+                            } else {
+                                x1 = bboxx1;
+                                // skip right side (fast exit loop):
+                                i = numCrossings;
+                            }
 
                             if (x0 < x1) {
                                 x0 -= bboxx0; // turn x0, x1 from coords to indices
                                 x1 -= bboxx0; // in the alpha array.
 

@@ -1252,11 +1247,12 @@
                                     _alpha[pix_x    ] += tmp;
                                     _alpha[pix_x + 1] -= tmp;
 
                                     if (useBlkFlags) {
                                         // flag used blocks:
-                                        _blkFlags[pix_x >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[pix_x       >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[(pix_x + 1) >> _BLK_SIZE_LG] = 1;
                                     }
                                 } else {
                                     tmp = (x0 & _SUBPIXEL_MASK_X);
                                     _alpha[pix_x    ]
                                         += (_SUBPIXEL_POSITIONS_X - tmp);

@@ -1271,12 +1267,14 @@
                                     _alpha[pix_xmax + 1]
                                         -= tmp;
 
                                     if (useBlkFlags) {
                                         // flag used blocks:
-                                        _blkFlags[pix_x    >> _BLK_SIZE_LG] = 1;
-                                        _blkFlags[pix_xmax >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[ pix_x         >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[(pix_x + 1)    >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[pix_xmax       >> _BLK_SIZE_LG] = 1;
+                                        _blkFlags[(pix_xmax + 1) >> _BLK_SIZE_LG] = 1;
                                     }
                                 }
                             }
                             prev = _MAX_VALUE;
                         }

@@ -1294,23 +1292,26 @@
                 }
             } // numCrossings > 0
 
             // even if this last row had no crossings, alpha will be zeroed
             // from the last emitRow call. But this doesn't matter because
-            // maxX < minX, so no row will be emitted to the MarlinCache.
+            // maxX < minX, so no row will be emitted to the AlphaConsumer.
             if ((y & _SUBPIXEL_MASK_Y) == _SUBPIXEL_MASK_Y) {
                 lastY = y >> _SUBPIXEL_LG_POSITIONS_Y;
 
                 // convert subpixel to pixel coordinate within boundaries:
                 minX = FloatMath.max(minX, bboxx0) >> _SUBPIXEL_LG_POSITIONS_X;
                 maxX = FloatMath.min(maxX, bboxx1) >> _SUBPIXEL_LG_POSITIONS_X;
 
                 if (maxX >= minX) {
                     // note: alpha array will be zeroed by copyAARow()
-                    // +2 because alpha [pix_minX; pix_maxX+1]
+                    // +1 because alpha [pix_minX; pix_maxX[
                     // fix range [x0; x1[
-                    copyAARow(_alpha, lastY, minX, maxX + 2, useBlkFlags);
+                    // note: if x1=bboxx1, then alpha is written up to bboxx1+1
+                    // inclusive: alpha[bboxx1] ignored, alpha[bboxx1+1] == 0
+                    // (normally so never cleared below)
+                    copyAARow(_alpha, lastY, minX, maxX + 1, useBlkFlags, ac);
 
                     // speculative for next pixel row (scanline coherence):
                     if (_enableBlkFlagsHeuristics) {
                         // Use block flags if large pixel span and few crossings:
                         // ie mean(distance between crossings) is larger than

@@ -1331,11 +1332,11 @@
                             rdrCtx.stats.hist_tile_generator_encoding_dist
                                 .add(maxX / tmp);
                         }
                     }
                 } else {
-                    _cache.clearAARow(lastY);
+                    ac.clearAlphas(lastY);
                 }
                 minX = _MAX_VALUE;
                 maxX = _MIN_VALUE;
             }
         } // scan line iterator

@@ -1348,15 +1349,18 @@
         minX = FloatMath.max(minX, bboxx0) >> _SUBPIXEL_LG_POSITIONS_X;
         maxX = FloatMath.min(maxX, bboxx1) >> _SUBPIXEL_LG_POSITIONS_X;
 
         if (maxX >= minX) {
             // note: alpha array will be zeroed by copyAARow()
-            // +2 because alpha [pix_minX; pix_maxX+1]
+            // +1 because alpha [pix_minX; pix_maxX[
             // fix range [x0; x1[
-            copyAARow(_alpha, y, minX, maxX + 2, useBlkFlags);
+            // note: if x1=bboxx1, then alpha is written up to bboxx1+1
+            // inclusive: alpha[bboxx1] ignored then cleared and
+            // alpha[bboxx1+1] == 0 (normally so never cleared after)
+            copyAARow(_alpha, y, minX, maxX + 1, useBlkFlags, ac);
         } else if (y != lastY) {
-            _cache.clearAARow(y);
+            ac.clearAlphas(y);
         }
 
         // update member:
         edgeCount = numCrossings;
         prevUseBlkFlags = useBlkFlags;

@@ -1365,16 +1369,16 @@
             // update max used mark
             activeEdgeMaxUsed = _arrayMaxUsed;
         }
     }
 
-    boolean endRendering() {
+    void endRendering() {
         if (DO_MONITORS) {
             rdrCtx.stats.mon_rdr_endRendering.start();
         }
         if (edgeMinY == Integer.MAX_VALUE) {
-            return false; // undefined edges bounds
+            return; // undefined edges bounds
         }
 
         final int _boundsMinY = boundsMinY;
         final int _boundsMaxY = boundsMaxY;
 

@@ -1403,11 +1407,11 @@
                                 + "][" + spminY + " ... " + spmaxY + "]");
         }
 
         // test clipping for shapes out of bounds
         if ((spminX > spmaxX) || (spminY > spmaxY)) {
-            return false;
+            return;
         }
 
         // half open intervals
         // inclusive:
         final int pminX =  spminX                    >> SUBPIXEL_LG_POSITIONS_X;

@@ -1417,23 +1421,23 @@
         final int pminY =  spminY                    >> SUBPIXEL_LG_POSITIONS_Y;
         // exclusive:
         final int pmaxY = (spmaxY + SUBPIXEL_MASK_Y) >> SUBPIXEL_LG_POSITIONS_Y;
 
         // store BBox to answer ptg.getBBox():
-        this.cache.init(pminX, pminY, pmaxX, pmaxY, edgeSumDeltaY);
+        initConsumer(pminX, pminY, pmaxX, pmaxY);
 
         // Heuristics for using block flags:
         if (ENABLE_BLOCK_FLAGS) {
-            enableBlkFlags = this.cache.useRLE;
+            enableBlkFlags = this.useRLE;
             prevUseBlkFlags = enableBlkFlags && !ENABLE_BLOCK_FLAGS_HEURISTICS;
 
             if (enableBlkFlags) {
                 // ensure blockFlags array is large enough:
                 // note: +2 to ensure enough space left at end
-                final int nxTiles = ((pmaxX - pminX) >> TILE_SIZE_LG) + 2;
-                if (nxTiles > INITIAL_ARRAY) {
-                    blkFlags = blkFlags_ref.getArray(nxTiles);
+                final int blkLen = ((pmaxX - pminX) >> BLOCK_SIZE_LG) + 2;
+                if (blkLen > INITIAL_ARRAY) {
+                    blkFlags = blkFlags_ref.getArray(blkLen);
                 }
             }
         }
 
         // memorize the rendering bounding box:

@@ -1465,55 +1469,108 @@
             if (DO_STATS) {
                 rdrCtx.stats.stat_array_renderer_alphaline.add(width);
             }
             alphaLine = alphaLine_ref.getArray(width);
         }
+    }
 
-        // process first tile line:
-        endRendering(pminY);
+    void initConsumer(int minx, int miny, int maxx, int maxy)
+    {
+        // assert maxy >= miny && maxx >= minx;
+        bboxX0 = minx;
+        bboxX1 = maxx;
+        bboxY0 = miny;
+        bboxY1 = maxy;
+
+        final int width = (maxx - minx);
+
+        if (FORCE_NO_RLE) {
+            useRLE = false;
+        } else if (FORCE_RLE) {
+            useRLE = true;
+        } else {
+            // heuristics: use both bbox area and complexity
+            // ie number of primitives:
 
-        return true;
+            // fast check min width:
+            if (width <= RLE_MIN_WIDTH) {
+                useRLE = false;
+            } else {
+                useRLE = true;
+            }
+        }
     }
 
     private int bbox_spminX, bbox_spmaxX, bbox_spminY, bbox_spmaxY;
 
-    void endRendering(final int pminY) {
+    public void produceAlphas(final MarlinAlphaConsumer ac) {
+        ac.setMaxAlpha(MAX_AA_ALPHA);
+
+        if (enableBlkFlags && !ac.supportBlockFlags()) {
+            // consumer does not support block flag optimization:
+            enableBlkFlags = false;
+            prevUseBlkFlags = false;
+        }
+
         if (DO_MONITORS) {
             rdrCtx.stats.mon_rdr_endRendering_Y.start();
         }
 
-        final int spminY       = pminY << SUBPIXEL_LG_POSITIONS_Y;
-        final int fixed_spminY = FloatMath.max(bbox_spminY, spminY);
-
-        // avoid rendering for last call to nextTile()
-        if (fixed_spminY < bbox_spmaxY) {
-            // process a complete tile line ie scanlines for 32 rows
-            final int spmaxY = FloatMath.min(bbox_spmaxY, spminY + SUBPIXEL_TILE);
+        // Process all scan lines:
+        _endRendering(bbox_spminY, bbox_spmaxY, ac);
 
-            // process tile line [0 - 32]
-            cache.resetTileLine(pminY);
-
-            // Process only one tile line:
-            _endRendering(fixed_spminY, spmaxY);
-        }
         if (DO_MONITORS) {
             rdrCtx.stats.mon_rdr_endRendering_Y.stop();
         }
     }
 
     void copyAARow(final int[] alphaRow,
                    final int pix_y, final int pix_from, final int pix_to,
-                   final boolean useBlockFlags)
+                   final boolean useBlockFlags,
+                   final MarlinAlphaConsumer ac)
     {
+        if (DO_MONITORS) {
+            rdrCtx.stats.mon_rdr_copyAARow.start();
+        }
+        if (DO_STATS) {
+            rdrCtx.stats.stat_cache_rowAA.add(pix_to - pix_from);
+        }
+
         if (useBlockFlags) {
             if (DO_STATS) {
                 rdrCtx.stats.hist_tile_generator_encoding.add(1);
             }
-            cache.copyAARowRLE_WithBlockFlags(blkFlags, alphaRow, pix_y, pix_from, pix_to);
+            ac.setAndClearRelativeAlphas(blkFlags, alphaRow, pix_y, pix_from, pix_to);
         } else {
             if (DO_STATS) {
                 rdrCtx.stats.hist_tile_generator_encoding.add(0);
             }
-            cache.copyAARowNoRLE(alphaRow, pix_y, pix_from, pix_to);
+            ac.setAndClearRelativeAlphas(alphaRow, pix_y, pix_from, pix_to);
+        }
+        if (DO_MONITORS) {
+            rdrCtx.stats.mon_rdr_copyAARow.stop();
         }
     }
+
+    // output pixel bounding box:
+    int bboxX0, bboxX1, bboxY0, bboxY1;
+
+    @Override
+    public int getOutpixMinX() {
+        return bboxX0;
+    }
+
+    @Override
+    public int getOutpixMaxX() {
+        return bboxX1;
+    }
+
+    @Override
+    public int getOutpixMinY() {
+        return bboxY0;
+    }
+
+    @Override
+    public int getOutpixMaxY() {
+        return bboxY1;
+    }
 }
< prev index next >