Compare commits

..

100 Commits

Author SHA1 Message Date
Harald Kuhr 488d6da71a [maven-release-plugin] prepare for next development iteration 2022-08-19 17:43:15 +02:00
Harald Kuhr b19e45a911 [maven-release-plugin] prepare release twelvemonkeys-3.8.3 2022-08-19 17:43:11 +02:00
Harald Kuhr a5b6cf898d DiscreteAlphaIndexColorModel num components fix
(cherry picked from commit 190fe87ee9)
2022-08-19 17:39:27 +02:00
Harald Kuhr ce597c437d #694: Fixed import order
(cherry picked from commit d1872ce94f)
2022-08-19 17:39:27 +02:00
Harald Kuhr fa4b3787d0 #694 BMP: Fixed subsampling for 24 bit/pixel case
(cherry picked from commit a5c52a99b4)
2022-08-19 17:39:25 +02:00
Harald Kuhr 3c18e8a510 #684 Add some tolerance for JDK 8...
(cherry picked from commit 4170b393fa)
2022-08-19 17:39:23 +02:00
Harald Kuhr 206481038e #684 Remove TODO as it's now fixed
(cherry picked from commit 53f9ba91e0)
2022-08-19 17:39:23 +02:00
Harald Kuhr cff4d88991 #684 Fix some render size issues in SVGImageReader
Bonus: Minor code clean-up.

(cherry picked from commit be2d7d5f10)
2022-08-19 17:39:22 +02:00
Harald Kuhr 8ea8e061a9 Minor code clean-up for WMFImageReader
(cherry picked from commit 00aec2c90e)
2022-08-19 17:39:22 +02:00
Harald Kuhr 101ad18f71 Minor optimizations.
(cherry picked from commit 2b04f7205c)
2022-08-19 17:39:22 +02:00
Harald Kuhr 08b441a17e #675 PSD 16/32 bit layer support pt2: Cross-platform test
(cherry picked from commit 7d401d0194)
2022-08-19 17:39:20 +02:00
Harald Kuhr b6c76d8566 #675 PSD 16/32 bit layer support
(cherry picked from commit 48691139a3)
2022-08-19 17:39:18 +02:00
Harald Kuhr 3f74b2ddf3 #681: Fix for little-endian "packed" USHORT types + rewritten stream handling
(cherry picked from commit bcb87c09d2)
2022-08-19 17:39:17 +02:00
Harald Kuhr 46b48f32c3 #683: Fix TIFF stripByteCounts computation for uncompressed data
(cherry picked from commit 84a8ceeb93)
2022-08-19 17:39:17 +02:00
Harald Kuhr a07d0285fe A new ImageInputStream adapter for InputStream.
(cherry picked from commit 0cb99feedf)
2022-08-19 17:39:15 +02:00
Harald Kuhr 98de4ad4ec #682 TIFF Lab w/alpha support
(cherry picked from commit 91493c5145)
2022-08-19 17:39:13 +02:00
Harald Kuhr aa82612765 Fix bad format in validator message.
(cherry picked from commit 6ddb799a95)
2022-08-19 17:39:12 +02:00
Harald Kuhr 9213da3184 TGAImageReader no longer reads single byte 0-terminator as Image Identification
(cherry picked from commit 8a187f6657)
2022-08-19 17:39:10 +02:00
Harald Kuhr a5e2226a5a #680 TGAImageReader now reads attribute bits with no extension area as alpha
(cherry picked from commit b7d865f2cf)
2022-08-19 17:39:09 +02:00
Harald Kuhr 773bedccca Fix bug in 0-terminated ASCII string parsing + test.
(cherry picked from commit d50fb1a51e)
2022-08-19 17:39:08 +02:00
Harald Kuhr 6bcc17a020 Documentation & details.
(cherry picked from commit 8992406f50)
2022-08-19 17:39:06 +02:00
Harald Kuhr 37d1da9b9d #678, #679: TIFF read support for YCbCr Planar with or without subsampling
(cherry picked from commit 44eebff62f)
2022-08-19 17:39:04 +02:00
Harald Kuhr 8cf1405dfc Simplified TIFF writing.
(cherry picked from commit 8c85c4ca96)
2022-08-19 17:39:02 +02:00
Harald Kuhr 8c37d19928 Added test cases for EncoderStream/DecoderStream and fixed a bug
+ code clean-up to make IntelliJ happy :-)

(cherry picked from commit fa5c77bff0)
2022-08-19 17:39:00 +02:00
Harald Kuhr 87cd506fdd PCX: Minor clean up
(cherry picked from commit d87b80deea)
2022-08-19 17:39:00 +02:00
Harald Kuhr e0c7edebbd Write LONG8 offsets for BigTIFF
(cherry picked from commit ae138c3b4e)
2022-08-19 17:39:00 +02:00
Harald Kuhr 5d13bd653f #677 Fixed integer overflow + added tests
(cherry picked from commit ab13fdd09d)
2022-08-19 17:38:59 +02:00
Harald Kuhr 2d974874a9 Fixed test typo.
(cherry picked from commit aab5b062bd)
2022-08-19 17:38:59 +02:00
Harald Kuhr f625622b10 [skip ci] Fixed some typos in comments. :-)
(cherry picked from commit 00d6acd1bf)
2022-08-19 17:38:59 +02:00
Harald Kuhr dbdd7ae3f1 Update feature_request.md
(cherry picked from commit 0f8a7ea482)
2022-08-19 17:38:59 +02:00
Harald Kuhr 73883ebf99 #672: WebPImageReader now supports unknown stream lengths
(cherry picked from commit 9fe87fe10d)
2022-08-19 17:38:59 +02:00
Harald Kuhr 970b238066 #666 Clean-up: No alpha for RGB 3/components
(cherry picked from commit 9e2f369459)
2022-08-19 17:36:49 +02:00
Harald Kuhr 6cb8ac4b68 #666 Support for TIFF RGB 2/4 bit per sample.
(cherry picked from commit d34b0b7fcf)
2022-08-19 17:36:49 +02:00
snyk-bot 1a2a4edfe8 fix: upgrade jmagick:jmagick from 6.2.4 to 6.6.9
Snyk has created this PR to upgrade jmagick:jmagick from 6.2.4 to 6.6.9.

See this package in Maven Repository:
https://mvnrepository.com/artifact/jmagick/jmagick/

See this project in Snyk:
https://app.snyk.io/org/haraldk/project/eca06326-94ac-456d-a029-f411089e7f16?utm_source=github&utm_medium=referral&page=upgrade-pr

(cherry picked from commit 5effcb1344)
2022-08-19 17:36:49 +02:00
Oliver Schmidtmer a12a1f73b5 TIFFImageMetadata: ImageOrientation in mergeTree (#667)
TIFFImageMetadata: ImageOrientation in mergeTree
(cherry picked from commit b67d687761)
2022-08-19 17:36:49 +02:00
Harald Kuhr 46bfdd93d8 [maven-release-plugin] prepare for next development iteration 2022-02-22 14:49:45 +01:00
Harald Kuhr 447ef6b8eb [maven-release-plugin] prepare release twelvemonkeys-3.8.2 2022-02-22 14:49:41 +01:00
Harald Kuhr c19b6ede0a Back to 3.8.2-SNAPSHOT [skip ci] 2022-02-22 14:48:06 +01:00
Harald Kuhr 401c6355a1 Attempt at adding JavaDocs + use "release" profile for release-plugin [skip ci]
(cherry picked from commit d0881c8b5c)
2022-02-22 14:47:28 +01:00
Harald Kuhr be0cf16124 [maven-release-plugin] prepare for next development iteration 2022-02-22 13:31:01 +01:00
Harald Kuhr 47b0cd6e9a [maven-release-plugin] prepare release twelvemonkeys-3.8.2 2022-02-22 13:30:56 +01:00
Harald Kuhr b52ab149b3 Back to 3.8.2-SNAPSHOT [skip ci] 2022-02-22 13:28:52 +01:00
Harald Kuhr 900c26a5ac [maven-release-plugin] prepare release twelvemonkeys-3.8.2 2022-02-22 13:22:39 +01:00
Harald Kuhr 7233c593ac Adding description to top-level POM [skip ci]
(cherry picked from commit 976928f48c)
2022-02-22 13:20:15 +01:00
Harald Kuhr 8159ba1245 [maven-release-plugin] prepare for next development iteration 2022-02-21 16:18:51 +01:00
Harald Kuhr 92581a077b [maven-release-plugin] prepare release twelvemonkeys-3.8.2 2022-02-21 16:18:44 +01:00
Harald Kuhr f87b4d6748 Thank you, Travis [skip ci]
(cherry picked from commit e1c2f2ee73)
2022-02-21 16:15:35 +01:00
Harald Kuhr 0495f6e266 #661: JUnit test results take 4
(cherry picked from commit 92632fa2a3)
2022-02-21 16:15:33 +01:00
Harald Kuhr bc7c8ba20c #661: JUnit test results take 3
(cherry picked from commit 5a563e315f)
2022-02-21 16:15:31 +01:00
Harald Kuhr 0c270f8343 #661: JUnit test results take 2
(cherry picked from commit c06d47d123)
2022-02-21 16:15:28 +01:00
Harald Kuhr e1db332dca #661: JUnit test results
(cherry picked from commit fea6beb364)
2022-02-21 16:15:26 +01:00
Harald Kuhr 016977e382 PNM: New attempt at making the new header parser work on Windows.
(cherry picked from commit 4b951c06cc)
2022-02-21 16:15:23 +01:00
Harald Kuhr 134eecc59f PNM Windows issue. Temporary roll-back to working version.
(cherry picked from commit a3e6e52c95)
2022-02-21 16:15:23 +01:00
Harald Kuhr 16c78052ee PNM clean-up: Avoid leading/trailing whitespace in comments.
(cherry picked from commit 5347015cbd)
2022-02-21 16:15:20 +01:00
Harald Kuhr b51e8ccf6e PNM clean-up.
(cherry picked from commit 4d190892df)
2022-02-21 16:15:18 +01:00
Harald Kuhr 76a9ff1122 #660: Attempt at making the comment parsing more Windows-friendly...
(cherry picked from commit 60eab81709)
2022-02-21 16:15:17 +01:00
Harald Kuhr e9996f096f #660: Make sure region is within bounds of new test image...
(cherry picked from commit b400b6b157)
2022-02-21 16:15:17 +01:00
Harald Kuhr 93d42e1c24 #660: Farewell, Lena
(cherry picked from commit 499b3ef120)
2022-02-21 16:15:17 +01:00
Harald Kuhr 5824167600 IFF: Simplified aspect.
(cherry picked from commit 92bc9c73f6)
2022-02-21 16:15:14 +01:00
Harald Kuhr 126956ebd0 IFF: XS24 clean-up (again...)
(cherry picked from commit 2a77558cac)
2022-02-21 16:15:12 +01:00
Harald Kuhr d54ceba3ff IFF: XS24 clean-up
(cherry picked from commit 816cad60a8)
2022-02-21 16:15:11 +01:00
Harald Kuhr 11ee7e5e23 IFF: Thumbnail support for XS24 chunk (now without stderr output)
(cherry picked from commit 7167f81c69)
2022-02-21 16:15:11 +01:00
Harald Kuhr 954dffd213 IFF: Thumbnail support for XS24 chunk.
(cherry picked from commit f5cfa0e619)
2022-02-21 16:15:08 +01:00
Harald Kuhr b7b2a61c93 IFF: Read support for TVPaint DEEP and TVPP
+ Bonus: Massive code clean-up/refactor.

(cherry picked from commit 73ad024833)
2022-02-21 16:15:05 +01:00
Harald Kuhr 3cf6a4b836 IFF: More clean-up
(cherry picked from commit 379449b621)
2022-02-21 16:15:04 +01:00
Harald Kuhr a93be99933 IFF: Read support for Impulse (Imagine, Turbo Silver) RGB8 format.
(cherry picked from commit e17faad6fb)
2022-02-21 16:15:04 +01:00
Harald Kuhr b19bd1441f IFF clean-up.
(cherry picked from commit 1271a3d55e)
2022-02-21 16:15:04 +01:00
Harald Kuhr 482af60534 RIP: Sandbox
(cherry picked from commit 1cd594d113)
2022-02-21 16:15:00 +01:00
Harald Kuhr f55a6d30dd A little safer way to skip 6 bytes...
(cherry picked from commit b76f74e79a)
2022-02-21 16:14:56 +01:00
Harald Kuhr e5f6227479 #658: TGAImageReader now allows extension area of size 0
(cherry picked from commit 78817a489b)
2022-02-21 16:14:53 +01:00
Harald Kuhr 01d6fc8b49 #658: TGAImageReaderSpi now recognizes "true color" images with valid palette depth != 0
(cherry picked from commit b8f2a80ca6)
2022-02-21 16:14:51 +01:00
Oliver Schmidtmer f133ea7d61 findCompressionType always uses RLE if leading EOL is missing (#657)
Update of the last read byte is missing since the last update. So if only the first EOL is missing, further EOLs after the lines are not detected.

(cherry picked from commit ac8a36db1c)
2022-02-21 16:14:48 +01:00
Harald Kuhr 3d5cf0eecd #655 Experimental force raster conversion switch.
(cherry picked from commit 7e0d8922da)
2022-02-21 16:14:45 +01:00
Harald Kuhr 975e23c28f Fix for IIOInvalidTreeException: Invalid DHT node #559
(cherry picked from commit 9a6b8c9bfe)
2022-02-21 16:14:42 +01:00
Harald Kuhr be60f307f7 #656 Code clean-up + minor refactorings.
(cherry picked from commit eced5b8efd)
2022-02-21 16:14:39 +01:00
Oliver Schmidtmer 3e783fba92 Support writing ASCII array in TIFF metadata (#656)
* Support writing ASCII array in TIFF metadata

* corrected formatting and extracted string writing to method

(cherry picked from commit 74611e4e52)
2022-02-21 16:14:37 +01:00
Harald Kuhr 35ffe29e03 New CI badge + new maven badges, replaces #653
(cherry picked from commit b8614eca4d)
2022-02-21 16:14:35 +01:00
Harald Kuhr 55155aa61c #636: Correct name for shaded artifact.
(cherry picked from commit efd24456ac)
2022-02-21 16:14:32 +01:00
Gauthier 9e9decd5dd add support for Github Actions, publish snapshots to OSSRH automatically (#633)
* remove oss-parent

* add github workflow

* use java 16 for now

* disable fail fast

* add java 15

* use only java 8 and 11 for now

* snapshot deploy

* snapshot deploy

* oracle jdk

* oracle jdk

* oracle jdk

* kcms matrix

* kcms job name

* only deploy for snapshots

* try not operator

* prepare PR

* restore groupId

* Fixed Travis link + bonus project summary updates

* Readme improvements

* #629: Preliminary WebP animation (ANIM/ANMF) support

* #629: Fixed build

* Make tests pass on JDK 16 and 17 (#635)

* make tests pass on JDK 16 and 17
replace deprecated mockito-all by mockito-core, and updated to latest 3.x
replace deprecated org.mockito.Matchers

* code cleanup from IDE suggestions

* add oracle jdk 16 and 17 to Travis

* test on java 17

* try to fix warning about maven-source-plugin

Co-authored-by: Harald Kuhr <harald.kuhr@gmail.com>
(cherry picked from commit 191b2371c8)
2022-02-21 16:14:30 +01:00
Harald Kuhr 3c0b78549e #652: Avoid OOME for large values outside TIFF, even if length is unknown
(cherry picked from commit 33419ef291)
2022-02-21 16:14:27 +01:00
Harald Kuhr 46db5a2fbf #648: (Re-)Added support for nested layer groups
(cherry picked from commit 123f0bb7fc)
2022-02-21 16:14:25 +01:00
Harald Kuhr f6a9477279 #648: Removed unnecessary parentheses.
(cherry picked from commit 99b5f28a49)
2022-02-21 16:14:24 +01:00
Harald Kuhr b7192ae857 #648: Simplified logic, code style fixes and clean up.
(cherry picked from commit b30fb4f8c3)
2022-02-21 16:14:24 +01:00
Jack Yun c0b2769e3b Support Group Layer in psd (#648)
(cherry picked from commit dc0bdcbd5b)
2022-02-21 16:14:24 +01:00
Harald Kuhr 6c27ec6b30 Updated with latest versions.
(cherry picked from commit 0cf29c167d)
2022-02-21 16:14:21 +01:00
Harald Kuhr 0c90196357 [maven-release-plugin] prepare for next development iteration 2021-12-27 09:53:44 +01:00
Harald Kuhr 48f82a159f [maven-release-plugin] prepare release twelvemonkeys-3.8.1 2021-12-27 09:53:40 +01:00
Harald Kuhr 7105738811 #651: Fix ExtraSamplesColorModel equals + hashcode to behave nicely with ImageTypeSpecifier comparison.
(cherry picked from commit 98e4b76206)
2021-12-24 13:00:15 +01:00
Harald Kuhr 10aa4ba41e Minor clean-up.
(cherry picked from commit aa4b5db054)
2021-12-24 13:00:15 +01:00
Harald Kuhr 6fb06da4d7 #651: Fix ExtraSamplesColorModel to create correct length elements array.
(cherry picked from commit 433311c10d)
2021-12-24 13:00:15 +01:00
Harald Kuhr a963e1c355 Alternative fix for #650: Allow usage in OSGi environment.
(cherry picked from commit f50178bc78)
2021-12-23 11:02:46 +01:00
Snyk bot 966a9da45d fix: upgrade commons-io:commons-io from 2.9.0 to 2.11.0 (#647)
Snyk has created this PR to upgrade commons-io:commons-io from 2.9.0 to 2.11.0.

See this package in Maven Repository:
https://mvnrepository.com/artifact/commons-io/commons-io/

See this project in Snyk:
https://app.snyk.io/org/haraldk/project/9a1f6304-68e0-49c5-af4f-db1f87bd4f90?utm_source=github&utm_medium=referral&page=upgrade-pr

(cherry picked from commit e016e970e5)
2021-12-16 08:48:35 +01:00
Harald Kuhr 319b2c4e18 Overriding transitive dependency.
(cherry picked from commit 444aeabf21)
2021-12-15 17:01:38 +01:00
Harald Kuhr e9bf7d080c Getting rid of the dependencies too.
(cherry picked from commit 05507a59d6)
2021-12-15 16:29:53 +01:00
Harald Kuhr fb3691e2ee Delete deprecated servlet classes
(cherry picked from commit c4c89a0a25)
2021-12-15 16:24:41 +01:00
Harald Kuhr 25f9cc5c55 Delete deprecated Servlet classes
(cherry picked from commit b0ad6b2a4b)
2021-12-15 16:24:41 +01:00
Harald Kuhr 94777ddc96 #646: Spi now recognizes VP8 encoded images in VP8X ("extended format").
(cherry picked from commit 25c703f4b2)
2021-12-15 16:09:33 +01:00
Oleh Astappiev a4c12d0d64 Create jakartified package on build (#636)
* feat(servlet): create jakartified package on build

* feat(servlet): update README to include Jakarta classifier

(cherry picked from commit 529c59f93f)
2021-12-15 16:09:33 +01:00
Harald Kuhr 08a69886b1 Updated with the latest versions.
(cherry picked from commit 584b1d9b21)
2021-12-15 16:09:33 +01:00
Harald Kuhr ab85ff0ec8 Prepare for next version. 2021-12-15 16:09:06 +01:00
120 changed files with 2527 additions and 6152 deletions
+9 -9
View File
@@ -9,11 +9,11 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
java: [ 8, 11, 17, 18 ]
java: [ 8, 11, 17 ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: ${{ matrix.java }}
@@ -22,7 +22,7 @@ jobs:
- name: Run Tests
run: mvn test
- name: Publish Test Report
uses: mikepenz/action-junit-report@v3
uses: mikepenz/action-junit-report@v2
if: ${{ !cancelled() }}
with:
report_paths: "**/target/surefire-reports/TEST*.xml"
@@ -35,11 +35,11 @@ jobs:
matrix:
kcms: [ true, false ]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- run: |
download_url="https://javadl.oracle.com/webapps/download/AutoDL?BundleId=245038_d3c52aa6bfa54d3ca74e617f18309292"
wget -O $RUNNER_TEMP/java_package.tar.gz $download_url
- uses: actions/setup-java@v3
- uses: actions/setup-java@v2
with:
distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz
@@ -54,7 +54,7 @@ jobs:
- name: Run Tests
run: mvn test
- name: Publish Test Report
uses: mikepenz/action-junit-report@v3
uses: mikepenz/action-junit-report@v2
if: ${{ !cancelled() }}
with:
report_paths: "**/target/surefire-reports/TEST*.xml"
@@ -66,9 +66,9 @@ jobs:
if: github.ref == 'refs/heads/master' # only perform on latest master
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Maven Central
uses: actions/setup-java@v3
uses: actions/setup-java@v2
with: # running setup-java again overwrites the settings.xml
distribution: 'temurin'
java-version: '8'
+2 -2
View File
@@ -50,7 +50,7 @@ As there is lots of legacy data out there, we see the need for open implementati
**Important note on using Batik:** *Please read [The Apache™ XML Graphics Project - Security](https://xmlgraphics.apache.org/security.html),
and make sure you use an updated and secure version.*
and make sure you use version 1.14 or later.*
Note that GIF, PNG and WBMP formats are already supported through the ImageIO API, using the
[JDK standard plugins](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/javax/imageio/package-summary.html).
@@ -523,7 +523,7 @@ q: What about JAI? Several of the formats are already supported by JAI.
a: While JAI (and jai-imageio in particular) have support for some of the same formats, JAI has some major issues.
The most obvious being:
- It's not actively developed. No issue has been fixed for years.
- It's not actively developed. No issues has been fixed for years.
- To get full format support, you need native libs.
Native libs does not exist for several popular platforms/architectures, and further the native libs are not open source.
Some environments may also prevent deployment of native libs, which brings us back to square one.
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<groupId>com.twelvemonkeys.bom</groupId>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>common-image</artifactId>
<packaging>jar</packaging>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>common-io</artifactId>
<packaging>jar</packaging>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>common-lang</artifactId>
<packaging>jar</packaging>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<groupId>com.twelvemonkeys.contrib</groupId>
<artifactId>contrib</artifactId>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-batik</artifactId>
<name>TwelveMonkeys :: ImageIO :: Batik Plugin</name>
@@ -17,7 +17,7 @@
<properties>
<project.jpms.module.name>com.twelvemonkeys.imageio.batik</project.jpms.module.name>
<batik.version>1.15</batik.version>
<batik.version>1.14</batik.version>
</properties>
<build>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-bmp</artifactId>
<name>TwelveMonkeys :: ImageIO :: BMP plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-clippath</artifactId>
<name>TwelveMonkeys :: ImageIO :: Photoshop Path Support</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-core</artifactId>
<name>TwelveMonkeys :: ImageIO :: Core</name>
@@ -1,627 +0,0 @@
package com.twelvemonkeys.imageio;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.*;
import java.awt.color.*;
import java.awt.image.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static com.twelvemonkeys.imageio.StandardImageMetadataSupport.ColorSpaceType.*;
import static com.twelvemonkeys.lang.Validate.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* Base class for easy read-only implementation of the standard image metadata format.
* Chroma, Data and Transparency nodes values are based on the required
* {@link ImageTypeSpecifier}.
* Other values or overrides may be specified using the builder.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
public class StandardImageMetadataSupport extends AbstractMetadata {
// The only required field, most standard metadata can be extracted from the type
private final ImageTypeSpecifier type;
protected final ColorSpaceType colorSpaceType;
protected final boolean blackIsZero;
private final IndexColorModel palette;
protected final String compressionName;
protected final boolean compressionLossless;
protected final PlanarConfiguration planarConfiguration;
private final int[] bitsPerSample;
private final int[] significantBits;
private final int[] sampleMSB;
protected final Double pixelAspectRatio;
protected final ImageOrientation orientation;
protected final String formatVersion;
protected final SubimageInterpretation subimageInterpretation;
private final Calendar documentCreationTime; // TODO: This field should be a LocalDateTime or other java.time type, Consider a long timestamp + TimeZone to avoid messing up the API...
private final Collection<TextEntry> textEntries;
protected StandardImageMetadataSupport(Builder builder) {
notNull(builder, "builder");
// Baseline
type = builder.type;
// Chroma
colorSpaceType = builder.colorSpaceType;
blackIsZero = builder.blackIsZero;
palette = builder.palette;
// Compression
compressionName = builder.compressionName;
compressionLossless = builder.compressionLossless;
// Data
planarConfiguration = builder.planarConfiguration;
bitsPerSample = builder.bitsPerSample;
significantBits = builder.significantBits;
sampleMSB = builder.sampleMSB;
// Dimension
orientation = builder.orientation;
pixelAspectRatio = builder.pixelAspectRatio;
// Document
formatVersion = builder.formatVersion;
documentCreationTime = builder.documentCreationTime;
subimageInterpretation = builder.subimageInterpretation;
// Text
textEntries = builder.textEntries;
}
public static Builder builder(ImageTypeSpecifier type) {
return new Builder(type);
}
public static class Builder {
private final ImageTypeSpecifier type;
private ColorSpaceType colorSpaceType;
private boolean blackIsZero = true;
private IndexColorModel palette;
private String compressionName;
private boolean compressionLossless = true;
private PlanarConfiguration planarConfiguration;
public int[] bitsPerSample;
private int[] significantBits;
private int[] sampleMSB;
private Double pixelAspectRatio;
private ImageOrientation orientation = ImageOrientation.Normal;
private String formatVersion;
private SubimageInterpretation subimageInterpretation;
private Calendar documentCreationTime; // TODO: This field should be a LocalDateTime or other java.time type
private final Collection<TextEntry> textEntries = new ArrayList<>();
protected Builder(ImageTypeSpecifier type) {
this.type = notNull(type, "type");
}
public Builder withColorSpaceType(ColorSpaceType colorSpaceType) {
this.colorSpaceType = colorSpaceType;
return this;
}
public Builder withBlackIsZero(boolean blackIsZero) {
this.blackIsZero = blackIsZero;
return this;
}
public Builder withPalette(IndexColorModel palette) {
this.palette = palette;
return this;
}
public Builder withCompressionTypeName(String compressionName) {
this.compressionName = notNull(compressionName, "compressionName").equalsIgnoreCase("none") ? null : compressionName;
return this;
}
public Builder withCompressionLossless(boolean lossless) {
this.compressionLossless = isTrue(lossless || compressionName != null, lossless, "Lossy compression requires compression name");
return this;
}
public Builder withPlanarConfiguration(PlanarConfiguration planarConfiguration) {
this.planarConfiguration = planarConfiguration;
return this;
}
public Builder withBitsPerSample(int... bitsPerSample) {
this.bitsPerSample = bitsPerSample;
return this;
}
public Builder withSignificantBitsPerSample(int... significantBits) {
this.significantBits = isTrue(significantBits.length == 1 || significantBits.length == type.getNumBands(),
significantBits,
String.format("single value or %d values expected", type.getNumBands()));
return this;
}
public Builder withSampleMSB(int... sampleMSB) {
this.sampleMSB = isTrue(sampleMSB.length == 1 || sampleMSB.length == type.getNumBands(),
sampleMSB,
String.format("single value or %d values expected", type.getNumBands()));
return this;
}
public Builder withPixelAspectRatio(Double pixelAspectRatio) {
this.pixelAspectRatio = pixelAspectRatio;
return this;
}
public Builder withOrientation(ImageOrientation orientation) {
this.orientation = notNull(orientation, "orientation");
return this;
}
public Builder withFormatVersion(String formatVersion) {
this.formatVersion = notNull(formatVersion, "formatVersion");
return this;
}
public Builder withSubimageInterpretation(SubimageInterpretation interpretation) {
this.subimageInterpretation = interpretation;
return this;
}
public Builder withDocumentCreationTime(Calendar creationTime) {
this.documentCreationTime = creationTime;
return this;
}
public Builder withTextEntries(Map<String, String> entries) {
return withTextEntries(toTextEntries(notNull(entries, "entries").entrySet()));
}
private Collection<TextEntry> toTextEntries(Collection<Map.Entry<String, String>> entries) {
TextEntry[] result = new TextEntry[entries.size()];
int i = 0;
for (Map.Entry<String, String> entry : entries) {
result[i++] = new TextEntry(entry.getKey(), entry.getValue());
}
return Arrays.asList(result);
}
public Builder withTextEntries(Collection<TextEntry> entries) {
this.textEntries.addAll(notNull(entries, "entries"));
return this;
}
public Builder withTextEntry(String keyword, String value) {
if (value != null && !value.isEmpty()) {
this.textEntries.add(new TextEntry(notNull(keyword, "keyword"), value));
}
return this;
}
public IIOMetadata build() {
return new StandardImageMetadataSupport(this);
}
}
protected enum ColorSpaceType {
XYZ(3),
Lab(3),
Luv(3),
YCbCr(3),
Yxy(3),
YCCK(4),
PhotoYCC(3),
RGB(3),
GRAY(1),
HSV(3),
HLS(3),
CMYK(3),
CMY(3),
// Generic types (so much extra work, because Java names can't start with a number, phew...)
GENERIC_2CLR(2, "2CLR"),
GENERIC_3CLR(3, "3CLR"),
GENERIC_4CLR(4, "4CLR"),
GENERIC_5CLR(5, "5CLR"),
GENERIC_6CLR(6, "6CLR"),
GENERIC_7CLR(7, "7CLR"),
GENERIC_8CLR(8, "8CLR"),
GENERIC_9CLR(9, "9CLR"),
GENERIC_ACLR(0xA, "ACLR"),
GENERIC_BCLR(0xB, "BCLR"),
GENERIC_CCLR(0xC, "CCLR"),
GENERIC_DCLR(0xD, "DCLR"),
GENERIC_ECLR(0xE, "ECLR"),
GENERIC_FCLR(0xF, "FCLR");
final int numChannels;
private final String nameOverride;
ColorSpaceType(int numChannels) {
this(numChannels, null);
}
ColorSpaceType(int numChannels, String nameOverride) {
this.numChannels = numChannels;
this.nameOverride = nameOverride;
}
@Override
public String toString() {
return nameOverride != null ? nameOverride : super.toString();
}
}
protected enum PlanarConfiguration {
PixelInterleaved,
PlaneInterleaved,
LineInterleaved,
TileInterleaved
}
protected enum ImageOrientation {
Normal,
Rotate90,
Rotate180,
Rotate270,
FlipH,
FlipV,
FlipHRotate90,
FlipVRotate90
}
protected enum SubimageInterpretation {
Standalone,
SinglePage,
FullResolution,
ReducedResolution,
PyramidLayer,
Preview,
VolumeSlice,
ObjectView,
Panorama,
AnimationFrame,
TransparencyMask,
CompositingLayer,
SpectralSlice,
Unknown
}
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chromaNode = new IIOMetadataNode("Chroma");
ColorModel colorModel = colorSpaceType != null ? null : type.getColorModel();
ColorSpaceType csType = colorSpaceType != null ? colorSpaceType : colorSpaceType(colorModel.getColorSpace());
int numComponents = colorSpaceType != null ? colorSpaceType.numChannels : colorModel.getNumComponents();
IIOMetadataNode colorSpaceTypeNode = new IIOMetadataNode("ColorSpaceType");
chromaNode.appendChild(colorSpaceTypeNode);
colorSpaceTypeNode.setAttribute("name", csType.toString());
IIOMetadataNode numChannelsNode = new IIOMetadataNode("NumChannels");
numChannelsNode.setAttribute("value", String.valueOf(numComponents));
chromaNode.appendChild(numChannelsNode);
IIOMetadataNode blackIsZeroNode = new IIOMetadataNode("BlackIsZero");
blackIsZeroNode.setAttribute("value", booleanString(blackIsZero));
chromaNode.appendChild(blackIsZeroNode);
if (colorModel instanceof IndexColorModel || palette != null) {
IndexColorModel colorMap = palette != null ? palette : (IndexColorModel) colorModel;
IIOMetadataNode paletteNode = new IIOMetadataNode("Palette");
chromaNode.appendChild(paletteNode);
for (int i = 0; i < colorMap.getMapSize(); i++) {
IIOMetadataNode paletteEntryNode = new IIOMetadataNode("PaletteEntry");
paletteNode.appendChild(paletteEntryNode);
paletteEntryNode.setAttribute("index", Integer.toString(i));
paletteEntryNode.setAttribute("red", Integer.toString(colorMap.getRed(i)));
paletteEntryNode.setAttribute("green", Integer.toString(colorMap.getGreen(i)));
paletteEntryNode.setAttribute("blue", Integer.toString(colorMap.getBlue(i)));
// Assumption: BITMASK transparency will use single transparent pixel
if (colorMap.getTransparency() == Transparency.TRANSLUCENT) {
paletteEntryNode.setAttribute("alpha", Integer.toString(colorMap.getAlpha(i)));
}
}
if (colorMap.getTransparentPixel() != -1) {
IIOMetadataNode backgroundIndexNode = new IIOMetadataNode("BackgroundIndex");
chromaNode.appendChild(backgroundIndexNode);
backgroundIndexNode.setAttribute("value", Integer.toString(colorMap.getTransparentPixel()));
}
}
// TODO: BackgroundColor?
return chromaNode;
}
private static ColorSpaceType colorSpaceType(ColorSpace colorSpace) {
switch (colorSpace.getType()) {
case ColorSpace.TYPE_XYZ:
return XYZ;
case ColorSpace.TYPE_Lab:
return Lab;
case ColorSpace.TYPE_Luv:
return Luv;
case ColorSpace.TYPE_YCbCr:
return YCbCr;
case ColorSpace.TYPE_Yxy:
return Yxy;
// Note: Can't map to YCCK or PhotoYCC, as there's no corresponding constant in java.awt.ColorSpace
case ColorSpace.TYPE_RGB:
return RGB;
case ColorSpace.TYPE_GRAY:
return GRAY;
case ColorSpace.TYPE_HSV:
return HSV;
case ColorSpace.TYPE_HLS:
return HLS;
case ColorSpace.TYPE_CMYK:
return CMYK;
case ColorSpace.TYPE_CMY:
return CMY;
default:
int numComponents = colorSpace.getNumComponents();
if (numComponents == 1) {
return GRAY;
}
else if (numComponents < 16) {
return ColorSpaceType.valueOf("GENERIC_" + Integer.toHexString(numComponents) + "CLR");
}
}
throw new IllegalArgumentException("Unknown ColorSpace type: " + colorSpace);
}
protected static final class TextEntry {
static final List<String> COMPRESSIONS = Arrays.asList("none", "lzw", "zip", "bzip", "other");
final String keyword;
final String value;
final String language;
final String encoding;
final String compression;
public TextEntry(final String keyword, final String value) {
this(keyword, value, null, null, null);
}
public TextEntry(final String keyword, final String value, final String language, final String encoding, final String compression) {
this.keyword = keyword;
this.value = notNull(value, "value");
this.language = language;
this.encoding = encoding;
this.compression = isTrue(compression == null || COMPRESSIONS.contains(compression), compression, String.format("Unknown compression: %s (expected: %s)", compression, COMPRESSIONS));
}
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
if (compressionName == null) {
return null;
}
IIOMetadataNode node = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
compressionTypeName.setAttribute("value", compressionName);
node.appendChild(compressionTypeName);
IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
lossless.setAttribute("value", booleanString(compressionLossless));
node.appendChild(lossless);
return node;
}
protected static String booleanString(boolean booleanValue) {
return booleanValue ? "TRUE" : "FALSE";
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode dataNode = new IIOMetadataNode("Data");
IIOMetadataNode planarConfigurationNode = new IIOMetadataNode("PlanarConfiguration");
dataNode.appendChild(planarConfigurationNode);
planarConfigurationNode.setAttribute("value", planarConfiguration != null ? planarConfiguration.toString() :
(type.getSampleModel() instanceof BandedSampleModel ? "PlaneInterleaved" : "PixelInterleaved"));
String sampleFormatValue = colorSpaceType == null && type.getColorModel() instanceof IndexColorModel
? "Index"
: sampleFormat(type.getSampleModel());
if (sampleFormatValue != null) {
IIOMetadataNode sampleFormatNode = new IIOMetadataNode("SampleFormat");
sampleFormatNode.setAttribute("value", sampleFormatValue);
dataNode.appendChild(sampleFormatNode);
}
int[] bitsPerSample = this.bitsPerSample != null ? this.bitsPerSample : type.getSampleModel().getSampleSize();
IIOMetadataNode bitsPerSampleNode = new IIOMetadataNode("BitsPerSample");
bitsPerSampleNode.setAttribute("value", createListValue(bitsPerSample.length, bitsPerSample));
dataNode.appendChild(bitsPerSampleNode);
if (significantBits != null) {
String significantBitsValue = createListValue(type.getNumBands(), significantBits);
if (!significantBitsValue.equals(bitsPerSampleNode.getAttribute("value"))) {
IIOMetadataNode significantBitsPerSampleNode = new IIOMetadataNode("SignificantBitsPerSample");
significantBitsPerSampleNode.setAttribute("value", significantBitsValue);
dataNode.appendChild(significantBitsPerSampleNode);
}
}
if (sampleMSB != null) {
// TODO: Only if different from default!
IIOMetadataNode sampleMSBNode = new IIOMetadataNode("SampleMSB");
sampleMSBNode.setAttribute("value", createListValue(type.getNumBands(), sampleMSB));
dataNode.appendChild(sampleMSBNode);
}
return dataNode;
}
private static String createListValue(final int itemCount, final int... values) {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < itemCount; i++) {
if (buffer.length() > 0) {
buffer.append(' ');
}
buffer.append(values[i % values.length]);
}
return buffer.toString();
}
private static String sampleFormat(SampleModel sampleModel) {
switch (sampleModel.getDataType()) {
case DataBuffer.TYPE_SHORT:
case DataBuffer.TYPE_INT:
if (sampleModel instanceof ComponentSampleModel) {
return "SignedIntegral";
}
// Otherwise fall-through, most likely a *PixelPackedSampleModel
case DataBuffer.TYPE_BYTE:
case DataBuffer.TYPE_USHORT:
return "UnsignedIntegral";
case DataBuffer.TYPE_FLOAT:
case DataBuffer.TYPE_DOUBLE:
return "Real";
default:
return null;
}
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimensionNode = new IIOMetadataNode("Dimension");
if (pixelAspectRatio != null) {
IIOMetadataNode pixelAspectRatioNode = new IIOMetadataNode("PixelAspectRatio");
pixelAspectRatioNode.setAttribute("value", String.valueOf(pixelAspectRatio));
dimensionNode.appendChild(pixelAspectRatioNode);
}
IIOMetadataNode imageOrientationNode = new IIOMetadataNode("ImageOrientation");
imageOrientationNode.setAttribute("value", orientation.toString());
dimensionNode.appendChild(imageOrientationNode);
return dimensionNode.hasChildNodes() ? dimensionNode : null;
}
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode documentNode = new IIOMetadataNode("Document");
if (formatVersion != null) {
IIOMetadataNode formatVersionNode = new IIOMetadataNode("FormatVersion");
documentNode.appendChild(formatVersionNode);
formatVersionNode.setAttribute("value", formatVersion);
}
if (subimageInterpretation != null) {
IIOMetadataNode subImageInterpretationNode = new IIOMetadataNode("SubimageInterpretation");
documentNode.appendChild(subImageInterpretationNode);
subImageInterpretationNode.setAttribute("value", subimageInterpretation.toString());
}
if (documentCreationTime != null) {
IIOMetadataNode imageCreationTimeNode = new IIOMetadataNode("ImageCreationTime");
documentNode.appendChild(imageCreationTimeNode);
imageCreationTimeNode.setAttribute("year", String.valueOf(documentCreationTime.get(Calendar.YEAR)));
imageCreationTimeNode.setAttribute("month", String.valueOf(documentCreationTime.get(Calendar.MONTH) + 1));
imageCreationTimeNode.setAttribute("day", String.valueOf(documentCreationTime.get(Calendar.DAY_OF_MONTH)));
imageCreationTimeNode.setAttribute("hour", String.valueOf(documentCreationTime.get(Calendar.HOUR_OF_DAY)));
imageCreationTimeNode.setAttribute("minute", String.valueOf(documentCreationTime.get(Calendar.MINUTE)));
imageCreationTimeNode.setAttribute("second", String.valueOf(documentCreationTime.get(Calendar.SECOND)));
}
return documentNode.hasChildNodes() ? documentNode : null;
}
@Override
protected IIOMetadataNode getStandardTextNode() {
if (textEntries.isEmpty()) {
return null;
}
IIOMetadataNode textNode = new IIOMetadataNode("Text");
// DocumentName, ImageDescription, Make, Model, PageName, Software, Artist, HostComputer, InkNames, Copyright:
// /Text/TextEntry@keyword = field name, /Text/TextEntry@value = field value.
for (TextEntry entry : textEntries) {
IIOMetadataNode textEntryNode = new IIOMetadataNode("TextEntry");
textNode.appendChild(textEntryNode);
if (entry.keyword != null) {
textEntryNode.setAttribute("keyword", entry.keyword);
}
textEntryNode.setAttribute("value", entry.value);
if (entry.language != null) {
textEntryNode.setAttribute("language", entry.language);
}
if (entry.encoding != null) {
textEntryNode.setAttribute("encoding", entry.encoding);
}
if (entry.compression != null) {
textEntryNode.setAttribute("compression", entry.compression);
}
}
return textNode;
}
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
IIOMetadataNode transparencyNode = new IIOMetadataNode("Transparency");
ColorModel colorModel = type.getColorModel();
IIOMetadataNode alphaNode = new IIOMetadataNode("Alpha");
transparencyNode.appendChild(alphaNode);
alphaNode.setAttribute("value", colorModel.hasAlpha() ? (colorModel.isAlphaPremultiplied() ? "premultiplied" : "nonpremultiplied") : "none");
if (colorModel instanceof IndexColorModel) {
IndexColorModel icm = (IndexColorModel) colorModel;
if (icm.getTransparentPixel() != -1) {
IIOMetadataNode transparentIndexNode = new IIOMetadataNode("TransparentIndex");
transparencyNode.appendChild(transparentIndexNode);
transparentIndexNode.setAttribute("value", Integer.toString(icm.getTransparentPixel()));
}
}
return transparencyNode;
}
}
@@ -1,348 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.stream;
import javax.imageio.stream.ImageInputStreamImpl;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import static com.twelvemonkeys.lang.Validate.notNull;
import static java.lang.Math.max;
/**
* A buffered {@link javax.imageio.stream.ImageInputStream} that is backed by a {@link java.nio.channels.SeekableByteChannel}
* and provides greatly improved performance
* compared to {@link javax.imageio.stream.FileCacheImageInputStream} or {@link javax.imageio.stream.MemoryCacheImageInputStream}
* for shorter reads, like single byte or bit reads.
*/
final class BufferedChannelImageInputStream extends ImageInputStreamImpl {
private static final Closeable CLOSEABLE_STUB = new Closeable() {
@Override public void close() {}
};
static final int DEFAULT_BUFFER_SIZE = 8192;
private ByteBuffer byteBuffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
private byte[] buffer = byteBuffer.array();
private int bufferPos;
private int bufferLimit;
private final ByteBuffer integralCache = ByteBuffer.allocate(8);
private final byte[] integralCacheArray = integralCache.array();
private SeekableByteChannel channel;
private Closeable closeable;
/**
* Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code File}.
*
* @param file a {@code File} to read from.
* @throws IllegalArgumentException if {@code file} is {@code null}.
* @throws SecurityException if a security manager is installed, and it denies read access to the file.
* @throws IOException if an I/O error occurs while opening the file.
*/
public BufferedChannelImageInputStream(final File file) throws IOException {
this(notNull(file, "file").toPath());
}
/**
* Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code Path}.
*
* @param file a {@code Path} to read from.
* @throws IllegalArgumentException if {@code file} is {@code null}.
* @throws UnsupportedOperationException if the {@code file} is associated with a provider that does not support creating file channels.
* @throws IOException if an I/O error occurs while opening the file.
* @throws SecurityException if a security manager is installed, and it denies read access to the file.
*/
public BufferedChannelImageInputStream(final Path file) throws IOException {
this(FileChannel.open(notNull(file, "file"), StandardOpenOption.READ), true);
}
/**
* Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code RandomAccessFile}.
*
* @param file a {@code RandomAccessFile} to read from.
* @throws IllegalArgumentException if {@code file} is {@code null}.
*/
public BufferedChannelImageInputStream(final RandomAccessFile file) {
// Closing the RAF is inconsistent, but emulates the behavior of javax.imageio.stream.FileImageInputStream
this(notNull(file, "file").getChannel(), true);
}
/**
* Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code FileInputStream}.
* <p>
* Closing this stream will <em>not</em> close the {@code FileInputStream}.
* </p>
*
* @param inputStream a {@code FileInputStream} to read from.
* @throws IllegalArgumentException if {@code inputStream} is {@code null}.
*/
public BufferedChannelImageInputStream(final FileInputStream inputStream) {
this(notNull(inputStream, "inputStream").getChannel(), false);
}
/**
* Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code SeekableByteChannel}.
* <p>
* Closing this stream will <em>not</em> close the {@code SeekableByteChannel}.
* </p>
*
* @param channel a {@code SeekableByteChannel} to read from.
* @throws IllegalArgumentException if {@code channel} is {@code null}.
*/
public BufferedChannelImageInputStream(final SeekableByteChannel channel) {
this(notNull(channel, "channel"), false);
}
/**
* Constructs a {@code BufferedChannelImageInputStream} that will read from a given {@code Cache}.
* <p>
* Closing this stream will close the {@code Cache}.
* </p>
*
* @param cache a {@code SeekableByteChannel} to read from.
* @throws IllegalArgumentException if {@code channel} is {@code null}.
*/
BufferedChannelImageInputStream(final Cache cache) {
this(notNull(cache, "cache"), true);
}
private BufferedChannelImageInputStream(final SeekableByteChannel channel, boolean closeChannelOnClose) {
this.channel = notNull(channel, "channel");
this.closeable = closeChannelOnClose ? this.channel : CLOSEABLE_STUB;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean fillBuffer() throws IOException {
byteBuffer.rewind();
int length = channel.read(byteBuffer);
bufferPos = 0;
bufferLimit = max(length, 0);
return bufferLimit > 0;
}
private boolean bufferEmpty() {
return bufferPos >= bufferLimit;
}
@Override
public void setByteOrder(ByteOrder byteOrder) {
super.setByteOrder(byteOrder);
integralCache.order(byteOrder);
}
@Override
public int read() throws IOException {
checkClosed();
if (bufferEmpty() && !fillBuffer()) {
return -1;
}
bitOffset = 0;
streamPos++;
return buffer[bufferPos++] & 0xff;
}
@Override
public int read(final byte[] bytes, final int offset, final int length) throws IOException {
checkClosed();
bitOffset = 0;
if (bufferEmpty()) {
// Bypass buffer if buffer is empty for reads longer than buffer
if (length >= buffer.length) {
return readDirect(bytes, offset, length);
}
else if (!fillBuffer()) {
return -1;
}
}
int fromBuffer = readBuffered(bytes, offset, length);
if (length > fromBuffer) {
// Due to known bugs in certain JDK-bundled ImageIO plugins expecting read to behave as readFully,
// we'll read as much as possible from the buffer, and the rest directly after
return fromBuffer + max(0, readDirect(bytes, offset + fromBuffer, length - fromBuffer));
}
return fromBuffer;
}
private int readDirect(final byte[] bytes, final int offset, final int length) throws IOException {
// Invalidate the buffer, as its contents is no longer in sync with the stream's position.
bufferLimit = 0;
ByteBuffer wrapped = ByteBuffer.wrap(bytes, offset, length);
int read = 0;
while (wrapped.hasRemaining()) {
int count = channel.read(wrapped);
if (count == -1) {
if (read == 0) {
return -1;
}
break;
}
read += count;
}
streamPos += read;
return read;
}
private int readBuffered(final byte[] bytes, final int offset, final int length) {
// Read as much as possible from buffer
int available = Math.min(bufferLimit - bufferPos, length);
if (available > 0) {
System.arraycopy(buffer, bufferPos, bytes, offset, available);
bufferPos += available;
streamPos += available;
}
return available;
}
public long length() {
// WTF?! This method is allowed to throw IOException in the interface...
try {
checkClosed();
return channel.size();
}
catch (IOException ignore) {
}
return -1;
}
public void close() throws IOException {
super.close();
buffer = null;
byteBuffer = null;
channel = null;
try {
closeable.close();
}
finally {
closeable = null;
}
}
// Need to override the readShort(), readInt() and readLong() methods,
// because the implementations in ImageInputStreamImpl expects the
// read(byte[], int, int) to always read the expected number of bytes,
// causing uninitialized values, alignment issues and EOFExceptions at
// random places...
// Notes:
// * readUnsignedXx() is covered by their signed counterparts
// * readChar() is covered by readShort()
// * readFloat() and readDouble() is covered by readInt() and readLong()
// respectively.
// * readLong() may be covered by two readInt()s, we'll override to be safe
@Override
public short readShort() throws IOException {
readFully(integralCacheArray, 0, 2);
return integralCache.getShort(0);
}
@Override
public int readInt() throws IOException {
readFully(integralCacheArray, 0, 4);
return integralCache.getInt(0);
}
@Override
public long readLong() throws IOException {
readFully(integralCacheArray, 0, 8);
return integralCache.getLong(0);
}
@Override
public void seek(long position) throws IOException {
checkClosed();
if (position < flushedPos) {
throw new IndexOutOfBoundsException("position < flushedPos!");
}
bitOffset = 0;
if (streamPos == position) {
return;
}
// Optimized to not invalidate buffer if new position is within current buffer
long newBufferPos = bufferPos + position - streamPos;
if (newBufferPos >= 0 && newBufferPos < bufferLimit) {
bufferPos = (int) newBufferPos;
}
else {
// Will invalidate buffer
bufferLimit = 0;
channel.position(position);
}
streamPos = position;
}
@Override
public void flushBefore(final long pos) throws IOException {
super.flushBefore(pos);
if (channel instanceof Cache) {
// In case of memory cache, free up memory
((Cache) channel).flushBefore(pos);
}
}
}
@@ -49,7 +49,6 @@ import static java.lang.Math.max;
* {@link File} or {@link RandomAccessFile} can be used as input.
*
* @see javax.imageio.stream.FileImageInputStream
* @deprecated Use {@link BufferedChannelImageInputStream} instead.
*/
// TODO: Create a memory-mapped version?
// Or not... From java.nio.channels.FileChannel.map:
@@ -58,7 +57,6 @@ import static java.lang.Math.max;
// the usual {@link #read read} and {@link #write write} methods. From the
// standpoint of performance it is generally only worth mapping relatively
// large files into memory.
@Deprecated
public final class BufferedFileImageInputStream extends ImageInputStreamImpl {
static final int DEFAULT_BUFFER_SIZE = 8192;
@@ -192,10 +190,10 @@ public final class BufferedFileImageInputStream extends ImageInputStreamImpl {
public void close() throws IOException {
super.close();
buffer = null;
raf.close();
raf = null;
buffer = null;
}
// Need to override the readShort(), readInt() and readLong() methods,
@@ -37,19 +37,18 @@ import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.util.Iterator;
import java.util.Locale;
/**
* BufferedFileImageInputStreamSpi
* Experimental
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$
*/
public final class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi {
public class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi {
public BufferedFileImageInputStreamSpi() {
this(new StreamProviderInfo());
}
@@ -70,13 +69,12 @@ public final class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi {
}
}
@Override
public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) throws IOException {
public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) {
if (input instanceof File) {
try {
return new BufferedChannelImageInputStream((File) input);
return new BufferedFileImageInputStream((File) input);
}
catch (FileNotFoundException | NoSuchFileException e) {
catch (FileNotFoundException e) {
// For consistency with the JRE bundled SPIs, we'll return null here,
// even though the spec does not say that's allowed.
// The problem is that the SPIs can only declare that they support an input type like a File,
@@ -93,8 +91,7 @@ public final class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi {
return false;
}
@Override
public String getDescription(final Locale locale) {
public String getDescription(final Locale pLocale) {
return "Service provider that instantiates an ImageInputStream from a File";
}
@@ -1,78 +0,0 @@
package com.twelvemonkeys.imageio.stream;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.util.Iterator;
import java.util.Locale;
/**
* BufferedInputStreamImageInputStreamSpi.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BufferedInputStreamImageInputStreamSpi.java,v 1.0 08/09/2022 haraldk Exp$
*/
public final class BufferedInputStreamImageInputStreamSpi extends ImageInputStreamSpi {
public BufferedInputStreamImageInputStreamSpi() {
this(new StreamProviderInfo());
}
private BufferedInputStreamImageInputStreamSpi(ProviderInfo providerInfo) {
super(providerInfo.getVendorName(), providerInfo.getVersion(), InputStream.class);
}
@Override
public void onRegistration(final ServiceRegistry registry, final Class<?> category) {
Iterator<ImageInputStreamSpi> providers = registry.getServiceProviders(ImageInputStreamSpi.class, new InputStreamFilter(), true);
while (providers.hasNext()) {
ImageInputStreamSpi provider = providers.next();
if (provider != this) {
registry.setOrdering(ImageInputStreamSpi.class, this, provider);
}
}
}
@Override
public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) throws IOException {
if (input instanceof InputStream) {
ReadableByteChannel channel = Channels.newChannel((InputStream) input);
if (channel instanceof SeekableByteChannel) {
// Special case for FileInputStream/FileChannel, we can get a seekable channel directly
return new BufferedChannelImageInputStream((SeekableByteChannel) channel);
}
// Otherwise, create a cache for backwards seeking
return new BufferedChannelImageInputStream(useCacheFile ? new FileCache(channel, cacheDir) : new MemoryCache(channel));
}
throw new IllegalArgumentException("Expected input of type InputStream: " + input);
}
@Override
public boolean canUseCacheFile() {
return true;
}
@Override
public String getDescription(final Locale locale) {
return "Service provider that instantiates an ImageInputStream from an InputStream";
}
private static class InputStreamFilter implements ServiceRegistry.Filter {
@Override
public boolean filter(final Object provider) {
return ((ImageInputStreamSpi) provider).getInputClass() == InputStream.class;
}
}
}
@@ -48,7 +48,7 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$
* @version $Id: BufferedRAFImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$
*/
public final class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
public class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
public BufferedRAFImageInputStreamSpi() {
this(new StreamProviderInfo());
}
@@ -69,10 +69,9 @@ public final class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
}
}
@Override
public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) {
public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) {
if (input instanceof RandomAccessFile) {
return new BufferedChannelImageInputStream((RandomAccessFile) input);
return new BufferedFileImageInputStream((RandomAccessFile) input);
}
throw new IllegalArgumentException("Expected input of type RandomAccessFile: " + input);
@@ -83,8 +82,7 @@ public final class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
return false;
}
@Override
public String getDescription(final Locale locale) {
public String getDescription(final Locale pLocale) {
return "Service provider that instantiates an ImageInputStream from a RandomAccessFile";
}
@@ -48,18 +48,18 @@ public final class ByteArrayImageInputStream extends ImageInputStreamImpl {
private final int dataOffset;
private final int dataLength;
public ByteArrayImageInputStream(final byte[] data) {
this(data, 0, data != null ? data.length : -1);
public ByteArrayImageInputStream(final byte[] pData) {
this(pData, 0, pData != null ? pData.length : -1);
}
public ByteArrayImageInputStream(final byte[] data, int offset, int length) {
this.data = notNull(data, "data");
dataOffset = isMax(data.length, offset, "offset");
dataLength = isMax(data.length - offset, length, "length");
public ByteArrayImageInputStream(final byte[] pData, int offset, int length) {
data = notNull(pData, "data");
dataOffset = isBetween(0, pData.length, offset, "offset");
dataLength = isBetween(0, pData.length - offset, length, "length");
}
private static int isMax(final int high, final int value, final String name) {
return isTrue(value >= 0 && value <= high, value, String.format("%s out of range [0, %d]: %d", name, high, value));
private static int isBetween(final int low, final int high, final int value, final String name) {
return isTrue(value >= low && value <= high, value, String.format("%s out of range [%d, %d]: %d", name, low, high, value));
}
public int read() throws IOException {
@@ -72,14 +72,14 @@ public final class ByteArrayImageInputStream extends ImageInputStreamImpl {
return data[((int) streamPos++) + dataOffset] & 0xff;
}
public int read(byte[] buffer, int offset, int len) throws IOException {
public int read(byte[] pBuffer, int pOffset, int pLength) throws IOException {
if (streamPos >= dataLength) {
return -1;
}
int length = (int) Math.min(dataLength - streamPos, len);
int length = (int) Math.min(this.dataLength - streamPos, pLength);
bitOffset = 0;
System.arraycopy(data, (int) streamPos + dataOffset, buffer, offset, length);
System.arraycopy(data, (int) streamPos + dataOffset, pBuffer, pOffset, length);
streamPos += length;
return length;
@@ -45,7 +45,7 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$
* @version $Id: ByteArrayImageInputStreamSpi.java,v 1.0 May 15, 2008 2:12:12 PM haraldk Exp$
*/
public final class ByteArrayImageInputStreamSpi extends ImageInputStreamSpi {
public class ByteArrayImageInputStreamSpi extends ImageInputStreamSpi {
public ByteArrayImageInputStreamSpi() {
this(new StreamProviderInfo());
@@ -55,17 +55,16 @@ public final class ByteArrayImageInputStreamSpi extends ImageInputStreamSpi {
super(providerInfo.getVendorName(), providerInfo.getVersion(), byte[].class);
}
@Override
public ImageInputStream createInputStreamInstance(Object input, boolean useCacheFile, File cacheDir) {
if (input instanceof byte[]) {
return new ByteArrayImageInputStream((byte[]) input);
public ImageInputStream createInputStreamInstance(Object pInput, boolean pUseCache, File pCacheDir) {
if (pInput instanceof byte[]) {
return new ByteArrayImageInputStream((byte[]) pInput);
}
else {
throw new IllegalArgumentException("Expected input of type byte[]: " + pInput);
}
throw new IllegalArgumentException("Expected input of type byte[]: " + input);
}
@Override
public String getDescription(Locale locale) {
public String getDescription(Locale pLocale) {
return "Service provider that instantiates an ImageInputStream from a byte array";
}
@@ -1,7 +0,0 @@
package com.twelvemonkeys.imageio.stream;
import java.nio.channels.SeekableByteChannel;
interface Cache extends SeekableByteChannel {
void flushBefore(long pos);
}
@@ -125,7 +125,7 @@ public final class DirectImageInputStream extends ImageInputStreamImpl {
@Override
public void close() throws IOException {
// We could seek to EOF here, but the usual case is we know where the next chunk of data is
// We could seek to EOF here, but the usual case
stream.close();
super.close();
@@ -1,112 +0,0 @@
package com.twelvemonkeys.imageio.stream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import static com.twelvemonkeys.lang.Validate.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull;
import static java.lang.Math.max;
import static java.nio.file.StandardOpenOption.DELETE_ON_CLOSE;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.WRITE;
// Note: We could consider creating a memory-mapped version...
// But, from java.nio.channels.FileChannel.map:
// For most operating systems, mapping a file into memory is more
// expensive than reading or writing a few tens of kilobytes of data via
// the usual {@link #read read} and {@link #write write} methods. From the
// standpoint of performance it is generally only worth mapping relatively
// large files into memory.
final class FileCache implements Cache {
final static int BLOCK_SIZE = 1 << 13;
private final FileChannel cache;
private final ReadableByteChannel channel;
// TODO: Perhaps skip this constructor?
FileCache(InputStream stream, File cacheDir) throws IOException {
// Stream will be closed with channel, documented behavior
this(Channels.newChannel(notNull(stream, "stream")), cacheDir);
}
public FileCache(ReadableByteChannel channel, File cacheDir) throws IOException {
this.channel = notNull(channel, "channel");
isTrue(cacheDir == null || cacheDir.isDirectory(), cacheDir, "%s is not a directory");
// Create a temp file to hold our cache,
// will be deleted when this channel is closed, as we close the cache
Path cacheFile = cacheDir == null
? Files.createTempFile("imageio", ".tmp")
: Files.createTempFile(cacheDir.toPath(), "imageio", ".tmp");
cache = FileChannel.open(cacheFile, DELETE_ON_CLOSE, READ, WRITE);
}
@SuppressWarnings("StatementWithEmptyBody")
void fetch() throws IOException {
while (cache.position() >= cache.size() && cache.transferFrom(channel, cache.size(), max(cache.position() - cache.size(), BLOCK_SIZE)) > 0) {
// Continue transfer...
}
}
@Override
public boolean isOpen() {
return channel.isOpen();
}
@Override
public void close() throws IOException {
cache.close();
}
@Override
public int read(ByteBuffer dest) throws IOException {
fetch();
if (cache.position() >= cache.size()) {
return -1;
}
return cache.read(dest);
}
@Override
public long position() throws IOException {
return cache.position();
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
cache.position(newPosition);
return this;
}
@Override
public long size() {
// We could allow the size to grow, but that means the stream cannot rely on this size, so we'll just pretend we don't know...
return -1;
}
@Override
public int write(ByteBuffer src) {
throw new NonWritableChannelException();
}
@Override
public SeekableByteChannel truncate(long size) {
throw new NonWritableChannelException();
}
@Override public void flushBefore(long pos) {
}
}
@@ -1,152 +0,0 @@
package com.twelvemonkeys.imageio.stream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.util.ArrayList;
import java.util.List;
import static com.twelvemonkeys.lang.Validate.notNull;
import static java.lang.Math.min;
final class MemoryCache implements Cache {
final static int BLOCK_SIZE = 1 << 13;
private final List<byte[]> cache = new ArrayList<>();
private final ReadableByteChannel channel;
private long length;
private long position;
private long start;
// TODO: Maybe get rid of this constructor, as we don't want to do this if we have a FileInputStream/FileChannel...
MemoryCache(InputStream stream) {
this(Channels.newChannel(notNull(stream, "stream")));
}
public MemoryCache(ReadableByteChannel channel) {
this.channel = notNull(channel, "channel");
}
byte[] fetchBlock() throws IOException {
long currPos = position;
long index = currPos / BLOCK_SIZE;
if (index >= Integer.MAX_VALUE) {
throw new IOException("Memory cache max size exceeded");
}
while (index >= cache.size()) {
byte[] block;
try {
block = new byte[BLOCK_SIZE];
}
catch (OutOfMemoryError e) {
throw new IOException("No more memory for cache: " + cache.size() * BLOCK_SIZE);
}
cache.add(block);
length += readBlock(block);
}
return cache.get((int) index);
}
private int readBlock(final byte[] block) throws IOException {
ByteBuffer wrapped = ByteBuffer.wrap(block);
while (wrapped.hasRemaining()) {
int count = channel.read(wrapped);
if (count == -1) {
// Last block
break;
}
}
return wrapped.position();
}
@Override
public boolean isOpen() {
return channel.isOpen();
}
@Override
public void close() throws IOException {
cache.clear();
}
@Override
public int read(ByteBuffer dest) throws IOException {
byte[] buffer = fetchBlock();
int bufferPos = (int) (position % BLOCK_SIZE);
if (position >= length) {
return -1;
}
int len = min(dest.remaining(), (int) min(BLOCK_SIZE - bufferPos, length - position));
dest.put(buffer, bufferPos, len);
position += len;
return len;
}
@Override
public long position() throws IOException {
return position;
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
if (newPosition < start) {
throw new IOException("Seek before flush position");
}
this.position = newPosition;
return this;
}
@Override
public long size() throws IOException {
// We could allow the size to grow, but that means the stream cannot rely on this size, so we'll just pretend we don't know...
return -1;
}
@Override
public int write(ByteBuffer src) {
throw new NonWritableChannelException();
}
@Override
public SeekableByteChannel truncate(long size) {
throw new NonWritableChannelException();
}
@Override
public void flushBefore(long pos) {
if (pos < start) {
throw new IndexOutOfBoundsException("pos < flushed position");
}
if (pos > position) {
throw new IndexOutOfBoundsException("pos > current position");
}
int blocks = (int) (pos / BLOCK_SIZE); // Overflow guarded for in fetchBlock
// Clear blocks no longer needed
for (int i = 0; i < blocks; i++) {
cache.set(i, null);
}
start = pos;
}
}
@@ -33,7 +33,9 @@ package com.twelvemonkeys.imageio.stream;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.stream.FileCacheImageInputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -50,7 +52,7 @@ import java.util.Locale;
* @version $Id: URLImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$
*/
// TODO: URI instead of URL?
public final class URLImageInputStreamSpi extends ImageInputStreamSpi {
public class URLImageInputStreamSpi extends ImageInputStreamSpi {
public URLImageInputStreamSpi() {
this(new StreamProviderInfo());
}
@@ -62,28 +64,53 @@ public final class URLImageInputStreamSpi extends ImageInputStreamSpi {
// TODO: Create a URI or URLImageInputStream class, with a getUR[I|L] method, to allow for multiple file formats
// The good thing with that is that it does not clash with the built-in Sun-stuff or other people's hacks
// The bad thing is that most people don't expect there to be an UR[I|L]ImageInputStreamSpi..
@Override
public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) throws IOException {
if (input instanceof URL) {
URL url = (URL) input;
public ImageInputStream createInputStreamInstance(final Object pInput, final boolean pUseCache, final File pCacheDir) throws IOException {
if (pInput instanceof URL) {
URL url = (URL) pInput;
// Special case for file protocol, a lot faster than FileCacheImageInputStream
if ("file".equals(url.getProtocol())) {
try {
return new BufferedChannelImageInputStream(new File(url.toURI()));
return new BufferedFileImageInputStream(new File(url.toURI()));
}
catch (URISyntaxException shouldNeverHappen) {
// This should never happen, but if it does, we'll fall back to using the stream
shouldNeverHappen.printStackTrace();
catch (URISyntaxException ignore) {
// This should never happen, but if it does, we'll fall back to using the stream
ignore.printStackTrace();
}
}
// Otherwise revert to cached
InputStream urlStream = url.openStream();
return new BufferedChannelImageInputStream(useCacheFile ? new FileCache(urlStream, cacheDir) : new MemoryCache(urlStream));
final InputStream urlStream = url.openStream();
if (pUseCache) {
return new FileCacheImageInputStream(urlStream, pCacheDir) {
@Override
public void close() throws IOException {
try {
super.close();
}
finally {
urlStream.close(); // NOTE: If this line throws IOE, it will shadow the original..
}
}
};
}
else {
return new MemoryCacheImageInputStream(urlStream) {
@Override
public void close() throws IOException {
try {
super.close();
}
finally {
urlStream.close(); // NOTE: If this line throws IOE, it will shadow the original..
}
}
};
}
}
else {
throw new IllegalArgumentException("Expected input of type URL: " + pInput);
}
throw new IllegalArgumentException("Expected input of type URL: " + input);
}
@Override
@@ -91,7 +118,7 @@ public final class URLImageInputStreamSpi extends ImageInputStreamSpi {
return true;
}
public String getDescription(final Locale locale) {
public String getDescription(final Locale pLocale) {
return "Service provider that instantiates an ImageInputStream from a URL";
}
}
@@ -30,15 +30,22 @@
package com.twelvemonkeys.imageio.util;
import com.twelvemonkeys.imageio.color.DiscreteAlphaIndexColorModel;
import javax.imageio.ImageTypeSpecifier;
import java.awt.color.*;
import java.awt.image.*;
import static com.twelvemonkeys.lang.Validate.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DirectColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.SampleModel;
import javax.imageio.ImageTypeSpecifier;
import com.twelvemonkeys.imageio.color.DiscreteAlphaIndexColorModel;
/**
* Factory class for creating {@code ImageTypeSpecifier}s.
* Fixes some subtle bugs in {@code ImageTypeSpecifier}'s factory methods, but
@@ -51,52 +58,28 @@ import static com.twelvemonkeys.lang.Validate.notNull;
*/
public final class ImageTypeSpecifiers {
private static final ImageTypeSpecifier TYPE_INT_RGB = createPackedOddBits(ColorSpace.getInstance(ColorSpace.CS_sRGB), 24,
0xFF0000,
0x00FF00,
0x0000FF,
0x0,
DataBuffer.TYPE_INT,
false);
private static final ImageTypeSpecifier TYPE_INT_BGR = createPackedOddBits(ColorSpace.getInstance(ColorSpace.CS_sRGB), 24,
0x0000FF,
0x00FF00,
0xFF0000,
0x0,
DataBuffer.TYPE_INT,
false);
private static final ImageTypeSpecifier TYPE_USHORT_565_RGB = createPackedOddBits(ColorSpace.getInstance(ColorSpace.CS_sRGB), 16,
0xF800,
0x07E0,
0x001F,
0x0,
DataBuffer.TYPE_USHORT,
false);
private static final ImageTypeSpecifier TYPE_USHORT_555_RGB = createPackedOddBits(ColorSpace.getInstance(ColorSpace.CS_sRGB), 15,
0x7C00,
0x03E0,
0x001F,
0x0,
DataBuffer.TYPE_USHORT,
false);
private ImageTypeSpecifiers() {}
public static ImageTypeSpecifier createFromBufferedImageType(final int bufferedImageType) {
switch (bufferedImageType) {
// ImageTypeSpecifier unconditionally uses bits == 32, we'll use a workaround for the INT_RGB and USHORT types
case BufferedImage.TYPE_INT_RGB:
return TYPE_INT_RGB;
case BufferedImage.TYPE_INT_BGR:
return TYPE_INT_BGR;
// ImageTypeSpecifier unconditionally uses bits == 32, we'll use a workaround for the USHORT types
case BufferedImage.TYPE_USHORT_565_RGB:
return TYPE_USHORT_565_RGB;
return createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB),
0xF800,
0x07E0,
0x001F,
0x0,
DataBuffer.TYPE_USHORT,
false);
case BufferedImage.TYPE_USHORT_555_RGB:
return TYPE_USHORT_555_RGB;
return createPacked(ColorSpace.getInstance(ColorSpace.CS_sRGB),
0x7C00,
0x03E0,
0x001F,
0x0,
DataBuffer.TYPE_USHORT,
false);
default:
}
@@ -107,41 +90,23 @@ public final class ImageTypeSpecifiers {
final int redMask, final int greenMask,
final int blueMask, final int alphaMask,
final int transferType, boolean isAlphaPremultiplied) {
int bits = calculateRequiredBits(redMask | greenMask | blueMask | alphaMask);
if (bits != 32) {
if (transferType == DataBuffer.TYPE_BYTE || transferType == DataBuffer.TYPE_USHORT) {
// ImageTypeSpecifier unconditionally uses bits == 32, we'll use a workaround for BYTE/USHORT types
return createPackedOddBits(colorSpace, bits, redMask, greenMask, blueMask, alphaMask, transferType, isAlphaPremultiplied);
notNull(colorSpace, "colorSpace");
isTrue(colorSpace.getType() == ColorSpace.TYPE_RGB, colorSpace, "ColorSpace must be TYPE_RGB");
isTrue(redMask != 0 || greenMask != 0 || blueMask != 0 || alphaMask != 0, "No mask has at least 1 bit set");
int bits = transferType == DataBuffer.TYPE_BYTE ? 8 : 16;
ColorModel colorModel = new DirectColorModel(colorSpace, bits, redMask, greenMask, blueMask, alphaMask,
isAlphaPremultiplied, transferType);
return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1));
}
return ImageTypeSpecifier.createPacked(colorSpace, redMask, greenMask, blueMask, alphaMask, transferType, isAlphaPremultiplied);
}
private static int calculateRequiredBits(int mask) {
// See https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogObvious
int r = 1;
while ((mask >>>= 1) != 0) {
r++;
}
return r;
}
static ImageTypeSpecifier createPackedOddBits(final ColorSpace colorSpace, int bits,
final int redMask, final int greenMask,
final int blueMask, final int alphaMask,
final int transferType, boolean isAlphaPremultiplied) {
// ImageTypeSpecifier unconditionally uses bits == 32, we'll use a workaround
notNull(colorSpace, "colorSpace");
isTrue(colorSpace.getType() == ColorSpace.TYPE_RGB, colorSpace, "ColorSpace must be TYPE_RGB");
isTrue(redMask != 0 || greenMask != 0 || blueMask != 0 || alphaMask != 0, "No mask has at least 1 bit set");
ColorModel colorModel = new DirectColorModel(colorSpace, bits, redMask, greenMask, blueMask, alphaMask,
isAlphaPremultiplied, transferType);
return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1));
}
public static ImageTypeSpecifier createInterleaved(final ColorSpace colorSpace,
final int[] bandOffsets,
final int dataType,
@@ -270,20 +235,4 @@ public final class ImageTypeSpecifiers {
ColorModel colorModel = new DiscreteAlphaIndexColorModel(pColorModel, extraSamples, hasAlpha);
return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1));
}
public static ImageTypeSpecifier createFromRenderedImage(RenderedImage image) {
if (image == null) {
throw new IllegalArgumentException("image == null!");
}
if (image instanceof BufferedImage) {
int bufferedImageType = ((BufferedImage) image).getType();
if (bufferedImageType != BufferedImage.TYPE_CUSTOM) {
return createFromBufferedImageType(bufferedImageType);
}
}
return new ImageTypeSpecifier(image);
}
}
@@ -1,5 +1,4 @@
com.twelvemonkeys.imageio.stream.BufferedFileImageInputStreamSpi
com.twelvemonkeys.imageio.stream.BufferedRAFImageInputStreamSpi
com.twelvemonkeys.imageio.stream.BufferedInputStreamImageInputStreamSpi
# Use SPI loading as a hook for early profile activation
com.twelvemonkeys.imageio.color.ProfileDeferralActivator$Spi
@@ -1,332 +0,0 @@
package com.twelvemonkeys.imageio;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport.ColorSpaceType;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport.ImageOrientation;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport.PlanarConfiguration;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport.SubimageInterpretation;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport.TextEntry;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import org.junit.Test;
import org.w3c.dom.NodeList;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.*;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import static com.twelvemonkeys.imageio.StandardImageMetadataSupport.builder;
import static org.junit.Assert.*;
public class StandardImageMetadataSupportTest {
@Test(expected = IllegalArgumentException.class)
public void createNullBuilder() {
new StandardImageMetadataSupport(null);
}
@Test(expected = IllegalArgumentException.class)
public void createNullType() {
new StandardImageMetadataSupport(builder(null));
}
@Test(expected = IllegalArgumentException.class)
public void builderNullType() {
builder(null).build();
}
@Test
public void createValid() {
IIOMetadata metadata = new StandardImageMetadataSupport(builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB)));
assertNotNull(metadata);
}
@Test
public void builderValid() {
IIOMetadata metadata = builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB))
.build();
assertNotNull(metadata);
}
@Test
public void compressionValuesUnspecified() {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.build();
assertNull(metadata.getStandardCompressionNode());
}
@Test
public void compressionValuesNone() {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withCompressionTypeName("nOnE") // Case-insensitive
.build();
assertNull(metadata.getStandardCompressionNode());
}
@Test
public void compressionValuesName() {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withCompressionTypeName("foo")
.build();
IIOMetadataNode compressionNode = metadata.getStandardCompressionNode();
assertNotNull(compressionNode);
IIOMetadataNode compressionName = (IIOMetadataNode) compressionNode.getElementsByTagName("CompressionTypeName").item(0);
assertEquals("foo", compressionName.getAttribute("value"));
// Defaults to lossless true
IIOMetadataNode compressionLossless = (IIOMetadataNode) compressionNode.getElementsByTagName("Lossless").item(0);
assertEquals("TRUE", compressionLossless.getAttribute("value"));
}
@Test(expected = IllegalArgumentException.class)
public void withCompressionLossyIllegal() {
builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withCompressionLossless(false);
}
@Test
public void compressionValuesLossy() {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withCompressionTypeName("bar")
.withCompressionLossless(false)
.build();
IIOMetadataNode compressionNode = metadata.getStandardCompressionNode();
assertNotNull(compressionNode);
IIOMetadataNode compressionName = (IIOMetadataNode) compressionNode.getElementsByTagName("CompressionTypeName").item(0);
assertEquals("bar", compressionName.getAttribute("value"));
IIOMetadataNode compressionLossless = (IIOMetadataNode) compressionNode.getElementsByTagName("Lossless").item(0);
assertEquals("FALSE", compressionLossless.getAttribute("value"));
}
@Test
public void withDocumentValuesDefault() {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.build();
IIOMetadataNode documentNode = metadata.getStandardDocumentNode();
assertNull(documentNode);
}
@Test
public void withDocumentValues() {
Calendar creationTime = Calendar.getInstance();
creationTime.set(2022, Calendar.SEPTEMBER, 8, 14, 5, 0);
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withFormatVersion("42")
.withDocumentCreationTime(creationTime)
.build();
IIOMetadataNode documentNode = metadata.getStandardDocumentNode();
assertNotNull(documentNode);
IIOMetadataNode formatVersion = (IIOMetadataNode) documentNode.getElementsByTagName("FormatVersion").item(0);
assertEquals("42", formatVersion.getAttribute("value"));
IIOMetadataNode imageCreationTime = (IIOMetadataNode) documentNode.getElementsByTagName("ImageCreationTime").item(0);
assertEquals("2022", imageCreationTime.getAttribute("year"));
assertEquals("9", imageCreationTime.getAttribute("month"));
assertEquals("8", imageCreationTime.getAttribute("day"));
assertEquals("14", imageCreationTime.getAttribute("hour"));
assertEquals("5", imageCreationTime.getAttribute("minute"));
assertEquals("0", imageCreationTime.getAttribute("second"));
}
@Test
public void withTextValuesDefault() {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.build();
IIOMetadataNode textNode = metadata.getStandardTextNode();
assertNull(textNode);
}
@Test
public void withTextValuesSingle() {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withTextEntry("foo", "bar")
.build();
IIOMetadataNode textNode = metadata.getStandardTextNode();
assertNotNull(textNode);
IIOMetadataNode textEntry = (IIOMetadataNode) textNode.getElementsByTagName("TextEntry").item(0);
assertEquals("foo", textEntry.getAttribute("keyword"));
assertEquals("bar", textEntry.getAttribute("value"));
}
@Test
public void withTextValuesMap() {
Map<String, String> entries = new HashMap<>();
entries.put("foo", "bar");
entries.put("bar", "xyzzy");
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withTextEntries(entries)
.build();
IIOMetadataNode textNode = metadata.getStandardTextNode();
assertNotNull(textNode);
NodeList textEntries = textNode.getElementsByTagName("TextEntry");
assertEquals(entries.size(), textEntries.getLength());
int i = 0;
for (Entry<String, String> entry : entries.entrySet()) {
IIOMetadataNode textEntry = (IIOMetadataNode) textEntries.item(i);
assertEquals(entry.getKey(), textEntry.getAttribute("keyword"));
assertEquals(entry.getValue(), textEntry.getAttribute("value"));
i++;
}
}
@Test
public void withTextValuesList() {
List<TextEntry> entries = Arrays.asList(
new TextEntry(null, "foo"), // No key allowed
new TextEntry("foo", "bar"),
new TextEntry("bar", "xyzzy"),
new TextEntry("bar", "nothing happens..."), // Duplicates allowed
new TextEntry("everything", "válüè", "unknown", "UTF-8", "zip")
);
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withTextEntries(entries)
.build();
IIOMetadataNode textNode = metadata.getStandardTextNode();
assertNotNull(textNode);
NodeList textEntries = textNode.getElementsByTagName("TextEntry");
assertEquals(entries.size(), textEntries.getLength());
for (int i = 0; i < entries.size(); i++) {
TextEntry entry = entries.get(i);
IIOMetadataNode textEntry = (IIOMetadataNode) textEntries.item(i);
assertAttributeEqualOrAbsent(entry.keyword, textEntry, "keyword");
assertEquals(entry.value, textEntry.getAttribute("value"));
assertAttributeEqualOrAbsent(entry.language, textEntry, "language");
assertAttributeEqualOrAbsent(entry.encoding, textEntry, "encoding");
assertAttributeEqualOrAbsent(entry.compression, textEntry, "compression");
}
}
private static void assertAttributeEqualOrAbsent(final String expectedValue, IIOMetadataNode node, final String attribute) {
if (expectedValue != null) {
assertEquals(expectedValue, node.getAttribute(attribute));
}
else {
assertFalse(node.hasAttribute(attribute));
}
}
@Test
public void withPlanarColorspaceType() {
// See: https://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/standard_metadata.html
Collection<String> allowedValues = Arrays.asList(
"XYZ", "Lab", "Luv", "YCbCr", "Yxy", "YCCK", "PhotoYCC",
"RGB", "GRAY", "HSV", "HLS", "CMYK", "CMY",
"2CLR", "3CLR", "4CLR", "5CLR", "6CLR", "7CLR", "8CLR",
"9CLR", "ACLR", "BCLR", "CCLR", "DCLR", "ECLR", "FCLR"
);
for (ColorSpaceType value : ColorSpaceType.values()) {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withColorSpaceType(value)
.build();
assertNotNull(metadata);
IIOMetadataNode documentNode = metadata.getStandardChromaNode();
assertNotNull(documentNode);
IIOMetadataNode subImageInterpretation = (IIOMetadataNode) documentNode.getElementsByTagName("ColorSpaceType").item(0);
assertEquals(value.toString(), subImageInterpretation.getAttribute("name")); // Format oddity: Why is this not "value"?
assertTrue(allowedValues.contains(value.toString()));
}
}
@Test
public void withPlanarConfiguration() {
// See: https://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/standard_metadata.html
Collection<String> allowedValues = Arrays.asList("PixelInterleaved", "PlaneInterleaved", "LineInterleaved", "TileInterleaved");
for (PlanarConfiguration value : PlanarConfiguration.values()) {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR))
.withPlanarConfiguration(value)
.build();
assertNotNull(metadata);
IIOMetadataNode documentNode = metadata.getStandardDataNode();
assertNotNull(documentNode);
IIOMetadataNode subImageInterpretation = (IIOMetadataNode) documentNode.getElementsByTagName("PlanarConfiguration").item(0);
assertEquals(value.toString(), subImageInterpretation.getAttribute("value"));
assertTrue(allowedValues.contains(value.toString()));
}
}
@Test
public void withImageOrientation() {
// See: https://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/standard_metadata.html
Collection<String> allowedValues = Arrays.asList("Normal", "Rotate90", "Rotate180", "Rotate270", "FlipH", "FlipV", "FlipHRotate90", "FlipVRotate90");
for (ImageOrientation value : ImageOrientation.values()) {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY))
.withOrientation(value)
.build();
assertNotNull(metadata);
IIOMetadataNode documentNode = metadata.getStandardDimensionNode();
assertNotNull(documentNode);
IIOMetadataNode subImageInterpretation = (IIOMetadataNode) documentNode.getElementsByTagName("ImageOrientation").item(0);
assertEquals(value.toString(), subImageInterpretation.getAttribute("value"));
assertTrue(allowedValues.contains(value.toString()));
}
}
@Test
public void withSubimageInterpretation() {
// See: https://docs.oracle.com/javase/7/docs/api/javax/imageio/metadata/doc-files/standard_metadata.html
Collection<String> allowedValues = Arrays.asList(
"Standalone", "SinglePage", "FullResolution", "ReducedResolution", "PyramidLayer",
"Preview", "VolumeSlice", "ObjectView", "Panorama", "AnimationFrame",
"TransparencyMask", "CompositingLayer", "SpectralSlice", "Unknown"
);
for (SubimageInterpretation value : SubimageInterpretation.values()) {
StandardImageMetadataSupport metadata = (StandardImageMetadataSupport) builder(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB))
.withSubimageInterpretation(value)
.build();
assertNotNull(metadata);
IIOMetadataNode documentNode = metadata.getStandardDocumentNode();
assertNotNull(documentNode);
IIOMetadataNode subImageInterpretation = (IIOMetadataNode) documentNode.getElementsByTagName("SubimageInterpretation").item(0);
assertEquals(value.toString(), subImageInterpretation.getAttribute("value"));
assertTrue(allowedValues.contains(value.toString()));
}
}
}
@@ -1,434 +0,0 @@
/*
* Copyright (c) 2020, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.stream;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import javax.imageio.stream.ImageInputStream;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.ReadableByteChannel;
import java.util.Random;
import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rangeEquals;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
/**
* BufferedFileImageInputStreamTestCase
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
*/
// TODO: Remove this test, and instead test the disk cache directly!
public class BufferedChannelImageInputStreamFileCacheTest {
private final Random random = new Random(170984354357234566L);
private InputStream randomDataToInputStream(byte[] data) {
random.nextBytes(data);
return new ByteArrayInputStream(data);
}
@Test
public void testCreate() throws IOException {
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(new ByteArrayInputStream(new byte[0]), null))) {
assertEquals("Stream length should be unknown", -1, stream.length());
}
}
@Test
public void testCreateNullStream() throws IOException {
try {
new FileCache((InputStream) null, null);
fail("Expected IllegalArgumentException");
}
catch (IllegalArgumentException expected) {
assertNotNull("Null exception message", expected.getMessage());
String message = expected.getMessage().toLowerCase();
assertTrue("Exception message does not contain parameter name", message.contains("stream"));
assertTrue("Exception message does not contain null", message.contains("null"));
}
}
@Test
public void testCreateNullChannel() throws IOException {
try {
new FileCache((ReadableByteChannel) null, null);
fail("Expected IllegalArgumentException");
}
catch (IllegalArgumentException expected) {
assertNotNull("Null exception message", expected.getMessage());
String message = expected.getMessage().toLowerCase();
assertTrue("Exception message does not contain parameter name", message.contains("channel"));
assertTrue("Exception message does not contain null", message.contains("null"));
}
}
@Test
public void testRead() throws IOException {
byte[] data = new byte[1024 * 1024];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
assertEquals("Stream length should be unknown", -1, stream.length());
for (byte value : data) {
assertEquals("Wrong data read", value & 0xff, stream.read());
}
assertEquals("Wrong data read", -1, stream.read());
}
}
@Test
public void testReadArray() throws IOException {
byte[] data = new byte[1024 * 1024];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
assertEquals("Stream length should be unknown", -1, stream.length());
byte[] result = new byte[1024];
for (int i = 0; i < data.length / result.length; i++) {
stream.readFully(result);
assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
}
assertEquals("Wrong data read", -1, stream.read());
}
}
@Test
public void testReadSkip() throws IOException {
byte[] data = new byte[1024 * 14];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
assertEquals("Stream length should be unknown", -1, stream.length());
byte[] result = new byte[7];
for (int i = 0; i < data.length / result.length; i += 2) {
stream.readFully(result);
stream.skipBytes(result.length);
assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
}
}
}
@Test
public void testReadSeek() throws IOException {
byte[] data = new byte[1024 * 18];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
assertEquals("Stream length should be unknown", -1, stream.length());
byte[] result = new byte[9];
for (int i = 0; i < data.length / result.length; i++) {
// Read backwards
long newPos = data.length - result.length - i * result.length;
stream.seek(newPos);
assertEquals("Wrong stream position", newPos, stream.getStreamPosition());
stream.readFully(result);
assertTrue("Wrong data read: " + i, rangeEquals(data, (int) newPos, result, 0, result.length));
}
}
}
@Test
public void testReadOutsideDataSeek0Read() throws IOException {
byte[] data = new byte[256];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
assertEquals("Stream length should be unknown", -1, stream.length());
byte[] buffer = new byte[data.length * 2];
stream.read(buffer);
stream.seek(0);
assertNotEquals(-1, stream.read());
assertNotEquals(-1, stream.read(buffer));
}
}
@Test
public void testReadBitRandom() throws IOException {
byte[] bytes = new byte[8];
InputStream input = randomDataToInputStream(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
try (ImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
for (int i = 1; i <= 64; i++) {
assertEquals(String.format("bit %d differ", i), (value << (i - 1L)) >>> 63L, stream.readBit());
}
}
}
@Test
public void testReadBitsRandom() throws IOException {
byte[] bytes = new byte[8];
InputStream input = randomDataToInputStream(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
try (ImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
for (int i = 1; i <= 64; i++) {
stream.seek(0);
assertEquals(String.format("bit %d differ", i), value >>> (64L - i), stream.readBits(i));
assertEquals(i % 8, stream.getBitOffset());
}
}
}
@Test
public void testReadBitsRandomOffset() throws IOException {
byte[] bytes = new byte[8];
InputStream input = randomDataToInputStream(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
try (ImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
for (int i = 1; i <= 60; i++) {
stream.seek(0);
stream.setBitOffset(i % 8);
assertEquals(String.format("bit %d differ", i), (value << (i % 8)) >>> (64L - i), stream.readBits(i));
assertEquals(i * 2 % 8, stream.getBitOffset());
}
}
}
@Test
public void testReadShort() throws IOException {
byte[] bytes = new byte[8743]; // Slightly more than one buffer size
InputStream input = randomDataToInputStream(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < bytes.length / 2; i++) {
assertEquals(buffer.getShort(), stream.readShort());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readShort();
}
});
stream.seek(0);
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < bytes.length / 2; i++) {
assertEquals(buffer.getShort(), stream.readShort());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readShort();
}
});
}
}
@Test
public void testReadInt() throws IOException {
byte[] bytes = new byte[8743]; // Slightly more than one buffer size
InputStream input = randomDataToInputStream(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < bytes.length / 4; i++) {
assertEquals(buffer.getInt(), stream.readInt());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readInt();
}
});
stream.seek(0);
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < bytes.length / 4; i++) {
assertEquals(buffer.getInt(), stream.readInt());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readInt();
}
});
}
}
@Test
public void testReadLong() throws IOException {
byte[] bytes = new byte[8743]; // Slightly more than one buffer size
InputStream input = randomDataToInputStream(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < bytes.length / 8; i++) {
assertEquals(buffer.getLong(), stream.readLong());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readLong();
}
});
stream.seek(0);
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < bytes.length / 8; i++) {
assertEquals(buffer.getLong(), stream.readLong());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readLong();
}
});
}
}
@Test
public void testSeekPastEOF() throws IOException {
byte[] bytes = new byte[9];
InputStream input = randomDataToInputStream(bytes);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
stream.seek(1000);
assertEquals(-1, stream.read());
assertEquals(-1, stream.read(new byte[1], 0, 1));
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readFully(new byte[1]);
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readByte();
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readShort();
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readInt();
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readLong();
}
});
stream.seek(0);
for (byte value : bytes) {
assertEquals(value, stream.readByte());
}
assertEquals(-1, stream.read());
}
}
@Test
public void testClose() throws IOException {
// Create wrapper stream
Cache cache = mock(Cache.class);
ImageInputStream stream = new BufferedChannelImageInputStream(cache);
stream.close();
verify(cache, only()).close();
}
@Test
public void testWorkaroundForWBMPImageReaderExpectsReadToBehaveAsReadFully() throws IOException {
// See #606 for details.
// Bug in JDK WBMPImageReader, uses read(byte[], int, int) instead of readFully(byte[], int, int).
// Ie: Relies on read to return all bytes at once, without blocking
int size = BufferedChannelImageInputStream.DEFAULT_BUFFER_SIZE * 7;
byte[] bytes = new byte[size];
InputStream input = randomDataToInputStream(bytes);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileCache(input, null))) {
byte[] result = new byte[size];
int head = stream.read(result, 0, 12); // Provoke a buffered read
int len = stream.read(result, 12, size - 12); // Rest of buffer + direct read
assertEquals(size, len + head);
assertArrayEquals(bytes, result);
}
}
}
@@ -1,434 +0,0 @@
/*
* Copyright (c) 2020, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.stream;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import javax.imageio.stream.ImageInputStream;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.ReadableByteChannel;
import java.util.Random;
import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rangeEquals;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
/**
* BufferedFileImageInputStreamTestCase
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
*/
// TODO: Remove this test, and instead test the memory cache directly!
public class BufferedChannelImageInputStreamMemoryCacheTest {
private final Random random = new Random(170984354357234566L);
private InputStream randomDataToInputStream(byte[] data) {
random.nextBytes(data);
return new ByteArrayInputStream(data);
}
@Test
public void testCreate() throws IOException {
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(new ByteArrayInputStream(new byte[0])))) {
assertEquals("Stream length should be unknown", -1, stream.length());
}
}
@Test
public void testCreateNullStream() {
try {
new MemoryCache((InputStream) null);
fail("Expected IllegalArgumentException");
}
catch (IllegalArgumentException expected) {
assertNotNull("Null exception message", expected.getMessage());
String message = expected.getMessage().toLowerCase();
assertTrue("Exception message does not contain parameter name", message.contains("stream"));
assertTrue("Exception message does not contain null", message.contains("null"));
}
}
@Test
public void testCreateNullChannel() {
try {
new MemoryCache((ReadableByteChannel) null);
fail("Expected IllegalArgumentException");
}
catch (IllegalArgumentException expected) {
assertNotNull("Null exception message", expected.getMessage());
String message = expected.getMessage().toLowerCase();
assertTrue("Exception message does not contain parameter name", message.contains("channel"));
assertTrue("Exception message does not contain null", message.contains("null"));
}
}
@Test
public void testRead() throws IOException {
byte[] data = new byte[1024 * 1024];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
assertEquals("Stream length should be unknown", -1, stream.length());
for (byte value : data) {
assertEquals("Wrong data read", value & 0xff, stream.read());
}
assertEquals("Wrong data read", -1, stream.read());
}
}
@Test
public void testReadArray() throws IOException {
byte[] data = new byte[1024 * 1024];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
assertEquals("Stream length should be unknown", -1, stream.length());
byte[] result = new byte[1024];
for (int i = 0; i < data.length / result.length; i++) {
stream.readFully(result);
assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
}
assertEquals("Wrong data read", -1, stream.read());
}
}
@Test
public void testReadSkip() throws IOException {
byte[] data = new byte[1024 * 14];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
assertEquals("Stream length should be unknown", -1, stream.length());
byte[] result = new byte[7];
for (int i = 0; i < data.length / result.length; i += 2) {
stream.readFully(result);
stream.skipBytes(result.length);
assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
}
}
}
@Test
public void testReadSeek() throws IOException {
byte[] data = new byte[1024 * 18];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
assertEquals("Stream length should be unknown", -1, stream.length());
byte[] result = new byte[9];
for (int i = 0; i < data.length / result.length; i++) {
// Read backwards
long newPos = data.length - result.length - i * result.length;
stream.seek(newPos);
assertEquals("Wrong stream position", newPos, stream.getStreamPosition());
stream.readFully(result);
assertTrue("Wrong data read: " + i, rangeEquals(data, (int) newPos, result, 0, result.length));
}
}
}
@Test
public void testReadOutsideDataSeek0Read() throws IOException {
byte[] data = new byte[256];
InputStream input = randomDataToInputStream(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
assertEquals("Stream length should be unknown", -1, stream.length());
byte[] buffer = new byte[data.length * 2];
stream.read(buffer);
stream.seek(0);
assertNotEquals(-1, stream.read());
assertNotEquals(-1, stream.read(buffer));
}
}
@Test
public void testReadBitRandom() throws IOException {
byte[] bytes = new byte[8];
InputStream input = randomDataToInputStream(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
try (ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
for (int i = 1; i <= 64; i++) {
assertEquals(String.format("bit %d differ", i), (value << (i - 1L)) >>> 63L, stream.readBit());
}
}
}
@Test
public void testReadBitsRandom() throws IOException {
byte[] bytes = new byte[8];
InputStream input = randomDataToInputStream(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
try (ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
for (int i = 1; i <= 64; i++) {
stream.seek(0);
assertEquals(String.format("bit %d differ", i), value >>> (64L - i), stream.readBits(i));
assertEquals(i % 8, stream.getBitOffset());
}
}
}
@Test
public void testReadBitsRandomOffset() throws IOException {
byte[] bytes = new byte[8];
InputStream input = randomDataToInputStream(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
try (ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
for (int i = 1; i <= 60; i++) {
stream.seek(0);
stream.setBitOffset(i % 8);
assertEquals(String.format("bit %d differ", i), (value << (i % 8)) >>> (64L - i), stream.readBits(i));
assertEquals(i * 2 % 8, stream.getBitOffset());
}
}
}
@Test
public void testReadShort() throws IOException {
byte[] bytes = new byte[8743]; // Slightly more than one buffer size
InputStream input = randomDataToInputStream(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < bytes.length / 2; i++) {
assertEquals(buffer.getShort(), stream.readShort());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readShort();
}
});
stream.seek(0);
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < bytes.length / 2; i++) {
assertEquals(buffer.getShort(), stream.readShort());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readShort();
}
});
}
}
@Test
public void testReadInt() throws IOException {
byte[] bytes = new byte[8743]; // Slightly more than one buffer size
InputStream input = randomDataToInputStream(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < bytes.length / 4; i++) {
assertEquals(buffer.getInt(), stream.readInt());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readInt();
}
});
stream.seek(0);
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < bytes.length / 4; i++) {
assertEquals(buffer.getInt(), stream.readInt());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readInt();
}
});
}
}
@Test
public void testReadLong() throws IOException {
byte[] bytes = new byte[8743]; // Slightly more than one buffer size
InputStream input = randomDataToInputStream(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < bytes.length / 8; i++) {
assertEquals(buffer.getLong(), stream.readLong());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readLong();
}
});
stream.seek(0);
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < bytes.length / 8; i++) {
assertEquals(buffer.getLong(), stream.readLong());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readLong();
}
});
}
}
@Test
public void testSeekPastEOF() throws IOException {
byte[] bytes = new byte[9];
InputStream input = randomDataToInputStream(bytes);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
stream.seek(1000);
assertEquals(-1, stream.read());
assertEquals(-1, stream.read(new byte[1], 0, 1));
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readFully(new byte[1]);
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readByte();
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readShort();
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readInt();
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readLong();
}
});
stream.seek(0);
for (byte value : bytes) {
assertEquals(value, stream.readByte());
}
assertEquals(-1, stream.read());
}
}
@Test
public void testClose() throws IOException {
// Create wrapper stream
Cache cache = mock(Cache.class);
ImageInputStream stream = new BufferedChannelImageInputStream(cache);
stream.close();
verify(cache, only()).close();
}
@Test
public void testWorkaroundForWBMPImageReaderExpectsReadToBehaveAsReadFully() throws IOException {
// See #606 for details.
// Bug in JDK WBMPImageReader, uses read(byte[], int, int) instead of readFully(byte[], int, int).
// Ie: Relies on read to return all bytes at once, without blocking
int size = BufferedChannelImageInputStream.DEFAULT_BUFFER_SIZE * 7;
byte[] bytes = new byte[size];
InputStream input = randomDataToInputStream(bytes);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new MemoryCache(input))) {
byte[] result = new byte[size];
int head = stream.read(result, 0, 12); // Provoke a buffered read
int len = stream.read(result, 12, size - 12); // Rest of buffer + direct read
assertEquals(size, len + head);
assertArrayEquals(bytes, result);
}
}
}
@@ -1,431 +0,0 @@
/*
* Copyright (c) 2020, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.stream;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import javax.imageio.stream.ImageInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.util.Random;
import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rangeEquals;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* BufferedFileImageInputStreamTestCase
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
*/
public class BufferedChannelImageInputStreamTest {
private final Random random = new Random(170984354357234566L);
private File randomDataToFile(byte[] data) throws IOException {
random.nextBytes(data);
File file = File.createTempFile("read", ".tmp");
Files.write(file.toPath(), data);
return file;
}
@Test
public void testCreate() throws IOException {
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(File.createTempFile("empty", ".tmp")))) {
assertEquals("Data length should be same as stream length", 0, stream.length());
}
}
@Test
public void testCreateNullFileInputStream() {
try {
new BufferedChannelImageInputStream((FileInputStream) null);
fail("Expected IllegalArgumentException");
}
catch (IllegalArgumentException expected) {
assertNotNull("Null exception message", expected.getMessage());
String message = expected.getMessage().toLowerCase();
assertTrue("Exception message does not contain parameter name", message.contains("inputstream"));
assertTrue("Exception message does not contain null", message.contains("null"));
}
}
@Test
public void testCreateNullByteChannel() {
try {
new BufferedChannelImageInputStream((SeekableByteChannel) null);
fail("Expected IllegalArgumentException");
}
catch (IllegalArgumentException expected) {
assertNotNull("Null exception message", expected.getMessage());
String message = expected.getMessage().toLowerCase();
assertTrue("Exception message does not contain parameter name", message.contains("channel"));
assertTrue("Exception message does not contain null", message.contains("null"));
}
}
@Test
public void testRead() throws IOException {
byte[] data = new byte[1024 * 1024];
File file = randomDataToFile(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
assertEquals("File length should be same as stream length", file.length(), stream.length());
for (byte value : data) {
assertEquals("Wrong data read", value & 0xff, stream.read());
}
}
}
@Test
public void testReadArray() throws IOException {
byte[] data = new byte[1024 * 1024];
File file = randomDataToFile(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
assertEquals("File length should be same as stream length", file.length(), stream.length());
byte[] result = new byte[1024];
for (int i = 0; i < data.length / result.length; i++) {
stream.readFully(result);
assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
}
}
}
@Test
public void testReadSkip() throws IOException {
byte[] data = new byte[1024 * 14];
File file = randomDataToFile(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
assertEquals("File length should be same as stream length", file.length(), stream.length());
byte[] result = new byte[7];
for (int i = 0; i < data.length / result.length; i += 2) {
stream.readFully(result);
stream.skipBytes(result.length);
assertTrue("Wrong data read: " + i, rangeEquals(data, i * result.length, result, 0, result.length));
}
}
}
@Test
public void testReadSeek() throws IOException {
byte[] data = new byte[1024 * 18];
File file = randomDataToFile(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
assertEquals("File length should be same as stream length", file.length(), stream.length());
byte[] result = new byte[9];
for (int i = 0; i < data.length / result.length; i++) {
// Read backwards
long newPos = stream.length() - result.length - i * result.length;
stream.seek(newPos);
assertEquals("Wrong stream position", newPos, stream.getStreamPosition());
stream.readFully(result);
assertTrue("Wrong data read: " + i, rangeEquals(data, (int) newPos, result, 0, result.length));
}
}
}
@Test
public void testReadOutsideDataSeek0Read() throws IOException {
byte[] data = new byte[256];
File file = randomDataToFile(data);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
assertEquals("File length should be same as stream length", file.length(), stream.length());
byte[] buffer = new byte[data.length * 2];
stream.read(buffer);
stream.seek(0);
assertNotEquals(-1, stream.read());
assertNotEquals(-1, stream.read(buffer));
}
}
@Test
public void testReadBitRandom() throws IOException {
byte[] bytes = new byte[8];
File file = randomDataToFile(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
try (ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
for (int i = 1; i <= 64; i++) {
assertEquals(String.format("bit %d differ", i), (value << (i - 1L)) >>> 63L, stream.readBit());
}
}
}
@Test
public void testReadBitsRandom() throws IOException {
byte[] bytes = new byte[8];
File file = randomDataToFile(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
try (ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
for (int i = 1; i <= 64; i++) {
stream.seek(0);
assertEquals(String.format("bit %d differ", i), value >>> (64L - i), stream.readBits(i));
assertEquals(i % 8, stream.getBitOffset());
}
}
}
@Test
public void testReadBitsRandomOffset() throws IOException {
byte[] bytes = new byte[8];
File file = randomDataToFile(bytes);
long value = ByteBuffer.wrap(bytes).getLong();
// Create stream
try (ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
for (int i = 1; i <= 60; i++) {
stream.seek(0);
stream.setBitOffset(i % 8);
assertEquals(String.format("bit %d differ", i), (value << (i % 8)) >>> (64L - i), stream.readBits(i));
assertEquals(i * 2 % 8, stream.getBitOffset());
}
}
}
@Test
public void testReadShort() throws IOException {
byte[] bytes = new byte[8743]; // Slightly more than one buffer size
File file = randomDataToFile(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < bytes.length / 2; i++) {
assertEquals(buffer.getShort(), stream.readShort());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readShort();
}
});
stream.seek(0);
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < bytes.length / 2; i++) {
assertEquals(buffer.getShort(), stream.readShort());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readShort();
}
});
}
}
@Test
public void testReadInt() throws IOException {
byte[] bytes = new byte[8743]; // Slightly more than one buffer size
File file = randomDataToFile(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < bytes.length / 4; i++) {
assertEquals(buffer.getInt(), stream.readInt());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readInt();
}
});
stream.seek(0);
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < bytes.length / 4; i++) {
assertEquals(buffer.getInt(), stream.readInt());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readInt();
}
});
}
}
@Test
public void testReadLong() throws IOException {
byte[] bytes = new byte[8743]; // Slightly more than one buffer size
File file = randomDataToFile(bytes);
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
stream.setByteOrder(ByteOrder.BIG_ENDIAN);
for (int i = 0; i < bytes.length / 8; i++) {
assertEquals(buffer.getLong(), stream.readLong());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readLong();
}
});
stream.seek(0);
stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < bytes.length / 8; i++) {
assertEquals(buffer.getLong(), stream.readLong());
}
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readLong();
}
});
}
}
@Test
public void testSeekPastEOF() throws IOException {
byte[] bytes = new byte[9];
File file = randomDataToFile(bytes);
try (final ImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
stream.seek(1000);
assertEquals(-1, stream.read());
assertEquals(-1, stream.read(new byte[1], 0, 1));
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readFully(new byte[1]);
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readByte();
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readShort();
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readInt();
}
});
assertThrows(EOFException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
stream.readLong();
}
});
stream.seek(0);
for (byte value : bytes) {
assertEquals(value, stream.readByte());
}
assertEquals(-1, stream.read());
}
}
@Test
public void testCloseChannel() throws IOException {
SeekableByteChannel channel = mock(SeekableByteChannel.class);
ImageInputStream stream = new BufferedChannelImageInputStream(channel);
stream.close();
verify(channel, never()).close();
}
@Test
public void testWorkaroundForWBMPImageReaderExpectsReadToBehaveAsReadFully() throws IOException {
// See #606 for details.
// Bug in JDK WBMPImageReader, uses read(byte[], int, int) instead of readFully(byte[], int, int).
// Ie: Relies on read to return all bytes at once, without blocking
int size = BufferedChannelImageInputStream.DEFAULT_BUFFER_SIZE * 7;
byte[] bytes = new byte[size];
File file = randomDataToFile(bytes);
try (BufferedChannelImageInputStream stream = new BufferedChannelImageInputStream(new FileInputStream(file))) {
byte[] result = new byte[size];
int head = stream.read(result, 0, 12); // Provoke a buffered read
int len = stream.read(result, 12, size - 12); // Rest of buffer + direct read
assertEquals(size, len + head);
assertArrayEquals(bytes, result);
}
}
}
@@ -45,9 +45,7 @@ import java.util.Random;
import static com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rangeEquals;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.*;
/**
* BufferedFileImageInputStreamTestCase
@@ -56,7 +54,6 @@ import static org.mockito.Mockito.verify;
* @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
*/
@Deprecated
public class BufferedFileImageInputStreamTest {
private final Random random = new Random(170984354357234566L);
@@ -75,7 +72,6 @@ public class BufferedFileImageInputStreamTest {
}
}
@SuppressWarnings("resource")
@Test
public void testCreateNullFile() throws IOException {
try {
@@ -1,26 +0,0 @@
package com.twelvemonkeys.imageio.stream;
import javax.imageio.spi.ImageInputStreamSpi;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/**
* BufferedInputStreamImageInputStreamSpiTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BufferedInputStreamImageInputStreamSpiTest.java,v 1.0 08/09/2022 haraldk Exp$
*/
public class BufferedFileInputStreamImageInputStreamSpiTest extends ImageInputStreamSpiTest<InputStream> {
@Override
protected ImageInputStreamSpi createProvider() {
return new BufferedInputStreamImageInputStreamSpi();
}
@Override
protected InputStream createInput() throws IOException {
return Files.newInputStream(File.createTempFile("test-", ".tst").toPath());
}
}
@@ -1,24 +0,0 @@
package com.twelvemonkeys.imageio.stream;
import javax.imageio.spi.ImageInputStreamSpi;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* BufferedInputStreamImageInputStreamSpiTest.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$
* @version $Id: BufferedInputStreamImageInputStreamSpiTest.java,v 1.0 08/09/2022 haraldk Exp$
*/
public class BufferedInputStreamImageInputStreamSpiTest extends ImageInputStreamSpiTest<InputStream> {
@Override
protected ImageInputStreamSpi createProvider() {
return new BufferedInputStreamImageInputStreamSpi();
}
@Override
protected InputStream createInput() {
return new ByteArrayInputStream(new byte[0]);
}
}
@@ -30,15 +30,20 @@
package com.twelvemonkeys.imageio.util;
import com.twelvemonkeys.lang.Validate;
import static org.junit.Assert.assertEquals;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DirectColorModel;
import java.awt.image.IndexColorModel;
import javax.imageio.ImageTypeSpecifier;
import org.junit.Test;
import javax.imageio.ImageTypeSpecifier;
import java.awt.color.*;
import java.awt.image.*;
import static org.junit.Assert.assertEquals;
import com.twelvemonkeys.lang.Validate;
public class ImageTypeSpecifiersTest {
@@ -65,19 +70,12 @@ public class ImageTypeSpecifiersTest {
ImageTypeSpecifier expected;
switch (type) {
// Special handling for INT_RGB and BGR, due to bug in ImageTypeSpecifier for these types (DirectColorModel is 32 bits)
case BufferedImage.TYPE_INT_RGB:
expected = createPacked(sRGB, 24, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, 0, DataBuffer.TYPE_INT, false);
break;
case BufferedImage.TYPE_INT_BGR:
expected = createPacked(sRGB, 24, DCM_BGR_RED_MASK, DCM_BGR_GRN_MASK, DCM_BGR_BLU_MASK, 0, DataBuffer.TYPE_INT, false);
break;
// Special handling for USHORT_565 and 555, due to bug in ImageTypeSpecifier for these types (DirectColorModel is 32 bits)
case BufferedImage.TYPE_USHORT_565_RGB:
expected = createPacked(sRGB, 16, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false);
expected = createPacked(sRGB, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false);
break;
case BufferedImage.TYPE_USHORT_555_RGB:
expected = createPacked(sRGB, 15, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false);
expected = createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false);
break;
default:
expected = ImageTypeSpecifier.createFromBufferedImageType(type);
@@ -88,24 +86,12 @@ public class ImageTypeSpecifiersTest {
}
@Test
public void testCreatePacked24() {
public void testCreatePacked32() {
// TYPE_INT_RGB
assertEquals(
createPacked(sRGB, 24, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, 0, DataBuffer.TYPE_INT, false),
ImageTypeSpecifier.createPacked(sRGB, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, 0, DataBuffer.TYPE_INT, false),
ImageTypeSpecifiers.createPacked(sRGB, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, 0, DataBuffer.TYPE_INT, false)
);
// TYPE_INT_BGR
assertEquals(
createPacked(sRGB, 24, DCM_BGR_RED_MASK, DCM_BGR_GRN_MASK, DCM_BGR_BLU_MASK, 0, DataBuffer.TYPE_INT, false),
ImageTypeSpecifiers.createPacked(sRGB, DCM_BGR_RED_MASK, DCM_BGR_GRN_MASK, DCM_BGR_BLU_MASK, 0, DataBuffer.TYPE_INT, false)
);
// Extra: Make sure color models bits is actually 24 (ImageTypeSpecifier equivalent returns 32)
assertEquals(24, ImageTypeSpecifiers.createPacked(sRGB, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, 0, DataBuffer.TYPE_INT, false).getColorModel().getPixelSize());
}
@Test
public void testCreatePacked32() {
// TYPE_INT_ARGB
assertEquals(
ImageTypeSpecifier.createPacked(sRGB, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, DCM_ALPHA_MASK, DataBuffer.TYPE_INT, false),
@@ -116,36 +102,35 @@ public class ImageTypeSpecifiersTest {
ImageTypeSpecifier.createPacked(sRGB, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, DCM_ALPHA_MASK, DataBuffer.TYPE_INT, true),
ImageTypeSpecifiers.createPacked(sRGB, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, DCM_ALPHA_MASK, DataBuffer.TYPE_INT, true)
);
}
@Test
public void testCreatePacked15() {
// TYPE_USHORT_555_RGB
// TYPE_INT_BGR
assertEquals(
createPacked(sRGB, 15, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false),
ImageTypeSpecifiers.createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false)
ImageTypeSpecifier.createPacked(sRGB, DCM_BGR_RED_MASK, DCM_BGR_GRN_MASK, DCM_BGR_BLU_MASK, 0, DataBuffer.TYPE_INT, false),
ImageTypeSpecifiers.createPacked(sRGB, DCM_BGR_RED_MASK, DCM_BGR_GRN_MASK, DCM_BGR_BLU_MASK, 0, DataBuffer.TYPE_INT, false)
);
// "SHORT 555 RGB" (impossible, only BYTE, USHORT, INT supported)
// Extra: Make sure color models bits is actually 15 (ImageTypeSpecifier equivalent returns 32)
assertEquals(15, ImageTypeSpecifiers.createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false).getColorModel().getPixelSize());
}
@Test
public void testCreatePacked16() {
// TYPE_USHORT_555_RGB
assertEquals(
createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false),
ImageTypeSpecifiers.createPacked(sRGB, DCM_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false)
);
// "SHORT 555 RGB" (impossible, only BYTE, USHORT, INT supported)
// TYPE_USHORT_565_RGB
assertEquals(
createPacked(sRGB, 16, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false),
createPacked(sRGB, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false),
ImageTypeSpecifiers.createPacked(sRGB, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false)
);
// "USHORT 4444 ARGB"
assertEquals(
createPacked(sRGB, 16,0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, false),
createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, false),
ImageTypeSpecifiers.createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, false)
);
// "USHORT 4444 ARGB PRE"
assertEquals(
createPacked(sRGB, 16, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, true),
createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, true),
ImageTypeSpecifiers.createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, true)
);
@@ -157,17 +142,17 @@ public class ImageTypeSpecifiersTest {
public void testCreatePacked8() {
// "BYTE 332 RGB"
assertEquals(
createPacked(sRGB, 8, 0xe0, 0x1c, 0x03, 0x0, DataBuffer.TYPE_BYTE, false),
createPacked(sRGB, 0xe0, 0x1c, 0x03, 0x0, DataBuffer.TYPE_BYTE, false),
ImageTypeSpecifiers.createPacked(sRGB, 0xe0, 0x1c, 0x3, 0x0, DataBuffer.TYPE_BYTE, false)
);
// "BYTE 2222 ARGB"
assertEquals(
createPacked(sRGB, 8, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, false),
createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, false),
ImageTypeSpecifiers.createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, false)
);
// "BYTE 2222 ARGB PRE"
assertEquals(
createPacked(sRGB, 8, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, true),
createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, true),
ImageTypeSpecifiers.createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, true)
);
@@ -175,12 +160,15 @@ public class ImageTypeSpecifiersTest {
assertEquals(8, ImageTypeSpecifiers.createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, false).getColorModel().getPixelSize());
}
private ImageTypeSpecifier createPacked(final ColorSpace colorSpace, final int bits,
private ImageTypeSpecifier createPacked(final ColorSpace colorSpace,
final int redMask, final int greenMask, final int blueMask, final int alphaMask,
final int transferType, final boolean isAlphaPremultiplied) {
Validate.isTrue(transferType == DataBuffer.TYPE_BYTE || transferType == DataBuffer.TYPE_USHORT || transferType == DataBuffer.TYPE_INT, transferType, "transferType: %s");
Validate.isTrue(transferType == DataBuffer.TYPE_BYTE || transferType == DataBuffer.TYPE_USHORT, transferType, "transferType: %s");
ColorModel colorModel = new DirectColorModel(colorSpace, bits, redMask, greenMask, blueMask, alphaMask, isAlphaPremultiplied, transferType);
int bits = transferType == DataBuffer.TYPE_BYTE ? 8 : 16;
ColorModel colorModel =
new DirectColorModel(colorSpace, bits, redMask, greenMask, blueMask, alphaMask, isAlphaPremultiplied, transferType);
return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1));
}
@@ -728,28 +716,6 @@ public class ImageTypeSpecifiersTest {
);
}
@Test
public void testCreateFromBufferedImageTypeShouldEqualConstructor() {
for (int type = BufferedImage.TYPE_INT_RGB; type < BufferedImage.TYPE_BYTE_INDEXED; type++) {
BufferedImage image = new BufferedImage(1, 1, type);
ImageTypeSpecifier fromConstructor = new ImageTypeSpecifier(image);
ImageTypeSpecifier fromType = ImageTypeSpecifiers.createFromBufferedImageType(type);
assertEquals(fromConstructor.getColorModel(), fromType.getColorModel());
}
}
@Test
public void testCreateFromRenderedImageShouldEqualConstructor() {
for (int type = BufferedImage.TYPE_INT_RGB; type < BufferedImage.TYPE_BYTE_INDEXED; type++) {
BufferedImage image = new BufferedImage(1, 1, type);
ImageTypeSpecifier fromConstructor = new ImageTypeSpecifier(image);
ImageTypeSpecifier fromImage = ImageTypeSpecifiers.createFromRenderedImage(image);
assertEquals(fromConstructor.getColorModel(), fromImage.getColorModel());
}
}
private static byte[] createByteLut(final int count) {
byte[] lut = new byte[count];
for (int i = 0; i < count; i++) {
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-hdr</artifactId>
<name>TwelveMonkeys :: ImageIO :: HDR plugin</name>
@@ -40,8 +40,11 @@ import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.color.*;
import java.awt.image.*;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
@@ -241,7 +244,10 @@ public final class HDRImageReader extends ImageReaderBase {
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
return new HDRMetadata(getRawImageType(imageIndex), header);
checkBounds(imageIndex);
readHeader();
return new HDRMetadata(header);
}
public static void main(final String[] args) throws IOException {
@@ -1,19 +1,83 @@
/*
* Copyright (c) 2015, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.hdr;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataNode;
public class HDRMetadata extends StandardImageMetadataSupport {
public HDRMetadata(ImageTypeSpecifier type, HDRHeader header) {
super(builder(type)
.withCompressionTypeName("RLE")
.withTextEntry("Software", header.getSoftware()));
final class HDRMetadata extends AbstractMetadata {
private final HDRHeader header;
HDRMetadata(final HDRHeader header) {
this.header = header;
}
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType);
csType.setAttribute("name", "RGB");
// TODO: Support XYZ
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
numChannels.setAttribute("value", "3");
chroma.appendChild(numChannels);
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
blackIsZero.setAttribute("value", "TRUE");
chroma.appendChild(blackIsZero);
return chroma;
}
// No compression
@Override
protected IIOMetadataNode getStandardCompressionNode() {
IIOMetadataNode node = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
compressionTypeName.setAttribute("value", "RLE");
node.appendChild(compressionTypeName);
IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
lossless.setAttribute("value", "TRUE");
node.appendChild(lossless);
return node;
}
// For HDR, the stored sample data is UnsignedIntegral and data is 4 channels (RGB+Exp),
// but decoded to Real (float) 3 chanel RGB
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode node = new IIOMetadataNode("Data");
@@ -28,4 +92,38 @@ public class HDRMetadata extends StandardImageMetadataSupport {
return node;
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
// TODO: Support other orientations
IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
imageOrientation.setAttribute("value", "Normal");
dimension.appendChild(imageOrientation);
return dimension;
}
// No document node
@Override
protected IIOMetadataNode getStandardTextNode() {
if (header.getSoftware() != null) {
IIOMetadataNode text = new IIOMetadataNode("Text");
IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
textEntry.setAttribute("keyword", "Software");
textEntry.setAttribute("value", header.getSoftware());
text.appendChild(textEntry);
return text;
}
return null;
}
// No tiling
// No transparency
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-icns</artifactId>
<name>TwelveMonkeys :: ImageIO :: ICNS plugin</name>
@@ -1,41 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.icns;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import javax.imageio.ImageTypeSpecifier;
final class ICNSImageMetadata extends StandardImageMetadataSupport {
ICNSImageMetadata(ImageTypeSpecifier type, String compressionName) {
super(builder(type).withCompressionTypeName(compressionName));
}
}
@@ -35,16 +35,11 @@ import com.twelvemonkeys.imageio.stream.SubImageInputStream;
import com.twelvemonkeys.imageio.util.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.*;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataInputStream;
import java.io.File;
@@ -66,9 +61,10 @@ import java.util.List;
* @see <a href="http://en.wikipedia.org/wiki/Apple_Icon_Image_format">Apple Icon Image format (Wikipedia)</a>
*/
public final class ICNSImageReader extends ImageReaderBase {
// TODO: Support ToC resource for faster parsing/faster determine number of icons?
// TODO: Subsampled reading for completeness, even if never used?
private final List<IconResource> icons = new ArrayList<>();
private final List<IconResource> masks = new ArrayList<>();
private List<IconResource> icons = new ArrayList<IconResource>();
private List<IconResource> masks = new ArrayList<IconResource>();
private IconResource lastResourceRead;
private int length;
@@ -140,7 +136,7 @@ public final class ICNSImageReader extends ImageReaderBase {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
IconResource resource = readIconResource(imageIndex);
List<ImageTypeSpecifier> specifiers = new ArrayList<>();
List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>();
switch (resource.depth()) {
case 1:
@@ -234,9 +230,14 @@ public final class ICNSImageReader extends ImageReaderBase {
packedSize -= 4;
}
try (InputStream input = IIOUtil.createStreamAdapter(imageInput, packedSize)) {
InputStream input = IIOUtil.createStreamAdapter(imageInput, packedSize);
try {
ICNSUtil.decompress(new DataInputStream(input), data, 0, (data.length * 24) / 32); // 24 bit data
}
finally {
input.close();
}
}
else {
data = new byte[resource.length - ICNS.RESOURCE_HEADER_SIZE];
@@ -490,7 +491,7 @@ public final class ICNSImageReader extends ImageReaderBase {
String format;
if (Arrays.equals(ICNS.PNG_MAGIC, Arrays.copyOfRange(magic, 0, ICNS.PNG_MAGIC.length))) {
if (Arrays.equals(ICNS.PNG_MAGIC, magic)) {
format = "PNG";
}
else if (Arrays.equals(ICNS.JPEG_2000_MAGIC, magic)) {
@@ -526,6 +527,7 @@ public final class ICNSImageReader extends ImageReaderBase {
IconResource resource = IconResource.read(imageInput);
if (resource.isTOC()) {
// TODO: IconResource.readTOC()?
int resourceCount = (resource.length - ICNS.RESOURCE_HEADER_SIZE) / ICNS.RESOURCE_HEADER_SIZE;
long pos = resource.start + resource.length;
@@ -568,23 +570,6 @@ public final class ICNSImageReader extends ImageReaderBase {
}
}
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
IconResource resource = readIconResource(imageIndex);
String compressionName;
if (resource.isForeignFormat()) {
// Special handling of PNG/JPEG 2000 icons
imageInput.seek(resource.start + ICNS.RESOURCE_HEADER_SIZE);
compressionName = getForeignFormat(imageInput);
}
else {
compressionName = resource.isCompressed() ? "RLE" : "None";
}
return new ICNSImageMetadata(getRawImageType(imageIndex), compressionName);
}
private static final class ICNSBitMaskColorModel extends IndexColorModel {
static final IndexColorModel INSTANCE = new ICNSBitMaskColorModel();
@@ -593,6 +578,7 @@ public final class ICNSImageReader extends ImageReaderBase {
}
}
@SuppressWarnings({"UnusedAssignment"})
public static void main(String[] args) throws IOException {
int argIndex = 0;
@@ -34,13 +34,7 @@ import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.*;
import javax.imageio.event.IIOWriteWarningListener;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
@@ -110,7 +104,6 @@ public final class ICNSImageWriter extends ImageWriterBase {
sequenceIndex = 0;
}
@SuppressWarnings("RedundantThrows")
@Override
public void endWriteSequence() throws IOException {
assertOutput();
@@ -38,13 +38,8 @@ import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Iterator;
/**
@@ -145,12 +140,17 @@ final class SipsJP2Reader {
}
private static String checkErrorMessage(final Process process) throws IOException {
try (InputStream stream = process.getErrorStream()) {
InputStream stream = process.getErrorStream();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
String message = reader.readLine();
return message != null && message.startsWith("Error: ") ? message.substring(7) : null;
}
finally {
stream.close();
}
}
private static String[] buildCommand(final File sipsCommand, final File tempFile) {
@@ -159,13 +159,19 @@ final class SipsJP2Reader {
};
}
private static File dumpToFile(final ImageInputStream stream) throws IOException {
File tempFile = File.createTempFile("imageio-icns-", ".png");
tempFile.deleteOnExit();
try (FileOutputStream out = new FileOutputStream(tempFile)) {
FileOutputStream out = new FileOutputStream(tempFile);
try {
FileUtil.copy(IIOUtil.createStreamAdapter(stream), out);
}
finally {
out.close();
}
return tempFile;
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-iff</artifactId>
<name>TwelveMonkeys :: ImageIO :: IFF plugin</name>
@@ -1,13 +1,13 @@
package com.twelvemonkeys.imageio.plugins.iff;
import javax.imageio.IIOException;
import java.awt.image.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.util.ArrayList;
import java.util.List;
import static com.twelvemonkeys.imageio.plugins.iff.IFF.*;
import static com.twelvemonkeys.imageio.plugins.iff.IFFUtil.toChunkStr;
import static com.twelvemonkeys.lang.Validate.isTrue;
/**
* Form.
@@ -27,7 +27,7 @@ abstract class Form {
abstract int width();
abstract int height();
abstract double aspect();
abstract float aspect();
abstract int bitplanes();
abstract int compressionType();
@@ -118,7 +118,7 @@ abstract class Form {
}
private ILBMForm(final int formType, final BMHDChunk bitmapHeader, final CAMGChunk viewMode, final CMAPChunk colorMap, final AbstractMultiPaletteChunk multiPalette, final XS24Chunk thumbnail, final BODYChunk body) {
super(isTrue(validFormType(formType), formType, "Unknown IFF Form type: %s"));
super(formType);
this.bitmapHeader = bitmapHeader;
this.viewMode = viewMode;
this.colorMap = colorMap;
@@ -127,19 +127,6 @@ abstract class Form {
this.body = body;
}
private static boolean validFormType(int formType) {
switch (formType) {
case TYPE_ACBM:
case TYPE_ILBM:
case TYPE_PBM:
case TYPE_RGB8:
case TYPE_RGBN:
return true;
default:
return false;
}
}
@Override
int width() {
return bitmapHeader.width;
@@ -161,8 +148,8 @@ abstract class Form {
}
@Override
double aspect() {
return bitmapHeader.yAspect == 0 ? 0 : (bitmapHeader.xAspect / (double) bitmapHeader.yAspect);
float aspect() {
return bitmapHeader.yAspect == 0 ? 0 : (bitmapHeader.xAspect / (float) bitmapHeader.yAspect);
}
@Override
@@ -310,7 +297,7 @@ abstract class Form {
}
private DEEPForm(final int formType, final DGBLChunk deepGlobal, final DLOCChunk deepLocation, final DPELChunk deepPixel, final XS24Chunk thumbnail, final BODYChunk body) {
super(isTrue(validFormType(formType), formType, "Unknown IFF Form type: %s"));
super(formType);
this.deepGlobal = deepGlobal;
this.deepLocation = deepLocation;
this.deepPixel = deepPixel;
@@ -318,15 +305,6 @@ abstract class Form {
this.body = body;
}
private static boolean validFormType(int formType) {
switch (formType) {
case TYPE_DEEP:
case TYPE_TVPP:
return true;
default:
return false;
}
}
@Override
int width() {
@@ -359,8 +337,8 @@ abstract class Form {
}
@Override
double aspect() {
return deepGlobal.yAspect == 0 ? 0 : deepGlobal.xAspect / (double) deepGlobal.yAspect;
float aspect() {
return deepGlobal.yAspect == 0 ? 0 : deepGlobal.xAspect / (float) deepGlobal.yAspect;
}
@Override
@@ -1,83 +1,188 @@
/*
* Copyright (c) 2020, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.ImageTypeSpecifier;
import java.awt.image.*;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.*;
import java.awt.image.IndexColorModel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static com.twelvemonkeys.imageio.plugins.iff.IFF.*;
import static com.twelvemonkeys.imageio.plugins.iff.IFFUtil.toChunkStr;
import static com.twelvemonkeys.lang.Validate.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull;
import static java.util.Collections.emptyList;
final class IFFImageMetadata extends StandardImageMetadataSupport {
IFFImageMetadata(ImageTypeSpecifier type, Form header, IndexColorModel palette) {
this(builder(type), notNull(header, "header"), palette);
final class IFFImageMetadata extends AbstractMetadata {
private final Form header;
private final IndexColorModel colorMap;
private final List<GenericChunk> meta;
IFFImageMetadata(Form header, IndexColorModel colorMap) {
this.header = notNull(header, "header");
isTrue(validFormType(header.formType), header.formType, "Unknown IFF Form type: %s");
this.colorMap = colorMap;
this.meta = header.meta;
}
private IFFImageMetadata(Builder builder, Form header, IndexColorModel palette) {
super(builder.withPalette(palette)
.withCompressionTypeName(compressionName(header))
.withBitsPerSample(bitsPerSample(header))
.withPlanarConfiguration(planarConfiguration(header))
.withPixelAspectRatio(header.aspect() != 0 ? header.aspect() : null)
.withFormatVersion("1.0")
.withTextEntries(textEntries(header)));
}
private static String compressionName(Form header) {
switch (header.compressionType()) {
case BMHDChunk.COMPRESSION_NONE:
return "None";
case BMHDChunk.COMPRESSION_BYTE_RUN:
return "RLE";
case 4:
// Compression type 4 means different things for different FORM types, we support
// Impulse RGB8 RLE compression: 24 bit RGB + 1 bit mask + 7 bit run count
if (header.formType == TYPE_RGB8) {
return "RGB8";
}
private boolean validFormType(int formType) {
switch (formType) {
default:
return "Unknown";
return false;
case TYPE_ACBM:
case TYPE_DEEP:
case TYPE_ILBM:
case TYPE_PBM:
case TYPE_RGB8:
case TYPE_RGBN:
case TYPE_TVPP:
return true;
}
}
private static int[] bitsPerSample(Form header) {
int bitplanes = header.bitplanes();
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType);
switch (header.bitplanes()) {
case 8:
if (colorMap == null) {
csType.setAttribute("name", "GRAY");
break;
}
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 24:
case 25:
case 32:
csType.setAttribute("name", "RGB");
break;
default:
csType.setAttribute("name", "Unknown");
}
// NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data)
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
chroma.appendChild(numChannels);
if (colorMap == null && header.bitplanes() == 8) {
numChannels.setAttribute("value", Integer.toString(1));
}
else if (header.bitplanes() == 25 || header.bitplanes() == 32) {
numChannels.setAttribute("value", Integer.toString(4));
}
else {
numChannels.setAttribute("value", Integer.toString(3));
}
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
chroma.appendChild(blackIsZero);
blackIsZero.setAttribute("value", "TRUE");
// NOTE: TGA files may contain a color map, even if true color...
// Not sure if this is a good idea to expose to the meta data,
// as it might be unexpected... Then again...
if (colorMap != null) {
IIOMetadataNode palette = new IIOMetadataNode("Palette");
chroma.appendChild(palette);
for (int i = 0; i < colorMap.getMapSize(); i++) {
IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry");
palette.appendChild(paletteEntry);
paletteEntry.setAttribute("index", Integer.toString(i));
paletteEntry.setAttribute("red", Integer.toString(colorMap.getRed(i)));
paletteEntry.setAttribute("green", Integer.toString(colorMap.getGreen(i)));
paletteEntry.setAttribute("blue", Integer.toString(colorMap.getBlue(i)));
}
if (colorMap.getTransparentPixel() != -1) {
IIOMetadataNode backgroundIndex = new IIOMetadataNode("BackgroundIndex");
chroma.appendChild(backgroundIndex);
backgroundIndex.setAttribute("value", Integer.toString(colorMap.getTransparentPixel()));
}
}
// TODO: TVPP TVPaint Project files have a MIXR chunk with a background color
// and also a BGP1 (background pen 1?) and BGP2 chunks
// if (extensions != null && extensions.getBackgroundColor() != 0) {
// Color background = new Color(extensions.getBackgroundColor(), true);
//
// IIOMetadataNode backgroundColor = new IIOMetadataNode("BackgroundColor");
// chroma.appendChild(backgroundColor);
//
// backgroundColor.setAttribute("red", Integer.toString(background.getRed()));
// backgroundColor.setAttribute("green", Integer.toString(background.getGreen()));
// backgroundColor.setAttribute("blue", Integer.toString(background.getBlue()));
// }
return chroma;
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
if (header.compressionType() == BMHDChunk.COMPRESSION_NONE) {
return null; // All defaults
}
IIOMetadataNode node = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
compressionTypeName.setAttribute("value", "RLE");
node.appendChild(compressionTypeName);
IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
lossless.setAttribute("value", "TRUE");
node.appendChild(lossless);
return node;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode data = new IIOMetadataNode("Data");
// PlanarConfiguration
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
switch (header.formType) {
case TYPE_DEEP:
case TYPE_TVPP:
case TYPE_RGB8:
case TYPE_PBM:
planarConfiguration.setAttribute("value", "PixelInterleaved");
break;
case TYPE_ILBM:
planarConfiguration.setAttribute("value", "PlaneInterleaved");
break;
default:
planarConfiguration.setAttribute("value", "Unknown " + IFFUtil.toChunkStr(header.formType));
break;
}
data.appendChild(planarConfiguration);
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
sampleFormat.setAttribute("value", colorMap != null ? "Index" : "UnsignedIntegral");
data.appendChild(sampleFormat);
// BitsPerSample
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
String value = bitsPerSampleValue(header.bitplanes());
bitsPerSample.setAttribute("value", value);
data.appendChild(bitsPerSample);
// SignificantBitsPerSample not in format
// SampleMSB not in format
return data;
}
private String bitsPerSampleValue(int bitplanes) {
switch (bitplanes) {
case 1:
case 2:
@@ -87,47 +192,91 @@ final class IFFImageMetadata extends StandardImageMetadataSupport {
case 6:
case 7:
case 8:
return new int[] {bitplanes};
return Integer.toString(bitplanes);
case 24:
return new int[] {8, 8, 8};
return "8 8 8";
case 25:
if (header.formType != TYPE_RGB8) {
throw new IllegalArgumentException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(header.formType)));
}
return new int[] {8, 8, 8, 1};
return "8 8 8 1";
case 32:
return new int[] {8, 8, 8, 8};
return "8 8 8 8";
default:
throw new IllegalArgumentException("Unknown bit count: " + bitplanes);
}
}
private static PlanarConfiguration planarConfiguration(Form header) {
switch (header.formType) {
case TYPE_DEEP:
case TYPE_TVPP:
case TYPE_RGB8:
case TYPE_PBM:
return PlanarConfiguration.PixelInterleaved;
case TYPE_ILBM:
return PlanarConfiguration.PlaneInterleaved;
default:
return null;
@Override
protected IIOMetadataNode getStandardDimensionNode() {
if (header.aspect() == 0) {
return null;
}
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
// PixelAspectRatio
IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio");
pixelAspectRatio.setAttribute("value", String.valueOf(header.aspect()));
dimension.appendChild(pixelAspectRatio);
// TODO: HorizontalScreenSize?
// TODO: VerticalScreenSize?
return dimension;
}
private static List<TextEntry> textEntries(Form header) {
if (header.meta.isEmpty()) {
return emptyList();
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode document = new IIOMetadataNode("Document");
IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion");
document.appendChild(formatVersion);
formatVersion.setAttribute("value", "1.0");
return document;
}
@Override
protected IIOMetadataNode getStandardTextNode() {
if (meta.isEmpty()) {
return null;
}
List<TextEntry> text = new ArrayList<>();
for (GenericChunk chunk : header.meta) {
text.add(new TextEntry(toChunkStr(chunk.chunkId),
new String(chunk.data, chunk.chunkId == IFF.CHUNK_UTF8 ? StandardCharsets.UTF_8:StandardCharsets.US_ASCII)));
IIOMetadataNode text = new IIOMetadataNode("Text");
// /Text/TextEntry@keyword = field name, /Text/TextEntry@value = field value.
for (GenericChunk chunk : meta) {
IIOMetadataNode node = new IIOMetadataNode("TextEntry");
node.setAttribute("keyword", IFFUtil.toChunkStr(chunk.chunkId));
node.setAttribute("value", new String(chunk.data, chunk.chunkId == IFF.CHUNK_UTF8 ? StandardCharsets.UTF_8 : StandardCharsets.US_ASCII));
text.appendChild(node);
}
return text;
}
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
if ((colorMap == null || !colorMap.hasAlpha()) && header.bitplanes() != 32 && header.bitplanes() != 25) {
return null;
}
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
if (header.bitplanes() == 25 || header.bitplanes() == 32) {
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
alpha.setAttribute("value", header.premultiplied() ? "premultiplied" : "nonpremultiplied");
transparency.appendChild(alpha);
}
if (colorMap != null && colorMap.getTransparency() == Transparency.BITMASK) {
IIOMetadataNode transparentIndex = new IIOMetadataNode("TransparentIndex");
transparentIndex.setAttribute("value", Integer.toString(colorMap.getTransparentPixel()));
transparency.appendChild(transparentIndex);
}
return transparency;
}
}
@@ -38,19 +38,14 @@ import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
@@ -355,7 +350,9 @@ public final class IFFImageReader extends ImageReaderBase {
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
return new IFFImageMetadata(getRawImageType(imageIndex), header, header.colorMap());
init(imageIndex);
return new IFFImageMetadata(header, header.colorMap());
}
@Override
@@ -1,37 +1,24 @@
package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.IIOException;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.*;
import java.awt.image.IndexColorModel;
import java.nio.charset.StandardCharsets;
import static java.awt.image.BufferedImage.*;
import static org.junit.Assert.*;
public class IFFImageMetadataTest {
private static final ImageTypeSpecifier TYPE_8_BIT_GRAY = ImageTypeSpecifiers.createFromBufferedImageType(TYPE_BYTE_GRAY);
private static final ImageTypeSpecifier TYPE_8_BIT_PALETTE = ImageTypeSpecifiers.createFromBufferedImageType(TYPE_BYTE_INDEXED);
private static final ImageTypeSpecifier TYPE_24_BIT_RGB = ImageTypeSpecifiers.createFromBufferedImageType(TYPE_3BYTE_BGR);
private static final ImageTypeSpecifier TYPE_32_BIT_ARGB = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR);
private static final ImageTypeSpecifier TYPE_32_BIT_ARGB_DEEP = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE);
@Test
public void testStandardFeatures() throws IIOException {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
final IFFImageMetadata metadata = new IFFImageMetadata(TYPE_24_BIT_RGB, header, header.colorMap());
final IFFImageMetadata metadata = new IFFImageMetadata(header, null);
// Standard metadata format
assertTrue(metadata.isStandardMetadataFormatSupported());
@@ -64,9 +51,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_GRAY, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma");
IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength());
@@ -91,9 +78,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_24_BIT_RGB, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma");
IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength());
@@ -119,10 +106,9 @@ public class IFFImageMetadataTest {
.with(new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1));
byte[] bw = {0, (byte) 0xff};
ImageTypeSpecifier fromIndexColorModel = ImageTypeSpecifiers.createFromIndexColorModel(new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex()));
IFFImageMetadata metadata = new IFFImageMetadata(fromIndexColorModel, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex()));
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma");
IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName());
assertEquals(5, chroma.getLength());
@@ -133,7 +119,7 @@ public class IFFImageMetadataTest {
IIOMetadataNode numChannels = (IIOMetadataNode) colorSpaceType.getNextSibling();
assertEquals("NumChannels", numChannels.getNodeName());
assertEquals("4", numChannels.getAttribute("value"));
assertEquals("3", numChannels.getAttribute("value"));
IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling();
assertEquals("BlackIsZero", blackIsZero.getNodeName());
@@ -167,9 +153,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_24_BIT_RGB, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode compression = getStandardNode(metadata, "Compression");
IIOMetadataNode compression = metadata.getStandardCompressionNode();
assertNotNull(compression);
assertEquals("Compression", compression.getNodeName());
assertEquals(2, compression.getLength());
@@ -190,9 +176,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_NONE, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_24_BIT_RGB, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
assertNull(getStandardNode(metadata, "Compression")); // No compression, all default...
assertNull(metadata.getStandardCompressionNode()); // No compression, all default...
}
@Test
@@ -200,9 +186,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_GRAY, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -227,9 +213,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_24_BIT_RGB, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -254,9 +240,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 32, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_32_BIT_ARGB, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -283,10 +269,9 @@ public class IFFImageMetadataTest {
.with(new BMHDChunk(300, 200, i, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
byte[] rgb = new byte[2 << i]; // Colors doesn't really matter here
ImageTypeSpecifier fromIndexColorModel = ImageTypeSpecifiers.createFromIndexColorModel(new IndexColorModel(header.bitplanes(), rgb.length, rgb, rgb, rgb, 0));
IFFImageMetadata metadata = new IFFImageMetadata(fromIndexColorModel, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, new IndexColorModel(header.bitplanes(), rgb.length, rgb, rgb, rgb, 0));
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -312,9 +297,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_PBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_GRAY, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -339,9 +324,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_PBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_24_BIT_RGB, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -371,20 +356,10 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(bitmapHeader);
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_PALETTE, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = getStandardNode(metadata, "Dimension");
if (dimension != null) {
assertEquals("Dimension", dimension.getNodeName());
assertEquals(1, dimension.getLength());
IIOMetadataNode imageOrientation = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
}
IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNull(dimension);
}
@Test
@@ -393,24 +368,20 @@ public class IFFImageMetadataTest {
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0))
.with(new CAMGChunk(4));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_PALETTE, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = getStandardNode(metadata, "Dimension");
IIOMetadataNode dimension = metadata.getStandardDimensionNode();
// No Dimension node is okay, or one with an aspect ratio of 1.0
if (dimension != null) {
assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength());
assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling();
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
}
@@ -427,22 +398,18 @@ public class IFFImageMetadataTest {
.with(bitmapHeader)
.with(viewPort);
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_PALETTE, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = getStandardNode(metadata, "Dimension");
IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength());
assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("2.0", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling();
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
@Test
@@ -458,22 +425,18 @@ public class IFFImageMetadataTest {
.with(bitmapHeader)
.with(viewPort);
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_PALETTE, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = getStandardNode(metadata, "Dimension");
IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength());
assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("0.5", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling();
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
@Test
@@ -484,22 +447,18 @@ public class IFFImageMetadataTest {
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0))
.with(viewPort);
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_PALETTE, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode dimension = getStandardNode(metadata, "Dimension");
IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength());
assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling();
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
@Test
@@ -507,33 +466,32 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_PALETTE, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode document = getStandardNode(metadata, "Document");
IIOMetadataNode document = metadata.getStandardDocumentNode();
assertNotNull(document);
assertEquals("Document", document.getNodeName());
assertEquals(1, document.getLength());
IIOMetadataNode formatVersion = (IIOMetadataNode) document.getFirstChild();
assertEquals("FormatVersion", formatVersion.getNodeName());
assertEquals("1.0", formatVersion.getAttribute("value"));
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) document.getFirstChild();
assertEquals("FormatVersion", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
assertNull(formatVersion.getNextSibling()); // No more children
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
@Test
public void testStandardText() throws IIOException {
int[] chunks = {IFF.CHUNK_ANNO, IFF.CHUNK_ANNO, IFF.CHUNK_UTF8};
String[] texts = {"annotation", "dupe", "äñnótâtïøñ"};
int[] chunks = {IFF.CHUNK_ANNO, IFF.CHUNK_UTF8};
String[] texts = {"annotation", "äñnótâtïøñ"};
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0))
.with(new GenericChunk(chunks[0], texts[0].getBytes(StandardCharsets.US_ASCII)))
.with(new GenericChunk(chunks[1], texts[1].getBytes(StandardCharsets.US_ASCII)))
.with(new GenericChunk(chunks[2], texts[2].getBytes(StandardCharsets.UTF_8)));
.with(new GenericChunk(chunks[1], texts[1].getBytes(StandardCharsets.UTF_8)));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_8_BIT_PALETTE, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode text = getStandardNode(metadata, "Text");
IIOMetadataNode text = metadata.getStandardTextNode();
assertNotNull(text);
assertEquals("Text", text.getNodeName());
assertEquals(texts.length, text.getLength());
@@ -551,21 +509,10 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_24_BIT_RGB, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency");
if (transparency != null) {
assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("Alpha", alpha.getNodeName());
assertEquals("none", alpha.getAttribute("value"));
assertNull(alpha.getNextSibling()); // No more children
}
// Otherwise, no transparency, just defaults
IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNull(transparency); // No transparency, just defaults
}
@Test
@@ -573,18 +520,18 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 32, BMHDChunk.MASK_HAS_MASK, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_32_BIT_ARGB, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency");
IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("Alpha", alpha.getNodeName());
assertEquals("nonpremultiplied", alpha.getAttribute("value"));
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("Alpha", pixelAspectRatio.getNodeName());
assertEquals("nonpremultiplied", pixelAspectRatio.getAttribute("value"));
assertNull(alpha.getNextSibling()); // No more children
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
@Test
@@ -593,33 +540,28 @@ public class IFFImageMetadataTest {
.with(new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1));
byte[] bw = {0, (byte) 0xff};
ImageTypeSpecifier fromIndexColorModel = ImageTypeSpecifiers.createFromIndexColorModel(new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex()));
IFFImageMetadata metadata = new IFFImageMetadata(fromIndexColorModel, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex()));
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency");
IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName());
assertEquals(2, transparency.getLength());
assertEquals(1, transparency.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("Alpha", alpha.getNodeName());
assertEquals("nonpremultiplied", alpha.getAttribute("value"));
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("TransparentIndex", pixelAspectRatio.getNodeName());
assertEquals("1", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode transparentIndex = (IIOMetadataNode) alpha.getNextSibling();
assertEquals("TransparentIndex", transparentIndex.getNodeName());
assertEquals("1", transparentIndex.getAttribute("value"));
assertNull(transparentIndex.getNextSibling()); // No more children
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
@Test
public void testStandardRGB8() throws IIOException {
Form header = Form.ofType(IFF.TYPE_RGB8)
.with(new BMHDChunk(300, 200, 25, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0));
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_32_BIT_ARGB, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
// Chroma
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma");
IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength());
@@ -639,7 +581,7 @@ public class IFFImageMetadataTest {
assertNull(blackIsZero.getNextSibling()); // No more children
// Data
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -659,7 +601,7 @@ public class IFFImageMetadataTest {
assertNull(bitsPerSample.getNextSibling()); // No more children
// Transparency
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency");
IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength());
@@ -682,10 +624,10 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_DEEP)
.with(new DGBLChunk(8))
.with(dpel);
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_32_BIT_ARGB_DEEP, header, header.colorMap());
IFFImageMetadata metadata = new IFFImageMetadata(header, null);
// Chroma
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma");
IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength());
@@ -707,7 +649,7 @@ public class IFFImageMetadataTest {
assertNull(blackIsZero.getNextSibling()); // No more children
// Data
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -727,7 +669,7 @@ public class IFFImageMetadataTest {
assertNull(bitsPerSample.getNextSibling()); // No more children
// Transparency
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency");
IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength());
@@ -738,13 +680,4 @@ public class IFFImageMetadataTest {
assertNull(alpha.getNextSibling()); // No more children
}
// TODO: Test RGB8 + ColorMap
private IIOMetadataNode getStandardNode(IIOMetadata metadata, String nodeName) {
IIOMetadataNode asTree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
NodeList nodes = asTree.getElementsByTagName(nodeName);
return nodes.getLength() > 0 ? (IIOMetadataNode) nodes.item(0) : null;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-jpeg-jai-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: JPEG/JAI TIFF Interop</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-jpeg-jep262-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: JPEG/JEP-262 Interop</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-jpeg</artifactId>
<name>TwelveMonkeys :: ImageIO :: JPEG plugin</name>
+1 -1
View File
@@ -3,7 +3,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>imageio-metadata</artifactId>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-pcx</artifactId>
<name>TwelveMonkeys :: ImageIO :: PCX plugin</name>
@@ -45,7 +45,7 @@ import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataInput;
import java.io.DataInputStream;
@@ -377,12 +377,10 @@ public final class PCXImageReader extends ImageReaderBase {
@Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
// checkBounds(imageIndex);
// readHeader();
//
// return new PCXMetadata(header, getVGAPalette());
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
return new PCXMetadata(rawType, header);
checkBounds(imageIndex);
readHeader();
return new PCXMetadata(header, getVGAPalette());
}
private IndexColorModel getVGAPalette() throws IOException {
@@ -1,29 +1,236 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.pcx;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.IndexColorModel;
final class PCXMetadata extends StandardImageMetadataSupport {
public PCXMetadata(ImageTypeSpecifier type, PCXHeader header) {
super(builder(type)
.withPlanarConfiguration(planarConfiguration(header))
.withCompressionTypeName(compressionName(header))
.withFormatVersion(String.valueOf(header.getVersion())));
final class PCXMetadata extends AbstractMetadata {
private final PCXHeader header;
private final IndexColorModel vgaPalette;
PCXMetadata(final PCXHeader header, final IndexColorModel vgaPalette) {
this.header = header;
this.vgaPalette = vgaPalette;
}
private static PlanarConfiguration planarConfiguration(PCXHeader header) {
return header.getChannels() > 1 ? PlanarConfiguration.LineInterleaved : null;
}
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
private static String compressionName(PCXHeader header) {
switch (header.getCompression()) {
case PCX.COMPRESSION_NONE:
return "None";
case PCX.COMPRESSION_RLE:
return "RLE";
IndexColorModel palette = null;
boolean gray = false;
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
switch (header.getBitsPerPixel()) {
case 1:
case 2:
case 4:
palette = header.getEGAPalette();
csType.setAttribute("name", "RGB");
break;
case 8:
// We may have IndexColorModel here for 1 channel images
if (header.getChannels() == 1 && vgaPalette != null) {
palette = vgaPalette;
csType.setAttribute("name", "RGB");
break;
}
if (header.getChannels() == 1) {
csType.setAttribute("name", "GRAY");
gray = true;
break;
}
csType.setAttribute("name", "RGB");
break;
case 24:
// Some sources says this is possible... Untested.
csType.setAttribute("name", "RGB");
break;
default:
csType.setAttribute("name", "Unknown");
}
return "Unknown";
chroma.appendChild(csType);
// NOTE: Channels in chroma node reflects channels in color model, not data! (see data node)
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
numChannels.setAttribute("value", gray ? "1" : "3");
chroma.appendChild(numChannels);
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
blackIsZero.setAttribute("value", "TRUE");
chroma.appendChild(blackIsZero);
if (palette != null) {
IIOMetadataNode paletteNode = new IIOMetadataNode("Palette");
chroma.appendChild(paletteNode);
for (int i = 0; i < palette.getMapSize(); i++) {
IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry");
paletteEntry.setAttribute("index", Integer.toString(i));
paletteEntry.setAttribute("red", Integer.toString(palette.getRed(i)));
paletteEntry.setAttribute("green", Integer.toString(palette.getGreen(i)));
paletteEntry.setAttribute("blue", Integer.toString(palette.getBlue(i)));
paletteNode.appendChild(paletteEntry);
}
}
return chroma;
}
// No compression
@Override
protected IIOMetadataNode getStandardCompressionNode() {
if (header.getCompression() != PCX.COMPRESSION_NONE) {
IIOMetadataNode node = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
compressionTypeName.setAttribute("value", header.getCompression() == PCX.COMPRESSION_RLE ? "RLE" : "Uknown");
node.appendChild(compressionTypeName);
IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
lossless.setAttribute("value", "TRUE");
node.appendChild(lossless);
return node;
}
return null;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode node = new IIOMetadataNode("Data");
// Planar configuration only makes sense for multi-channel images
if (header.getChannels() > 1) {
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
planarConfiguration.setAttribute("value", "LineInterleaved");
node.appendChild(planarConfiguration);
}
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
switch (header.getBitsPerPixel()) {
case 1:
case 2:
case 4:
sampleFormat.setAttribute("value", "Index");
break;
case 8:
if (header.getChannels() == 1 && vgaPalette != null) {
sampleFormat.setAttribute("value", "Index");
break;
}
// Else fall through for GRAY
default:
sampleFormat.setAttribute("value", "UnsignedIntegral");
break;
}
node.appendChild(sampleFormat);
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
bitsPerSample.setAttribute("value", createListValue(header.getChannels(), Integer.toString(header.getBitsPerPixel())));
node.appendChild(bitsPerSample);
IIOMetadataNode significantBitsPerSample = new IIOMetadataNode("SignificantBitsPerSample");
significantBitsPerSample.setAttribute("value", createListValue(header.getChannels(), Integer.toString(header.getBitsPerPixel())));
node.appendChild(significantBitsPerSample);
IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB");
sampleMSB.setAttribute("value", createListValue(header.getChannels(), "0"));
return node;
}
private String createListValue(final int itemCount, final String... values) {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < itemCount; i++) {
if (buffer.length() > 0) {
buffer.append(' ');
}
buffer.append(values[i % values.length]);
}
return buffer.toString();
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
imageOrientation.setAttribute("value", "Normal");
dimension.appendChild(imageOrientation);
return dimension;
}
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode dimension = new IIOMetadataNode("Document");
IIOMetadataNode imageOrientation = new IIOMetadataNode("FormatVersion");
imageOrientation.setAttribute("value", String.valueOf(header.getVersion()));
dimension.appendChild(imageOrientation);
return dimension;
}
// No text node
// No tiling
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
// NOTE: There doesn't seem to be any god way to determine transparency, other than by convention
// 1 channel: Gray, 2 channel: Gray + Alpha, 3 channel: RGB, 4 channel: RGBA (hopefully never CMYK...)
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
alpha.setAttribute("value", header.getChannels() == 1 || header.getChannels() == 3 ? "none" : "nonpremultiplied");
transparency.appendChild(alpha);
return transparency;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-pdf</artifactId>
<name>TwelveMonkeys :: ImageIO :: PDF plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-pict</artifactId>
<name>TwelveMonkeys :: ImageIO :: PICT plugin</name>
@@ -67,28 +67,18 @@ import com.twelvemonkeys.io.enc.Decoder;
import com.twelvemonkeys.io.enc.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.geom.*;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.image.*;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.io.*;
import java.util.List;
import java.util.*;
/**
* Reader for Apple Mac Paint Picture (PICT) format.
@@ -2621,9 +2611,7 @@ public final class PICTImageReader extends ImageReaderBase {
return getYPtCoord(getPICTFrame().height);
}
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
checkBounds(imageIndex);
public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) {
// TODO: The images look slightly different in Preview.. Could indicate the color space is wrong...
return Collections.singletonList(
ImageTypeSpecifiers.createPacked(
@@ -2635,10 +2623,10 @@ public final class PICTImageReader extends ImageReaderBase {
@Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
checkBounds(imageIndex);
getPICTFrame(); // TODO: Would probably be better to use readPictHeader here, but it isn't cached
return new PICTMetadata(rawType, version, screenImageXRatio, screenImageYRatio);
return new PICTMetadata(version, screenImageXRatio, screenImageYRatio);
}
protected static void showIt(final BufferedImage pImage, final String pTitle) {
@@ -1,38 +1,8 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.pict;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataNode;
/**
* PICTMetadata.
@@ -41,13 +11,82 @@ import javax.imageio.ImageTypeSpecifier;
* @author last modified by $Author: haraldk$
* @version $Id: PICTMetadata.java,v 1.0 23/03/2021 haraldk Exp$
*/
final class PICTMetadata extends StandardImageMetadataSupport {
PICTMetadata(final ImageTypeSpecifier type, final int version, final double screenImageXRatio, final double screenImageYRatio) {
super(builder(type)
.withPixelAspectRatio(screenImageXRatio > 0.0d && screenImageYRatio > 0.0d ? screenImageXRatio / screenImageYRatio : 1)
.withFormatVersion(Integer.toString(version))
);
// As this is a vector-ish format, some of the data makes no sense... :-P
// It is, however, consistent with the getRawImageTyp/getImageTypes
public class PICTMetadata extends AbstractMetadata {
private final int version;
private final double screenImageXRatio;
private final double screenImageYRatio;
PICTMetadata(final int version, final double screenImageXRatio, final double screenImageYRatio) {
this.version = version;
this.screenImageXRatio = screenImageXRatio;
this.screenImageYRatio = screenImageYRatio;
}
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType);
csType.setAttribute("name", "RGB");
// NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data)
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
chroma.appendChild(numChannels);
numChannels.setAttribute("value", "3");
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
chroma.appendChild(blackIsZero);
blackIsZero.setAttribute("value", "TRUE");
return chroma;
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
if (screenImageXRatio > 0.0d && screenImageYRatio > 0.0d) {
IIOMetadataNode node = new IIOMetadataNode("Dimension");
double ratio = screenImageXRatio / screenImageYRatio;
IIOMetadataNode subNode = new IIOMetadataNode("PixelAspectRatio");
subNode.setAttribute("value", "" + ratio);
node.appendChild(subNode);
return node;
}
return null;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode data = new IIOMetadataNode("Data");
// As this is a vector-ish format, with possibly multiple regions of pixel data, this makes no sense... :-P
// This is, however, consistent with the getRawImageTyp/getImageTypes
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
planarConfiguration.setAttribute("value", "PixelInterleaved");
data.appendChild(planarConfiguration);
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
sampleFormat.setAttribute("value", "UnsignedIntegral");
data.appendChild(sampleFormat);
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
bitsPerSample.setAttribute("value", "32");
data.appendChild(bitsPerSample);
return data;
}
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode document = new IIOMetadataNode("Document");
IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion");
document.appendChild(formatVersion);
formatVersion.setAttribute("value", Integer.toString(version));
return document;
}
}
@@ -1,33 +1,3 @@
/*
* Copyright (c) 2021, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.ImageReaderBase;
@@ -64,7 +34,7 @@ public final class PNTGImageReader extends ImageReaderBase {
private static final Set<ImageTypeSpecifier> IMAGE_TYPES =
Collections.singleton(ImageTypeSpecifiers.createIndexed(new int[] {-1, 0}, false, -1, 1, DataBuffer.TYPE_BYTE));
PNTGImageReader(final ImageReaderSpi provider) {
protected PNTGImageReader(final ImageReaderSpi provider) {
super(provider);
}
@@ -153,7 +123,9 @@ public final class PNTGImageReader extends ImageReaderBase {
@Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
return new PNTGMetadata(getRawImageType(imageIndex));
checkBounds(imageIndex);
return new PNTGMetadata();
}
private void readHeader() throws IOException {
@@ -1,33 +1,3 @@
/*
* Copyright (c) 2021, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase;
@@ -1,38 +1,8 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataNode;
/**
* PNTGMetadata.
@@ -41,11 +11,76 @@ import javax.imageio.ImageTypeSpecifier;
* @author last modified by $Author: haraldk$
* @version $Id: PNTGMetadata.java,v 1.0 23/03/2021 haraldk Exp$
*/
final class PNTGMetadata extends StandardImageMetadataSupport {
public PNTGMetadata(ImageTypeSpecifier type) {
super(builder(type)
.withBlackIsZero(false)
.withCompressionTypeName("PackBits")
.withFormatVersion("1.0"));
public class PNTGMetadata extends AbstractMetadata {
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType);
csType.setAttribute("name", "GRAY");
// NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data)
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
chroma.appendChild(numChannels);
numChannels.setAttribute("value", "1");
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
chroma.appendChild(blackIsZero);
blackIsZero.setAttribute("value", "FALSE");
return chroma;
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
IIOMetadataNode compressionNode = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
compressionTypeName.setAttribute("value", "PackBits"); // RLE?
compressionNode.appendChild(compressionTypeName);
compressionNode.appendChild(new IIOMetadataNode("Lossless"));
// "value" defaults to TRUE
return compressionNode;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode data = new IIOMetadataNode("Data");
// PlanarConfiguration
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
planarConfiguration.setAttribute("value", "PixelInterleaved");
data.appendChild(planarConfiguration);
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
sampleFormat.setAttribute("value", "UnsignedIntegral");
data.appendChild(sampleFormat);
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
bitsPerSample.setAttribute("value", "1");
data.appendChild(bitsPerSample);
return data;
}
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode document = new IIOMetadataNode("Document");
IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion");
document.appendChild(formatVersion);
formatVersion.setAttribute("value", "1.0");
// TODO: We could get the file creation time from MacBinary header here...
return document;
}
@Override
protected IIOMetadataNode getStandardTextNode() {
// TODO: We could get the file name from MacBinary header here...
return super.getStandardTextNode();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Harald Kuhr
* Copyright (c) 2015, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -1,11 +1,7 @@
package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import org.junit.Test;
import java.awt.image.*;
/**
* PNTGMetadataTest.
*
@@ -16,6 +12,6 @@ import java.awt.image.*;
public class PNTGMetadataTest {
@Test
public void testCreate() {
new PNTGMetadata(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_BINARY));
new PNTGMetadata();
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-pnm</artifactId>
<name>TwelveMonkeys :: ImageIO :: PNM plugin</name>
@@ -43,7 +43,7 @@ import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataInput;
import java.io.DataInputStream;
@@ -468,7 +468,10 @@ public final class PNMImageReader extends ImageReaderBase {
@Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
return new PNMMetadata(getRawImageType(imageIndex), header);
checkBounds(imageIndex);
readHeader();
return new PNMMetadata(header);
}
public static void main(String[] args) throws IOException {
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Harald Kuhr
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -30,35 +30,92 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.*;
import java.awt.*;
import java.awt.image.DataBuffer;
import java.nio.ByteOrder;
/**
* PNMMetadata.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
final class PNMMetadata extends StandardImageMetadataSupport {
final class PNMMetadata extends AbstractMetadata {
private final PNMHeader header;
PNMMetadata(ImageTypeSpecifier type, PNMHeader header) {
super(builder(type)
.withColorSpaceType(colorSpace(header))
// TODO: Might make sense to set gamma?
.withBlackIsZero(header.getTupleType() != TupleType.BLACKANDWHITE_WHITE_IS_ZERO)
.withSignificantBitsPerSample(significantBits(header))
.withSampleMSB(header.getByteOrder() == ByteOrder.BIG_ENDIAN ? 0 : header.getBitsPerSample() - 1)
.withOrientation(orientation(header))
);
PNMMetadata(final PNMHeader header) {
this.header = header;
}
private static int significantBits(PNMHeader header) {
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
switch (header.getTupleType()) {
case BLACKANDWHITE:
case BLACKANDWHITE_ALPHA:
case BLACKANDWHITE_WHITE_IS_ZERO:
case GRAYSCALE:
case GRAYSCALE_ALPHA:
csType.setAttribute("name", "GRAY");
break;
case RGB:
case RGB_ALPHA:
csType.setAttribute("name", "RGB");
break;
case CMYK:
case CMYK_ALPHA:
csType.setAttribute("name", "CMYK");
break;
}
if (csType.getAttribute("name") != null) {
chroma.appendChild(csType);
}
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
numChannels.setAttribute("value", Integer.toString(header.getSamplesPerPixel()));
chroma.appendChild(numChannels);
// TODO: Might make sense to set gamma?
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
blackIsZero.setAttribute("value", header.getTupleType() == TupleType.BLACKANDWHITE_WHITE_IS_ZERO
? "FALSE"
: "TRUE");
chroma.appendChild(blackIsZero);
return chroma;
}
// No compression
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode node = new IIOMetadataNode("Data");
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
sampleFormat.setAttribute("value", header.getTransferType() == DataBuffer.TYPE_FLOAT
? "Real"
: "UnsignedIntegral");
node.appendChild(sampleFormat);
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
bitsPerSample.setAttribute("value", createListValue(header.getSamplesPerPixel(), Integer.toString(header.getBitsPerSample())));
node.appendChild(bitsPerSample);
IIOMetadataNode significantBitsPerSample = new IIOMetadataNode("SignificantBitsPerSample");
significantBitsPerSample.setAttribute("value", createListValue(header.getSamplesPerPixel(), Integer.toString(computeSignificantBits())));
node.appendChild(significantBitsPerSample);
String msb = header.getByteOrder() == ByteOrder.BIG_ENDIAN
? "0"
: Integer.toString(header.getBitsPerSample() - 1);
IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB");
sampleMSB.setAttribute("value", createListValue(header.getSamplesPerPixel(), msb));
return node;
}
private int computeSignificantBits() {
if (header.getTransferType() == DataBuffer.TYPE_FLOAT) {
return header.getBitsPerSample();
}
@@ -75,30 +132,38 @@ final class PNMMetadata extends StandardImageMetadataSupport {
return significantBits;
}
private static ColorSpaceType colorSpace(PNMHeader header) {
switch (header.getTupleType()) {
case BLACKANDWHITE:
case BLACKANDWHITE_ALPHA:
case BLACKANDWHITE_WHITE_IS_ZERO:
case GRAYSCALE:
case GRAYSCALE_ALPHA:
return ColorSpaceType.GRAY;
default:
return null; // Fall back to color model's type
}
}
private String createListValue(final int itemCount, final String... values) {
StringBuilder buffer = new StringBuilder();
private static ImageOrientation orientation(PNMHeader header) {
// For some reason, the float values are stored bottom-up
return header.getFileType() == PNM.PFM_GRAY || header.getFileType() == PNM.PFM_RGB
? ImageOrientation.FlipH
: ImageOrientation.Normal;
for (int i = 0; i < itemCount; i++) {
if (buffer.length() > 0) {
buffer.append(' ');
}
buffer.append(values[i % values.length]);
}
return buffer.toString();
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
imageOrientation.setAttribute("value",
header.getFileType() == PNM.PFM_GRAY || header.getFileType() == PNM.PFM_RGB
? "FlipH"
: "Normal");
dimension.appendChild(imageOrientation);
return dimension;
}
// No document node
@Override
protected IIOMetadataNode getStandardTextNode() {
// TODO: Could avoid this override, by changing the StandardImageMetadataSupport to
// use List<Entry<String, String>> instead of Map<String, String> (we use duplicate "comment"s).
if (!header.getComments().isEmpty()) {
IIOMetadataNode text = new IIOMetadataNode("Text");
@@ -114,4 +179,17 @@ final class PNMMetadata extends StandardImageMetadataSupport {
return null;
}
// No tiling
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
alpha.setAttribute("value", header.getTransparency() == Transparency.OPAQUE ? "none" : "nonpremultiplied");
transparency.appendChild(alpha);
return transparency;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-psd</artifactId>
<name>TwelveMonkeys :: ImageIO :: PSD plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-reference</artifactId>
<name>TwelveMonkeys :: ImageIO :: JDK Reference Tests</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-sgi</artifactId>
<name>TwelveMonkeys :: ImageIO :: SGI plugin</name>
@@ -33,7 +33,7 @@ package com.twelvemonkeys.imageio.plugins.sgi;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.Charset;
final class SGIHeader {
private int compression;
@@ -157,6 +157,6 @@ final class SGIHeader {
}
}
return new String(bytes, 0, len, StandardCharsets.US_ASCII);
return new String(bytes, 0, len, Charset.forName("ASCII"));
}
}
@@ -45,7 +45,7 @@ import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataInput;
import java.io.DataInputStream;
@@ -378,10 +378,11 @@ public final class SGIImageReader extends ImageReaderBase {
imageInput.seek(imageInput.getFlushedPosition());
}
@Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
return new SGIMetadata(rawType, header);
@Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();
return new SGIMetadata(header);
}
public static void main(String[] args) throws IOException {
@@ -1,39 +1,209 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.sgi;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataNode;
final class SGIMetadata extends StandardImageMetadataSupport {
public SGIMetadata(ImageTypeSpecifier type, SGIHeader header) {
super(builder(type)
.withSignificantBitsPerSample(computeSignificantBits(header))
.withCompressionTypeName(compressionName(header))
.withOrientation(ImageOrientation.FlipV)
.withTextEntry("DocumentName", header.getName())
);
final class SGIMetadata extends AbstractMetadata {
private final SGIHeader header;
SGIMetadata(final SGIHeader header) {
this.header = header;
}
private static int computeSignificantBits(SGIHeader header) {
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
// NOTE: There doesn't seem to be any god way to determine color space, other than by convention
// 1 channel: Gray, 2 channel: Gray + Alpha, 3 channel: RGB, 4 channel: RGBA (hopefully never CMYK...)
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
switch (header.getColorMode()) {
case SGI.COLORMODE_NORMAL:
switch (header.getChannels()) {
case 1:
case 2:
csType.setAttribute("name", "GRAY");
break;
case 3:
case 4:
csType.setAttribute("name", "RGB");
break;
default:
csType.setAttribute("name", Integer.toHexString(header.getChannels()).toUpperCase() + "CLR");
break;
}
break;
// SGIIMAGE.TXT describes these as RGB
case SGI.COLORMODE_DITHERED:
case SGI.COLORMODE_SCREEN:
case SGI.COLORMODE_COLORMAP:
csType.setAttribute("name", "RGB");
break;
}
if (csType.getAttribute("name") != null) {
chroma.appendChild(csType);
}
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
numChannels.setAttribute("value", Integer.toString(header.getChannels()));
chroma.appendChild(numChannels);
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
blackIsZero.setAttribute("value", "TRUE");
chroma.appendChild(blackIsZero);
return chroma;
}
// No compression
@Override
protected IIOMetadataNode getStandardCompressionNode() {
if (header.getCompression() != SGI.COMPRESSION_NONE) {
IIOMetadataNode node = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
compressionTypeName.setAttribute("value", header.getCompression() == SGI.COMPRESSION_RLE
? "RLE"
: "Uknown");
node.appendChild(compressionTypeName);
IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
lossless.setAttribute("value", "TRUE");
node.appendChild(lossless);
return node;
}
return null;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode node = new IIOMetadataNode("Data");
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
sampleFormat.setAttribute("value", "UnsignedIntegral");
node.appendChild(sampleFormat);
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
bitsPerSample.setAttribute("value", createListValue(header.getChannels(), Integer.toString(header.getBytesPerPixel() * 8)));
node.appendChild(bitsPerSample);
IIOMetadataNode significantBitsPerSample = new IIOMetadataNode("SignificantBitsPerSample");
significantBitsPerSample.setAttribute("value", createListValue(header.getChannels(), Integer.toString(computeSignificantBits())));
node.appendChild(significantBitsPerSample);
IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB");
sampleMSB.setAttribute("value", createListValue(header.getChannels(), "0"));
return node;
}
private int computeSignificantBits() {
int significantBits = 0;
int maxSample = header.getMaxValue();
int significantBits = 1;
while ((maxSample >>>= 1) != 0) {
while (maxSample > 0) {
maxSample >>>= 1;
significantBits++;
}
return significantBits;
}
private static String compressionName(SGIHeader header) {
switch (header.getCompression()) {
case SGI.COMPRESSION_NONE:
return "None";
case SGI.COMPRESSION_RLE:
return "RLE";
private String createListValue(final int itemCount, final String... values) {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < itemCount; i++) {
if (buffer.length() > 0) {
buffer.append(' ');
}
buffer.append(values[i % values.length]);
}
return "Uknown";
return buffer.toString();
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
imageOrientation.setAttribute("value", "FlipV");
dimension.appendChild(imageOrientation);
return dimension;
}
// No document node
@Override
protected IIOMetadataNode getStandardTextNode() {
if (!header.getName().isEmpty()) {
IIOMetadataNode text = new IIOMetadataNode("Text");
IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
textEntry.setAttribute("keyword", "name");
textEntry.setAttribute("value", header.getName());
text.appendChild(textEntry);
return text;
}
return null;
}
// No tiling
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
// NOTE: There doesn't seem to be any god way to determine transparency, other than by convention
// 1 channel: Gray, 2 channel: Gray + Alpha, 3 channel: RGB, 4 channel: RGBA (hopefully never CMYK...)
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
alpha.setAttribute("value", header.getChannels() == 1 || header.getChannels() == 3
? "none"
: "nonpremultiplied");
transparency.appendChild(alpha);
return transparency;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-tga</artifactId>
<name>TwelveMonkeys :: ImageIO :: TGA plugin</name>
@@ -31,16 +31,18 @@
package com.twelvemonkeys.imageio.plugins.tga;
import javax.imageio.IIOException;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.*;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static com.twelvemonkeys.lang.Validate.notNull;
import static java.awt.color.ColorSpace.*;
import static java.awt.color.ColorSpace.TYPE_GRAY;
import static java.awt.color.ColorSpace.TYPE_RGB;
final class TGAHeader {
@@ -116,14 +118,10 @@ final class TGAHeader {
'}';
}
static TGAHeader from(final ImageTypeSpecifier type, final boolean compressed) {
return from(type, 0, 0, compressed);
}
static TGAHeader from(final RenderedImage image, final boolean compressed) {
notNull(image, "image");
static TGAHeader from(final ImageTypeSpecifier type, int width, int height, final boolean compressed) {
notNull(type, "type");
ColorModel colorModel = type.getColorModel();
ColorModel colorModel = image.getColorModel();
IndexColorModel colorMap = colorModel instanceof IndexColorModel ? (IndexColorModel) colorModel : null;
TGAHeader header = new TGAHeader();
@@ -137,8 +135,8 @@ final class TGAHeader {
header.x = 0;
header.y = 0;
header.width = width;
header.height = height;
header.width = image.getWidth(); // TODO: Param source region/subsampling might affect this
header.height = image.getHeight(); // // TODO: Param source region/subsampling might affect this
header.pixelDepth = colorModel.getPixelSize() == 15 ? 16 : colorModel.getPixelSize();
header.origin = TGA.ORIGIN_UPPER_LEFT; // TODO: Allow parameter to control this?
@@ -47,7 +47,7 @@ import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataInput;
import java.io.File;
@@ -538,8 +538,10 @@ final class TGAImageReader extends ImageReaderBase {
@Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
return new TGAMetadata(rawType, header, extensions);
checkBounds(imageIndex);
readHeader();
return new TGAMetadata(header, extensions);
}
public static void main(String[] args) throws IOException {
@@ -37,16 +37,12 @@ import com.twelvemonkeys.io.LittleEndianDataOutputStream;
import com.twelvemonkeys.io.enc.EncoderStream;
import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.color.*;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.DataOutput;
import java.io.File;
@@ -69,7 +65,8 @@ final class TGAImageWriter extends ImageWriterBase {
public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) {
Validate.notNull(imageType, "imageType");
return new TGAMetadata(imageType, TGAHeader.from(imageType, isRLE(param, null)), null);
TGAHeader header = TGAHeader.from(imageType.createBufferedImage(1, 1), isRLE(param, null));
return new TGAMetadata(header, null);
}
@Override
@@ -110,8 +107,7 @@ final class TGAImageWriter extends ImageWriterBase {
final boolean compressed = isRLE(param, image.getMetadata());
RenderedImage renderedImage = image.getRenderedImage();
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromRenderedImage(renderedImage);
TGAHeader header = TGAHeader.from(type, renderedImage.getWidth(), renderedImage.getHeight(), compressed);
TGAHeader header = TGAHeader.from(renderedImage, compressed);
header.write(imageOutput);
@@ -121,7 +117,7 @@ final class TGAImageWriter extends ImageWriterBase {
? ImageTypeSpecifiers.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {2, 1, 0, 3}, DataBuffer.TYPE_BYTE, true, false).createBufferedImage(renderedImage.getWidth(), 1).getRaster()
: renderedImage.getSampleModel().getTransferType() == DataBuffer.TYPE_INT
? ImageTypeSpecifiers.createInterleaved(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {2, 1, 0}, DataBuffer.TYPE_BYTE, false, false).createBufferedImage(renderedImage.getWidth(), 1).getRaster()
: type.createBufferedImage(renderedImage.getWidth(), 1).getRaster();
: ImageTypeSpecifier.createFromRenderedImage(renderedImage).createBufferedImage(renderedImage.getWidth(), 1).getRaster();
final DataBuffer buffer = rowRaster.getDataBuffer();
@@ -139,7 +135,7 @@ final class TGAImageWriter extends ImageWriterBase {
break;
}
DataOutput imageOutput = compressed ? createRLEStream(this.imageOutput, header.getPixelDepth()) : this.imageOutput;
DataOutput imageOutput = compressed ? createRLEStream(header, this.imageOutput) : this.imageOutput;
switch (buffer.getDataType()) {
case DataBuffer.TYPE_BYTE:
@@ -178,8 +174,8 @@ final class TGAImageWriter extends ImageWriterBase {
processImageComplete();
}
private static LittleEndianDataOutputStream createRLEStream(final ImageOutputStream stream, int pixelDepth) {
return new LittleEndianDataOutputStream(new EncoderStream(IIOUtil.createStreamAdapter(stream), new RLEEncoder(pixelDepth)));
private static LittleEndianDataOutputStream createRLEStream(final TGAHeader header, final ImageOutputStream stream) {
return new LittleEndianDataOutputStream(new EncoderStream(IIOUtil.createStreamAdapter(stream), new RLEEncoder(header.getPixelDepth())));
}
// TODO: Refactor to common util
@@ -1,83 +1,340 @@
/*
* Copyright (c) 2014, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.tga;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.*;
import java.awt.image.IndexColorModel;
import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.Map;
final class TGAMetadata extends StandardImageMetadataSupport {
TGAMetadata(ImageTypeSpecifier type, TGAHeader header, TGAExtensions extensions) {
super(builder(type)
.withCompressionTypeName(compressionName(header))
.withPixelAspectRatio(pixelAspectRatio(extensions))
.withOrientation(orientation(header))
.withFormatVersion(extensions == null ? "1.0" : "2.0")
.withDocumentCreationTime(documentCreationTime(extensions))
.withTextEntries(textEntries(header, extensions))
);
import static com.twelvemonkeys.lang.Validate.notNull;
final class TGAMetadata extends AbstractMetadata {
private final TGAHeader header;
private final TGAExtensions extensions;
TGAMetadata(final TGAHeader header, final TGAExtensions extensions) {
this.header = notNull(header, "header");
this.extensions = extensions;
}
private static String compressionName(TGAHeader header) {
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType);
switch (header.getImageType()) {
case TGA.IMAGETYPE_NONE:
case TGA.IMAGETYPE_COLORMAPPED:
case TGA.IMAGETYPE_TRUECOLOR:
case TGA.IMAGETYPE_MONOCHROME:
return "None";
case TGA.IMAGETYPE_MONOCHROME_RLE:
csType.setAttribute("name", "GRAY");
break;
case TGA.IMAGETYPE_TRUECOLOR:
case TGA.IMAGETYPE_TRUECOLOR_RLE:
case TGA.IMAGETYPE_COLORMAPPED:
case TGA.IMAGETYPE_COLORMAPPED_RLE:
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN:
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE:
csType.setAttribute("name", "RGB");
break;
default:
csType.setAttribute("name", "Unknown");
}
// NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data)
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
chroma.appendChild(numChannels);
switch (header.getPixelDepth()) {
case 8:
if (header.getImageType() == TGA.IMAGETYPE_MONOCHROME || header.getImageType() == TGA.IMAGETYPE_MONOCHROME_RLE) {
numChannels.setAttribute("value", Integer.toString(1));
}
else {
numChannels.setAttribute("value", Integer.toString(3));
}
break;
case 16:
if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) {
numChannels.setAttribute("value", Integer.toString(4));
}
else {
numChannels.setAttribute("value", Integer.toString(3));
}
break;
case 24:
numChannels.setAttribute("value", Integer.toString(3));
break;
case 32:
numChannels.setAttribute("value", Integer.toString(4));
break;
}
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
chroma.appendChild(blackIsZero);
blackIsZero.setAttribute("value", "TRUE");
// NOTE: TGA files may contain a color map, even if true color...
// Not sure if this is a good idea to expose to the meta data,
// as it might be unexpected... Then again...
IndexColorModel colorMap = header.getColorMap();
if (colorMap != null) {
IIOMetadataNode palette = new IIOMetadataNode("Palette");
chroma.appendChild(palette);
for (int i = 0; i < colorMap.getMapSize(); i++) {
IIOMetadataNode paletteEntry = new IIOMetadataNode("PaletteEntry");
palette.appendChild(paletteEntry);
paletteEntry.setAttribute("index", Integer.toString(i));
paletteEntry.setAttribute("red", Integer.toString(colorMap.getRed(i)));
paletteEntry.setAttribute("green", Integer.toString(colorMap.getGreen(i)));
paletteEntry.setAttribute("blue", Integer.toString(colorMap.getBlue(i)));
}
}
if (extensions != null && extensions.getBackgroundColor() != 0) {
Color background = new Color(extensions.getBackgroundColor(), true);
IIOMetadataNode backgroundColor = new IIOMetadataNode("BackgroundColor");
chroma.appendChild(backgroundColor);
backgroundColor.setAttribute("red", Integer.toString(background.getRed()));
backgroundColor.setAttribute("green", Integer.toString(background.getGreen()));
backgroundColor.setAttribute("blue", Integer.toString(background.getBlue()));
}
return chroma;
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
switch (header.getImageType()) {
case TGA.IMAGETYPE_COLORMAPPED_RLE:
case TGA.IMAGETYPE_TRUECOLOR_RLE:
case TGA.IMAGETYPE_MONOCHROME_RLE:
return "RLE";
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN:
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE:
IIOMetadataNode node = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
node.appendChild(compressionTypeName);
String value = header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN || header.getImageType() == TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE
? "Unknown" : "RLE";
compressionTypeName.setAttribute("value", value);
IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
node.appendChild(lossless);
lossless.setAttribute("value", "TRUE");
return node;
default:
return "Unknown";
// No compression
return null;
}
}
private static double pixelAspectRatio(TGAExtensions extensions) {
return extensions != null ? extensions.getPixelAspectRatio() : 1f;
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode node = new IIOMetadataNode("Data");
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
node.appendChild(planarConfiguration);
planarConfiguration.setAttribute("value", "PixelInterleaved");
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
node.appendChild(sampleFormat);
switch (header.getImageType()) {
case TGA.IMAGETYPE_COLORMAPPED:
case TGA.IMAGETYPE_COLORMAPPED_RLE:
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN:
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE:
sampleFormat.setAttribute("value", "Index");
break;
default:
sampleFormat.setAttribute("value", "UnsignedIntegral");
break;
}
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
node.appendChild(bitsPerSample);
switch (header.getPixelDepth()) {
case 8:
bitsPerSample.setAttribute("value", createListValue(1, "8"));
break;
case 16:
if (header.getImageType() == TGA.IMAGETYPE_MONOCHROME || header.getImageType() == TGA.IMAGETYPE_MONOCHROME_RLE) {
bitsPerSample.setAttribute("value", "16");
}
else if (header.getAttributeBits() > 0 && extensions != null && extensions.hasAlpha()) {
bitsPerSample.setAttribute("value", "5, 5, 5, 1");
}
else {
bitsPerSample.setAttribute("value", createListValue(3, "5"));
}
break;
case 24:
bitsPerSample.setAttribute("value", createListValue(3, "8"));
break;
case 32:
bitsPerSample.setAttribute("value", createListValue(4, "8"));
break;
}
return node;
}
private static ImageOrientation orientation(TGAHeader header) {
switch (header.origin) {
private String createListValue(final int itemCount, final String... values) {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < itemCount; i++) {
if (buffer.length() > 0) {
buffer.append(' ');
}
buffer.append(values[i % values.length]);
}
return buffer.toString();
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
dimension.appendChild(imageOrientation);
switch (header.getOrigin()) {
case TGA.ORIGIN_LOWER_LEFT:
return ImageOrientation.FlipH;
imageOrientation.setAttribute("value", "FlipH");
break;
case TGA.ORIGIN_LOWER_RIGHT:
return ImageOrientation.Rotate180;
imageOrientation.setAttribute("value", "Rotate180");
break;
case TGA.ORIGIN_UPPER_LEFT:
return ImageOrientation.Normal;
imageOrientation.setAttribute("value", "Normal");
break;
case TGA.ORIGIN_UPPER_RIGHT:
return ImageOrientation.FlipV;
default:
throw new IllegalArgumentException("Unknown orientation: " + header.origin);
imageOrientation.setAttribute("value", "FlipV");
break;
}
IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio");
dimension.appendChild(pixelAspectRatio);
pixelAspectRatio.setAttribute("value", extensions != null ? String.valueOf(extensions.getPixelAspectRatio()) : "1.0");
return dimension;
}
private static Calendar documentCreationTime(TGAExtensions extensions) {
return extensions != null ? extensions.creationDate : null;
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode document = new IIOMetadataNode("Document");
IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion");
document.appendChild(formatVersion);
formatVersion.setAttribute("value", extensions == null ? "1.0" : "2.0");
// ImageCreationTime from extensions date
if (extensions != null && extensions.getCreationDate() != null) {
IIOMetadataNode imageCreationTime = new IIOMetadataNode("ImageCreationTime");
document.appendChild(imageCreationTime);
Calendar date = extensions.getCreationDate();
imageCreationTime.setAttribute("year", String.valueOf(date.get(Calendar.YEAR)));
imageCreationTime.setAttribute("month", String.valueOf(date.get(Calendar.MONTH) + 1));
imageCreationTime.setAttribute("day", String.valueOf(date.get(Calendar.DAY_OF_MONTH)));
imageCreationTime.setAttribute("hour", String.valueOf(date.get(Calendar.HOUR_OF_DAY)));
imageCreationTime.setAttribute("minute", String.valueOf(date.get(Calendar.MINUTE)));
imageCreationTime.setAttribute("second", String.valueOf(date.get(Calendar.SECOND)));
}
return document;
}
private static Map<String, String> textEntries(TGAHeader header, TGAExtensions extensions) {
LinkedHashMap<String, String> textEntries = new LinkedHashMap<>();
@Override
protected IIOMetadataNode getStandardTextNode() {
IIOMetadataNode text = new IIOMetadataNode("Text");
// NOTE: Keywords follow TIFF standard naming
putIfValue(textEntries, "DocumentName", header.getIdentification());
// NOTE: Names corresponds to equivalent fields in TIFF
appendTextEntry(text, "DocumentName", header.getIdentification());
if (extensions != null) {
putIfValue(textEntries, "Software", extensions.getSoftwareVersion() == null ? extensions.getSoftware() : (extensions.getSoftware() + " " + extensions.getSoftwareVersion()));
putIfValue(textEntries, "Artist", extensions.getAuthorName());
putIfValue(textEntries, "UserComment", extensions.getAuthorComments());
appendTextEntry(text, "Software", extensions.getSoftwareVersion() == null ? extensions.getSoftware() : (extensions.getSoftware() + " " + extensions.getSoftwareVersion()));
appendTextEntry(text, "Artist", extensions.getAuthorName());
appendTextEntry(text, "UserComment", extensions.getAuthorComments());
}
return textEntries;
return text.hasChildNodes() ? text : null;
}
private static void putIfValue(final Map<String, String> textEntries, final String keyword, final String value) {
private void appendTextEntry(final IIOMetadataNode parent, final String keyword, final String value) {
if (value != null && !value.isEmpty()) {
textEntries.put(keyword, value);
IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
parent.appendChild(textEntry);
textEntry.setAttribute("keyword", keyword);
textEntry.setAttribute("value", value);
}
}
// No tiling
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
transparency.appendChild(alpha);
if (extensions != null) {
if (extensions.hasAlpha()) {
alpha.setAttribute("value", extensions.isAlphaPremultiplied() ? "premultiplied" : "nonpremultiplied");
}
else {
alpha.setAttribute("value", "none");
}
}
else if (header.getAttributeBits() == 8) {
alpha.setAttribute("value", "nonpremultiplied");
}
else {
alpha.setAttribute("value", "none");
}
return transparency;
}
}
@@ -30,17 +30,12 @@
package com.twelvemonkeys.imageio.plugins.tga;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import org.junit.Test;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import java.awt.image.*;
import java.awt.image.BufferedImage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeFalse;
/**
@@ -51,9 +46,6 @@ import static org.junit.Assume.assumeFalse;
* @version $Id: TGAImageWriteParamTest.java,v 1.0 08/04/2021 haraldk Exp$
*/
public class TGAImageWriteParamTest {
private static final ImageTypeSpecifier TYPE_3BYTE_BGR = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
@Test
public void testDefaultCopyFromMetadata() {
TGAImageWriteParam param = new TGAImageWriteParam();
@@ -115,8 +107,8 @@ public class TGAImageWriteParamTest {
ImageWriteParam param = new ImageWriteParam(null);
assumeFalse(param.canWriteCompressed());
assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TYPE_3BYTE_BGR, TGAHeader.from(TYPE_3BYTE_BGR, false), null)));
assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TYPE_3BYTE_BGR, TGAHeader.from(TYPE_3BYTE_BGR, true), null)));
assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false), null)));
assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null)));
}
@Test
@@ -124,7 +116,7 @@ public class TGAImageWriteParamTest {
ImageWriteParam param = new TGAImageWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA);
assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TYPE_3BYTE_BGR, TGAHeader.from(TYPE_3BYTE_BGR, true), null)));
assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null)));
}
@Test
@@ -132,7 +124,7 @@ public class TGAImageWriteParamTest {
ImageWriteParam param = new TGAImageWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA);
assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TYPE_3BYTE_BGR, TGAHeader.from(TYPE_3BYTE_BGR, true), null)));
assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null)));
}
@Test
@@ -140,12 +132,12 @@ public class TGAImageWriteParamTest {
TGAImageWriteParam param = new TGAImageWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TYPE_3BYTE_BGR, TGAHeader.from(TYPE_3BYTE_BGR, false), null)));
assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TYPE_3BYTE_BGR, TGAHeader.from(TYPE_3BYTE_BGR, true), null)));
assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false), null)));
assertFalse(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null)));
param.setCompressionType("RLE");
assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TYPE_3BYTE_BGR, TGAHeader.from(TYPE_3BYTE_BGR, false), null)));
assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TYPE_3BYTE_BGR, TGAHeader.from(TYPE_3BYTE_BGR, true), null)));
assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false), null)));
assertTrue(TGAImageWriteParam.isRLE(param, new TGAMetadata(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true), null)));
}
}
@@ -30,18 +30,14 @@
package com.twelvemonkeys.imageio.plugins.tga;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.*;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.util.Calendar;
import static org.junit.Assert.*;
@@ -54,15 +50,10 @@ import static org.junit.Assert.*;
* @version $Id: TGAMetadataTest.java,v 1.0 08/04/2021 haraldk Exp$
*/
public class TGAMetadataTest {
private static final ImageTypeSpecifier TYPE_BYTE_GRAY = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_GRAY);
private static final ImageTypeSpecifier TYPE_3BYTE_BGR = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
@Test
public void testStandardFeatures() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, false);
final TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false);
final TGAMetadata metadata = new TGAMetadata(header, null);
// Standard metadata format
assertTrue(metadata.isStandardMetadataFormatSupported());
@@ -92,10 +83,10 @@ public class TGAMetadataTest {
@Test
public void testStandardChromaGray() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, false);
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), false);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma");
IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength());
@@ -117,10 +108,10 @@ public class TGAMetadataTest {
@Test
public void testStandardChromaRGB() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, false);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma");
IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength());
@@ -144,11 +135,10 @@ public class TGAMetadataTest {
public void testStandardChromaPalette() {
byte[] bw = {0, (byte) 0xff};
IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, -1);
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromIndexColorModel(indexColorModel);
TGAHeader header = TGAHeader.from(type, false);
TGAMetadata metadata = new TGAMetadata(type, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), false);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma");
IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName());
assertEquals(4, chroma.getLength());
@@ -184,10 +174,10 @@ public class TGAMetadataTest {
@Test
public void testStandardCompressionRLE() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, true);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode compression = getStandardNode(metadata, "Compression");
IIOMetadataNode compression = metadata.getStandardCompressionNode();
assertNotNull(compression);
assertEquals("Compression", compression.getNodeName());
assertEquals(2, compression.getLength());
@@ -205,18 +195,18 @@ public class TGAMetadataTest {
@Test
public void testStandardCompressionNone() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, false);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false);
TGAMetadata metadata = new TGAMetadata(header, null);
assertNull(getStandardNode(metadata, "Compression")); // No compression, all default...
assertNull(metadata.getStandardCompressionNode()); // No compression, all default...
}
@Test
public void testStandardDataGray() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, true);
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -238,10 +228,10 @@ public class TGAMetadataTest {
@Test
public void testStandardDataRGB() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, true);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -263,11 +253,10 @@ public class TGAMetadataTest {
@Test
public void testStandardDataRGBA() {
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB);
TGAHeader header = TGAHeader.from(type, true);
TGAMetadata metadata = new TGAMetadata(type, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -291,11 +280,10 @@ public class TGAMetadataTest {
public void testStandardDataPalette() {
byte[] rgb = new byte[1 << 8]; // Colors doesn't really matter here
IndexColorModel indexColorModel = new IndexColorModel(8, rgb.length, rgb, rgb, rgb, 0);
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromIndexColorModel(indexColorModel);
TGAHeader header = TGAHeader.from(type, true);
TGAMetadata metadata = new TGAMetadata(type, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data");
IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data);
assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength());
@@ -317,56 +305,53 @@ public class TGAMetadataTest {
@Test
public void testStandardDimensionNormal() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, true);
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode dimension = getStandardNode(metadata, "Dimension");
IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling();
IIOMetadataNode imageOrientation = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) imageOrientation.getNextSibling();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
@Test
public void testStandardDimensionFlipH() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, true);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true);
header.origin = TGA.ORIGIN_LOWER_LEFT;
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, null);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode dimension = getStandardNode(metadata, "Dimension");
IIOMetadataNode dimension = metadata.getStandardDimensionNode();
assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling();
IIOMetadataNode imageOrientation = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("FlipH", imageOrientation.getAttribute("value"));
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) imageOrientation.getNextSibling();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
assertNull(pixelAspectRatio.getNextSibling()); // No more children
}
@Test
public void testStandardDocument() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, true);
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode document = getStandardNode(metadata, "Document");
IIOMetadataNode document = metadata.getStandardDocumentNode();
assertNotNull(document);
assertEquals("Document", document.getNodeName());
assertEquals(1, document.getLength());
@@ -380,13 +365,13 @@ public class TGAMetadataTest {
@Test
public void testStandardDocumentExtensions() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, true);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true);
TGAExtensions extensions = new TGAExtensions();
extensions.creationDate = Calendar.getInstance();
extensions.creationDate.set(2021, Calendar.APRIL, 8, 18, 55, 0);
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, extensions);
TGAMetadata metadata = new TGAMetadata(header, extensions);
IIOMetadataNode document = getStandardNode(metadata, "Document");
IIOMetadataNode document = metadata.getStandardDocumentNode();
assertNotNull(document);
assertEquals("Document", document.getNodeName());
assertEquals(2, document.getLength());
@@ -409,7 +394,7 @@ public class TGAMetadataTest {
@Test
public void testStandardText() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, true);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true);
header.identification = "MY_FILE.TGA";
TGAExtensions extensions = new TGAExtensions();
@@ -417,9 +402,9 @@ public class TGAMetadataTest {
extensions.authorName = "Harald K";
extensions.authorComments = "Comments, comments... ";
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, extensions);
TGAMetadata metadata = new TGAMetadata(header, extensions);
IIOMetadataNode text = getStandardNode(metadata, "Text");
IIOMetadataNode text = metadata.getStandardTextNode();
assertNotNull(text);
assertEquals("Text", text.getNodeName());
assertEquals(4, text.getLength());
@@ -447,10 +432,10 @@ public class TGAMetadataTest {
@Test
public void testStandardTransparencyRGB() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, true);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency");
IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength());
@@ -464,11 +449,10 @@ public class TGAMetadataTest {
@Test
public void testStandardTransparencyRGBA() {
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR);
TGAHeader header = TGAHeader.from(type, true);
TGAMetadata metadata = new TGAMetadata(type, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency");
IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength());
@@ -484,30 +468,19 @@ public class TGAMetadataTest {
public void testStandardTransparencyPalette() {
byte[] bw = {0, (byte) 0xff};
IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, 1);
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromIndexColorModel(indexColorModel);
TGAHeader header = TGAHeader.from(type, true);
TGAMetadata metadata = new TGAMetadata(type, header, null);
TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), true);
TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency");
IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName());
assertEquals(2, transparency.getLength());
assertEquals(1, transparency.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("Alpha", alpha.getNodeName());
assertEquals("nonpremultiplied", alpha.getAttribute("value"));
IIOMetadataNode transparentIndex = (IIOMetadataNode) alpha.getNextSibling();
assertEquals("TransparentIndex", transparentIndex.getNodeName());
assertEquals("1", transparentIndex.getAttribute("value"));
assertNull(transparentIndex.getNextSibling()); // No more children
assertNull(alpha.getNextSibling()); // No more children
}
private IIOMetadataNode getStandardNode(IIOMetadata metadata, String nodeName) {
IIOMetadataNode asTree = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
NodeList nodes = asTree.getElementsByTagName(nodeName);
return nodes.getLength() > 0 ? (IIOMetadataNode) nodes.item(0) : null;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-thumbsdb</artifactId>
<name>TwelveMonkeys :: ImageIO :: Thumbs.db plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-tiff-jai-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: TIFF/JAI Metadata Interop</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-tiff-jdk-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: TIFF/JDK JPEG Interop</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-tiff</artifactId>
<name>TwelveMonkeys :: ImageIO :: TIFF plugin</name>
@@ -39,6 +39,7 @@ import com.twelvemonkeys.imageio.stream.ByteArrayImageInputStream;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest;
import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.NullOutputStream;
import org.junit.Test;
import org.w3c.dom.NodeList;
@@ -556,7 +557,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest<TIFFImageWriter
IIOWriteProgressListener progress = mock(IIOWriteProgressListener.class, "progress");
writer.addIIOWriteProgressListener(progress);
try (ImageOutputStream output = new NullImageOutputStream()) {
try (ImageOutputStream output = ImageIO.createImageOutputStream(new NullOutputStream())) {
writer.setOutput(output);
try {
@@ -614,7 +615,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest<TIFFImageWriter
public void testWriteParamJPEGQuality() throws IOException {
ImageWriter writer = createWriter();
try (ImageOutputStream output = new NullImageOutputStream()) {
try (ImageOutputStream output = ImageIO.createImageOutputStream(new NullOutputStream())) {
writer.setOutput(output);
try {
@@ -1365,30 +1366,21 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest<TIFFImageWriter
private static final class NullImageOutputStream extends ImageOutputStreamImpl {
@Override
public void write(int b) {
streamPos++;
}
@Override
public void write(byte[] b, int off, int len) {
streamPos += len;
}
@Override
public int read() {
streamPos++;
return 0;
}
@Override
public int read(byte[] b, int off, int len) {
streamPos += len;
return 0;
}
@Override
public long length() {
return streamPos;
}
}
// Special purpose data buffer that does not require memory, to allow very large images
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.9.2</version>
<version>3.8.4-SNAPSHOT</version>
</parent>
<artifactId>imageio-webp</artifactId>
<name>TwelveMonkeys :: ImageIO :: WebP plugin</name>
@@ -1,42 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp;
/**
* @author Simon Kammermeier
*/
interface AlphaFiltering {
int NONE = 0;
int HORIZONTAL = 1;
int VERTICAL = 2;
int GRADIENT = 3;
}
@@ -1,169 +1,56 @@
/*
* Copyright (c) 2020, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp;
import javax.imageio.stream.ImageInputStream;
import java.io.EOFException;
import java.io.IOException;
import static com.twelvemonkeys.lang.Validate.notNull;
/**
* LSBBitReader
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author Simon Kammermeier
*/
public final class LSBBitReader {
// TODO: Consider creating an ImageInputStream wrapper with the WebP implementation of readBit(s)?
private final ImageInputStream imageInput;
private int bitOffset = 64;
private long streamPosition = -1;
/**
* Pre-buffers up to the next 8 Bytes in input.
* Contains valid bits in bits 63 to {@code bitOffset} (inclusive).
*/
private long buffer;
int bitOffset = 0;
public LSBBitReader(ImageInputStream imageInput) {
this.imageInput = notNull(imageInput);
this.imageInput = imageInput;
}
/**
* Reads the specified number of bits from the stream in an LSB-first way and advances the bitOffset.
* The underlying ImageInputStream will be advanced to the first not (completely) read byte.
* Requesting more than 64 bits will advance the reader by the correct amount and return the lowest 64 bits of
* the read number
*
* @param bits the number of bits to read
* @return a signed long built from the requested bits (truncated to the low 64 bits)
* @throws IOException if an I/O error occurs
* @see LSBBitReader#peekBits
*/
// TODO: Optimize this... Read many bits at once!
public long readBits(int bits) throws IOException {
return readBits(bits, false);
long result = 0;
for (int i = 0; i < bits; i++) {
result |= (long) readBit() << i;
}
return result;
}
/**
* Reads the specified number of bits from the buffer in an LSB-first way.
* Does not advance the bitOffset or the underlying input stream.
* As only 56 bits are buffered (in the worst case) peeking more is not possible without advancing the reader and
* as such disallowed.
*
* @param bits the number of bits to peek (max 56)
* @return a signed long built from the requested bits
* @throws IOException if an I/O error occurs
* @see LSBBitReader#readBits
*/
public long peekBits(int bits) throws IOException {
if (bits > 56) {
throw new IllegalArgumentException("Tried peeking over 56");
}
return readBits(bits, true);
}
private long readBits(int bits, boolean peek) throws IOException {
if (bits <= 56) {
// Could eliminate if we never read from the underlying InputStream
// outside this class after the object is created
if (streamPosition != imageInput.getStreamPosition()) {
// Need to reset buffer as stream was read in the meantime
resetBuffer();
}
long ret = (buffer >>> bitOffset) & ((1L << bits) - 1);
if (!peek) {
bitOffset += bits;
if (bitOffset >= 8) {
refillBuffer();
}
}
return ret;
}
else {
// Peek always false in this case
long lower = readBits(56);
return (readBits(bits - 56) << (56)) | lower;
}
}
private void refillBuffer() throws IOException {
// Set to stream position consistent with buffered bytes
imageInput.readLong(); // Don't replace with skipBytes(8) or seek(+8), this will invalidate stream buffer... TODO: Fix streams to cope...
for (; bitOffset >= 8; bitOffset -= 8) {
try {
byte b = imageInput.readByte();
buffer = ((long) b << 56) | buffer >>> 8;
streamPosition++;
}
catch (EOFException e) {
imageInput.seek(streamPosition);
return;
}
}
// Reset to guarantee stream position consistent with returned bytes
// Would not need to do this seeking around when the underlying ImageInputStream is never read from outside
// this class after the object is created.
imageInput.seek(streamPosition);
}
private void resetBuffer() throws IOException {
long inputStreamPosition = imageInput.getStreamPosition();
try {
buffer = imageInput.readLong();
bitOffset = 0;
streamPosition = inputStreamPosition;
imageInput.seek(inputStreamPosition);
}
catch (EOFException e) {
// Retry byte by byte
streamPosition = inputStreamPosition - 8;
bitOffset = 64;
refillBuffer();
}
}
// Left for backwards compatibility / Compatibility with ImageInputStream interface
// TODO: Optimize this...
// TODO: Consider not reading value over and over....
public int readBit() throws IOException {
return (int) readBits(1);
int bit = 7 - bitOffset;
imageInput.setBitOffset(bit);
// Compute final bit offset before we call read() and seek()
int newBitOffset = (bitOffset + 1) & 0x7;
int val = imageInput.read();
if (val == -1) {
throw new EOFException();
}
if (newBitOffset != 0) {
// Move byte position back if in the middle of a byte
// NOTE: RESETS bit offset!
imageInput.seek(imageInput.getStreamPosition() - 1);
}
bitOffset = newBitOffset;
// Shift the bit to be read to the rightmost position
return (val >> (7 - bit)) & 0x1;
}
}
@@ -1,19 +1,184 @@
/*
* Copyright (c) 2017, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp;
import com.twelvemonkeys.imageio.StandardImageMetadataSupport;
import com.twelvemonkeys.imageio.AbstractMetadata;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataNode;
import static com.twelvemonkeys.lang.Validate.notNull;
final class WebPImageMetadata extends StandardImageMetadataSupport {
WebPImageMetadata(ImageTypeSpecifier type, VP8xChunk header) {
super(builder(type)
.withCompressionTypeName(notNull(header, "header").isLossless ? "VP8L" : "VP8")
.withCompressionLossless(header.isLossless)
.withPixelAspectRatio(1.0)
.withFormatVersion("1.0")
// TODO: Get useful text nodes from EXIF or XMP
);
/**
* WebPMetadata
*/
final class WebPImageMetadata extends AbstractMetadata {
private final VP8xChunk header;
WebPImageMetadata(final VP8xChunk header) {
this.header = notNull(header, "header");
}
@Override
protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
chroma.appendChild(csType);
csType.setAttribute("name", "RGB");
// NOTE: Channels in chroma node reflects channels in color model (see data node, for channels in data)
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
chroma.appendChild(numChannels);
numChannels.setAttribute("value", Integer.toString(header.containsALPH ? 4 : 3));
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
chroma.appendChild(blackIsZero);
blackIsZero.setAttribute("value", "TRUE");
return chroma;
}
@Override
protected IIOMetadataNode getStandardCompressionNode() {
IIOMetadataNode node = new IIOMetadataNode("Compression");
IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName");
node.appendChild(compressionTypeName);
String value = header.isLossless ? "VP8L" : "VP8"; // TODO: Naming: VP8L and VP8 or WebP and WebP Lossless?
compressionTypeName.setAttribute("value", value);
// TODO: VP8 + lossless alpha!
IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
node.appendChild(lossless);
lossless.setAttribute("value", header.isLossless ? "TRUE" : "FALSE");
return node;
}
@Override
protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode node = new IIOMetadataNode("Data");
// TODO: WebP seems to support planar as well?
IIOMetadataNode planarConfiguration = new IIOMetadataNode("PlanarConfiguration");
node.appendChild(planarConfiguration);
planarConfiguration.setAttribute("value", "PixelInterleaved");
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
node.appendChild(sampleFormat);
sampleFormat.setAttribute("value", "UnsignedIntegral");
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
node.appendChild(bitsPerSample);
bitsPerSample.setAttribute("value", createListValue(header.containsALPH ? 4 : 3, Integer.toString(8)));
return node;
}
private String createListValue(final int itemCount, final String... values) {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < itemCount; i++) {
if (buffer.length() > 0) {
buffer.append(' ');
}
buffer.append(values[i % values.length]);
}
return buffer.toString();
}
@Override
protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
dimension.appendChild(imageOrientation);
imageOrientation.setAttribute("value", "Normal");
IIOMetadataNode pixelAspectRatio = new IIOMetadataNode("PixelAspectRatio");
dimension.appendChild(pixelAspectRatio);
pixelAspectRatio.setAttribute("value", "1.0");
return dimension;
}
@Override
protected IIOMetadataNode getStandardDocumentNode() {
IIOMetadataNode document = new IIOMetadataNode("Document");
IIOMetadataNode formatVersion = new IIOMetadataNode("FormatVersion");
document.appendChild(formatVersion);
formatVersion.setAttribute("value", "1.0");
return document;
}
@Override
protected IIOMetadataNode getStandardTextNode() {
IIOMetadataNode text = new IIOMetadataNode("Text");
// TODO: Get useful text nodes from EXIF or XMP
// NOTE: Names corresponds to equivalent fields in TIFF
return text.hasChildNodes() ? text : null;
}
// private void appendTextEntry(final IIOMetadataNode parent, final String keyword, final String value) {
// if (value != null) {
// IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
// parent.appendChild(textEntry);
// textEntry.setAttribute("keyword", keyword);
// textEntry.setAttribute("value", value);
// }
// }
// No tiling
@Override
protected IIOMetadataNode getStandardTransparencyNode() {
if (header.containsALPH) {
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
transparency.appendChild(alpha);
alpha.setAttribute("value", "nonpremultiplied");
return transparency;
}
return null;
}
// TODO: Define native WebP metadata format (probably use RIFF structure)
}
@@ -31,6 +31,28 @@
package com.twelvemonkeys.imageio.plugins.webp;
import java.awt.*;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ICC_Profile;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorProfiles;
import com.twelvemonkeys.imageio.color.ColorSpaces;
@@ -45,25 +67,6 @@ import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import com.twelvemonkeys.imageio.util.RasterUtils;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.color.*;
import java.awt.image.*;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import static java.lang.Math.max;
import static java.lang.Math.min;
/**
* WebPImageReader
*/
@@ -96,9 +99,7 @@ final class WebPImageReader extends ImageReaderBase {
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
super.setInput(input, seekForwardOnly, ignoreMetadata);
if (imageInput != null) {
lsbBitReader = new LSBBitReader(imageInput);
}
lsbBitReader = new LSBBitReader(imageInput);
}
private void readHeader(int imageIndex) throws IOException {
@@ -140,16 +141,6 @@ final class WebPImageReader extends ImageReaderBase {
case WebP.CHUNK_ANIM:
// TODO: 32 bit bg color (hint!) + 16 bit loop count
// + expose bg color in std image metadata...
/*
int b = (int) lsbBitReader.readBits(8);
int g = (int) lsbBitReader.readBits(8);
int r = (int) lsbBitReader.readBits(8);
int a = (int) lsbBitReader.readBits(8);
Color bg = new Color(r, g, b, a);
short loopCount = (short) lsbBitReader.readBits(16);
*/
break;
case WebP.CHUNK_ANMF:
@@ -162,7 +153,7 @@ final class WebPImageReader extends ImageReaderBase {
Rectangle bounds = new Rectangle(x, y, w, h);
// TODO: Expose duration/flags in image metadata
int duration = (int) lsbBitReader.readBits(24);
int duration = (int) imageInput.readBits(24);
int flags = imageInput.readUnsignedByte(); // 6 bit reserved + blend mode + disposal mode
frames.add(new AnimationFrame(chunkLength, chunkStart, bounds, duration, flags));
@@ -214,7 +205,7 @@ final class WebPImageReader extends ImageReaderBase {
switch (chunk) {
case WebP.CHUNK_VP8_:
// https://tools.ietf.org/html/rfc6386#section-9.1
//https://tools.ietf.org/html/rfc6386#section-9.1
int frameType = lsbBitReader.readBit(); // 0 = key frame, 1 = inter frame (not used in WebP)
if (frameType != 0) {
@@ -274,19 +265,19 @@ final class WebPImageReader extends ImageReaderBase {
}
// RsV|I|L|E|X|A|R
int reserved = lsbBitReader.readBit();
int reserved = (int) imageInput.readBits(2);
if (reserved != 0) {
// Spec says SHOULD be 0
throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved));
}
header.containsANIM = lsbBitReader.readBit() == 1; // A -> Anim
header.containsXMP_ = lsbBitReader.readBit() == 1;
header.containsEXIF = lsbBitReader.readBit() == 1;
header.containsALPH = lsbBitReader.readBit() == 1; // L -> aLpha
header.containsICCP = lsbBitReader.readBit() == 1;
header.containsICCP = imageInput.readBit() == 1;
header.containsALPH = imageInput.readBit() == 1; // L -> aLpha
header.containsEXIF = imageInput.readBit() == 1;
header.containsXMP_ = imageInput.readBit() == 1;
header.containsANIM = imageInput.readBit() == 1; // A -> Anim
reserved = (int) lsbBitReader.readBits(26); // 2 + 24 bits reserved
reserved = (int) imageInput.readBits(25); // 1 + 24 bits reserved
if (reserved != 0) {
// Spec says SHOULD be 0
throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved));
@@ -434,7 +425,8 @@ final class WebPImageReader extends ImageReaderBase {
if (header.containsANIM) {
AnimationFrame frame = frames.get(imageIndex);
imageInput.seek(frame.offset + 16);
readVP8Extended(destination, param, frame.offset + frame.length, frame.bounds.width, frame.bounds.height);
opaqueAlpha(destination.getAlphaRaster());
readVP8Extended(destination, param, frame.offset + frame.length);
}
else {
imageInput.seek(header.offset + header.length);
@@ -460,10 +452,6 @@ final class WebPImageReader extends ImageReaderBase {
}
private void readVP8Extended(BufferedImage destination, ImageReadParam param, long streamEnd) throws IOException {
readVP8Extended(destination, param, streamEnd, header.width, header.height);
}
private void readVP8Extended(BufferedImage destination, ImageReadParam param, long streamEnd, final int width, final int height) throws IOException {
while (imageInput.getStreamPosition() < streamEnd) {
int nextChunk = imageInput.readInt();
long chunkLength = imageInput.readUnsignedInt();
@@ -477,16 +465,45 @@ final class WebPImageReader extends ImageReaderBase {
switch (nextChunk) {
case WebP.CHUNK_ALPH:
readAlpha(destination, param, width, height);
int reserved = (int) imageInput.readBits(2);
if (reserved != 0) {
// Spec says SHOULD be 0
processWarningOccurred(String.format("Unexpected 'ALPH' chunk reserved value, expected 0: %d", reserved));
}
int preProcessing = (int) imageInput.readBits(2);
int filtering = (int) imageInput.readBits(2);
int compression = (int) imageInput.readBits(2);
if (DEBUG) {
System.out.println("preProcessing: " + preProcessing);
System.out.println("filtering: " + filtering);
System.out.println("compression: " + compression);
}
switch (compression) {
case 0:
readUncompressedAlpha(destination.getAlphaRaster());
break;
case 1:
opaqueAlpha(destination.getAlphaRaster()); // TODO: Remove when correctly implemented!
// readVP8Lossless(destination.getAlphaRaster(), param);
break;
default:
processWarningOccurred("Unknown WebP alpha compression: " + compression);
opaqueAlpha(destination.getAlphaRaster());
break;
}
break;
case WebP.CHUNK_VP8_:
readVP8(RasterUtils.asByteRaster(destination.getRaster())
.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), 0, 0, new int[] {0, 1, 2}), param);
.createWritableChild(0, 0, destination.getWidth(), destination.getHeight(), 0, 0, new int[]{ 0, 1, 2}), param);
break;
case WebP.CHUNK_VP8L:
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param, width, height);
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param);
break;
case WebP.CHUNK_ANIM:
@@ -510,92 +527,6 @@ final class WebPImageReader extends ImageReaderBase {
}
}
private void readAlpha(BufferedImage destination, ImageReadParam param, final int width, final int height) throws IOException {
int reserved = (int) lsbBitReader.readBits(2);
if (reserved != 0) {
// Spec says SHOULD be 0
processWarningOccurred(String.format("Unexpected 'ALPH' chunk reserved value, expected 0: %d", reserved));
}
int preProcessing = (int) lsbBitReader.readBits(2);
int filtering = (int) lsbBitReader.readBits(2);
int compression = (int) lsbBitReader.readBits(2);
if (DEBUG) {
System.out.println("preProcessing: " + preProcessing);
System.out.println("filtering: " + filtering);
System.out.println("compression: " + compression);
}
WritableRaster alphaRaster = destination.getAlphaRaster();
switch (compression) {
case 0:
readUncompressedAlpha(alphaRaster);
break;
case 1:
// Simulate header
imageInput.seek(imageInput.getStreamPosition() - 5);
WritableRaster tempRaster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, destination.getWidth(), destination.getHeight(), 4, null);
readVP8Lossless(tempRaster, param, width, height);
// Copy from green (band 1) in temp to alpha in destination
alphaRaster.setRect(tempRaster.createChild(0, 0, tempRaster.getWidth(), tempRaster.getHeight(), 0, 0, new int[] {1}));
break;
default:
processWarningOccurred("Unknown WebP alpha compression: " + compression);
opaqueAlpha(alphaRaster);
break;
}
if (filtering != AlphaFiltering.NONE) {
for (int y = 0; y < destination.getHeight(); y++) {
for (int x = 0; x < destination.getWidth(); x++) {
int predictorAlpha = getPredictorAlpha(alphaRaster, filtering, y, x);
alphaRaster.setSample(x, y, 0, alphaRaster.getSample(x, y, 0) + predictorAlpha % 256);
}
}
}
}
private int getPredictorAlpha(WritableRaster alphaRaster, int filtering, int y, int x) {
switch (filtering) {
case AlphaFiltering.NONE:
return 0;
case AlphaFiltering.HORIZONTAL:
if (x == 0) {
return y == 0 ? 0 : alphaRaster.getSample(0, y - 1, 0);
}
else {
return alphaRaster.getSample(x - 1, y, 0);
}
case AlphaFiltering.VERTICAL:
if (y == 0) {
return x == 0 ? 0 : alphaRaster.getSample(x - 1, 0, 0);
}
else {
return alphaRaster.getSample(x, y - 1, 0);
}
case AlphaFiltering.GRADIENT:
if (x == 0) {
return y == 0 ? 0 : alphaRaster.getSample(0, y - 1, 0);
}
else if (y == 0) {
return alphaRaster.getSample(x - 1, 0, 0);
}
else {
int left = alphaRaster.getSample(x - 1, y, 0);
int top = alphaRaster.getSample(x, y - 1, 0);
int topLeft = alphaRaster.getSample(x - 1, y - 1, 0);
return max(0, min(left + top - topLeft, 255));
}
default:
processWarningOccurred("Unknown WebP alpha filtering: " + filtering);
return 0;
}
}
private void applyICCProfileIfNeeded(final BufferedImage destination) {
if (iccProfile != null) {
ColorModel colorModel = destination.getColorModel();
@@ -627,7 +558,6 @@ final class WebPImageReader extends ImageReaderBase {
}
}
@SuppressWarnings("RedundantThrows")
private void readUncompressedAlpha(final WritableRaster alphaRaster) throws IOException {
// Hardly used in practice, need to find a sample file
processWarningOccurred("Uncompressed WebP alpha not implemented");
@@ -635,13 +565,8 @@ final class WebPImageReader extends ImageReaderBase {
}
private void readVP8Lossless(final WritableRaster raster, final ImageReadParam param) throws IOException {
readVP8Lossless(raster, param, header.width, header.height);
}
private void readVP8Lossless(final WritableRaster raster, final ImageReadParam param,
final int width, final int height) throws IOException {
VP8LDecoder decoder = new VP8LDecoder(imageInput, DEBUG);
decoder.readVP8Lossless(raster, true, param, width, height);
decoder.readVP8Lossless(raster, true);
}
private void readVP8(final WritableRaster raster, final ImageReadParam param) throws IOException {
@@ -663,7 +588,10 @@ final class WebPImageReader extends ImageReaderBase {
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
return new WebPImageMetadata(getRawImageType(imageIndex), header);
readHeader(imageIndex);
readMeta();
return new WebPImageMetadata(header);
}
private void readMeta() throws IOException {
@@ -75,8 +75,10 @@ public final class WebPImageReaderSpi extends ImageReaderSpiBase {
int chunk = stream.readInt();
switch (chunk) {
case WebP.CHUNK_VP8L:
// TODO. Support lossless
// case WebP.CHUNK_VP8L:
case WebP.CHUNK_VP8X:
return containsSupportedChunk(stream, chunk);
case WebP.CHUNK_VP8_:
return true;
default:
@@ -89,6 +91,30 @@ public final class WebPImageReaderSpi extends ImageReaderSpiBase {
}
}
private static boolean containsSupportedChunk(ImageInputStream stream, int chunk) throws IOException {
// Temporary: Seek for VP8_, either first or second (after ICCP), or inside ANMF...
try {
while (chunk != WebP.CHUNK_VP8L && chunk != WebP.CHUNK_ALPH) {
long length = stream.readUnsignedInt();
stream.seek(stream.getStreamPosition() + length);
chunk = stream.readInt();
// Look inside ANMF chunks...
if (chunk == WebP.CHUNK_ANMF) {
stream.seek(stream.getStreamPosition() + 4 + 16);
chunk = stream.readInt();
}
if (chunk == WebP.CHUNK_VP8_) {
return true;
}
}
}
catch (EOFException ignore) {}
return false;
}
@Override
public ImageReader createReaderInstance(final Object extension) {
return new WebPImageReader(this);
@@ -1,74 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp.lossless;
import java.awt.image.*;
/**
* @author Simon Kammermeier
*/
final class ColorIndexingTransform implements Transform {
private final byte[] colorTable;
private final byte bits;
public ColorIndexingTransform(byte[] colorTable, byte bits) {
this.colorTable = colorTable;
this.bits = bits;
}
@Override
public void applyInverse(WritableRaster raster) {
int width = raster.getWidth();
int height = raster.getHeight();
byte[] rgba = new byte[4];
for (int y = 0; y < height; y++) {
// Reversed so no used elements are overridden (in case of packing)
for (int x = width - 1; x >= 0; x--) {
int componentSize = 8 >> bits;
int packed = 1 << bits;
int xC = x / packed;
int componentOffset = componentSize * (x % packed);
int sample = raster.getSample(xC, y, 1);
int index = sample >> componentOffset & ((1 << componentSize) - 1);
// Arraycopy for 4 elements might not be beneficial
System.arraycopy(colorTable, index * 4, rgba, 0, 4);
raster.setDataElements(x, y, rgba);
}
}
}
}
@@ -1,127 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp.lossless;
import java.awt.image.*;
/**
* @author Simon Kammermeier
*/
final class ColorTransform implements Transform {
private final Raster data;
private final byte bits;
public ColorTransform(Raster raster, byte bits) {
this.data = raster;
this.bits = bits;
}
@Override
public void applyInverse(WritableRaster raster) {
int width = raster.getWidth();
int height = raster.getHeight();
byte[] rgba = new byte[4];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
data.getDataElements(x >> bits, y >> bits, rgba);
ColorTransformElement trans = new ColorTransformElement(rgba);
raster.getDataElements(x, y, rgba);
trans.inverseTransform(rgba);
raster.setDataElements(x, y, rgba);
}
}
}
// NOTE: For encoding!
private static void colorTransform(final int red, final int blue, final int green,
final ColorTransformElement trans,
final int[] newRedBlue) {
// Transformed values of red and blue components
int tmp_red = red;
int tmp_blue = blue;
// Applying transform is just adding the transform deltas
tmp_red += colorTransformDelta((byte) trans.green_to_red, (byte) green);
tmp_blue += colorTransformDelta((byte) trans.green_to_blue, (byte) green);
tmp_blue += colorTransformDelta((byte) trans.red_to_blue, (byte) red);
// No pointer dereferences in Java...
// TODO: Consider passing an offset too, so we can modify in-place
newRedBlue[0] = tmp_red & 0xff;
newRedBlue[1] = tmp_blue & 0xff;
}
// A conversion from the 8-bit unsigned representation (uint8) to the 8-bit
// signed one (int8) is required before calling ColorTransformDelta(). It
// should be performed using 8-bit two's complement (that is: uint8 range
// [128-255] is mapped to the [-128, -1] range of its converted int8
// value).
private static byte colorTransformDelta(final byte t, final byte c) {
return (byte) ((t * c) >> 5);
}
private static final class ColorTransformElement {
final int green_to_red;
final int green_to_blue;
final int red_to_blue;
ColorTransformElement(final byte[] rgba) {
this.green_to_red = rgba[2];
this.green_to_blue = rgba[1];
this.red_to_blue = rgba[0];
}
private void inverseTransform(final byte[] rgb) {
// Applying inverse transform is just adding (!, different from specification) the
// color transform deltas 3
// Transformed values of red and blue components
int tmp_red = rgb[0];
int tmp_blue = rgb[2];
tmp_red += colorTransformDelta((byte) this.green_to_red, rgb[1]);
tmp_blue += colorTransformDelta((byte) this.green_to_blue, rgb[1]);
tmp_blue += colorTransformDelta((byte) this.red_to_blue, (byte) tmp_red); // Spec has red & 0xff
rgb[0] = (byte) (tmp_red & 0xff);
rgb[2] = (byte) (tmp_blue & 0xff);
}
}
}
@@ -1,59 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp.lossless;
import com.twelvemonkeys.imageio.plugins.webp.LSBBitReader;
import java.io.IOException;
/**
* @author Simon Kammermeier
*/
final class HuffmanCodeGroup {
/**
* Used for green, backward reference length and color cache
*/
public final HuffmanTable mainCode;
public final HuffmanTable redCode;
public final HuffmanTable blueCode;
public final HuffmanTable alphaCode;
public final HuffmanTable distanceCode;
public HuffmanCodeGroup(LSBBitReader lsbBitReader, int colorCacheBits) throws IOException {
mainCode = new HuffmanTable(lsbBitReader, 256 + 24 + (colorCacheBits > 0 ? 1 << colorCacheBits : 0));
redCode = new HuffmanTable(lsbBitReader, 256);
blueCode = new HuffmanTable(lsbBitReader, 256);
alphaCode = new HuffmanTable(lsbBitReader, 256);
distanceCode = new HuffmanTable(lsbBitReader, 40);
}
}
@@ -1,51 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp.lossless;
import java.awt.image.*;
/**
* @author Simon Kammermeier
*/
final class HuffmanInfo {
public final Raster huffmanMetaCodes; // Raster allows intuitive lookup by x and y
public final int metaCodeBits;
public final HuffmanCodeGroup[] huffmanGroups;
public HuffmanInfo(Raster huffmanMetaCodes, int metaCodeBits, HuffmanCodeGroup[] huffmanGroups) {
this.huffmanMetaCodes = huffmanMetaCodes;
this.metaCodeBits = metaCodeBits;
this.huffmanGroups = huffmanGroups;
}
}
@@ -1,351 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp.lossless;
import com.twelvemonkeys.imageio.plugins.webp.LSBBitReader;
import javax.imageio.IIOException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Represents a single huffman tree as a table.
* <p>
* Decoding a symbol just involves reading bits from the input stream and using that read value to index into the
* lookup table.
* <p>
* Code length and the corresponding symbol are packed into one array element (int).
* This is done to avoid the overhead and the fragmentation over the whole heap involved with creating objects
* of a custom class. The upper 16 bits of each element are the code length and lower 16 bits are the symbol.
* <p>
* The max allowed code length by the WEBP specification is 15, therefore this would mean the table needs to have
* 2^15 elements. To keep a reasonable memory usage, instead the lookup table only directly holds symbols with code
* length up to {@code LEVEL1_BITS} (Currently 8 bits). For longer codes the lookup table stores a reference to a
* second level lookup table. This reference consists of an element with length as the max length of the level 2
* table and value as the index of the table in the list of level 2 tables.
* <p>
* Reading bits from the input is done in a least significant bit first way (LSB) way, therefore the prefix of the
* read value of length i is the lowest i bits in inverse order.
* The lookup table is directly indexed by the {@code LEVEL1_BITS} next bits read from the input (i.e. the bits
* corresponding to next code are inverse suffix of the read value/index).
* So for a code length of l all values with the lowest l bits the same need to decode to the same symbol
* regardless of the {@code (LEVEL1_BITS - l)} higher bits. So the lookup table needs to have the entry of this symbol
* repeated every 2^(l + 1) spots starting from the bitwise inverse of the code.
*
* @author Simon Kammermeier
*/
final class HuffmanTable {
private static final int LEVEL1_BITS = 8;
/**
* Symbols of the L-code in the order they need to be read
*/
private static final int[] L_CODE_ORDER = {17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
private final int[] level1 = new int[1 << LEVEL1_BITS];
private final List<int[]> level2 = new ArrayList<>();
/**
* Build a Huffman table by reading the encoded symbol lengths from the reader
*
* @param lsbBitReader the reader to read from
* @param alphabetSize the number of symbols in the alphabet to be decoded by this huffman table
* @throws IOException when reading produces an exception
*/
public HuffmanTable(LSBBitReader lsbBitReader, int alphabetSize) throws IOException {
boolean simpleLengthCode = lsbBitReader.readBit() == 1;
if (simpleLengthCode) {
int symbolNum = lsbBitReader.readBit() + 1;
boolean first8Bits = lsbBitReader.readBit() == 1;
short symbol1 = (short) lsbBitReader.readBits(first8Bits ? 8 : 1);
if (symbolNum == 2) {
short symbol2 = (short) lsbBitReader.readBits(8);
for (int i = 0; i < (1 << LEVEL1_BITS); i += 2) {
level1[i] = 1 << 16 | symbol1;
level1[i + 1] = 1 << 16 | symbol2;
}
}
else {
Arrays.fill(level1, symbol1);
}
}
else {
// code lengths also huffman coded
// first read the "first stage" code lengths
// In the following this is called the L-Code (for length code)
int numLCodeLengths = (int) (lsbBitReader.readBits(4) + 4);
short[] lCodeLengths = new short[L_CODE_ORDER.length];
int numPosCodeLens = 0;
for (int i = 0; i < numLCodeLengths; i++) {
short len = (short) lsbBitReader.readBits(3);
lCodeLengths[L_CODE_ORDER[i]] = len;
if (len > 0) {
numPosCodeLens++;
}
}
// Use L-Code to read the actual code lengths
short[] codeLengths = readCodeLengths(lsbBitReader, lCodeLengths, alphabetSize, numPosCodeLens);
buildFromLengths(codeLengths);
}
}
/**
* Builds a Huffman table by using already given code lengths to generate the codes from
*
* @param codeLengths the array specifying the bit length of the code for a symbol (i.e. {@code codeLengths[i]}
* is the bit length of the code for the symbol i)
* @param numPosCodeLens the number of positive (i.e. non-zero) codeLengths in the array (allows more efficient
* table generation)
*/
private HuffmanTable(short[] codeLengths, int numPosCodeLens) {
buildFromLengths(codeLengths, numPosCodeLens);
}
// Helper methods to allow reusing in different constructors
private void buildFromLengths(short[] codeLengths) {
int numPosCodeLens = 0;
for (short codeLength : codeLengths) {
if (codeLength != 0) {
numPosCodeLens++;
}
}
buildFromLengths(codeLengths, numPosCodeLens);
}
private void buildFromLengths(short[] codeLengths, int numPosCodeLens) {
// Pack code length and corresponding symbols as described above
int[] lengthsAndSymbols = new int[numPosCodeLens];
int index = 0;
for (int i = 0; i < codeLengths.length; i++) {
if (codeLengths[i] != 0) {
lengthsAndSymbols[index++] = codeLengths[i] << 16 | i;
}
}
// Special case: Only 1 code value
if (numPosCodeLens == 1) {
// Length is 0 so mask to clear length bits
Arrays.fill(level1, lengthsAndSymbols[0] & 0xffff);
}
// Due to the layout of the elements this effectively first sorts by length and then symbol.
Arrays.sort(lengthsAndSymbols);
// The next code, in the bit order it would appear on the input stream, i.e. it is reversed.
// Only the lowest bits (corresponding to the bit length of the code) are considered.
// Example: code 0..010 (length 2) would appear as 0..001.
int code = 0;
// Used for level2 lookup
int rootEntry = -1;
int[] currentTable = null;
for (int i = 0; i < lengthsAndSymbols.length; i++) {
int lengthAndSymbol = lengthsAndSymbols[i];
int length = lengthAndSymbol >>> 16;
if (length <= LEVEL1_BITS) {
for (int j = code; j < level1.length; j += 1 << length) {
level1[j] = lengthAndSymbol;
}
}
else {
// Existing level2 table not fitting
if ((code & ((1 << LEVEL1_BITS) - 1)) != rootEntry) {
// Figure out needed table size.
// Start at current symbol and length.
// Every symbol uses 1 slot at the current bit length.
// Going up 1 bit in length multiplies the slots by 2.
// No more open slots indicate the table size to be big enough.
int maxLength = length;
for (int j = i, openSlots = 1 << (length - LEVEL1_BITS);
j < lengthsAndSymbols.length && openSlots > 0;
j++, openSlots--) {
int innerLength = lengthsAndSymbols[j] >>> 16;
while (innerLength != maxLength) {
maxLength++;
openSlots <<= 1;
}
}
int level2Size = maxLength - LEVEL1_BITS;
currentTable = new int[1 << level2Size];
rootEntry = code & ((1 << LEVEL1_BITS) - 1);
level2.add(currentTable);
// Set root table indirection
level1[rootEntry] = (LEVEL1_BITS + level2Size) << 16 | (level2.size() - 1);
}
// Add to existing (or newly generated) 2nd level table
for (int j = (code >>> LEVEL1_BITS); j < currentTable.length; j += 1 << (length - LEVEL1_BITS)) {
currentTable[j] = (length - LEVEL1_BITS) << 16 | (lengthAndSymbol & 0xffff);
}
}
code = nextCode(code, length);
}
}
/**
* Computes the next code
*
* @param code the current code
* @param length the currently valid length
* @return {@code reverse(reverse(code, length) + 1, length)} where {@code reverse(a, b)} is the lowest b bits of
* a in inverted order
*/
private int nextCode(int code, int length) {
int a = (~code) & ((1 << length) - 1);
// This will result in the highest 0-bit in the lower length bits of code set (by construction of a)
// I.e. the lowest 0-bit in the value code represents
int step = Integer.highestOneBit(a);
// In the represented value this clears the consecutive 1-bits starting at bit 0 and then sets the lowest 0 bit
// This corresponds to adding 1 to the value
return (code & (step - 1)) | step;
}
private static short[] readCodeLengths(LSBBitReader lsbBitReader, short[] aCodeLengths, int alphabetSize,
int numPosCodeLens) throws IOException {
HuffmanTable huffmanTable = new HuffmanTable(aCodeLengths, numPosCodeLens);
// Not sure where this comes from. Just adapted from the libwebp implementation
int codedSymbols;
if (lsbBitReader.readBit() == 1) {
int maxSymbolBitLength = (int) (2 + 2 * lsbBitReader.readBits(3));
codedSymbols = (int) (2 + lsbBitReader.readBits(maxSymbolBitLength));
}
else {
codedSymbols = alphabetSize;
}
short[] codeLengths = new short[alphabetSize];
// Default code for repeating
short prevLength = 8;
for (int i = 0; i < alphabetSize && codedSymbols > 0; i++, codedSymbols--) {
short len = huffmanTable.readSymbol(lsbBitReader);
if (len < 16) { // Literal length
codeLengths[i] = len;
if (len != 0) {
prevLength = len;
}
}
else {
short repeatSymbol = 0;
int extraBits;
int repeatOffset;
switch (len) {
case 16: // Repeat previous
repeatSymbol = prevLength;
extraBits = 2;
repeatOffset = 3;
break;
case 17: // Repeat 0 short
extraBits = 3;
repeatOffset = 3;
break;
case 18: // Repeat 0 long
extraBits = 7;
repeatOffset = 11;
break;
default:
throw new IIOException("Huffman: Unreachable: Decoded Code Length > 18.");
}
int repeatCount = (int) (lsbBitReader.readBits(extraBits) + repeatOffset);
if (i + repeatCount > alphabetSize) {
throw new IIOException(
String.format(
"Huffman: Code length repeat count overflows alphabet: Start index: %d, count: " +
"%d, alphabet size: %d", i, repeatCount, alphabetSize)
);
}
Arrays.fill(codeLengths, i, i + repeatCount, repeatSymbol);
i += repeatCount - 1;
}
}
return codeLengths;
}
/**
* Reads the next code symbol from the streaming and decode it using the Huffman table
*
* @param lsbBitReader the reader to read a symbol from (will be advanced accordingly)
* @return the decoded symbol
* @throws IOException when the reader throws one reading a symbol
*/
public short readSymbol(LSBBitReader lsbBitReader) throws IOException {
int index = (int) lsbBitReader.peekBits(LEVEL1_BITS);
int lengthAndSymbol = level1[index];
int length = lengthAndSymbol >>> 16;
if (length > LEVEL1_BITS) {
// Lvl2 lookup
lsbBitReader.readBits(LEVEL1_BITS); // Consume bits of first level
int level2Index = (int) lsbBitReader.peekBits(length - LEVEL1_BITS); // Peek remaining required bits
lengthAndSymbol = level2.get(lengthAndSymbol & 0xffff)[level2Index];
length = lengthAndSymbol >>> 16;
}
lsbBitReader.readBits(length); // Consume bits
return (short) (lengthAndSymbol & 0xffff);
}
}
@@ -36,7 +36,7 @@ package com.twelvemonkeys.imageio.plugins.webp.lossless;
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
interface PredictorMode {
public interface PredictorMode {
// Special rules:
// Top-left pixel of image is predicted BLACK
// Rest of top pixels is predicted L
@@ -1,270 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp.lossless;
import java.awt.image.*;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import static java.lang.Math.min;
/**
* @author Simon Kammermeier
*/
final class PredictorTransform implements Transform {
private final Raster data;
private final byte bits;
public PredictorTransform(Raster raster, byte bits) {
this.data = raster;
this.bits = bits;
}
@Override
public void applyInverse(WritableRaster raster) {
int width = raster.getWidth();
int height = raster.getHeight();
byte[] rgba = new byte[4];
// Handle top and left border separately
// (0,0) Black (0x000000ff) predict
raster.getDataElements(0, 0, rgba);
rgba[3] += 0xff;
raster.setDataElements(0, 0, rgba);
byte[] predictor = new byte[4];
byte[] predictor2 = new byte[4];
byte[] predictor3 = new byte[4];
// (x,0) L predict
for (int x = 1; x < width; x++) {
raster.getDataElements(x, 0, rgba);
raster.getDataElements(x - 1, 0, predictor);
addPixels(rgba, predictor);
raster.setDataElements(x, 0, rgba);
}
// (0,y) T predict
for (int y = 1; y < height; y++) {
raster.getDataElements(0, y, rgba);
raster.getDataElements(0, y - 1, predictor);
addPixels(rgba, predictor);
raster.setDataElements(0, y, rgba);
}
for (int y = 1; y < height; y++) {
for (int x = 1; x < width; x++) {
int transformType = data.getSample(x >> bits, y >> bits, 1);
raster.getDataElements(x, y, rgba);
int lX = x - 1; // x for left
int tY = y - 1; // y for top
// top right is not (x+1, tY) if last pixel in line instead (0, y)
int trX = x == width - 1 ? 0 : x + 1;
int trY = x == width - 1 ? y : tY;
switch (transformType) {
case PredictorMode.BLACK:
rgba[3] += 0xff;
break;
case PredictorMode.L:
raster.getDataElements(lX, y, predictor);
addPixels(rgba, predictor);
break;
case PredictorMode.T:
raster.getDataElements(x, tY, predictor);
addPixels(rgba, predictor);
break;
case PredictorMode.TR:
raster.getDataElements(trX, trY, predictor);
addPixels(rgba, predictor);
break;
case PredictorMode.TL:
raster.getDataElements(lX, tY, predictor);
addPixels(rgba, predictor);
break;
case PredictorMode.AVG_L_TR_T:
raster.getDataElements(lX, y, predictor);
raster.getDataElements(trX, trY, predictor2);
average2(predictor, predictor2);
raster.getDataElements(x, tY, predictor2);
average2(predictor, predictor2);
addPixels(rgba, predictor);
break;
case PredictorMode.AVG_L_TL:
raster.getDataElements(lX, y, predictor);
raster.getDataElements(lX, tY, predictor2);
average2(predictor, predictor2);
addPixels(rgba, predictor);
break;
case PredictorMode.AVG_L_T:
raster.getDataElements(lX, y, predictor);
raster.getDataElements(x, tY, predictor2);
average2(predictor, predictor2);
addPixels(rgba, predictor);
break;
case PredictorMode.AVG_TL_T:
raster.getDataElements(lX, tY, predictor);
raster.getDataElements(x, tY, predictor2);
average2(predictor, predictor2);
addPixels(rgba, predictor);
break;
case PredictorMode.AVG_T_TR:
raster.getDataElements(x, tY, predictor);
raster.getDataElements(trX, trY, predictor2);
average2(predictor, predictor2);
addPixels(rgba, predictor);
break;
case PredictorMode.AVG_L_TL_T_TR:
raster.getDataElements(lX, y, predictor);
raster.getDataElements(lX, tY, predictor2);
average2(predictor, predictor2);
raster.getDataElements(x, tY, predictor2);
raster.getDataElements(trX, trY, predictor3);
average2(predictor2, predictor3);
average2(predictor, predictor2);
addPixels(rgba, predictor);
break;
case PredictorMode.SELECT:
raster.getDataElements(lX, y, predictor);
raster.getDataElements(x, tY, predictor2);
raster.getDataElements(lX, tY, predictor3);
addPixels(rgba, select(predictor, predictor2, predictor3));
break;
case PredictorMode.CLAMP_ADD_SUB_FULL:
raster.getDataElements(lX, y, predictor);
raster.getDataElements(x, tY, predictor2);
raster.getDataElements(lX, tY, predictor3);
clampAddSubtractFull(predictor, predictor2, predictor3);
addPixels(rgba, predictor);
break;
case PredictorMode.CLAMP_ADD_SUB_HALF:
raster.getDataElements(lX, y, predictor);
raster.getDataElements(x, tY, predictor2);
average2(predictor, predictor2);
raster.getDataElements(lX, tY, predictor2);
clampAddSubtractHalf(predictor, predictor2);
addPixels(rgba, predictor);
break;
}
raster.setDataElements(x, y, rgba);
}
}
}
private static byte[] select(final byte[] l, final byte[] t, final byte[] tl) {
// l = left pixel, t = top pixel, tl = top left pixel.
// ARGB component estimates for prediction.
int pAlpha = addSubtractFull(l[3], t[3], tl[3]);
int pRed = addSubtractFull(l[0], t[0], tl[0]);
int pGreen = addSubtractFull(l[1], t[1], tl[1]);
int pBlue = addSubtractFull(l[2], t[2], tl[2]);
// Manhattan distances to estimates for left and top pixels.
int pL = manhattanDistance(l, pAlpha, pRed, pGreen, pBlue);
int pT = manhattanDistance(t, pAlpha, pRed, pGreen, pBlue);
// Return either left or top, the one closer to the prediction.
return pL < pT ? l : t;
}
private static int manhattanDistance(byte[] rgba, int pAlpha, int pRed, int pGreen, int pBlue) {
return abs(pAlpha - (rgba[3] & 0xff)) + abs(pRed - (rgba[0] & 0xff)) +
abs(pGreen - (rgba[1] & 0xff)) + abs(pBlue - (rgba[2] & 0xff));
}
private static void average2(final byte[] rgba1, final byte[] rgba2) {
rgba1[0] = (byte) (((rgba1[0] & 0xff) + (rgba2[0] & 0xff)) / 2);
rgba1[1] = (byte) (((rgba1[1] & 0xff) + (rgba2[1] & 0xff)) / 2);
rgba1[2] = (byte) (((rgba1[2] & 0xff) + (rgba2[2] & 0xff)) / 2);
rgba1[3] = (byte) (((rgba1[3] & 0xff) + (rgba2[3] & 0xff)) / 2);
}
// Clamp the input value between 0 and 255.
private static int clamp(final int a) {
return max(0, min(a, 255));
}
private static void clampAddSubtractFull(final byte[] a, final byte[] b, final byte[] c) {
a[0] = (byte) clamp(addSubtractFull(a[0], b[0], c[0]));
a[1] = (byte) clamp(addSubtractFull(a[1], b[1], c[1]));
a[2] = (byte) clamp(addSubtractFull(a[2], b[2], c[2]));
a[3] = (byte) clamp(addSubtractFull(a[3], b[3], c[3]));
}
private static void clampAddSubtractHalf(final byte[] a, final byte[] b) {
a[0] = (byte) clamp(addSubtractHalf(a[0], b[0]));
a[1] = (byte) clamp(addSubtractHalf(a[1], b[1]));
a[2] = (byte) clamp(addSubtractHalf(a[2], b[2]));
a[3] = (byte) clamp(addSubtractHalf(a[3], b[3]));
}
private static int addSubtractFull(byte a, byte b, byte c) {
return (a & 0xff) + (b & 0xff) - (c & 0xff);
}
private static int addSubtractHalf(byte a, byte b) {
return (a & 0xff) + ((a & 0xff) - (b & 0xff)) / 2;
}
private static void addPixels(byte[] rgba, byte[] predictor) {
rgba[0] += predictor[0];
rgba[1] += predictor[1];
rgba[2] += predictor[2];
rgba[3] += predictor[3];
}
}
@@ -1,61 +0,0 @@
/*
* Copyright (c) 2022, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.imageio.plugins.webp.lossless;
import java.awt.image.*;
/**
* @author Simon Kammermeier
*/
final class SubtractGreenTransform implements Transform {
private static void addGreenToBlueAndRed(byte[] rgb) {
rgb[0] = (byte) ((rgb[0] + rgb[1]) & 0xff);
rgb[2] = (byte) ((rgb[2] + rgb[1]) & 0xff);
}
@Override
public void applyInverse(WritableRaster raster) {
int width = raster.getWidth();
int height = raster.getHeight();
byte[] rgba = new byte[4];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
raster.getDataElements(x, y, rgba);
addGreenToBlueAndRed(rgba);
raster.setDataElements(x, y, rgba);
}
}
}
}
@@ -31,14 +31,25 @@
package com.twelvemonkeys.imageio.plugins.webp.lossless;
import java.awt.image.*;
/**
* Transform.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
interface Transform {
final class Transform {
final int type;
final Object data;
void applyInverse(WritableRaster raster);
Transform(final int type, final Object data) {
this.type = type;
this.data = data;
}
byte[] getData() {
return (byte[]) data;
}
int[] getColorMap() {
return (int[]) data;
}
}

Some files were not shown because too many files have changed in this diff Show More