[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