This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pdfbox-jbig2.git

commit 383663c9d1bbb1138a79db9dd427bdc3c4b08a38
Author: Maruan Sahyoun <[email protected]>
AuthorDate: Tue May 12 12:47:10 2026 +0200

    PDFBOX-6151: add SlidingBitmapWindow with unit tests and JMH benchmark
---
 .../apache/pdfbox/jbig2/SlidingBitmapWindow.java   | 273 ++++++++++++++++++
 .../pdfbox/jbig2/SlidingBitmapWindowBenchmark.java | 188 ++++++++++++
 .../pdfbox/jbig2/SlidingBitmapWindowTest.java      | 320 +++++++++++++++++++++
 3 files changed, 781 insertions(+)

diff --git a/src/main/java/org/apache/pdfbox/jbig2/SlidingBitmapWindow.java 
b/src/main/java/org/apache/pdfbox/jbig2/SlidingBitmapWindow.java
new file mode 100644
index 0000000..8447305
--- /dev/null
+++ b/src/main/java/org/apache/pdfbox/jbig2/SlidingBitmapWindow.java
@@ -0,0 +1,273 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pdfbox.jbig2;
+
+/**
+ * A sliding 3-row bit window over a {@link Bitmap}, providing efficient
+ * single-bit access to a 3-row neighbourhood around a current position.
+ *
+ * <p>The window maintains three rows simultaneously: the row above (-1),
+ * the current row (0), and the row below (+1). Each row is held as a
+ * 24-bit register (prevByte | currentByte | nextByte). As the window
+ * advances horizontally via {@link #advance()}, bits slide through the
+ * register so that only a new byte needs to be fetched at each byte
+ * boundary. As the window moves down via {@link #nextRow()}, registers
+ * rotate so that only the new bottom row needs to be fetched.</p>
+ *
+ * <p>The optional {@code dx}/{@code dy} offsets allow the window to track
+ * a spatially shifted bitmap (e.g. GRREFERENCEDX / GRREFERENCEDY in §6.3.5).
+ * A call to {@link #getBit(int, int)} at logical position (x, y) reads
+ * bitmap position (x - dx, y - dy).</p>
+ *
+ * <p>All out-of-bounds accesses return 0 per §6.3.5.2.</p>
+ *
+ * <p><b>Current status:</b> this class is not yet used in production decoding
+ * paths. It exists as a standalone, testable abstraction of the sliding window
+ * bit-access mechanics that are currently inlined inside
+ * {@link 
org.apache.pdfbox.jbig2.decoder.GenericRefinementRegionDecodingProcedure}
+ * for the template 0 paths ({@code decodeTemplate},
+ * {@code decodeTypicalPredictedLineTemplate0}). Its primary purpose is to
+ * provide a test bed for verifying the correctness of those paths — in
+ * particular at byte boundaries, edge rows, and non-zero reference offsets —
+ * where latent bugs may exist that are not yet covered by the current test
+ * suite.</p>
+ */
+public class SlidingBitmapWindow
+{
+    private final Bitmap bitmap;
+    private final int dx;
+    private final int dy;
+    private final int width;
+    private final int height;
+    private final int rowStride;
+
+    // Bitmap coordinates after applying offset: bitmapX = currentX - dx, 
bitmapY =
+    // currentY - dy
+    private int bitmapX;
+    private int bitmapY;
+
+    // Bit position within the current byte (0=MSB=leftmost, 7=LSB=rightmost)
+    private int bitPosition;
+
+    // Current byte column in the bitmap: bitmapX >> 3
+    private int byteColumn;
+
+    // 24-bit sliding registers: (prevByte << 16) | (currentByte << 8) | 
nextByte
+    // Bit for colOffset c relative to current position is at shift (15 -
+    // bitPosition - c)
+    private int previousLine;
+    private int currentLine;
+    private int nextLine;
+
+    /**
+     * Constructs a window over the given bitmap, positioned at (0, 0).
+     *
+     * @param bitmap the bitmap to window over
+     */
+    public SlidingBitmapWindow(Bitmap bitmap)
+    {
+        this(bitmap, 0, 0);
+    }
+
+    /**
+     * Constructs a window over the given bitmap with a coordinate offset, 
positioned at (0, 0). The offsets are
+     * subtracted from all pixel accesses: getBit() at logical (x, y) reads 
bitmap position (x-dx, y-dy).
+     *
+     * @param bitmap the bitmap to window over
+     * @param dx horizontal offset (GRREFERENCEDX)
+     * @param dy vertical offset (GRREFERENCEDY)
+     */
+    public SlidingBitmapWindow(Bitmap bitmap, int dx, int dy)
+    {
+        this.bitmap = bitmap;
+        this.dx = dx;
+        this.dy = dy;
+        this.width = bitmap.getWidth();
+        this.height = bitmap.getHeight();
+        this.rowStride = bitmap.getRowStride();
+        moveTo(0, 0);
+    }
+
+    /**
+     * Repositions the window to (x, y), loading the 3-row neighbourhood. Must 
be called when changing position
+     * non-incrementally.
+     *
+     * @param x logical x coordinate
+     * @param y logical y coordinate
+     */
+    public void moveTo(int x, int y)
+    {
+        this.bitmapX = x - dx;
+        this.bitmapY = y - dy;
+        this.bitPosition = bitmapX & 7;
+        this.byteColumn = bitmapX >> 3;
+
+        previousLine = buildRegister(bitmapY - 1);
+        currentLine = buildRegister(bitmapY);
+        nextLine = buildRegister(bitmapY + 1);
+    }
+
+    /**
+     * Resets the window to (0, 0), reloading the 3-row neighbourhood. 
Equivalent to {@link #moveTo(int, int) moveTo(0,
+     * 0)} but intended for use in benchmarks to avoid object allocation 
between invocations.
+     */
+    public void reset()
+    {
+        moveTo(0, 0);
+    }
+
+    /**
+     * Advances the window one pixel to the right. Leftmost bits fall out, 
existing bits shift left, new rightmost bits
+     * enter from the data layer at byte boundaries.
+     */
+    public void advance()
+    {
+        bitmapX++;
+        bitPosition = (bitPosition + 1) & 7;
+
+        if (bitPosition == 0)
+        {
+            // Byte boundary — slide all registers and load the next byte
+            byteColumn++;
+            previousLine = slideRegister(previousLine, bitmapY - 1);
+            currentLine = slideRegister(currentLine, bitmapY);
+            nextLine = slideRegister(nextLine, bitmapY + 1);
+        }
+    }
+
+    /**
+     * Moves the window one row down and resets to x=0. The row above is 
discarded, current becomes above, below becomes
+     * current, and a new bottom row is fetched.
+     */
+    public void nextRow()
+    {
+        bitmapY++;
+        // Reset to start of row (x=0) and recompute bitmap coordinates
+        bitmapX = -dx; // logical x=0 maps to bitmap x=-dx
+        bitPosition = (-dx) & 7;
+        // IMPORTANT: must use >> (not /8) to preserve floor division for 
negatives
+        byteColumn = (-dx) >> 3;
+
+        previousLine = buildRegister(bitmapY - 1);
+        currentLine = buildRegister(bitmapY);
+        nextLine = buildRegister(bitmapY + 1);
+    }
+
+    /**
+     * Returns the bit value at the given row and column offset relative to 
the current position.
+     *
+     * @param rowOffset -1 (above), 0 (current), or +1 (below)
+     * @param colOffset column offset relative to current position, typically 
in the range -2 to +2
+     * @return 0 or 1; 0 if the target position is out of bounds
+     */
+    public int getBit(int rowOffset, int colOffset)
+    {
+        // Early exit for invalid rowOffset
+        if (rowOffset < -1 || rowOffset > 1)
+        {
+            return 0;
+        }
+
+        final int targetBmpX = bitmapX + colOffset;
+        final int targetBmpY = bitmapY + rowOffset;
+
+        if (targetBmpX < 0 || targetBmpX >= width || targetBmpY < 0 || 
targetBmpY >= height)
+        {
+            return 0;
+        }
+
+        final int reg;
+        switch (rowOffset)
+        {
+        case -1:
+            reg = previousLine;
+            break;
+        case 0:
+            reg = currentLine;
+            break;
+        default:
+            reg = nextLine;
+            break;
+        }
+
+        final int baseShift = 15 - bitPosition;
+        return (reg >>> (baseShift - colOffset)) & 1;
+    }
+
+    /**
+     * Returns the bit value at the given row and column offset without bounds 
checking. Only call this when the current
+     * position is guaranteed to be in the interior of the bitmap (not near 
any edge).
+     *
+     * @param rowOffset -1 (above), 0 (current), or +1 (below)
+     * @param colOffset column offset, typically -2 to +2
+     * @return 0 or 1
+     */
+    public int getBitFast(final int rowOffset, final int colOffset)
+    {
+        final int reg = (rowOffset == -1) ? previousLine
+                : (rowOffset == 0) ? currentLine : nextLine;
+        final int baseShift = 15 - bitPosition;
+        return (reg >>> (baseShift - colOffset)) & 1;
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Private helpers
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Builds a fresh 24-bit register for the given bitmap row at the current 
byte column position.
+     */
+    private int buildRegister(final int bmpRow)
+    {
+        if (bmpRow < 0 || bmpRow >= height)
+        {
+            return 0;
+        }
+        final int prev = loadByte(byteColumn - 1, bmpRow);
+        final int current = loadByte(byteColumn, bmpRow);
+        final int next = loadByte(byteColumn + 1, bmpRow);
+        return (prev << 16) | (current << 8) | next;
+    }
+
+    /**
+     * Advances a 24-bit register by one byte: prevByte is dropped, 
currentByte becomes prevByte, nextByte becomes
+     * currentByte, and a new nextByte is fetched.
+     */
+    private int slideRegister(final int reg, final int bmpRow)
+    {
+        if (bmpRow < 0 || bmpRow >= height)
+        {
+            return 0;
+        }
+        final int newNext = loadByte(byteColumn + 1, bmpRow);
+        return ((reg << 8) | newNext) & 0xFFFFFF; // Keep only 24 bits
+    }
+
+    /**
+     * Loads one byte from the bitmap at the given byte column and row. 
Returns 0 for out-of-bounds positions per
+     * §6.3.5.2.
+     */
+    private int loadByte(final int col, final int row)
+    {
+        if (col < 0 || col >= rowStride || row < 0 || row >= height)
+        {
+            return 0;
+        }
+        return bitmap.getByteAsInteger(row * rowStride + col);
+    }
+}
diff --git 
a/src/test/java/org/apache/pdfbox/jbig2/SlidingBitmapWindowBenchmark.java 
b/src/test/java/org/apache/pdfbox/jbig2/SlidingBitmapWindowBenchmark.java
new file mode 100644
index 0000000..187ffec
--- /dev/null
+++ b/src/test/java/org/apache/pdfbox/jbig2/SlidingBitmapWindowBenchmark.java
@@ -0,0 +1,188 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pdfbox.jbig2;
+
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.Blackhole;
+
+import java.util.concurrent.TimeUnit;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Benchmark)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 10, time = 2)
+@Fork(0)
+public class SlidingBitmapWindowBenchmark
+{
+    private static final int WIDTH  = 1728;
+    private static final int HEIGHT = 2339;
+
+    private Bitmap regionBitmap;
+    private Bitmap referenceBitmap;
+
+    private SlidingBitmapWindow regWindow;
+    private SlidingBitmapWindow refWindow;
+
+    @Setup(Level.Trial)
+    public void setUp()
+    {
+        regionBitmap    = new Bitmap(WIDTH, HEIGHT);
+        referenceBitmap = new Bitmap(WIDTH, HEIGHT);
+
+        // Fill with realistic patterns — different for each bitmap
+        for (int i = 0; i < regionBitmap.getLength(); i++)
+        {
+            regionBitmap.setByte(i,    (byte) (i % 2 == 0 ? 0xAA : 0x55));
+            referenceBitmap.setByte(i, (byte) (i % 3 == 0 ? 0xFF : 0x00));
+        }
+
+        regWindow = new SlidingBitmapWindow(regionBitmap);
+        refWindow = new SlidingBitmapWindow(referenceBitmap);
+    }
+
+    @Setup(Level.Invocation)
+    public void resetWindows()
+    {
+        regWindow.reset();
+        refWindow.reset();
+    }
+
+    /**
+     * Baseline: pixel-by-pixel using getPixelSafe() on two bitmaps.
+     */
+    @Benchmark
+    public void pixelByPixel(Blackhole bh)
+    {
+        int sum = 0;
+        for (int y = 0; y < HEIGHT; y++)
+        {
+            for (int x = 0; x < WIDTH; x++)
+            {
+                sum += buildContextGetPixel(x, y);
+            }
+        }
+        bh.consume(sum);
+    }
+
+    /**
+     * New approach: SlidingBitmapWindow on two windows.
+     */
+    @Benchmark
+    public void slidingWindow(Blackhole bh)
+    {
+        int sum = 0;
+        for (int y = 0; y < HEIGHT; y++, regWindow.nextRow(), 
refWindow.nextRow())
+        {
+            for (int x = 0; x < WIDTH; x++, regWindow.advance(), 
refWindow.advance())
+            {
+                sum += buildContextWindow();
+            }
+        }
+        bh.consume(sum);
+    }
+    
+    @Benchmark
+    public void slidingWindowFast(Blackhole bh)
+    {
+        int sum = 0;
+
+        // Row 0 and last row — safe path
+        for (int x = 0; x < WIDTH; x++, regWindow.advance(), 
refWindow.advance())
+            sum += buildContextWindow();
+        regWindow.nextRow();
+        refWindow.nextRow();
+
+        // Interior rows — fast path for interior pixels, safe for edges
+        for (int y = 1; y < HEIGHT - 1; y++, regWindow.nextRow(), 
refWindow.nextRow())
+        {
+            // x=0 — left edge, safe path
+            sum += buildContextWindow();
+            regWindow.advance();
+            refWindow.advance();
+
+            // x=1 to WIDTH-2 — interior, fast path
+            for (int x = 1; x < WIDTH - 1; x++, regWindow.advance(), 
refWindow.advance())
+                sum += buildContextWindowFast();
+
+            // x=WIDTH-1 — right edge, safe path
+            sum += buildContextWindow();
+            regWindow.advance();
+            refWindow.advance();
+        }
+
+        // Last row — safe path
+        for (int x = 0; x < WIDTH; x++, regWindow.advance(), 
refWindow.advance())
+            sum += buildContextWindow();
+
+        bh.consume(sum);
+    }
+
+    // 
-------------------------------------------------------------------------
+    // Context builders — mirror buildContextT1 from the decoding procedure
+    // 
-------------------------------------------------------------------------
+
+    private int buildContextGetPixel(final int x, final int y)
+    {
+        return (getPixelSafe(regionBitmap,    x - 1, y - 1) << 9)
+             | (getPixelSafe(regionBitmap,    x,     y - 1) << 8)
+             | (getPixelSafe(regionBitmap,    x + 1, y - 1) << 7)
+             | (getPixelSafe(regionBitmap,    x - 1, y    ) << 6)
+             | (getPixelSafe(referenceBitmap, x,     y - 1) << 5)
+             | (getPixelSafe(referenceBitmap, x - 1, y    ) << 4)
+             | (getPixelSafe(referenceBitmap, x,     y    ) << 3)
+             | (getPixelSafe(referenceBitmap, x + 1, y    ) << 2)
+             | (getPixelSafe(referenceBitmap, x,     y + 1) << 1)
+             | (getPixelSafe(referenceBitmap, x + 1, y + 1));
+    }
+
+    private int buildContextWindow()
+    {
+        return (regWindow.getBit(-1, -1) << 9)
+             | (regWindow.getBit( 0, -1) << 8)
+             | (regWindow.getBit( 1, -1) << 7)
+             | (regWindow.getBit(-1,  0) << 6)
+             | (refWindow.getBit( 0, -1) << 5)
+             | (refWindow.getBit(-1,  0) << 4)
+             | (refWindow.getBit( 0,  0) << 3)
+             | (refWindow.getBit( 1,  0) << 2)
+             | (refWindow.getBit( 0,  1) << 1)
+             | (refWindow.getBit( 1,  1));
+    }
+
+    private int buildContextWindowFast()
+    {
+        return (regWindow.getBitFast(-1, -1) << 9)
+            | (regWindow.getBitFast( 0, -1) << 8)
+            | (regWindow.getBitFast( 1, -1) << 7)
+            | (regWindow.getBitFast(-1,  0) << 6)
+            | (refWindow.getBitFast( 0, -1) << 5)
+            | (refWindow.getBitFast(-1,  0) << 4)
+            | (refWindow.getBitFast( 0,  0) << 3)
+            | (refWindow.getBitFast( 1,  0) << 2)
+            | (refWindow.getBitFast( 0,  1) << 1)
+            | (refWindow.getBitFast( 1,  1));
+    }
+
+    private int getPixelSafe(final Bitmap bitmap, final int x, final int y)
+    {
+        if (x < 0 || y < 0 || x >= WIDTH || y >= HEIGHT)
+            return 0;
+        return bitmap.getPixel(x, y);
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/pdfbox/jbig2/SlidingBitmapWindowTest.java 
b/src/test/java/org/apache/pdfbox/jbig2/SlidingBitmapWindowTest.java
new file mode 100644
index 0000000..776b07b
--- /dev/null
+++ b/src/test/java/org/apache/pdfbox/jbig2/SlidingBitmapWindowTest.java
@@ -0,0 +1,320 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pdfbox.jbig2;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+public class SlidingBitmapWindowTest
+{
+
+    private Bitmap createTestBitmap(int[][] pixels)
+    {
+        int height = pixels.length;
+        int width = pixels[0].length;
+        Bitmap bitmap = new Bitmap(width, height);
+
+        for (int y = 0; y < height; y++)
+        {
+            for (int x = 0; x < width; x++)
+            {
+                if (pixels[y][x] != 0)
+                {
+                    bitmap.setPixel(x, y, (byte) 1);
+                }
+            }
+        }
+        return bitmap;
+    }
+
+    @Test
+    public void testBasicBitAccess()
+    {
+        int[][] pixels = { { 1, 0, 1 }, { 0, 1, 0 }, { 1, 0, 1 } };
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+
+        window.moveTo(1, 1); // Center of the bitmap
+
+        assertEquals(1, window.getBit(0, 0)); // Center (1,1) -> 1
+        assertEquals(0, window.getBit(0, -1)); // Left (1,0) -> 0
+        assertEquals(0, window.getBit(0, 1)); // Right (1,2) -> 0
+        assertEquals(0, window.getBit(-1, 0)); // Above (0,1) -> 0
+        assertEquals(0, window.getBit(1, 0)); // Below (2,1) -> 0
+    }
+
+    @Test
+    public void testOutOfBoundsAccess()
+    {
+        int[][] pixels = { { 1, 0 }, { 0, 1 } };
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+
+        window.moveTo(0, 0);
+
+        assertEquals(0, window.getBit(-2, 0)); // Row out of bounds
+        assertEquals(0, window.getBit(0, -1)); // Column out of bounds (left)
+        assertEquals(0, window.getBit(0, 2)); // Column out of bounds (right)
+    }
+
+    @Test
+    public void testAdvance()
+    {
+        // Create a 1x8 bitmap: 1 0 1 0 1 0 1 0
+        int[][] pixels = { { 1, 0, 1, 0, 1, 0, 1, 0 } };
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+
+        window.moveTo(0, 0);
+
+        // Check initial position
+        assertEquals(1, window.getBit(0, 0)); // First bit
+
+        window.advance();
+        assertEquals(0, window.getBit(0, 0)); // Second bit
+
+        window.advance();
+        assertEquals(1, window.getBit(0, 0)); // Third bit
+    }
+
+    @Test
+    public void testNextRow()
+    {
+        // Create a 2x2 bitmap:
+        // 1 0
+        // 0 1
+        int[][] pixels = { { 1, 0 }, { 0, 1 } };
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+
+        window.moveTo(0, 0);
+        assertEquals(1, window.getBit(0, 0)); // (0,0) -> 1
+
+        window.nextRow();
+        assertEquals(0, window.getBit(0, 0)); // (0,1) -> 0
+    }
+
+    @Test
+    public void testWithOffsets()
+    {
+        int[][] pixels = { { 1, 0, 1 }, { 0, 1, 0 }, { 1, 0, 1 } };
+        Bitmap bitmap = createTestBitmap(pixels);
+
+        // To read bitmap (1,1) from logical (0,0):
+        // We need: bmpX = 0 - dx = 1 → dx = -1
+        // bmpY = 0 - dy = 1 → dy = -1
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap, -1, -1);
+
+        window.moveTo(0, 0); // Logical (0,0) → bitmap (1,1)
+
+        assertEquals(1, window.getBit(0, 0)); // bitmap (1,1) → 1
+        assertEquals(0, window.getBit(0, -1)); // bitmap (1,0) → 0
+        assertEquals(0, window.getBit(-1, 0)); // bitmap (0,1) → 0
+    }
+
+    @Test
+    public void testByteBoundaryCrossing()
+    {
+        // Create a 1x16 bitmap with alternating bits
+        int[][] pixels = new int[1][16];
+        for (int x = 0; x < 16; x++)
+        {
+            pixels[0][x] = x % 2;
+        }
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+
+        window.moveTo(0, 0);
+
+        // Check bits across byte boundary (x=7 and x=8)
+        for (int x = 0; x < 16; x++)
+        {
+            assertEquals(x % 2, window.getBit(0, 0));
+            window.advance();
+        }
+    }
+
+    @Test
+    public void testByteBoundaryWithNegativeColOffset()
+    {
+        // At x=8 (start of second byte), colOffset=-1 reaches back into first 
byte
+        // This is the case that fails with a 16-bit register
+        // Pattern: 0xAA 0x55 = 10101010 01010101
+        Bitmap bitmap = new Bitmap(16, 1);
+        bitmap.setByte(0, (byte) 0xAA); // 10101010
+        bitmap.setByte(1, (byte) 0x55); // 01010101
+
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+        window.moveTo(8, 0); // Start of second byte
+
+        // At x=8, colOffset=-1 should reach x=7 in first byte (bit 0 of 0xAA 
= 0)
+        assertEquals(0, window.getBit(0, -1)); // x=7 → last bit of 0xAA = 0
+        assertEquals(0, window.getBit(0, 0)); // x=8 → first bit of 0x55 = 0
+        assertEquals(1, window.getBit(0, 1)); // x=9 → second bit of 0x55 = 1
+    }
+
+    @Test
+    public void testByteBoundaryWithNegativeColOffsetMinus2()
+    {
+        // colOffset=-2 is used by template 0 context (x-2, y-1)
+        // At x=8, colOffset=-2 reaches x=6 in first byte
+        Bitmap bitmap = new Bitmap(16, 1);
+        bitmap.setByte(0, (byte) 0xAA); // 10101010
+        bitmap.setByte(1, (byte) 0x55); // 01010101
+
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+        window.moveTo(8, 0);
+
+        // x=6 → bit 6 of 0xAA (0-indexed from MSB) = 1
+        assertEquals(1, window.getBit(0, -2));
+    }
+
+    @Test
+    public void testNegativeBmpXWithPositiveDx()
+    {
+        // dx=2 means logical x=0 maps to bitmap x=-2 (out of bounds → 0)
+        // logical x=1 maps to bitmap x=-1 (out of bounds → 0)
+        // logical x=2 maps to bitmap x=0 (in bounds)
+        int[][] pixels = { { 1, 0, 1, 0, 1, 0, 1, 0 } };
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap, 2, 0);
+
+        window.moveTo(0, 0); // bmpX = 0 - 2 = -2 → out of bounds
+        assertEquals(0, window.getBit(0, 0)); // bmpX=-2 → 0
+        assertEquals(0, window.getBit(0, 1)); // bmpX=-1 → 0
+        assertEquals(1, window.getBit(0, 2)); // bmpX= 0 → 1
+    }
+
+    @Test
+    public void testNextRowAtFirstRow()
+    {
+        int[][] pixels = { { 1, 0 }, { 0, 1 } };
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+
+        window.moveTo(0, 0);
+        // Above row 0 is out of bounds → must return 0
+        assertEquals(0, window.getBit(-1, 0));
+        assertEquals(1, window.getBit(0, 0)); // current row
+        assertEquals(0, window.getBit(+1, 0)); // row below
+    }
+
+    @Test
+    public void testNextRowAtLastRow()
+    {
+        int[][] pixels = { { 1, 0 }, { 0, 1 } };
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+
+        window.moveTo(0, 1); // Last row
+        assertEquals(1, window.getBit(-1, 0)); // row above
+        assertEquals(0, window.getBit(0, 0)); // current row
+        // Below last row is out of bounds → must return 0
+        assertEquals(0, window.getBit(+1, 0));
+    }
+
+    @Test
+    public void testNextRowRotation()
+    {
+        // Verify that nextRow() correctly rotates:
+        // after nextRow(), what was rowBelow is now rowCurrent
+        int[][] pixels = { { 1, 1, 1 }, // row 0
+                { 0, 0, 0 }, // row 1
+                { 1, 0, 1 } // row 2
+        };
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+
+        window.moveTo(0, 0);
+        assertEquals(1, window.getBit(0, 0)); // row 0, x=0 → 1
+        assertEquals(0, window.getBit(+1, 0)); // row 1, x=0 → 0
+
+        window.nextRow();
+        assertEquals(1, window.getBit(-1, 0)); // row 0, x=0 → 1 (was above)
+        assertEquals(0, window.getBit(0, 0)); // row 1, x=0 → 0 (was below, 
now current)
+        assertEquals(1, window.getBit(+1, 0)); // row 2, x=0 → 1 (newly 
fetched)
+    }
+
+    @Test
+    public void testAdvanceAcrossMultipleByteBoundaries()
+    {
+        // 3 bytes: 0xFF 0x00 0xFF = 11111111 00000000 11111111
+        Bitmap bitmap = new Bitmap(24, 1);
+        bitmap.setByte(0, (byte) 0xFF);
+        bitmap.setByte(1, (byte) 0x00);
+        bitmap.setByte(2, (byte) 0xFF);
+
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+        window.moveTo(0, 0);
+
+        for (int x = 0; x < 24; x++)
+        {
+            int expected = (x < 8 || x >= 16) ? 1 : 0;
+            assertEquals("x=" + x, expected, window.getBit(0, 0));
+            if (x < 23)
+                window.advance();
+        }
+    }
+
+    @Test
+    public void testNextRowResetsToX0()
+    {
+        // After nextRow(), window must be at x=0, not where advance() left off
+        int[][] pixels = { { 1, 0, 1, 0, 1, 0, 1, 0, 1, 0 }, // row 0
+                { 0, 1, 0, 1, 0, 1, 0, 1, 0, 1 } // row 1
+        };
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+
+        window.moveTo(0, 0);
+        // Advance to x=5
+        for (int i = 0; i < 5; i++)
+            window.advance();
+
+        window.nextRow();
+        // Should be at x=0 of row 1, not x=5
+        assertEquals(0, window.getBit(0, 0)); // row 1, x=0 → 0
+    }
+
+    @Test
+    public void testGenericRefinementContext()
+    {
+        // 3x3 checkerboard: (x+y)%2
+        // 0 1 0
+        // 1 0 1
+        // 0 1 0
+        int[][] pixels = new int[3][3];
+        for (int y = 0; y < 3; y++)
+            for (int x = 0; x < 3; x++)
+                pixels[y][x] = (x + y) % 2;
+
+        Bitmap bitmap = createTestBitmap(pixels);
+        SlidingBitmapWindow window = new SlidingBitmapWindow(bitmap);
+        window.moveTo(1, 1); // Center
+
+        assertEquals(0, window.getBit(-1, -1)); // (0,0) → 0
+        assertEquals(1, window.getBit(-1, 0)); // (1,0) → 1
+        assertEquals(0, window.getBit(-1, 1)); // (2,0) → 0
+        assertEquals(1, window.getBit(0, -1)); // (0,1) → 1
+        assertEquals(0, window.getBit(0, 0)); // (1,1) → 0
+        assertEquals(1, window.getBit(0, 1)); // (2,1) → 1
+        assertEquals(0, window.getBit(1, -1)); // (0,2) → 0
+        assertEquals(1, window.getBit(1, 0)); // (1,2) → 1
+        assertEquals(0, window.getBit(1, 1)); // (2,2) → 0
+    }
+}

Reply via email to