[DEV] add code and remove dependency of java.desktop

This commit is contained in:
Edouard DUPIN 2021-05-03 16:39:31 +02:00
parent 69301848cd
commit 5319b12a9d
42 changed files with 3120 additions and 0 deletions

230
.gitignore vendored Normal file
View File

@ -0,0 +1,230 @@
# IntelliJ project file
*.iml
# Java Flight Recorder
*.jfr
#################### Language ####################
# Language general ignores.
### Maven
# https://github.com/github/gitignore/blob/master/Maven.gitignore
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
#################### IDE ####################
# IDE general ignores.
### Eclipse
# https://github.com/github/gitignore/blob/master/Global/Eclipse.gitignore
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# Eclipse Core
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# JDT-specific (Eclipse Java Development Tools)
.classpath
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
### JetBrains
# https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### SublimeText
# https://github.com/github/gitignore/blob/master/Global/SublimeText.gitignore
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
#################### OS ####################
# Operating system general ignores.
### https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### https://github.com/github/gitignore/blob/master/Global/Linux.gitignore
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
### https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
#################### OTHER ####################
# Other general ignores.
### Dropbox
# https://github.com/github/gitignore/blob/master/Global/Dropbox.gitignore
# Dropbox settings and caches
.dropbox
.dropbox.attr
.dropbox.cache

11
.travis.yml Normal file
View File

@ -0,0 +1,11 @@
sudo: false
language: java
script: mvn clean verify -P coverage
jdk:
- openjdk8
after_success:
- bash <(curl -s https://codecov.io/bash)
cache:
timeout: 1000
directories:
- $HOME/.m2

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Olof Larsson and Johan Tidén
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

187
__pom.xml__ Normal file
View File

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.pngencoder</groupId>
<artifactId>pngencoder</artifactId>
<version>0.12.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>${project.groupId}:${project.artifactId}</name>
<description>A really fast encoder for PNG images.</description>
<url>https://github.com/pngencoder/pngencoder</url>
<inceptionYear>2020</inceptionYear>
<licenses>
<license>
<name>MIT License</name>
<url>http://www.opensource.org/licenses/mit-license.php</url>
</license>
</licenses>
<developers>
<developer>
<name>Olof Larsson</name>
<email>olof.larsson@looklet.com</email>
<organization>Looklet</organization>
<organizationUrl>https://looklet.com</organizationUrl>
</developer>
<developer>
<name>Johan Tidén</name>
<email>johan.tiden@looklet.com</email>
<organization>Looklet</organization>
<organizationUrl>https://looklet.com</organizationUrl>
</developer>
<developer>
<name>Johan Kaving</name>
<email>johan.kaving@looklet.com</email>
<organization>Looklet</organization>
<organizationUrl>https://looklet.com</organizationUrl>
</developer>
<developer>
<name>Raul Jimenez</name>
<email>raul.jimenez@looklet.com</email>
<organization>Looklet</organization>
<organizationUrl>https://looklet.com</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/pngencoder/pngencoder.git</connection>
<developerConnection>scm:git:ssh://github.com:pngencoder/pngencoder.git</developerConnection>
<url>http://github.com/pngencoder/pngencoder</url>
</scm>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
<maven.compiler.failOnWarning>true</maven.compiler.failOnWarning>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>sign</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>coverage</id>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.5</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

6
src/module-info.java Normal file
View File

@ -0,0 +1,6 @@
module org.atriasoft.pngencoder {
exports org.atriasoft.pngencoder;
requires transitive org.atriasoft.egami;
requires transitive org.atriasoft.etk;
}

View 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);
}
}

View File

@ -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++;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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() {}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,138 @@
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.convertInByteBufferRGBA(bufferedImage);
} else {
scanlineBytes = ToolImage.convertInByteBufferRGB(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();
} else {
PngEncoderDeflaterOutputStream deflaterOutputStream = new PngEncoderDeflaterOutputStream(idatChunksOutputStream, compressionLevel, segmentMaxLengthOriginal);
deflaterOutputStream.write(scanlineBytes);
deflaterOutputStream.finish();
}
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() {}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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() {}
}

View File

@ -0,0 +1,159 @@
package org.atriasoft.pngencoder;
import org.atriasoft.egami.Image;
public class PngEncoderImageInterfaceConverter {
private static final int[] BAND_MASKS_INT_ARGB = { 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 };
private static final int[] BAND_MASKS_INT_ARGB_PRE = { 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 };
private static final int[] BAND_MASKS_INT_BGR = { 0x000000ff, 0x0000ff00, 0x00ff0000 };
private static final int[] BAND_MASKS_INT_RGB = { 0x00ff0000, 0x0000ff00, 0x000000ff };
private static final int[] BAND_MASKS_USHORT_555_RGB = { 0x7C00, 0x03E0, 0x001F };
private static final int[] BAND_MASKS_USHORT_565_RGB = { 0xf800, 0x07E0, 0x001F };
private static final ColorModel COLOR_MODEL_INT_ARGB = ColorModel.getRGBdefault();
private static final ColorModel COLOR_MODEL_INT_ARGB_PRE = new DirectColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000, true,
DataBuffer.TYPE_INT);
private static final ColorModel COLOR_MODEL_INT_BGR = new DirectColorModel(24, 0x000000ff, 0x0000ff00, 0x00ff0000);
private static final ColorModel COLOR_MODEL_INT_RGB = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x0);
public static Image copyType(final Image ImageInterface, final PngEncoderImageInterfaceType type) {
final int width = ImageInterface.getWidth();
final int height = ImageInterface.getHeight();
final Image convertedImageInterface = new Image(width, height, type.ordinal());
final Graphics graphics = convertedImageInterface.getGraphics();
if (!convertedImageInterface.hasAlpha()) {
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, width, height);
}
graphics.drawImage(ImageInterface, 0, 0, null);
graphics.dispose();
return convertedImageInterface;
}
public static Image createFrom3ByteBgr(final byte[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferByte(data, data.length);
ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB);
int[] nBits = { 8, 8, 8 };
int[] bOffs = { 2, 1, 0 };
ColorModel colorModel = new ComponentColorModel(colorSpace, nBits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, width, height, width * 3, 3, bOffs, null);
return new Image(colorModel, raster, false, null);
}
public static Image createFrom4ByteAbgr(final byte[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferByte(data, data.length);
ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB);
int[] nBits = { 8, 8, 8, 8 };
int[] bOffs = { 3, 2, 1, 0 };
ColorModel colorModel = new ComponentColorModel(colorSpace, nBits, true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, width, height, width * 4, 4, bOffs, null);
return new Image(colorModel, raster, false, null);
}
public static Image createFrom4ByteAbgrPre(final byte[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferByte(data, data.length);
ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_sRGB);
int[] nBits = { 8, 8, 8, 8 };
int[] bOffs = { 3, 2, 1, 0 };
ColorModel colorModel = new ComponentColorModel(colorSpace, nBits, true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, width, height, width * 4, 4, bOffs, null);
return new Image(colorModel, raster, true, null);
}
public static Image createFromByteBinary(final byte[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferByte(data, data.length);
byte[] arr = { (byte) 0, (byte) 0xff };
IndexColorModel colorModel = new IndexColorModel(1, 2, arr, arr, arr);
WritableRaster raster = Raster.createPackedRaster(dataBuffer, width, height, 1, null);
return new Image(colorModel, raster, false, null);
}
public static Image createFromByteGray(final byte[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferByte(data, data.length);
ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_GRAY);
int[] nBits = { 8 };
ColorModel colorModel = new ComponentColorModel(colorSpace, nBits, false, true, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
int[] bandOffsets = { 0 };
WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, width, height, width, 1, bandOffsets, null);
return new Image(colorModel, raster, true, null);
}
public static Image createFromIntArgb(final int[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferInt(data, data.length);
WritableRaster raster = Raster.createPackedRaster(dataBuffer, width, height, width, PngEncoderImageInterfaceConverter.BAND_MASKS_INT_ARGB, null);
return new Image(PngEncoderImageInterfaceConverter.COLOR_MODEL_INT_ARGB, raster, false, null);
}
public static Image createFromIntArgbPre(final int[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferInt(data, data.length);
WritableRaster raster = Raster.createPackedRaster(dataBuffer, width, height, width, PngEncoderImageInterfaceConverter.BAND_MASKS_INT_ARGB_PRE, null);
return new Image(PngEncoderImageInterfaceConverter.COLOR_MODEL_INT_ARGB_PRE, raster, true, null);
}
public static Image createFromIntBgr(final int[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferInt(data, data.length);
WritableRaster raster = Raster.createPackedRaster(dataBuffer, width, height, width, PngEncoderImageInterfaceConverter.BAND_MASKS_INT_BGR, null);
return new Image(PngEncoderImageInterfaceConverter.COLOR_MODEL_INT_BGR, raster, false, null);
}
public static Image createFromIntRgb(final int[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferInt(data, data.length);
WritableRaster raster = Raster.createPackedRaster(dataBuffer, width, height, width, PngEncoderImageInterfaceConverter.BAND_MASKS_INT_RGB, null);
return new Image(PngEncoderImageInterfaceConverter.COLOR_MODEL_INT_RGB, raster, false, null);
}
public static Image createFromUshort555Rgb(final short[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferUShort(data, data.length);
ColorModel colorModel = new DirectColorModel(15, PngEncoderImageInterfaceConverter.BAND_MASKS_USHORT_555_RGB[0], PngEncoderImageInterfaceConverter.BAND_MASKS_USHORT_555_RGB[1],
PngEncoderImageInterfaceConverter.BAND_MASKS_USHORT_555_RGB[2]);
WritableRaster raster = Raster.createPackedRaster(dataBuffer, width, height, width, PngEncoderImageInterfaceConverter.BAND_MASKS_USHORT_555_RGB, null);
return new Image(colorModel, raster, false, null);
}
public static Image createFromUshort565Rgb(final short[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferUShort(data, data.length);
ColorModel colorModel = new DirectColorModel(16, PngEncoderImageInterfaceConverter.BAND_MASKS_USHORT_565_RGB[0], PngEncoderImageInterfaceConverter.BAND_MASKS_USHORT_565_RGB[1],
PngEncoderImageInterfaceConverter.BAND_MASKS_USHORT_565_RGB[2]);
WritableRaster raster = Raster.createPackedRaster(dataBuffer, width, height, width, PngEncoderImageInterfaceConverter.BAND_MASKS_USHORT_565_RGB, null);
return new Image(colorModel, raster, false, null);
}
public static Image createFromUshortGray(final short[] data, final int width, final int height) {
DataBuffer dataBuffer = new DataBufferUShort(data, data.length);
ColorSpace colorSpace = ColorSpace.getInstance(ColorSpace.CS_GRAY);
int[] nBits = { 16 };
ColorModel colorModel = new ComponentColorModel(colorSpace, nBits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_USHORT);
int[] bandOffsets = { 0 };
WritableRaster raster = Raster.createInterleavedRaster(dataBuffer, width, height, width, 1, bandOffsets, null);
return new Image(colorModel, raster, false, null);
}
public static Image ensureType(final Image ImageInterface, final PngEncoderImageInterfaceType type) {
if (PngEncoderImageInterfaceType.valueOf(ImageInterface) == type) {
return ImageInterface;
}
return PngEncoderImageInterfaceConverter.copyType(ImageInterface, type);
}
public static DataBuffer getDataBuffer(final Image ImageInterface) {
return ImageInterface.getRaster().getDataBuffer();
}
public static DataBufferByte getDataBufferByte(final Image ImageInterface) {
return (DataBufferByte) PngEncoderImageInterfaceConverter.getDataBuffer(ImageInterface);
}
public static DataBufferInt getDataBufferInt(final Image ImageInterface) {
return (DataBufferInt) PngEncoderImageInterfaceConverter.getDataBuffer(ImageInterface);
}
public static DataBufferUShort getDataBufferUShort(final Image ImageInterface) {
return (DataBufferUShort) PngEncoderImageInterfaceConverter.getDataBuffer(ImageInterface);
}
private PngEncoderImageInterfaceConverter() {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -0,0 +1,76 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class PngEncoderBenchmarkAssorted {
@Disabled("run manually")
@Test
public void runBenchmarkCustom() throws IOException {
Timing.message("started");
//final Image original = readTestImageResource("png-encoder-logo.png");
final Image original = PngEncoderImageConverter.ensureType(PngEncoderTestUtil.readTestImageResource("looklet-look-scale6.png"), PngEncoderImageType.TYPE_INT_RGB);
Timing.message("loaded");
final File outImageIO = File.createTempFile("out-imageio", ".png");
//final File outPngEncoder = File.createTempFile("out-pngencoder", ".png");
final File outPngEncoder = new File("/Users/olof/Desktop/out.png");
ImageIO.write(original, "png", outImageIO);
Timing.message("ImageIO Warmup");
ImageIO.write(original, "png", outImageIO);
Timing.message("ImageIO Result");
PngEncoder pngEncoder = new PngEncoder()
//.withMultiThreadedCompressionEnabled(false)
.withCompressionLevel(9).withImage(original);
System.out.println(outPngEncoder);
pngEncoder.toFile(outPngEncoder);
Timing.message("PngEncoder Warmup");
pngEncoder.toFile(outPngEncoder);
Timing.message("PngEncoder Result");
final long imageIOSize = outImageIO.length();
final long pngEncoderSize = outPngEncoder.length();
if (imageIOSize != 0) {
System.out.println("imageIOSize: " + imageIOSize);
}
if (pngEncoderSize != 0) {
System.out.println("pngEncoderSize: " + pngEncoderSize);
}
if (imageIOSize != 0 && pngEncoderSize != 0) {
System.out.println("pngEncoderSize / imageIOSize: " + (double) pngEncoderSize / (double) imageIOSize);
}
}
@Disabled("run manually")
@Test
public void runBenchmarkIntelliJIdeaProfilerImageIO() {
final int times = 1;
final Image Image = PngEncoderImageConverter.ensureType(PngEncoderTestUtil.readTestImageResource("looklet-look-scale6.png"), PngEncoderImageType.TYPE_INT_ARGB);
for (int i = 0; i < times; i++) {
PngEncoderTestUtil.encodeWithImageIO(Image);
}
}
@Disabled("run manually")
@Test
public void runBenchmarkIntelliJIdeaProfilerPngEncoder() {
final int times = 10;
final Image Image = PngEncoderImageConverter.ensureType(PngEncoderTestUtil.readTestImageResource("looklet-look-scale6.png"), PngEncoderImageType.TYPE_INT_ARGB);
for (int i = 0; i < times; i++) {
PngEncoderTestUtil.encodeWithPngEncoder(Image);
}
}
}

View File

@ -0,0 +1,86 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;
public class PngEncoderBenchmarkCompressionSpeedVsSize {
@State(Scope.Benchmark)
public static class BenchmarkState {
final Image Image = PngEncoderBenchmarkCompressionSpeedVsSize.createTestImage();
}
private static final Options OPTIONS = new OptionsBuilder().include(PngEncoderBenchmarkCompressionSpeedVsSize.class.getSimpleName() + ".*").shouldFailOnError(true).mode(Mode.Throughput)
.timeUnit(TimeUnit.SECONDS).threads(1).forks(1).warmupIterations(1).measurementIterations(1).warmupTime(TimeValue.seconds(2)).measurementTime(TimeValue.seconds(5)).build();
private static Image createTestImage() {
return PngEncoderTestUtil.readTestImageResource("png-encoder-logo.png");
}
@Benchmark
public void compressionLevel0(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 0);
}
@Benchmark
public void compressionLevel1(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 1);
}
@Benchmark
public void compressionLevel2(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 2);
}
@Benchmark
public void compressionLevel3(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 3);
}
@Benchmark
public void compressionLevel4(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 4);
}
@Benchmark
public void compressionLevel5(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 5);
}
@Benchmark
public void compressionLevel6(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 6);
}
@Benchmark
public void compressionLevel7(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 7);
}
@Benchmark
public void compressionLevel8(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 8);
}
@Benchmark
public void compressionLevel9(final BenchmarkState state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image, 9);
}
@Disabled("run manually")
@Test
public void runBenchmarkSize() {
final Image Image = PngEncoderBenchmarkCompressionSpeedVsSize.createTestImage();
for (int compressionLevel = 0; compressionLevel <= 9; compressionLevel++) {
final int fileSize = PngEncoderTestUtil.encodeWithPngEncoder(Image, compressionLevel);
String message = String.format("compressionLevel: %d fileSize: %d", compressionLevel, fileSize);
System.out.println(message);
}
}
@Disabled("run manually")
@Test
public void runBenchmarkSpeed() throws Exception {
new Runner(PngEncoderBenchmarkCompressionSpeedVsSize.OPTIONS).run();
}
}

View File

@ -0,0 +1,77 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.awt.image.Image;
import java.util.concurrent.TimeUnit;
/**
* Benchmark Mode Cnt Score Error Units
*
* PngEncoderBenchmarkPngEncoderVsImageIO.random1024x1024ImageIO thrpt 5.150 ops/s
* PngEncoderBenchmarkPngEncoderVsImageIO.random1024x1024PngEncoder thrpt 36.324 ops/s
* 36.324 / 5.150 = 7.1 times faster
*
* PngEncoderBenchmarkPngEncoderVsImageIO.logo2121x350ImageIO thrpt 24.857 ops/s
* PngEncoderBenchmarkPngEncoderVsImageIO.logo2121x350PngEncoder thrpt 127.034 ops/s
* 127.034 / 24.857 = 5.1 times faster
*
* PngEncoderBenchmarkPngEncoderVsImageIO.looklet4900x6000ImageIO thrpt 0.029 ops/s
* PngEncoderBenchmarkPngEncoderVsImageIO.looklet4900x6000PngEncoder thrpt 0.159 ops/s
* 0.159 / 0.029 = 5.5 times faster
*/
public class PngEncoderBenchmarkPngEncoderVsImageIO {
@State(Scope.Benchmark)
public static class BenchmarkStateLogo2121x350 {
final Image Image = PngEncoderTestUtil.readTestImageResource("png-encoder-logo.png");
}
@State(Scope.Benchmark)
public static class BenchmarkStateLooklet4900x6000 {
final Image Image = PngEncoderImageConverter.ensureType(PngEncoderTestUtil.readTestImageResource("looklet-look-scale6.png"), PngEncoderImageType.TYPE_INT_ARGB);
}
@State(Scope.Benchmark)
public static class BenchmarkStateRandom1024x1024 {
final Image Image = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_ARGB, 1024);
}
private static final Options OPTIONS = new OptionsBuilder().include(PngEncoderBenchmarkPngEncoderVsImageIO.class.getSimpleName() + ".*").shouldFailOnError(true).mode(Mode.Throughput)
.timeUnit(TimeUnit.SECONDS).threads(1).forks(1).warmupIterations(1).measurementIterations(1).warmupTime(TimeValue.seconds(2)).measurementTime(TimeValue.seconds(5)).build();
@Benchmark
public void logo2121x350ImageIO(final BenchmarkStateLogo2121x350 state) {
PngEncoderTestUtil.encodeWithImageIO(state.Image);
}
@Benchmark
public void logo2121x350PngEncoder(final BenchmarkStateLogo2121x350 state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image);
}
@Benchmark
public void looklet4900x6000ImageIO(final BenchmarkStateLooklet4900x6000 state) {
PngEncoderTestUtil.encodeWithImageIO(state.Image);
}
@Benchmark
public void looklet4900x6000PngEncoder(final BenchmarkStateLooklet4900x6000 state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image);
}
@Benchmark
public void random1024x1024ImageIO(final BenchmarkStateRandom1024x1024 state) {
PngEncoderTestUtil.encodeWithImageIO(state.Image);
}
@Benchmark
public void random1024x1024PngEncoder(final BenchmarkStateRandom1024x1024 state) {
PngEncoderTestUtil.encodeWithPngEncoder(state.Image);
}
@Disabled("run manually")
@Test
public void runBenchmark() throws Exception {
new Runner(PngEncoderBenchmarkPngEncoderVsImageIO.OPTIONS).run();
}
}

View File

@ -0,0 +1,168 @@
package test.atriasoft.pngencoder;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Test;
import java.awt.image.Image;
import static org.hamcrest.MatcherAssert.assertThat;
public class PngEncoderImageConverterTest {
private static void assertEquals(final Image actual, final Image expected) {
MatcherAssert.assertThat(actual.getWidth(), is(expected.getWidth()));
MatcherAssert.assertThat(actual.getHeight(), is(expected.getHeight()));
for (int y = 0; y < actual.getWidth(); y++) {
for (int x = 0; x < actual.getWidth(); x++) {
MatcherAssert.assertThat(actual.getRGB(x, y), is(expected.getRGB(x, y)));
}
}
}
@Test
public void copyTypeReturnsDifferentForDifferentType() {
Image original = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_ARGB);
Image ensured = PngEncoderImageConverter.copyType(original, PngEncoderImageType.TYPE_USHORT_GRAY);
MatcherAssert.assertThat(original, is(not(ensured)));
}
@Test
public void copyTypeReturnsDifferentForSameType() {
Image original = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_ARGB);
Image ensured = PngEncoderImageConverter.copyType(original, PngEncoderImageType.TYPE_INT_ARGB);
MatcherAssert.assertThat(original, is(not(ensured)));
}
@Test
public void createFrom3ByteBgr() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_3BYTE_BGR);
final byte[] data = PngEncoderImageConverter.getDataBufferByte(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFrom3ByteBgr(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFrom4ByteAbgr() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_4BYTE_ABGR);
final byte[] data = PngEncoderImageConverter.getDataBufferByte(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFrom4ByteAbgr(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFrom4ByteAbgrPre() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_4BYTE_ABGR_PRE);
final byte[] data = PngEncoderImageConverter.getDataBufferByte(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFrom4ByteAbgrPre(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFromByteBinary() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_BYTE_BINARY);
final byte[] data = PngEncoderImageConverter.getDataBufferByte(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFromByteBinary(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFromByteGray() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_BYTE_GRAY);
final byte[] data = PngEncoderImageConverter.getDataBufferByte(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFromByteGray(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFromIntArgb() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_ARGB);
final int[] data = PngEncoderImageConverter.getDataBufferInt(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFromIntArgb(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFromIntArgbPre() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_ARGB_PRE);
final int[] data = PngEncoderImageConverter.getDataBufferInt(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFromIntArgbPre(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFromIntBgr() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_BGR);
final int[] data = PngEncoderImageConverter.getDataBufferInt(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFromIntBgr(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFromIntRgb() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_RGB);
final int[] data = PngEncoderImageConverter.getDataBufferInt(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFromIntRgb(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFromUshort555Rgb() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_USHORT_555_RGB);
final short[] data = PngEncoderImageConverter.getDataBufferUShort(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFromUshort555Rgb(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFromUshort565Rgb() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_USHORT_565_RGB);
final short[] data = PngEncoderImageConverter.getDataBufferUShort(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFromUshort565Rgb(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void createFromUshortGray() {
final Image expected = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_USHORT_GRAY);
final short[] data = PngEncoderImageConverter.getDataBufferUShort(expected).getData();
final int width = expected.getWidth();
final int height = expected.getHeight();
final Image actual = PngEncoderImageConverter.createFromUshortGray(data, width, height);
PngEncoderImageConverterTest.assertEquals(actual, expected);
}
@Test
public void ensureTypeReturnsDifferentForDifferentType() {
Image original = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_ARGB);
Image ensured = PngEncoderImageConverter.ensureType(original, PngEncoderImageType.TYPE_USHORT_GRAY);
MatcherAssert.assertThat(original, is(not(ensured)));
}
@Test
public void ensureTypeReturnsSameForSameType() {
Image original = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_ARGB);
Image ensured = PngEncoderImageConverter.ensureType(original, PngEncoderImageType.TYPE_INT_ARGB);
MatcherAssert.assertThat(original, is(ensured));
}
}

View File

@ -0,0 +1,63 @@
package test.atriasoft.pngencoder;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.awt.image.Image;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class PngEncoderImageTypeTest {
@Test
public void containsAllImageTypes1() {
new Image(1, 1, PngEncoderImageType.values().length - 1);
}
@Test
public void containsAllImageTypes2() {
Assertions.assertThrows(IllegalArgumentException.class, () -> new Image(1, 1, PngEncoderImageType.values().length));
}
@Test
public void ToStringCombinesNameAndOrdinal() {
String actual = PngEncoderImageType.TYPE_INT_ARGB.toString();
String expected = "TYPE_INT_ARGB#2";
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void valueOfAllInOrdinalLoop() {
for (PngEncoderImageType expected : PngEncoderImageType.values()) {
PngEncoderImageType actual = PngEncoderImageType.valueOf(expected.ordinal());
MatcherAssert.assertThat(actual, is(expected));
}
}
@Test
public void valueOfImage() {
Image Image = new Image(1, 1, Image.TYPE_INT_ARGB_PRE);
PngEncoderImageType actual = PngEncoderImageType.valueOf(Image);
PngEncoderImageType expected = PngEncoderImageType.TYPE_INT_ARGB_PRE;
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void valueOfTypeCustom() {
PngEncoderImageType actual = PngEncoderImageType.valueOf(Image.TYPE_CUSTOM);
PngEncoderImageType expected = PngEncoderImageType.TYPE_CUSTOM;
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void valueOfTypeIntArgb() {
PngEncoderImageType actual = PngEncoderImageType.valueOf(Image.TYPE_INT_ARGB);
PngEncoderImageType expected = PngEncoderImageType.TYPE_INT_ARGB;
MatcherAssert.assertThat(actual, is(expected));
}
}

View File

@ -0,0 +1,50 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class PngEncoderDeflaterBufferPoolTest {
@Test
public void bufferBytesLengthIsBufferMaxLength() {
final PngEncoderDeflaterBufferPool bufferPool = new PngEncoderDeflaterBufferPool(1337);
PngEncoderDeflaterBuffer borrowed = bufferPool.borrow();
final int actual = borrowed.bytes.length;
final int expected = 1337;
assertThat(actual, is(expected));
}
@Test
public void initialSizeIsZero() {
final PngEncoderDeflaterBufferPool bufferPool = new PngEncoderDeflaterBufferPool(1337);
final int actual = bufferPool.size();
final int expected = 0;
assertThat(actual, is(expected));
}
@Test
public void sizeIs1AfterBorrowingAndGivingBackTwice() {
final PngEncoderDeflaterBufferPool bufferPool = new PngEncoderDeflaterBufferPool(1337);
PngEncoderDeflaterBuffer borrowed1 = bufferPool.borrow();
borrowed1.giveBack();
PngEncoderDeflaterBuffer borrowed2 = bufferPool.borrow();
borrowed2.giveBack();
final int actual = bufferPool.size();
final int expected = 1;
assertThat(actual, is(expected));
}
@Test
public void sizeIsZeroAfterBorrowingTwiceAndNotGivingBack() {
final PngEncoderDeflaterBufferPool bufferPool = new PngEncoderDeflaterBufferPool(1337);
bufferPool.borrow();
bufferPool.borrow();
final int actual = bufferPool.size();
final int expected = 0;
assertThat(actual, is(expected));
}
}

View File

@ -0,0 +1,17 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import java.util.concurrent.ExecutorService;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class PngEncoderDeflaterExecutorServiceTest {
@Test
public void getInstanceReturnsSameInstance() {
ExecutorService expected = PngEncoderDeflaterExecutorService.getInstance();
ExecutorService actual = PngEncoderDeflaterExecutorService.getInstance();
assertThat(actual, is(expected));
}
}

View File

@ -0,0 +1,37 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class PngEncoderDeflaterExecutorServiceThreadFactoryTest {
private static Thread newThread() {
PngEncoderDeflaterExecutorServiceThreadFactory threadFactory = new PngEncoderDeflaterExecutorServiceThreadFactory();
return threadFactory.newThread(() -> {});
}
@Test
public void daemonIsTrue() {
Thread thread = PngEncoderDeflaterExecutorServiceThreadFactoryTest.newThread();
boolean actual = thread.isDaemon();
boolean expected = true;
assertThat(actual, is(expected));
}
@Test
public void getInstanceReturnsSameInstance() {
PngEncoderDeflaterExecutorServiceThreadFactory expected = PngEncoderDeflaterExecutorServiceThreadFactory.getInstance();
PngEncoderDeflaterExecutorServiceThreadFactory actual = PngEncoderDeflaterExecutorServiceThreadFactory.getInstance();
assertThat(actual, is(expected));
}
@Test
public void nameIsCustom() {
Thread thread = PngEncoderDeflaterExecutorServiceThreadFactoryTest.newThread();
String actual = thread.getName();
assertThat(actual, is("PngEncoder Deflater (0)"));
}
}

View File

@ -0,0 +1,263 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterOutputStream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class PngEncoderDeflaterOutputStreamTest {
private static class PngEncoderDeflaterBufferPoolAssertive extends PngEncoderDeflaterBufferPool {
private final Set<PngEncoderDeflaterBuffer> borrowed = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Set<PngEncoderDeflaterBuffer> given = Collections.newSetFromMap(new ConcurrentHashMap<>());
public PngEncoderDeflaterBufferPoolAssertive(final int bufferMaxLength) {
super(bufferMaxLength);
}
void assertThatGivenIsBorrowed() {
assertThat(this.given, is(this.borrowed));
}
@Override
PngEncoderDeflaterBuffer borrow() {
PngEncoderDeflaterBuffer buffer = super.borrow();
this.borrowed.add(buffer);
return buffer;
}
@Override
void giveBack(final PngEncoderDeflaterBuffer buffer) {
if (buffers.contains(buffer)) {
throw new IllegalArgumentException("Adding an already present buffer to pool is not allowed.");
}
this.given.add(buffer);
super.giveBack(buffer);
}
}
private static class RiggedOutputStream extends OutputStream {
private int count;
private final int countBytesToThrowException;
public RiggedOutputStream(final int countBytesToThrowException) {
this.countBytesToThrowException = countBytesToThrowException;
this.count = 0;
}
@Override
public void write(final int b) throws IOException {
this.count++;
if (this.count >= this.countBytesToThrowException) {
throw new IOException("This exception was generated for the purpose of testing.");
}
}
}
private static class RiggedPngEncoderDeflaterSegmentTask extends PngEncoderDeflaterSegmentTask {
private static final PngEncoderDeflaterBufferPool pool = new PngEncoderDeflaterBufferPool(1337);
public RiggedPngEncoderDeflaterSegmentTask() {
super(RiggedPngEncoderDeflaterSegmentTask.pool.borrow(), RiggedPngEncoderDeflaterSegmentTask.pool.borrow(), PngEncoder.DEFAULT_COMPRESSION_LEVEL, false);
}
@Override
public PngEncoderDeflaterSegmentResult get() {
throw new RuntimeException("This exception was generated for the purpose of testing.");
}
}
private static final BiConsumer<byte[], OutputStream> MULTI_THREADED_DEFLATER = (bytes, outputStream) -> {
try (PngEncoderDeflaterOutputStream deflaterOutputStream = new PngEncoderDeflaterOutputStream(outputStream, PngEncoder.DEFAULT_COMPRESSION_LEVEL,
PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL)) {
deflaterOutputStream.write(bytes);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};
private static final int SEGMENT_MAX_LENGTH_ORIGINAL = 64 * 1024;
private static final BiConsumer<byte[], OutputStream> SINGLE_THREADED_DEFLATER = (bytes, outputStream) -> {
try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(outputStream)) {
deflaterOutputStream.write(bytes);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};
private static void assertThatBytesIsSameAfterDeflateAndInflate(final byte[] expected, final BiConsumer<byte[], OutputStream> deflater) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
deflater.accept(expected, outputStream);
byte[] deflated = outputStream.toByteArray();
byte[] actual = PngEncoderDeflaterOutputStreamTest.inflate(deflated);
assertThat(actual.length, is(expected.length));
assertThat(actual, is(expected));
}
private static void assertThatBytesIsSameAfterDeflateAndInflateFast(final byte[] expected, final BiConsumer<byte[], OutputStream> deflater) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
deflater.accept(expected, outputStream);
byte[] deflated = outputStream.toByteArray();
byte[] actual = PngEncoderDeflaterOutputStreamTest.inflate(deflated);
assertThat(actual.length, is(expected.length));
for (int i = 0; i < actual.length; i += 11) {
assertThat(actual[i], is(expected[i]));
}
}
private static byte[] createRandomBytes(final int length) {
Random random = new Random(12345);
byte[] randomBytes = new byte[length];
random.nextBytes(randomBytes);
return randomBytes;
}
private static byte[] inflate(final byte[] deflated) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (InflaterOutputStream inflaterOutputStream = new InflaterOutputStream(byteArrayOutputStream)) {
inflaterOutputStream.write(deflated);
}
return byteArrayOutputStream.toByteArray();
}
@Test
public void assertiveBufferPool10Bytes() throws IOException {
PngEncoderDeflaterBufferPoolAssertive pool = new PngEncoderDeflaterBufferPoolAssertive(
PngEncoderDeflaterOutputStream.getSegmentMaxLengthDeflated(PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PngEncoderDeflaterOutputStream deflaterOutputStream = new PngEncoderDeflaterOutputStream(outputStream, PngEncoder.DEFAULT_COMPRESSION_LEVEL,
PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL, pool);
byte[] bytesToWrite = PngEncoderDeflaterOutputStreamTest.createRandomBytes(10);
deflaterOutputStream.write(bytesToWrite);
deflaterOutputStream.finish();
pool.assertThatGivenIsBorrowed();
}
@Test
public void assertiveBufferPoolManyBytes() throws IOException {
PngEncoderDeflaterBufferPoolAssertive pool = new PngEncoderDeflaterBufferPoolAssertive(
PngEncoderDeflaterOutputStream.getSegmentMaxLengthDeflated(PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PngEncoderDeflaterOutputStream deflaterOutputStream = new PngEncoderDeflaterOutputStream(outputStream, PngEncoder.DEFAULT_COMPRESSION_LEVEL,
PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL, pool);
byte[] bytesToWrite = PngEncoderDeflaterOutputStreamTest.createRandomBytes(PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL * 2);
deflaterOutputStream.write(bytesToWrite);
deflaterOutputStream.finish();
pool.assertThatGivenIsBorrowed();
}
@Test
public void constructorThrowsIOExceptionOnWritingDeflateHeaderWithRiggedOutputStream() throws IOException {
RiggedOutputStream riggedOutputStream = new RiggedOutputStream(1);
assertThrows(IOException.class, () -> new PngEncoderDeflaterOutputStream(riggedOutputStream, PngEncoder.DEFAULT_COMPRESSION_LEVEL, SEGMENT_MAX_LENGTH_ORIGINAL));
}
@Test
public void deflateMultiThreaded300SegmentsToTestThreadSafety() throws Exception {
byte[] expected = PngEncoderDeflaterOutputStreamTest.createRandomBytes(PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL * 300);
PngEncoderDeflaterOutputStreamTest.assertThatBytesIsSameAfterDeflateAndInflateFast(expected, PngEncoderDeflaterOutputStreamTest.MULTI_THREADED_DEFLATER);
}
@Test
public void deflateMultiThreadedJustFiveBytes() throws Exception {
byte[] expected = { 1, 2, 3, 4, 5 };
PngEncoderDeflaterOutputStreamTest.assertThatBytesIsSameAfterDeflateAndInflate(expected, PngEncoderDeflaterOutputStreamTest.MULTI_THREADED_DEFLATER);
}
@Test
public void deflateMultiThreadedOneSegment() throws Exception {
byte[] expected = PngEncoderDeflaterOutputStreamTest.createRandomBytes(PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL / 2);
PngEncoderDeflaterOutputStreamTest.assertThatBytesIsSameAfterDeflateAndInflate(expected, PngEncoderDeflaterOutputStreamTest.MULTI_THREADED_DEFLATER);
}
@Test
public void deflateMultiThreadedTwoSegmentsToTestSegmentBoundary() throws Exception {
byte[] expected = PngEncoderDeflaterOutputStreamTest.createRandomBytes(PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL * 2);
PngEncoderDeflaterOutputStreamTest.assertThatBytesIsSameAfterDeflateAndInflate(expected, PngEncoderDeflaterOutputStreamTest.MULTI_THREADED_DEFLATER);
}
@Test
public void deflateSingleThreadedJustFiveBytes() throws Exception {
byte[] expected = { 1, 2, 3, 4, 5 };
PngEncoderDeflaterOutputStreamTest.assertThatBytesIsSameAfterDeflateAndInflate(expected, PngEncoderDeflaterOutputStreamTest.SINGLE_THREADED_DEFLATER);
}
@Test
public void deflateSingleThreadedOneSegment() throws Exception {
byte[] expected = PngEncoderDeflaterOutputStreamTest.createRandomBytes(PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL / 2);
PngEncoderDeflaterOutputStreamTest.assertThatBytesIsSameAfterDeflateAndInflate(expected, PngEncoderDeflaterOutputStreamTest.SINGLE_THREADED_DEFLATER);
}
@Test
public void deflateSingleThreadedTwoSegmentsToTestSegmentBoundary() throws Exception {
byte[] expected = PngEncoderDeflaterOutputStreamTest.createRandomBytes(PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL * 2);
PngEncoderDeflaterOutputStreamTest.assertThatBytesIsSameAfterDeflateAndInflate(expected, PngEncoderDeflaterOutputStreamTest.SINGLE_THREADED_DEFLATER);
}
@Test
public void finishThrowsIOExceptionOnJoiningWithRiggedOutputStream() throws IOException {
RiggedOutputStream riggedOutputStream = new RiggedOutputStream(3);
PngEncoderDeflaterOutputStream deflaterOutputStream = new PngEncoderDeflaterOutputStream(riggedOutputStream, PngEncoder.DEFAULT_COMPRESSION_LEVEL,
PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL);
byte[] bytesToWrite = PngEncoderDeflaterOutputStreamTest.createRandomBytes(10);
deflaterOutputStream.write(bytesToWrite);
assertThrows(IOException.class, deflaterOutputStream::finish);
}
@Test
public void finishThrowsIOExceptionOnJoiningWithRiggedPngEncoderDeflaterSegmentTask() throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PngEncoderDeflaterOutputStream deflaterOutputStream = new PngEncoderDeflaterOutputStream(byteArrayOutputStream, PngEncoder.DEFAULT_COMPRESSION_LEVEL,
PngEncoderDeflaterOutputStreamTest.SEGMENT_MAX_LENGTH_ORIGINAL);
deflaterOutputStream.submitTask(new RiggedPngEncoderDeflaterSegmentTask());
assertThrows(IOException.class, deflaterOutputStream::finish);
}
@Test
public void getSegmentMaxLengthOriginalDoesNotIncreaseImmediatelyOverMin() {
final int actual = PngEncoderDeflaterOutputStream.getSegmentMaxLengthOriginal(PngEncoderDeflaterOutputStream.SEGMENT_MAX_LENGTH_ORIGINAL_MIN + 1);
final int expected = PngEncoderDeflaterOutputStream.SEGMENT_MAX_LENGTH_ORIGINAL_MIN;
assertThat(actual, is(expected));
}
@Test
public void getSegmentMaxLengthOriginalRespectsMin() {
final int actual = PngEncoderDeflaterOutputStream.getSegmentMaxLengthOriginal(1);
final int expected = PngEncoderDeflaterOutputStream.SEGMENT_MAX_LENGTH_ORIGINAL_MIN;
assertThat(actual, is(expected));
}
@Test
public void segmentMaxLengthDeflatedGreaterThanSegmentMaxLengthOriginal() {
final int segmentMaxLengthOriginal = 64 * 1024;
final int segmentMaxLengthDeflated = PngEncoderDeflaterOutputStream.getSegmentMaxLengthDeflated(segmentMaxLengthOriginal);
assertThat(segmentMaxLengthDeflated, is(greaterThan(segmentMaxLengthOriginal)));
}
@Test
public void segmentMaxLengthDictionaryIsExactly32k() {
assertThat(PngEncoderDeflaterOutputStream.SEGMENT_MAX_LENGTH_DICTIONARY, is(32 * 1024));
}
@Test
public void segmentMaxLengthOriginalMinGreaterThanSegmentMaxLengthDictionary() {
assertThat(PngEncoderDeflaterOutputStream.SEGMENT_MAX_LENGTH_ORIGINAL_MIN, is(greaterThan(PngEncoderDeflaterOutputStream.SEGMENT_MAX_LENGTH_DICTIONARY)));
}
}

View File

@ -0,0 +1,33 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import java.util.zip.Adler32;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class PngEncoderDeflaterSegmentResultTest {
@Test
public void combineAdler32() {
byte[] bytesAll = { 1, 2, 3, 4, 5, 6, 7 };
byte[] bytes1 = { 1, 2, 3 };
byte[] bytes2 = { 4, 5, 6, 7 };
Adler32 bytes1Adler32 = new Adler32();
bytes1Adler32.update(bytes1);
long adler1 = bytes1Adler32.getValue();
Adler32 bytes2Adler32 = new Adler32();
bytes2Adler32.update(bytes2);
long adler2 = bytes2Adler32.getValue();
long actual = PngEncoderDeflaterSegmentResult.combine(adler1, adler2, bytes2.length);
Adler32 bytesAllAdler32 = new Adler32();
bytesAllAdler32.update(bytesAll);
long expected = bytesAllAdler32.getValue();
assertThat(actual, is(expected));
}
}

View File

@ -0,0 +1,27 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import java.util.zip.Deflater;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.sameInstance;
public class PngEncoderDeflaterThreadLocalDeflaterTest {
@Test
public void assertThatAllCompressionLevelInstancesAreGettable() {
for (int compressionLevel = -1; compressionLevel <= 9; compressionLevel++) {
Deflater deflater = PngEncoderDeflaterThreadLocalDeflater.getInstance(1);
assertThat(deflater, is(notNullValue(Deflater.class)));
}
}
@Test
public void sameCompressionLevelReturnsSameInstance() {
final Deflater expected = PngEncoderDeflaterThreadLocalDeflater.getInstance(1);
final Deflater actual = PngEncoderDeflaterThreadLocalDeflater.getInstance(1);
assertThat(actual, is(sameInstance(expected)));
}
}

View File

@ -0,0 +1,39 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class PngEncoderIdatChunksOutputStreamTest {
@Test
public void assertThatIdatChunkContainsIdatStringBytes() throws IOException {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
final PngEncoderIdatChunksOutputStream idatChunksOutputStream = new PngEncoderIdatChunksOutputStream(byteArrayOutputStream);
final byte[] content = { 1, 2, 3 };
idatChunksOutputStream.write(content);
idatChunksOutputStream.flush();
final byte[] idatChunkBytes = byteArrayOutputStream.toByteArray();
final byte[] actual = Arrays.copyOfRange(idatChunkBytes, 4, 8);
final byte[] expected = PngEncoderIdatChunksOutputStream.IDAT_BYTES;
assertThat(actual, is(expected));
}
@Test
public void assertThatIdatChunkStartsWithContentLength() throws IOException {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
final PngEncoderIdatChunksOutputStream idatChunksOutputStream = new PngEncoderIdatChunksOutputStream(byteArrayOutputStream);
final byte[] content = { 1, 2, 3 };
idatChunksOutputStream.write(content);
idatChunksOutputStream.flush();
final byte[] idatChunkBytes = byteArrayOutputStream.toByteArray();
final int actual = ByteBuffer.wrap(idatChunkBytes, 0, 4).asIntBuffer().get();
final int expected = content.length;
assertThat(actual, is(expected));
}
}

View File

@ -0,0 +1,101 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.util.zip.CRC32;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class PngEncoderLogicTest {
public static final String VALID_CHUNK_TYPE = "IDAT";
private static int getSimpleCrc(final byte[] b) {
CRC32 crc32 = new CRC32();
crc32.update(b);
return (int) crc32.getValue();
}
private static byte[] intToBytes(final int val) {
ByteBuffer byteBuffer = ByteBuffer.allocate(4);
byteBuffer.putInt(val);
return byteBuffer.array();
}
@Test
public void testAsChunkAdds12Bytes() {
byte[] data = { 5 };
byte[] chunk = PngEncoderLogic.asChunk(PngEncoderLogicTest.VALID_CHUNK_TYPE, data);
int expected = 13;
int actual = chunk.length;
assertThat(actual, is(expected));
}
@Test
public void testAsChunkCrcIsCalculatedFromTypeAndData() {
byte[] data = { 5, 7 };
byte[] chunk = PngEncoderLogic.asChunk(PngEncoderLogicTest.VALID_CHUNK_TYPE, data);
ByteBuffer byteBuffer = ByteBuffer.allocate(6);
byteBuffer.mark();
byteBuffer.put(PngEncoderLogicTest.VALID_CHUNK_TYPE.getBytes());
byteBuffer.put(data);
byteBuffer.reset();
int expectedCrc = PngEncoderLogic.getCrc32(byteBuffer);
byte[] expected = PngEncoderLogicTest.intToBytes(expectedCrc);
assertThat(chunk[10], is(expected[0]));
assertThat(chunk[11], is(expected[1]));
assertThat(chunk[12], is(expected[2]));
assertThat(chunk[13], is(expected[3]));
}
@Test
public void testAsChunkFirstByteIsSizeOfData() {
byte[] data = { 5, 7 };
byte[] chunk = PngEncoderLogic.asChunk(PngEncoderLogicTest.VALID_CHUNK_TYPE, data);
byte[] expected = PngEncoderLogicTest.intToBytes(data.length);
assertThat(chunk[0], is(expected[0]));
assertThat(chunk[1], is(expected[1]));
assertThat(chunk[2], is(expected[2]));
assertThat(chunk[3], is(expected[3]));
}
@Test
public void testAsChunkNullBytes() {
assertThrows(NullPointerException.class, () -> PngEncoderLogic.asChunk(VALID_CHUNK_TYPE, null));
}
@Test
public void testAsChunkTypeInvalid() {
assertThrows(IllegalArgumentException.class, () -> PngEncoderLogic.asChunk("chunk types must be four characters long", new byte[1]));
}
@Test
public void testAsChunkTypeNull() {
assertThrows(NullPointerException.class, () -> PngEncoderLogic.asChunk(null, new byte[1]));
}
@Test
public void testCrcWithPartialByteBuffer() {
byte[] b = { 5 };
ByteBuffer byteBuffer = ByteBuffer.allocate(20);
byteBuffer.put("garbage".getBytes());
byteBuffer.mark();
ByteBuffer slice = byteBuffer.slice().asReadOnlyBuffer();
byteBuffer.put(b);
slice.limit(b.length);
byteBuffer.put("more garbage".getBytes());
int actual = PngEncoderLogic.getCrc32(slice);
int expected = PngEncoderLogicTest.getSimpleCrc(b);
assertThat(actual, is(expected));
}
}

View File

@ -0,0 +1,36 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class PngEncoderPhysicalPixelDimensionsTest {
@Test
public void aspectRatio() {
PngEncoderPhysicalPixelDimensions physicalPixelDimensions = PngEncoderPhysicalPixelDimensions.aspectRatio(2, 3);
assertThat(physicalPixelDimensions.getPixelsPerUnitX(), is(2));
assertThat(physicalPixelDimensions.getPixelsPerUnitY(), is(3));
assertThat(physicalPixelDimensions.getUnit(), is(PngEncoderPhysicalPixelDimensions.Unit.UNKNOWN));
}
@Test
public void dpi() {
PngEncoderPhysicalPixelDimensions physicalPixelDimensions = PngEncoderPhysicalPixelDimensions.dotsPerInch(72);
assertThat(physicalPixelDimensions.getPixelsPerUnitX(), is(2835));
assertThat(physicalPixelDimensions.getPixelsPerUnitY(), is(2835));
assertThat(physicalPixelDimensions.getUnit(), is(PngEncoderPhysicalPixelDimensions.Unit.METER));
}
@Test
public void pixelsPerMeter() {
PngEncoderPhysicalPixelDimensions physicalPixelDimensions = PngEncoderPhysicalPixelDimensions.pixelsPerMeter(2835);
assertThat(physicalPixelDimensions.getPixelsPerUnitX(), is(2835));
assertThat(physicalPixelDimensions.getPixelsPerUnitY(), is(2835));
assertThat(physicalPixelDimensions.getUnit(), is(PngEncoderPhysicalPixelDimensions.Unit.METER));
}
}

View File

@ -0,0 +1,61 @@
package test.atriasoft.pngencoder;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Test;
import java.awt.image.Image;
import static org.hamcrest.MatcherAssert.assertThat;
public class PngEncoderScanlineUtilTest {
private void assertThatScanlineOfTestImageEqualsIntRgbOrArgb(final PngEncoderImageType type, final boolean alpha) {
final Image Image = PngEncoderTestUtil.createTestImage(type);
final Image ImageEnsured = PngEncoderImageConverter.ensureType(Image, alpha ? PngEncoderImageType.TYPE_INT_ARGB : PngEncoderImageType.TYPE_INT_RGB);
final byte[] actual = PngEncoderScanlineUtil.get(Image);
final byte[] expected = PngEncoderScanlineUtil.get(ImageEnsured);
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void get3ByteBgr() {
assertThatScanlineOfTestImageEqualsIntRgbOrArgb(PngEncoderImageType.TYPE_3BYTE_BGR, false);
}
@Test
public void get4ByteAbgr() {
assertThatScanlineOfTestImageEqualsIntRgbOrArgb(PngEncoderImageType.TYPE_4BYTE_ABGR, true);
}
@Test
public void getByteGray() {
assertThatScanlineOfTestImageEqualsIntRgbOrArgb(PngEncoderImageType.TYPE_BYTE_GRAY, false);
}
@Test
public void getIntArgbSize() {
final Image Image = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_ARGB);
final byte[] data = PngEncoderScanlineUtil.get(Image);
final int actual = data.length;
final int expected = Image.getHeight() * (Image.getWidth() * 4 + 1);
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void getIntBgr() {
assertThatScanlineOfTestImageEqualsIntRgbOrArgb(PngEncoderImageType.TYPE_INT_BGR, false);
}
@Test
public void getIntRgbSize() {
final Image Image = PngEncoderTestUtil.createTestImage(PngEncoderImageType.TYPE_INT_RGB);
final byte[] data = PngEncoderScanlineUtil.get(Image);
final int actual = data.length;
final int expected = Image.getHeight() * (Image.getWidth() * 3 + 1);
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void getUshortGray() {
assertThatScanlineOfTestImageEqualsIntRgbOrArgb(PngEncoderImageType.TYPE_USHORT_GRAY, false);
}
}

View File

@ -0,0 +1,221 @@
package test.atriasoft.pngencoder;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.w3c.dom.Element;
import java.awt.image.Image;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.stream.IntStream;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageInputStream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class PngEncoderTest {
private static final int BLACK = 0xFF000000;
private static final int BLUE = 0xFF0000FF;
private static final int GREEN = 0XFF00FF00;
private static final Image ONE_PIXEL = PngEncoderImageConverter.createFromIntArgb(new int[1], 1, 1);
private static final int RED = 0xFFFF0000;
private static final int WHITE = 0xFFFFFFFF;
public static IIOMetadata getImageMetaDataWithImageIO(final byte[] filesBytes) throws IOException {
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(filesBytes); ImageInputStream input = ImageIO.createImageInputStream(inputStream)) {
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
ImageReader reader = readers.next();
reader.setInput(input);
return reader.getImageMetadata(0);
}
}
public static Image readWithImageIO(final byte[] filesBytes) throws IOException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(filesBytes)) {
return ImageIO.read(bais);
}
}
private static int[] readWithImageIOgetRGB(final byte[] fileBytes) throws IOException {
Image Image = PngEncoderTest.readWithImageIO(fileBytes);
int width = Image.getWidth();
int height = Image.getHeight();
int[] argbImage = new int[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
argbImage[y * width + x] = Image.getRGB(x, y);
}
}
return argbImage;
}
static IntStream validCompressionLevels() {
// Compression level value must be between -1 and 9 inclusive.
return IntStream.rangeClosed(-1, 9);
}
@ParameterizedTest()
@ValueSource(ints = { -2, 10 })
public void invalidCompressionLevel(final int compressionLevel) {
Assertions.assertThrows(IllegalArgumentException.class, () -> new PngEncoder().withCompressionLevel(compressionLevel));
}
@Test
public void testEncode() {
byte[] bytes = new PngEncoder().withImage(PngEncoderTest.ONE_PIXEL).withCompressionLevel(1).toBytes();
int pngHeaderLength = PngEncoderLogic.FILE_BEGINNING.length;
int ihdrLength = 25; // length(4)+"IHDR"(4)+values(13)+crc(4)
int idatLength = 23; // length(4)+"IDAT"(4)+compressed scanline(11)+crc(4)
int iendLength = PngEncoderLogic.FILE_ENDING.length;
int expected = pngHeaderLength + ihdrLength + idatLength + iendLength;
int actual = bytes.length;
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void testEncodeAndReadBlackTransparency() throws IOException {
int width = 0xFF;
int height = 1;
int[] image = new int[width];
for (int x = 0; x < width; x++) {
image[x] = x << 24;
}
Image Image = PngEncoderImageConverter.createFromIntArgb(image, width, height);
byte[] bytes = new PngEncoder().withImage(Image).withCompressionLevel(1).toBytes();
int[] actual = PngEncoderTest.readWithImageIOgetRGB(bytes);
int[] expected = image;
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void testEncodeAndReadOpaque() throws IOException {
int width = 3;
int height = 2;
int[] image = { PngEncoderTest.WHITE, PngEncoderTest.BLACK, PngEncoderTest.RED, PngEncoderTest.GREEN, PngEncoderTest.WHITE, PngEncoderTest.BLUE };
Image Image = PngEncoderImageConverter.createFromIntArgb(image, width, height);
byte[] bytes = new PngEncoder().withImage(Image).withCompressionLevel(1).toBytes();
int[] actual = PngEncoderTest.readWithImageIOgetRGB(bytes);
int[] expected = image;
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void testEncodeAndReadRedTransparency() throws IOException {
int width = 0xFF;
int height = 1;
int[] image = new int[width];
for (int x = 0; x < width; x++) {
int pixel = (x << 24) + (x << 16);
image[x] = pixel;
}
Image Image = PngEncoderImageConverter.createFromIntArgb(image, width, height);
byte[] bytes = new PngEncoder().withImage(Image).withCompressionLevel(1).toBytes();
int[] actual = PngEncoderTest.readWithImageIOgetRGB(bytes);
int[] expected = image;
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void testEncodeWithoutImage() {
// Document the fact that, at the moment, attempting to encode without providing an
// image throws NullPointException.
PngEncoder emptyEncoder = new PngEncoder();
Assertions.assertThrows(NullPointerException.class, () -> emptyEncoder.toBytes());
PngEncoder encoderWithoutImage = new PngEncoder().withCompressionLevel(9).withMultiThreadedCompressionEnabled(true);
Assertions.assertThrows(NullPointerException.class, () -> encoderWithoutImage.toBytes());
}
@Test
public void testEncodeWithPhysicalPixelDimensions() {
byte[] bytes = new PngEncoder().withImage(PngEncoderTest.ONE_PIXEL).withCompressionLevel(1).withPhysicalPixelDimensions(PngEncoderPhysicalPixelDimensions.dotsPerInch(300)).toBytes();
int pngHeaderLength = PngEncoderLogic.FILE_BEGINNING.length;
int ihdrLength = 25; // length(4)+"IHDR"(4)+values(13)+crc(4)
int physLength = 21; // length(4)+"pHYs"(4)+values(9)+crc(4)
int idatLength = 23; // length(4)+"IDAT"(4)+compressed scanline(11)+crc(4)
int iendLength = PngEncoderLogic.FILE_ENDING.length;
int expected = pngHeaderLength + ihdrLength + physLength + idatLength + iendLength;
int actual = bytes.length;
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void testEncodeWithPhysicalPixelDimensionsAndReadMetadata() throws IOException {
int dotsPerInch = 150;
byte[] bytes = new PngEncoder().withImage(PngEncoderTest.ONE_PIXEL).withCompressionLevel(1).withPhysicalPixelDimensions(PngEncoderPhysicalPixelDimensions.dotsPerInch(dotsPerInch)).toBytes();
IIOMetadata metadata = PngEncoderTest.getImageMetaDataWithImageIO(bytes);
IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree("javax_imageio_1.0");
float horizontalPixelSize = Float.parseFloat(((Element) root.getElementsByTagName("HorizontalPixelSize").item(0)).getAttribute("value"));
float verticalPixelSize = Float.parseFloat(((Element) root.getElementsByTagName("VerticalPixelSize").item(0)).getAttribute("value"));
// Standard metadata contains the width/height of a pixel in millimeters
float mmPerInch = 25.4f;
MatcherAssert.assertThat(Math.round(mmPerInch / horizontalPixelSize), is(dotsPerInch));
MatcherAssert.assertThat(Math.round(mmPerInch / verticalPixelSize), is(dotsPerInch));
}
@Test
public void testEncodeWithSrgb() {
byte[] bytes = new PngEncoder().withImage(PngEncoderTest.ONE_PIXEL).withCompressionLevel(1).withSrgbRenderingIntent(PngEncoderSrgbRenderingIntent.PERCEPTUAL).toBytes();
int pngHeaderLength = PngEncoderLogic.FILE_BEGINNING.length;
int ihdrLength = 25; // length(4)+"IHDR"(4)+values(13)+crc(4)
int srgbLength = 13; // length(4)+"sRGB"(4)+value(1)+crc(4)
int gamaLength = 16; // length(4)+"gAMA"(4)+value(4)+crc(4)
int chrmLength = 44; // length(4)+"sRGB"(4)+value(32)+crc(4)
int idatLength = 23; // length(4)+"IDAT"(4)+compressed scanline(11)+crc(4)
int iendLength = PngEncoderLogic.FILE_ENDING.length;
int expected = pngHeaderLength + ihdrLength + srgbLength + gamaLength + chrmLength + idatLength + iendLength;
int actual = bytes.length;
MatcherAssert.assertThat(actual, is(expected));
}
@Test
public void testEncodeWithSrgbAndReadMetadata() throws IOException {
int width = 3;
int height = 2;
int[] image = { PngEncoderTest.WHITE, PngEncoderTest.BLACK, PngEncoderTest.RED, PngEncoderTest.GREEN, PngEncoderTest.WHITE, PngEncoderTest.BLUE };
Image Image = PngEncoderImageConverter.createFromIntArgb(image, width, height);
byte[] bytes = new PngEncoder().withImage(Image).withCompressionLevel(1).withSrgbRenderingIntent(PngEncoderSrgbRenderingIntent.PERCEPTUAL).toBytes();
IIOMetadata metadata = PngEncoderTest.getImageMetaDataWithImageIO(bytes);
IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
MatcherAssert.assertThat(root.getElementsByTagName("sRGB").getLength(), is(1));
MatcherAssert.assertThat(root.getElementsByTagName("cHRM").getLength(), is(1));
MatcherAssert.assertThat(root.getElementsByTagName("gAMA").getLength(), is(1));
}
@ParameterizedTest()
@MethodSource("validCompressionLevels")
public void validCompressionLevel(final int compressionLevel) {
Assertions.assertDoesNotThrow(() -> new PngEncoder().withCompressionLevel(compressionLevel));
}
}

View File

@ -0,0 +1,59 @@
package test.atriasoft.pngencoder;
import java.awt.image.Image;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.Random;
import javax.imageio.ImageIO;
class PngEncoderTestUtil {
private static final int DEFAULT_SIDE = 128;
private static final OutputStream NULL_OUTPUT_STREAM = new NullOutputStream();
private static final Random RANDOM = new Random();
static Image createTestImage(final PngEncoderImageType type) {
return PngEncoderTestUtil.createTestImage(type, PngEncoderTestUtil.DEFAULT_SIDE);
}
static Image createTestImage(final PngEncoderImageType type, final int side) {
final Image Image = new Image(side, side, PngEncoderImageType.TYPE_INT_ARGB.ordinal());
for (int y = 0; y < Image.getHeight(); y++) {
for (int x = 0; x < Image.getWidth(); x++) {
int a = PngEncoderTestUtil.RANDOM.nextInt(256);
int r = PngEncoderTestUtil.RANDOM.nextInt(256);
int g = PngEncoderTestUtil.RANDOM.nextInt(256);
int b = PngEncoderTestUtil.RANDOM.nextInt(256);
int argb = a << 24 | r << 16 | g << 8 | b;
Image.setRGB(x, y, argb);
}
}
return PngEncoderImageConverter.ensureType(Image, type);
}
static void encodeWithImageIO(final Image Image) {
try {
ImageIO.write(Image, "png", PngEncoderTestUtil.NULL_OUTPUT_STREAM);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
static int encodeWithPngEncoder(final Image Image) {
return new PngEncoder().withImage(Image).toStream(PngEncoderTestUtil.NULL_OUTPUT_STREAM);
}
static int encodeWithPngEncoder(final Image Image, final int compressionLevel) {
return new PngEncoder().withImage(Image).withCompressionLevel(compressionLevel).toStream(PngEncoderTestUtil.NULL_OUTPUT_STREAM);
}
static Image readTestImageResource(final String name) {
try (InputStream inputStream = PngEncoderTestUtil.class.getResourceAsStream("/" + name)) {
return ImageIO.read(inputStream);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read test image resource: " + name, e);
}
}
}

View File

@ -0,0 +1,37 @@
package test.atriasoft.pngencoder;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class PngEncoderVerificationUtilTest {
@Test
public void verifyChunkTypeAcceptsIDAT() {
PngEncoderVerificationUtil.verifyChunkType("IDAT");
}
@Test
public void verifyChunkTypeRejectsLorem() {
assertThrows(IllegalArgumentException.class, () -> PngEncoderVerificationUtil.verifyChunkType("Lorem"));
}
@Test
public void verifyCompressionLevelAcceptsMinusOne() {
PngEncoderVerificationUtil.verifyCompressionLevel(-1);
}
@Test
public void verifyCompressionLevelAcceptsNine() {
PngEncoderVerificationUtil.verifyCompressionLevel(9);
}
@Test
public void verifyCompressionLevelRejectsMinusTwo() {
assertThrows(IllegalArgumentException.class, () -> PngEncoderVerificationUtil.verifyCompressionLevel(-2));
}
@Test
public void verifyCompressionLevelRejectsTen() {
assertThrows(IllegalArgumentException.class, () -> PngEncoderVerificationUtil.verifyCompressionLevel(10));
}
}

View File

@ -0,0 +1,12 @@
package test.atriasoft.pngencoder;
class Timing {
private static long previously = 0;
static void message(final String message) {
long now = System.currentTimeMillis();
long delta = Timing.previously > 0 ? now - Timing.previously : 0;
Timing.previously = now;
System.out.println("[" + delta + "] " + message);
}
}