[DEV] add code and remove dependency of java.desktop
This commit is contained in:
parent
69301848cd
commit
5319b12a9d
230
.gitignore
vendored
Normal file
230
.gitignore
vendored
Normal 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
11
.travis.yml
Normal 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
21
LICENSE
Normal 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
187
__pom.xml__
Normal 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
6
src/module-info.java
Normal file
@ -0,0 +1,6 @@
|
||||
module org.atriasoft.pngencoder {
|
||||
exports org.atriasoft.pngencoder;
|
||||
|
||||
requires transitive org.atriasoft.egami;
|
||||
requires transitive org.atriasoft.etk;
|
||||
}
|
173
src/org/atriasoft/pngencoder/PngEncoder.java
Normal file
173
src/org/atriasoft/pngencoder/PngEncoder.java
Normal file
@ -0,0 +1,173 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
import org.atriasoft.egami.Image;
|
||||
|
||||
/**
|
||||
* Main class, containing the interface for PngEncoder.
|
||||
* PngEncoder is a really fast encoder for PNG images in Java.
|
||||
*/
|
||||
public class PngEncoder {
|
||||
/**
|
||||
* Compression level 9 is the default.
|
||||
* It produces images with a size comparable to ImageIO.
|
||||
*/
|
||||
public static int DEFAULT_COMPRESSION_LEVEL = Deflater.BEST_COMPRESSION;
|
||||
|
||||
private final Image bufferedImage;
|
||||
private final int compressionLevel;
|
||||
private final boolean multiThreadedCompressionEnabled;
|
||||
private final PngEncoderPhysicalPixelDimensions physicalPixelDimensions;
|
||||
private final PngEncoderSrgbRenderingIntent srgbRenderingIntent;
|
||||
|
||||
/**
|
||||
* Constructs an empty PngEncoder. Usually combined with methods named with*.
|
||||
*/
|
||||
public PngEncoder() {
|
||||
this(null, PngEncoder.DEFAULT_COMPRESSION_LEVEL, true, null, null);
|
||||
}
|
||||
|
||||
private PngEncoder(final Image bufferedImage, final int compressionLevel, final boolean multiThreadedCompressionEnabled, final PngEncoderSrgbRenderingIntent srgbRenderingIntent,
|
||||
final PngEncoderPhysicalPixelDimensions physicalPixelDimensions) {
|
||||
this.bufferedImage = bufferedImage;
|
||||
this.compressionLevel = PngEncoderVerificationUtil.verifyCompressionLevel(compressionLevel);
|
||||
this.multiThreadedCompressionEnabled = multiThreadedCompressionEnabled;
|
||||
this.srgbRenderingIntent = srgbRenderingIntent;
|
||||
this.physicalPixelDimensions = physicalPixelDimensions;
|
||||
}
|
||||
|
||||
public Image getBufferedImage() {
|
||||
return this.bufferedImage;
|
||||
}
|
||||
|
||||
public int getCompressionLevel() {
|
||||
return this.compressionLevel;
|
||||
}
|
||||
|
||||
public PngEncoderSrgbRenderingIntent getSrgbRenderingIntent() {
|
||||
return this.srgbRenderingIntent;
|
||||
}
|
||||
|
||||
public boolean isMultiThreadedCompressionEnabled() {
|
||||
return this.multiThreadedCompressionEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image and returns data as {@code byte[]}.
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @return encoded data
|
||||
*/
|
||||
public byte[] toBytes() {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(64 * 1024);
|
||||
toStream(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image and saves data into {@code file}.
|
||||
* @param file destination file where the encoded data will be written
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @throws UncheckedIOException instead of IOException
|
||||
* @return number of bytes written
|
||||
*/
|
||||
public int toFile(final File file) {
|
||||
return toFile(file.toPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image and saves data into {@code filePath}.
|
||||
* @param filePath destination file where the encoded data will be written
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @throws UncheckedIOException instead of IOException
|
||||
* @return number of bytes written
|
||||
*/
|
||||
public int toFile(final Path filePath) {
|
||||
try (OutputStream outputStream = Files.newOutputStream(filePath)) {
|
||||
return toStream(outputStream);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image and saves data into {@code fileName}.
|
||||
* @param fileName destination file where the encoded data will be written
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @throws UncheckedIOException instead of IOException
|
||||
* @return number of bytes written
|
||||
*/
|
||||
public int toFile(final String fileName) {
|
||||
return toFile(Paths.get(fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the image to outputStream.
|
||||
* @param outputStream destination of the encoded data
|
||||
* @throws NullPointerException if the image has not been set.
|
||||
* @return number of bytes written
|
||||
*/
|
||||
public int toStream(final OutputStream outputStream) {
|
||||
try {
|
||||
return PngEncoderLogic.encode(this.bufferedImage, outputStream, this.compressionLevel, this.multiThreadedCompressionEnabled, this.srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PngEncoder which has the same configuration as this one except {@code bufferedImage}.
|
||||
* The new PngEncoder will use the provided {@code bufferedImage}.
|
||||
*
|
||||
* @param bufferedImage input image
|
||||
* @return a new PngEncoder
|
||||
*/
|
||||
public PngEncoder withBufferedImage(final Image bufferedImage) {
|
||||
return new PngEncoder(bufferedImage, this.compressionLevel, this.multiThreadedCompressionEnabled, this.srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PngEncoder which has the same configuration as this one except {@code compressionLevel}.
|
||||
* The new PngEncoder will use the provided {@code compressionLevel}.
|
||||
*
|
||||
* @param compressionLevel input image (must be between -1 and 9 inclusive)
|
||||
* @return a new PngEncoder
|
||||
*/
|
||||
public PngEncoder withCompressionLevel(final int compressionLevel) {
|
||||
return new PngEncoder(this.bufferedImage, compressionLevel, this.multiThreadedCompressionEnabled, this.srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PngEncoder which has the same configuration as this one except {@code multiThreadedCompressionEnabled}.
|
||||
* The new PngEncoder will use the provided {@code multiThreadedCompressionEnabled}.
|
||||
*
|
||||
* @param multiThreadedCompressionEnabled when {@code true}, multithreaded compression will be used
|
||||
* @return a new PngEncoder
|
||||
*/
|
||||
public PngEncoder withMultiThreadedCompressionEnabled(final boolean multiThreadedCompressionEnabled) {
|
||||
return new PngEncoder(this.bufferedImage, this.compressionLevel, multiThreadedCompressionEnabled, this.srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
}
|
||||
|
||||
public PngEncoder withPhysicalPixelDimensions(final PngEncoderPhysicalPixelDimensions physicalPixelDimensions) {
|
||||
return new PngEncoder(this.bufferedImage, this.compressionLevel, this.multiThreadedCompressionEnabled, this.srgbRenderingIntent, physicalPixelDimensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PngEncoder which has the same configuration as this one except {@code srgbRenderingIntent}.
|
||||
* The new PngEncoder will add an sRGB chunk to the encoded PNG and use the provided {@code srgbRenderingIntent}.
|
||||
*
|
||||
* @param srgbRenderingIntent the rendering intent that should be used when displaying the image
|
||||
* @return a new PngEncoder
|
||||
*/
|
||||
public PngEncoder withSrgbRenderingIntent(final PngEncoderSrgbRenderingIntent srgbRenderingIntent) {
|
||||
return new PngEncoder(this.bufferedImage, this.compressionLevel, this.multiThreadedCompressionEnabled, srgbRenderingIntent, this.physicalPixelDimensions);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
class PngEncoderCountingOutputStream extends FilterOutputStream {
|
||||
private int count;
|
||||
|
||||
PngEncoderCountingOutputStream(final OutputStream out) {
|
||||
super(Objects.requireNonNull(out, "out"));
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] b, final int off, final int len) throws IOException {
|
||||
this.out.write(b, off, len);
|
||||
this.count += len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final int b) throws IOException {
|
||||
this.out.write(b);
|
||||
this.count++;
|
||||
}
|
||||
}
|
32
src/org/atriasoft/pngencoder/PngEncoderDeflaterBuffer.java
Normal file
32
src/org/atriasoft/pngencoder/PngEncoderDeflaterBuffer.java
Normal 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);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
class PngEncoderDeflaterBufferPool {
|
||||
private final int bufferMaxLength;
|
||||
protected final Queue<PngEncoderDeflaterBuffer> buffers;
|
||||
|
||||
PngEncoderDeflaterBufferPool(final int bufferMaxLength) {
|
||||
this.bufferMaxLength = bufferMaxLength;
|
||||
this.buffers = new LinkedList<>();
|
||||
}
|
||||
|
||||
PngEncoderDeflaterBuffer borrow() {
|
||||
PngEncoderDeflaterBuffer buffer = this.buffers.poll();
|
||||
if (buffer == null) {
|
||||
buffer = new PngEncoderDeflaterBuffer(this, this.bufferMaxLength);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public int getBufferMaxLength() {
|
||||
return this.bufferMaxLength;
|
||||
}
|
||||
|
||||
void giveBack(final PngEncoderDeflaterBuffer buffer) {
|
||||
buffer.length = 0;
|
||||
this.buffers.offer(buffer);
|
||||
}
|
||||
|
||||
int size() {
|
||||
return this.buffers.size();
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
class PngEncoderDeflaterExecutorService {
|
||||
private static class Holder {
|
||||
private static final ExecutorService INSTANCE = Executors.newFixedThreadPool(PngEncoderDeflaterExecutorService.NUM_THREADS_IS_AVAILABLE_PROCESSORS,
|
||||
PngEncoderDeflaterExecutorServiceThreadFactory.getInstance());
|
||||
}
|
||||
|
||||
public static int NUM_THREADS_IS_AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
static ExecutorService getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
private PngEncoderDeflaterExecutorService() {}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
class PngEncoderDeflaterExecutorServiceThreadFactory implements ThreadFactory {
|
||||
private static class Holder {
|
||||
private static final PngEncoderDeflaterExecutorServiceThreadFactory INSTANCE = new PngEncoderDeflaterExecutorServiceThreadFactory();
|
||||
}
|
||||
|
||||
static PngEncoderDeflaterExecutorServiceThreadFactory getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
private final AtomicLong counter;
|
||||
private final ThreadFactory defaultThreadFactory;
|
||||
|
||||
PngEncoderDeflaterExecutorServiceThreadFactory() {
|
||||
this.defaultThreadFactory = Executors.defaultThreadFactory();
|
||||
this.counter = new AtomicLong(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(final Runnable runnable) {
|
||||
Thread thread = this.defaultThreadFactory.newThread(runnable);
|
||||
thread.setName("PngEncoder Deflater (" + this.counter.getAndIncrement() + ")");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
}
|
206
src/org/atriasoft/pngencoder/PngEncoderDeflaterOutputStream.java
Normal file
206
src/org/atriasoft/pngencoder/PngEncoderDeflaterOutputStream.java
Normal 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);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
class PngEncoderDeflaterSegmentResult {
|
||||
// https://github.com/madler/zlib/blob/master/adler32.c#L143
|
||||
static long combine(final long adler1, final long adler2, final long len2) {
|
||||
long BASEL = 65521;
|
||||
long sum1;
|
||||
long sum2;
|
||||
long rem;
|
||||
|
||||
rem = len2 % BASEL;
|
||||
sum1 = adler1 & 0xffffL;
|
||||
sum2 = rem * sum1;
|
||||
sum2 %= BASEL;
|
||||
sum1 += (adler2 & 0xffffL) + BASEL - 1;
|
||||
sum2 += ((adler1 >> 16) & 0xffffL) + ((adler2 >> 16) & 0xffffL) + BASEL - rem;
|
||||
if (sum1 >= BASEL) {
|
||||
sum1 -= BASEL;
|
||||
}
|
||||
if (sum1 >= BASEL) {
|
||||
sum1 -= BASEL;
|
||||
}
|
||||
if (sum2 >= (BASEL << 1)) {
|
||||
sum2 -= (BASEL << 1);
|
||||
}
|
||||
if (sum2 >= BASEL) {
|
||||
sum2 -= BASEL;
|
||||
}
|
||||
return sum1 | (sum2 << 16);
|
||||
}
|
||||
|
||||
private final PngEncoderDeflaterBuffer deflatedSegment;
|
||||
private final PngEncoderDeflaterBuffer originalSegment;
|
||||
private final long originalSegmentAdler32;
|
||||
|
||||
private final int originalSegmentLength;
|
||||
|
||||
PngEncoderDeflaterSegmentResult(final PngEncoderDeflaterBuffer originalSegment, final PngEncoderDeflaterBuffer deflatedSegment, final long originalSegmentAdler32,
|
||||
final int originalSegmentLength) {
|
||||
this.originalSegment = Objects.requireNonNull(originalSegment, "originalSegment");
|
||||
this.deflatedSegment = Objects.requireNonNull(deflatedSegment, "deflatedSegment");
|
||||
this.originalSegmentAdler32 = originalSegmentAdler32;
|
||||
this.originalSegmentLength = originalSegmentLength;
|
||||
}
|
||||
|
||||
public PngEncoderDeflaterBuffer getDeflatedSegment() {
|
||||
return this.deflatedSegment;
|
||||
}
|
||||
|
||||
public PngEncoderDeflaterBuffer getOriginalSegment() {
|
||||
return this.originalSegment;
|
||||
}
|
||||
|
||||
long getUpdatedAdler32(final long originalAdler32) {
|
||||
return PngEncoderDeflaterSegmentResult.combine(originalAdler32, this.originalSegmentAdler32, this.originalSegmentLength);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
class PngEncoderDeflaterSegmentTask implements Supplier<PngEncoderDeflaterSegmentResult> {
|
||||
static void deflate(final PngEncoderDeflaterBuffer originalSegment, final PngEncoderDeflaterBuffer deflatedSegment, final int compressionLevel, final boolean lastSegment) {
|
||||
final Deflater deflater = PngEncoderDeflaterThreadLocalDeflater.getInstance(compressionLevel);
|
||||
deflater.setInput(originalSegment.bytes, 0, originalSegment.length);
|
||||
|
||||
if (lastSegment) {
|
||||
deflater.finish();
|
||||
}
|
||||
|
||||
deflatedSegment.length = deflater.deflate(deflatedSegment.bytes, 0, deflatedSegment.bytes.length, lastSegment ? Deflater.NO_FLUSH : Deflater.SYNC_FLUSH);
|
||||
}
|
||||
|
||||
private final int compressionLevel;
|
||||
private final PngEncoderDeflaterBuffer deflatedSegment;
|
||||
private final boolean lastSegment;
|
||||
|
||||
private final PngEncoderDeflaterBuffer originalSegment;
|
||||
|
||||
public PngEncoderDeflaterSegmentTask(final PngEncoderDeflaterBuffer originalSegment, final PngEncoderDeflaterBuffer deflatedSegment, final int compressionLevel, final boolean lastSegment) {
|
||||
this.originalSegment = Objects.requireNonNull(originalSegment, "originalSegment");
|
||||
this.deflatedSegment = Objects.requireNonNull(deflatedSegment, "deflatedSegment");
|
||||
this.compressionLevel = compressionLevel;
|
||||
this.lastSegment = lastSegment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PngEncoderDeflaterSegmentResult get() {
|
||||
final long originalSegmentAdler32 = this.originalSegment.calculateAdler32();
|
||||
final int originalSegmentLength = this.originalSegment.length;
|
||||
|
||||
PngEncoderDeflaterSegmentTask.deflate(this.originalSegment, this.deflatedSegment, this.compressionLevel, this.lastSegment);
|
||||
|
||||
return new PngEncoderDeflaterSegmentResult(this.originalSegment, this.deflatedSegment, originalSegmentAdler32, originalSegmentLength);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
/**
|
||||
* We save time by allocating and reusing some thread local state.
|
||||
*
|
||||
* Creating a new Deflater instance takes a surprising amount of time.
|
||||
* Resetting an existing Deflater instance is almost free though.
|
||||
*/
|
||||
class PngEncoderDeflaterThreadLocalDeflater {
|
||||
private static final ThreadLocal<PngEncoderDeflaterThreadLocalDeflater> THREAD_LOCAL = ThreadLocal.withInitial(PngEncoderDeflaterThreadLocalDeflater::new);
|
||||
|
||||
static Deflater getInstance(final int compressionLevel) {
|
||||
return PngEncoderDeflaterThreadLocalDeflater.THREAD_LOCAL.get().getDeflater(compressionLevel);
|
||||
}
|
||||
|
||||
private final Deflater[] deflaters;
|
||||
|
||||
private PngEncoderDeflaterThreadLocalDeflater() {
|
||||
this.deflaters = new Deflater[11];
|
||||
for (int compressionLevel = -1; compressionLevel <= 9; compressionLevel++) {
|
||||
boolean nowrap = true;
|
||||
this.deflaters[compressionLevel + 1] = new Deflater(compressionLevel, nowrap);
|
||||
}
|
||||
}
|
||||
|
||||
private Deflater getDeflater(final int compressionLevel) {
|
||||
Deflater deflater = this.deflaters[compressionLevel + 1];
|
||||
deflater.reset();
|
||||
return deflater;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
class PngEncoderIdatChunksOutputStream extends FilterOutputStream {
|
||||
// An IDAT chunk adds 12 bytes of overhead to the data within.
|
||||
// 12 / (32 * 1024) = 0.00037 meaning the size overhead is just 0.037% which should be negligible.
|
||||
static final int DEFAULT_BUFFER_LENGTH = 32 * 1024;
|
||||
|
||||
static final byte[] IDAT_BYTES = "IDAT".getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
private final byte[] buf;
|
||||
private int count;
|
||||
private final CRC32 crc;
|
||||
|
||||
PngEncoderIdatChunksOutputStream(final OutputStream out) {
|
||||
this(out, PngEncoderIdatChunksOutputStream.DEFAULT_BUFFER_LENGTH);
|
||||
}
|
||||
|
||||
PngEncoderIdatChunksOutputStream(final OutputStream out, final int bufferLength) {
|
||||
super(out);
|
||||
this.crc = new CRC32();
|
||||
this.buf = new byte[bufferLength];
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
flushBuffer();
|
||||
super.flush();
|
||||
}
|
||||
|
||||
private void flushBuffer() throws IOException {
|
||||
if (this.count > 0) {
|
||||
writeIdatChunk(this.buf, 0, this.count);
|
||||
this.count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final byte[] b, final int off, final int len) throws IOException {
|
||||
if (len >= this.buf.length) {
|
||||
flushBuffer();
|
||||
writeIdatChunk(b, off, len);
|
||||
return;
|
||||
}
|
||||
if (len > this.buf.length - this.count) {
|
||||
flushBuffer();
|
||||
}
|
||||
System.arraycopy(b, off, this.buf, this.count, len);
|
||||
this.count += len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(final int b) throws IOException {
|
||||
if (this.count >= this.buf.length) {
|
||||
flushBuffer();
|
||||
}
|
||||
this.buf[this.count++] = (byte) b;
|
||||
}
|
||||
|
||||
private void writeIdatChunk(final byte[] b, final int off, final int len) throws IOException {
|
||||
writeInt(len);
|
||||
this.out.write(PngEncoderIdatChunksOutputStream.IDAT_BYTES);
|
||||
this.out.write(b, off, len);
|
||||
this.crc.reset();
|
||||
this.crc.update(PngEncoderIdatChunksOutputStream.IDAT_BYTES);
|
||||
this.crc.update(b, off, len);
|
||||
writeInt((int) this.crc.getValue());
|
||||
}
|
||||
|
||||
private void writeInt(final int i) throws IOException {
|
||||
this.out.write((byte) (i >> 24) & 0xFF);
|
||||
this.out.write((byte) (i >> 16) & 0xFF);
|
||||
this.out.write((byte) (i >> 8) & 0xFF);
|
||||
this.out.write((byte) i & 0xFF);
|
||||
}
|
||||
}
|
138
src/org/atriasoft/pngencoder/PngEncoderLogic.java
Normal file
138
src/org/atriasoft/pngencoder/PngEncoderLogic.java
Normal 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() {}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
/**
|
||||
* Represents PNG physical pixel dimensions
|
||||
*
|
||||
* Use one of the static methods to create physical pixel dimensions based
|
||||
* on pixels per meter, dots per inch or a unit-less aspect ratio.
|
||||
*
|
||||
* @see <a href="https://www.w3.org/TR/PNG/#11pHYs">https://www.w3.org/TR/PNG/#11pHYs</a>
|
||||
*/
|
||||
public class PngEncoderPhysicalPixelDimensions {
|
||||
|
||||
public enum Unit {
|
||||
METER((byte) 1), UNKNOWN((byte) 0);
|
||||
|
||||
private final byte value;
|
||||
|
||||
Unit(final byte value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
private static final float INCHES_PER_METER = 100 / 2.54f;
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions that only specifies the aspect ratio,
|
||||
* but not the size, of the pixels
|
||||
*
|
||||
* @param pixelsPerUnitX the number of pixels per unit in the horizontal dimension
|
||||
* @param pixelsPerUnitY the number of pixels per unit in the vertical dimension
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions aspectRatio(final int pixelsPerUnitX, final int pixelsPerUnitY) {
|
||||
return new PngEncoderPhysicalPixelDimensions(pixelsPerUnitX, pixelsPerUnitY, Unit.UNKNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions with square pixels
|
||||
* with a size specified in dots per inch
|
||||
*
|
||||
* Note that dots per inch (DPI) cannot be exactly represented by the PNG format's
|
||||
* integer value for pixels per meter. There will be a slight rounding error.
|
||||
*
|
||||
* @param dotsPerInch the DPI value for both dimensions
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions dotsPerInch(final int dotsPerInch) {
|
||||
return PngEncoderPhysicalPixelDimensions.dotsPerInch(dotsPerInch, dotsPerInch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions with possibly non-square pixels
|
||||
* with a size specified in dots per inch
|
||||
*
|
||||
* Note that dots per inch (DPI) cannot be exactly represented by the PNG format's
|
||||
* integer value for pixels per meter. There will be a slight rounding error.
|
||||
*
|
||||
* @param dotsPerInchX the DPI value for the horizontal dimension
|
||||
* @param dotsPerInchY the DPI value for the vertical dimension
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions dotsPerInch(final int dotsPerInchX, final int dotsPerInchY) {
|
||||
int pixelsPerMeterX = Math.round(dotsPerInchX * PngEncoderPhysicalPixelDimensions.INCHES_PER_METER);
|
||||
int pixelsPerMeterY = Math.round(dotsPerInchY * PngEncoderPhysicalPixelDimensions.INCHES_PER_METER);
|
||||
|
||||
return new PngEncoderPhysicalPixelDimensions(pixelsPerMeterX, pixelsPerMeterY, Unit.METER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions with square pixels
|
||||
* with a size specified in pixels per meter
|
||||
*
|
||||
* @param pixelsPerMeter the pixels per meter value for both dimensions
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions pixelsPerMeter(final int pixelsPerMeter) {
|
||||
return PngEncoderPhysicalPixelDimensions.pixelsPerMeter(pixelsPerMeter, pixelsPerMeter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PngEncoderPhysicalPixelDimensions with possibly non-square pixels
|
||||
* with a size specified in pixels per meter
|
||||
*
|
||||
* @param pixelsPerMeterX the pixels per meter value for the horizontal dimension
|
||||
* @param pixelsPerMeterY the pixels per meter value for the vertical dimension
|
||||
*/
|
||||
public static PngEncoderPhysicalPixelDimensions pixelsPerMeter(final int pixelsPerMeterX, final int pixelsPerMeterY) {
|
||||
return new PngEncoderPhysicalPixelDimensions(pixelsPerMeterX, pixelsPerMeterY, Unit.METER);
|
||||
}
|
||||
|
||||
private final int pixelsPerUnitX;
|
||||
|
||||
private final int pixelsPerUnitY;
|
||||
|
||||
private final Unit unit;
|
||||
|
||||
private PngEncoderPhysicalPixelDimensions(final int pixelsPerUnitX, final int pixelsPerUnitY, final Unit unit) {
|
||||
this.pixelsPerUnitX = pixelsPerUnitX;
|
||||
this.pixelsPerUnitY = pixelsPerUnitY;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of pixels per unit in the horizontal dimension
|
||||
*/
|
||||
public int getPixelsPerUnitX() {
|
||||
return this.pixelsPerUnitX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of pixels per unit in the vertical dimension
|
||||
*/
|
||||
public int getPixelsPerUnitY() {
|
||||
return this.pixelsPerUnitY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the unit of the pixel size (either {@link Unit#METER} or {@link Unit#UNKNOWN})
|
||||
*/
|
||||
public Unit getUnit() {
|
||||
return this.unit;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package org.atriasoft.pngencoder;
|
||||
|
||||
public enum PngEncoderSrgbRenderingIntent {
|
||||
ABSOLUTE_COLORIMETRIC((byte) 3), PERCEPTUAL((byte) 0), RELATIVE_COLORIMETRIC((byte) 1), SATURATION((byte) 2);
|
||||
|
||||
private final byte value;
|
||||
|
||||
PngEncoderSrgbRenderingIntent(final byte value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
21
src/org/atriasoft/pngencoder/PngEncoderVerificationUtil.java
Normal file
21
src/org/atriasoft/pngencoder/PngEncoderVerificationUtil.java
Normal 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() {}
|
||||
}
|
159
test/PngEncoderBufferedImageConverter.java
Normal file
159
test/PngEncoderBufferedImageConverter.java
Normal 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() {}
|
||||
}
|
BIN
test/resources/looklet-look-scale6.png
Normal file
BIN
test/resources/looklet-look-scale6.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 MiB |
BIN
test/resources/png-encoder-logo.png
Normal file
BIN
test/resources/png-encoder-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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)"));
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
101
test/src/test/atriasoft/pngencoder/PngEncoderLogicTest.java
Normal file
101
test/src/test/atriasoft/pngencoder/PngEncoderLogicTest.java
Normal 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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
221
test/src/test/atriasoft/pngencoder/PngEncoderTest.java
Normal file
221
test/src/test/atriasoft/pngencoder/PngEncoderTest.java
Normal 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));
|
||||
}
|
||||
}
|
59
test/src/test/atriasoft/pngencoder/PngEncoderTestUtil.java
Normal file
59
test/src/test/atriasoft/pngencoder/PngEncoderTestUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
12
test/src/test/atriasoft/pngencoder/Timing.java
Normal file
12
test/src/test/atriasoft/pngencoder/Timing.java
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user