mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2025-08-02 11:05:29 -04:00
TMI-TIFF: Fixed bug in YCbCr reading. Implemented "old-style" JPEG reading for two test images. More work needed.
This commit is contained in:
parent
fcd15a9e36
commit
c394f8a4bc
@ -163,4 +163,10 @@ public interface TIFF {
|
||||
int TAG_TILE_BYTE_COUNTS = 325;
|
||||
|
||||
int TAG_JPEG_TABLES = 347;
|
||||
|
||||
// "Old-style" JPEG (Obsolete)
|
||||
int TAG_JPEG_PROC = 512;
|
||||
int TAG_JPEG_QTABLES = 519;
|
||||
int TAG_JPEG_DCTABLES = 520;
|
||||
int TAG_JPEG_ACTABLES = 521;
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ class JPEGTables {
|
||||
|
||||
// Read lengths as short array
|
||||
short[] lengths = new short[DHT_LENGTH];
|
||||
for (int i = 0, lengthsLength = lengths.length; i < lengthsLength; i++) {
|
||||
for (int i = 0; i < DHT_LENGTH; i++) {
|
||||
lengths[i] = (short) data.readUnsignedByte();
|
||||
}
|
||||
read += lengths.length;
|
||||
|
@ -65,4 +65,8 @@ interface TIFFExtension {
|
||||
int SAMPLEFORMAT_INT = 2;
|
||||
int SAMPLEFORMAT_FP = 3;
|
||||
int SAMPLEFORMAT_UNDEFINED = 4;
|
||||
|
||||
// "Old-style" JPEG (obsolete)
|
||||
int JPEG_PROC_BASELINE = 1;
|
||||
int JPEG_PROC_LOSSLESS = 14;
|
||||
}
|
||||
|
@ -59,9 +59,7 @@ import java.awt.color.ICC_Profile;
|
||||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
@ -485,9 +483,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
WritableRaster rowRaster = rawType.getColorModel().createCompatibleWritableRaster(stripTileWidth, 1);
|
||||
int row = 0;
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
switch (compression) {
|
||||
// TIFF Baseline
|
||||
case TIFFBaseline.COMPRESSION_NONE:
|
||||
@ -547,6 +542,9 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
}
|
||||
}
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
// TODO: Read only tiles that lies within region
|
||||
// General uncompressed/compressed reading
|
||||
for (int y = 0; y < tilesDown; y++) {
|
||||
@ -606,6 +604,7 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
case TIFFExtension.COMPRESSION_JPEG:
|
||||
// JPEG ('new-style' JPEG)
|
||||
// TODO: Refactor all JPEG reading out to separate JPEG support class?
|
||||
// TODO: Cache the JPEG reader for later use? Remember to reset to avoid resource leaks
|
||||
|
||||
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
|
||||
ImageReader jpegReader = new JPEGImageReader(getOriginatingProvider());
|
||||
@ -680,6 +679,9 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// ...and the JPEG reader will probably choke on missing tables...
|
||||
}
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
for (int y = 0; y < tilesDown; y++) {
|
||||
int col = 0;
|
||||
int rowsInTile = Math.min(stripTileHeight, height - row);
|
||||
@ -689,14 +691,14 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
int colsInTile = Math.min(stripTileWidth, width - col);
|
||||
|
||||
imageInput.seek(stripTileOffsets[i]);
|
||||
SubImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
|
||||
ImageInputStream subStream = new SubImageInputStream(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE);
|
||||
try {
|
||||
jpegReader.setInput(subStream);
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||
jpegParam.setDestinationOffset(new Point(col, row));
|
||||
jpegParam.setDestination(destination);
|
||||
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
|
||||
// In the latter case we will have to use readAsRaster
|
||||
// In the latter case we will have to use readAsRaster and do color conversion ourselves
|
||||
jpegReader.read(0, jpegParam);
|
||||
}
|
||||
finally {
|
||||
@ -722,6 +724,240 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
|
||||
break;
|
||||
|
||||
case TIFFExtension.COMPRESSION_OLD_JPEG:
|
||||
// JPEG ('old-style' JPEG, later overridden in Technote2)
|
||||
|
||||
// http://www.remotesensing.org/libtiff/TIFFTechNote2.html
|
||||
// TODO: Issue warning?
|
||||
|
||||
int mode = getValueAsIntWithDefault(TIFF.TAG_JPEG_PROC, 1);
|
||||
if (mode == TIFFExtension.JPEG_PROC_LOSSLESS) {
|
||||
throw new IIOException("Unsupported TIFF JPEGProcessingMode: Lossless (14)");
|
||||
}
|
||||
else if (mode != TIFFExtension.JPEG_PROC_BASELINE) {
|
||||
throw new IIOException("Unknown TIFF JPEGProcessingMode value: " + mode);
|
||||
}
|
||||
|
||||
// May use normal tiling??
|
||||
|
||||
// 512/JPEGProc: 1=Baseline, 14=Lossless (with Huffman coding), no default, although 1 is assumed if absent
|
||||
// 513/JPEGInterchangeFormat (may be absent...)
|
||||
// 514/JPEGInterchangeFormatLength (may be absent...)
|
||||
// 515/JPEGRestartInterval (may be absent)
|
||||
|
||||
// 517/JPEGLosslessPredictors
|
||||
// 518/JPEGPointTransforms
|
||||
|
||||
// 519/JPEGQTables
|
||||
// 520/JPEGDCTables
|
||||
// 521/JPEGACTables
|
||||
|
||||
// This field was originally intended to point to a list of offsets to the quantization tables, one per
|
||||
// component. Each table consists of 64 BYTES (one for each DCT coefficient in the 8x8 block). The
|
||||
// quantization tables are stored in zigzag order, and are compatible with the quantization tables
|
||||
// usually found in a JPEG stream DQT marker.
|
||||
|
||||
// The original specification strongly recommended that, within the TIFF file, each component be
|
||||
// assigned separate tables, and labelled this field as mandatory whenever the JPEGProc field specifies
|
||||
// a DCT-based process.
|
||||
|
||||
// We've seen old-style JPEG in TIFF files where some or all Table offsets, contained the JPEGQTables,
|
||||
// JPEGDCTables, and JPEGACTables tags are incorrect values beyond EOF. However, these files do always
|
||||
// seem to contain a useful JPEGInterchangeFormat tag. Therefore, we recommend a careful attempt to read
|
||||
// the Tables tags only as a last resort, if no table data is found in a JPEGInterchangeFormat stream.
|
||||
|
||||
|
||||
// TIFF is strictly ISO JPEG, so we should probably stick to the standard reader
|
||||
jpegReader = new JPEGImageReader(getOriginatingProvider());
|
||||
jpegParam = (JPEGImageReadParam) jpegReader.getDefaultReadParam();
|
||||
|
||||
int jpegOffset = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT, -1);
|
||||
int jpegLenght = getValueAsIntWithDefault(TIFF.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, -1);
|
||||
|
||||
ImageInputStream subStream;
|
||||
|
||||
if (jpegOffset != -1) {
|
||||
// Straight forward case: We're good to go! We'll disregard tiling and any tables tags
|
||||
|
||||
imageInput.seek(jpegOffset);
|
||||
subStream = new SubImageInputStream(imageInput, jpegLenght != -1 ? jpegLenght : Short.MAX_VALUE);
|
||||
jpegReader.setInput(subStream);
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
try {
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, width, height));
|
||||
jpegParam.setDestination(destination);
|
||||
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
|
||||
// In the latter case we will have to use readAsRaster and do color conversion ourselves
|
||||
jpegReader.read(0, jpegParam);
|
||||
}
|
||||
finally {
|
||||
subStream.close();
|
||||
}
|
||||
|
||||
processImageProgress(100f * row / (float) height);
|
||||
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// The hard way: Read tables and re-create a full JFIF stream
|
||||
|
||||
// TODO: If any of the q/dc/ac tables are equal (or have same offset, even if "spec" violation),
|
||||
// use only the first occurrence, and update selectors in SOF0 and SOS
|
||||
|
||||
long[] qTablesOffsets = getValueAsLongArray(TIFF.TAG_JPEG_QTABLES, "JPEGQTables", true);
|
||||
byte[][] qTables = new byte[3][(int) (qTablesOffsets[1] - qTablesOffsets[0])]; // TODO: Using the offsets seems fragile.. Use fixed length??
|
||||
for (int j = 0; j < 3; j++) {
|
||||
imageInput.seek(qTablesOffsets[j]);
|
||||
imageInput.readFully(qTables[j]);
|
||||
}
|
||||
// System.err.println("qTables: " + qTables[0].length);
|
||||
|
||||
long[] dcTablesOffsets = getValueAsLongArray(TIFF.TAG_JPEG_DCTABLES, "JPEGDCTables", true);
|
||||
byte[][] dcTables = new byte[3][(int) (dcTablesOffsets[1] - dcTablesOffsets[0])];
|
||||
for (int j = 0; j < 3; j++) {
|
||||
imageInput.seek(dcTablesOffsets[j]);
|
||||
imageInput.readFully(dcTables[j]);
|
||||
}
|
||||
// System.err.println("dcTables: " + dcTables[0].length);
|
||||
|
||||
long[] acTablesOffsets = getValueAsLongArray(TIFF.TAG_JPEG_ACTABLES, "JPEGACTables", true);
|
||||
byte[][] acTables = new byte[3][(int) (acTablesOffsets[1] - acTablesOffsets[0])];
|
||||
for (int j = 0; j < 3; j++) {
|
||||
imageInput.seek(acTablesOffsets[j]);
|
||||
imageInput.readFully(acTables[j]);
|
||||
}
|
||||
// System.err.println("acTables: " + acTables[0].length);
|
||||
|
||||
// Read data
|
||||
processImageStarted(imageIndex);
|
||||
|
||||
for (int y = 0; y < tilesDown; y++) {
|
||||
int col = 0;
|
||||
int rowsInTile = Math.min(stripTileHeight, height - row);
|
||||
|
||||
for (int x = 0; x < tilesAcross; x++) {
|
||||
int colsInTile = Math.min(stripTileWidth, width - col);
|
||||
int i = y * tilesAcross + x;
|
||||
|
||||
imageInput.seek(stripTileOffsets[i]);
|
||||
subStream = ImageIO.createImageInputStream(new SequenceInputStream(Collections.enumeration(
|
||||
Arrays.asList(
|
||||
// TODO; Get rid of hardcoded data + extract method/class...
|
||||
// TODO:
|
||||
// - Create a BAIS with size large enough to keep JFIF structure incl tables and SOS,
|
||||
// - Wrap in DataInput,
|
||||
// - Insert width/height, component ids etc at correct place
|
||||
// - Insert tables at correct place
|
||||
|
||||
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd8, // SOI
|
||||
// SOF0 (short), length (short)
|
||||
(byte) 0xff, (byte) 0xc0, 0x00, 0x11, // SOF0, 17 bytes
|
||||
// bits (byte), width (short), height (short)
|
||||
0x08, 0x00, (byte) 0xe0, 0x00, (byte) 0xf0,
|
||||
// num comp (byte), (id (byte) h/vsub (byte), qtsel (byte) * num comp)
|
||||
0x03, 0x00, 0x22, 0x00, 0x01, 0x11, 0x01, 0x02, 0x11, 0x02,
|
||||
// 0x03, 0x00, 0x22, 0x00, 0x01, 0x11, 0x01, 0x02, 0x11, 0x01,
|
||||
// DQT
|
||||
(byte) 0xff, (byte) 0xdb, 0x00, 0x43, 0x00,
|
||||
}),
|
||||
// ... table data
|
||||
new ByteArrayInputStream(qTables[0]),
|
||||
new ByteArrayInputStream(new byte[] {
|
||||
(byte) 0xff, (byte) 0xdb, 0x00, 0x43, 0x01,
|
||||
}),
|
||||
// ... table data
|
||||
new ByteArrayInputStream(qTables[1]),
|
||||
new ByteArrayInputStream(new byte[] {
|
||||
(byte) 0xff, (byte) 0xdb, 0x00, 0x43, 0x02,
|
||||
}),
|
||||
// ... table data
|
||||
new ByteArrayInputStream(qTables[2]),
|
||||
|
||||
// DHT (DC)
|
||||
new ByteArrayInputStream(new byte[] {
|
||||
(byte) 0xff, (byte) 0xc4, 0x00, 0x1f, 0x00,
|
||||
}),
|
||||
// ... table data
|
||||
new ByteArrayInputStream(dcTables[0]),
|
||||
new ByteArrayInputStream(new byte[] {
|
||||
(byte) 0xff, (byte) 0xc4, 0x00, 0x1f, 0x01,
|
||||
}),
|
||||
// ... table data
|
||||
new ByteArrayInputStream(dcTables[1]),
|
||||
new ByteArrayInputStream(new byte[] {
|
||||
(byte) 0xff, (byte) 0xc4, 0x00, 0x1f, 0x02,
|
||||
}),
|
||||
// ... table data
|
||||
new ByteArrayInputStream(dcTables[2]),
|
||||
|
||||
// DHT (AC)
|
||||
new ByteArrayInputStream(new byte[] {
|
||||
(byte) 0xff, (byte) 0xc4, 0x00, (byte) 0xb5, 0x10,
|
||||
}),
|
||||
// ... table data
|
||||
new ByteArrayInputStream(acTables[0]),
|
||||
new ByteArrayInputStream(new byte[] {
|
||||
(byte) 0xff, (byte) 0xc4, 0x00, (byte) 0xb5, 0x11,
|
||||
}),
|
||||
// ... table data
|
||||
new ByteArrayInputStream(acTables[1]),
|
||||
new ByteArrayInputStream(new byte[] {
|
||||
(byte) 0xff, (byte) 0xc4, 0x00, (byte) 0xb5, 0x12,
|
||||
}),
|
||||
// ... table data
|
||||
new ByteArrayInputStream(acTables[2]),
|
||||
|
||||
new ByteArrayInputStream(new byte[] {
|
||||
(byte) 0xff, (byte) 0xda, // SOS
|
||||
// TODO: Figure out what the last 3 bytes are...
|
||||
// Length: 12 (short), num comp (byte), (id (byte), dc/ac sel (byte) * num comp), ?? byte, ?? byte ?? byte
|
||||
0x00, 0x0C, 0x03, 0x00, 0x00, 0x01, 0x11, 0x02, 0x11, 0x00, 0x00, 0x00
|
||||
// 0x00, 0x0C, 0x03, 0x00, 0x00, 0x01, 0x11, 0x02, 0x12, 0x00, 0x63, 0x00
|
||||
}),
|
||||
IIOUtil.createStreamAdapter(imageInput, stripTileByteCounts != null ? (int) stripTileByteCounts[i] : Short.MAX_VALUE),
|
||||
new ByteArrayInputStream(new byte[] {(byte) 0xff, (byte) 0xd9}) // EOI
|
||||
)
|
||||
)));
|
||||
|
||||
jpegReader.setInput(subStream);
|
||||
|
||||
try {
|
||||
jpegParam.setSourceRegion(new Rectangle(0, 0, colsInTile, rowsInTile));
|
||||
jpegParam.setDestinationOffset(new Point(col, row));
|
||||
jpegParam.setDestination(destination);
|
||||
// TODO: This works only if Gray/YCbCr/RGB, not CMYK/LAB/etc...
|
||||
// In the latter case we will have to use readAsRaster and do color conversion ourselves
|
||||
jpegReader.read(0, jpegParam);
|
||||
}
|
||||
finally {
|
||||
subStream.close();
|
||||
}
|
||||
|
||||
if (abortRequested()) {
|
||||
break;
|
||||
}
|
||||
|
||||
col += colsInTile;
|
||||
}
|
||||
|
||||
processImageProgress(100f * row / (float) height);
|
||||
|
||||
if (abortRequested()) {
|
||||
processReadAborted();
|
||||
break;
|
||||
}
|
||||
|
||||
row += rowsInTile;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TIFFBaseline.COMPRESSION_CCITT_HUFFMAN:
|
||||
// CCITT modified Huffman
|
||||
// Additionally, the specification defines these values as part of the TIFF extensions:
|
||||
@ -729,8 +965,6 @@ public class TIFFImageReader extends ImageReaderBase {
|
||||
// CCITT Group 3 fax encoding
|
||||
case TIFFExtension.COMPRESSION_CCITT_T6:
|
||||
// CCITT Group 4 fax encoding
|
||||
case TIFFExtension.COMPRESSION_OLD_JPEG:
|
||||
// JPEG ('old-style' JPEG, later overridden in Technote2)
|
||||
|
||||
throw new IIOException("Unsupported TIFF Compression value: " + compression);
|
||||
default:
|
||||
|
@ -216,18 +216,21 @@ final class YCbCrUpsamplerStream extends FilterInputStream {
|
||||
}
|
||||
|
||||
private void convertYCbCr2RGB(final byte[] yCbCr, final byte[] rgb, final double[] coefficients, final int offset) {
|
||||
// TODO: FixMe: This is bogus...
|
||||
double y = yCbCr[offset ] & 0xff;
|
||||
double cb = yCbCr[offset + 1] & 0xff;
|
||||
double cr = yCbCr[offset + 2] & 0xff;
|
||||
double y = (yCbCr[offset ] & 0xff);
|
||||
double cb = (yCbCr[offset + 1] & 0xff) - 128; // TODO: The -128 part seems bogus... Consult ReferenceBlackWhite??? But default to these values?
|
||||
double cr = (yCbCr[offset + 2] & 0xff) - 128;
|
||||
|
||||
double lumaRed = coefficients[0];
|
||||
double lumaGreen = coefficients[1];
|
||||
double lumaBlue = coefficients[2];
|
||||
|
||||
rgb[offset ] = clamp((int) Math.round(cr * (2 - 2 * lumaRed) + y));
|
||||
rgb[offset + 2] = clamp((int) Math.round(cb * (2 - 2 * lumaBlue) + y));
|
||||
rgb[offset + 1] = clamp((int) Math.round((y - lumaRed * (rgb[offset] & 0xff) - lumaBlue * (rgb[offset + 2] & 0xff)) / lumaGreen));
|
||||
int red = (int) Math.round(cr * (2 - 2 * lumaRed) + y);
|
||||
int blue = (int) Math.round(cb * (2 - 2 * lumaBlue) + y);
|
||||
int green = (int) Math.round((y - lumaRed * (rgb[offset] & 0xff) - lumaBlue * (rgb[offset + 2] & 0xff)) / lumaGreen);
|
||||
|
||||
rgb[offset ] = clamp(red);
|
||||
rgb[offset + 2] = clamp(blue);
|
||||
rgb[offset + 1] = clamp(green);
|
||||
}
|
||||
|
||||
private static byte clamp(int val) {
|
||||
|
@ -52,11 +52,14 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
|
||||
new TestData(getClassLoaderResource("/tiff/sm_colors_tile.tif"), new Dimension(64, 64)), // RGB, uncompressed, tiled
|
||||
new TestData(getClassLoaderResource("/tiff/sm_colors_pb_tile.tif"), new Dimension(64, 64)), // RGB, PackBits compressed, tiled
|
||||
new TestData(getClassLoaderResource("/tiff/galaxy.tif"), new Dimension(965, 965)), // RGB, LZW compressed
|
||||
new TestData(getClassLoaderResource("/tiff/quad-lzw.tif"), new Dimension(512, 384)), // RGB, OLD LZW compressed, tiled
|
||||
new TestData(getClassLoaderResource("/tiff/quad-lzw.tif"), new Dimension(512, 384)), // RGB, Old spec (reversed) LZW compressed, tiled
|
||||
new TestData(getClassLoaderResource("/tiff/bali.tif"), new Dimension(725, 489)), // Palette-based, LZW compressed
|
||||
new TestData(getClassLoaderResource("/tiff/f14.tif"), new Dimension(640, 480)), // Gray, uncompressed
|
||||
new TestData(getClassLoaderResource("/tiff/marbles.tif"), new Dimension(1419, 1001)), // RGB, LZW compressed w/predictor
|
||||
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)) // CMYK, uncompressed
|
||||
new TestData(getClassLoaderResource("/tiff/chifley_logo.tif"), new Dimension(591, 177)), // CMYK, uncompressed
|
||||
new TestData(getClassLoaderResource("/tiff/ycbcr-cat.tif"), new Dimension(250, 325)), // YCbCr, LZW compressed
|
||||
new TestData(getClassLoaderResource("/tiff/smallliz.tif"), new Dimension(160, 160)), // YCbCr, Old-Style JPEG compressed (full JFIF stream)
|
||||
new TestData(getClassLoaderResource("/tiff/zackthecat.tif"), new Dimension(234, 213)) // YCbCr, Old-Style JPEG compressed (tables, no JFIF stream)
|
||||
);
|
||||
}
|
||||
|
||||
@ -89,4 +92,6 @@ public class TIFFImageReaderTest extends ImageReaderAbstractTestCase<TIFFImageRe
|
||||
protected List<String> getMIMETypes() {
|
||||
return Arrays.asList("image/tiff");
|
||||
}
|
||||
|
||||
// TODO: Test YCbCr colors
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ import java.io.InputStream;
|
||||
*/
|
||||
@Ignore
|
||||
public class YCbCrUpsamplerStreamTest extends InputStreamAbstractTestCase {
|
||||
// TODO: Implement
|
||||
// TODO: Implement + add @Ignore for all tests that makes no sense for this class.
|
||||
@Override
|
||||
protected InputStream makeInputStream(byte[] pBytes) {
|
||||
return new YCbCrUpsamplerStream(new ByteArrayInputStream(pBytes), new int[] {2, 2}, pBytes.length / 4, null);
|
||||
|
BIN
imageio/imageio-tiff/src/test/resources/tiff/smallliz.tif
Executable file
BIN
imageio/imageio-tiff/src/test/resources/tiff/smallliz.tif
Executable file
Binary file not shown.
BIN
imageio/imageio-tiff/src/test/resources/tiff/ycbcr-cat.tif
Normal file
BIN
imageio/imageio-tiff/src/test/resources/tiff/ycbcr-cat.tif
Normal file
Binary file not shown.
BIN
imageio/imageio-tiff/src/test/resources/tiff/zackthecat.tif
Normal file
BIN
imageio/imageio-tiff/src/test/resources/tiff/zackthecat.tif
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user