Fixed graphic glitches when changing screen

This commit is contained in:
Cavallium 2018-10-23 17:28:05 +02:00
parent 76d9d77e13
commit 87151ed606
113 changed files with 154 additions and 11660 deletions

2
Flow

@ -1 +1 @@
Subproject commit fc24680f8f2178fa550174c75d7dbacb6065358c
Subproject commit 59b89e9d165c3fc615ca44aa742b531caf13fcb6

View File

@ -44,6 +44,43 @@ public class MarioScreen extends Screen {
historyBehavior = HistoryBehavior.ALWAYS_KEEP_IN_HISTORY;
}
@Override
public void graphicInitialized() {
try {
if (MarioScreen.skin == null) {
MarioScreen.skin = Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine.loadSkin("/marioskin.png");
}
if (MarioScreen.groundskin == null) {
MarioScreen.groundskin = Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine.loadSkin("/marioground.png");
}
if (MarioScreen.gpuTest2 == null) {
try {
MarioScreen.gpuTest2 = Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine.loadFont("N:\\gputest\\gputest2");
} catch (final Exception ex) {}
}
if (MarioScreen.gpuTest1 == null) {
try {
MarioScreen.gpuTest1 = Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine.loadFont("N:\\gputest\\gputest12");
MarioScreen.gpuTest12 = true;
} catch (final Exception ex) {
MarioScreen.gpuTest12 = false;
try {
MarioScreen.gpuTest1 = Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine.loadFont("N:\\gputest\\gputest1");
} catch (final Exception ex2) {}
}
}
if (MarioScreen.gpuTest3 == null) {
try {
MarioScreen.gpuTest3 = Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine.loadSkin("N:\\gputest\\font_gputest3.png");
} catch (final Exception ex) {
ex.printStackTrace();
}
}
} catch (final IOException e) {
e.printStackTrace();
}
}
@Override
public void initialized() {
try {

View File

@ -43,13 +43,17 @@ public class TetrisScreen extends Screen {
@Override
public void initialized() {
StaticVars.windowZoom.onNext(2f);
}
@Override
public void graphicInitialized() {
try {
e = d.engine;
r = d.renderer;
if (TetrisScreen.skin == null) {
TetrisScreen.skin = Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine.loadSkin("/tetrisskin.png");
}
StaticVars.windowZoom.onNext(2f);
} catch (final IOException e) {
e.printStackTrace();
}

View File

@ -24,6 +24,12 @@ public class CalculatorHUD extends HUD {
}
@Override
public void graphicInitialized() throws InterruptedException {
// TODO Auto-generated method stub
}
@Override
public void render() {
// TODO Auto-generated method stub

View File

@ -242,10 +242,10 @@ public final class DisplayManager implements RenderingLoop {
screen.create();
}
this.screen = screen;
screenChange.release();
if (screen.initialized == false) {
screen.initialize();
}
screenChange.release();
} catch (final Exception e) {
e.printStackTrace();
Engine.getPlatform().exit(0);
@ -267,10 +267,10 @@ public final class DisplayManager implements RenderingLoop {
try {
screen.create();
this.screen = screen;
screenChange.release();
if (screen.initialized == false) {
screen.initialize();
}
screenChange.release();
} catch (final Exception e) {
e.printStackTrace();
Engine.getPlatform().exit(0);
@ -372,6 +372,13 @@ public final class DisplayManager implements RenderingLoop {
}
}
}
if (!screen.graphicInitialized) {
try {
screen.initializeGraphic();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
renderer.glClear(engine.getWidth(), engine.getHeight());
}
@ -439,7 +446,6 @@ public final class DisplayManager implements RenderingLoop {
setScreen(initialScreen);
initialScreen = null;
}
screen.initialize();
} catch (final Exception e) {
e.printStackTrace();
Engine.getPlatform().exit(0);

View File

@ -2,8 +2,10 @@ package it.cavallium.warppi.gui;
public interface GraphicalInterface {
void create() throws InterruptedException;
void initialize() throws InterruptedException;
void initializeGraphic() throws InterruptedException;
void render();

View File

@ -3,6 +3,7 @@ package it.cavallium.warppi.gui;
public abstract class HUD implements GraphicalInterface {
public DisplayManager d;
public boolean created = false;
public boolean graphicInitialized = false;
public boolean initialized = false;
public boolean visible = true;
@ -16,6 +17,14 @@ public abstract class HUD implements GraphicalInterface {
}
}
@Override
public void initializeGraphic() throws InterruptedException {
if (!graphicInitialized) {
graphicInitialized = true;
graphicInitialized();
}
}
@Override
public void create() throws InterruptedException {
if (!created) {
@ -26,6 +35,8 @@ public abstract class HUD implements GraphicalInterface {
public abstract void created() throws InterruptedException;
public abstract void graphicInitialized() throws InterruptedException;
public abstract void initialized() throws InterruptedException;
public abstract void renderBackground();

View File

@ -13,7 +13,15 @@ public interface Skin {
void use(GraphicEngine d);
/**
* May not be available before initialization
* @return skin width
*/
int getSkinWidth();
/**
* May not be available before initialization
* @return skin height
*/
int getSkinHeight();
}

View File

@ -27,6 +27,9 @@ public class ChooseVariableValueScreen extends Screen {
@Override
public void initialized() throws InterruptedException {}
@Override
public void graphicInitialized() throws InterruptedException {}
@Override
public void render() {
Utils.getFont(false, true).use(Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine);

View File

@ -19,6 +19,9 @@ public class EmptyScreen extends Screen {
@Override
public void initialized() throws InterruptedException {}
@Override
public void graphicInitialized() throws InterruptedException {}
@Override
public void render() {
// TODO Auto-generated method stub

View File

@ -27,6 +27,9 @@ public class KeyboardDebugScreen extends Screen {
@Override
public void initialized() throws InterruptedException {}
@Override
public void graphicInitialized() throws InterruptedException {}
@Override
public void render() {
final Renderer renderer = Engine.INSTANCE.getHardwareDevice().getDisplayManager().renderer;

View File

@ -33,6 +33,9 @@ public class LoadingScreen extends Screen {
Engine.INSTANCE.getHardwareDevice().getDisplayManager().getHUD().hide();
StaticVars.windowZoom.onNext(1f);
}
@Override
public void graphicInitialized() throws InterruptedException {}
@Override
public void beforeRender(final float dt) {

View File

@ -105,6 +105,11 @@ public class MathInputScreen extends Screen {
/* Fine caricamento */
}
@Override
public void graphicInitialized() throws InterruptedException {
/* Fine caricamento */
}
@Override
public void beforeRender(final float dt) {
if (Engine.INSTANCE.getHardwareDevice().getDisplayManager().error == null) {

View File

@ -10,6 +10,7 @@ public abstract class Screen implements KeyboardEventListener, TouchEventListene
public DisplayManager d;
public boolean created = false;
public boolean initialized = false;
public boolean graphicInitialized = false;
public HistoryBehavior historyBehavior = HistoryBehavior.NORMAL;
public static long lastDebugScreenID = 1;
@ -19,6 +20,14 @@ public abstract class Screen implements KeyboardEventListener, TouchEventListene
debugScreenID = lastDebugScreenID++;
}
@Override
public void initializeGraphic() throws InterruptedException {
if (!graphicInitialized) {
graphicInitialized = true;
graphicInitialized();
}
}
@Override
public void initialize() throws InterruptedException {
if (!initialized) {
@ -35,10 +44,30 @@ public abstract class Screen implements KeyboardEventListener, TouchEventListene
}
}
/**
* Called when creating the screen
* Called before initialized()
* Called before graphicInitialized()
* @throws InterruptedException
*/
public abstract void created() throws InterruptedException;
/**
* Load everything except skins, etc...
* Called after created()
* Called after graphicInitialized()
* @throws InterruptedException
*/
public abstract void initialized() throws InterruptedException;
/**
* Load skins, etc...
* Called after created()
* Called before initialized()
* @throws InterruptedException
*/
public abstract void graphicInitialized() throws InterruptedException;
@Override
public abstract void render();

View File

@ -23,6 +23,9 @@ public class SolveForXScreen extends Screen {
@Override
public void initialized() throws InterruptedException {}
@Override
public void graphicInitialized() throws InterruptedException {}
@Override
public void render() {
Engine.INSTANCE.getHardwareDevice().getDisplayManager().renderer.glColor4i(0, 0, 0, 64);

View File

@ -372,6 +372,7 @@ public class SwingWindow extends JFrame {
if (renderingLoop != null) {
renderingLoop.refresh();
final int[] a = ((DataBufferInt) display.g.getRaster().getDataBuffer()).getData();
SwingRenderer.canvas2d = a;
g.clearRect(0, 0, display.r.size[0] * mult, display.r.size[1] * mult);

View File

@ -261,7 +261,7 @@ public class JOGLRenderer implements Renderer {
}
static OpenedTextureData openTexture(final String file, final boolean isResource) throws GLException, IOException {
BufferedImage img = ImageIO.read(isResource ? JOGLRenderer.class.getResource("/" + file) : new File(file).toURI().toURL());
BufferedImage img = ImageIO.read(isResource ? JOGLRenderer.class.getResource(file) : new File(file).toURI().toURL());
File f;
if (isResource) {
f = Files.createTempFile("texture-", ".png").toFile();

View File

@ -483,7 +483,6 @@ class NEWTWindow implements GLEventListener {
final boolean linear = windowZoom % (int) windowZoom != 0f;
if (refreshViewport) {
System.err.println("[[[REFVP");
refreshViewport = false;
gl.glViewport(0, 0, realWindowSize[0], realWindowSize[1]);

View File

@ -1,208 +0,0 @@
package ar.com.hjg.pngj;
import java.io.IOException;
import java.io.InputStream;
/**
* Reads bytes from an input stream, and feeds a IBytesConsumer.
*/
public class BufferedStreamFeeder {
private InputStream stream;
private byte[] buf;
private int pendinglen; // bytes read and stored in buf that have not yet still been fed to
// IBytesConsumer
private int offset;
private boolean eof = false;
private boolean closeStream = true;
private boolean failIfNoFeed = false;
private static final int DEFAULTSIZE = 8192;
/** By default, the stream will be closed on close() */
public BufferedStreamFeeder(final InputStream is) {
this(is, BufferedStreamFeeder.DEFAULTSIZE);
}
public BufferedStreamFeeder(final InputStream is, final int bufsize) {
stream = is;
buf = new byte[bufsize < 1 ? BufferedStreamFeeder.DEFAULTSIZE : bufsize];
}
/**
* Returns inputstream
*
* @return Input Stream from which bytes are read
*/
public InputStream getStream() {
return stream;
}
/**
* Feeds bytes to the consumer <br>
* Returns bytes actually consumed <br>
* This should return 0 only if the stream is EOF or the consumer is done
*/
public int feed(final IBytesConsumer consumer) {
return feed(consumer, Integer.MAX_VALUE);
}
/**
* Feeds the consumer (with at most maxbytes) <br>
* Returns 0 only if the stream is EOF (or maxbytes=0). Returns negative is
* the consumer is done.<br>
* It can return less than maxbytes (that doesn't mean that the consumer or
* the input stream is done)
*/
public int feed(final IBytesConsumer consumer, final int maxbytes) {
if (pendinglen == 0)
refillBuffer();
final int tofeed = maxbytes >= 0 && maxbytes < pendinglen ? maxbytes : pendinglen;
int n = 0;
if (tofeed > 0) {
n = consumer.consume(buf, offset, tofeed);
if (n > 0) {
offset += n;
pendinglen -= n;
}
}
if (n < 1 && failIfNoFeed)
throw new PngjInputException("Failed to feed bytes (premature ending?)");
return n;
}
/**
* Feeds as much bytes as it can to the consumer, in a loop. <br>
* Returns bytes actually consumed <br>
* This will stop when either the input stream is eof, or when the consumer
* refuses to eat more bytes. The caller can
* distinguish both cases by calling {@link #hasMoreToFeed()}
*/
public long feedAll(final IBytesConsumer consumer) {
long n = 0;
while (hasMoreToFeed()) {
final int n1 = feed(consumer);
if (n1 < 1)
break;
n += n1;
}
return n;
}
/**
* Feeds exactly nbytes, retrying if necessary
*
* @param consumer
* Consumer
* @param nbytes
* Number of bytes
* @return true if success, false otherwise (EOF on stream, or consumer is
* done)
*/
public boolean feedFixed(final IBytesConsumer consumer, final int nbytes) {
int remain = nbytes;
while (remain > 0) {
final int n = feed(consumer, remain);
if (n < 1)
return false;
remain -= n;
}
return true;
}
/**
* If there are not pending bytes to be consumed tries to fill the buffer
* with bytes from the stream.
*/
protected void refillBuffer() {
if (pendinglen > 0 || eof)
return; // only if not pending data
try {
// try to read
offset = 0;
pendinglen = stream.read(buf);
if (pendinglen < 0) {
close();
return;
} else
return;
} catch (final IOException e) {
throw new PngjInputException(e);
}
}
/**
* Returuns true if we have more data to fed the consumer. This internally
* tries to grabs more bytes from the stream
* if necessary
*/
public boolean hasMoreToFeed() {
if (eof)
return pendinglen > 0;
else
refillBuffer();
return pendinglen > 0;
}
/**
* @param closeStream
* If true, the underlying stream will be closed on when close()
* is called
*/
public void setCloseStream(final boolean closeStream) {
this.closeStream = closeStream;
}
/**
* Closes this object.
*
* Sets EOF=true, and closes the stream if <tt>closeStream</tt> is true
*
* This can be called internally, or from outside.
*
* Idempotent, secure, never throws exception.
**/
public void close() {
eof = true;
buf = null;
pendinglen = 0;
offset = 0;
if (stream != null && closeStream)
try {
stream.close();
} catch (final Exception e) {
// PngHelperInternal.LOGGER.log(Level.WARNING, "Exception closing stream", e);
}
stream = null;
}
/**
* Sets a new underlying inputstream. This allows to reuse this object. The
* old underlying is not closed and the state
* is not reset (you should call close() previously if you want that)
*
* @param is
*/
public void setInputStream(final InputStream is) { // to reuse this object
stream = is;
eof = false;
}
/**
* @return EOF on stream, or close() was called
*/
public boolean isEof() {
return eof;
}
/**
* If this flag is set (default: false), any call to feed() that returns
* zero (no byte feed) will throw an exception.
* This is useful to be sure of avoid infinite loops in some scenarios.
*
* @param failIfNoFeed
*/
public void setFailIfNoFeed(final boolean failIfNoFeed) {
this.failIfNoFeed = failIfNoFeed;
}
}

View File

@ -1,231 +0,0 @@
package ar.com.hjg.pngj;
import ar.com.hjg.pngj.chunks.ChunkRaw;
/**
* Parses a PNG chunk, consuming bytes in one mode:
* {@link ChunkReaderMode#BUFFER}, {@link ChunkReaderMode#PROCESS},
* {@link ChunkReaderMode#SKIP}.
* <p>
* It calls {@link #chunkDone()} when done. Also calls
* {@link #processData(byte[], int, int)} if <code>PROCESS</code>
* mode. Apart from thas, it's totally agnostic (it doesn't know about IDAT
* chunks, or PNG general structure)
* <p>
* The object wraps a ChunkRaw instance (content filled only if BUFFER mode); it
* should be short lived (one instance
* created for each chunk, and discarded after reading), but the wrapped
* chunkRaw can be (usually is) long lived.
*/
public abstract class ChunkReader {
/**
* see {@link ChunkReaderMode}
*/
public final ChunkReaderMode mode;
private final ChunkRaw chunkRaw;
private boolean crcCheck; // by default, this is false for SKIP, true elsewhere
/**
* How many bytes have been read for this chunk, data only
*/
protected int read = 0;
private int crcn = 0; // how many bytes have been read from crc
/**
* Modes of ChunkReader chunk processing.
*/
public enum ChunkReaderMode {
/**
* Stores full chunk data in buffer
*/
BUFFER,
/**
* Does not store content, processes on the fly, calling processData()
* for each partial read
*/
PROCESS,
/**
* Does not store nor process - implies crcCheck=false (by default).
*/
SKIP;
}
/**
* The constructor creates also a chunkRaw, preallocated if mode =
* ChunkReaderMode.BUFFER
*
* @param clen
* @param id
* @param offsetInPng
* Informational, is stored in chunkRaw
* @param mode
*/
public ChunkReader(final int clen, final String id, final long offsetInPng, final ChunkReaderMode mode) {
if (mode == null || id.length() != 4 || clen < 0)
throw new PngjExceptionInternal("Bad chunk paramenters: " + mode);
this.mode = mode;
chunkRaw = new ChunkRaw(clen, id, mode == ChunkReaderMode.BUFFER);
chunkRaw.setOffset(offsetInPng);
crcCheck = mode == ChunkReaderMode.SKIP ? false : true; // can be changed with setter
}
/**
* Returns raw chunk (data can be empty or not, depending on
* ChunkReaderMode)
*
* @return Raw chunk - never null
*/
public ChunkRaw getChunkRaw() {
return chunkRaw;
}
/**
* Consumes data for the chunk (data and CRC). This never consumes more
* bytes than for this chunk.
*
* In HOT_PROCESS can call processData() (not more than once)
*
* If this ends the chunk (included CRC) it checks CRC (if checking) and
* calls chunkDone()
*
* @param buf
* @param off
* @param len
* @return How many bytes have been consumed
*/
public final int feedBytes(final byte[] buf, int off, int len) {
if (len == 0)
return 0;
if (len < 0)
throw new PngjException("negative length??");
if (read == 0 && crcn == 0 && crcCheck)
chunkRaw.updateCrc(chunkRaw.idbytes, 0, 4); // initializes crc calculation with the Chunk ID
int bytesForData = chunkRaw.len - read; // bytesForData : bytes to be actually read from chunk data
if (bytesForData > len)
bytesForData = len;
// we want to call processData even for empty chunks (IEND:bytesForData=0) at least once
if (bytesForData > 0 || crcn == 0) {
// in buffer mode we compute the CRC at the end
if (crcCheck && mode != ChunkReaderMode.BUFFER && bytesForData > 0)
chunkRaw.updateCrc(buf, off, bytesForData);
if (mode == ChunkReaderMode.BUFFER) {
// just copy the contents to the internal buffer
if (chunkRaw.data != buf && bytesForData > 0)
// if the buffer passed if the same as this one, we don't copy the caller should know what he's doing
System.arraycopy(buf, off, chunkRaw.data, read, bytesForData);
} else if (mode == ChunkReaderMode.PROCESS)
processData(read, buf, off, bytesForData);
else {
// mode == ChunkReaderMode.SKIP; nothing to do
}
read += bytesForData;
off += bytesForData;
len -= bytesForData;
}
int crcRead = 0;
if (read == chunkRaw.len) { // data done - read crc?
crcRead = 4 - crcn;
if (crcRead > len)
crcRead = len;
if (crcRead > 0) {
if (buf != chunkRaw.crcval)
System.arraycopy(buf, off, chunkRaw.crcval, crcn, crcRead);
crcn += crcRead;
if (crcn == 4) {
if (crcCheck) {
if (mode == ChunkReaderMode.BUFFER)
chunkRaw.updateCrc(chunkRaw.data, 0, chunkRaw.len);
chunkRaw.checkCrc();
}
chunkDone();
}
}
}
return bytesForData + crcRead;
}
/**
* Chunks has been read
*
* @return true if we have read all chunk, including trailing CRC
*/
public final boolean isDone() {
return crcn == 4; // has read all 4 bytes from the crc
}
/**
* Determines if CRC should be checked. This should be called before
* starting reading.
*
* @param crcCheck
*/
public void setCrcCheck(final boolean crcCheck) {
if (read != 0 && crcCheck && !this.crcCheck)
throw new PngjException("too late!");
this.crcCheck = crcCheck;
}
/**
* This method will only be called in PROCESS mode, probably several times,
* each time with a new fragment of data
* inside the chunk. For chunks with zero-length data, this will still be
* called once.
*
* It's guaranteed that the data corresponds exclusively to this chunk data
* (no crc, no data from no other chunks, )
*
* @param offsetInchunk
* data bytes that had already been read/processed for this chunk
* @param buf
* @param off
* @param len
*/
protected abstract void processData(int offsetInchunk, byte[] buf, int off, int len);
/**
* This method will be called (in all modes) when the full chunk -including
* crc- has been read
*/
protected abstract void chunkDone();
public boolean isFromDeflatedSet() {
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (chunkRaw == null ? 0 : chunkRaw.hashCode());
return result;
}
/**
* Equality (and hash) is basically delegated to the ChunkRaw
*/
@Override
public boolean equals(final Object obj) { // delegates to chunkraw
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final ChunkReader other = (ChunkReader) obj;
if (chunkRaw == null) {
if (other.chunkRaw != null)
return false;
} else if (!chunkRaw.equals(other.chunkRaw))
return false;
return true;
}
@Override
public String toString() {
return chunkRaw.toString();
}
}

View File

@ -1,30 +0,0 @@
package ar.com.hjg.pngj;
/**
* This loads the png as a plain sequence of chunks, buffering all
*
* Useful to do things like insert or delete a ancilllary chunk. This does not
* distinguish IDAT from others
**/
public class ChunkSeqBuffering extends ChunkSeqReader {
protected boolean checkCrc = true;
public ChunkSeqBuffering() {
super();
}
@Override
protected boolean isIdatKind(final String id) {
return false;
}
@Override
protected boolean shouldCheckCrc(final int len, final String id) {
return checkCrc;
}
public void setCheckCrc(final boolean checkCrc) {
this.checkCrc = checkCrc;
}
}

View File

@ -1,432 +0,0 @@
package ar.com.hjg.pngj;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Arrays;
import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode;
import ar.com.hjg.pngj.chunks.ChunkHelper;
/**
* Consumes a stream of bytes that consist of a series of PNG-like chunks.
* <p>
* This has little intelligence, it's quite low-level and general (it could even
* be used for a MNG stream, for example).
* It supports signature recognition and idat deflate
*/
public class ChunkSeqReader implements IBytesConsumer {
protected static final int SIGNATURE_LEN = 8;
protected final boolean withSignature;
private final byte[] buf0 = new byte[8]; // for signature or chunk starts
private int buf0len = 0;
private boolean signatureDone = false;
private boolean done = false; // ended, normally or not
private int chunkCount = 0;
private long bytesCount = 0;
private DeflatedChunksSet curReaderDeflatedSet; // one instance is created for each
// "idat-like set". Normally one.
private ChunkReader curChunkReader;
private long idatBytes; // this is only for the IDAT (not mrerely "idat-like")
/**
* Creates a ChunkSeqReader (with signature)
*/
public ChunkSeqReader() {
this(true);
}
/**
* @param withSignature
* If true, the stream is assumed be prepended by 8 bit signature
*/
public ChunkSeqReader(final boolean withSignature) {
this.withSignature = withSignature;
signatureDone = !withSignature;
}
/**
* Consumes (in general, partially) a number of bytes. A single call never
* involves more than one chunk.
*
* When the signature is read, it calls checkSignature()
*
* When the start of a chunk is detected, it calls
* {@link #startNewChunk(int, String, long)}
*
* When data from a chunk is being read, it delegates to
* {@link ChunkReader#feedBytes(byte[], int, int)}
*
* The caller might want to call this method more than once in succesion
*
* This should rarely be overriden
*
* @param buffer
* @param offset
* Offset in buffer
* @param len
* Valid bytes that can be consumed
* @return processed bytes, in the 1-len range. -1 if done. Only returns 0
* if len=0.
**/
@Override
public int consume(final byte[] buffer, final int offset, final int len) {
if (done)
return -1;
if (len == 0)
return 0; // nothing to do
if (len < 0)
throw new PngjInputException("Bad len: " + len);
int processed = 0;
if (signatureDone) {
if (curChunkReader == null || curChunkReader.isDone()) { // new chunk: read first 8 bytes
int read0 = 8 - buf0len;
if (read0 > len)
read0 = len;
System.arraycopy(buffer, offset, buf0, buf0len, read0);
buf0len += read0;
processed += read0;
bytesCount += read0;
// len -= read0;
// offset += read0;
if (buf0len == 8) { // end reading chunk length and id
chunkCount++;
final int clen = PngHelperInternal.readInt4fromBytes(buf0, 0);
final String cid = ChunkHelper.toString(buf0, 4, 4);
startNewChunk(clen, cid, bytesCount - 8);
buf0len = 0;
}
} else { // reading chunk, delegates to curChunkReader
final int read1 = curChunkReader.feedBytes(buffer, offset, len);
processed += read1;
bytesCount += read1;
}
} else { // reading signature
int read = ChunkSeqReader.SIGNATURE_LEN - buf0len;
if (read > len)
read = len;
System.arraycopy(buffer, offset, buf0, buf0len, read);
buf0len += read;
if (buf0len == ChunkSeqReader.SIGNATURE_LEN) {
checkSignature(buf0);
buf0len = 0;
signatureDone = true;
}
processed += read;
bytesCount += read;
}
return processed;
}
/**
* Trys to feeds exactly <tt>len</tt> bytes, calling
* {@link #consume(byte[], int, int)} retrying if necessary.
*
* This should only be used in callback mode
*
* @return true if succceded
*/
public boolean feedAll(final byte[] buf, int off, int len) {
while (len > 0) {
final int n = consume(buf, off, len);
if (n < 1)
return false;
len -= n;
off += n;
}
return true;
}
/**
* Called for all chunks when a chunk start has been read (id and length),
* before the chunk data itself is read. It
* creates a new ChunkReader (field accesible via
* {@link #getCurChunkReader()}) in the corresponding mode, and
* eventually a curReaderDeflatedSet.(field accesible via
* {@link #getCurReaderDeflatedSet()})
*
* To decide the mode and options, it calls
* {@link #shouldCheckCrc(int, String)},
* {@link #shouldSkipContent(int, String)}, {@link #isIdatKind(String)}.
* Those methods should be overriden in
* preference to this; if overriden, this should be called first.
*
* The respective {@link ChunkReader#chunkDone()} method is directed to this
* {@link #postProcessChunk(ChunkReader)}.
*
* Instead of overriding this, see also
* {@link #createChunkReaderForNewChunk(String, int, long, boolean)}
*/
protected void startNewChunk(final int len, final String id, final long offset) {
if (id.equals(ChunkHelper.IDAT))
idatBytes += len;
final boolean checkCrc = shouldCheckCrc(len, id);
final boolean skip = shouldSkipContent(len, id);
final boolean isIdatType = isIdatKind(id);
// PngHelperInternal.debug("start new chunk id=" + id + " off=" + offset + " skip=" + skip + " idat=" +
// isIdatType);
// first see if we should terminate an active curReaderDeflatedSet
boolean forCurrentIdatSet = false;
if (curReaderDeflatedSet != null)
forCurrentIdatSet = curReaderDeflatedSet.ackNextChunkId(id);
if (isIdatType && !skip) { // IDAT non skipped: create a DeflatedChunkReader owned by a idatSet
if (!forCurrentIdatSet) {
if (curReaderDeflatedSet != null && !curReaderDeflatedSet.isDone())
throw new PngjInputException("new IDAT-like chunk when previous was not done");
curReaderDeflatedSet = createIdatSet(id);
}
curChunkReader = new DeflatedChunkReader(len, id, checkCrc, offset, curReaderDeflatedSet) {
@Override
protected void chunkDone() {
super.chunkDone();
postProcessChunk(this);
}
};
} else { // for non-idat chunks (or skipped idat like)
curChunkReader = createChunkReaderForNewChunk(id, len, offset, skip);
if (!checkCrc)
curChunkReader.setCrcCheck(false);
}
}
/**
* This will be called for all chunks (even skipped), except for IDAT-like
* non-skiped chunks
*
* The default behaviour is to create a ChunkReader in BUFFER mode (or SKIP
* if skip==true) that calls
* {@link #postProcessChunk(ChunkReader)} (always) when done.
*
* @param id
* Chunk id
* @param len
* Chunk length
* @param offset
* offset inside PNG stream , merely informative
* @param skip
* flag: is true, the content will not be buffered (nor
* processed)
* @return a newly created ChunkReader that will create the ChunkRaw and
* then discarded
*/
protected ChunkReader createChunkReaderForNewChunk(final String id, final int len, final long offset,
final boolean skip) {
return new ChunkReader(len, id, offset, skip ? ChunkReaderMode.SKIP : ChunkReaderMode.BUFFER) {
@Override
protected void chunkDone() {
postProcessChunk(this);
}
@Override
protected void processData(final int offsetinChhunk, final byte[] buf, final int off, final int len) {
throw new PngjExceptionInternal("should never happen");
}
};
}
/**
* This is called after a chunk is read, in all modes
*
* This implementation only chenks the id of the first chunk, and process
* the IEND chunk (sets done=true)
**
* Further processing should be overriden (call this first!)
**/
protected void postProcessChunk(final ChunkReader chunkR) { // called after chunk is read
if (chunkCount == 1) {
final String cid = firstChunkId();
if (cid != null && !cid.equals(chunkR.getChunkRaw().id))
throw new PngjInputException("Bad first chunk: " + chunkR.getChunkRaw().id + " expected: " + firstChunkId());
}
if (chunkR.getChunkRaw().id.equals(endChunkId()))
done = true;
}
/**
* DeflatedChunksSet factory. This implementation is quite dummy, it usually
* should be overriden.
*/
protected DeflatedChunksSet createIdatSet(final String id) {
return new DeflatedChunksSet(id, 1024, 1024); // sizes: arbitrary This should normally be
// overriden
}
/**
* Decides if this Chunk is of "IDAT" kind (in concrete: if it is, and if
* it's not to be skiped, a DeflatedChunksSet
* will be created to deflate it and process+ the deflated data)
*
* This implementation always returns always false
*
* @param id
*/
protected boolean isIdatKind(final String id) {
return false;
}
/**
* Chunks can be skipped depending on id and/or length. Skipped chunks are
* still processed, but their data will be
* null, and CRC will never checked
*
* @param len
* @param id
*/
protected boolean shouldSkipContent(final int len, final String id) {
return false;
}
protected boolean shouldCheckCrc(final int len, final String id) {
return true;
}
/**
* Throws PngjInputException if bad signature
*
* @param buf
* Signature. Should be of length 8
*/
protected void checkSignature(final byte[] buf) {
if (!Arrays.equals(buf, PngHelperInternal.getPngIdSignature()))
throw new PngjInputException("Bad PNG signature");
}
/**
* If false, we are still reading the signature
*
* @return true if signature has been read (or if we don't have signature)
*/
public boolean isSignatureDone() {
return signatureDone;
}
/**
* If true, we either have processe the IEND chunk, or close() has been
* called, or a fatal error has happened
*/
public boolean isDone() {
return done;
}
/**
* total of bytes read (buffered or not)
*/
public long getBytesCount() {
return bytesCount;
}
/**
* @return Chunks already read, including partial reading (currently
* reading)
*/
public int getChunkCount() {
return chunkCount;
}
/**
* Currently reading chunk, or just ended reading
*
* @return null only if still reading signature
*/
public ChunkReader getCurChunkReader() {
return curChunkReader;
}
/**
* The latest deflated set (typically IDAT chunks) reader. Notice that there
* could be several idat sets (eg for APNG)
*/
public DeflatedChunksSet getCurReaderDeflatedSet() {
return curReaderDeflatedSet;
}
/**
* Closes this object and release resources. For normal termination or
* abort. Secure and idempotent.
*/
public void close() { // forced closing
if (curReaderDeflatedSet != null)
curReaderDeflatedSet.close();
done = true;
}
/**
* Returns true if we are not in middle of a chunk: we have just ended
* reading past chunk , or we are at the start, or
* end of signature, or we are done
*/
public boolean isAtChunkBoundary() {
return bytesCount == 0 || bytesCount == 8 || done || curChunkReader == null || curChunkReader.isDone();
}
/**
* Which should be the id of the first chunk
*
* @return null if you don't want to check it
*/
protected String firstChunkId() {
return "IHDR";
}
/**
* Helper method, reports amount of bytes inside IDAT chunks.
*
* @return Bytes in IDAT chunks
*/
public long getIdatBytes() {
return idatBytes;
}
/**
* Which should be the id of the last chunk
*
* @return "IEND"
*/
protected String endChunkId() {
return "IEND";
}
/**
* Reads all content from a file. Helper method, only for callback mode
*/
public void feedFromFile(final File f) {
try {
feedFromInputStream(new FileInputStream(f), true);
} catch (final FileNotFoundException e) {
throw new PngjInputException(e.getMessage());
}
}
/**
* Reads all content from an input stream. Helper method, only for callback
* mode
*
* @param is
* @param closeStream
* Closes the input stream when done (or if error)
*/
public void feedFromInputStream(final InputStream is, final boolean closeStream) {
final BufferedStreamFeeder sf = new BufferedStreamFeeder(is);
sf.setCloseStream(closeStream);
try {
sf.feedAll(this);
} finally {
close();
sf.close();
}
}
public void feedFromInputStream(final InputStream is) {
feedFromInputStream(is, true);
}
}

View File

@ -1,310 +0,0 @@
package ar.com.hjg.pngj;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode;
import ar.com.hjg.pngj.chunks.ChunkFactory;
import ar.com.hjg.pngj.chunks.ChunkHelper;
import ar.com.hjg.pngj.chunks.ChunkLoadBehaviour;
import ar.com.hjg.pngj.chunks.ChunksList;
import ar.com.hjg.pngj.chunks.PngChunk;
import ar.com.hjg.pngj.chunks.PngChunkIDAT;
import ar.com.hjg.pngj.chunks.PngChunkIEND;
import ar.com.hjg.pngj.chunks.PngChunkIHDR;
import ar.com.hjg.pngj.chunks.PngChunkPLTE;
/**
* Adds to ChunkSeqReader the storing of PngChunk, with a PngFactory, and
* imageInfo + deinterlacer.
* <p>
* Most usual PNG reading should use this class, or a {@link PngReader}, which
* is a thin wrapper over this.
*/
public class ChunkSeqReaderPng extends ChunkSeqReader {
protected ImageInfo imageInfo; // initialized at parsing the IHDR
protected ImageInfo curImageInfo; // can vary, for apng
protected Deinterlacer deinterlacer;
protected int currentChunkGroup = -1;
/**
* All chunks, but some of them can have the buffer empty (IDAT and skipped)
*/
protected ChunksList chunksList = null;
protected final boolean callbackMode;
private long bytesAncChunksLoaded = 0; // bytes loaded from buffered chunks non-critical chunks (data only)
private boolean checkCrc = true;
// --- parameters to be set prior to reading ---
private boolean includeNonBufferedChunks = false;
private final Set<String> chunksToSkip = new HashSet<>();
private long maxTotalBytesRead = 0;
private long skipChunkMaxSize = 0;
private long maxBytesMetadata = 0;
private IChunkFactory chunkFactory;
private ChunkLoadBehaviour chunkLoadBehaviour = ChunkLoadBehaviour.LOAD_CHUNK_ALWAYS;
public ChunkSeqReaderPng(final boolean callbackMode) {
super();
this.callbackMode = callbackMode;
chunkFactory = new ChunkFactory(); // default factory
}
private void updateAndCheckChunkGroup(final String id) {
if (id.equals(PngChunkIHDR.ID)) { // IDHR
if (currentChunkGroup < 0)
currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR;
else
throw new PngjInputException("unexpected chunk " + id);
} else if (id.equals(PngChunkPLTE.ID)) { // PLTE
if (currentChunkGroup == ChunksList.CHUNK_GROUP_0_IDHR || currentChunkGroup == ChunksList.CHUNK_GROUP_1_AFTERIDHR)
currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE;
else
throw new PngjInputException("unexpected chunk " + id);
} else if (id.equals(PngChunkIDAT.ID)) { // IDAT (no necessarily the first)
if (currentChunkGroup >= ChunksList.CHUNK_GROUP_0_IDHR && currentChunkGroup <= ChunksList.CHUNK_GROUP_4_IDAT)
currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT;
else
throw new PngjInputException("unexpected chunk " + id);
} else if (id.equals(PngChunkIEND.ID)) { // END
if (currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT)
currentChunkGroup = ChunksList.CHUNK_GROUP_6_END;
else
throw new PngjInputException("unexpected chunk " + id);
} else if (currentChunkGroup <= ChunksList.CHUNK_GROUP_1_AFTERIDHR)
currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR;
else if (currentChunkGroup <= ChunksList.CHUNK_GROUP_3_AFTERPLTE)
currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE;
else
currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT;
}
@Override
public boolean shouldSkipContent(final int len, final String id) {
if (super.shouldSkipContent(len, id))
return true;
if (ChunkHelper.isCritical(id))
return false;// critical chunks are never skipped
if (maxTotalBytesRead > 0 && len + getBytesCount() > maxTotalBytesRead)
throw new PngjInputException("Maximum total bytes to read exceeeded: " + maxTotalBytesRead + " offset:" + getBytesCount() + " len=" + len);
if (chunksToSkip.contains(id))
return true; // specific skip
if (skipChunkMaxSize > 0 && len > skipChunkMaxSize)
return true; // too big chunk
if (maxBytesMetadata > 0 && len > maxBytesMetadata - bytesAncChunksLoaded)
return true; // too much ancillary chunks loaded
switch (chunkLoadBehaviour) {
case LOAD_CHUNK_IF_SAFE:
if (!ChunkHelper.isSafeToCopy(id))
return true;
break;
case LOAD_CHUNK_NEVER:
return true;
default:
break;
}
return false;
}
public long getBytesChunksLoaded() {
return bytesAncChunksLoaded;
}
public int getCurrentChunkGroup() {
return currentChunkGroup;
}
public void setChunksToSkip(final String... chunksToSkip) {
this.chunksToSkip.clear();
for (final String c : chunksToSkip)
this.chunksToSkip.add(c);
}
public void addChunkToSkip(final String chunkToSkip) {
chunksToSkip.add(chunkToSkip);
}
public void dontSkipChunk(final String chunkToSkip) {
chunksToSkip.remove(chunkToSkip);
}
public boolean firstChunksNotYetRead() {
return getCurrentChunkGroup() < ChunksList.CHUNK_GROUP_4_IDAT;
}
@Override
protected void postProcessChunk(final ChunkReader chunkR) {
super.postProcessChunk(chunkR);
if (chunkR.getChunkRaw().id.equals(PngChunkIHDR.ID)) {
final PngChunkIHDR ch = new PngChunkIHDR(null);
ch.parseFromRaw(chunkR.getChunkRaw());
imageInfo = ch.createImageInfo();
curImageInfo = imageInfo;
if (ch.isInterlaced())
deinterlacer = new Deinterlacer(curImageInfo);
chunksList = new ChunksList(imageInfo);
}
if (chunkR.mode == ChunkReaderMode.BUFFER && countChunkTypeAsAncillary(chunkR.getChunkRaw().id))
bytesAncChunksLoaded += chunkR.getChunkRaw().len;
if (chunkR.mode == ChunkReaderMode.BUFFER || includeNonBufferedChunks) {
final PngChunk chunk = chunkFactory.createChunk(chunkR.getChunkRaw(), getImageInfo());
chunksList.appendReadChunk(chunk, currentChunkGroup);
}
if (isDone())
processEndPng();
}
protected boolean countChunkTypeAsAncillary(final String id) {
return !ChunkHelper.isCritical(id);
}
@Override
protected DeflatedChunksSet createIdatSet(final String id) {
final IdatSet ids = new IdatSet(id, getCurImgInfo(), deinterlacer);
ids.setCallbackMode(callbackMode);
return ids;
}
public IdatSet getIdatSet() {
final DeflatedChunksSet c = getCurReaderDeflatedSet();
return c instanceof IdatSet ? (IdatSet) c : null;
}
@Override
protected boolean isIdatKind(final String id) {
return id.equals(PngChunkIDAT.ID);
}
@Override
public int consume(final byte[] buf, final int off, final int len) {
return super.consume(buf, off, len);
}
/**
* sets a custom chunk factory. This is typically called with a custom class
* extends ChunkFactory, to adds custom
* chunks to the default well-know ones
*
* @param chunkFactory
*/
public void setChunkFactory(final IChunkFactory chunkFactory) {
this.chunkFactory = chunkFactory;
}
/**
* Things to be done after IEND processing. This is not called if
* prematurely closed.
*/
protected void processEndPng() {
// nothing to do
}
public ImageInfo getImageInfo() {
return imageInfo;
}
public boolean isInterlaced() {
return deinterlacer != null;
}
public Deinterlacer getDeinterlacer() {
return deinterlacer;
}
@Override
protected void startNewChunk(final int len, final String id, final long offset) {
updateAndCheckChunkGroup(id);
super.startNewChunk(len, id, offset);
}
@Override
public void close() {
if (currentChunkGroup != ChunksList.CHUNK_GROUP_6_END)// this could only happen if forced close
currentChunkGroup = ChunksList.CHUNK_GROUP_6_END;
super.close();
}
public List<PngChunk> getChunks() {
return chunksList.getChunks();
}
public void setMaxTotalBytesRead(final long maxTotalBytesRead) {
this.maxTotalBytesRead = maxTotalBytesRead;
}
public long getSkipChunkMaxSize() {
return skipChunkMaxSize;
}
public void setSkipChunkMaxSize(final long skipChunkMaxSize) {
this.skipChunkMaxSize = skipChunkMaxSize;
}
public long getMaxBytesMetadata() {
return maxBytesMetadata;
}
public void setMaxBytesMetadata(final long maxBytesMetadata) {
this.maxBytesMetadata = maxBytesMetadata;
}
public long getMaxTotalBytesRead() {
return maxTotalBytesRead;
}
@Override
protected boolean shouldCheckCrc(final int len, final String id) {
return checkCrc;
}
public boolean isCheckCrc() {
return checkCrc;
}
public void setCheckCrc(final boolean checkCrc) {
this.checkCrc = checkCrc;
}
public boolean isCallbackMode() {
return callbackMode;
}
public Set<String> getChunksToSkip() {
return chunksToSkip;
}
public void setChunkLoadBehaviour(final ChunkLoadBehaviour chunkLoadBehaviour) {
this.chunkLoadBehaviour = chunkLoadBehaviour;
}
public ImageInfo getCurImgInfo() {
return curImageInfo;
}
public void updateCurImgInfo(final ImageInfo iminfo) {
if (!iminfo.equals(curImageInfo))
curImageInfo = iminfo;
if (deinterlacer != null)
deinterlacer = new Deinterlacer(curImageInfo); // we could reset it, but...
}
/**
* If true, the chunks with no data (because skipped or because processed
* like IDAT-type) are still stored in the
* PngChunks list, which might be more informative.
*
* Setting this to false saves a few bytes
*
* Default: false
*
* @param includeNonBufferedChunks
*/
public void setIncludeNonBufferedChunks(final boolean includeNonBufferedChunks) {
this.includeNonBufferedChunks = includeNonBufferedChunks;
}
}

View File

@ -1,76 +0,0 @@
package ar.com.hjg.pngj;
import java.util.ArrayList;
import java.util.List;
import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode;
import ar.com.hjg.pngj.chunks.ChunkRaw;
/**
* This simple reader skips all chunks contents and stores the chunkRaw in a
* list. Useful to read chunks structure.
*
* Optionally the contents might be processed. This doesn't distinguish IDAT
* chunks
*/
public class ChunkSeqSkipping extends ChunkSeqReader {
private final List<ChunkRaw> chunks = new ArrayList<>();
private boolean skip = true;
/**
* @param skipAll
* if true, contents will be truly skipped, and CRC will not be
* computed
*/
public ChunkSeqSkipping(final boolean skipAll) {
super(true);
skip = skipAll;
}
public ChunkSeqSkipping() {
this(true);
}
@Override
protected ChunkReader createChunkReaderForNewChunk(final String id, final int len, final long offset,
final boolean skip) {
return new ChunkReader(len, id, offset, skip ? ChunkReaderMode.SKIP : ChunkReaderMode.PROCESS) {
@Override
protected void chunkDone() {
postProcessChunk(this);
}
@Override
protected void processData(final int offsetinChhunk, final byte[] buf, final int off, final int len) {
processChunkContent(getChunkRaw(), offsetinChhunk, buf, off, len);
}
};
}
protected void processChunkContent(final ChunkRaw chunkRaw, final int offsetinChhunk, final byte[] buf,
final int off, final int len) {
// does nothing
}
@Override
protected void postProcessChunk(final ChunkReader chunkR) {
super.postProcessChunk(chunkR);
chunks.add(chunkR.getChunkRaw());
}
@Override
protected boolean shouldSkipContent(final int len, final String id) {
return skip;
}
@Override
protected boolean isIdatKind(final String id) {
return false;
}
public List<ChunkRaw> getChunks() {
return chunks;
}
}

View File

@ -1,78 +0,0 @@
package ar.com.hjg.pngj;
import ar.com.hjg.pngj.chunks.PngChunkFDAT;
/**
*
* Specialization of ChunkReader, for IDAT-like chunks. These chunks are part of
* a set of similar chunks (contiguos
* normally, not necessariyl) which conforms a zlib stream
*/
public class DeflatedChunkReader extends ChunkReader {
protected final DeflatedChunksSet deflatedChunksSet;
protected boolean alsoBuffer = false;
protected boolean skipBytes = false; // fDAT (APNG) skips 4 bytes)
protected byte[] skippedBytes; // only for fDAT
protected int seqNumExpected = -1; // only for fDAT
public DeflatedChunkReader(final int clen, final String chunkid, final boolean checkCrc, final long offsetInPng, final DeflatedChunksSet iDatSet) {
super(clen, chunkid, offsetInPng, ChunkReaderMode.PROCESS);
deflatedChunksSet = iDatSet;
if (chunkid.equals(PngChunkFDAT.ID)) {
skipBytes = true;
skippedBytes = new byte[4];
}
iDatSet.appendNewChunk(this);
}
/**
* Delegates to ChunkReaderDeflatedSet.processData()
*/
@Override
protected void processData(final int offsetInchunk, final byte[] buf, int off, int len) {
if (skipBytes && offsetInchunk < 4)
for (int oc = offsetInchunk; oc < 4 && len > 0; oc++, off++, len--)
skippedBytes[oc] = buf[off];
if (len > 0) { // delegate to idatSet
deflatedChunksSet.processBytes(buf, off, len);
if (alsoBuffer)
System.arraycopy(buf, off, getChunkRaw().data, read, len);
}
}
/**
* only a stupid check for fDAT (I wonder how many APGN readers do this)
*/
@Override
protected void chunkDone() {
if (skipBytes && getChunkRaw().id.equals(PngChunkFDAT.ID))
if (seqNumExpected >= 0) {
final int seqNum = PngHelperInternal.readInt4fromBytes(skippedBytes, 0);
if (seqNum != seqNumExpected)
throw new PngjInputException("bad chunk sequence for fDAT chunk " + seqNum + " expected " + seqNumExpected);
}
}
@Override
public boolean isFromDeflatedSet() {
return true;
}
/**
* In some rare cases you might want to also buffer the data?
*/
public void setAlsoBuffer() {
if (read > 0)
throw new RuntimeException("too late");
alsoBuffer = true;
getChunkRaw().allocData();
}
/** only relevant for fDAT */
public void setSeqNumExpected(final int seqNumExpected) {
this.seqNumExpected = seqNumExpected;
}
}

View File

@ -1,422 +0,0 @@
package ar.com.hjg.pngj;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
/**
* A set of IDAT-like chunks which, concatenated, form a zlib stream.
* <p>
* The inflated stream is intented to be read as a sequence of "rows", of which
* the caller knows the lengths (not
* necessary equal) and number.
* <p>
* Eg: For IDAT non-interlaced images, a row has bytesPerRow + 1 filter byte<br>
* For interlaced images, the lengths are variable.
* <p>
* This class can work in sync (polled) mode or async (callback) mode. But for
* callback mode the method
* processRowCallback() must be overriden
* <p>
* See {@link IdatSet}, which is mostly used and has a slightly simpler use.<br>
* See <code>DeflatedChunkSetTest</code> for example of use.
*/
public class DeflatedChunksSet {
protected byte[] row; // a "row" here means a raw (uncopressed filtered) part of the IDAT stream,
// normally a image row (or subimage row for interlaced) plus a filter byte
private int rowfilled; // effective/valid length of row
private int rowlen; // what amount of bytes is to be interpreted as a complete "row". can change
// (for interlaced)
private int rown; // only coincide with image row if non-interlaced - incremented by
// setNextRowSize()
/*
* States WAITING_FOR_INPUT ROW_READY WORK_DONE TERMINATED
*
* processBytes() is externally called, prohibited in READY (in DONE it's ignored)
*
* WARNING: inflater.finished() != DONE (not enough, not neccesary) DONE means that we have already uncompressed all
* the data of interest.
*
* In non-callback mode, prepareForNextRow() is also externally called, in
*
* Flow: - processBytes() calls inflateData() - inflateData() : if buffer is filled goes to READY else if !
* inf.finished goes to WAITING else if any data goes to READY (incomplete data to be read) else goes to DONE - in
* Callback mode, after going to READY, n=processCallback() is called and then prepareForNextRow(n) is called. - in
* Polled mode, prepareForNextRow(n) must be called from outside (after checking state=READY) - prepareForNextRow(n)
* goes to DONE if n==0 calls inflateData() again - end() goes to DONE
*/
private enum State {
WAITING_FOR_INPUT, // waiting for more input
ROW_READY, // ready for consumption (might be less than fully filled), ephemeral for CALLBACK
// mode
WORK_DONE, // all data of interest has been read, but we might accept still more trailing chunks
// (we'll ignore them)
TERMINATED; // we are done, and also won't accept more IDAT chunks
public boolean isDone() {
return this == WORK_DONE || this == TERMINATED;
} // the caller has already uncompressed all the data of interest or EOF
public boolean isTerminated() {
return this == TERMINATED;
} // we dont accept more chunks
}
State state = State.WAITING_FOR_INPUT; // never null
private Inflater inf;
private final boolean infOwn; // true if we own the inflater (we created it)
private DeflatedChunkReader curChunk;
private boolean callbackMode = true;
private long nBytesIn = 0; // count the total compressed bytes that have been fed
private long nBytesOut = 0; // count the total uncompressed bytes
int chunkNum = -1; // incremented at each new chunk start
int firstChunqSeqNum = -1; // expected seq num for first chunk. used only for fDAT (APNG)
/**
* All IDAT-like chunks that form a same DeflatedChunksSet should have the
* same id
*/
public final String chunkid;
/**
* @param initialRowLen
* Length in bytes of first "row" (see description)
* @param maxRowLen
* Max length in bytes of "rows"
* @param inflater
* Can be null. If not null, must be already reset (and it must
* be closed/released by caller!)
*/
public DeflatedChunksSet(final String chunkid, final int initialRowLen, final int maxRowLen, final Inflater inflater, final byte[] buffer) {
this.chunkid = chunkid;
rowlen = initialRowLen;
if (initialRowLen < 1 || maxRowLen < initialRowLen)
throw new PngjException("bad inital row len " + initialRowLen);
if (inflater != null) {
inf = inflater;
infOwn = false;
} else {
inf = new Inflater();
infOwn = true; // inflater is own, we will release on close()
}
row = buffer != null && buffer.length >= initialRowLen ? buffer : new byte[maxRowLen];
rown = -1;
state = State.WAITING_FOR_INPUT;
try {
prepareForNextRow(initialRowLen);
} catch (final RuntimeException e) {
close();
throw e;
}
}
public DeflatedChunksSet(final String chunkid, final int initialRowLen, final int maxRowLen) {
this(chunkid, initialRowLen, maxRowLen, null, null);
}
protected void appendNewChunk(final DeflatedChunkReader cr) {
// all chunks must have same id
if (!chunkid.equals(cr.getChunkRaw().id))
throw new PngjInputException("Bad chunk inside IdatSet, id:" + cr.getChunkRaw().id + ", expected:" + chunkid);
curChunk = cr;
chunkNum++;
if (firstChunqSeqNum >= 0)
cr.setSeqNumExpected(chunkNum + firstChunqSeqNum);
}
/**
* Feeds the inflater with the compressed bytes
*
* In poll mode, the caller should not call repeatedly this, without
* consuming first, checking
* isDataReadyForConsumer()
*
* @param buf
* @param off
* @param len
*/
protected void processBytes(final byte[] buf, final int off, final int len) {
nBytesIn += len;
// PngHelperInternal.LOGGER.info("processing compressed bytes in chunkreader : " + len);
if (len < 1 || state.isDone())
return;
if (state == State.ROW_READY)
throw new PngjInputException("this should only be called if waitingForMoreInput");
if (inf.needsDictionary() || !inf.needsInput())
throw new RuntimeException("should not happen");
inf.setInput(buf, off, len);
// PngHelperInternal.debug("entering processs bytes, state=" + state +
// " callback="+callbackMode);
if (isCallbackMode())
while (inflateData()) {
final int nextRowLen = processRowCallback();
prepareForNextRow(nextRowLen);
if (isDone())
processDoneCallback();
}
else
inflateData();
}
/*
* This never inflates more than one row This returns true if this has resulted in a row being ready and preprocessed
* with preProcessRow (in callback mode, we should call immediately processRowCallback() and
* prepareForNextRow(nextRowLen)
*/
private boolean inflateData() {
try {
// PngHelperInternal.debug("entering inflateData bytes, state=" + state +
// " callback="+callbackMode);
if (state == State.ROW_READY)
throw new PngjException("invalid state");// assert
if (state.isDone())
return false;
int ninflated = 0;
if (row == null || row.length < rowlen)
row = new byte[rowlen]; // should not happen
if (rowfilled < rowlen && !inf.finished()) {
try {
ninflated = inf.inflate(row, rowfilled, rowlen - rowfilled);
} catch (final DataFormatException e) {
throw new PngjInputException("error decompressing zlib stream ", e);
}
rowfilled += ninflated;
nBytesOut += ninflated;
}
State nextstate = null;
if (rowfilled == rowlen)
nextstate = State.ROW_READY; // complete row, process it
else if (!inf.finished())
nextstate = State.WAITING_FOR_INPUT;
else if (rowfilled > 0)
nextstate = State.ROW_READY; // complete row, process it
else
nextstate = State.WORK_DONE; // eof, no more data
state = nextstate;
if (state == State.ROW_READY) {
preProcessRow();
return true;
}
} catch (final RuntimeException e) {
close();
throw e;
}
return false;
}
/**
* Called automatically in all modes when a full row has been inflated.
*/
protected void preProcessRow() {
}
/**
* Callback, must be implemented in callbackMode
* <p>
* This should use {@link #getRowFilled()} and {@link #getInflatedRow()} to
* access the row.
* <p>
* Must return byes of next row, for next callback.
*/
protected int processRowCallback() {
throw new PngjInputException("not implemented");
}
/**
* Callback, to be implemented in callbackMode
* <p>
* This will be called once to notify state done
*/
protected void processDoneCallback() {}
/**
* Inflated buffer.
*
* The effective length is given by {@link #getRowFilled()}
*/
public byte[] getInflatedRow() {
return row;
}
/**
* Should be called after the previous row was processed
* <p>
* Pass 0 or negative to signal that we are done (not expecting more bytes)
* <p>
* This resets {@link #rowfilled}
* <p>
* The
*/
public void prepareForNextRow(final int len) {
rowfilled = 0;
rown++;
if (len < 1) {
rowlen = 0;
done();
} else if (inf.finished()) {
rowlen = 0;
done();
} else {
state = State.WAITING_FOR_INPUT;
rowlen = len;
if (!callbackMode)
inflateData();
}
}
/**
* In this state, the object is waiting for more input to deflate.
* <p>
* Only in this state it's legal to feed this
*/
public boolean isWaitingForMoreInput() {
return state == State.WAITING_FOR_INPUT;
}
/**
* In this state, the object is waiting the caller to retrieve inflated data
* <p>
* Effective length: see {@link #getRowFilled()}
*/
public boolean isRowReady() {
return state == State.ROW_READY;
}
/**
* In this state, all relevant data has been uncompressed and retrieved
* (exceptionally, the reading has ended
* prematurely).
* <p>
* We can still feed this object, but the bytes will be swallowed/ignored.
*/
public boolean isDone() {
return state.isDone();
}
public boolean isTerminated() {
return state.isTerminated();
}
/**
* This will be called by the owner to report us the next chunk to come. We
* can make our own internal changes and
* checks. This returns true if we acknowledge the next chunk as part of
* this set
*/
public boolean ackNextChunkId(final String id) {
if (state.isTerminated())
return false;
else if (id.equals(chunkid))
return true;
else if (!allowOtherChunksInBetween(id)) {
if (state.isDone()) {
if (!isTerminated())
terminate();
return false;
} else
throw new PngjInputException("Unexpected chunk " + id + " while " + chunkid + " set is not done");
} else
return true;
}
protected void terminate() {
close();
}
/**
* This should be called when discarding this object, or for aborting.
* Secure, idempotent Don't use this just to
* notify this object that it has no more work to do, see {@link #done()}
*/
public void close() {
try {
if (!state.isTerminated())
state = State.TERMINATED;
if (infOwn && inf != null) {
inf.end();// we end the Inflater only if we created it
inf = null;
}
} catch (final Exception e) {}
}
/**
* Forces the DONE state, this object won't uncompress more data. It's still
* not terminated, it will accept more IDAT
* chunks, but will ignore them.
*/
public void done() {
if (!isDone())
state = State.WORK_DONE;
}
/**
* Target size of the current row, including filter byte. <br>
* should coincide (or be less than) with row.length
*/
public int getRowLen() {
return rowlen;
}
/** This the amount of valid bytes in the buffer */
public int getRowFilled() {
return rowfilled;
}
/**
* Get current (last) row number.
* <p>
* This corresponds to the raw numeration of rows as seen by the deflater.
* Not the same as the real image row, if
* interlaced.
*
*/
public int getRown() {
return rown;
}
/**
* Some IDAT-like set can allow other chunks in between (APGN?).
* <p>
* Normally false.
*
* @param id
* Id of the other chunk that appeared in middel of this set.
* @return true if allowed
*/
public boolean allowOtherChunksInBetween(final String id) {
return false;
}
/**
* Callback mode = async processing
*/
public boolean isCallbackMode() {
return callbackMode;
}
public void setCallbackMode(final boolean callbackMode) {
this.callbackMode = callbackMode;
}
/** total number of bytes that have been fed to this object */
public long getBytesIn() {
return nBytesIn;
}
/** total number of bytes that have been uncompressed */
public long getBytesOut() {
return nBytesOut;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("idatSet : " + curChunk.getChunkRaw().id + " state=" + state + " rows=" + rown + " bytes=" + nBytesIn + "/" + nBytesOut);
return sb.toString();
}
}

View File

@ -1,197 +0,0 @@
package ar.com.hjg.pngj;
public class Deinterlacer {
final ImageInfo imi;
private int pass; // 1-7
private int rows, cols;
int dY, dX, oY, oX; // current step and offset (in pixels)
int oXsamples, dXsamples; // step in samples
// current row in the virtual subsampled image; this increments (by 1) from 0 to rows/dy 7 times
private int currRowSubimg;
// in the real image, this will cycle from 0 to im.rows in different steps, 7 times
private int currRowReal;
private int currRowSeq; // not counting empty rows
int totalRows;
private boolean ended = false;
public Deinterlacer(final ImageInfo iminfo) {
imi = iminfo;
pass = 0;
currRowSubimg = -1;
currRowReal = -1;
currRowSeq = 0;
ended = false;
totalRows = 0; // lazy compute
setPass(1);
setRow(0);
}
/** this refers to the row currRowSubimg */
private void setRow(final int n) { // This should be called only intercally, in sequential order
currRowSubimg = n;
currRowReal = n * dY + oY;
if (currRowReal < 0 || currRowReal >= imi.rows)
throw new PngjExceptionInternal("bad row - this should not happen");
}
/** Skips passes with no rows. Return false is no more rows */
boolean nextRow() {
currRowSeq++;
if (rows == 0 || currRowSubimg >= rows - 1) { // next pass
if (pass == 7) {
ended = true;
return false;
}
setPass(pass + 1);
if (rows == 0) {
currRowSeq--;
return nextRow();
}
setRow(0);
} else
setRow(currRowSubimg + 1);
return true;
}
boolean isEnded() {
return ended;
}
void setPass(final int p) {
if (pass == p)
return;
pass = p;
final byte[] pp = Deinterlacer.paramsForPass(p);// dx,dy,ox,oy
dX = pp[0];
dY = pp[1];
oX = pp[2];
oY = pp[3];
rows = imi.rows > oY ? (imi.rows + dY - 1 - oY) / dY : 0;
cols = imi.cols > oX ? (imi.cols + dX - 1 - oX) / dX : 0;
if (cols == 0)
rows = 0; // well, really...
dXsamples = dX * imi.channels;
oXsamples = oX * imi.channels;
}
static byte[] paramsForPass(final int p) {// dx,dy,ox,oy
switch (p) {
case 1:
return new byte[] { 8, 8, 0, 0 };
case 2:
return new byte[] { 8, 8, 4, 0 };
case 3:
return new byte[] { 4, 8, 0, 4 };
case 4:
return new byte[] { 4, 4, 2, 0 };
case 5:
return new byte[] { 2, 4, 0, 2 };
case 6:
return new byte[] { 2, 2, 1, 0 };
case 7:
return new byte[] { 1, 2, 0, 1 };
default:
throw new PngjExceptionInternal("bad interlace pass" + p);
}
}
/**
* current row number inside the "sub image"
*/
int getCurrRowSubimg() {
return currRowSubimg;
}
/**
* current row number inside the "real image"
*/
int getCurrRowReal() {
return currRowReal;
}
/**
* current pass number (1-7)
*/
int getPass() {
return pass;
}
/**
* How many rows has the current pass?
**/
int getRows() {
return rows;
}
/**
* How many columns (pixels) are there in the current row
*/
int getCols() {
return cols;
}
public int getPixelsToRead() {
return getCols();
}
public int getBytesToRead() { // not including filter byte
return (imi.bitspPixel * getPixelsToRead() + 7) / 8;
}
public int getdY() {
return dY;
}
/*
* in pixels
*/
public int getdX() {
return dX;
}
public int getoY() {
return oY;
}
/*
* in pixels
*/
public int getoX() {
return oX;
}
public int getTotalRows() {
if (totalRows == 0)
for (int p = 1; p <= 7; p++) {
final byte[] pp = Deinterlacer.paramsForPass(p); // dx dy ox oy
final int rows = imi.rows > pp[3] ? (imi.rows + pp[1] - 1 - pp[3]) / pp[1] : 0;
final int cols = imi.cols > pp[2] ? (imi.cols + pp[0] - 1 - pp[2]) / pp[0] : 0;
if (rows > 0 && cols > 0)
totalRows += rows;
}
return totalRows;
}
/**
* total unfiltered bytes in the image, including the filter byte
*/
public long getTotalRawBytes() { // including the filter byte
long bytes = 0;
for (int p = 1; p <= 7; p++) {
final byte[] pp = Deinterlacer.paramsForPass(p); // dx dy ox oy
final int rows = imi.rows > pp[3] ? (imi.rows + pp[1] - 1 - pp[3]) / pp[1] : 0;
final int cols = imi.cols > pp[2] ? (imi.cols + pp[0] - 1 - pp[2]) / pp[0] : 0;
final int bytesr = (imi.bitspPixel * cols + 7) / 8; // without filter byte
if (rows > 0 && cols > 0)
bytes += rows * (1 + (long) bytesr);
}
return bytes;
}
public int getCurrRowSeq() {
return currRowSeq;
}
}

View File

@ -1,125 +0,0 @@
package ar.com.hjg.pngj;
import java.util.HashMap;
/**
* Internal PNG predictor filter type
*
* Negative values are pseudo types, actually global strategies for writing,
* that (can) result on different real filters
* for different rows
*/
public enum FilterType {
/**
* No filter.
*/
FILTER_NONE(0),
/**
* SUB filter (uses same row)
*/
FILTER_SUB(1),
/**
* UP filter (uses previous row)
*/
FILTER_UP(2),
/**
* AVERAGE filter
*/
FILTER_AVERAGE(3),
/**
* PAETH predictor
*/
FILTER_PAETH(4),
/**
* Default strategy: select one of the standard filters depending on global
* image parameters
*/
FILTER_DEFAULT(-1),
/**
* @deprecated use #FILTER_ADAPTIVE_FAST
*/
FILTER_AGGRESSIVE(-2),
/**
* @deprecated use #FILTER_ADAPTIVE_MEDIUM or #FILTER_ADAPTIVE_FULL
*/
FILTER_VERYAGGRESSIVE(-4),
/**
* Adaptative strategy, sampling each row, or almost
*/
FILTER_ADAPTIVE_FULL(-4),
/**
* Adaptive strategy, skippping some rows
*/
FILTER_ADAPTIVE_MEDIUM(-3), // samples about 1/4 row
/**
* Adaptative strategy, skipping many rows - more speed
*/
FILTER_ADAPTIVE_FAST(-2), // samples each 8 or 16 rows
/**
* Experimental
*/
FILTER_SUPER_ADAPTIVE(-10), //
/**
* Preserves the filter passed in original row.
*/
FILTER_PRESERVE(-40),
/**
* Uses all fiters, one for lines, cyciclally. Only for tests.
*/
FILTER_CYCLIC(-50),
/**
* Not specified, placeholder for unknown or NA filters.
*/
FILTER_UNKNOWN(-100);
public final int val;
private FilterType(final int val) {
this.val = val;
}
private static HashMap<Integer, FilterType> byVal;
static {
FilterType.byVal = new HashMap<>();
for (final FilterType ft : FilterType.values())
FilterType.byVal.put(ft.val, ft);
}
public static FilterType getByVal(final int i) {
return FilterType.byVal.get(i);
}
/** only considers standard */
public static boolean isValidStandard(final int i) {
return i >= 0 && i <= 4;
}
public static boolean isValidStandard(final FilterType fy) {
return fy != null && FilterType.isValidStandard(fy.val);
}
public static boolean isAdaptive(final FilterType fy) {
return fy.val <= -2 && fy.val >= -4;
}
/**
* Returns all "standard" filters
*/
public static FilterType[] getAllStandard() {
return new FilterType[] { FILTER_NONE, FILTER_SUB, FILTER_UP, FILTER_AVERAGE, FILTER_PAETH };
}
public static FilterType[] getAllStandardNoneLast() {
return new FilterType[] { FILTER_SUB, FILTER_UP, FILTER_AVERAGE, FILTER_PAETH, FILTER_NONE };
}
public static FilterType[] getAllStandardExceptNone() {
return new FilterType[] { FILTER_SUB, FILTER_UP, FILTER_AVERAGE, FILTER_PAETH };
}
static FilterType[] getAllStandardForFirstRow() {
return new FilterType[] { FILTER_SUB, FILTER_NONE };
}
}

View File

@ -1,16 +0,0 @@
package ar.com.hjg.pngj;
/**
* Bytes consumer. Objects implementing this interface can act as bytes
* consumers, that are "fed" with bytes.
*/
public interface IBytesConsumer {
/**
* Eats some bytes, at most len.
* <p>
* Returns bytes actually consumed. A negative return value signals that the
* consumer is done, it refuses to eat more
* bytes. This should only return 0 if len is 0
*/
int consume(byte[] buf, int offset, int len);
}

View File

@ -1,23 +0,0 @@
package ar.com.hjg.pngj;
import ar.com.hjg.pngj.chunks.ChunkRaw;
import ar.com.hjg.pngj.chunks.PngChunk;
/**
* Factory to create a {@link PngChunk} from a {@link ChunkRaw}.
* <p>
* Used by {@link PngReader}
*/
public interface IChunkFactory {
/**
* @param chunkRaw
* Chunk in raw form. Data can be null if it was skipped or
* processed directly (eg IDAT)
* @param imgInfo
* Not normally necessary, but some chunks want this info
* @return should never return null.
*/
PngChunk createChunk(ChunkRaw chunkRaw, ImageInfo imgInfo);
}

View File

@ -1,132 +0,0 @@
package ar.com.hjg.pngj;
import java.io.OutputStream;
import ar.com.hjg.pngj.chunks.ChunkHelper;
import ar.com.hjg.pngj.chunks.ChunkRaw;
/**
* Outputs a sequence of IDAT-like chunk, that is filled progressively until the
* max chunk length is reached (or until
* flush())
*/
public class IDatChunkWriter {
private static final int MAX_LEN_DEFAULT = 32768; // 32K rather arbitrary - data only
private final OutputStream outputStream;
private final int maxChunkLen;
private byte[] buf;
private int offset = 0;
private int availLen;
private long totalBytesWriten = 0; // including header+crc
private int chunksWriten = 0;
public IDatChunkWriter(final OutputStream outputStream) {
this(outputStream, 0);
}
public IDatChunkWriter(final OutputStream outputStream, final int maxChunkLength) {
this.outputStream = outputStream;
maxChunkLen = maxChunkLength > 0 ? maxChunkLength : IDatChunkWriter.MAX_LEN_DEFAULT;
buf = new byte[maxChunkLen];
availLen = maxChunkLen - offset;
postReset();
}
public IDatChunkWriter(final OutputStream outputStream, final byte[] b) {
this.outputStream = outputStream;
buf = b != null ? b : new byte[IDatChunkWriter.MAX_LEN_DEFAULT];
maxChunkLen = b.length;
availLen = maxChunkLen - offset;
postReset();
}
protected byte[] getChunkId() {
return ChunkHelper.b_IDAT;
}
/**
* Writes a chhunk if there is more than minLenToWrite.
*
* This is normally called internally, but can be called explicitly to force
* flush.
*/
public final void flush() {
if (offset > 0 && offset >= minLenToWrite()) {
final ChunkRaw c = new ChunkRaw(offset, getChunkId(), false);
c.data = buf;
c.writeChunk(outputStream);
totalBytesWriten += c.len + 12;
chunksWriten++;
offset = 0;
availLen = maxChunkLen;
postReset();
}
}
public int getOffset() {
return offset;
}
public int getAvailLen() {
return availLen;
}
/** triggers an flush+reset if appropiate */
public void incrementOffset(final int n) {
offset += n;
availLen -= n;
if (availLen < 0)
throw new PngjOutputException("Anomalous situation");
if (availLen == 0)
flush();
}
/**
* this should rarely be used, the normal way (to avoid double copying) is
* to get the buffer and write directly to it
*/
public void write(final byte[] b, int o, int len) {
while (len > 0) {
final int n = len <= availLen ? len : availLen;
System.arraycopy(b, o, buf, offset, n);
incrementOffset(n);
len -= n;
o += n;
}
}
/** this will be called after reset */
protected void postReset() {
// fdat could override this (and minLenToWrite) to add a prefix
}
protected int minLenToWrite() {
return 1;
}
public void close() {
flush();
offset = 0;
buf = null;
}
/**
* You can write directly to this buffer, using {@link #getOffset()} and
* {@link #getAvailLen()}. You should call
* {@link #incrementOffset(int)} inmediately after.
*/
public byte[] getBuf() {
return buf;
}
public long getTotalBytesWriten() {
return totalBytesWriten;
}
public int getChunksWriten() {
return chunksWriten;
}
}

View File

@ -1,50 +0,0 @@
package ar.com.hjg.pngj;
/**
* General format-translated image line.
* <p>
* The methods from this interface provides translation from/to PNG raw
* unfiltered pixel data, for each image line. This
* doesn't make any assumptions of underlying storage.
* <p>
* The user of this library will not normally use this methods, but instead will
* cast to a more concrete implementation,
* as {@link ImageLineInt} or {@link ImageLineByte} with its methods for
* accessing the pixel values.
*/
public interface IImageLine {
/**
* Extract pixels from a raw unlfilterd PNG row. Len is the total amount of
* bytes in the array, including the first
* byte (filter type)
*
* Arguments offset and step (0 and 1 for non interlaced) are in PIXELS.
* It's guaranteed that when step==1 then
* offset=0
*
* Notice that when step!=1 the data is partial, this method will be called
* several times
*
* Warning: the data in array 'raw' starts at position 0 and has 'len'
* consecutive bytes. 'offset' and 'step' refer to
* the pixels in destination
*/
void readFromPngRaw(byte[] raw, int len, int offset, int step);
/**
* This is called when the read for the line has been completed (eg for
* interlaced). It's called exactly once for each
* line. This is provided in case the class needs to to some postprocessing.
*/
void endReadFromPngRaw();
/**
* Writes the line to a PNG raw byte array, in the unfiltered PNG format
* Notice that the first byte is the filter
* type, you should write it only if you know it.
*
*/
void writeToPngRaw(byte[] raw);
}

View File

@ -1,26 +0,0 @@
package ar.com.hjg.pngj;
/**
* This interface is just for the sake of unifying some methods of
* {@link ImageLineHelper} that can use both
* {@link ImageLineInt} or {@link ImageLineByte}. It's not very useful outside
* that, and the user should not rely much
* on this.
*/
public interface IImageLineArray {
ImageInfo getImageInfo();
FilterType getFilterType();
/**
* length of array (should correspond to samples)
*/
int getSize();
/**
* Get i-th element of array (for 0 to size-1). The meaning of this is type
* dependent. For ImageLineInt and
* ImageLineByte is the sample value.
*/
int getElem(int i);
}

View File

@ -1,8 +0,0 @@
package ar.com.hjg.pngj;
/**
* Image Line factory.
*/
public interface IImageLineFactory<T extends IImageLine> {
T createImageLine(ImageInfo iminfo);
}

View File

@ -1,64 +0,0 @@
package ar.com.hjg.pngj;
/**
* Set of {@link IImageLine} elements.
* <p>
* This is actually a "virtual" set, it can be implemented in several ways; for
* example
* <ul>
* <li>Cursor-like: stores only one line, which is implicitly moved when
* requested</li>
* <li>All lines: all lines stored as an array of <tt>IImageLine</tt></li>
* <li>
* Subset of lines: eg, only first 3 lines, or odd numbered lines. Or a band of
* neighbours lines that is moved like a
* cursor.</li>
* The ImageLine that PngReader returns is hosted by a IImageLineSet (this
* abstraction allows the implementation to deal
* with interlaced images cleanly) but the library user does not normally needs
* to know that (or rely on that), except
* for the {@link PngReader#readRows()} method.
* </ul>
*/
public interface IImageLineSet<T extends IImageLine> {
/**
* Asks for imageline corresponding to row <tt>n</tt> in the original image
* (zero based). This can trigger side
* effects in this object (eg, advance a cursor, set current row number...)
* In some scenarios, this should be consider
* as alias to (pseudocode) <tt>positionAtLine(n); getCurrentLine();</tt>
* <p>
* Throws exception if not available. The caller is supposed to know what
* he/she is doing
**/
IImageLine getImageLine(int n);
/**
* Like {@link #getImageLine(int)} but uses the raw numbering inside the
* LineSet This makes little sense for a cursor
*
* @param n
* Should normally go from 0 to {@link #size()}
* @return
*/
IImageLine getImageLineRawNum(int n);
/**
* Returns true if the set contain row <tt>n</tt> (in the original
* image,zero based) currently allocated.
* <p>
* If it's a single-cursor, this should return true only if it's positioned
* there. (notice that hasImageLine(n) can
* return false, but getImageLine(n) can be ok)
*
**/
boolean hasImageLine(int n);
/**
* Internal size of allocated rows This is informational, it should rarely
* be important for the caller.
**/
int size();
}

View File

@ -1,32 +0,0 @@
package ar.com.hjg.pngj;
/**
* Factory of {@link IImageLineSet}, used by {@link PngReader}.
* <p>
*
* @param <T>
* Generic type of IImageLine
*/
public interface IImageLineSetFactory<T extends IImageLine> {
/**
* Creates a new {@link IImageLineSet}
*
* If singleCursor=true, the caller will read and write one row fully at a
* time, in order (it'll never try to read out
* of order lines), so the implementation can opt for allocate only one
* line.
*
* @param imgInfo
* Image info
* @param singleCursor
* : will read/write one row at a time
* @param nlines
* : how many lines we plan to read
* @param noffset
* : how many lines we want to skip from the original image
* (normally 0)
* @param step
* : row step (normally 1)
*/
IImageLineSet<T> create(ImageInfo imgInfo, boolean singleCursor, int nlines, int noffset, int step);
}

View File

@ -1,7 +0,0 @@
package ar.com.hjg.pngj;
import java.io.OutputStream;
public interface IPngWriterFactory {
PngWriter createPngWriter(OutputStream outputStream, ImageInfo imgInfo);
}

View File

@ -1,246 +0,0 @@
package ar.com.hjg.pngj;
import java.util.Arrays;
import java.util.zip.Checksum;
import java.util.zip.Inflater;
/**
* This object process the concatenation of IDAT chunks.
* <p>
* It extends {@link DeflatedChunksSet}, adding the intelligence to unfilter
* rows, and to understand row lenghts in
* terms of ImageInfo and (eventually) Deinterlacer
*/
public class IdatSet extends DeflatedChunksSet {
protected byte rowUnfiltered[];
protected byte rowUnfilteredPrev[];
protected final ImageInfo imgInfo; // in the case of APNG this is the frame image
protected final Deinterlacer deinterlacer;
final RowInfo rowinfo; // info for the last processed row, for debug
protected int filterUseStat[] = new int[5]; // for stats
/**
* @param id
* Chunk id (first chunk), should be shared by all concatenated
* chunks
* @param iminfo
* Image info
* @param deinterlacer
* Not null if interlaced
*/
public IdatSet(final String id, final ImageInfo iminfo, final Deinterlacer deinterlacer) {
this(id, iminfo, deinterlacer, null, null);
}
/**
* Special constructor with preallocated buffer.
* <p>
* <p>
* Same as {@link #IdatSet(String, ImageInfo, Deinterlacer)}, but you can
* pass a Inflater (will be reset internally),
* and a buffer (will be used only if size is enough)
*/
public IdatSet(final String id, final ImageInfo iminfo, final Deinterlacer deinterlacer, final Inflater inf, final byte[] buffer) {
super(id, deinterlacer != null ? deinterlacer.getBytesToRead() + 1 : iminfo.bytesPerRow + 1, iminfo.bytesPerRow + 1, inf, buffer);
imgInfo = iminfo;
this.deinterlacer = deinterlacer;
rowinfo = new RowInfo(iminfo, deinterlacer);
}
/**
* Applies PNG un-filter to inflated raw line. Result in
* {@link #getUnfilteredRow()} {@link #getRowLen()}
*/
public void unfilterRow() {
unfilterRow(rowinfo.bytesRow);
}
// nbytes: NOT including the filter byte. leaves result in rowUnfiltered
protected void unfilterRow(final int nbytes) {
if (rowUnfiltered == null || rowUnfiltered.length < row.length) {
rowUnfiltered = new byte[row.length];
rowUnfilteredPrev = new byte[row.length];
}
if (rowinfo.rowNsubImg == 0)
Arrays.fill(rowUnfiltered, (byte) 0); // see swap that follows
// swap
final byte[] tmp = rowUnfiltered;
rowUnfiltered = rowUnfilteredPrev;
rowUnfilteredPrev = tmp;
final int ftn = row[0];
if (!FilterType.isValidStandard(ftn))
throw new PngjInputException("Filter type " + ftn + " invalid");
final FilterType ft = FilterType.getByVal(ftn);
filterUseStat[ftn]++;
rowUnfiltered[0] = row[0]; // we copy the filter type, can be useful
switch (ft) {
case FILTER_NONE:
unfilterRowNone(nbytes);
break;
case FILTER_SUB:
unfilterRowSub(nbytes);
break;
case FILTER_UP:
unfilterRowUp(nbytes);
break;
case FILTER_AVERAGE:
unfilterRowAverage(nbytes);
break;
case FILTER_PAETH:
unfilterRowPaeth(nbytes);
break;
default:
throw new PngjInputException("Filter type " + ftn + " not implemented");
}
}
private void unfilterRowAverage(final int nbytes) {
int i, j, x;
for (j = 1 - imgInfo.bytesPixel, i = 1; i <= nbytes; i++, j++) {
x = j > 0 ? rowUnfiltered[j] & 0xff : 0;
rowUnfiltered[i] = (byte) (row[i] + (x + (rowUnfilteredPrev[i] & 0xFF)) / 2);
}
}
private void unfilterRowNone(final int nbytes) {
for (int i = 1; i <= nbytes; i++)
rowUnfiltered[i] = row[i];
}
private void unfilterRowPaeth(final int nbytes) {
int i, j, x, y;
for (j = 1 - imgInfo.bytesPixel, i = 1; i <= nbytes; i++, j++) {
x = j > 0 ? rowUnfiltered[j] & 0xFF : 0;
y = j > 0 ? rowUnfilteredPrev[j] & 0xFF : 0;
rowUnfiltered[i] = (byte) (row[i] + PngHelperInternal.filterPaethPredictor(x, rowUnfilteredPrev[i] & 0xFF, y));
}
}
private void unfilterRowSub(final int nbytes) {
int i, j;
for (i = 1; i <= imgInfo.bytesPixel; i++)
rowUnfiltered[i] = row[i];
for (j = 1, i = imgInfo.bytesPixel + 1; i <= nbytes; i++, j++)
rowUnfiltered[i] = (byte) (row[i] + rowUnfiltered[j]);
}
private void unfilterRowUp(final int nbytes) {
for (int i = 1; i <= nbytes; i++)
rowUnfiltered[i] = (byte) (row[i] + rowUnfilteredPrev[i]);
}
/**
* does the unfiltering of the inflated row, and updates row info
*/
@Override
protected void preProcessRow() {
super.preProcessRow();
rowinfo.update(getRown());
unfilterRow();
rowinfo.updateBuf(rowUnfiltered, rowinfo.bytesRow + 1);
}
/**
* Method for async/callback mode .
* <p>
* In callback mode will be called as soon as each row is retrieved
* (inflated and unfiltered), after
* {@link #preProcessRow()}
* <p>
* This is a dummy implementation (this normally should be overriden) that
* does nothing more than compute the length
* of next row.
* <p>
* The return value is essential
* <p>
*
* @return Length of next row, in bytes (including filter byte),
* non-positive if done
*/
@Override
protected int processRowCallback() {
final int bytesNextRow = advanceToNextRow();
return bytesNextRow;
}
@Override
protected void processDoneCallback() {
super.processDoneCallback();
}
/**
* Signals that we are done with the previous row, begin reading the next
* one.
* <p>
* In polled mode, calls setNextRowLen()
* <p>
* Warning: after calling this, the unfilterRow is invalid!
*
* @return Returns nextRowLen
*/
public int advanceToNextRow() {
// PngHelperInternal.LOGGER.info("advanceToNextRow");
int bytesNextRow;
if (deinterlacer == null)
bytesNextRow = getRown() >= imgInfo.rows - 1 ? 0 : imgInfo.bytesPerRow + 1;
else {
final boolean more = deinterlacer.nextRow();
bytesNextRow = more ? deinterlacer.getBytesToRead() + 1 : 0;
}
if (!isCallbackMode())
prepareForNextRow(bytesNextRow);
return bytesNextRow;
}
@Override
public boolean isRowReady() {
return !isWaitingForMoreInput();
}
/**
* Unfiltered row.
* <p>
* This should be called only if {@link #isRowReady()} returns true.
* <p>
* To get real length, use {@link #getRowLen()}
* <p>
*
* @return Unfiltered row, includes filter byte
*/
public byte[] getUnfilteredRow() {
return rowUnfiltered;
}
public Deinterlacer getDeinterlacer() {
return deinterlacer;
}
void updateCrcs(final Checksum... idatCrcs) {
for (final Checksum idatCrca : idatCrcs)
if (idatCrca != null)// just for testing
idatCrca.update(getUnfilteredRow(), 1, getRowFilled() - 1);
}
@Override
public void close() {
super.close();
rowUnfiltered = null;// not really necessary...
rowUnfilteredPrev = null;
}
/**
* Only for debug/stats
*
* @return Array of 5 integers (sum equal numbers of rows) counting each
* filter use
*/
public int[] getFilterUseStat() {
return filterUseStat;
}
}

View File

@ -1,265 +0,0 @@
package ar.com.hjg.pngj;
import java.util.zip.Checksum;
/**
* Simple immutable wrapper for basic image info.
* <p>
* Some parameters are redundant, but the constructor receives an 'orthogonal'
* subset.
* <p>
* ref: http://www.w3.org/TR/PNG/#11IHDR
*/
public class ImageInfo {
/**
* Absolute allowed maximum value for rows and cols (2^24 ~16 million).
* (bytesPerRow must fit in a 32bit integer,
* though total amount of pixels not necessarily).
*/
public static final int MAX_COLS_ROW = 16777216;
/**
* Cols= Image width, in pixels.
*/
public final int cols;
/**
* Rows= Image height, in pixels
*/
public final int rows;
/**
* Bits per sample (per channel) in the buffer (1-2-4-8-16). This is 8-16
* for RGB/ARGB images, 1-2-4-8 for grayscale.
* For indexed images, number of bits per palette index (1-2-4-8)
*/
public final int bitDepth;
/**
* Number of channels, as used internally: 3 for RGB, 4 for RGBA, 2 for GA
* (gray with alpha), 1 for grayscale or
* indexed.
*/
public final int channels;
/**
* Flag: true if has alpha channel (RGBA/GA)
*/
public final boolean alpha;
/**
* Flag: true if is grayscale (G/GA)
*/
public final boolean greyscale;
/**
* Flag: true if image is indexed, i.e., it has a palette
*/
public final boolean indexed;
/**
* Flag: true if image internally uses less than one byte per sample (bit
* depth 1-2-4)
*/
public final boolean packed;
/**
* Bits used for each pixel in the buffer: channel * bitDepth
*/
public final int bitspPixel;
/**
* rounded up value: this is only used internally for filter
*/
public final int bytesPixel;
/**
* ceil(bitspp*cols/8) - does not include filter
*/
public final int bytesPerRow;
/**
* Equals cols * channels
*/
public final int samplesPerRow;
/**
* Amount of "packed samples" : when several samples are stored in a single
* byte (bitdepth 1,2 4) they are counted as
* one "packed sample". This is less that samplesPerRow only when bitdepth
* is 1-2-4 (flag packed = true)
* <p>
* This equals the number of elements in the scanline array if working with
* packedMode=true
* <p>
* For internal use, client code should rarely access this.
*/
public final int samplesPerRowPacked;
private long totalPixels = -1; // lazy getter
private long totalRawBytes = -1; // lazy getter
/**
* Short constructor: assumes truecolor (RGB/RGBA)
*/
public ImageInfo(final int cols, final int rows, final int bitdepth, final boolean alpha) {
this(cols, rows, bitdepth, alpha, false, false);
}
/**
* Full constructor
*
* @param cols
* Width in pixels
* @param rows
* Height in pixels
* @param bitdepth
* Bits per sample, in the buffer : 8-16 for RGB true color and
* greyscale
* @param alpha
* Flag: has an alpha channel (RGBA or GA)
* @param grayscale
* Flag: is gray scale (any bitdepth, with or without alpha)
* @param indexed
* Flag: has palette
*/
public ImageInfo(final int cols, final int rows, final int bitdepth, final boolean alpha, final boolean grayscale, final boolean indexed) {
this.cols = cols;
this.rows = rows;
this.alpha = alpha;
this.indexed = indexed;
greyscale = grayscale;
if (greyscale && indexed)
throw new PngjException("palette and greyscale are mutually exclusive");
channels = grayscale || indexed ? alpha ? 2 : 1 : alpha ? 4 : 3;
// http://www.w3.org/TR/PNG/#11IHDR
bitDepth = bitdepth;
packed = bitdepth < 8;
bitspPixel = channels * bitDepth;
bytesPixel = (bitspPixel + 7) / 8;
bytesPerRow = (bitspPixel * cols + 7) / 8;
samplesPerRow = channels * this.cols;
samplesPerRowPacked = packed ? bytesPerRow : samplesPerRow;
// several checks
switch (bitDepth) {
case 1:
case 2:
case 4:
if (!(this.indexed || greyscale))
throw new PngjException("only indexed or grayscale can have bitdepth=" + bitDepth);
break;
case 8:
break;
case 16:
if (this.indexed)
throw new PngjException("indexed can't have bitdepth=" + bitDepth);
break;
default:
throw new PngjException("invalid bitdepth=" + bitDepth);
}
if (cols < 1 || cols > ImageInfo.MAX_COLS_ROW)
throw new PngjException("invalid cols=" + cols + " ???");
if (rows < 1 || rows > ImageInfo.MAX_COLS_ROW)
throw new PngjException("invalid rows=" + rows + " ???");
if (samplesPerRow < 1)
throw new PngjException("invalid image parameters (overflow?)");
}
/**
* returns a copy with different size
*
* @param cols
* if non-positive, the original is used
* @param rows
* if non-positive, the original is used
* @return a new copy with the specified size and same properties
*/
public ImageInfo withSize(final int cols, final int rows) {
return new ImageInfo(cols > 0 ? cols : this.cols, rows > 0 ? rows : this.rows, bitDepth, alpha, greyscale, indexed);
}
public long getTotalPixels() {
if (totalPixels < 0)
totalPixels = cols * (long) rows;
return totalPixels;
}
/**
* Total uncompressed bytes in IDAT, including filter byte. This is not
* valid for interlaced.
*/
public long getTotalRawBytes() {
if (totalRawBytes < 0)
totalRawBytes = (bytesPerRow + 1) * (long) rows;
return totalRawBytes;
}
@Override
public String toString() {
return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels=" + channels + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + indexed + "]";
}
/**
* Brief info: COLSxROWS[dBITDEPTH][a][p][g] ( the default dBITDEPTH='d8' is
* ommited)
**/
public String toStringBrief() {
return String.valueOf(cols) + "x" + rows + (bitDepth != 8 ? "d" + bitDepth : "") + (alpha ? "a" : "") + (indexed ? "p" : "") + (greyscale ? "g" : "");
}
public String toStringDetail() {
return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels=" + channels + ", bitspPixel=" + bitspPixel + ", bytesPixel=" + bytesPixel + ", bytesPerRow=" + bytesPerRow + ", samplesPerRow=" + samplesPerRow + ", samplesPerRowP=" + samplesPerRowPacked + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + indexed + ", packed=" + packed + "]";
}
void updateCrc(final Checksum crc) {
crc.update((byte) rows);
crc.update((byte) (rows >> 8));
crc.update((byte) (rows >> 16));
crc.update((byte) cols);
crc.update((byte) (cols >> 8));
crc.update((byte) (cols >> 16));
crc.update((byte) bitDepth);
crc.update((byte) (indexed ? 1 : 2));
crc.update((byte) (greyscale ? 3 : 4));
crc.update((byte) (alpha ? 3 : 4));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (alpha ? 1231 : 1237);
result = prime * result + bitDepth;
result = prime * result + cols;
result = prime * result + (greyscale ? 1231 : 1237);
result = prime * result + (indexed ? 1231 : 1237);
result = prime * result + rows;
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final ImageInfo other = (ImageInfo) obj;
if (alpha != other.alpha)
return false;
if (bitDepth != other.bitDepth)
return false;
if (cols != other.cols)
return false;
if (greyscale != other.greyscale)
return false;
if (indexed != other.indexed)
return false;
if (rows != other.rows)
return false;
return true;
}
}

View File

@ -1,191 +0,0 @@
package ar.com.hjg.pngj;
/**
* Lightweight wrapper for an image scanline, used for read and write.
* <p>
* This object can be (usually it is) reused while iterating over the image
* lines.
* <p>
* See <code>scanline</code> field, to understand the format.
*
* Format: byte (one bytes per sample) (for 16bpp the extra byte is placed in an
* extra array)
*/
public class ImageLineByte implements IImageLine, IImageLineArray {
public final ImageInfo imgInfo;
final byte[] scanline;
final byte[] scanline2; // only used for 16 bpp (less significant byte) Normally you'd prefer
// ImageLineInt in this case
protected FilterType filterType; // informational ; only filled by the reader. not significant for
// interlaced
final int size; // = imgInfo.samplePerRowPacked, if packed:imgInfo.samplePerRow elswhere
public ImageLineByte(final ImageInfo imgInfo) {
this(imgInfo, null);
}
public ImageLineByte(final ImageInfo imgInfo, final byte[] sci) {
this.imgInfo = imgInfo;
filterType = FilterType.FILTER_UNKNOWN;
size = imgInfo.samplesPerRow;
scanline = sci != null && sci.length >= size ? sci : new byte[size];
scanline2 = imgInfo.bitDepth == 16 ? new byte[size] : null;
}
/**
* Returns a factory for this object
*/
public static IImageLineFactory<ImageLineByte> getFactory() {
return iminfo -> new ImageLineByte(iminfo);
}
public FilterType getFilterUsed() {
return filterType;
}
/**
* One byte per sample. This can be used also for 16bpp images, but in this
* case this loses the less significant
* 8-bits ; see also getScanlineByte2 and getElem.
*/
public byte[] getScanlineByte() {
return scanline;
}
/**
* only for 16bpp (less significant byte)
*
* @return null for less than 16bpp
*/
public byte[] getScanlineByte2() {
return scanline2;
}
/**
* Basic info
*/
@Override
public String toString() {
return " cols=" + imgInfo.cols + " bpc=" + imgInfo.bitDepth + " size=" + scanline.length;
}
@Override
public void readFromPngRaw(final byte[] raw, final int len, final int offset, final int step) {
filterType = FilterType.getByVal(raw[0]); // only for non interlaced line the filter is significative
final int len1 = len - 1;
final int step1 = (step - 1) * imgInfo.channels;
if (imgInfo.bitDepth == 8) {
if (step == 1)
System.arraycopy(raw, 1, scanline, 0, len1);
else
for (int s = 1, c = 0, i = offset * imgInfo.channels; s <= len1; s++, i++) {
scanline[i] = raw[s];
c++;
if (c == imgInfo.channels) {
c = 0;
i += step1;
}
}
} else if (imgInfo.bitDepth == 16) {
if (step == 1)
for (int i = 0, s = 1; i < imgInfo.samplesPerRow; i++) {
scanline[i] = raw[s++]; // get the first byte
scanline2[i] = raw[s++]; // get the first byte
}
else
for (int s = 1, c = 0, i = offset != 0 ? offset * imgInfo.channels : 0; s <= len1; i++) {
scanline[i] = raw[s++];
scanline2[i] = raw[s++];
c++;
if (c == imgInfo.channels) {
c = 0;
i += step1;
}
}
} else { // packed formats
int mask0, mask, shi, bd;
bd = imgInfo.bitDepth;
mask0 = ImageLineHelper.getMaskForPackedFormats(bd);
for (int i = offset * imgInfo.channels, r = 1, c = 0; r < len; r++) {
mask = mask0;
shi = 8 - bd;
do {
scanline[i] = (byte) ((raw[r] & mask) >> shi);
mask >>= bd;
shi -= bd;
i++;
c++;
if (c == imgInfo.channels) {
c = 0;
i += step1;
}
} while (mask != 0 && i < size);
}
}
}
@Override
public void writeToPngRaw(final byte[] raw) {
raw[0] = (byte) filterType.val;
if (imgInfo.bitDepth == 8)
System.arraycopy(scanline, 0, raw, 1, size);
else if (imgInfo.bitDepth == 16)
for (int i = 0, s = 1; i < size; i++) {
raw[s++] = scanline[i];
raw[s++] = scanline2[i];
}
else { // packed formats
int shi, bd, v;
bd = imgInfo.bitDepth;
shi = 8 - bd;
v = 0;
for (int i = 0, r = 1; i < size; i++) {
v |= scanline[i] << shi;
shi -= bd;
if (shi < 0 || i == size - 1) {
raw[r++] = (byte) v;
shi = 8 - bd;
v = 0;
}
}
}
}
@Override
public void endReadFromPngRaw() {}
@Override
public int getSize() {
return size;
}
@Override
public int getElem(final int i) {
return scanline2 == null ? scanline[i] & 0xFF : (scanline[i] & 0xFF) << 8 | scanline2[i] & 0xFF;
}
public byte[] getScanline() {
return scanline;
}
@Override
public ImageInfo getImageInfo() {
return imgInfo;
}
@Override
public FilterType getFilterType() {
return filterType;
}
/**
* This should rarely be used by client code. Only relevant if
* FilterPreserve==true
*/
public void setFilterType(final FilterType ft) {
filterType = ft;
}
}

View File

@ -1,469 +0,0 @@
package ar.com.hjg.pngj;
import java.util.Arrays;
import ar.com.hjg.pngj.chunks.PngChunkPLTE;
import ar.com.hjg.pngj.chunks.PngChunkTRNS;
/**
* Bunch of utility static methods to proces an image line at the pixel level.
* <p>
* WARNING: this has little testing/optimizing, and this API is not stable. some
* methods will probably be changed or
* removed if future releases.
* <p>
* WARNING: most methods for getting/setting values work currently only for
* ImageLine or ImageLineByte
*/
public class ImageLineHelper {
static int[] DEPTH_UNPACK_1;
static int[] DEPTH_UNPACK_2;
static int[] DEPTH_UNPACK_4;
static int[][] DEPTH_UNPACK;
static {
ImageLineHelper.initDepthScale();
}
private static void initDepthScale() {
ImageLineHelper.DEPTH_UNPACK_1 = new int[2];
for (int i = 0; i < 2; i++)
ImageLineHelper.DEPTH_UNPACK_1[i] = i * 255;
ImageLineHelper.DEPTH_UNPACK_2 = new int[4];
for (int i = 0; i < 4; i++)
ImageLineHelper.DEPTH_UNPACK_2[i] = i * 255 / 3;
ImageLineHelper.DEPTH_UNPACK_4 = new int[16];
for (int i = 0; i < 16; i++)
ImageLineHelper.DEPTH_UNPACK_4[i] = i * 255 / 15;
ImageLineHelper.DEPTH_UNPACK = new int[][] { null, ImageLineHelper.DEPTH_UNPACK_1, ImageLineHelper.DEPTH_UNPACK_2, null, ImageLineHelper.DEPTH_UNPACK_4 };
}
/**
* When the bitdepth is less than 8, the imageLine is usually
* returned/expected unscaled. This method upscales it in
* place. Eg, if bitdepth=1, values 0-1 will be converted to 0-255
*/
public static void scaleUp(final IImageLineArray line) {
if (line.getImageInfo().indexed || line.getImageInfo().bitDepth >= 8)
return;
final int[] scaleArray = ImageLineHelper.DEPTH_UNPACK[line.getImageInfo().bitDepth];
if (line instanceof ImageLineInt) {
final ImageLineInt iline = (ImageLineInt) line;
for (int i = 0; i < iline.getSize(); i++)
iline.scanline[i] = scaleArray[iline.scanline[i]];
} else if (line instanceof ImageLineByte) {
final ImageLineByte iline = (ImageLineByte) line;
for (int i = 0; i < iline.getSize(); i++)
iline.scanline[i] = (byte) scaleArray[iline.scanline[i]];
} else
throw new PngjException("not implemented");
}
/**
* Reverse of {@link #scaleUp(IImageLineArray)}
*/
public static void scaleDown(final IImageLineArray line) {
if (line.getImageInfo().indexed || line.getImageInfo().bitDepth >= 8)
return;
if (line instanceof ImageLineInt) {
final int scalefactor = 8 - line.getImageInfo().bitDepth;
if (line instanceof ImageLineInt) {
final ImageLineInt iline = (ImageLineInt) line;
for (int i = 0; i < line.getSize(); i++)
iline.scanline[i] = iline.scanline[i] >> scalefactor;
} else if (line instanceof ImageLineByte) {
final ImageLineByte iline = (ImageLineByte) line;
for (int i = 0; i < line.getSize(); i++)
iline.scanline[i] = (byte) ((iline.scanline[i] & 0xFF) >> scalefactor);
}
} else
throw new PngjException("not implemented");
}
public static byte scaleUp(final int bitdepth, final byte v) {
return bitdepth < 8 ? (byte) ImageLineHelper.DEPTH_UNPACK[bitdepth][v] : v;
}
public static byte scaleDown(final int bitdepth, final byte v) {
return bitdepth < 8 ? (byte) (v >> 8 - bitdepth) : v;
}
/**
* Given an indexed line with a palette, unpacks as a RGB array, or RGBA if
* a non nul PngChunkTRNS chunk is passed
*
* @param line
* ImageLine as returned from PngReader
* @param pal
* Palette chunk
* @param trns
* Transparency chunk, can be null (absent)
* @param buf
* Preallocated array, optional
* @return R G B (A), one sample 0-255 per array element. Ready for
* pngw.writeRowInt()
*/
public static int[] palette2rgb(final ImageLineInt line, final PngChunkPLTE pal, final PngChunkTRNS trns,
final int[] buf) {
return ImageLineHelper.palette2rgb(line, pal, trns, buf, false);
}
/**
* Warning: the line should be upscaled, see
* {@link #scaleUp(IImageLineArray)}
*/
static int[] lineToARGB32(final ImageLineByte line, final PngChunkPLTE pal, final PngChunkTRNS trns, int[] buf) {
final boolean alphachannel = line.imgInfo.alpha;
final int cols = line.getImageInfo().cols;
if (buf == null || buf.length < cols)
buf = new int[cols];
int index, rgb, alpha, ga, g;
if (line.getImageInfo().indexed) {// palette
final int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0;
for (int c = 0; c < cols; c++) {
index = line.scanline[c] & 0xFF;
rgb = pal.getEntry(index);
alpha = index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255;
buf[c] = alpha << 24 | rgb;
}
} else if (line.imgInfo.greyscale) { // gray
ga = trns != null ? trns.getGray() : -1;
for (int c = 0, c2 = 0; c < cols; c++) {
g = line.scanline[c2++] & 0xFF;
alpha = alphachannel ? line.scanline[c2++] & 0xFF : g != ga ? 255 : 0;
buf[c] = alpha << 24 | g | g << 8 | g << 16;
}
} else { // true color
ga = trns != null ? trns.getRGB888() : -1;
for (int c = 0, c2 = 0; c < cols; c++) {
rgb = (line.scanline[c2++] & 0xFF) << 16 | (line.scanline[c2++] & 0xFF) << 8 | line.scanline[c2++] & 0xFF;
alpha = alphachannel ? line.scanline[c2++] & 0xFF : rgb != ga ? 255 : 0;
buf[c] = alpha << 24 | rgb;
}
}
return buf;
}
/**
* Warning: the line should be upscaled, see
* {@link #scaleUp(IImageLineArray)}
*/
static byte[] lineToRGBA8888(final ImageLineByte line, final PngChunkPLTE pal, final PngChunkTRNS trns,
byte[] buf) {
final boolean alphachannel = line.imgInfo.alpha;
final int cols = line.imgInfo.cols;
final int bytes = cols * 4;
if (buf == null || buf.length < bytes)
buf = new byte[bytes];
int index, rgb, ga;
byte val;
if (line.imgInfo.indexed) {// palette
final int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0;
for (int c = 0, b = 0; c < cols; c++) {
index = line.scanline[c] & 0xFF;
rgb = pal.getEntry(index);
buf[b++] = (byte) (rgb >> 16 & 0xFF);
buf[b++] = (byte) (rgb >> 8 & 0xFF);
buf[b++] = (byte) (rgb & 0xFF);
buf[b++] = (byte) (index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255);
}
} else if (line.imgInfo.greyscale) { //
ga = trns != null ? trns.getGray() : -1;
for (int c = 0, b = 0; b < bytes;) {
val = line.scanline[c++];
buf[b++] = val;
buf[b++] = val;
buf[b++] = val;
buf[b++] = alphachannel ? line.scanline[c++] : (val & 0xFF) == ga ? (byte) 0 : (byte) 255;
}
} else if (alphachannel) // same format!
System.arraycopy(line.scanline, 0, buf, 0, bytes);
else
for (int c = 0, b = 0; b < bytes;) {
buf[b++] = line.scanline[c++];
buf[b++] = line.scanline[c++];
buf[b++] = line.scanline[c++];
buf[b++] = (byte) 255; // tentative (probable)
if (trns != null && buf[b - 3] == (byte) trns.getRGB()[0] && buf[b - 2] == (byte) trns.getRGB()[1] && buf[b - 1] == (byte) trns.getRGB()[2]) // not
// very
// efficient,
// but
// not
// frecuent
buf[b - 1] = 0;
}
return buf;
}
static byte[] lineToRGB888(final ImageLineByte line, final PngChunkPLTE pal, byte[] buf) {
final boolean alphachannel = line.imgInfo.alpha;
final int cols = line.imgInfo.cols;
final int bytes = cols * 3;
if (buf == null || buf.length < bytes)
buf = new byte[bytes];
byte val;
final int[] rgb = new int[3];
if (line.imgInfo.indexed)
for (int c = 0, b = 0; c < cols; c++) {
pal.getEntryRgb(line.scanline[c] & 0xFF, rgb);
buf[b++] = (byte) rgb[0];
buf[b++] = (byte) rgb[1];
buf[b++] = (byte) rgb[2];
}
else if (line.imgInfo.greyscale)
for (int c = 0, b = 0; b < bytes;) {
val = line.scanline[c++];
buf[b++] = val;
buf[b++] = val;
buf[b++] = val;
if (alphachannel)
c++; // skip alpha
}
else if (!alphachannel) // same format!
System.arraycopy(line.scanline, 0, buf, 0, bytes);
else
for (int c = 0, b = 0; b < bytes;) {
buf[b++] = line.scanline[c++];
buf[b++] = line.scanline[c++];
buf[b++] = line.scanline[c++];
c++;// skip alpha
}
return buf;
}
/**
* Same as palette2rgbx , but returns rgba always, even if trns is null
*
* @param line
* ImageLine as returned from PngReader
* @param pal
* Palette chunk
* @param trns
* Transparency chunk, can be null (absent)
* @param buf
* Preallocated array, optional
* @return R G B (A), one sample 0-255 per array element. Ready for
* pngw.writeRowInt()
*/
public static int[] palette2rgba(final ImageLineInt line, final PngChunkPLTE pal, final PngChunkTRNS trns,
final int[] buf) {
return ImageLineHelper.palette2rgb(line, pal, trns, buf, true);
}
public static int[] palette2rgb(final ImageLineInt line, final PngChunkPLTE pal, final int[] buf) {
return ImageLineHelper.palette2rgb(line, pal, null, buf, false);
}
/** this is not very efficient, only for tests and troubleshooting */
public static int[] convert2rgba(final IImageLineArray line, final PngChunkPLTE pal, final PngChunkTRNS trns,
int[] buf) {
final ImageInfo imi = line.getImageInfo();
final int nsamples = imi.cols * 4;
if (buf == null || buf.length < nsamples)
buf = new int[nsamples];
final int maxval = imi.bitDepth == 16 ? (1 << 16) - 1 : 255;
Arrays.fill(buf, maxval);
if (imi.indexed) {
final int tlen = trns != null ? trns.getPalletteAlpha().length : 0;
for (int s = 0; s < imi.cols; s++) {
final int index = line.getElem(s);
pal.getEntryRgb(index, buf, s * 4);
if (index < tlen)
buf[s * 4 + 3] = trns.getPalletteAlpha()[index];
}
} else if (imi.greyscale) {
int[] unpack = null;
if (imi.bitDepth < 8)
unpack = ImageLineHelper.DEPTH_UNPACK[imi.bitDepth];
for (int s = 0, i = 0, p = 0; p < imi.cols; p++) {
buf[s++] = unpack != null ? unpack[line.getElem(i++)] : line.getElem(i++);
buf[s] = buf[s - 1];
s++;
buf[s] = buf[s - 1];
s++;
if (imi.channels == 2)
buf[s++] = unpack != null ? unpack[line.getElem(i++)] : line.getElem(i++);
else
buf[s++] = maxval;
}
} else
for (int s = 0, i = 0, p = 0; p < imi.cols; p++) {
buf[s++] = line.getElem(i++);
buf[s++] = line.getElem(i++);
buf[s++] = line.getElem(i++);
buf[s++] = imi.alpha ? line.getElem(i++) : maxval;
}
return buf;
}
private static int[] palette2rgb(final IImageLine line, final PngChunkPLTE pal, final PngChunkTRNS trns, int[] buf,
final boolean alphaForced) {
final boolean isalpha = trns != null;
final int channels = isalpha ? 4 : 3;
final ImageLineInt linei = (ImageLineInt) (line instanceof ImageLineInt ? line : null);
final ImageLineByte lineb = (ImageLineByte) (line instanceof ImageLineByte ? line : null);
final boolean isbyte = lineb != null;
final int cols = linei != null ? linei.imgInfo.cols : lineb.imgInfo.cols;
final int nsamples = cols * channels;
if (buf == null || buf.length < nsamples)
buf = new int[nsamples];
final int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0;
for (int c = 0; c < cols; c++) {
final int index = isbyte ? lineb.scanline[c] & 0xFF : linei.scanline[c];
pal.getEntryRgb(index, buf, c * channels);
if (isalpha) {
final int alpha = index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255;
buf[c * channels + 3] = alpha;
}
}
return buf;
}
/**
* what follows is pretty uninteresting/untested/obsolete, subject to change
*/
/**
* Just for basic info or debugging. Shows values for first and last pixel.
* Does not include alpha
*/
public static String infoFirstLastPixels(final ImageLineInt line) {
return line.imgInfo.channels == 1 ? String.format("first=(%d) last=(%d)", line.scanline[0], line.scanline[line.scanline.length - 1]) : String.format("first=(%d %d %d) last=(%d %d %d)", line.scanline[0], line.scanline[1], line.scanline[2], line.scanline[line.scanline.length - line.imgInfo.channels], line.scanline[line.scanline.length - line.imgInfo.channels + 1], line.scanline[line.scanline.length - line.imgInfo.channels + 2]);
}
/**
* integer packed R G B only for bitdepth=8! (does not check!)
*
**/
public static int getPixelRGB8(final IImageLine line, final int column) {
if (line instanceof ImageLineInt) {
final int offset = column * ((ImageLineInt) line).imgInfo.channels;
final int[] scanline = ((ImageLineInt) line).getScanline();
return scanline[offset] << 16 | scanline[offset + 1] << 8 | scanline[offset + 2];
} else if (line instanceof ImageLineByte) {
final int offset = column * ((ImageLineByte) line).imgInfo.channels;
final byte[] scanline = ((ImageLineByte) line).getScanline();
return (scanline[offset] & 0xff) << 16 | (scanline[offset + 1] & 0xff) << 8 | scanline[offset + 2] & 0xff;
} else
throw new PngjException("Not supported " + line.getClass());
}
public static int getPixelARGB8(final IImageLine line, final int column) {
if (line instanceof ImageLineInt) {
final int offset = column * ((ImageLineInt) line).imgInfo.channels;
final int[] scanline = ((ImageLineInt) line).getScanline();
return scanline[offset + 3] << 24 | scanline[offset] << 16 | scanline[offset + 1] << 8 | scanline[offset + 2];
} else if (line instanceof ImageLineByte) {
final int offset = column * ((ImageLineByte) line).imgInfo.channels;
final byte[] scanline = ((ImageLineByte) line).getScanline();
return (scanline[offset + 3] & 0xff) << 24 | (scanline[offset] & 0xff) << 16 | (scanline[offset + 1] & 0xff) << 8 | scanline[offset + 2] & 0xff;
} else
throw new PngjException("Not supported " + line.getClass());
}
public static void setPixelsRGB8(final ImageLineInt line, final int[] rgb) {
for (int i = 0, j = 0; i < line.imgInfo.cols; i++) {
line.scanline[j++] = rgb[i] >> 16 & 0xFF;
line.scanline[j++] = rgb[i] >> 8 & 0xFF;
line.scanline[j++] = rgb[i] & 0xFF;
}
}
public static void setPixelRGB8(final ImageLineInt line, int col, final int r, final int g, final int b) {
col *= line.imgInfo.channels;
line.scanline[col++] = r;
line.scanline[col++] = g;
line.scanline[col] = b;
}
public static void setPixelRGB8(final ImageLineInt line, final int col, final int rgb) {
ImageLineHelper.setPixelRGB8(line, col, rgb >> 16 & 0xFF, rgb >> 8 & 0xFF, rgb & 0xFF);
}
public static void setPixelsRGBA8(final ImageLineInt line, final int[] rgb) {
for (int i = 0, j = 0; i < line.imgInfo.cols; i++) {
line.scanline[j++] = rgb[i] >> 16 & 0xFF;
line.scanline[j++] = rgb[i] >> 8 & 0xFF;
line.scanline[j++] = rgb[i] & 0xFF;
line.scanline[j++] = rgb[i] >> 24 & 0xFF;
}
}
public static void setPixelRGBA8(final ImageLineInt line, int col, final int r, final int g, final int b,
final int a) {
col *= line.imgInfo.channels;
line.scanline[col++] = r;
line.scanline[col++] = g;
line.scanline[col++] = b;
line.scanline[col] = a;
}
public static void setPixelRGBA8(final ImageLineInt line, final int col, final int rgb) {
ImageLineHelper.setPixelRGBA8(line, col, rgb >> 16 & 0xFF, rgb >> 8 & 0xFF, rgb & 0xFF, rgb >> 24 & 0xFF);
}
public static void setValD(final ImageLineInt line, final int i, final double d) {
line.scanline[i] = ImageLineHelper.double2int(line, d);
}
public static int interpol(final int a, final int b, final int c, final int d, final double dx, final double dy) {
// a b -> x (0-1)
// c d
final double e = a * (1.0 - dx) + b * dx;
final double f = c * (1.0 - dx) + d * dx;
return (int) (e * (1 - dy) + f * dy + 0.5);
}
public static double int2double(final ImageLineInt line, final int p) {
return line.imgInfo.bitDepth == 16 ? p / 65535.0 : p / 255.0;
// TODO: replace my multiplication? check for other bitdepths
}
public static double int2doubleClamped(final ImageLineInt line, final int p) {
// TODO: replace my multiplication?
final double d = line.imgInfo.bitDepth == 16 ? p / 65535.0 : p / 255.0;
return d <= 0.0 ? 0 : d >= 1.0 ? 1.0 : d;
}
public static int double2int(final ImageLineInt line, double d) {
d = d <= 0.0 ? 0 : d >= 1.0 ? 1.0 : d;
return line.imgInfo.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); //
}
public static int double2intClamped(final ImageLineInt line, double d) {
d = d <= 0.0 ? 0 : d >= 1.0 ? 1.0 : d;
return line.imgInfo.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); //
}
public static int clampTo_0_255(final int i) {
return i > 255 ? 255 : i < 0 ? 0 : i;
}
public static int clampTo_0_65535(final int i) {
return i > 65535 ? 65535 : i < 0 ? 0 : i;
}
public static int clampTo_128_127(final int x) {
return x > 127 ? 127 : x < -128 ? -128 : x;
}
public static int getMaskForPackedFormats(final int bitDepth) { // Utility function for pack/unpack
if (bitDepth == 4)
return 0xf0;
else if (bitDepth == 2)
return 0xc0;
else
return 0x80; // bitDepth == 1
}
public static int getMaskForPackedFormatsLs(final int bitDepth) { // Utility function for pack/unpack
if (bitDepth == 4)
return 0x0f;
else if (bitDepth == 2)
return 0x03;
else
return 0x01; // bitDepth == 1
}
}

View File

@ -1,203 +0,0 @@
package ar.com.hjg.pngj;
/**
* Represents an image line, integer format (one integer by sample). See
* {@link #scanline} to understand the format.
*/
public class ImageLineInt implements IImageLine, IImageLineArray {
public final ImageInfo imgInfo;
/**
* The 'scanline' is an array of integers, corresponds to an image line
* (row).
* <p>
* Each <code>int</code> is a "sample" (one for channel), (0-255 or 0-65535)
* in the corresponding PNG sequence:
* <code>R G B R G B...</code> or <code>R G B A R G B A...</tt>
* or <code>g g g ...</code> or <code>i i i</code> (palette index)
* <p>
* For bitdepth=1/2/4 the value is not scaled (hence, eg, if bitdepth=2 the
* range will be 0-4)
* <p>
* To convert a indexed line to RGB values, see
* {@link ImageLineHelper#palette2rgb(ImageLineInt, ar.com.hjg.pngj.chunks.PngChunkPLTE, int[])}
* (you can't do the
* reverse)
*/
protected final int[] scanline;
/**
* number of elements in the scanline
*/
protected final int size;
/**
* informational ; only filled by the reader. not meaningful for interlaced
*/
protected FilterType filterType = FilterType.FILTER_UNKNOWN;
/**
* @param imgInfo
* Inmutable ImageInfo, basic parameters of the image we are
* reading or writing
*/
public ImageLineInt(final ImageInfo imgInfo) {
this(imgInfo, null);
}
/**
* @param imgInfo
* Inmutable ImageInfo, basic parameters of the image we are
* reading or writing
* @param sci
* prealocated buffer (can be null)
*/
public ImageLineInt(final ImageInfo imgInfo, final int[] sci) {
this.imgInfo = imgInfo;
filterType = FilterType.FILTER_UNKNOWN;
size = imgInfo.samplesPerRow;
scanline = sci != null && sci.length >= size ? sci : new int[size];
}
/**
* Helper method, returns a default factory for this object
*
*/
public static IImageLineFactory<ImageLineInt> getFactory() {
return iminfo -> new ImageLineInt(iminfo);
}
@Override
public FilterType getFilterType() {
return filterType;
}
/**
* This should rarely be used by client code. Only relevant if
* FilterPreserve==true
*/
public void setFilterType(final FilterType ft) {
filterType = ft;
}
/**
* Basic info
*/
@Override
public String toString() {
return " cols=" + imgInfo.cols + " bpc=" + imgInfo.bitDepth + " size=" + scanline.length;
}
@Override
public void readFromPngRaw(final byte[] raw, final int len, final int offset, final int step) {
setFilterType(FilterType.getByVal(raw[0]));
final int len1 = len - 1;
final int step1 = (step - 1) * imgInfo.channels;
if (imgInfo.bitDepth == 8) {
if (step == 1)
for (int i = 0; i < size; i++)
scanline[i] = raw[i + 1] & 0xff;
else
for (int s = 1, c = 0, i = offset * imgInfo.channels; s <= len1; s++, i++) {
scanline[i] = raw[s] & 0xff;
c++;
if (c == imgInfo.channels) {
c = 0;
i += step1;
}
}
} else if (imgInfo.bitDepth == 16) {
if (step == 1)
for (int i = 0, s = 1; i < size; i++)
scanline[i] = (raw[s++] & 0xFF) << 8 | raw[s++] & 0xFF; // 16 bitspc
else
for (int s = 1, c = 0, i = offset != 0 ? offset * imgInfo.channels : 0; s <= len1; s++, i++) {
scanline[i] = (raw[s++] & 0xFF) << 8 | raw[s] & 0xFF; // 16 bitspc
c++;
if (c == imgInfo.channels) {
c = 0;
i += step1;
}
}
} else { // packed formats
int mask0, mask, shi, bd;
bd = imgInfo.bitDepth;
mask0 = ImageLineHelper.getMaskForPackedFormats(bd);
for (int i = offset * imgInfo.channels, r = 1, c = 0; r < len; r++) {
mask = mask0;
shi = 8 - bd;
do {
scanline[i++] = (raw[r] & mask) >> shi;
mask >>= bd;
shi -= bd;
c++;
if (c == imgInfo.channels) {
c = 0;
i += step1;
}
} while (mask != 0 && i < size);
}
}
}
@Override
public void writeToPngRaw(final byte[] raw) {
raw[0] = (byte) filterType.val;
if (imgInfo.bitDepth == 8)
for (int i = 0; i < size; i++)
raw[i + 1] = (byte) scanline[i];
else if (imgInfo.bitDepth == 16)
for (int i = 0, s = 1; i < size; i++) {
raw[s++] = (byte) (scanline[i] >> 8);
raw[s++] = (byte) (scanline[i] & 0xff);
}
else { // packed formats
int shi, bd, v;
bd = imgInfo.bitDepth;
shi = 8 - bd;
v = 0;
for (int i = 0, r = 1; i < size; i++) {
v |= scanline[i] << shi;
shi -= bd;
if (shi < 0 || i == size - 1) {
raw[r++] = (byte) v;
shi = 8 - bd;
v = 0;
}
}
}
}
/**
* Does nothing in this implementation
*/
@Override
public void endReadFromPngRaw() {
}
/**
* @see #size
*/
@Override
public int getSize() {
return size;
}
@Override
public int getElem(final int i) {
return scanline[i];
}
/**
* @return see {@link #scanline}
*/
public int[] getScanline() {
return scanline;
}
@Override
public ImageInfo getImageInfo() {
return imgInfo;
}
}

View File

@ -1,159 +0,0 @@
package ar.com.hjg.pngj;
import java.util.ArrayList;
import java.util.List;
/**
* Default implementation of {@link IImageLineSet}.
* <P>
* Supports all modes: single cursor, full rows, or partial. This should not be
* used for
*/
public abstract class ImageLineSetDefault<T extends IImageLine> implements IImageLineSet<T> {
protected final ImageInfo imgInfo;
private final boolean singleCursor;
private final int nlines, offset, step;
protected List<T> imageLines; // null if single cursor
protected T imageLine; // null unless single cursor
protected int currentRow = -1; // only relevant (and not much) for cursor
public ImageLineSetDefault(final ImageInfo imgInfo, final boolean singleCursor, final int nlinesx, final int noffsetx, final int stepx) {
this.imgInfo = imgInfo;
this.singleCursor = singleCursor;
if (singleCursor) {
this.nlines = 1; // we store only one line, no matter how many will be read
offset = 0;
this.step = 1;// don't matter
} else {
this.nlines = nlinesx; // note that it can also be 1
offset = noffsetx;
this.step = stepx;// don't matter
}
createImageLines();
}
private void createImageLines() {
if (singleCursor)
imageLine = createImageLine();
else {
imageLines = new ArrayList<>();
for (int i = 0; i < nlines; i++)
imageLines.add(createImageLine());
}
}
protected abstract T createImageLine();
/**
* Retrieves the image line
* <p>
* Warning: the argument is the row number in the original image
* <p>
* If this is a cursor, no check is done, always the same row is returned
*/
@Override
public T getImageLine(final int n) {
currentRow = n;
if (singleCursor)
return imageLine;
else {
final int r = imageRowToMatrixRowStrict(n);
if (r < 0)
throw new PngjException("Invalid row number");
return imageLines.get(r);
}
}
/**
* does not check for valid range
*/
@Override
public T getImageLineRawNum(final int r) {
if (singleCursor)
return imageLine;
else
return imageLines.get(r);
}
/**
* True if the set contains this image line
* <p>
* Warning: the argument is the row number in the original image
* <p>
* If this works as cursor, this returns true only if that is the number of
* its "current" line
*/
@Override
public boolean hasImageLine(final int n) {
return singleCursor ? currentRow == n : imageRowToMatrixRowStrict(n) >= 0;
}
/**
* How many lines does this object contain?
*/
@Override
public int size() {
return nlines;
}
/**
* Same as {@link #imageRowToMatrixRow(int)}, but returns negative if
* invalid
*/
public int imageRowToMatrixRowStrict(int imrow) {
imrow -= offset;
final int mrow = imrow >= 0 && (step == 1 || imrow % step == 0) ? imrow / step : -1;
return mrow < nlines ? mrow : -1;
}
/**
* Converts from matrix row number (0 : nRows-1) to image row number
*
* @param mrow
* Matrix row number
* @return Image row number. Returns trash if mrow is invalid
*/
public int matrixRowToImageRow(final int mrow) {
return mrow * step + offset;
}
/**
* Converts from real image row to this object row number.
* <p>
* Warning: this always returns a valid matrix row (clamping on 0 : nrows-1,
* and rounding down)
* <p>
* Eg: rowOffset=4,rowStep=2 imageRowToMatrixRow(17) returns 6 ,
* imageRowToMatrixRow(1) returns 0
*/
public int imageRowToMatrixRow(final int imrow) {
final int r = (imrow - offset) / step;
return r < 0 ? 0 : r < nlines ? r : nlines - 1;
}
/**
* utility function, given a factory for one line, returns a factory for a
* set
*/
public static <T extends IImageLine> IImageLineSetFactory<T> createImageLineSetFactoryFromImageLineFactory(
final IImageLineFactory<T> ifactory) { // ugly method must have ugly name. don't let this intimidate you
return (iminfo, singleCursor, nlines, noffset,
step) -> new ImageLineSetDefault<T>(iminfo, singleCursor, nlines, noffset, step) {
@Override
protected T createImageLine() {
return ifactory.createImageLine(iminfo);
}
};
}
/** utility function, returns default factory for {@link ImageLineInt} */
public static IImageLineSetFactory<ImageLineInt> getFactoryInt() {
return ImageLineSetDefault.createImageLineSetFactoryFromImageLineFactory(ImageLineInt.getFactory());
}
/** utility function, returns default factory for {@link ImageLineByte} */
public static IImageLineSetFactory<ImageLineByte> getFactoryByte() {
return ImageLineSetDefault.createImageLineSetFactoryFromImageLineFactory(ImageLineByte.getFactory());
}
}

View File

@ -1,332 +0,0 @@
package ar.com.hjg.pngj;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.logging.Logger;
/**
* Some utility static methods for internal use.
* <p>
* Client code should not normally use this class
* <p>
*/
public final class PngHelperInternal {
public static final String KEY_LOGGER = "ar.com.pngj";
public static final Logger LOGGER = Logger.getLogger(PngHelperInternal.KEY_LOGGER);
/**
* Default charset, used internally by PNG for several things
*/
public static String charsetLatin1name = "UTF-8";
public static Charset charsetLatin1 = Charset.forName(PngHelperInternal.charsetLatin1name);
/**
* UTF-8 is only used for some chunks
*/
public static String charsetUTF8name = "UTF-8";
public static Charset charsetUTF8 = Charset.forName(PngHelperInternal.charsetUTF8name);
private static ThreadLocal<Boolean> DEBUG = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return Boolean.FALSE;
}
};
/**
* PNG magic bytes
*/
public static byte[] getPngIdSignature() {
return new byte[] { -119, 80, 78, 71, 13, 10, 26, 10 };
}
public static int doubleToInt100000(final double d) {
return (int) (d * 100000.0 + 0.5);
}
public static double intToDouble100000(final int i) {
return i / 100000.0;
}
public static int readByte(final InputStream is) {
try {
return is.read();
} catch (final IOException e) {
throw new PngjInputException("error reading byte", e);
}
}
/**
* -1 if eof
*
* PNG uses "network byte order"
*/
public static int readInt2(final InputStream is) {
try {
final int b1 = is.read();
final int b2 = is.read();
if (b1 == -1 || b2 == -1)
return -1;
return b1 << 8 | b2;
} catch (final IOException e) {
throw new PngjInputException("error reading Int2", e);
}
}
/**
* -1 if eof
*/
public static int readInt4(final InputStream is) {
try {
final int b1 = is.read();
final int b2 = is.read();
final int b3 = is.read();
final int b4 = is.read();
if (b1 == -1 || b2 == -1 || b3 == -1 || b4 == -1)
return -1;
return b1 << 24 | b2 << 16 | (b3 << 8) + b4;
} catch (final IOException e) {
throw new PngjInputException("error reading Int4", e);
}
}
public static int readInt1fromByte(final byte[] b, final int offset) {
return b[offset] & 0xff;
}
public static int readInt2fromBytes(final byte[] b, final int offset) {
return (b[offset] & 0xff) << 8 | b[offset + 1] & 0xff;
}
public static int readInt4fromBytes(final byte[] b, final int offset) {
return (b[offset] & 0xff) << 24 | (b[offset + 1] & 0xff) << 16 | (b[offset + 2] & 0xff) << 8 | b[offset + 3] & 0xff;
}
public static void writeByte(final OutputStream os, final byte b) {
try {
os.write(b);
} catch (final IOException e) {
throw new PngjOutputException(e);
}
}
public static void writeByte(final OutputStream os, final byte[] bs) {
try {
os.write(bs);
} catch (final IOException e) {
throw new PngjOutputException(e);
}
}
public static void writeInt2(final OutputStream os, final int n) {
final byte[] temp = { (byte) (n >> 8 & 0xff), (byte) (n & 0xff) };
PngHelperInternal.writeBytes(os, temp);
}
public static void writeInt4(final OutputStream os, final int n) {
final byte[] temp = new byte[4];
PngHelperInternal.writeInt4tobytes(n, temp, 0);
PngHelperInternal.writeBytes(os, temp);
}
public static void writeInt2tobytes(final int n, final byte[] b, final int offset) {
b[offset] = (byte) (n >> 8 & 0xff);
b[offset + 1] = (byte) (n & 0xff);
}
public static void writeInt4tobytes(final int n, final byte[] b, final int offset) {
b[offset] = (byte) (n >> 24 & 0xff);
b[offset + 1] = (byte) (n >> 16 & 0xff);
b[offset + 2] = (byte) (n >> 8 & 0xff);
b[offset + 3] = (byte) (n & 0xff);
}
/**
* guaranteed to read exactly len bytes. throws error if it can't
*/
public static void readBytes(final InputStream is, final byte[] b, final int offset, final int len) {
if (len == 0)
return;
try {
int read = 0;
while (read < len) {
final int n = is.read(b, offset + read, len - read);
if (n < 1)
throw new PngjInputException("error reading bytes, " + n + " !=" + len);
read += n;
}
} catch (final IOException e) {
throw new PngjInputException("error reading", e);
}
}
public static void skipBytes(final InputStream is, long len) {
try {
while (len > 0) {
final long n1 = is.skip(len);
if (n1 > 0)
len -= n1;
else if (n1 == 0) { // should we retry? lets read one byte
if (is.read() == -1) // EOF
break;
else
len--;
} else
// negative? this should never happen but...
throw new IOException("skip() returned a negative value ???");
}
} catch (final IOException e) {
throw new PngjInputException(e);
}
}
public static void writeBytes(final OutputStream os, final byte[] b) {
try {
os.write(b);
} catch (final IOException e) {
throw new PngjOutputException(e);
}
}
public static void writeBytes(final OutputStream os, final byte[] b, final int offset, final int n) {
try {
os.write(b, offset, n);
} catch (final IOException e) {
throw new PngjOutputException(e);
}
}
public static void logdebug(final String msg) {
if (PngHelperInternal.isDebug())
System.err.println("logdebug: " + msg);
}
// / filters
public static int filterRowNone(final int r) {
return r & 0xFF;
}
public static int filterRowSub(final int r, final int left) {
return r - left & 0xFF;
}
public static int filterRowUp(final int r, final int up) {
return r - up & 0xFF;
}
public static int filterRowAverage(final int r, final int left, final int up) {
return r - (left + up) / 2 & 0xFF;
}
public static int filterRowPaeth(final int r, final int left, final int up, final int upleft) { // a = left, b = above, c
// = upper left
return r - PngHelperInternal.filterPaethPredictor(left, up, upleft) & 0xFF;
}
static int filterPaethPredictor(final int a, final int b, final int c) { // a = left, b =
// above, c = upper
// left
// from http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html
final int p = a + b - c;// ; initial estimate
final int pa = p >= a ? p - a : a - p;
final int pb = p >= b ? p - b : b - p;
final int pc = p >= c ? p - c : c - p;
// ; return nearest of a,b,c,
// ; breaking ties in order a,b,c.
if (pa <= pb && pa <= pc)
return a;
else if (pb <= pc)
return b;
else
return c;
}
/**
* Prits a debug message (prints class name, method and line number)
*
* @param obj
* : Object to print
*/
public static void debug(final Object obj) {
PngHelperInternal.debug(obj, 1, true);
}
/**
* Prits a debug message (prints class name, method and line number)
*
* @param obj
* : Object to print
* @param offset
* : Offset N lines from stacktrace
*/
static void debug(final Object obj, final int offset) {
PngHelperInternal.debug(obj, offset, true);
}
public static InputStream istreamFromFile(final File f) {
FileInputStream is;
try {
is = new FileInputStream(f);
} catch (final Exception e) {
throw new PngjInputException("Could not open " + f, e);
}
return is;
}
static OutputStream ostreamFromFile(final File f) {
return PngHelperInternal.ostreamFromFile(f, true);
}
static OutputStream ostreamFromFile(final File f, final boolean overwrite) {
return PngHelperInternal2.ostreamFromFile(f, overwrite);
}
/**
* Prints a debug message (prints class name, method and line number) to
* stderr and logFile
*
* @param obj
* : Object to print
* @param offset
* : Offset N lines from stacktrace
* @param newLine
* : Print a newline char at the end ('\n')
*/
static void debug(final Object obj, final int offset, final boolean newLine) {
final StackTraceElement ste = new Exception().getStackTrace()[1 + offset];
String steStr = ste.getClassName();
final int ind = steStr.lastIndexOf('.');
steStr = steStr.substring(ind + 1);
steStr += "." + ste.getMethodName() + "(" + ste.getLineNumber() + "): " + (obj == null ? null : obj.toString());
System.err.println(steStr);
}
/**
* Sets a global debug flag. This is bound to a thread.
*/
public static void setDebug(final boolean b) {
PngHelperInternal.DEBUG.set(b);
}
public static boolean isDebug() {
return PngHelperInternal.DEBUG.get().booleanValue();
}
public static long getDigest(final PngReader pngr) {
return pngr.getSimpleDigest();
}
public static void initCrcForTests(final PngReader pngr) {
pngr.prepareSimpleDigestComputation();
}
public static long getRawIdatBytes(final PngReader r) { // in case of image with frames, returns the current one
return r.interlaced ? r.getChunkseq().getDeinterlacer().getTotalRawBytes() : r.getCurImgInfo().getTotalRawBytes();
}
}

View File

@ -1,35 +0,0 @@
package ar.com.hjg.pngj;
import java.io.File;
import java.io.OutputStream;
/**
* For organization purposes, this class is the onlt that uses classes not in
* GAE (Google App Engine) white list
* <p>
* You should not use this class in GAE
*/
final class PngHelperInternal2 {
/**
* WARNING: this uses FileOutputStream which is not allowed in
* GoogleAppEngine
*
* In GAE, dont use this
*
* @param f
* @param allowoverwrite
* @return
*/
static OutputStream ostreamFromFile(final File f, final boolean allowoverwrite) {
java.io.FileOutputStream os = null; // this will fail in GAE!
if (f.exists() && !allowoverwrite)
throw new PngjOutputException("File already exists: " + f);
try {
os = new java.io.FileOutputStream(f);
} catch (final Exception e) {
throw new PngjInputException("Could not open for write" + f, e);
}
return os;
}
}

View File

@ -1,647 +0,0 @@
package ar.com.hjg.pngj;
import java.io.File;
import java.io.InputStream;
import java.util.zip.Adler32;
import java.util.zip.CRC32;
import ar.com.hjg.pngj.chunks.ChunkLoadBehaviour;
import ar.com.hjg.pngj.chunks.ChunksList;
import ar.com.hjg.pngj.chunks.PngChunkFCTL;
import ar.com.hjg.pngj.chunks.PngChunkFDAT;
import ar.com.hjg.pngj.chunks.PngChunkIDAT;
import ar.com.hjg.pngj.chunks.PngMetadata;
/**
* Reads a PNG image (pixels and/or metadata) from a file or stream.
* <p>
* Each row is read as an {@link ImageLineInt} object (one int per sample), but
* this can be changed by setting a
* different ImageLineFactory
* <p>
* Internally, this wraps a {@link ChunkSeqReaderPng} with a
* {@link BufferedStreamFeeder}
* <p>
* The reading sequence is as follows: <br>
* 1. At construction time, the header and IHDR chunk are read (basic image
* info) <br>
* 2. Afterwards you can set some additional global options. Eg.
* {@link #setCrcCheckDisabled()}.<br>
* 3. Optional: If you call getMetadata() or getChunksLisk() before start
* reading the rows, all the chunks before IDAT
* are then loaded and available <br>
* 4a. The rows are read in order by calling {@link #readRow()}. You can also
* call {@link #readRow(int)} to skip rows
* -but you can't go backwards, at least not with this implementation. This
* method returns a {@link IImageLine} object
* which can be casted to the concrete class. This class returns by default a
* {@link ImageLineInt}, but this can be
* changed.<br>
* 4b. Alternatively, you can read all rows, or a subset, in a single call:
* {@link #readRows()},
* {@link #readRows(int, int, int)} ,etc. In general this consumes more memory,
* but for interlaced images this is
* equally efficient, and more so if reading a small subset of rows.<br>
* 5. Reading of the last row automatically loads the trailing chunks, and ends
* the reader.<br>
* 6. end() also loads the trailing chunks, if not done, and finishes cleanly
* the reading and closes the stream.
* <p>
* See also {@link PngReaderInt} (esentially the same as this, and slightly
* preferred) and {@link PngReaderByte} (uses
* byte instead of int to store the samples).
*/
public class PngReader {
// some performance/defensive limits
/**
* Defensive limit: refuse to read more than 900MB, can be changed with
* {@link #setMaxTotalBytesRead(long)}
*/
public static final long MAX_TOTAL_BYTES_READ_DEFAULT = 901001001L; // ~ 900MB
/**
* Defensive limit: refuse to load more than 5MB of ancillary metadata, see
* {@link #setMaxBytesMetadata(long)} and
* also {@link #addChunkToSkip(String)}
*/
public static final long MAX_BYTES_METADATA_DEFAULT = 5024024; // for ancillary chunks
/**
* Skip ancillary chunks greater than 2MB, see
* {@link #setSkipChunkMaxSize(long)}
*/
public static final long MAX_CHUNK_SIZE_SKIP = 2024024; // chunks exceeding this size will be skipped (nor even CRC
// checked)
/**
* Basic image info - final and inmutable.
*/
public final ImageInfo imgInfo; // People always told me: be careful what you do, and don't go around declaring public
// fields...
/**
* flag: image was in interlaced format
*/
public final boolean interlaced;
/**
* This object has most of the intelligence to parse the chunks and
* decompress the IDAT stream
*/
protected final ChunkSeqReaderPng chunkseq;
/**
* Takes bytes from the InputStream and passes it to the ChunkSeqReaderPng.
* Never null.
*/
protected final BufferedStreamFeeder streamFeeder;
/**
* @see #getMetadata()
*/
protected final PngMetadata metadata; // this a wrapper over chunks
/**
* Current row number (reading or read), numbered from 0
*/
protected int rowNum = -1;
/**
* Represents the set of lines (rows) being read. Normally this works as a
* cursor, storing only one (the current) row.
* This stores several (perhaps all) rows only if calling
* {@link #readRows()} or for interlaced images (this later is
* transparent to the user)
*/
protected IImageLineSet<? extends IImageLine> imlinesSet;
/**
* This factory decides the concrete type of the ImageLine that will be
* used. See {@link ImageLineSetDefault} for
* examples
*/
private IImageLineSetFactory<? extends IImageLine> imageLineSetFactory;
CRC32 idatCrca;// for internal testing
Adler32 idatCrcb;// for internal testing
/**
* Constructs a PngReader object from a stream, with default options. This
* reads the signature and the first IHDR
* chunk only.
* <p>
* Warning: In case of exception the stream is NOT closed.
* <p>
* Warning: By default the stream will be closed when this object is
* {@link #close()}d. See
* {@link #PngReader(InputStream,boolean)} or
* {@link #setShouldCloseStream(boolean)}
* <p>
*
* @param inputStream
* PNG stream
*/
public PngReader(final InputStream inputStream) {
this(inputStream, true);
}
/**
* Same as {@link #PngReader(InputStream)} but allows to specify early if
* the stream must be closed
*
* @param inputStream
* @param shouldCloseStream
* The stream will be closed in case of exception (constructor
* included) or normal
* termination.
*/
public PngReader(final InputStream inputStream, final boolean shouldCloseStream) {
streamFeeder = new BufferedStreamFeeder(inputStream);
streamFeeder.setCloseStream(shouldCloseStream);
chunkseq = createChunkSeqReader();
try {
streamFeeder.setFailIfNoFeed(true);
if (!streamFeeder.feedFixed(chunkseq, 36)) // 8+13+12=36 PNG signature+IHDR chunk
throw new PngjInputException("error reading first 21 bytes");
imgInfo = chunkseq.getImageInfo();
interlaced = chunkseq.getDeinterlacer() != null;
setMaxBytesMetadata(PngReader.MAX_BYTES_METADATA_DEFAULT);
setMaxTotalBytesRead(PngReader.MAX_TOTAL_BYTES_READ_DEFAULT);
setSkipChunkMaxSize(PngReader.MAX_CHUNK_SIZE_SKIP);
chunkseq.addChunkToSkip(PngChunkFDAT.ID);// default: skip fdAT chunks!
chunkseq.addChunkToSkip(PngChunkFCTL.ID);// default: skip fctl chunks!
metadata = new PngMetadata(chunkseq.chunksList);
// sets a default factory (with ImageLineInt),
// this can be overwriten by a extended constructor, or by a setter
setLineSetFactory(ImageLineSetDefault.getFactoryInt());
rowNum = -1;
} catch (final RuntimeException e) {
streamFeeder.close();
chunkseq.close();
throw e;
}
}
/**
* Constructs a PngReader opening a file. Sets
* <tt>shouldCloseStream=true</tt>, so that the stream will be closed with
* this object.
*
* @param file
* PNG image file
*/
public PngReader(final File file) {
this(PngHelperInternal.istreamFromFile(file), true);
}
/**
* Reads chunks before first IDAT. Normally this is called automatically
* <p>
* Position before: after IDHR (crc included) Position after: just after the
* first IDAT chunk id
* <P>
* This can be called several times (tentatively), it does nothing if
* already run
* <p>
* (Note: when should this be called? in the constructor? hardly, because we
* loose the opportunity to call
* setChunkLoadBehaviour() and perhaps other settings before reading the
* first row? but sometimes we want to access
* some metadata (plte, phys) before. Because of this, this method can be
* called explicitly but is also called
* implicititly in some methods (getMetatada(), getChunksList())
*/
protected void readFirstChunks() {
while (chunkseq.currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT)
if (streamFeeder.feed(chunkseq) <= 0)
throw new PngjInputException("premature ending reading first chunks");
}
/**
* Determines which ancillary chunks (metadata) are to be loaded and which
* skipped.
* <p>
* Additional restrictions may apply. See also
* {@link #setChunksToSkip(String...)}, {@link #addChunkToSkip(String)},
* {@link #setMaxBytesMetadata(long)}, {@link #setSkipChunkMaxSize(long)}
*
* @param chunkLoadBehaviour
* {@link ChunkLoadBehaviour}
*/
public void setChunkLoadBehaviour(final ChunkLoadBehaviour chunkLoadBehaviour) {
chunkseq.setChunkLoadBehaviour(chunkLoadBehaviour);
}
/**
* All loaded chunks (metada). If we have not yet end reading the image,
* this will include only the chunks before the
* pixels data (IDAT)
* <p>
* Critical chunks are included, except that all IDAT chunks appearance are
* replaced by a single dummy-marker IDAT
* chunk. These might be copied to the PngWriter
* <p>
*
* @see #getMetadata()
*/
public ChunksList getChunksList() {
return getChunksList(true);
}
public ChunksList getChunksList(final boolean forceLoadingOfFirstChunks) {
if (forceLoadingOfFirstChunks && chunkseq.firstChunksNotYetRead())
readFirstChunks();
return chunkseq.chunksList;
}
int getCurrentChunkGroup() {
return chunkseq.currentChunkGroup;
}
/**
* High level wrapper over chunksList
*
* @see #getChunksList()
*/
public PngMetadata getMetadata() {
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
return metadata;
}
/**
* Reads next row.
*
* The caller must know that there are more rows to read.
*
* @return Never null. Throws PngInputException if no more
*/
public IImageLine readRow() {
return readRow(rowNum + 1);
}
/**
* True if last row has not yet been read
*/
public boolean hasMoreRows() {
return rowNum < getCurImgInfo().rows - 1;
}
/**
* The row number is mostly meant as a check, the rows must be called in
* ascending order (not necessarily consecutive)
*/
public IImageLine readRow(final int nrow) {
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
if (!interlaced) {
if (imlinesSet == null)
imlinesSet = createLineSet(true, -1, 0, 1);
final IImageLine line = imlinesSet.getImageLine(nrow);
if (nrow == rowNum)
return line; // already read??
else if (nrow < rowNum)
throw new PngjInputException("rows must be read in increasing order: " + nrow);
while (rowNum < nrow) {
while (!chunkseq.getIdatSet().isRowReady())
if (streamFeeder.feed(chunkseq) < 1)
throw new PngjInputException("premature ending");
rowNum++;
chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb);
if (rowNum == nrow) {
line.readFromPngRaw(chunkseq.getIdatSet().getUnfilteredRow(), getCurImgInfo().bytesPerRow + 1, 0, 1);
line.endReadFromPngRaw();
}
chunkseq.getIdatSet().advanceToNextRow();
}
return line;
} else { // and now, for something completely different (interlaced!)
if (imlinesSet == null) {
imlinesSet = createLineSet(false, getCurImgInfo().rows, 0, 1);
loadAllInterlaced(getCurImgInfo().rows, 0, 1);
}
rowNum = nrow;
return imlinesSet.getImageLine(nrow);
}
}
/**
* Reads all rows in a ImageLineSet This is handy, but less memory-efficient
* (except for interlaced)
*/
public IImageLineSet<? extends IImageLine> readRows() {
return readRows(getCurImgInfo().rows, 0, 1);
}
/**
* Reads a subset of rows.
* <p>
* This method should called once, and not be mixed with {@link #readRow()}
*
* @param nRows
* how many rows to read (default: imageInfo.rows; negative:
* autocompute)
* @param rowOffset
* rows to skip (default:0)
* @param rowStep
* step between rows to load( default:1)
*/
public IImageLineSet<? extends IImageLine> readRows(int nRows, final int rowOffset, final int rowStep) {
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
if (nRows < 0)
nRows = (getCurImgInfo().rows - rowOffset) / rowStep;
if (rowStep < 1 || rowOffset < 0 || nRows == 0 || nRows * rowStep + rowOffset > getCurImgInfo().rows)
throw new PngjInputException("bad args");
if (rowNum >= rowOffset)
throw new PngjInputException("readRows cannot be mixed with readRow");
imlinesSet = createLineSet(false, nRows, rowOffset, rowStep);
if (!interlaced) {
int m = -1; // last row already read in
while (m < nRows - 1) {
while (!chunkseq.getIdatSet().isRowReady())
if (streamFeeder.feed(chunkseq) < 1)
throw new PngjInputException("Premature ending");
rowNum++;
chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb);
m = (rowNum - rowOffset) / rowStep;
if (rowNum >= rowOffset && rowStep * m + rowOffset == rowNum) {
final IImageLine line = imlinesSet.getImageLine(rowNum);
line.readFromPngRaw(chunkseq.getIdatSet().getUnfilteredRow(), getCurImgInfo().bytesPerRow + 1, 0, 1);
line.endReadFromPngRaw();
}
chunkseq.getIdatSet().advanceToNextRow();
}
} else
loadAllInterlaced(nRows, rowOffset, rowStep);
chunkseq.getIdatSet().done();
return imlinesSet;
}
/**
* Sets the factory that creates the ImageLine. By default, this
* implementation uses ImageLineInt but this can be
* changed (at construction time or later) by calling this method.
* <p>
* See also {@link #createLineSet(boolean, int, int, int)}
*
* @param factory
*/
public void setLineSetFactory(final IImageLineSetFactory<? extends IImageLine> factory) {
imageLineSetFactory = factory;
}
/**
* By default this uses the factory (which, by default creates
* ImageLineInt). You should rarely override this.
* <p>
* See doc in
* {@link IImageLineSetFactory#create(ImageInfo, boolean, int, int, int)}
*/
protected IImageLineSet<? extends IImageLine> createLineSet(final boolean singleCursor, final int nlines,
final int noffset, final int step) {
return imageLineSetFactory.create(getCurImgInfo(), singleCursor, nlines, noffset, step);
}
protected void loadAllInterlaced(final int nRows, final int rowOffset, final int rowStep) {
final IdatSet idat = chunkseq.getIdatSet();
int nread = 0;
do {
while (!chunkseq.getIdatSet().isRowReady())
if (streamFeeder.feed(chunkseq) <= 0)
break;
if (!chunkseq.getIdatSet().isRowReady())
throw new PngjInputException("Premature ending?");
chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb);
final int rowNumreal = idat.rowinfo.rowNreal;
final boolean inset = imlinesSet.hasImageLine(rowNumreal);
if (inset) {
imlinesSet.getImageLine(rowNumreal).readFromPngRaw(idat.getUnfilteredRow(), idat.rowinfo.buflen, idat.rowinfo.oX, idat.rowinfo.dX);
nread++;
}
idat.advanceToNextRow();
} while (nread < nRows || !idat.isDone());
idat.done();
for (int i = 0, j = rowOffset; i < nRows; i++, j += rowStep)
imlinesSet.getImageLine(j).endReadFromPngRaw();
}
/**
* Reads all the (remaining) file, skipping the pixels data. This is much
* more efficient that calling
* {@link #readRow()}, specially for big files (about 10 times faster!),
* because it doesn't even decompress the IDAT
* stream and disables CRC check Use this if you are not interested in
* reading pixels,only metadata.
*/
public void readSkippingAllRows() {
chunkseq.addChunkToSkip(PngChunkIDAT.ID);
chunkseq.addChunkToSkip(PngChunkFDAT.ID);
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
end();
}
/**
* Set total maximum bytes to read (0: unlimited; default: 200MB). <br>
* These are the bytes read (not loaded) in the input stream. If exceeded,
* an exception will be thrown.
*/
public void setMaxTotalBytesRead(final long maxTotalBytesToRead) {
chunkseq.setMaxTotalBytesRead(maxTotalBytesToRead);
}
/**
* Set total maximum bytes to load from ancillary chunks (0: unlimited;
* default: 5Mb).<br>
* If exceeded, some chunks will be skipped
*/
public void setMaxBytesMetadata(final long maxBytesMetadata) {
chunkseq.setMaxBytesMetadata(maxBytesMetadata);
}
/**
* Set maximum size in bytes for individual ancillary chunks (0: unlimited;
* default: 2MB). <br>
* Chunks exceeding this length will be skipped (the CRC will not be
* checked) and the chunk will be saved as a
* PngChunkSkipped object. See also setSkipChunkIds
*/
public void setSkipChunkMaxSize(final long skipChunkMaxSize) {
chunkseq.setSkipChunkMaxSize(skipChunkMaxSize);
}
/**
* Chunks ids to be skipped. <br>
* These chunks will be skipped (the CRC will not be checked) and the chunk
* will be saved as a PngChunkSkipped object.
* See also setSkipChunkMaxSize
*/
public void setChunksToSkip(final String... chunksToSkip) {
chunkseq.setChunksToSkip(chunksToSkip);
}
public void addChunkToSkip(final String chunkToSkip) {
chunkseq.addChunkToSkip(chunkToSkip);
}
public void dontSkipChunk(final String chunkToSkip) {
chunkseq.dontSkipChunk(chunkToSkip);
}
/**
* if true, input stream will be closed after ending read
* <p>
* default=true
*/
public void setShouldCloseStream(final boolean shouldCloseStream) {
streamFeeder.setCloseStream(shouldCloseStream);
}
/**
* Reads till end of PNG stream and call <tt>close()</tt>
*
* This should normally be called after reading the pixel data, to read the
* trailing chunks and close the stream. But
* it can be called at anytime. This will also read the first chunks if not
* still read, and skip pixels (IDAT) if
* still pending.
*
* If you want to read all metadata skipping pixels, readSkippingAllRows()
* is a little more efficient.
*
* If you want to abort immediately, call instead <tt>close()</tt>
*/
public void end() {
try {
if (chunkseq.firstChunksNotYetRead())
readFirstChunks();
if (chunkseq.getIdatSet() != null && !chunkseq.getIdatSet().isDone())
chunkseq.getIdatSet().done();
while (!chunkseq.isDone())
if (streamFeeder.feed(chunkseq) <= 0)
break;
} finally {
close();
}
}
/**
* Releases resources, and closes stream if corresponds. Idempotent, secure,
* no exceptions.
*
* This can be also called for abort. It is recommended to call this in case
* of exceptions
*/
public void close() {
try {
if (chunkseq != null)
chunkseq.close();
} catch (final Exception e) {
PngHelperInternal.LOGGER.warning("error closing chunk sequence:" + e.getMessage());
}
if (streamFeeder != null)
streamFeeder.close();
}
/**
* Interlaced PNG is accepted -though not welcomed- now...
*/
public boolean isInterlaced() {
return interlaced;
}
/**
* Disables the CRC integrity check in IDAT chunks and ancillary chunks,
* this gives a slight increase in reading speed
* for big files
*/
public void setCrcCheckDisabled() {
chunkseq.setCheckCrc(false);
}
/**
* Gets wrapped {@link ChunkSeqReaderPng} object
*/
public ChunkSeqReaderPng getChunkseq() {
return chunkseq;
}
/**
* called on construction time. Override if you want an alternative class
*/
protected ChunkSeqReaderPng createChunkSeqReader() {
return new ChunkSeqReaderPng(false);
}
/**
* Enables and prepare the simple digest computation. Must be called before
* reading the pixels. See
* {@link #getSimpleDigestHex()}
*/
public void prepareSimpleDigestComputation() {
if (idatCrca == null)
idatCrca = new CRC32();
else
idatCrca.reset();
if (idatCrcb == null)
idatCrcb = new Adler32();
else
idatCrcb.reset();
imgInfo.updateCrc(idatCrca);
idatCrcb.update((byte) imgInfo.rows); // not important
}
long getSimpleDigest() {
if (idatCrca == null)
return 0;
else
return idatCrca.getValue() ^ idatCrcb.getValue() << 31;
}
/**
* Pseudo 64-bits digest computed over the basic image properties and the
* raw pixels data: it should coincide for
* equivalent images encoded with different filters and compressors; but
* will not coincide for
* interlaced/non-interlaced; also, this does not take into account the
* palette info. This will be valid only if
* {@link #prepareSimpleDigestComputation()} has been called, and all rows
* have been read. Not fool-proof, not
* cryptografically secure, only for informal testing and duplicates
* detection.
*
* @return A 64-digest in hexadecimal
*/
public String getSimpleDigestHex() {
return String.format("%016X", getSimpleDigest());
}
/**
* Basic info, for debugging.
*/
@Override
public String toString() { // basic info
return imgInfo.toString() + " interlaced=" + interlaced;
}
/**
* Basic info, in a compact format, apt for scripting
* COLSxROWS[dBITDEPTH][a][p][g][i] ( the default dBITDEPTH='d8' is
* ommited)
*
*/
public String toStringCompact() {
return imgInfo.toStringBrief() + (interlaced ? "i" : "");
}
public ImageInfo getImgInfo() {
return imgInfo;
}
public ImageInfo getCurImgInfo() {
return chunkseq.getCurImgInfo();
}
}

View File

@ -1,211 +0,0 @@
package ar.com.hjg.pngj;
import java.io.File;
import java.io.InputStream;
import java.util.List;
import ar.com.hjg.pngj.chunks.PngChunk;
import ar.com.hjg.pngj.chunks.PngChunkACTL;
import ar.com.hjg.pngj.chunks.PngChunkFCTL;
import ar.com.hjg.pngj.chunks.PngChunkFDAT;
import ar.com.hjg.pngj.chunks.PngChunkIDAT;
/**
*/
public class PngReaderApng extends PngReaderByte {
public PngReaderApng(final File file) {
super(file);
dontSkipChunk(PngChunkFCTL.ID);
}
public PngReaderApng(final InputStream inputStream) {
super(inputStream);
dontSkipChunk(PngChunkFCTL.ID);
}
private Boolean apngKind = null;
private boolean firsIdatApngFrame = false;
protected PngChunkACTL actlChunk; // null if not APNG
private PngChunkFCTL fctlChunk; // current (null for the pseudo still frame)
/**
* Current frame number (reading or read). First animated frame is 0. Frame
* -1 represents the IDAT (default image)
* when it's not part of the animation
*/
protected int frameNum = -1; // incremented after each fctl finding
public boolean isApng() {
if (apngKind == null) {
// this triggers the loading of first chunks;
actlChunk = (PngChunkACTL) getChunksList().getById1(PngChunkACTL.ID); // null if not apng
apngKind = actlChunk != null;
firsIdatApngFrame = fctlChunk != null;
}
return apngKind.booleanValue();
}
public void advanceToFrame(final int frame) {
if (frame < frameNum)
throw new PngjInputException("Cannot go backwards");
if (frame >= getApngNumFrames())
throw new PngjInputException("Frame out of range " + frame);
if (frame > frameNum) {
addChunkToSkip(PngChunkIDAT.ID);
addChunkToSkip(PngChunkFDAT.ID);
if (chunkseq.getIdatSet() != null && !chunkseq.getIdatSet().isDone())
chunkseq.getIdatSet().done(); // seems to be necessary sometimes (we should check this)
while (frameNum < frame & !chunkseq.isDone())
if (streamFeeder.feed(chunkseq) <= 0)
break;
}
if (frame == frameNum) { // prepare to read rows. at this point we have a new
dontSkipChunk(PngChunkIDAT.ID);
dontSkipChunk(PngChunkFDAT.ID);
rowNum = -1;
imlinesSet = null;// force recreation (this is slightly dirty)
// seek the next IDAT/fDAT - TODO: set the expected sequence number
while (!chunkseq.isDone() && !chunkseq.getCurChunkReader().isFromDeflatedSet())
if (streamFeeder.feed(chunkseq) <= 0)
break;
} else
throw new PngjInputException("unexpected error seeking from frame " + frame);
}
/**
* True if it has a default image (IDAT) that is not part of the animation.
* In that case, we consider it as a
* pseudo-frame (number -1)
*/
public boolean hasExtraStillImage() {
return isApng() && !firsIdatApngFrame;
}
/**
* Only counts true animation frames.
*/
public int getApngNumFrames() {
if (isApng())
return actlChunk.getNumFrames();
else
return 0;
}
/**
* 0 if it's to been played infinitely. -1 if not APNG
*/
public int getApngNumPlays() {
if (isApng())
return actlChunk.getNumPlays();
else
return -1;
}
@Override
public IImageLine readRow() {
// TODO Auto-generated method stub
return super.readRow();
}
@Override
public boolean hasMoreRows() {
// TODO Auto-generated method stub
return super.hasMoreRows();
}
@Override
public IImageLine readRow(final int nrow) {
// TODO Auto-generated method stub
return super.readRow(nrow);
}
@Override
public IImageLineSet<? extends IImageLine> readRows() {
// TODO Auto-generated method stub
return super.readRows();
}
@Override
public IImageLineSet<? extends IImageLine> readRows(final int nRows, final int rowOffset, final int rowStep) {
// TODO Auto-generated method stub
return super.readRows(nRows, rowOffset, rowStep);
}
@Override
public void readSkippingAllRows() {
// TODO Auto-generated method stub
super.readSkippingAllRows();
}
@Override
protected ChunkSeqReaderPng createChunkSeqReader() {
final ChunkSeqReaderPng cr = new ChunkSeqReaderPng(false) {
@Override
public boolean shouldSkipContent(final int len, final String id) {
return super.shouldSkipContent(len, id);
}
@Override
protected boolean isIdatKind(final String id) {
return id.equals(PngChunkIDAT.ID) || id.equals(PngChunkFDAT.ID);
}
@Override
protected DeflatedChunksSet createIdatSet(final String id) {
final IdatSet ids = new IdatSet(id, getCurImgInfo(), deinterlacer);
ids.setCallbackMode(callbackMode);
return ids;
}
@Override
protected void startNewChunk(final int len, final String id, final long offset) {
super.startNewChunk(len, id, offset);
}
@Override
protected void postProcessChunk(final ChunkReader chunkR) {
super.postProcessChunk(chunkR);
if (chunkR.getChunkRaw().id.equals(PngChunkFCTL.ID)) {
frameNum++;
final List<PngChunk> chunkslist = chunkseq.getChunks();
fctlChunk = (PngChunkFCTL) chunkslist.get(chunkslist.size() - 1);
// as this is slightly dirty, we check
if (chunkR.getChunkRaw().getOffset() != fctlChunk.getRaw().getOffset())
throw new PngjInputException("something went wrong");
final ImageInfo frameInfo = fctlChunk.getEquivImageInfo();
getChunkseq().updateCurImgInfo(frameInfo);
}
}
@SuppressWarnings("unlikely-arg-type")
@Override
protected boolean countChunkTypeAsAncillary(final String id) {
// we don't count fdat as ancillary data
return super.countChunkTypeAsAncillary(id) && !id.equals(id.equals(PngChunkFDAT.ID));
}
};
return cr;
}
/**
* @see #frameNum
*/
public int getFrameNum() {
return frameNum;
}
@Override
public void end() {
// TODO Auto-generated method stub
super.end();
}
public PngChunkFCTL getFctl() {
return fctlChunk;
}
}

View File

@ -1,32 +0,0 @@
package ar.com.hjg.pngj;
import java.io.File;
import java.io.InputStream;
/**
* Trivial extension of {@link PngReader} that uses {@link ImageLineByte}
* <p>
* The factory is set at construction time. Remember that this could still be
* changed at runtime.
*/
public class PngReaderByte extends PngReader {
public PngReaderByte(final File file) {
super(file);
setLineSetFactory(ImageLineSetDefault.getFactoryByte());
}
public PngReaderByte(final InputStream inputStream) {
super(inputStream);
setLineSetFactory(ImageLineSetDefault.getFactoryByte());
}
/**
* Utility method that casts {@link #readRow()} return to
* {@link ImageLineByte}.
*/
public ImageLineByte readRowByte() {
return (ImageLineByte) readRow();
}
}

View File

@ -1,101 +0,0 @@
package ar.com.hjg.pngj;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import ar.com.hjg.pngj.chunks.PngChunk;
/**
* This class allows to use a simple PNG reader as an input filter, wrapping a
* ChunkSeqReaderPng in callback mode.
*
* In this sample implementation, all IDAT chunks are skipped and the rest are
* stored. An example of use, that lets us
* grab the Metadata and let the pixels go towards a BufferedImage:
*
*
* <pre class="code">
* PngReaderFilter reader = new PngReaderFilter(new FileInputStream(&quot;image.png&quot;));
* BufferedImage image1 = ImageIO.read(reader);
* reader.readUntilEndAndClose(); // in case ImageIO.read() does not read the traling chunks (it happens)
* System.out.println(reader.getChunksList());
* </pre>
*
*/
public class PngReaderFilter extends FilterInputStream {
private final ChunkSeqReaderPng chunkseq;
public PngReaderFilter(final InputStream arg0) {
super(arg0);
chunkseq = createChunkSequenceReader();
}
protected ChunkSeqReaderPng createChunkSequenceReader() {
return new ChunkSeqReaderPng(true) {
@Override
public boolean shouldSkipContent(final int len, final String id) {
return super.shouldSkipContent(len, id) || id.equals("IDAT");
}
@Override
protected boolean shouldCheckCrc(final int len, final String id) {
return false;
}
@Override
protected void postProcessChunk(final ChunkReader chunkR) {
super.postProcessChunk(chunkR);
// System.out.println("processed chunk " + chunkR.getChunkRaw().id);
}
};
}
@Override
public void close() throws IOException {
super.close();
chunkseq.close();
}
@Override
public int read() throws IOException {
final int r = super.read();
if (r > 0)
chunkseq.feedAll(new byte[] { (byte) r }, 0, 1);
return r;
}
@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
final int res = super.read(b, off, len);
if (res > 0)
chunkseq.feedAll(b, off, res);
return res;
}
@Override
public int read(final byte[] b) throws IOException {
final int res = super.read(b);
if (res > 0)
chunkseq.feedAll(b, 0, res);
return res;
}
public void readUntilEndAndClose() throws IOException {
final BufferedStreamFeeder br = new BufferedStreamFeeder(in);
while (!chunkseq.isDone() && br.hasMoreToFeed())
br.feed(chunkseq);
close();
}
public List<PngChunk> getChunksList() {
return chunkseq.getChunks();
}
public ChunkSeqReaderPng getChunkseq() {
return chunkseq;
}
}

View File

@ -1,39 +0,0 @@
package ar.com.hjg.pngj;
import java.io.File;
import java.io.InputStream;
/**
* Trivial extension of {@link PngReader} that uses {@link ImageLineInt}.
* <p>
* In the current implementation this is quite dummy/redundant, because (for
* backward compatibility) PngReader already
* uses a {@link ImageLineInt}.
* <p>
* The factory is set at construction time. Remember that this could still be
* changed at runtime.
*/
public class PngReaderInt extends PngReader {
public PngReaderInt(final File file) {
super(file); // not necessary to set factory, PngReader already does that
}
public PngReaderInt(final InputStream inputStream) {
super(inputStream);
}
/**
* Utility method that casts the IImageLine to a ImageLineInt
*
* This only make sense for this concrete class
*
*/
public ImageLineInt readRowInt() {
final IImageLine line = readRow();
if (line instanceof ImageLineInt)
return (ImageLineInt) line;
else
throw new PngjException("This is not a ImageLineInt : " + line.getClass());
}
}

View File

@ -1,453 +0,0 @@
package ar.com.hjg.pngj;
import java.io.File;
import java.io.OutputStream;
import java.util.List;
import ar.com.hjg.pngj.chunks.ChunkCopyBehaviour;
import ar.com.hjg.pngj.chunks.ChunkPredicate;
import ar.com.hjg.pngj.chunks.ChunksList;
import ar.com.hjg.pngj.chunks.ChunksListForWrite;
import ar.com.hjg.pngj.chunks.PngChunk;
import ar.com.hjg.pngj.chunks.PngChunkIEND;
import ar.com.hjg.pngj.chunks.PngChunkIHDR;
import ar.com.hjg.pngj.chunks.PngChunkPLTE;
import ar.com.hjg.pngj.chunks.PngMetadata;
import ar.com.hjg.pngj.pixels.PixelsWriter;
import ar.com.hjg.pngj.pixels.PixelsWriterDefault;
/**
* Writes a PNG image, line by line.
*/
public class PngWriter {
public final ImageInfo imgInfo;
/**
* last writen row number, starting from 0
*/
protected int rowNum = -1;
private final ChunksListForWrite chunksList;
private final PngMetadata metadata;
/**
* Current chunk grounp, (0-6) already written or currently writing (this is
* advanced when just starting to write the
* new group, not when finalizing the previous)
* <p>
* see {@link ChunksList}
*/
protected int currentChunkGroup = -1;
private final int passes = 1; // Some writes might require two passes (NOT USED STILL)
private int currentpass = 0; // numbered from 1
private boolean shouldCloseStream = true;
private int idatMaxSize = 0; // 0=use default (PngIDatChunkOutputStream 64k)
// private PngIDatChunkOutputStream datStream;
protected PixelsWriter pixelsWriter;
private final OutputStream os;
private ChunkPredicate copyFromPredicate = null;
private ChunksList copyFromList = null;
protected StringBuilder debuginfo = new StringBuilder();
/**
* Opens a file for writing.
* <p>
* Sets shouldCloseStream=true. For more info see
* {@link #PngWriter(OutputStream, ImageInfo)}
*
* @param file
* @param imgInfo
* @param allowoverwrite
* If false and file exists, an {@link PngjOutputException} is
* thrown
*/
public PngWriter(final File file, final ImageInfo imgInfo, final boolean allowoverwrite) {
this(PngHelperInternal.ostreamFromFile(file, allowoverwrite), imgInfo);
setShouldCloseStream(true);
}
/**
* @see #PngWriter(File, ImageInfo, boolean) (overwrite=true)
*/
public PngWriter(final File file, final ImageInfo imgInfo) {
this(file, imgInfo, true);
}
/**
* Constructs a new PngWriter from a output stream. After construction
* nothing is writen yet. You still can set some
* parameters (compression, filters) and queue chunks before start writing
* the pixels.
* <p>
*
* @param outputStream
* Open stream for binary writing
* @param imgInfo
* Basic image parameters
*/
public PngWriter(final OutputStream outputStream, final ImageInfo imgInfo) {
os = outputStream;
this.imgInfo = imgInfo;
// prealloc
chunksList = new ChunksListForWrite(imgInfo);
metadata = new PngMetadata(chunksList);
pixelsWriter = createPixelsWriter(imgInfo);
setCompLevel(9);
}
private void initIdat() { // this triggers the writing of first chunks
pixelsWriter.setOs(os);
pixelsWriter.setIdatMaxSize(idatMaxSize);
writeSignatureAndIHDR();
writeFirstChunks();
}
private void writeEndChunk() {
currentChunkGroup = ChunksList.CHUNK_GROUP_6_END;
final PngChunkIEND c = new PngChunkIEND(imgInfo);
c.createRawChunk().writeChunk(os);
chunksList.getChunks().add(c);
}
private void writeFirstChunks() {
if (currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT)
return;
int nw = 0;
currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR;
queueChunksFromOther();
nw = chunksList.writeChunks(os, currentChunkGroup);
currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE;
nw = chunksList.writeChunks(os, currentChunkGroup);
if (nw > 0 && imgInfo.greyscale)
throw new PngjOutputException("cannot write palette for this format");
if (nw == 0 && imgInfo.indexed)
throw new PngjOutputException("missing palette");
currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE;
nw = chunksList.writeChunks(os, currentChunkGroup);
}
private void writeLastChunks() { // not including end
currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT;
queueChunksFromOther();
chunksList.writeChunks(os, currentChunkGroup);
// should not be unwriten chunks
final List<PngChunk> pending = chunksList.getQueuedChunks();
if (!pending.isEmpty())
throw new PngjOutputException(pending.size() + " chunks were not written! Eg: " + pending.get(0).toString());
}
/**
* Write id signature and also "IHDR" chunk
*/
private void writeSignatureAndIHDR() {
PngHelperInternal.writeBytes(os, PngHelperInternal.getPngIdSignature()); // signature
currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR;
final PngChunkIHDR ihdr = new PngChunkIHDR(imgInfo);
// http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
ihdr.createRawChunk().writeChunk(os);
chunksList.getChunks().add(ihdr);
}
private void queueChunksFromOther() {
if (copyFromList == null || copyFromPredicate == null)
return;
final boolean idatDone = currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT; // we assume this method is not either before
// or after the IDAT writing, not in the
// middle!
for (final PngChunk chunk : copyFromList.getChunks()) {
if (chunk.getRaw().data == null)
continue; // we cannot copy skipped chunks?
final int groupOri = chunk.getChunkGroup();
if (groupOri <= ChunksList.CHUNK_GROUP_4_IDAT && idatDone)
continue;
if (groupOri >= ChunksList.CHUNK_GROUP_4_IDAT && !idatDone)
continue;
if (chunk.crit && !chunk.id.equals(PngChunkPLTE.ID))
continue; // critical chunks (except perhaps PLTE) are never
// copied
final boolean copy = copyFromPredicate.match(chunk);
if (copy)
// but if the chunk is already queued or writen, it's ommited!
if (chunksList.getEquivalent(chunk).isEmpty() && chunksList.getQueuedEquivalent(chunk).isEmpty())
chunksList.queue(chunk);
}
}
/**
* Queues an ancillary chunk for writing.
* <p>
* If a "equivalent" chunk is already queued (see
* {@link ChunkHelper#equivalent(PngChunk, PngChunk)), this overwrites it.
* <p>
* The chunk will be written as late as possible, unless the priority is
* set.
*
* @param chunk
*/
public void queueChunk(final PngChunk chunk) {
for (final PngChunk other : chunksList.getQueuedEquivalent(chunk))
getChunksList().removeChunk(other);
chunksList.queue(chunk);
}
/**
* Sets an origin (typically from a {@link PngReader}) of Chunks to be
* copied. This should be called only once, before
* starting writing the rows. It doesn't matter the current state of the
* PngReader reading, this is a live object and
* what matters is that when the writer writes the pixels (IDAT) the reader
* has already read them, and that when the
* writer ends, the reader is already ended (all this is very natural).
* <p>
* Apart from the copyMask, there is some addional heuristics:
* <p>
* - The chunks will be queued, but will be written as late as possible
* (unless you explicitly set priority=true)
* <p>
* - The chunk will not be queued if an "equivalent" chunk was already
* queued explicitly. And it will be overwriten
* another is queued explicitly.
*
* @param chunks
* @param copyMask
* Some bitmask from {@link ChunkCopyBehaviour}
*
* @see #copyChunksFrom(ChunksList, ChunkPredicate)
*/
public void copyChunksFrom(final ChunksList chunks, final int copyMask) {
copyChunksFrom(chunks, ChunkCopyBehaviour.createPredicate(copyMask, imgInfo));
}
/**
* Copy all chunks from origin. See {@link #copyChunksFrom(ChunksList, int)}
* for more info
*/
public void copyChunksFrom(final ChunksList chunks) {
copyChunksFrom(chunks, ChunkCopyBehaviour.COPY_ALL);
}
/**
* Copy chunks from origin depending on some {@link ChunkPredicate}
*
* @param chunks
* @param predicate
* The chunks (ancillary or PLTE) will be copied if and only if
* predicate matches
*
* @see #copyChunksFrom(ChunksList, int) for more info
*/
public void copyChunksFrom(final ChunksList chunks, final ChunkPredicate predicate) {
if (copyFromList != null && chunks != null)
PngHelperInternal.LOGGER.warning("copyChunksFrom should only be called once");
if (predicate == null)
throw new PngjOutputException("copyChunksFrom requires a predicate");
copyFromList = chunks;
copyFromPredicate = predicate;
}
/**
* Computes compressed size/raw size, approximate.
* <p>
* Actually: compressed size = total size of IDAT data , raw size =
* uncompressed pixel bytes = rows * (bytesPerRow +
* 1).
*
* This must be called after pngw.end()
*/
public double computeCompressionRatio() {
if (currentChunkGroup < ChunksList.CHUNK_GROUP_5_AFTERIDAT)
throw new PngjOutputException("must be called after end()");
return pixelsWriter.getCompression();
}
/**
* Finalizes all the steps and closes the stream. This must be called after
* writing the lines. Idempotent
*/
public void end() {
if (rowNum != imgInfo.rows - 1 || !pixelsWriter.isDone())
throw new PngjOutputException("all rows have not been written");
try {
if (pixelsWriter != null)
pixelsWriter.close();
if (currentChunkGroup < ChunksList.CHUNK_GROUP_5_AFTERIDAT)
writeLastChunks();
if (currentChunkGroup < ChunksList.CHUNK_GROUP_6_END)
writeEndChunk();
} finally {
close();
}
}
/**
* Closes and releases resources
* <p>
* This is normally called internally from {@link #end()}, you should only
* call this for aborting the writing and
* release resources (close the stream).
* <p>
* Idempotent and secure - never throws exceptions
*/
public void close() {
if (pixelsWriter != null)
pixelsWriter.close();
if (shouldCloseStream && os != null)
try {
os.close();
} catch (final Exception e) {
PngHelperInternal.LOGGER.warning("Error closing writer " + e.toString());
}
}
/**
* returns the chunks list (queued and writen chunks)
*/
public ChunksListForWrite getChunksList() {
return chunksList;
}
/**
* Retruns a high level wrapper over for metadata handling
*/
public PngMetadata getMetadata() {
return metadata;
}
/**
* Sets internal prediction filter type, or strategy to choose it.
* <p>
* This must be called just after constructor, before starting writing.
* <p>
*/
public void setFilterType(final FilterType filterType) {
pixelsWriter.setFilterType(filterType);
}
/**
* This is kept for backwards compatibility, now the PixelsWriter object
* should be used for setting
* compression/filtering options
*
* @see PixelsWriter#setCompressionFactor(double)
* @param compLevel
* between 0 (no compression, max speed) and 9 (max compression)
*/
public void setCompLevel(final int complevel) {
pixelsWriter.setDeflaterCompLevel(complevel);
}
/**
*
*/
public void setFilterPreserve(final boolean filterPreserve) {
if (filterPreserve)
pixelsWriter.setFilterType(FilterType.FILTER_PRESERVE);
else if (pixelsWriter.getFilterType() == null)
pixelsWriter.setFilterType(FilterType.FILTER_DEFAULT);
}
/**
* Sets maximum size of IDAT fragments. Incrementing this from the default
* has very little effect on compression and
* increments memory usage. You should rarely change this.
* <p>
*
* @param idatMaxSize
* default=0 : use defaultSize (32K)
*/
public void setIdatMaxSize(final int idatMaxSize) {
this.idatMaxSize = idatMaxSize;
}
/**
* If true, output stream will be closed after ending write
* <p>
* default=true
*/
public void setShouldCloseStream(final boolean shouldCloseStream) {
this.shouldCloseStream = shouldCloseStream;
}
/**
* Writes next row, does not check row number.
*
* @param imgline
*/
public void writeRow(final IImageLine imgline) {
writeRow(imgline, rowNum + 1);
}
/**
* Writes the full set of row. The ImageLineSet should contain (allow to
* acces) imgInfo.rows
*/
public void writeRows(final IImageLineSet<? extends IImageLine> imglines) {
for (int i = 0; i < imgInfo.rows; i++)
writeRow(imglines.getImageLineRawNum(i));
}
public void writeRow(final IImageLine imgline, int rownumber) {
rowNum++;
if (rowNum == imgInfo.rows)
rowNum = 0;
if (rownumber == imgInfo.rows)
rownumber = 0;
if (rownumber >= 0 && rowNum != rownumber)
throw new PngjOutputException("rows must be written in order: expected:" + rowNum + " passed:" + rownumber);
if (rowNum == 0)
currentpass++;
if (rownumber == 0 && currentpass == passes) {
initIdat();
currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT; // we just begin writing IDAT
}
final byte[] rowb = pixelsWriter.getRowb();
imgline.writeToPngRaw(rowb);
pixelsWriter.processRow(rowb);
}
/**
* Utility method, uses internaly a ImageLineInt
*/
public void writeRowInt(final int[] buf) {
writeRow(new ImageLineInt(imgInfo, buf));
}
/**
* Factory method for pixels writer. This will be called once at the moment
* at start writing a set of IDAT chunks
* (typically once in a normal PNG)
*
* This should be overriden if custom filtering strategies are desired.
* Remember to release this with close()
*
* @param imginfo
* Might be different than that of this object (eg: APNG with
* subimages)
* @param os
* Output stream
* @return new PixelsWriter. Don't forget to call close() when discarding it
*/
protected PixelsWriter createPixelsWriter(final ImageInfo imginfo) {
final PixelsWriterDefault pw = new PixelsWriterDefault(imginfo);
return pw;
}
public final PixelsWriter getPixelsWriter() {
return pixelsWriter;
}
public String getDebuginfo() {
return debuginfo.toString();
}
}

View File

@ -1,35 +0,0 @@
package ar.com.hjg.pngj;
import java.io.File;
import java.io.OutputStream;
import ar.com.hjg.pngj.pixels.PixelsWriter;
import ar.com.hjg.pngj.pixels.PixelsWriterMultiple;
/** Pngwriter with High compression EXPERIMENTAL */
public class PngWriterHc extends PngWriter {
public PngWriterHc(final File file, final ImageInfo imgInfo, final boolean allowoverwrite) {
super(file, imgInfo, allowoverwrite);
setFilterType(FilterType.FILTER_SUPER_ADAPTIVE);
}
public PngWriterHc(final File file, final ImageInfo imgInfo) {
super(file, imgInfo);
}
public PngWriterHc(final OutputStream outputStream, final ImageInfo imgInfo) {
super(outputStream, imgInfo);
}
@Override
protected PixelsWriter createPixelsWriter(final ImageInfo imginfo) {
final PixelsWriterMultiple pw = new PixelsWriterMultiple(imginfo);
return pw;
}
public PixelsWriterMultiple getPixelWriterMultiple() {
return (PixelsWriterMultiple) pixelsWriter;
}
}

View File

@ -1,20 +0,0 @@
package ar.com.hjg.pngj;
/**
* Exception thrown by bad CRC check
*/
public class PngjBadCrcException extends PngjInputException {
private static final long serialVersionUID = 1L;
public PngjBadCrcException(final String message, final Throwable cause) {
super(message, cause);
}
public PngjBadCrcException(final String message) {
super(message);
}
public PngjBadCrcException(final Throwable cause) {
super(cause);
}
}

View File

@ -1,20 +0,0 @@
package ar.com.hjg.pngj;
/**
* Generic exception for this library. It's a RuntimeException (unchecked)
*/
public class PngjException extends RuntimeException {
private static final long serialVersionUID = 1L;
public PngjException(final String message, final Throwable cause) {
super(message, cause);
}
public PngjException(final String message) {
super(message);
}
public PngjException(final Throwable cause) {
super(cause);
}
}

View File

@ -1,24 +0,0 @@
package ar.com.hjg.pngj;
/**
* Exception for anomalous internal problems (sort of asserts) that point to
* some issue with the library
*
* @author Hernan J Gonzalez
*
*/
public class PngjExceptionInternal extends RuntimeException {
private static final long serialVersionUID = 1L;
public PngjExceptionInternal(final String message, final Throwable cause) {
super(message, cause);
}
public PngjExceptionInternal(final String message) {
super(message);
}
public PngjExceptionInternal(final Throwable cause) {
super(cause);
}
}

View File

@ -1,20 +0,0 @@
package ar.com.hjg.pngj;
/**
* Exception thrown when reading a PNG.
*/
public class PngjInputException extends PngjException {
private static final long serialVersionUID = 1L;
public PngjInputException(final String message, final Throwable cause) {
super(message, cause);
}
public PngjInputException(final String message) {
super(message);
}
public PngjInputException(final Throwable cause) {
super(cause);
}
}

View File

@ -1,20 +0,0 @@
package ar.com.hjg.pngj;
/**
* Exception thrown by writing process
*/
public class PngjOutputException extends PngjException {
private static final long serialVersionUID = 1L;
public PngjOutputException(final String message, final Throwable cause) {
super(message, cause);
}
public PngjOutputException(final String message) {
super(message);
}
public PngjOutputException(final Throwable cause) {
super(cause);
}
}

View File

@ -1,25 +0,0 @@
package ar.com.hjg.pngj;
/**
* Exception thrown because of some valid feature of PNG standard that this
* library does not support.
*/
public class PngjUnsupportedException extends RuntimeException {
private static final long serialVersionUID = 1L;
public PngjUnsupportedException() {
super();
}
public PngjUnsupportedException(final String message, final Throwable cause) {
super(message, cause);
}
public PngjUnsupportedException(final String message) {
super(message);
}
public PngjUnsupportedException(final Throwable cause) {
super(cause);
}
}

View File

@ -1,55 +0,0 @@
package ar.com.hjg.pngj;
/**
* Packs information of current row. Only used internally
*/
class RowInfo {
public final ImageInfo imgInfo;
public final Deinterlacer deinterlacer;
public final boolean imode; // Interlaced
int dY, dX, oY, oX; // current step and offset (in pixels)
int rowNseq; // row number (from 0) in sequential read order
int rowNreal; // row number in the real image
int rowNsubImg; // current row in the virtual subsampled image; this increments (by 1) from 0 to
// rows/dy 7 times
int rowsSubImg, colsSubImg; // size of current subimage , in pixels
int bytesRow;
int pass; // 1-7
byte[] buf; // non-deep copy
int buflen; // valid bytes in buffer (include filter byte)
public RowInfo(final ImageInfo imgInfo, final Deinterlacer deinterlacer) {
this.imgInfo = imgInfo;
this.deinterlacer = deinterlacer;
imode = deinterlacer != null;
}
void update(final int rowseq) {
rowNseq = rowseq;
if (imode) {
pass = deinterlacer.getPass();
dX = deinterlacer.dX;
dY = deinterlacer.dY;
oX = deinterlacer.oX;
oY = deinterlacer.oY;
rowNreal = deinterlacer.getCurrRowReal();
rowNsubImg = deinterlacer.getCurrRowSubimg();
rowsSubImg = deinterlacer.getRows();
colsSubImg = deinterlacer.getCols();
bytesRow = (imgInfo.bitspPixel * colsSubImg + 7) / 8;
} else {
pass = 1;
dX = dY = 1;
oX = oY = 0;
rowNreal = rowNsubImg = rowseq;
rowsSubImg = imgInfo.rows;
colsSubImg = imgInfo.cols;
bytesRow = imgInfo.bytesPerRow;
}
}
void updateBuf(final byte[] buf, final int buflen) {
this.buf = buf;
this.buflen = buflen;
}
}

View File

@ -1,97 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngReader;
import ar.com.hjg.pngj.PngWriter;
/**
* Chunk copy policy to apply when copyng from a {@link PngReader} to a
* {@link PngWriter}.
* <p>
* The constants are bit-masks, they can be OR-ed
* <p>
* Reference:
* <a href="http://www.w3.org/TR/PNG/#14">http://www.w3.org/TR/PNG/#14</a> <br>
*/
public class ChunkCopyBehaviour {
/** Don't copy anything */
public static final int COPY_NONE = 0;
/** copy the palette */
public static final int COPY_PALETTE = 1;
/** copy all 'safe to copy' chunks */
public static final int COPY_ALL_SAFE = 1 << 2;
/**
* copy all, including palette
*/
public static final int COPY_ALL = 1 << 3; // includes palette!
/**
* Copy PHYS chunk (physical resolution)
*/
public static final int COPY_PHYS = 1 << 4; // dpi
/**
* Copy al textual chunks.
*/
public static final int COPY_TEXTUAL = 1 << 5; // all textual types
/**
* Copy TRNS chunk
*/
public static final int COPY_TRANSPARENCY = 1 << 6; //
/**
* Copy unknown chunks (unknown by our factory)
*/
public static final int COPY_UNKNOWN = 1 << 7; // all unknown (by the factory!)
/**
* Copy almost all: excepts only HIST (histogram) TIME and TEXTUAL chunks
*/
public static final int COPY_ALMOSTALL = 1 << 8;
private static boolean maskMatch(final int v, final int mask) {
return (v & mask) != 0;
}
/**
* Creates a predicate equivalent to the copy mask
* <p>
* Given a copy mask (see static fields) and the ImageInfo of the target
* PNG, returns a predicate that tells if a
* chunk should be copied.
* <p>
* This is a handy helper method, you can also create and set your own
* predicate
*/
public static ChunkPredicate createPredicate(final int copyFromMask, final ImageInfo imgInfo) {
return chunk -> {
if (chunk.crit) {
if (chunk.id.equals(ChunkHelper.PLTE)) {
if (imgInfo.indexed && ChunkCopyBehaviour.maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_PALETTE))
return true;
if (!imgInfo.greyscale && ChunkCopyBehaviour.maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALL))
return true;
}
} else { // ancillary
final boolean text = chunk instanceof PngChunkTextVar;
final boolean safe = chunk.safe;
// notice that these if are not exclusive
if (ChunkCopyBehaviour.maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALL))
return true;
if (safe && ChunkCopyBehaviour.maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALL_SAFE))
return true;
if (chunk.id.equals(ChunkHelper.tRNS) && ChunkCopyBehaviour.maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_TRANSPARENCY))
return true;
if (chunk.id.equals(ChunkHelper.pHYs) && ChunkCopyBehaviour.maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_PHYS))
return true;
if (text && ChunkCopyBehaviour.maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_TEXTUAL))
return true;
if (ChunkCopyBehaviour.maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALMOSTALL) && !(ChunkHelper.isUnknown(chunk) || text || chunk.id.equals(ChunkHelper.hIST) || chunk.id.equals(ChunkHelper.tIME)))
return true;
if (ChunkCopyBehaviour.maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_UNKNOWN) && ChunkHelper.isUnknown(chunk))
return true;
}
return false;
};
}
}

View File

@ -1,112 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.IChunkFactory;
import ar.com.hjg.pngj.ImageInfo;
/**
* Default chunk factory.
* <p>
* The user that wants to parse custom chunks can extend
* {@link #createEmptyChunkExtended(String, ImageInfo)}
*/
public class ChunkFactory implements IChunkFactory {
boolean parse;
public ChunkFactory() {
this(true);
}
public ChunkFactory(final boolean parse) {
this.parse = parse;
}
@Override
public final PngChunk createChunk(final ChunkRaw chunkRaw, final ImageInfo imgInfo) {
PngChunk c = createEmptyChunkKnown(chunkRaw.id, imgInfo);
if (c == null)
c = createEmptyChunkExtended(chunkRaw.id, imgInfo);
if (c == null)
c = createEmptyChunkUnknown(chunkRaw.id, imgInfo);
c.setRaw(chunkRaw);
if (parse && chunkRaw.data != null)
c.parseFromRaw(chunkRaw);
return c;
}
protected final PngChunk createEmptyChunkKnown(final String id, final ImageInfo imgInfo) {
if (id.equals(ChunkHelper.IDAT))
return new PngChunkIDAT(imgInfo);
if (id.equals(ChunkHelper.IHDR))
return new PngChunkIHDR(imgInfo);
if (id.equals(ChunkHelper.PLTE))
return new PngChunkPLTE(imgInfo);
if (id.equals(ChunkHelper.IEND))
return new PngChunkIEND(imgInfo);
if (id.equals(ChunkHelper.tEXt))
return new PngChunkTEXT(imgInfo);
if (id.equals(ChunkHelper.iTXt))
return new PngChunkITXT(imgInfo);
if (id.equals(ChunkHelper.zTXt))
return new PngChunkZTXT(imgInfo);
if (id.equals(ChunkHelper.bKGD))
return new PngChunkBKGD(imgInfo);
if (id.equals(ChunkHelper.gAMA))
return new PngChunkGAMA(imgInfo);
if (id.equals(ChunkHelper.pHYs))
return new PngChunkPHYS(imgInfo);
if (id.equals(ChunkHelper.iCCP))
return new PngChunkICCP(imgInfo);
if (id.equals(ChunkHelper.tIME))
return new PngChunkTIME(imgInfo);
if (id.equals(ChunkHelper.tRNS))
return new PngChunkTRNS(imgInfo);
if (id.equals(ChunkHelper.cHRM))
return new PngChunkCHRM(imgInfo);
if (id.equals(ChunkHelper.sBIT))
return new PngChunkSBIT(imgInfo);
if (id.equals(ChunkHelper.sRGB))
return new PngChunkSRGB(imgInfo);
if (id.equals(ChunkHelper.hIST))
return new PngChunkHIST(imgInfo);
if (id.equals(ChunkHelper.sPLT))
return new PngChunkSPLT(imgInfo);
// apng
if (id.equals(PngChunkFDAT.ID))
return new PngChunkFDAT(imgInfo);
if (id.equals(PngChunkACTL.ID))
return new PngChunkACTL(imgInfo);
if (id.equals(PngChunkFCTL.ID))
return new PngChunkFCTL(imgInfo);
return null;
}
/**
* This is used as last resort factory method.
* <p>
* It creates a {@link PngChunkUNKNOWN} chunk.
*/
protected final PngChunk createEmptyChunkUnknown(final String id, final ImageInfo imgInfo) {
return new PngChunkUNKNOWN(id, imgInfo);
}
/**
* Factory for chunks that are not in the original PNG standard. This can be
* overriden (but dont forget to call this
* also)
*
* @param id
* Chunk id , 4 letters
* @param imgInfo
* Usually not needed
* @return null if chunk id not recognized
*/
protected PngChunk createEmptyChunkExtended(final String id, final ImageInfo imgInfo) {
if (id.equals(PngChunkOFFS.ID))
return new PngChunkOFFS(imgInfo);
if (id.equals(PngChunkSTER.ID))
return new PngChunkSTER(imgInfo);
return null; // extend!
}
}

View File

@ -1,291 +0,0 @@
package ar.com.hjg.pngj.chunks;
// see http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
// http://www.w3.org/TR/PNG/#5Chunk-naming-conventions
// http://www.w3.org/TR/PNG/#table53
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* Helper methods and constants related to Chunk processing.
* <p>
* This should only be of interest to developers doing special chunk processing
* or extending the ChunkFactory
*/
public class ChunkHelper {
ChunkHelper() {}
public static final String IHDR = "IHDR";
public static final String PLTE = "PLTE";
public static final String IDAT = "IDAT";
public static final String IEND = "IEND";
public static final String cHRM = "cHRM";
public static final String gAMA = "gAMA";
public static final String iCCP = "iCCP";
public static final String sBIT = "sBIT";
public static final String sRGB = "sRGB";
public static final String bKGD = "bKGD";
public static final String hIST = "hIST";
public static final String tRNS = "tRNS";
public static final String pHYs = "pHYs";
public static final String sPLT = "sPLT";
public static final String tIME = "tIME";
public static final String iTXt = "iTXt";
public static final String tEXt = "tEXt";
public static final String zTXt = "zTXt";
public static final byte[] b_IHDR = ChunkHelper.toBytes(ChunkHelper.IHDR);
public static final byte[] b_PLTE = ChunkHelper.toBytes(ChunkHelper.PLTE);
public static final byte[] b_IDAT = ChunkHelper.toBytes(ChunkHelper.IDAT);
public static final byte[] b_IEND = ChunkHelper.toBytes(ChunkHelper.IEND);
/*
* static auxiliary buffer. any method that uses this should synchronize against this
*/
private static byte[] tmpbuffer = new byte[4096];
/**
* Converts to bytes using Latin1 (ISO-8859-1)
*/
public static byte[] toBytes(final String x) {
try {
return x.getBytes(PngHelperInternal.charsetLatin1name);
} catch (final UnsupportedEncodingException e) {
throw new PngBadCharsetException(e);
}
}
/**
* Converts to String using Latin1 (ISO-8859-1)
*/
public static String toString(final byte[] x) {
try {
return new String(x, PngHelperInternal.charsetLatin1name);
} catch (final UnsupportedEncodingException e) {
throw new PngBadCharsetException(e);
}
}
/**
* Converts to String using Latin1 (ISO-8859-1)
*/
public static String toString(final byte[] x, final int offset, final int len) {
try {
return new String(x, offset, len, PngHelperInternal.charsetLatin1name);
} catch (final UnsupportedEncodingException e) {
throw new PngBadCharsetException(e);
}
}
/**
* Converts to bytes using UTF-8
*/
public static byte[] toBytesUTF8(final String x) {
try {
return x.getBytes(PngHelperInternal.charsetUTF8name);
} catch (final UnsupportedEncodingException e) {
throw new PngBadCharsetException(e);
}
}
/**
* Converts to string using UTF-8
*/
public static String toStringUTF8(final byte[] x) {
try {
return new String(x, PngHelperInternal.charsetUTF8name);
} catch (final UnsupportedEncodingException e) {
throw new PngBadCharsetException(e);
}
}
/**
* Converts to string using UTF-8
*/
public static String toStringUTF8(final byte[] x, final int offset, final int len) {
try {
return new String(x, offset, len, PngHelperInternal.charsetUTF8name);
} catch (final UnsupportedEncodingException e) {
throw new PngBadCharsetException(e);
}
}
/**
* critical chunk : first letter is uppercase
*/
public static boolean isCritical(final String id) {
return Character.isUpperCase(id.charAt(0));
}
/**
* public chunk: second letter is uppercase
*/
public static boolean isPublic(final String id) { //
return Character.isUpperCase(id.charAt(1));
}
/**
* Safe to copy chunk: fourth letter is lower case
*/
public static boolean isSafeToCopy(final String id) {
return !Character.isUpperCase(id.charAt(3));
}
/**
* "Unknown" just means that our chunk factory (even when it has been
* augmented by client code) did not recognize its
* id
*/
public static boolean isUnknown(final PngChunk c) {
return c instanceof PngChunkUNKNOWN;
}
/**
* Finds position of null byte in array
*
* @param b
* @return -1 if not found
*/
public static int posNullByte(final byte[] b) {
for (int i = 0; i < b.length; i++)
if (b[i] == 0)
return i;
return -1;
}
/**
* Decides if a chunk should be loaded, according to a ChunkLoadBehaviour
*
* @param id
* @param behav
* @return true/false
*/
public static boolean shouldLoad(final String id, final ChunkLoadBehaviour behav) {
if (ChunkHelper.isCritical(id))
return true;
switch (behav) {
case LOAD_CHUNK_ALWAYS:
return true;
case LOAD_CHUNK_IF_SAFE:
return ChunkHelper.isSafeToCopy(id);
case LOAD_CHUNK_NEVER:
return false;
case LOAD_CHUNK_MOST_IMPORTANT:
return id.equals(PngChunkTRNS.ID);
}
return false; // should not reach here
}
public final static byte[] compressBytes(final byte[] ori, final boolean compress) {
return ChunkHelper.compressBytes(ori, 0, ori.length, compress);
}
public static byte[] compressBytes(final byte[] ori, final int offset, final int len, final boolean compress) {
try {
final ByteArrayInputStream inb = new ByteArrayInputStream(ori, offset, len);
final InputStream in = compress ? inb : new InflaterInputStream(inb);
final ByteArrayOutputStream outb = new ByteArrayOutputStream();
final OutputStream out = compress ? new DeflaterOutputStream(outb) : outb;
ChunkHelper.shovelInToOut(in, out);
in.close();
out.close();
return outb.toByteArray();
} catch (final Exception e) {
throw new PngjException(e);
}
}
/**
* Shovels all data from an input stream to an output stream.
*/
private static void shovelInToOut(final InputStream in, final OutputStream out) throws IOException {
synchronized (ChunkHelper.tmpbuffer) {
int len;
while ((len = in.read(ChunkHelper.tmpbuffer)) > 0)
out.write(ChunkHelper.tmpbuffer, 0, len);
}
}
/**
* Returns only the chunks that "match" the predicate
*
* See also trimList()
*/
public static List<PngChunk> filterList(final List<PngChunk> target, final ChunkPredicate predicateKeep) {
final List<PngChunk> result = new ArrayList<>();
for (final PngChunk element : target)
if (predicateKeep.match(element))
result.add(element);
return result;
}
/**
* Remove (in place) the chunks that "match" the predicate
*
* See also filterList
*/
public static int trimList(final List<PngChunk> target, final ChunkPredicate predicateRemove) {
final Iterator<PngChunk> it = target.iterator();
int cont = 0;
while (it.hasNext()) {
final PngChunk c = it.next();
if (predicateRemove.match(c)) {
it.remove();
cont++;
}
}
return cont;
}
/**
* Adhoc criteria: two ancillary chunks are "equivalent" ("practically same
* type") if they have same id and (perhaps,
* if multiple are allowed) if the match also in some "internal key" (eg:
* key for string values, palette for sPLT,
* etc)
*
* When we use this method, we implicitly assume that we don't allow/expect
* two "equivalent" chunks in a single PNG
*
* Notice that the use of this is optional, and that the PNG standard
* actually allows text chunks that have same key
*
* @return true if "equivalent"
*/
public static final boolean equivalent(final PngChunk c1, final PngChunk c2) {
if (c1 == c2)
return true;
if (c1 == null || c2 == null || !c1.id.equals(c2.id))
return false;
if (c1.crit)
return false;
// same id
if (c1.getClass() != c2.getClass())
return false; // should not happen
if (!c2.allowsMultiple())
return true;
if (c1 instanceof PngChunkTextVar)
return ((PngChunkTextVar) c1).getKey().equals(((PngChunkTextVar) c2).getKey());
if (c1 instanceof PngChunkSPLT)
return ((PngChunkSPLT) c1).getPalName().equals(((PngChunkSPLT) c2).getPalName());
// unknown chunks that allow multiple? consider they don't match
return false;
}
public static boolean isText(final PngChunk c) {
return c instanceof PngChunkTextVar;
}
}

View File

@ -1,27 +0,0 @@
package ar.com.hjg.pngj.chunks;
/**
* What to do with ancillary (non-critical) chunks when reading.
* <p>
*
*/
public enum ChunkLoadBehaviour {
/**
* All non-critical chunks are skipped
*/
LOAD_CHUNK_NEVER,
/**
* Load chunk if "safe to copy"
*/
LOAD_CHUNK_IF_SAFE,
/**
* Load only most important chunk: TRNS
*/
LOAD_CHUNK_MOST_IMPORTANT,
/**
* Load all chunks. <br>
* Notice that other restrictions might apply, see
* PngReader.skipChunkMaxSize PngReader.skipChunkIds
*/
LOAD_CHUNK_ALWAYS;
}

View File

@ -1,14 +0,0 @@
package ar.com.hjg.pngj.chunks;
/**
* Decides if another chunk "matches", according to some criterion
*/
public interface ChunkPredicate {
/**
* The other chunk matches with this one
*
* @param chunk
* @return true if match
*/
boolean match(PngChunk chunk);
}

View File

@ -1,175 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.io.ByteArrayInputStream;
import java.io.OutputStream;
import java.util.zip.CRC32;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjBadCrcException;
import ar.com.hjg.pngj.PngjException;
import ar.com.hjg.pngj.PngjOutputException;
/**
* Raw (physical) chunk.
* <p>
* Short lived object, to be created while serialing/deserializing Do not reuse
* it for different chunks. <br>
* See http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html
*/
public class ChunkRaw {
/**
* The length counts only the data field, not itself, the chunk type code,
* or the CRC. Zero is a valid length.
* Although encoders and decoders should treat the length as unsigned, its
* value must not exceed 231-1 bytes.
*/
public final int len;
/**
* A 4-byte chunk type code. uppercase and lowercase ASCII letters
*/
public final byte[] idbytes;
public final String id;
/**
* The data bytes appropriate to the chunk type, if any. This field can be
* of zero length. Does not include crc. If
* it's null, it means that the data is ot available
*/
public byte[] data = null;
/**
* @see ChunkRaw#getOffset()
*/
private long offset = 0;
/**
* A 4-byte CRC (Cyclic Redundancy Check) calculated on the preceding bytes
* in the chunk, including the chunk type
* code and chunk data fields, but not including the length field.
*/
public byte[] crcval = new byte[4];
private CRC32 crcengine; // lazily instantiated
public ChunkRaw(final int len, final String id, final boolean alloc) {
this.len = len;
this.id = id;
idbytes = ChunkHelper.toBytes(id);
for (int i = 0; i < 4; i++)
if (idbytes[i] < 65 || idbytes[i] > 122 || idbytes[i] > 90 && idbytes[i] < 97)
throw new PngjException("Bad id chunk: must be ascii letters " + id);
if (alloc)
allocData();
}
public ChunkRaw(final int len, final byte[] idbytes, final boolean alloc) {
this(len, ChunkHelper.toString(idbytes), alloc);
}
public void allocData() { // TODO: not public
if (data == null || data.length < len)
data = new byte[len];
}
/**
* this is called after setting data, before writing to os
*/
private void computeCrcForWriting() {
crcengine = new CRC32();
crcengine.update(idbytes, 0, 4);
if (len > 0)
crcengine.update(data, 0, len); //
PngHelperInternal.writeInt4tobytes((int) crcengine.getValue(), crcval, 0);
}
/**
* Computes the CRC and writes to the stream. If error, a
* PngjOutputException is thrown
*
* Note that this is only used for non idat chunks
*/
public void writeChunk(final OutputStream os) {
writeChunkHeader(os);
if (len > 0) {
if (data == null)
throw new PngjOutputException("cannot write chunk, raw chunk data is null [" + id + "]");
PngHelperInternal.writeBytes(os, data, 0, len);
}
computeCrcForWriting();
writeChunkCrc(os);
}
public void writeChunkHeader(final OutputStream os) {
if (idbytes.length != 4)
throw new PngjOutputException("bad chunkid [" + id + "]");
PngHelperInternal.writeInt4(os, len);
PngHelperInternal.writeBytes(os, idbytes);
}
public void writeChunkCrc(final OutputStream os) {
PngHelperInternal.writeBytes(os, crcval, 0, 4);
}
public void checkCrc() {
final int crcComputed = (int) crcengine.getValue();
final int crcExpected = PngHelperInternal.readInt4fromBytes(crcval, 0);
if (crcComputed != crcExpected)
throw new PngjBadCrcException("chunk: " + toString() + " expected=" + crcExpected + " read=" + crcComputed);
}
public void updateCrc(final byte[] buf, final int off, final int len) {
if (crcengine == null)
crcengine = new CRC32();
crcengine.update(buf, off, len);
}
ByteArrayInputStream getAsByteStream() { // only the data
return new ByteArrayInputStream(data);
}
/**
* offset in the full PNG stream, in bytes. only informational, for read
* chunks (0=NA)
*/
public long getOffset() {
return offset;
}
public void setOffset(final long offset) {
this.offset = offset;
}
@Override
public String toString() {
return "chunkid=" + ChunkHelper.toString(idbytes) + " len=" + len;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (id == null ? 0 : id.hashCode());
result = prime * result + (int) (offset ^ offset >>> 32);
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final ChunkRaw other = (ChunkRaw) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (offset != other.offset)
return false;
return true;
}
}

View File

@ -1,160 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.util.ArrayList;
import java.util.List;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngjException;
/**
* All chunks that form an image, read or to be written.
* <p>
* chunks include all chunks, but IDAT is a single pseudo chunk without data
**/
public class ChunksList {
// ref: http://www.w3.org/TR/PNG/#table53
public static final int CHUNK_GROUP_0_IDHR = 0; // required - single
public static final int CHUNK_GROUP_1_AFTERIDHR = 1; // optional - multiple
public static final int CHUNK_GROUP_2_PLTE = 2; // optional - single
public static final int CHUNK_GROUP_3_AFTERPLTE = 3; // optional - multple
public static final int CHUNK_GROUP_4_IDAT = 4; // required (single pseudo chunk)
public static final int CHUNK_GROUP_5_AFTERIDAT = 5; // optional - multple
public static final int CHUNK_GROUP_6_END = 6; // only 1 chunk - requried
/**
* All chunks, read (or written)
*
* But IDAT is a single pseudo chunk without data
*/
List<PngChunk> chunks = new ArrayList<>();
// protected HashMap<String, List<PngChunk>> chunksById = new HashMap<String, List<PngChunk>>();
// // does not include IDAT
final ImageInfo imageInfo; // only required for writing
boolean withPlte = false;
public ChunksList(final ImageInfo imfinfo) {
imageInfo = imfinfo;
}
/**
* WARNING: this does NOT return a copy, but the list itself. The called
* should not modify this directly! Don't use
* this to manipulate the chunks.
*/
public List<PngChunk> getChunks() {
return chunks;
}
protected static List<PngChunk> getXById(final List<PngChunk> list, final String id, final String innerid) {
if (innerid == null)
return ChunkHelper.filterList(list, c -> c.id.equals(id));
else
return ChunkHelper.filterList(list, c -> {
if (!c.id.equals(id))
return false;
if (c instanceof PngChunkTextVar && !((PngChunkTextVar) c).getKey().equals(innerid))
return false;
if (c instanceof PngChunkSPLT && !((PngChunkSPLT) c).getPalName().equals(innerid))
return false;
return true;
});
}
/**
* Adds chunk in next position. This is used onyl by the pngReader
*/
public void appendReadChunk(final PngChunk chunk, final int chunkGroup) {
chunk.setChunkGroup(chunkGroup);
chunks.add(chunk);
if (chunk.id.equals(PngChunkPLTE.ID))
withPlte = true;
}
/**
* All chunks with this ID
*
* @param id
* @return List, empty if none
*/
public List<? extends PngChunk> getById(final String id) {
return getById(id, null);
}
/**
* If innerid!=null and the chunk is PngChunkTextVar or PngChunkSPLT, it's
* filtered by that id
*
* @param id
* @return innerid Only used for text and SPLT chunks
* @return List, empty if none
*/
public List<? extends PngChunk> getById(final String id, final String innerid) {
return ChunksList.getXById(chunks, id, innerid);
}
/**
* Returns only one chunk
*
* @param id
* @return First chunk found, null if not found
*/
public PngChunk getById1(final String id) {
return getById1(id, false);
}
/**
* Returns only one chunk or null if nothing found - does not include queued
* <p>
* If more than one chunk is found, then an exception is thrown
* (failifMultiple=true or chunk is single) or the last
* one is returned (failifMultiple=false)
**/
public PngChunk getById1(final String id, final boolean failIfMultiple) {
return getById1(id, null, failIfMultiple);
}
/**
* Returns only one chunk or null if nothing found - does not include queued
* <p>
* If more than one chunk (after filtering by inner id) is found, then an
* exception is thrown (failifMultiple=true or
* chunk is single) or the last one is returned (failifMultiple=false)
**/
public PngChunk getById1(final String id, final String innerid, final boolean failIfMultiple) {
final List<? extends PngChunk> list = getById(id, innerid);
if (list.isEmpty())
return null;
if (list.size() > 1 && (failIfMultiple || !list.get(0).allowsMultiple()))
throw new PngjException("unexpected multiple chunks id=" + id);
return list.get(list.size() - 1);
}
/**
* Finds all chunks "equivalent" to this one
*
* @param c2
* @return Empty if nothing found
*/
public List<PngChunk> getEquivalent(final PngChunk c2) {
return ChunkHelper.filterList(chunks, c -> ChunkHelper.equivalent(c, c2));
}
@Override
public String toString() {
return "ChunkList: read: " + chunks.size();
}
/**
* for debugging
*/
public String toStringFull() {
final StringBuilder sb = new StringBuilder(toString());
sb.append("\n Read:\n");
for (final PngChunk chunk : chunks)
sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n");
return sb.toString();
}
}

View File

@ -1,185 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngjException;
import ar.com.hjg.pngj.PngjOutputException;
public class ChunksListForWrite extends ChunksList {
/**
* chunks not yet writen - does not include IHDR, IDAT, END, perhaps yes
* PLTE
*/
private final List<PngChunk> queuedChunks = new ArrayList<>();
// redundant, just for eficciency
private final HashMap<String, Integer> alreadyWrittenKeys = new HashMap<>();
public ChunksListForWrite(final ImageInfo imfinfo) {
super(imfinfo);
}
/**
* Same as getById(), but looking in the queued chunks
*/
public List<? extends PngChunk> getQueuedById(final String id) {
return getQueuedById(id, null);
}
/**
* Same as getById(), but looking in the queued chunks
*/
public List<? extends PngChunk> getQueuedById(final String id, final String innerid) {
return ChunksList.getXById(queuedChunks, id, innerid);
}
/**
* Same as getById1(), but looking in the queued chunks
**/
public PngChunk getQueuedById1(final String id, final String innerid, final boolean failIfMultiple) {
final List<? extends PngChunk> list = getQueuedById(id, innerid);
if (list.isEmpty())
return null;
if (list.size() > 1 && (failIfMultiple || !list.get(0).allowsMultiple()))
throw new PngjException("unexpected multiple chunks id=" + id);
return list.get(list.size() - 1);
}
/**
* Same as getById1(), but looking in the queued chunks
**/
public PngChunk getQueuedById1(final String id, final boolean failIfMultiple) {
return getQueuedById1(id, null, failIfMultiple);
}
/**
* Same as getById1(), but looking in the queued chunks
**/
public PngChunk getQueuedById1(final String id) {
return getQueuedById1(id, false);
}
/**
* Finds all chunks "equivalent" to this one
*
* @param c2
* @return Empty if nothing found
*/
public List<PngChunk> getQueuedEquivalent(final PngChunk c2) {
return ChunkHelper.filterList(queuedChunks, c -> ChunkHelper.equivalent(c, c2));
}
/**
* Remove Chunk: only from queued
*
* WARNING: this depends on c.equals() implementation, which is
* straightforward for SingleChunks. For MultipleChunks,
* it will normally check for reference equality!
*/
public boolean removeChunk(final PngChunk c) {
if (c == null)
return false;
return queuedChunks.remove(c);
}
/**
* Adds chunk to queue
*
* If there
*
* @param c
*/
public boolean queue(final PngChunk c) {
queuedChunks.add(c);
return true;
}
/**
* this should be called only for ancillary chunks and PLTE (groups 1 - 3 -
* 5)
**/
private static boolean shouldWrite(final PngChunk c, final int currentGroup) {
if (currentGroup == ChunksList.CHUNK_GROUP_2_PLTE)
return c.id.equals(ChunkHelper.PLTE);
if (currentGroup % 2 == 0)
throw new PngjOutputException("bad chunk group?");
int minChunkGroup, maxChunkGroup;
if (c.getOrderingConstraint().mustGoBeforePLTE())
minChunkGroup = maxChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR;
else if (c.getOrderingConstraint().mustGoBeforeIDAT()) {
maxChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE;
minChunkGroup = c.getOrderingConstraint().mustGoAfterPLTE() ? ChunksList.CHUNK_GROUP_3_AFTERPLTE : ChunksList.CHUNK_GROUP_1_AFTERIDHR;
} else {
maxChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT;
minChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR;
}
int preferred = maxChunkGroup;
if (c.hasPriority())
preferred = minChunkGroup;
if (ChunkHelper.isUnknown(c) && c.getChunkGroup() > 0)
preferred = c.getChunkGroup();
if (currentGroup == preferred)
return true;
if (currentGroup > preferred && currentGroup <= maxChunkGroup)
return true;
return false;
}
public int writeChunks(final OutputStream os, final int currentGroup) {
int cont = 0;
final Iterator<PngChunk> it = queuedChunks.iterator();
while (it.hasNext()) {
final PngChunk c = it.next();
if (!ChunksListForWrite.shouldWrite(c, currentGroup))
continue;
if (ChunkHelper.isCritical(c.id) && !c.id.equals(ChunkHelper.PLTE))
throw new PngjOutputException("bad chunk queued: " + c);
if (alreadyWrittenKeys.containsKey(c.id) && !c.allowsMultiple())
throw new PngjOutputException("duplicated chunk does not allow multiple: " + c);
c.write(os);
chunks.add(c);
alreadyWrittenKeys.put(c.id, alreadyWrittenKeys.containsKey(c.id) ? alreadyWrittenKeys.get(c.id) + 1 : 1);
c.setChunkGroup(currentGroup);
it.remove();
cont++;
}
return cont;
}
/**
* warning: this is NOT a copy, do not modify
*/
public List<PngChunk> getQueuedChunks() {
return queuedChunks;
}
@Override
public String toString() {
return "ChunkList: written: " + getChunks().size() + " queue: " + queuedChunks.size();
}
/**
* for debugging
*/
@Override
public String toStringFull() {
final StringBuilder sb = new StringBuilder(toString());
sb.append("\n Written:\n");
for (final PngChunk chunk : getChunks())
sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n");
if (!queuedChunks.isEmpty()) {
sb.append(" Queued:\n");
for (final PngChunk chunk : queuedChunks)
sb.append(chunk).append("\n");
}
return sb.toString();
}
}

View File

@ -1,20 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.PngjException;
public class PngBadCharsetException extends PngjException {
private static final long serialVersionUID = 1L;
public PngBadCharsetException(final String message, final Throwable cause) {
super(message, cause);
}
public PngBadCharsetException(final String message) {
super(message);
}
public PngBadCharsetException(final Throwable cause) {
super(cause);
}
}

View File

@ -1,227 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.io.OutputStream;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngjExceptionInternal;
/**
* Represents a instance of a PNG chunk.
* <p>
* See
* <a href="http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html">http://www
* .libpng.org/pub/png/spec/1.2/PNG-Chunks .html</a> </a>
* <p>
* Concrete classes should extend {@link PngChunkSingle} or
* {@link PngChunkMultiple}
* <p>
* Note that some methods/fields are type-specific (getOrderingConstraint(),
* allowsMultiple()),<br>
* some are 'almost' type-specific (id,crit,pub,safe; the exception is
* PngUKNOWN), <br>
* and the rest are instance-specific
*/
public abstract class PngChunk {
/**
* Chunk-id: 4 letters
*/
public final String id;
/**
* Autocomputed at creation time
*/
public final boolean crit, pub, safe;
protected final ImageInfo imgInfo;
protected ChunkRaw raw;
private boolean priority = false; // For writing. Queued chunks with high priority will be written
// as soon as
// possible
protected int chunkGroup = -1; // chunk group where it was read or writen
/**
* Possible ordering constraint for a PngChunk type -only relevant for
* ancillary chunks. Theoretically, there could be
* more general constraints, but these cover the constraints for standard
* chunks.
*/
public enum ChunkOrderingConstraint {
/**
* no ordering constraint
*/
NONE,
/**
* Must go before PLTE (and hence, also before IDAT)
*/
BEFORE_PLTE_AND_IDAT,
/**
* Must go after PLTE (if exists) but before IDAT
*/
AFTER_PLTE_BEFORE_IDAT,
/**
* Must go after PLTE (and it must exist) but before IDAT
*/
AFTER_PLTE_BEFORE_IDAT_PLTE_REQUIRED,
/**
* Must before IDAT (before or after PLTE)
*/
BEFORE_IDAT,
/**
* After IDAT (this restriction does not apply to the standard PNG
* chunks)
*/
AFTER_IDAT,
/**
* Does not apply
*/
NA;
public boolean mustGoBeforePLTE() {
return this == BEFORE_PLTE_AND_IDAT;
}
public boolean mustGoBeforeIDAT() {
return this == BEFORE_IDAT || this == BEFORE_PLTE_AND_IDAT || this == AFTER_PLTE_BEFORE_IDAT;
}
/**
* after pallete, if exists
*/
public boolean mustGoAfterPLTE() {
return this == AFTER_PLTE_BEFORE_IDAT || this == AFTER_PLTE_BEFORE_IDAT_PLTE_REQUIRED;
}
public boolean mustGoAfterIDAT() {
return this == AFTER_IDAT;
}
public boolean isOk(final int currentChunkGroup, final boolean hasplte) {
if (this == NONE)
return true;
else if (this == BEFORE_IDAT)
return currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT;
else if (this == BEFORE_PLTE_AND_IDAT)
return currentChunkGroup < ChunksList.CHUNK_GROUP_2_PLTE;
else if (this == AFTER_PLTE_BEFORE_IDAT)
return hasplte ? currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT : currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT && currentChunkGroup > ChunksList.CHUNK_GROUP_2_PLTE;
else if (this == AFTER_IDAT)
return currentChunkGroup > ChunksList.CHUNK_GROUP_4_IDAT;
return false;
}
}
public PngChunk(final String id, final ImageInfo imgInfo) {
this.id = id;
this.imgInfo = imgInfo;
crit = ChunkHelper.isCritical(id);
pub = ChunkHelper.isPublic(id);
safe = ChunkHelper.isSafeToCopy(id);
}
protected final ChunkRaw createEmptyChunk(final int len, final boolean alloc) {
final ChunkRaw c = new ChunkRaw(len, ChunkHelper.toBytes(id), alloc);
return c;
}
/**
* In which "chunkGroup" (see {@link ChunksList}for definition) this chunks
* instance was read or written.
* <p>
* -1 if not read or written (eg, queued)
*/
final public int getChunkGroup() {
return chunkGroup;
}
/**
* @see #getChunkGroup()
*/
final void setChunkGroup(final int chunkGroup) {
this.chunkGroup = chunkGroup;
}
public boolean hasPriority() {
return priority;
}
public void setPriority(final boolean priority) {
this.priority = priority;
}
final void write(final OutputStream os) {
if (raw == null || raw.data == null)
raw = createRawChunk();
if (raw == null)
throw new PngjExceptionInternal("null chunk ! creation failed for " + this);
raw.writeChunk(os);
}
/**
* Creates the physical chunk. This is used when writing (serialization).
* Each particular chunk class implements its
* own logic.
*
* @return A newly allocated and filled raw chunk
*/
public abstract ChunkRaw createRawChunk();
/**
* Parses raw chunk and fill inside data. This is used when reading
* (deserialization). Each particular chunk class
* implements its own logic.
*/
protected abstract void parseFromRaw(ChunkRaw c);
/**
* See {@link PngChunkMultiple} and {@link PngChunkSingle}
*
* @return true if PNG accepts multiple chunks of this class
*/
protected abstract boolean allowsMultiple();
public ChunkRaw getRaw() {
return raw;
}
void setRaw(final ChunkRaw raw) {
this.raw = raw;
}
/**
* @see ChunkRaw#len
*/
public int getLen() {
return raw != null ? raw.len : -1;
}
/**
* @see ChunkRaw#getOffset()
*/
public long getOffset() {
return raw != null ? raw.getOffset() : -1;
}
/**
* This signals that the raw chunk (serialized data) as invalid, so that
* it's regenerated on write. This should be
* called for the (infrequent) case of chunks that were copied from a
* PngReader and we want to manually modify it.
*/
public void invalidateRawData() {
raw = null;
}
/**
* see {@link ChunkOrderingConstraint}
*/
public abstract ChunkOrderingConstraint getOrderingConstraint();
@Override
public String toString() {
return "chunk id= " + id + " (len=" + getLen() + " offset=" + getOffset() + ")";
}
}

View File

@ -1,57 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
/**
* acTL chunk. For APGN, not PGN standard
* <p>
* see
* https://wiki.mozilla.org/APNG_Specification#.60acTL.60:_The_Animation_Control_Chunk
* <p>
*/
public class PngChunkACTL extends PngChunkSingle {
public final static String ID = "acTL";
private int numFrames;
private int numPlays;
public PngChunkACTL(final ImageInfo info) {
super(PngChunkACTL.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.BEFORE_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = createEmptyChunk(8, true);
PngHelperInternal.writeInt4tobytes(numFrames, c.data, 0);
PngHelperInternal.writeInt4tobytes(numPlays, c.data, 4);
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
numFrames = PngHelperInternal.readInt4fromBytes(chunk.data, 0);
numPlays = PngHelperInternal.readInt4fromBytes(chunk.data, 4);
}
public int getNumFrames() {
return numFrames;
}
public void setNumFrames(final int numFrames) {
this.numFrames = numFrames;
}
public int getNumPlays() {
return numPlays;
}
public void setNumPlays(final int numPlays) {
this.numPlays = numPlays;
}
}

View File

@ -1,112 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* bKGD Chunk.
* <p>
* see {@link http://www.w3.org/TR/PNG/#11bKGD}
* <p>
* This chunk structure depends on the image type
*/
public class PngChunkBKGD extends PngChunkSingle {
public final static String ID = ChunkHelper.bKGD;
// only one of these is meaningful
private int gray;
private int red, green, blue;
private int paletteIndex;
public PngChunkBKGD(final ImageInfo info) {
super(ChunkHelper.bKGD, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
ChunkRaw c = null;
if (imgInfo.greyscale) {
c = createEmptyChunk(2, true);
PngHelperInternal.writeInt2tobytes(gray, c.data, 0);
} else if (imgInfo.indexed) {
c = createEmptyChunk(1, true);
c.data[0] = (byte) paletteIndex;
} else {
c = createEmptyChunk(6, true);
PngHelperInternal.writeInt2tobytes(red, c.data, 0);
PngHelperInternal.writeInt2tobytes(green, c.data, 0);
PngHelperInternal.writeInt2tobytes(blue, c.data, 0);
}
return c;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
if (imgInfo.greyscale)
gray = PngHelperInternal.readInt2fromBytes(c.data, 0);
else if (imgInfo.indexed)
paletteIndex = c.data[0] & 0xff;
else {
red = PngHelperInternal.readInt2fromBytes(c.data, 0);
green = PngHelperInternal.readInt2fromBytes(c.data, 2);
blue = PngHelperInternal.readInt2fromBytes(c.data, 4);
}
}
/**
* Set gray value (0-255 if bitdept=8)
*
* @param gray
*/
public void setGray(final int gray) {
if (!imgInfo.greyscale)
throw new PngjException("only gray images support this");
this.gray = gray;
}
public int getGray() {
if (!imgInfo.greyscale)
throw new PngjException("only gray images support this");
return gray;
}
/**
* Set pallette index
*
*/
public void setPaletteIndex(final int i) {
if (!imgInfo.indexed)
throw new PngjException("only indexed (pallete) images support this");
paletteIndex = i;
}
public int getPaletteIndex() {
if (!imgInfo.indexed)
throw new PngjException("only indexed (pallete) images support this");
return paletteIndex;
}
/**
* Set rgb values
*
*/
public void setRGB(final int r, final int g, final int b) {
if (imgInfo.greyscale || imgInfo.indexed)
throw new PngjException("only rgb or rgba images support this");
red = r;
green = g;
blue = b;
}
public int[] getRGB() {
if (imgInfo.greyscale || imgInfo.indexed)
throw new PngjException("only rgb or rgba images support this");
return new int[] { red, green, blue };
}
}

View File

@ -1,75 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* cHRM chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11cHRM
*/
public class PngChunkCHRM extends PngChunkSingle {
public final static String ID = ChunkHelper.cHRM;
// http://www.w3.org/TR/PNG/#11cHRM
private double whitex, whitey;
private double redx, redy;
private double greenx, greeny;
private double bluex, bluey;
public PngChunkCHRM(final ImageInfo info) {
super(PngChunkCHRM.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
ChunkRaw c = null;
c = createEmptyChunk(32, true);
PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(whitex), c.data, 0);
PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(whitey), c.data, 4);
PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(redx), c.data, 8);
PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(redy), c.data, 12);
PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(greenx), c.data, 16);
PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(greeny), c.data, 20);
PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(bluex), c.data, 24);
PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(bluey), c.data, 28);
return c;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
if (c.len != 32)
throw new PngjException("bad chunk " + c);
whitex = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 0));
whitey = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 4));
redx = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 8));
redy = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 12));
greenx = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 16));
greeny = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 20));
bluex = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 24));
bluey = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 28));
}
public void setChromaticities(final double whitex, final double whitey, final double redx, final double redy,
final double greenx, final double greeny, final double bluex, final double bluey) {
this.whitex = whitex;
this.redx = redx;
this.greenx = greenx;
this.bluex = bluex;
this.whitey = whitey;
this.redy = redy;
this.greeny = greeny;
this.bluey = bluey;
}
public double[] getChromaticities() {
return new double[] { whitex, whitey, redx, redy, greenx, greeny, bluex, bluey };
}
}

View File

@ -1,158 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
/**
* fcTL chunk. For APGN, not PGN standard
* <p>
* see
* https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
* <p>
*/
public class PngChunkFCTL extends PngChunkMultiple {
public final static String ID = "fcTL";
public final static byte APNG_DISPOSE_OP_NONE = 0;
public final static byte APNG_DISPOSE_OP_BACKGROUND = 1;
public final static byte APNG_DISPOSE_OP_PREVIOUS = 2;
public final static byte APNG_BLEND_OP_SOURCE = 0;
public final static byte APNG_BLEND_OP_OVER = 1;
private int seqNum;
private int width, height, xOff, yOff;
private int delayNum, delayDen;
private byte disposeOp, blendOp;
public PngChunkFCTL(final ImageInfo info) {
super(PngChunkFCTL.ID, info);
}
public ImageInfo getEquivImageInfo() {
return new ImageInfo(width, height, imgInfo.bitDepth, imgInfo.alpha, imgInfo.greyscale, imgInfo.indexed);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.NONE;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = createEmptyChunk(8, true);
int off = 0;
PngHelperInternal.writeInt4tobytes(seqNum, c.data, off);
off += 4;
PngHelperInternal.writeInt4tobytes(width, c.data, off);
off += 4;
PngHelperInternal.writeInt4tobytes(height, c.data, off);
off += 4;
PngHelperInternal.writeInt4tobytes(xOff, c.data, off);
off += 4;
PngHelperInternal.writeInt4tobytes(yOff, c.data, off);
off += 4;
PngHelperInternal.writeInt2tobytes(delayNum, c.data, off);
off += 2;
PngHelperInternal.writeInt2tobytes(delayDen, c.data, off);
off += 2;
c.data[off] = disposeOp;
off += 1;
c.data[off] = blendOp;
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
int off = 0;
seqNum = PngHelperInternal.readInt4fromBytes(chunk.data, off);
off += 4;
width = PngHelperInternal.readInt4fromBytes(chunk.data, off);
off += 4;
height = PngHelperInternal.readInt4fromBytes(chunk.data, off);
off += 4;
xOff = PngHelperInternal.readInt4fromBytes(chunk.data, off);
off += 4;
yOff = PngHelperInternal.readInt4fromBytes(chunk.data, off);
off += 4;
delayNum = PngHelperInternal.readInt2fromBytes(chunk.data, off);
off += 2;
delayDen = PngHelperInternal.readInt2fromBytes(chunk.data, off);
off += 2;
disposeOp = chunk.data[off];
off += 1;
blendOp = chunk.data[off];
}
public int getSeqNum() {
return seqNum;
}
public void setSeqNum(final int seqNum) {
this.seqNum = seqNum;
}
public int getWidth() {
return width;
}
public void setWidth(final int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(final int height) {
this.height = height;
}
public int getxOff() {
return xOff;
}
public void setxOff(final int xOff) {
this.xOff = xOff;
}
public int getyOff() {
return yOff;
}
public void setyOff(final int yOff) {
this.yOff = yOff;
}
public int getDelayNum() {
return delayNum;
}
public void setDelayNum(final int delayNum) {
this.delayNum = delayNum;
}
public int getDelayDen() {
return delayDen;
}
public void setDelayDen(final int delayDen) {
this.delayDen = delayDen;
}
public byte getDisposeOp() {
return disposeOp;
}
public void setDisposeOp(final byte disposeOp) {
this.disposeOp = disposeOp;
}
public byte getBlendOp() {
return blendOp;
}
public void setBlendOp(final byte blendOp) {
this.blendOp = blendOp;
}
}

View File

@ -1,72 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* fdAT chunk. For APGN, not PGN standard
* <p>
* see
* https://wiki.mozilla.org/APNG_Specification#.60fdAT.60:_The_Frame_Data_Chunk
* <p>
* This implementation does not support buffering, this should be not managed
* similar to a IDAT chunk
*
*/
public class PngChunkFDAT extends PngChunkMultiple {
public final static String ID = "fdAT";
private int seqNum;
private byte[] buffer; // normally not allocated - if so, it's the raw data, so it includes the 4bytes seqNum
int datalen; // length of idat data, excluding seqNUm (= chunk.len-4)
public PngChunkFDAT(final ImageInfo info) {
super(PngChunkFDAT.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.AFTER_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
if (buffer == null)
throw new PngjException("not buffered");
final ChunkRaw c = createEmptyChunk(datalen + 4, false);
c.data = buffer; // shallow copy!
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
seqNum = PngHelperInternal.readInt4fromBytes(chunk.data, 0);
datalen = chunk.len - 4;
buffer = chunk.data;
}
public int getSeqNum() {
return seqNum;
}
public void setSeqNum(final int seqNum) {
this.seqNum = seqNum;
}
public byte[] getBuffer() {
return buffer;
}
public void setBuffer(final byte[] buffer) {
this.buffer = buffer;
}
public int getDatalen() {
return datalen;
}
public void setDatalen(final int datalen) {
this.datalen = datalen;
}
}

View File

@ -1,51 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* gAMA chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11gAMA
*/
public class PngChunkGAMA extends PngChunkSingle {
public final static String ID = ChunkHelper.gAMA;
// http://www.w3.org/TR/PNG/#11gAMA
private double gamma;
public PngChunkGAMA(final ImageInfo info) {
super(PngChunkGAMA.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = createEmptyChunk(4, true);
final int g = (int) (gamma * 100000 + 0.5);
PngHelperInternal.writeInt4tobytes(g, c.data, 0);
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
if (chunk.len != 4)
throw new PngjException("bad chunk " + chunk);
final int g = PngHelperInternal.readInt4fromBytes(chunk.data, 0);
gamma = g / 100000.0;
}
public double getGamma() {
return gamma;
}
public void setGamma(final double gamma) {
this.gamma = gamma;
}
}

View File

@ -1,56 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* hIST chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11hIST <br>
* only for palette images
*/
public class PngChunkHIST extends PngChunkSingle {
public final static String ID = ChunkHelper.hIST;
private int[] hist = new int[0]; // should have same lenght as palette
public PngChunkHIST(final ImageInfo info) {
super(PngChunkHIST.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
if (!imgInfo.indexed)
throw new PngjException("only indexed images accept a HIST chunk");
final int nentries = c.data.length / 2;
hist = new int[nentries];
for (int i = 0; i < hist.length; i++)
hist[i] = PngHelperInternal.readInt2fromBytes(c.data, i * 2);
}
@Override
public ChunkRaw createRawChunk() {
if (!imgInfo.indexed)
throw new PngjException("only indexed images accept a HIST chunk");
ChunkRaw c = null;
c = createEmptyChunk(hist.length * 2, true);
for (int i = 0; i < hist.length; i++)
PngHelperInternal.writeInt2tobytes(hist[i], c.data, i * 2);
return c;
}
public int[] getHist() {
return hist;
}
public void setHist(final int[] hist) {
this.hist = hist;
}
}

View File

@ -1,76 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngjException;
/**
* iCCP chunk.
* <p>
* See {@link http://www.w3.org/TR/PNG/#11iCCP}
*/
public class PngChunkICCP extends PngChunkSingle {
public final static String ID = ChunkHelper.iCCP;
// http://www.w3.org/TR/PNG/#11iCCP
private String profileName;
private byte[] compressedProfile; // copmression/decopmresion is done in getter/setter
public PngChunkICCP(final ImageInfo info) {
super(PngChunkICCP.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = createEmptyChunk(profileName.length() + compressedProfile.length + 2, true);
System.arraycopy(ChunkHelper.toBytes(profileName), 0, c.data, 0, profileName.length());
c.data[profileName.length()] = 0;
c.data[profileName.length() + 1] = 0;
System.arraycopy(compressedProfile, 0, c.data, profileName.length() + 2, compressedProfile.length);
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
final int pos0 = ChunkHelper.posNullByte(chunk.data);
profileName = ChunkHelper.toString(chunk.data, 0, pos0);
final int comp = chunk.data[pos0 + 1] & 0xff;
if (comp != 0)
throw new PngjException("bad compression for ChunkTypeICCP");
final int compdatasize = chunk.data.length - (pos0 + 2);
compressedProfile = new byte[compdatasize];
System.arraycopy(chunk.data, pos0 + 2, compressedProfile, 0, compdatasize);
}
/**
* The profile should be uncompressed bytes
*/
public void setProfileNameAndContent(final String name, final byte[] profile) {
profileName = name;
compressedProfile = ChunkHelper.compressBytes(profile, true);
}
public void setProfileNameAndContent(final String name, final String profile) {
setProfileNameAndContent(name, ChunkHelper.toBytes(profile));
}
public String getProfileName() {
return profileName;
}
/**
* uncompressed
**/
public byte[] getProfile() {
return ChunkHelper.compressBytes(compressedProfile, false);
}
public String getProfileAsString() {
return ChunkHelper.toString(getProfile());
}
}

View File

@ -1,35 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
/**
* IDAT chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11IDAT
* <p>
* This is dummy placeholder - we write/read this chunk (actually several) by
* special code.
*/
public class PngChunkIDAT extends PngChunkMultiple {
public final static String ID = ChunkHelper.IDAT;
// http://www.w3.org/TR/PNG/#11IDAT
public PngChunkIDAT(final ImageInfo i) {
super(PngChunkIDAT.ID, i);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.NA;
}
@Override
public ChunkRaw createRawChunk() {// does nothing
return null;
}
@Override
public void parseFromRaw(final ChunkRaw c) { // does nothing
}
}

View File

@ -1,35 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
/**
* IEND chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11IEND
*/
public class PngChunkIEND extends PngChunkSingle {
public final static String ID = ChunkHelper.IEND;
// http://www.w3.org/TR/PNG/#11IEND
// this is a dummy placeholder
public PngChunkIEND(final ImageInfo info) {
super(PngChunkIEND.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.NA;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = new ChunkRaw(0, ChunkHelper.b_IEND, false);
return c;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
// this is not used
}
}

View File

@ -1,184 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.io.ByteArrayInputStream;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
import ar.com.hjg.pngj.PngjInputException;
/**
* IHDR chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11IHDR
* <p>
* This is a special critical Chunk.
*/
public class PngChunkIHDR extends PngChunkSingle {
public final static String ID = ChunkHelper.IHDR;
private int cols;
private int rows;
private int bitspc;
private int colormodel;
private int compmeth;
private int filmeth;
private int interlaced;
// http://www.w3.org/TR/PNG/#11IHDR
//
public PngChunkIHDR(final ImageInfo info) { // argument is normally null here, if not null is used to fill the fields
super(PngChunkIHDR.ID, info);
if (info != null)
fillFromInfo(info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.NA;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = new ChunkRaw(13, ChunkHelper.b_IHDR, true);
int offset = 0;
PngHelperInternal.writeInt4tobytes(cols, c.data, offset);
offset += 4;
PngHelperInternal.writeInt4tobytes(rows, c.data, offset);
offset += 4;
c.data[offset++] = (byte) bitspc;
c.data[offset++] = (byte) colormodel;
c.data[offset++] = (byte) compmeth;
c.data[offset++] = (byte) filmeth;
c.data[offset++] = (byte) interlaced;
return c;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
if (c.len != 13)
throw new PngjException("Bad IDHR len " + c.len);
final ByteArrayInputStream st = c.getAsByteStream();
cols = PngHelperInternal.readInt4(st);
rows = PngHelperInternal.readInt4(st);
// bit depth: number of bits per channel
bitspc = PngHelperInternal.readByte(st);
colormodel = PngHelperInternal.readByte(st);
compmeth = PngHelperInternal.readByte(st);
filmeth = PngHelperInternal.readByte(st);
interlaced = PngHelperInternal.readByte(st);
}
public int getCols() {
return cols;
}
public void setCols(final int cols) {
this.cols = cols;
}
public int getRows() {
return rows;
}
public void setRows(final int rows) {
this.rows = rows;
}
public int getBitspc() {
return bitspc;
}
public void setBitspc(final int bitspc) {
this.bitspc = bitspc;
}
public int getColormodel() {
return colormodel;
}
public void setColormodel(final int colormodel) {
this.colormodel = colormodel;
}
public int getCompmeth() {
return compmeth;
}
public void setCompmeth(final int compmeth) {
this.compmeth = compmeth;
}
public int getFilmeth() {
return filmeth;
}
public void setFilmeth(final int filmeth) {
this.filmeth = filmeth;
}
public int getInterlaced() {
return interlaced;
}
public void setInterlaced(final int interlaced) {
this.interlaced = interlaced;
}
public boolean isInterlaced() {
return getInterlaced() == 1;
}
public void fillFromInfo(final ImageInfo info) {
setCols(imgInfo.cols);
setRows(imgInfo.rows);
setBitspc(imgInfo.bitDepth);
int colormodel = 0;
if (imgInfo.alpha)
colormodel += 0x04;
if (imgInfo.indexed)
colormodel += 0x01;
if (!imgInfo.greyscale)
colormodel += 0x02;
setColormodel(colormodel);
setCompmeth(0); // compression method 0=deflate
setFilmeth(0); // filter method (0)
setInterlaced(0); // we never interlace
}
/** throws PngInputException if unexpected values */
public ImageInfo createImageInfo() {
check();
final boolean alpha = (getColormodel() & 0x04) != 0;
final boolean palette = (getColormodel() & 0x01) != 0;
final boolean grayscale = getColormodel() == 0 || getColormodel() == 4;
// creates ImgInfo and imgLine, and allocates buffers
return new ImageInfo(getCols(), getRows(), getBitspc(), alpha, grayscale, palette);
}
public void check() {
if (cols < 1 || rows < 1 || compmeth != 0 || filmeth != 0)
throw new PngjInputException("bad IHDR: col/row/compmethod/filmethod invalid");
if (bitspc != 1 && bitspc != 2 && bitspc != 4 && bitspc != 8 && bitspc != 16)
throw new PngjInputException("bad IHDR: bitdepth invalid");
if (interlaced < 0 || interlaced > 1)
throw new PngjInputException("bad IHDR: interlace invalid");
switch (colormodel) {
case 0:
break;
case 3:
if (bitspc == 16)
throw new PngjInputException("bad IHDR: bitdepth invalid");
break;
case 2:
case 4:
case 6:
if (bitspc != 8 && bitspc != 16)
throw new PngjInputException("bad IHDR: bitdepth invalid");
break;
default:
throw new PngjInputException("bad IHDR: invalid colormodel");
}
}
}

View File

@ -1,108 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngjException;
/**
* iTXt chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11iTXt
*/
public class PngChunkITXT extends PngChunkTextVar {
public final static String ID = ChunkHelper.iTXt;
private boolean compressed = false;
private String langTag = "";
private String translatedTag = "";
// http://www.w3.org/TR/PNG/#11iTXt
public PngChunkITXT(final ImageInfo info) {
super(PngChunkITXT.ID, info);
}
@Override
public ChunkRaw createRawChunk() {
if (key == null || key.trim().length() == 0)
throw new PngjException("Text chunk key must be non empty");
try {
final ByteArrayOutputStream ba = new ByteArrayOutputStream();
ba.write(ChunkHelper.toBytes(key));
ba.write(0); // separator
ba.write(compressed ? 1 : 0);
ba.write(0); // compression method (always 0)
ba.write(ChunkHelper.toBytes(langTag));
ba.write(0); // separator
ba.write(ChunkHelper.toBytesUTF8(translatedTag));
ba.write(0); // separator
byte[] textbytes = ChunkHelper.toBytesUTF8(val);
if (compressed)
textbytes = ChunkHelper.compressBytes(textbytes, true);
ba.write(textbytes);
final byte[] b = ba.toByteArray();
final ChunkRaw chunk = createEmptyChunk(b.length, false);
chunk.data = b;
return chunk;
} catch (final IOException e) {
throw new PngjException(e);
}
}
@Override
public void parseFromRaw(final ChunkRaw c) {
int nullsFound = 0;
final int[] nullsIdx = new int[3];
for (int i = 0; i < c.data.length; i++) {
if (c.data[i] != 0)
continue;
nullsIdx[nullsFound] = i;
nullsFound++;
if (nullsFound == 1)
i += 2;
if (nullsFound == 3)
break;
}
if (nullsFound != 3)
throw new PngjException("Bad formed PngChunkITXT chunk");
key = ChunkHelper.toString(c.data, 0, nullsIdx[0]);
int i = nullsIdx[0] + 1;
compressed = c.data[i] == 0 ? false : true;
i++;
if (compressed && c.data[i] != 0)
throw new PngjException("Bad formed PngChunkITXT chunk - bad compression method ");
langTag = ChunkHelper.toString(c.data, i, nullsIdx[1] - i);
translatedTag = ChunkHelper.toStringUTF8(c.data, nullsIdx[1] + 1, nullsIdx[2] - nullsIdx[1] - 1);
i = nullsIdx[2] + 1;
if (compressed) {
final byte[] bytes = ChunkHelper.compressBytes(c.data, i, c.data.length - i, false);
val = ChunkHelper.toStringUTF8(bytes);
} else
val = ChunkHelper.toStringUTF8(c.data, i, c.data.length - i);
}
public boolean isCompressed() {
return compressed;
}
public void setCompressed(final boolean compressed) {
this.compressed = compressed;
}
public String getLangtag() {
return langTag;
}
public void setLangtag(final String langtag) {
langTag = langtag;
}
public String getTranslatedTag() {
return translatedTag;
}
public void setTranslatedTag(final String translatedTag) {
this.translatedTag = translatedTag;
}
}

View File

@ -1,28 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
/**
* PNG chunk type (abstract) that allows multiple instances in same image.
*/
public abstract class PngChunkMultiple extends PngChunk {
protected PngChunkMultiple(final String id, final ImageInfo imgInfo) {
super(id, imgInfo);
}
@Override
public final boolean allowsMultiple() {
return true;
}
/**
* NOTE: this chunk uses the default Object's equals() hashCode()
* implementation.
*
* This is the right thing to do, normally.
*
* This is important, eg see ChunkList.removeFromList()
*/
}

View File

@ -1,81 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* oFFs chunk.
* <p>
* see http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.oFFs
*/
public class PngChunkOFFS extends PngChunkSingle {
public final static String ID = "oFFs";
// http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.oFFs
private long posX;
private long posY;
private int units; // 0: pixel 1:micrometer
public PngChunkOFFS(final ImageInfo info) {
super(PngChunkOFFS.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.BEFORE_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = createEmptyChunk(9, true);
PngHelperInternal.writeInt4tobytes((int) posX, c.data, 0);
PngHelperInternal.writeInt4tobytes((int) posY, c.data, 4);
c.data[8] = (byte) units;
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
if (chunk.len != 9)
throw new PngjException("bad chunk length " + chunk);
posX = PngHelperInternal.readInt4fromBytes(chunk.data, 0);
if (posX < 0)
posX += 0x100000000L;
posY = PngHelperInternal.readInt4fromBytes(chunk.data, 4);
if (posY < 0)
posY += 0x100000000L;
units = PngHelperInternal.readInt1fromByte(chunk.data, 8);
}
/**
* 0: pixel, 1:micrometer
*/
public int getUnits() {
return units;
}
/**
* 0: pixel, 1:micrometer
*/
public void setUnits(final int units) {
this.units = units;
}
public long getPosX() {
return posX;
}
public void setPosX(final long posX) {
this.posX = posX;
}
public long getPosY() {
return posY;
}
public void setPosY(final long posY) {
this.posY = posY;
}
}

View File

@ -1,107 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* pHYs chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11pHYs
*/
public class PngChunkPHYS extends PngChunkSingle {
public final static String ID = ChunkHelper.pHYs;
// http://www.w3.org/TR/PNG/#11pHYs
private long pixelsxUnitX;
private long pixelsxUnitY;
private int units; // 0: unknown 1:metre
public PngChunkPHYS(final ImageInfo info) {
super(PngChunkPHYS.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.BEFORE_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = createEmptyChunk(9, true);
PngHelperInternal.writeInt4tobytes((int) pixelsxUnitX, c.data, 0);
PngHelperInternal.writeInt4tobytes((int) pixelsxUnitY, c.data, 4);
c.data[8] = (byte) units;
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
if (chunk.len != 9)
throw new PngjException("bad chunk length " + chunk);
pixelsxUnitX = PngHelperInternal.readInt4fromBytes(chunk.data, 0);
if (pixelsxUnitX < 0)
pixelsxUnitX += 0x100000000L;
pixelsxUnitY = PngHelperInternal.readInt4fromBytes(chunk.data, 4);
if (pixelsxUnitY < 0)
pixelsxUnitY += 0x100000000L;
units = PngHelperInternal.readInt1fromByte(chunk.data, 8);
}
public long getPixelsxUnitX() {
return pixelsxUnitX;
}
public void setPixelsxUnitX(final long pixelsxUnitX) {
this.pixelsxUnitX = pixelsxUnitX;
}
public long getPixelsxUnitY() {
return pixelsxUnitY;
}
public void setPixelsxUnitY(final long pixelsxUnitY) {
this.pixelsxUnitY = pixelsxUnitY;
}
public int getUnits() {
return units;
}
public void setUnits(final int units) {
this.units = units;
}
// special getters / setters
/**
* returns -1 if the physicial unit is unknown, or X-Y are not equal
*/
public double getAsDpi() {
if (units != 1 || pixelsxUnitX != pixelsxUnitY)
return -1;
return pixelsxUnitX * 0.0254;
}
/**
* returns -1 if the physicial unit is unknown
*/
public double[] getAsDpi2() {
if (units != 1)
return new double[] { -1, -1 };
return new double[] { pixelsxUnitX * 0.0254, pixelsxUnitY * 0.0254 };
}
public void setAsDpi(final double dpi) {
units = 1;
pixelsxUnitX = (long) (dpi / 0.0254 + 0.5);
pixelsxUnitY = pixelsxUnitX;
}
public void setAsDpi2(final double dpix, final double dpiy) {
units = 1;
pixelsxUnitX = (long) (dpix / 0.0254 + 0.5);
pixelsxUnitY = (long) (dpiy / 0.0254 + 0.5);
}
}

View File

@ -1,95 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngjException;
/**
* PLTE chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11PLTE
* <p>
* Critical chunk
*/
public class PngChunkPLTE extends PngChunkSingle {
public final static String ID = ChunkHelper.PLTE;
// http://www.w3.org/TR/PNG/#11PLTE
private int nentries = 0;
/**
* RGB8 packed in one integer
*/
private int[] entries;
public PngChunkPLTE(final ImageInfo info) {
super(PngChunkPLTE.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.NA;
}
@Override
public ChunkRaw createRawChunk() {
final int len = 3 * nentries;
final int[] rgb = new int[3];
final ChunkRaw c = createEmptyChunk(len, true);
for (int n = 0, i = 0; n < nentries; n++) {
getEntryRgb(n, rgb);
c.data[i++] = (byte) rgb[0];
c.data[i++] = (byte) rgb[1];
c.data[i++] = (byte) rgb[2];
}
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
setNentries(chunk.len / 3);
for (int n = 0, i = 0; n < nentries; n++)
setEntry(n, chunk.data[i++] & 0xff, chunk.data[i++] & 0xff, chunk.data[i++] & 0xff);
}
public void setNentries(final int n) {
nentries = n;
if (nentries < 1 || nentries > 256)
throw new PngjException("invalid pallette - nentries=" + nentries);
if (entries == null || entries.length != nentries)
entries = new int[nentries];
}
public int getNentries() {
return nentries;
}
public void setEntry(final int n, final int r, final int g, final int b) {
entries[n] = r << 16 | g << 8 | b;
}
public int getEntry(final int n) {
return entries[n];
}
public void getEntryRgb(final int n, final int[] rgb) {
getEntryRgb(n, rgb, 0);
}
public void getEntryRgb(final int n, final int[] rgb, final int offset) {
final int v = entries[n];
rgb[offset + 0] = (v & 0xff0000) >> 16;
rgb[offset + 1] = (v & 0xff00) >> 8;
rgb[offset + 2] = v & 0xff;
}
public int minBitDepth() {
if (nentries <= 2)
return 1;
else if (nentries <= 4)
return 2;
else if (nentries <= 16)
return 4;
else
return 8;
}
}

View File

@ -1,114 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* sBIT chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11sBIT
* <p>
* this chunk structure depends on the image type
*/
public class PngChunkSBIT extends PngChunkSingle {
public final static String ID = ChunkHelper.sBIT;
// http://www.w3.org/TR/PNG/#11sBIT
// significant bits
private int graysb, alphasb;
private int redsb, greensb, bluesb;
public PngChunkSBIT(final ImageInfo info) {
super(PngChunkSBIT.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT;
}
private int getCLen() {
int len = imgInfo.greyscale ? 1 : 3;
if (imgInfo.alpha)
len += 1;
return len;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
if (c.len != getCLen())
throw new PngjException("bad chunk length " + c);
if (imgInfo.greyscale) {
graysb = PngHelperInternal.readInt1fromByte(c.data, 0);
if (imgInfo.alpha)
alphasb = PngHelperInternal.readInt1fromByte(c.data, 1);
} else {
redsb = PngHelperInternal.readInt1fromByte(c.data, 0);
greensb = PngHelperInternal.readInt1fromByte(c.data, 1);
bluesb = PngHelperInternal.readInt1fromByte(c.data, 2);
if (imgInfo.alpha)
alphasb = PngHelperInternal.readInt1fromByte(c.data, 3);
}
}
@Override
public ChunkRaw createRawChunk() {
ChunkRaw c = null;
c = createEmptyChunk(getCLen(), true);
if (imgInfo.greyscale) {
c.data[0] = (byte) graysb;
if (imgInfo.alpha)
c.data[1] = (byte) alphasb;
} else {
c.data[0] = (byte) redsb;
c.data[1] = (byte) greensb;
c.data[2] = (byte) bluesb;
if (imgInfo.alpha)
c.data[3] = (byte) alphasb;
}
return c;
}
public void setGraysb(final int gray) {
if (!imgInfo.greyscale)
throw new PngjException("only greyscale images support this");
graysb = gray;
}
public int getGraysb() {
if (!imgInfo.greyscale)
throw new PngjException("only greyscale images support this");
return graysb;
}
public void setAlphasb(final int a) {
if (!imgInfo.alpha)
throw new PngjException("only images with alpha support this");
alphasb = a;
}
public int getAlphasb() {
if (!imgInfo.alpha)
throw new PngjException("only images with alpha support this");
return alphasb;
}
/**
* Set rgb values
*
*/
public void setRGB(final int r, final int g, final int b) {
if (imgInfo.greyscale || imgInfo.indexed)
throw new PngjException("only rgb or rgba images support this");
redsb = r;
greensb = g;
bluesb = b;
}
public int[] getRGB() {
if (imgInfo.greyscale || imgInfo.indexed)
throw new PngjException("only rgb or rgba images support this");
return new int[] { redsb, greensb, bluesb };
}
}

View File

@ -1,129 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* sPLT chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11sPLT
*/
public class PngChunkSPLT extends PngChunkMultiple {
public final static String ID = ChunkHelper.sPLT;
// http://www.w3.org/TR/PNG/#11sPLT
private String palName;
private int sampledepth; // 8/16
private int[] palette; // 5 elements per entry
public PngChunkSPLT(final ImageInfo info) {
super(PngChunkSPLT.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.BEFORE_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
try {
final ByteArrayOutputStream ba = new ByteArrayOutputStream();
ba.write(ChunkHelper.toBytes(palName));
ba.write(0); // separator
ba.write((byte) sampledepth);
final int nentries = getNentries();
for (int n = 0; n < nentries; n++) {
for (int i = 0; i < 4; i++)
if (sampledepth == 8)
PngHelperInternal.writeByte(ba, (byte) palette[n * 5 + i]);
else
PngHelperInternal.writeInt2(ba, palette[n * 5 + i]);
PngHelperInternal.writeInt2(ba, palette[n * 5 + 4]);
}
final byte[] b = ba.toByteArray();
final ChunkRaw chunk = createEmptyChunk(b.length, false);
chunk.data = b;
return chunk;
} catch (final IOException e) {
throw new PngjException(e);
}
}
@Override
public void parseFromRaw(final ChunkRaw c) {
int t = -1;
for (int i = 0; i < c.data.length; i++)
if (c.data[i] == 0) {
t = i;
break;
}
if (t <= 0 || t > c.data.length - 2)
throw new PngjException("bad sPLT chunk: no separator found");
palName = ChunkHelper.toString(c.data, 0, t);
sampledepth = PngHelperInternal.readInt1fromByte(c.data, t + 1);
t += 2;
final int nentries = (c.data.length - t) / (sampledepth == 8 ? 6 : 10);
palette = new int[nentries * 5];
int r, g, b, a, f, ne;
ne = 0;
for (int i = 0; i < nentries; i++) {
if (sampledepth == 8) {
r = PngHelperInternal.readInt1fromByte(c.data, t++);
g = PngHelperInternal.readInt1fromByte(c.data, t++);
b = PngHelperInternal.readInt1fromByte(c.data, t++);
a = PngHelperInternal.readInt1fromByte(c.data, t++);
} else {
r = PngHelperInternal.readInt2fromBytes(c.data, t);
t += 2;
g = PngHelperInternal.readInt2fromBytes(c.data, t);
t += 2;
b = PngHelperInternal.readInt2fromBytes(c.data, t);
t += 2;
a = PngHelperInternal.readInt2fromBytes(c.data, t);
t += 2;
}
f = PngHelperInternal.readInt2fromBytes(c.data, t);
t += 2;
palette[ne++] = r;
palette[ne++] = g;
palette[ne++] = b;
palette[ne++] = a;
palette[ne++] = f;
}
}
public int getNentries() {
return palette.length / 5;
}
public String getPalName() {
return palName;
}
public void setPalName(final String palName) {
this.palName = palName;
}
public int getSampledepth() {
return sampledepth;
}
public void setSampledepth(final int sampledepth) {
this.sampledepth = sampledepth;
}
public int[] getPalette() {
return palette;
}
public void setPalette(final int[] palette) {
this.palette = palette;
}
}

View File

@ -1,55 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* sRGB chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11sRGB
*/
public class PngChunkSRGB extends PngChunkSingle {
public final static String ID = ChunkHelper.sRGB;
// http://www.w3.org/TR/PNG/#11sRGB
public static final int RENDER_INTENT_Perceptual = 0;
public static final int RENDER_INTENT_Relative_colorimetric = 1;
public static final int RENDER_INTENT_Saturation = 2;
public static final int RENDER_INTENT_Absolute_colorimetric = 3;
private int intent;
public PngChunkSRGB(final ImageInfo info) {
super(PngChunkSRGB.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
if (c.len != 1)
throw new PngjException("bad chunk length " + c);
intent = PngHelperInternal.readInt1fromByte(c.data, 0);
}
@Override
public ChunkRaw createRawChunk() {
ChunkRaw c = null;
c = createEmptyChunk(1, true);
c.data[0] = (byte) intent;
return c;
}
public int getIntent() {
return intent;
}
public void setIntent(final int intent) {
this.intent = intent;
}
}

View File

@ -1,54 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngjException;
/**
* sTER chunk.
* <p>
* see http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.sTER
*/
public class PngChunkSTER extends PngChunkSingle {
public final static String ID = "sTER";
// http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.sTER
private byte mode; // 0: cross-fuse layout 1: diverging-fuse layout
public PngChunkSTER(final ImageInfo info) {
super(PngChunkSTER.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.BEFORE_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = createEmptyChunk(1, true);
c.data[0] = mode;
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
if (chunk.len != 1)
throw new PngjException("bad chunk length " + chunk);
mode = chunk.data[0];
}
/**
* 0: cross-fuse layout 1: diverging-fuse layout
*/
public byte getMode() {
return mode;
}
/**
* 0: cross-fuse layout 1: diverging-fuse layout
*/
public void setMode(final byte mode) {
this.mode = mode;
}
}

View File

@ -1,45 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
/**
* PNG chunk type (abstract) that does not allow multiple instances in same
* image.
*/
public abstract class PngChunkSingle extends PngChunk {
protected PngChunkSingle(final String id, final ImageInfo imgInfo) {
super(id, imgInfo);
}
@Override
public final boolean allowsMultiple() {
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (id == null ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final PngChunkSingle other = (PngChunkSingle) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
}

View File

@ -1,44 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngjException;
/**
* tEXt chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11tEXt
*/
public class PngChunkTEXT extends PngChunkTextVar {
public final static String ID = ChunkHelper.tEXt;
public PngChunkTEXT(final ImageInfo info) {
super(PngChunkTEXT.ID, info);
}
public PngChunkTEXT(final ImageInfo info, final String key, final String val) {
super(PngChunkTEXT.ID, info);
setKeyVal(key, val);
}
@Override
public ChunkRaw createRawChunk() {
if (key == null || key.trim().length() == 0)
throw new PngjException("Text chunk key must be non empty");
final byte[] b = ChunkHelper.toBytes(key + "\0" + val);
final ChunkRaw chunk = createEmptyChunk(b.length, false);
chunk.data = b;
return chunk;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
int i;
for (i = 0; i < c.data.length; i++)
if (c.data[i] == 0)
break;
key = ChunkHelper.toString(c.data, 0, i);
i++;
val = i < c.data.length ? ChunkHelper.toString(c.data, i, c.data.length - i) : "";
}
}

View File

@ -1,83 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.util.Calendar;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* tIME chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11tIME
*/
public class PngChunkTIME extends PngChunkSingle {
public final static String ID = ChunkHelper.tIME;
// http://www.w3.org/TR/PNG/#11tIME
private int year, mon, day, hour, min, sec;
public PngChunkTIME(final ImageInfo info) {
super(PngChunkTIME.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.NONE;
}
@Override
public ChunkRaw createRawChunk() {
final ChunkRaw c = createEmptyChunk(7, true);
PngHelperInternal.writeInt2tobytes(year, c.data, 0);
c.data[2] = (byte) mon;
c.data[3] = (byte) day;
c.data[4] = (byte) hour;
c.data[5] = (byte) min;
c.data[6] = (byte) sec;
return c;
}
@Override
public void parseFromRaw(final ChunkRaw chunk) {
if (chunk.len != 7)
throw new PngjException("bad chunk " + chunk);
year = PngHelperInternal.readInt2fromBytes(chunk.data, 0);
mon = PngHelperInternal.readInt1fromByte(chunk.data, 2);
day = PngHelperInternal.readInt1fromByte(chunk.data, 3);
hour = PngHelperInternal.readInt1fromByte(chunk.data, 4);
min = PngHelperInternal.readInt1fromByte(chunk.data, 5);
sec = PngHelperInternal.readInt1fromByte(chunk.data, 6);
}
public void setNow(final int secsAgo) {
final Calendar d = Calendar.getInstance();
d.setTimeInMillis(System.currentTimeMillis() - 1000 * (long) secsAgo);
year = d.get(Calendar.YEAR);
mon = d.get(Calendar.MONTH) + 1;
day = d.get(Calendar.DAY_OF_MONTH);
hour = d.get(Calendar.HOUR_OF_DAY);
min = d.get(Calendar.MINUTE);
sec = d.get(Calendar.SECOND);
}
public void setYMDHMS(final int yearx, final int monx, final int dayx, final int hourx, final int minx,
final int secx) {
year = yearx;
mon = monx;
day = dayx;
hour = hourx;
min = minx;
sec = secx;
}
public int[] getYMDHMS() {
return new int[] { year, mon, day, hour, min, sec };
}
/** format YYYY/MM/DD HH:mm:SS */
public String getAsString() {
return String.format("%04d/%02d/%02d %02d:%02d:%02d", year, mon, day, hour, min, sec);
}
}

View File

@ -1,150 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngHelperInternal;
import ar.com.hjg.pngj.PngjException;
/**
* tRNS chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11tRNS
* <p>
* this chunk structure depends on the image type
*/
public class PngChunkTRNS extends PngChunkSingle {
public final static String ID = ChunkHelper.tRNS;
// http://www.w3.org/TR/PNG/#11tRNS
// only one of these is meaningful, depending on the image type
private int gray;
private int red, green, blue;
private int[] paletteAlpha = new int[] {};
public PngChunkTRNS(final ImageInfo info) {
super(PngChunkTRNS.ID, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT;
}
@Override
public ChunkRaw createRawChunk() {
ChunkRaw c = null;
if (imgInfo.greyscale) {
c = createEmptyChunk(2, true);
PngHelperInternal.writeInt2tobytes(gray, c.data, 0);
} else if (imgInfo.indexed) {
c = createEmptyChunk(paletteAlpha.length, true);
for (int n = 0; n < c.len; n++)
c.data[n] = (byte) paletteAlpha[n];
} else {
c = createEmptyChunk(6, true);
PngHelperInternal.writeInt2tobytes(red, c.data, 0);
PngHelperInternal.writeInt2tobytes(green, c.data, 0);
PngHelperInternal.writeInt2tobytes(blue, c.data, 0);
}
return c;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
if (imgInfo.greyscale)
gray = PngHelperInternal.readInt2fromBytes(c.data, 0);
else if (imgInfo.indexed) {
final int nentries = c.data.length;
paletteAlpha = new int[nentries];
for (int n = 0; n < nentries; n++)
paletteAlpha[n] = c.data[n] & 0xff;
} else {
red = PngHelperInternal.readInt2fromBytes(c.data, 0);
green = PngHelperInternal.readInt2fromBytes(c.data, 2);
blue = PngHelperInternal.readInt2fromBytes(c.data, 4);
}
}
/**
* Set rgb values
*
*/
public void setRGB(final int r, final int g, final int b) {
if (imgInfo.greyscale || imgInfo.indexed)
throw new PngjException("only rgb or rgba images support this");
red = r;
green = g;
blue = b;
}
public int[] getRGB() {
if (imgInfo.greyscale || imgInfo.indexed)
throw new PngjException("only rgb or rgba images support this");
return new int[] { red, green, blue };
}
public int getRGB888() {
if (imgInfo.greyscale || imgInfo.indexed)
throw new PngjException("only rgb or rgba images support this");
return red << 16 | green << 8 | blue;
}
public void setGray(final int g) {
if (!imgInfo.greyscale)
throw new PngjException("only grayscale images support this");
gray = g;
}
public int getGray() {
if (!imgInfo.greyscale)
throw new PngjException("only grayscale images support this");
return gray;
}
/**
* Sets the length of the palette alpha. This should be followed by
* #setNentriesPalAlpha
*
* @param idx
* index inside the table
* @param val
* alpha value (0-255)
*/
public void setEntryPalAlpha(final int idx, final int val) {
paletteAlpha[idx] = val;
}
public void setNentriesPalAlpha(final int len) {
paletteAlpha = new int[len];
}
/**
* WARNING: non deep copy. See also {@link #setNentriesPalAlpha(int)}
* {@link #setEntryPalAlpha(int, int)}
*/
public void setPalAlpha(final int[] palAlpha) {
if (!imgInfo.indexed)
throw new PngjException("only indexed images support this");
paletteAlpha = palAlpha;
}
/**
* WARNING: non deep copy
*/
public int[] getPalletteAlpha() {
return paletteAlpha;
}
/**
* to use when only one pallete index is set as totally transparent
*/
public void setIndexEntryAsTransparent(final int palAlphaIndex) {
if (!imgInfo.indexed)
throw new PngjException("only indexed images support this");
paletteAlpha = new int[] { palAlphaIndex + 1 };
for (int i = 0; i < palAlphaIndex; i++)
paletteAlpha[i] = 255;
paletteAlpha[palAlphaIndex] = 0;
}
}

View File

@ -1,60 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
/**
* Superclass (abstract) for three textual chunks (TEXT, ITXT, ZTXT)
*/
public abstract class PngChunkTextVar extends PngChunkMultiple {
protected String key; // key/val: only for tEXt. lazy computed
protected String val;
// http://www.w3.org/TR/PNG/#11keywords
public final static String KEY_Title = "Title"; // Short (one line) title or caption for image
public final static String KEY_Author = "Author"; // Name of image's creator
public final static String KEY_Description = "Description"; // Description of image (possibly
// long)
public final static String KEY_Copyright = "Copyright"; // Copyright notice
public final static String KEY_Creation_Time = "Creation Time"; // Time of original image creation
public final static String KEY_Software = "Software"; // Software used to create the image
public final static String KEY_Disclaimer = "Disclaimer"; // Legal disclaimer
public final static String KEY_Warning = "Warning"; // Warning of nature of content
public final static String KEY_Source = "Source"; // Device used to create the image
public final static String KEY_Comment = "Comment"; // Miscellaneous comment
protected PngChunkTextVar(final String id, final ImageInfo info) {
super(id, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.NONE;
}
public static class PngTxtInfo {
public String title;
public String author;
public String description;
public String creation_time;// = (new Date()).toString();
public String software;
public String disclaimer;
public String warning;
public String source;
public String comment;
}
public String getKey() {
return key;
}
public String getVal() {
return val;
}
public void setKeyVal(final String key, final String val) {
this.key = key;
this.val = val;
}
}

View File

@ -1,40 +0,0 @@
package ar.com.hjg.pngj.chunks;
import ar.com.hjg.pngj.ImageInfo;
/**
* Placeholder for UNKNOWN (custom or not) chunks.
* <p>
* For PngReader, a chunk is unknown if it's not registered in the chunk factory
*/
public class PngChunkUNKNOWN extends PngChunkMultiple { // unkown, custom or not
public PngChunkUNKNOWN(final String id, final ImageInfo info) {
super(id, info);
}
@Override
public ChunkOrderingConstraint getOrderingConstraint() {
return ChunkOrderingConstraint.NONE;
}
@Override
public ChunkRaw createRawChunk() {
return raw;
}
@Override
public void parseFromRaw(final ChunkRaw c) {
}
/* does not do deep copy! */
public byte[] getData() {
return raw.data;
}
/* does not do deep copy! */
public void setData(final byte[] data) {
raw.data = data;
}
}

View File

@ -1,61 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.PngjException;
/**
* zTXt chunk.
* <p>
* see http://www.w3.org/TR/PNG/#11zTXt
*/
public class PngChunkZTXT extends PngChunkTextVar {
public final static String ID = ChunkHelper.zTXt;
// http://www.w3.org/TR/PNG/#11zTXt
public PngChunkZTXT(final ImageInfo info) {
super(PngChunkZTXT.ID, info);
}
@Override
public ChunkRaw createRawChunk() {
if (key == null || key.trim().length() == 0)
throw new PngjException("Text chunk key must be non empty");
try {
final ByteArrayOutputStream ba = new ByteArrayOutputStream();
ba.write(ChunkHelper.toBytes(key));
ba.write(0); // separator
ba.write(0); // compression method: 0
final byte[] textbytes = ChunkHelper.compressBytes(ChunkHelper.toBytes(val), true);
ba.write(textbytes);
final byte[] b = ba.toByteArray();
final ChunkRaw chunk = createEmptyChunk(b.length, false);
chunk.data = b;
return chunk;
} catch (final IOException e) {
throw new PngjException(e);
}
}
@Override
public void parseFromRaw(final ChunkRaw c) {
int nullsep = -1;
for (int i = 0; i < c.data.length; i++) { // look for first zero
if (c.data[i] != 0)
continue;
nullsep = i;
break;
}
if (nullsep < 0 || nullsep > c.data.length - 2)
throw new PngjException("bad zTXt chunk: no separator found");
key = ChunkHelper.toString(c.data, 0, nullsep);
final int compmet = c.data[nullsep + 1];
if (compmet != 0)
throw new PngjException("bad zTXt chunk: unknown compression method");
final byte[] uncomp = ChunkHelper.compressBytes(c.data, nullsep + 2, c.data.length - nullsep - 2, false); // uncompress
val = ChunkHelper.toString(uncomp);
}
}

View File

@ -1,235 +0,0 @@
package ar.com.hjg.pngj.chunks;
import java.util.ArrayList;
import java.util.List;
import ar.com.hjg.pngj.PngjException;
/**
* We consider "image metadata" every info inside the image except for the most
* basic image info (IHDR chunk - ImageInfo
* class) and the pixels values.
* <p>
* This includes the palette (if present) and all the ancillary chunks
* <p>
* This class provides a wrapper over the collection of chunks of a image (read
* or to write) and provides some high
* level methods to access them
*/
public class PngMetadata {
private final ChunksList chunkList;
private final boolean readonly;
public PngMetadata(final ChunksList chunks) {
chunkList = chunks;
if (chunks instanceof ChunksListForWrite)
readonly = false;
else
readonly = true;
}
/**
* Queues the chunk at the writer
* <p>
* lazyOverwrite: if true, checks if there is a queued "equivalent" chunk
* and if so, overwrites it. However if that
* not check for already written chunks.
*/
public void queueChunk(final PngChunk c, final boolean lazyOverwrite) {
final ChunksListForWrite cl = getChunkListW();
if (readonly)
throw new PngjException("cannot set chunk : readonly metadata");
if (lazyOverwrite)
ChunkHelper.trimList(cl.getQueuedChunks(), c2 -> ChunkHelper.equivalent(c, c2));
cl.queue(c);
}
public void queueChunk(final PngChunk c) {
queueChunk(c, true);
}
private ChunksListForWrite getChunkListW() {
return (ChunksListForWrite) chunkList;
}
// ///// high level utility methods follow ////////////
// //////////// DPI
/**
* returns -1 if not found or dimension unknown
*/
public double[] getDpi() {
final PngChunk c = chunkList.getById1(ChunkHelper.pHYs, true);
if (c == null)
return new double[] { -1, -1 };
else
return ((PngChunkPHYS) c).getAsDpi2();
}
public void setDpi(final double x) {
setDpi(x, x);
}
public void setDpi(final double x, final double y) {
final PngChunkPHYS c = new PngChunkPHYS(chunkList.imageInfo);
c.setAsDpi2(x, y);
queueChunk(c);
}
// //////////// TIME
/**
* Creates a time chunk with current time, less secsAgo seconds
* <p>
*
* @return Returns the created-queued chunk, just in case you want to
* examine or modify it
*/
public PngChunkTIME setTimeNow(final int secsAgo) {
final PngChunkTIME c = new PngChunkTIME(chunkList.imageInfo);
c.setNow(secsAgo);
queueChunk(c);
return c;
}
public PngChunkTIME setTimeNow() {
return setTimeNow(0);
}
/**
* Creates a time chunk with diven date-time
* <p>
*
* @return Returns the created-queued chunk, just in case you want to
* examine or modify it
*/
public PngChunkTIME setTimeYMDHMS(final int yearx, final int monx, final int dayx, final int hourx, final int minx,
final int secx) {
final PngChunkTIME c = new PngChunkTIME(chunkList.imageInfo);
c.setYMDHMS(yearx, monx, dayx, hourx, minx, secx);
queueChunk(c, true);
return c;
}
/**
* null if not found
*/
public PngChunkTIME getTime() {
return (PngChunkTIME) chunkList.getById1(ChunkHelper.tIME);
}
public String getTimeAsString() {
final PngChunkTIME c = getTime();
return c == null ? "" : c.getAsString();
}
// //////////// TEXT
/**
* Creates a text chunk and queue it.
* <p>
*
* @param k
* : key (latin1)
* @param val
* (arbitrary, should be latin1 if useLatin1)
* @param useLatin1
* @param compress
* @return Returns the created-queued chunks, just in case you want to
* examine, touch it
*/
public PngChunkTextVar setText(final String k, final String val, final boolean useLatin1, final boolean compress) {
if (compress && !useLatin1)
throw new PngjException("cannot compress non latin text");
PngChunkTextVar c;
if (useLatin1) {
if (compress)
c = new PngChunkZTXT(chunkList.imageInfo);
else
c = new PngChunkTEXT(chunkList.imageInfo);
} else {
c = new PngChunkITXT(chunkList.imageInfo);
((PngChunkITXT) c).setLangtag(k); // we use the same orig tag (this is not quite right)
}
c.setKeyVal(k, val);
queueChunk(c, true);
return c;
}
public PngChunkTextVar setText(final String k, final String val) {
return setText(k, val, false, false);
}
/**
* gets all text chunks with a given key
* <p>
* returns null if not found
* <p>
* Warning: this does not check the "lang" key of iTxt
*/
@SuppressWarnings("unchecked")
public List<? extends PngChunkTextVar> getTxtsForKey(final String k) {
@SuppressWarnings("rawtypes")
final List c = new ArrayList();
c.addAll(chunkList.getById(ChunkHelper.tEXt, k));
c.addAll(chunkList.getById(ChunkHelper.zTXt, k));
c.addAll(chunkList.getById(ChunkHelper.iTXt, k));
return c;
}
/**
* Returns empty if not found, concatenated (with newlines) if multiple! -
* and trimmed
* <p>
* Use getTxtsForKey() if you don't want this behaviour
*/
public String getTxtForKey(final String k) {
final List<? extends PngChunkTextVar> li = getTxtsForKey(k);
if (li.isEmpty())
return "";
final StringBuilder t = new StringBuilder();
for (final PngChunkTextVar c : li)
t.append(c.getVal()).append("\n");
return t.toString().trim();
}
/**
* Returns the palette chunk, if present
*
* @return null if not present
*/
public PngChunkPLTE getPLTE() {
return (PngChunkPLTE) chunkList.getById1(PngChunkPLTE.ID);
}
/**
* Creates a new empty palette chunk, queues it for write and return it to
* the caller, who should fill its entries
*/
public PngChunkPLTE createPLTEChunk() {
final PngChunkPLTE plte = new PngChunkPLTE(chunkList.imageInfo);
queueChunk(plte);
return plte;
}
/**
* Returns the TRNS chunk, if present
*
* @return null if not present
*/
public PngChunkTRNS getTRNS() {
return (PngChunkTRNS) chunkList.getById1(PngChunkTRNS.ID);
}
/**
* Creates a new empty TRNS chunk, queues it for write and return it to the
* caller, who should fill its entries
*/
public PngChunkTRNS createTRNSChunk() {
final PngChunkTRNS trns = new PngChunkTRNS(chunkList.imageInfo);
queueChunk(trns);
return trns;
}
}

View File

@ -1,9 +0,0 @@
<html>
<body bgcolor="white">
<p>
Contains the code related to chunk management for the PNGJ library.</p>
<p>
Only needed by client code if some special chunk handling is required.
</p>
</body>
</html>

View File

@ -1,49 +0,0 @@
<html>
<body bgcolor="white">
<p>
PNGJ main package
</p>
<p>
Users of this library should rarely need more than the public members of this package.<br>
Newcomers: start with <a href="PngReader.html">PngReader</a> and <a href="PngWriter.html">PngWriter</a>.
</p>
<p>
Example of use: this code reads a true colour PNG image (RGB8 or RGBA8)
and reduces the red channel by half, increasing the green by 20.
It copies all the "safe" metadata from the original image, and adds a textual metadata.
<pre class="code">
public static void convert(String origFilename, String destFilename) {
// you can also use PngReader (esentially the same) or PngReaderByte
PngReaderInt pngr = new PngReaderInt(new File(origFilename));
System.out.println(pngr.toString());
int channels = pngr.imgInfo.channels;
if (channels &lt; 3 || pngr.imgInfo.bitDepth != 8)
throw new RuntimeException("For simplicity this supports only RGB8/RGBA8 images");
// writer with same image properties as original
PngWriter pngw = new PngWriter(new File(destFilename), pngr.imgInfo, true);
// instruct the writer to grab all ancillary chunks from the original
pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_ALL_SAFE);
// add a textual chunk to writer
pngw.getMetadata().setText(PngChunkTextVar.KEY_Description, "Decreased red and increased green");
// also: while(pngr.hasMoreRows())
for (int row = 0; row &lt; pngr.imgInfo.rows; row++) {
ImageLineInt l1 = pngr.readRowInt(); // each element is a sample
int[] scanline = l1.getScanline(); // to save typing
for (int j = 0; j < pngr.imgInfo.cols; j++) {
scanline[j * channels] /= 2;
scanline[j * channels + 1] = ImageLineHelper.clampTo_0_255(scanline[j * channels + 1] + 20);
}
pngw.writeRow(l1);
}
pngr.end(); // it's recommended to end the reader first, in case there are trailing chunks to read
pngw.end();
}
</pre>
For more examples, see the tests and samples.
</p>
</body>
</html>

View File

@ -1,168 +0,0 @@
package ar.com.hjg.pngj.pixels;
import java.io.OutputStream;
import ar.com.hjg.pngj.IDatChunkWriter;
/**
* This is an OutputStream that compresses (via Deflater or a deflater-like
* object), and optionally passes the
* compressed stream to another output stream.
*
* It allows to compute in/out/ratio stats.
*
* It works as a stream (similar to DeflaterOutputStream), but it's peculiar in
* that it expects that each writes has a
* fixed length (other lenghts are accepted, but it's less efficient) and that
* the total amount of bytes is known (so it
* can close itself, but it can also be closed on demand) In PNGJ use, the block
* is typically a row (including filter
* byte).
*
* We use this to do the real compression (with Deflate) but also to compute
* tentative estimators
*
* If not closed, it can be recicled via reset()
*
*
*/
public abstract class CompressorStream extends OutputStream {
protected IDatChunkWriter idatChunkWriter;
public final int blockLen;
public final long totalbytes;
boolean closed = false;
protected boolean done = false;
protected long bytesIn = 0;
protected long bytesOut = 0;
protected int block = -1;
/** optionally stores the first byte of each block (row) */
private byte[] firstBytes;
protected boolean storeFirstByte = false;
/**
*
* @param idatCw
* Can be null (if we are only interested in compute compression
* ratio)
* @param blockLen
* Estimated maximum block length. If unknown, use -1.
* @param totalbytes
* Expected total bytes to be fed. If unknown, use -1.
*/
public CompressorStream(final IDatChunkWriter idatCw, int blockLen, long totalbytes) {
idatChunkWriter = idatCw;
if (blockLen < 0)
blockLen = 4096;
if (totalbytes < 0)
totalbytes = Long.MAX_VALUE;
if (blockLen < 1 || totalbytes < 1)
throw new RuntimeException(" maxBlockLen or totalLen invalid");
this.blockLen = blockLen;
this.totalbytes = totalbytes;
}
/** Releases resources. Idempotent. */
@Override
public void close() {
done();
if (idatChunkWriter != null)
idatChunkWriter.close();
closed = true;
}
/**
* Will be called automatically when the number of bytes reaches the total
* expected Can be also be called from
* outside. This should set the flag done=true
*/
public abstract void done();
@Override
public final void write(final byte[] data) {
write(data, 0, data.length);
}
@Override
public final void write(final byte[] data, int off, int len) {
block++;
if (len <= blockLen) { // normal case
mywrite(data, off, len);
if (storeFirstByte && block < firstBytes.length)
firstBytes[block] = data[off]; // only makes sense in this case
} else
while (len > 0) {
mywrite(data, off, blockLen);
off += blockLen;
len -= blockLen;
}
if (bytesIn >= totalbytes)
done();
}
/**
* same as write, but guarantedd to not exceed blockLen The implementation
* should update bytesOut and bytesInt but not
* check for totalBytes
*/
public abstract void mywrite(byte[] data, int off, int len);
/**
* compressed/raw. This should be called only when done
*/
public final double getCompressionRatio() {
return bytesOut == 0 ? 1.0 : bytesOut / (double) bytesIn;
}
/**
* raw (input) bytes. This should be called only when done
*/
public final long getBytesRaw() {
return bytesIn;
}
/**
* compressed (out) bytes. This should be called only when done
*/
public final long getBytesCompressed() {
return bytesOut;
}
public boolean isClosed() {
return closed;
}
public boolean isDone() {
return done;
}
public byte[] getFirstBytes() {
return firstBytes;
}
public void setStoreFirstByte(final boolean storeFirstByte, final int nblocks) {
this.storeFirstByte = storeFirstByte;
if (this.storeFirstByte) {
if (firstBytes == null || firstBytes.length < nblocks)
firstBytes = new byte[nblocks];
} else
firstBytes = null;
}
public void reset() {
done();
bytesIn = 0;
bytesOut = 0;
block = -1;
done = false;
}
@Override
public void write(final int i) { // should not be used
write(new byte[] { (byte) i });
}
}

Some files were not shown because too many files have changed in this diff Show More