[FEAT] add removed sources
This commit is contained in:
parent
38cb1e09ed
commit
27033472c9
173
src/main/org/atriasoft/pngencoder/PngEncoder.java
Normal file
173
src/main/org/atriasoft/pngencoder/PngEncoder.java
Normal file
@ -0,0 +1,173 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
import org.atriasoft.egami.Image;
|
||||
|
||||
/**
|
||||
* Main class, containing the interface for PngEncoder.
|
||||
* PngEncoder is a really fast encoder for PNG images in Java.
|
||||
*/
|
||||
public class PngEncoder {
|
||||
/**
|
||||
* Compression level 9 is the default.
|
||||
* It produces images with a size comparable to ImageIO.
|
||||
*/
|
||||
public static int DEFAULT_COMPRESSION_LEVEL = Deflater.BEST_COMPRESSION;
|
||||
|
||||
private final Image bufferedImage;
|
||||
private final int compressionLevel;
|
||||
private final boolean multiThreadedCompressionEnabled;
|
||||
private final PngEncoderPhysicalPixelDimensions physicalPixelDimensions;
|
||||
private final PngEncoderSrgbRenderingIntent srgbRenderingIntent;
|
||||
|
||||
/**
|
||||
* Constructs an empty PngEncoder. Usually combined with methods named with*.
|
||||
*/
|
||||
public PngEncoder() {
|
||||
this(null, PngEncoder.DEFAULT_COMPRESSION_LEVEL, true, null, null);
|
||||
}
|
||||
|
||||
private PngEncoder(final Image bufferedImage, final int compressionLevel, final boolean multiThreadedCompressionEnabled, final PngEncoderSrgbRenderingIntent srgbRenderingIntent,
|
||||
final PngEncoderPhysicalPixelDimensions physicalPixelDimensions) {
|
||||
this.bufferedImage = bufferedImage;
|
||||
this.compressionLevel = PngEncoderVerificationUtil.verifyCompressionLevel(compressionLevel);
|
||||
this.multiThreadedCompressionEnabled = multiThreadedCompressionEnabled;
|
||||
this.srgbRenderingIntent = srgbRenderingIntent;
|
||||
this.physicalPixelDimensions = physicalPixelDimensions;
|
||||
}
|
||||
|
||||
public Image getBufferedImage() {
|
||||
return this.bufferedImage;
|
||||
}
|
||||
|
||||
public int getCompressionLevel() {
|
||||
return this.compressionLevel;
|
||||
}
|
||||
|
||||
public PngEncoderSrgbRenderingIntent getSrgbRenderingIntent() {
|
||||
return this.srgbRenderingIntent;
|
||||
}
|
||||
|
||||
public boolean isMultiThreadedCompressionEnabled() {
|
||||
return this.multiThreadedCompressionEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image and returns data as {@code byte[]}.
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @return encoded data
|
||||
*/
|
||||
public byte[] toBytes() {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(64 * 1024);
|
||||
toStream(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image and saves data into {@code file}.
|
||||
* @param file destination file where the encoded data will be written
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @throws UncheckedIOException instead of IOException
|
||||
* @return number of bytes written
|
||||
*/
|
||||
public int toFile(final File file) {
|
||||
return toFile(file.toPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image and saves data into {@code filePath}.
|
||||
* @param filePath destination file where the encoded data will be written
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @throws UncheckedIOException instead of IOException
|
||||
* @return number of bytes written
|
||||
*/
|
||||
public int toFile(final Path filePath) {
|
||||
try (OutputStream outputStream = Files.newOutputStream(filePath)) {
|
||||
return toStream(outputStream);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image and saves data into {@code fileName}.
|
||||
* @param fileName destination file where the encoded data will be written
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @throws UncheckedIOException instead of IOException
|
||||
* @return number of bytes written
|
||||
*/
|
||||
public int toFile(final String fileName) {
|
||||
return toFile(Paths.get(fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image to outputStream.
|
||||
* @param outputStream destination of the encoded data
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @return number of bytes written
|
||||
*/
|
||||
public int toStream(final OutputStream outputStream) {
|
||||
try {
|
||||
return PngEncoderLogic.encode(this.bufferedImage, outputStream, this.compressionLevel, this.multiThreadedCompressionEnabled, this.srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PngEncoder which has the same configuration as this one except {@code bufferedImage}.
|
||||
* The new PngEncoder will use the provided {@code bufferedImage}.
|
||||
*
|
||||
* @param bufferedImage input image
|
||||
* @return a new PngEncoder
|
||||
*/
|
||||
public PngEncoder withBufferedImage(final Image bufferedImage) {
|
||||
return new PngEncoder(bufferedImage, this.compressionLevel, this.multiThreadedCompressionEnabled, this.srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PngEncoder which has the same configuration as this one except {@code compressionLevel}.
|
||||
* The new PngEncoder will use the provided {@code compressionLevel}.
|
||||
*
|
||||
* @param compressionLevel input image (must be between -1 and 9 inclusive)
|
||||
* @return a new PngEncoder
|
||||
*/
|
||||
public PngEncoder withCompressionLevel(final int compressionLevel) {
|
||||
return new PngEncoder(this.bufferedImage, compressionLevel, this.multiThreadedCompressionEnabled, this.srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PngEncoder which has the same configuration as this one except {@code multiThreadedCompressionEnabled}.
|
||||
* The new PngEncoder will use the provided {@code multiThreadedCompressionEnabled}.
|
||||
*
|
||||
* @param multiThreadedCompressionEnabled when {@code true}, multithreaded compression will be used
|
||||
* @return a new PngEncoder
|
||||
*/
|
||||
public PngEncoder withMultiThreadedCompressionEnabled(final boolean multiThreadedCompressionEnabled) {
|
||||
return new PngEncoder(this.bufferedImage, this.compressionLevel, multiThreadedCompressionEnabled, this.srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
}
|
||||
|
||||
public PngEncoder withPhysicalPixelDimensions(final PngEncoderPhysicalPixelDimensions physicalPixelDimensions) {
|
||||
return new PngEncoder(this.bufferedImage, this.compressionLevel, this.multiThreadedCompressionEnabled, this.srgbRenderingIntent, physicalPixelDimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PngEncoder which has the same configuration as this one except {@code srgbRenderingIntent}.
|
||||
* The new PngEncoder will add an sRGB chunk to the encoded PNG and use the provided {@code srgbRenderingIntent}.
|
||||
*
|
||||
* @param srgbRenderingIntent the rendering intent that should be used when displaying the image
|
||||
* @return a new PngEncoder
|
||||
*/
|
||||
public PngEncoder withSrgbRenderingIntent(final PngEncoderSrgbRenderingIntent srgbRenderingIntent) {
|
||||
return new PngEncoder(this.bufferedImage, this.compressionLevel, this.multiThreadedCompressionEnabled, srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
class PngEncoderCountingOutputStream extends FilterOutputStream {
|
||||
private int count;
|
||||
|
||||
PngEncoderCountingOutputStream(final OutputStream out) {
|
||||
super(Objects.requireNonNull(out, "out"));
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] b, final int off, final int len) throws IOException {
|
||||
this.out.write(b, off, len);
|
||||
this.count += len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final int b) throws IOException {
|
||||
this.out.write(b);
|
||||
this.count++;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.zip.Adler32;
|
||||
|
||||
class PngEncoderDeflaterBuffer {
|
||||
final byte[] bytes;
|
||||
int length;
|
||||
final PngEncoderDeflaterBufferPool pool;
|
||||
|
||||
PngEncoderDeflaterBuffer(final PngEncoderDeflaterBufferPool pool, final int maxLength) {
|
||||
this.pool = Objects.requireNonNull(pool, "pool");
|
||||
this.bytes = new byte[maxLength];
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
long calculateAdler32() {
|
||||
Adler32 adler32 = new Adler32();
|
||||
adler32.update(this.bytes, 0, this.length);
|
||||
return adler32.getValue();
|
||||
}
|
||||
|
||||
void giveBack() {
|
||||
this.pool.giveBack(this);
|
||||
}
|
||||
|
||||
void write(final OutputStream outputStream) throws IOException {
|
||||
outputStream.write(this.bytes, 0, this.length);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
class PngEncoderDeflaterBufferPool {
|
||||
private final int bufferMaxLength;
|
||||
protected final Queue<PngEncoderDeflaterBuffer> buffers;
|
||||
|
||||
PngEncoderDeflaterBufferPool(final int bufferMaxLength) {
|
||||
this.bufferMaxLength = bufferMaxLength;
|
||||
this.buffers = new LinkedList<>();
|
||||
}
|
||||
|
||||
PngEncoderDeflaterBuffer borrow() {
|
||||
PngEncoderDeflaterBuffer buffer = this.buffers.poll();
|
||||
if (buffer == null) {
|
||||
buffer = new PngEncoderDeflaterBuffer(this, this.bufferMaxLength);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public int getBufferMaxLength() {
|
||||
return this.bufferMaxLength;
|
||||
}
|
||||
|
||||
void giveBack(final PngEncoderDeflaterBuffer buffer) {
|
||||
buffer.length = 0;
|
||||
this.buffers.offer(buffer);
|
||||
}
|
||||
|
||||
int size() {
|
||||
return this.buffers.size();
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
class PngEncoderDeflaterExecutorService {
|
||||
private static class Holder {
|
||||
private static final ExecutorService INSTANCE = Executors.newFixedThreadPool(PngEncoderDeflaterExecutorService.NUM_THREADS_IS_AVAILABLE_PROCESSORS,
|
||||
PngEncoderDeflaterExecutorServiceThreadFactory.getInstance());
|
||||
}
|
||||
|
||||
public static int NUM_THREADS_IS_AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
static ExecutorService getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
private PngEncoderDeflaterExecutorService() {}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
class PngEncoderDeflaterExecutorServiceThreadFactory implements ThreadFactory {
|
||||
private static class Holder {
|
||||
private static final PngEncoderDeflaterExecutorServiceThreadFactory INSTANCE = new PngEncoderDeflaterExecutorServiceThreadFactory();
|
||||
}
|
||||
|
||||
static PngEncoderDeflaterExecutorServiceThreadFactory getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
private final AtomicLong counter;
|
||||
private final ThreadFactory defaultThreadFactory;
|
||||
|
||||
PngEncoderDeflaterExecutorServiceThreadFactory() {
|
||||
this.defaultThreadFactory = Executors.defaultThreadFactory();
|
||||
this.counter = new AtomicLong(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(final Runnable runnable) {
|
||||
Thread thread = this.defaultThreadFactory.newThread(runnable);
|
||||
thread.setName("PngEncoder Deflater (" + this.counter.getAndIncrement() + ")");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
}
|
@ -0,0 +1,206 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
// https://tools.ietf.org/html/rfc1950
|
||||
// https://stackoverflow.com/questions/9050260/what-does-a-zlib-header-look-like
|
||||
// https://www.euccas.me/zlib/
|
||||
// https://stackoverflow.com/questions/13132136/java-multithreaded-compression-with-deflater
|
||||
class PngEncoderDeflaterOutputStream extends FilterOutputStream {
|
||||
// The maximum amount of queued tasks.
|
||||
// Multiplied because some segments compress faster than others.
|
||||
// A value of 3 seems to keep all threads busy.
|
||||
static final int COUNT_MAX_QUEUED_TASKS = PngEncoderDeflaterExecutorService.NUM_THREADS_IS_AVAILABLE_PROCESSORS * 3;
|
||||
|
||||
// Enforces writing to underlying stream in main thread.
|
||||
// Multiplied so that not all work is finished before flush to underlying stream.
|
||||
static final int COUNT_MAX_TOTAL_SEGMENTS = PngEncoderDeflaterOutputStream.COUNT_MAX_QUEUED_TASKS * 3;
|
||||
|
||||
// The maximum dictionary size according to the deflate specification.
|
||||
// A segment max length lower than this would not allow for future use of dictionary.
|
||||
// Used for unit test sanity checking.
|
||||
static final int SEGMENT_MAX_LENGTH_DICTIONARY = 32 * 1024;
|
||||
|
||||
// Our minimum segment length.
|
||||
// Corresponds to about 2% size overhead.
|
||||
// A lower value would better parallelize images but increase the size overhead.
|
||||
static final int SEGMENT_MAX_LENGTH_ORIGINAL_MIN = 128 * 1024;
|
||||
|
||||
static byte getFlg(final int compressionLevel) {
|
||||
if (compressionLevel == -1 || compressionLevel == 6) {
|
||||
return (byte) 0x9C;
|
||||
}
|
||||
|
||||
if (compressionLevel >= 0 && compressionLevel <= 1) {
|
||||
return (byte) 0x01;
|
||||
}
|
||||
|
||||
if (compressionLevel >= 2 && compressionLevel <= 5) {
|
||||
return (byte) 0x5E;
|
||||
}
|
||||
|
||||
if (compressionLevel >= 7 && compressionLevel <= 9) {
|
||||
return (byte) 0xDA;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Invalid compressionLevel: " + compressionLevel);
|
||||
}
|
||||
|
||||
public static int getSegmentMaxLengthDeflated(final int segmentMaxLengthOriginal) {
|
||||
return segmentMaxLengthOriginal + (segmentMaxLengthOriginal >> 3);
|
||||
}
|
||||
|
||||
public static int getSegmentMaxLengthOriginal(final int totalOriginalBytesLength) {
|
||||
return Math.max(totalOriginalBytesLength / PngEncoderDeflaterOutputStream.COUNT_MAX_TOTAL_SEGMENTS, PngEncoderDeflaterOutputStream.SEGMENT_MAX_LENGTH_ORIGINAL_MIN);
|
||||
}
|
||||
|
||||
static void writeDeflateHeader(final OutputStream outputStream, final int compressionLevel) throws IOException {
|
||||
// Write "CMF"
|
||||
// " ... In practice, this means the first byte is almost always 78 (hex) ..."
|
||||
outputStream.write(0x78);
|
||||
|
||||
// Write "FLG"
|
||||
byte flg = PngEncoderDeflaterOutputStream.getFlg(compressionLevel);
|
||||
outputStream.write(flg);
|
||||
}
|
||||
|
||||
private long adler32;
|
||||
private boolean closed;
|
||||
private final int compressionLevel;
|
||||
private boolean finished;
|
||||
private PngEncoderDeflaterBuffer originalSegment;
|
||||
private final PngEncoderDeflaterBufferPool pool;
|
||||
private final ConcurrentLinkedQueue<CompletableFuture<PngEncoderDeflaterSegmentResult>> resultQueue;
|
||||
|
||||
private final int segmentMaxLengthOriginal;
|
||||
|
||||
private final byte[] singleByte;
|
||||
|
||||
PngEncoderDeflaterOutputStream(final OutputStream out, final int compressionLevel, final int segmentMaxLengthOriginal) throws IOException {
|
||||
this(out, compressionLevel, segmentMaxLengthOriginal, new PngEncoderDeflaterBufferPool(PngEncoderDeflaterOutputStream.getSegmentMaxLengthDeflated(segmentMaxLengthOriginal)));
|
||||
}
|
||||
|
||||
PngEncoderDeflaterOutputStream(final OutputStream out, final int compressionLevel, final int segmentMaxLengthOriginal, final PngEncoderDeflaterBufferPool pool) throws IOException {
|
||||
super(Objects.requireNonNull(out, "out"));
|
||||
this.pool = Objects.requireNonNull(pool, "pool");
|
||||
this.singleByte = new byte[1];
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.segmentMaxLengthOriginal = segmentMaxLengthOriginal;
|
||||
this.resultQueue = new ConcurrentLinkedQueue<>();
|
||||
this.originalSegment = pool.borrow();
|
||||
this.adler32 = 1;
|
||||
this.finished = false;
|
||||
this.closed = false;
|
||||
if (pool.getBufferMaxLength() != PngEncoderDeflaterOutputStream.getSegmentMaxLengthDeflated(segmentMaxLengthOriginal)) {
|
||||
throw new IllegalArgumentException("Mismatch between segmentMaxLengthOriginal and pool.");
|
||||
}
|
||||
PngEncoderDeflaterOutputStream.writeDeflateHeader(out, compressionLevel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
finish();
|
||||
super.close();
|
||||
}
|
||||
|
||||
public void finish() throws IOException {
|
||||
if (this.finished) {
|
||||
return;
|
||||
}
|
||||
this.finished = true;
|
||||
try {
|
||||
submitTask(true);
|
||||
joinUntilMaximumQueueSize(0);
|
||||
this.out.write(ByteBuffer.allocate(4).putInt((int) this.adler32).array());
|
||||
this.out.flush();
|
||||
} finally {
|
||||
this.originalSegment.giveBack();
|
||||
}
|
||||
}
|
||||
|
||||
void joinOne() throws IOException {
|
||||
CompletableFuture<PngEncoderDeflaterSegmentResult> resultFuture = this.resultQueue.poll();
|
||||
if (resultFuture != null) {
|
||||
final PngEncoderDeflaterSegmentResult result;
|
||||
try {
|
||||
result = resultFuture.join();
|
||||
} catch (RuntimeException e) {
|
||||
throw new IOException("An async segment task failed.", e);
|
||||
}
|
||||
try {
|
||||
this.adler32 = result.getUpdatedAdler32(this.adler32);
|
||||
result.getDeflatedSegment().write(this.out);
|
||||
} finally {
|
||||
result.getOriginalSegment().giveBack();
|
||||
result.getDeflatedSegment().giveBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void joinUntilMaximumQueueSize(final int maximumResultQueueSize) throws IOException {
|
||||
while (this.resultQueue.size() > maximumResultQueueSize) {
|
||||
joinOne();
|
||||
}
|
||||
}
|
||||
|
||||
void submitTask(final boolean lastSegment) {
|
||||
final PngEncoderDeflaterBuffer deflatedSegment = this.pool.borrow();
|
||||
final PngEncoderDeflaterSegmentTask task = new PngEncoderDeflaterSegmentTask(this.originalSegment, deflatedSegment, this.compressionLevel, lastSegment);
|
||||
submitTask(task);
|
||||
this.originalSegment = this.pool.borrow();
|
||||
}
|
||||
|
||||
void submitTask(final PngEncoderDeflaterSegmentTask task) {
|
||||
CompletableFuture<PngEncoderDeflaterSegmentResult> future = CompletableFuture.supplyAsync(task, PngEncoderDeflaterExecutorService.getInstance());
|
||||
this.resultQueue.offer(future);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] b, int off, int len) throws IOException {
|
||||
if (this.finished) {
|
||||
throw new IOException("write beyond end of stream");
|
||||
}
|
||||
if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (len > 0) {
|
||||
int freeBufCount = this.segmentMaxLengthOriginal - this.originalSegment.length;
|
||||
if (freeBufCount == 0) {
|
||||
// Submit task if the buffer is full and there still is more to write.
|
||||
joinUntilMaximumQueueSize(PngEncoderDeflaterOutputStream.COUNT_MAX_QUEUED_TASKS - 1);
|
||||
submitTask(false);
|
||||
} else {
|
||||
int toCopyCount = Math.min(len, freeBufCount);
|
||||
System.arraycopy(b, off, this.originalSegment.bytes, this.originalSegment.length, toCopyCount);
|
||||
this.originalSegment.length += toCopyCount;
|
||||
off += toCopyCount;
|
||||
len -= toCopyCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final int b) throws IOException {
|
||||
this.singleByte[0] = (byte) (b & 0xff);
|
||||
write(this.singleByte, 0, 1);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
class PngEncoderDeflaterSegmentResult {
|
||||
// https://github.com/madler/zlib/blob/master/adler32.c#L143
|
||||
static long combine(final long adler1, final long adler2, final long len2) {
|
||||
long BASEL = 65521;
|
||||
long sum1;
|
||||
long sum2;
|
||||
long rem;
|
||||
|
||||
rem = len2 % BASEL;
|
||||
sum1 = adler1 & 0xffffL;
|
||||
sum2 = rem * sum1;
|
||||
sum2 %= BASEL;
|
||||
sum1 += (adler2 & 0xffffL) + BASEL - 1;
|
||||
sum2 += ((adler1 >> 16) & 0xffffL) + ((adler2 >> 16) & 0xffffL) + BASEL - rem;
|
||||
if (sum1 >= BASEL) {
|
||||
sum1 -= BASEL;
|
||||
}
|
||||
if (sum1 >= BASEL) {
|
||||
sum1 -= BASEL;
|
||||
}
|
||||
if (sum2 >= (BASEL << 1)) {
|
||||
sum2 -= (BASEL << 1);
|
||||
}
|
||||
if (sum2 >= BASEL) {
|
||||
sum2 -= BASEL;
|
||||
}
|
||||
return sum1 | (sum2 << 16);
|
||||
}
|
||||
|
||||
private final PngEncoderDeflaterBuffer deflatedSegment;
|
||||
private final PngEncoderDeflaterBuffer originalSegment;
|
||||
private final long originalSegmentAdler32;
|
||||
|
||||
private final int originalSegmentLength;
|
||||
|
||||
PngEncoderDeflaterSegmentResult(final PngEncoderDeflaterBuffer originalSegment, final PngEncoderDeflaterBuffer deflatedSegment, final long originalSegmentAdler32,
|
||||
final int originalSegmentLength) {
|
||||
this.originalSegment = Objects.requireNonNull(originalSegment, "originalSegment");
|
||||
this.deflatedSegment = Objects.requireNonNull(deflatedSegment, "deflatedSegment");
|
||||
this.originalSegmentAdler32 = originalSegmentAdler32;
|
||||
this.originalSegmentLength = originalSegmentLength;
|
||||
}
|
||||
|
||||
public PngEncoderDeflaterBuffer getDeflatedSegment() {
|
||||
return this.deflatedSegment;
|
||||
}
|
||||
|
||||
public PngEncoderDeflaterBuffer getOriginalSegment() {
|
||||
return this.originalSegment;
|
||||
}
|
||||
|
||||
long getUpdatedAdler32(final long originalAdler32) {
|
||||
return PngEncoderDeflaterSegmentResult.combine(originalAdler32, this.originalSegmentAdler32, this.originalSegmentLength);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
class PngEncoderDeflaterSegmentTask implements Supplier<PngEncoderDeflaterSegmentResult> {
|
||||
static void deflate(final PngEncoderDeflaterBuffer originalSegment, final PngEncoderDeflaterBuffer deflatedSegment, final int compressionLevel, final boolean lastSegment) {
|
||||
final Deflater deflater = PngEncoderDeflaterThreadLocalDeflater.getInstance(compressionLevel);
|
||||
deflater.setInput(originalSegment.bytes, 0, originalSegment.length);
|
||||
|
||||
if (lastSegment) {
|
||||
deflater.finish();
|
||||
}
|
||||
|
||||
deflatedSegment.length = deflater.deflate(deflatedSegment.bytes, 0, deflatedSegment.bytes.length, lastSegment ? Deflater.NO_FLUSH : Deflater.SYNC_FLUSH);
|
||||
}
|
||||
|
||||
private final int compressionLevel;
|
||||
private final PngEncoderDeflaterBuffer deflatedSegment;
|
||||
private final boolean lastSegment;
|
||||
|
||||
private final PngEncoderDeflaterBuffer originalSegment;
|
||||
|
||||
public PngEncoderDeflaterSegmentTask(final PngEncoderDeflaterBuffer originalSegment, final PngEncoderDeflaterBuffer deflatedSegment, final int compressionLevel, final boolean lastSegment) {
|
||||
this.originalSegment = Objects.requireNonNull(originalSegment, "originalSegment");
|
||||
this.deflatedSegment = Objects.requireNonNull(deflatedSegment, "deflatedSegment");
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.lastSegment = lastSegment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PngEncoderDeflaterSegmentResult get() {
|
||||
final long originalSegmentAdler32 = this.originalSegment.calculateAdler32();
|
||||
final int originalSegmentLength = this.originalSegment.length;
|
||||
|
||||
PngEncoderDeflaterSegmentTask.deflate(this.originalSegment, this.deflatedSegment, this.compressionLevel, this.lastSegment);
|
||||
|
||||
return new PngEncoderDeflaterSegmentResult(this.originalSegment, this.deflatedSegment, originalSegmentAdler32, originalSegmentLength);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
/**
|
||||
* We save time by allocating and reusing some thread local state.
|
||||
*
|
||||
* Creating a new Deflater instance takes a surprising amount of time.
|
||||
* Resetting an existing Deflater instance is almost free though.
|
||||
*/
|
||||
class PngEncoderDeflaterThreadLocalDeflater {
|
||||
private static final ThreadLocal<PngEncoderDeflaterThreadLocalDeflater> THREAD_LOCAL = ThreadLocal.withInitial(PngEncoderDeflaterThreadLocalDeflater::new);
|
||||
|
||||
static Deflater getInstance(final int compressionLevel) {
|
||||
return PngEncoderDeflaterThreadLocalDeflater.THREAD_LOCAL.get().getDeflater(compressionLevel);
|
||||
}
|
||||
|
||||
private final Deflater[] deflaters;
|
||||
|
||||
private PngEncoderDeflaterThreadLocalDeflater() {
|
||||
this.deflaters = new Deflater[11];
|
||||
for (int compressionLevel = -1; compressionLevel <= 9; compressionLevel++) {
|
||||
boolean nowrap = true;
|
||||
this.deflaters[compressionLevel + 1] = new Deflater(compressionLevel, nowrap);
|
||||
}
|
||||
}
|
||||
|
||||
private Deflater getDeflater(final int compressionLevel) {
|
||||
Deflater deflater = this.deflaters[compressionLevel + 1];
|
||||
deflater.reset();
|
||||
return deflater;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
class PngEncoderIdatChunksOutputStream extends FilterOutputStream {
|
||||
// An IDAT chunk adds 12 bytes of overhead to the data within.
|
||||
// 12 / (32 * 1024) = 0.00037 meaning the size overhead is just 0.037% which should be negligible.
|
||||
static final int DEFAULT_BUFFER_LENGTH = 32 * 1024;
|
||||
|
||||
static final byte[] IDAT_BYTES = "IDAT".getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
private final byte[] buf;
|
||||
private int count;
|
||||
private final CRC32 crc;
|
||||
|
||||
PngEncoderIdatChunksOutputStream(final OutputStream out) {
|
||||
this(out, PngEncoderIdatChunksOutputStream.DEFAULT_BUFFER_LENGTH);
|
||||
}
|
||||
|
||||
PngEncoderIdatChunksOutputStream(final OutputStream out, final int bufferLength) {
|
||||
super(out);
|
||||
this.crc = new CRC32();
|
||||
this.buf = new byte[bufferLength];
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
flushBuffer();
|
||||
super.flush();
|
||||
}
|
||||
|
||||
private void flushBuffer() throws IOException {
|
||||
if (this.count > 0) {
|
||||
writeIdatChunk(this.buf, 0, this.count);
|
||||
this.count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] b, final int off, final int len) throws IOException {
|
||||
if (len >= this.buf.length) {
|
||||
flushBuffer();
|
||||
writeIdatChunk(b, off, len);
|
||||
return;
|
||||
}
|
||||
if (len > this.buf.length - this.count) {
|
||||
flushBuffer();
|
||||
}
|
||||
System.arraycopy(b, off, this.buf, this.count, len);
|
||||
this.count += len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final int b) throws IOException {
|
||||
if (this.count >= this.buf.length) {
|
||||
flushBuffer();
|
||||
}
|
||||
this.buf[this.count++] = (byte) b;
|
||||
}
|
||||
|
||||
private void writeIdatChunk(final byte[] b, final int off, final int len) throws IOException {
|
||||
writeInt(len);
|
||||
this.out.write(PngEncoderIdatChunksOutputStream.IDAT_BYTES);
|
||||
this.out.write(b, off, len);
|
||||
this.crc.reset();
|
||||
this.crc.update(PngEncoderIdatChunksOutputStream.IDAT_BYTES);
|
||||
this.crc.update(b, off, len);
|
||||
writeInt((int) this.crc.getValue());
|
||||
}
|
||||
|
||||
private void writeInt(final int i) throws IOException {
|
||||
this.out.write((byte) (i >> 24) & 0xFF);
|
||||
this.out.write((byte) (i >> 16) & 0xFF);
|
||||
this.out.write((byte) (i >> 8) & 0xFF);
|
||||
this.out.write((byte) i & 0xFF);
|
||||
}
|
||||
}
|
140
src/main/org/atriasoft/pngencoder/PngEncoderLogic.java
Normal file
140
src/main/org/atriasoft/pngencoder/PngEncoderLogic.java
Normal file
@ -0,0 +1,140 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
import org.atriasoft.egami.ToolImage;
|
||||
import org.atriasoft.egami.Image;
|
||||
|
||||
class PngEncoderLogic {
|
||||
public static final byte[] CHRM_SRGB_VALUE = ByteBuffer.allocate(8 * 4).putInt(31270).putInt(32900).putInt(64000).putInt(33000).putInt(30000).putInt(60000).putInt(15000).putInt(6000).array();
|
||||
|
||||
// In hex: 89 50 4E 47 0D 0A 1A 0A
|
||||
// This is the "file beginning" aka "header" aka "signature" aka "magicnumber".
|
||||
// https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header
|
||||
// http://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.2
|
||||
// All PNGs start this way and it does not include any pixel format info.
|
||||
static final byte[] FILE_BEGINNING = { -119, 80, 78, 71, 13, 10, 26, 10 };
|
||||
|
||||
// In hex: 00 00 00 00 49 45 4E 44 AE 42 60 82
|
||||
// This is the "file ending"
|
||||
static final byte[] FILE_ENDING = { 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126 };
|
||||
// Default values for the gAMA and cHRM chunks when an sRGB chunk is used,
|
||||
// as specified at http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.sRGB
|
||||
// "An application that writes the sRGB chunk should also write a gAMA chunk (and perhaps a cHRM chunk)
|
||||
// for compatibility with applications that do not use the sRGB chunk.
|
||||
// In this situation, only the following values may be used:
|
||||
// ..."
|
||||
public static final byte[] GAMA_SRGB_VALUE = ByteBuffer.allocate(4).putInt(45455).array();
|
||||
static final byte IHDR_BIT_DEPTH = 8;
|
||||
static final byte IHDR_COLOR_TYPE_RGB = 2;
|
||||
static final byte IHDR_COLOR_TYPE_RGBA = 6;
|
||||
static final byte IHDR_COMPRESSION_METHOD = 0;
|
||||
|
||||
static final byte IHDR_FILTER_METHOD = 0;
|
||||
static final byte IHDR_INTERLACE_METHOD = 0;
|
||||
|
||||
static byte[] asChunk(final String type, final byte[] data) {
|
||||
PngEncoderVerificationUtil.verifyChunkType(type);
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(data.length + 12);
|
||||
byteBuffer.putInt(data.length);
|
||||
ByteBuffer byteBufferForCrc = byteBuffer.slice().asReadOnlyBuffer();
|
||||
byteBufferForCrc.limit(4 + data.length);
|
||||
byteBuffer.put(type.getBytes(StandardCharsets.US_ASCII));
|
||||
byteBuffer.put(data);
|
||||
byteBuffer.putInt(PngEncoderLogic.getCrc32(byteBufferForCrc));
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
static int encode(final Image bufferedImage, final OutputStream outputStream, final int compressionLevel, final boolean multiThreadedCompressionEnabled,
|
||||
final PngEncoderSrgbRenderingIntent srgbRenderingIntent, final PngEncoderPhysicalPixelDimensions physicalPixelDimensions) throws IOException {
|
||||
Objects.requireNonNull(bufferedImage, "bufferedImage");
|
||||
Objects.requireNonNull(outputStream, "outputStream");
|
||||
|
||||
final boolean alpha = bufferedImage.hasAlpha();
|
||||
final int width = bufferedImage.getWidth();
|
||||
final int height = bufferedImage.getHeight();
|
||||
final PngEncoderCountingOutputStream countingOutputStream = new PngEncoderCountingOutputStream(outputStream);
|
||||
|
||||
countingOutputStream.write(PngEncoderLogic.FILE_BEGINNING);
|
||||
|
||||
final byte[] ihdr = PngEncoderLogic.getIhdrHeader(width, height, alpha);
|
||||
final byte[] ihdrChunk = PngEncoderLogic.asChunk("IHDR", ihdr);
|
||||
countingOutputStream.write(ihdrChunk);
|
||||
|
||||
if (srgbRenderingIntent != null) {
|
||||
outputStream.write(PngEncoderLogic.asChunk("sRGB", new byte[] { srgbRenderingIntent.getValue() }));
|
||||
outputStream.write(PngEncoderLogic.asChunk("gAMA", PngEncoderLogic.GAMA_SRGB_VALUE));
|
||||
outputStream.write(PngEncoderLogic.asChunk("cHRM", PngEncoderLogic.CHRM_SRGB_VALUE));
|
||||
}
|
||||
|
||||
if (physicalPixelDimensions != null) {
|
||||
outputStream.write(PngEncoderLogic.asChunk("pHYs", PngEncoderLogic.getPhysicalPixelDimensions(physicalPixelDimensions)));
|
||||
}
|
||||
|
||||
PngEncoderIdatChunksOutputStream idatChunksOutputStream = new PngEncoderIdatChunksOutputStream(countingOutputStream);
|
||||
final byte[] scanlineBytes;
|
||||
|
||||
if (bufferedImage.hasAlpha()) {
|
||||
scanlineBytes = ToolImage.pngConvertInByteBufferRGBA(bufferedImage);
|
||||
} else {
|
||||
scanlineBytes = ToolImage.pngConvertInByteBufferRGB(bufferedImage);
|
||||
}
|
||||
|
||||
final int segmentMaxLengthOriginal = PngEncoderDeflaterOutputStream.getSegmentMaxLengthOriginal(scanlineBytes.length);
|
||||
|
||||
if (scanlineBytes.length <= segmentMaxLengthOriginal || !multiThreadedCompressionEnabled) {
|
||||
Deflater deflater = new Deflater(compressionLevel);
|
||||
DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(idatChunksOutputStream, deflater);
|
||||
deflaterOutputStream.write(scanlineBytes);
|
||||
deflaterOutputStream.finish();
|
||||
deflaterOutputStream.flush();
|
||||
//deflaterOutputStream.close();
|
||||
} else {
|
||||
PngEncoderDeflaterOutputStream deflaterOutputStream = new PngEncoderDeflaterOutputStream(idatChunksOutputStream, compressionLevel, segmentMaxLengthOriginal);
|
||||
deflaterOutputStream.write(scanlineBytes);
|
||||
deflaterOutputStream.finish();
|
||||
//deflaterOutputStream.close();
|
||||
}
|
||||
|
||||
countingOutputStream.write(PngEncoderLogic.FILE_ENDING);
|
||||
|
||||
countingOutputStream.flush();
|
||||
|
||||
return countingOutputStream.getCount();
|
||||
}
|
||||
|
||||
static int getCrc32(final ByteBuffer byteBuffer) {
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(byteBuffer);
|
||||
return (int) crc.getValue();
|
||||
}
|
||||
|
||||
static byte[] getIhdrHeader(final int width, final int height, final boolean alpha) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(13);
|
||||
buffer.putInt(width);
|
||||
buffer.putInt(height);
|
||||
buffer.put(PngEncoderLogic.IHDR_BIT_DEPTH);
|
||||
buffer.put(alpha ? PngEncoderLogic.IHDR_COLOR_TYPE_RGBA : PngEncoderLogic.IHDR_COLOR_TYPE_RGB);
|
||||
buffer.put(PngEncoderLogic.IHDR_COMPRESSION_METHOD);
|
||||
buffer.put(PngEncoderLogic.IHDR_FILTER_METHOD);
|
||||
buffer.put(PngEncoderLogic.IHDR_INTERLACE_METHOD);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
static byte[] getPhysicalPixelDimensions(final PngEncoderPhysicalPixelDimensions physicalPixelDimensions) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(9);
|
||||
buffer.putInt(physicalPixelDimensions.getPixelsPerUnitX());
|
||||
buffer.putInt(physicalPixelDimensions.getPixelsPerUnitY());
|
||||
buffer.put(physicalPixelDimensions.getUnit().getValue());
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
private PngEncoderLogic() {}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
/**
|
||||
* Represents PNG physical pixel dimensions
|
||||
*
|
||||
* Use one of the static methods to create physical pixel dimensions based
|
||||
* on pixels per meter, dots per inch or a unit-less aspect ratio.
|
||||
*
|
||||
* @see <a href="https://www.w3.org/TR/PNG/#11pHYs">https://www.w3.org/TR/PNG/#11pHYs</a>
|
||||
*/
|
||||
public class PngEncoderPhysicalPixelDimensions {
|
||||
|
||||
public enum Unit {
|
||||
METER((byte) 1), UNKNOWN((byte) 0);
|
||||
|
||||
private final byte value;
|
||||
|
||||
Unit(final byte value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
private static final float INCHES_PER_METER = 100 / 2.54f;
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions that only specifies the aspect ratio,
|
||||
* but not the size, of the pixels
|
||||
*
|
||||
* @param pixelsPerUnitX the number of pixels per unit in the horizontal dimension
|
||||
* @param pixelsPerUnitY the number of pixels per unit in the vertical dimension
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions aspectRatio(final int pixelsPerUnitX, final int pixelsPerUnitY) {
|
||||
return new PngEncoderPhysicalPixelDimensions(pixelsPerUnitX, pixelsPerUnitY, Unit.UNKNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions with square pixels
|
||||
* with a size specified in dots per inch
|
||||
*
|
||||
* Note that dots per inch (DPI) cannot be exactly represented by the PNG format's
|
||||
* integer value for pixels per meter. There will be a slight rounding error.
|
||||
*
|
||||
* @param dotsPerInch the DPI value for both dimensions
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions dotsPerInch(final int dotsPerInch) {
|
||||
return PngEncoderPhysicalPixelDimensions.dotsPerInch(dotsPerInch, dotsPerInch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions with possibly non-square pixels
|
||||
* with a size specified in dots per inch
|
||||
*
|
||||
* Note that dots per inch (DPI) cannot be exactly represented by the PNG format's
|
||||
* integer value for pixels per meter. There will be a slight rounding error.
|
||||
*
|
||||
* @param dotsPerInchX the DPI value for the horizontal dimension
|
||||
* @param dotsPerInchY the DPI value for the vertical dimension
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions dotsPerInch(final int dotsPerInchX, final int dotsPerInchY) {
|
||||
int pixelsPerMeterX = Math.round(dotsPerInchX * PngEncoderPhysicalPixelDimensions.INCHES_PER_METER);
|
||||
int pixelsPerMeterY = Math.round(dotsPerInchY * PngEncoderPhysicalPixelDimensions.INCHES_PER_METER);
|
||||
|
||||
return new PngEncoderPhysicalPixelDimensions(pixelsPerMeterX, pixelsPerMeterY, Unit.METER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions with square pixels
|
||||
* with a size specified in pixels per meter
|
||||
*
|
||||
* @param pixelsPerMeter the pixels per meter value for both dimensions
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions pixelsPerMeter(final int pixelsPerMeter) {
|
||||
return PngEncoderPhysicalPixelDimensions.pixelsPerMeter(pixelsPerMeter, pixelsPerMeter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions with possibly non-square pixels
|
||||
* with a size specified in pixels per meter
|
||||
*
|
||||
* @param pixelsPerMeterX the pixels per meter value for the horizontal dimension
|
||||
* @param pixelsPerMeterY the pixels per meter value for the vertical dimension
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions pixelsPerMeter(final int pixelsPerMeterX, final int pixelsPerMeterY) {
|
||||
return new PngEncoderPhysicalPixelDimensions(pixelsPerMeterX, pixelsPerMeterY, Unit.METER);
|
||||
}
|
||||
|
||||
private final int pixelsPerUnitX;
|
||||
|
||||
private final int pixelsPerUnitY;
|
||||
|
||||
private final Unit unit;
|
||||
|
||||
private PngEncoderPhysicalPixelDimensions(final int pixelsPerUnitX, final int pixelsPerUnitY, final Unit unit) {
|
||||
this.pixelsPerUnitX = pixelsPerUnitX;
|
||||
this.pixelsPerUnitY = pixelsPerUnitY;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of pixels per unit in the horizontal dimension
|
||||
*/
|
||||
public int getPixelsPerUnitX() {
|
||||
return this.pixelsPerUnitX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of pixels per unit in the vertical dimension
|
||||
*/
|
||||
public int getPixelsPerUnitY() {
|
||||
return this.pixelsPerUnitY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the unit of the pixel size (either {@link Unit#METER} or {@link Unit#UNKNOWN})
|
||||
*/
|
||||
public Unit getUnit() {
|
||||
return this.unit;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
public enum PngEncoderSrgbRenderingIntent {
|
||||
ABSOLUTE_COLORIMETRIC((byte) 3), PERCEPTUAL((byte) 0), RELATIVE_COLORIMETRIC((byte) 1), SATURATION((byte) 2);
|
||||
|
||||
private final byte value;
|
||||
|
||||
PngEncoderSrgbRenderingIntent(final byte value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
class PngEncoderVerificationUtil {
|
||||
static String verifyChunkType(final String chunkType) {
|
||||
if (chunkType.length() != 4) {
|
||||
String message = String.format("The chunkType must be four letters, but was \"%s\". See http://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.1", chunkType);
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
return chunkType;
|
||||
}
|
||||
|
||||
static int verifyCompressionLevel(final int compressionLevel) {
|
||||
if ((compressionLevel < -1) || (compressionLevel > 9)) {
|
||||
String message = String.format("The compressionLevel must be between -1 and 9 inclusive, but was %d.", compressionLevel);
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
return compressionLevel;
|
||||
}
|
||||
|
||||
private PngEncoderVerificationUtil() {}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user