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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-imaging.git

commit e9d72270b3c01bcde5c5e64fe1ec83307fad2793
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Jul 11 16:45:22 2026 -0400

    Add tests.
    
    A version of PR #553 that doesn't break the build.
---
 .../commons/imaging/AbstractImagingTest.java       |  23 +-
 .../imaging/bytesource/ByteSourceImageTest.java    |   8 +-
 .../imaging/common/BinaryFileFunctionsTest.java    |  27 +++
 .../imaging/common/BinaryFunctionsTest.java        |  76 ++++++
 .../imaging/formats/bmp/BmpWriterPaletteTest.java  |  98 ++++++++
 .../formats/bmp/PixelParserBitFieldsTest.java      | 120 ++++++++++
 .../imaging/formats/bmp/PixelParserRgbTest.java    | 104 +++++++++
 .../imaging/formats/bmp/PixelParserRleTest.java    | 102 ++++++++
 .../formats/jpeg/exif/ExifRewriterTest.java        | 223 ++++++++++++++++++
 .../formats/png/PngMultipleRoundtripTest.java      |   5 +-
 .../imaging/formats/rgbe/RgbeParserTest.java       | 112 +++++++++
 .../imaging/formats/tiff/TiffClassesTest.java      | 203 ++++++++++++++++
 .../imaging/formats/tiff/TiffTagIntegrityTest.java |   5 +-
 .../commons/imaging/formats/xpm/XpmParserTest.java | 139 +++++++++++
 .../imaging/palette/ColorComponentTest.java        |  34 +++
 .../commons/imaging/palette/ColorCountTest.java    |  50 ++++
 .../commons/imaging/palette/ColorGroupCutTest.java |  51 ++++
 .../commons/imaging/palette/ColorGroupTest.java    | 198 ++++++++++++++++
 .../imaging/palette/ColorSpaceSubsetTest.java      | 134 +++++++++++
 .../commons/imaging/palette/DitheringTest.java     | 142 ++++++++++-
 .../imaging/palette/LongestAxisMedianCutTest.java  | 260 +++++++++++++++++++++
 .../imaging/palette/PaletteFactoryTest.java        | 149 ++++++++++++
 22 files changed, 2226 insertions(+), 37 deletions(-)

diff --git a/src/test/java/org/apache/commons/imaging/AbstractImagingTest.java 
b/src/test/java/org/apache/commons/imaging/AbstractImagingTest.java
index 375600fc..e9e2415f 100644
--- a/src/test/java/org/apache/commons/imaging/AbstractImagingTest.java
+++ b/src/test/java/org/apache/commons/imaging/AbstractImagingTest.java
@@ -31,19 +31,16 @@ import org.apache.commons.imaging.test.FileSystemTraversal;
 public abstract class AbstractImagingTest {
 
     public interface ImageFilter {
+
         boolean accept(File file) throws IOException, ImagingException;
     }
 
     private static final List<File> ALL_IMAGES = new ArrayList<>();
-
     static {
         File imagesFolder = ImagingTestConstants.TEST_IMAGE_FOLDER;
-
         imagesFolder = imagesFolder.getAbsoluteFile();
-
         Debug.debug("imagesFolder", imagesFolder);
         assertTrue(imagesFolder.exists());
-
         final FileSystemTraversal.Visitor visitor = (file, progressEstimate) 
-> {
             if (!Imaging.hasImageFileExtension(file)) {
                 return true;
@@ -64,29 +61,17 @@ public abstract class AbstractImagingTest {
 
     protected static List<File> getTestImages(final ImageFilter filter, final 
int max) throws IOException, ImagingException {
         final List<File> images = new ArrayList<>();
-
         for (final File file : ALL_IMAGES) {
-            if (!Imaging.hasImageFileExtension(file)) {
-                continue;
-            }
-
-            if 
(file.getParentFile().getName().toLowerCase().equals("@broken")) {
+            if (!Imaging.hasImageFileExtension(file) || 
file.getParentFile().getName().toLowerCase().equals("@broken")
+                    || filter != null && !filter.accept(file)) {
                 continue;
             }
-
-            if (filter != null && !filter.accept(file)) {
-                continue;
-            }
-
             images.add(file);
-
             if (max > 0 && images.size() >= max) {
                 break;
             }
         }
-
         assertFalse(images.isEmpty());
-
         return images;
     }
 
@@ -96,9 +81,7 @@ public abstract class AbstractImagingTest {
 
     protected File getTestImage(final ImageFilter filter) throws IOException, 
ImagingException {
         final List<File> images = getTestImages(filter, 1);
-
         assertFalse(images.isEmpty());
-
         return images.get(0);
     }
 
diff --git 
a/src/test/java/org/apache/commons/imaging/bytesource/ByteSourceImageTest.java 
b/src/test/java/org/apache/commons/imaging/bytesource/ByteSourceImageTest.java
index 20a33414..3d7ee93f 100644
--- 
a/src/test/java/org/apache/commons/imaging/bytesource/ByteSourceImageTest.java
+++ 
b/src/test/java/org/apache/commons/imaging/bytesource/ByteSourceImageTest.java
@@ -106,13 +106,7 @@ class ByteSourceImageTest extends AbstractByteSourceTest {
 
         final Method[] methods = ImageInfo.class.getMethods();
         for (final Method method2 : methods) {
-            if (!Modifier.isPublic(method2.getModifiers())) {
-                continue;
-            }
-            if (!method2.getName().startsWith("get")) {
-                continue;
-            }
-            if (method2.getName().equals("getClass")) {
+            if (!Modifier.isPublic(method2.getModifiers()) || 
!method2.getName().startsWith("get") || method2.getName().equals("getClass")) {
                 continue;
                 // if (method.getGenericParameterTypes().length > 0)
                 // continue;
diff --git 
a/src/test/java/org/apache/commons/imaging/common/BinaryFileFunctionsTest.java 
b/src/test/java/org/apache/commons/imaging/common/BinaryFileFunctionsTest.java
index 2801a06d..004edb5b 100644
--- 
a/src/test/java/org/apache/commons/imaging/common/BinaryFileFunctionsTest.java
+++ 
b/src/test/java/org/apache/commons/imaging/common/BinaryFileFunctionsTest.java
@@ -15,6 +15,7 @@
 
 package org.apache.commons.imaging.common;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.nio.ByteOrder;
@@ -52,4 +53,30 @@ class BinaryFileFunctionsTest extends AbstractImagingTest {
         final byte[] bytesBE = ByteConversions.toBytes(TARGET_LONG, 
ByteOrder.BIG_ENDIAN);
         assertEquals(ByteConversions.toLong(bytesBE, ByteOrder.BIG_ENDIAN), 
TARGET_LONG);
     }
+    @Test
+    void testIntToByteConversion() {
+        final int target = 0x01020304;
+        final byte[] bytesLE = ByteConversions.toBytes(target, 
ByteOrder.LITTLE_ENDIAN);
+        assertEquals(ByteConversions.toInt(bytesLE, ByteOrder.LITTLE_ENDIAN), 
target);
+
+        final byte[] bytesBE = ByteConversions.toBytes(target, 
ByteOrder.BIG_ENDIAN);
+        assertEquals(ByteConversions.toInt(bytesBE, ByteOrder.BIG_ENDIAN), 
target);
+    }
+
+    @Test
+    void testShortToByteConversion() {
+        final short target = 0x0102;
+        final byte[] bytesLE = ByteConversions.toBytes(target, 
ByteOrder.LITTLE_ENDIAN);
+        assertEquals(ByteConversions.toShort(bytesLE, 
ByteOrder.LITTLE_ENDIAN), target);
+
+        final byte[] bytesBE = ByteConversions.toBytes(target, 
ByteOrder.BIG_ENDIAN);
+        assertEquals(ByteConversions.toShort(bytesBE, ByteOrder.BIG_ENDIAN), 
target);
+    }
+
+    @Test
+    void testArrayConversions() {
+        final int[] targets = {0x01020304, 0x05060708};
+        final byte[] bytesBE = ByteConversions.toBytes(targets, 
ByteOrder.BIG_ENDIAN);
+        assertArrayEquals(targets, ByteConversions.toInts(bytesBE, 
ByteOrder.BIG_ENDIAN));
+    }
 }
diff --git 
a/src/test/java/org/apache/commons/imaging/common/BinaryFunctionsTest.java 
b/src/test/java/org/apache/commons/imaging/common/BinaryFunctionsTest.java
new file mode 100644
index 00000000..c3b93742
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/common/BinaryFunctionsTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.common;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.junit.jupiter.api.Test;
+
+public class BinaryFunctionsTest {
+
+    @Test
+    public void testCharsToQuad() {
+        assertEquals(0x01020304, BinaryFunctions.charsToQuad((char) 1, (char) 
2, (char) 3, (char) 4));
+    }
+
+    @Test
+    public void testQuadsToByteArray() {
+        assertArrayEquals(new byte[] { 1, 2, 3, 4 }, 
BinaryFunctions.quadsToByteArray(0x01020304));
+    }
+
+    @Test
+    public void testCompareBytes() {
+        final byte[] a = { 1, 2, 3, 4, 5 };
+        final byte[] b = { 0, 2, 3, 4, 6 };
+        assertTrue(BinaryFunctions.compareBytes(a, 1, b, 1, 3));
+        assertFalse(BinaryFunctions.compareBytes(a, 0, b, 0, 5));
+        assertFalse(BinaryFunctions.compareBytes(a, 4, b, 4, 1));
+    }
+
+    @Test
+    public void testStartsWith() {
+        final byte[] haystack = { 1, 2, 3, 4, 5 };
+        final byte[] needle = { 1, 2, 3 };
+        final byte[] wrongNeedle = { 1, 2, 4 };
+        assertTrue(BinaryFunctions.startsWith(haystack, needle));
+        assertFalse(BinaryFunctions.startsWith(haystack, wrongNeedle));
+        assertFalse(BinaryFunctions.startsWith(needle, haystack));
+    }
+
+    @Test
+    public void testReadBytes() throws IOException {
+        final byte[] data = { 1, 2, 3, 4, 5 };
+        final InputStream is = new ByteArrayInputStream(data);
+        final byte[] read = BinaryFunctions.readBytes("test", is, 3, "Error");
+        assertArrayEquals(new byte[] { 1, 2, 3 }, read);
+    }
+
+    @Test
+    public void testCopyOfRange() {
+        final byte[] original = { 1, 2, 3, 4, 5 };
+        final byte[] copy = BinaryFunctions.copyOfRange(original, 1, 3);
+        assertArrayEquals(new byte[] { 2, 3, 4 }, copy);
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/bmp/BmpWriterPaletteTest.java
 
b/src/test/java/org/apache/commons/imaging/formats/bmp/BmpWriterPaletteTest.java
new file mode 100644
index 00000000..0091fdf6
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/imaging/formats/bmp/BmpWriterPaletteTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.formats.bmp;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.apache.commons.imaging.common.AbstractBinaryOutputStream;
+import org.apache.commons.imaging.palette.SimplePalette;
+import org.junit.jupiter.api.Test;
+
+public class BmpWriterPaletteTest {
+
+    @Test
+    public void testBmpWriterPalette_1Bit() {
+        final int[] colors = { 0x000000, 0xFFFFFF };
+        final SimplePalette palette = new SimplePalette(colors);
+        final BmpWriterPalette writer = new BmpWriterPalette(palette);
+        assertEquals(1, writer.getBitsPerPixel());
+        assertEquals(2, writer.getPaletteSize());
+        final BufferedImage image = new BufferedImage(2, 1, 
BufferedImage.TYPE_INT_RGB);
+        image.setRGB(0, 0, 0xFFFFFF); // White (index 1)
+        image.setRGB(1, 0, 0x000000); // Black (index 0)
+        final byte[] imageData = writer.getImageData(image);
+        // 2 pixels at 1 bit each = 2 bits. Packed into one byte as 0b10000000 
= 0x80.
+        // Padding to 4 bytes: 0x80, 0, 0, 0.
+        assertArrayEquals(new byte[] { (byte) 0x80, 0, 0, 0 }, imageData);
+    }
+
+    @Test
+    public void testBmpWriterPalette_4Bit() {
+        final int[] colors = new int[16];
+        for (int i = 0; i < 16; i++) {
+            colors[i] = i;
+        }
+        final SimplePalette palette = new SimplePalette(colors);
+        final BmpWriterPalette writer = new BmpWriterPalette(palette);
+        assertEquals(4, writer.getBitsPerPixel());
+        final BufferedImage image = new BufferedImage(2, 1, 
BufferedImage.TYPE_INT_RGB);
+        image.setRGB(0, 0, 0x000002); // Index 2
+        image.setRGB(1, 0, 0x000003); // Index 3
+        final byte[] imageData = writer.getImageData(image);
+        // 2 pixels at 4 bits each = 8 bits. 0x23.
+        // Padding to 4 bytes: 0x23, 0, 0, 0.
+        assertArrayEquals(new byte[] { 0x23, 0, 0, 0 }, imageData);
+    }
+
+    @Test
+    public void testBmpWriterPalette_8Bit() {
+        final int[] colors = new int[256];
+        for (int i = 0; i < 256; i++) {
+            colors[i] = i;
+        }
+        final SimplePalette palette = new SimplePalette(colors);
+        final BmpWriterPalette writer = new BmpWriterPalette(palette);
+        assertEquals(8, writer.getBitsPerPixel());
+        final BufferedImage image = new BufferedImage(2, 1, 
BufferedImage.TYPE_INT_RGB);
+        image.setRGB(0, 0, 0x00000A); // Index 10
+        image.setRGB(1, 0, 0x00000B); // Index 11
+        final byte[] imageData = writer.getImageData(image);
+        // 2 pixels at 8 bits each = 2 bytes. 0x0A, 0x0B.
+        // Padding to 4 bytes: 0x0A, 0x0B, 0, 0.
+        assertArrayEquals(new byte[] { 0x0A, 0x0B, 0, 0 }, imageData);
+    }
+
+    @Test
+    public void testWritePalette() throws IOException {
+        final int[] colors = { 0x112233, 0x445566 };
+        final SimplePalette palette = new SimplePalette(colors);
+        final BmpWriterPalette writer = new BmpWriterPalette(palette);
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        final AbstractBinaryOutputStream bos = 
AbstractBinaryOutputStream.littleEndian(baos);
+        writer.writePalette(bos);
+        final byte[] paletteData = baos.toByteArray();
+        // Each entry is 4 bytes: Blue, Green, Red, 0
+        final byte[] expected = { 0x33, 0x22, 0x11, 0, 0x66, 0x55, 0x44, 0 };
+        assertArrayEquals(expected, paletteData);
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/bmp/PixelParserBitFieldsTest.java
 
b/src/test/java/org/apache/commons/imaging/formats/bmp/PixelParserBitFieldsTest.java
new file mode 100644
index 00000000..43cc671c
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/imaging/formats/bmp/PixelParserBitFieldsTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.formats.bmp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+
+import org.apache.commons.imaging.ImagingException;
+import org.junit.jupiter.api.Test;
+
+public class PixelParserBitFieldsTest {
+
+    @Test
+    public void testGetNextRgb_16Bit() throws IOException {
+        // 5-6-5 RGB
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 16, 3, 0, 0, 0, 0, 0, 0b1111100000000000, // red
+                0b0000011111100000, // green
+                0b0000000000011111, // blue
+                0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // Data: 0b11111_000000_00000 -> 0xF800 (Pure Red in 565)
+        // Little endian: 0x00, 0xF8
+        final byte[] imageData = { 0x00, (byte) 0xF8 };
+        final PixelParserBitFields parser = new PixelParserBitFields(bhi, 
null, imageData);
+        final int rgb = parser.getNextRgb();
+        // 0xF800 -> red = 0b11111 (31). 31 << 3 = 248.
+        // Expected ARGB: 0xFFF80000
+        assertEquals(0xFFF80000, rgb);
+    }
+
+    @Test
+    public void testGetNextRgb_32Bit_WithAlpha() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 32, 3, 0, 0, 0, 0, 0, 0x00FF0000, // red
+                0x0000FF00, // green
+                0x000000FF, // blue
+                0xFF000000, // alpha
+                0, null, 0, 0, 0, 0, 0, 0, 0);
+        // Data: ARGB = 0x80402010
+        // Little endian: 0x10, 0x20, 0x40, 0x80
+        final byte[] imageData = { 0x10, 0x20, 0x40, (byte) 0x80 };
+        final PixelParserBitFields parser = new PixelParserBitFields(bhi, 
null, imageData);
+        final int rgb = parser.getNextRgb();
+        assertEquals(0x80402010, rgb);
+    }
+
+    @Test
+    public void testGetNextRgb_24Bit() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 24, 3, 0, 0, 0, 0, 0, 0x00FF0000, // red
+                0x0000FF00, // green
+                0x000000FF, // blue
+                0, // alpha
+                0, null, 0, 0, 0, 0, 0, 0, 0);
+        // Data: RGB = 0x112233
+        // Little endian: 0x33, 0x22, 0x11
+        final byte[] imageData = { 0x33, 0x22, 0x11 };
+        final PixelParserBitFields parser = new PixelParserBitFields(bhi, 
null, imageData);
+        final int rgb = parser.getNextRgb();
+        assertEquals(0xFF112233, rgb);
+    }
+
+    @Test
+    public void testGetNextRgb_8Bit() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 8, 3, 0, 0, 0, 0, 0, 0b11100000, // red
+                0b00011100, // green
+                0b00000011, // blue
+                0, // alpha
+                0, null, 0, 0, 0, 0, 0, 0, 0);
+        // Data: 0b101_010_11 -> 0xAB
+        final byte[] imageData = { (byte) 0xAB };
+        final PixelParserBitFields parser = new PixelParserBitFields(bhi, 
null, imageData);
+        final int rgb = parser.getNextRgb();
+        // red = 0b101 (5). 5 << 5 = 160.
+        // green = 0b010 (2). 2 << 5 = 64.
+        // blue = 0b11 (3). 3 << 6 = 192.
+        // wait, getMaskShift for blue (mask=3):
+        // trailingZeroes = 0.
+        // maskLength = 2.
+        // return 0 - (8 - 2) = -6.
+        // blue << -(-6) = blue << 6. 3 << 6 = 192. Correct.
+        assertEquals(0xFFA040C0, rgb);
+    }
+
+    @Test
+    public void testNewlinePadding() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 16, 3, 0, 0, 0, 0, 0, 0xF800, 0x07E0, 0x001F, 0, 0, null, 0, 0, 
0,
+                0, 0, 0, 0);
+        // 16 bits = 2 bytes. 2 bytes padding needed to reach the 4-byte 
boundary.
+        final byte[] imageData = { 0x00, 0x00, 0x55, 0x66 };
+        final PixelParserBitFields parser = new PixelParserBitFields(bhi, 
null, imageData);
+        parser.getNextRgb(); // consumes 2 bytes
+        parser.newline(); // should consume 2 more bytes
+        // Next read should fail, or we can check internal byteCount if it was 
accessible
+        // Since we can't check byteCount, we can try to read again and see if 
it fails.
+        assertThrows(IOException.class, () -> parser.getNextRgb());
+    }
+
+    @Test
+    public void testUnknownBitsPerPixel() {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        final byte[] imageData = { 0 };
+        final PixelParserBitFields parser = new PixelParserBitFields(bhi, 
null, imageData);
+        assertThrows(ImagingException.class, parser::getNextRgb);
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/bmp/PixelParserRgbTest.java 
b/src/test/java/org/apache/commons/imaging/formats/bmp/PixelParserRgbTest.java
new file mode 100644
index 00000000..76409395
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/imaging/formats/bmp/PixelParserRgbTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.formats.bmp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+
+public class PixelParserRgbTest {
+
+    private static final byte[] COLOR_TABLE = { (byte) 0x00, (byte) 0x00, 
(byte) 0x00, (byte) 0xFF, // 0: Black
+            (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, // 1: White
+            (byte) 0x00, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, // 2: Red
+            (byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xFF, // 3: Green
+    };
+
+    @Test
+    public void testGetNextRgb_1Bit() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // Data: 0b10100000 -> sample 1, then 0, then 1, then 0...
+        final byte[] imageData = { (byte) 0xA0 };
+        final PixelParserRgb parser = new PixelParserRgb(bhi, COLOR_TABLE, 
imageData);
+        assertEquals(0xFFFFFFFF, parser.getNextRgb()); // 1 -> White
+        assertEquals(0xFF000000, parser.getNextRgb()); // 0 -> Black
+    }
+
+    @Test
+    public void testGetNextRgb_4Bit() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 2, 1, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // Data: 0x23 -> sample 2, then 3
+        final byte[] imageData = { 0x23 };
+        final PixelParserRgb parser = new PixelParserRgb(bhi, COLOR_TABLE, 
imageData);
+        assertEquals(0xFFFF0000, parser.getNextRgb()); // 2 -> Red
+        assertEquals(0xFF00FF00, parser.getNextRgb()); // 3 -> Green
+    }
+
+    @Test
+    public void testGetNextRgb_8Bit() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        final byte[] imageData = { 2 };
+        final PixelParserRgb parser = new PixelParserRgb(bhi, COLOR_TABLE, 
imageData);
+        assertEquals(0xFFFF0000, parser.getNextRgb()); // 2 -> Red
+    }
+
+    @Test
+    public void testGetNextRgb_16Bit() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // RGB 555: Pure Blue = 0b0_00000_00000_11111 -> 0x001F
+        // Little Endian: 0x1F, 0x00
+        final byte[] imageData = { 0x1F, 0x00 };
+        final PixelParserRgb parser = new PixelParserRgb(bhi, null, imageData);
+        // blue = (0x1F) << 3 = 248. red=0, green=0.
+        assertEquals(0xFF0000F8, parser.getNextRgb());
+    }
+
+    @Test
+    public void testGetNextRgb_24Bit() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // RGB: 0x112233. Little Endian: 0x33, 0x22, 0x11
+        final byte[] imageData = { 0x33, 0x22, 0x11 };
+        final PixelParserRgb parser = new PixelParserRgb(bhi, null, imageData);
+        assertEquals(0xFF112233, parser.getNextRgb());
+    }
+
+    @Test
+    public void testGetNextRgb_32Bit() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // RGB: 0x112233. Little Endian: 0x33, 0x22, 0x11, 0x00
+        final byte[] imageData = { 0x33, 0x22, 0x11, 0x00 };
+        final PixelParserRgb parser = new PixelParserRgb(bhi, null, imageData);
+        assertEquals(0xFF112233, parser.getNextRgb());
+    }
+
+    @Test
+    public void testNewlinePadding() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 1, 1, 1, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // 24 bits = 3 bytes. 1 byte padding needed.
+        final byte[] imageData = { 0x33, 0x22, 0x11, 0x55, (byte) 0xFF };
+        final PixelParserRgb parser = new PixelParserRgb(bhi, null, imageData);
+        parser.getNextRgb(); // consumes 3 bytes
+        parser.newline(); // should consume 1 more byte (0x55)
+        // Next read should fail with ArrayIndexOutOfBoundsException or 
IOException
+        // Case 24 uses imageData[byteCount + 0], which throws AIOOBE if 
byteCount is at the end.
+        assertThrows(RuntimeException.class, parser::getNextRgb);
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/bmp/PixelParserRleTest.java 
b/src/test/java/org/apache/commons/imaging/formats/bmp/PixelParserRleTest.java
new file mode 100644
index 00000000..8eaa79a2
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/imaging/formats/bmp/PixelParserRleTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.formats.bmp;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+
+import org.apache.commons.imaging.common.ImageBuilder;
+import org.junit.jupiter.api.Test;
+
+public class PixelParserRleTest {
+
+    private static final byte[] COLOR_TABLE = { (byte) 0x00, (byte) 0x00, 
(byte) 0x00, (byte) 0xFF, // 0: Black
+            (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, // 1: White
+            (byte) 0x00, (byte) 0x00, (byte) 0xFF, (byte) 0xFF, // 2: Red
+            (byte) 0x00, (byte) 0xFF, (byte) 0x00, (byte) 0xFF, // 3: Green
+    };
+
+    @Test
+    public void testProcessImage_Rle8() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 2, 2, 1, 8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // RLE8 data:
+        // 02 01 -> Encoded mode: repeat color index 1, 2 times
+        // 00 00 -> Escape mode: End of line
+        // 02 02 -> Encoded mode: repeat color index 2, 2 times
+        // 00 01 -> Escape mode: End of file
+        final byte[] imageData = { 0x02, 0x01, 0x00, 0x00, 0x02, 0x02, 0x00, 
0x01 };
+        final PixelParserRle parser = new PixelParserRle(bhi, COLOR_TABLE, 
imageData);
+        final ImageBuilder imageBuilder = new ImageBuilder(2, 2, false);
+        parser.processImage(imageBuilder);
+        // Row 1 (bottom row in BMP, y=1)
+        assertEquals(0xFFFFFFFF, imageBuilder.getRgb(0, 1));
+        assertEquals(0xFFFFFFFF, imageBuilder.getRgb(1, 1));
+        // Row 0 (y=0)
+        assertEquals(0xFFFF0000, imageBuilder.getRgb(0, 0));
+        assertEquals(0xFFFF0000, imageBuilder.getRgb(1, 0));
+    }
+
+    @Test
+    public void testProcessImage_Rle4() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 4, 1, 1, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // RLE4 data:
+        // 04 23 -> Encoded mode: repeat colors index 2 and 3, 4 times (2, 3, 
2, 3)
+        // 00 01 -> Escape mode: End of file
+        final byte[] imageData = { 0x04, 0x23, 0x00, 0x01 };
+        final PixelParserRle parser = new PixelParserRle(bhi, COLOR_TABLE, 
imageData);
+        final ImageBuilder imageBuilder = new ImageBuilder(4, 1, false);
+        parser.processImage(imageBuilder);
+        assertEquals(0xFFFF0000, imageBuilder.getRgb(0, 0)); // 2
+        assertEquals(0xFF00FF00, imageBuilder.getRgb(1, 0)); // 3
+        assertEquals(0xFFFF0000, imageBuilder.getRgb(2, 0)); // 2
+        assertEquals(0xFF00FF00, imageBuilder.getRgb(3, 0)); // 3
+    }
+
+    @Test
+    public void testProcessImage_AbsoluteMode_Rle8() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 3, 1, 1, 8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // 00 03 01 02 03 00 -> Absolute mode: 3 pixels (1, 2, 3).
+        // 00 03 means absolute mode, 3 pixels.
+        // Next bytes: 01, 02, 03.
+        // Total bytes read must be even, so one padding byte 00.
+        // 00 01 -> End of file.
+        final byte[] imageData = { 0x00, 0x03, 0x01, 0x02, 0x03, 0x00, 0x00, 
0x01 };
+        final PixelParserRle parser = new PixelParserRle(bhi, COLOR_TABLE, 
imageData);
+        final ImageBuilder imageBuilder = new ImageBuilder(3, 1, false);
+        parser.processImage(imageBuilder);
+        assertEquals(0xFFFFFFFF, imageBuilder.getRgb(0, 0)); // 1
+        assertEquals(0xFFFF0000, imageBuilder.getRgb(1, 0)); // 2
+        assertEquals(0xFF00FF00, imageBuilder.getRgb(2, 0)); // 3
+    }
+
+    @Test
+    public void testProcessImage_DeltaMode() throws IOException {
+        final BmpHeaderInfo bhi = new BmpHeaderInfo((byte) 0, (byte) 0, 0, 0, 
0, 0, 4, 4, 1, 8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, null, 0, 0, 0, 0, 0, 0, 0);
+        // 00 02 01 01 -> Delta mode: move x+1, y+1 (y is bottom-up, so move 
up/left relative to start)
+        // Initial x=0, y=3.
+        // Delta 1, 1 -> x=1, y=2.
+        // 01 02 -> Encoded mode: repeat color 2, 1 time.
+        // 00 01 -> EOF.
+        final byte[] imageData = { 0x00, 0x02, 0x01, 0x01, 0x01, 0x02, 0x00, 
0x01 };
+        final PixelParserRle parser = new PixelParserRle(bhi, COLOR_TABLE, 
imageData);
+        final ImageBuilder imageBuilder = new ImageBuilder(4, 4, false);
+        parser.processImage(imageBuilder);
+        assertEquals(0xFFFF0000, imageBuilder.getRgb(1, 2));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/jpeg/exif/ExifRewriterTest.java
 
b/src/test/java/org/apache/commons/imaging/formats/jpeg/exif/ExifRewriterTest.java
new file mode 100644
index 00000000..a39070b1
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/imaging/formats/jpeg/exif/ExifRewriterTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.formats.jpeg.exif;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+
+import org.apache.commons.imaging.Imaging;
+import org.apache.commons.imaging.ImagingOverflowException;
+import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
+import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
+import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
+import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
+import org.junit.jupiter.api.Test;
+
+public class ExifRewriterTest extends AbstractExifTest {
+
+    @Test
+    public void testRemoveExifMetadata_ByteArray() throws Exception {
+        final File imageFile = getImageWithExifData();
+        final byte[] imageBytes = Files.readAllBytes(imageFile.toPath());
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        new ExifRewriter().removeExifMetadata(imageBytes, os);
+        final byte[] outBytes = os.toByteArray();
+        assertNotNull(outBytes);
+        assertTrue(outBytes.length > 0);
+        assertFalse(hasExifData("test.jpg", outBytes));
+    }
+
+    @Test
+    public void testRemoveExifMetadata_File() throws Exception {
+        final File imageFile = getImageWithExifData();
+        final File tempFile = Files.createTempFile("test", ".jpg").toFile();
+        try (OutputStream os = new BufferedOutputStream(new 
FileOutputStream(tempFile))) {
+            new ExifRewriter().removeExifMetadata(imageFile, os);
+        }
+        assertTrue(tempFile.exists());
+        assertFalse(hasExifData(tempFile));
+        Files.delete(tempFile.toPath());
+    }
+
+    @Test
+    public void testRemoveExifMetadata_InputStream() throws Exception {
+        final File imageFile = getImageWithExifData();
+        try (InputStream is = Files.newInputStream(imageFile.toPath())) {
+            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+            new ExifRewriter().removeExifMetadata(is, os);
+            final byte[] outBytes = os.toByteArray();
+            assertNotNull(outBytes);
+            assertFalse(hasExifData("test.jpg", outBytes));
+        }
+    }
+
+    @Test
+    public void testUpdateExifMetadataLossless_ByteArray() throws Exception {
+        final File imageFile = getImageWithExifData();
+        final byte[] imageBytes = Files.readAllBytes(imageFile.toPath());
+        final JpegImageMetadata metadata = (JpegImageMetadata) 
Imaging.getMetadata(imageFile);
+        final TiffOutputSet outputSet = metadata.getExif().getOutputSet();
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        new ExifRewriter().updateExifMetadataLossless(imageBytes, os, 
outputSet);
+        final byte[] outBytes = os.toByteArray();
+        assertNotNull(outBytes);
+        assertTrue(hasExifData("test.jpg", outBytes));
+    }
+
+    @Test
+    public void testUpdateExifMetadataLossless_File() throws Exception {
+        final File imageFile = getImageWithExifData();
+        final JpegImageMetadata metadata = (JpegImageMetadata) 
Imaging.getMetadata(imageFile);
+        final TiffOutputSet outputSet = metadata.getExif().getOutputSet();
+        final File tempFile = Files.createTempFile("test", ".jpg").toFile();
+        try (OutputStream os = new BufferedOutputStream(new 
FileOutputStream(tempFile))) {
+            new ExifRewriter().updateExifMetadataLossless(imageFile, os, 
outputSet);
+        }
+        assertTrue(tempFile.exists());
+        assertTrue(hasExifData(tempFile));
+        Files.delete(tempFile.toPath());
+    }
+
+    @Test
+    public void testUpdateExifMetadataLossless_InputStream() throws Exception {
+        final File imageFile = getImageWithExifData();
+        final JpegImageMetadata metadata = (JpegImageMetadata) 
Imaging.getMetadata(imageFile);
+        final TiffOutputSet outputSet = metadata.getExif().getOutputSet();
+        try (InputStream is = Files.newInputStream(imageFile.toPath())) {
+            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+            new ExifRewriter().updateExifMetadataLossless(is, os, outputSet);
+            final byte[] outBytes = os.toByteArray();
+            assertNotNull(outBytes);
+            assertTrue(hasExifData("test.jpg", outBytes));
+        }
+    }
+
+    @Test
+    public void testUpdateExifMetadataLossy_ByteArray() throws Exception {
+        final File imageFile = getImageWithExifData();
+        final byte[] imageBytes = Files.readAllBytes(imageFile.toPath());
+        final JpegImageMetadata metadata = (JpegImageMetadata) 
Imaging.getMetadata(imageFile);
+        final TiffOutputSet outputSet = metadata.getExif().getOutputSet();
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        new ExifRewriter().updateExifMetadataLossy(imageBytes, os, outputSet);
+        final byte[] outBytes = os.toByteArray();
+        assertNotNull(outBytes);
+        assertTrue(hasExifData("test.jpg", outBytes));
+    }
+
+    @Test
+    public void testUpdateExifMetadataLossy_File() throws Exception {
+        final File imageFile = getImageWithExifData();
+        final JpegImageMetadata metadata = (JpegImageMetadata) 
Imaging.getMetadata(imageFile);
+        final TiffOutputSet outputSet = metadata.getExif().getOutputSet();
+        final File tempFile = Files.createTempFile("test", ".jpg").toFile();
+        try (OutputStream os = new BufferedOutputStream(new 
FileOutputStream(tempFile))) {
+            new ExifRewriter().updateExifMetadataLossy(imageFile, os, 
outputSet);
+        }
+        assertTrue(tempFile.exists());
+        assertTrue(hasExifData(tempFile));
+        Files.delete(tempFile.toPath());
+    }
+
+    @Test
+    public void testUpdateExifMetadataLossy_InputStream() throws Exception {
+        final File imageFile = getImageWithExifData();
+        final JpegImageMetadata metadata = (JpegImageMetadata) 
Imaging.getMetadata(imageFile);
+        final TiffOutputSet outputSet = metadata.getExif().getOutputSet();
+        try (InputStream is = Files.newInputStream(imageFile.toPath())) {
+            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+            new ExifRewriter().updateExifMetadataLossy(is, os, outputSet);
+            final byte[] outBytes = os.toByteArray();
+            assertNotNull(outBytes);
+            assertTrue(hasExifData("test.jpg", outBytes));
+        }
+    }
+
+    @Test
+    public void testAddExifToImageWithoutExif() throws Exception {
+        // Find a JPEG image without EXIF
+        final File imageFile = getTestImage(file -> 
file.getName().toLowerCase().endsWith(".jpg") && !hasExifData(file));
+        assertNotNull(imageFile);
+        final TiffOutputSet outputSet = new TiffOutputSet();
+        final TiffOutputDirectory root = outputSet.getOrCreateRootDirectory();
+        root.add(ExifTagConstants.EXIF_TAG_EXIF_VERSION, (byte) 1, (byte) 2, 
(byte) 3, (byte) 4);
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        new ExifRewriter().updateExifMetadataLossy(imageFile, os, outputSet);
+        final byte[] outBytes = os.toByteArray();
+        assertTrue(hasExifData("test.jpg", outBytes));
+    }
+
+    @Test
+    public void testAddExifToImageWithJfif() throws Exception {
+        // Create a minimal JPEG with SOI and JFIF (APP0) and SOS
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        baos.write(0xFF);
+        baos.write(0xD8); // SOI
+        baos.write(0xFF);
+        baos.write(0xE0); // APP0 (JFIF)
+        baos.write(0x00);
+        baos.write(0x10); // Length (16)
+        baos.write("JFIF".getBytes());
+        baos.write(0); // Version
+        baos.write(new byte[9]); // Rest of JFIF
+        baos.write(0xFF);
+        baos.write(0xDA); // SOS
+        baos.write(0x00);
+        baos.write(0x0C); // Length
+        baos.write(new byte[10]); // SOS data
+        baos.write(0xFF);
+        baos.write(0xD9); // EOI
+        final byte[] imageBytes = baos.toByteArray();
+        final TiffOutputSet outputSet = new TiffOutputSet();
+        outputSet.getOrCreateRootDirectory();
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        new ExifRewriter().updateExifMetadataLossy(imageBytes, os, outputSet);
+        final byte[] outBytes = os.toByteArray();
+        assertTrue(hasExifData("test.jpg", outBytes));
+        // Check that EXIF (APP1) is AFTER JFIF (APP0)
+        // SOI (2) + JFIF (16+2=18) = 20
+        // Next marker at index 20 should be APP1 (0xFFE1)
+        assertEquals((byte) 0xFF, outBytes[20]);
+        assertEquals((byte) 0xE1, outBytes[21]);
+    }
+
+    @Test
+    public void testImagingOverflowException() throws Exception {
+        final File imageFile = getImageWithExifData();
+        final TiffOutputSet outputSet = new TiffOutputSet();
+        final TiffOutputDirectory root = outputSet.getOrCreateRootDirectory();
+        // Add a very large field to exceed 64KB limit of APP1 segment
+        final byte[] largeData = new byte[70000];
+        root.add(ExifTagConstants.EXIF_TAG_MAKER_NOTE, largeData);
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        assertThrows(ImagingOverflowException.class, () -> {
+            new ExifRewriter().updateExifMetadataLossy(imageFile, os, 
outputSet);
+        });
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/png/PngMultipleRoundtripTest.java
 
b/src/test/java/org/apache/commons/imaging/formats/png/PngMultipleRoundtripTest.java
index 1aa98927..09a7904d 100644
--- 
a/src/test/java/org/apache/commons/imaging/formats/png/PngMultipleRoundtripTest.java
+++ 
b/src/test/java/org/apache/commons/imaging/formats/png/PngMultipleRoundtripTest.java
@@ -42,10 +42,7 @@ class PngMultipleRoundtripTest extends AbstractPngTest {
         final File[] files = imagesFolder.listFiles();
         for (final File file : files) {
             final File imageFile = file;
-            if (!imageFile.isFile()) {
-                continue;
-            }
-            if (!imageFile.getName().toLowerCase().endsWith(".png")) {
+            if (!imageFile.isFile() || 
!imageFile.getName().toLowerCase().endsWith(".png")) {
                 continue;
             }
 
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/rgbe/RgbeParserTest.java 
b/src/test/java/org/apache/commons/imaging/formats/rgbe/RgbeParserTest.java
new file mode 100644
index 00000000..59f23cbc
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/formats/rgbe/RgbeParserTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.formats.rgbe;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.awt.Dimension;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.imaging.ImageFormat;
+import org.apache.commons.imaging.ImageFormats;
+import org.apache.commons.imaging.ImageInfo;
+import org.apache.commons.imaging.ImagingException;
+import org.apache.commons.imaging.bytesource.ByteSource;
+import org.apache.commons.imaging.common.ImageMetadata;
+import org.junit.jupiter.api.Test;
+
+public class RgbeParserTest {
+
+    @Test
+    public void testRgbeImageParserBasics() {
+        final RgbeImageParser parser = new RgbeImageParser();
+        assertEquals("Radiance HDR", parser.getName());
+        assertEquals("hdr", parser.getDefaultExtension());
+        final String[] extensions = parser.getAcceptedExtensions();
+        assertTrue(extensions.length > 0);
+        final ImageFormat[] types = parser.getAcceptedTypes();
+        assertEquals(1, types.length);
+        assertEquals(ImageFormats.RGBE, types[0]);
+        assertNotNull(parser.getDefaultParameters());
+    }
+
+    @Test
+    public void testRgbeImagingParameters() {
+        final RgbeImagingParameters params = new RgbeImagingParameters();
+        assertNotNull(params);
+    }
+
+    @Test
+    public void testGetIccProfileBytes() throws IOException {
+        final RgbeImageParser parser = new RgbeImageParser();
+        final byte[] bytes = "#?RADIANCE\n\n-Y 1 +X 
1\n".getBytes(StandardCharsets.US_ASCII);
+        assertNull(parser.getIccProfileBytes(ByteSource.array(bytes), new 
RgbeImagingParameters()));
+    }
+
+    @Test
+    public void testGetMetadata() throws IOException {
+        final RgbeImageParser parser = new RgbeImageParser();
+        final String header = 
"#?RADIANCE\nFORMAT=32-bit_rle_rgbe\nEXPOSURE=1.0\n\n-Y 1 +X 1\n";
+        final byte[] bytes = header.getBytes(StandardCharsets.US_ASCII);
+        final ImageMetadata metadata = 
parser.getMetadata(ByteSource.array(bytes), new RgbeImagingParameters());
+        assertNotNull(metadata);
+        assertEquals(2, metadata.getItems().size());
+    }
+
+    @Test
+    public void testGetImageInfo() throws IOException {
+        final RgbeImageParser parser = new RgbeImageParser();
+        final String header = "#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n-Y 10 +X 
20\n";
+        final byte[] bytes = header.getBytes(StandardCharsets.US_ASCII);
+        final ImageInfo info = parser.getImageInfo(ByteSource.array(bytes), 
new RgbeImagingParameters());
+        assertNotNull(info);
+        assertEquals(20, info.getWidth());
+        assertEquals(10, info.getHeight());
+        assertEquals(ImageFormats.RGBE, info.getFormat());
+    }
+
+    @Test
+    public void testGetImageSize() throws IOException {
+        final RgbeImageParser parser = new RgbeImageParser();
+        final String header = "#?RADIANCE\n\n-Y 10 +X 20\n";
+        final byte[] bytes = header.getBytes(StandardCharsets.US_ASCII);
+        final Dimension size = parser.getImageSize(ByteSource.array(bytes), 
new RgbeImagingParameters());
+        assertEquals(20, size.width);
+        assertEquals(10, size.height);
+    }
+
+    @Test
+    public void testInvalidHeader() {
+        final RgbeImageParser parser = new RgbeImageParser();
+        final byte[] bytes = "INVALID".getBytes(StandardCharsets.US_ASCII);
+        assertThrows(ImagingException.class, () -> 
parser.getImageSize(ByteSource.array(bytes), new RgbeImagingParameters()));
+    }
+
+    @Test
+    public void testInvalidResolutionString() {
+        final RgbeImageParser parser = new RgbeImageParser();
+        final String header = "#?RADIANCE\n\nINVALID_RESOLUTION\n";
+        final byte[] bytes = header.getBytes(StandardCharsets.US_ASCII);
+        assertThrows(ImagingException.class, () -> 
parser.getImageSize(ByteSource.array(bytes), new RgbeImagingParameters()));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffClassesTest.java 
b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffClassesTest.java
new file mode 100644
index 00000000..77ae92b0
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffClassesTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.formats.tiff;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.imaging.ImagingException;
+import 
org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryConstants;
+import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
+import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldTypeShort;
+import org.junit.jupiter.api.Test;
+
+public class TiffClassesTest {
+
+    @Test
+    public void testTiffHeader() {
+        final ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN;
+        final int tiffVersion = 42;
+        final long offsetToFirstIFD = 8;
+        final boolean bigTiff = false;
+        final TiffHeader header = new TiffHeader(byteOrder, tiffVersion, 
offsetToFirstIFD, bigTiff);
+        assertEquals(byteOrder, header.byteOrder);
+        assertEquals(tiffVersion, header.tiffVersion);
+        assertEquals(offsetToFirstIFD, header.offsetToFirstIFD);
+        assertEquals(bigTiff, header.bigTiff);
+        assertEquals("TIFF Header", header.getElementDescription());
+        assertEquals(0, header.offset);
+    }
+
+    @Test
+    public void testTiffContents() throws ImagingException {
+        final TiffHeader header = new TiffHeader(ByteOrder.LITTLE_ENDIAN, 42, 
8, false);
+        final List<TiffDirectory> directories = new ArrayList<>();
+        final List<TiffField> fields = new ArrayList<>();
+        final TiffContents contents = new TiffContents(header, directories, 
fields);
+        assertEquals(header, contents.header);
+        assertEquals(0, contents.directories.size());
+        assertEquals(0, contents.tiffFields.size());
+        assertNull(contents.findField(TiffTagConstants.TIFF_TAG_COMPRESSION));
+        assertNotNull(contents.getElements());
+        assertEquals(1, contents.getElements().size()); // Only header
+        // Test dissect (should not throw exception)
+        contents.dissect();
+    }
+
+    @Test
+    public void testTiffRasterDataType() {
+        assertEquals(TiffRasterDataType.INTEGER, 
TiffRasterDataType.valueOf("INTEGER"));
+        assertEquals(TiffRasterDataType.FLOAT, 
TiffRasterDataType.valueOf("FLOAT"));
+        assertEquals(2, TiffRasterDataType.values().length);
+    }
+
+    @Test
+    public void testTiffRasterStatistics() {
+        final float[] data = { 1.0f, 2.0f, Float.NaN, 3.0f, 4.0f, 5.0f };
+        final TiffRasterDataFloat raster = new TiffRasterDataFloat(2, 3, data);
+        final TiffRasterStatistics stats = new TiffRasterStatistics(raster, 
5.0f);
+        assertEquals(1, stats.getCountOfNulls());
+        assertEquals(4, stats.getCountOfSamples());
+        assertEquals(5.0f, stats.getExcludedValue());
+        assertEquals(4.0f, stats.getMaxValue());
+        assertEquals(1.0f, stats.getMinValue());
+        assertEquals(2.5f, stats.getMeanValue(), 0.001f);
+        assertTrue(stats.isAnExcludedValueSet());
+        final TiffRasterStatistics statsNoExcluded = new 
TiffRasterStatistics(raster, Float.NaN);
+        assertEquals(5, statsNoExcluded.getCountOfSamples());
+        assertFalse(statsNoExcluded.isAnExcludedValueSet());
+    }
+
+    @Test
+    public void testTiffRasterStatisticsAllNaN() {
+        final float[] data = { Float.NaN, Float.NaN };
+        final TiffRasterDataFloat raster = new TiffRasterDataFloat(1, 2, data);
+        final TiffRasterStatistics stats = new TiffRasterStatistics(raster, 
Float.NaN);
+        assertEquals(2, stats.getCountOfNulls());
+        assertEquals(0, stats.getCountOfSamples());
+        assertEquals(0.0f, stats.getMeanValue());
+        assertEquals(Float.POSITIVE_INFINITY, stats.getMinValue());
+        assertEquals(Float.NEGATIVE_INFINITY, stats.getMaxValue());
+    }
+
+    @Test
+    public void testTiffTags() {
+        
assertNotNull(TiffTags.getTag(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT, 
TiffTagConstants.TIFF_TAG_COMPRESSION.tag));
+        assertEquals(TiffTagConstants.TIFF_TAG_COMPRESSION.name,
+                TiffTags.getTag(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT, 
TiffTagConstants.TIFF_TAG_COMPRESSION.tag).name);
+        // Unknown tag
+        assertEquals(TiffTagConstants.TIFF_TAG_UNKNOWN.name, 
TiffTags.getTag(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT, -1).name);
+        
assertNotNull(TiffTags.getTagCount(TiffTagConstants.TIFF_TAG_COMPRESSION.tag));
+    }
+
+    @Test
+    public void testTiffField() throws ImagingException {
+        final FieldTypeShort fieldTypeShort = new FieldTypeShort(3, "Short");
+        final TiffField field = new 
TiffField(TiffTagConstants.TIFF_TAG_COMPRESSION.tag, 
TiffDirectoryConstants.DIRECTORY_TYPE_ROOT, fieldTypeShort, 1, 0,
+                new byte[] { 1, 0 }, ByteOrder.LITTLE_ENDIAN, 0);
+        assertEquals(TiffTagConstants.TIFF_TAG_COMPRESSION.tag, 
field.getTag());
+        assertEquals(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT, 
field.getDirectoryType());
+        assertEquals(fieldTypeShort, field.getFieldType());
+        assertEquals(1, field.getCount());
+        assertEquals(0, field.getOffset());
+        assertEquals(ByteOrder.LITTLE_ENDIAN, field.getByteOrder());
+        assertEquals(1, field.getIntValue());
+        assertNotNull(field.getTagName());
+        assertNotNull(field.getValueDescription());
+        assertNotNull(field.toString());
+    }
+
+    @Test
+    public void testTiffDirectory() {
+        final List<TiffField> entries = new ArrayList<>();
+        final TiffDirectory directory = new 
TiffDirectory(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT, entries, 0, 0, 
ByteOrder.LITTLE_ENDIAN);
+        assertEquals(TiffDirectoryConstants.DIRECTORY_TYPE_ROOT, 
directory.type);
+        assertEquals(0, directory.offset);
+        assertEquals(0, directory.getNextDirectoryOffset());
+        assertEquals(ByteOrder.LITTLE_ENDIAN, directory.getByteOrder());
+        assertNotNull(directory.getElementDescription());
+        assertEquals(0, directory.size());
+        assertFalse(directory.iterator().hasNext());
+    }
+
+    @Test
+    public void testTiffImagingParameters() {
+        final TiffImagingParameters params = new TiffImagingParameters();
+        assertNull(params.getCompression());
+        params.setCompression(TiffTagConstants.COMPRESSION_VALUE_LZW);
+        assertEquals(TiffTagConstants.COMPRESSION_VALUE_LZW, 
params.getCompression());
+        assertFalse(params.isSubImageSet());
+        params.setSubImage(1, 2, 3, 4);
+        assertTrue(params.isSubImageSet());
+        assertEquals(1, params.getSubImageX());
+        assertEquals(2, params.getSubImageY());
+        assertEquals(3, params.getSubImageWidth());
+        assertEquals(4, params.getSubImageHeight());
+        params.clearSubImage();
+        assertFalse(params.isSubImageSet());
+    }
+
+    @Test
+    public void testTiffRasterDataFloat() {
+        final TiffRasterDataFloat raster = new TiffRasterDataFloat(2, 2);
+        assertEquals(2, raster.getWidth());
+        assertEquals(2, raster.getHeight());
+        assertEquals(1, raster.getSamplesPerPixel());
+        assertEquals(TiffRasterDataType.FLOAT, raster.getDataType());
+        raster.setValue(0, 0, 1.0f);
+        assertEquals(1.0f, raster.getValue(0, 0));
+        assertEquals(1, raster.getIntValue(0, 0));
+        raster.setIntValue(1, 1, 42);
+        assertEquals(42.0f, raster.getValue(1, 1));
+        assertEquals(42, raster.getIntValue(1, 1));
+        final float[] data = raster.getData();
+        assertEquals(4, data.length);
+        assertEquals(1.0f, data[0]);
+        assertEquals(42.0f, data[3]);
+        assertNotNull(raster.getIntData());
+        assertNotNull(raster.getSimpleStatistics());
+    }
+
+    @Test
+    public void testTiffRasterDataInt() {
+        final TiffRasterDataInt raster = new TiffRasterDataInt(2, 2);
+        assertEquals(2, raster.getWidth());
+        assertEquals(2, raster.getHeight());
+        assertEquals(1, raster.getSamplesPerPixel());
+        assertEquals(TiffRasterDataType.INTEGER, raster.getDataType());
+        raster.setIntValue(0, 0, 10);
+        assertEquals(10, raster.getIntValue(0, 0));
+        assertEquals(10.0f, raster.getValue(0, 0));
+        raster.setValue(1, 1, 20.5f);
+        assertEquals(20, raster.getIntValue(1, 1)); // Truncated
+        assertEquals(20.0f, raster.getValue(1, 1));
+        final int[] intData = raster.getIntData();
+        assertEquals(4, intData.length);
+        assertEquals(10, intData[0]);
+        assertEquals(20, intData[3]);
+        assertNotNull(raster.getData());
+        assertNotNull(raster.getSimpleStatistics());
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffTagIntegrityTest.java
 
b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffTagIntegrityTest.java
index 943ee4ed..313983ed 100644
--- 
a/src/test/java/org/apache/commons/imaging/formats/tiff/TiffTagIntegrityTest.java
+++ 
b/src/test/java/org/apache/commons/imaging/formats/tiff/TiffTagIntegrityTest.java
@@ -80,10 +80,7 @@ class TiffTagIntegrityTest extends AbstractImagingTest {
                 obj = field.get(null);
             } catch (final IllegalAccessException illegalAccess) {
             }
-            if (obj == null) {
-                continue;
-            }
-            if (!(obj instanceof TagInfo)) {
+            if (obj == null || !(obj instanceof TagInfo)) {
                 continue;
             }
             final TagInfo src = (TagInfo) obj;
diff --git 
a/src/test/java/org/apache/commons/imaging/formats/xpm/XpmParserTest.java 
b/src/test/java/org/apache/commons/imaging/formats/xpm/XpmParserTest.java
new file mode 100644
index 00000000..87f04d79
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/formats/xpm/XpmParserTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.formats.xpm;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.junit.jupiter.api.Test;
+
+public class XpmParserTest {
+
+    @Test
+    public void testXpmHeader() throws Exception {
+        // XpmHeader is private static final, use reflection to test it
+        final Class<?> headerClass = 
Class.forName("org.apache.commons.imaging.formats.xpm.XpmImageParser$XpmHeader");
+        final Constructor<?> constructor = 
headerClass.getDeclaredConstructor(int.class, int.class, int.class, int.class, 
int.class, int.class, boolean.class);
+        constructor.setAccessible(true);
+        final Object header = constructor.newInstance(10, 20, 2, 1, 5, 5, 
true);
+        final StringWriter sw = new StringWriter();
+        final PrintWriter pw = new PrintWriter(sw);
+        final Method dumpMethod = headerClass.getMethod("dump", 
PrintWriter.class);
+        dumpMethod.invoke(header, pw);
+        pw.flush();
+        final String output = sw.toString();
+        assertNotNull(output);
+        // Using contains because exact line endings might vary by OS
+        assertTrue(output.contains("Width: 10"));
+        assertTrue(output.contains("Height: 20"));
+        assertTrue(output.contains("NumColors: 2"));
+        assertTrue(output.contains("NumCharsPerPixel: 1"));
+        assertTrue(output.contains("X hotspot: 5"));
+        assertTrue(output.contains("Y hotspot: 5"));
+        assertTrue(output.contains("XpmExt: true"));
+    }
+
+    @Test
+    public void testPaletteEntry() throws Exception {
+        final Class<?> entryClass = 
Class.forName("org.apache.commons.imaging.formats.xpm.XpmImageParser$PaletteEntry");
+        final Constructor<?> constructor = entryClass.getDeclaredConstructor();
+        constructor.setAccessible(true);
+        final Object entry = constructor.newInstance();
+        final Method getBestArgbMethod = 
entryClass.getDeclaredMethod("getBestArgb");
+        getBestArgbMethod.setAccessible(true);
+        final Field colorArgbField = entryClass.getDeclaredField("colorArgb");
+        colorArgbField.setAccessible(true);
+        final Field haveColorField = entryClass.getDeclaredField("haveColor");
+        haveColorField.setAccessible(true);
+        final Field grayArgbField = entryClass.getDeclaredField("grayArgb");
+        grayArgbField.setAccessible(true);
+        final Field haveGrayField = entryClass.getDeclaredField("haveGray");
+        haveGrayField.setAccessible(true);
+        final Field gray4LevelArgbField = 
entryClass.getDeclaredField("gray4LevelArgb");
+        gray4LevelArgbField.setAccessible(true);
+        final Field haveGray4LevelField = 
entryClass.getDeclaredField("haveGray4Level");
+        haveGray4LevelField.setAccessible(true);
+        final Field monoArgbField = entryClass.getDeclaredField("monoArgb");
+        monoArgbField.setAccessible(true);
+        final Field haveMonoField = entryClass.getDeclaredField("haveMono");
+        haveMonoField.setAccessible(true);
+        // Default
+        assertEquals(0, getBestArgbMethod.invoke(entry));
+        // Color
+        colorArgbField.set(entry, 0xFF112233);
+        haveColorField.set(entry, true);
+        assertEquals(0xFF112233, getBestArgbMethod.invoke(entry));
+        // Gray (should still return color if color is present)
+        grayArgbField.set(entry, 0xFF444444);
+        haveGrayField.set(entry, true);
+        assertEquals(0xFF112233, getBestArgbMethod.invoke(entry));
+        // Remove color, should return gray
+        haveColorField.set(entry, false);
+        assertEquals(0xFF444444, getBestArgbMethod.invoke(entry));
+        // Gray4Level
+        gray4LevelArgbField.set(entry, 0xFF555555);
+        haveGray4LevelField.set(entry, true);
+        haveGrayField.set(entry, false);
+        assertEquals(0xFF555555, getBestArgbMethod.invoke(entry));
+        // Mono
+        monoArgbField.set(entry, 0xFF666666);
+        haveMonoField.set(entry, true);
+        haveGray4LevelField.set(entry, false);
+        assertEquals(0xFF666666, getBestArgbMethod.invoke(entry));
+    }
+
+    @Test
+    public void testXpmImagingParameters() {
+        final XpmImagingParameters params = new XpmImagingParameters();
+        assertNotNull(params);
+    }
+
+    @Test
+    public void testXpmImageParserBasics() {
+        final XpmImageParser parser = new XpmImageParser();
+        assertEquals("X PixMap", parser.getName());
+        assertEquals("xpm", parser.getDefaultExtension());
+    }
+
+    @Test
+    public void testParseColor() throws Exception {
+        final XpmImageParser parser = new XpmImageParser();
+        final Method parseColorMethod = 
XpmImageParser.class.getDeclaredMethod("parseColor", String.class);
+        parseColorMethod.setAccessible(true);
+        assertEquals(0xFF000000, parseColorMethod.invoke(parser, "#000000"));
+        assertEquals(0xFFFFFFFF, parseColorMethod.invoke(parser, "#FFFFFF"));
+        assertEquals(0x00000000, parseColorMethod.invoke(parser, "None"));
+        // Test #RGB (4-bit per channel expanded to 8-bit)
+        final int rgb = (int) parseColorMethod.invoke(parser, "#FFF");
+        // red = 0xF << 20 = 0xF00000, green = 0xF << 12 = 0x00F000, blue = 
0xF << 4 = 0x0000F0
+        // result = 0xFF000000 | 0xF00000 | 0x00F000 | 0x0000F0 = 0xFFF0F0F0
+        assertEquals(0xFFF0F0F0, rgb);
+        // Test names
+        assertEquals(0xFF0000FF, parseColorMethod.invoke(parser, "blue"));
+        assertEquals(0xFF0000FF, parseColorMethod.invoke(parser, "BLUE"));
+        // Unknown color name
+        assertEquals(0x00000000, parseColorMethod.invoke(parser, 
"nonexistent_color_name"));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/palette/ColorComponentTest.java 
b/src/test/java/org/apache/commons/imaging/palette/ColorComponentTest.java
new file mode 100644
index 00000000..6d849f01
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/palette/ColorComponentTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.palette;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+public class ColorComponentTest {
+
+    @Test
+    public void testArgbComponent() {
+        final int argb = 0x11223344;
+        assertEquals(0x11, ColorComponent.ALPHA.argbComponent(argb));
+        assertEquals(0x22, ColorComponent.RED.argbComponent(argb));
+        assertEquals(0x33, ColorComponent.GREEN.argbComponent(argb));
+        assertEquals(0x44, ColorComponent.BLUE.argbComponent(argb));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/palette/ColorCountTest.java 
b/src/test/java/org/apache/commons/imaging/palette/ColorCountTest.java
new file mode 100644
index 00000000..215226cb
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/palette/ColorCountTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.palette;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.junit.jupiter.api.Test;
+
+public class ColorCountTest {
+
+    @Test
+    public void testColorCount() {
+        final int argb = 0x11223344;
+        final ColorCount cc = new ColorCount(argb);
+        assertEquals(argb, cc.argb);
+        assertEquals(0x11, cc.alpha);
+        assertEquals(0x22, cc.red);
+        assertEquals(0x33, cc.green);
+        assertEquals(0x44, cc.blue);
+        cc.count = 5;
+        assertEquals(5, cc.count);
+    }
+
+    @Test
+    public void testEqualsAndHashCode() {
+        final ColorCount cc1 = new ColorCount(0xFF112233);
+        final ColorCount cc2 = new ColorCount(0xFF112233);
+        final ColorCount cc3 = new ColorCount(0xFF445566);
+        assertEquals(cc1, cc2);
+        assertNotEquals(cc1, cc3);
+        assertEquals(cc1.hashCode(), cc2.hashCode());
+        assertNotEquals(cc1.hashCode(), cc3.hashCode());
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/palette/ColorGroupCutTest.java 
b/src/test/java/org/apache/commons/imaging/palette/ColorGroupCutTest.java
new file mode 100644
index 00000000..cc8d3d32
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/palette/ColorGroupCutTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.palette;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.imaging.ImagingException;
+import org.junit.jupiter.api.Test;
+
+public class ColorGroupCutTest {
+
+    @Test
+    public void testGetColorGroup() throws ImagingException {
+        final List<ColorCount> lessCounts = new ArrayList<>();
+        lessCounts.add(new ColorCount(0xFF000000));
+        final ColorGroup less = new ColorGroup(lessCounts, false);
+        final List<ColorCount> moreCounts = new ArrayList<>();
+        moreCounts.add(new ColorCount(0xFFFFFFFF));
+        final ColorGroup more = new ColorGroup(moreCounts, false);
+        final ColorGroupCut cut = new ColorGroupCut(less, more, 
ColorComponent.RED, 127);
+        assertEquals(127, cut.limit);
+        assertSame(ColorComponent.RED, cut.mode);
+        assertSame(less, cut.less);
+        assertSame(more, cut.more);
+        // Red is 0, 0 <= 127 -> less
+        assertSame(less, cut.getColorGroup(0xFF000000));
+        // Red is 127, 127 <= 127 -> less
+        assertSame(less, cut.getColorGroup(0xFF7F0000));
+        // Red is 128, 128 > 127 -> more
+        assertSame(more, cut.getColorGroup(0xFF800000));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/palette/ColorGroupTest.java 
b/src/test/java/org/apache/commons/imaging/palette/ColorGroupTest.java
new file mode 100644
index 00000000..58381cae
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/palette/ColorGroupTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.palette;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.imaging.ImagingException;
+import org.junit.jupiter.api.Test;
+
+public class ColorGroupTest {
+
+    @Test
+    public void testConstructor() throws ImagingException {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        final ColorCount cc1 = new ColorCount(0x01020304);
+        cc1.count = 10;
+        colorCounts.add(cc1);
+        final ColorCount cc2 = new ColorCount(0x10203040);
+        cc2.count = 20;
+        colorCounts.add(cc2);
+        final ColorGroup group = new ColorGroup(colorCounts, false);
+        assertEquals(30, group.totalPoints);
+        assertEquals(0x01, group.minAlpha);
+        assertEquals(0x10, group.maxAlpha);
+        assertEquals(0x02, group.minRed);
+        assertEquals(0x20, group.maxRed);
+        assertEquals(0x03, group.minGreen);
+        assertEquals(0x30, group.maxGreen);
+        assertEquals(0x04, group.minBlue);
+        assertEquals(0x40, group.maxBlue);
+        assertEquals(0x10 - 0x01, group.alphaDiff);
+        assertEquals(0x20 - 0x02, group.redDiff);
+        assertEquals(0x30 - 0x03, group.greenDiff);
+        assertEquals(0x40 - 0x04, group.blueDiff);
+        // maxDiff = max(max(alphaDiff, redDiff), max(greenDiff, blueDiff))
+        // alphaDiff = 15, redDiff = 30, greenDiff = 45, blueDiff = 60
+        assertEquals(60, group.maxDiff);
+        assertEquals(15 + 30 + 45 + 60, group.diffTotal);
+    }
+
+    @Test
+    public void testConstructorWithIgnoreAlpha() throws ImagingException {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        final ColorCount cc1 = new ColorCount(0xFF000000);
+        cc1.count = 1;
+        colorCounts.add(cc1);
+        final ColorCount cc2 = new ColorCount(0x00FFFFFF);
+        cc2.count = 1;
+        colorCounts.add(cc2);
+        final ColorGroup group = new ColorGroup(colorCounts, true);
+        // ignoreAlpha affects maxDiff and diffTotal
+        // alphaDiff = 255, redDiff = 255, greenDiff = 255, blueDiff = 255
+        // if ignoreAlpha, maxDiff = max(redDiff, max(greenDiff, blueDiff)) = 
255
+        // diffTotal = 0 + 255 + 255 + 255 = 765
+        assertEquals(255, group.maxDiff);
+        assertEquals(765, group.diffTotal);
+        assertTrue(group.ignoreAlpha);
+    }
+
+    @Test
+    public void testConstructorEmpty() {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        assertThrows(ImagingException.class, () -> new ColorGroup(colorCounts, 
false));
+    }
+
+    @Test
+    public void testContains() throws ImagingException {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        colorCounts.add(new ColorCount(0xFF102030));
+        colorCounts.add(new ColorCount(0xFF405060));
+        final ColorGroup group = new ColorGroup(colorCounts, false);
+        // Inside
+        assertTrue(group.contains(0xFF102030));
+        assertTrue(group.contains(0xFF405060));
+        assertTrue(group.contains(0xFF203040));
+        // Outside Alpha
+        assertFalse(group.contains(0x00102030));
+        // Outside Red
+        assertFalse(group.contains(0xFF0F2030));
+        assertFalse(group.contains(0xFF412030));
+        // Outside Green
+        assertFalse(group.contains(0xFF101F30));
+        assertFalse(group.contains(0xFF105130));
+        // Outside Blue
+        assertFalse(group.contains(0xFF10202F));
+        assertFalse(group.contains(0xFF102061));
+    }
+
+    @Test
+    public void testContainsIgnoreAlpha() throws ImagingException {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        colorCounts.add(new ColorCount(0xFF102030));
+        colorCounts.add(new ColorCount(0xFF405060));
+        final ColorGroup group = new ColorGroup(colorCounts, true);
+        // Inside regardless of alpha
+        assertTrue(group.contains(0xFF102030));
+        assertTrue(group.contains(0x00102030));
+        assertTrue(group.contains(0x80203040));
+        // Still outside other components
+        assertFalse(group.contains(0xFF0F2030));
+    }
+
+    @Test
+    public void testGetColorCounts() throws ImagingException {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        colorCounts.add(new ColorCount(0xFF102030));
+        final ColorGroup group = new ColorGroup(colorCounts, false);
+        final List<ColorCount> copy = group.getColorCounts();
+        assertEquals(1, copy.size());
+        assertEquals(colorCounts.get(0).argb, copy.get(0).argb);
+        // Verify it's a copy
+        copy.clear();
+        assertEquals(1, group.getColorCounts().size());
+    }
+
+    @Test
+    public void testGetMedianValue() throws ImagingException {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        // 10 pixels of 0x00000000
+        final ColorCount cc1 = new ColorCount(0x00000000);
+        cc1.count = 10;
+        colorCounts.add(cc1);
+        // 10 pixels of 0xFFFFFFFF
+        final ColorCount cc2 = new ColorCount(0xFFFFFFFF);
+        cc2.count = 10;
+        colorCounts.add(cc2);
+        final ColorGroup group = new ColorGroup(colorCounts, false);
+        // Median should be (0+255)/2 = 127.5, rounded to 128
+        // 0x80808080
+        final int median = group.getMedianValue();
+        assertEquals(0x80808080, median);
+    }
+
+    @Test
+    public void testGetMedianValueIgnoreAlpha() throws ImagingException {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        final ColorCount cc1 = new ColorCount(0x00000000);
+        cc1.count = 10;
+        colorCounts.add(cc1);
+        final ColorGroup group = new ColorGroup(colorCounts, true);
+        // If ignoreAlpha is true, alpha is forced to 0xFF
+        final int median = group.getMedianValue();
+        assertEquals(0xFF000000, median);
+    }
+
+    @Test
+    public void testGetMedianValueWeighted() throws ImagingException {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        // 1 pixel of 0x00000000
+        final ColorCount cc1 = new ColorCount(0x00000000);
+        cc1.count = 1;
+        colorCounts.add(cc1);
+        // 2 pixels of 0x00FFFFFF (R,G,B = 255)
+        final ColorCount cc2 = new ColorCount(0x00FFFFFF);
+        cc2.count = 2;
+        colorCounts.add(cc2);
+        final ColorGroup group = new ColorGroup(colorCounts, false);
+        // total points = 3
+        // red total = 1*0 + 2*255 = 510
+        // red average = 510 / 3 = 170
+        // Same for green and blue.
+        // alpha average = 0
+        final int median = group.getMedianValue();
+        assertEquals(0x00AAAAAA, median); // 170 is 0xAA
+    }
+
+    @Test
+    public void testToString() throws ImagingException {
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        colorCounts.add(new ColorCount(0xFF102030));
+        final ColorGroup group = new ColorGroup(colorCounts, false);
+        final String str = group.toString();
+        assertTrue(str.contains("minRed: 10"));
+        assertTrue(str.contains("maxRed: 10"));
+        assertTrue(str.contains("minAlpha: ff"));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/palette/ColorSpaceSubsetTest.java 
b/src/test/java/org/apache/commons/imaging/palette/ColorSpaceSubsetTest.java
new file mode 100644
index 00000000..b91b1436
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/palette/ColorSpaceSubsetTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.palette;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+public class ColorSpaceSubsetTest {
+
+    @Test
+    public void testConstructorAndGetters() {
+        final int total = 100;
+        final int precision = 5;
+        final ColorSpaceSubset subset = new ColorSpaceSubset(total, precision);
+        assertEquals(total, subset.total);
+        assertEquals(precision, subset.precision);
+        assertEquals(-1, subset.rgb);
+        subset.setIndex(12);
+        assertEquals(12, subset.getIndex());
+    }
+
+    @Test
+    public void testConstructorWithMinsMaxs() {
+        final int total = 50;
+        final int precision = 6;
+        final int[] mins = { 1, 2, 3 };
+        final int[] maxs = { 10, 11, 12 };
+        final ColorSpaceSubset subset = new ColorSpaceSubset(total, precision, 
mins, maxs);
+        assertEquals(total, subset.total);
+        assertEquals(precision, subset.precision);
+        assertEquals(mins, subset.mins);
+        assertEquals(maxs, subset.maxs);
+    }
+
+    @Test
+    public void testContains() {
+        // Precision 8 means no bit shifting in contains() effectively (8-8=0)
+        // However, the code uses 8 - precision.
+        // If precision is 8, red >>= 0.
+        // If precision is 5, red >>= 3.
+        final int precision = 5;
+        final int precisionMask = (1 << precision) - 1; // 31
+        final ColorSpaceSubset subset = new ColorSpaceSubset(100, precision);
+        // Default mins are 0, maxs are precisionMask (31)
+        // Color (128, 128, 128)
+        // red = 128 >> 3 = 16. 0 <= 16 <= 31.
+        assertTrue(subset.contains(128, 128, 128));
+        // Boundary test
+        final int[] mins = { 10, 10, 10 };
+        final int[] maxs = { 20, 20, 20 };
+        final ColorSpaceSubset restricted = new ColorSpaceSubset(100, 
precision, mins, maxs);
+        // red = 80 >> 3 = 10. OK.
+        assertTrue(restricted.contains(80, 80, 80));
+        assertTrue(restricted.contains(160, 160, 160)); // 160 >> 3 = 20. OK.
+        assertFalse(restricted.contains(79, 80, 80)); // 79 >> 3 = 9. Too 
small.
+        assertFalse(restricted.contains(168, 80, 80)); // 168 >> 3 = 21. Too 
large.
+        assertFalse(restricted.contains(80, 79, 80));
+        assertFalse(restricted.contains(80, 168, 80));
+        assertFalse(restricted.contains(80, 80, 79));
+        assertFalse(restricted.contains(80, 80, 168));
+    }
+
+    @Test
+    public void testGetArea() {
+        final int[] mins = { 10, 20, 30 };
+        final int[] maxs = { 15, 25, 35 };
+        // diffs are 6, 6, 6
+        final ColorSpaceSubset subset = new ColorSpaceSubset(100, 8, mins, 
maxs);
+        assertEquals(6 * 6 * 6, subset.getArea());
+    }
+
+    @Test
+    public void testSetAverageRgb() {
+        final int precision = 2; // 2 bits per component. 0, 1, 2, 3
+        // index = blue << 4 | green << 2 | red
+        // table size = 1 << 6 = 64
+        final int[] table = new int[64];
+        // mins/maxs are inclusive
+        final int[] mins = { 1, 1, 1 };
+        final int[] maxs = { 2, 2, 2 };
+        // colors in subset: (1,1,1), (2,1,1), (1,2,1), (2,2,1), (1,1,2), 
(2,1,2), (1,2,2), (2,2,2)
+        // Let's put 10 pixels of (1,1,1) shifted by (8-precision) = 6
+        // actual color = 1 << 6 = 64
+        final int idx1 = 1 << 4 | 1 << 2 | 1;
+        table[idx1] = 10;
+        // Let's put 10 pixels of (2,2,2)
+        // actual color = 2 << 6 = 128
+        final int idx2 = 2 << 4 | 2 << 2 | 2;
+        table[idx2] = 10;
+        final ColorSpaceSubset subset = new ColorSpaceSubset(20, precision, 
mins, maxs);
+        subset.setAverageRgb(table);
+        // average red = (10 * 64 + 10 * 128) / 20 = (640 + 1280) / 20 = 1920 
/ 20 = 96
+        // rgb = (96 << 16) | (96 << 8) | 96
+        assertEquals(0x606060, subset.rgb & 0xffffff);
+    }
+
+    @Test
+    public void testRgbComparator() {
+        final ColorSpaceSubset subset1 = new ColorSpaceSubset(100, 8);
+        subset1.rgb = 0x102030;
+        final ColorSpaceSubset subset2 = new ColorSpaceSubset(100, 8);
+        subset2.rgb = 0x102040;
+        assertTrue(ColorSpaceSubset.RGB_COMPARATOR.compare(subset1, subset2) < 
0);
+        assertTrue(ColorSpaceSubset.RGB_COMPARATOR.compare(subset2, subset1) > 
0);
+        assertEquals(0, ColorSpaceSubset.RGB_COMPARATOR.compare(subset1, 
subset1));
+    }
+
+    @Test
+    public void testDump() {
+        // Just verify it doesn't throw anything
+        final ColorSpaceSubset subset = new ColorSpaceSubset(100, 8);
+        subset.rgb = 0x123456;
+        subset.dump("Test");
+        subset.dumpJustRgb("TestJustRgb");
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/palette/DitheringTest.java 
b/src/test/java/org/apache/commons/imaging/palette/DitheringTest.java
index ec7e8194..020c3464 100644
--- a/src/test/java/org/apache/commons/imaging/palette/DitheringTest.java
+++ b/src/test/java/org/apache/commons/imaging/palette/DitheringTest.java
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.apache.commons.imaging.palette;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -40,7 +41,6 @@ class DitheringTest {
         linkedList.add(colorSpaceSubset);
         final QuantizedPalette quantizedPalette = new 
QuantizedPalette(linkedList, 8);
         Dithering.applyFloydSteinbergDithering(bufferedImage, 
quantizedPalette);
-
         assertEquals(-16777208, bufferedImage.getRGB(0, 0));
         assertEquals(-16777208, bufferedImage.getRGB(1, 1));
         assertEquals(-16777208, bufferedImage.getRGB(2, 1));
@@ -56,11 +56,149 @@ class DitheringTest {
         linkedList.add(colorSpaceSubset);
         final QuantizedPalette quantizedPalette = new 
QuantizedPalette(linkedList, 3);
         Dithering.applyFloydSteinbergDithering(bufferedImage, 
quantizedPalette);
-
         assertEquals(-1, bufferedImage.getRGB(0, 0));
         assertEquals(-1, bufferedImage.getRGB(1, 1));
         assertEquals(-1, bufferedImage.getRGB(2, 1));
         assertEquals(-1, bufferedImage.getRGB(2, 2));
     }
 
+    @Test
+    void testApplyFloydSteinbergDitheringBlackAndWhite() throws 
ImagingException {
+        final BufferedImage image = new BufferedImage(2, 2, 
BufferedImage.TYPE_INT_RGB);
+        // All gray 0x808080 (128, 128, 128)
+        image.setRGB(0, 0, 0x808080);
+        image.setRGB(1, 0, 0x808080);
+        image.setRGB(0, 1, 0x808080);
+        image.setRGB(1, 1, 0x808080);
+        // Palette: Black and White
+        // We use a custom Palette implementation that returns the closest 
color
+        final Palette palette = new Palette() {
+
+            @Override
+            public int getEntry(final int index) {
+                return index == 0 ? 0x000000 : 0xffffff;
+            }
+
+            @Override
+            public int getPaletteIndex(final int rgb) {
+                final int r = rgb >> 16 & 0xff;
+                final int g = rgb >> 8 & 0xff;
+                final int b = rgb & 0xff;
+                final int gray = (r + g + b) / 3;
+                return gray < 128 ? 0 : 1;
+            }
+
+            @Override
+            public int length() {
+                return 2;
+            }
+        };
+        Dithering.applyFloydSteinbergDithering(image, palette);
+        // (0,0) is 128,128,128 -> closest is White (1) because we used gray < 
128 ? 0 : 1
+        // Actually, 128 < 128 is false, so it picks 1 (White: 0xffffff)
+        // Error = 128 - 255 = -127
+        // (1,0) gets 7/16 of error: 128 + (-127 * 7 / 16) = 128 - 55 = 73
+        // (0,1) gets 5/16 of error: 128 + (-127 * 5 / 16) = 128 - 39 = 89
+        // (1,1) gets 1/16 of error: 128 + (-127 * 1 / 16) = 128 - 7 = 121
+        assertEquals(0xffffff, image.getRGB(0, 0) & 0xffffff);
+        // (1,0) is 73 -> closest is Black (0x000000)
+        assertEquals(0x000000, image.getRGB(1, 0) & 0xffffff);
+    }
+
+    @Test
+    void testApplyFloydSteinbergDitheringSmallImage() throws ImagingException {
+        // 1x1 image
+        final BufferedImage image = new BufferedImage(1, 1, 
BufferedImage.TYPE_INT_RGB);
+        image.setRGB(0, 0, 0x123456);
+        // SimplePalette uses exact match.
+        // We must include the alpha channel because TYPE_INT_RGB uses 0xff as 
alpha in getRGB.
+        final Palette palette = new SimplePalette(new int[] { 0xff123456 });
+        // Should not throw exception
+        Dithering.applyFloydSteinbergDithering(image, palette);
+        assertEquals(0x123456, image.getRGB(0, 0) & 0xffffff);
+    }
+
+    @Test
+    void testApplyFloydSteinbergDitheringAlpha() throws ImagingException {
+        // Image with alpha channel
+        final BufferedImage image = new BufferedImage(2, 1, 
BufferedImage.TYPE_INT_ARGB);
+        // (0,0) is semi-transparent red
+        image.setRGB(0, 0, 0x80ff0000);
+        // (1,0) is fully transparent
+        image.setRGB(1, 0, 0x00000000);
+        final Palette palette = new Palette() {
+
+            @Override
+            public int getEntry(final int index) {
+                return index == 0 ? 0xff000000 : 0x00ffffff; // Black or 
Transparent White
+            }
+
+            @Override
+            public int getPaletteIndex(final int rgb) {
+                final int alpha = rgb >> 24 & 0xff;
+                return alpha < 128 ? 1 : 0;
+            }
+
+            @Override
+            public int length() {
+                return 2;
+            }
+        };
+        Dithering.applyFloydSteinbergDithering(image, palette);
+        // (0,0) alpha is 128 -> index 0 (Black: 0xff000000)
+        // (1,0) should receive error from (0,0)
+        // Initial (0,0) ARGB: 128, 255, 0, 0
+        // Palette (0,0) ARGB: 255, 0, 0, 0
+        // Error A: 128-255 = -127
+        // Error R: 255-0 = 255
+        // (1,0) was 0, 0, 0, 0
+        // New (1,0) A: 0 + (-127 * 7/16) = -55 -> clipped to 0
+        // New (1,0) R: 0 + (255 * 7/16) = 111
+        // New (1,0) ARGB approx: 0, 111, 0, 0
+        // palette.getPaletteIndex(0, 111, 0, 0) -> alpha is 0 < 128 -> index 
1 (Transparent White: 0x00ffffff)
+        assertEquals(0xff000000, image.getRGB(0, 0));
+        assertEquals(0x00ffffff, image.getRGB(1, 0));
+    }
+
+    @Test
+    void testApplyFloydSteinbergDitheringClipping() throws ImagingException {
+        final BufferedImage image = new BufferedImage(2, 1, 
BufferedImage.TYPE_INT_RGB);
+        // (0,0) is White, (1,0) is White.
+        // We force a large positive error at (0,0) that would push (1,0) 
beyond 255.
+        image.setRGB(0, 0, 0xffffff);
+        image.setRGB(1, 0, 0xffffff);
+        final Palette palette = new Palette() {
+
+            @Override
+            public int getEntry(final int index) {
+                return index == 0 ? 0x000000 : 0x808080;
+            }
+
+            @Override
+            public int getPaletteIndex(final int rgb) {
+                // If it's pure white, return index 0 (Black)
+                // This creates an error of +255 in each channel
+                if ((rgb & 0xffffff) == 0xffffff) {
+                    return 0;
+                }
+                // Otherwise return index 1
+                return 1;
+            }
+
+            @Override
+            public int length() {
+                return 2;
+            }
+        };
+        // (0,0) ARGB is 0xffffff (255, 255, 255)
+        // palette.getPaletteIndex(0xffffff) returns 0 -> 0x000000
+        // error is (255, 255, 255)
+        // (1,0) gets 7/16 of 255 = 111.
+        // New (1,0) = 255 + 111 = 366 -> should be clipped to 255.
+        // palette.getPaletteIndex(clipped value) -> 366 clipped to 255 is 
0xffffff.
+        // palette.getPaletteIndex(0xffffff) returns 0 -> 0x000000
+        Dithering.applyFloydSteinbergDithering(image, palette);
+        assertEquals(0x000000, image.getRGB(0, 0) & 0xffffff);
+        assertEquals(0x000000, image.getRGB(1, 0) & 0xffffff);
+    }
 }
diff --git 
a/src/test/java/org/apache/commons/imaging/palette/LongestAxisMedianCutTest.java
 
b/src/test/java/org/apache/commons/imaging/palette/LongestAxisMedianCutTest.java
new file mode 100644
index 00000000..a83789f7
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/imaging/palette/LongestAxisMedianCutTest.java
@@ -0,0 +1,260 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.palette;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.imaging.ImagingException;
+import org.junit.jupiter.api.Test;
+
+public class LongestAxisMedianCutTest {
+
+    @Test
+    public void testPerformNextMedianCut() throws ImagingException {
+        final LongestAxisMedianCut medianCut = new LongestAxisMedianCut();
+        final List<ColorGroup> colorGroups = new ArrayList<>();
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        // Red variation: 0 to 100
+        for (int i = 0; i <= 100; i++) {
+            colorCounts.add(new ColorCount(0xff000000 | i << 16));
+        }
+        // Green variation: 0 to 50
+        // Blue variation: 0 to 50
+        final ColorGroup root = new ColorGroup(colorCounts, false);
+        colorGroups.add(root);
+        // Red is the longest axis (100 > 50, 0, 0)
+        final boolean result = medianCut.performNextMedianCut(colorGroups, 
false);
+        assertTrue(result);
+        assertEquals(2, colorGroups.size());
+    }
+
+    @Test
+    public void testPerformNextMedianCutWithIgnoreAlpha() throws 
ImagingException {
+        final LongestAxisMedianCut medianCut = new LongestAxisMedianCut();
+        final List<ColorGroup> colorGroups = new ArrayList<>();
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        // Alpha variation: 0 to 200
+        // Red variation: 0 to 100
+        for (int i = 0; i <= 100; i++) {
+            // ARGB: i*2, i, 0, 0
+            colorCounts.add(new ColorCount(i * 2 << 24 | i << 16));
+        }
+        final ColorGroup root = new ColorGroup(colorCounts, true);
+        colorGroups.add(root);
+        // If ignoreAlpha is true, Red should be the longest axis (100) even 
though Alpha has 200 range
+        medianCut.performNextMedianCut(colorGroups, true);
+        assertEquals(2, colorGroups.size());
+        final ColorGroup cg1 = colorGroups.get(0);
+        final ColorGroup cg2 = colorGroups.get(1);
+        // Both should have same green and blue (0)
+        assertEquals(0, cg1.minGreen);
+        assertEquals(0, cg1.maxGreen);
+        assertEquals(0, cg2.minGreen);
+        assertEquals(0, cg2.maxGreen);
+    }
+
+    @Test
+    public void testPerformNextMedianCutSingleColor() throws ImagingException {
+        final LongestAxisMedianCut medianCut = new LongestAxisMedianCut();
+        final List<ColorGroup> colorGroups = new ArrayList<>();
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        colorCounts.add(new ColorCount(0xff000000));
+        final ColorGroup root = new ColorGroup(colorCounts, false);
+        colorGroups.add(root);
+        // maxDiff is 0, should return false
+        final boolean result = medianCut.performNextMedianCut(colorGroups, 
false);
+        assertFalse(result);
+        assertEquals(1, colorGroups.size());
+    }
+
+    @Test
+    public void testPerformNextMedianCutDetailed() throws ImagingException {
+        final LongestAxisMedianCut medianCut = new LongestAxisMedianCut();
+        final List<ColorGroup> colorGroups = new ArrayList<>();
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        // Create 10 pixels of color 1 and 10 pixels of color 2
+        final ColorCount cc1 = new ColorCount(0xff000000); // Black
+        cc1.count = 10;
+        colorCounts.add(cc1);
+        final ColorCount cc2 = new ColorCount(0xffff0000); // Red
+        cc2.count = 10;
+        colorCounts.add(cc2);
+        final ColorGroup root = new ColorGroup(colorCounts, false);
+        colorGroups.add(root);
+        final boolean result = medianCut.performNextMedianCut(colorGroups, 
false);
+        assertTrue(result);
+        assertEquals(2, colorGroups.size());
+        // Each group should have 10 points
+        assertEquals(10, colorGroups.get(0).totalPoints);
+        assertEquals(10, colorGroups.get(1).totalPoints);
+    }
+
+    @Test
+    public void testPerformNextMedianCutAllAxes() throws ImagingException {
+        final LongestAxisMedianCut medianCut = new LongestAxisMedianCut();
+        // Test Green axis
+        {
+            final List<ColorGroup> colorGroups = new ArrayList<>();
+            final List<ColorCount> colorCounts = new ArrayList<>();
+            colorCounts.add(new ColorCount(0xff000000));
+            colorCounts.add(new ColorCount(0xff00ff00));
+            colorGroups.add(new ColorGroup(colorCounts, false));
+            assertTrue(medianCut.performNextMedianCut(colorGroups, false));
+            // After cut, the original group in the list is replaced or new 
ones added.
+            // LongestAxisMedianCut.doCut removes the old group and adds two 
new ones.
+            // The list will have 2 groups.
+            assertEquals(2, colorGroups.size());
+        }
+        // Test Blue axis
+        {
+            final List<ColorGroup> colorGroups = new ArrayList<>();
+            final List<ColorCount> colorCounts = new ArrayList<>();
+            colorCounts.add(new ColorCount(0xff000000));
+            colorCounts.add(new ColorCount(0xff0000ff));
+            colorGroups.add(new ColorGroup(colorCounts, false));
+            assertTrue(medianCut.performNextMedianCut(colorGroups, false));
+            assertEquals(2, colorGroups.size());
+        }
+        // Test Alpha axis
+        {
+            final List<ColorGroup> colorGroups = new ArrayList<>();
+            final List<ColorCount> colorCounts = new ArrayList<>();
+            colorCounts.add(new ColorCount(0x00000000));
+            colorCounts.add(new ColorCount(0xff000000));
+            colorGroups.add(new ColorGroup(colorCounts, false));
+            assertTrue(medianCut.performNextMedianCut(colorGroups, false));
+            assertEquals(2, colorGroups.size());
+        }
+    }
+
+    @Test
+    public void testPerformNextMedianCutMultipleGroups() throws 
ImagingException {
+        final LongestAxisMedianCut medianCut = new LongestAxisMedianCut();
+        final List<ColorGroup> colorGroups = new ArrayList<>();
+        // Group 1: Range 10 (Red)
+        final List<ColorCount> counts1 = new ArrayList<>();
+        counts1.add(new ColorCount(0xff000000));
+        counts1.add(new ColorCount(0xff0a0000));
+        colorGroups.add(new ColorGroup(counts1, false));
+        // Group 2: Range 20 (Red)
+        final List<ColorCount> counts2 = new ArrayList<>();
+        counts2.add(new ColorCount(0xff000000));
+        counts2.add(new ColorCount(0xff140000));
+        colorGroups.add(new ColorGroup(counts2, false));
+        // Should cut Group 2 because it has larger maxDiff (20 > 10)
+        assertTrue(medianCut.performNextMedianCut(colorGroups, false));
+        assertEquals(3, colorGroups.size());
+        // Check that one group still has range 10, and two new groups are 
created from the range 20 group
+        boolean foundRange10 = false;
+        for (final ColorGroup cg : colorGroups) {
+            if (cg.maxDiff == 10) {
+                foundRange10 = true;
+                break;
+            }
+        }
+        assertTrue(foundRange10);
+    }
+
+    @Test
+    public void testPerformNextMedianCutAmbiguousMedian() throws 
ImagingException {
+        final LongestAxisMedianCut medianCut = new LongestAxisMedianCut();
+        final List<ColorGroup> colorGroups = new ArrayList<>();
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        // 3 colors with counts that make median selection interesting
+        // Total points = 10
+        // countHalf = 5
+        final ColorCount cc1 = new ColorCount(0xff000000); // 0
+        cc1.count = 3;
+        colorCounts.add(cc1);
+        final ColorCount cc2 = new ColorCount(0xff010000); // 1
+        cc2.count = 4;
+        colorCounts.add(cc2);
+        final ColorCount cc3 = new ColorCount(0xff020000); // 2
+        cc3.count = 3;
+        colorCounts.add(cc3);
+        colorGroups.add(new ColorGroup(colorCounts, false));
+        // medianIndex loop:
+        // i=0: newCount=3. 3 < 5.
+        // i=1: newCount=7. 7 >= 5. break. medianIndex=1.
+        // newDiff = |7 - 5| = 2
+        // oldDiff = |5 - 3| = 2
+        // oldDiff < newDiff is false (2 < 2 is false).
+        // medianIndex stays 1.
+        // colorCounts1: [cc1, cc2], colorCounts2: [cc3]
+        assertTrue(medianCut.performNextMedianCut(colorGroups, false));
+        assertEquals(2, colorGroups.size());
+        // Find the group with 7 points
+        boolean found7 = false;
+        boolean found3 = false;
+        for (final ColorGroup cg : colorGroups) {
+            if (cg.totalPoints == 7) {
+                found7 = true;
+            }
+            if (cg.totalPoints == 3) {
+                found3 = true;
+            }
+        }
+        assertTrue(found7);
+        assertTrue(found3);
+    }
+
+    @Test
+    public void testPerformNextMedianCutOldDiffSmaller() throws 
ImagingException {
+        final LongestAxisMedianCut medianCut = new LongestAxisMedianCut();
+        final List<ColorGroup> colorGroups = new ArrayList<>();
+        final List<ColorCount> colorCounts = new ArrayList<>();
+        // countHalf = 5
+        final ColorCount cc1 = new ColorCount(0xff000000);
+        cc1.count = 4;
+        colorCounts.add(cc1);
+        final ColorCount cc2 = new ColorCount(0xff010000);
+        cc2.count = 3;
+        colorCounts.add(cc2);
+        final ColorCount cc3 = new ColorCount(0xff020000);
+        cc3.count = 3;
+        colorCounts.add(cc3);
+        colorGroups.add(new ColorGroup(colorCounts, false));
+        // medianIndex loop:
+        // i=0: newCount=4.
+        // i=1: newCount=7. break. medianIndex=1.
+        // newDiff = |7 - 5| = 2
+        // oldDiff = |5 - 4| = 1
+        // oldDiff < newDiff (1 < 2) IS TRUE.
+        // medianIndex becomes 0.
+        // colorCounts1: [cc1], colorCounts2: [cc2, cc3]
+        assertTrue(medianCut.performNextMedianCut(colorGroups, false));
+        assertEquals(2, colorGroups.size());
+        boolean found4 = false;
+        boolean found6 = false;
+        for (final ColorGroup cg : colorGroups) {
+            if (cg.totalPoints == 4) {
+                found4 = true;
+            }
+            if (cg.totalPoints == 6) {
+                found6 = true;
+            }
+        }
+        assertTrue(found4);
+        assertTrue(found6);
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/imaging/palette/PaletteFactoryTest.java 
b/src/test/java/org/apache/commons/imaging/palette/PaletteFactoryTest.java
new file mode 100644
index 00000000..30cc946d
--- /dev/null
+++ b/src/test/java/org/apache/commons/imaging/palette/PaletteFactoryTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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
+ *
+ *      https://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.commons.imaging.palette;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.awt.image.BufferedImage;
+
+import org.apache.commons.imaging.ImagingException;
+import org.junit.jupiter.api.Test;
+
+public class PaletteFactoryTest {
+
+    @Test
+    public void testHasTransparency() {
+        final PaletteFactory factory = new PaletteFactory();
+        final BufferedImage imageNoAlpha = new BufferedImage(10, 10, 
BufferedImage.TYPE_INT_RGB);
+        assertFalse(factory.hasTransparency(imageNoAlpha));
+        assertFalse(factory.hasTransparency(imageNoAlpha, 128));
+        final BufferedImage imageAlpha = new BufferedImage(10, 10, 
BufferedImage.TYPE_INT_ARGB);
+        // Default is all 0 (transparent)
+        assertTrue(factory.hasTransparency(imageAlpha));
+        assertTrue(factory.hasTransparency(imageAlpha, 128));
+        // Make it opaque
+        for (int x = 0; x < 10; x++) {
+            for (int y = 0; y < 10; y++) {
+                imageAlpha.setRGB(x, y, 0xFFFFFFFF);
+            }
+        }
+        assertFalse(factory.hasTransparency(imageAlpha));
+        // One pixel semi-transparent
+        imageAlpha.setRGB(5, 5, 0x80FFFFFF);
+        assertTrue(factory.hasTransparency(imageAlpha));
+        assertFalse(factory.hasTransparency(imageAlpha, 127)); // 128 is not < 
127
+        assertTrue(factory.hasTransparency(imageAlpha, 129)); // 128 is < 129
+    }
+
+    @Test
+    public void testIsGrayscale() {
+        final PaletteFactory factory = new PaletteFactory();
+        final BufferedImage image = new BufferedImage(10, 10, 
BufferedImage.TYPE_INT_RGB);
+        for (int x = 0; x < 10; x++) {
+            for (int y = 0; y < 10; y++) {
+                image.setRGB(x, y, 0xFF111111 * (x + 1));
+            }
+        }
+        assertTrue(factory.isGrayscale(image));
+        image.setRGB(5, 5, 0xFF111211);
+        assertFalse(factory.isGrayscale(image));
+    }
+
+    @Test
+    public void testCountTransparentColors() {
+        final PaletteFactory factory = new PaletteFactory();
+        final BufferedImage image = new BufferedImage(10, 10, 
BufferedImage.TYPE_INT_ARGB);
+        // All transparent, all same color (0x00000000)
+        assertEquals(1, factory.countTransparentColors(image));
+        for (int x = 0; x < 10; x++) {
+            for (int y = 0; y < 10; y++) {
+                image.setRGB(x, y, 0x00FFFFFF & (x << 16 | y << 8));
+            }
+        }
+        // All have alpha 0, but different RGB.
+        // first = 0x00000000 (at 0,0). next is 0x00000800 (at 0,1).
+        // Since 0x00000000 != 0x00000800, it should return 2.
+        assertEquals(2, factory.countTransparentColors(image));
+        // Mixed alpha: all opaque
+        for (int x = 0; x < 10; x++) {
+            for (int y = 0; y < 10; y++) {
+                image.setRGB(x, y, 0xFFFFFFFF);
+            }
+        }
+        assertEquals(0, factory.countTransparentColors(image));
+        // One pixel transparent
+        image.setRGB(0, 0, 0x00FFFFFF);
+        assertEquals(1, factory.countTransparentColors(image));
+        final int[] rgbs = { 0x00112233, 0x00112233, 0xFF112233, 0x00445566 };
+        assertEquals(2, factory.countTrasparentColors(rgbs)); // 0x00112233 
and 0x00445566 are different
+        final int[] rgbs1 = { 0x00112233, 0x00112233, 0xFF112233 };
+        assertEquals(1, factory.countTrasparentColors(rgbs1));
+        final int[] rgbs0 = { 0xFF112233, 0xFF445566 };
+        assertEquals(0, factory.countTrasparentColors(rgbs0));
+    }
+
+    @Test
+    public void testMakeExactRgbPaletteSimple() {
+        final PaletteFactory factory = new PaletteFactory();
+        final BufferedImage image = new BufferedImage(10, 10, 
BufferedImage.TYPE_INT_RGB);
+        image.setRGB(0, 0, 0xFF0000);
+        image.setRGB(1, 1, 0x00FF00);
+        image.setRGB(2, 2, 0x0000FF);
+        final SimplePalette palette = factory.makeExactRgbPaletteSimple(image, 
10);
+        assertNotNull(palette);
+        assertEquals(4, palette.length()); // Black (default) + 3 colors
+    }
+
+    @Test
+    public void testMakeExactRgbPaletteFancy() {
+        final PaletteFactory factory = new PaletteFactory();
+        final BufferedImage image = new BufferedImage(10, 10, 
BufferedImage.TYPE_INT_RGB);
+        image.setRGB(0, 0, 0xFF0000);
+        image.setRGB(1, 1, 0x00FF00);
+        final Palette palette = factory.makeExactRgbPaletteFancy(image);
+        assertNotNull(palette);
+        assertEquals(3, palette.length()); // Black + 2 colors
+    }
+
+    @Test
+    public void testMakeQuantizedRgbPalette() {
+        final PaletteFactory factory = new PaletteFactory();
+        final BufferedImage image = new BufferedImage(10, 10, 
BufferedImage.TYPE_INT_RGB);
+        for (int i = 0; i < 100; i++) {
+            image.setRGB(i % 10, i / 10, i << 16 | i << 8 | i);
+        }
+        final Palette palette = factory.makeQuantizedRgbPalette(image, 10);
+        assertNotNull(palette);
+        assertTrue(palette.length() <= 10);
+    }
+
+    @Test
+    public void testMakeQuantizedRgbaPalette() throws ImagingException {
+        final PaletteFactory factory = new PaletteFactory();
+        final BufferedImage image = new BufferedImage(10, 10, 
BufferedImage.TYPE_INT_ARGB);
+        for (int i = 0; i < 100; i++) {
+            image.setRGB(i % 10, i / 10, i << 24 | i << 16 | i << 8 | i);
+        }
+        final Palette palette = factory.makeQuantizedRgbaPalette(image, true, 
10);
+        assertNotNull(palette);
+        assertTrue(palette.length() <= 10);
+    }
+}

Reply via email to