Fixed graphic glitches when changing screen
This commit is contained in:
parent
76d9d77e13
commit
87151ed606
2
Flow
2
Flow
@ -1 +1 @@
|
|||||||
Subproject commit fc24680f8f2178fa550174c75d7dbacb6065358c
|
Subproject commit 59b89e9d165c3fc615ca44aa742b531caf13fcb6
|
@ -44,6 +44,43 @@ public class MarioScreen extends Screen {
|
|||||||
historyBehavior = HistoryBehavior.ALWAYS_KEEP_IN_HISTORY;
|
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
|
@Override
|
||||||
public void initialized() {
|
public void initialized() {
|
||||||
try {
|
try {
|
||||||
|
@ -43,13 +43,17 @@ public class TetrisScreen extends Screen {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialized() {
|
public void initialized() {
|
||||||
|
StaticVars.windowZoom.onNext(2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void graphicInitialized() {
|
||||||
try {
|
try {
|
||||||
e = d.engine;
|
e = d.engine;
|
||||||
r = d.renderer;
|
r = d.renderer;
|
||||||
if (TetrisScreen.skin == null) {
|
if (TetrisScreen.skin == null) {
|
||||||
TetrisScreen.skin = Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine.loadSkin("/tetrisskin.png");
|
TetrisScreen.skin = Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine.loadSkin("/tetrisskin.png");
|
||||||
}
|
}
|
||||||
StaticVars.windowZoom.onNext(2f);
|
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,12 @@ public class CalculatorHUD extends HUD {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void graphicInitialized() throws InterruptedException {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render() {
|
public void render() {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
|
@ -242,10 +242,10 @@ public final class DisplayManager implements RenderingLoop {
|
|||||||
screen.create();
|
screen.create();
|
||||||
}
|
}
|
||||||
this.screen = screen;
|
this.screen = screen;
|
||||||
screenChange.release();
|
|
||||||
if (screen.initialized == false) {
|
if (screen.initialized == false) {
|
||||||
screen.initialize();
|
screen.initialize();
|
||||||
}
|
}
|
||||||
|
screenChange.release();
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
Engine.getPlatform().exit(0);
|
Engine.getPlatform().exit(0);
|
||||||
@ -267,10 +267,10 @@ public final class DisplayManager implements RenderingLoop {
|
|||||||
try {
|
try {
|
||||||
screen.create();
|
screen.create();
|
||||||
this.screen = screen;
|
this.screen = screen;
|
||||||
screenChange.release();
|
|
||||||
if (screen.initialized == false) {
|
if (screen.initialized == false) {
|
||||||
screen.initialize();
|
screen.initialize();
|
||||||
}
|
}
|
||||||
|
screenChange.release();
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
Engine.getPlatform().exit(0);
|
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());
|
renderer.glClear(engine.getWidth(), engine.getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,7 +446,6 @@ public final class DisplayManager implements RenderingLoop {
|
|||||||
setScreen(initialScreen);
|
setScreen(initialScreen);
|
||||||
initialScreen = null;
|
initialScreen = null;
|
||||||
}
|
}
|
||||||
screen.initialize();
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
Engine.getPlatform().exit(0);
|
Engine.getPlatform().exit(0);
|
||||||
|
@ -2,8 +2,10 @@ package it.cavallium.warppi.gui;
|
|||||||
|
|
||||||
public interface GraphicalInterface {
|
public interface GraphicalInterface {
|
||||||
void create() throws InterruptedException;
|
void create() throws InterruptedException;
|
||||||
|
|
||||||
void initialize() throws InterruptedException;
|
void initialize() throws InterruptedException;
|
||||||
|
|
||||||
|
void initializeGraphic() throws InterruptedException;
|
||||||
|
|
||||||
void render();
|
void render();
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package it.cavallium.warppi.gui;
|
|||||||
public abstract class HUD implements GraphicalInterface {
|
public abstract class HUD implements GraphicalInterface {
|
||||||
public DisplayManager d;
|
public DisplayManager d;
|
||||||
public boolean created = false;
|
public boolean created = false;
|
||||||
|
public boolean graphicInitialized = false;
|
||||||
public boolean initialized = false;
|
public boolean initialized = false;
|
||||||
public boolean visible = true;
|
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
|
@Override
|
||||||
public void create() throws InterruptedException {
|
public void create() throws InterruptedException {
|
||||||
if (!created) {
|
if (!created) {
|
||||||
@ -26,6 +35,8 @@ public abstract class HUD implements GraphicalInterface {
|
|||||||
|
|
||||||
public abstract void created() throws InterruptedException;
|
public abstract void created() throws InterruptedException;
|
||||||
|
|
||||||
|
public abstract void graphicInitialized() throws InterruptedException;
|
||||||
|
|
||||||
public abstract void initialized() throws InterruptedException;
|
public abstract void initialized() throws InterruptedException;
|
||||||
|
|
||||||
public abstract void renderBackground();
|
public abstract void renderBackground();
|
||||||
|
@ -13,7 +13,15 @@ public interface Skin {
|
|||||||
|
|
||||||
void use(GraphicEngine d);
|
void use(GraphicEngine d);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* May not be available before initialization
|
||||||
|
* @return skin width
|
||||||
|
*/
|
||||||
int getSkinWidth();
|
int getSkinWidth();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* May not be available before initialization
|
||||||
|
* @return skin height
|
||||||
|
*/
|
||||||
int getSkinHeight();
|
int getSkinHeight();
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,9 @@ public class ChooseVariableValueScreen extends Screen {
|
|||||||
@Override
|
@Override
|
||||||
public void initialized() throws InterruptedException {}
|
public void initialized() throws InterruptedException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void graphicInitialized() throws InterruptedException {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render() {
|
public void render() {
|
||||||
Utils.getFont(false, true).use(Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine);
|
Utils.getFont(false, true).use(Engine.INSTANCE.getHardwareDevice().getDisplayManager().engine);
|
||||||
|
@ -19,6 +19,9 @@ public class EmptyScreen extends Screen {
|
|||||||
@Override
|
@Override
|
||||||
public void initialized() throws InterruptedException {}
|
public void initialized() throws InterruptedException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void graphicInitialized() throws InterruptedException {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render() {
|
public void render() {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
|
@ -27,6 +27,9 @@ public class KeyboardDebugScreen extends Screen {
|
|||||||
@Override
|
@Override
|
||||||
public void initialized() throws InterruptedException {}
|
public void initialized() throws InterruptedException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void graphicInitialized() throws InterruptedException {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render() {
|
public void render() {
|
||||||
final Renderer renderer = Engine.INSTANCE.getHardwareDevice().getDisplayManager().renderer;
|
final Renderer renderer = Engine.INSTANCE.getHardwareDevice().getDisplayManager().renderer;
|
||||||
|
@ -33,6 +33,9 @@ public class LoadingScreen extends Screen {
|
|||||||
Engine.INSTANCE.getHardwareDevice().getDisplayManager().getHUD().hide();
|
Engine.INSTANCE.getHardwareDevice().getDisplayManager().getHUD().hide();
|
||||||
StaticVars.windowZoom.onNext(1f);
|
StaticVars.windowZoom.onNext(1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void graphicInitialized() throws InterruptedException {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeRender(final float dt) {
|
public void beforeRender(final float dt) {
|
||||||
|
@ -105,6 +105,11 @@ public class MathInputScreen extends Screen {
|
|||||||
/* Fine caricamento */
|
/* Fine caricamento */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void graphicInitialized() throws InterruptedException {
|
||||||
|
/* Fine caricamento */
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeRender(final float dt) {
|
public void beforeRender(final float dt) {
|
||||||
if (Engine.INSTANCE.getHardwareDevice().getDisplayManager().error == null) {
|
if (Engine.INSTANCE.getHardwareDevice().getDisplayManager().error == null) {
|
||||||
|
@ -10,6 +10,7 @@ public abstract class Screen implements KeyboardEventListener, TouchEventListene
|
|||||||
public DisplayManager d;
|
public DisplayManager d;
|
||||||
public boolean created = false;
|
public boolean created = false;
|
||||||
public boolean initialized = false;
|
public boolean initialized = false;
|
||||||
|
public boolean graphicInitialized = false;
|
||||||
public HistoryBehavior historyBehavior = HistoryBehavior.NORMAL;
|
public HistoryBehavior historyBehavior = HistoryBehavior.NORMAL;
|
||||||
|
|
||||||
public static long lastDebugScreenID = 1;
|
public static long lastDebugScreenID = 1;
|
||||||
@ -19,6 +20,14 @@ public abstract class Screen implements KeyboardEventListener, TouchEventListene
|
|||||||
debugScreenID = lastDebugScreenID++;
|
debugScreenID = lastDebugScreenID++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeGraphic() throws InterruptedException {
|
||||||
|
if (!graphicInitialized) {
|
||||||
|
graphicInitialized = true;
|
||||||
|
graphicInitialized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() throws InterruptedException {
|
public void initialize() throws InterruptedException {
|
||||||
if (!initialized) {
|
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;
|
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;
|
public abstract void initialized() throws InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load skins, etc...
|
||||||
|
* Called after created()
|
||||||
|
* Called before initialized()
|
||||||
|
* @throws InterruptedException
|
||||||
|
*/
|
||||||
|
public abstract void graphicInitialized() throws InterruptedException;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public abstract void render();
|
public abstract void render();
|
||||||
|
|
||||||
|
@ -23,6 +23,9 @@ public class SolveForXScreen extends Screen {
|
|||||||
@Override
|
@Override
|
||||||
public void initialized() throws InterruptedException {}
|
public void initialized() throws InterruptedException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void graphicInitialized() throws InterruptedException {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render() {
|
public void render() {
|
||||||
Engine.INSTANCE.getHardwareDevice().getDisplayManager().renderer.glColor4i(0, 0, 0, 64);
|
Engine.INSTANCE.getHardwareDevice().getDisplayManager().renderer.glColor4i(0, 0, 0, 64);
|
||||||
|
@ -372,6 +372,7 @@ public class SwingWindow extends JFrame {
|
|||||||
if (renderingLoop != null) {
|
if (renderingLoop != null) {
|
||||||
renderingLoop.refresh();
|
renderingLoop.refresh();
|
||||||
|
|
||||||
|
|
||||||
final int[] a = ((DataBufferInt) display.g.getRaster().getDataBuffer()).getData();
|
final int[] a = ((DataBufferInt) display.g.getRaster().getDataBuffer()).getData();
|
||||||
SwingRenderer.canvas2d = a;
|
SwingRenderer.canvas2d = a;
|
||||||
g.clearRect(0, 0, display.r.size[0] * mult, display.r.size[1] * mult);
|
g.clearRect(0, 0, display.r.size[0] * mult, display.r.size[1] * mult);
|
||||||
|
@ -261,7 +261,7 @@ public class JOGLRenderer implements Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static OpenedTextureData openTexture(final String file, final boolean isResource) throws GLException, IOException {
|
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;
|
File f;
|
||||||
if (isResource) {
|
if (isResource) {
|
||||||
f = Files.createTempFile("texture-", ".png").toFile();
|
f = Files.createTempFile("texture-", ".png").toFile();
|
||||||
|
@ -483,7 +483,6 @@ class NEWTWindow implements GLEventListener {
|
|||||||
|
|
||||||
final boolean linear = windowZoom % (int) windowZoom != 0f;
|
final boolean linear = windowZoom % (int) windowZoom != 0f;
|
||||||
if (refreshViewport) {
|
if (refreshViewport) {
|
||||||
System.err.println("[[[REFVP");
|
|
||||||
refreshViewport = false;
|
refreshViewport = false;
|
||||||
gl.glViewport(0, 0, realWindowSize[0], realWindowSize[1]);
|
gl.glViewport(0, 0, realWindowSize[0], realWindowSize[1]);
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package ar.com.hjg.pngj;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Image Line factory.
|
|
||||||
*/
|
|
||||||
public interface IImageLineFactory<T extends IImageLine> {
|
|
||||||
T createImageLine(ImageInfo iminfo);
|
|
||||||
}
|
|
@ -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();
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package ar.com.hjg.pngj;
|
|
||||||
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public interface IPngWriterFactory {
|
|
||||||
PngWriter createPngWriter(OutputStream outputStream, ImageInfo imgInfo);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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("image.png"));
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -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!
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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() + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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 };
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
|
@ -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 < 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 < 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>
|
|
@ -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
Loading…
Reference in New Issue
Block a user