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 + } +}
