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 2535 additions and 6181 deletions
+9 -9
View File
@@ -9,11 +9,11 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ] os: [ ubuntu-latest, windows-latest, macos-latest ]
java: [ 8, 11, 17, 18 ] java: [ 8, 11, 17 ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: actions/setup-java@v3 - uses: actions/setup-java@v2
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: ${{ matrix.java }} java-version: ${{ matrix.java }}
@@ -22,7 +22,7 @@ jobs:
- name: Run Tests - name: Run Tests
run: mvn test run: mvn test
- name: Publish Test Report - name: Publish Test Report
uses: mikepenz/action-junit-report@v3 uses: mikepenz/action-junit-report@v2
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:
report_paths: "**/target/surefire-reports/TEST*.xml" report_paths: "**/target/surefire-reports/TEST*.xml"
@@ -35,11 +35,11 @@ jobs:
matrix: matrix:
kcms: [ true, false ] kcms: [ true, false ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- run: | - run: |
download_url="https://javadl.oracle.com/webapps/download/AutoDL?BundleId=245038_d3c52aa6bfa54d3ca74e617f18309292" download_url="https://javadl.oracle.com/webapps/download/AutoDL?BundleId=245038_d3c52aa6bfa54d3ca74e617f18309292"
wget -O $RUNNER_TEMP/java_package.tar.gz $download_url wget -O $RUNNER_TEMP/java_package.tar.gz $download_url
- uses: actions/setup-java@v3 - uses: actions/setup-java@v2
with: with:
distribution: 'jdkfile' distribution: 'jdkfile'
jdkFile: ${{ runner.temp }}/java_package.tar.gz jdkFile: ${{ runner.temp }}/java_package.tar.gz
@@ -54,7 +54,7 @@ jobs:
- name: Run Tests - name: Run Tests
run: mvn test run: mvn test
- name: Publish Test Report - name: Publish Test Report
uses: mikepenz/action-junit-report@v3 uses: mikepenz/action-junit-report@v2
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:
report_paths: "**/target/surefire-reports/TEST*.xml" report_paths: "**/target/surefire-reports/TEST*.xml"
@@ -66,9 +66,9 @@ jobs:
if: github.ref == 'refs/heads/master' # only perform on latest master if: github.ref == 'refs/heads/master' # only perform on latest master
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Set up Maven Central - name: Set up Maven Central
uses: actions/setup-java@v3 uses: actions/setup-java@v2
with: # running setup-java again overwrites the settings.xml with: # running setup-java again overwrites the settings.xml
distribution: 'temurin' distribution: 'temurin'
java-version: '8' 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), **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 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). [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. 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: 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. - 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. 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. 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> <parent>
<groupId>com.twelvemonkeys</groupId> <groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys</artifactId> <artifactId>twelvemonkeys</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<groupId>com.twelvemonkeys.bom</groupId> <groupId>com.twelvemonkeys.bom</groupId>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.common</groupId> <groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId> <artifactId>common</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>common-image</artifactId> <artifactId>common-image</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.common</groupId> <groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId> <artifactId>common</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>common-io</artifactId> <artifactId>common-io</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.common</groupId> <groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId> <artifactId>common</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>common-lang</artifactId> <artifactId>common-lang</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys</groupId> <groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys</artifactId> <artifactId>twelvemonkeys</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<groupId>com.twelvemonkeys.common</groupId> <groupId>com.twelvemonkeys.common</groupId>
<artifactId>common</artifactId> <artifactId>common</artifactId>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys</groupId> <groupId>com.twelvemonkeys</groupId>
<artifactId>twelvemonkeys</artifactId> <artifactId>twelvemonkeys</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<groupId>com.twelvemonkeys.contrib</groupId> <groupId>com.twelvemonkeys.contrib</groupId>
<artifactId>contrib</artifactId> <artifactId>contrib</artifactId>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-batik</artifactId> <artifactId>imageio-batik</artifactId>
<name>TwelveMonkeys :: ImageIO :: Batik Plugin</name> <name>TwelveMonkeys :: ImageIO :: Batik Plugin</name>
@@ -17,7 +17,7 @@
<properties> <properties>
<project.jpms.module.name>com.twelvemonkeys.imageio.batik</project.jpms.module.name> <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> </properties>
<build> <build>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-bmp</artifactId> <artifactId>imageio-bmp</artifactId>
<name>TwelveMonkeys :: ImageIO :: BMP plugin</name> <name>TwelveMonkeys :: ImageIO :: BMP plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-clippath</artifactId> <artifactId>imageio-clippath</artifactId>
<name>TwelveMonkeys :: ImageIO :: Photoshop Path Support</name> <name>TwelveMonkeys :: ImageIO :: Photoshop Path Support</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-core</artifactId> <artifactId>imageio-core</artifactId>
<name>TwelveMonkeys :: ImageIO :: Core</name> <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. * {@link File} or {@link RandomAccessFile} can be used as input.
* *
* @see javax.imageio.stream.FileImageInputStream * @see javax.imageio.stream.FileImageInputStream
* @deprecated Use {@link BufferedChannelImageInputStream} instead.
*/ */
// TODO: Create a memory-mapped version? // TODO: Create a memory-mapped version?
// Or not... From java.nio.channels.FileChannel.map: // 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 // the usual {@link #read read} and {@link #write write} methods. From the
// standpoint of performance it is generally only worth mapping relatively // standpoint of performance it is generally only worth mapping relatively
// large files into memory. // large files into memory.
@Deprecated
public final class BufferedFileImageInputStream extends ImageInputStreamImpl { public final class BufferedFileImageInputStream extends ImageInputStreamImpl {
static final int DEFAULT_BUFFER_SIZE = 8192; static final int DEFAULT_BUFFER_SIZE = 8192;
@@ -192,10 +190,10 @@ public final class BufferedFileImageInputStream extends ImageInputStreamImpl {
public void close() throws IOException { public void close() throws IOException {
super.close(); super.close();
buffer = null;
raf.close(); raf.close();
raf = null; raf = null;
buffer = null;
} }
// Need to override the readShort(), readInt() and readLong() methods, // Need to override the readShort(), readInt() and readLong() methods,
@@ -37,19 +37,18 @@ import javax.imageio.spi.ServiceRegistry;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.util.Iterator; import java.util.Iterator;
import java.util.Locale; import java.util.Locale;
/** /**
* BufferedFileImageInputStreamSpi * BufferedFileImageInputStreamSpi
* Experimental
* *
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a> * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$ * @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() { public BufferedFileImageInputStreamSpi() {
this(new StreamProviderInfo()); this(new StreamProviderInfo());
} }
@@ -70,13 +69,12 @@ public final class BufferedFileImageInputStreamSpi extends ImageInputStreamSpi {
} }
} }
@Override public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) {
public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) throws IOException {
if (input instanceof File) { if (input instanceof File) {
try { 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, // For consistency with the JRE bundled SPIs, we'll return null here,
// even though the spec does not say that's allowed. // 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, // 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; return false;
} }
@Override public String getDescription(final Locale pLocale) {
public String getDescription(final Locale locale) {
return "Service provider that instantiates an ImageInputStream from a File"; 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$ * @author last modified by $Author: haraldk$
* @version $Id: BufferedRAFImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$ * @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() { public BufferedRAFImageInputStreamSpi() {
this(new StreamProviderInfo()); this(new StreamProviderInfo());
} }
@@ -69,10 +69,9 @@ public final class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
} }
} }
@Override public ImageInputStream createInputStreamInstance(final Object input, final boolean pUseCache, final File pCacheDir) {
public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) {
if (input instanceof RandomAccessFile) { if (input instanceof RandomAccessFile) {
return new BufferedChannelImageInputStream((RandomAccessFile) input); return new BufferedFileImageInputStream((RandomAccessFile) input);
} }
throw new IllegalArgumentException("Expected input of type RandomAccessFile: " + input); throw new IllegalArgumentException("Expected input of type RandomAccessFile: " + input);
@@ -83,8 +82,7 @@ public final class BufferedRAFImageInputStreamSpi extends ImageInputStreamSpi {
return false; return false;
} }
@Override public String getDescription(final Locale pLocale) {
public String getDescription(final Locale locale) {
return "Service provider that instantiates an ImageInputStream from a RandomAccessFile"; 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 dataOffset;
private final int dataLength; private final int dataLength;
public ByteArrayImageInputStream(final byte[] data) { public ByteArrayImageInputStream(final byte[] pData) {
this(data, 0, data != null ? data.length : -1); this(pData, 0, pData != null ? pData.length : -1);
} }
public ByteArrayImageInputStream(final byte[] data, int offset, int length) { public ByteArrayImageInputStream(final byte[] pData, int offset, int length) {
this.data = notNull(data, "data"); data = notNull(pData, "data");
dataOffset = isMax(data.length, offset, "offset"); dataOffset = isBetween(0, pData.length, offset, "offset");
dataLength = isMax(data.length - offset, length, "length"); dataLength = isBetween(0, pData.length - offset, length, "length");
} }
private static int isMax(final int high, final int value, final String name) { private static int isBetween(final int low, 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)); 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 { public int read() throws IOException {
@@ -72,14 +72,14 @@ public final class ByteArrayImageInputStream extends ImageInputStreamImpl {
return data[((int) streamPos++) + dataOffset] & 0xff; 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) { if (streamPos >= dataLength) {
return -1; return -1;
} }
int length = (int) Math.min(dataLength - streamPos, len); int length = (int) Math.min(this.dataLength - streamPos, pLength);
bitOffset = 0; bitOffset = 0;
System.arraycopy(data, (int) streamPos + dataOffset, buffer, offset, length); System.arraycopy(data, (int) streamPos + dataOffset, pBuffer, pOffset, length);
streamPos += length; streamPos += length;
return length; return length;
@@ -45,7 +45,7 @@ import java.util.Locale;
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: ByteArrayImageInputStreamSpi.java,v 1.0 May 15, 2008 2:12:12 PM haraldk Exp$ * @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() { public ByteArrayImageInputStreamSpi() {
this(new StreamProviderInfo()); this(new StreamProviderInfo());
@@ -55,17 +55,16 @@ public final class ByteArrayImageInputStreamSpi extends ImageInputStreamSpi {
super(providerInfo.getVendorName(), providerInfo.getVersion(), byte[].class); super(providerInfo.getVendorName(), providerInfo.getVersion(), byte[].class);
} }
@Override public ImageInputStream createInputStreamInstance(Object pInput, boolean pUseCache, File pCacheDir) {
public ImageInputStream createInputStreamInstance(Object input, boolean useCacheFile, File cacheDir) { if (pInput instanceof byte[]) {
if (input instanceof byte[]) { return new ByteArrayImageInputStream((byte[]) pInput);
return new ByteArrayImageInputStream((byte[]) input); }
else {
throw new IllegalArgumentException("Expected input of type byte[]: " + pInput);
} }
throw new IllegalArgumentException("Expected input of type byte[]: " + input);
} }
@Override public String getDescription(Locale pLocale) {
public String getDescription(Locale locale) {
return "Service provider that instantiates an ImageInputStream from a byte array"; 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 @Override
public void close() throws IOException { 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(); stream.close();
super.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 com.twelvemonkeys.imageio.spi.ProviderInfo;
import javax.imageio.spi.ImageInputStreamSpi; import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.stream.FileCacheImageInputStream;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; 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$ * @version $Id: URLImageInputStreamSpi.java,v 1.0 May 15, 2008 2:14:59 PM haraldk Exp$
*/ */
// TODO: URI instead of URL? // TODO: URI instead of URL?
public final class URLImageInputStreamSpi extends ImageInputStreamSpi { public class URLImageInputStreamSpi extends ImageInputStreamSpi {
public URLImageInputStreamSpi() { public URLImageInputStreamSpi() {
this(new StreamProviderInfo()); 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 // 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 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.. // The bad thing is that most people don't expect there to be an UR[I|L]ImageInputStreamSpi..
@Override public ImageInputStream createInputStreamInstance(final Object pInput, final boolean pUseCache, final File pCacheDir) throws IOException {
public ImageInputStream createInputStreamInstance(final Object input, final boolean useCacheFile, final File cacheDir) throws IOException { if (pInput instanceof URL) {
if (input instanceof URL) { URL url = (URL) pInput;
URL url = (URL) input;
// Special case for file protocol, a lot faster than FileCacheImageInputStream // Special case for file protocol, a lot faster than FileCacheImageInputStream
if ("file".equals(url.getProtocol())) { if ("file".equals(url.getProtocol())) {
try { try {
return new BufferedChannelImageInputStream(new File(url.toURI())); return new BufferedFileImageInputStream(new File(url.toURI()));
} }
catch (URISyntaxException shouldNeverHappen) { catch (URISyntaxException ignore) {
// This should never happen, but if it does, we'll fall back to using the stream // This should never happen, but if it does, we'll fall back to using the stream
shouldNeverHappen.printStackTrace(); ignore.printStackTrace();
} }
} }
// Otherwise revert to cached // Otherwise revert to cached
InputStream urlStream = url.openStream(); final InputStream urlStream = url.openStream();
return new BufferedChannelImageInputStream(useCacheFile ? new FileCache(urlStream, cacheDir) : new MemoryCache(urlStream)); 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 @Override
@@ -91,7 +118,7 @@ public final class URLImageInputStreamSpi extends ImageInputStreamSpi {
return true; return true;
} }
public String getDescription(final Locale locale) { public String getDescription(final Locale pLocale) {
return "Service provider that instantiates an ImageInputStream from a URL"; return "Service provider that instantiates an ImageInputStream from a URL";
} }
} }
@@ -30,15 +30,22 @@
package com.twelvemonkeys.imageio.util; 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.isTrue;
import static com.twelvemonkeys.lang.Validate.notNull; 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. * Factory class for creating {@code ImageTypeSpecifier}s.
* Fixes some subtle bugs in {@code ImageTypeSpecifier}'s factory methods, but * 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 { 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() {} private ImageTypeSpecifiers() {}
public static ImageTypeSpecifier createFromBufferedImageType(final int bufferedImageType) { public static ImageTypeSpecifier createFromBufferedImageType(final int bufferedImageType) {
switch (bufferedImageType) { switch (bufferedImageType) {
// ImageTypeSpecifier unconditionally uses bits == 32, we'll use a workaround for the INT_RGB and USHORT types // ImageTypeSpecifier unconditionally uses bits == 32, we'll use a workaround for the USHORT types
case BufferedImage.TYPE_INT_RGB:
return TYPE_INT_RGB;
case BufferedImage.TYPE_INT_BGR:
return TYPE_INT_BGR;
case BufferedImage.TYPE_USHORT_565_RGB: 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: 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: default:
} }
@@ -107,41 +90,23 @@ public final class ImageTypeSpecifiers {
final int redMask, final int greenMask, final int redMask, final int greenMask,
final int blueMask, final int alphaMask, final int blueMask, final int alphaMask,
final int transferType, boolean isAlphaPremultiplied) { final int transferType, boolean isAlphaPremultiplied) {
int bits = calculateRequiredBits(redMask | greenMask | blueMask | alphaMask); if (transferType == DataBuffer.TYPE_BYTE || transferType == DataBuffer.TYPE_USHORT) {
if (bits != 32) {
// ImageTypeSpecifier unconditionally uses bits == 32, we'll use a workaround for BYTE/USHORT types // 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); 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, public static ImageTypeSpecifier createInterleaved(final ColorSpace colorSpace,
final int[] bandOffsets, final int[] bandOffsets,
final int dataType, final int dataType,
@@ -270,20 +235,4 @@ public final class ImageTypeSpecifiers {
ColorModel colorModel = new DiscreteAlphaIndexColorModel(pColorModel, extraSamples, hasAlpha); ColorModel colorModel = new DiscreteAlphaIndexColorModel(pColorModel, extraSamples, hasAlpha);
return new ImageTypeSpecifier(colorModel, colorModel.createCompatibleSampleModel(1, 1)); 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.BufferedFileImageInputStreamSpi
com.twelvemonkeys.imageio.stream.BufferedRAFImageInputStreamSpi com.twelvemonkeys.imageio.stream.BufferedRAFImageInputStreamSpi
com.twelvemonkeys.imageio.stream.BufferedInputStreamImageInputStreamSpi
# Use SPI loading as a hook for early profile activation # Use SPI loading as a hook for early profile activation
com.twelvemonkeys.imageio.color.ProfileDeferralActivator$Spi 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 com.twelvemonkeys.imageio.stream.BufferedImageInputStreamTest.rangeEquals;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
/** /**
* BufferedFileImageInputStreamTestCase * BufferedFileImageInputStreamTestCase
@@ -56,7 +54,6 @@ import static org.mockito.Mockito.verify;
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$ * @version $Id: BufferedFileImageInputStreamTestCase.java,v 1.0 Apr 21, 2009 10:58:48 AM haraldk Exp$
*/ */
@Deprecated
public class BufferedFileImageInputStreamTest { public class BufferedFileImageInputStreamTest {
private final Random random = new Random(170984354357234566L); private final Random random = new Random(170984354357234566L);
@@ -75,7 +72,6 @@ public class BufferedFileImageInputStreamTest {
} }
} }
@SuppressWarnings("resource")
@Test @Test
public void testCreateNullFile() throws IOException { public void testCreateNullFile() throws IOException {
try { 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; 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 org.junit.Test;
import javax.imageio.ImageTypeSpecifier; import com.twelvemonkeys.lang.Validate;
import java.awt.color.*;
import java.awt.image.*;
import static org.junit.Assert.assertEquals;
public class ImageTypeSpecifiersTest { public class ImageTypeSpecifiersTest {
@@ -65,19 +70,12 @@ public class ImageTypeSpecifiersTest {
ImageTypeSpecifier expected; ImageTypeSpecifier expected;
switch (type) { 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) // 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: 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; break;
case BufferedImage.TYPE_USHORT_555_RGB: 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; break;
default: default:
expected = ImageTypeSpecifier.createFromBufferedImageType(type); expected = ImageTypeSpecifier.createFromBufferedImageType(type);
@@ -88,24 +86,12 @@ public class ImageTypeSpecifiersTest {
} }
@Test @Test
public void testCreatePacked24() { public void testCreatePacked32() {
// TYPE_INT_RGB // TYPE_INT_RGB
assertEquals( 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) 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 // TYPE_INT_ARGB
assertEquals( assertEquals(
ImageTypeSpecifier.createPacked(sRGB, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, DCM_ALPHA_MASK, DataBuffer.TYPE_INT, false), 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), 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) ImageTypeSpecifiers.createPacked(sRGB, DCM_RED_MASK, DCM_GREEN_MASK, DCM_BLUE_MASK, DCM_ALPHA_MASK, DataBuffer.TYPE_INT, true)
); );
} // TYPE_INT_BGR
@Test
public void testCreatePacked15() {
// TYPE_USHORT_555_RGB
assertEquals( assertEquals(
createPacked(sRGB, 15, 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_555_RED_MASK, DCM_555_GRN_MASK, DCM_555_BLU_MASK, 0, DataBuffer.TYPE_USHORT, 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 @Test
public void testCreatePacked16() { 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 // TYPE_USHORT_565_RGB
assertEquals( 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) ImageTypeSpecifiers.createPacked(sRGB, DCM_565_RED_MASK, DCM_565_GRN_MASK, DCM_565_BLU_MASK, 0, DataBuffer.TYPE_USHORT, false)
); );
// "USHORT 4444 ARGB" // "USHORT 4444 ARGB"
assertEquals( 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) ImageTypeSpecifiers.createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, false)
); );
// "USHORT 4444 ARGB PRE" // "USHORT 4444 ARGB PRE"
assertEquals( 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) ImageTypeSpecifiers.createPacked(sRGB, 0xf00, 0xf0, 0xf, 0xf000, DataBuffer.TYPE_USHORT, true)
); );
@@ -157,17 +142,17 @@ public class ImageTypeSpecifiersTest {
public void testCreatePacked8() { public void testCreatePacked8() {
// "BYTE 332 RGB" // "BYTE 332 RGB"
assertEquals( 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) ImageTypeSpecifiers.createPacked(sRGB, 0xe0, 0x1c, 0x3, 0x0, DataBuffer.TYPE_BYTE, false)
); );
// "BYTE 2222 ARGB" // "BYTE 2222 ARGB"
assertEquals( 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) ImageTypeSpecifiers.createPacked(sRGB, 0xc0, 0x30, 0x0c, 0x03, DataBuffer.TYPE_BYTE, false)
); );
// "BYTE 2222 ARGB PRE" // "BYTE 2222 ARGB PRE"
assertEquals( 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) 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()); 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 redMask, final int greenMask, final int blueMask, final int alphaMask,
final int transferType, final boolean isAlphaPremultiplied) { 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)); 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) { private static byte[] createByteLut(final int count) {
byte[] lut = new byte[count]; byte[] lut = new byte[count];
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-hdr</artifactId> <artifactId>imageio-hdr</artifactId>
<name>TwelveMonkeys :: ImageIO :: HDR plugin</name> <name>TwelveMonkeys :: ImageIO :: HDR plugin</name>
@@ -40,8 +40,11 @@ import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import java.awt.*; import java.awt.*;
import java.awt.color.*; import java.awt.color.ColorSpace;
import java.awt.image.*; 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.File;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
@@ -241,7 +244,10 @@ public final class HDRImageReader extends ImageReaderBase {
@Override @Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException { 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 { 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; 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; import javax.imageio.metadata.IIOMetadataNode;
public class HDRMetadata extends StandardImageMetadataSupport { final class HDRMetadata extends AbstractMetadata {
public HDRMetadata(ImageTypeSpecifier type, HDRHeader header) { private final HDRHeader header;
super(builder(type)
.withCompressionTypeName("RLE") HDRMetadata(final HDRHeader header) {
.withTextEntry("Software", header.getSoftware())); 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 @Override
protected IIOMetadataNode getStandardDataNode() { protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode node = new IIOMetadataNode("Data"); IIOMetadataNode node = new IIOMetadataNode("Data");
@@ -28,4 +92,38 @@ public class HDRMetadata extends StandardImageMetadataSupport {
return node; 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> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-icns</artifactId> <artifactId>imageio-icns</artifactId>
<name>TwelveMonkeys :: ImageIO :: ICNS plugin</name> <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.IIOUtil;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import javax.imageio.IIOException; import javax.imageio.*;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.*; import java.awt.color.ColorSpace;
import java.awt.image.*; import java.awt.image.*;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.File; 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> * @see <a href="http://en.wikipedia.org/wiki/Apple_Icon_Image_format">Apple Icon Image format (Wikipedia)</a>
*/ */
public final class ICNSImageReader extends ImageReaderBase { 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? // TODO: Subsampled reading for completeness, even if never used?
private final List<IconResource> icons = new ArrayList<>(); private List<IconResource> icons = new ArrayList<IconResource>();
private final List<IconResource> masks = new ArrayList<>(); private List<IconResource> masks = new ArrayList<IconResource>();
private IconResource lastResourceRead; private IconResource lastResourceRead;
private int length; private int length;
@@ -140,7 +136,7 @@ public final class ICNSImageReader extends ImageReaderBase {
ImageTypeSpecifier rawType = getRawImageType(imageIndex); ImageTypeSpecifier rawType = getRawImageType(imageIndex);
IconResource resource = readIconResource(imageIndex); IconResource resource = readIconResource(imageIndex);
List<ImageTypeSpecifier> specifiers = new ArrayList<>(); List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>();
switch (resource.depth()) { switch (resource.depth()) {
case 1: case 1:
@@ -234,9 +230,14 @@ public final class ICNSImageReader extends ImageReaderBase {
packedSize -= 4; 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 ICNSUtil.decompress(new DataInputStream(input), data, 0, (data.length * 24) / 32); // 24 bit data
} }
finally {
input.close();
}
} }
else { else {
data = new byte[resource.length - ICNS.RESOURCE_HEADER_SIZE]; data = new byte[resource.length - ICNS.RESOURCE_HEADER_SIZE];
@@ -490,7 +491,7 @@ public final class ICNSImageReader extends ImageReaderBase {
String format; 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"; format = "PNG";
} }
else if (Arrays.equals(ICNS.JPEG_2000_MAGIC, magic)) { else if (Arrays.equals(ICNS.JPEG_2000_MAGIC, magic)) {
@@ -526,6 +527,7 @@ public final class ICNSImageReader extends ImageReaderBase {
IconResource resource = IconResource.read(imageInput); IconResource resource = IconResource.read(imageInput);
if (resource.isTOC()) { if (resource.isTOC()) {
// TODO: IconResource.readTOC()?
int resourceCount = (resource.length - ICNS.RESOURCE_HEADER_SIZE) / ICNS.RESOURCE_HEADER_SIZE; int resourceCount = (resource.length - ICNS.RESOURCE_HEADER_SIZE) / ICNS.RESOURCE_HEADER_SIZE;
long pos = resource.start + resource.length; 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 { private static final class ICNSBitMaskColorModel extends IndexColorModel {
static final IndexColorModel INSTANCE = new ICNSBitMaskColorModel(); 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 { public static void main(String[] args) throws IOException {
int argIndex = 0; int argIndex = 0;
@@ -34,13 +34,7 @@ import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.stream.SubImageOutputStream; import com.twelvemonkeys.imageio.stream.SubImageOutputStream;
import com.twelvemonkeys.imageio.util.ProgressListenerBase; import com.twelvemonkeys.imageio.util.ProgressListenerBase;
import javax.imageio.IIOException; import javax.imageio.*;
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.event.IIOWriteWarningListener; import javax.imageio.event.IIOWriteWarningListener;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi; import javax.imageio.spi.ImageWriterSpi;
@@ -110,7 +104,6 @@ public final class ICNSImageWriter extends ImageWriterBase {
sequenceIndex = 0; sequenceIndex = 0;
} }
@SuppressWarnings("RedundantThrows")
@Override @Override
public void endWriteSequence() throws IOException { public void endWriteSequence() throws IOException {
assertOutput(); assertOutput();
@@ -38,13 +38,8 @@ import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam; import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader; import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.image.*; import java.awt.image.BufferedImage;
import java.io.BufferedReader; import java.io.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Iterator; import java.util.Iterator;
/** /**
@@ -145,12 +140,17 @@ final class SipsJP2Reader {
} }
private static String checkErrorMessage(final Process process) throws IOException { 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)); BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
String message = reader.readLine(); String message = reader.readLine();
return message != null && message.startsWith("Error: ") ? message.substring(7) : null; return message != null && message.startsWith("Error: ") ? message.substring(7) : null;
} }
finally {
stream.close();
}
} }
private static String[] buildCommand(final File sipsCommand, final File tempFile) { 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 { private static File dumpToFile(final ImageInputStream stream) throws IOException {
File tempFile = File.createTempFile("imageio-icns-", ".png"); File tempFile = File.createTempFile("imageio-icns-", ".png");
tempFile.deleteOnExit(); tempFile.deleteOnExit();
try (FileOutputStream out = new FileOutputStream(tempFile)) { FileOutputStream out = new FileOutputStream(tempFile);
try {
FileUtil.copy(IIOUtil.createStreamAdapter(stream), out); FileUtil.copy(IIOUtil.createStreamAdapter(stream), out);
} }
finally {
out.close();
}
return tempFile; return tempFile;
} }
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-iff</artifactId> <artifactId>imageio-iff</artifactId>
<name>TwelveMonkeys :: ImageIO :: IFF plugin</name> <name>TwelveMonkeys :: ImageIO :: IFF plugin</name>
@@ -1,13 +1,13 @@
package com.twelvemonkeys.imageio.plugins.iff; package com.twelvemonkeys.imageio.plugins.iff;
import javax.imageio.IIOException; 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.ArrayList;
import java.util.List; 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.imageio.plugins.iff.IFFUtil.toChunkStr;
import static com.twelvemonkeys.lang.Validate.isTrue;
/** /**
* Form. * Form.
@@ -27,7 +27,7 @@ abstract class Form {
abstract int width(); abstract int width();
abstract int height(); abstract int height();
abstract double aspect(); abstract float aspect();
abstract int bitplanes(); abstract int bitplanes();
abstract int compressionType(); 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) { 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.bitmapHeader = bitmapHeader;
this.viewMode = viewMode; this.viewMode = viewMode;
this.colorMap = colorMap; this.colorMap = colorMap;
@@ -127,19 +127,6 @@ abstract class Form {
this.body = body; 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 @Override
int width() { int width() {
return bitmapHeader.width; return bitmapHeader.width;
@@ -161,8 +148,8 @@ abstract class Form {
} }
@Override @Override
double aspect() { float aspect() {
return bitmapHeader.yAspect == 0 ? 0 : (bitmapHeader.xAspect / (double) bitmapHeader.yAspect); return bitmapHeader.yAspect == 0 ? 0 : (bitmapHeader.xAspect / (float) bitmapHeader.yAspect);
} }
@Override @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) { 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.deepGlobal = deepGlobal;
this.deepLocation = deepLocation; this.deepLocation = deepLocation;
this.deepPixel = deepPixel; this.deepPixel = deepPixel;
@@ -318,15 +305,6 @@ abstract class Form {
this.body = body; this.body = body;
} }
private static boolean validFormType(int formType) {
switch (formType) {
case TYPE_DEEP:
case TYPE_TVPP:
return true;
default:
return false;
}
}
@Override @Override
int width() { int width() {
@@ -359,8 +337,8 @@ abstract class Form {
} }
@Override @Override
double aspect() { float aspect() {
return deepGlobal.yAspect == 0 ? 0 : deepGlobal.xAspect / (double) deepGlobal.yAspect; return deepGlobal.yAspect == 0 ? 0 : deepGlobal.xAspect / (float) deepGlobal.yAspect;
} }
@Override @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; package com.twelvemonkeys.imageio.plugins.iff;
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.IndexColorModel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import static com.twelvemonkeys.imageio.plugins.iff.IFF.*; 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 com.twelvemonkeys.lang.Validate.notNull;
import static java.util.Collections.emptyList;
final class IFFImageMetadata extends StandardImageMetadataSupport { final class IFFImageMetadata extends AbstractMetadata {
IFFImageMetadata(ImageTypeSpecifier type, Form header, IndexColorModel palette) { private final Form header;
this(builder(type), notNull(header, "header"), palette); 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) { private boolean validFormType(int formType) {
super(builder.withPalette(palette) switch (formType) {
.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";
}
default: 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) { @Override
int bitplanes = header.bitplanes(); 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) { switch (bitplanes) {
case 1: case 1:
case 2: case 2:
@@ -87,47 +192,91 @@ final class IFFImageMetadata extends StandardImageMetadataSupport {
case 6: case 6:
case 7: case 7:
case 8: case 8:
return new int[] {bitplanes}; return Integer.toString(bitplanes);
case 24: case 24:
return new int[] {8, 8, 8}; return "8 8 8";
case 25: case 25:
if (header.formType != TYPE_RGB8) { if (header.formType != TYPE_RGB8) {
throw new IllegalArgumentException(String.format("25 bit depth only supported for FORM type RGB8: %s", IFFUtil.toChunkStr(header.formType))); 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: case 32:
return new int[] {8, 8, 8, 8}; return "8 8 8 8";
default: default:
throw new IllegalArgumentException("Unknown bit count: " + bitplanes); throw new IllegalArgumentException("Unknown bit count: " + bitplanes);
} }
} }
private static PlanarConfiguration planarConfiguration(Form header) { @Override
switch (header.formType) { protected IIOMetadataNode getStandardDimensionNode() {
case TYPE_DEEP: if (header.aspect() == 0) {
case TYPE_TVPP: return null;
case TYPE_RGB8:
case TYPE_PBM:
return PlanarConfiguration.PixelInterleaved;
case TYPE_ILBM:
return PlanarConfiguration.PlaneInterleaved;
default:
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) { @Override
if (header.meta.isEmpty()) { protected IIOMetadataNode getStandardDocumentNode() {
return emptyList(); 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<>(); IIOMetadataNode text = new IIOMetadataNode("Text");
for (GenericChunk chunk : header.meta) {
text.add(new TextEntry(toChunkStr(chunk.chunkId), // /Text/TextEntry@keyword = field name, /Text/TextEntry@value = field value.
new String(chunk.data, chunk.chunkId == IFF.CHUNK_UTF8 ? StandardCharsets.UTF_8:StandardCharsets.US_ASCII))); 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; 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.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder; import com.twelvemonkeys.io.enc.PackBitsDecoder;
import javax.imageio.IIOException; import javax.imageio.*;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.*; import java.awt.color.ColorSpace;
import java.awt.image.*; import java.awt.image.*;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
@@ -355,7 +350,9 @@ public final class IFFImageReader extends ImageReaderBase {
@Override @Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException { public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
return new IFFImageMetadata(getRawImageType(imageIndex), header, header.colorMap()); init(imageIndex);
return new IFFImageMetadata(header, header.colorMap());
} }
@Override @Override
@@ -1,37 +1,24 @@
package com.twelvemonkeys.imageio.plugins.iff; package com.twelvemonkeys.imageio.plugins.iff;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import org.junit.Test; import org.junit.Test;
import org.junit.function.ThrowingRunnable; import org.junit.function.ThrowingRunnable;
import org.w3c.dom.Node; import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl; import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.*; import java.awt.image.IndexColorModel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import static java.awt.image.BufferedImage.*;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class IFFImageMetadataTest { 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 @Test
public void testStandardFeatures() throws IIOException { public void testStandardFeatures() throws IIOException {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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 // Standard metadata format
assertTrue(metadata.isStandardMetadataFormatSupported()); assertTrue(metadata.isStandardMetadataFormatSupported());
@@ -64,9 +51,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName()); assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength()); assertEquals(3, chroma.getLength());
@@ -91,9 +78,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName()); assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength()); 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)); .with(new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1));
byte[] bw = {0, (byte) 0xff}; byte[] bw = {0, (byte) 0xff};
ImageTypeSpecifier fromIndexColorModel = ImageTypeSpecifiers.createFromIndexColorModel(new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex())); IFFImageMetadata metadata = new IFFImageMetadata(header, new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex()));
IFFImageMetadata metadata = new IFFImageMetadata(fromIndexColorModel, header, header.colorMap());
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma"); IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma); assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName()); assertEquals("Chroma", chroma.getNodeName());
assertEquals(5, chroma.getLength()); assertEquals(5, chroma.getLength());
@@ -133,7 +119,7 @@ public class IFFImageMetadataTest {
IIOMetadataNode numChannels = (IIOMetadataNode) colorSpaceType.getNextSibling(); IIOMetadataNode numChannels = (IIOMetadataNode) colorSpaceType.getNextSibling();
assertEquals("NumChannels", numChannels.getNodeName()); assertEquals("NumChannels", numChannels.getNodeName());
assertEquals("4", numChannels.getAttribute("value")); assertEquals("3", numChannels.getAttribute("value"));
IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling(); IIOMetadataNode blackIsZero = (IIOMetadataNode) numChannels.getNextSibling();
assertEquals("BlackIsZero", blackIsZero.getNodeName()); assertEquals("BlackIsZero", blackIsZero.getNodeName());
@@ -167,9 +153,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(compression);
assertEquals("Compression", compression.getNodeName()); assertEquals("Compression", compression.getNodeName());
assertEquals(2, compression.getLength()); assertEquals(2, compression.getLength());
@@ -190,9 +176,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_NONE, 0)); .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 @Test
@@ -200,9 +186,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -227,9 +213,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -254,9 +240,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 32, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); 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)); .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 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(header, new IndexColorModel(header.bitplanes(), rgb.length, rgb, rgb, rgb, 0));
IFFImageMetadata metadata = new IFFImageMetadata(fromIndexColorModel, header, header.colorMap());
IIOMetadataNode data = getStandardNode(metadata, "Data"); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -312,9 +297,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_PBM) Form header = Form.ofType(IFF.TYPE_PBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -339,9 +324,9 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_PBM) Form header = Form.ofType(IFF.TYPE_PBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -371,20 +356,10 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(bitmapHeader); .with(bitmapHeader);
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();
assertNull(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
}
} }
@Test @Test
@@ -393,24 +368,20 @@ public class IFFImageMetadataTest {
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)) .with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0))
.with(new CAMGChunk(4)); .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 // No Dimension node is okay, or one with an aspect ratio of 1.0
if (dimension != null) { if (dimension != null) {
assertEquals("Dimension", dimension.getNodeName()); assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength()); assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild(); IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value")); assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling(); assertNull(pixelAspectRatio.getNextSibling()); // No more children
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
} }
} }
@@ -427,22 +398,18 @@ public class IFFImageMetadataTest {
.with(bitmapHeader) .with(bitmapHeader)
.with(viewPort); .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); assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName()); assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength()); assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild(); IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("2.0", pixelAspectRatio.getAttribute("value")); assertEquals("2.0", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling(); assertNull(pixelAspectRatio.getNextSibling()); // No more children
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
} }
@Test @Test
@@ -458,22 +425,18 @@ public class IFFImageMetadataTest {
.with(bitmapHeader) .with(bitmapHeader)
.with(viewPort); .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); assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName()); assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength()); assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild(); IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("0.5", pixelAspectRatio.getAttribute("value")); assertEquals("0.5", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling(); assertNull(pixelAspectRatio.getNextSibling()); // No more children
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
} }
@Test @Test
@@ -484,22 +447,18 @@ public class IFFImageMetadataTest {
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)) .with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0))
.with(viewPort); .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); assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName()); assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength()); assertEquals(1, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild(); IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName()); assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value")); assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling(); assertNull(pixelAspectRatio.getNextSibling()); // No more children
assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value"));
assertNull(imageOrientation.getNextSibling()); // No more children
} }
@Test @Test
@@ -507,33 +466,32 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(document);
assertEquals("Document", document.getNodeName()); assertEquals("Document", document.getNodeName());
assertEquals(1, document.getLength()); assertEquals(1, document.getLength());
IIOMetadataNode formatVersion = (IIOMetadataNode) document.getFirstChild(); IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) document.getFirstChild();
assertEquals("FormatVersion", formatVersion.getNodeName()); assertEquals("FormatVersion", pixelAspectRatio.getNodeName());
assertEquals("1.0", formatVersion.getAttribute("value")); assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
assertNull(formatVersion.getNextSibling()); // No more children assertNull(pixelAspectRatio.getNextSibling()); // No more children
} }
@Test @Test
public void testStandardText() throws IIOException { public void testStandardText() throws IIOException {
int[] chunks = {IFF.CHUNK_ANNO, IFF.CHUNK_ANNO, IFF.CHUNK_UTF8}; int[] chunks = {IFF.CHUNK_ANNO, IFF.CHUNK_UTF8};
String[] texts = {"annotation", "dupe", "äñnótâtïøñ"}; String[] texts = {"annotation", "äñnótâtïøñ"};
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 8, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)) .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[0], texts[0].getBytes(StandardCharsets.US_ASCII)))
.with(new GenericChunk(chunks[1], texts[1].getBytes(StandardCharsets.US_ASCII))) .with(new GenericChunk(chunks[1], texts[1].getBytes(StandardCharsets.UTF_8)));
.with(new GenericChunk(chunks[2], texts[2].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); assertNotNull(text);
assertEquals("Text", text.getNodeName()); assertEquals("Text", text.getNodeName());
assertEquals(texts.length, text.getLength()); assertEquals(texts.length, text.getLength());
@@ -551,21 +509,10 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 24, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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"); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNull(transparency); // No transparency, just defaults
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
} }
@Test @Test
@@ -573,18 +520,18 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_ILBM) Form header = Form.ofType(IFF.TYPE_ILBM)
.with(new BMHDChunk(300, 200, 32, BMHDChunk.MASK_HAS_MASK, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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); assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName()); assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength()); assertEquals(1, transparency.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild(); IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("Alpha", alpha.getNodeName()); assertEquals("Alpha", pixelAspectRatio.getNodeName());
assertEquals("nonpremultiplied", alpha.getAttribute("value")); assertEquals("nonpremultiplied", pixelAspectRatio.getAttribute("value"));
assertNull(alpha.getNextSibling()); // No more children assertNull(pixelAspectRatio.getNextSibling()); // No more children
} }
@Test @Test
@@ -593,33 +540,28 @@ public class IFFImageMetadataTest {
.with(new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1)); .with(new BMHDChunk(300, 200, 1, BMHDChunk.MASK_TRANSPARENT_COLOR, BMHDChunk.COMPRESSION_BYTE_RUN, 1));
byte[] bw = {0, (byte) 0xff}; byte[] bw = {0, (byte) 0xff};
ImageTypeSpecifier fromIndexColorModel = ImageTypeSpecifiers.createFromIndexColorModel(new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex())); IFFImageMetadata metadata = new IFFImageMetadata(header, new IndexColorModel(header.bitplanes(), bw.length, bw, bw, bw, header.transparentIndex()));
IFFImageMetadata metadata = new IFFImageMetadata(fromIndexColorModel, header, header.colorMap());
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency"); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency); assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName()); assertEquals("Transparency", transparency.getNodeName());
assertEquals(2, transparency.getLength()); assertEquals(1, transparency.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild(); IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("Alpha", alpha.getNodeName()); assertEquals("TransparentIndex", pixelAspectRatio.getNodeName());
assertEquals("nonpremultiplied", alpha.getAttribute("value")); assertEquals("1", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode transparentIndex = (IIOMetadataNode) alpha.getNextSibling(); assertNull(pixelAspectRatio.getNextSibling()); // No more children
assertEquals("TransparentIndex", transparentIndex.getNodeName());
assertEquals("1", transparentIndex.getAttribute("value"));
assertNull(transparentIndex.getNextSibling()); // No more children
} }
@Test @Test
public void testStandardRGB8() throws IIOException { public void testStandardRGB8() throws IIOException {
Form header = Form.ofType(IFF.TYPE_RGB8) Form header = Form.ofType(IFF.TYPE_RGB8)
.with(new BMHDChunk(300, 200, 25, BMHDChunk.MASK_NONE, BMHDChunk.COMPRESSION_BYTE_RUN, 0)); .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 // Chroma
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma"); IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma); assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName()); assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength()); assertEquals(3, chroma.getLength());
@@ -639,7 +581,7 @@ public class IFFImageMetadataTest {
assertNull(blackIsZero.getNextSibling()); // No more children assertNull(blackIsZero.getNextSibling()); // No more children
// Data // Data
IIOMetadataNode data = getStandardNode(metadata, "Data"); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -659,7 +601,7 @@ public class IFFImageMetadataTest {
assertNull(bitsPerSample.getNextSibling()); // No more children assertNull(bitsPerSample.getNextSibling()); // No more children
// Transparency // Transparency
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency"); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency); assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName()); assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength()); assertEquals(1, transparency.getLength());
@@ -682,10 +624,10 @@ public class IFFImageMetadataTest {
Form header = Form.ofType(IFF.TYPE_DEEP) Form header = Form.ofType(IFF.TYPE_DEEP)
.with(new DGBLChunk(8)) .with(new DGBLChunk(8))
.with(dpel); .with(dpel);
IFFImageMetadata metadata = new IFFImageMetadata(TYPE_32_BIT_ARGB_DEEP, header, header.colorMap()); IFFImageMetadata metadata = new IFFImageMetadata(header, null);
// Chroma // Chroma
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma"); IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma); assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName()); assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength()); assertEquals(3, chroma.getLength());
@@ -707,7 +649,7 @@ public class IFFImageMetadataTest {
assertNull(blackIsZero.getNextSibling()); // No more children assertNull(blackIsZero.getNextSibling()); // No more children
// Data // Data
IIOMetadataNode data = getStandardNode(metadata, "Data"); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -727,7 +669,7 @@ public class IFFImageMetadataTest {
assertNull(bitsPerSample.getNextSibling()); // No more children assertNull(bitsPerSample.getNextSibling()); // No more children
// Transparency // Transparency
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency"); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency); assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName()); assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength()); assertEquals(1, transparency.getLength());
@@ -738,13 +680,4 @@ public class IFFImageMetadataTest {
assertNull(alpha.getNextSibling()); // No more children 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> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-jpeg-jai-interop</artifactId> <artifactId>imageio-jpeg-jai-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: JPEG/JAI TIFF Interop</name> <name>TwelveMonkeys :: ImageIO :: JPEG/JAI TIFF Interop</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-jpeg-jep262-interop</artifactId> <artifactId>imageio-jpeg-jep262-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: JPEG/JEP-262 Interop</name> <name>TwelveMonkeys :: ImageIO :: JPEG/JEP-262 Interop</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-jpeg</artifactId> <artifactId>imageio-jpeg</artifactId>
<name>TwelveMonkeys :: ImageIO :: JPEG plugin</name> <name>TwelveMonkeys :: ImageIO :: JPEG plugin</name>
+1 -1
View File
@@ -3,7 +3,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>imageio-metadata</artifactId> <artifactId>imageio-metadata</artifactId>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-pcx</artifactId> <artifactId>imageio-pcx</artifactId>
<name>TwelveMonkeys :: ImageIO :: PCX plugin</name> <name>TwelveMonkeys :: ImageIO :: PCX plugin</name>
@@ -45,7 +45,7 @@ import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.*; import java.awt.color.ColorSpace;
import java.awt.image.*; import java.awt.image.*;
import java.io.DataInput; import java.io.DataInput;
import java.io.DataInputStream; import java.io.DataInputStream;
@@ -377,12 +377,10 @@ public final class PCXImageReader extends ImageReaderBase {
@Override @Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
// checkBounds(imageIndex); checkBounds(imageIndex);
// readHeader(); readHeader();
//
// return new PCXMetadata(header, getVGAPalette()); return new PCXMetadata(header, getVGAPalette());
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
return new PCXMetadata(rawType, header);
} }
private IndexColorModel getVGAPalette() throws IOException { 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; 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 { final class PCXMetadata extends AbstractMetadata {
public PCXMetadata(ImageTypeSpecifier type, PCXHeader header) { private final PCXHeader header;
super(builder(type) private final IndexColorModel vgaPalette;
.withPlanarConfiguration(planarConfiguration(header))
.withCompressionTypeName(compressionName(header)) PCXMetadata(final PCXHeader header, final IndexColorModel vgaPalette) {
.withFormatVersion(String.valueOf(header.getVersion()))); this.header = header;
this.vgaPalette = vgaPalette;
} }
private static PlanarConfiguration planarConfiguration(PCXHeader header) { @Override
return header.getChannels() > 1 ? PlanarConfiguration.LineInterleaved : null; protected IIOMetadataNode getStandardChromaNode() {
} IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
private static String compressionName(PCXHeader header) { IndexColorModel palette = null;
switch (header.getCompression()) { boolean gray = false;
case PCX.COMPRESSION_NONE:
return "None"; IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
case PCX.COMPRESSION_RLE: switch (header.getBitsPerPixel()) {
return "RLE"; 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> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-pdf</artifactId> <artifactId>imageio-pdf</artifactId>
<name>TwelveMonkeys :: ImageIO :: PDF plugin</name> <name>TwelveMonkeys :: ImageIO :: PDF plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-pict</artifactId> <artifactId>imageio-pict</artifactId>
<name>TwelveMonkeys :: ImageIO :: PICT plugin</name> <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.DecoderStream;
import com.twelvemonkeys.io.enc.PackBitsDecoder; import com.twelvemonkeys.io.enc.PackBitsDecoder;
import javax.imageio.IIOException; import javax.imageio.*;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.*; import java.awt.color.ColorSpace;
import java.awt.geom.*; import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.image.*; import java.awt.image.*;
import java.io.DataInput; import java.io.*;
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.util.List; import java.util.List;
import java.util.*;
/** /**
* Reader for Apple Mac Paint Picture (PICT) format. * Reader for Apple Mac Paint Picture (PICT) format.
@@ -2621,9 +2611,7 @@ public final class PICTImageReader extends ImageReaderBase {
return getYPtCoord(getPICTFrame().height); return getYPtCoord(getPICTFrame().height);
} }
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException { public Iterator<ImageTypeSpecifier> getImageTypes(int pIndex) {
checkBounds(imageIndex);
// TODO: The images look slightly different in Preview.. Could indicate the color space is wrong... // TODO: The images look slightly different in Preview.. Could indicate the color space is wrong...
return Collections.singletonList( return Collections.singletonList(
ImageTypeSpecifiers.createPacked( ImageTypeSpecifiers.createPacked(
@@ -2635,10 +2623,10 @@ public final class PICTImageReader extends ImageReaderBase {
@Override @Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { 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 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) { 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; 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. * PICTMetadata.
@@ -41,13 +11,82 @@ import javax.imageio.ImageTypeSpecifier;
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: PICTMetadata.java,v 1.0 23/03/2021 haraldk Exp$ * @version $Id: PICTMetadata.java,v 1.0 23/03/2021 haraldk Exp$
*/ */
final class PICTMetadata extends StandardImageMetadataSupport { public class PICTMetadata extends AbstractMetadata {
PICTMetadata(final ImageTypeSpecifier type, final int version, final double screenImageXRatio, final double screenImageYRatio) {
super(builder(type) private final int version;
.withPixelAspectRatio(screenImageXRatio > 0.0d && screenImageYRatio > 0.0d ? screenImageXRatio / screenImageYRatio : 1) private final double screenImageXRatio;
.withFormatVersion(Integer.toString(version)) private final double screenImageYRatio;
);
// As this is a vector-ish format, some of the data makes no sense... :-P PICTMetadata(final int version, final double screenImageXRatio, final double screenImageYRatio) {
// It is, however, consistent with the getRawImageTyp/getImageTypes 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; package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.ImageReaderBase; import com.twelvemonkeys.imageio.ImageReaderBase;
@@ -64,7 +34,7 @@ public final class PNTGImageReader extends ImageReaderBase {
private static final Set<ImageTypeSpecifier> IMAGE_TYPES = private static final Set<ImageTypeSpecifier> IMAGE_TYPES =
Collections.singleton(ImageTypeSpecifiers.createIndexed(new int[] {-1, 0}, false, -1, 1, DataBuffer.TYPE_BYTE)); 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); super(provider);
} }
@@ -153,7 +123,9 @@ public final class PNTGImageReader extends ImageReaderBase {
@Override @Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
return new PNTGMetadata(getRawImageType(imageIndex)); checkBounds(imageIndex);
return new PNTGMetadata();
} }
private void readHeader() throws IOException { 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; package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; 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; 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. * PNTGMetadata.
@@ -41,11 +11,76 @@ import javax.imageio.ImageTypeSpecifier;
* @author last modified by $Author: haraldk$ * @author last modified by $Author: haraldk$
* @version $Id: PNTGMetadata.java,v 1.0 23/03/2021 haraldk Exp$ * @version $Id: PNTGMetadata.java,v 1.0 23/03/2021 haraldk Exp$
*/ */
final class PNTGMetadata extends StandardImageMetadataSupport { public class PNTGMetadata extends AbstractMetadata {
public PNTGMetadata(ImageTypeSpecifier type) { @Override
super(builder(type) protected IIOMetadataNode getStandardChromaNode() {
.withBlackIsZero(false) IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
.withCompressionTypeName("PackBits")
.withFormatVersion("1.0")); 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. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
@@ -1,11 +1,7 @@
package com.twelvemonkeys.imageio.plugins.pntg; package com.twelvemonkeys.imageio.plugins.pntg;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import org.junit.Test; import org.junit.Test;
import java.awt.image.*;
/** /**
* PNTGMetadataTest. * PNTGMetadataTest.
* *
@@ -16,6 +12,6 @@ import java.awt.image.*;
public class PNTGMetadataTest { public class PNTGMetadataTest {
@Test @Test
public void testCreate() { public void testCreate() {
new PNTGMetadata(ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_BYTE_BINARY)); new PNTGMetadata();
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-pnm</artifactId> <artifactId>imageio-pnm</artifactId>
<name>TwelveMonkeys :: ImageIO :: PNM plugin</name> <name>TwelveMonkeys :: ImageIO :: PNM plugin</name>
@@ -43,7 +43,7 @@ import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.*; import java.awt.color.ColorSpace;
import java.awt.image.*; import java.awt.image.*;
import java.io.DataInput; import java.io.DataInput;
import java.io.DataInputStream; import java.io.DataInputStream;
@@ -468,7 +468,10 @@ public final class PNMImageReader extends ImageReaderBase {
@Override @Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { 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 { 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. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without * Redistribution and use in source and binary forms, with or without
@@ -30,35 +30,92 @@
package com.twelvemonkeys.imageio.plugins.pnm; 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 javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.*; import java.awt.*;
import java.awt.image.DataBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
/** final class PNMMetadata extends AbstractMetadata {
* PNMMetadata.
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
final class PNMMetadata extends StandardImageMetadataSupport {
private final PNMHeader header; private final PNMHeader header;
PNMMetadata(ImageTypeSpecifier type, PNMHeader header) { PNMMetadata(final 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))
);
this.header = 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) { if (header.getTransferType() == DataBuffer.TYPE_FLOAT) {
return header.getBitsPerSample(); return header.getBitsPerSample();
} }
@@ -75,30 +132,38 @@ final class PNMMetadata extends StandardImageMetadataSupport {
return significantBits; return significantBits;
} }
private static ColorSpaceType colorSpace(PNMHeader header) { private String createListValue(final int itemCount, final String... values) {
switch (header.getTupleType()) { StringBuilder buffer = new StringBuilder();
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 static ImageOrientation orientation(PNMHeader header) { for (int i = 0; i < itemCount; i++) {
// For some reason, the float values are stored bottom-up if (buffer.length() > 0) {
return header.getFileType() == PNM.PFM_GRAY || header.getFileType() == PNM.PFM_RGB buffer.append(' ');
? ImageOrientation.FlipH }
: ImageOrientation.Normal;
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 @Override
protected IIOMetadataNode getStandardTextNode() { 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()) { if (!header.getComments().isEmpty()) {
IIOMetadataNode text = new IIOMetadataNode("Text"); IIOMetadataNode text = new IIOMetadataNode("Text");
@@ -114,4 +179,17 @@ final class PNMMetadata extends StandardImageMetadataSupport {
return null; 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> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-psd</artifactId> <artifactId>imageio-psd</artifactId>
<name>TwelveMonkeys :: ImageIO :: PSD plugin</name> <name>TwelveMonkeys :: ImageIO :: PSD plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-reference</artifactId> <artifactId>imageio-reference</artifactId>
<name>TwelveMonkeys :: ImageIO :: JDK Reference Tests</name> <name>TwelveMonkeys :: ImageIO :: JDK Reference Tests</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-sgi</artifactId> <artifactId>imageio-sgi</artifactId>
<name>TwelveMonkeys :: ImageIO :: SGI plugin</name> <name>TwelveMonkeys :: ImageIO :: SGI plugin</name>
@@ -33,7 +33,7 @@ package com.twelvemonkeys.imageio.plugins.sgi;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.Charset;
final class SGIHeader { final class SGIHeader {
private int compression; 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.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.*; import java.awt.color.ColorSpace;
import java.awt.image.*; import java.awt.image.*;
import java.io.DataInput; import java.io.DataInput;
import java.io.DataInputStream; import java.io.DataInputStream;
@@ -378,10 +378,11 @@ public final class SGIImageReader extends ImageReaderBase {
imageInput.seek(imageInput.getFlushedPosition()); imageInput.seek(imageInput.getFlushedPosition());
} }
@Override @Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { checkBounds(imageIndex);
ImageTypeSpecifier rawType = getRawImageType(imageIndex); readHeader();
return new SGIMetadata(rawType, header);
return new SGIMetadata(header);
} }
public static void main(String[] args) throws IOException { 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; 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 { final class SGIMetadata extends AbstractMetadata {
public SGIMetadata(ImageTypeSpecifier type, SGIHeader header) { private final SGIHeader header;
super(builder(type)
.withSignificantBitsPerSample(computeSignificantBits(header)) SGIMetadata(final SGIHeader header) {
.withCompressionTypeName(compressionName(header)) this.header = header;
.withOrientation(ImageOrientation.FlipV)
.withTextEntry("DocumentName", header.getName())
);
} }
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 maxSample = header.getMaxValue();
int significantBits = 1; while (maxSample > 0) {
maxSample >>>= 1;
while ((maxSample >>>= 1) != 0) {
significantBits++; significantBits++;
} }
return significantBits; return significantBits;
} }
private static String compressionName(SGIHeader header) { private String createListValue(final int itemCount, final String... values) {
switch (header.getCompression()) { StringBuilder buffer = new StringBuilder();
case SGI.COMPRESSION_NONE:
return "None"; for (int i = 0; i < itemCount; i++) {
case SGI.COMPRESSION_RLE: if (buffer.length() > 0) {
return "RLE"; 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> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-tga</artifactId> <artifactId>imageio-tga</artifactId>
<name>TwelveMonkeys :: ImageIO :: TGA plugin</name> <name>TwelveMonkeys :: ImageIO :: TGA plugin</name>
@@ -31,16 +31,18 @@
package com.twelvemonkeys.imageio.plugins.tga; package com.twelvemonkeys.imageio.plugins.tga;
import javax.imageio.IIOException; import javax.imageio.IIOException;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.stream.ImageInputStream; 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.DataInput;
import java.io.DataOutput; import java.io.DataOutput;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import static com.twelvemonkeys.lang.Validate.notNull; 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 { final class TGAHeader {
@@ -116,14 +118,10 @@ final class TGAHeader {
'}'; '}';
} }
static TGAHeader from(final ImageTypeSpecifier type, final boolean compressed) { static TGAHeader from(final RenderedImage image, final boolean compressed) {
return from(type, 0, 0, compressed); notNull(image, "image");
}
static TGAHeader from(final ImageTypeSpecifier type, int width, int height, final boolean compressed) { ColorModel colorModel = image.getColorModel();
notNull(type, "type");
ColorModel colorModel = type.getColorModel();
IndexColorModel colorMap = colorModel instanceof IndexColorModel ? (IndexColorModel) colorModel : null; IndexColorModel colorMap = colorModel instanceof IndexColorModel ? (IndexColorModel) colorModel : null;
TGAHeader header = new TGAHeader(); TGAHeader header = new TGAHeader();
@@ -137,8 +135,8 @@ final class TGAHeader {
header.x = 0; header.x = 0;
header.y = 0; header.y = 0;
header.width = width; header.width = image.getWidth(); // TODO: Param source region/subsampling might affect this
header.height = height; header.height = image.getHeight(); // // TODO: Param source region/subsampling might affect this
header.pixelDepth = colorModel.getPixelSize() == 15 ? 16 : colorModel.getPixelSize(); header.pixelDepth = colorModel.getPixelSize() == 15 ? 16 : colorModel.getPixelSize();
header.origin = TGA.ORIGIN_UPPER_LEFT; // TODO: Allow parameter to control this? 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.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.*; import java.awt.color.ColorSpace;
import java.awt.image.*; import java.awt.image.*;
import java.io.DataInput; import java.io.DataInput;
import java.io.File; import java.io.File;
@@ -538,8 +538,10 @@ final class TGAImageReader extends ImageReaderBase {
@Override @Override
public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
ImageTypeSpecifier rawType = getRawImageType(imageIndex); checkBounds(imageIndex);
return new TGAMetadata(rawType, header, extensions); readHeader();
return new TGAMetadata(header, extensions);
} }
public static void main(String[] args) throws IOException { 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.io.enc.EncoderStream;
import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.lang.Validate;
import javax.imageio.IIOException; import javax.imageio.*;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi; import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStream;
import java.awt.*; import java.awt.*;
import java.awt.color.*; import java.awt.color.ColorSpace;
import java.awt.image.*; import java.awt.image.*;
import java.io.DataOutput; import java.io.DataOutput;
import java.io.File; import java.io.File;
@@ -69,7 +65,8 @@ final class TGAImageWriter extends ImageWriterBase {
public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) { public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) {
Validate.notNull(imageType, "imageType"); 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 @Override
@@ -110,8 +107,7 @@ final class TGAImageWriter extends ImageWriterBase {
final boolean compressed = isRLE(param, image.getMetadata()); final boolean compressed = isRLE(param, image.getMetadata());
RenderedImage renderedImage = image.getRenderedImage(); RenderedImage renderedImage = image.getRenderedImage();
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromRenderedImage(renderedImage); TGAHeader header = TGAHeader.from(renderedImage, compressed);
TGAHeader header = TGAHeader.from(type, renderedImage.getWidth(), renderedImage.getHeight(), compressed);
header.write(imageOutput); 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() ? 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 : 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() ? 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(); final DataBuffer buffer = rowRaster.getDataBuffer();
@@ -139,7 +135,7 @@ final class TGAImageWriter extends ImageWriterBase {
break; break;
} }
DataOutput imageOutput = compressed ? createRLEStream(this.imageOutput, header.getPixelDepth()) : this.imageOutput; DataOutput imageOutput = compressed ? createRLEStream(header, this.imageOutput) : this.imageOutput;
switch (buffer.getDataType()) { switch (buffer.getDataType()) {
case DataBuffer.TYPE_BYTE: case DataBuffer.TYPE_BYTE:
@@ -178,8 +174,8 @@ final class TGAImageWriter extends ImageWriterBase {
processImageComplete(); processImageComplete();
} }
private static LittleEndianDataOutputStream createRLEStream(final ImageOutputStream stream, int pixelDepth) { private static LittleEndianDataOutputStream createRLEStream(final TGAHeader header, final ImageOutputStream stream) {
return new LittleEndianDataOutputStream(new EncoderStream(IIOUtil.createStreamAdapter(stream), new RLEEncoder(pixelDepth))); return new LittleEndianDataOutputStream(new EncoderStream(IIOUtil.createStreamAdapter(stream), new RLEEncoder(header.getPixelDepth())));
} }
// TODO: Refactor to common util // 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; 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.Calendar;
import java.util.LinkedHashMap;
import java.util.Map;
final class TGAMetadata extends StandardImageMetadataSupport { import static com.twelvemonkeys.lang.Validate.notNull;
TGAMetadata(ImageTypeSpecifier type, TGAHeader header, TGAExtensions extensions) {
super(builder(type) final class TGAMetadata extends AbstractMetadata {
.withCompressionTypeName(compressionName(header)) private final TGAHeader header;
.withPixelAspectRatio(pixelAspectRatio(extensions)) private final TGAExtensions extensions;
.withOrientation(orientation(header))
.withFormatVersion(extensions == null ? "1.0" : "2.0") TGAMetadata(final TGAHeader header, final TGAExtensions extensions) {
.withDocumentCreationTime(documentCreationTime(extensions)) this.header = notNull(header, "header");
.withTextEntries(textEntries(header, extensions)) 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()) { switch (header.getImageType()) {
case TGA.IMAGETYPE_NONE:
case TGA.IMAGETYPE_COLORMAPPED:
case TGA.IMAGETYPE_TRUECOLOR:
case TGA.IMAGETYPE_MONOCHROME: 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_COLORMAPPED_RLE:
case TGA.IMAGETYPE_TRUECOLOR_RLE: case TGA.IMAGETYPE_TRUECOLOR_RLE:
case TGA.IMAGETYPE_MONOCHROME_RLE: case TGA.IMAGETYPE_MONOCHROME_RLE:
return "RLE";
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN: case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN:
case TGA.IMAGETYPE_COLORMAPPED_HUFFMAN_QUADTREE: 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: default:
return "Unknown"; // No compression
return null;
} }
} }
private static double pixelAspectRatio(TGAExtensions extensions) { @Override
return extensions != null ? extensions.getPixelAspectRatio() : 1f; 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) { private String createListValue(final int itemCount, final String... values) {
switch (header.origin) { 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: case TGA.ORIGIN_LOWER_LEFT:
return ImageOrientation.FlipH; imageOrientation.setAttribute("value", "FlipH");
break;
case TGA.ORIGIN_LOWER_RIGHT: case TGA.ORIGIN_LOWER_RIGHT:
return ImageOrientation.Rotate180; imageOrientation.setAttribute("value", "Rotate180");
break;
case TGA.ORIGIN_UPPER_LEFT: case TGA.ORIGIN_UPPER_LEFT:
return ImageOrientation.Normal; imageOrientation.setAttribute("value", "Normal");
break;
case TGA.ORIGIN_UPPER_RIGHT: case TGA.ORIGIN_UPPER_RIGHT:
return ImageOrientation.FlipV; imageOrientation.setAttribute("value", "FlipV");
default: break;
throw new IllegalArgumentException("Unknown orientation: " + header.origin);
} }
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) { @Override
return extensions != null ? extensions.creationDate : null; 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) { @Override
LinkedHashMap<String, String> textEntries = new LinkedHashMap<>(); protected IIOMetadataNode getStandardTextNode() {
IIOMetadataNode text = new IIOMetadataNode("Text");
// NOTE: Keywords follow TIFF standard naming // NOTE: Names corresponds to equivalent fields in TIFF
putIfValue(textEntries, "DocumentName", header.getIdentification()); appendTextEntry(text, "DocumentName", header.getIdentification());
if (extensions != null) { if (extensions != null) {
putIfValue(textEntries, "Software", extensions.getSoftwareVersion() == null ? extensions.getSoftware() : (extensions.getSoftware() + " " + extensions.getSoftwareVersion())); appendTextEntry(text, "Software", extensions.getSoftwareVersion() == null ? extensions.getSoftware() : (extensions.getSoftware() + " " + extensions.getSoftwareVersion()));
putIfValue(textEntries, "Artist", extensions.getAuthorName()); appendTextEntry(text, "Artist", extensions.getAuthorName());
putIfValue(textEntries, "UserComment", extensions.getAuthorComments()); 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()) { 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; package com.twelvemonkeys.imageio.plugins.tga;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import org.junit.Test; import org.junit.Test;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriteParam;
import java.awt.image.*; import java.awt.image.BufferedImage;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse; 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$ * @version $Id: TGAImageWriteParamTest.java,v 1.0 08/04/2021 haraldk Exp$
*/ */
public class TGAImageWriteParamTest { public class TGAImageWriteParamTest {
private static final ImageTypeSpecifier TYPE_3BYTE_BGR = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR);
@Test @Test
public void testDefaultCopyFromMetadata() { public void testDefaultCopyFromMetadata() {
TGAImageWriteParam param = new TGAImageWriteParam(); TGAImageWriteParam param = new TGAImageWriteParam();
@@ -115,8 +107,8 @@ public class TGAImageWriteParamTest {
ImageWriteParam param = new ImageWriteParam(null); ImageWriteParam param = new ImageWriteParam(null);
assumeFalse(param.canWriteCompressed()); 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(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.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), true), null)));
} }
@Test @Test
@@ -124,7 +116,7 @@ public class TGAImageWriteParamTest {
ImageWriteParam param = new TGAImageWriteParam(); ImageWriteParam param = new TGAImageWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA); 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 @Test
@@ -132,7 +124,7 @@ public class TGAImageWriteParamTest {
ImageWriteParam param = new TGAImageWriteParam(); ImageWriteParam param = new TGAImageWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_COPY_FROM_METADATA); 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 @Test
@@ -140,12 +132,12 @@ public class TGAImageWriteParamTest {
TGAImageWriteParam param = new TGAImageWriteParam(); TGAImageWriteParam param = new TGAImageWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); 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(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.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), true), null)));
param.setCompressionType("RLE"); 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(TGAHeader.from(new BufferedImage(1, 1, BufferedImage.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), true), null)));
} }
} }
@@ -30,18 +30,14 @@
package com.twelvemonkeys.imageio.plugins.tga; package com.twelvemonkeys.imageio.plugins.tga;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import org.junit.Test; import org.junit.Test;
import org.junit.function.ThrowingRunnable; import org.junit.function.ThrowingRunnable;
import org.w3c.dom.Node; 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.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.metadata.IIOMetadataNode;
import java.awt.image.*; import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.util.Calendar; import java.util.Calendar;
import static org.junit.Assert.*; 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$ * @version $Id: TGAMetadataTest.java,v 1.0 08/04/2021 haraldk Exp$
*/ */
public class TGAMetadataTest { 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 @Test
public void testStandardFeatures() { public void testStandardFeatures() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, false); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false);
final TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null); final TGAMetadata metadata = new TGAMetadata(header, null);
// Standard metadata format // Standard metadata format
assertTrue(metadata.isStandardMetadataFormatSupported()); assertTrue(metadata.isStandardMetadataFormatSupported());
@@ -92,10 +83,10 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardChromaGray() { public void testStandardChromaGray() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, false); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), false);
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, null); TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma"); IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma); assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName()); assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength()); assertEquals(3, chroma.getLength());
@@ -117,10 +108,10 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardChromaRGB() { public void testStandardChromaRGB() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, false); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null); TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma"); IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma); assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName()); assertEquals("Chroma", chroma.getNodeName());
assertEquals(3, chroma.getLength()); assertEquals(3, chroma.getLength());
@@ -144,11 +135,10 @@ public class TGAMetadataTest {
public void testStandardChromaPalette() { public void testStandardChromaPalette() {
byte[] bw = {0, (byte) 0xff}; byte[] bw = {0, (byte) 0xff};
IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, -1); IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, -1);
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromIndexColorModel(indexColorModel); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), false);
TGAHeader header = TGAHeader.from(type, false); TGAMetadata metadata = new TGAMetadata(header, null);
TGAMetadata metadata = new TGAMetadata(type, header, null);
IIOMetadataNode chroma = getStandardNode(metadata, "Chroma"); IIOMetadataNode chroma = metadata.getStandardChromaNode();
assertNotNull(chroma); assertNotNull(chroma);
assertEquals("Chroma", chroma.getNodeName()); assertEquals("Chroma", chroma.getNodeName());
assertEquals(4, chroma.getLength()); assertEquals(4, chroma.getLength());
@@ -184,10 +174,10 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardCompressionRLE() { public void testStandardCompressionRLE() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, true); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null); TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode compression = getStandardNode(metadata, "Compression"); IIOMetadataNode compression = metadata.getStandardCompressionNode();
assertNotNull(compression); assertNotNull(compression);
assertEquals("Compression", compression.getNodeName()); assertEquals("Compression", compression.getNodeName());
assertEquals(2, compression.getLength()); assertEquals(2, compression.getLength());
@@ -205,18 +195,18 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardCompressionNone() { public void testStandardCompressionNone() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, false); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), false);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null); TGAMetadata metadata = new TGAMetadata(header, null);
assertNull(getStandardNode(metadata, "Compression")); // No compression, all default... assertNull(metadata.getStandardCompressionNode()); // No compression, all default...
} }
@Test @Test
public void testStandardDataGray() { public void testStandardDataGray() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, true); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true);
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, null); TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data"); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -238,10 +228,10 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardDataRGB() { public void testStandardDataRGB() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, true); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null); TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data"); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -263,11 +253,10 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardDataRGBA() { public void testStandardDataRGBA() {
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB), true);
TGAHeader header = TGAHeader.from(type, true); TGAMetadata metadata = new TGAMetadata(header, null);
TGAMetadata metadata = new TGAMetadata(type, header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data"); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -291,11 +280,10 @@ public class TGAMetadataTest {
public void testStandardDataPalette() { public void testStandardDataPalette() {
byte[] rgb = new byte[1 << 8]; // Colors doesn't really matter here byte[] rgb = new byte[1 << 8]; // Colors doesn't really matter here
IndexColorModel indexColorModel = new IndexColorModel(8, rgb.length, rgb, rgb, rgb, 0); IndexColorModel indexColorModel = new IndexColorModel(8, rgb.length, rgb, rgb, rgb, 0);
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromIndexColorModel(indexColorModel); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), true);
TGAHeader header = TGAHeader.from(type, true); TGAMetadata metadata = new TGAMetadata(header, null);
TGAMetadata metadata = new TGAMetadata(type, header, null);
IIOMetadataNode data = getStandardNode(metadata, "Data"); IIOMetadataNode data = metadata.getStandardDataNode();
assertNotNull(data); assertNotNull(data);
assertEquals("Data", data.getNodeName()); assertEquals("Data", data.getNodeName());
assertEquals(3, data.getLength()); assertEquals(3, data.getLength());
@@ -317,56 +305,53 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardDimensionNormal() { public void testStandardDimensionNormal() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, true); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true);
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); assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName()); assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength()); assertEquals(2, dimension.getLength());
IIOMetadataNode pixelAspectRatio = (IIOMetadataNode) dimension.getFirstChild(); IIOMetadataNode imageOrientation = (IIOMetadataNode) dimension.getFirstChild();
assertEquals("PixelAspectRatio", pixelAspectRatio.getNodeName());
assertEquals("1.0", pixelAspectRatio.getAttribute("value"));
IIOMetadataNode imageOrientation = (IIOMetadataNode) pixelAspectRatio.getNextSibling();
assertEquals("ImageOrientation", imageOrientation.getNodeName()); assertEquals("ImageOrientation", imageOrientation.getNodeName());
assertEquals("Normal", imageOrientation.getAttribute("value")); 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 @Test
public void testStandardDimensionFlipH() { 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; 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); assertNotNull(dimension);
assertEquals("Dimension", dimension.getNodeName()); assertEquals("Dimension", dimension.getNodeName());
assertEquals(2, dimension.getLength()); assertEquals(2, dimension.getLength());
IIOMetadataNode imageOrientation = (IIOMetadataNode) dimension.getFirstChild();
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("ImageOrientation", imageOrientation.getNodeName());
assertEquals("FlipH", imageOrientation.getAttribute("value")); 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 @Test
public void testStandardDocument() { public void testStandardDocument() {
TGAHeader header = TGAHeader.from(TYPE_BYTE_GRAY, true); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), true);
TGAMetadata metadata = new TGAMetadata(TYPE_BYTE_GRAY, header, null); TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode document = getStandardNode(metadata, "Document"); IIOMetadataNode document = metadata.getStandardDocumentNode();
assertNotNull(document); assertNotNull(document);
assertEquals("Document", document.getNodeName()); assertEquals("Document", document.getNodeName());
assertEquals(1, document.getLength()); assertEquals(1, document.getLength());
@@ -380,13 +365,13 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardDocumentExtensions() { 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(); TGAExtensions extensions = new TGAExtensions();
extensions.creationDate = Calendar.getInstance(); extensions.creationDate = Calendar.getInstance();
extensions.creationDate.set(2021, Calendar.APRIL, 8, 18, 55, 0); 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); assertNotNull(document);
assertEquals("Document", document.getNodeName()); assertEquals("Document", document.getNodeName());
assertEquals(2, document.getLength()); assertEquals(2, document.getLength());
@@ -409,7 +394,7 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardText() { 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"; header.identification = "MY_FILE.TGA";
TGAExtensions extensions = new TGAExtensions(); TGAExtensions extensions = new TGAExtensions();
@@ -417,9 +402,9 @@ public class TGAMetadataTest {
extensions.authorName = "Harald K"; extensions.authorName = "Harald K";
extensions.authorComments = "Comments, comments... "; 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); assertNotNull(text);
assertEquals("Text", text.getNodeName()); assertEquals("Text", text.getNodeName());
assertEquals(4, text.getLength()); assertEquals(4, text.getLength());
@@ -447,10 +432,10 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardTransparencyRGB() { public void testStandardTransparencyRGB() {
TGAHeader header = TGAHeader.from(TYPE_3BYTE_BGR, true); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR), true);
TGAMetadata metadata = new TGAMetadata(TYPE_3BYTE_BGR, header, null); TGAMetadata metadata = new TGAMetadata(header, null);
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency"); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency); assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName()); assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength()); assertEquals(1, transparency.getLength());
@@ -464,11 +449,10 @@ public class TGAMetadataTest {
@Test @Test
public void testStandardTransparencyRGBA() { public void testStandardTransparencyRGBA() {
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR), true);
TGAHeader header = TGAHeader.from(type, true); TGAMetadata metadata = new TGAMetadata(header, null);
TGAMetadata metadata = new TGAMetadata(type, header, null);
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency"); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency); assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName()); assertEquals("Transparency", transparency.getNodeName());
assertEquals(1, transparency.getLength()); assertEquals(1, transparency.getLength());
@@ -484,30 +468,19 @@ public class TGAMetadataTest {
public void testStandardTransparencyPalette() { public void testStandardTransparencyPalette() {
byte[] bw = {0, (byte) 0xff}; byte[] bw = {0, (byte) 0xff};
IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, 1); IndexColorModel indexColorModel = new IndexColorModel(8, bw.length, bw, bw, bw, 1);
ImageTypeSpecifier type = ImageTypeSpecifiers.createFromIndexColorModel(indexColorModel); TGAHeader header = TGAHeader.from(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, indexColorModel), true);
TGAHeader header = TGAHeader.from(type, true); TGAMetadata metadata = new TGAMetadata(header, null);
TGAMetadata metadata = new TGAMetadata(type, header, null);
IIOMetadataNode transparency = getStandardNode(metadata, "Transparency"); IIOMetadataNode transparency = metadata.getStandardTransparencyNode();
assertNotNull(transparency); assertNotNull(transparency);
assertEquals("Transparency", transparency.getNodeName()); assertEquals("Transparency", transparency.getNodeName());
assertEquals(2, transparency.getLength()); assertEquals(1, transparency.getLength());
IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild(); IIOMetadataNode alpha = (IIOMetadataNode) transparency.getFirstChild();
assertEquals("Alpha", alpha.getNodeName()); assertEquals("Alpha", alpha.getNodeName());
assertEquals("nonpremultiplied", alpha.getAttribute("value")); assertEquals("nonpremultiplied", alpha.getAttribute("value"));
IIOMetadataNode transparentIndex = (IIOMetadataNode) alpha.getNextSibling(); assertNull(alpha.getNextSibling()); // No more children
assertEquals("TransparentIndex", transparentIndex.getNodeName());
assertEquals("1", transparentIndex.getAttribute("value"));
assertNull(transparentIndex.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> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-thumbsdb</artifactId> <artifactId>imageio-thumbsdb</artifactId>
<name>TwelveMonkeys :: ImageIO :: Thumbs.db plugin</name> <name>TwelveMonkeys :: ImageIO :: Thumbs.db plugin</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-tiff-jai-interop</artifactId> <artifactId>imageio-tiff-jai-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: TIFF/JAI Metadata Interop</name> <name>TwelveMonkeys :: ImageIO :: TIFF/JAI Metadata Interop</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-tiff-jdk-interop</artifactId> <artifactId>imageio-tiff-jdk-interop</artifactId>
<name>TwelveMonkeys :: ImageIO :: TIFF/JDK JPEG Interop</name> <name>TwelveMonkeys :: ImageIO :: TIFF/JDK JPEG Interop</name>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-tiff</artifactId> <artifactId>imageio-tiff</artifactId>
<name>TwelveMonkeys :: ImageIO :: TIFF plugin</name> <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.ImageTypeSpecifiers;
import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest; import com.twelvemonkeys.imageio.util.ImageWriterAbstractTest;
import com.twelvemonkeys.io.FastByteArrayOutputStream; import com.twelvemonkeys.io.FastByteArrayOutputStream;
import com.twelvemonkeys.io.NullOutputStream;
import org.junit.Test; import org.junit.Test;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
@@ -556,7 +557,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest<TIFFImageWriter
IIOWriteProgressListener progress = mock(IIOWriteProgressListener.class, "progress"); IIOWriteProgressListener progress = mock(IIOWriteProgressListener.class, "progress");
writer.addIIOWriteProgressListener(progress); writer.addIIOWriteProgressListener(progress);
try (ImageOutputStream output = new NullImageOutputStream()) { try (ImageOutputStream output = ImageIO.createImageOutputStream(new NullOutputStream())) {
writer.setOutput(output); writer.setOutput(output);
try { try {
@@ -614,7 +615,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest<TIFFImageWriter
public void testWriteParamJPEGQuality() throws IOException { public void testWriteParamJPEGQuality() throws IOException {
ImageWriter writer = createWriter(); ImageWriter writer = createWriter();
try (ImageOutputStream output = new NullImageOutputStream()) { try (ImageOutputStream output = ImageIO.createImageOutputStream(new NullOutputStream())) {
writer.setOutput(output); writer.setOutput(output);
try { try {
@@ -1365,30 +1366,21 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest<TIFFImageWriter
private static final class NullImageOutputStream extends ImageOutputStreamImpl { private static final class NullImageOutputStream extends ImageOutputStreamImpl {
@Override @Override
public void write(int b) { public void write(int b) {
streamPos++;
} }
@Override @Override
public void write(byte[] b, int off, int len) { public void write(byte[] b, int off, int len) {
streamPos += len;
} }
@Override @Override
public int read() { public int read() {
streamPos++;
return 0; return 0;
} }
@Override @Override
public int read(byte[] b, int off, int len) { public int read(byte[] b, int off, int len) {
streamPos += len;
return 0; return 0;
} }
@Override
public long length() {
return streamPos;
}
} }
// Special purpose data buffer that does not require memory, to allow very large images // Special purpose data buffer that does not require memory, to allow very large images
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>com.twelvemonkeys.imageio</groupId> <groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId> <artifactId>imageio</artifactId>
<version>3.9.3</version> <version>3.8.4-SNAPSHOT</version>
</parent> </parent>
<artifactId>imageio-webp</artifactId> <artifactId>imageio-webp</artifactId>
<name>TwelveMonkeys :: ImageIO :: WebP plugin</name> <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; package com.twelvemonkeys.imageio.plugins.webp;
import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageInputStream;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import static com.twelvemonkeys.lang.Validate.notNull;
/** /**
* LSBBitReader * LSBBitReader
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
* @author Simon Kammermeier
*/ */
public final class LSBBitReader { public final class LSBBitReader {
// TODO: Consider creating an ImageInputStream wrapper with the WebP implementation of readBit(s)? // TODO: Consider creating an ImageInputStream wrapper with the WebP implementation of readBit(s)?
private final ImageInputStream imageInput; private final ImageInputStream imageInput;
private int bitOffset = 64; int bitOffset = 0;
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;
public LSBBitReader(ImageInputStream imageInput) { public LSBBitReader(ImageInputStream imageInput) {
this.imageInput = notNull(imageInput); this.imageInput = imageInput;
} }
/** // TODO: Optimize this... Read many bits at once!
* 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
*/
public long readBits(int bits) throws IOException { 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;
} }
/** // TODO: Optimize this...
* Reads the specified number of bits from the buffer in an LSB-first way. // TODO: Consider not reading value over and over....
* 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
public int readBit() throws IOException { 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; 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; import static com.twelvemonkeys.lang.Validate.notNull;
final class WebPImageMetadata extends StandardImageMetadataSupport { /**
WebPImageMetadata(ImageTypeSpecifier type, VP8xChunk header) { * WebPMetadata
super(builder(type) */
.withCompressionTypeName(notNull(header, "header").isLossless ? "VP8L" : "VP8") final class WebPImageMetadata extends AbstractMetadata {
.withCompressionLossless(header.isLossless) private final VP8xChunk header;
.withPixelAspectRatio(1.0)
.withFormatVersion("1.0") WebPImageMetadata(final VP8xChunk header) {
// TODO: Get useful text nodes from EXIF or XMP 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; 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.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorProfiles; import com.twelvemonkeys.imageio.color.ColorProfiles;
import com.twelvemonkeys.imageio.color.ColorSpaces; 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.ProgressListenerBase;
import com.twelvemonkeys.imageio.util.RasterUtils; 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 * WebPImageReader
*/ */
@@ -96,9 +99,7 @@ final class WebPImageReader extends ImageReaderBase {
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
super.setInput(input, seekForwardOnly, ignoreMetadata); super.setInput(input, seekForwardOnly, ignoreMetadata);
if (imageInput != null) { lsbBitReader = new LSBBitReader(imageInput);
lsbBitReader = new LSBBitReader(imageInput);
}
} }
private void readHeader(int imageIndex) throws IOException { private void readHeader(int imageIndex) throws IOException {
@@ -140,16 +141,6 @@ final class WebPImageReader extends ImageReaderBase {
case WebP.CHUNK_ANIM: case WebP.CHUNK_ANIM:
// TODO: 32 bit bg color (hint!) + 16 bit loop count // TODO: 32 bit bg color (hint!) + 16 bit loop count
// + expose bg color in std image metadata... // + 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; break;
case WebP.CHUNK_ANMF: case WebP.CHUNK_ANMF:
@@ -162,7 +153,7 @@ final class WebPImageReader extends ImageReaderBase {
Rectangle bounds = new Rectangle(x, y, w, h); Rectangle bounds = new Rectangle(x, y, w, h);
// TODO: Expose duration/flags in image metadata // 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 int flags = imageInput.readUnsignedByte(); // 6 bit reserved + blend mode + disposal mode
frames.add(new AnimationFrame(chunkLength, chunkStart, bounds, duration, flags)); frames.add(new AnimationFrame(chunkLength, chunkStart, bounds, duration, flags));
@@ -214,7 +205,7 @@ final class WebPImageReader extends ImageReaderBase {
switch (chunk) { switch (chunk) {
case WebP.CHUNK_VP8_: 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) int frameType = lsbBitReader.readBit(); // 0 = key frame, 1 = inter frame (not used in WebP)
if (frameType != 0) { if (frameType != 0) {
@@ -274,19 +265,19 @@ final class WebPImageReader extends ImageReaderBase {
} }
// RsV|I|L|E|X|A|R // RsV|I|L|E|X|A|R
int reserved = lsbBitReader.readBit(); int reserved = (int) imageInput.readBits(2);
if (reserved != 0) { if (reserved != 0) {
// Spec says SHOULD be 0 // Spec says SHOULD be 0
throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved)); throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved));
} }
header.containsANIM = lsbBitReader.readBit() == 1; // A -> Anim header.containsICCP = imageInput.readBit() == 1;
header.containsXMP_ = lsbBitReader.readBit() == 1; header.containsALPH = imageInput.readBit() == 1; // L -> aLpha
header.containsEXIF = lsbBitReader.readBit() == 1; header.containsEXIF = imageInput.readBit() == 1;
header.containsALPH = lsbBitReader.readBit() == 1; // L -> aLpha header.containsXMP_ = imageInput.readBit() == 1;
header.containsICCP = lsbBitReader.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) { if (reserved != 0) {
// Spec says SHOULD be 0 // Spec says SHOULD be 0
throw new IIOException(String.format("Unexpected 'VP8X' chunk reserved value, expected 0: %d", reserved)); 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) { if (header.containsANIM) {
AnimationFrame frame = frames.get(imageIndex); AnimationFrame frame = frames.get(imageIndex);
imageInput.seek(frame.offset + 16); 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 { else {
imageInput.seek(header.offset + header.length); 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 { 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) { while (imageInput.getStreamPosition() < streamEnd) {
int nextChunk = imageInput.readInt(); int nextChunk = imageInput.readInt();
long chunkLength = imageInput.readUnsignedInt(); long chunkLength = imageInput.readUnsignedInt();
@@ -477,16 +465,45 @@ final class WebPImageReader extends ImageReaderBase {
switch (nextChunk) { switch (nextChunk) {
case WebP.CHUNK_ALPH: 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; break;
case WebP.CHUNK_VP8_: case WebP.CHUNK_VP8_:
readVP8(RasterUtils.asByteRaster(destination.getRaster()) 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; break;
case WebP.CHUNK_VP8L: case WebP.CHUNK_VP8L:
readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param, width, height); readVP8Lossless(RasterUtils.asByteRaster(destination.getRaster()), param);
break; break;
case WebP.CHUNK_ANIM: 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 compression = (int) lsbBitReader.readBits(2);
int filtering = (int) lsbBitReader.readBits(2);
int preProcessing = (int) lsbBitReader.readBits(2);
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));
}
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) { private void applyICCProfileIfNeeded(final BufferedImage destination) {
if (iccProfile != null) { if (iccProfile != null) {
ColorModel colorModel = destination.getColorModel(); ColorModel colorModel = destination.getColorModel();
@@ -627,7 +558,6 @@ final class WebPImageReader extends ImageReaderBase {
} }
} }
@SuppressWarnings("RedundantThrows")
private void readUncompressedAlpha(final WritableRaster alphaRaster) throws IOException { private void readUncompressedAlpha(final WritableRaster alphaRaster) throws IOException {
// Hardly used in practice, need to find a sample file // Hardly used in practice, need to find a sample file
processWarningOccurred("Uncompressed WebP alpha not implemented"); 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 { 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); 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 { private void readVP8(final WritableRaster raster, final ImageReadParam param) throws IOException {
@@ -663,7 +588,10 @@ final class WebPImageReader extends ImageReaderBase {
@Override @Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException { 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 { private void readMeta() throws IOException {
@@ -75,8 +75,10 @@ public final class WebPImageReaderSpi extends ImageReaderSpiBase {
int chunk = stream.readInt(); int chunk = stream.readInt();
switch (chunk) { switch (chunk) {
case WebP.CHUNK_VP8L: // TODO. Support lossless
// case WebP.CHUNK_VP8L:
case WebP.CHUNK_VP8X: case WebP.CHUNK_VP8X:
return containsSupportedChunk(stream, chunk);
case WebP.CHUNK_VP8_: case WebP.CHUNK_VP8_:
return true; return true;
default: 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 @Override
public ImageReader createReaderInstance(final Object extension) { public ImageReader createReaderInstance(final Object extension) {
return new WebPImageReader(this); 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> * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/ */
interface PredictorMode { public interface PredictorMode {
// Special rules: // Special rules:
// Top-left pixel of image is predicted BLACK // Top-left pixel of image is predicted BLACK
// Rest of top pixels is predicted L // 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; package com.twelvemonkeys.imageio.plugins.webp.lossless;
import java.awt.image.*;
/** /**
* Transform. * Transform.
* *
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a> * @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