From e0fd70d0eff2e943df3745f691fc08d31f57a08f Mon Sep 17 00:00:00 2001 From: Andrea Cavalli Date: Mon, 11 Jun 2018 22:41:11 +0200 Subject: [PATCH] Big update, added support for TeaVM (javascript) --- .classpath | 20 +- .settings/org.eclipse.core.resources.prefs | 1 + math-rules-cache.zip | Bin 94222 -> 94222 bytes math-rules-cache.zip114 | Bin 0 -> 94104 bytes pom.xml | 159 +++-- pom.xml.versionsBackup | 276 +++++++++ .../ar/com/hjg/pngj/BufferedStreamFeeder.java | 199 ++++++ .../java/ar/com/hjg/pngj/ChunkReader.java | 216 +++++++ .../ar/com/hjg/pngj/ChunkSeqBuffering.java | 30 + .../java/ar/com/hjg/pngj/ChunkSeqReader.java | 396 ++++++++++++ .../ar/com/hjg/pngj/ChunkSeqReaderPng.java | 313 ++++++++++ .../ar/com/hjg/pngj/ChunkSeqSkipping.java | 70 +++ .../ar/com/hjg/pngj/DeflatedChunkReader.java | 83 +++ .../ar/com/hjg/pngj/DeflatedChunksSet.java | 417 +++++++++++++ .../java/ar/com/hjg/pngj/Deinterlacer.java | 199 ++++++ .../java/ar/com/hjg/pngj/FilterType.java | 124 ++++ .../java/ar/com/hjg/pngj/IBytesConsumer.java | 14 + .../java/ar/com/hjg/pngj/IChunkFactory.java | 20 + .../java/ar/com/hjg/pngj/IDatChunkWriter.java | 129 ++++ .../java/ar/com/hjg/pngj/IImageLine.java | 41 ++ .../java/ar/com/hjg/pngj/IImageLineArray.java | 23 + .../ar/com/hjg/pngj/IImageLineFactory.java | 8 + .../java/ar/com/hjg/pngj/IImageLineSet.java | 53 ++ .../ar/com/hjg/pngj/IImageLineSetFactory.java | 24 + .../ar/com/hjg/pngj/IPngWriterFactory.java | 7 + .../java/ar/com/hjg/pngj/IdatSet.java | 242 ++++++++ .../java/ar/com/hjg/pngj/ImageInfo.java | 255 ++++++++ .../java/ar/com/hjg/pngj/ImageLineByte.java | 186 ++++++ .../java/ar/com/hjg/pngj/ImageLineHelper.java | 470 ++++++++++++++ .../java/ar/com/hjg/pngj/ImageLineInt.java | 193 ++++++ .../ar/com/hjg/pngj/ImageLineSetDefault.java | 151 +++++ .../ar/com/hjg/pngj/PngHelperInternal.java | 329 ++++++++++ .../ar/com/hjg/pngj/PngHelperInternal2.java | 33 + .../java/ar/com/hjg/pngj/PngReader.java | 586 ++++++++++++++++++ .../java/ar/com/hjg/pngj/PngReaderApng.java | 213 +++++++ .../java/ar/com/hjg/pngj/PngReaderByte.java | 30 + .../java/ar/com/hjg/pngj/PngReaderFilter.java | 99 +++ .../java/ar/com/hjg/pngj/PngReaderInt.java | 37 ++ .../java/ar/com/hjg/pngj/PngWriter.java | 427 +++++++++++++ .../java/ar/com/hjg/pngj/PngWriterHc.java | 35 ++ .../ar/com/hjg/pngj/PngjBadCrcException.java | 20 + .../java/ar/com/hjg/pngj/PngjException.java | 20 + .../com/hjg/pngj/PngjExceptionInternal.java | 23 + .../ar/com/hjg/pngj/PngjInputException.java | 20 + .../ar/com/hjg/pngj/PngjOutputException.java | 20 + .../hjg/pngj/PngjUnsupportedException.java | 24 + .../java/ar/com/hjg/pngj/RowInfo.java | 55 ++ .../hjg/pngj/chunks/ChunkCopyBehaviour.java | 101 +++ .../ar/com/hjg/pngj/chunks/ChunkFactory.java | 107 ++++ .../ar/com/hjg/pngj/chunks/ChunkHelper.java | 290 +++++++++ .../hjg/pngj/chunks/ChunkLoadBehaviour.java | 26 + .../com/hjg/pngj/chunks/ChunkPredicate.java | 14 + .../java/ar/com/hjg/pngj/chunks/ChunkRaw.java | 169 +++++ .../ar/com/hjg/pngj/chunks/ChunksList.java | 167 +++++ .../hjg/pngj/chunks/ChunksListForWrite.java | 189 ++++++ .../pngj/chunks/PngBadCharsetException.java | 20 + .../java/ar/com/hjg/pngj/chunks/PngChunk.java | 216 +++++++ .../ar/com/hjg/pngj/chunks/PngChunkACTL.java | 59 ++ .../ar/com/hjg/pngj/chunks/PngChunkBKGD.java | 112 ++++ .../ar/com/hjg/pngj/chunks/PngChunkCHRM.java | 75 +++ .../ar/com/hjg/pngj/chunks/PngChunkFCTL.java | 158 +++++ .../ar/com/hjg/pngj/chunks/PngChunkFDAT.java | 70 +++ .../ar/com/hjg/pngj/chunks/PngChunkGAMA.java | 51 ++ .../ar/com/hjg/pngj/chunks/PngChunkHIST.java | 58 ++ .../ar/com/hjg/pngj/chunks/PngChunkICCP.java | 77 +++ .../ar/com/hjg/pngj/chunks/PngChunkIDAT.java | 34 + .../ar/com/hjg/pngj/chunks/PngChunkIEND.java | 35 ++ .../ar/com/hjg/pngj/chunks/PngChunkIHDR.java | 185 ++++++ .../ar/com/hjg/pngj/chunks/PngChunkITXT.java | 111 ++++ .../com/hjg/pngj/chunks/PngChunkMultiple.java | 27 + .../ar/com/hjg/pngj/chunks/PngChunkOFFS.java | 81 +++ .../ar/com/hjg/pngj/chunks/PngChunkPHYS.java | 107 ++++ .../ar/com/hjg/pngj/chunks/PngChunkPLTE.java | 98 +++ .../ar/com/hjg/pngj/chunks/PngChunkSBIT.java | 114 ++++ .../ar/com/hjg/pngj/chunks/PngChunkSPLT.java | 131 ++++ .../ar/com/hjg/pngj/chunks/PngChunkSRGB.java | 55 ++ .../ar/com/hjg/pngj/chunks/PngChunkSTER.java | 54 ++ .../com/hjg/pngj/chunks/PngChunkSingle.java | 43 ++ .../ar/com/hjg/pngj/chunks/PngChunkTEXT.java | 44 ++ .../ar/com/hjg/pngj/chunks/PngChunkTIME.java | 82 +++ .../ar/com/hjg/pngj/chunks/PngChunkTRNS.java | 149 +++++ .../com/hjg/pngj/chunks/PngChunkTextVar.java | 60 ++ .../com/hjg/pngj/chunks/PngChunkUNKNOWN.java | 40 ++ .../ar/com/hjg/pngj/chunks/PngChunkZTXT.java | 62 ++ .../ar/com/hjg/pngj/chunks/PngMetadata.java | 230 +++++++ .../java/ar/com/hjg/pngj/chunks/package.html | 9 + .../java/ar/com/hjg/pngj/package.html | 49 ++ .../com/hjg/pngj/pixels/CompressorStream.java | 160 +++++ .../pngj/pixels/CompressorStreamDeflater.java | 104 ++++ .../hjg/pngj/pixels/CompressorStreamLz4.java | 94 +++ .../hjg/pngj/pixels/DeflaterEstimatorHjg.java | 258 ++++++++ .../hjg/pngj/pixels/DeflaterEstimatorLz4.java | 272 ++++++++ .../hjg/pngj/pixels/FiltersPerformance.java | 203 ++++++ .../ar/com/hjg/pngj/pixels/PixelsWriter.java | 263 ++++++++ .../hjg/pngj/pixels/PixelsWriterDefault.java | 158 +++++ .../hjg/pngj/pixels/PixelsWriterMultiple.java | 241 +++++++ .../java/ar/com/hjg/pngj/pixels/package.html | 14 + .../org/warp/picalculator/PlatformUtils.java | 19 + .../org/warp/picalculator/deps/DEngine.java | 3 + .../warp/picalculator/deps/DSemaphore.java | 20 + .../deps/DStandardOpenOption.java | 125 ++++ .../picalculator/deps/DURLClassLoader.java | 24 + .../warp/picalculator/deps/StorageUtils.java | 159 ++++- .../ar/com/hjg/pngj/BufferedStreamFeeder.java | 199 ++++++ .../java/ar/com/hjg/pngj/ChunkReader.java | 216 +++++++ .../ar/com/hjg/pngj/ChunkSeqBuffering.java | 30 + .../java/ar/com/hjg/pngj/ChunkSeqReader.java | 396 ++++++++++++ .../ar/com/hjg/pngj/ChunkSeqReaderPng.java | 313 ++++++++++ .../ar/com/hjg/pngj/ChunkSeqSkipping.java | 70 +++ .../ar/com/hjg/pngj/DeflatedChunkReader.java | 83 +++ .../ar/com/hjg/pngj/DeflatedChunksSet.java | 417 +++++++++++++ .../java/ar/com/hjg/pngj/Deinterlacer.java | 199 ++++++ .../java/ar/com/hjg/pngj/FilterType.java | 124 ++++ .../java/ar/com/hjg/pngj/IBytesConsumer.java | 14 + .../java/ar/com/hjg/pngj/IChunkFactory.java | 20 + .../java/ar/com/hjg/pngj/IDatChunkWriter.java | 129 ++++ .../java/ar/com/hjg/pngj/IImageLine.java | 41 ++ .../java/ar/com/hjg/pngj/IImageLineArray.java | 23 + .../ar/com/hjg/pngj/IImageLineFactory.java | 8 + .../java/ar/com/hjg/pngj/IImageLineSet.java | 53 ++ .../ar/com/hjg/pngj/IImageLineSetFactory.java | 24 + .../ar/com/hjg/pngj/IPngWriterFactory.java | 7 + .../java/ar/com/hjg/pngj/IdatSet.java | 242 ++++++++ .../java/ar/com/hjg/pngj/ImageInfo.java | 255 ++++++++ .../java/ar/com/hjg/pngj/ImageLineByte.java | 186 ++++++ .../java/ar/com/hjg/pngj/ImageLineHelper.java | 470 ++++++++++++++ .../java/ar/com/hjg/pngj/ImageLineInt.java | 193 ++++++ .../ar/com/hjg/pngj/ImageLineSetDefault.java | 151 +++++ .../ar/com/hjg/pngj/PngHelperInternal.java | 329 ++++++++++ .../ar/com/hjg/pngj/PngHelperInternal2.java | 33 + .../java/ar/com/hjg/pngj/PngReader.java | 586 ++++++++++++++++++ .../java/ar/com/hjg/pngj/PngReaderApng.java | 213 +++++++ .../java/ar/com/hjg/pngj/PngReaderByte.java | 30 + .../java/ar/com/hjg/pngj/PngReaderFilter.java | 99 +++ .../java/ar/com/hjg/pngj/PngReaderInt.java | 37 ++ .../java/ar/com/hjg/pngj/PngWriter.java | 427 +++++++++++++ .../java/ar/com/hjg/pngj/PngWriterHc.java | 35 ++ .../ar/com/hjg/pngj/PngjBadCrcException.java | 20 + .../java/ar/com/hjg/pngj/PngjException.java | 20 + .../com/hjg/pngj/PngjExceptionInternal.java | 23 + .../ar/com/hjg/pngj/PngjInputException.java | 20 + .../ar/com/hjg/pngj/PngjOutputException.java | 20 + .../hjg/pngj/PngjUnsupportedException.java | 24 + .../java/ar/com/hjg/pngj/RowInfo.java | 55 ++ .../hjg/pngj/chunks/ChunkCopyBehaviour.java | 101 +++ .../ar/com/hjg/pngj/chunks/ChunkFactory.java | 107 ++++ .../ar/com/hjg/pngj/chunks/ChunkHelper.java | 290 +++++++++ .../hjg/pngj/chunks/ChunkLoadBehaviour.java | 26 + .../com/hjg/pngj/chunks/ChunkPredicate.java | 14 + .../java/ar/com/hjg/pngj/chunks/ChunkRaw.java | 169 +++++ .../ar/com/hjg/pngj/chunks/ChunksList.java | 167 +++++ .../hjg/pngj/chunks/ChunksListForWrite.java | 189 ++++++ .../pngj/chunks/PngBadCharsetException.java | 20 + .../java/ar/com/hjg/pngj/chunks/PngChunk.java | 216 +++++++ .../ar/com/hjg/pngj/chunks/PngChunkACTL.java | 59 ++ .../ar/com/hjg/pngj/chunks/PngChunkBKGD.java | 112 ++++ .../ar/com/hjg/pngj/chunks/PngChunkCHRM.java | 75 +++ .../ar/com/hjg/pngj/chunks/PngChunkFCTL.java | 158 +++++ .../ar/com/hjg/pngj/chunks/PngChunkFDAT.java | 70 +++ .../ar/com/hjg/pngj/chunks/PngChunkGAMA.java | 51 ++ .../ar/com/hjg/pngj/chunks/PngChunkHIST.java | 58 ++ .../ar/com/hjg/pngj/chunks/PngChunkICCP.java | 77 +++ .../ar/com/hjg/pngj/chunks/PngChunkIDAT.java | 34 + .../ar/com/hjg/pngj/chunks/PngChunkIEND.java | 35 ++ .../ar/com/hjg/pngj/chunks/PngChunkIHDR.java | 185 ++++++ .../ar/com/hjg/pngj/chunks/PngChunkITXT.java | 111 ++++ .../com/hjg/pngj/chunks/PngChunkMultiple.java | 27 + .../ar/com/hjg/pngj/chunks/PngChunkOFFS.java | 81 +++ .../ar/com/hjg/pngj/chunks/PngChunkPHYS.java | 107 ++++ .../ar/com/hjg/pngj/chunks/PngChunkPLTE.java | 98 +++ .../ar/com/hjg/pngj/chunks/PngChunkSBIT.java | 114 ++++ .../ar/com/hjg/pngj/chunks/PngChunkSPLT.java | 131 ++++ .../ar/com/hjg/pngj/chunks/PngChunkSRGB.java | 55 ++ .../ar/com/hjg/pngj/chunks/PngChunkSTER.java | 54 ++ .../com/hjg/pngj/chunks/PngChunkSingle.java | 43 ++ .../ar/com/hjg/pngj/chunks/PngChunkTEXT.java | 44 ++ .../ar/com/hjg/pngj/chunks/PngChunkTIME.java | 82 +++ .../ar/com/hjg/pngj/chunks/PngChunkTRNS.java | 149 +++++ .../com/hjg/pngj/chunks/PngChunkTextVar.java | 60 ++ .../com/hjg/pngj/chunks/PngChunkUNKNOWN.java | 40 ++ .../ar/com/hjg/pngj/chunks/PngChunkZTXT.java | 62 ++ .../ar/com/hjg/pngj/chunks/PngMetadata.java | 230 +++++++ .../java/ar/com/hjg/pngj/chunks/package.html | 9 + .../java/ar/com/hjg/pngj/package.html | 49 ++ .../com/hjg/pngj/pixels/CompressorStream.java | 160 +++++ .../pngj/pixels/CompressorStreamDeflater.java | 104 ++++ .../hjg/pngj/pixels/CompressorStreamLz4.java | 94 +++ .../hjg/pngj/pixels/DeflaterEstimatorHjg.java | 258 ++++++++ .../hjg/pngj/pixels/DeflaterEstimatorLz4.java | 272 ++++++++ .../hjg/pngj/pixels/FiltersPerformance.java | 203 ++++++ .../ar/com/hjg/pngj/pixels/PixelsWriter.java | 263 ++++++++ .../hjg/pngj/pixels/PixelsWriterDefault.java | 158 +++++ .../hjg/pngj/pixels/PixelsWriterMultiple.java | 241 +++++++ .../java/ar/com/hjg/pngj/pixels/package.html | 14 + .../org/warp/picalculator/PlatformUtils.java | 74 +++ .../org/warp/picalculator/deps/DEngine.java | 4 + .../warp/picalculator/deps/DSemaphore.java | 41 ++ .../deps/DStandardOpenOption.java | 125 ++++ .../picalculator/deps/DURLClassLoader.java | 20 + .../warp/picalculator/deps/StorageUtils.java | 206 +++++- src/main/java/org/warp/picalculator/Main.java | 27 +- .../java/org/warp/picalculator/Utils.java | 78 +-- .../warp/picalculator/device/Keyboard.java | 6 +- .../warp/picalculator/gui/DisplayManager.java | 30 +- .../picalculator/gui/graphicengine/Skin.java | 3 +- .../gui/graphicengine/cpu/CPUFont.java | 22 +- .../gui/graphicengine/cpu/CPUSkin.java | 70 ++- .../gui/graphicengine/gpu/GPUFont.java | 10 +- .../gui/graphicengine/gpu/GPURenderer.java | 3 +- .../gui/graphicengine/html/HtmlEngine.java | 274 ++++++++ .../gui/graphicengine/html/HtmlFont.java | 25 + .../gui/graphicengine/html/HtmlRenderer.java | 457 ++++++++++++++ .../gui/graphicengine/html/HtmlSkin.java | 20 + .../gui/graphicengine/html/InputEvent.java | 10 + .../gui/graphicengine/nogui/NoGuiEngine.java | 5 +- .../picalculator/math/rules/RulesManager.java | 263 ++++---- .../rules/ExpandRule1.java | 3 + .../rules/ExpandRule2.java | 1 + .../rules/ExpandRule5.java | 1 + .../rules/ExponentRule1.java | 1 + .../rules/ExponentRule15.java | 1 + .../rules/ExponentRule16.java | 1 + .../rules/ExponentRule17.java | 1 + .../rules/ExponentRule2.java | 1 + .../rules/ExponentRule3.java | 1 + .../rules/ExponentRule4.java | 1 + .../rules/ExponentRule8.java | 1 + .../rules/ExponentRule9.java | 1 + .../rules/FractionsRule1.java | 1 + .../rules/FractionsRule10.java | 1 + .../rules/FractionsRule11.java | 1 + .../rules/FractionsRule12.java | 1 + .../rules/FractionsRule14.java | 1 + .../rules/FractionsRule2.java | 1 + .../rules/FractionsRule3.java | 1 + .../rules/FractionsRule4.java | 1 + .../rules/FractionsRule5.java | 4 +- .../rules/FractionsRule6.java | 4 +- .../rules/FractionsRule7.java | 9 +- .../rules/FractionsRule8.java | 9 +- .../rules/FractionsRule9.java | 7 +- .../rules/NumberRule1.java | 4 +- .../rules/NumberRule2.java | 6 +- .../rules/NumberRule3.java | 4 +- .../rules/NumberRule4.java | 4 +- .../rules/NumberRule5.java | 4 +- .../rules/NumberRule7.java | 4 +- .../rules/UndefinedRule1.java | 4 +- .../rules/UndefinedRule2.java | 4 +- .../rules/VariableRule1.java | 6 +- .../rules/VariableRule2.java | 6 +- .../rules/VariableRule3.java | 8 +- .../rules/functions/DivisionRule.java | 13 +- .../rules/functions/EmptyNumberRule.java | 1 + .../rules/functions/ExpressionRule.java | 1 + .../rules/functions/JokeRule.java | 1 + .../rules/functions/MultiplicationRule.java | 3 +- .../rules/functions/NegativeRule.java | 5 +- .../rules/functions/NumberRule.java | 7 +- .../rules/functions/PowerRule.java | 7 +- .../rules/functions/RootRule.java | 19 +- .../rules/functions/SubtractionRule.java | 7 +- .../rules/functions/SumRule.java | 7 +- .../rules/functions/SumSubtractionRule.java | 9 +- .../rules/functions/VariableRule.java | 3 +- 265 files changed, 25108 insertions(+), 383 deletions(-) create mode 100644 math-rules-cache.zip114 create mode 100644 pom.xml.versionsBackup create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/BufferedStreamFeeder.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ChunkReader.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqBuffering.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqReader.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqReaderPng.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqSkipping.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/DeflatedChunkReader.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/DeflatedChunksSet.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/Deinterlacer.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/FilterType.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IBytesConsumer.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IChunkFactory.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IDatChunkWriter.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IImageLine.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IImageLineArray.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IImageLineFactory.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IImageLineSet.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IImageLineSetFactory.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IPngWriterFactory.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/IdatSet.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ImageInfo.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ImageLineByte.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ImageLineHelper.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ImageLineInt.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/ImageLineSetDefault.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngHelperInternal.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngHelperInternal2.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngReader.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngReaderApng.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngReaderByte.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngReaderFilter.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngReaderInt.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngWriter.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngWriterHc.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngjBadCrcException.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngjException.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngjExceptionInternal.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngjInputException.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngjOutputException.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/PngjUnsupportedException.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/RowInfo.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkCopyBehaviour.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkFactory.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkHelper.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkLoadBehaviour.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkPredicate.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkRaw.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunksList.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunksListForWrite.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngBadCharsetException.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunk.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkACTL.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkBKGD.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkCHRM.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkFCTL.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkFDAT.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkGAMA.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkHIST.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkICCP.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIDAT.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIEND.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIHDR.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkITXT.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkMultiple.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkOFFS.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkPHYS.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkPLTE.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSBIT.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSPLT.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSRGB.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSTER.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSingle.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTEXT.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTIME.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTRNS.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTextVar.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkUNKNOWN.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkZTXT.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/PngMetadata.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/chunks/package.html create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/package.html create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStream.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamDeflater.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamLz4.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorHjg.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorLz4.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/FiltersPerformance.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriter.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterDefault.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterMultiple.java create mode 100644 src/jar-specific/java/ar/com/hjg/pngj/pixels/package.html create mode 100644 src/jar-specific/java/org/warp/picalculator/deps/DSemaphore.java create mode 100644 src/jar-specific/java/org/warp/picalculator/deps/DStandardOpenOption.java create mode 100644 src/jar-specific/java/org/warp/picalculator/deps/DURLClassLoader.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/BufferedStreamFeeder.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ChunkReader.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ChunkSeqBuffering.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ChunkSeqReader.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ChunkSeqReaderPng.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ChunkSeqSkipping.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/DeflatedChunkReader.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/DeflatedChunksSet.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/Deinterlacer.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/FilterType.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IBytesConsumer.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IChunkFactory.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IDatChunkWriter.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IImageLine.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IImageLineArray.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IImageLineFactory.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IImageLineSet.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IImageLineSetFactory.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IPngWriterFactory.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/IdatSet.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ImageInfo.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ImageLineByte.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ImageLineHelper.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ImageLineInt.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/ImageLineSetDefault.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngHelperInternal.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngHelperInternal2.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngReader.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngReaderApng.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngReaderByte.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngReaderFilter.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngReaderInt.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngWriter.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngWriterHc.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngjBadCrcException.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngjException.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngjExceptionInternal.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngjInputException.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngjOutputException.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/PngjUnsupportedException.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/RowInfo.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkCopyBehaviour.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkFactory.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkHelper.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkLoadBehaviour.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkPredicate.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkRaw.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/ChunksList.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/ChunksListForWrite.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngBadCharsetException.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunk.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkACTL.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkBKGD.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkCHRM.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkFCTL.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkFDAT.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkGAMA.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkHIST.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkICCP.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIDAT.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIEND.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIHDR.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkITXT.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkMultiple.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkOFFS.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkPHYS.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkPLTE.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSBIT.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSPLT.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSRGB.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSTER.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSingle.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTEXT.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTIME.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTRNS.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTextVar.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkUNKNOWN.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkZTXT.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/PngMetadata.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/chunks/package.html create mode 100644 src/js-specific/java/ar/com/hjg/pngj/package.html create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStream.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamDeflater.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamLz4.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorHjg.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorLz4.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/FiltersPerformance.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriter.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterDefault.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterMultiple.java create mode 100644 src/js-specific/java/ar/com/hjg/pngj/pixels/package.html create mode 100644 src/js-specific/java/org/warp/picalculator/deps/DSemaphore.java create mode 100644 src/js-specific/java/org/warp/picalculator/deps/DStandardOpenOption.java create mode 100644 src/js-specific/java/org/warp/picalculator/deps/DURLClassLoader.java create mode 100644 src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlEngine.java create mode 100644 src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlFont.java create mode 100644 src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlRenderer.java create mode 100644 src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlSkin.java create mode 100644 src/main/java/org/warp/picalculator/gui/graphicengine/html/InputEvent.java rename src/main/{resources => rules}/rules/ExpandRule1.java (96%) rename src/main/{resources => rules}/rules/ExpandRule2.java (95%) rename src/main/{resources => rules}/rules/ExpandRule5.java (96%) rename src/main/{resources => rules}/rules/ExponentRule1.java (94%) rename src/main/{resources => rules}/rules/ExponentRule15.java (95%) rename src/main/{resources => rules}/rules/ExponentRule16.java (96%) rename src/main/{resources => rules}/rules/ExponentRule17.java (95%) rename src/main/{resources => rules}/rules/ExponentRule2.java (94%) rename src/main/{resources => rules}/rules/ExponentRule3.java (94%) rename src/main/{resources => rules}/rules/ExponentRule4.java (95%) rename src/main/{resources => rules}/rules/ExponentRule8.java (95%) rename src/main/{resources => rules}/rules/ExponentRule9.java (95%) rename src/main/{resources => rules}/rules/FractionsRule1.java (95%) rename src/main/{resources => rules}/rules/FractionsRule10.java (95%) rename src/main/{resources => rules}/rules/FractionsRule11.java (95%) rename src/main/{resources => rules}/rules/FractionsRule12.java (95%) rename src/main/{resources => rules}/rules/FractionsRule14.java (96%) rename src/main/{resources => rules}/rules/FractionsRule2.java (94%) rename src/main/{resources => rules}/rules/FractionsRule3.java (94%) rename src/main/{resources => rules}/rules/FractionsRule4.java (95%) rename src/main/{resources => rules}/rules/FractionsRule5.java (96%) rename src/main/{resources => rules}/rules/FractionsRule6.java (95%) rename src/main/{resources => rules}/rules/FractionsRule7.java (84%) rename src/main/{resources => rules}/rules/FractionsRule8.java (84%) rename src/main/{resources => rules}/rules/FractionsRule9.java (89%) rename src/main/{resources => rules}/rules/NumberRule1.java (95%) rename src/main/{resources => rules}/rules/NumberRule2.java (95%) rename src/main/{resources => rules}/rules/NumberRule3.java (96%) rename src/main/{resources => rules}/rules/NumberRule4.java (95%) rename src/main/{resources => rules}/rules/NumberRule5.java (95%) rename src/main/{resources => rules}/rules/NumberRule7.java (95%) rename src/main/{resources => rules}/rules/UndefinedRule1.java (95%) rename src/main/{resources => rules}/rules/UndefinedRule2.java (95%) rename src/main/{resources => rules}/rules/VariableRule1.java (96%) rename src/main/{resources => rules}/rules/VariableRule2.java (95%) rename src/main/{resources => rules}/rules/VariableRule3.java (95%) rename src/main/{resources => rules}/rules/functions/DivisionRule.java (82%) rename src/main/{resources => rules}/rules/functions/EmptyNumberRule.java (93%) rename src/main/{resources => rules}/rules/functions/ExpressionRule.java (94%) rename src/main/{resources => rules}/rules/functions/JokeRule.java (93%) rename src/main/{resources => rules}/rules/functions/MultiplicationRule.java (92%) rename src/main/{resources => rules}/rules/functions/NegativeRule.java (89%) rename src/main/{resources => rules}/rules/functions/NumberRule.java (84%) rename src/main/{resources => rules}/rules/functions/PowerRule.java (86%) rename src/main/{resources => rules}/rules/functions/RootRule.java (75%) rename src/main/{resources => rules}/rules/functions/SubtractionRule.java (84%) rename src/main/{resources => rules}/rules/functions/SumRule.java (85%) rename src/main/{resources => rules}/rules/functions/SumSubtractionRule.java (81%) rename src/main/{resources => rules}/rules/functions/VariableRule.java (93%) diff --git a/.classpath b/.classpath index 7f11a3cd..7e5c742e 100644 --- a/.classpath +++ b/.classpath @@ -1,11 +1,6 @@ - - - - - @@ -18,13 +13,24 @@ - + - + + + + + + + + + + + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs index ca06868c..20149c2e 100644 --- a/.settings/org.eclipse.core.resources.prefs +++ b/.settings/org.eclipse.core.resources.prefs @@ -5,4 +5,5 @@ encoding//src/main/java=UTF-8 encoding//src/main/java/org/warp/picalculator/gui/expression/blocks/BlockParenthesis.java=UTF-8 encoding//src/main/java/org/warp/picalculator/math/MathematicalSymbols.java=UTF-8 encoding//src/main/resources=UTF-8 +encoding//src/main/rules=UTF-8 encoding//src/test/java=UTF-8 diff --git a/math-rules-cache.zip b/math-rules-cache.zip index c242a2f9a1ab3cbd666472ccad8a76b842bccfbc..447cb2cae3dbf8627fe0f6a490e2b8a626c6bb39 100644 GIT binary patch delta 1894 zcmY+FZA?>F7{~Xt6a)l@D~^RBjseb~159*-(m8Zk-YJ4W%UcmBZGp0GC}W9=(y zFss}-p~Gzd5|3LOcDZBzlpXuA|?+lLAitE=dpdYcW8uLGn zn?<&7cvZfR04Kbx72eJQZ*g*?0dtbyd{G(jY)@aFBX(_3sO>US$;OYHV$ON@7VOo8H&f`gc|q9{i`GQ@e-v|0Slco^-(GAp+H z#`F)eP-T#(Ph)y=Z4cUatVS=sNlA7+mK|}#q7F8>&^I(kBJcT-Hg)x$R(ZRA=kt*X zY=Hsd->u)$!~>()vf|JeSo-8}1!`&g1N8HbZ9@N(&U}pDIjKbr@4kip<1ceK3lXu9 zjE#?d{i%W-ymM8riJyOKZyDZ`zs`Uc=c%)5Zj1l^;ESp}&z8!#cff)A(x5+T%+P

pFvW9E1CZL{nGu-Pqh=aJ*)*>`7raN#a;-Ggal{i7hhWL%;kq zZO-7J1i_<*|FvVxPFNMJt?4yc+0gMd40+|@`i-yY>xwZ!I(A@0Me#6M4^$;t6 z-TM5Tm6~|BX#;4mxO*eq$A`8=z}G-vku*Wy2tMveE%eC0p2n9~52QdbzdV956@N5@ zbE#0vHc|-N24=1Ywh`ZxxIjldtRp`<4Xiw`BMmIP&te+bxSG>hoauNvu{MLaFN0W} zNxY9MtDnoH`hqNq|C~jaVBxK0Va&b==cayf}|| zB#$^@JF#m!v0pxMb3XC3q`3vu@23Ka$Lt{fYzOg@#Fj$h8-?U=E+RftM7*+?_@iRt zd5H}M;$IBp#~T^5eK}olRUO!b$^Ga)I-b4+}OE

^v@***#(!UCZtf)1$S-4<#|x0VcXZ=^Q#N?-W63%UhARwm?}ol(9sI(pAhWNZ8{C3GNkvQ*!S62FIbk zwxkPnEYu&>8>T{i7=A*=^NudJAuH5>AbX=^K0(jcZ4PvWsI~63tLM;&Ki2NXk}WAp)ctRzqF&S8M87X% zGxBWqpU7ABJLQrQyM5V+@h-19$bGP2ALcA6%8|AVb)7M9bbo6)fOcZ36-$0?{s&p8 zG)mJaF+I7q2kkpnlMmmlB)cA$9dyK^4mKS}-`K21-t|6h>WW>h(sKQ-=hX>pK&AWd z)^Die-e0g}`Toyw>5~KHs3q+W(9b)(0sW6U^D%zss1`M%`xg3-zsTV%M8-lg7C!vd z$J}r&?={O#I^C_kr3$WpodzGS6K7O1uK#}Tk19XMMrGVP;6PnA7>F7(^dIne)Nq7c zdGeX_Rp@G;(+Ky=;01Tr1-6E5NARTxto`c92Ar+t>MhhwzmB25Y_tb?=*Cmzp__Gh zc3-*8*0J@vaUJ>{Q@^8co2CQMGII_&ZkC>X-`ri~x(Ab(uYVMbS}|XWdST%@_kTD7 z*x%#u0Xa83JN@8^^r&&7yvMu_G`P5XJ>17HZHd6H!6~P}B58uY5&S%AE%ZnqPvg&552S#>ho_oS@kc{A zn+mmTNFjU+)NtLqh4`Mx1v=tk9r@8|VC8uoX<*?xp9VIrHR%)|O()i75cg#et1^i{ z5OpS#))!<^{HHA9)!D?KW)nXZ*_uOqJ%{`^a*6wL#rk@&z8>oMJooezH{=mt&Ld9P zN_>1PaX>zCb3XB;sJR7H@5cg)$8011WE=5<$d*Fl8-?U=EFwN#M7-2M{Gow(R%D}* z_-7;e@h0M9Cg7J;STMmmTph*W;5tvr|D}(pA zhL%G!-;cI(swXqEdW?6N;cc$dW@zMUs35&k!Rjzh+)moRoz-I;R>}G^ZLehgnJTI% z-c$wkoF}SSKc*$s)Nibs^<$h~L;cRz&^d~(rE_$smiVE_WftNQ3;A(Y;?Jzeoi;+N zjqFj8lj?}StfOrVs;6yisVBZGvc7@hLk(bA#Q(!C=3YM3*$7L86gy<|K|Ad1K(UK4 LZ>P-aM(F<^No}?@ diff --git a/math-rules-cache.zip114 b/math-rules-cache.zip114 new file mode 100644 index 0000000000000000000000000000000000000000..4df5c5b124841b8fcd696f45ee0ba49ea9f1b3a5 GIT binary patch literal 94104 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 literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml index 765b460d..b12c4746 100755 --- a/pom.xml +++ b/pom.xml @@ -8,15 +8,19 @@ 1.0-SNAPSHOT WarpPI Calculator http://warp.ovh - + 1.8 1.8 - src/main/java - + src/main/java + src/main/java + src/main/resources + teavm-dev - https://dl.bintray.com/konsoletyper/teavm + https://dl.bintray.com/konsoletyper/teavm + true + default oss-snapshots-repo @@ -28,12 +32,21 @@ + + + 1_teavm-dev + https://dl.bintray.com/konsoletyper/teavm + true + default + + jarprofile - - jar-specific - + + jar-specific + src/main/rules + true @@ -64,14 +77,9 @@ 1.3.2 - org.eclipse.jdt.core.compiler - ecj - 4.6.1 - - - ar.com.hjg - pngj - 2.1.0 + org.eclipse.jdt.core.compiler + ecj + 4.6.1 @@ -82,6 +90,9 @@ maven-compiler-plugin 2.3.2 + + org/warp/picalculator/gui/graphicengine/html/* + 1.8 1.8 UTF-8 @@ -92,21 +103,48 @@ jsprofile - - js-specific - + + js-specific + src/main/rules + false + + + org.teavm + teavm-classlib + 0.6.0-dev-529 + + + + org.codehaus.mojo + versions-maven-plugin + 2.5 + + + org.teavm:* + + + + + + use-latest-versions + + + + org.apache.maven.plugins maven-compiler-plugin 2.3.2 - org/warp/picalculator/gui/graphicengine/cpu/* + org/warp/picalculator/gui/graphicengine/cpu/CPUEngine + org/warp/picalculator/gui/graphicengine/cpu/CPURenderer + org/warp/picalculator/gui/graphicengine/cpu/SwingWindow org/warp/picalculator/gui/graphicengine/gpu/* org/warp/picalculator/gui/graphicengine/headless24bit/* org/warp/picalculator/gui/graphicengine/headless256/* @@ -121,16 +159,7 @@ org.teavm teavm-maven-plugin - 0.5.1 - - - - org.teavm - teavm-classlib - 0.5.1 - - + 0.6.0-dev-529 @@ -165,34 +194,56 @@ gson 2.8.2 + + + commons-io + commons-io + 2.6 + + + + + src/main/resources + + + ${src.resdir} + + WarpPICalculator - - org.codehaus.mojo - build-helper-maven-plugin - 3.0.0 - - - add-source - generate-sources - - add-source - - - - - ${basedir}/src/main/java - - - ${basedir}/src/${src.dir}/java - - - - - - + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + add-source + generate-sources + + add-source + + + + + ${basedir}/src/main/java + + + ${basedir}/src/${src.dir}/java + + + ${basedir}/${src.dir2} + + + + + + @@ -201,7 +252,7 @@ 2.9 true - false + true diff --git a/pom.xml.versionsBackup b/pom.xml.versionsBackup new file mode 100644 index 00000000..334e937b --- /dev/null +++ b/pom.xml.versionsBackup @@ -0,0 +1,276 @@ + + 4.0.0 + org.warp.picalculator + warppi-calculator + jar + 1.0-SNAPSHOT + WarpPI Calculator + http://warp.ovh + + 1.8 + 1.8 + src/main/java + + + + teavm-dev + https://dl.bintray.com/konsoletyper/teavm + + + oss-snapshots-repo + Sonatype OSS Maven Repository + https://oss.sonatype.org/content/groups/public + + true + always + + + + + + jarprofile + + jar-specific + + + true + + + + org.jogamp.jogl + jogl-all-main + 2.3.2 + + + org.jogamp.gluegen + gluegen-rt-main + 2.3.2 + + + com.pi4j + pi4j-core + 1.2-SNAPSHOT + + + org.fusesource.jansi + jansi + 1.15 + + + net.lingala.zip4j + zip4j + 1.3.2 + + + org.eclipse.jdt.core.compiler + ecj + 4.6.1 + + + ar.com.hjg + pngj + 2.1.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.8 + 1.8 + UTF-8 + + + + + + + jsprofile + + js-specific + + + false + + + + + org.codehaus.mojo + versions-maven-plugin + 2.5 + + + org.teavm:* + + + + + + use-latest-versions + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + + org/warp/picalculator/gui/graphicengine/cpu/* + org/warp/picalculator/gui/graphicengine/gpu/* + org/warp/picalculator/gui/graphicengine/headless24bit/* + org/warp/picalculator/gui/graphicengine/headless256/* + org/warp/picalculator/gui/graphicengine/headless8/* + org/warp/picalculator/gui/graphicengine/framebuffer/* + + 1.8 + 1.8 + UTF-8 + + + + org.teavm + teavm-maven-plugin + 0.5.1 + + + + org.teavm + teavm-classlib + 0.5.1 + + + + + + compile + + process-classes + + org.warp.picalculator.Main + true + true + true + + + + + + + + + + + + + org.teavm + teavm-classlib + 0.5.1 + + + + it.unimi.dsi + fastutil + 7.2.0 + + + com.google.code.gson + gson + 2.8.2 + + + + WarpPICalculator + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + add-source + generate-sources + + add-source + + + + + ${basedir}/src/main/java + + + ${basedir}/src/${src.dir}/java + + + + + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.9 + + true + true + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.2 + + UTF-8 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.4.1 + + + + jar-with-dependencies + + + + + org.warp.picalculator.Main + + + + + + + make-assembly + + package + + single + + + + + + + + diff --git a/src/jar-specific/java/ar/com/hjg/pngj/BufferedStreamFeeder.java b/src/jar-specific/java/ar/com/hjg/pngj/BufferedStreamFeeder.java new file mode 100644 index 00000000..ef93efc9 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/BufferedStreamFeeder.java @@ -0,0 +1,199 @@ +package ar.com.hjg.pngj; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Reads bytes from an input stream, and feeds a IBytesConsumer. + */ +public class BufferedStreamFeeder { + + private InputStream stream; + private byte[] buf; + private int pendinglen; // bytes read and stored in buf that have not yet still been fed to + // IBytesConsumer + private int offset; + private boolean eof = false; + private boolean closeStream = true; + private boolean failIfNoFeed = false; + + private static final int DEFAULTSIZE = 8192; + + /** By default, the stream will be closed on close() */ + public BufferedStreamFeeder(InputStream is) { + this(is, DEFAULTSIZE); + } + + public BufferedStreamFeeder(InputStream is, int bufsize) { + this.stream = is; + buf = new byte[bufsize < 1 ? DEFAULTSIZE : bufsize]; + } + + /** + * Returns inputstream + * + * @return Input Stream from which bytes are read + */ + public InputStream getStream() { + return stream; + } + + /** + * Feeds bytes to the consumer
+ * Returns bytes actually consumed
+ * This should return 0 only if the stream is EOF or the consumer is done + */ + public int feed(IBytesConsumer consumer) { + return feed(consumer, Integer.MAX_VALUE); + } + + /** + * Feeds the consumer (with at most maxbytes)
+ * Returns 0 only if the stream is EOF (or maxbytes=0). Returns negative is the consumer is done.
+ * It can return less than maxbytes (that doesn't mean that the consumer or the input stream is done) + */ + public int feed(IBytesConsumer consumer, int maxbytes) { + if (pendinglen == 0) + refillBuffer(); + int tofeed = maxbytes >= 0 && maxbytes < pendinglen ? maxbytes : pendinglen; + int n = 0; + if (tofeed > 0) { + n = consumer.consume(buf, offset, tofeed); + if (n > 0) { + offset += n; + pendinglen -= n; + } + } + if (n < 1 && failIfNoFeed) + throw new PngjInputException("Failed to feed bytes (premature ending?)"); + return n; + } + + + /** + * Feeds as much bytes as it can to the consumer, in a loop.
+ * Returns bytes actually consumed
+ * This will stop when either the input stream is eof, or when the consumer refuses to eat more bytes. The caller can + * distinguish both cases by calling {@link #hasMoreToFeed()} + */ + public long feedAll(IBytesConsumer consumer) { + long n = 0; + while (hasMoreToFeed()) { + int n1 = feed(consumer); + if (n1 < 1) + break; + n += n1; + } + return n; + } + + + /** + * Feeds exactly nbytes, retrying if necessary + * + * @param consumer Consumer + * @param nbytes Number of bytes + * @return true if success, false otherwise (EOF on stream, or consumer is done) + */ + public boolean feedFixed(IBytesConsumer consumer, int nbytes) { + int remain = nbytes; + while (remain > 0) { + int n = feed(consumer, remain); + if (n < 1) + return false; + remain -= n; + } + return true; + } + + /** + * If there are not pending bytes to be consumed tries to fill the buffer with bytes from the stream. + */ + protected void refillBuffer() { + if (pendinglen > 0 || eof) + return; // only if not pending data + try { + // try to read + offset = 0; + pendinglen = stream.read(buf); + if (pendinglen < 0) { + close(); + return; + } else + return; + } catch (IOException e) { + throw new PngjInputException(e); + } + } + + /** + * Returuns true if we have more data to fed the consumer. This internally tries to grabs more bytes from the stream + * if necessary + */ + public boolean hasMoreToFeed() { + if (eof) + return pendinglen > 0; + else + refillBuffer(); + return pendinglen > 0; + } + + /** + * @param closeStream If true, the underlying stream will be closed on when close() is called + */ + public void setCloseStream(boolean closeStream) { + this.closeStream = closeStream; + } + + /** + * Closes this object. + * + * Sets EOF=true, and closes the stream if closeStream is true + * + * This can be called internally, or from outside. + * + * Idempotent, secure, never throws exception. + **/ + public void close() { + eof = true; + buf = null; + pendinglen = 0; + offset = 0; + if (stream != null && closeStream) { + try { + stream.close(); + } catch (Exception e) { + // PngHelperInternal.LOGGER.log(Level.WARNING, "Exception closing stream", e); + } + } + stream = null; + } + + /** + * Sets a new underlying inputstream. This allows to reuse this object. The old underlying is not closed and the state + * is not reset (you should call close() previously if you want that) + * + * @param is + */ + public void setInputStream(InputStream is) { // to reuse this object + this.stream = is; + eof = false; + } + + /** + * @return EOF on stream, or close() was called + */ + public boolean isEof() { + return eof; + } + + /** + * If this flag is set (default: false), any call to feed() that returns zero (no byte feed) will throw an exception. + * This is useful to be sure of avoid infinite loops in some scenarios. + * + * @param failIfNoFeed + */ + public void setFailIfNoFeed(boolean failIfNoFeed) { + this.failIfNoFeed = failIfNoFeed; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ChunkReader.java b/src/jar-specific/java/ar/com/hjg/pngj/ChunkReader.java new file mode 100644 index 00000000..4c045853 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ChunkReader.java @@ -0,0 +1,216 @@ +package ar.com.hjg.pngj; + +import ar.com.hjg.pngj.chunks.ChunkRaw; + +/** + * Parses a PNG chunk, consuming bytes in one mode: {@link ChunkReaderMode#BUFFER}, {@link ChunkReaderMode#PROCESS}, + * {@link ChunkReaderMode#SKIP}. + *

+ * It calls {@link #chunkDone()} when done. Also calls {@link #processData(byte[], int, int)} if PROCESS + * mode. Apart from thas, it's totally agnostic (it doesn't know about IDAT chunks, or PNG general structure) + *

+ * The object wraps a ChunkRaw instance (content filled only if BUFFER mode); it should be short lived (one instance + * created for each chunk, and discarded after reading), but the wrapped chunkRaw can be (usually is) long lived. + */ +public abstract class ChunkReader { + + /** + * see {@link ChunkReaderMode} + */ + public final ChunkReaderMode mode; + private final ChunkRaw chunkRaw; + + private boolean crcCheck; // by default, this is false for SKIP, true elsewhere + + /** + * How many bytes have been read for this chunk, data only + */ + protected int read = 0; + private int crcn = 0; // how many bytes have been read from crc + + /** + * Modes of ChunkReader chunk processing. + */ + public enum ChunkReaderMode { + /** + * Stores full chunk data in buffer + */ + BUFFER, + /** + * Does not store content, processes on the fly, calling processData() for each partial read + */ + PROCESS, + /** + * Does not store nor process - implies crcCheck=false (by default). + */ + SKIP; + } + + /** + * The constructor creates also a chunkRaw, preallocated if mode = ChunkReaderMode.BUFFER + * + * @param clen + * @param id + * @param offsetInPng Informational, is stored in chunkRaw + * @param mode + */ + public ChunkReader(int clen, String id, long offsetInPng, ChunkReaderMode mode) { + if (mode == null || id.length() != 4 || clen < 0) + throw new PngjExceptionInternal("Bad chunk paramenters: " + mode); + this.mode = mode; + chunkRaw = new ChunkRaw(clen, id, mode == ChunkReaderMode.BUFFER); + chunkRaw.setOffset(offsetInPng); + this.crcCheck = mode == ChunkReaderMode.SKIP ? false : true; // can be changed with setter + } + + /** + * Returns raw chunk (data can be empty or not, depending on ChunkReaderMode) + * + * @return Raw chunk - never null + */ + public ChunkRaw getChunkRaw() { + return chunkRaw; + } + + /** + * Consumes data for the chunk (data and CRC). This never consumes more bytes than for this chunk. + * + * In HOT_PROCESS can call processData() (not more than once) + * + * If this ends the chunk (included CRC) it checks CRC (if checking) and calls chunkDone() + * + * @param buf + * @param off + * @param len + * @return How many bytes have been consumed + */ + public final int feedBytes(byte[] buf, int off, int len) { + if (len == 0) + return 0; + if (len < 0) + throw new PngjException("negative length??"); + if (read == 0 && crcn == 0 && crcCheck) + chunkRaw.updateCrc(chunkRaw.idbytes, 0, 4); // initializes crc calculation with the Chunk ID + int bytesForData = chunkRaw.len - read; // bytesForData : bytes to be actually read from chunk data + if (bytesForData > len) + bytesForData = len; + // we want to call processData even for empty chunks (IEND:bytesForData=0) at least once + if (bytesForData > 0 || crcn == 0) { + // in buffer mode we compute the CRC at the end + if (crcCheck && mode != ChunkReaderMode.BUFFER && bytesForData > 0) + chunkRaw.updateCrc(buf, off, bytesForData); + + if (mode == ChunkReaderMode.BUFFER) { + // just copy the contents to the internal buffer + if (chunkRaw.data != buf && bytesForData > 0) { + // if the buffer passed if the same as this one, we don't copy the caller should know what he's doing + System.arraycopy(buf, off, chunkRaw.data, read, bytesForData); + } + } else if (mode == ChunkReaderMode.PROCESS) { + processData(read, buf, off, bytesForData); + } else { + // mode == ChunkReaderMode.SKIP; nothing to do + } + read += bytesForData; + off += bytesForData; + len -= bytesForData; + } + int crcRead = 0; + if (read == chunkRaw.len) { // data done - read crc? + crcRead = 4 - crcn; + if (crcRead > len) + crcRead = len; + if (crcRead > 0) { + if (buf != chunkRaw.crcval) + System.arraycopy(buf, off, chunkRaw.crcval, crcn, crcRead); + crcn += crcRead; + if (crcn == 4) { + if (crcCheck) { + if (mode == ChunkReaderMode.BUFFER) { // in buffer mode we compute the CRC on one single call + chunkRaw.updateCrc(chunkRaw.data, 0, chunkRaw.len); + } + chunkRaw.checkCrc(); + } + chunkDone(); + } + } + } + return bytesForData + crcRead; + } + + /** + * Chunks has been read + * + * @return true if we have read all chunk, including trailing CRC + */ + public final boolean isDone() { + return crcn == 4; // has read all 4 bytes from the crc + } + + /** + * Determines if CRC should be checked. This should be called before starting reading. + * + * @param crcCheck + */ + public void setCrcCheck(boolean crcCheck) { + if (read != 0 && crcCheck && !this.crcCheck) + throw new PngjException("too late!"); + this.crcCheck = crcCheck; + } + + /** + * This method will only be called in PROCESS mode, probably several times, each time with a new fragment of data + * inside the chunk. For chunks with zero-length data, this will still be called once. + * + * It's guaranteed that the data corresponds exclusively to this chunk data (no crc, no data from no other chunks, ) + * + * @param offsetInchunk data bytes that had already been read/processed for this chunk + * @param buf + * @param off + * @param len + */ + protected abstract void processData(int offsetInchunk, byte[] buf, int off, int len); + + /** + * This method will be called (in all modes) when the full chunk -including crc- has been read + */ + protected abstract void chunkDone(); + + public boolean isFromDeflatedSet() { + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((chunkRaw == null) ? 0 : chunkRaw.hashCode()); + return result; + } + + /** + * Equality (and hash) is basically delegated to the ChunkRaw + */ + @Override + public boolean equals(Object obj) { // delegates to chunkraw + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ChunkReader other = (ChunkReader) obj; + if (chunkRaw == null) { + if (other.chunkRaw != null) + return false; + } else if (!chunkRaw.equals(other.chunkRaw)) + return false; + return true; + } + + @Override + public String toString() { + return chunkRaw.toString(); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqBuffering.java b/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqBuffering.java new file mode 100644 index 00000000..39e19dac --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqBuffering.java @@ -0,0 +1,30 @@ +package ar.com.hjg.pngj; + +/** + * This loads the png as a plain sequence of chunks, buffering all + * + * Useful to do things like insert or delete a ancilllary chunk. This does not distinguish IDAT from others + **/ +public class ChunkSeqBuffering extends ChunkSeqReader { + protected boolean checkCrc = true; + + public ChunkSeqBuffering() { + super(); + } + + @Override + protected boolean isIdatKind(String id) { + return false; + } + + @Override + protected boolean shouldCheckCrc(int len, String id) { + return checkCrc; + } + + public void setCheckCrc(boolean checkCrc) { + this.checkCrc = checkCrc; + } + + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqReader.java b/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqReader.java new file mode 100644 index 00000000..82b661f1 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqReader.java @@ -0,0 +1,396 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Arrays; + +import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode; +import ar.com.hjg.pngj.chunks.ChunkHelper; + +/** + * Consumes a stream of bytes that consist of a series of PNG-like chunks. + *

+ * This has little intelligence, it's quite low-level and general (it could even be used for a MNG stream, for example). + * It supports signature recognition and idat deflate + */ +public class ChunkSeqReader implements IBytesConsumer { + + protected static final int SIGNATURE_LEN = 8; + protected final boolean withSignature; + + private byte[] buf0 = new byte[8]; // for signature or chunk starts + private int buf0len = 0; + + private boolean signatureDone = false; + private boolean done = false; // ended, normally or not + + private int chunkCount = 0; + + private long bytesCount = 0; + + private DeflatedChunksSet curReaderDeflatedSet; // one instance is created for each + // "idat-like set". Normally one. + + private ChunkReader curChunkReader; + + private long idatBytes; // this is only for the IDAT (not mrerely "idat-like") + + /** + * Creates a ChunkSeqReader (with signature) + */ + public ChunkSeqReader() { + this(true); + } + + /** + * @param withSignature If true, the stream is assumed be prepended by 8 bit signature + */ + public ChunkSeqReader(boolean withSignature) { + this.withSignature = withSignature; + signatureDone = !withSignature; + } + + /** + * Consumes (in general, partially) a number of bytes. A single call never involves more than one chunk. + * + * When the signature is read, it calls checkSignature() + * + * When the start of a chunk is detected, it calls {@link #startNewChunk(int, String, long)} + * + * When data from a chunk is being read, it delegates to {@link ChunkReader#feedBytes(byte[], int, int)} + * + * The caller might want to call this method more than once in succesion + * + * This should rarely be overriden + * + * @param buffer + * @param offset Offset in buffer + * @param len Valid bytes that can be consumed + * @return processed bytes, in the 1-len range. -1 if done. Only returns 0 if len=0. + **/ + public int consume(byte[] buffer, int offset, int len) { + if (done) + return -1; + if (len == 0) + return 0; // nothing to do + if (len < 0) + throw new PngjInputException("Bad len: " + len); + int processed = 0; + if (signatureDone) { + if (curChunkReader == null || curChunkReader.isDone()) { // new chunk: read first 8 bytes + int read0 = 8 - buf0len; + if (read0 > len) + read0 = len; + System.arraycopy(buffer, offset, buf0, buf0len, read0); + buf0len += read0; + processed += read0; + bytesCount += read0; + // len -= read0; + // offset += read0; + if (buf0len == 8) { // end reading chunk length and id + chunkCount++; + int clen = PngHelperInternal.readInt4fromBytes(buf0, 0); + String cid = ChunkHelper.toString(buf0, 4, 4); + startNewChunk(clen, cid, bytesCount - 8); + buf0len = 0; + } + } else { // reading chunk, delegates to curChunkReader + int read1 = curChunkReader.feedBytes(buffer, offset, len); + processed += read1; + bytesCount += read1; + } + } else { // reading signature + int read = SIGNATURE_LEN - buf0len; + if (read > len) + read = len; + System.arraycopy(buffer, offset, buf0, buf0len, read); + buf0len += read; + if (buf0len == SIGNATURE_LEN) { + checkSignature(buf0); + buf0len = 0; + signatureDone = true; + } + processed += read; + bytesCount += read; + } + return processed; + } + + /** + * Trys to feeds exactly len bytes, calling {@link #consume(byte[], int, int)} retrying if necessary. + * + * This should only be used in callback mode + * + * @return true if succceded + */ + public boolean feedAll(byte[] buf, int off, int len) { + while (len > 0) { + int n = consume(buf, off, len); + if (n < 1) + return false; + len -= n; + off += n; + } + return true; + } + + /** + * Called for all chunks when a chunk start has been read (id and length), before the chunk data itself is read. It + * creates a new ChunkReader (field accesible via {@link #getCurChunkReader()}) in the corresponding mode, and + * eventually a curReaderDeflatedSet.(field accesible via {@link #getCurReaderDeflatedSet()}) + * + * To decide the mode and options, it calls {@link #shouldCheckCrc(int, String)}, + * {@link #shouldSkipContent(int, String)}, {@link #isIdatKind(String)}. Those methods should be overriden in + * preference to this; if overriden, this should be called first. + * + * The respective {@link ChunkReader#chunkDone()} method is directed to this {@link #postProcessChunk(ChunkReader)}. + * + * Instead of overriding this, see also {@link #createChunkReaderForNewChunk(String, int, long, boolean)} + */ + protected void startNewChunk(int len, String id, long offset) { + if (id.equals(ChunkHelper.IDAT)) + idatBytes += len; + boolean checkCrc = shouldCheckCrc(len, id); + boolean skip = shouldSkipContent(len, id); + boolean isIdatType = isIdatKind(id); + // PngHelperInternal.debug("start new chunk id=" + id + " off=" + offset + " skip=" + skip + " idat=" + + // isIdatType); + // first see if we should terminate an active curReaderDeflatedSet + boolean forCurrentIdatSet = false; + if (curReaderDeflatedSet != null) + forCurrentIdatSet = curReaderDeflatedSet.ackNextChunkId(id); + if (isIdatType && !skip) { // IDAT non skipped: create a DeflatedChunkReader owned by a idatSet + if (!forCurrentIdatSet) { + if (curReaderDeflatedSet != null && !curReaderDeflatedSet.isDone()) + throw new PngjInputException("new IDAT-like chunk when previous was not done"); + curReaderDeflatedSet = createIdatSet(id); + } + curChunkReader = new DeflatedChunkReader(len, id, checkCrc, offset, curReaderDeflatedSet) { + @Override + protected void chunkDone() { + super.chunkDone(); + postProcessChunk(this); + } + }; + + } else { // for non-idat chunks (or skipped idat like) + curChunkReader = createChunkReaderForNewChunk(id, len, offset, skip); + if (!checkCrc) + curChunkReader.setCrcCheck(false); + } + } + + /** + * This will be called for all chunks (even skipped), except for IDAT-like non-skiped chunks + * + * The default behaviour is to create a ChunkReader in BUFFER mode (or SKIP if skip==true) that calls + * {@link #postProcessChunk(ChunkReader)} (always) when done. + * + * @param id Chunk id + * @param len Chunk length + * @param offset offset inside PNG stream , merely informative + * @param skip flag: is true, the content will not be buffered (nor processed) + * @return a newly created ChunkReader that will create the ChunkRaw and then discarded + */ + protected ChunkReader createChunkReaderForNewChunk(String id, int len, long offset, boolean skip) { + return new ChunkReader(len, id, offset, skip ? ChunkReaderMode.SKIP : ChunkReaderMode.BUFFER) { + @Override + protected void chunkDone() { + postProcessChunk(this); + } + + @Override + protected void processData(int offsetinChhunk, byte[] buf, int off, int len) { + throw new PngjExceptionInternal("should never happen"); + } + }; + } + + /** + * This is called after a chunk is read, in all modes + * + * This implementation only chenks the id of the first chunk, and process the IEND chunk (sets done=true) + ** + * Further processing should be overriden (call this first!) + **/ + protected void postProcessChunk(ChunkReader chunkR) { // called after chunk is read + if (chunkCount == 1) { + String cid = firstChunkId(); + if (cid != null && !cid.equals(chunkR.getChunkRaw().id)) + throw new PngjInputException("Bad first chunk: " + chunkR.getChunkRaw().id + " expected: " + + firstChunkId()); + } + if (chunkR.getChunkRaw().id.equals(endChunkId())) + done = true; + } + + /** + * DeflatedChunksSet factory. This implementation is quite dummy, it usually should be overriden. + */ + protected DeflatedChunksSet createIdatSet(String id) { + return new DeflatedChunksSet(id, 1024, 1024); // sizes: arbitrary This should normally be + // overriden + } + + /** + * Decides if this Chunk is of "IDAT" kind (in concrete: if it is, and if it's not to be skiped, a DeflatedChunksSet + * will be created to deflate it and process+ the deflated data) + * + * This implementation always returns always false + * + * @param id + */ + protected boolean isIdatKind(String id) { + return false; + } + + /** + * Chunks can be skipped depending on id and/or length. Skipped chunks are still processed, but their data will be + * null, and CRC will never checked + * + * @param len + * @param id + */ + protected boolean shouldSkipContent(int len, String id) { + return false; + } + + protected boolean shouldCheckCrc(int len, String id) { + return true; + } + + /** + * Throws PngjInputException if bad signature + * + * @param buf Signature. Should be of length 8 + */ + protected void checkSignature(byte[] buf) { + if (!Arrays.equals(buf, PngHelperInternal.getPngIdSignature())) + throw new PngjInputException("Bad PNG signature"); + } + + /** + * If false, we are still reading the signature + * + * @return true if signature has been read (or if we don't have signature) + */ + public boolean isSignatureDone() { + return signatureDone; + } + + /** + * If true, we either have processe the IEND chunk, or close() has been called, or a fatal error has happened + */ + public boolean isDone() { + return done; + } + + /** + * total of bytes read (buffered or not) + */ + public long getBytesCount() { + return bytesCount; + } + + /** + * @return Chunks already read, including partial reading (currently reading) + */ + public int getChunkCount() { + return chunkCount; + } + + /** + * Currently reading chunk, or just ended reading + * + * @return null only if still reading signature + */ + public ChunkReader getCurChunkReader() { + return curChunkReader; + } + + /** + * The latest deflated set (typically IDAT chunks) reader. Notice that there could be several idat sets (eg for APNG) + */ + public DeflatedChunksSet getCurReaderDeflatedSet() { + return curReaderDeflatedSet; + } + + /** + * Closes this object and release resources. For normal termination or abort. Secure and idempotent. + */ + public void close() { // forced closing + if (curReaderDeflatedSet != null) + curReaderDeflatedSet.close(); + done = true; + } + + /** + * Returns true if we are not in middle of a chunk: we have just ended reading past chunk , or we are at the start, or + * end of signature, or we are done + */ + public boolean isAtChunkBoundary() { + return bytesCount == 0 || bytesCount == 8 || done || curChunkReader == null + || curChunkReader.isDone(); + } + + /** + * Which should be the id of the first chunk + * + * @return null if you don't want to check it + */ + protected String firstChunkId() { + return "IHDR"; + } + + /** + * Helper method, reports amount of bytes inside IDAT chunks. + * + * @return Bytes in IDAT chunks + */ + public long getIdatBytes() { + return idatBytes; + } + + /** + * Which should be the id of the last chunk + * + * @return "IEND" + */ + protected String endChunkId() { + return "IEND"; + } + + /** + * Reads all content from a file. Helper method, only for callback mode + */ + public void feedFromFile(File f) { + try { + feedFromInputStream(new FileInputStream(f), true); + } catch (FileNotFoundException e) { + throw new PngjInputException(e.getMessage()); + } + } + + /** + * Reads all content from an input stream. Helper method, only for callback mode + * + * @param is + * @param closeStream Closes the input stream when done (or if error) + */ + public void feedFromInputStream(InputStream is, boolean closeStream) { + BufferedStreamFeeder sf = new BufferedStreamFeeder(is); + sf.setCloseStream(closeStream); + try { + sf.feedAll(this); + } finally { + close(); + sf.close(); + } + } + + public void feedFromInputStream(InputStream is) { + feedFromInputStream(is, true); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqReaderPng.java b/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqReaderPng.java new file mode 100644 index 00000000..44779c44 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqReaderPng.java @@ -0,0 +1,313 @@ +package ar.com.hjg.pngj; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode; +import ar.com.hjg.pngj.chunks.ChunkFactory; +import ar.com.hjg.pngj.chunks.ChunkHelper; +import ar.com.hjg.pngj.chunks.ChunkLoadBehaviour; +import ar.com.hjg.pngj.chunks.ChunksList; +import ar.com.hjg.pngj.chunks.PngChunk; +import ar.com.hjg.pngj.chunks.PngChunkIDAT; +import ar.com.hjg.pngj.chunks.PngChunkIEND; +import ar.com.hjg.pngj.chunks.PngChunkIHDR; +import ar.com.hjg.pngj.chunks.PngChunkPLTE; + +/** + * Adds to ChunkSeqReader the storing of PngChunk, with a PngFactory, and imageInfo + deinterlacer. + *

+ * Most usual PNG reading should use this class, or a {@link PngReader}, which is a thin wrapper over this. + */ +public class ChunkSeqReaderPng extends ChunkSeqReader { + + protected ImageInfo imageInfo; // initialized at parsing the IHDR + protected ImageInfo curImageInfo; // can vary, for apng + protected Deinterlacer deinterlacer; + protected int currentChunkGroup = -1; + + /** + * All chunks, but some of them can have the buffer empty (IDAT and skipped) + */ + protected ChunksList chunksList = null; + protected final boolean callbackMode; + private long bytesAncChunksLoaded = 0; // bytes loaded from buffered chunks non-critical chunks (data only) + + private boolean checkCrc = true; + + // --- parameters to be set prior to reading --- + private boolean includeNonBufferedChunks = false; + + private Set chunksToSkip = new HashSet(); + private long maxTotalBytesRead = 0; + private long skipChunkMaxSize = 0; + private long maxBytesMetadata = 0; + private IChunkFactory chunkFactory; + private ChunkLoadBehaviour chunkLoadBehaviour = ChunkLoadBehaviour.LOAD_CHUNK_ALWAYS; + + public ChunkSeqReaderPng(boolean callbackMode) { + super(); + this.callbackMode = callbackMode; + chunkFactory = new ChunkFactory(); // default factory + } + + private void updateAndCheckChunkGroup(String id) { + if (id.equals(PngChunkIHDR.ID)) { // IDHR + if (currentChunkGroup < 0) + currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR; + else + throw new PngjInputException("unexpected chunk " + id); + } else if (id.equals(PngChunkPLTE.ID)) { // PLTE + if ((currentChunkGroup == ChunksList.CHUNK_GROUP_0_IDHR || currentChunkGroup == ChunksList.CHUNK_GROUP_1_AFTERIDHR)) + currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE; + else + throw new PngjInputException("unexpected chunk " + id); + } else if (id.equals(PngChunkIDAT.ID)) { // IDAT (no necessarily the first) + if ((currentChunkGroup >= ChunksList.CHUNK_GROUP_0_IDHR && currentChunkGroup <= ChunksList.CHUNK_GROUP_4_IDAT)) + currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT; + else + throw new PngjInputException("unexpected chunk " + id); + } else if (id.equals(PngChunkIEND.ID)) { // END + if ((currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT)) + currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; + else + throw new PngjInputException("unexpected chunk " + id); + } else { // ancillary + if (currentChunkGroup <= ChunksList.CHUNK_GROUP_1_AFTERIDHR) + currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + else if (currentChunkGroup <= ChunksList.CHUNK_GROUP_3_AFTERPLTE) + currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE; + else + currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; + } + } + + @Override + public boolean shouldSkipContent(int len, String id) { + if (super.shouldSkipContent(len, id)) + return true; + if (ChunkHelper.isCritical(id)) + return false;// critical chunks are never skipped + if (maxTotalBytesRead > 0 && len + getBytesCount() > maxTotalBytesRead) + throw new PngjInputException("Maximum total bytes to read exceeeded: " + maxTotalBytesRead + + " offset:" + getBytesCount() + " len=" + len); + if (chunksToSkip.contains(id)) + return true; // specific skip + if (skipChunkMaxSize > 0 && len > skipChunkMaxSize) + return true; // too big chunk + if (maxBytesMetadata > 0 && len > maxBytesMetadata - bytesAncChunksLoaded) + return true; // too much ancillary chunks loaded + switch (chunkLoadBehaviour) { + case LOAD_CHUNK_IF_SAFE: + if (!ChunkHelper.isSafeToCopy(id)) + return true; + break; + case LOAD_CHUNK_NEVER: + return true; + default: + break; + } + return false; + } + + public long getBytesChunksLoaded() { + return bytesAncChunksLoaded; + } + + public int getCurrentChunkGroup() { + return currentChunkGroup; + } + + public void setChunksToSkip(String... chunksToSkip) { + this.chunksToSkip.clear(); + for (String c : chunksToSkip) + this.chunksToSkip.add(c); + } + + public void addChunkToSkip(String chunkToSkip) { + this.chunksToSkip.add(chunkToSkip); + } + + public void dontSkipChunk(String chunkToSkip) { + this.chunksToSkip.remove(chunkToSkip); + } + + public boolean firstChunksNotYetRead() { + return getCurrentChunkGroup() < ChunksList.CHUNK_GROUP_4_IDAT; + } + + @Override + protected void postProcessChunk(ChunkReader chunkR) { + super.postProcessChunk(chunkR); + if (chunkR.getChunkRaw().id.equals(PngChunkIHDR.ID)) { + PngChunkIHDR ch = new PngChunkIHDR(null); + ch.parseFromRaw(chunkR.getChunkRaw()); + imageInfo = ch.createImageInfo(); + curImageInfo = imageInfo; + if (ch.isInterlaced()) + deinterlacer = new Deinterlacer(curImageInfo); + chunksList = new ChunksList(imageInfo); + } + if (chunkR.mode == ChunkReaderMode.BUFFER && countChunkTypeAsAncillary(chunkR.getChunkRaw().id)) { + bytesAncChunksLoaded += chunkR.getChunkRaw().len; + } + if (chunkR.mode == ChunkReaderMode.BUFFER || includeNonBufferedChunks) { + PngChunk chunk = chunkFactory.createChunk(chunkR.getChunkRaw(), getImageInfo()); + chunksList.appendReadChunk(chunk, currentChunkGroup); + } + if (isDone()) { + processEndPng(); + } + } + + protected boolean countChunkTypeAsAncillary(String id) { + return !ChunkHelper.isCritical(id); + } + + @Override + protected DeflatedChunksSet createIdatSet(String id) { + IdatSet ids = new IdatSet(id, getCurImgInfo(), deinterlacer); + ids.setCallbackMode(callbackMode); + return ids; + } + + public IdatSet getIdatSet() { + DeflatedChunksSet c = getCurReaderDeflatedSet(); + return c instanceof IdatSet ? (IdatSet) c : null; + } + + @Override + protected boolean isIdatKind(String id) { + return id.equals(PngChunkIDAT.ID); + } + + @Override + public int consume(byte[] buf, int off, int len) { + return super.consume(buf, off, len); + } + + /** + * sets a custom chunk factory. This is typically called with a custom class extends ChunkFactory, to adds custom + * chunks to the default well-know ones + * + * @param chunkFactory + */ + public void setChunkFactory(IChunkFactory chunkFactory) { + this.chunkFactory = chunkFactory; + } + + /** + * Things to be done after IEND processing. This is not called if prematurely closed. + */ + protected void processEndPng() { + // nothing to do + } + + public ImageInfo getImageInfo() { + return imageInfo; + } + + public boolean isInterlaced() { + return deinterlacer != null; + } + + public Deinterlacer getDeinterlacer() { + return deinterlacer; + } + + @Override + protected void startNewChunk(int len, String id, long offset) { + updateAndCheckChunkGroup(id); + super.startNewChunk(len, id, offset); + } + + @Override + public void close() { + if (currentChunkGroup != ChunksList.CHUNK_GROUP_6_END)// this could only happen if forced close + currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; + super.close(); + } + + public List getChunks() { + return chunksList.getChunks(); + } + + public void setMaxTotalBytesRead(long maxTotalBytesRead) { + this.maxTotalBytesRead = maxTotalBytesRead; + } + + public long getSkipChunkMaxSize() { + return skipChunkMaxSize; + } + + public void setSkipChunkMaxSize(long skipChunkMaxSize) { + this.skipChunkMaxSize = skipChunkMaxSize; + } + + public long getMaxBytesMetadata() { + return maxBytesMetadata; + } + + public void setMaxBytesMetadata(long maxBytesMetadata) { + this.maxBytesMetadata = maxBytesMetadata; + } + + public long getMaxTotalBytesRead() { + return maxTotalBytesRead; + } + + @Override + protected boolean shouldCheckCrc(int len, String id) { + return checkCrc; + } + + public boolean isCheckCrc() { + return checkCrc; + } + + public void setCheckCrc(boolean checkCrc) { + this.checkCrc = checkCrc; + } + + public boolean isCallbackMode() { + return callbackMode; + } + + public Set getChunksToSkip() { + return chunksToSkip; + } + + public void setChunkLoadBehaviour(ChunkLoadBehaviour chunkLoadBehaviour) { + this.chunkLoadBehaviour = chunkLoadBehaviour; + } + + public ImageInfo getCurImgInfo() { + return curImageInfo; + } + + public void updateCurImgInfo(ImageInfo iminfo) { + if (!iminfo.equals(curImageInfo)) { + curImageInfo = iminfo; + } + if (deinterlacer != null) + deinterlacer = new Deinterlacer(curImageInfo); // we could reset it, but... + } + + /** + * If true, the chunks with no data (because skipped or because processed like IDAT-type) are still stored in the + * PngChunks list, which might be more informative. + * + * Setting this to false saves a few bytes + * + * Default: false + * + * @param includeNonBufferedChunks + */ + public void setIncludeNonBufferedChunks(boolean includeNonBufferedChunks) { + this.includeNonBufferedChunks = includeNonBufferedChunks; + } + + + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqSkipping.java b/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqSkipping.java new file mode 100644 index 00000000..5448c118 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ChunkSeqSkipping.java @@ -0,0 +1,70 @@ +package ar.com.hjg.pngj; + +import java.util.ArrayList; +import java.util.List; + +import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode; +import ar.com.hjg.pngj.chunks.ChunkRaw; + +/** + * This simple reader skips all chunks contents and stores the chunkRaw in a list. Useful to read chunks structure. + * + * Optionally the contents might be processed. This doesn't distinguish IDAT chunks + */ +public class ChunkSeqSkipping extends ChunkSeqReader { + + private List chunks = new ArrayList(); + private boolean skip = true; + + /** + * @param skipAll if true, contents will be truly skipped, and CRC will not be computed + */ + public ChunkSeqSkipping(boolean skipAll) { + super(true); + skip = skipAll; + } + + public ChunkSeqSkipping() { + this(true); + } + + protected ChunkReader createChunkReaderForNewChunk(String id, int len, long offset, boolean skip) { + return new ChunkReader(len, id, offset, skip ? ChunkReaderMode.SKIP : ChunkReaderMode.PROCESS) { + @Override + protected void chunkDone() { + postProcessChunk(this); + } + + @Override + protected void processData(int offsetinChhunk, byte[] buf, int off, int len) { + processChunkContent(getChunkRaw(), offsetinChhunk, buf, off, len); + } + }; + } + + protected void processChunkContent(ChunkRaw chunkRaw, int offsetinChhunk, byte[] buf, int off, + int len) { + // does nothing + } + + @Override + protected void postProcessChunk(ChunkReader chunkR) { + super.postProcessChunk(chunkR); + chunks.add(chunkR.getChunkRaw()); + } + + @Override + protected boolean shouldSkipContent(int len, String id) { + return skip; + } + + @Override + protected boolean isIdatKind(String id) { + return false; + } + + public List getChunks() { + return chunks; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/DeflatedChunkReader.java b/src/jar-specific/java/ar/com/hjg/pngj/DeflatedChunkReader.java new file mode 100644 index 00000000..6abac098 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/DeflatedChunkReader.java @@ -0,0 +1,83 @@ +package ar.com.hjg.pngj; + +import ar.com.hjg.pngj.chunks.PngChunkFDAT; + +/** + * + * Specialization of ChunkReader, for IDAT-like chunks. These chunks are part of a set of similar chunks (contiguos + * normally, not necessariyl) which conforms a zlib stream + */ +public class DeflatedChunkReader extends ChunkReader { + + protected final DeflatedChunksSet deflatedChunksSet; + protected boolean alsoBuffer = false; + + protected boolean skipBytes = false; // fDAT (APNG) skips 4 bytes) + protected byte[] skippedBytes; // only for fDAT + protected int seqNumExpected = -1; // only for fDAT + + public DeflatedChunkReader(int clen, String chunkid, boolean checkCrc, long offsetInPng, + DeflatedChunksSet iDatSet) { + super(clen, chunkid, offsetInPng, ChunkReaderMode.PROCESS); + this.deflatedChunksSet = iDatSet; + if (chunkid.equals(PngChunkFDAT.ID)) { + skipBytes = true; + skippedBytes = new byte[4]; + } + iDatSet.appendNewChunk(this); + } + + /** + * Delegates to ChunkReaderDeflatedSet.processData() + */ + @Override + protected void processData(int offsetInchunk, byte[] buf, int off, int len) { + if (skipBytes && offsetInchunk < 4) {// only for APNG (sigh) + for (int oc = offsetInchunk; oc < 4 && len > 0; oc++, off++, len--) + skippedBytes[oc] = buf[off]; + } + if (len > 0) { // delegate to idatSet + deflatedChunksSet.processBytes(buf, off, len); + if (alsoBuffer) { // very rare! + System.arraycopy(buf, off, getChunkRaw().data, read, len); + } + } + } + + /** + * only a stupid check for fDAT (I wonder how many APGN readers do this) + */ + @Override + protected void chunkDone() { + if (skipBytes && getChunkRaw().id.equals(PngChunkFDAT.ID)) { + if (seqNumExpected >= 0) { + int seqNum = PngHelperInternal.readInt4fromBytes(skippedBytes, 0); + if (seqNum != seqNumExpected) + throw new PngjInputException("bad chunk sequence for fDAT chunk " + seqNum + " expected " + + seqNumExpected); + } + } + } + + @Override + public boolean isFromDeflatedSet() { + return true; + } + + /** + * In some rare cases you might want to also buffer the data? + */ + public void setAlsoBuffer() { + if (read > 0) + throw new RuntimeException("too late"); + alsoBuffer = true; + getChunkRaw().allocData(); + } + + /** only relevant for fDAT */ + public void setSeqNumExpected(int seqNumExpected) { + this.seqNumExpected = seqNumExpected; + } + + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/DeflatedChunksSet.java b/src/jar-specific/java/ar/com/hjg/pngj/DeflatedChunksSet.java new file mode 100644 index 00000000..1d496e8c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/DeflatedChunksSet.java @@ -0,0 +1,417 @@ +package ar.com.hjg.pngj; + +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * A set of IDAT-like chunks which, concatenated, form a zlib stream. + *

+ * The inflated stream is intented to be read as a sequence of "rows", of which the caller knows the lengths (not + * necessary equal) and number. + *

+ * Eg: For IDAT non-interlaced images, a row has bytesPerRow + 1 filter byte
+ * For interlaced images, the lengths are variable. + *

+ * This class can work in sync (polled) mode or async (callback) mode. But for callback mode the method + * processRowCallback() must be overriden + *

+ * See {@link IdatSet}, which is mostly used and has a slightly simpler use.
+ * See DeflatedChunkSetTest for example of use. + */ +public class DeflatedChunksSet { + + protected byte[] row; // a "row" here means a raw (uncopressed filtered) part of the IDAT stream, + // normally a image row (or subimage row for interlaced) plus a filter byte + private int rowfilled; // effective/valid length of row + private int rowlen; // what amount of bytes is to be interpreted as a complete "row". can change + // (for interlaced) + private int rown; // only coincide with image row if non-interlaced - incremented by + // setNextRowSize() + + /* + * States WAITING_FOR_INPUT ROW_READY WORK_DONE TERMINATED + * + * processBytes() is externally called, prohibited in READY (in DONE it's ignored) + * + * WARNING: inflater.finished() != DONE (not enough, not neccesary) DONE means that we have already uncompressed all + * the data of interest. + * + * In non-callback mode, prepareForNextRow() is also externally called, in + * + * Flow: - processBytes() calls inflateData() - inflateData() : if buffer is filled goes to READY else if ! + * inf.finished goes to WAITING else if any data goes to READY (incomplete data to be read) else goes to DONE - in + * Callback mode, after going to READY, n=processCallback() is called and then prepareForNextRow(n) is called. - in + * Polled mode, prepareForNextRow(n) must be called from outside (after checking state=READY) - prepareForNextRow(n) + * goes to DONE if n==0 calls inflateData() again - end() goes to DONE + */ + private enum State { + WAITING_FOR_INPUT, // waiting for more input + ROW_READY, // ready for consumption (might be less than fully filled), ephemeral for CALLBACK + // mode + WORK_DONE, // all data of interest has been read, but we might accept still more trailing chunks + // (we'll ignore them) + TERMINATED; // we are done, and also won't accept more IDAT chunks + + public boolean isDone() { + return this == WORK_DONE || this == TERMINATED; + } // the caller has already uncompressed all the data of interest or EOF + + public boolean isTerminated() { + return this == TERMINATED; + } // we dont accept more chunks + } + + State state = State.WAITING_FOR_INPUT; // never null + + private Inflater inf; + private final boolean infOwn; // true if we own the inflater (we created it) + + private DeflatedChunkReader curChunk; + + private boolean callbackMode = true; + private long nBytesIn = 0; // count the total compressed bytes that have been fed + private long nBytesOut = 0; // count the total uncompressed bytes + int chunkNum = -1; // incremented at each new chunk start + int firstChunqSeqNum = -1; // expected seq num for first chunk. used only for fDAT (APNG) + + /** + * All IDAT-like chunks that form a same DeflatedChunksSet should have the same id + */ + public final String chunkid; + + /** + * @param initialRowLen Length in bytes of first "row" (see description) + * @param maxRowLen Max length in bytes of "rows" + * @param inflater Can be null. If not null, must be already reset (and it must be closed/released by caller!) + */ + public DeflatedChunksSet(String chunkid, int initialRowLen, int maxRowLen, Inflater inflater, + byte[] buffer) { + this.chunkid = chunkid; + this.rowlen = initialRowLen; + if (initialRowLen < 1 || maxRowLen < initialRowLen) + throw new PngjException("bad inital row len " + initialRowLen); + if (inflater != null) { + this.inf = inflater; + infOwn = false; + } else { + this.inf = new Inflater(); + infOwn = true; // inflater is own, we will release on close() + } + this.row = buffer != null && buffer.length >= initialRowLen ? buffer : new byte[maxRowLen]; + rown = -1; + this.state = State.WAITING_FOR_INPUT; + try { + prepareForNextRow(initialRowLen); + } catch (RuntimeException e) { + close(); + throw e; + } + } + + public DeflatedChunksSet(String chunkid, int initialRowLen, int maxRowLen) { + this(chunkid, initialRowLen, maxRowLen, null, null); + } + + protected void appendNewChunk(DeflatedChunkReader cr) { + // all chunks must have same id + if (!this.chunkid.equals(cr.getChunkRaw().id)) + throw new PngjInputException("Bad chunk inside IdatSet, id:" + cr.getChunkRaw().id + + ", expected:" + this.chunkid); + this.curChunk = cr; + chunkNum++; + if (firstChunqSeqNum >= 0) + cr.setSeqNumExpected(chunkNum + firstChunqSeqNum); + } + + /** + * Feeds the inflater with the compressed bytes + * + * In poll mode, the caller should not call repeatedly this, without consuming first, checking + * isDataReadyForConsumer() + * + * @param buf + * @param off + * @param len + */ + protected void processBytes(byte[] buf, int off, int len) { + nBytesIn += len; + // PngHelperInternal.LOGGER.info("processing compressed bytes in chunkreader : " + len); + if (len < 1 || state.isDone()) + return; + if (state == State.ROW_READY) + throw new PngjInputException("this should only be called if waitingForMoreInput"); + if (inf.needsDictionary() || !inf.needsInput()) + throw new RuntimeException("should not happen"); + inf.setInput(buf, off, len); + // PngHelperInternal.debug("entering processs bytes, state=" + state + + // " callback="+callbackMode); + if (isCallbackMode()) { + while (inflateData()) { + int nextRowLen = processRowCallback(); + prepareForNextRow(nextRowLen); + if (isDone()) + processDoneCallback(); + } + } else + inflateData(); + } + + /* + * This never inflates more than one row This returns true if this has resulted in a row being ready and preprocessed + * with preProcessRow (in callback mode, we should call immediately processRowCallback() and + * prepareForNextRow(nextRowLen) + */ + private boolean inflateData() { + try { + // PngHelperInternal.debug("entering inflateData bytes, state=" + state + + // " callback="+callbackMode); + if (state == State.ROW_READY) + throw new PngjException("invalid state");// assert + if (state.isDone()) + return false; + int ninflated = 0; + if (row == null || row.length < rowlen) + row = new byte[rowlen]; // should not happen + if (rowfilled < rowlen && !inf.finished()) { + try { + ninflated = inf.inflate(row, rowfilled, rowlen - rowfilled); + } catch (DataFormatException e) { + throw new PngjInputException("error decompressing zlib stream ", e); + } + rowfilled += ninflated; + nBytesOut += ninflated; + } + State nextstate = null; + if (rowfilled == rowlen) + nextstate = State.ROW_READY; // complete row, process it + else if (!inf.finished()) + nextstate = State.WAITING_FOR_INPUT; + else if (rowfilled > 0) + nextstate = State.ROW_READY; // complete row, process it + else { + nextstate = State.WORK_DONE; // eof, no more data + } + state = nextstate; + if (state == State.ROW_READY) { + preProcessRow(); + return true; + } + } catch (RuntimeException e) { + close(); + throw e; + } + return false; + } + + /** + * Called automatically in all modes when a full row has been inflated. + */ + protected void preProcessRow() { + + } + + /** + * Callback, must be implemented in callbackMode + *

+ * This should use {@link #getRowFilled()} and {@link #getInflatedRow()} to access the row. + *

+ * Must return byes of next row, for next callback. + */ + protected int processRowCallback() { + throw new PngjInputException("not implemented"); + } + + /** + * Callback, to be implemented in callbackMode + *

+ * This will be called once to notify state done + */ + protected void processDoneCallback() {} + + /** + * Inflated buffer. + * + * The effective length is given by {@link #getRowFilled()} + */ + public byte[] getInflatedRow() { + return row; + } + + /** + * Should be called after the previous row was processed + *

+ * Pass 0 or negative to signal that we are done (not expecting more bytes) + *

+ * This resets {@link #rowfilled} + *

+ * The + */ + public void prepareForNextRow(int len) { + rowfilled = 0; + rown++; + if (len < 1) { + rowlen = 0; + done(); + } else if (inf.finished()) { + rowlen = 0; + done(); + } else { + state = State.WAITING_FOR_INPUT; + rowlen = len; + if (!callbackMode) + inflateData(); + } + } + + /** + * In this state, the object is waiting for more input to deflate. + *

+ * Only in this state it's legal to feed this + */ + public boolean isWaitingForMoreInput() { + return state == State.WAITING_FOR_INPUT; + } + + /** + * In this state, the object is waiting the caller to retrieve inflated data + *

+ * Effective length: see {@link #getRowFilled()} + */ + public boolean isRowReady() { + return state == State.ROW_READY; + } + + /** + * In this state, all relevant data has been uncompressed and retrieved (exceptionally, the reading has ended + * prematurely). + *

+ * We can still feed this object, but the bytes will be swallowed/ignored. + */ + public boolean isDone() { + return state.isDone(); + } + + public boolean isTerminated() { + return state.isTerminated(); + } + + /** + * This will be called by the owner to report us the next chunk to come. We can make our own internal changes and + * checks. This returns true if we acknowledge the next chunk as part of this set + */ + public boolean ackNextChunkId(String id) { + if (state.isTerminated()) + return false; + else if (id.equals(chunkid)) { + return true; + } else { + if (!allowOtherChunksInBetween(id)) { + if (state.isDone()) { + if (!isTerminated()) + terminate(); + return false; + } else { + throw new PngjInputException("Unexpected chunk " + id + " while " + chunkid + + " set is not done"); + } + } else + return true; + } + } + + protected void terminate() { + close(); + } + + /** + * This should be called when discarding this object, or for aborting. Secure, idempotent Don't use this just to + * notify this object that it has no more work to do, see {@link #done()} + * */ + public void close() { + try { + if (!state.isTerminated()) { + state = State.TERMINATED; + } + if (infOwn && inf != null) { + inf.end();// we end the Inflater only if we created it + inf = null; + } + } catch (Exception e) { + } + } + + /** + * Forces the DONE state, this object won't uncompress more data. It's still not terminated, it will accept more IDAT + * chunks, but will ignore them. + */ + public void done() { + if (!isDone()) + state = State.WORK_DONE; + } + + /** + * Target size of the current row, including filter byte.
+ * should coincide (or be less than) with row.length + */ + public int getRowLen() { + return rowlen; + } + + /** This the amount of valid bytes in the buffer */ + public int getRowFilled() { + return rowfilled; + } + + /** + * Get current (last) row number. + *

+ * This corresponds to the raw numeration of rows as seen by the deflater. Not the same as the real image row, if + * interlaced. + * + */ + public int getRown() { + return rown; + } + + /** + * Some IDAT-like set can allow other chunks in between (APGN?). + *

+ * Normally false. + * + * @param id Id of the other chunk that appeared in middel of this set. + * @return true if allowed + */ + public boolean allowOtherChunksInBetween(String id) { + return false; + } + + /** + * Callback mode = async processing + */ + public boolean isCallbackMode() { + return callbackMode; + } + + public void setCallbackMode(boolean callbackMode) { + this.callbackMode = callbackMode; + } + + /** total number of bytes that have been fed to this object */ + public long getBytesIn() { + return nBytesIn; + } + + /** total number of bytes that have been uncompressed */ + public long getBytesOut() { + return nBytesOut; + } + + @Override + public String toString() { + StringBuilder sb = + new StringBuilder("idatSet : " + curChunk.getChunkRaw().id + " state=" + state + " rows=" + + rown + " bytes=" + nBytesIn + "/" + nBytesOut); + return sb.toString(); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/Deinterlacer.java b/src/jar-specific/java/ar/com/hjg/pngj/Deinterlacer.java new file mode 100644 index 00000000..ffb7260c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/Deinterlacer.java @@ -0,0 +1,199 @@ +package ar.com.hjg.pngj; + +public class Deinterlacer { + final ImageInfo imi; + private int pass; // 1-7 + private int rows, cols; + int dY, dX, oY, oX; // current step and offset (in pixels) + int oXsamples, dXsamples; // step in samples + + // current row in the virtual subsampled image; this increments (by 1) from 0 to rows/dy 7 times + private int currRowSubimg; + // in the real image, this will cycle from 0 to im.rows in different steps, 7 times + private int currRowReal; + private int currRowSeq; // not counting empty rows + + int totalRows; + private boolean ended = false; + + public Deinterlacer(ImageInfo iminfo) { + this.imi = iminfo; + pass = 0; + currRowSubimg = -1; + currRowReal = -1; + currRowSeq = 0; + ended = false; + totalRows = 0; // lazy compute + setPass(1); + setRow(0); + } + + /** this refers to the row currRowSubimg */ + private void setRow(int n) { // This should be called only intercally, in sequential order + currRowSubimg = n; + currRowReal = n * dY + oY; + if (currRowReal < 0 || currRowReal >= imi.rows) + throw new PngjExceptionInternal("bad row - this should not happen"); + } + + /** Skips passes with no rows. Return false is no more rows */ + boolean nextRow() { + currRowSeq++; + if (rows == 0 || currRowSubimg >= rows - 1) { // next pass + if (pass == 7) { + ended = true; + return false; + } + setPass(pass + 1); + if (rows == 0) { + currRowSeq--; + return nextRow(); + } + setRow(0); + } else { + setRow(currRowSubimg + 1); + } + return true; + } + + boolean isEnded() { + return ended; + } + + void setPass(int p) { + if (this.pass == p) + return; + pass = p; + byte[] pp = paramsForPass(p);// dx,dy,ox,oy + dX = pp[0]; + dY = pp[1]; + oX = pp[2]; + oY = pp[3]; + rows = imi.rows > oY ? (imi.rows + dY - 1 - oY) / dY : 0; + cols = imi.cols > oX ? (imi.cols + dX - 1 - oX) / dX : 0; + if (cols == 0) + rows = 0; // well, really... + dXsamples = dX * imi.channels; + oXsamples = oX * imi.channels; + } + + static byte[] paramsForPass(final int p) {// dx,dy,ox,oy + switch (p) { + case 1: + return new byte[] {8, 8, 0, 0}; + case 2: + return new byte[] {8, 8, 4, 0}; + case 3: + return new byte[] {4, 8, 0, 4}; + case 4: + return new byte[] {4, 4, 2, 0}; + case 5: + return new byte[] {2, 4, 0, 2}; + case 6: + return new byte[] {2, 2, 1, 0}; + case 7: + return new byte[] {1, 2, 0, 1}; + default: + throw new PngjExceptionInternal("bad interlace pass" + p); + } + } + + /** + * current row number inside the "sub image" + */ + int getCurrRowSubimg() { + return currRowSubimg; + } + + /** + * current row number inside the "real image" + */ + int getCurrRowReal() { + return currRowReal; + } + + /** + * current pass number (1-7) + */ + int getPass() { + return pass; + } + + /** + * How many rows has the current pass? + **/ + int getRows() { + return rows; + } + + /** + * How many columns (pixels) are there in the current row + */ + int getCols() { + return cols; + } + + public int getPixelsToRead() { + return getCols(); + } + + public int getBytesToRead() { // not including filter byte + return (imi.bitspPixel * getPixelsToRead() + 7) / 8; + } + + public int getdY() { + return dY; + } + + /* + * in pixels + */ + public int getdX() { + return dX; + } + + public int getoY() { + return oY; + } + + /* + * in pixels + */ + public int getoX() { + return oX; + } + + public int getTotalRows() { + if (totalRows == 0) { // lazy compute + for (int p = 1; p <= 7; p++) { + byte[] pp = paramsForPass(p); // dx dy ox oy + int rows = imi.rows > pp[3] ? (imi.rows + pp[1] - 1 - pp[3]) / pp[1] : 0; + int cols = imi.cols > pp[2] ? (imi.cols + pp[0] - 1 - pp[2]) / pp[0] : 0; + if (rows > 0 && cols > 0) + totalRows += rows; + } + } + return totalRows; + } + + /** + * total unfiltered bytes in the image, including the filter byte + */ + public long getTotalRawBytes() { // including the filter byte + long bytes = 0; + for (int p = 1; p <= 7; p++) { + byte[] pp = paramsForPass(p); // dx dy ox oy + int rows = imi.rows > pp[3] ? (imi.rows + pp[1] - 1 - pp[3]) / pp[1] : 0; + int cols = imi.cols > pp[2] ? (imi.cols + pp[0] - 1 - pp[2]) / pp[0] : 0; + int bytesr = (imi.bitspPixel * cols + 7) / 8; // without filter byte + if (rows > 0 && cols > 0) + bytes += rows * (1 + (long) bytesr); + } + return bytes; + } + + public int getCurrRowSeq() { + return currRowSeq; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/FilterType.java b/src/jar-specific/java/ar/com/hjg/pngj/FilterType.java new file mode 100644 index 00000000..66128b75 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/FilterType.java @@ -0,0 +1,124 @@ +package ar.com.hjg.pngj; + +import java.util.HashMap; + +/** + * Internal PNG predictor filter type + * + * Negative values are pseudo types, actually global strategies for writing, that (can) result on different real filters + * for different rows + */ +public enum FilterType { + /** + * No filter. + */ + FILTER_NONE(0), + /** + * SUB filter (uses same row) + */ + FILTER_SUB(1), + /** + * UP filter (uses previous row) + */ + FILTER_UP(2), + /** + * AVERAGE filter + */ + FILTER_AVERAGE(3), + /** + * PAETH predictor + */ + FILTER_PAETH(4), + /** + * Default strategy: select one of the standard filters depending on global image parameters + */ + FILTER_DEFAULT(-1), + /** + * @deprecated use #FILTER_ADAPTIVE_FAST + */ + FILTER_AGGRESSIVE(-2), + /** + * @deprecated use #FILTER_ADAPTIVE_MEDIUM or #FILTER_ADAPTIVE_FULL + */ + FILTER_VERYAGGRESSIVE(-4), + /** + * Adaptative strategy, sampling each row, or almost + */ + FILTER_ADAPTIVE_FULL(-4), + /** + * Adaptive strategy, skippping some rows + */ + FILTER_ADAPTIVE_MEDIUM(-3), // samples about 1/4 row + /** + * Adaptative strategy, skipping many rows - more speed + */ + FILTER_ADAPTIVE_FAST(-2), // samples each 8 or 16 rows + /** + * Experimental + */ + FILTER_SUPER_ADAPTIVE(-10), // + /** + * Preserves the filter passed in original row. + */ + FILTER_PRESERVE(-40), + /** + * Uses all fiters, one for lines, cyciclally. Only for tests. + */ + FILTER_CYCLIC(-50), + /** + * Not specified, placeholder for unknown or NA filters. + */ + FILTER_UNKNOWN(-100); + + public final int val; + + private FilterType(int val) { + this.val = val; + } + + private static HashMap byVal; + + static { + byVal = new HashMap(); + for (FilterType ft : values()) { + byVal.put(ft.val, ft); + } + } + + public static FilterType getByVal(int i) { + return byVal.get(i); + } + + /** only considers standard */ + public static boolean isValidStandard(int i) { + return i >= 0 && i <= 4; + } + + public static boolean isValidStandard(FilterType fy) { + return fy != null && isValidStandard(fy.val); + } + + public static boolean isAdaptive(FilterType fy) { + return fy.val <= -2 && fy.val >= -4; + } + + /** + * Returns all "standard" filters + */ + public static FilterType[] getAllStandard() { + return new FilterType[] {FILTER_NONE, FILTER_SUB, FILTER_UP, FILTER_AVERAGE, FILTER_PAETH}; + } + + public static FilterType[] getAllStandardNoneLast() { + return new FilterType[] {FILTER_SUB, FILTER_UP, FILTER_AVERAGE, FILTER_PAETH, FILTER_NONE}; + } + + public static FilterType[] getAllStandardExceptNone() { + return new FilterType[] {FILTER_SUB, FILTER_UP, FILTER_AVERAGE, FILTER_PAETH}; + } + + static FilterType[] getAllStandardForFirstRow() { + return new FilterType[] {FILTER_SUB, FILTER_NONE}; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IBytesConsumer.java b/src/jar-specific/java/ar/com/hjg/pngj/IBytesConsumer.java new file mode 100644 index 00000000..b2fcde3e --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IBytesConsumer.java @@ -0,0 +1,14 @@ +package ar.com.hjg.pngj; + +/** + * Bytes consumer. Objects implementing this interface can act as bytes consumers, that are "fed" with bytes. + */ +public interface IBytesConsumer { + /** + * Eats some bytes, at most len. + *

+ * Returns bytes actually consumed. A negative return value signals that the consumer is done, it refuses to eat more + * bytes. This should only return 0 if len is 0 + */ + int consume(byte[] buf, int offset, int len); +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IChunkFactory.java b/src/jar-specific/java/ar/com/hjg/pngj/IChunkFactory.java new file mode 100644 index 00000000..013f3628 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IChunkFactory.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +import ar.com.hjg.pngj.chunks.ChunkRaw; +import ar.com.hjg.pngj.chunks.PngChunk; + +/** + * Factory to create a {@link PngChunk} from a {@link ChunkRaw}. + *

+ * Used by {@link PngReader} + */ +public interface IChunkFactory { + + /** + * @param chunkRaw Chunk in raw form. Data can be null if it was skipped or processed directly (eg IDAT) + * @param imgInfo Not normally necessary, but some chunks want this info + * @return should never return null. + */ + public PngChunk createChunk(ChunkRaw chunkRaw, ImageInfo imgInfo); + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IDatChunkWriter.java b/src/jar-specific/java/ar/com/hjg/pngj/IDatChunkWriter.java new file mode 100644 index 00000000..e314193c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IDatChunkWriter.java @@ -0,0 +1,129 @@ +package ar.com.hjg.pngj; + +import java.io.OutputStream; + +import ar.com.hjg.pngj.chunks.ChunkHelper; +import ar.com.hjg.pngj.chunks.ChunkRaw; + +/** + * Outputs a sequence of IDAT-like chunk, that is filled progressively until the max chunk length is reached (or until + * flush()) + */ +public class IDatChunkWriter { + + private static final int MAX_LEN_DEFAULT = 32768; // 32K rather arbitrary - data only + + private final OutputStream outputStream; + private final int maxChunkLen; + private byte[] buf; + + private int offset = 0; + private int availLen; + private long totalBytesWriten = 0; // including header+crc + private int chunksWriten = 0; + + public IDatChunkWriter(OutputStream outputStream) { + this(outputStream, 0); + } + + public IDatChunkWriter(OutputStream outputStream, int maxChunkLength) { + this.outputStream = outputStream; + this.maxChunkLen = maxChunkLength > 0 ? maxChunkLength : MAX_LEN_DEFAULT; + buf = new byte[maxChunkLen]; + availLen = maxChunkLen - offset; + postReset(); + } + + public IDatChunkWriter(OutputStream outputStream, byte[] b) { + this.outputStream = outputStream; + this.buf = b != null ? b : new byte[MAX_LEN_DEFAULT]; + this.maxChunkLen = b.length; + availLen = maxChunkLen - offset; + postReset(); + } + + protected byte[] getChunkId() { + return ChunkHelper.b_IDAT; + } + + /** + * Writes a chhunk if there is more than minLenToWrite. + * + * This is normally called internally, but can be called explicitly to force flush. + */ + public final void flush() { + if (offset > 0 && offset >= minLenToWrite()) { + ChunkRaw c = new ChunkRaw(offset, getChunkId(), false); + c.data = buf; + c.writeChunk(outputStream); + totalBytesWriten += c.len + 12; + chunksWriten++; + offset = 0; + availLen = maxChunkLen; + postReset(); + } + } + + public int getOffset() { + return offset; + } + + public int getAvailLen() { + return availLen; + } + + /** triggers an flush+reset if appropiate */ + public void incrementOffset(int n) { + offset += n; + availLen -= n; + if (availLen < 0) + throw new PngjOutputException("Anomalous situation"); + if (availLen == 0) { + flush(); + } + } + + /** + * this should rarely be used, the normal way (to avoid double copying) is to get the buffer and write directly to it + */ + public void write(byte[] b, int o, int len) { + while (len > 0) { + int n = len <= availLen ? len : availLen; + System.arraycopy(b, o, buf, offset, n); + incrementOffset(n); + len -= n; + o += n; + } + } + + /** this will be called after reset */ + protected void postReset() { + // fdat could override this (and minLenToWrite) to add a prefix + } + + protected int minLenToWrite() { + return 1; + } + + public void close() { + flush(); + offset = 0; + buf = null; + } + + /** + * You can write directly to this buffer, using {@link #getOffset()} and {@link #getAvailLen()}. You should call + * {@link #incrementOffset(int)} inmediately after. + * */ + public byte[] getBuf() { + return buf; + } + + public long getTotalBytesWriten() { + return totalBytesWriten; + } + + public int getChunksWriten() { + return chunksWriten; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IImageLine.java b/src/jar-specific/java/ar/com/hjg/pngj/IImageLine.java new file mode 100644 index 00000000..f40f13d0 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IImageLine.java @@ -0,0 +1,41 @@ +package ar.com.hjg.pngj; + +/** + * General format-translated image line. + *

+ * The methods from this interface provides translation from/to PNG raw unfiltered pixel data, for each image line. This + * doesn't make any assumptions of underlying storage. + *

+ * The user of this library will not normally use this methods, but instead will cast to a more concrete implementation, + * as {@link ImageLineInt} or {@link ImageLineByte} with its methods for accessing the pixel values. + */ +public interface IImageLine { + + /** + * Extract pixels from a raw unlfilterd PNG row. Len is the total amount of bytes in the array, including the first + * byte (filter type) + * + * Arguments offset and step (0 and 1 for non interlaced) are in PIXELS. It's guaranteed that when step==1 then + * offset=0 + * + * Notice that when step!=1 the data is partial, this method will be called several times + * + * Warning: the data in array 'raw' starts at position 0 and has 'len' consecutive bytes. 'offset' and 'step' refer to + * the pixels in destination + */ + void readFromPngRaw(byte[] raw, int len, int offset, int step); + + /** + * This is called when the read for the line has been completed (eg for interlaced). It's called exactly once for each + * line. This is provided in case the class needs to to some postprocessing. + */ + void endReadFromPngRaw(); + + /** + * Writes the line to a PNG raw byte array, in the unfiltered PNG format Notice that the first byte is the filter + * type, you should write it only if you know it. + * + */ + void writeToPngRaw(byte[] raw); + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IImageLineArray.java b/src/jar-specific/java/ar/com/hjg/pngj/IImageLineArray.java new file mode 100644 index 00000000..6d3d6691 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IImageLineArray.java @@ -0,0 +1,23 @@ +package ar.com.hjg.pngj; + +/** + * This interface is just for the sake of unifying some methods of {@link ImageLineHelper} that can use both + * {@link ImageLineInt} or {@link ImageLineByte}. It's not very useful outside that, and the user should not rely much + * on this. + */ +public interface IImageLineArray { + public ImageInfo getImageInfo(); + + public FilterType getFilterType(); + + /** + * length of array (should correspond to samples) + */ + public int getSize(); + + /** + * Get i-th element of array (for 0 to size-1). The meaning of this is type dependent. For ImageLineInt and + * ImageLineByte is the sample value. + */ + public int getElem(int i); +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IImageLineFactory.java b/src/jar-specific/java/ar/com/hjg/pngj/IImageLineFactory.java new file mode 100644 index 00000000..1c0a4bd2 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IImageLineFactory.java @@ -0,0 +1,8 @@ +package ar.com.hjg.pngj; + +/** + * Image Line factory. + */ +public interface IImageLineFactory { + public T createImageLine(ImageInfo iminfo); +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IImageLineSet.java b/src/jar-specific/java/ar/com/hjg/pngj/IImageLineSet.java new file mode 100644 index 00000000..595001aa --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IImageLineSet.java @@ -0,0 +1,53 @@ +package ar.com.hjg.pngj; + + +/** + * Set of {@link IImageLine} elements. + *

+ * This is actually a "virtual" set, it can be implemented in several ways; for example + *

    + *
  • Cursor-like: stores only one line, which is implicitly moved when requested
  • + *
  • All lines: all lines stored as an array of IImageLine
  • + *
  • + * Subset of lines: eg, only first 3 lines, or odd numbered lines. Or a band of neighbours lines that is moved like a + * cursor.
  • + * The ImageLine that PngReader returns is hosted by a IImageLineSet (this abstraction allows the implementation to deal + * with interlaced images cleanly) but the library user does not normally needs to know that (or rely on that), except + * for the {@link PngReader#readRows()} method. + *
+ */ +public interface IImageLineSet { + + /** + * Asks for imageline corresponding to row n in the original image (zero based). This can trigger side + * effects in this object (eg, advance a cursor, set current row number...) In some scenarios, this should be consider + * as alias to (pseudocode) positionAtLine(n); getCurrentLine(); + *

+ * Throws exception if not available. The caller is supposed to know what he/she is doing + **/ + public IImageLine getImageLine(int n); + + /** + * Like {@link #getImageLine(int)} but uses the raw numbering inside the LineSet This makes little sense for a cursor + * + * @param n Should normally go from 0 to {@link #size()} + * @return + */ + public IImageLine getImageLineRawNum(int n); + + + /** + * Returns true if the set contain row n (in the original image,zero based) currently allocated. + *

+ * If it's a single-cursor, this should return true only if it's positioned there. (notice that hasImageLine(n) can + * return false, but getImageLine(n) can be ok) + * + **/ + public boolean hasImageLine(int n); + + /** + * Internal size of allocated rows This is informational, it should rarely be important for the caller. + **/ + public int size(); + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IImageLineSetFactory.java b/src/jar-specific/java/ar/com/hjg/pngj/IImageLineSetFactory.java new file mode 100644 index 00000000..e9aeba50 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IImageLineSetFactory.java @@ -0,0 +1,24 @@ +package ar.com.hjg.pngj; + +/** + * Factory of {@link IImageLineSet}, used by {@link PngReader}. + *

+ * + * @param Generic type of IImageLine + */ +public interface IImageLineSetFactory { + /** + * Creates a new {@link IImageLineSet} + * + * If singleCursor=true, the caller will read and write one row fully at a time, in order (it'll never try to read out + * of order lines), so the implementation can opt for allocate only one line. + * + * @param imgInfo Image info + * @param singleCursor : will read/write one row at a time + * @param nlines : how many lines we plan to read + * @param noffset : how many lines we want to skip from the original image (normally 0) + * @param step : row step (normally 1) + */ + public IImageLineSet create(ImageInfo imgInfo, boolean singleCursor, int nlines, int noffset, + int step); +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IPngWriterFactory.java b/src/jar-specific/java/ar/com/hjg/pngj/IPngWriterFactory.java new file mode 100644 index 00000000..617f5855 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IPngWriterFactory.java @@ -0,0 +1,7 @@ +package ar.com.hjg.pngj; + +import java.io.OutputStream; + +public interface IPngWriterFactory { + public PngWriter createPngWriter(OutputStream outputStream, ImageInfo imgInfo); +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/IdatSet.java b/src/jar-specific/java/ar/com/hjg/pngj/IdatSet.java new file mode 100644 index 00000000..82d83a11 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/IdatSet.java @@ -0,0 +1,242 @@ +package ar.com.hjg.pngj; + +import java.util.Arrays; +import java.util.zip.Checksum; +import java.util.zip.Inflater; + +/** + * This object process the concatenation of IDAT chunks. + *

+ * It extends {@link DeflatedChunksSet}, adding the intelligence to unfilter rows, and to understand row lenghts in + * terms of ImageInfo and (eventually) Deinterlacer + */ +public class IdatSet extends DeflatedChunksSet { + + protected byte rowUnfiltered[]; + protected byte rowUnfilteredPrev[]; + protected final ImageInfo imgInfo; // in the case of APNG this is the frame image + protected final Deinterlacer deinterlacer; + + final RowInfo rowinfo; // info for the last processed row, for debug + + protected int filterUseStat[] = new int[5]; // for stats + + /** + * @param id Chunk id (first chunk), should be shared by all concatenated chunks + * @param iminfo Image info + * @param deinterlacer Not null if interlaced + */ + public IdatSet(String id, ImageInfo iminfo, Deinterlacer deinterlacer) { + this(id, iminfo, deinterlacer, null, null); + } + + /** + * Special constructor with preallocated buffer. + *

+ *

+ * Same as {@link #IdatSet(String, ImageInfo, Deinterlacer)}, but you can pass a Inflater (will be reset internally), + * and a buffer (will be used only if size is enough) + */ + public IdatSet(String id, ImageInfo iminfo, Deinterlacer deinterlacer, Inflater inf, byte[] buffer) { + super(id, deinterlacer != null ? deinterlacer.getBytesToRead() + 1 : iminfo.bytesPerRow + 1, + iminfo.bytesPerRow + 1, inf, buffer); + this.imgInfo = iminfo; + this.deinterlacer = deinterlacer; + this.rowinfo = new RowInfo(iminfo, deinterlacer); + } + + /** + * Applies PNG un-filter to inflated raw line. Result in {@link #getUnfilteredRow()} {@link #getRowLen()} + */ + public void unfilterRow() { + unfilterRow(rowinfo.bytesRow); + } + + // nbytes: NOT including the filter byte. leaves result in rowUnfiltered + protected void unfilterRow(int nbytes) { + if (rowUnfiltered == null || rowUnfiltered.length < row.length) { + rowUnfiltered = new byte[row.length]; + rowUnfilteredPrev = new byte[row.length]; + } + if (rowinfo.rowNsubImg == 0) + Arrays.fill(rowUnfiltered, (byte) 0); // see swap that follows + // swap + byte[] tmp = rowUnfiltered; + rowUnfiltered = rowUnfilteredPrev; + rowUnfilteredPrev = tmp; + + int ftn = row[0]; + if (!FilterType.isValidStandard(ftn)) + throw new PngjInputException("Filter type " + ftn + " invalid"); + FilterType ft = FilterType.getByVal(ftn); + filterUseStat[ftn]++; + rowUnfiltered[0] = row[0]; // we copy the filter type, can be useful + switch (ft) { + case FILTER_NONE: + unfilterRowNone(nbytes); + break; + case FILTER_SUB: + unfilterRowSub(nbytes); + break; + case FILTER_UP: + unfilterRowUp(nbytes); + break; + case FILTER_AVERAGE: + unfilterRowAverage(nbytes); + break; + case FILTER_PAETH: + unfilterRowPaeth(nbytes); + break; + default: + throw new PngjInputException("Filter type " + ftn + " not implemented"); + } + } + + private void unfilterRowAverage(final int nbytes) { + int i, j, x; + for (j = 1 - imgInfo.bytesPixel, i = 1; i <= nbytes; i++, j++) { + x = j > 0 ? (rowUnfiltered[j] & 0xff) : 0; + rowUnfiltered[i] = (byte) (row[i] + (x + (rowUnfilteredPrev[i] & 0xFF)) / 2); + } + } + + private void unfilterRowNone(final int nbytes) { + for (int i = 1; i <= nbytes; i++) { + rowUnfiltered[i] = (byte) (row[i]); + } + } + + private void unfilterRowPaeth(final int nbytes) { + int i, j, x, y; + for (j = 1 - imgInfo.bytesPixel, i = 1; i <= nbytes; i++, j++) { + x = j > 0 ? (rowUnfiltered[j] & 0xFF) : 0; + y = j > 0 ? (rowUnfilteredPrev[j] & 0xFF) : 0; + rowUnfiltered[i] = + (byte) (row[i] + PngHelperInternal + .filterPaethPredictor(x, rowUnfilteredPrev[i] & 0xFF, y)); + } + } + + private void unfilterRowSub(final int nbytes) { + int i, j; + for (i = 1; i <= imgInfo.bytesPixel; i++) { + rowUnfiltered[i] = (byte) (row[i]); + } + for (j = 1, i = imgInfo.bytesPixel + 1; i <= nbytes; i++, j++) { + rowUnfiltered[i] = (byte) (row[i] + rowUnfiltered[j]); + } + } + + private void unfilterRowUp(final int nbytes) { + for (int i = 1; i <= nbytes; i++) { + rowUnfiltered[i] = (byte) (row[i] + rowUnfilteredPrev[i]); + } + } + + /** + * does the unfiltering of the inflated row, and updates row info + */ + @Override + protected void preProcessRow() { + super.preProcessRow(); + rowinfo.update(getRown()); + unfilterRow(); + rowinfo.updateBuf(rowUnfiltered, rowinfo.bytesRow + 1); + } + + /** + * Method for async/callback mode . + *

+ * In callback mode will be called as soon as each row is retrieved (inflated and unfiltered), after + * {@link #preProcessRow()} + *

+ * This is a dummy implementation (this normally should be overriden) that does nothing more than compute the length + * of next row. + *

+ * The return value is essential + *

+ * + * @return Length of next row, in bytes (including filter byte), non-positive if done + */ + @Override + protected int processRowCallback() { + int bytesNextRow = advanceToNextRow(); + return bytesNextRow; + } + + @Override + protected void processDoneCallback() { + super.processDoneCallback(); + } + + /** + * Signals that we are done with the previous row, begin reading the next one. + *

+ * In polled mode, calls setNextRowLen() + *

+ * Warning: after calling this, the unfilterRow is invalid! + * + * @return Returns nextRowLen + */ + public int advanceToNextRow() { + // PngHelperInternal.LOGGER.info("advanceToNextRow"); + int bytesNextRow; + if (deinterlacer == null) { + bytesNextRow = getRown() >= imgInfo.rows - 1 ? 0 : imgInfo.bytesPerRow + 1; + } else { + boolean more = deinterlacer.nextRow(); + bytesNextRow = more ? deinterlacer.getBytesToRead() + 1 : 0; + } + if (!isCallbackMode()) { // in callback mode, setNextRowLen() is called internally + prepareForNextRow(bytesNextRow); + } + return bytesNextRow; + } + + public boolean isRowReady() { + return !isWaitingForMoreInput(); + + } + + /** + * Unfiltered row. + *

+ * This should be called only if {@link #isRowReady()} returns true. + *

+ * To get real length, use {@link #getRowLen()} + *

+ * + * @return Unfiltered row, includes filter byte + */ + public byte[] getUnfilteredRow() { + return rowUnfiltered; + } + + public Deinterlacer getDeinterlacer() { + return deinterlacer; + } + + void updateCrcs(Checksum... idatCrcs) { + for (Checksum idatCrca : idatCrcs) + if (idatCrca != null)// just for testing + idatCrca.update(getUnfilteredRow(), 1, getRowFilled() - 1); + } + + @Override + public void close() { + super.close(); + rowUnfiltered = null;// not really necessary... + rowUnfilteredPrev = null; + } + + /** + * Only for debug/stats + * + * @return Array of 5 integers (sum equal numbers of rows) counting each filter use + */ + public int[] getFilterUseStat() { + return filterUseStat; + } + + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ImageInfo.java b/src/jar-specific/java/ar/com/hjg/pngj/ImageInfo.java new file mode 100644 index 00000000..80758c8a --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ImageInfo.java @@ -0,0 +1,255 @@ +package ar.com.hjg.pngj; + +import java.util.zip.Checksum; + +/** + * Simple immutable wrapper for basic image info. + *

+ * Some parameters are redundant, but the constructor receives an 'orthogonal' subset. + *

+ * ref: http://www.w3.org/TR/PNG/#11IHDR + */ +public class ImageInfo { + + /** + * Absolute allowed maximum value for rows and cols (2^24 ~16 million). (bytesPerRow must fit in a 32bit integer, + * though total amount of pixels not necessarily). + */ + public static final int MAX_COLS_ROW = 16777216; + + /** + * Cols= Image width, in pixels. + */ + public final int cols; + + /** + * Rows= Image height, in pixels + */ + public final int rows; + + /** + * Bits per sample (per channel) in the buffer (1-2-4-8-16). This is 8-16 for RGB/ARGB images, 1-2-4-8 for grayscale. + * For indexed images, number of bits per palette index (1-2-4-8) + */ + public final int bitDepth; + + /** + * Number of channels, as used internally: 3 for RGB, 4 for RGBA, 2 for GA (gray with alpha), 1 for grayscale or + * indexed. + */ + public final int channels; + + /** + * Flag: true if has alpha channel (RGBA/GA) + */ + public final boolean alpha; + + /** + * Flag: true if is grayscale (G/GA) + */ + public final boolean greyscale; + + /** + * Flag: true if image is indexed, i.e., it has a palette + */ + public final boolean indexed; + + /** + * Flag: true if image internally uses less than one byte per sample (bit depth 1-2-4) + */ + public final boolean packed; + + /** + * Bits used for each pixel in the buffer: channel * bitDepth + */ + public final int bitspPixel; + + /** + * rounded up value: this is only used internally for filter + */ + public final int bytesPixel; + + /** + * ceil(bitspp*cols/8) - does not include filter + */ + public final int bytesPerRow; + + /** + * Equals cols * channels + */ + public final int samplesPerRow; + + /** + * Amount of "packed samples" : when several samples are stored in a single byte (bitdepth 1,2 4) they are counted as + * one "packed sample". This is less that samplesPerRow only when bitdepth is 1-2-4 (flag packed = true) + *

+ * This equals the number of elements in the scanline array if working with packedMode=true + *

+ * For internal use, client code should rarely access this. + */ + public final int samplesPerRowPacked; + + private long totalPixels = -1; // lazy getter + + private long totalRawBytes = -1; // lazy getter + + /** + * Short constructor: assumes truecolor (RGB/RGBA) + */ + public ImageInfo(int cols, int rows, int bitdepth, boolean alpha) { + this(cols, rows, bitdepth, alpha, false, false); + } + + /** + * Full constructor + * + * @param cols Width in pixels + * @param rows Height in pixels + * @param bitdepth Bits per sample, in the buffer : 8-16 for RGB true color and greyscale + * @param alpha Flag: has an alpha channel (RGBA or GA) + * @param grayscale Flag: is gray scale (any bitdepth, with or without alpha) + * @param indexed Flag: has palette + */ + public ImageInfo(int cols, int rows, int bitdepth, boolean alpha, boolean grayscale, + boolean indexed) { + this.cols = cols; + this.rows = rows; + this.alpha = alpha; + this.indexed = indexed; + this.greyscale = grayscale; + if (greyscale && indexed) + throw new PngjException("palette and greyscale are mutually exclusive"); + this.channels = (grayscale || indexed) ? (alpha ? 2 : 1) : (alpha ? 4 : 3); + // http://www.w3.org/TR/PNG/#11IHDR + this.bitDepth = bitdepth; + this.packed = bitdepth < 8; + this.bitspPixel = (channels * this.bitDepth); + this.bytesPixel = (bitspPixel + 7) / 8; + this.bytesPerRow = (bitspPixel * cols + 7) / 8; + this.samplesPerRow = channels * this.cols; + this.samplesPerRowPacked = packed ? bytesPerRow : samplesPerRow; + // several checks + switch (this.bitDepth) { + case 1: + case 2: + case 4: + if (!(this.indexed || this.greyscale)) + throw new PngjException("only indexed or grayscale can have bitdepth=" + this.bitDepth); + break; + case 8: + break; + case 16: + if (this.indexed) + throw new PngjException("indexed can't have bitdepth=" + this.bitDepth); + break; + default: + throw new PngjException("invalid bitdepth=" + this.bitDepth); + } + if (cols < 1 || cols > MAX_COLS_ROW) + throw new PngjException("invalid cols=" + cols + " ???"); + if (rows < 1 || rows > MAX_COLS_ROW) + throw new PngjException("invalid rows=" + rows + " ???"); + if (samplesPerRow < 1) + throw new PngjException("invalid image parameters (overflow?)"); + } + + /** + * returns a copy with different size + * + * @param cols if non-positive, the original is used + * @param rows if non-positive, the original is used + * @return a new copy with the specified size and same properties + */ + public ImageInfo withSize(int cols, int rows) { + return new ImageInfo(cols > 0 ? cols : this.cols, rows > 0 ? rows : this.rows, this.bitDepth, + this.alpha, this.greyscale, this.indexed); + } + + public long getTotalPixels() { + if (totalPixels < 0) + totalPixels = cols * (long) rows; + return totalPixels; + } + + /** + * Total uncompressed bytes in IDAT, including filter byte. This is not valid for interlaced. + */ + public long getTotalRawBytes() { + if (totalRawBytes < 0) + totalRawBytes = (bytesPerRow + 1) * (long) rows; + return totalRawBytes; + } + + @Override + public String toString() { + return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels=" + + channels + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + indexed + "]"; + } + + /** + * Brief info: COLSxROWS[dBITDEPTH][a][p][g] ( the default dBITDEPTH='d8' is ommited) + **/ + public String toStringBrief() { + return String.valueOf(cols) + "x" + rows + (bitDepth != 8 ? ("d" + bitDepth) : "") + + (alpha ? "a" : "") + (indexed ? "p" : "") + (greyscale ? "g" : ""); + } + + public String toStringDetail() { + return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels=" + + channels + ", bitspPixel=" + bitspPixel + ", bytesPixel=" + bytesPixel + ", bytesPerRow=" + + bytesPerRow + ", samplesPerRow=" + samplesPerRow + ", samplesPerRowP=" + + samplesPerRowPacked + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + + indexed + ", packed=" + packed + "]"; + } + + + void updateCrc(Checksum crc) { + crc.update((byte) rows); + crc.update((byte) (rows >> 8)); + crc.update((byte) (rows >> 16)); + crc.update((byte) cols); + crc.update((byte) (cols >> 8)); + crc.update((byte) (cols >> 16)); + crc.update((byte) (bitDepth)); + crc.update((byte) (indexed ? 1 : 2)); + crc.update((byte) (greyscale ? 3 : 4)); + crc.update((byte) (alpha ? 3 : 4)); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (alpha ? 1231 : 1237); + result = prime * result + bitDepth; + result = prime * result + cols; + result = prime * result + (greyscale ? 1231 : 1237); + result = prime * result + (indexed ? 1231 : 1237); + result = prime * result + rows; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ImageInfo other = (ImageInfo) obj; + if (alpha != other.alpha) + return false; + if (bitDepth != other.bitDepth) + return false; + if (cols != other.cols) + return false; + if (greyscale != other.greyscale) + return false; + if (indexed != other.indexed) + return false; + if (rows != other.rows) + return false; + return true; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ImageLineByte.java b/src/jar-specific/java/ar/com/hjg/pngj/ImageLineByte.java new file mode 100644 index 00000000..3c1146ca --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ImageLineByte.java @@ -0,0 +1,186 @@ +package ar.com.hjg.pngj; + +/** + * Lightweight wrapper for an image scanline, used for read and write. + *

+ * This object can be (usually it is) reused while iterating over the image lines. + *

+ * See scanline field, to understand the format. + * + * Format: byte (one bytes per sample) (for 16bpp the extra byte is placed in an extra array) + */ +public class ImageLineByte implements IImageLine, IImageLineArray { + public final ImageInfo imgInfo; + + final byte[] scanline; + final byte[] scanline2; // only used for 16 bpp (less significant byte) Normally you'd prefer + // ImageLineInt in this case + + protected FilterType filterType; // informational ; only filled by the reader. not significant for + // interlaced + final int size; // = imgInfo.samplePerRowPacked, if packed:imgInfo.samplePerRow elswhere + + public ImageLineByte(ImageInfo imgInfo) { + this(imgInfo, null); + } + + public ImageLineByte(ImageInfo imgInfo, byte[] sci) { + this.imgInfo = imgInfo; + filterType = FilterType.FILTER_UNKNOWN; + size = imgInfo.samplesPerRow; + scanline = sci != null && sci.length >= size ? sci : new byte[size]; + scanline2 = imgInfo.bitDepth == 16 ? new byte[size] : null; + } + + /** + * Returns a factory for this object + */ + public static IImageLineFactory getFactory() { + return new IImageLineFactory() { + public ImageLineByte createImageLine(ImageInfo iminfo) { + return new ImageLineByte(iminfo); + } + }; + } + + public FilterType getFilterUsed() { + return filterType; + } + + /** + * One byte per sample. This can be used also for 16bpp images, but in this case this loses the less significant + * 8-bits ; see also getScanlineByte2 and getElem. + */ + public byte[] getScanlineByte() { + return scanline; + } + + /** + * only for 16bpp (less significant byte) + * + * @return null for less than 16bpp + */ + public byte[] getScanlineByte2() { + return scanline2; + } + + /** + * Basic info + */ + public String toString() { + return " cols=" + imgInfo.cols + " bpc=" + imgInfo.bitDepth + " size=" + scanline.length; + } + + public void readFromPngRaw(byte[] raw, final int len, final int offset, final int step) { + filterType = FilterType.getByVal(raw[0]); // only for non interlaced line the filter is significative + int len1 = len - 1; + int step1 = (step - 1) * imgInfo.channels; + if (imgInfo.bitDepth == 8) { + if (step == 1) {// 8bispp non-interlaced: most important case, should be optimized + System.arraycopy(raw, 1, scanline, 0, len1); + } else {// 8bispp interlaced + for (int s = 1, c = 0, i = offset * imgInfo.channels; s <= len1; s++, i++) { + scanline[i] = raw[s]; + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } + } + } else if (imgInfo.bitDepth == 16) { + if (step == 1) {// 16bispp non-interlaced + for (int i = 0, s = 1; i < imgInfo.samplesPerRow; i++) { + scanline[i] = raw[s++]; // get the first byte + scanline2[i] = raw[s++]; // get the first byte + } + } else { + for (int s = 1, c = 0, i = offset != 0 ? offset * imgInfo.channels : 0; s <= len1; i++) { + scanline[i] = raw[s++]; + scanline2[i] = raw[s++]; + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } + } + } else { // packed formats + int mask0, mask, shi, bd; + bd = imgInfo.bitDepth; + mask0 = ImageLineHelper.getMaskForPackedFormats(bd); + for (int i = offset * imgInfo.channels, r = 1, c = 0; r < len; r++) { + mask = mask0; + shi = 8 - bd; + do { + scanline[i] = (byte) ((raw[r] & mask) >> shi); + mask >>= bd; + shi -= bd; + i++; + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } while (mask != 0 && i < size); + } + } + } + + public void writeToPngRaw(byte[] raw) { + raw[0] = (byte) filterType.val; + if (imgInfo.bitDepth == 8) { + System.arraycopy(scanline, 0, raw, 1, size); + } else if (imgInfo.bitDepth == 16) { + for (int i = 0, s = 1; i < size; i++) { + raw[s++] = scanline[i]; + raw[s++] = scanline2[i]; + } + } else { // packed formats + int shi, bd, v; + bd = imgInfo.bitDepth; + shi = 8 - bd; + v = 0; + for (int i = 0, r = 1; i < size; i++) { + v |= (scanline[i] << shi); + shi -= bd; + if (shi < 0 || i == size - 1) { + raw[r++] = (byte) v; + shi = 8 - bd; + v = 0; + } + } + } + } + + public void endReadFromPngRaw() {} + + public int getSize() { + return size; + } + + public int getElem(int i) { + return scanline2 == null ? scanline[i] & 0xFF : ((scanline[i] & 0xFF) << 8) + | (scanline2[i] & 0xFF); + } + + public byte[] getScanline() { + return scanline; + } + + public ImageInfo getImageInfo() { + return imgInfo; + } + + public FilterType getFilterType() { + return filterType; + } + + /** + * This should rarely be used by client code. Only relevant if FilterPreserve==true + */ + public void setFilterType(FilterType ft) { + filterType = ft; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ImageLineHelper.java b/src/jar-specific/java/ar/com/hjg/pngj/ImageLineHelper.java new file mode 100644 index 00000000..ee22d7e0 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ImageLineHelper.java @@ -0,0 +1,470 @@ +package ar.com.hjg.pngj; + +import java.util.Arrays; + +import ar.com.hjg.pngj.chunks.PngChunkPLTE; +import ar.com.hjg.pngj.chunks.PngChunkTRNS; + +/** + * Bunch of utility static methods to proces an image line at the pixel level. + *

+ * WARNING: this has little testing/optimizing, and this API is not stable. some methods will probably be changed or + * removed if future releases. + *

+ * WARNING: most methods for getting/setting values work currently only for ImageLine or ImageLineByte + */ +public class ImageLineHelper { + + static int[] DEPTH_UNPACK_1; + static int[] DEPTH_UNPACK_2; + static int[] DEPTH_UNPACK_4; + static int[][] DEPTH_UNPACK; + + static { + initDepthScale(); + } + + private static void initDepthScale() { + DEPTH_UNPACK_1 = new int[2]; + for (int i = 0; i < 2; i++) + DEPTH_UNPACK_1[i] = i * 255; + DEPTH_UNPACK_2 = new int[4]; + for (int i = 0; i < 4; i++) + DEPTH_UNPACK_2[i] = (i * 255) / 3; + DEPTH_UNPACK_4 = new int[16]; + for (int i = 0; i < 16; i++) + DEPTH_UNPACK_4[i] = (i * 255) / 15; + DEPTH_UNPACK = new int[][] {null, DEPTH_UNPACK_1, DEPTH_UNPACK_2, null, DEPTH_UNPACK_4}; + } + + /** + * When the bitdepth is less than 8, the imageLine is usually returned/expected unscaled. This method upscales it in + * place. Eg, if bitdepth=1, values 0-1 will be converted to 0-255 + */ + public static void scaleUp(IImageLineArray line) { + if (line.getImageInfo().indexed || line.getImageInfo().bitDepth >= 8) + return; + final int[] scaleArray = DEPTH_UNPACK[line.getImageInfo().bitDepth]; + if (line instanceof ImageLineInt) { + ImageLineInt iline = (ImageLineInt) line; + for (int i = 0; i < iline.getSize(); i++) + iline.scanline[i] = scaleArray[iline.scanline[i]]; + } else if (line instanceof ImageLineByte) { + ImageLineByte iline = (ImageLineByte) line; + for (int i = 0; i < iline.getSize(); i++) + iline.scanline[i] = (byte) scaleArray[iline.scanline[i]]; + } else + throw new PngjException("not implemented"); + } + + /** + * Reverse of {@link #scaleUp(IImageLineArray)} + */ + public static void scaleDown(IImageLineArray line) { + if (line.getImageInfo().indexed || line.getImageInfo().bitDepth >= 8) + return; + if (line instanceof ImageLineInt) { + final int scalefactor = 8 - line.getImageInfo().bitDepth; + if (line instanceof ImageLineInt) { + ImageLineInt iline = (ImageLineInt) line; + for (int i = 0; i < line.getSize(); i++) + iline.scanline[i] = iline.scanline[i] >> scalefactor; + } else if (line instanceof ImageLineByte) { + ImageLineByte iline = (ImageLineByte) line; + for (int i = 0; i < line.getSize(); i++) + iline.scanline[i] = (byte) ((iline.scanline[i] & 0xFF) >> scalefactor); + } + } else + throw new PngjException("not implemented"); + } + + public static byte scaleUp(int bitdepth, byte v) { + return bitdepth < 8 ? (byte) DEPTH_UNPACK[bitdepth][v] : v; + } + + public static byte scaleDown(int bitdepth, byte v) { + return bitdepth < 8 ? (byte) (v >> (8 - bitdepth)) : v; + } + + /** + * Given an indexed line with a palette, unpacks as a RGB array, or RGBA if a non nul PngChunkTRNS chunk is passed + * + * @param line ImageLine as returned from PngReader + * @param pal Palette chunk + * @param trns Transparency chunk, can be null (absent) + * @param buf Preallocated array, optional + * @return R G B (A), one sample 0-255 per array element. Ready for pngw.writeRowInt() + */ + public static int[] palette2rgb(ImageLineInt line, PngChunkPLTE pal, PngChunkTRNS trns, int[] buf) { + return palette2rgb(line, pal, trns, buf, false); + } + + /** + * Warning: the line should be upscaled, see {@link #scaleUp(IImageLineArray)} + */ + static int[] lineToARGB32(ImageLineByte line, PngChunkPLTE pal, PngChunkTRNS trns, int[] buf) { + boolean alphachannel = line.imgInfo.alpha; + int cols = line.getImageInfo().cols; + if (buf == null || buf.length < cols) + buf = new int[cols]; + int index, rgb, alpha, ga, g; + if (line.getImageInfo().indexed) {// palette + int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0; + for (int c = 0; c < cols; c++) { + index = line.scanline[c] & 0xFF; + rgb = pal.getEntry(index); + alpha = index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255; + buf[c] = (alpha << 24) | rgb; + } + } else if (line.imgInfo.greyscale) { // gray + ga = trns != null ? trns.getGray() : -1; + for (int c = 0, c2 = 0; c < cols; c++) { + g = (line.scanline[c2++] & 0xFF); + alpha = alphachannel ? line.scanline[c2++] & 0xFF : (g != ga ? 255 : 0); + buf[c] = (alpha << 24) | g | (g << 8) | (g << 16); + } + } else { // true color + ga = trns != null ? trns.getRGB888() : -1; + for (int c = 0, c2 = 0; c < cols; c++) { + rgb = + ((line.scanline[c2++] & 0xFF) << 16) | ((line.scanline[c2++] & 0xFF) << 8) + | (line.scanline[c2++] & 0xFF); + alpha = alphachannel ? line.scanline[c2++] & 0xFF : (rgb != ga ? 255 : 0); + buf[c] = (alpha << 24) | rgb; + } + } + return buf; + } + + /** + * Warning: the line should be upscaled, see {@link #scaleUp(IImageLineArray)} + */ + static byte[] lineToRGBA8888(ImageLineByte line, PngChunkPLTE pal, PngChunkTRNS trns, byte[] buf) { + boolean alphachannel = line.imgInfo.alpha; + int cols = line.imgInfo.cols; + int bytes = cols * 4; + if (buf == null || buf.length < bytes) + buf = new byte[bytes]; + int index, rgb, ga; + byte val; + if (line.imgInfo.indexed) {// palette + int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0; + for (int c = 0, b = 0; c < cols; c++) { + index = line.scanline[c] & 0xFF; + rgb = pal.getEntry(index); + buf[b++] = (byte) ((rgb >> 16) & 0xFF); + buf[b++] = (byte) ((rgb >> 8) & 0xFF); + buf[b++] = (byte) (rgb & 0xFF); + buf[b++] = (byte) (index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255); + } + } else if (line.imgInfo.greyscale) { // + ga = trns != null ? trns.getGray() : -1; + for (int c = 0, b = 0; b < bytes;) { + val = line.scanline[c++]; + buf[b++] = val; + buf[b++] = val; + buf[b++] = val; + buf[b++] = + alphachannel ? line.scanline[c++] : ((int) (val & 0xFF) == ga) ? (byte) 0 : (byte) 255; + } + } else { // true color + if (alphachannel) // same format! + System.arraycopy(line.scanline, 0, buf, 0, bytes); + else { + for (int c = 0, b = 0; b < bytes;) { + buf[b++] = line.scanline[c++]; + buf[b++] = line.scanline[c++]; + buf[b++] = line.scanline[c++]; + buf[b++] = (byte) (255); // tentative (probable) + if (trns != null && buf[b - 3] == (byte) trns.getRGB()[0] + && buf[b - 2] == (byte) trns.getRGB()[1] && buf[b - 1] == (byte) trns.getRGB()[2]) // not + // very + // efficient, + // but + // not + // frecuent + buf[b - 1] = 0; + } + } + } + return buf; + } + + static byte[] lineToRGB888(ImageLineByte line, PngChunkPLTE pal, byte[] buf) { + boolean alphachannel = line.imgInfo.alpha; + int cols = line.imgInfo.cols; + int bytes = cols * 3; + if (buf == null || buf.length < bytes) + buf = new byte[bytes]; + byte val; + int[] rgb = new int[3]; + if (line.imgInfo.indexed) {// palette + for (int c = 0, b = 0; c < cols; c++) { + pal.getEntryRgb(line.scanline[c] & 0xFF, rgb); + buf[b++] = (byte) rgb[0]; + buf[b++] = (byte) rgb[1]; + buf[b++] = (byte) rgb[2]; + } + } else if (line.imgInfo.greyscale) { // + for (int c = 0, b = 0; b < bytes;) { + val = line.scanline[c++]; + buf[b++] = val; + buf[b++] = val; + buf[b++] = val; + if (alphachannel) + c++; // skip alpha + } + } else { // true color + if (!alphachannel) // same format! + System.arraycopy(line.scanline, 0, buf, 0, bytes); + else { + for (int c = 0, b = 0; b < bytes;) { + buf[b++] = line.scanline[c++]; + buf[b++] = line.scanline[c++]; + buf[b++] = line.scanline[c++]; + c++;// skip alpha + } + } + } + return buf; + } + + /** + * Same as palette2rgbx , but returns rgba always, even if trns is null + * + * @param line ImageLine as returned from PngReader + * @param pal Palette chunk + * @param trns Transparency chunk, can be null (absent) + * @param buf Preallocated array, optional + * @return R G B (A), one sample 0-255 per array element. Ready for pngw.writeRowInt() + */ + public static int[] palette2rgba(ImageLineInt line, PngChunkPLTE pal, PngChunkTRNS trns, int[] buf) { + return palette2rgb(line, pal, trns, buf, true); + } + + public static int[] palette2rgb(ImageLineInt line, PngChunkPLTE pal, int[] buf) { + return palette2rgb(line, pal, null, buf, false); + } + + /** this is not very efficient, only for tests and troubleshooting */ + public static int[] convert2rgba(IImageLineArray line, PngChunkPLTE pal, PngChunkTRNS trns, + int[] buf) { + ImageInfo imi = line.getImageInfo(); + int nsamples = imi.cols * 4; + if (buf == null || buf.length < nsamples) + buf = new int[nsamples]; + int maxval = imi.bitDepth == 16 ? (1 << 16) - 1 : 255; + Arrays.fill(buf, maxval); + + if (imi.indexed) { + int tlen = trns != null ? trns.getPalletteAlpha().length : 0; + for (int s = 0; s < imi.cols; s++) { + int index = line.getElem(s); + pal.getEntryRgb(index, buf, s * 4); + if (index < tlen) { + buf[s * 4 + 3] = trns.getPalletteAlpha()[index]; + } + } + } else if (imi.greyscale) { + int[] unpack = null; + if (imi.bitDepth < 8) + unpack = ImageLineHelper.DEPTH_UNPACK[imi.bitDepth]; + for (int s = 0, i = 0, p = 0; p < imi.cols; p++) { + buf[s++] = unpack != null ? unpack[line.getElem(i++)] : line.getElem(i++); + buf[s] = buf[s - 1]; + s++; + buf[s] = buf[s - 1]; + s++; + if (imi.channels == 2) + buf[s++] = unpack != null ? unpack[line.getElem(i++)] : line.getElem(i++); + else + buf[s++] = maxval; + } + } else { + for (int s = 0, i = 0, p = 0; p < imi.cols; p++) { + buf[s++] = line.getElem(i++); + buf[s++] = line.getElem(i++); + buf[s++] = line.getElem(i++); + buf[s++] = imi.alpha ? line.getElem(i++) : maxval; + } + } + return buf; + } + + + + private static int[] palette2rgb(IImageLine line, PngChunkPLTE pal, PngChunkTRNS trns, int[] buf, + boolean alphaForced) { + boolean isalpha = trns != null; + int channels = isalpha ? 4 : 3; + ImageLineInt linei = (ImageLineInt) (line instanceof ImageLineInt ? line : null); + ImageLineByte lineb = (ImageLineByte) (line instanceof ImageLineByte ? line : null); + boolean isbyte = lineb != null; + int cols = linei != null ? linei.imgInfo.cols : lineb.imgInfo.cols; + int nsamples = cols * channels; + if (buf == null || buf.length < nsamples) + buf = new int[nsamples]; + int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0; + for (int c = 0; c < cols; c++) { + int index = isbyte ? (lineb.scanline[c] & 0xFF) : linei.scanline[c]; + pal.getEntryRgb(index, buf, c * channels); + if (isalpha) { + int alpha = index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255; + buf[c * channels + 3] = alpha; + } + } + return buf; + } + + /** + * what follows is pretty uninteresting/untested/obsolete, subject to change + */ + /** + * Just for basic info or debugging. Shows values for first and last pixel. Does not include alpha + */ + public static String infoFirstLastPixels(ImageLineInt line) { + return line.imgInfo.channels == 1 ? String.format("first=(%d) last=(%d)", line.scanline[0], + line.scanline[line.scanline.length - 1]) : String.format( + "first=(%d %d %d) last=(%d %d %d)", line.scanline[0], line.scanline[1], line.scanline[2], + line.scanline[line.scanline.length - line.imgInfo.channels], + line.scanline[line.scanline.length - line.imgInfo.channels + 1], + line.scanline[line.scanline.length - line.imgInfo.channels + 2]); + } + + /** + * integer packed R G B only for bitdepth=8! (does not check!) + * + **/ + public static int getPixelRGB8(IImageLine line, int column) { + if (line instanceof ImageLineInt) { + int offset = column * ((ImageLineInt) line).imgInfo.channels; + int[] scanline = ((ImageLineInt) line).getScanline(); + return (scanline[offset] << 16) | (scanline[offset + 1] << 8) | (scanline[offset + 2]); + } else if (line instanceof ImageLineByte) { + int offset = column * ((ImageLineByte) line).imgInfo.channels; + byte[] scanline = ((ImageLineByte) line).getScanline(); + return ((scanline[offset] & 0xff) << 16) | ((scanline[offset + 1] & 0xff) << 8) + | ((scanline[offset + 2] & 0xff)); + } else + throw new PngjException("Not supported " + line.getClass()); + } + + public static int getPixelARGB8(IImageLine line, int column) { + if (line instanceof ImageLineInt) { + int offset = column * ((ImageLineInt) line).imgInfo.channels; + int[] scanline = ((ImageLineInt) line).getScanline(); + return (scanline[offset + 3] << 24) | (scanline[offset] << 16) | (scanline[offset + 1] << 8) + | (scanline[offset + 2]); + } else if (line instanceof ImageLineByte) { + int offset = column * ((ImageLineByte) line).imgInfo.channels; + byte[] scanline = ((ImageLineByte) line).getScanline(); + return (((scanline[offset + 3] & 0xff) << 24) | ((scanline[offset] & 0xff) << 16) + | ((scanline[offset + 1] & 0xff) << 8) | ((scanline[offset + 2] & 0xff))); + } else + throw new PngjException("Not supported " + line.getClass()); + } + + public static void setPixelsRGB8(ImageLineInt line, int[] rgb) { + for (int i = 0, j = 0; i < line.imgInfo.cols; i++) { + line.scanline[j++] = ((rgb[i] >> 16) & 0xFF); + line.scanline[j++] = ((rgb[i] >> 8) & 0xFF); + line.scanline[j++] = ((rgb[i] & 0xFF)); + } + } + + public static void setPixelRGB8(ImageLineInt line, int col, int r, int g, int b) { + col *= line.imgInfo.channels; + line.scanline[col++] = r; + line.scanline[col++] = g; + line.scanline[col] = b; + } + + public static void setPixelRGB8(ImageLineInt line, int col, int rgb) { + setPixelRGB8(line, col, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); + } + + public static void setPixelsRGBA8(ImageLineInt line, int[] rgb) { + for (int i = 0, j = 0; i < line.imgInfo.cols; i++) { + line.scanline[j++] = ((rgb[i] >> 16) & 0xFF); + line.scanline[j++] = ((rgb[i] >> 8) & 0xFF); + line.scanline[j++] = ((rgb[i] & 0xFF)); + line.scanline[j++] = ((rgb[i] >> 24) & 0xFF); + } + } + + public static void setPixelRGBA8(ImageLineInt line, int col, int r, int g, int b, int a) { + col *= line.imgInfo.channels; + line.scanline[col++] = r; + line.scanline[col++] = g; + line.scanline[col++] = b; + line.scanline[col] = a; + } + + public static void setPixelRGBA8(ImageLineInt line, int col, int rgb) { + setPixelRGBA8(line, col, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, (rgb >> 24) & 0xFF); + } + + public static void setValD(ImageLineInt line, int i, double d) { + line.scanline[i] = double2int(line, d); + } + + public static int interpol(int a, int b, int c, int d, double dx, double dy) { + // a b -> x (0-1) + // c d + double e = a * (1.0 - dx) + b * dx; + double f = c * (1.0 - dx) + d * dx; + return (int) (e * (1 - dy) + f * dy + 0.5); + } + + public static double int2double(ImageLineInt line, int p) { + return line.imgInfo.bitDepth == 16 ? p / 65535.0 : p / 255.0; + // TODO: replace my multiplication? check for other bitdepths + } + + public static double int2doubleClamped(ImageLineInt line, int p) { + // TODO: replace my multiplication? + double d = line.imgInfo.bitDepth == 16 ? p / 65535.0 : p / 255.0; + return d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d); + } + + public static int double2int(ImageLineInt line, double d) { + d = d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d); + return line.imgInfo.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); // + } + + public static int double2intClamped(ImageLineInt line, double d) { + d = d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d); + return line.imgInfo.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); // + } + + public static int clampTo_0_255(int i) { + return i > 255 ? 255 : (i < 0 ? 0 : i); + } + + public static int clampTo_0_65535(int i) { + return i > 65535 ? 65535 : (i < 0 ? 0 : i); + } + + public static int clampTo_128_127(int x) { + return x > 127 ? 127 : (x < -128 ? -128 : x); + } + + public static int getMaskForPackedFormats(int bitDepth) { // Utility function for pack/unpack + if (bitDepth == 4) + return 0xf0; + else if (bitDepth == 2) + return 0xc0; + else + return 0x80; // bitDepth == 1 + } + + public static int getMaskForPackedFormatsLs(int bitDepth) { // Utility function for pack/unpack + if (bitDepth == 4) + return 0x0f; + else if (bitDepth == 2) + return 0x03; + else + return 0x01; // bitDepth == 1 + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ImageLineInt.java b/src/jar-specific/java/ar/com/hjg/pngj/ImageLineInt.java new file mode 100644 index 00000000..2671276e --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ImageLineInt.java @@ -0,0 +1,193 @@ +package ar.com.hjg.pngj; + +/** + * Represents an image line, integer format (one integer by sample). See {@link #scanline} to understand the format. + */ +public class ImageLineInt implements IImageLine, IImageLineArray { + public final ImageInfo imgInfo; + + /** + * The 'scanline' is an array of integers, corresponds to an image line (row). + *

+ * Each int is a "sample" (one for channel), (0-255 or 0-65535) in the corresponding PNG sequence: + * R G B R G B... or R G B A R G B A... + * or g g g ... or i i i (palette index) + *

+ * For bitdepth=1/2/4 the value is not scaled (hence, eg, if bitdepth=2 the range will be 0-4) + *

+ * To convert a indexed line to RGB values, see + * {@link ImageLineHelper#palette2rgb(ImageLineInt, ar.com.hjg.pngj.chunks.PngChunkPLTE, int[])} (you can't do the + * reverse) + */ + protected final int[] scanline; + + /** + * number of elements in the scanline + */ + protected final int size; + + /** + * informational ; only filled by the reader. not meaningful for interlaced + */ + protected FilterType filterType = FilterType.FILTER_UNKNOWN; + + /** + * @param imgInfo Inmutable ImageInfo, basic parameters of the image we are reading or writing + */ + public ImageLineInt(ImageInfo imgInfo) { + this(imgInfo, null); + } + + /** + * @param imgInfo Inmutable ImageInfo, basic parameters of the image we are reading or writing + * @param sci prealocated buffer (can be null) + */ + public ImageLineInt(ImageInfo imgInfo, int[] sci) { + this.imgInfo = imgInfo; + filterType = FilterType.FILTER_UNKNOWN; + size = imgInfo.samplesPerRow; + scanline = sci != null && sci.length >= size ? sci : new int[size]; + } + + /** + * Helper method, returns a default factory for this object + * + */ + public static IImageLineFactory getFactory() { + return new IImageLineFactory() { + public ImageLineInt createImageLine(ImageInfo iminfo) { + return new ImageLineInt(iminfo); + } + }; + } + + public FilterType getFilterType() { + return filterType; + } + + /** + * This should rarely be used by client code. Only relevant if FilterPreserve==true + */ + public void setFilterType(FilterType ft) { + filterType = ft; + } + + /** + * Basic info + */ + public String toString() { + return " cols=" + imgInfo.cols + " bpc=" + imgInfo.bitDepth + " size=" + scanline.length; + } + + public void readFromPngRaw(byte[] raw, final int len, final int offset, final int step) { + setFilterType(FilterType.getByVal(raw[0])); + int len1 = len - 1; + int step1 = (step - 1) * imgInfo.channels; + if (imgInfo.bitDepth == 8) { + if (step == 1) {// 8bispp non-interlaced: most important case, should be optimized + for (int i = 0; i < size; i++) { + scanline[i] = (raw[i + 1] & 0xff); + } + } else {// 8bispp interlaced + for (int s = 1, c = 0, i = offset * imgInfo.channels; s <= len1; s++, i++) { + scanline[i] = (raw[s] & 0xff); + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } + } + } else if (imgInfo.bitDepth == 16) { + if (step == 1) {// 16bispp non-interlaced + for (int i = 0, s = 1; i < size; i++) { + scanline[i] = ((raw[s++] & 0xFF) << 8) | (raw[s++] & 0xFF); // 16 bitspc + } + } else { + for (int s = 1, c = 0, i = offset != 0 ? offset * imgInfo.channels : 0; s <= len1; s++, i++) { + scanline[i] = ((raw[s++] & 0xFF) << 8) | (raw[s] & 0xFF); // 16 bitspc + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } + } + } else { // packed formats + int mask0, mask, shi, bd; + bd = imgInfo.bitDepth; + mask0 = ImageLineHelper.getMaskForPackedFormats(bd); + for (int i = offset * imgInfo.channels, r = 1, c = 0; r < len; r++) { + mask = mask0; + shi = 8 - bd; + do { + scanline[i++] = (raw[r] & mask) >> shi; + mask >>= bd; + shi -= bd; + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } while (mask != 0 && i < size); + } + } + } + + public void writeToPngRaw(byte[] raw) { + raw[0] = (byte) filterType.val; + if (imgInfo.bitDepth == 8) { + for (int i = 0; i < size; i++) { + raw[i + 1] = (byte) scanline[i]; + } + } else if (imgInfo.bitDepth == 16) { + for (int i = 0, s = 1; i < size; i++) { + raw[s++] = (byte) (scanline[i] >> 8); + raw[s++] = (byte) (scanline[i] & 0xff); + } + } else { // packed formats + int shi, bd, v; + bd = imgInfo.bitDepth; + shi = 8 - bd; + v = 0; + for (int i = 0, r = 1; i < size; i++) { + v |= (scanline[i] << shi); + shi -= bd; + if (shi < 0 || i == size - 1) { + raw[r++] = (byte) v; + shi = 8 - bd; + v = 0; + } + } + } + } + + /** + * Does nothing in this implementation + */ + public void endReadFromPngRaw() { + + } + + /** + * @see #size + */ + public int getSize() { + return size; + } + + public int getElem(int i) { + return scanline[i]; + } + + /** + * @return see {@link #scanline} + */ + public int[] getScanline() { + return scanline; + } + + public ImageInfo getImageInfo() { + return imgInfo; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/ImageLineSetDefault.java b/src/jar-specific/java/ar/com/hjg/pngj/ImageLineSetDefault.java new file mode 100644 index 00000000..4dbdce30 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/ImageLineSetDefault.java @@ -0,0 +1,151 @@ +package ar.com.hjg.pngj; + +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation of {@link IImageLineSet}. + *

+ * Supports all modes: single cursor, full rows, or partial. This should not be used for + */ +public abstract class ImageLineSetDefault implements IImageLineSet { + + protected final ImageInfo imgInfo; + private final boolean singleCursor; + private final int nlines, offset, step; + protected List imageLines; // null if single cursor + protected T imageLine; // null unless single cursor + protected int currentRow = -1; // only relevant (and not much) for cursor + + public ImageLineSetDefault(ImageInfo imgInfo, final boolean singleCursor, final int nlinesx, + final int noffsetx, final int stepx) { + this.imgInfo = imgInfo; + this.singleCursor = singleCursor; + if (singleCursor) { + this.nlines = 1; // we store only one line, no matter how many will be read + offset = 0; + this.step = 1;// don't matter + } else { + this.nlines = nlinesx; // note that it can also be 1 + offset = noffsetx; + this.step = stepx;// don't matter + } + createImageLines(); + } + + private void createImageLines() { + if (singleCursor) + imageLine = createImageLine(); + else { + imageLines = new ArrayList(); + for (int i = 0; i < nlines; i++) + imageLines.add(createImageLine()); + } + } + + protected abstract T createImageLine(); + + /** + * Retrieves the image line + *

+ * Warning: the argument is the row number in the original image + *

+ * If this is a cursor, no check is done, always the same row is returned + */ + public T getImageLine(int n) { + currentRow = n; + if (singleCursor) + return imageLine; + else { + int r = imageRowToMatrixRowStrict(n); + if (r < 0) + throw new PngjException("Invalid row number"); + return imageLines.get(r); + } + } + + /** + * does not check for valid range + */ + public T getImageLineRawNum(int r) { + if (singleCursor) + return imageLine; + else + return imageLines.get(r); + } + + /** + * True if the set contains this image line + *

+ * Warning: the argument is the row number in the original image + *

+ * If this works as cursor, this returns true only if that is the number of its "current" line + */ + public boolean hasImageLine(int n) { + return singleCursor ? currentRow == n : imageRowToMatrixRowStrict(n) >= 0; + } + + /** + * How many lines does this object contain? + */ + public int size() { + return nlines; + } + + /** + * Same as {@link #imageRowToMatrixRow(int)}, but returns negative if invalid + */ + public int imageRowToMatrixRowStrict(int imrow) { + imrow -= offset; + int mrow = imrow >= 0 && (step == 1 || imrow % step == 0) ? imrow / step : -1; + return mrow < nlines ? mrow : -1; + } + + /** + * Converts from matrix row number (0 : nRows-1) to image row number + * + * @param mrow Matrix row number + * @return Image row number. Returns trash if mrow is invalid + */ + public int matrixRowToImageRow(int mrow) { + return mrow * step + offset; + } + + /** + * Converts from real image row to this object row number. + *

+ * Warning: this always returns a valid matrix row (clamping on 0 : nrows-1, and rounding down) + *

+ * Eg: rowOffset=4,rowStep=2 imageRowToMatrixRow(17) returns 6 , imageRowToMatrixRow(1) returns 0 + */ + public int imageRowToMatrixRow(int imrow) { + int r = (imrow - offset) / step; + return r < 0 ? 0 : (r < nlines ? r : nlines - 1); + } + + /** utility function, given a factory for one line, returns a factory for a set */ + public static IImageLineSetFactory createImageLineSetFactoryFromImageLineFactory( + final IImageLineFactory ifactory) { // ugly method must have ugly name. don't let this intimidate you + return new IImageLineSetFactory() { + public IImageLineSet create(final ImageInfo iminfo, boolean singleCursor, int nlines, + int noffset, int step) { + return new ImageLineSetDefault(iminfo, singleCursor, nlines, noffset, step) { + @Override + protected T createImageLine() { + return ifactory.createImageLine(iminfo); + } + }; + }; + }; + } + + /** utility function, returns default factory for {@link ImageLineInt} */ + public static IImageLineSetFactory getFactoryInt() { + return createImageLineSetFactoryFromImageLineFactory(ImageLineInt.getFactory()); + } + + /** utility function, returns default factory for {@link ImageLineByte} */ + public static IImageLineSetFactory getFactoryByte() { + return createImageLineSetFactoryFromImageLineFactory(ImageLineByte.getFactory()); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngHelperInternal.java b/src/jar-specific/java/ar/com/hjg/pngj/PngHelperInternal.java new file mode 100644 index 00000000..7c8423f7 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngHelperInternal.java @@ -0,0 +1,329 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.logging.Logger; + +/** + * Some utility static methods for internal use. + *

+ * Client code should not normally use this class + *

+ */ +public final class PngHelperInternal { + + public static final String KEY_LOGGER = "ar.com.pngj"; + public static final Logger LOGGER = Logger.getLogger(KEY_LOGGER); + + /** + * Default charset, used internally by PNG for several things + */ + public static String charsetLatin1name = "ISO-8859-1"; + public static Charset charsetLatin1 = Charset.forName(charsetLatin1name); + /** + * UTF-8 is only used for some chunks + */ + public static String charsetUTF8name = "UTF-8"; + public static Charset charsetUTF8 = Charset.forName(charsetUTF8name); + + private static ThreadLocal DEBUG = new ThreadLocal() { + protected Boolean initialValue() { + return Boolean.FALSE; + } + }; + + /** + * PNG magic bytes + */ + public static byte[] getPngIdSignature() { + return new byte[] {-119, 80, 78, 71, 13, 10, 26, 10}; + } + + public static int doubleToInt100000(double d) { + return (int) (d * 100000.0 + 0.5); + } + + public static double intToDouble100000(int i) { + return i / 100000.0; + } + + public static int readByte(InputStream is) { + try { + return is.read(); + } catch (IOException e) { + throw new PngjInputException("error reading byte", e); + } + } + + /** + * -1 if eof + * + * PNG uses "network byte order" + */ + public static int readInt2(InputStream is) { + try { + int b1 = is.read(); + int b2 = is.read(); + if (b1 == -1 || b2 == -1) + return -1; + return (b1 << 8) | b2; + } catch (IOException e) { + throw new PngjInputException("error reading Int2", e); + } + } + + /** + * -1 if eof + */ + public static int readInt4(InputStream is) { + try { + int b1 = is.read(); + int b2 = is.read(); + int b3 = is.read(); + int b4 = is.read(); + if (b1 == -1 || b2 == -1 || b3 == -1 || b4 == -1) + return -1; + return (b1 << 24) | (b2 << 16) | (b3 << 8) + b4; + } catch (IOException e) { + throw new PngjInputException("error reading Int4", e); + } + } + + public static int readInt1fromByte(byte[] b, int offset) { + return (b[offset] & 0xff); + } + + public static int readInt2fromBytes(byte[] b, int offset) { + return ((b[offset] & 0xff) << 8) | ((b[offset + 1] & 0xff)); + } + + public static final int readInt4fromBytes(byte[] b, int offset) { + return ((b[offset] & 0xff) << 24) | ((b[offset + 1] & 0xff) << 16) + | ((b[offset + 2] & 0xff) << 8) | (b[offset + 3] & 0xff); + } + + public static void writeByte(OutputStream os, byte b) { + try { + os.write(b); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void writeByte(OutputStream os, byte[] bs) { + try { + os.write(bs); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void writeInt2(OutputStream os, int n) { + byte[] temp = {(byte) ((n >> 8) & 0xff), (byte) (n & 0xff)}; + writeBytes(os, temp); + } + + public static void writeInt4(OutputStream os, int n) { + byte[] temp = new byte[4]; + writeInt4tobytes(n, temp, 0); + writeBytes(os, temp); + } + + public static void writeInt2tobytes(int n, byte[] b, int offset) { + b[offset] = (byte) ((n >> 8) & 0xff); + b[offset + 1] = (byte) (n & 0xff); + } + + public static void writeInt4tobytes(int n, byte[] b, int offset) { + b[offset] = (byte) ((n >> 24) & 0xff); + b[offset + 1] = (byte) ((n >> 16) & 0xff); + b[offset + 2] = (byte) ((n >> 8) & 0xff); + b[offset + 3] = (byte) (n & 0xff); + } + + + /** + * guaranteed to read exactly len bytes. throws error if it can't + */ + public static void readBytes(InputStream is, byte[] b, int offset, int len) { + if (len == 0) + return; + try { + int read = 0; + while (read < len) { + int n = is.read(b, offset + read, len - read); + if (n < 1) + throw new PngjInputException("error reading bytes, " + n + " !=" + len); + read += n; + } + } catch (IOException e) { + throw new PngjInputException("error reading", e); + } + } + + public static void skipBytes(InputStream is, long len) { + try { + while (len > 0) { + long n1 = is.skip(len); + if (n1 > 0) { + len -= n1; + } else if (n1 == 0) { // should we retry? lets read one byte + if (is.read() == -1) // EOF + break; + else + len--; + } else + // negative? this should never happen but... + throw new IOException("skip() returned a negative value ???"); + } + } catch (IOException e) { + throw new PngjInputException(e); + } + } + + public static void writeBytes(OutputStream os, byte[] b) { + try { + os.write(b); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void writeBytes(OutputStream os, byte[] b, int offset, int n) { + try { + os.write(b, offset, n); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void logdebug(String msg) { + if (isDebug()) + System.err.println("logdebug: " + msg); + } + + // / filters + public static int filterRowNone(int r) { + return (int) (r & 0xFF); + } + + public static int filterRowSub(int r, int left) { + return ((int) (r - left) & 0xFF); + } + + public static int filterRowUp(int r, int up) { + return ((int) (r - up) & 0xFF); + } + + public static int filterRowAverage(int r, int left, int up) { + return (r - (left + up) / 2) & 0xFF; + } + + public static int filterRowPaeth(int r, int left, int up, int upleft) { // a = left, b = above, c + // = upper left + return (r - filterPaethPredictor(left, up, upleft)) & 0xFF; + } + + final static int filterPaethPredictor(final int a, final int b, final int c) { // a = left, b = + // above, c = upper + // left + // from http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html + + final int p = a + b - c;// ; initial estimate + final int pa = p >= a ? p - a : a - p; + final int pb = p >= b ? p - b : b - p; + final int pc = p >= c ? p - c : c - p; + // ; return nearest of a,b,c, + // ; breaking ties in order a,b,c. + if (pa <= pb && pa <= pc) + return a; + else if (pb <= pc) + return b; + else + return c; + } + + /** + * Prits a debug message (prints class name, method and line number) + * + * @param obj : Object to print + */ + public static void debug(Object obj) { + debug(obj, 1, true); + } + + /** + * Prits a debug message (prints class name, method and line number) + * + * @param obj : Object to print + * @param offset : Offset N lines from stacktrace + */ + static void debug(Object obj, int offset) { + debug(obj, offset, true); + } + + public static InputStream istreamFromFile(File f) { + FileInputStream is; + try { + is = new FileInputStream(f); + } catch (Exception e) { + throw new PngjInputException("Could not open " + f, e); + } + return is; + } + + static OutputStream ostreamFromFile(File f) { + return ostreamFromFile(f, true); + } + + static OutputStream ostreamFromFile(File f, boolean overwrite) { + return PngHelperInternal2.ostreamFromFile(f, overwrite); + } + + /** + * Prints a debug message (prints class name, method and line number) to stderr and logFile + * + * @param obj : Object to print + * @param offset : Offset N lines from stacktrace + * @param newLine : Print a newline char at the end ('\n') + */ + static void debug(Object obj, int offset, boolean newLine) { + StackTraceElement ste = new Exception().getStackTrace()[1 + offset]; + String steStr = ste.getClassName(); + int ind = steStr.lastIndexOf('.'); + steStr = steStr.substring(ind + 1); + steStr += + "." + ste.getMethodName() + "(" + ste.getLineNumber() + "): " + + (obj == null ? null : obj.toString()); + System.err.println(steStr); + } + + /** + * Sets a global debug flag. This is bound to a thread. + */ + public static void setDebug(boolean b) { + DEBUG.set(b); + } + + public static boolean isDebug() { + return DEBUG.get().booleanValue(); + } + + public static long getDigest(PngReader pngr) { + return pngr.getSimpleDigest(); + } + + public static void initCrcForTests(PngReader pngr) { + pngr.prepareSimpleDigestComputation(); + } + + public static long getRawIdatBytes(PngReader r) { // in case of image with frames, returns the current one + return r.interlaced ? r.getChunkseq().getDeinterlacer().getTotalRawBytes() : r.getCurImgInfo() + .getTotalRawBytes(); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngHelperInternal2.java b/src/jar-specific/java/ar/com/hjg/pngj/PngHelperInternal2.java new file mode 100644 index 00000000..ab34ce15 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngHelperInternal2.java @@ -0,0 +1,33 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.OutputStream; + +/** + * For organization purposes, this class is the onlt that uses classes not in GAE (Google App Engine) white list + *

+ * You should not use this class in GAE + */ +final class PngHelperInternal2 { + + /** + * WARNING: this uses FileOutputStream which is not allowed in GoogleAppEngine + * + * In GAE, dont use this + * + * @param f + * @param allowoverwrite + * @return + */ + static OutputStream ostreamFromFile(File f, boolean allowoverwrite) { + java.io.FileOutputStream os = null; // this will fail in GAE! + if (f.exists() && !allowoverwrite) + throw new PngjOutputException("File already exists: " + f); + try { + os = new java.io.FileOutputStream(f); + } catch (Exception e) { + throw new PngjInputException("Could not open for write" + f, e); + } + return os; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngReader.java b/src/jar-specific/java/ar/com/hjg/pngj/PngReader.java new file mode 100644 index 00000000..6367761c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngReader.java @@ -0,0 +1,586 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.InputStream; +import java.util.zip.Adler32; +import java.util.zip.CRC32; + +import ar.com.hjg.pngj.chunks.ChunkLoadBehaviour; +import ar.com.hjg.pngj.chunks.ChunksList; +import ar.com.hjg.pngj.chunks.PngChunkFCTL; +import ar.com.hjg.pngj.chunks.PngChunkFDAT; +import ar.com.hjg.pngj.chunks.PngChunkIDAT; +import ar.com.hjg.pngj.chunks.PngMetadata; + +/** + * Reads a PNG image (pixels and/or metadata) from a file or stream. + *

+ * Each row is read as an {@link ImageLineInt} object (one int per sample), but this can be changed by setting a + * different ImageLineFactory + *

+ * Internally, this wraps a {@link ChunkSeqReaderPng} with a {@link BufferedStreamFeeder} + *

+ * The reading sequence is as follows:
+ * 1. At construction time, the header and IHDR chunk are read (basic image info)
+ * 2. Afterwards you can set some additional global options. Eg. {@link #setCrcCheckDisabled()}.
+ * 3. Optional: If you call getMetadata() or getChunksLisk() before start reading the rows, all the chunks before IDAT + * are then loaded and available
+ * 4a. The rows are read in order by calling {@link #readRow()}. You can also call {@link #readRow(int)} to skip rows + * -but you can't go backwards, at least not with this implementation. This method returns a {@link IImageLine} object + * which can be casted to the concrete class. This class returns by default a {@link ImageLineInt}, but this can be + * changed.
+ * 4b. Alternatively, you can read all rows, or a subset, in a single call: {@link #readRows()}, + * {@link #readRows(int, int, int)} ,etc. In general this consumes more memory, but for interlaced images this is + * equally efficient, and more so if reading a small subset of rows.
+ * 5. Reading of the last row automatically loads the trailing chunks, and ends the reader.
+ * 6. end() also loads the trailing chunks, if not done, and finishes cleanly the reading and closes the stream. + *

+ * See also {@link PngReaderInt} (esentially the same as this, and slightly preferred) and {@link PngReaderByte} (uses + * byte instead of int to store the samples). + */ +public class PngReader { + + // some performance/defensive limits + /** + * Defensive limit: refuse to read more than 900MB, can be changed with {@link #setMaxTotalBytesRead(long)} + */ + public static final long MAX_TOTAL_BYTES_READ_DEFAULT = 901001001L; // ~ 900MB + + /** + * Defensive limit: refuse to load more than 5MB of ancillary metadata, see {@link #setMaxBytesMetadata(long)} and + * also {@link #addChunkToSkip(String)} + */ + public static final long MAX_BYTES_METADATA_DEFAULT = 5024024; // for ancillary chunks + + /** + * Skip ancillary chunks greater than 2MB, see {@link #setSkipChunkMaxSize(long)} + */ + public static final long MAX_CHUNK_SIZE_SKIP = 2024024; // chunks exceeding this size will be skipped (nor even CRC + // checked) + + /** + * Basic image info - final and inmutable. + */ + public final ImageInfo imgInfo; // People always told me: be careful what you do, and don't go around declaring public + // fields... + /** + * flag: image was in interlaced format + */ + public final boolean interlaced; + + /** + * This object has most of the intelligence to parse the chunks and decompress the IDAT stream + */ + protected final ChunkSeqReaderPng chunkseq; + + /** + * Takes bytes from the InputStream and passes it to the ChunkSeqReaderPng. Never null. + */ + protected final BufferedStreamFeeder streamFeeder; + + /** + * @see #getMetadata() + */ + protected final PngMetadata metadata; // this a wrapper over chunks + + /** + * Current row number (reading or read), numbered from 0 + */ + protected int rowNum = -1; + + /** + * Represents the set of lines (rows) being read. Normally this works as a cursor, storing only one (the current) row. + * This stores several (perhaps all) rows only if calling {@link #readRows()} or for interlaced images (this later is + * transparent to the user) + */ + protected IImageLineSet imlinesSet; + + /** + * This factory decides the concrete type of the ImageLine that will be used. See {@link ImageLineSetDefault} for + * examples + */ + private IImageLineSetFactory imageLineSetFactory; + + CRC32 idatCrca;// for internal testing + Adler32 idatCrcb;// for internal testing + + /** + * Constructs a PngReader object from a stream, with default options. This reads the signature and the first IHDR + * chunk only. + *

+ * Warning: In case of exception the stream is NOT closed. + *

+ * Warning: By default the stream will be closed when this object is {@link #close()}d. See + * {@link #PngReader(InputStream,boolean)} or {@link #setShouldCloseStream(boolean)} + *

+ * + * @param inputStream PNG stream + */ + public PngReader(InputStream inputStream) { + this(inputStream, true); + } + + /** + * Same as {@link #PngReader(InputStream)} but allows to specify early if the stream must be closed + * + * @param inputStream + * @param shouldCloseStream The stream will be closed in case of exception (constructor included) or normal + * termination. + */ + public PngReader(InputStream inputStream, boolean shouldCloseStream) { + streamFeeder = new BufferedStreamFeeder(inputStream); + streamFeeder.setCloseStream(shouldCloseStream); + chunkseq = createChunkSeqReader(); + try { + streamFeeder.setFailIfNoFeed(true); + if (!streamFeeder.feedFixed(chunkseq, 36)) // 8+13+12=36 PNG signature+IHDR chunk + throw new PngjInputException("error reading first 21 bytes"); + imgInfo = chunkseq.getImageInfo(); + interlaced = chunkseq.getDeinterlacer() != null; + setMaxBytesMetadata(MAX_BYTES_METADATA_DEFAULT); + setMaxTotalBytesRead(MAX_TOTAL_BYTES_READ_DEFAULT); + setSkipChunkMaxSize(MAX_CHUNK_SIZE_SKIP); + chunkseq.addChunkToSkip(PngChunkFDAT.ID);// default: skip fdAT chunks! + chunkseq.addChunkToSkip(PngChunkFCTL.ID);// default: skip fctl chunks! + this.metadata = new PngMetadata(chunkseq.chunksList); + // sets a default factory (with ImageLineInt), + // this can be overwriten by a extended constructor, or by a setter + setLineSetFactory(ImageLineSetDefault.getFactoryInt()); + rowNum = -1; + } catch (RuntimeException e) { + streamFeeder.close(); + chunkseq.close(); + throw e; + } + } + + + /** + * Constructs a PngReader opening a file. Sets shouldCloseStream=true, so that the stream will be closed with + * this object. + * + * @param file PNG image file + */ + public PngReader(File file) { + this(PngHelperInternal.istreamFromFile(file), true); + } + + + /** + * Reads chunks before first IDAT. Normally this is called automatically + *

+ * Position before: after IDHR (crc included) Position after: just after the first IDAT chunk id + *

+ * This can be called several times (tentatively), it does nothing if already run + *

+ * (Note: when should this be called? in the constructor? hardly, because we loose the opportunity to call + * setChunkLoadBehaviour() and perhaps other settings before reading the first row? but sometimes we want to access + * some metadata (plte, phys) before. Because of this, this method can be called explicitly but is also called + * implicititly in some methods (getMetatada(), getChunksList()) + */ + protected void readFirstChunks() { + while (chunkseq.currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT) + if (streamFeeder.feed(chunkseq) <= 0) + throw new PngjInputException("premature ending reading first chunks"); + } + + /** + * Determines which ancillary chunks (metadata) are to be loaded and which skipped. + *

+ * Additional restrictions may apply. See also {@link #setChunksToSkip(String...)}, {@link #addChunkToSkip(String)}, + * {@link #setMaxBytesMetadata(long)}, {@link #setSkipChunkMaxSize(long)} + * + * @param chunkLoadBehaviour {@link ChunkLoadBehaviour} + */ + public void setChunkLoadBehaviour(ChunkLoadBehaviour chunkLoadBehaviour) { + this.chunkseq.setChunkLoadBehaviour(chunkLoadBehaviour); + } + + /** + * All loaded chunks (metada). If we have not yet end reading the image, this will include only the chunks before the + * pixels data (IDAT) + *

+ * Critical chunks are included, except that all IDAT chunks appearance are replaced by a single dummy-marker IDAT + * chunk. These might be copied to the PngWriter + *

+ * + * @see #getMetadata() + */ + public ChunksList getChunksList() { + return getChunksList(true); + } + + public ChunksList getChunksList(boolean forceLoadingOfFirstChunks) { + if (forceLoadingOfFirstChunks && chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + return chunkseq.chunksList; + } + + int getCurrentChunkGroup() { + return chunkseq.currentChunkGroup; + } + + /** + * High level wrapper over chunksList + * + * @see #getChunksList() + */ + public PngMetadata getMetadata() { + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + return metadata; + } + + /** + * Reads next row. + * + * The caller must know that there are more rows to read. + * + * @return Never null. Throws PngInputException if no more + */ + public IImageLine readRow() { + return readRow(rowNum + 1); + } + + /** + * True if last row has not yet been read + */ + public boolean hasMoreRows() { + return rowNum < getCurImgInfo().rows - 1; + } + + /** + * The row number is mostly meant as a check, the rows must be called in ascending order (not necessarily consecutive) + */ + public IImageLine readRow(int nrow) { + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + if (!interlaced) { + if (imlinesSet == null) + imlinesSet = createLineSet(true, -1, 0, 1); + IImageLine line = imlinesSet.getImageLine(nrow); + if (nrow == rowNum) + return line; // already read?? + else if (nrow < rowNum) + throw new PngjInputException("rows must be read in increasing order: " + nrow); + while (rowNum < nrow) { + while (!chunkseq.getIdatSet().isRowReady()) + if (streamFeeder.feed(chunkseq) < 1) + throw new PngjInputException("premature ending"); + rowNum++; + chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb); + if (rowNum == nrow) { + line.readFromPngRaw(chunkseq.getIdatSet().getUnfilteredRow(), + getCurImgInfo().bytesPerRow + 1, 0, 1); + line.endReadFromPngRaw(); + } + chunkseq.getIdatSet().advanceToNextRow(); + } + return line; + } else { // and now, for something completely different (interlaced!) + if (imlinesSet == null) { + imlinesSet = createLineSet(false, getCurImgInfo().rows, 0, 1); + loadAllInterlaced(getCurImgInfo().rows, 0, 1); + } + rowNum = nrow; + return imlinesSet.getImageLine(nrow); + } + + } + + /** + * Reads all rows in a ImageLineSet This is handy, but less memory-efficient (except for interlaced) + */ + public IImageLineSet readRows() { + return readRows(getCurImgInfo().rows, 0, 1); + } + + /** + * Reads a subset of rows. + *

+ * This method should called once, and not be mixed with {@link #readRow()} + * + * @param nRows how many rows to read (default: imageInfo.rows; negative: autocompute) + * @param rowOffset rows to skip (default:0) + * @param rowStep step between rows to load( default:1) + */ + public IImageLineSet readRows(int nRows, int rowOffset, int rowStep) { + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + if (nRows < 0) + nRows = (getCurImgInfo().rows - rowOffset) / rowStep; + if (rowStep < 1 || rowOffset < 0 || nRows == 0 + || nRows * rowStep + rowOffset > getCurImgInfo().rows) + throw new PngjInputException("bad args"); + if (rowNum >= rowOffset) + throw new PngjInputException("readRows cannot be mixed with readRow"); + imlinesSet = createLineSet(false, nRows, rowOffset, rowStep); + if (!interlaced) { + int m = -1; // last row already read in + while (m < nRows - 1) { + while (!chunkseq.getIdatSet().isRowReady()) + if (streamFeeder.feed(chunkseq) < 1) + throw new PngjInputException("Premature ending"); + rowNum++; + chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb); + m = (rowNum - rowOffset) / rowStep; + if (rowNum >= rowOffset && rowStep * m + rowOffset == rowNum) { + IImageLine line = imlinesSet.getImageLine(rowNum); + line.readFromPngRaw(chunkseq.getIdatSet().getUnfilteredRow(), + getCurImgInfo().bytesPerRow + 1, 0, 1); + line.endReadFromPngRaw(); + } + chunkseq.getIdatSet().advanceToNextRow(); + } + } else { // and now, for something completely different (interlaced) + loadAllInterlaced(nRows, rowOffset, rowStep); + } + chunkseq.getIdatSet().done(); + return imlinesSet; + } + + /** + * Sets the factory that creates the ImageLine. By default, this implementation uses ImageLineInt but this can be + * changed (at construction time or later) by calling this method. + *

+ * See also {@link #createLineSet(boolean, int, int, int)} + * + * @param factory + */ + public void setLineSetFactory(IImageLineSetFactory factory) { + imageLineSetFactory = factory; + } + + /** + * By default this uses the factory (which, by default creates ImageLineInt). You should rarely override this. + *

+ * See doc in {@link IImageLineSetFactory#create(ImageInfo, boolean, int, int, int)} + */ + protected IImageLineSet createLineSet(boolean singleCursor, int nlines, + int noffset, int step) { + return imageLineSetFactory.create(getCurImgInfo(), singleCursor, nlines, noffset, step); + } + + protected void loadAllInterlaced(int nRows, int rowOffset, int rowStep) { + IdatSet idat = chunkseq.getIdatSet(); + int nread = 0; + do { + while (!chunkseq.getIdatSet().isRowReady()) + if (streamFeeder.feed(chunkseq) <= 0) + break; + if (!chunkseq.getIdatSet().isRowReady()) + throw new PngjInputException("Premature ending?"); + chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb); + int rowNumreal = idat.rowinfo.rowNreal; + boolean inset = imlinesSet.hasImageLine(rowNumreal); + if (inset) { + imlinesSet.getImageLine(rowNumreal).readFromPngRaw(idat.getUnfilteredRow(), + idat.rowinfo.buflen, idat.rowinfo.oX, idat.rowinfo.dX); + nread++; + } + idat.advanceToNextRow(); + } while (nread < nRows || !idat.isDone()); + idat.done(); + for (int i = 0, j = rowOffset; i < nRows; i++, j += rowStep) { + imlinesSet.getImageLine(j).endReadFromPngRaw(); + } + } + + /** + * Reads all the (remaining) file, skipping the pixels data. This is much more efficient that calling + * {@link #readRow()}, specially for big files (about 10 times faster!), because it doesn't even decompress the IDAT + * stream and disables CRC check Use this if you are not interested in reading pixels,only metadata. + */ + public void readSkippingAllRows() { + chunkseq.addChunkToSkip(PngChunkIDAT.ID); + chunkseq.addChunkToSkip(PngChunkFDAT.ID); + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + end(); + } + + /** + * Set total maximum bytes to read (0: unlimited; default: 200MB).
+ * These are the bytes read (not loaded) in the input stream. If exceeded, an exception will be thrown. + */ + public void setMaxTotalBytesRead(long maxTotalBytesToRead) { + chunkseq.setMaxTotalBytesRead(maxTotalBytesToRead); + } + + /** + * Set total maximum bytes to load from ancillary chunks (0: unlimited; default: 5Mb).
+ * If exceeded, some chunks will be skipped + */ + public void setMaxBytesMetadata(long maxBytesMetadata) { + chunkseq.setMaxBytesMetadata(maxBytesMetadata); + } + + /** + * Set maximum size in bytes for individual ancillary chunks (0: unlimited; default: 2MB).
+ * Chunks exceeding this length will be skipped (the CRC will not be checked) and the chunk will be saved as a + * PngChunkSkipped object. See also setSkipChunkIds + */ + public void setSkipChunkMaxSize(long skipChunkMaxSize) { + chunkseq.setSkipChunkMaxSize(skipChunkMaxSize); + } + + /** + * Chunks ids to be skipped.
+ * These chunks will be skipped (the CRC will not be checked) and the chunk will be saved as a PngChunkSkipped object. + * See also setSkipChunkMaxSize + */ + public void setChunksToSkip(String... chunksToSkip) { + chunkseq.setChunksToSkip(chunksToSkip); + } + + public void addChunkToSkip(String chunkToSkip) { + chunkseq.addChunkToSkip(chunkToSkip); + } + + public void dontSkipChunk(String chunkToSkip) { + chunkseq.dontSkipChunk(chunkToSkip); + } + + + /** + * if true, input stream will be closed after ending read + *

+ * default=true + */ + public void setShouldCloseStream(boolean shouldCloseStream) { + streamFeeder.setCloseStream(shouldCloseStream); + } + + /** + * Reads till end of PNG stream and call close() + * + * This should normally be called after reading the pixel data, to read the trailing chunks and close the stream. But + * it can be called at anytime. This will also read the first chunks if not still read, and skip pixels (IDAT) if + * still pending. + * + * If you want to read all metadata skipping pixels, readSkippingAllRows() is a little more efficient. + * + * If you want to abort immediately, call instead close() + */ + public void end() { + try { + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + if (chunkseq.getIdatSet() != null && !chunkseq.getIdatSet().isDone()) + chunkseq.getIdatSet().done(); + while (!chunkseq.isDone()) + if (streamFeeder.feed(chunkseq) <= 0) + break; + } finally { + close(); + } + } + + /** + * Releases resources, and closes stream if corresponds. Idempotent, secure, no exceptions. + * + * This can be also called for abort. It is recommended to call this in case of exceptions + */ + public void close() { + try { + if (chunkseq != null) + chunkseq.close(); + } catch (Exception e) { + PngHelperInternal.LOGGER.warning("error closing chunk sequence:" + e.getMessage()); + } + if (streamFeeder != null) + streamFeeder.close(); + } + + /** + * Interlaced PNG is accepted -though not welcomed- now... + */ + public boolean isInterlaced() { + return interlaced; + } + + /** + * Disables the CRC integrity check in IDAT chunks and ancillary chunks, this gives a slight increase in reading speed + * for big files + */ + public void setCrcCheckDisabled() { + chunkseq.setCheckCrc(false); + } + + /** + * Gets wrapped {@link ChunkSeqReaderPng} object + */ + public ChunkSeqReaderPng getChunkseq() { + return chunkseq; + } + + /** called on construction time. Override if you want an alternative class */ + protected ChunkSeqReaderPng createChunkSeqReader() { + return new ChunkSeqReaderPng(false); + } + + + /** + * Enables and prepare the simple digest computation. Must be called before reading the pixels. See + * {@link #getSimpleDigestHex()} + */ + public void prepareSimpleDigestComputation() { + if (idatCrca == null) + idatCrca = new CRC32(); + else + idatCrca.reset(); + if (idatCrcb == null) + idatCrcb = new Adler32(); + else + idatCrcb.reset(); + imgInfo.updateCrc(idatCrca); + idatCrcb.update((byte) imgInfo.rows); // not important + } + + long getSimpleDigest() { + if (idatCrca == null) + return 0; + else + return (idatCrca.getValue() ^ (idatCrcb.getValue() << 31)); + } + + /** + * Pseudo 64-bits digest computed over the basic image properties and the raw pixels data: it should coincide for + * equivalent images encoded with different filters and compressors; but will not coincide for + * interlaced/non-interlaced; also, this does not take into account the palette info. This will be valid only if + * {@link #prepareSimpleDigestComputation()} has been called, and all rows have been read. Not fool-proof, not + * cryptografically secure, only for informal testing and duplicates detection. + * + * @return A 64-digest in hexadecimal + */ + public String getSimpleDigestHex() { + return String.format("%016X", getSimpleDigest()); + } + + /** + * Basic info, for debugging. + */ + public String toString() { // basic info + return imgInfo.toString() + " interlaced=" + interlaced; + } + + /** + * Basic info, in a compact format, apt for scripting COLSxROWS[dBITDEPTH][a][p][g][i] ( the default dBITDEPTH='d8' is + * ommited) + * + */ + public String toStringCompact() { + return imgInfo.toStringBrief() + (interlaced ? "i" : ""); + } + + public ImageInfo getImgInfo() { + return imgInfo; + } + + public ImageInfo getCurImgInfo() { + return chunkseq.getCurImgInfo(); + } + + + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngReaderApng.java b/src/jar-specific/java/ar/com/hjg/pngj/PngReaderApng.java new file mode 100644 index 00000000..105edab7 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngReaderApng.java @@ -0,0 +1,213 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.InputStream; +import java.util.List; + +import ar.com.hjg.pngj.chunks.PngChunk; +import ar.com.hjg.pngj.chunks.PngChunkACTL; +import ar.com.hjg.pngj.chunks.PngChunkFCTL; +import ar.com.hjg.pngj.chunks.PngChunkFDAT; +import ar.com.hjg.pngj.chunks.PngChunkIDAT; + +/** + */ +public class PngReaderApng extends PngReaderByte { + + public PngReaderApng(File file) { + super(file); + dontSkipChunk(PngChunkFCTL.ID); + } + + public PngReaderApng(InputStream inputStream) { + super(inputStream); + dontSkipChunk(PngChunkFCTL.ID); + } + + private Boolean apngKind = null; + private boolean firsIdatApngFrame = false; + protected PngChunkACTL actlChunk; // null if not APNG + private PngChunkFCTL fctlChunk; // current (null for the pseudo still frame) + + /** + * Current frame number (reading or read). First animated frame is 0. Frame -1 represents the IDAT (default image) + * when it's not part of the animation + */ + protected int frameNum = -1; // incremented after each fctl finding + + public boolean isApng() { + if (apngKind == null) { + // this triggers the loading of first chunks; + actlChunk = (PngChunkACTL) getChunksList().getById1(PngChunkACTL.ID); // null if not apng + apngKind = actlChunk != null; + firsIdatApngFrame = fctlChunk != null; + + } + return apngKind.booleanValue(); + } + + + public void advanceToFrame(int frame) { + if (frame < frameNum) + throw new PngjInputException("Cannot go backwards"); + if (frame >= getApngNumFrames()) + throw new PngjInputException("Frame out of range " + frame); + if (frame > frameNum) { + addChunkToSkip(PngChunkIDAT.ID); + addChunkToSkip(PngChunkFDAT.ID); + if (chunkseq.getIdatSet() != null && !chunkseq.getIdatSet().isDone()) + chunkseq.getIdatSet().done(); // seems to be necessary sometimes (we should check this) + while (frameNum < frame & !chunkseq.isDone()) + if (streamFeeder.feed(chunkseq) <= 0) + break; + } + if (frame == frameNum) { // prepare to read rows. at this point we have a new + dontSkipChunk(PngChunkIDAT.ID); + dontSkipChunk(PngChunkFDAT.ID); + rowNum = -1; + imlinesSet = null;// force recreation (this is slightly dirty) + // seek the next IDAT/fDAT - TODO: set the expected sequence number + while (!chunkseq.isDone() && !chunkseq.getCurChunkReader().isFromDeflatedSet()) + if (streamFeeder.feed(chunkseq) <= 0) + break; + } else { + throw new PngjInputException("unexpected error seeking from frame " + frame); + } + } + + /** + * True if it has a default image (IDAT) that is not part of the animation. In that case, we consider it as a + * pseudo-frame (number -1) + */ + public boolean hasExtraStillImage() { + return isApng() && !firsIdatApngFrame; + } + + /** + * Only counts true animation frames. + */ + public int getApngNumFrames() { + if (isApng()) + return actlChunk.getNumFrames(); + else + return 0; + } + + /** + * 0 if it's to been played infinitely. -1 if not APNG + */ + public int getApngNumPlays() { + if (isApng()) + return actlChunk.getNumPlays(); + else + return -1; + } + + @Override + public IImageLine readRow() { + // TODO Auto-generated method stub + return super.readRow(); + } + + @Override + public boolean hasMoreRows() { + // TODO Auto-generated method stub + return super.hasMoreRows(); + } + + @Override + public IImageLine readRow(int nrow) { + // TODO Auto-generated method stub + return super.readRow(nrow); + } + + @Override + public IImageLineSet readRows() { + // TODO Auto-generated method stub + return super.readRows(); + } + + @Override + public IImageLineSet readRows(int nRows, int rowOffset, int rowStep) { + // TODO Auto-generated method stub + return super.readRows(nRows, rowOffset, rowStep); + } + + @Override + public void readSkippingAllRows() { + // TODO Auto-generated method stub + super.readSkippingAllRows(); + } + + @Override + protected ChunkSeqReaderPng createChunkSeqReader() { + ChunkSeqReaderPng cr = new ChunkSeqReaderPng(false) { + + @Override + public boolean shouldSkipContent(int len, String id) { + return super.shouldSkipContent(len, id); + } + + @Override + protected boolean isIdatKind(String id) { + return id.equals(PngChunkIDAT.ID) || id.equals(PngChunkFDAT.ID); + } + + @Override + protected DeflatedChunksSet createIdatSet(String id) { + IdatSet ids = new IdatSet(id, getCurImgInfo(), deinterlacer); + ids.setCallbackMode(callbackMode); + return ids; + } + + + @Override + protected void startNewChunk(int len, String id, long offset) { + super.startNewChunk(len, id, offset); + } + + @Override + protected void postProcessChunk(ChunkReader chunkR) { + super.postProcessChunk(chunkR); + if (chunkR.getChunkRaw().id.equals(PngChunkFCTL.ID)) { + frameNum++; + List chunkslist = chunkseq.getChunks(); + fctlChunk = (PngChunkFCTL) chunkslist.get(chunkslist.size() - 1); + // as this is slightly dirty, we check + if (chunkR.getChunkRaw().getOffset() != fctlChunk.getRaw().getOffset()) + throw new PngjInputException("something went wrong"); + ImageInfo frameInfo = fctlChunk.getEquivImageInfo(); + getChunkseq().updateCurImgInfo(frameInfo); + } + } + + @Override + protected boolean countChunkTypeAsAncillary(String id) { + // we don't count fdat as ancillary data + return super.countChunkTypeAsAncillary(id) && !id.equals(id.equals(PngChunkFDAT.ID)); + } + + }; + return cr; + } + + /** + * @see #frameNum + */ + public int getFrameNum() { + return frameNum; + } + + @Override + public void end() { + // TODO Auto-generated method stub + super.end(); + } + + public PngChunkFCTL getFctl() { + return fctlChunk; + } + + + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngReaderByte.java b/src/jar-specific/java/ar/com/hjg/pngj/PngReaderByte.java new file mode 100644 index 00000000..3b34fdf6 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngReaderByte.java @@ -0,0 +1,30 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.InputStream; + +/** + * Trivial extension of {@link PngReader} that uses {@link ImageLineByte} + *

+ * The factory is set at construction time. Remember that this could still be changed at runtime. + */ +public class PngReaderByte extends PngReader { + + public PngReaderByte(File file) { + super(file); + setLineSetFactory(ImageLineSetDefault.getFactoryByte()); + } + + public PngReaderByte(InputStream inputStream) { + super(inputStream); + setLineSetFactory(ImageLineSetDefault.getFactoryByte()); + } + + /** + * Utility method that casts {@link #readRow()} return to {@link ImageLineByte}. + */ + public ImageLineByte readRowByte() { + return (ImageLineByte) readRow(); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngReaderFilter.java b/src/jar-specific/java/ar/com/hjg/pngj/PngReaderFilter.java new file mode 100644 index 00000000..cafe2d4c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngReaderFilter.java @@ -0,0 +1,99 @@ +package ar.com.hjg.pngj; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import ar.com.hjg.pngj.chunks.PngChunk; + +/** + * This class allows to use a simple PNG reader as an input filter, wrapping a ChunkSeqReaderPng in callback mode. + * + * In this sample implementation, all IDAT chunks are skipped and the rest are stored. An example of use, that lets us + * grab the Metadata and let the pixels go towards a BufferedImage: + * + * + *

+ * PngReaderFilter reader = new PngReaderFilter(new FileInputStream("image.png"));
+ * BufferedImage image1 = ImageIO.read(reader);
+ * reader.readUntilEndAndClose(); // in case ImageIO.read() does not read the traling chunks (it happens)
+ * System.out.println(reader.getChunksList());
+ * 
+ * + */ +public class PngReaderFilter extends FilterInputStream { + + private ChunkSeqReaderPng chunkseq; + + public PngReaderFilter(InputStream arg0) { + super(arg0); + chunkseq = createChunkSequenceReader(); + } + + protected ChunkSeqReaderPng createChunkSequenceReader() { + return new ChunkSeqReaderPng(true) { + @Override + public boolean shouldSkipContent(int len, String id) { + return super.shouldSkipContent(len, id) || id.equals("IDAT"); + } + + @Override + protected boolean shouldCheckCrc(int len, String id) { + return false; + } + + @Override + protected void postProcessChunk(ChunkReader chunkR) { + super.postProcessChunk(chunkR); + // System.out.println("processed chunk " + chunkR.getChunkRaw().id); + } + }; + } + + @Override + public void close() throws IOException { + super.close(); + chunkseq.close(); + } + + @Override + public int read() throws IOException { + int r = super.read(); + if (r > 0) + chunkseq.feedAll(new byte[] {(byte) r}, 0, 1); + return r; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int res = super.read(b, off, len); + if (res > 0) + chunkseq.feedAll(b, off, res); + return res; + } + + @Override + public int read(byte[] b) throws IOException { + int res = super.read(b); + if (res > 0) + chunkseq.feedAll(b, 0, res); + return res; + } + + public void readUntilEndAndClose() throws IOException { + BufferedStreamFeeder br = new BufferedStreamFeeder(this.in); + while ((!chunkseq.isDone()) && br.hasMoreToFeed()) + br.feed(chunkseq); + close(); + } + + public List getChunksList() { + return chunkseq.getChunks(); + } + + public ChunkSeqReaderPng getChunkseq() { + return chunkseq; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngReaderInt.java b/src/jar-specific/java/ar/com/hjg/pngj/PngReaderInt.java new file mode 100644 index 00000000..9f18cfb6 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngReaderInt.java @@ -0,0 +1,37 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.InputStream; + +/** + * Trivial extension of {@link PngReader} that uses {@link ImageLineInt}. + *

+ * In the current implementation this is quite dummy/redundant, because (for backward compatibility) PngReader already + * uses a {@link ImageLineInt}. + *

+ * The factory is set at construction time. Remember that this could still be changed at runtime. + */ +public class PngReaderInt extends PngReader { + + public PngReaderInt(File file) { + super(file); // not necessary to set factory, PngReader already does that + } + + public PngReaderInt(InputStream inputStream) { + super(inputStream); + } + + /** + * Utility method that casts the IImageLine to a ImageLineInt + * + * This only make sense for this concrete class + * + */ + public ImageLineInt readRowInt() { + IImageLine line = readRow(); + if (line instanceof ImageLineInt) + return (ImageLineInt) line; + else + throw new PngjException("This is not a ImageLineInt : " + line.getClass()); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngWriter.java b/src/jar-specific/java/ar/com/hjg/pngj/PngWriter.java new file mode 100644 index 00000000..c8906906 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngWriter.java @@ -0,0 +1,427 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.OutputStream; +import java.util.List; + +import ar.com.hjg.pngj.chunks.ChunkCopyBehaviour; +import ar.com.hjg.pngj.chunks.ChunkPredicate; +import ar.com.hjg.pngj.chunks.ChunksList; +import ar.com.hjg.pngj.chunks.ChunksListForWrite; +import ar.com.hjg.pngj.chunks.PngChunk; +import ar.com.hjg.pngj.chunks.PngChunkIEND; +import ar.com.hjg.pngj.chunks.PngChunkIHDR; +import ar.com.hjg.pngj.chunks.PngChunkPLTE; +import ar.com.hjg.pngj.chunks.PngMetadata; +import ar.com.hjg.pngj.pixels.PixelsWriter; +import ar.com.hjg.pngj.pixels.PixelsWriterDefault; + +/** + * Writes a PNG image, line by line. + */ +public class PngWriter { + + public final ImageInfo imgInfo; + + /** + * last writen row number, starting from 0 + */ + protected int rowNum = -1; + + private final ChunksListForWrite chunksList; + + private final PngMetadata metadata; + + /** + * Current chunk grounp, (0-6) already written or currently writing (this is advanced when just starting to write the + * new group, not when finalizing the previous) + *

+ * see {@link ChunksList} + */ + protected int currentChunkGroup = -1; + + private int passes = 1; // Some writes might require two passes (NOT USED STILL) + private int currentpass = 0; // numbered from 1 + + private boolean shouldCloseStream = true; + + private int idatMaxSize = 0; // 0=use default (PngIDatChunkOutputStream 64k) + // private PngIDatChunkOutputStream datStream; + + protected PixelsWriter pixelsWriter; + + private final OutputStream os; + + private ChunkPredicate copyFromPredicate = null; + private ChunksList copyFromList = null; + + protected StringBuilder debuginfo = new StringBuilder(); + + /** + * Opens a file for writing. + *

+ * Sets shouldCloseStream=true. For more info see {@link #PngWriter(OutputStream, ImageInfo)} + * + * @param file + * @param imgInfo + * @param allowoverwrite If false and file exists, an {@link PngjOutputException} is thrown + */ + public PngWriter(File file, ImageInfo imgInfo, boolean allowoverwrite) { + this(PngHelperInternal.ostreamFromFile(file, allowoverwrite), imgInfo); + setShouldCloseStream(true); + } + + /** + * @see #PngWriter(File, ImageInfo, boolean) (overwrite=true) + */ + public PngWriter(File file, ImageInfo imgInfo) { + this(file, imgInfo, true); + } + + /** + * Constructs a new PngWriter from a output stream. After construction nothing is writen yet. You still can set some + * parameters (compression, filters) and queue chunks before start writing the pixels. + *

+ * + * @param outputStream Open stream for binary writing + * @param imgInfo Basic image parameters + */ + public PngWriter(OutputStream outputStream, ImageInfo imgInfo) { + this.os = outputStream; + this.imgInfo = imgInfo; + // prealloc + chunksList = new ChunksListForWrite(imgInfo); + metadata = new PngMetadata(chunksList); + pixelsWriter = createPixelsWriter(imgInfo); + setCompLevel(9); + } + + private void initIdat() { // this triggers the writing of first chunks + pixelsWriter.setOs(this.os); + pixelsWriter.setIdatMaxSize(idatMaxSize); + writeSignatureAndIHDR(); + writeFirstChunks(); + } + + private void writeEndChunk() { + currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; + PngChunkIEND c = new PngChunkIEND(imgInfo); + c.createRawChunk().writeChunk(os); + chunksList.getChunks().add(c); + } + + private void writeFirstChunks() { + if (currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT) + return; + int nw = 0; + currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + queueChunksFromOther(); + nw = chunksList.writeChunks(os, currentChunkGroup); + currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE; + nw = chunksList.writeChunks(os, currentChunkGroup); + if (nw > 0 && imgInfo.greyscale) + throw new PngjOutputException("cannot write palette for this format"); + if (nw == 0 && imgInfo.indexed) + throw new PngjOutputException("missing palette"); + currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE; + nw = chunksList.writeChunks(os, currentChunkGroup); + } + + private void writeLastChunks() { // not including end + currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; + queueChunksFromOther(); + chunksList.writeChunks(os, currentChunkGroup); + // should not be unwriten chunks + List pending = chunksList.getQueuedChunks(); + if (!pending.isEmpty()) + throw new PngjOutputException(pending.size() + " chunks were not written! Eg: " + + pending.get(0).toString()); + } + + /** + * Write id signature and also "IHDR" chunk + */ + private void writeSignatureAndIHDR() { + PngHelperInternal.writeBytes(os, PngHelperInternal.getPngIdSignature()); // signature + currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR; + PngChunkIHDR ihdr = new PngChunkIHDR(imgInfo); + // http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html + ihdr.createRawChunk().writeChunk(os); + chunksList.getChunks().add(ihdr); + } + + private void queueChunksFromOther() { + if (copyFromList == null || copyFromPredicate == null) + return; + boolean idatDone = currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT; // we assume this method is not either before + // or after the IDAT writing, not in the + // middle! + for (PngChunk chunk : copyFromList.getChunks()) { + if (chunk.getRaw().data == null) + continue; // we cannot copy skipped chunks? + int groupOri = chunk.getChunkGroup(); + if (groupOri <= ChunksList.CHUNK_GROUP_4_IDAT && idatDone) + continue; + if (groupOri >= ChunksList.CHUNK_GROUP_4_IDAT && !idatDone) + continue; + if (chunk.crit && !chunk.id.equals(PngChunkPLTE.ID)) + continue; // critical chunks (except perhaps PLTE) are never + // copied + boolean copy = copyFromPredicate.match(chunk); + if (copy) { + // but if the chunk is already queued or writen, it's ommited! + if (chunksList.getEquivalent(chunk).isEmpty() + && chunksList.getQueuedEquivalent(chunk).isEmpty()) { + chunksList.queue(chunk); + } + } + } + } + +/** + * Queues an ancillary chunk for writing. + *

+ * If a "equivalent" chunk is already queued (see {@link ChunkHelper#equivalent(PngChunk, PngChunk)), this overwrites it. + *

+ * The chunk will be written as late as possible, unless the priority is set. + * + * @param chunk + */ + public void queueChunk(PngChunk chunk) { + for (PngChunk other : chunksList.getQueuedEquivalent(chunk)) { + getChunksList().removeChunk(other); + } + chunksList.queue(chunk); + } + + /** + * Sets an origin (typically from a {@link PngReader}) of Chunks to be copied. This should be called only once, before + * starting writing the rows. It doesn't matter the current state of the PngReader reading, this is a live object and + * what matters is that when the writer writes the pixels (IDAT) the reader has already read them, and that when the + * writer ends, the reader is already ended (all this is very natural). + *

+ * Apart from the copyMask, there is some addional heuristics: + *

+ * - The chunks will be queued, but will be written as late as possible (unless you explicitly set priority=true) + *

+ * - The chunk will not be queued if an "equivalent" chunk was already queued explicitly. And it will be overwriten + * another is queued explicitly. + * + * @param chunks + * @param copyMask Some bitmask from {@link ChunkCopyBehaviour} + * + * @see #copyChunksFrom(ChunksList, ChunkPredicate) + */ + public void copyChunksFrom(ChunksList chunks, int copyMask) { + copyChunksFrom(chunks, ChunkCopyBehaviour.createPredicate(copyMask, imgInfo)); + } + + /** + * Copy all chunks from origin. See {@link #copyChunksFrom(ChunksList, int)} for more info + */ + public void copyChunksFrom(ChunksList chunks) { + copyChunksFrom(chunks, ChunkCopyBehaviour.COPY_ALL); + } + + /** + * Copy chunks from origin depending on some {@link ChunkPredicate} + * + * @param chunks + * @param predicate The chunks (ancillary or PLTE) will be copied if and only if predicate matches + * + * @see #copyChunksFrom(ChunksList, int) for more info + */ + public void copyChunksFrom(ChunksList chunks, ChunkPredicate predicate) { + if (copyFromList != null && chunks != null) + PngHelperInternal.LOGGER.warning("copyChunksFrom should only be called once"); + if (predicate == null) + throw new PngjOutputException("copyChunksFrom requires a predicate"); + this.copyFromList = chunks; + this.copyFromPredicate = predicate; + } + + /** + * Computes compressed size/raw size, approximate. + *

+ * Actually: compressed size = total size of IDAT data , raw size = uncompressed pixel bytes = rows * (bytesPerRow + + * 1). + * + * This must be called after pngw.end() + */ + public double computeCompressionRatio() { + if (currentChunkGroup < ChunksList.CHUNK_GROUP_5_AFTERIDAT) + throw new PngjOutputException("must be called after end()"); + return pixelsWriter.getCompression(); + } + + /** + * Finalizes all the steps and closes the stream. This must be called after writing the lines. Idempotent + */ + public void end() { + if (rowNum != imgInfo.rows - 1 || !pixelsWriter.isDone()) + throw new PngjOutputException("all rows have not been written"); + try { + if (pixelsWriter != null) + pixelsWriter.close(); + if (currentChunkGroup < ChunksList.CHUNK_GROUP_5_AFTERIDAT) + writeLastChunks(); + if (currentChunkGroup < ChunksList.CHUNK_GROUP_6_END) + writeEndChunk(); + } finally { + close(); + } + } + + /** + * Closes and releases resources + *

+ * This is normally called internally from {@link #end()}, you should only call this for aborting the writing and + * release resources (close the stream). + *

+ * Idempotent and secure - never throws exceptions + */ + public void close() { + if (pixelsWriter != null) + pixelsWriter.close(); + if (shouldCloseStream && os != null) + try { + os.close(); + } catch (Exception e) { + PngHelperInternal.LOGGER.warning("Error closing writer " + e.toString()); + } + } + + /** + * returns the chunks list (queued and writen chunks) + */ + public ChunksListForWrite getChunksList() { + return chunksList; + } + + /** + * Retruns a high level wrapper over for metadata handling + */ + public PngMetadata getMetadata() { + return metadata; + } + + /** + * Sets internal prediction filter type, or strategy to choose it. + *

+ * This must be called just after constructor, before starting writing. + *

+ */ + public void setFilterType(FilterType filterType) { + pixelsWriter.setFilterType(filterType); + } + + /** + * This is kept for backwards compatibility, now the PixelsWriter object should be used for setting + * compression/filtering options + * + * @see PixelsWriter#setCompressionFactor(double) + * @param compLevel between 0 (no compression, max speed) and 9 (max compression) + */ + public void setCompLevel(int complevel) { + pixelsWriter.setDeflaterCompLevel(complevel); + } + + /** + * + */ + public void setFilterPreserve(boolean filterPreserve) { + if (filterPreserve) + pixelsWriter.setFilterType(FilterType.FILTER_PRESERVE); + else if (pixelsWriter.getFilterType() == null) + pixelsWriter.setFilterType(FilterType.FILTER_DEFAULT); + } + + /** + * Sets maximum size of IDAT fragments. Incrementing this from the default has very little effect on compression and + * increments memory usage. You should rarely change this. + *

+ * + * @param idatMaxSize default=0 : use defaultSize (32K) + */ + public void setIdatMaxSize(int idatMaxSize) { + this.idatMaxSize = idatMaxSize; + } + + /** + * If true, output stream will be closed after ending write + *

+ * default=true + */ + public void setShouldCloseStream(boolean shouldCloseStream) { + this.shouldCloseStream = shouldCloseStream; + } + + /** + * Writes next row, does not check row number. + * + * @param imgline + */ + public void writeRow(IImageLine imgline) { + writeRow(imgline, rowNum + 1); + } + + /** + * Writes the full set of row. The ImageLineSet should contain (allow to acces) imgInfo.rows + */ + public void writeRows(IImageLineSet imglines) { + for (int i = 0; i < imgInfo.rows; i++) + writeRow(imglines.getImageLineRawNum(i)); + } + + public void writeRow(IImageLine imgline, int rownumber) { + rowNum++; + if (rowNum == imgInfo.rows) + rowNum = 0; + if (rownumber == imgInfo.rows) + rownumber = 0; + if (rownumber >= 0 && rowNum != rownumber) + throw new PngjOutputException("rows must be written in order: expected:" + rowNum + + " passed:" + rownumber); + if (rowNum == 0) + currentpass++; + if (rownumber == 0 && currentpass == passes) { + initIdat(); + currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT; // we just begin writing IDAT + } + byte[] rowb = pixelsWriter.getRowb(); + imgline.writeToPngRaw(rowb); + pixelsWriter.processRow(rowb); + + } + + /** + * Utility method, uses internaly a ImageLineInt + */ + public void writeRowInt(int[] buf) { + writeRow(new ImageLineInt(imgInfo, buf)); + } + + /** + * Factory method for pixels writer. This will be called once at the moment at start writing a set of IDAT chunks + * (typically once in a normal PNG) + * + * This should be overriden if custom filtering strategies are desired. Remember to release this with close() + * + * @param imginfo Might be different than that of this object (eg: APNG with subimages) + * @param os Output stream + * @return new PixelsWriter. Don't forget to call close() when discarding it + */ + protected PixelsWriter createPixelsWriter(ImageInfo imginfo) { + PixelsWriterDefault pw = new PixelsWriterDefault(imginfo); + return pw; + } + + public final PixelsWriter getPixelsWriter() { + return pixelsWriter; + } + + public String getDebuginfo() { + return debuginfo.toString(); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngWriterHc.java b/src/jar-specific/java/ar/com/hjg/pngj/PngWriterHc.java new file mode 100644 index 00000000..cc83df84 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngWriterHc.java @@ -0,0 +1,35 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.OutputStream; + +import ar.com.hjg.pngj.pixels.PixelsWriter; +import ar.com.hjg.pngj.pixels.PixelsWriterMultiple; + +/** Pngwriter with High compression EXPERIMENTAL */ +public class PngWriterHc extends PngWriter { + + public PngWriterHc(File file, ImageInfo imgInfo, boolean allowoverwrite) { + super(file, imgInfo, allowoverwrite); + setFilterType(FilterType.FILTER_SUPER_ADAPTIVE); + } + + public PngWriterHc(File file, ImageInfo imgInfo) { + super(file, imgInfo); + } + + public PngWriterHc(OutputStream outputStream, ImageInfo imgInfo) { + super(outputStream, imgInfo); + } + + @Override + protected PixelsWriter createPixelsWriter(ImageInfo imginfo) { + PixelsWriterMultiple pw = new PixelsWriterMultiple(imginfo); + return pw; + } + + public PixelsWriterMultiple getPixelWriterMultiple() { + return (PixelsWriterMultiple) pixelsWriter; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngjBadCrcException.java b/src/jar-specific/java/ar/com/hjg/pngj/PngjBadCrcException.java new file mode 100644 index 00000000..8e337a0c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngjBadCrcException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +/** + * Exception thrown by bad CRC check + */ +public class PngjBadCrcException extends PngjInputException { + private static final long serialVersionUID = 1L; + + public PngjBadCrcException(String message, Throwable cause) { + super(message, cause); + } + + public PngjBadCrcException(String message) { + super(message); + } + + public PngjBadCrcException(Throwable cause) { + super(cause); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngjException.java b/src/jar-specific/java/ar/com/hjg/pngj/PngjException.java new file mode 100644 index 00000000..8471c70a --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngjException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +/** + * Generic exception for this library. It's a RuntimeException (unchecked) + */ +public class PngjException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public PngjException(String message, Throwable cause) { + super(message, cause); + } + + public PngjException(String message) { + super(message); + } + + public PngjException(Throwable cause) { + super(cause); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngjExceptionInternal.java b/src/jar-specific/java/ar/com/hjg/pngj/PngjExceptionInternal.java new file mode 100644 index 00000000..8059e35e --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngjExceptionInternal.java @@ -0,0 +1,23 @@ +package ar.com.hjg.pngj; + +/** + * Exception for anomalous internal problems (sort of asserts) that point to some issue with the library + * + * @author Hernan J Gonzalez + * + */ +public class PngjExceptionInternal extends RuntimeException { + private static final long serialVersionUID = 1L; + + public PngjExceptionInternal(String message, Throwable cause) { + super(message, cause); + } + + public PngjExceptionInternal(String message) { + super(message); + } + + public PngjExceptionInternal(Throwable cause) { + super(cause); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngjInputException.java b/src/jar-specific/java/ar/com/hjg/pngj/PngjInputException.java new file mode 100644 index 00000000..668d6b68 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngjInputException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +/** + * Exception thrown when reading a PNG. + */ +public class PngjInputException extends PngjException { + private static final long serialVersionUID = 1L; + + public PngjInputException(String message, Throwable cause) { + super(message, cause); + } + + public PngjInputException(String message) { + super(message); + } + + public PngjInputException(Throwable cause) { + super(cause); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngjOutputException.java b/src/jar-specific/java/ar/com/hjg/pngj/PngjOutputException.java new file mode 100644 index 00000000..6fea798c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngjOutputException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +/** + * Exception thrown by writing process + */ +public class PngjOutputException extends PngjException { + private static final long serialVersionUID = 1L; + + public PngjOutputException(String message, Throwable cause) { + super(message, cause); + } + + public PngjOutputException(String message) { + super(message); + } + + public PngjOutputException(Throwable cause) { + super(cause); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/PngjUnsupportedException.java b/src/jar-specific/java/ar/com/hjg/pngj/PngjUnsupportedException.java new file mode 100644 index 00000000..46290c20 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/PngjUnsupportedException.java @@ -0,0 +1,24 @@ +package ar.com.hjg.pngj; + +/** + * Exception thrown because of some valid feature of PNG standard that this library does not support. + */ +public class PngjUnsupportedException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public PngjUnsupportedException() { + super(); + } + + public PngjUnsupportedException(String message, Throwable cause) { + super(message, cause); + } + + public PngjUnsupportedException(String message) { + super(message); + } + + public PngjUnsupportedException(Throwable cause) { + super(cause); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/RowInfo.java b/src/jar-specific/java/ar/com/hjg/pngj/RowInfo.java new file mode 100644 index 00000000..e033989c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/RowInfo.java @@ -0,0 +1,55 @@ +package ar.com.hjg.pngj; + +/** + * Packs information of current row. Only used internally + */ +class RowInfo { + public final ImageInfo imgInfo; + public final Deinterlacer deinterlacer; + public final boolean imode; // Interlaced + int dY, dX, oY, oX; // current step and offset (in pixels) + int rowNseq; // row number (from 0) in sequential read order + int rowNreal; // row number in the real image + int rowNsubImg; // current row in the virtual subsampled image; this increments (by 1) from 0 to + // rows/dy 7 times + int rowsSubImg, colsSubImg; // size of current subimage , in pixels + int bytesRow; + int pass; // 1-7 + byte[] buf; // non-deep copy + int buflen; // valid bytes in buffer (include filter byte) + + public RowInfo(ImageInfo imgInfo, Deinterlacer deinterlacer) { + this.imgInfo = imgInfo; + this.deinterlacer = deinterlacer; + this.imode = deinterlacer != null; + } + + void update(int rowseq) { + rowNseq = rowseq; + if (imode) { + pass = deinterlacer.getPass(); + dX = deinterlacer.dX; + dY = deinterlacer.dY; + oX = deinterlacer.oX; + oY = deinterlacer.oY; + rowNreal = deinterlacer.getCurrRowReal(); + rowNsubImg = deinterlacer.getCurrRowSubimg(); + rowsSubImg = deinterlacer.getRows(); + colsSubImg = deinterlacer.getCols(); + bytesRow = (imgInfo.bitspPixel * colsSubImg + 7) / 8; + } else { + pass = 1; + dX = dY = 1; + oX = oY = 0; + rowNreal = rowNsubImg = rowseq; + rowsSubImg = imgInfo.rows; + colsSubImg = imgInfo.cols; + bytesRow = imgInfo.bytesPerRow; + } + } + + void updateBuf(byte[] buf, int buflen) { + this.buf = buf; + this.buflen = buflen; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkCopyBehaviour.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkCopyBehaviour.java new file mode 100644 index 00000000..9e292f50 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkCopyBehaviour.java @@ -0,0 +1,101 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngReader; +import ar.com.hjg.pngj.PngWriter; + +/** + * Chunk copy policy to apply when copyng from a {@link PngReader} to a {@link PngWriter}. + *

+ * The constants are bit-masks, they can be OR-ed + *

+ * Reference: http://www.w3.org/TR/PNG/#14
+ */ +public class ChunkCopyBehaviour { + + /** Don't copy anything */ + public static final int COPY_NONE = 0; + + /** copy the palette */ + public static final int COPY_PALETTE = 1; + + /** copy all 'safe to copy' chunks */ + public static final int COPY_ALL_SAFE = 1 << 2; + + /** + * copy all, including palette + */ + public static final int COPY_ALL = 1 << 3; // includes palette! + /** + * Copy PHYS chunk (physical resolution) + */ + public static final int COPY_PHYS = 1 << 4; // dpi + /** + * Copy al textual chunks. + */ + public static final int COPY_TEXTUAL = 1 << 5; // all textual types + /** + * Copy TRNS chunk + */ + public static final int COPY_TRANSPARENCY = 1 << 6; // + /** + * Copy unknown chunks (unknown by our factory) + */ + public static final int COPY_UNKNOWN = 1 << 7; // all unknown (by the factory!) + /** + * Copy almost all: excepts only HIST (histogram) TIME and TEXTUAL chunks + */ + public static final int COPY_ALMOSTALL = 1 << 8; + + private static boolean maskMatch(int v, int mask) { + return (v & mask) != 0; + } + + /** + * Creates a predicate equivalent to the copy mask + *

+ * Given a copy mask (see static fields) and the ImageInfo of the target PNG, returns a predicate that tells if a + * chunk should be copied. + *

+ * This is a handy helper method, you can also create and set your own predicate + */ + public static ChunkPredicate createPredicate(final int copyFromMask, final ImageInfo imgInfo) { + return new ChunkPredicate() { + public boolean match(PngChunk chunk) { + if (chunk.crit) { + if (chunk.id.equals(ChunkHelper.PLTE)) { + if (imgInfo.indexed && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_PALETTE)) + return true; + if (!imgInfo.greyscale && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALL)) + return true; + } + } else { // ancillary + boolean text = (chunk instanceof PngChunkTextVar); + boolean safe = chunk.safe; + // notice that these if are not exclusive + if (maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALL)) + return true; + if (safe && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALL_SAFE)) + return true; + if (chunk.id.equals(ChunkHelper.tRNS) + && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_TRANSPARENCY)) + return true; + if (chunk.id.equals(ChunkHelper.pHYs) + && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_PHYS)) + return true; + if (text && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_TEXTUAL)) + return true; + if (maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALMOSTALL) + && !(ChunkHelper.isUnknown(chunk) || text || chunk.id.equals(ChunkHelper.hIST) || chunk.id + .equals(ChunkHelper.tIME))) + return true; + if (maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_UNKNOWN) + && ChunkHelper.isUnknown(chunk)) + return true; + } + return false; + } + + }; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkFactory.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkFactory.java new file mode 100644 index 00000000..06969eca --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkFactory.java @@ -0,0 +1,107 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.IChunkFactory; +import ar.com.hjg.pngj.ImageInfo; + +/** + * Default chunk factory. + *

+ * The user that wants to parse custom chunks can extend {@link #createEmptyChunkExtended(String, ImageInfo)} + */ +public class ChunkFactory implements IChunkFactory { + + boolean parse; + + public ChunkFactory() { + this(true); + } + + public ChunkFactory(boolean parse) { + this.parse = parse; + } + + public final PngChunk createChunk(ChunkRaw chunkRaw, ImageInfo imgInfo) { + PngChunk c = createEmptyChunkKnown(chunkRaw.id, imgInfo); + if (c == null) + c = createEmptyChunkExtended(chunkRaw.id, imgInfo); + if (c == null) + c = createEmptyChunkUnknown(chunkRaw.id, imgInfo); + c.setRaw(chunkRaw); + if (parse && chunkRaw.data != null) + c.parseFromRaw(chunkRaw); + return c; + } + + protected final PngChunk createEmptyChunkKnown(String id, ImageInfo imgInfo) { + if (id.equals(ChunkHelper.IDAT)) + return new PngChunkIDAT(imgInfo); + if (id.equals(ChunkHelper.IHDR)) + return new PngChunkIHDR(imgInfo); + if (id.equals(ChunkHelper.PLTE)) + return new PngChunkPLTE(imgInfo); + if (id.equals(ChunkHelper.IEND)) + return new PngChunkIEND(imgInfo); + if (id.equals(ChunkHelper.tEXt)) + return new PngChunkTEXT(imgInfo); + if (id.equals(ChunkHelper.iTXt)) + return new PngChunkITXT(imgInfo); + if (id.equals(ChunkHelper.zTXt)) + return new PngChunkZTXT(imgInfo); + if (id.equals(ChunkHelper.bKGD)) + return new PngChunkBKGD(imgInfo); + if (id.equals(ChunkHelper.gAMA)) + return new PngChunkGAMA(imgInfo); + if (id.equals(ChunkHelper.pHYs)) + return new PngChunkPHYS(imgInfo); + if (id.equals(ChunkHelper.iCCP)) + return new PngChunkICCP(imgInfo); + if (id.equals(ChunkHelper.tIME)) + return new PngChunkTIME(imgInfo); + if (id.equals(ChunkHelper.tRNS)) + return new PngChunkTRNS(imgInfo); + if (id.equals(ChunkHelper.cHRM)) + return new PngChunkCHRM(imgInfo); + if (id.equals(ChunkHelper.sBIT)) + return new PngChunkSBIT(imgInfo); + if (id.equals(ChunkHelper.sRGB)) + return new PngChunkSRGB(imgInfo); + if (id.equals(ChunkHelper.hIST)) + return new PngChunkHIST(imgInfo); + if (id.equals(ChunkHelper.sPLT)) + return new PngChunkSPLT(imgInfo); + // apng + if (id.equals(PngChunkFDAT.ID)) + return new PngChunkFDAT(imgInfo); + if (id.equals(PngChunkACTL.ID)) + return new PngChunkACTL(imgInfo); + if (id.equals(PngChunkFCTL.ID)) + return new PngChunkFCTL(imgInfo); + return null; + } + + /** + * This is used as last resort factory method. + *

+ * It creates a {@link PngChunkUNKNOWN} chunk. + */ + protected final PngChunk createEmptyChunkUnknown(String id, ImageInfo imgInfo) { + return new PngChunkUNKNOWN(id, imgInfo); + } + + /** + * Factory for chunks that are not in the original PNG standard. This can be overriden (but dont forget to call this + * also) + * + * @param id Chunk id , 4 letters + * @param imgInfo Usually not needed + * @return null if chunk id not recognized + */ + protected PngChunk createEmptyChunkExtended(String id, ImageInfo imgInfo) { + if (id.equals(PngChunkOFFS.ID)) + return new PngChunkOFFS(imgInfo); + if (id.equals(PngChunkSTER.ID)) + return new PngChunkSTER(imgInfo); + return null; // extend! + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkHelper.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkHelper.java new file mode 100644 index 00000000..68858978 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkHelper.java @@ -0,0 +1,290 @@ +package ar.com.hjg.pngj.chunks; + +// see http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html +// http://www.w3.org/TR/PNG/#5Chunk-naming-conventions +// http://www.w3.org/TR/PNG/#table53 +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * Helper methods and constants related to Chunk processing. + *

+ * This should only be of interest to developers doing special chunk processing or extending the ChunkFactory + */ +public class ChunkHelper { + ChunkHelper() {} + + public static final String IHDR = "IHDR"; + public static final String PLTE = "PLTE"; + public static final String IDAT = "IDAT"; + public static final String IEND = "IEND"; + public static final String cHRM = "cHRM"; + public static final String gAMA = "gAMA"; + public static final String iCCP = "iCCP"; + public static final String sBIT = "sBIT"; + public static final String sRGB = "sRGB"; + public static final String bKGD = "bKGD"; + public static final String hIST = "hIST"; + public static final String tRNS = "tRNS"; + public static final String pHYs = "pHYs"; + public static final String sPLT = "sPLT"; + public static final String tIME = "tIME"; + public static final String iTXt = "iTXt"; + public static final String tEXt = "tEXt"; + public static final String zTXt = "zTXt"; + + public static final byte[] b_IHDR = toBytes(IHDR); + public static final byte[] b_PLTE = toBytes(PLTE); + public static final byte[] b_IDAT = toBytes(IDAT); + public static final byte[] b_IEND = toBytes(IEND); + + /* + * static auxiliary buffer. any method that uses this should synchronize against this + */ + private static byte[] tmpbuffer = new byte[4096]; + + /** + * Converts to bytes using Latin1 (ISO-8859-1) + */ + public static byte[] toBytes(String x) { + try { + return x.getBytes(PngHelperInternal.charsetLatin1name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to String using Latin1 (ISO-8859-1) + */ + public static String toString(byte[] x) { + try { + return new String(x, PngHelperInternal.charsetLatin1name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to String using Latin1 (ISO-8859-1) + */ + public static String toString(byte[] x, int offset, int len) { + try { + return new String(x, offset, len, PngHelperInternal.charsetLatin1name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to bytes using UTF-8 + */ + public static byte[] toBytesUTF8(String x) { + try { + return x.getBytes(PngHelperInternal.charsetUTF8name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to string using UTF-8 + */ + public static String toStringUTF8(byte[] x) { + try { + return new String(x, PngHelperInternal.charsetUTF8name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to string using UTF-8 + */ + public static String toStringUTF8(byte[] x, int offset, int len) { + try { + return new String(x, offset, len, PngHelperInternal.charsetUTF8name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * critical chunk : first letter is uppercase + */ + public static boolean isCritical(String id) { + return (Character.isUpperCase(id.charAt(0))); + } + + /** + * public chunk: second letter is uppercase + */ + public static boolean isPublic(String id) { // + return (Character.isUpperCase(id.charAt(1))); + } + + /** + * Safe to copy chunk: fourth letter is lower case + */ + public static boolean isSafeToCopy(String id) { + return (!Character.isUpperCase(id.charAt(3))); + } + + /** + * "Unknown" just means that our chunk factory (even when it has been augmented by client code) did not recognize its + * id + */ + public static boolean isUnknown(PngChunk c) { + return c instanceof PngChunkUNKNOWN; + } + + /** + * Finds position of null byte in array + * + * @param b + * @return -1 if not found + */ + public static int posNullByte(byte[] b) { + for (int i = 0; i < b.length; i++) + if (b[i] == 0) + return i; + return -1; + } + + /** + * Decides if a chunk should be loaded, according to a ChunkLoadBehaviour + * + * @param id + * @param behav + * @return true/false + */ + public static boolean shouldLoad(String id, ChunkLoadBehaviour behav) { + if (isCritical(id)) + return true; + switch (behav) { + case LOAD_CHUNK_ALWAYS: + return true; + case LOAD_CHUNK_IF_SAFE: + return isSafeToCopy(id); + case LOAD_CHUNK_NEVER: + return false; + case LOAD_CHUNK_MOST_IMPORTANT: + return id.equals(PngChunkTRNS.ID); + } + return false; // should not reach here + } + + public final static byte[] compressBytes(byte[] ori, boolean compress) { + return compressBytes(ori, 0, ori.length, compress); + } + + public static byte[] compressBytes(byte[] ori, int offset, int len, boolean compress) { + try { + ByteArrayInputStream inb = new ByteArrayInputStream(ori, offset, len); + InputStream in = compress ? inb : new InflaterInputStream(inb); + ByteArrayOutputStream outb = new ByteArrayOutputStream(); + OutputStream out = compress ? new DeflaterOutputStream(outb) : outb; + shovelInToOut(in, out); + in.close(); + out.close(); + return outb.toByteArray(); + } catch (Exception e) { + throw new PngjException(e); + } + } + + /** + * Shovels all data from an input stream to an output stream. + */ + private static void shovelInToOut(InputStream in, OutputStream out) throws IOException { + synchronized (tmpbuffer) { + int len; + while ((len = in.read(tmpbuffer)) > 0) { + out.write(tmpbuffer, 0, len); + } + } + } + + /** + * Returns only the chunks that "match" the predicate + * + * See also trimList() + */ + public static List filterList(List target, ChunkPredicate predicateKeep) { + List result = new ArrayList(); + for (PngChunk element : target) { + if (predicateKeep.match(element)) { + result.add(element); + } + } + return result; + } + + /** + * Remove (in place) the chunks that "match" the predicate + * + * See also filterList + */ + public static int trimList(List target, ChunkPredicate predicateRemove) { + Iterator it = target.iterator(); + int cont = 0; + while (it.hasNext()) { + PngChunk c = it.next(); + if (predicateRemove.match(c)) { + it.remove(); + cont++; + } + } + return cont; + } + + /** + * Adhoc criteria: two ancillary chunks are "equivalent" ("practically same type") if they have same id and (perhaps, + * if multiple are allowed) if the match also in some "internal key" (eg: key for string values, palette for sPLT, + * etc) + * + * When we use this method, we implicitly assume that we don't allow/expect two "equivalent" chunks in a single PNG + * + * Notice that the use of this is optional, and that the PNG standard actually allows text chunks that have same key + * + * @return true if "equivalent" + */ + public static final boolean equivalent(PngChunk c1, PngChunk c2) { + if (c1 == c2) + return true; + if (c1 == null || c2 == null || !c1.id.equals(c2.id)) + return false; + if (c1.crit) + return false; + // same id + if (c1.getClass() != c2.getClass()) + return false; // should not happen + if (!c2.allowsMultiple()) + return true; + if (c1 instanceof PngChunkTextVar) { + return ((PngChunkTextVar) c1).getKey().equals(((PngChunkTextVar) c2).getKey()); + } + if (c1 instanceof PngChunkSPLT) { + return ((PngChunkSPLT) c1).getPalName().equals(((PngChunkSPLT) c2).getPalName()); + } + // unknown chunks that allow multiple? consider they don't match + return false; + } + + public static boolean isText(PngChunk c) { + return c instanceof PngChunkTextVar; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkLoadBehaviour.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkLoadBehaviour.java new file mode 100644 index 00000000..84b92f3f --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkLoadBehaviour.java @@ -0,0 +1,26 @@ +package ar.com.hjg.pngj.chunks; + +/** + * What to do with ancillary (non-critical) chunks when reading. + *

+ * + */ +public enum ChunkLoadBehaviour { + /** + * All non-critical chunks are skipped + */ + LOAD_CHUNK_NEVER, + /** + * Load chunk if "safe to copy" + */ + LOAD_CHUNK_IF_SAFE, + /** + * Load only most important chunk: TRNS + */ + LOAD_CHUNK_MOST_IMPORTANT, + /** + * Load all chunks.
+ * Notice that other restrictions might apply, see PngReader.skipChunkMaxSize PngReader.skipChunkIds + */ + LOAD_CHUNK_ALWAYS; +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkPredicate.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkPredicate.java new file mode 100644 index 00000000..72b2a6f3 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkPredicate.java @@ -0,0 +1,14 @@ +package ar.com.hjg.pngj.chunks; + +/** + * Decides if another chunk "matches", according to some criterion + */ +public interface ChunkPredicate { + /** + * The other chunk matches with this one + * + * @param chunk + * @return true if match + */ + boolean match(PngChunk chunk); +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkRaw.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkRaw.java new file mode 100644 index 00000000..e02a8fb6 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunkRaw.java @@ -0,0 +1,169 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayInputStream; +import java.io.OutputStream; +import java.util.zip.CRC32; + +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjBadCrcException; +import ar.com.hjg.pngj.PngjException; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * Raw (physical) chunk. + *

+ * Short lived object, to be created while serialing/deserializing Do not reuse it for different chunks.
+ * See http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html + */ +public class ChunkRaw { + /** + * The length counts only the data field, not itself, the chunk type code, or the CRC. Zero is a valid length. + * Although encoders and decoders should treat the length as unsigned, its value must not exceed 231-1 bytes. + */ + public final int len; + + /** + * A 4-byte chunk type code. uppercase and lowercase ASCII letters + */ + public final byte[] idbytes; + public final String id; + + /** + * The data bytes appropriate to the chunk type, if any. This field can be of zero length. Does not include crc. If + * it's null, it means that the data is ot available + */ + public byte[] data = null; + /** + * @see ChunkRaw#getOffset() + */ + private long offset = 0; + + /** + * A 4-byte CRC (Cyclic Redundancy Check) calculated on the preceding bytes in the chunk, including the chunk type + * code and chunk data fields, but not including the length field. + */ + public byte[] crcval = new byte[4]; + + private CRC32 crcengine; // lazily instantiated + + public ChunkRaw(int len, String id, boolean alloc) { + this.len = len; + this.id = id; + this.idbytes = ChunkHelper.toBytes(id); + for (int i = 0; i < 4; i++) { + if (idbytes[i] < 65 || idbytes[i] > 122 || (idbytes[i] > 90 && idbytes[i] < 97)) + throw new PngjException("Bad id chunk: must be ascii letters " + id); + } + if (alloc) + allocData(); + } + + public ChunkRaw(int len, byte[] idbytes, boolean alloc) { + this(len, ChunkHelper.toString(idbytes), alloc); + } + + public void allocData() { // TODO: not public + if (data == null || data.length < len) + data = new byte[len]; + } + + /** + * this is called after setting data, before writing to os + */ + private void computeCrcForWriting() { + crcengine = new CRC32(); + crcengine.update(idbytes, 0, 4); + if (len > 0) + crcengine.update(data, 0, len); // + PngHelperInternal.writeInt4tobytes((int) crcengine.getValue(), crcval, 0); + } + + /** + * Computes the CRC and writes to the stream. If error, a PngjOutputException is thrown + * + * Note that this is only used for non idat chunks + */ + public void writeChunk(OutputStream os) { + writeChunkHeader(os); + if (len > 0) { + if (data == null) + throw new PngjOutputException("cannot write chunk, raw chunk data is null [" + id + "]"); + PngHelperInternal.writeBytes(os, data, 0, len); + } + computeCrcForWriting(); + writeChunkCrc(os); + } + + public void writeChunkHeader(OutputStream os) { + if (idbytes.length != 4) + throw new PngjOutputException("bad chunkid [" + id + "]"); + PngHelperInternal.writeInt4(os, len); + PngHelperInternal.writeBytes(os, idbytes); + } + + public void writeChunkCrc(OutputStream os) { + PngHelperInternal.writeBytes(os, crcval, 0, 4); + } + + public void checkCrc() { + int crcComputed = (int) crcengine.getValue(); + int crcExpected = PngHelperInternal.readInt4fromBytes(crcval, 0); + if (crcComputed != crcExpected) + throw new PngjBadCrcException("chunk: " + this.toString() + " expected=" + crcExpected + + " read=" + crcComputed); + } + + public void updateCrc(byte[] buf, int off, int len) { + if (crcengine == null) + crcengine = new CRC32(); + crcengine.update(buf, off, len); + } + + ByteArrayInputStream getAsByteStream() { // only the data + return new ByteArrayInputStream(data); + } + + /** + * offset in the full PNG stream, in bytes. only informational, for read chunks (0=NA) + */ + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public String toString() { + return "chunkid=" + ChunkHelper.toString(idbytes) + " len=" + len; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + (int) (offset ^ (offset >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ChunkRaw other = (ChunkRaw) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (offset != other.offset) + return false; + return true; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunksList.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunksList.java new file mode 100644 index 00000000..f028c67c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunksList.java @@ -0,0 +1,167 @@ +package ar.com.hjg.pngj.chunks; + +import java.util.ArrayList; +import java.util.List; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * All chunks that form an image, read or to be written. + *

+ * chunks include all chunks, but IDAT is a single pseudo chunk without data + **/ +public class ChunksList { + // ref: http://www.w3.org/TR/PNG/#table53 + public static final int CHUNK_GROUP_0_IDHR = 0; // required - single + public static final int CHUNK_GROUP_1_AFTERIDHR = 1; // optional - multiple + public static final int CHUNK_GROUP_2_PLTE = 2; // optional - single + public static final int CHUNK_GROUP_3_AFTERPLTE = 3; // optional - multple + public static final int CHUNK_GROUP_4_IDAT = 4; // required (single pseudo chunk) + public static final int CHUNK_GROUP_5_AFTERIDAT = 5; // optional - multple + public static final int CHUNK_GROUP_6_END = 6; // only 1 chunk - requried + + /** + * All chunks, read (or written) + * + * But IDAT is a single pseudo chunk without data + */ + List chunks = new ArrayList(); + // protected HashMap> chunksById = new HashMap>(); + // // does not include IDAT + + final ImageInfo imageInfo; // only required for writing + + boolean withPlte = false; + + public ChunksList(ImageInfo imfinfo) { + this.imageInfo = imfinfo; + } + + /** + * WARNING: this does NOT return a copy, but the list itself. The called should not modify this directly! Don't use + * this to manipulate the chunks. + */ + public List getChunks() { + return chunks; + } + + protected static List getXById(final List list, final String id, + final String innerid) { + if (innerid == null) + return ChunkHelper.filterList(list, new ChunkPredicate() { + public boolean match(PngChunk c) { + return c.id.equals(id); + } + }); + else + return ChunkHelper.filterList(list, new ChunkPredicate() { + public boolean match(PngChunk c) { + if (!c.id.equals(id)) + return false; + if (c instanceof PngChunkTextVar && !((PngChunkTextVar) c).getKey().equals(innerid)) + return false; + if (c instanceof PngChunkSPLT && !((PngChunkSPLT) c).getPalName().equals(innerid)) + return false; + return true; + } + }); + } + + /** + * Adds chunk in next position. This is used onyl by the pngReader + */ + public void appendReadChunk(PngChunk chunk, int chunkGroup) { + chunk.setChunkGroup(chunkGroup); + chunks.add(chunk); + if (chunk.id.equals(PngChunkPLTE.ID)) + withPlte = true; + } + + /** + * All chunks with this ID + * + * @param id + * @return List, empty if none + */ + public List getById(final String id) { + return getById(id, null); + } + + /** + * If innerid!=null and the chunk is PngChunkTextVar or PngChunkSPLT, it's filtered by that id + * + * @param id + * @return innerid Only used for text and SPLT chunks + * @return List, empty if none + */ + public List getById(final String id, final String innerid) { + return getXById(chunks, id, innerid); + } + + /** + * Returns only one chunk + * + * @param id + * @return First chunk found, null if not found + */ + public PngChunk getById1(final String id) { + return getById1(id, false); + } + + /** + * Returns only one chunk or null if nothing found - does not include queued + *

+ * If more than one chunk is found, then an exception is thrown (failifMultiple=true or chunk is single) or the last + * one is returned (failifMultiple=false) + **/ + public PngChunk getById1(final String id, final boolean failIfMultiple) { + return getById1(id, null, failIfMultiple); + } + + /** + * Returns only one chunk or null if nothing found - does not include queued + *

+ * If more than one chunk (after filtering by inner id) is found, then an exception is thrown (failifMultiple=true or + * chunk is single) or the last one is returned (failifMultiple=false) + **/ + public PngChunk getById1(final String id, final String innerid, final boolean failIfMultiple) { + List list = getById(id, innerid); + if (list.isEmpty()) + return null; + if (list.size() > 1 && (failIfMultiple || !list.get(0).allowsMultiple())) + throw new PngjException("unexpected multiple chunks id=" + id); + return list.get(list.size() - 1); + } + + /** + * Finds all chunks "equivalent" to this one + * + * @param c2 + * @return Empty if nothing found + */ + public List getEquivalent(final PngChunk c2) { + return ChunkHelper.filterList(chunks, new ChunkPredicate() { + public boolean match(PngChunk c) { + return ChunkHelper.equivalent(c, c2); + } + }); + } + + public String toString() { + return "ChunkList: read: " + chunks.size(); + } + + /** + * for debugging + */ + public String toStringFull() { + StringBuilder sb = new StringBuilder(toString()); + sb.append("\n Read:\n"); + for (PngChunk chunk : chunks) { + sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n"); + } + return sb.toString(); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunksListForWrite.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunksListForWrite.java new file mode 100644 index 00000000..c2e58d7f --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/ChunksListForWrite.java @@ -0,0 +1,189 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; +import ar.com.hjg.pngj.PngjOutputException; + +public class ChunksListForWrite extends ChunksList { + + /** + * chunks not yet writen - does not include IHDR, IDAT, END, perhaps yes PLTE + */ + private final List queuedChunks = new ArrayList(); + + // redundant, just for eficciency + private HashMap alreadyWrittenKeys = new HashMap(); + + public ChunksListForWrite(ImageInfo imfinfo) { + super(imfinfo); + } + + /** + * Same as getById(), but looking in the queued chunks + */ + public List getQueuedById(final String id) { + return getQueuedById(id, null); + } + + /** + * Same as getById(), but looking in the queued chunks + */ + public List getQueuedById(final String id, final String innerid) { + return getXById(queuedChunks, id, innerid); + } + + /** + * Same as getById1(), but looking in the queued chunks + **/ + public PngChunk getQueuedById1(final String id, final String innerid, final boolean failIfMultiple) { + List list = getQueuedById(id, innerid); + if (list.isEmpty()) + return null; + if (list.size() > 1 && (failIfMultiple || !list.get(0).allowsMultiple())) + throw new PngjException("unexpected multiple chunks id=" + id); + return list.get(list.size() - 1); + } + + /** + * Same as getById1(), but looking in the queued chunks + **/ + public PngChunk getQueuedById1(final String id, final boolean failIfMultiple) { + return getQueuedById1(id, null, failIfMultiple); + } + + /** + * Same as getById1(), but looking in the queued chunks + **/ + public PngChunk getQueuedById1(final String id) { + return getQueuedById1(id, false); + } + + /** + * Finds all chunks "equivalent" to this one + * + * @param c2 + * @return Empty if nothing found + */ + public List getQueuedEquivalent(final PngChunk c2) { + return ChunkHelper.filterList(queuedChunks, new ChunkPredicate() { + public boolean match(PngChunk c) { + return ChunkHelper.equivalent(c, c2); + } + }); + } + + /** + * Remove Chunk: only from queued + * + * WARNING: this depends on c.equals() implementation, which is straightforward for SingleChunks. For MultipleChunks, + * it will normally check for reference equality! + */ + public boolean removeChunk(PngChunk c) { + if (c == null) + return false; + return queuedChunks.remove(c); + } + + /** + * Adds chunk to queue + * + * If there + * + * @param c + */ + public boolean queue(PngChunk c) { + queuedChunks.add(c); + return true; + } + + /** + * this should be called only for ancillary chunks and PLTE (groups 1 - 3 - 5) + **/ + private static boolean shouldWrite(PngChunk c, int currentGroup) { + if (currentGroup == CHUNK_GROUP_2_PLTE) + return c.id.equals(ChunkHelper.PLTE); + if (currentGroup % 2 == 0) + throw new PngjOutputException("bad chunk group?"); + int minChunkGroup, maxChunkGroup; + if (c.getOrderingConstraint().mustGoBeforePLTE()) + minChunkGroup = maxChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + else if (c.getOrderingConstraint().mustGoBeforeIDAT()) { + maxChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE; + minChunkGroup = + c.getOrderingConstraint().mustGoAfterPLTE() ? ChunksList.CHUNK_GROUP_3_AFTERPLTE + : ChunksList.CHUNK_GROUP_1_AFTERIDHR; + } else { + maxChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; + minChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + } + + int preferred = maxChunkGroup; + if (c.hasPriority()) + preferred = minChunkGroup; + if (ChunkHelper.isUnknown(c) && c.getChunkGroup() > 0) + preferred = c.getChunkGroup(); + if (currentGroup == preferred) + return true; + if (currentGroup > preferred && currentGroup <= maxChunkGroup) + return true; + return false; + } + + public int writeChunks(OutputStream os, int currentGroup) { + int cont = 0; + Iterator it = queuedChunks.iterator(); + while (it.hasNext()) { + PngChunk c = it.next(); + if (!shouldWrite(c, currentGroup)) + continue; + if (ChunkHelper.isCritical(c.id) && !c.id.equals(ChunkHelper.PLTE)) + throw new PngjOutputException("bad chunk queued: " + c); + if (alreadyWrittenKeys.containsKey(c.id) && !c.allowsMultiple()) + throw new PngjOutputException("duplicated chunk does not allow multiple: " + c); + c.write(os); + chunks.add(c); + alreadyWrittenKeys.put(c.id, + alreadyWrittenKeys.containsKey(c.id) ? alreadyWrittenKeys.get(c.id) + 1 : 1); + c.setChunkGroup(currentGroup); + it.remove(); + cont++; + } + return cont; + } + + /** + * warning: this is NOT a copy, do not modify + */ + public List getQueuedChunks() { + return queuedChunks; + } + + public String toString() { + return "ChunkList: written: " + getChunks().size() + " queue: " + queuedChunks.size(); + } + + /** + * for debugging + */ + public String toStringFull() { + StringBuilder sb = new StringBuilder(toString()); + sb.append("\n Written:\n"); + for (PngChunk chunk : getChunks()) { + sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n"); + } + if (!queuedChunks.isEmpty()) { + sb.append(" Queued:\n"); + for (PngChunk chunk : queuedChunks) { + sb.append(chunk).append("\n"); + } + + } + return sb.toString(); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngBadCharsetException.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngBadCharsetException.java new file mode 100644 index 00000000..cc41c064 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngBadCharsetException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.PngjException; + +public class PngBadCharsetException extends PngjException { + private static final long serialVersionUID = 1L; + + public PngBadCharsetException(String message, Throwable cause) { + super(message, cause); + } + + public PngBadCharsetException(String message) { + super(message); + } + + public PngBadCharsetException(Throwable cause) { + super(cause); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunk.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunk.java new file mode 100644 index 00000000..16b82204 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunk.java @@ -0,0 +1,216 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.OutputStream; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjExceptionInternal; + +/** + * Represents a instance of a PNG chunk. + *

+ * See http://www + * .libpng.org/pub/png/spec/1.2/PNG-Chunks .html + *

+ * Concrete classes should extend {@link PngChunkSingle} or {@link PngChunkMultiple} + *

+ * Note that some methods/fields are type-specific (getOrderingConstraint(), allowsMultiple()),
+ * some are 'almost' type-specific (id,crit,pub,safe; the exception is PngUKNOWN),
+ * and the rest are instance-specific + */ +public abstract class PngChunk { + + /** + * Chunk-id: 4 letters + */ + public final String id; + /** + * Autocomputed at creation time + */ + public final boolean crit, pub, safe; + + protected final ImageInfo imgInfo; + + protected ChunkRaw raw; + + private boolean priority = false; // For writing. Queued chunks with high priority will be written + // as soon as + // possible + + protected int chunkGroup = -1; // chunk group where it was read or writen + + /** + * Possible ordering constraint for a PngChunk type -only relevant for ancillary chunks. Theoretically, there could be + * more general constraints, but these cover the constraints for standard chunks. + */ + public enum ChunkOrderingConstraint { + /** + * no ordering constraint + */ + NONE, + /** + * Must go before PLTE (and hence, also before IDAT) + */ + BEFORE_PLTE_AND_IDAT, + /** + * Must go after PLTE (if exists) but before IDAT + */ + AFTER_PLTE_BEFORE_IDAT, + /** + * Must go after PLTE (and it must exist) but before IDAT + */ + AFTER_PLTE_BEFORE_IDAT_PLTE_REQUIRED, + /** + * Must before IDAT (before or after PLTE) + */ + BEFORE_IDAT, + /** + * After IDAT (this restriction does not apply to the standard PNG chunks) + */ + AFTER_IDAT, + /** + * Does not apply + */ + NA; + + public boolean mustGoBeforePLTE() { + return this == BEFORE_PLTE_AND_IDAT; + } + + public boolean mustGoBeforeIDAT() { + return this == BEFORE_IDAT || this == BEFORE_PLTE_AND_IDAT || this == AFTER_PLTE_BEFORE_IDAT; + } + + /** + * after pallete, if exists + */ + public boolean mustGoAfterPLTE() { + return this == AFTER_PLTE_BEFORE_IDAT || this == AFTER_PLTE_BEFORE_IDAT_PLTE_REQUIRED; + } + + public boolean mustGoAfterIDAT() { + return this == AFTER_IDAT; + } + + public boolean isOk(int currentChunkGroup, boolean hasplte) { + if (this == NONE) + return true; + else if (this == BEFORE_IDAT) + return currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT; + else if (this == BEFORE_PLTE_AND_IDAT) + return currentChunkGroup < ChunksList.CHUNK_GROUP_2_PLTE; + else if (this == AFTER_PLTE_BEFORE_IDAT) + return hasplte ? currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT + : (currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT && currentChunkGroup > ChunksList.CHUNK_GROUP_2_PLTE); + else if (this == AFTER_IDAT) + return currentChunkGroup > ChunksList.CHUNK_GROUP_4_IDAT; + return false; + } + } + + public PngChunk(String id, ImageInfo imgInfo) { + this.id = id; + this.imgInfo = imgInfo; + this.crit = ChunkHelper.isCritical(id); + this.pub = ChunkHelper.isPublic(id); + this.safe = ChunkHelper.isSafeToCopy(id); + } + + protected final ChunkRaw createEmptyChunk(int len, boolean alloc) { + ChunkRaw c = new ChunkRaw(len, ChunkHelper.toBytes(id), alloc); + return c; + } + + /** + * In which "chunkGroup" (see {@link ChunksList}for definition) this chunks instance was read or written. + *

+ * -1 if not read or written (eg, queued) + */ + final public int getChunkGroup() { + return chunkGroup; + } + + /** + * @see #getChunkGroup() + */ + final void setChunkGroup(int chunkGroup) { + this.chunkGroup = chunkGroup; + } + + public boolean hasPriority() { + return priority; + } + + public void setPriority(boolean priority) { + this.priority = priority; + } + + final void write(OutputStream os) { + if (raw == null || raw.data == null) + raw = createRawChunk(); + if (raw == null) + throw new PngjExceptionInternal("null chunk ! creation failed for " + this); + raw.writeChunk(os); + } + + /** + * Creates the physical chunk. This is used when writing (serialization). Each particular chunk class implements its + * own logic. + * + * @return A newly allocated and filled raw chunk + */ + public abstract ChunkRaw createRawChunk(); + + /** + * Parses raw chunk and fill inside data. This is used when reading (deserialization). Each particular chunk class + * implements its own logic. + */ + protected abstract void parseFromRaw(ChunkRaw c); + + /** + * See {@link PngChunkMultiple} and {@link PngChunkSingle} + * + * @return true if PNG accepts multiple chunks of this class + */ + protected abstract boolean allowsMultiple(); + + public ChunkRaw getRaw() { + return raw; + } + + void setRaw(ChunkRaw raw) { + this.raw = raw; + } + + /** + * @see ChunkRaw#len + */ + public int getLen() { + return raw != null ? raw.len : -1; + } + + /** + * @see ChunkRaw#getOffset() + */ + public long getOffset() { + return raw != null ? raw.getOffset() : -1; + } + + /** + * This signals that the raw chunk (serialized data) as invalid, so that it's regenerated on write. This should be + * called for the (infrequent) case of chunks that were copied from a PngReader and we want to manually modify it. + */ + public void invalidateRawData() { + raw = null; + } + + /** + * see {@link ChunkOrderingConstraint} + */ + public abstract ChunkOrderingConstraint getOrderingConstraint(); + + @Override + public String toString() { + return "chunk id= " + id + " (len=" + getLen() + " offset=" + getOffset() + ")"; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkACTL.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkACTL.java new file mode 100644 index 00000000..de08207b --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkACTL.java @@ -0,0 +1,59 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; + +/** + * acTL chunk. For APGN, not PGN standard + *

+ * see https://wiki.mozilla.org/APNG_Specification#.60acTL.60:_The_Animation_Control_Chunk + *

+ */ +public class PngChunkACTL extends PngChunkSingle { + public final static String ID = "acTL"; + private int numFrames; + private int numPlays; + + + public PngChunkACTL(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(8, true); + PngHelperInternal.writeInt4tobytes((int) numFrames, c.data, 0); + PngHelperInternal.writeInt4tobytes((int) numPlays, c.data, 4); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + numFrames = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + numPlays = PngHelperInternal.readInt4fromBytes(chunk.data, 4); + } + + public int getNumFrames() { + return numFrames; + } + + public void setNumFrames(int numFrames) { + this.numFrames = numFrames; + } + + public int getNumPlays() { + return numPlays; + } + + public void setNumPlays(int numPlays) { + this.numPlays = numPlays; + } + + + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkBKGD.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkBKGD.java new file mode 100644 index 00000000..5ef2b305 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkBKGD.java @@ -0,0 +1,112 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * bKGD Chunk. + *

+ * see {@link http://www.w3.org/TR/PNG/#11bKGD} + *

+ * This chunk structure depends on the image type + */ +public class PngChunkBKGD extends PngChunkSingle { + public final static String ID = ChunkHelper.bKGD; + // only one of these is meaningful + private int gray; + private int red, green, blue; + private int paletteIndex; + + public PngChunkBKGD(ImageInfo info) { + super(ChunkHelper.bKGD, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + if (imgInfo.greyscale) { + c = createEmptyChunk(2, true); + PngHelperInternal.writeInt2tobytes(gray, c.data, 0); + } else if (imgInfo.indexed) { + c = createEmptyChunk(1, true); + c.data[0] = (byte) paletteIndex; + } else { + c = createEmptyChunk(6, true); + PngHelperInternal.writeInt2tobytes(red, c.data, 0); + PngHelperInternal.writeInt2tobytes(green, c.data, 0); + PngHelperInternal.writeInt2tobytes(blue, c.data, 0); + } + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (imgInfo.greyscale) { + gray = PngHelperInternal.readInt2fromBytes(c.data, 0); + } else if (imgInfo.indexed) { + paletteIndex = (int) (c.data[0] & 0xff); + } else { + red = PngHelperInternal.readInt2fromBytes(c.data, 0); + green = PngHelperInternal.readInt2fromBytes(c.data, 2); + blue = PngHelperInternal.readInt2fromBytes(c.data, 4); + } + } + + /** + * Set gray value (0-255 if bitdept=8) + * + * @param gray + */ + public void setGray(int gray) { + if (!imgInfo.greyscale) + throw new PngjException("only gray images support this"); + this.gray = gray; + } + + public int getGray() { + if (!imgInfo.greyscale) + throw new PngjException("only gray images support this"); + return gray; + } + + /** + * Set pallette index + * + */ + public void setPaletteIndex(int i) { + if (!imgInfo.indexed) + throw new PngjException("only indexed (pallete) images support this"); + this.paletteIndex = i; + } + + public int getPaletteIndex() { + if (!imgInfo.indexed) + throw new PngjException("only indexed (pallete) images support this"); + return paletteIndex; + } + + /** + * Set rgb values + * + */ + public void setRGB(int r, int g, int b) { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + red = r; + green = g; + blue = b; + } + + public int[] getRGB() { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + return new int[] {red, green, blue}; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkCHRM.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkCHRM.java new file mode 100644 index 00000000..f1b28fdc --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkCHRM.java @@ -0,0 +1,75 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * cHRM chunk. + *

+ * see http://www.w3.org/TR/PNG/#11cHRM + */ +public class PngChunkCHRM extends PngChunkSingle { + public final static String ID = ChunkHelper.cHRM; + + // http://www.w3.org/TR/PNG/#11cHRM + private double whitex, whitey; + private double redx, redy; + private double greenx, greeny; + private double bluex, bluey; + + public PngChunkCHRM(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + c = createEmptyChunk(32, true); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(whitex), c.data, 0); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(whitey), c.data, 4); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(redx), c.data, 8); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(redy), c.data, 12); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(greenx), c.data, 16); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(greeny), c.data, 20); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(bluex), c.data, 24); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(bluey), c.data, 28); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (c.len != 32) + throw new PngjException("bad chunk " + c); + whitex = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 0)); + whitey = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 4)); + redx = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 8)); + redy = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 12)); + greenx = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 16)); + greeny = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 20)); + bluex = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 24)); + bluey = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 28)); + } + + public void setChromaticities(double whitex, double whitey, double redx, double redy, + double greenx, double greeny, double bluex, double bluey) { + this.whitex = whitex; + this.redx = redx; + this.greenx = greenx; + this.bluex = bluex; + this.whitey = whitey; + this.redy = redy; + this.greeny = greeny; + this.bluey = bluey; + } + + public double[] getChromaticities() { + return new double[] {whitex, whitey, redx, redy, greenx, greeny, bluex, bluey}; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkFCTL.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkFCTL.java new file mode 100644 index 00000000..f55617cb --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkFCTL.java @@ -0,0 +1,158 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; + +/** + * fcTL chunk. For APGN, not PGN standard + *

+ * see https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk + *

+ */ +public class PngChunkFCTL extends PngChunkMultiple { + public final static String ID = "fcTL"; + + public final static byte APNG_DISPOSE_OP_NONE = 0; + public final static byte APNG_DISPOSE_OP_BACKGROUND = 1; + public final static byte APNG_DISPOSE_OP_PREVIOUS = 2; + public final static byte APNG_BLEND_OP_SOURCE = 0; + public final static byte APNG_BLEND_OP_OVER = 1; + + private int seqNum; + private int width, height, xOff, yOff; + private int delayNum, delayDen; + private byte disposeOp, blendOp; + + public PngChunkFCTL(ImageInfo info) { + super(ID, info); + } + + public ImageInfo getEquivImageInfo() { + return new ImageInfo(width, height, imgInfo.bitDepth, imgInfo.alpha, imgInfo.greyscale, + imgInfo.indexed); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(8, true); + int off = 0; + PngHelperInternal.writeInt4tobytes(seqNum, c.data, off); + off += 4; + PngHelperInternal.writeInt4tobytes(width, c.data, off); + off += 4; + PngHelperInternal.writeInt4tobytes(height, c.data, off); + off += 4; + PngHelperInternal.writeInt4tobytes(xOff, c.data, off); + off += 4; + PngHelperInternal.writeInt4tobytes(yOff, c.data, off); + off += 4; + PngHelperInternal.writeInt2tobytes(delayNum, c.data, off); + off += 2; + PngHelperInternal.writeInt2tobytes(delayDen, c.data, off); + off += 2; + c.data[off] = disposeOp; + off += 1; + c.data[off] = blendOp; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + int off = 0; + seqNum = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + width = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + height = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + xOff = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + yOff = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + delayNum = PngHelperInternal.readInt2fromBytes(chunk.data, off); + off += 2; + delayDen = PngHelperInternal.readInt2fromBytes(chunk.data, off); + off += 2; + disposeOp = chunk.data[off]; + off += 1; + blendOp = chunk.data[off]; + } + + public int getSeqNum() { + return seqNum; + } + + public void setSeqNum(int seqNum) { + this.seqNum = seqNum; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getxOff() { + return xOff; + } + + public void setxOff(int xOff) { + this.xOff = xOff; + } + + public int getyOff() { + return yOff; + } + + public void setyOff(int yOff) { + this.yOff = yOff; + } + + public int getDelayNum() { + return delayNum; + } + + public void setDelayNum(int delayNum) { + this.delayNum = delayNum; + } + + public int getDelayDen() { + return delayDen; + } + + public void setDelayDen(int delayDen) { + this.delayDen = delayDen; + } + + public byte getDisposeOp() { + return disposeOp; + } + + public void setDisposeOp(byte disposeOp) { + this.disposeOp = disposeOp; + } + + public byte getBlendOp() { + return blendOp; + } + + public void setBlendOp(byte blendOp) { + this.blendOp = blendOp; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkFDAT.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkFDAT.java new file mode 100644 index 00000000..16ecc7ad --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkFDAT.java @@ -0,0 +1,70 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * fdAT chunk. For APGN, not PGN standard + *

+ * see https://wiki.mozilla.org/APNG_Specification#.60fdAT.60:_The_Frame_Data_Chunk + *

+ * This implementation does not support buffering, this should be not managed similar to a IDAT chunk + * + */ +public class PngChunkFDAT extends PngChunkMultiple { + public final static String ID = "fdAT"; + private int seqNum; + private byte[] buffer; // normally not allocated - if so, it's the raw data, so it includes the 4bytes seqNum + int datalen; // length of idat data, excluding seqNUm (= chunk.len-4) + + public PngChunkFDAT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + if (buffer == null) + throw new PngjException("not buffered"); + ChunkRaw c = createEmptyChunk(datalen + 4, false); + c.data = buffer; // shallow copy! + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + seqNum = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + datalen = chunk.len - 4; + buffer = chunk.data; + } + + public int getSeqNum() { + return seqNum; + } + + public void setSeqNum(int seqNum) { + this.seqNum = seqNum; + } + + public byte[] getBuffer() { + return buffer; + } + + public void setBuffer(byte[] buffer) { + this.buffer = buffer; + } + + public int getDatalen() { + return datalen; + } + + public void setDatalen(int datalen) { + this.datalen = datalen; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkGAMA.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkGAMA.java new file mode 100644 index 00000000..d1920c88 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkGAMA.java @@ -0,0 +1,51 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * gAMA chunk. + *

+ * see http://www.w3.org/TR/PNG/#11gAMA + */ +public class PngChunkGAMA extends PngChunkSingle { + public final static String ID = ChunkHelper.gAMA; + + // http://www.w3.org/TR/PNG/#11gAMA + private double gamma; + + public PngChunkGAMA(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(4, true); + int g = (int) (gamma * 100000 + 0.5); + PngHelperInternal.writeInt4tobytes(g, c.data, 0); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 4) + throw new PngjException("bad chunk " + chunk); + int g = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + gamma = ((double) g) / 100000.0; + } + + public double getGamma() { + return gamma; + } + + public void setGamma(double gamma) { + this.gamma = gamma; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkHIST.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkHIST.java new file mode 100644 index 00000000..ebdf2355 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkHIST.java @@ -0,0 +1,58 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * hIST chunk. + *

+ * see http://www.w3.org/TR/PNG/#11hIST
+ * only for palette images + */ +public class PngChunkHIST extends PngChunkSingle { + public final static String ID = ChunkHelper.hIST; + + private int[] hist = new int[0]; // should have same lenght as palette + + public PngChunkHIST(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (!imgInfo.indexed) + throw new PngjException("only indexed images accept a HIST chunk"); + int nentries = c.data.length / 2; + hist = new int[nentries]; + for (int i = 0; i < hist.length; i++) { + hist[i] = PngHelperInternal.readInt2fromBytes(c.data, i * 2); + } + } + + @Override + public ChunkRaw createRawChunk() { + if (!imgInfo.indexed) + throw new PngjException("only indexed images accept a HIST chunk"); + ChunkRaw c = null; + c = createEmptyChunk(hist.length * 2, true); + for (int i = 0; i < hist.length; i++) { + PngHelperInternal.writeInt2tobytes(hist[i], c.data, i * 2); + } + return c; + } + + public int[] getHist() { + return hist; + } + + public void setHist(int[] hist) { + this.hist = hist; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkICCP.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkICCP.java new file mode 100644 index 00000000..b3b1109f --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkICCP.java @@ -0,0 +1,77 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * iCCP chunk. + *

+ * See {@link http://www.w3.org/TR/PNG/#11iCCP} + */ +public class PngChunkICCP extends PngChunkSingle { + public final static String ID = ChunkHelper.iCCP; + + // http://www.w3.org/TR/PNG/#11iCCP + private String profileName; + private byte[] compressedProfile; // copmression/decopmresion is done in getter/setter + + public PngChunkICCP(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(profileName.length() + compressedProfile.length + 2, true); + System.arraycopy(ChunkHelper.toBytes(profileName), 0, c.data, 0, profileName.length()); + c.data[profileName.length()] = 0; + c.data[profileName.length() + 1] = 0; + System.arraycopy(compressedProfile, 0, c.data, profileName.length() + 2, + compressedProfile.length); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + int pos0 = ChunkHelper.posNullByte(chunk.data); + profileName = ChunkHelper.toString(chunk.data, 0, pos0); + int comp = (chunk.data[pos0 + 1] & 0xff); + if (comp != 0) + throw new PngjException("bad compression for ChunkTypeICCP"); + int compdatasize = chunk.data.length - (pos0 + 2); + compressedProfile = new byte[compdatasize]; + System.arraycopy(chunk.data, pos0 + 2, compressedProfile, 0, compdatasize); + } + + /** + * The profile should be uncompressed bytes + */ + public void setProfileNameAndContent(String name, byte[] profile) { + profileName = name; + compressedProfile = ChunkHelper.compressBytes(profile, true); + } + + public void setProfileNameAndContent(String name, String profile) { + setProfileNameAndContent(name, ChunkHelper.toBytes(profile)); + } + + public String getProfileName() { + return profileName; + } + + /** + * uncompressed + **/ + public byte[] getProfile() { + return ChunkHelper.compressBytes(compressedProfile, false); + } + + public String getProfileAsString() { + return ChunkHelper.toString(getProfile()); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIDAT.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIDAT.java new file mode 100644 index 00000000..625aefaa --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIDAT.java @@ -0,0 +1,34 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * IDAT chunk. + *

+ * see http://www.w3.org/TR/PNG/#11IDAT + *

+ * This is dummy placeholder - we write/read this chunk (actually several) by special code. + */ +public class PngChunkIDAT extends PngChunkMultiple { + public final static String ID = ChunkHelper.IDAT; + + // http://www.w3.org/TR/PNG/#11IDAT + public PngChunkIDAT(ImageInfo i) { + super(ID, i); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; + } + + @Override + public ChunkRaw createRawChunk() {// does nothing + return null; + } + + @Override + public void parseFromRaw(ChunkRaw c) { // does nothing + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIEND.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIEND.java new file mode 100644 index 00000000..58073d77 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIEND.java @@ -0,0 +1,35 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * IEND chunk. + *

+ * see http://www.w3.org/TR/PNG/#11IEND + */ +public class PngChunkIEND extends PngChunkSingle { + public final static String ID = ChunkHelper.IEND; + + // http://www.w3.org/TR/PNG/#11IEND + // this is a dummy placeholder + public PngChunkIEND(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = new ChunkRaw(0, ChunkHelper.b_IEND, false); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + // this is not used + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIHDR.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIHDR.java new file mode 100644 index 00000000..a2ea517e --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkIHDR.java @@ -0,0 +1,185 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayInputStream; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; +import ar.com.hjg.pngj.PngjInputException; + +/** + * IHDR chunk. + *

+ * see http://www.w3.org/TR/PNG/#11IHDR + *

+ * This is a special critical Chunk. + */ +public class PngChunkIHDR extends PngChunkSingle { + public final static String ID = ChunkHelper.IHDR; + + private int cols; + private int rows; + private int bitspc; + private int colormodel; + private int compmeth; + private int filmeth; + private int interlaced; + + // http://www.w3.org/TR/PNG/#11IHDR + // + public PngChunkIHDR(ImageInfo info) { // argument is normally null here, if not null is used to fill the fields + super(ID, info); + if (info != null) + fillFromInfo(info); + } + + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = new ChunkRaw(13, ChunkHelper.b_IHDR, true); + int offset = 0; + PngHelperInternal.writeInt4tobytes(cols, c.data, offset); + offset += 4; + PngHelperInternal.writeInt4tobytes(rows, c.data, offset); + offset += 4; + c.data[offset++] = (byte) bitspc; + c.data[offset++] = (byte) colormodel; + c.data[offset++] = (byte) compmeth; + c.data[offset++] = (byte) filmeth; + c.data[offset++] = (byte) interlaced; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (c.len != 13) + throw new PngjException("Bad IDHR len " + c.len); + ByteArrayInputStream st = c.getAsByteStream(); + cols = PngHelperInternal.readInt4(st); + rows = PngHelperInternal.readInt4(st); + // bit depth: number of bits per channel + bitspc = PngHelperInternal.readByte(st); + colormodel = PngHelperInternal.readByte(st); + compmeth = PngHelperInternal.readByte(st); + filmeth = PngHelperInternal.readByte(st); + interlaced = PngHelperInternal.readByte(st); + } + + public int getCols() { + return cols; + } + + public void setCols(int cols) { + this.cols = cols; + } + + public int getRows() { + return rows; + } + + public void setRows(int rows) { + this.rows = rows; + } + + public int getBitspc() { + return bitspc; + } + + public void setBitspc(int bitspc) { + this.bitspc = bitspc; + } + + public int getColormodel() { + return colormodel; + } + + public void setColormodel(int colormodel) { + this.colormodel = colormodel; + } + + public int getCompmeth() { + return compmeth; + } + + public void setCompmeth(int compmeth) { + this.compmeth = compmeth; + } + + public int getFilmeth() { + return filmeth; + } + + public void setFilmeth(int filmeth) { + this.filmeth = filmeth; + } + + public int getInterlaced() { + return interlaced; + } + + public void setInterlaced(int interlaced) { + this.interlaced = interlaced; + } + + public boolean isInterlaced() { + return getInterlaced() == 1; + } + + public void fillFromInfo(ImageInfo info) { + setCols(imgInfo.cols); + setRows(imgInfo.rows); + setBitspc(imgInfo.bitDepth); + int colormodel = 0; + if (imgInfo.alpha) + colormodel += 0x04; + if (imgInfo.indexed) + colormodel += 0x01; + if (!imgInfo.greyscale) + colormodel += 0x02; + setColormodel(colormodel); + setCompmeth(0); // compression method 0=deflate + setFilmeth(0); // filter method (0) + setInterlaced(0); // we never interlace + } + + /** throws PngInputException if unexpected values */ + public ImageInfo createImageInfo() { + check(); + boolean alpha = (getColormodel() & 0x04) != 0; + boolean palette = (getColormodel() & 0x01) != 0; + boolean grayscale = (getColormodel() == 0 || getColormodel() == 4); + // creates ImgInfo and imgLine, and allocates buffers + return new ImageInfo(getCols(), getRows(), getBitspc(), alpha, grayscale, palette); + } + + public void check() { + if (cols < 1 || rows < 1 || compmeth != 0 || filmeth != 0) + throw new PngjInputException("bad IHDR: col/row/compmethod/filmethod invalid"); + if (bitspc != 1 && bitspc != 2 && bitspc != 4 && bitspc != 8 && bitspc != 16) + throw new PngjInputException("bad IHDR: bitdepth invalid"); + if (interlaced < 0 || interlaced > 1) + throw new PngjInputException("bad IHDR: interlace invalid"); + switch (colormodel) { + case 0: + break; + case 3: + if (bitspc == 16) + throw new PngjInputException("bad IHDR: bitdepth invalid"); + break; + case 2: + case 4: + case 6: + if (bitspc != 8 && bitspc != 16) + throw new PngjInputException("bad IHDR: bitdepth invalid"); + break; + default: + throw new PngjInputException("bad IHDR: invalid colormodel"); + } + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkITXT.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkITXT.java new file mode 100644 index 00000000..f24974ac --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkITXT.java @@ -0,0 +1,111 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * iTXt chunk. + *

+ * see http://www.w3.org/TR/PNG/#11iTXt + */ +public class PngChunkITXT extends PngChunkTextVar { + public final static String ID = ChunkHelper.iTXt; + + private boolean compressed = false; + private String langTag = ""; + private String translatedTag = ""; + + // http://www.w3.org/TR/PNG/#11iTXt + public PngChunkITXT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkRaw createRawChunk() { + if (key == null || key.trim().length() == 0) + throw new PngjException("Text chunk key must be non empty"); + try { + ByteArrayOutputStream ba = new ByteArrayOutputStream(); + ba.write(ChunkHelper.toBytes(key)); + ba.write(0); // separator + ba.write(compressed ? 1 : 0); + ba.write(0); // compression method (always 0) + ba.write(ChunkHelper.toBytes(langTag)); + ba.write(0); // separator + ba.write(ChunkHelper.toBytesUTF8(translatedTag)); + ba.write(0); // separator + byte[] textbytes = ChunkHelper.toBytesUTF8(val); + if (compressed) { + textbytes = ChunkHelper.compressBytes(textbytes, true); + } + ba.write(textbytes); + byte[] b = ba.toByteArray(); + ChunkRaw chunk = createEmptyChunk(b.length, false); + chunk.data = b; + return chunk; + } catch (IOException e) { + throw new PngjException(e); + } + } + + @Override + public void parseFromRaw(ChunkRaw c) { + int nullsFound = 0; + int[] nullsIdx = new int[3]; + for (int i = 0; i < c.data.length; i++) { + if (c.data[i] != 0) + continue; + nullsIdx[nullsFound] = i; + nullsFound++; + if (nullsFound == 1) + i += 2; + if (nullsFound == 3) + break; + } + if (nullsFound != 3) + throw new PngjException("Bad formed PngChunkITXT chunk"); + key = ChunkHelper.toString(c.data, 0, nullsIdx[0]); + int i = nullsIdx[0] + 1; + compressed = c.data[i] == 0 ? false : true; + i++; + if (compressed && c.data[i] != 0) + throw new PngjException("Bad formed PngChunkITXT chunk - bad compression method "); + langTag = ChunkHelper.toString(c.data, i, nullsIdx[1] - i); + translatedTag = + ChunkHelper.toStringUTF8(c.data, nullsIdx[1] + 1, nullsIdx[2] - nullsIdx[1] - 1); + i = nullsIdx[2] + 1; + if (compressed) { + byte[] bytes = ChunkHelper.compressBytes(c.data, i, c.data.length - i, false); + val = ChunkHelper.toStringUTF8(bytes); + } else { + val = ChunkHelper.toStringUTF8(c.data, i, c.data.length - i); + } + } + + public boolean isCompressed() { + return compressed; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + + public String getLangtag() { + return langTag; + } + + public void setLangtag(String langtag) { + this.langTag = langtag; + } + + public String getTranslatedTag() { + return translatedTag; + } + + public void setTranslatedTag(String translatedTag) { + this.translatedTag = translatedTag; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkMultiple.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkMultiple.java new file mode 100644 index 00000000..8dd37524 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkMultiple.java @@ -0,0 +1,27 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * PNG chunk type (abstract) that allows multiple instances in same image. + */ +public abstract class PngChunkMultiple extends PngChunk { + + protected PngChunkMultiple(String id, ImageInfo imgInfo) { + super(id, imgInfo); + } + + @Override + public final boolean allowsMultiple() { + return true; + } + + /** + * NOTE: this chunk uses the default Object's equals() hashCode() implementation. + * + * This is the right thing to do, normally. + * + * This is important, eg see ChunkList.removeFromList() + */ + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkOFFS.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkOFFS.java new file mode 100644 index 00000000..e47cf600 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkOFFS.java @@ -0,0 +1,81 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * oFFs chunk. + *

+ * see http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.oFFs + */ +public class PngChunkOFFS extends PngChunkSingle { + public final static String ID = "oFFs"; + + // http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.oFFs + private long posX; + private long posY; + private int units; // 0: pixel 1:micrometer + + public PngChunkOFFS(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(9, true); + PngHelperInternal.writeInt4tobytes((int) posX, c.data, 0); + PngHelperInternal.writeInt4tobytes((int) posY, c.data, 4); + c.data[8] = (byte) units; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 9) + throw new PngjException("bad chunk length " + chunk); + posX = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + if (posX < 0) + posX += 0x100000000L; + posY = PngHelperInternal.readInt4fromBytes(chunk.data, 4); + if (posY < 0) + posY += 0x100000000L; + units = PngHelperInternal.readInt1fromByte(chunk.data, 8); + } + + /** + * 0: pixel, 1:micrometer + */ + public int getUnits() { + return units; + } + + /** + * 0: pixel, 1:micrometer + */ + public void setUnits(int units) { + this.units = units; + } + + public long getPosX() { + return posX; + } + + public void setPosX(long posX) { + this.posX = posX; + } + + public long getPosY() { + return posY; + } + + public void setPosY(long posY) { + this.posY = posY; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkPHYS.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkPHYS.java new file mode 100644 index 00000000..98debb1f --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkPHYS.java @@ -0,0 +1,107 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * pHYs chunk. + *

+ * see http://www.w3.org/TR/PNG/#11pHYs + */ +public class PngChunkPHYS extends PngChunkSingle { + public final static String ID = ChunkHelper.pHYs; + + // http://www.w3.org/TR/PNG/#11pHYs + private long pixelsxUnitX; + private long pixelsxUnitY; + private int units; // 0: unknown 1:metre + + public PngChunkPHYS(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(9, true); + PngHelperInternal.writeInt4tobytes((int) pixelsxUnitX, c.data, 0); + PngHelperInternal.writeInt4tobytes((int) pixelsxUnitY, c.data, 4); + c.data[8] = (byte) units; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 9) + throw new PngjException("bad chunk length " + chunk); + pixelsxUnitX = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + if (pixelsxUnitX < 0) + pixelsxUnitX += 0x100000000L; + pixelsxUnitY = PngHelperInternal.readInt4fromBytes(chunk.data, 4); + if (pixelsxUnitY < 0) + pixelsxUnitY += 0x100000000L; + units = PngHelperInternal.readInt1fromByte(chunk.data, 8); + } + + public long getPixelsxUnitX() { + return pixelsxUnitX; + } + + public void setPixelsxUnitX(long pixelsxUnitX) { + this.pixelsxUnitX = pixelsxUnitX; + } + + public long getPixelsxUnitY() { + return pixelsxUnitY; + } + + public void setPixelsxUnitY(long pixelsxUnitY) { + this.pixelsxUnitY = pixelsxUnitY; + } + + public int getUnits() { + return units; + } + + public void setUnits(int units) { + this.units = units; + } + + // special getters / setters + + /** + * returns -1 if the physicial unit is unknown, or X-Y are not equal + */ + public double getAsDpi() { + if (units != 1 || pixelsxUnitX != pixelsxUnitY) + return -1; + return ((double) pixelsxUnitX) * 0.0254; + } + + /** + * returns -1 if the physicial unit is unknown + */ + public double[] getAsDpi2() { + if (units != 1) + return new double[] {-1, -1}; + return new double[] {((double) pixelsxUnitX) * 0.0254, ((double) pixelsxUnitY) * 0.0254}; + } + + public void setAsDpi(double dpi) { + units = 1; + pixelsxUnitX = (long) (dpi / 0.0254 + 0.5); + pixelsxUnitY = pixelsxUnitX; + } + + public void setAsDpi2(double dpix, double dpiy) { + units = 1; + pixelsxUnitX = (long) (dpix / 0.0254 + 0.5); + pixelsxUnitY = (long) (dpiy / 0.0254 + 0.5); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkPLTE.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkPLTE.java new file mode 100644 index 00000000..f647bde0 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkPLTE.java @@ -0,0 +1,98 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * PLTE chunk. + *

+ * see http://www.w3.org/TR/PNG/#11PLTE + *

+ * Critical chunk + */ +public class PngChunkPLTE extends PngChunkSingle { + public final static String ID = ChunkHelper.PLTE; + + // http://www.w3.org/TR/PNG/#11PLTE + private int nentries = 0; + /** + * RGB8 packed in one integer + */ + private int[] entries; + + public PngChunkPLTE(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; + } + + @Override + public ChunkRaw createRawChunk() { + int len = 3 * nentries; + int[] rgb = new int[3]; + ChunkRaw c = createEmptyChunk(len, true); + for (int n = 0, i = 0; n < nentries; n++) { + getEntryRgb(n, rgb); + c.data[i++] = (byte) rgb[0]; + c.data[i++] = (byte) rgb[1]; + c.data[i++] = (byte) rgb[2]; + } + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + setNentries(chunk.len / 3); + for (int n = 0, i = 0; n < nentries; n++) { + setEntry(n, (int) (chunk.data[i++] & 0xff), (int) (chunk.data[i++] & 0xff), + (int) (chunk.data[i++] & 0xff)); + } + } + + public void setNentries(int n) { + nentries = n; + if (nentries < 1 || nentries > 256) + throw new PngjException("invalid pallette - nentries=" + nentries); + if (entries == null || entries.length != nentries) { // alloc + entries = new int[nentries]; + } + } + + public int getNentries() { + return nentries; + } + + public void setEntry(int n, int r, int g, int b) { + entries[n] = ((r << 16) | (g << 8) | b); + } + + public int getEntry(int n) { + return entries[n]; + } + + public void getEntryRgb(int n, int[] rgb) { + getEntryRgb(n, rgb, 0); + } + + public void getEntryRgb(int n, int[] rgb, int offset) { + int v = entries[n]; + rgb[offset + 0] = ((v & 0xff0000) >> 16); + rgb[offset + 1] = ((v & 0xff00) >> 8); + rgb[offset + 2] = (v & 0xff); + } + + public int minBitDepth() { + if (nentries <= 2) + return 1; + else if (nentries <= 4) + return 2; + else if (nentries <= 16) + return 4; + else + return 8; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSBIT.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSBIT.java new file mode 100644 index 00000000..6c6c7a62 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSBIT.java @@ -0,0 +1,114 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * sBIT chunk. + *

+ * see http://www.w3.org/TR/PNG/#11sBIT + *

+ * this chunk structure depends on the image type + */ +public class PngChunkSBIT extends PngChunkSingle { + public final static String ID = ChunkHelper.sBIT; + // http://www.w3.org/TR/PNG/#11sBIT + + // significant bits + private int graysb, alphasb; + private int redsb, greensb, bluesb; + + public PngChunkSBIT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; + } + + private int getCLen() { + int len = imgInfo.greyscale ? 1 : 3; + if (imgInfo.alpha) + len += 1; + return len; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (c.len != getCLen()) + throw new PngjException("bad chunk length " + c); + if (imgInfo.greyscale) { + graysb = PngHelperInternal.readInt1fromByte(c.data, 0); + if (imgInfo.alpha) + alphasb = PngHelperInternal.readInt1fromByte(c.data, 1); + } else { + redsb = PngHelperInternal.readInt1fromByte(c.data, 0); + greensb = PngHelperInternal.readInt1fromByte(c.data, 1); + bluesb = PngHelperInternal.readInt1fromByte(c.data, 2); + if (imgInfo.alpha) + alphasb = PngHelperInternal.readInt1fromByte(c.data, 3); + } + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + c = createEmptyChunk(getCLen(), true); + if (imgInfo.greyscale) { + c.data[0] = (byte) graysb; + if (imgInfo.alpha) + c.data[1] = (byte) alphasb; + } else { + c.data[0] = (byte) redsb; + c.data[1] = (byte) greensb; + c.data[2] = (byte) bluesb; + if (imgInfo.alpha) + c.data[3] = (byte) alphasb; + } + return c; + } + + public void setGraysb(int gray) { + if (!imgInfo.greyscale) + throw new PngjException("only greyscale images support this"); + graysb = gray; + } + + public int getGraysb() { + if (!imgInfo.greyscale) + throw new PngjException("only greyscale images support this"); + return graysb; + } + + public void setAlphasb(int a) { + if (!imgInfo.alpha) + throw new PngjException("only images with alpha support this"); + alphasb = a; + } + + public int getAlphasb() { + if (!imgInfo.alpha) + throw new PngjException("only images with alpha support this"); + return alphasb; + } + + /** + * Set rgb values + * + */ + public void setRGB(int r, int g, int b) { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + redsb = r; + greensb = g; + bluesb = b; + } + + public int[] getRGB() { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + return new int[] {redsb, greensb, bluesb}; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSPLT.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSPLT.java new file mode 100644 index 00000000..89bd57e6 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSPLT.java @@ -0,0 +1,131 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * sPLT chunk. + *

+ * see http://www.w3.org/TR/PNG/#11sPLT + */ +public class PngChunkSPLT extends PngChunkMultiple { + public final static String ID = ChunkHelper.sPLT; + + // http://www.w3.org/TR/PNG/#11sPLT + + private String palName; + private int sampledepth; // 8/16 + private int[] palette; // 5 elements per entry + + public PngChunkSPLT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + try { + ByteArrayOutputStream ba = new ByteArrayOutputStream(); + ba.write(ChunkHelper.toBytes(palName)); + ba.write(0); // separator + ba.write((byte) sampledepth); + int nentries = getNentries(); + for (int n = 0; n < nentries; n++) { + for (int i = 0; i < 4; i++) { + if (sampledepth == 8) + PngHelperInternal.writeByte(ba, (byte) palette[n * 5 + i]); + else + PngHelperInternal.writeInt2(ba, palette[n * 5 + i]); + } + PngHelperInternal.writeInt2(ba, palette[n * 5 + 4]); + } + byte[] b = ba.toByteArray(); + ChunkRaw chunk = createEmptyChunk(b.length, false); + chunk.data = b; + return chunk; + } catch (IOException e) { + throw new PngjException(e); + } + } + + @Override + public void parseFromRaw(ChunkRaw c) { + int t = -1; + for (int i = 0; i < c.data.length; i++) { // look for first zero + if (c.data[i] == 0) { + t = i; + break; + } + } + if (t <= 0 || t > c.data.length - 2) + throw new PngjException("bad sPLT chunk: no separator found"); + palName = ChunkHelper.toString(c.data, 0, t); + sampledepth = PngHelperInternal.readInt1fromByte(c.data, t + 1); + t += 2; + int nentries = (c.data.length - t) / (sampledepth == 8 ? 6 : 10); + palette = new int[nentries * 5]; + int r, g, b, a, f, ne; + ne = 0; + for (int i = 0; i < nentries; i++) { + if (sampledepth == 8) { + r = PngHelperInternal.readInt1fromByte(c.data, t++); + g = PngHelperInternal.readInt1fromByte(c.data, t++); + b = PngHelperInternal.readInt1fromByte(c.data, t++); + a = PngHelperInternal.readInt1fromByte(c.data, t++); + } else { + r = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + g = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + b = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + a = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + } + f = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + palette[ne++] = r; + palette[ne++] = g; + palette[ne++] = b; + palette[ne++] = a; + palette[ne++] = f; + } + } + + public int getNentries() { + return palette.length / 5; + } + + public String getPalName() { + return palName; + } + + public void setPalName(String palName) { + this.palName = palName; + } + + public int getSampledepth() { + return sampledepth; + } + + public void setSampledepth(int sampledepth) { + this.sampledepth = sampledepth; + } + + public int[] getPalette() { + return palette; + } + + public void setPalette(int[] palette) { + this.palette = palette; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSRGB.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSRGB.java new file mode 100644 index 00000000..ff54b4c8 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSRGB.java @@ -0,0 +1,55 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * sRGB chunk. + *

+ * see http://www.w3.org/TR/PNG/#11sRGB + */ +public class PngChunkSRGB extends PngChunkSingle { + public final static String ID = ChunkHelper.sRGB; + + // http://www.w3.org/TR/PNG/#11sRGB + + public static final int RENDER_INTENT_Perceptual = 0; + public static final int RENDER_INTENT_Relative_colorimetric = 1; + public static final int RENDER_INTENT_Saturation = 2; + public static final int RENDER_INTENT_Absolute_colorimetric = 3; + + private int intent; + + public PngChunkSRGB(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (c.len != 1) + throw new PngjException("bad chunk length " + c); + intent = PngHelperInternal.readInt1fromByte(c.data, 0); + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + c = createEmptyChunk(1, true); + c.data[0] = (byte) intent; + return c; + } + + public int getIntent() { + return intent; + } + + public void setIntent(int intent) { + this.intent = intent; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSTER.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSTER.java new file mode 100644 index 00000000..dd33c4d3 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSTER.java @@ -0,0 +1,54 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * sTER chunk. + *

+ * see http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.sTER + */ +public class PngChunkSTER extends PngChunkSingle { + public final static String ID = "sTER"; + + // http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.sTER + private byte mode; // 0: cross-fuse layout 1: diverging-fuse layout + + public PngChunkSTER(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(1, true); + c.data[0] = (byte) mode; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 1) + throw new PngjException("bad chunk length " + chunk); + mode = chunk.data[0]; + } + + /** + * 0: cross-fuse layout 1: diverging-fuse layout + */ + public byte getMode() { + return mode; + } + + /** + * 0: cross-fuse layout 1: diverging-fuse layout + */ + public void setMode(byte mode) { + this.mode = mode; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSingle.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSingle.java new file mode 100644 index 00000000..58c23494 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkSingle.java @@ -0,0 +1,43 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * PNG chunk type (abstract) that does not allow multiple instances in same image. + */ +public abstract class PngChunkSingle extends PngChunk { + + protected PngChunkSingle(String id, ImageInfo imgInfo) { + super(id, imgInfo); + } + + public final boolean allowsMultiple() { + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PngChunkSingle other = (PngChunkSingle) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTEXT.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTEXT.java new file mode 100644 index 00000000..ea404edc --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTEXT.java @@ -0,0 +1,44 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * tEXt chunk. + *

+ * see http://www.w3.org/TR/PNG/#11tEXt + */ +public class PngChunkTEXT extends PngChunkTextVar { + public final static String ID = ChunkHelper.tEXt; + + public PngChunkTEXT(ImageInfo info) { + super(ID, info); + } + + public PngChunkTEXT(ImageInfo info, String key, String val) { + super(ID, info); + setKeyVal(key, val); + } + + @Override + public ChunkRaw createRawChunk() { + if (key == null || key.trim().length() == 0) + throw new PngjException("Text chunk key must be non empty"); + byte[] b = ChunkHelper.toBytes(key + "\0" + val); + ChunkRaw chunk = createEmptyChunk(b.length, false); + chunk.data = b; + return chunk; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + int i; + for (i = 0; i < c.data.length; i++) + if (c.data[i] == 0) + break; + key = ChunkHelper.toString(c.data, 0, i); + i++; + val = i < c.data.length ? ChunkHelper.toString(c.data, i, c.data.length - i) : ""; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTIME.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTIME.java new file mode 100644 index 00000000..21e15132 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTIME.java @@ -0,0 +1,82 @@ +package ar.com.hjg.pngj.chunks; + +import java.util.Calendar; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * tIME chunk. + *

+ * see http://www.w3.org/TR/PNG/#11tIME + */ +public class PngChunkTIME extends PngChunkSingle { + public final static String ID = ChunkHelper.tIME; + + // http://www.w3.org/TR/PNG/#11tIME + private int year, mon, day, hour, min, sec; + + public PngChunkTIME(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(7, true); + PngHelperInternal.writeInt2tobytes(year, c.data, 0); + c.data[2] = (byte) mon; + c.data[3] = (byte) day; + c.data[4] = (byte) hour; + c.data[5] = (byte) min; + c.data[6] = (byte) sec; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 7) + throw new PngjException("bad chunk " + chunk); + year = PngHelperInternal.readInt2fromBytes(chunk.data, 0); + mon = PngHelperInternal.readInt1fromByte(chunk.data, 2); + day = PngHelperInternal.readInt1fromByte(chunk.data, 3); + hour = PngHelperInternal.readInt1fromByte(chunk.data, 4); + min = PngHelperInternal.readInt1fromByte(chunk.data, 5); + sec = PngHelperInternal.readInt1fromByte(chunk.data, 6); + } + + public void setNow(int secsAgo) { + Calendar d = Calendar.getInstance(); + d.setTimeInMillis(System.currentTimeMillis() - 1000 * (long) secsAgo); + year = d.get(Calendar.YEAR); + mon = d.get(Calendar.MONTH) + 1; + day = d.get(Calendar.DAY_OF_MONTH); + hour = d.get(Calendar.HOUR_OF_DAY); + min = d.get(Calendar.MINUTE); + sec = d.get(Calendar.SECOND); + } + + public void setYMDHMS(int yearx, int monx, int dayx, int hourx, int minx, int secx) { + year = yearx; + mon = monx; + day = dayx; + hour = hourx; + min = minx; + sec = secx; + } + + public int[] getYMDHMS() { + return new int[] {year, mon, day, hour, min, sec}; + } + + /** format YYYY/MM/DD HH:mm:SS */ + public String getAsString() { + return String.format("%04d/%02d/%02d %02d:%02d:%02d", year, mon, day, hour, min, sec); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTRNS.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTRNS.java new file mode 100644 index 00000000..82ad30fd --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTRNS.java @@ -0,0 +1,149 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * tRNS chunk. + *

+ * see http://www.w3.org/TR/PNG/#11tRNS + *

+ * this chunk structure depends on the image type + */ +public class PngChunkTRNS extends PngChunkSingle { + public final static String ID = ChunkHelper.tRNS; + + // http://www.w3.org/TR/PNG/#11tRNS + + // only one of these is meaningful, depending on the image type + private int gray; + private int red, green, blue; + private int[] paletteAlpha = new int[] {}; + + public PngChunkTRNS(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + if (imgInfo.greyscale) { + c = createEmptyChunk(2, true); + PngHelperInternal.writeInt2tobytes(gray, c.data, 0); + } else if (imgInfo.indexed) { + c = createEmptyChunk(paletteAlpha.length, true); + for (int n = 0; n < c.len; n++) { + c.data[n] = (byte) paletteAlpha[n]; + } + } else { + c = createEmptyChunk(6, true); + PngHelperInternal.writeInt2tobytes(red, c.data, 0); + PngHelperInternal.writeInt2tobytes(green, c.data, 0); + PngHelperInternal.writeInt2tobytes(blue, c.data, 0); + } + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (imgInfo.greyscale) { + gray = PngHelperInternal.readInt2fromBytes(c.data, 0); + } else if (imgInfo.indexed) { + int nentries = c.data.length; + paletteAlpha = new int[nentries]; + for (int n = 0; n < nentries; n++) { + paletteAlpha[n] = (int) (c.data[n] & 0xff); + } + } else { + red = PngHelperInternal.readInt2fromBytes(c.data, 0); + green = PngHelperInternal.readInt2fromBytes(c.data, 2); + blue = PngHelperInternal.readInt2fromBytes(c.data, 4); + } + } + + /** + * Set rgb values + * + */ + public void setRGB(int r, int g, int b) { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + red = r; + green = g; + blue = b; + } + + public int[] getRGB() { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + return new int[] {red, green, blue}; + } + + public int getRGB888() { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + return (red << 16) | (green << 8) | blue; + } + + public void setGray(int g) { + if (!imgInfo.greyscale) + throw new PngjException("only grayscale images support this"); + gray = g; + } + + public int getGray() { + if (!imgInfo.greyscale) + throw new PngjException("only grayscale images support this"); + return gray; + } + + /** + * Sets the length of the palette alpha. This should be followed by #setNentriesPalAlpha + * + * @param idx index inside the table + * @param val alpha value (0-255) + */ + public void setEntryPalAlpha(int idx, int val) { + paletteAlpha[idx] = val; + } + + public void setNentriesPalAlpha(int len) { + paletteAlpha = new int[len]; + } + + /** + * WARNING: non deep copy. See also {@link #setNentriesPalAlpha(int)} {@link #setEntryPalAlpha(int, int)} + */ + public void setPalAlpha(int[] palAlpha) { + if (!imgInfo.indexed) + throw new PngjException("only indexed images support this"); + paletteAlpha = palAlpha; + } + + /** + * WARNING: non deep copy + */ + public int[] getPalletteAlpha() { + return paletteAlpha; + } + + /** + * to use when only one pallete index is set as totally transparent + */ + public void setIndexEntryAsTransparent(int palAlphaIndex) { + if (!imgInfo.indexed) + throw new PngjException("only indexed images support this"); + paletteAlpha = new int[] {palAlphaIndex + 1}; + for (int i = 0; i < palAlphaIndex; i++) + paletteAlpha[i] = 255; + paletteAlpha[palAlphaIndex] = 0; + } + + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTextVar.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTextVar.java new file mode 100644 index 00000000..24ece4de --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkTextVar.java @@ -0,0 +1,60 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * Superclass (abstract) for three textual chunks (TEXT, ITXT, ZTXT) + */ +public abstract class PngChunkTextVar extends PngChunkMultiple { + protected String key; // key/val: only for tEXt. lazy computed + protected String val; + + // http://www.w3.org/TR/PNG/#11keywords + public final static String KEY_Title = "Title"; // Short (one line) title or caption for image + public final static String KEY_Author = "Author"; // Name of image's creator + public final static String KEY_Description = "Description"; // Description of image (possibly + // long) + public final static String KEY_Copyright = "Copyright"; // Copyright notice + public final static String KEY_Creation_Time = "Creation Time"; // Time of original image creation + public final static String KEY_Software = "Software"; // Software used to create the image + public final static String KEY_Disclaimer = "Disclaimer"; // Legal disclaimer + public final static String KEY_Warning = "Warning"; // Warning of nature of content + public final static String KEY_Source = "Source"; // Device used to create the image + public final static String KEY_Comment = "Comment"; // Miscellaneous comment + + protected PngChunkTextVar(String id, ImageInfo info) { + super(id, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + public static class PngTxtInfo { + public String title; + public String author; + public String description; + public String creation_time;// = (new Date()).toString(); + public String software; + public String disclaimer; + public String warning; + public String source; + public String comment; + + } + + public String getKey() { + return key; + } + + public String getVal() { + return val; + } + + public void setKeyVal(String key, String val) { + this.key = key; + this.val = val; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkUNKNOWN.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkUNKNOWN.java new file mode 100644 index 00000000..a9778f76 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkUNKNOWN.java @@ -0,0 +1,40 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * Placeholder for UNKNOWN (custom or not) chunks. + *

+ * For PngReader, a chunk is unknown if it's not registered in the chunk factory + */ +public class PngChunkUNKNOWN extends PngChunkMultiple { // unkown, custom or not + + public PngChunkUNKNOWN(String id, ImageInfo info) { + super(id, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + @Override + public ChunkRaw createRawChunk() { + return raw; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + + } + + /* does not do deep copy! */ + public byte[] getData() { + return raw.data; + } + + /* does not do deep copy! */ + public void setData(byte[] data) { + raw.data = data; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkZTXT.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkZTXT.java new file mode 100644 index 00000000..6b172e62 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngChunkZTXT.java @@ -0,0 +1,62 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * zTXt chunk. + *

+ * see http://www.w3.org/TR/PNG/#11zTXt + */ +public class PngChunkZTXT extends PngChunkTextVar { + public final static String ID = ChunkHelper.zTXt; + + // http://www.w3.org/TR/PNG/#11zTXt + public PngChunkZTXT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkRaw createRawChunk() { + if (key == null || key.trim().length() == 0) + throw new PngjException("Text chunk key must be non empty"); + try { + ByteArrayOutputStream ba = new ByteArrayOutputStream(); + ba.write(ChunkHelper.toBytes(key)); + ba.write(0); // separator + ba.write(0); // compression method: 0 + byte[] textbytes = ChunkHelper.compressBytes(ChunkHelper.toBytes(val), true); + ba.write(textbytes); + byte[] b = ba.toByteArray(); + ChunkRaw chunk = createEmptyChunk(b.length, false); + chunk.data = b; + return chunk; + } catch (IOException e) { + throw new PngjException(e); + } + } + + @Override + public void parseFromRaw(ChunkRaw c) { + int nullsep = -1; + for (int i = 0; i < c.data.length; i++) { // look for first zero + if (c.data[i] != 0) + continue; + nullsep = i; + break; + } + if (nullsep < 0 || nullsep > c.data.length - 2) + throw new PngjException("bad zTXt chunk: no separator found"); + key = ChunkHelper.toString(c.data, 0, nullsep); + int compmet = (int) c.data[nullsep + 1]; + if (compmet != 0) + throw new PngjException("bad zTXt chunk: unknown compression method"); + byte[] uncomp = + ChunkHelper.compressBytes(c.data, nullsep + 2, c.data.length - nullsep - 2, false); // uncompress + val = ChunkHelper.toString(uncomp); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngMetadata.java b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngMetadata.java new file mode 100644 index 00000000..64912c45 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/PngMetadata.java @@ -0,0 +1,230 @@ +package ar.com.hjg.pngj.chunks; + +import java.util.ArrayList; +import java.util.List; + +import ar.com.hjg.pngj.PngjException; + +/** + * We consider "image metadata" every info inside the image except for the most basic image info (IHDR chunk - ImageInfo + * class) and the pixels values. + *

+ * This includes the palette (if present) and all the ancillary chunks + *

+ * This class provides a wrapper over the collection of chunks of a image (read or to write) and provides some high + * level methods to access them + */ +public class PngMetadata { + private final ChunksList chunkList; + private final boolean readonly; + + public PngMetadata(ChunksList chunks) { + this.chunkList = chunks; + if (chunks instanceof ChunksListForWrite) { + this.readonly = false; + } else { + this.readonly = true; + } + } + + /** + * Queues the chunk at the writer + *

+ * lazyOverwrite: if true, checks if there is a queued "equivalent" chunk and if so, overwrites it. However if that + * not check for already written chunks. + */ + public void queueChunk(final PngChunk c, boolean lazyOverwrite) { + ChunksListForWrite cl = getChunkListW(); + if (readonly) + throw new PngjException("cannot set chunk : readonly metadata"); + if (lazyOverwrite) { + ChunkHelper.trimList(cl.getQueuedChunks(), new ChunkPredicate() { + public boolean match(PngChunk c2) { + return ChunkHelper.equivalent(c, c2); + } + }); + } + cl.queue(c); + } + + public void queueChunk(final PngChunk c) { + queueChunk(c, true); + } + + private ChunksListForWrite getChunkListW() { + return (ChunksListForWrite) chunkList; + } + + // ///// high level utility methods follow //////////// + + // //////////// DPI + + /** + * returns -1 if not found or dimension unknown + */ + public double[] getDpi() { + PngChunk c = chunkList.getById1(ChunkHelper.pHYs, true); + if (c == null) + return new double[] {-1, -1}; + else + return ((PngChunkPHYS) c).getAsDpi2(); + } + + public void setDpi(double x) { + setDpi(x, x); + } + + public void setDpi(double x, double y) { + PngChunkPHYS c = new PngChunkPHYS(chunkList.imageInfo); + c.setAsDpi2(x, y); + queueChunk(c); + } + + // //////////// TIME + + /** + * Creates a time chunk with current time, less secsAgo seconds + *

+ * + * @return Returns the created-queued chunk, just in case you want to examine or modify it + */ + public PngChunkTIME setTimeNow(int secsAgo) { + PngChunkTIME c = new PngChunkTIME(chunkList.imageInfo); + c.setNow(secsAgo); + queueChunk(c); + return c; + } + + public PngChunkTIME setTimeNow() { + return setTimeNow(0); + } + + /** + * Creates a time chunk with diven date-time + *

+ * + * @return Returns the created-queued chunk, just in case you want to examine or modify it + */ + public PngChunkTIME setTimeYMDHMS(int yearx, int monx, int dayx, int hourx, int minx, int secx) { + PngChunkTIME c = new PngChunkTIME(chunkList.imageInfo); + c.setYMDHMS(yearx, monx, dayx, hourx, minx, secx); + queueChunk(c, true); + return c; + } + + /** + * null if not found + */ + public PngChunkTIME getTime() { + return (PngChunkTIME) chunkList.getById1(ChunkHelper.tIME); + } + + public String getTimeAsString() { + PngChunkTIME c = getTime(); + return c == null ? "" : c.getAsString(); + } + + // //////////// TEXT + + /** + * Creates a text chunk and queue it. + *

+ * + * @param k : key (latin1) + * @param val (arbitrary, should be latin1 if useLatin1) + * @param useLatin1 + * @param compress + * @return Returns the created-queued chunks, just in case you want to examine, touch it + */ + public PngChunkTextVar setText(String k, String val, boolean useLatin1, boolean compress) { + if (compress && !useLatin1) + throw new PngjException("cannot compress non latin text"); + PngChunkTextVar c; + if (useLatin1) { + if (compress) { + c = new PngChunkZTXT(chunkList.imageInfo); + } else { + c = new PngChunkTEXT(chunkList.imageInfo); + } + } else { + c = new PngChunkITXT(chunkList.imageInfo); + ((PngChunkITXT) c).setLangtag(k); // we use the same orig tag (this is not quite right) + } + c.setKeyVal(k, val); + queueChunk(c, true); + return c; + } + + public PngChunkTextVar setText(String k, String val) { + return setText(k, val, false, false); + } + + /** + * gets all text chunks with a given key + *

+ * returns null if not found + *

+ * Warning: this does not check the "lang" key of iTxt + */ + @SuppressWarnings("unchecked") + public List getTxtsForKey(String k) { + @SuppressWarnings("rawtypes") + List c = new ArrayList(); + c.addAll(chunkList.getById(ChunkHelper.tEXt, k)); + c.addAll(chunkList.getById(ChunkHelper.zTXt, k)); + c.addAll(chunkList.getById(ChunkHelper.iTXt, k)); + return c; + } + + /** + * Returns empty if not found, concatenated (with newlines) if multiple! - and trimmed + *

+ * Use getTxtsForKey() if you don't want this behaviour + */ + public String getTxtForKey(String k) { + List li = getTxtsForKey(k); + if (li.isEmpty()) + return ""; + StringBuilder t = new StringBuilder(); + for (PngChunkTextVar c : li) + t.append(c.getVal()).append("\n"); + return t.toString().trim(); + } + + /** + * Returns the palette chunk, if present + * + * @return null if not present + */ + public PngChunkPLTE getPLTE() { + return (PngChunkPLTE) chunkList.getById1(PngChunkPLTE.ID); + } + + /** + * Creates a new empty palette chunk, queues it for write and return it to the caller, who should fill its entries + */ + public PngChunkPLTE createPLTEChunk() { + PngChunkPLTE plte = new PngChunkPLTE(chunkList.imageInfo); + queueChunk(plte); + return plte; + } + + /** + * Returns the TRNS chunk, if present + * + * @return null if not present + */ + public PngChunkTRNS getTRNS() { + return (PngChunkTRNS) chunkList.getById1(PngChunkTRNS.ID); + } + + /** + * Creates a new empty TRNS chunk, queues it for write and return it to the caller, who should fill its entries + */ + public PngChunkTRNS createTRNSChunk() { + PngChunkTRNS trns = new PngChunkTRNS(chunkList.imageInfo); + queueChunk(trns); + return trns; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/chunks/package.html b/src/jar-specific/java/ar/com/hjg/pngj/chunks/package.html new file mode 100644 index 00000000..13740669 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/chunks/package.html @@ -0,0 +1,9 @@ + + +

+Contains the code related to chunk management for the PNGJ library.

+

+Only needed by client code if some special chunk handling is required. +

+ + diff --git a/src/jar-specific/java/ar/com/hjg/pngj/package.html b/src/jar-specific/java/ar/com/hjg/pngj/package.html new file mode 100644 index 00000000..a417d56b --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/package.html @@ -0,0 +1,49 @@ + + +

+PNGJ main package +

+

+Users of this library should rarely need more than the public members of this package.
+Newcomers: start with PngReader and PngWriter. +

+

+Example of use: this code reads a true colour PNG image (RGB8 or RGBA8) +and reduces the red channel by half, increasing the green by 20. +It copies all the "safe" metadata from the original image, and adds a textual metadata. + +

+  public static void convert(String origFilename, String destFilename) {
+    // you can also use PngReader (esentially the same) or PngReaderByte 
+    PngReaderInt pngr = new PngReaderInt(new File(origFilename));  
+    System.out.println(pngr.toString());
+    int channels = pngr.imgInfo.channels;
+    if (channels < 3 || pngr.imgInfo.bitDepth != 8)
+       throw new RuntimeException("For simplicity this supports only RGB8/RGBA8 images");
+    // writer with same image properties as original
+    PngWriter pngw = new PngWriter(new File(destFilename), pngr.imgInfo, true);
+    // instruct the writer to grab all ancillary chunks from the original
+    pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_ALL_SAFE);
+    // add a textual chunk to writer
+    pngw.getMetadata().setText(PngChunkTextVar.KEY_Description, "Decreased red and increased green");
+    // also: while(pngr.hasMoreRows())
+    for (int row = 0; row < pngr.imgInfo.rows; row++) {  
+       ImageLineInt l1 = pngr.readRowInt(); // each element is a sample
+       int[] scanline = l1.getScanline(); // to save typing
+       for (int j = 0; j < pngr.imgInfo.cols; j++) {
+          scanline[j * channels] /= 2;
+          scanline[j * channels + 1] = ImageLineHelper.clampTo_0_255(scanline[j * channels + 1] + 20);
+       }
+       pngw.writeRow(l1);
+    }
+    pngr.end(); // it's recommended to end the reader first, in case there are trailing chunks to read
+    pngw.end();
+ }
+
+
+ +For more examples, see the tests and samples. + +

+ + diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStream.java b/src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStream.java new file mode 100644 index 00000000..6b4d86f3 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStream.java @@ -0,0 +1,160 @@ +package ar.com.hjg.pngj.pixels; + +import java.io.OutputStream; + +import ar.com.hjg.pngj.IDatChunkWriter; + +/** + * This is an OutputStream that compresses (via Deflater or a deflater-like object), and optionally passes the + * compressed stream to another output stream. + * + * It allows to compute in/out/ratio stats. + * + * It works as a stream (similar to DeflaterOutputStream), but it's peculiar in that it expects that each writes has a + * fixed length (other lenghts are accepted, but it's less efficient) and that the total amount of bytes is known (so it + * can close itself, but it can also be closed on demand) In PNGJ use, the block is typically a row (including filter + * byte). + * + * We use this to do the real compression (with Deflate) but also to compute tentative estimators + * + * If not closed, it can be recicled via reset() + * + * + */ +public abstract class CompressorStream extends OutputStream { + + protected IDatChunkWriter idatChunkWriter; + public final int blockLen; + public final long totalbytes; + + boolean closed = false; + protected boolean done = false; + protected long bytesIn = 0; + protected long bytesOut = 0; + protected int block = -1; + + /** optionally stores the first byte of each block (row) */ + private byte[] firstBytes; + protected boolean storeFirstByte = false; + + /** + * + * @param idatCw Can be null (if we are only interested in compute compression ratio) + * @param blockLen Estimated maximum block length. If unknown, use -1. + * @param totalbytes Expected total bytes to be fed. If unknown, use -1. + */ + public CompressorStream(IDatChunkWriter idatCw, int blockLen, long totalbytes) { + this.idatChunkWriter = idatCw; + if (blockLen < 0) + blockLen = 4096; + if (totalbytes < 0) + totalbytes = Long.MAX_VALUE; + if (blockLen < 1 || totalbytes < 1) + throw new RuntimeException(" maxBlockLen or totalLen invalid"); + this.blockLen = blockLen; + this.totalbytes = totalbytes; + } + + /** Releases resources. Idempotent. */ + @Override + public void close() { + done(); + if(idatChunkWriter!=null) idatChunkWriter.close(); + closed = true; + } + + /** + * Will be called automatically when the number of bytes reaches the total expected Can be also be called from + * outside. This should set the flag done=true + */ + public abstract void done(); + + @Override + public final void write(byte[] data) { + write(data, 0, data.length); + } + + @Override + public final void write(byte[] data, int off, int len) { + block++; + if (len <= blockLen) { // normal case + mywrite(data, off, len); + if (storeFirstByte && block < firstBytes.length) { + firstBytes[block] = data[off]; // only makes sense in this case + } + } else { + while (len > 0) { + mywrite(data, off, blockLen); + off += blockLen; + len -= blockLen; + } + } + if (bytesIn >= totalbytes) + done(); + + } + + /** + * same as write, but guarantedd to not exceed blockLen The implementation should update bytesOut and bytesInt but not + * check for totalBytes + */ + public abstract void mywrite(byte[] data, int off, int len); + + + /** + * compressed/raw. This should be called only when done + */ + public final double getCompressionRatio() { + return bytesOut == 0 ? 1.0 : bytesOut / (double) bytesIn; + } + + /** + * raw (input) bytes. This should be called only when done + */ + public final long getBytesRaw() { + return bytesIn; + } + + /** + * compressed (out) bytes. This should be called only when done + */ + public final long getBytesCompressed() { + return bytesOut; + } + + public boolean isClosed() { + return closed; + } + + public boolean isDone() { + return done; + } + + public byte[] getFirstBytes() { + return firstBytes; + } + + public void setStoreFirstByte(boolean storeFirstByte, int nblocks) { + this.storeFirstByte = storeFirstByte; + if (this.storeFirstByte) { + if (firstBytes == null || firstBytes.length < nblocks) + firstBytes = new byte[nblocks]; + } else + firstBytes = null; + } + + public void reset() { + done(); + bytesIn = 0; + bytesOut = 0; + block = -1; + done = false; + } + + @Override + public void write(int i) { // should not be used + write(new byte[] {(byte) i}); + } + + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamDeflater.java b/src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamDeflater.java new file mode 100644 index 00000000..24bb7e43 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamDeflater.java @@ -0,0 +1,104 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.zip.Deflater; + +import ar.com.hjg.pngj.IDatChunkWriter; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * CompressorStream backed by a Deflater. + * + * Note that the Deflater is not disposed after done, you should either recycle this with reset() or dispose it with + * close() + * + */ +public class CompressorStreamDeflater extends CompressorStream { + + protected Deflater deflater; + protected byte[] buf1; // temporary storage of compressed bytes: only used if idatWriter is null + protected boolean deflaterIsOwn = true; + + /** if a deflater is passed, it must be already reset. It will not be released on close */ + public CompressorStreamDeflater(IDatChunkWriter idatCw, int maxBlockLen, long totalLen, + Deflater def) { + super(idatCw, maxBlockLen, totalLen); + this.deflater = def == null ? new Deflater() : def; + this.deflaterIsOwn = def == null; + } + + public CompressorStreamDeflater(IDatChunkWriter idatCw, int maxBlockLen, long totalLen) { + this(idatCw, maxBlockLen, totalLen, null); + } + + public CompressorStreamDeflater(IDatChunkWriter idatCw, int maxBlockLen, long totalLen, + int deflaterCompLevel, int deflaterStrategy) { + this(idatCw, maxBlockLen, totalLen, new Deflater(deflaterCompLevel)); + this.deflaterIsOwn = true; + deflater.setStrategy(deflaterStrategy); + } + + @Override + public void mywrite(byte[] data, int off, final int len) { + if (deflater.finished() || done || closed) + throw new PngjOutputException("write beyond end of stream"); + deflater.setInput(data, off, len); + bytesIn += len; + while (!deflater.needsInput()) + deflate(); + } + + protected void deflate() { + byte[] buf; + int off, n; + if (idatChunkWriter != null) { + buf = idatChunkWriter.getBuf(); + off = idatChunkWriter.getOffset(); + n = idatChunkWriter.getAvailLen(); + } else { + if (buf1 == null) + buf1 = new byte[4096]; + buf = buf1; + off = 0; + n = buf1.length; + } + int len = deflater.deflate(buf, off, n); + if (len > 0) { + if (idatChunkWriter != null) + idatChunkWriter.incrementOffset(len); + bytesOut += len; + } + } + + /** automatically called when done */ + @Override + public void done() { + if (done) + return; + if (!deflater.finished()) { + deflater.finish(); + while (!deflater.finished()) + deflate(); + } + done = true; + if (idatChunkWriter != null) + idatChunkWriter.close(); + } + + public void close() { + done(); + try { + if (deflaterIsOwn) { + deflater.end(); + } + } catch (Exception e) { + } + super.close(); + } + + @Override + public void reset() { + deflater.reset(); + super.reset(); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamLz4.java b/src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamLz4.java new file mode 100644 index 00000000..299d3668 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamLz4.java @@ -0,0 +1,94 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.zip.Deflater; + +import ar.com.hjg.pngj.IDatChunkWriter; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * This class uses a quick compressor to get a rough estimate of deflate compression ratio. + * + * This just ignores the outputStream, and the deflater related parameters + */ +public class CompressorStreamLz4 extends CompressorStream { + + private final DeflaterEstimatorLz4 lz4; + + private byte[] buf; // lazily allocated, only if needed + private final int buffer_size; + // bufpos=bytes in buffer yet not compressed (bytesIn include this) + private int inbuf = 0; + + private static final int MAX_BUFFER_SIZE = 16000; + + public CompressorStreamLz4(IDatChunkWriter os, int maxBlockLen, long totalLen) { + super(os, maxBlockLen, totalLen); + lz4 = new DeflaterEstimatorLz4(); + buffer_size = (int) (totalLen > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : totalLen); + } + + public CompressorStreamLz4(IDatChunkWriter os, int maxBlockLen, long totalLen, Deflater def) { + this(os, maxBlockLen, totalLen);// edlfater ignored + } + + public CompressorStreamLz4(IDatChunkWriter os, int maxBlockLen, long totalLen, + int deflaterCompLevel, int deflaterStrategy) { + this(os, maxBlockLen, totalLen); // paramters ignored + } + + @Override + public void mywrite(byte[] b, int off, int len) { + if (len == 0) + return; + if (done || closed) + throw new PngjOutputException("write beyond end of stream"); + bytesIn += len; + while (len > 0) { + if (inbuf == 0 && (len >= MAX_BUFFER_SIZE || bytesIn == totalbytes)) { + // direct copy (buffer might be null or empty) + bytesOut += lz4.compressEstim(b, off, len); + len = 0; + } else { + if (buf == null) + buf = new byte[buffer_size]; + int len1 = inbuf + len <= buffer_size ? len : buffer_size - inbuf; // to copy + if (len1 > 0) + System.arraycopy(b, off, buf, inbuf, len1); + inbuf += len1; + len -= len1; + off += len1; + if (inbuf == buffer_size) + compressFromBuffer(); + } + } + } + + void compressFromBuffer() { + if (inbuf > 0) { + bytesOut += lz4.compressEstim(buf, 0, inbuf); + inbuf = 0; + } + } + + @Override + public void done() { + if (!done) { + compressFromBuffer(); + done = true; + } + } + + @Override + public void close() { + done(); + if (!closed) { + super.close(); + buf = null; + } + } + + public void reset() { + super.reset(); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorHjg.java b/src/jar-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorHjg.java new file mode 100644 index 00000000..cfd332e2 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorHjg.java @@ -0,0 +1,258 @@ +package ar.com.hjg.pngj.pixels; + + +final public class DeflaterEstimatorHjg { + + /** + * This object is stateless, it's thread safe and can be reused + */ + public DeflaterEstimatorHjg() {} + + /** + * Estimates the length of the compressed bytes, as compressed by Lz4 WARNING: if larger than LZ4_64K_LIMIT it cuts it + * in fragments + * + * WARNING: if some part of the input is discarded, this should return the proportional (so that + * returnValue/srcLen=compressionRatio) + * + * @param src + * @param srcOff + * @param srcLen + * @return length of the compressed bytes + */ + public int compressEstim(byte[] src, int srcOff, final int srcLen) { + if (srcLen < 10) + return srcLen; // too small + int stride = LZ4_64K_LIMIT - 1; + int segments = (srcLen + stride - 1) / stride; + stride = srcLen / segments; + if (stride >= LZ4_64K_LIMIT - 1 || stride * segments > srcLen || segments < 1 || stride < 1) + throw new RuntimeException("?? " + srcLen); + int bytesIn = 0; + int bytesOut = 0; + int len = srcLen; + while (len > 0) { + if (len > stride) + len = stride; + bytesOut += compress64k(src, srcOff, len); + srcOff += len; + bytesIn += len; + len = srcLen - bytesIn; + } + double ratio = bytesOut / (double) bytesIn; + return bytesIn == srcLen ? bytesOut : (int) (ratio * srcLen + 0.5); + } + + public int compressEstim(byte[] src) { + return compressEstim(src, 0, src.length); + } + + static final int MEMORY_USAGE = 14; + static final int NOT_COMPRESSIBLE_DETECTION_LEVEL = 6; // see SKIP_STRENGTH + + static final int MIN_MATCH = 4; + + static final int HASH_LOG = MEMORY_USAGE - 2; + static final int HASH_TABLE_SIZE = 1 << HASH_LOG; + + static final int SKIP_STRENGTH = Math.max(NOT_COMPRESSIBLE_DETECTION_LEVEL, 2); // 6 findMatchAttempts = + // 2^SKIP_STRENGTH+3 + static final int COPY_LENGTH = 8; + static final int LAST_LITERALS = 5; + static final int MF_LIMIT = COPY_LENGTH + MIN_MATCH; + static final int MIN_LENGTH = MF_LIMIT + 1; + + static final int MAX_DISTANCE = 1 << 16; + + static final int ML_BITS = 4; + static final int ML_MASK = (1 << ML_BITS) - 1; + static final int RUN_BITS = 8 - ML_BITS; + static final int RUN_MASK = (1 << RUN_BITS) - 1; + + static final int LZ4_64K_LIMIT = (1 << 16) + (MF_LIMIT - 1); + static final int HASH_LOG_64K = HASH_LOG + 1; + static final int HASH_TABLE_SIZE_64K = 1 << HASH_LOG_64K; + + static final int HASH_LOG_HC = 15; + static final int HASH_TABLE_SIZE_HC = 1 << HASH_LOG_HC; + static final int OPTIMAL_ML = ML_MASK - 1 + MIN_MATCH; + + static int compress64k(byte[] src, final int srcOff, final int srcLen) { + final int srcEnd = srcOff + srcLen; + final int srcLimit = srcEnd - LAST_LITERALS; + final int mflimit = srcEnd - MF_LIMIT; + + int sOff = srcOff, dOff = 0; + + int anchor = sOff; + + if (srcLen >= MIN_LENGTH) { + + final short[] hashTable = new short[HASH_TABLE_SIZE_64K]; + + ++sOff; + + main: while (true) { + + // find a match + int forwardOff = sOff; + + int ref; + int findMatchAttempts1 = (1 << SKIP_STRENGTH) + 3; // 64+3=67 + do { + sOff = forwardOff; + forwardOff += findMatchAttempts1++ >>> SKIP_STRENGTH; + + if (forwardOff > mflimit) { + break main; // ends all + } + + final int h = hash64k(readInt(src, sOff)); + ref = srcOff + readShort(hashTable, h); + writeShort(hashTable, h, sOff - srcOff); + } while (!readIntEquals(src, ref, sOff)); + + // catch up + final int excess = commonBytesBackward(src, ref, sOff, srcOff, anchor); + sOff -= excess; + ref -= excess; + // sequence == refsequence + final int runLen = sOff - anchor; + dOff++; + + if (runLen >= RUN_MASK) { + if (runLen > RUN_MASK) + dOff += (runLen - RUN_MASK) / 0xFF; + dOff++; + } + dOff += runLen; + while (true) { + // encode offset + dOff += 2; + // count nb matches + sOff += MIN_MATCH; + ref += MIN_MATCH; + final int matchLen = commonBytes(src, ref, sOff, srcLimit); + sOff += matchLen; + // encode match len + if (matchLen >= ML_MASK) { + if (matchLen >= ML_MASK + 0xFF) + dOff += (matchLen - ML_MASK) / 0xFF; + dOff++; + } + // test end of chunk + if (sOff > mflimit) { + anchor = sOff; + break main; + } + // fill table + writeShort(hashTable, hash64k(readInt(src, sOff - 2)), sOff - 2 - srcOff); + // test next position + final int h = hash64k(readInt(src, sOff)); + ref = srcOff + readShort(hashTable, h); + writeShort(hashTable, h, sOff - srcOff); + if (!readIntEquals(src, sOff, ref)) { + break; + } + dOff++; + } + // prepare next loop + anchor = sOff++; + } + } + int runLen = srcEnd - anchor; + if (runLen >= RUN_MASK + 0xFF) { + dOff += (runLen - RUN_MASK) / 0xFF; + } + dOff++; + dOff += runLen; + return dOff; + } + + static final int maxCompressedLength(int length) { + if (length < 0) { + throw new IllegalArgumentException("length must be >= 0, got " + length); + } + return length + length / 255 + 16; + } + + static int hash(int i) { + return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG); + } + + static int hash64k(int i) { + return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG_64K); + } + + static int readShortLittleEndian(byte[] buf, int i) { + return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8); + } + + static boolean readIntEquals(byte[] buf, int i, int j) { + return buf[i] == buf[j] && buf[i + 1] == buf[j + 1] && buf[i + 2] == buf[j + 2] + && buf[i + 3] == buf[j + 3]; + } + + static int commonBytes(byte[] b, int o1, int o2, int limit) { + int count = 0; + while (o2 < limit && b[o1++] == b[o2++]) { + ++count; + } + return count; + } + + static int commonBytesBackward(byte[] b, int o1, int o2, int l1, int l2) { + int count = 0; + while (o1 > l1 && o2 > l2 && b[--o1] == b[--o2]) { + ++count; + } + return count; + } + + static int readShort(short[] buf, int off) { + return buf[off] & 0xFFFF; + } + + static byte readByte(byte[] buf, int i) { + return buf[i]; + } + + static void checkRange(byte[] buf, int off) { + if (off < 0 || off >= buf.length) { + throw new ArrayIndexOutOfBoundsException(off); + } + } + + static void checkRange(byte[] buf, int off, int len) { + checkLength(len); + if (len > 0) { + checkRange(buf, off); + checkRange(buf, off + len - 1); + } + } + + static void checkLength(int len) { + if (len < 0) { + throw new IllegalArgumentException("lengths must be >= 0"); + } + } + + static int readIntBE(byte[] buf, int i) { + return ((buf[i] & 0xFF) << 24) | ((buf[i + 1] & 0xFF) << 16) | ((buf[i + 2] & 0xFF) << 8) + | (buf[i + 3] & 0xFF); + } + + static int readIntLE(byte[] buf, int i) { + return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8) | ((buf[i + 2] & 0xFF) << 16) + | ((buf[i + 3] & 0xFF) << 24); + } + + static int readInt(byte[] buf, int i) { + return readIntBE(buf, i); + } + + static void writeShort(short[] buf, int off, int v) { + buf[off] = (short) v; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorLz4.java b/src/jar-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorLz4.java new file mode 100644 index 00000000..9c69d9de --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorLz4.java @@ -0,0 +1,272 @@ +package ar.com.hjg.pngj.pixels; + +import java.nio.ByteOrder; + +/** + * This estimator actually uses the LZ4 compression algorithm, and hopes that it's well correlated with Deflater. It's + * about 3 to 4 times faster than Deflater. + * + * This is a modified heavily trimmed version of the net.jpountz.lz4.LZ4JavaSafeCompressor class plus some methods from + * other classes from LZ4 Java library: https://github.com/jpountz/lz4-java , originally licensed under the Apache + * License 2.0 + */ +final public class DeflaterEstimatorLz4 { + + /** + * This object is stateless, it's thread safe and can be reused + */ + public DeflaterEstimatorLz4() {} + + /** + * Estimates the length of the compressed bytes, as compressed by Lz4 WARNING: if larger than LZ4_64K_LIMIT it cuts it + * in fragments + * + * WARNING: if some part of the input is discarded, this should return the proportional (so that + * returnValue/srcLen=compressionRatio) + * + * @param src + * @param srcOff + * @param srcLen + * @return length of the compressed bytes + */ + public int compressEstim(byte[] src, int srcOff, final int srcLen) { + if (srcLen < 10) + return srcLen; // too small + int stride = LZ4_64K_LIMIT - 1; + int segments = (srcLen + stride - 1) / stride; + stride = srcLen / segments; + if (stride >= LZ4_64K_LIMIT - 1 || stride * segments > srcLen || segments < 1 || stride < 1) + throw new RuntimeException("?? " + srcLen); + int bytesIn = 0; + int bytesOut = 0; + int len = srcLen; + while (len > 0) { + if (len > stride) + len = stride; + bytesOut += compress64k(src, srcOff, len); + srcOff += len; + bytesIn += len; + len = srcLen - bytesIn; + } + double ratio = bytesOut / (double) bytesIn; + return bytesIn == srcLen ? bytesOut : (int) (ratio * srcLen + 0.5); + } + + public int compressEstim(byte[] src) { + return compressEstim(src, 0, src.length); + } + + static final ByteOrder NATIVE_BYTE_ORDER = ByteOrder.nativeOrder(); + + static final int MEMORY_USAGE = 14; + static final int NOT_COMPRESSIBLE_DETECTION_LEVEL = 6; + + static final int MIN_MATCH = 4; + + static final int HASH_LOG = MEMORY_USAGE - 2; + static final int HASH_TABLE_SIZE = 1 << HASH_LOG; + + static final int SKIP_STRENGTH = Math.max(NOT_COMPRESSIBLE_DETECTION_LEVEL, 2); + static final int COPY_LENGTH = 8; + static final int LAST_LITERALS = 5; + static final int MF_LIMIT = COPY_LENGTH + MIN_MATCH; + static final int MIN_LENGTH = MF_LIMIT + 1; + + static final int MAX_DISTANCE = 1 << 16; + + static final int ML_BITS = 4; + static final int ML_MASK = (1 << ML_BITS) - 1; + static final int RUN_BITS = 8 - ML_BITS; + static final int RUN_MASK = (1 << RUN_BITS) - 1; + + static final int LZ4_64K_LIMIT = (1 << 16) + (MF_LIMIT - 1); + static final int HASH_LOG_64K = HASH_LOG + 1; + static final int HASH_TABLE_SIZE_64K = 1 << HASH_LOG_64K; + + static final int HASH_LOG_HC = 15; + static final int HASH_TABLE_SIZE_HC = 1 << HASH_LOG_HC; + static final int OPTIMAL_ML = ML_MASK - 1 + MIN_MATCH; + + static int compress64k(byte[] src, int srcOff, int srcLen) { + final int srcEnd = srcOff + srcLen; + final int srcLimit = srcEnd - LAST_LITERALS; + final int mflimit = srcEnd - MF_LIMIT; + + int sOff = srcOff, dOff = 0; + + int anchor = sOff; + + if (srcLen >= MIN_LENGTH) { + + final short[] hashTable = new short[HASH_TABLE_SIZE_64K]; + + ++sOff; + + main: while (true) { + + // find a match + int forwardOff = sOff; + + int ref; + int findMatchAttempts = (1 << SKIP_STRENGTH) + 3; + do { + sOff = forwardOff; + forwardOff += findMatchAttempts++ >>> SKIP_STRENGTH; + + if (forwardOff > mflimit) { + break main; + } + + final int h = hash64k(readInt(src, sOff)); + ref = srcOff + readShort(hashTable, h); + writeShort(hashTable, h, sOff - srcOff); + } while (!readIntEquals(src, ref, sOff)); + + // catch up + final int excess = commonBytesBackward(src, ref, sOff, srcOff, anchor); + sOff -= excess; + ref -= excess; + // sequence == refsequence + final int runLen = sOff - anchor; + dOff++; + + if (runLen >= RUN_MASK) { + if (runLen > RUN_MASK) + dOff += (runLen - RUN_MASK) / 0xFF; + dOff++; + } + dOff += runLen; + while (true) { + // encode offset + dOff += 2; + // count nb matches + sOff += MIN_MATCH; + ref += MIN_MATCH; + final int matchLen = commonBytes(src, ref, sOff, srcLimit); + sOff += matchLen; + // encode match len + if (matchLen >= ML_MASK) { + if (matchLen >= ML_MASK + 0xFF) + dOff += (matchLen - ML_MASK) / 0xFF; + dOff++; + } + // test end of chunk + if (sOff > mflimit) { + anchor = sOff; + break main; + } + // fill table + writeShort(hashTable, hash64k(readInt(src, sOff - 2)), sOff - 2 - srcOff); + // test next position + final int h = hash64k(readInt(src, sOff)); + ref = srcOff + readShort(hashTable, h); + writeShort(hashTable, h, sOff - srcOff); + if (!readIntEquals(src, sOff, ref)) { + break; + } + dOff++; + } + // prepare next loop + anchor = sOff++; + } + } + int runLen = srcEnd - anchor; + if (runLen >= RUN_MASK + 0xFF) { + dOff += (runLen - RUN_MASK) / 0xFF; + } + dOff++; + dOff += runLen; + return dOff; + } + + static final int maxCompressedLength(int length) { + if (length < 0) { + throw new IllegalArgumentException("length must be >= 0, got " + length); + } + return length + length / 255 + 16; + } + + static int hash(int i) { + return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG); + } + + static int hash64k(int i) { + return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG_64K); + } + + static int readShortLittleEndian(byte[] buf, int i) { + return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8); + } + + static boolean readIntEquals(byte[] buf, int i, int j) { + return buf[i] == buf[j] && buf[i + 1] == buf[j + 1] && buf[i + 2] == buf[j + 2] + && buf[i + 3] == buf[j + 3]; + } + + static int commonBytes(byte[] b, int o1, int o2, int limit) { + int count = 0; + while (o2 < limit && b[o1++] == b[o2++]) { + ++count; + } + return count; + } + + static int commonBytesBackward(byte[] b, int o1, int o2, int l1, int l2) { + int count = 0; + while (o1 > l1 && o2 > l2 && b[--o1] == b[--o2]) { + ++count; + } + return count; + } + + static int readShort(short[] buf, int off) { + return buf[off] & 0xFFFF; + } + + static byte readByte(byte[] buf, int i) { + return buf[i]; + } + + static void checkRange(byte[] buf, int off) { + if (off < 0 || off >= buf.length) { + throw new ArrayIndexOutOfBoundsException(off); + } + } + + static void checkRange(byte[] buf, int off, int len) { + checkLength(len); + if (len > 0) { + checkRange(buf, off); + checkRange(buf, off + len - 1); + } + } + + static void checkLength(int len) { + if (len < 0) { + throw new IllegalArgumentException("lengths must be >= 0"); + } + } + + static int readIntBE(byte[] buf, int i) { + return ((buf[i] & 0xFF) << 24) | ((buf[i + 1] & 0xFF) << 16) | ((buf[i + 2] & 0xFF) << 8) + | (buf[i + 3] & 0xFF); + } + + static int readIntLE(byte[] buf, int i) { + return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8) | ((buf[i + 2] & 0xFF) << 16) + | ((buf[i + 3] & 0xFF) << 24); + } + + static int readInt(byte[] buf, int i) { + if (NATIVE_BYTE_ORDER == ByteOrder.BIG_ENDIAN) { + return readIntBE(buf, i); + } else { + return readIntLE(buf, i); + } + } + + static void writeShort(short[] buf, int off, int v) { + buf[off] = (short) v; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/FiltersPerformance.java b/src/jar-specific/java/ar/com/hjg/pngj/pixels/FiltersPerformance.java new file mode 100644 index 00000000..5a3c043c --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/FiltersPerformance.java @@ -0,0 +1,203 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.Arrays; + +import ar.com.hjg.pngj.FilterType; +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjExceptionInternal; + +/** for use in adaptative strategy */ +public class FiltersPerformance { + + private final ImageInfo iminfo; + private double memoryA = 0.7; // empirical (not very critical: 0.72) + private int lastrow = -1; + private double[] absum = new double[5];// depending on the strategy not all values might be + // computed for all + private double[] entropy = new double[5]; + private double[] cost = new double[5]; + private int[] histog = new int[256]; // temporary, not normalized + private int lastprefered = -1; + private boolean initdone = false; + private double preferenceForNone = 1.0; // higher gives more preference to NONE + + // this values are empirical (montecarlo), for RGB8 images with entropy estimator for NONE and + // memory=0.7 + // DONT MODIFY THIS + public static final double[] FILTER_WEIGHTS_DEFAULT = {0.73, 1.03, 0.97, 1.11, 1.22}; // lower is + // better! + + private double[] filter_weights = new double[] {-1, -1, -1, -1, -1}; + + private final static double LOG2NI = -1.0 / Math.log(2.0); + + public FiltersPerformance(ImageInfo imgInfo) { + this.iminfo = imgInfo; + } + + private void init() { + if (filter_weights[0] < 0) {// has not been set from outside + System.arraycopy(FILTER_WEIGHTS_DEFAULT, 0, filter_weights, 0, 5); + double wNone = filter_weights[0]; + if (iminfo.bitDepth == 16) + wNone = 1.2; + else if (iminfo.alpha) + wNone = 0.8; + else if (iminfo.indexed || iminfo.bitDepth < 8) + wNone = 0.4; // we prefer NONE strongly + wNone /= preferenceForNone; + filter_weights[0] = wNone; + } + Arrays.fill(cost, 1.0); + initdone = true; + } + + public void updateFromFiltered(FilterType ftype, byte[] rowff, int rown) { + updateFromRawOrFiltered(ftype, rowff, null, null, rown); + } + + /** alternative: computes statistic without filtering */ + public void updateFromRaw(FilterType ftype, byte[] rowb, byte[] rowbprev, int rown) { + updateFromRawOrFiltered(ftype, null, rowb, rowbprev, rown); + } + + private void updateFromRawOrFiltered(FilterType ftype, byte[] rowff, byte[] rowb, + byte[] rowbprev, int rown) { + if (!initdone) + init(); + if (rown != lastrow) { + Arrays.fill(absum, Double.NaN); + Arrays.fill(entropy, Double.NaN); + } + lastrow = rown; + if (rowff != null) + computeHistogram(rowff); + else + computeHistogramForFilter(ftype, rowb, rowbprev); + if (ftype == FilterType.FILTER_NONE) + entropy[ftype.val] = computeEntropyFromHistogram(); + else + absum[ftype.val] = computeAbsFromHistogram(); + } + + /* WARNING: this is not idempotent, call it just once per cycle (sigh) */ + public FilterType getPreferred() { + int fi = 0; + double vali = Double.MAX_VALUE, val = 0; // lower wins + for (int i = 0; i < 5; i++) { + if (!Double.isNaN(absum[i])) { + val = absum[i]; + } else if (!Double.isNaN(entropy[i])) { + val = (Math.pow(2.0, entropy[i]) - 1.0) * 0.5; + } else + continue; + val *= filter_weights[i]; + val = cost[i] * memoryA + (1 - memoryA) * val; + cost[i] = val; + if (val < vali) { + vali = val; + fi = i; + } + } + lastprefered = fi; + return FilterType.getByVal(lastprefered); + } + + public final void computeHistogramForFilter(FilterType filterType, byte[] rowb, byte[] rowbprev) { + Arrays.fill(histog, 0); + int i, j, imax = iminfo.bytesPerRow; + switch (filterType) { + case FILTER_NONE: + for (i = 1; i <= imax; i++) + histog[rowb[i] & 0xFF]++; + break; + case FILTER_PAETH: + for (i = 1; i <= imax; i++) + histog[PngHelperInternal.filterRowPaeth(rowb[i], 0, rowbprev[i] & 0xFF, 0)]++; + for (j = 1, i = iminfo.bytesPixel + 1; i <= imax; i++, j++) + histog[PngHelperInternal.filterRowPaeth(rowb[i], rowb[j] & 0xFF, rowbprev[i] & 0xFF, + rowbprev[j] & 0xFF)]++; + break; + case FILTER_SUB: + for (i = 1; i <= iminfo.bytesPixel; i++) + histog[rowb[i] & 0xFF]++; + for (j = 1, i = iminfo.bytesPixel + 1; i <= imax; i++, j++) + histog[(rowb[i] - rowb[j]) & 0xFF]++; + break; + case FILTER_UP: + for (i = 1; i <= iminfo.bytesPerRow; i++) + histog[(rowb[i] - rowbprev[i]) & 0xFF]++; + break; + case FILTER_AVERAGE: + for (i = 1; i <= iminfo.bytesPixel; i++) + histog[((rowb[i] & 0xFF) - ((rowbprev[i] & 0xFF)) / 2) & 0xFF]++; + for (j = 1, i = iminfo.bytesPixel + 1; i <= imax; i++, j++) + histog[((rowb[i] & 0xFF) - ((rowbprev[i] & 0xFF) + (rowb[j] & 0xFF)) / 2) & 0xFF]++; + break; + default: + throw new PngjExceptionInternal("Bad filter:" + filterType); + } + } + + public void computeHistogram(byte[] rowff) { + Arrays.fill(histog, 0); + for (int i = 1; i < iminfo.bytesPerRow; i++) + histog[rowff[i] & 0xFF]++; + } + + public double computeAbsFromHistogram() { + int s = 0; + for (int i = 1; i < 128; i++) + s += histog[i] * i; + for (int i = 128, j = 128; j > 0; i++, j--) + s += histog[i] * j; + return s / (double) iminfo.bytesPerRow; + } + + public final double computeEntropyFromHistogram() { + double s = 1.0 / iminfo.bytesPerRow; + double ls = Math.log(s); + + double h = 0; + for (int x : histog) { + if (x > 0) + h += (Math.log(x) + ls) * x; + } + h *= s * LOG2NI; + if (h < 0.0) + h = 0.0; + return h; + } + + /** + * If larger than 1.0, NONE will be more prefered. This must be called before init + * + * @param preferenceForNone around 1.0 (default: 1.0) + */ + public void setPreferenceForNone(double preferenceForNone) { + this.preferenceForNone = preferenceForNone; + } + + /** + * Values greater than 1.0 (towards infinite) increase the memory towards 1. Values smaller than 1.0 (towards zero) + * decreases the memory . + * + */ + public void tuneMemory(double m) { + if (m == 0) + memoryA = 0.0; + else + memoryA = Math.pow(memoryA, 1.0 / m); + } + + /** + * To set manually the filter weights. This is not recommended, unless you know what you are doing. Setting this + * ignores preferenceForNone and omits some heuristics + * + * @param weights Five doubles around 1.0, one for each filter type. Lower is preferered + */ + public void setFilterWeights(double[] weights) { + System.arraycopy(weights, 0, filter_weights, 0, 5); + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriter.java b/src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriter.java new file mode 100644 index 00000000..677cef1a --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriter.java @@ -0,0 +1,263 @@ +package ar.com.hjg.pngj.pixels; + +import java.io.OutputStream; +import java.util.zip.Deflater; + +import ar.com.hjg.pngj.FilterType; +import ar.com.hjg.pngj.IDatChunkWriter; +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * Encodes a set of rows (pixels) as a continuous deflated stream (does not know about IDAT chunk segmentation). + *

+ * This includes the filter selection strategy, plus the filtering itself and the deflating. Only supports fixed length + * rows (no interlaced writing). + *

+ * Typically an instance of this is hold by a PngWriter - but more instances could be used (for APGN) + */ +public abstract class PixelsWriter { + + private static final int IDAT_MAX_SIZE_DEFAULT = 32000; + + protected final ImageInfo imgInfo; + /** + * row buffer length, including filter byte (imgInfo.bytesPerRow + 1) + */ + protected final int buflen; + + protected final int bytesPixel; + protected final int bytesRow; + + private CompressorStream compressorStream; // to compress the idat stream + + protected int deflaterCompLevel = 6; + protected int deflaterStrategy = Deflater.DEFAULT_STRATEGY; + + protected boolean initdone = false; + + /** + * This is the globally configured filter type - it can be a concrete type or a pseudo type (hint or strategy) + */ + protected FilterType filterType; + + // counts the filters used - just for stats + private int[] filtersUsed = new int[5]; + + // this is the raw underlying os (shared with the PngWriter) + private OutputStream os; + + private int idatMaxSize = IDAT_MAX_SIZE_DEFAULT; + + /** + * row being processed, couting from zero + */ + protected int currentRow; + + + public PixelsWriter(ImageInfo imgInfo) { + this.imgInfo = imgInfo; + bytesRow = imgInfo.bytesPerRow; + buflen = bytesRow + 1; + bytesPixel = imgInfo.bytesPixel; + currentRow = -1; + filterType = FilterType.FILTER_DEFAULT; + } + + /** + * main internal point for external call. It does the lazy initializion if necessary, sets current row, and call + * {@link #filterAndWrite(byte[])} + */ + public final void processRow(final byte[] rowb) { + if (!initdone) + init(); + currentRow++; + filterAndWrite(rowb); + } + + protected void sendToCompressedStream(byte[] rowf) { + compressorStream.write(rowf, 0, rowf.length); + filtersUsed[rowf[0]]++; + } + + /** + * This does the filtering and send to stream. Typically should decide the filtering, call + * {@link #filterRowWithFilterType(FilterType, byte[], byte[], byte[])} and and + * {@link #sendToCompressedStream(byte[])} + * + * @param rowb + */ + protected abstract void filterAndWrite(final byte[] rowb); + + /** + * Does the real filtering. This must be called with the real (standard) filterType. This should rarely be overriden. + *

+ * WARNING: look out the contract + * + * @param _filterType + * @param _rowb current row (the first byte might be modified) + * @param _rowbprev previous row (should be all zero the first time) + * @param _rowf tentative buffer to store the filtered bytes. might not be used! + * @return normally _rowf, but eventually _rowb. This MUST NOT BE MODIFIED nor reused by caller + */ + final protected byte[] filterRowWithFilterType(FilterType _filterType, byte[] _rowb, + byte[] _rowbprev, byte[] _rowf) { + // warning: some filters rely on: "previous row" (rowbprev) it must be initialized to 0 the + // first time + if (_filterType == FilterType.FILTER_NONE) + _rowf = _rowb; + _rowf[0] = (byte) _filterType.val; + int i, j; + switch (_filterType) { + case FILTER_NONE: + // we return the same original (be careful!) + break; + case FILTER_PAETH: + for (i = 1; i <= bytesPixel; i++) + _rowf[i] = (byte) PngHelperInternal.filterRowPaeth(_rowb[i], 0, _rowbprev[i] & 0xFF, 0); + for (j = 1, i = bytesPixel + 1; i <= bytesRow; i++, j++) + _rowf[i] = + (byte) PngHelperInternal.filterRowPaeth(_rowb[i], _rowb[j] & 0xFF, + _rowbprev[i] & 0xFF, _rowbprev[j] & 0xFF); + break; + case FILTER_SUB: + for (i = 1; i <= bytesPixel; i++) + _rowf[i] = (byte) _rowb[i]; + for (j = 1, i = bytesPixel + 1; i <= bytesRow; i++, j++) + _rowf[i] = (byte) (_rowb[i] - _rowb[j]); + break; + case FILTER_AVERAGE: + for (i = 1; i <= bytesPixel; i++) + _rowf[i] = (byte) (_rowb[i] - (_rowbprev[i] & 0xFF) / 2); + for (j = 1, i = bytesPixel + 1; i <= bytesRow; i++, j++) + _rowf[i] = (byte) (_rowb[i] - ((_rowbprev[i] & 0xFF) + (_rowb[j] & 0xFF)) / 2); + break; + case FILTER_UP: + for (i = 1; i <= bytesRow; i++) + _rowf[i] = (byte) (_rowb[i] - _rowbprev[i]); + break; + default: + throw new PngjOutputException("Filter type not recognized: " + _filterType); + } + return _rowf; + } + + /** + * This will be called by the PngWrite to fill the raw pixels for each row. This can change from call to call. + * Warning: this can be called before the object is init, implementations should call init() to be sure + */ + public abstract byte[] getRowb(); + + /** + * This will be called lazily just before writing row 0. Idempotent. + */ + protected final void init() { + if (!initdone) { + initParams(); + initdone = true; + } + } + + /** called by init(); override (calling this first) to do additional initialization */ + protected void initParams() { + IDatChunkWriter idatWriter = new IDatChunkWriter(os, idatMaxSize); + if (compressorStream == null) { // if not set, use the deflater + compressorStream = + new CompressorStreamDeflater(idatWriter, buflen, imgInfo.getTotalRawBytes(), + deflaterCompLevel, deflaterStrategy); + } + } + + /** cleanup. This should be called explicitly. Idempotent and secure */ + public void close() { + if (compressorStream != null) { + compressorStream.close(); + } + } + + /** + * Deflater (ZLIB) strategy. You should rarely change this from the default (Deflater.DEFAULT_STRATEGY) to + * Deflater.FILTERED (Deflater.HUFFMAN_ONLY is fast but compress poorly) + */ + public void setDeflaterStrategy(Integer deflaterStrategy) { + this.deflaterStrategy = deflaterStrategy; + } + + /** + * Deflater (ZLIB) compression level, between 0 (no compression) and 9 + */ + public void setDeflaterCompLevel(Integer deflaterCompLevel) { + this.deflaterCompLevel = deflaterCompLevel; + } + + public Integer getDeflaterCompLevel() { + return deflaterCompLevel; + } + + + public final void setOs(OutputStream datStream) { + this.os = datStream; + } + + public OutputStream getOs() { + return os; + } + + /** @see #filterType */ + final public FilterType getFilterType() { + return filterType; + } + + /** @see #filterType */ + final public void setFilterType(FilterType filterType) { + this.filterType = filterType; + } + + /* out/in This should be called only after end() to get reliable results */ + public double getCompression() { + return compressorStream.isDone() ? compressorStream.getCompressionRatio() : 1.0; + } + + public void setCompressorStream(CompressorStream compressorStream) { + this.compressorStream = compressorStream; + } + + public long getTotalBytesToWrite() { + return imgInfo.getTotalRawBytes(); + } + + public boolean isDone() { + return currentRow == imgInfo.rows - 1; + } + + /** + * computed default fixed filter type to use, if specified DEFAULT; wilde guess based on image properties + * + * @return One of the five concrete filter types + */ + protected FilterType getDefaultFilter() { + if (imgInfo.indexed || imgInfo.bitDepth < 8) + return FilterType.FILTER_NONE; + else if (imgInfo.getTotalPixels() < 1024) + return FilterType.FILTER_NONE; + else if (imgInfo.rows == 1) + return FilterType.FILTER_SUB; + else if (imgInfo.cols == 1) + return FilterType.FILTER_UP; + else + return FilterType.FILTER_PAETH; + } + + /** informational stats : filter used, in percentages */ + final public String getFiltersUsed() { + return String.format("%d,%d,%d,%d,%d", (int) (filtersUsed[0] * 100.0 / imgInfo.rows + 0.5), + (int) (filtersUsed[1] * 100.0 / imgInfo.rows + 0.5), (int) (filtersUsed[2] * 100.0 + / imgInfo.rows + 0.5), (int) (filtersUsed[3] * 100.0 / imgInfo.rows + 0.5), + (int) (filtersUsed[4] * 100.0 / imgInfo.rows + 0.5)); + } + + public void setIdatMaxSize(int idatMaxSize) { + this.idatMaxSize = idatMaxSize; + } +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterDefault.java b/src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterDefault.java new file mode 100644 index 00000000..5b8752a6 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterDefault.java @@ -0,0 +1,158 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.Arrays; + +import ar.com.hjg.pngj.FilterType; +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * Default implementation of PixelsWriter, with fixed filters and also adaptive strategies. + */ +public class PixelsWriterDefault extends PixelsWriter { + /** current raw row */ + protected byte[] rowb; + /** previous raw row */ + protected byte[] rowbprev; + /** buffer for filtered row */ + protected byte[] rowbfilter; + + /** evaluates different filters, for adaptive strategy */ + protected FiltersPerformance filtersPerformance; + + /** currently concrete selected filter type */ + protected FilterType curfilterType; + + /** parameters for adaptive strategy */ + protected int adaptMaxSkip; // set in initParams, does not change + protected int adaptSkipIncreaseSinceRow; // set in initParams, does not change + protected double adaptSkipIncreaseFactor; // set in initParams, does not change + protected int adaptNextRow = 0; + + public PixelsWriterDefault(ImageInfo imgInfo) { + super(imgInfo); + filtersPerformance = new FiltersPerformance(imgInfo); + } + + @Override + protected void initParams() { + super.initParams(); + + if (rowb == null || rowb.length < buflen) + rowb = new byte[buflen]; + if (rowbfilter == null || rowbfilter.length < buflen) + rowbfilter = new byte[buflen]; + if (rowbprev == null || rowbprev.length < buflen) + rowbprev = new byte[buflen]; + else + Arrays.fill(rowbprev, (byte) 0); + + // if adaptative but too few rows or columns, use default + if (imgInfo.cols < 3 && !FilterType.isValidStandard(filterType)) + filterType = FilterType.FILTER_DEFAULT; + if (imgInfo.rows < 3 && !FilterType.isValidStandard(filterType)) + filterType = FilterType.FILTER_DEFAULT; + + if (imgInfo.getTotalPixels() <= 1024 && !FilterType.isValidStandard(filterType)) + filterType = getDefaultFilter(); + + if (FilterType.isAdaptive(filterType)) { + // adaptCurSkip = 0; + adaptNextRow = 0; + if (filterType == FilterType.FILTER_ADAPTIVE_FAST) { + adaptMaxSkip = 200; + adaptSkipIncreaseSinceRow = 3; + adaptSkipIncreaseFactor = 1 / 4.0; // skip ~ row/3 + } else if (filterType == FilterType.FILTER_ADAPTIVE_MEDIUM) { + adaptMaxSkip = 8; + adaptSkipIncreaseSinceRow = 32; + adaptSkipIncreaseFactor = 1 / 80.0; + } else if (filterType == FilterType.FILTER_ADAPTIVE_FULL) { + adaptMaxSkip = 0; + adaptSkipIncreaseSinceRow = 128; + adaptSkipIncreaseFactor = 1 / 120.0; + } else + throw new PngjOutputException("bad filter " + filterType); + } + } + + @Override + protected void filterAndWrite(final byte[] rowb) { + if (rowb != this.rowb) + throw new RuntimeException("??"); // we rely on this + decideCurFilterType(); + byte[] filtered = filterRowWithFilterType(curfilterType, rowb, rowbprev, rowbfilter); + sendToCompressedStream(filtered); + // swap rowb <-> rowbprev + byte[] aux = this.rowb; + this.rowb = rowbprev; + rowbprev = aux; + } + + protected void decideCurFilterType() { + // decide the real filter and store in curfilterType + if (FilterType.isValidStandard(getFilterType())) { + curfilterType = getFilterType(); + } else if (getFilterType() == FilterType.FILTER_PRESERVE) { + curfilterType = FilterType.getByVal(rowb[0]); + } else if (getFilterType() == FilterType.FILTER_CYCLIC) { + curfilterType = FilterType.getByVal(currentRow % 5); + } else if (getFilterType() == FilterType.FILTER_DEFAULT) { + setFilterType(getDefaultFilter()); + curfilterType = getFilterType(); // this could be done once + } else if (FilterType.isAdaptive(getFilterType())) {// adaptive + if (currentRow == adaptNextRow) { + for (FilterType ftype : FilterType.getAllStandard()) + filtersPerformance.updateFromRaw(ftype, rowb, rowbprev, currentRow); + curfilterType = filtersPerformance.getPreferred(); + int skip = + (currentRow >= adaptSkipIncreaseSinceRow ? (int) Math + .round((currentRow - adaptSkipIncreaseSinceRow) * adaptSkipIncreaseFactor) : 0); + if (skip > adaptMaxSkip) + skip = adaptMaxSkip; + if (currentRow == 0) + skip = 0; + adaptNextRow = currentRow + 1 + skip; + } + } else { + throw new PngjOutputException("not implemented filter: " + getFilterType()); + } + if (currentRow == 0 && curfilterType != FilterType.FILTER_NONE + && curfilterType != FilterType.FILTER_SUB) + curfilterType = FilterType.FILTER_SUB; // first row should always be none or sub + } + + @Override + public byte[] getRowb() { + if (!initdone) + init(); + return rowb; + } + + @Override + public void close() { + super.close(); + } + + /** + * Only for adaptive strategies. See {@link FiltersPerformance#setPreferenceForNone(double)} + */ + public void setPreferenceForNone(double preferenceForNone) { + filtersPerformance.setPreferenceForNone(preferenceForNone); + } + + /** + * Only for adaptive strategies. See {@link FiltersPerformance#tuneMemory(double)} + */ + public void tuneMemory(double m) { + filtersPerformance.tuneMemory(m); + } + + /** + * Only for adaptive strategies. See {@link FiltersPerformance#setFilterWeights(double[])} + */ + public void setFilterWeights(double[] weights) { + filtersPerformance.setFilterWeights(weights); + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterMultiple.java b/src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterMultiple.java new file mode 100644 index 00000000..367dd981 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterMultiple.java @@ -0,0 +1,241 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.LinkedList; +import java.util.zip.Deflater; + +import ar.com.hjg.pngj.FilterType; +import ar.com.hjg.pngj.ImageInfo; + +/** Special pixels writer for experimental super adaptive strategy */ +public class PixelsWriterMultiple extends PixelsWriter { + /** + * unfiltered rowsperband elements, 0 is the current (rowb). This should include all rows of current band, plus one + */ + protected LinkedList rows; + + /** + * bank of compressor estimators, one for each filter and (perhaps) an adaptive strategy + */ + protected CompressorStream[] filterBank = new CompressorStream[6]; + /** + * stored filtered rows, one for each filter (0=none is not allocated but linked) + */ + protected byte[][] filteredRows = new byte[5][]; + protected byte[] filteredRowTmp; // + + protected FiltersPerformance filtersPerf; + protected int rowsPerBand = 0; // This is a 'nominal' size + protected int rowsPerBandCurrent = 0; // lastRowInThisBand-firstRowInThisBand +1 : might be + // smaller than rowsPerBand + protected int rowInBand = -1; + protected int bandNum = -1; + protected int firstRowInThisBand, lastRowInThisBand; + private boolean tryAdaptive = true; + + protected static final int HINT_MEMORY_DEFAULT_KB = 100; + // we will consume about (not more than) this memory (in buffers, not counting the compressors) + protected int hintMemoryKb = HINT_MEMORY_DEFAULT_KB; + + private int hintRowsPerBand = 1000; // default: very large number, can be changed + + private boolean useLz4 = true; + + public PixelsWriterMultiple(ImageInfo imgInfo) { + super(imgInfo); + filtersPerf = new FiltersPerformance(imgInfo); + rows = new LinkedList(); + for (int i = 0; i < 2; i++) + rows.add(new byte[buflen]); // we preallocate 2 rows (rowb and rowbprev) + filteredRowTmp = new byte[buflen]; + } + + @Override + protected void filterAndWrite(byte[] rowb) { + if (!initdone) + init(); + if (rowb != rows.get(0)) + throw new RuntimeException("?"); + setBandFromNewRown(); + byte[] rowbprev = rows.get(1); + for (FilterType ftype : FilterType.getAllStandardNoneLast()) { + // this has a special behaviour for NONE: filteredRows[0] is null, and the returned value is + // rowb + if (currentRow == 0 && ftype != FilterType.FILTER_NONE && ftype != FilterType.FILTER_SUB) + continue; + byte[] filtered = filterRowWithFilterType(ftype, rowb, rowbprev, filteredRows[ftype.val]); + filterBank[ftype.val].write(filtered); + if (currentRow == 0 && ftype == FilterType.FILTER_SUB) { // litle lie, only for first row + filterBank[FilterType.FILTER_PAETH.val].write(filtered); + filterBank[FilterType.FILTER_AVERAGE.val].write(filtered); + filterBank[FilterType.FILTER_UP.val].write(filtered); + } + // adptive: report each filterted + if (tryAdaptive) { + filtersPerf.updateFromFiltered(ftype, filtered, currentRow); + } + } + filteredRows[0] = rowb; + if (tryAdaptive) { + FilterType preferredAdaptive = filtersPerf.getPreferred(); + filterBank[5].write(filteredRows[preferredAdaptive.val]); + } + if (currentRow == lastRowInThisBand) { + int best = getBestCompressor(); + // PngHelperInternal.debug("won: " + best + " (rows: " + firstRowInThisBand + ":" + lastRowInThisBand + ")"); + // if(currentRow>90&¤tRow<100) + // PngHelperInternal.debug(String.format("row=%d ft=%s",currentRow,FilterType.getByVal(best))); + byte[] filtersAdapt = filterBank[best].getFirstBytes(); + for (int r = firstRowInThisBand, i = 0, j = lastRowInThisBand - firstRowInThisBand; r <= lastRowInThisBand; r++, j--, i++) { + int fti = filtersAdapt[i]; + byte[] filtered = null; + if (r != lastRowInThisBand) { + filtered = + filterRowWithFilterType(FilterType.getByVal(fti), rows.get(j), rows.get(j + 1), + filteredRowTmp); + } else { // no need to do this filtering, we already have it + filtered = filteredRows[fti]; + } + sendToCompressedStream(filtered); + } + } + // rotate + if (rows.size() > rowsPerBandCurrent) { + rows.addFirst(rows.removeLast()); + } else + rows.addFirst(new byte[buflen]); + } + + @Override + public byte[] getRowb() { + return rows.get(0); + } + + + private void setBandFromNewRown() { + boolean newBand = currentRow == 0 || currentRow > lastRowInThisBand; + if (currentRow == 0) + bandNum = -1; + if (newBand) { + bandNum++; + rowInBand = 0; + } else { + rowInBand++; + } + if (newBand) { + firstRowInThisBand = currentRow; + lastRowInThisBand = firstRowInThisBand + rowsPerBand - 1; + int lastRowInNextBand = firstRowInThisBand + 2 * rowsPerBand - 1; + if (lastRowInNextBand >= imgInfo.rows) // hack:make this band bigger, so we don't have a small + // last band + lastRowInThisBand = imgInfo.rows - 1; + rowsPerBandCurrent = 1 + lastRowInThisBand - firstRowInThisBand; + tryAdaptive = + rowsPerBandCurrent <= 3 || (rowsPerBandCurrent < 10 && imgInfo.bytesPerRow < 64) ? false + : true; + // rebuild bank + rebuildFiltersBank(); + } + } + + private void rebuildFiltersBank() { + long bytesPerBandCurrent = rowsPerBandCurrent * (long) buflen; + final int DEFLATER_COMP_LEVEL = 4; + for (int i = 0; i <= 5; i++) {// one for each filter plus one adaptive + CompressorStream cp = filterBank[i]; + if (cp == null || cp.totalbytes != bytesPerBandCurrent) { + if (cp != null) + cp.close(); + if (useLz4) + cp = new CompressorStreamLz4(null, buflen, bytesPerBandCurrent); + else + cp = + new CompressorStreamDeflater(null, buflen, bytesPerBandCurrent, DEFLATER_COMP_LEVEL, + Deflater.DEFAULT_STRATEGY); + filterBank[i] = cp; + } else { + cp.reset(); + } + cp.setStoreFirstByte(true, rowsPerBandCurrent); // TODO: only for adaptive? + } + } + + private int computeInitialRowsPerBand() { + // memory (only buffers) ~ (r+1+5) * bytesPerRow + int r = (int) ((hintMemoryKb * 1024.0) / (imgInfo.bytesPerRow + 1) - 5); + if (r < 1) + r = 1; + if (hintRowsPerBand > 0 && r > hintRowsPerBand) + r = hintRowsPerBand; + if (r > imgInfo.rows) + r = imgInfo.rows; + if (r > 2 && r > imgInfo.rows / 8) { // redistribute more evenly + int k = (imgInfo.rows + (r - 1)) / r; + r = (imgInfo.rows + k / 2) / k; + } + // PngHelperInternal.debug("rows :" + r + "/" + imgInfo.rows); + return r; + } + + private int getBestCompressor() { + double bestcr = Double.MAX_VALUE; + int bestb = -1; + for (int i = tryAdaptive ? 5 : 4; i >= 0; i--) { + CompressorStream fb = filterBank[i]; + double cr = fb.getCompressionRatio(); + if (cr <= bestcr) { // dirty trick, here the equality gains for row 0, so that SUB is prefered + // over PAETH, UP, AVE... + bestb = i; + bestcr = cr; + } + } + return bestb; + } + + @Override + protected void initParams() { + super.initParams(); + // if adaptative but too few rows or columns, use default + if (imgInfo.cols < 3 && !FilterType.isValidStandard(filterType)) + filterType = FilterType.FILTER_DEFAULT; + if (imgInfo.rows < 3 && !FilterType.isValidStandard(filterType)) + filterType = FilterType.FILTER_DEFAULT; + for (int i = 1; i <= 4; i++) { // element 0 is not allocated + if (filteredRows[i] == null || filteredRows[i].length < buflen) + filteredRows[i] = new byte[buflen]; + } + if (rowsPerBand == 0) + rowsPerBand = computeInitialRowsPerBand(); + } + + @Override + public void close() { + super.close(); + rows.clear(); + for (CompressorStream f : filterBank) { + f.close(); + } + } + + public void setHintMemoryKb(int hintMemoryKb) { + this.hintMemoryKb = + hintMemoryKb <= 0 ? HINT_MEMORY_DEFAULT_KB : (hintMemoryKb > 10000 ? 10000 : hintMemoryKb); + } + + public void setHintRowsPerBand(int hintRowsPerBand) { + this.hintRowsPerBand = hintRowsPerBand; + } + + public void setUseLz4(boolean lz4) { + this.useLz4 = lz4; + } + + /** for tuning memory or other parameters */ + public FiltersPerformance getFiltersPerf() { + return filtersPerf; + } + + public void setTryAdaptive(boolean tryAdaptive) { + this.tryAdaptive = tryAdaptive; + } + +} diff --git a/src/jar-specific/java/ar/com/hjg/pngj/pixels/package.html b/src/jar-specific/java/ar/com/hjg/pngj/pixels/package.html new file mode 100644 index 00000000..85bd6a02 --- /dev/null +++ b/src/jar-specific/java/ar/com/hjg/pngj/pixels/package.html @@ -0,0 +1,14 @@ + + +

+Mostly related with logic specific to reading/writing pixels. +

+

+Includes ImageLine related classes, and rows filtering +

+

+Some classes like ImageLineInt should belong here, but we keep them in the main package for backward compatibility. + +

+ + diff --git a/src/jar-specific/java/org/warp/picalculator/PlatformUtils.java b/src/jar-specific/java/org/warp/picalculator/PlatformUtils.java index 5d6ba709..842db53c 100644 --- a/src/jar-specific/java/org/warp/picalculator/PlatformUtils.java +++ b/src/jar-specific/java/org/warp/picalculator/PlatformUtils.java @@ -2,6 +2,7 @@ package org.warp.picalculator; import java.io.PrintWriter; import java.io.StringWriter; +import java.lang.ref.WeakReference; public final class PlatformUtils { public static final boolean isJavascript = false; @@ -29,4 +30,22 @@ public final class PlatformUtils { e.printStackTrace(pw); return sw.toString().toUpperCase().replace("\t", " ").replace("\r", "").split("\n"); } + + public static void loadPlatformRules() { + } + + public static void gc() { + Object obj = new Object(); + final WeakReference ref = new WeakReference<>(obj); + obj = null; + while (ref.get() != null) { + System.gc(); + } + } + + public static void shiftChanged(boolean alpha) { + } + + public static void alphaChanged(boolean alpha) { + } } diff --git a/src/jar-specific/java/org/warp/picalculator/deps/DEngine.java b/src/jar-specific/java/org/warp/picalculator/deps/DEngine.java index 4f70e5e7..7f45661d 100644 --- a/src/jar-specific/java/org/warp/picalculator/deps/DEngine.java +++ b/src/jar-specific/java/org/warp/picalculator/deps/DEngine.java @@ -24,4 +24,7 @@ public class DEngine { public static GraphicEngine newFBEngine() { return new FBEngine(); } + public static GraphicEngine newHtmlEngine() { + return null; + } } diff --git a/src/jar-specific/java/org/warp/picalculator/deps/DSemaphore.java b/src/jar-specific/java/org/warp/picalculator/deps/DSemaphore.java new file mode 100644 index 00000000..4fa97005 --- /dev/null +++ b/src/jar-specific/java/org/warp/picalculator/deps/DSemaphore.java @@ -0,0 +1,20 @@ +package org.warp.picalculator.deps; + +import java.util.ArrayList; +import java.util.Queue; +import java.util.concurrent.Semaphore; + +public class DSemaphore extends Semaphore { + + private static final long serialVersionUID = -2362314723921013871L; + + public DSemaphore(int arg0) { + super(arg0); + // TODO Auto-generated constructor stub + } + + public DSemaphore(int permits, boolean fair) { + super(permits, fair); + // TODO Auto-generated constructor stub + } +} diff --git a/src/jar-specific/java/org/warp/picalculator/deps/DStandardOpenOption.java b/src/jar-specific/java/org/warp/picalculator/deps/DStandardOpenOption.java new file mode 100644 index 00000000..31bf39c9 --- /dev/null +++ b/src/jar-specific/java/org/warp/picalculator/deps/DStandardOpenOption.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2007, 2009, Oracle and/or its affiliates. All rights reserved. + * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + +package org.warp.picalculator.deps; + +/** + * Defines the standard open options. + * + * @since 1.7 + */ + +public enum DStandardOpenOption { + /** + * Open for read access. + */ + READ, + + /** + * Open for write access. + */ + WRITE, + + /** + * If the file is opened for {@link #WRITE} access then bytes will be written + * to the end of the file rather than the beginning. + * + *

If the file is opened for write access by other programs, then it + * is file system specific if writing to the end of the file is atomic. + */ + APPEND, + + /** + * If the file already exists and it is opened for {@link #WRITE} + * access, then its length is truncated to 0. This option is ignored + * if the file is opened only for {@link #READ} access. + */ + TRUNCATE_EXISTING, + + /** + * Create a new file if it does not exist. + * This option is ignored if the {@link #CREATE_NEW} option is also set. + * The check for the existence of the file and the creation of the file + * if it does not exist is atomic with respect to other file system + * operations. + */ + CREATE, + + /** + * Create a new file, failing if the file already exists. + * The check for the existence of the file and the creation of the file + * if it does not exist is atomic with respect to other file system + * operations. + */ + CREATE_NEW, + + /** + * Delete on close. When this option is present then the implementation + * makes a best effort attempt to delete the file when closed + * by the appropriate {@code close} method. If the {@code close} method is + * not invoked then a best effort attempt is made to delete the + * file when the Java virtual machine terminates (either normally, as + * defined by the Java Language Specification, or where possible, abnormally). + * This option is primarily intended for use with work files that + * are used solely by a single instance of the Java virtual machine. This + * option is not recommended for use when opening files that are open + * concurrently by other entities. Many of the details as to when and how + * the file is deleted are implementation specific and therefore not + * specified. In particular, an implementation may be unable to guarantee + * that it deletes the expected file when replaced by an attacker while the + * file is open. Consequently, security sensitive applications should take + * care when using this option. + * + *

For security reasons, this option may imply the {@link + * LinkOption#NOFOLLOW_LINKS} option. In other words, if the option is present + * when opening an existing file that is a symbolic link then it may fail + * (by throwing {@link java.io.IOException}). + */ + DELETE_ON_CLOSE, + + /** + * Sparse file. When used with the {@link #CREATE_NEW} option then this + * option provides a hint that the new file will be sparse. The + * option is ignored when the file system does not support the creation of + * sparse files. + */ + SPARSE, + + /** + * Requires that every update to the file's content or metadata be written + * synchronously to the underlying storage device. + * + * @see Synchronized I/O file integrity + */ + SYNC, + + /** + * Requires that every update to the file's content be written + * synchronously to the underlying storage device. + * + * @see Synchronized I/O file integrity + */ + DSYNC; +} diff --git a/src/jar-specific/java/org/warp/picalculator/deps/DURLClassLoader.java b/src/jar-specific/java/org/warp/picalculator/deps/DURLClassLoader.java new file mode 100644 index 00000000..bd79271c --- /dev/null +++ b/src/jar-specific/java/org/warp/picalculator/deps/DURLClassLoader.java @@ -0,0 +1,24 @@ +package org.warp.picalculator.deps; + +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLStreamHandlerFactory; +import java.security.AccessControlContext; + +public class DURLClassLoader extends URLClassLoader { + + public DURLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { + super(urls, parent, factory); + // TODO Auto-generated constructor stub + } + + public DURLClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + // TODO Auto-generated constructor stub + } + + public DURLClassLoader(URL[] urls) { + super(urls); + // TODO Auto-generated constructor stub + } +} diff --git a/src/jar-specific/java/org/warp/picalculator/deps/StorageUtils.java b/src/jar-specific/java/org/warp/picalculator/deps/StorageUtils.java index 1369df76..d6454d11 100644 --- a/src/jar-specific/java/org/warp/picalculator/deps/StorageUtils.java +++ b/src/jar-specific/java/org/warp/picalculator/deps/StorageUtils.java @@ -1,16 +1,171 @@ package org.warp.picalculator.deps; +import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.warp.picalculator.Main; + +import com.jogamp.common.util.IOUtil; public class StorageUtils { public static final boolean exists(Path f) { return Files.exists(f); } + + public static final boolean exists(File f) { + return f.exists(); + } - public static Path get(String path) { - return Paths.get(path); + public static File get(String path) { + return Paths.get(path).toFile(); + } + + public static File get(String path, String... others) { + return Paths.get(path, others).toFile(); + } + + private static Map resourcesCache = new HashMap(); + + @Deprecated() + public static File getResource(String string) throws IOException, URISyntaxException { + final URL res = Main.instance.getClass().getResource(string); + final boolean isResource = res != null; + if (isResource) { + try { + final URI uri = res.toURI(); + if (res.getProtocol().equalsIgnoreCase("jar")) { + if (resourcesCache.containsKey(string)) { + File f; + if ((f = resourcesCache.get(string)).exists()) { + return f; + } else { + resourcesCache.remove(string); + } + } + try { + FileSystems.newFileSystem(uri, Collections.emptyMap()); + } catch (final FileSystemAlreadyExistsException e) { + FileSystems.getFileSystem(uri); + } + final Path myFolderPath = Paths.get(uri); + + InputStream is = Files.newInputStream(myFolderPath); + final File tempFile = File.createTempFile("picalcresource-", ""); + tempFile.deleteOnExit(); + try (FileOutputStream out = new FileOutputStream(tempFile)) + { + IOUtil.copyStream2Stream(is, out, (int) tempFile.length()); + } + resourcesCache.put(string, tempFile); + + return tempFile; + } else { + return Paths.get(uri).toFile(); + } + } catch (final java.lang.IllegalArgumentException e) { + throw e; + } + } else { + return Paths.get(string.substring(1)).toFile(); + } + } + + public static InputStream getResourceStream(String string) throws IOException, URISyntaxException { + final URL res = Main.instance.getClass().getResource(string); + final boolean isResource = res != null; + if (isResource) { + try { + final URI uri = res.toURI(); + if (res.getProtocol().equalsIgnoreCase("jar")) { + try { + FileSystems.newFileSystem(uri, Collections.emptyMap()); + } catch (final FileSystemAlreadyExistsException e) { + FileSystems.getFileSystem(uri); + } + final Path myFolderPath = Paths.get(uri); + return Files.newInputStream(myFolderPath); + } else { + return Files.newInputStream(Paths.get(uri)); + } + } catch (final java.lang.IllegalArgumentException e) { + throw e; + } + } else { + return Files.newInputStream(Paths.get(string.substring(1))); + } + } + + public static List readAllLines(File file) throws IOException { + return Files.readAllLines(file.toPath()); + } + + public static String read(InputStream input) throws IOException { + try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input))) { + return buffer.lines().collect(Collectors.joining("\n")); + } + } + + public static List walk(File dir) throws IOException { + List out = new ArrayList<>(); + try (Stream paths = Files.walk(dir.toPath())) { + paths.filter(Files::isRegularFile).forEach((Path p) -> { + out.add(p.toFile()); + }); + } + return out; + } + + public static File relativize(File rulesPath, File f) { + return rulesPath.toPath().relativize(f.toPath()).toFile(); + } + + public static File resolve(File file, String string) { + return file.toPath().resolve(string).toFile(); + } + + public static File getParent(File f) { + return f.toPath().getParent().toFile(); + } + + public static void createDirectories(File dir) throws IOException { + Files.createDirectories(dir.toPath()); + } + + public static void write(File f, byte[] bytes, DStandardOpenOption... options) throws IOException { + StandardOpenOption[] noptions = new StandardOpenOption[options.length]; + int i = 0; + for (DStandardOpenOption opt : options) { + noptions[i] = StandardOpenOption.values()[opt.ordinal()]; + i++; + } + Files.write(f.toPath(), bytes, noptions); + } + + public static List readAllLines(InputStream input) throws IOException { + try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input))) { + return buffer.lines().collect(Collectors.toList()); + } } } diff --git a/src/js-specific/java/ar/com/hjg/pngj/BufferedStreamFeeder.java b/src/js-specific/java/ar/com/hjg/pngj/BufferedStreamFeeder.java new file mode 100644 index 00000000..ef93efc9 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/BufferedStreamFeeder.java @@ -0,0 +1,199 @@ +package ar.com.hjg.pngj; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Reads bytes from an input stream, and feeds a IBytesConsumer. + */ +public class BufferedStreamFeeder { + + private InputStream stream; + private byte[] buf; + private int pendinglen; // bytes read and stored in buf that have not yet still been fed to + // IBytesConsumer + private int offset; + private boolean eof = false; + private boolean closeStream = true; + private boolean failIfNoFeed = false; + + private static final int DEFAULTSIZE = 8192; + + /** By default, the stream will be closed on close() */ + public BufferedStreamFeeder(InputStream is) { + this(is, DEFAULTSIZE); + } + + public BufferedStreamFeeder(InputStream is, int bufsize) { + this.stream = is; + buf = new byte[bufsize < 1 ? DEFAULTSIZE : bufsize]; + } + + /** + * Returns inputstream + * + * @return Input Stream from which bytes are read + */ + public InputStream getStream() { + return stream; + } + + /** + * Feeds bytes to the consumer
+ * Returns bytes actually consumed
+ * This should return 0 only if the stream is EOF or the consumer is done + */ + public int feed(IBytesConsumer consumer) { + return feed(consumer, Integer.MAX_VALUE); + } + + /** + * Feeds the consumer (with at most maxbytes)
+ * Returns 0 only if the stream is EOF (or maxbytes=0). Returns negative is the consumer is done.
+ * It can return less than maxbytes (that doesn't mean that the consumer or the input stream is done) + */ + public int feed(IBytesConsumer consumer, int maxbytes) { + if (pendinglen == 0) + refillBuffer(); + int tofeed = maxbytes >= 0 && maxbytes < pendinglen ? maxbytes : pendinglen; + int n = 0; + if (tofeed > 0) { + n = consumer.consume(buf, offset, tofeed); + if (n > 0) { + offset += n; + pendinglen -= n; + } + } + if (n < 1 && failIfNoFeed) + throw new PngjInputException("Failed to feed bytes (premature ending?)"); + return n; + } + + + /** + * Feeds as much bytes as it can to the consumer, in a loop.
+ * Returns bytes actually consumed
+ * This will stop when either the input stream is eof, or when the consumer refuses to eat more bytes. The caller can + * distinguish both cases by calling {@link #hasMoreToFeed()} + */ + public long feedAll(IBytesConsumer consumer) { + long n = 0; + while (hasMoreToFeed()) { + int n1 = feed(consumer); + if (n1 < 1) + break; + n += n1; + } + return n; + } + + + /** + * Feeds exactly nbytes, retrying if necessary + * + * @param consumer Consumer + * @param nbytes Number of bytes + * @return true if success, false otherwise (EOF on stream, or consumer is done) + */ + public boolean feedFixed(IBytesConsumer consumer, int nbytes) { + int remain = nbytes; + while (remain > 0) { + int n = feed(consumer, remain); + if (n < 1) + return false; + remain -= n; + } + return true; + } + + /** + * If there are not pending bytes to be consumed tries to fill the buffer with bytes from the stream. + */ + protected void refillBuffer() { + if (pendinglen > 0 || eof) + return; // only if not pending data + try { + // try to read + offset = 0; + pendinglen = stream.read(buf); + if (pendinglen < 0) { + close(); + return; + } else + return; + } catch (IOException e) { + throw new PngjInputException(e); + } + } + + /** + * Returuns true if we have more data to fed the consumer. This internally tries to grabs more bytes from the stream + * if necessary + */ + public boolean hasMoreToFeed() { + if (eof) + return pendinglen > 0; + else + refillBuffer(); + return pendinglen > 0; + } + + /** + * @param closeStream If true, the underlying stream will be closed on when close() is called + */ + public void setCloseStream(boolean closeStream) { + this.closeStream = closeStream; + } + + /** + * Closes this object. + * + * Sets EOF=true, and closes the stream if closeStream is true + * + * This can be called internally, or from outside. + * + * Idempotent, secure, never throws exception. + **/ + public void close() { + eof = true; + buf = null; + pendinglen = 0; + offset = 0; + if (stream != null && closeStream) { + try { + stream.close(); + } catch (Exception e) { + // PngHelperInternal.LOGGER.log(Level.WARNING, "Exception closing stream", e); + } + } + stream = null; + } + + /** + * Sets a new underlying inputstream. This allows to reuse this object. The old underlying is not closed and the state + * is not reset (you should call close() previously if you want that) + * + * @param is + */ + public void setInputStream(InputStream is) { // to reuse this object + this.stream = is; + eof = false; + } + + /** + * @return EOF on stream, or close() was called + */ + public boolean isEof() { + return eof; + } + + /** + * If this flag is set (default: false), any call to feed() that returns zero (no byte feed) will throw an exception. + * This is useful to be sure of avoid infinite loops in some scenarios. + * + * @param failIfNoFeed + */ + public void setFailIfNoFeed(boolean failIfNoFeed) { + this.failIfNoFeed = failIfNoFeed; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ChunkReader.java b/src/js-specific/java/ar/com/hjg/pngj/ChunkReader.java new file mode 100644 index 00000000..4c045853 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ChunkReader.java @@ -0,0 +1,216 @@ +package ar.com.hjg.pngj; + +import ar.com.hjg.pngj.chunks.ChunkRaw; + +/** + * Parses a PNG chunk, consuming bytes in one mode: {@link ChunkReaderMode#BUFFER}, {@link ChunkReaderMode#PROCESS}, + * {@link ChunkReaderMode#SKIP}. + *

+ * It calls {@link #chunkDone()} when done. Also calls {@link #processData(byte[], int, int)} if PROCESS + * mode. Apart from thas, it's totally agnostic (it doesn't know about IDAT chunks, or PNG general structure) + *

+ * The object wraps a ChunkRaw instance (content filled only if BUFFER mode); it should be short lived (one instance + * created for each chunk, and discarded after reading), but the wrapped chunkRaw can be (usually is) long lived. + */ +public abstract class ChunkReader { + + /** + * see {@link ChunkReaderMode} + */ + public final ChunkReaderMode mode; + private final ChunkRaw chunkRaw; + + private boolean crcCheck; // by default, this is false for SKIP, true elsewhere + + /** + * How many bytes have been read for this chunk, data only + */ + protected int read = 0; + private int crcn = 0; // how many bytes have been read from crc + + /** + * Modes of ChunkReader chunk processing. + */ + public enum ChunkReaderMode { + /** + * Stores full chunk data in buffer + */ + BUFFER, + /** + * Does not store content, processes on the fly, calling processData() for each partial read + */ + PROCESS, + /** + * Does not store nor process - implies crcCheck=false (by default). + */ + SKIP; + } + + /** + * The constructor creates also a chunkRaw, preallocated if mode = ChunkReaderMode.BUFFER + * + * @param clen + * @param id + * @param offsetInPng Informational, is stored in chunkRaw + * @param mode + */ + public ChunkReader(int clen, String id, long offsetInPng, ChunkReaderMode mode) { + if (mode == null || id.length() != 4 || clen < 0) + throw new PngjExceptionInternal("Bad chunk paramenters: " + mode); + this.mode = mode; + chunkRaw = new ChunkRaw(clen, id, mode == ChunkReaderMode.BUFFER); + chunkRaw.setOffset(offsetInPng); + this.crcCheck = mode == ChunkReaderMode.SKIP ? false : true; // can be changed with setter + } + + /** + * Returns raw chunk (data can be empty or not, depending on ChunkReaderMode) + * + * @return Raw chunk - never null + */ + public ChunkRaw getChunkRaw() { + return chunkRaw; + } + + /** + * Consumes data for the chunk (data and CRC). This never consumes more bytes than for this chunk. + * + * In HOT_PROCESS can call processData() (not more than once) + * + * If this ends the chunk (included CRC) it checks CRC (if checking) and calls chunkDone() + * + * @param buf + * @param off + * @param len + * @return How many bytes have been consumed + */ + public final int feedBytes(byte[] buf, int off, int len) { + if (len == 0) + return 0; + if (len < 0) + throw new PngjException("negative length??"); + if (read == 0 && crcn == 0 && crcCheck) + chunkRaw.updateCrc(chunkRaw.idbytes, 0, 4); // initializes crc calculation with the Chunk ID + int bytesForData = chunkRaw.len - read; // bytesForData : bytes to be actually read from chunk data + if (bytesForData > len) + bytesForData = len; + // we want to call processData even for empty chunks (IEND:bytesForData=0) at least once + if (bytesForData > 0 || crcn == 0) { + // in buffer mode we compute the CRC at the end + if (crcCheck && mode != ChunkReaderMode.BUFFER && bytesForData > 0) + chunkRaw.updateCrc(buf, off, bytesForData); + + if (mode == ChunkReaderMode.BUFFER) { + // just copy the contents to the internal buffer + if (chunkRaw.data != buf && bytesForData > 0) { + // if the buffer passed if the same as this one, we don't copy the caller should know what he's doing + System.arraycopy(buf, off, chunkRaw.data, read, bytesForData); + } + } else if (mode == ChunkReaderMode.PROCESS) { + processData(read, buf, off, bytesForData); + } else { + // mode == ChunkReaderMode.SKIP; nothing to do + } + read += bytesForData; + off += bytesForData; + len -= bytesForData; + } + int crcRead = 0; + if (read == chunkRaw.len) { // data done - read crc? + crcRead = 4 - crcn; + if (crcRead > len) + crcRead = len; + if (crcRead > 0) { + if (buf != chunkRaw.crcval) + System.arraycopy(buf, off, chunkRaw.crcval, crcn, crcRead); + crcn += crcRead; + if (crcn == 4) { + if (crcCheck) { + if (mode == ChunkReaderMode.BUFFER) { // in buffer mode we compute the CRC on one single call + chunkRaw.updateCrc(chunkRaw.data, 0, chunkRaw.len); + } + chunkRaw.checkCrc(); + } + chunkDone(); + } + } + } + return bytesForData + crcRead; + } + + /** + * Chunks has been read + * + * @return true if we have read all chunk, including trailing CRC + */ + public final boolean isDone() { + return crcn == 4; // has read all 4 bytes from the crc + } + + /** + * Determines if CRC should be checked. This should be called before starting reading. + * + * @param crcCheck + */ + public void setCrcCheck(boolean crcCheck) { + if (read != 0 && crcCheck && !this.crcCheck) + throw new PngjException("too late!"); + this.crcCheck = crcCheck; + } + + /** + * This method will only be called in PROCESS mode, probably several times, each time with a new fragment of data + * inside the chunk. For chunks with zero-length data, this will still be called once. + * + * It's guaranteed that the data corresponds exclusively to this chunk data (no crc, no data from no other chunks, ) + * + * @param offsetInchunk data bytes that had already been read/processed for this chunk + * @param buf + * @param off + * @param len + */ + protected abstract void processData(int offsetInchunk, byte[] buf, int off, int len); + + /** + * This method will be called (in all modes) when the full chunk -including crc- has been read + */ + protected abstract void chunkDone(); + + public boolean isFromDeflatedSet() { + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((chunkRaw == null) ? 0 : chunkRaw.hashCode()); + return result; + } + + /** + * Equality (and hash) is basically delegated to the ChunkRaw + */ + @Override + public boolean equals(Object obj) { // delegates to chunkraw + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ChunkReader other = (ChunkReader) obj; + if (chunkRaw == null) { + if (other.chunkRaw != null) + return false; + } else if (!chunkRaw.equals(other.chunkRaw)) + return false; + return true; + } + + @Override + public String toString() { + return chunkRaw.toString(); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqBuffering.java b/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqBuffering.java new file mode 100644 index 00000000..39e19dac --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqBuffering.java @@ -0,0 +1,30 @@ +package ar.com.hjg.pngj; + +/** + * This loads the png as a plain sequence of chunks, buffering all + * + * Useful to do things like insert or delete a ancilllary chunk. This does not distinguish IDAT from others + **/ +public class ChunkSeqBuffering extends ChunkSeqReader { + protected boolean checkCrc = true; + + public ChunkSeqBuffering() { + super(); + } + + @Override + protected boolean isIdatKind(String id) { + return false; + } + + @Override + protected boolean shouldCheckCrc(int len, String id) { + return checkCrc; + } + + public void setCheckCrc(boolean checkCrc) { + this.checkCrc = checkCrc; + } + + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqReader.java b/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqReader.java new file mode 100644 index 00000000..82b661f1 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqReader.java @@ -0,0 +1,396 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Arrays; + +import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode; +import ar.com.hjg.pngj.chunks.ChunkHelper; + +/** + * Consumes a stream of bytes that consist of a series of PNG-like chunks. + *

+ * This has little intelligence, it's quite low-level and general (it could even be used for a MNG stream, for example). + * It supports signature recognition and idat deflate + */ +public class ChunkSeqReader implements IBytesConsumer { + + protected static final int SIGNATURE_LEN = 8; + protected final boolean withSignature; + + private byte[] buf0 = new byte[8]; // for signature or chunk starts + private int buf0len = 0; + + private boolean signatureDone = false; + private boolean done = false; // ended, normally or not + + private int chunkCount = 0; + + private long bytesCount = 0; + + private DeflatedChunksSet curReaderDeflatedSet; // one instance is created for each + // "idat-like set". Normally one. + + private ChunkReader curChunkReader; + + private long idatBytes; // this is only for the IDAT (not mrerely "idat-like") + + /** + * Creates a ChunkSeqReader (with signature) + */ + public ChunkSeqReader() { + this(true); + } + + /** + * @param withSignature If true, the stream is assumed be prepended by 8 bit signature + */ + public ChunkSeqReader(boolean withSignature) { + this.withSignature = withSignature; + signatureDone = !withSignature; + } + + /** + * Consumes (in general, partially) a number of bytes. A single call never involves more than one chunk. + * + * When the signature is read, it calls checkSignature() + * + * When the start of a chunk is detected, it calls {@link #startNewChunk(int, String, long)} + * + * When data from a chunk is being read, it delegates to {@link ChunkReader#feedBytes(byte[], int, int)} + * + * The caller might want to call this method more than once in succesion + * + * This should rarely be overriden + * + * @param buffer + * @param offset Offset in buffer + * @param len Valid bytes that can be consumed + * @return processed bytes, in the 1-len range. -1 if done. Only returns 0 if len=0. + **/ + public int consume(byte[] buffer, int offset, int len) { + if (done) + return -1; + if (len == 0) + return 0; // nothing to do + if (len < 0) + throw new PngjInputException("Bad len: " + len); + int processed = 0; + if (signatureDone) { + if (curChunkReader == null || curChunkReader.isDone()) { // new chunk: read first 8 bytes + int read0 = 8 - buf0len; + if (read0 > len) + read0 = len; + System.arraycopy(buffer, offset, buf0, buf0len, read0); + buf0len += read0; + processed += read0; + bytesCount += read0; + // len -= read0; + // offset += read0; + if (buf0len == 8) { // end reading chunk length and id + chunkCount++; + int clen = PngHelperInternal.readInt4fromBytes(buf0, 0); + String cid = ChunkHelper.toString(buf0, 4, 4); + startNewChunk(clen, cid, bytesCount - 8); + buf0len = 0; + } + } else { // reading chunk, delegates to curChunkReader + int read1 = curChunkReader.feedBytes(buffer, offset, len); + processed += read1; + bytesCount += read1; + } + } else { // reading signature + int read = SIGNATURE_LEN - buf0len; + if (read > len) + read = len; + System.arraycopy(buffer, offset, buf0, buf0len, read); + buf0len += read; + if (buf0len == SIGNATURE_LEN) { + checkSignature(buf0); + buf0len = 0; + signatureDone = true; + } + processed += read; + bytesCount += read; + } + return processed; + } + + /** + * Trys to feeds exactly len bytes, calling {@link #consume(byte[], int, int)} retrying if necessary. + * + * This should only be used in callback mode + * + * @return true if succceded + */ + public boolean feedAll(byte[] buf, int off, int len) { + while (len > 0) { + int n = consume(buf, off, len); + if (n < 1) + return false; + len -= n; + off += n; + } + return true; + } + + /** + * Called for all chunks when a chunk start has been read (id and length), before the chunk data itself is read. It + * creates a new ChunkReader (field accesible via {@link #getCurChunkReader()}) in the corresponding mode, and + * eventually a curReaderDeflatedSet.(field accesible via {@link #getCurReaderDeflatedSet()}) + * + * To decide the mode and options, it calls {@link #shouldCheckCrc(int, String)}, + * {@link #shouldSkipContent(int, String)}, {@link #isIdatKind(String)}. Those methods should be overriden in + * preference to this; if overriden, this should be called first. + * + * The respective {@link ChunkReader#chunkDone()} method is directed to this {@link #postProcessChunk(ChunkReader)}. + * + * Instead of overriding this, see also {@link #createChunkReaderForNewChunk(String, int, long, boolean)} + */ + protected void startNewChunk(int len, String id, long offset) { + if (id.equals(ChunkHelper.IDAT)) + idatBytes += len; + boolean checkCrc = shouldCheckCrc(len, id); + boolean skip = shouldSkipContent(len, id); + boolean isIdatType = isIdatKind(id); + // PngHelperInternal.debug("start new chunk id=" + id + " off=" + offset + " skip=" + skip + " idat=" + + // isIdatType); + // first see if we should terminate an active curReaderDeflatedSet + boolean forCurrentIdatSet = false; + if (curReaderDeflatedSet != null) + forCurrentIdatSet = curReaderDeflatedSet.ackNextChunkId(id); + if (isIdatType && !skip) { // IDAT non skipped: create a DeflatedChunkReader owned by a idatSet + if (!forCurrentIdatSet) { + if (curReaderDeflatedSet != null && !curReaderDeflatedSet.isDone()) + throw new PngjInputException("new IDAT-like chunk when previous was not done"); + curReaderDeflatedSet = createIdatSet(id); + } + curChunkReader = new DeflatedChunkReader(len, id, checkCrc, offset, curReaderDeflatedSet) { + @Override + protected void chunkDone() { + super.chunkDone(); + postProcessChunk(this); + } + }; + + } else { // for non-idat chunks (or skipped idat like) + curChunkReader = createChunkReaderForNewChunk(id, len, offset, skip); + if (!checkCrc) + curChunkReader.setCrcCheck(false); + } + } + + /** + * This will be called for all chunks (even skipped), except for IDAT-like non-skiped chunks + * + * The default behaviour is to create a ChunkReader in BUFFER mode (or SKIP if skip==true) that calls + * {@link #postProcessChunk(ChunkReader)} (always) when done. + * + * @param id Chunk id + * @param len Chunk length + * @param offset offset inside PNG stream , merely informative + * @param skip flag: is true, the content will not be buffered (nor processed) + * @return a newly created ChunkReader that will create the ChunkRaw and then discarded + */ + protected ChunkReader createChunkReaderForNewChunk(String id, int len, long offset, boolean skip) { + return new ChunkReader(len, id, offset, skip ? ChunkReaderMode.SKIP : ChunkReaderMode.BUFFER) { + @Override + protected void chunkDone() { + postProcessChunk(this); + } + + @Override + protected void processData(int offsetinChhunk, byte[] buf, int off, int len) { + throw new PngjExceptionInternal("should never happen"); + } + }; + } + + /** + * This is called after a chunk is read, in all modes + * + * This implementation only chenks the id of the first chunk, and process the IEND chunk (sets done=true) + ** + * Further processing should be overriden (call this first!) + **/ + protected void postProcessChunk(ChunkReader chunkR) { // called after chunk is read + if (chunkCount == 1) { + String cid = firstChunkId(); + if (cid != null && !cid.equals(chunkR.getChunkRaw().id)) + throw new PngjInputException("Bad first chunk: " + chunkR.getChunkRaw().id + " expected: " + + firstChunkId()); + } + if (chunkR.getChunkRaw().id.equals(endChunkId())) + done = true; + } + + /** + * DeflatedChunksSet factory. This implementation is quite dummy, it usually should be overriden. + */ + protected DeflatedChunksSet createIdatSet(String id) { + return new DeflatedChunksSet(id, 1024, 1024); // sizes: arbitrary This should normally be + // overriden + } + + /** + * Decides if this Chunk is of "IDAT" kind (in concrete: if it is, and if it's not to be skiped, a DeflatedChunksSet + * will be created to deflate it and process+ the deflated data) + * + * This implementation always returns always false + * + * @param id + */ + protected boolean isIdatKind(String id) { + return false; + } + + /** + * Chunks can be skipped depending on id and/or length. Skipped chunks are still processed, but their data will be + * null, and CRC will never checked + * + * @param len + * @param id + */ + protected boolean shouldSkipContent(int len, String id) { + return false; + } + + protected boolean shouldCheckCrc(int len, String id) { + return true; + } + + /** + * Throws PngjInputException if bad signature + * + * @param buf Signature. Should be of length 8 + */ + protected void checkSignature(byte[] buf) { + if (!Arrays.equals(buf, PngHelperInternal.getPngIdSignature())) + throw new PngjInputException("Bad PNG signature"); + } + + /** + * If false, we are still reading the signature + * + * @return true if signature has been read (or if we don't have signature) + */ + public boolean isSignatureDone() { + return signatureDone; + } + + /** + * If true, we either have processe the IEND chunk, or close() has been called, or a fatal error has happened + */ + public boolean isDone() { + return done; + } + + /** + * total of bytes read (buffered or not) + */ + public long getBytesCount() { + return bytesCount; + } + + /** + * @return Chunks already read, including partial reading (currently reading) + */ + public int getChunkCount() { + return chunkCount; + } + + /** + * Currently reading chunk, or just ended reading + * + * @return null only if still reading signature + */ + public ChunkReader getCurChunkReader() { + return curChunkReader; + } + + /** + * The latest deflated set (typically IDAT chunks) reader. Notice that there could be several idat sets (eg for APNG) + */ + public DeflatedChunksSet getCurReaderDeflatedSet() { + return curReaderDeflatedSet; + } + + /** + * Closes this object and release resources. For normal termination or abort. Secure and idempotent. + */ + public void close() { // forced closing + if (curReaderDeflatedSet != null) + curReaderDeflatedSet.close(); + done = true; + } + + /** + * Returns true if we are not in middle of a chunk: we have just ended reading past chunk , or we are at the start, or + * end of signature, or we are done + */ + public boolean isAtChunkBoundary() { + return bytesCount == 0 || bytesCount == 8 || done || curChunkReader == null + || curChunkReader.isDone(); + } + + /** + * Which should be the id of the first chunk + * + * @return null if you don't want to check it + */ + protected String firstChunkId() { + return "IHDR"; + } + + /** + * Helper method, reports amount of bytes inside IDAT chunks. + * + * @return Bytes in IDAT chunks + */ + public long getIdatBytes() { + return idatBytes; + } + + /** + * Which should be the id of the last chunk + * + * @return "IEND" + */ + protected String endChunkId() { + return "IEND"; + } + + /** + * Reads all content from a file. Helper method, only for callback mode + */ + public void feedFromFile(File f) { + try { + feedFromInputStream(new FileInputStream(f), true); + } catch (FileNotFoundException e) { + throw new PngjInputException(e.getMessage()); + } + } + + /** + * Reads all content from an input stream. Helper method, only for callback mode + * + * @param is + * @param closeStream Closes the input stream when done (or if error) + */ + public void feedFromInputStream(InputStream is, boolean closeStream) { + BufferedStreamFeeder sf = new BufferedStreamFeeder(is); + sf.setCloseStream(closeStream); + try { + sf.feedAll(this); + } finally { + close(); + sf.close(); + } + } + + public void feedFromInputStream(InputStream is) { + feedFromInputStream(is, true); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqReaderPng.java b/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqReaderPng.java new file mode 100644 index 00000000..44779c44 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqReaderPng.java @@ -0,0 +1,313 @@ +package ar.com.hjg.pngj; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode; +import ar.com.hjg.pngj.chunks.ChunkFactory; +import ar.com.hjg.pngj.chunks.ChunkHelper; +import ar.com.hjg.pngj.chunks.ChunkLoadBehaviour; +import ar.com.hjg.pngj.chunks.ChunksList; +import ar.com.hjg.pngj.chunks.PngChunk; +import ar.com.hjg.pngj.chunks.PngChunkIDAT; +import ar.com.hjg.pngj.chunks.PngChunkIEND; +import ar.com.hjg.pngj.chunks.PngChunkIHDR; +import ar.com.hjg.pngj.chunks.PngChunkPLTE; + +/** + * Adds to ChunkSeqReader the storing of PngChunk, with a PngFactory, and imageInfo + deinterlacer. + *

+ * Most usual PNG reading should use this class, or a {@link PngReader}, which is a thin wrapper over this. + */ +public class ChunkSeqReaderPng extends ChunkSeqReader { + + protected ImageInfo imageInfo; // initialized at parsing the IHDR + protected ImageInfo curImageInfo; // can vary, for apng + protected Deinterlacer deinterlacer; + protected int currentChunkGroup = -1; + + /** + * All chunks, but some of them can have the buffer empty (IDAT and skipped) + */ + protected ChunksList chunksList = null; + protected final boolean callbackMode; + private long bytesAncChunksLoaded = 0; // bytes loaded from buffered chunks non-critical chunks (data only) + + private boolean checkCrc = true; + + // --- parameters to be set prior to reading --- + private boolean includeNonBufferedChunks = false; + + private Set chunksToSkip = new HashSet(); + private long maxTotalBytesRead = 0; + private long skipChunkMaxSize = 0; + private long maxBytesMetadata = 0; + private IChunkFactory chunkFactory; + private ChunkLoadBehaviour chunkLoadBehaviour = ChunkLoadBehaviour.LOAD_CHUNK_ALWAYS; + + public ChunkSeqReaderPng(boolean callbackMode) { + super(); + this.callbackMode = callbackMode; + chunkFactory = new ChunkFactory(); // default factory + } + + private void updateAndCheckChunkGroup(String id) { + if (id.equals(PngChunkIHDR.ID)) { // IDHR + if (currentChunkGroup < 0) + currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR; + else + throw new PngjInputException("unexpected chunk " + id); + } else if (id.equals(PngChunkPLTE.ID)) { // PLTE + if ((currentChunkGroup == ChunksList.CHUNK_GROUP_0_IDHR || currentChunkGroup == ChunksList.CHUNK_GROUP_1_AFTERIDHR)) + currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE; + else + throw new PngjInputException("unexpected chunk " + id); + } else if (id.equals(PngChunkIDAT.ID)) { // IDAT (no necessarily the first) + if ((currentChunkGroup >= ChunksList.CHUNK_GROUP_0_IDHR && currentChunkGroup <= ChunksList.CHUNK_GROUP_4_IDAT)) + currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT; + else + throw new PngjInputException("unexpected chunk " + id); + } else if (id.equals(PngChunkIEND.ID)) { // END + if ((currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT)) + currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; + else + throw new PngjInputException("unexpected chunk " + id); + } else { // ancillary + if (currentChunkGroup <= ChunksList.CHUNK_GROUP_1_AFTERIDHR) + currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + else if (currentChunkGroup <= ChunksList.CHUNK_GROUP_3_AFTERPLTE) + currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE; + else + currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; + } + } + + @Override + public boolean shouldSkipContent(int len, String id) { + if (super.shouldSkipContent(len, id)) + return true; + if (ChunkHelper.isCritical(id)) + return false;// critical chunks are never skipped + if (maxTotalBytesRead > 0 && len + getBytesCount() > maxTotalBytesRead) + throw new PngjInputException("Maximum total bytes to read exceeeded: " + maxTotalBytesRead + + " offset:" + getBytesCount() + " len=" + len); + if (chunksToSkip.contains(id)) + return true; // specific skip + if (skipChunkMaxSize > 0 && len > skipChunkMaxSize) + return true; // too big chunk + if (maxBytesMetadata > 0 && len > maxBytesMetadata - bytesAncChunksLoaded) + return true; // too much ancillary chunks loaded + switch (chunkLoadBehaviour) { + case LOAD_CHUNK_IF_SAFE: + if (!ChunkHelper.isSafeToCopy(id)) + return true; + break; + case LOAD_CHUNK_NEVER: + return true; + default: + break; + } + return false; + } + + public long getBytesChunksLoaded() { + return bytesAncChunksLoaded; + } + + public int getCurrentChunkGroup() { + return currentChunkGroup; + } + + public void setChunksToSkip(String... chunksToSkip) { + this.chunksToSkip.clear(); + for (String c : chunksToSkip) + this.chunksToSkip.add(c); + } + + public void addChunkToSkip(String chunkToSkip) { + this.chunksToSkip.add(chunkToSkip); + } + + public void dontSkipChunk(String chunkToSkip) { + this.chunksToSkip.remove(chunkToSkip); + } + + public boolean firstChunksNotYetRead() { + return getCurrentChunkGroup() < ChunksList.CHUNK_GROUP_4_IDAT; + } + + @Override + protected void postProcessChunk(ChunkReader chunkR) { + super.postProcessChunk(chunkR); + if (chunkR.getChunkRaw().id.equals(PngChunkIHDR.ID)) { + PngChunkIHDR ch = new PngChunkIHDR(null); + ch.parseFromRaw(chunkR.getChunkRaw()); + imageInfo = ch.createImageInfo(); + curImageInfo = imageInfo; + if (ch.isInterlaced()) + deinterlacer = new Deinterlacer(curImageInfo); + chunksList = new ChunksList(imageInfo); + } + if (chunkR.mode == ChunkReaderMode.BUFFER && countChunkTypeAsAncillary(chunkR.getChunkRaw().id)) { + bytesAncChunksLoaded += chunkR.getChunkRaw().len; + } + if (chunkR.mode == ChunkReaderMode.BUFFER || includeNonBufferedChunks) { + PngChunk chunk = chunkFactory.createChunk(chunkR.getChunkRaw(), getImageInfo()); + chunksList.appendReadChunk(chunk, currentChunkGroup); + } + if (isDone()) { + processEndPng(); + } + } + + protected boolean countChunkTypeAsAncillary(String id) { + return !ChunkHelper.isCritical(id); + } + + @Override + protected DeflatedChunksSet createIdatSet(String id) { + IdatSet ids = new IdatSet(id, getCurImgInfo(), deinterlacer); + ids.setCallbackMode(callbackMode); + return ids; + } + + public IdatSet getIdatSet() { + DeflatedChunksSet c = getCurReaderDeflatedSet(); + return c instanceof IdatSet ? (IdatSet) c : null; + } + + @Override + protected boolean isIdatKind(String id) { + return id.equals(PngChunkIDAT.ID); + } + + @Override + public int consume(byte[] buf, int off, int len) { + return super.consume(buf, off, len); + } + + /** + * sets a custom chunk factory. This is typically called with a custom class extends ChunkFactory, to adds custom + * chunks to the default well-know ones + * + * @param chunkFactory + */ + public void setChunkFactory(IChunkFactory chunkFactory) { + this.chunkFactory = chunkFactory; + } + + /** + * Things to be done after IEND processing. This is not called if prematurely closed. + */ + protected void processEndPng() { + // nothing to do + } + + public ImageInfo getImageInfo() { + return imageInfo; + } + + public boolean isInterlaced() { + return deinterlacer != null; + } + + public Deinterlacer getDeinterlacer() { + return deinterlacer; + } + + @Override + protected void startNewChunk(int len, String id, long offset) { + updateAndCheckChunkGroup(id); + super.startNewChunk(len, id, offset); + } + + @Override + public void close() { + if (currentChunkGroup != ChunksList.CHUNK_GROUP_6_END)// this could only happen if forced close + currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; + super.close(); + } + + public List getChunks() { + return chunksList.getChunks(); + } + + public void setMaxTotalBytesRead(long maxTotalBytesRead) { + this.maxTotalBytesRead = maxTotalBytesRead; + } + + public long getSkipChunkMaxSize() { + return skipChunkMaxSize; + } + + public void setSkipChunkMaxSize(long skipChunkMaxSize) { + this.skipChunkMaxSize = skipChunkMaxSize; + } + + public long getMaxBytesMetadata() { + return maxBytesMetadata; + } + + public void setMaxBytesMetadata(long maxBytesMetadata) { + this.maxBytesMetadata = maxBytesMetadata; + } + + public long getMaxTotalBytesRead() { + return maxTotalBytesRead; + } + + @Override + protected boolean shouldCheckCrc(int len, String id) { + return checkCrc; + } + + public boolean isCheckCrc() { + return checkCrc; + } + + public void setCheckCrc(boolean checkCrc) { + this.checkCrc = checkCrc; + } + + public boolean isCallbackMode() { + return callbackMode; + } + + public Set getChunksToSkip() { + return chunksToSkip; + } + + public void setChunkLoadBehaviour(ChunkLoadBehaviour chunkLoadBehaviour) { + this.chunkLoadBehaviour = chunkLoadBehaviour; + } + + public ImageInfo getCurImgInfo() { + return curImageInfo; + } + + public void updateCurImgInfo(ImageInfo iminfo) { + if (!iminfo.equals(curImageInfo)) { + curImageInfo = iminfo; + } + if (deinterlacer != null) + deinterlacer = new Deinterlacer(curImageInfo); // we could reset it, but... + } + + /** + * If true, the chunks with no data (because skipped or because processed like IDAT-type) are still stored in the + * PngChunks list, which might be more informative. + * + * Setting this to false saves a few bytes + * + * Default: false + * + * @param includeNonBufferedChunks + */ + public void setIncludeNonBufferedChunks(boolean includeNonBufferedChunks) { + this.includeNonBufferedChunks = includeNonBufferedChunks; + } + + + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqSkipping.java b/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqSkipping.java new file mode 100644 index 00000000..5448c118 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ChunkSeqSkipping.java @@ -0,0 +1,70 @@ +package ar.com.hjg.pngj; + +import java.util.ArrayList; +import java.util.List; + +import ar.com.hjg.pngj.ChunkReader.ChunkReaderMode; +import ar.com.hjg.pngj.chunks.ChunkRaw; + +/** + * This simple reader skips all chunks contents and stores the chunkRaw in a list. Useful to read chunks structure. + * + * Optionally the contents might be processed. This doesn't distinguish IDAT chunks + */ +public class ChunkSeqSkipping extends ChunkSeqReader { + + private List chunks = new ArrayList(); + private boolean skip = true; + + /** + * @param skipAll if true, contents will be truly skipped, and CRC will not be computed + */ + public ChunkSeqSkipping(boolean skipAll) { + super(true); + skip = skipAll; + } + + public ChunkSeqSkipping() { + this(true); + } + + protected ChunkReader createChunkReaderForNewChunk(String id, int len, long offset, boolean skip) { + return new ChunkReader(len, id, offset, skip ? ChunkReaderMode.SKIP : ChunkReaderMode.PROCESS) { + @Override + protected void chunkDone() { + postProcessChunk(this); + } + + @Override + protected void processData(int offsetinChhunk, byte[] buf, int off, int len) { + processChunkContent(getChunkRaw(), offsetinChhunk, buf, off, len); + } + }; + } + + protected void processChunkContent(ChunkRaw chunkRaw, int offsetinChhunk, byte[] buf, int off, + int len) { + // does nothing + } + + @Override + protected void postProcessChunk(ChunkReader chunkR) { + super.postProcessChunk(chunkR); + chunks.add(chunkR.getChunkRaw()); + } + + @Override + protected boolean shouldSkipContent(int len, String id) { + return skip; + } + + @Override + protected boolean isIdatKind(String id) { + return false; + } + + public List getChunks() { + return chunks; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/DeflatedChunkReader.java b/src/js-specific/java/ar/com/hjg/pngj/DeflatedChunkReader.java new file mode 100644 index 00000000..6abac098 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/DeflatedChunkReader.java @@ -0,0 +1,83 @@ +package ar.com.hjg.pngj; + +import ar.com.hjg.pngj.chunks.PngChunkFDAT; + +/** + * + * Specialization of ChunkReader, for IDAT-like chunks. These chunks are part of a set of similar chunks (contiguos + * normally, not necessariyl) which conforms a zlib stream + */ +public class DeflatedChunkReader extends ChunkReader { + + protected final DeflatedChunksSet deflatedChunksSet; + protected boolean alsoBuffer = false; + + protected boolean skipBytes = false; // fDAT (APNG) skips 4 bytes) + protected byte[] skippedBytes; // only for fDAT + protected int seqNumExpected = -1; // only for fDAT + + public DeflatedChunkReader(int clen, String chunkid, boolean checkCrc, long offsetInPng, + DeflatedChunksSet iDatSet) { + super(clen, chunkid, offsetInPng, ChunkReaderMode.PROCESS); + this.deflatedChunksSet = iDatSet; + if (chunkid.equals(PngChunkFDAT.ID)) { + skipBytes = true; + skippedBytes = new byte[4]; + } + iDatSet.appendNewChunk(this); + } + + /** + * Delegates to ChunkReaderDeflatedSet.processData() + */ + @Override + protected void processData(int offsetInchunk, byte[] buf, int off, int len) { + if (skipBytes && offsetInchunk < 4) {// only for APNG (sigh) + for (int oc = offsetInchunk; oc < 4 && len > 0; oc++, off++, len--) + skippedBytes[oc] = buf[off]; + } + if (len > 0) { // delegate to idatSet + deflatedChunksSet.processBytes(buf, off, len); + if (alsoBuffer) { // very rare! + System.arraycopy(buf, off, getChunkRaw().data, read, len); + } + } + } + + /** + * only a stupid check for fDAT (I wonder how many APGN readers do this) + */ + @Override + protected void chunkDone() { + if (skipBytes && getChunkRaw().id.equals(PngChunkFDAT.ID)) { + if (seqNumExpected >= 0) { + int seqNum = PngHelperInternal.readInt4fromBytes(skippedBytes, 0); + if (seqNum != seqNumExpected) + throw new PngjInputException("bad chunk sequence for fDAT chunk " + seqNum + " expected " + + seqNumExpected); + } + } + } + + @Override + public boolean isFromDeflatedSet() { + return true; + } + + /** + * In some rare cases you might want to also buffer the data? + */ + public void setAlsoBuffer() { + if (read > 0) + throw new RuntimeException("too late"); + alsoBuffer = true; + getChunkRaw().allocData(); + } + + /** only relevant for fDAT */ + public void setSeqNumExpected(int seqNumExpected) { + this.seqNumExpected = seqNumExpected; + } + + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/DeflatedChunksSet.java b/src/js-specific/java/ar/com/hjg/pngj/DeflatedChunksSet.java new file mode 100644 index 00000000..1d496e8c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/DeflatedChunksSet.java @@ -0,0 +1,417 @@ +package ar.com.hjg.pngj; + +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * A set of IDAT-like chunks which, concatenated, form a zlib stream. + *

+ * The inflated stream is intented to be read as a sequence of "rows", of which the caller knows the lengths (not + * necessary equal) and number. + *

+ * Eg: For IDAT non-interlaced images, a row has bytesPerRow + 1 filter byte
+ * For interlaced images, the lengths are variable. + *

+ * This class can work in sync (polled) mode or async (callback) mode. But for callback mode the method + * processRowCallback() must be overriden + *

+ * See {@link IdatSet}, which is mostly used and has a slightly simpler use.
+ * See DeflatedChunkSetTest for example of use. + */ +public class DeflatedChunksSet { + + protected byte[] row; // a "row" here means a raw (uncopressed filtered) part of the IDAT stream, + // normally a image row (or subimage row for interlaced) plus a filter byte + private int rowfilled; // effective/valid length of row + private int rowlen; // what amount of bytes is to be interpreted as a complete "row". can change + // (for interlaced) + private int rown; // only coincide with image row if non-interlaced - incremented by + // setNextRowSize() + + /* + * States WAITING_FOR_INPUT ROW_READY WORK_DONE TERMINATED + * + * processBytes() is externally called, prohibited in READY (in DONE it's ignored) + * + * WARNING: inflater.finished() != DONE (not enough, not neccesary) DONE means that we have already uncompressed all + * the data of interest. + * + * In non-callback mode, prepareForNextRow() is also externally called, in + * + * Flow: - processBytes() calls inflateData() - inflateData() : if buffer is filled goes to READY else if ! + * inf.finished goes to WAITING else if any data goes to READY (incomplete data to be read) else goes to DONE - in + * Callback mode, after going to READY, n=processCallback() is called and then prepareForNextRow(n) is called. - in + * Polled mode, prepareForNextRow(n) must be called from outside (after checking state=READY) - prepareForNextRow(n) + * goes to DONE if n==0 calls inflateData() again - end() goes to DONE + */ + private enum State { + WAITING_FOR_INPUT, // waiting for more input + ROW_READY, // ready for consumption (might be less than fully filled), ephemeral for CALLBACK + // mode + WORK_DONE, // all data of interest has been read, but we might accept still more trailing chunks + // (we'll ignore them) + TERMINATED; // we are done, and also won't accept more IDAT chunks + + public boolean isDone() { + return this == WORK_DONE || this == TERMINATED; + } // the caller has already uncompressed all the data of interest or EOF + + public boolean isTerminated() { + return this == TERMINATED; + } // we dont accept more chunks + } + + State state = State.WAITING_FOR_INPUT; // never null + + private Inflater inf; + private final boolean infOwn; // true if we own the inflater (we created it) + + private DeflatedChunkReader curChunk; + + private boolean callbackMode = true; + private long nBytesIn = 0; // count the total compressed bytes that have been fed + private long nBytesOut = 0; // count the total uncompressed bytes + int chunkNum = -1; // incremented at each new chunk start + int firstChunqSeqNum = -1; // expected seq num for first chunk. used only for fDAT (APNG) + + /** + * All IDAT-like chunks that form a same DeflatedChunksSet should have the same id + */ + public final String chunkid; + + /** + * @param initialRowLen Length in bytes of first "row" (see description) + * @param maxRowLen Max length in bytes of "rows" + * @param inflater Can be null. If not null, must be already reset (and it must be closed/released by caller!) + */ + public DeflatedChunksSet(String chunkid, int initialRowLen, int maxRowLen, Inflater inflater, + byte[] buffer) { + this.chunkid = chunkid; + this.rowlen = initialRowLen; + if (initialRowLen < 1 || maxRowLen < initialRowLen) + throw new PngjException("bad inital row len " + initialRowLen); + if (inflater != null) { + this.inf = inflater; + infOwn = false; + } else { + this.inf = new Inflater(); + infOwn = true; // inflater is own, we will release on close() + } + this.row = buffer != null && buffer.length >= initialRowLen ? buffer : new byte[maxRowLen]; + rown = -1; + this.state = State.WAITING_FOR_INPUT; + try { + prepareForNextRow(initialRowLen); + } catch (RuntimeException e) { + close(); + throw e; + } + } + + public DeflatedChunksSet(String chunkid, int initialRowLen, int maxRowLen) { + this(chunkid, initialRowLen, maxRowLen, null, null); + } + + protected void appendNewChunk(DeflatedChunkReader cr) { + // all chunks must have same id + if (!this.chunkid.equals(cr.getChunkRaw().id)) + throw new PngjInputException("Bad chunk inside IdatSet, id:" + cr.getChunkRaw().id + + ", expected:" + this.chunkid); + this.curChunk = cr; + chunkNum++; + if (firstChunqSeqNum >= 0) + cr.setSeqNumExpected(chunkNum + firstChunqSeqNum); + } + + /** + * Feeds the inflater with the compressed bytes + * + * In poll mode, the caller should not call repeatedly this, without consuming first, checking + * isDataReadyForConsumer() + * + * @param buf + * @param off + * @param len + */ + protected void processBytes(byte[] buf, int off, int len) { + nBytesIn += len; + // PngHelperInternal.LOGGER.info("processing compressed bytes in chunkreader : " + len); + if (len < 1 || state.isDone()) + return; + if (state == State.ROW_READY) + throw new PngjInputException("this should only be called if waitingForMoreInput"); + if (inf.needsDictionary() || !inf.needsInput()) + throw new RuntimeException("should not happen"); + inf.setInput(buf, off, len); + // PngHelperInternal.debug("entering processs bytes, state=" + state + + // " callback="+callbackMode); + if (isCallbackMode()) { + while (inflateData()) { + int nextRowLen = processRowCallback(); + prepareForNextRow(nextRowLen); + if (isDone()) + processDoneCallback(); + } + } else + inflateData(); + } + + /* + * This never inflates more than one row This returns true if this has resulted in a row being ready and preprocessed + * with preProcessRow (in callback mode, we should call immediately processRowCallback() and + * prepareForNextRow(nextRowLen) + */ + private boolean inflateData() { + try { + // PngHelperInternal.debug("entering inflateData bytes, state=" + state + + // " callback="+callbackMode); + if (state == State.ROW_READY) + throw new PngjException("invalid state");// assert + if (state.isDone()) + return false; + int ninflated = 0; + if (row == null || row.length < rowlen) + row = new byte[rowlen]; // should not happen + if (rowfilled < rowlen && !inf.finished()) { + try { + ninflated = inf.inflate(row, rowfilled, rowlen - rowfilled); + } catch (DataFormatException e) { + throw new PngjInputException("error decompressing zlib stream ", e); + } + rowfilled += ninflated; + nBytesOut += ninflated; + } + State nextstate = null; + if (rowfilled == rowlen) + nextstate = State.ROW_READY; // complete row, process it + else if (!inf.finished()) + nextstate = State.WAITING_FOR_INPUT; + else if (rowfilled > 0) + nextstate = State.ROW_READY; // complete row, process it + else { + nextstate = State.WORK_DONE; // eof, no more data + } + state = nextstate; + if (state == State.ROW_READY) { + preProcessRow(); + return true; + } + } catch (RuntimeException e) { + close(); + throw e; + } + return false; + } + + /** + * Called automatically in all modes when a full row has been inflated. + */ + protected void preProcessRow() { + + } + + /** + * Callback, must be implemented in callbackMode + *

+ * This should use {@link #getRowFilled()} and {@link #getInflatedRow()} to access the row. + *

+ * Must return byes of next row, for next callback. + */ + protected int processRowCallback() { + throw new PngjInputException("not implemented"); + } + + /** + * Callback, to be implemented in callbackMode + *

+ * This will be called once to notify state done + */ + protected void processDoneCallback() {} + + /** + * Inflated buffer. + * + * The effective length is given by {@link #getRowFilled()} + */ + public byte[] getInflatedRow() { + return row; + } + + /** + * Should be called after the previous row was processed + *

+ * Pass 0 or negative to signal that we are done (not expecting more bytes) + *

+ * This resets {@link #rowfilled} + *

+ * The + */ + public void prepareForNextRow(int len) { + rowfilled = 0; + rown++; + if (len < 1) { + rowlen = 0; + done(); + } else if (inf.finished()) { + rowlen = 0; + done(); + } else { + state = State.WAITING_FOR_INPUT; + rowlen = len; + if (!callbackMode) + inflateData(); + } + } + + /** + * In this state, the object is waiting for more input to deflate. + *

+ * Only in this state it's legal to feed this + */ + public boolean isWaitingForMoreInput() { + return state == State.WAITING_FOR_INPUT; + } + + /** + * In this state, the object is waiting the caller to retrieve inflated data + *

+ * Effective length: see {@link #getRowFilled()} + */ + public boolean isRowReady() { + return state == State.ROW_READY; + } + + /** + * In this state, all relevant data has been uncompressed and retrieved (exceptionally, the reading has ended + * prematurely). + *

+ * We can still feed this object, but the bytes will be swallowed/ignored. + */ + public boolean isDone() { + return state.isDone(); + } + + public boolean isTerminated() { + return state.isTerminated(); + } + + /** + * This will be called by the owner to report us the next chunk to come. We can make our own internal changes and + * checks. This returns true if we acknowledge the next chunk as part of this set + */ + public boolean ackNextChunkId(String id) { + if (state.isTerminated()) + return false; + else if (id.equals(chunkid)) { + return true; + } else { + if (!allowOtherChunksInBetween(id)) { + if (state.isDone()) { + if (!isTerminated()) + terminate(); + return false; + } else { + throw new PngjInputException("Unexpected chunk " + id + " while " + chunkid + + " set is not done"); + } + } else + return true; + } + } + + protected void terminate() { + close(); + } + + /** + * This should be called when discarding this object, or for aborting. Secure, idempotent Don't use this just to + * notify this object that it has no more work to do, see {@link #done()} + * */ + public void close() { + try { + if (!state.isTerminated()) { + state = State.TERMINATED; + } + if (infOwn && inf != null) { + inf.end();// we end the Inflater only if we created it + inf = null; + } + } catch (Exception e) { + } + } + + /** + * Forces the DONE state, this object won't uncompress more data. It's still not terminated, it will accept more IDAT + * chunks, but will ignore them. + */ + public void done() { + if (!isDone()) + state = State.WORK_DONE; + } + + /** + * Target size of the current row, including filter byte.
+ * should coincide (or be less than) with row.length + */ + public int getRowLen() { + return rowlen; + } + + /** This the amount of valid bytes in the buffer */ + public int getRowFilled() { + return rowfilled; + } + + /** + * Get current (last) row number. + *

+ * This corresponds to the raw numeration of rows as seen by the deflater. Not the same as the real image row, if + * interlaced. + * + */ + public int getRown() { + return rown; + } + + /** + * Some IDAT-like set can allow other chunks in between (APGN?). + *

+ * Normally false. + * + * @param id Id of the other chunk that appeared in middel of this set. + * @return true if allowed + */ + public boolean allowOtherChunksInBetween(String id) { + return false; + } + + /** + * Callback mode = async processing + */ + public boolean isCallbackMode() { + return callbackMode; + } + + public void setCallbackMode(boolean callbackMode) { + this.callbackMode = callbackMode; + } + + /** total number of bytes that have been fed to this object */ + public long getBytesIn() { + return nBytesIn; + } + + /** total number of bytes that have been uncompressed */ + public long getBytesOut() { + return nBytesOut; + } + + @Override + public String toString() { + StringBuilder sb = + new StringBuilder("idatSet : " + curChunk.getChunkRaw().id + " state=" + state + " rows=" + + rown + " bytes=" + nBytesIn + "/" + nBytesOut); + return sb.toString(); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/Deinterlacer.java b/src/js-specific/java/ar/com/hjg/pngj/Deinterlacer.java new file mode 100644 index 00000000..ffb7260c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/Deinterlacer.java @@ -0,0 +1,199 @@ +package ar.com.hjg.pngj; + +public class Deinterlacer { + final ImageInfo imi; + private int pass; // 1-7 + private int rows, cols; + int dY, dX, oY, oX; // current step and offset (in pixels) + int oXsamples, dXsamples; // step in samples + + // current row in the virtual subsampled image; this increments (by 1) from 0 to rows/dy 7 times + private int currRowSubimg; + // in the real image, this will cycle from 0 to im.rows in different steps, 7 times + private int currRowReal; + private int currRowSeq; // not counting empty rows + + int totalRows; + private boolean ended = false; + + public Deinterlacer(ImageInfo iminfo) { + this.imi = iminfo; + pass = 0; + currRowSubimg = -1; + currRowReal = -1; + currRowSeq = 0; + ended = false; + totalRows = 0; // lazy compute + setPass(1); + setRow(0); + } + + /** this refers to the row currRowSubimg */ + private void setRow(int n) { // This should be called only intercally, in sequential order + currRowSubimg = n; + currRowReal = n * dY + oY; + if (currRowReal < 0 || currRowReal >= imi.rows) + throw new PngjExceptionInternal("bad row - this should not happen"); + } + + /** Skips passes with no rows. Return false is no more rows */ + boolean nextRow() { + currRowSeq++; + if (rows == 0 || currRowSubimg >= rows - 1) { // next pass + if (pass == 7) { + ended = true; + return false; + } + setPass(pass + 1); + if (rows == 0) { + currRowSeq--; + return nextRow(); + } + setRow(0); + } else { + setRow(currRowSubimg + 1); + } + return true; + } + + boolean isEnded() { + return ended; + } + + void setPass(int p) { + if (this.pass == p) + return; + pass = p; + byte[] pp = paramsForPass(p);// dx,dy,ox,oy + dX = pp[0]; + dY = pp[1]; + oX = pp[2]; + oY = pp[3]; + rows = imi.rows > oY ? (imi.rows + dY - 1 - oY) / dY : 0; + cols = imi.cols > oX ? (imi.cols + dX - 1 - oX) / dX : 0; + if (cols == 0) + rows = 0; // well, really... + dXsamples = dX * imi.channels; + oXsamples = oX * imi.channels; + } + + static byte[] paramsForPass(final int p) {// dx,dy,ox,oy + switch (p) { + case 1: + return new byte[] {8, 8, 0, 0}; + case 2: + return new byte[] {8, 8, 4, 0}; + case 3: + return new byte[] {4, 8, 0, 4}; + case 4: + return new byte[] {4, 4, 2, 0}; + case 5: + return new byte[] {2, 4, 0, 2}; + case 6: + return new byte[] {2, 2, 1, 0}; + case 7: + return new byte[] {1, 2, 0, 1}; + default: + throw new PngjExceptionInternal("bad interlace pass" + p); + } + } + + /** + * current row number inside the "sub image" + */ + int getCurrRowSubimg() { + return currRowSubimg; + } + + /** + * current row number inside the "real image" + */ + int getCurrRowReal() { + return currRowReal; + } + + /** + * current pass number (1-7) + */ + int getPass() { + return pass; + } + + /** + * How many rows has the current pass? + **/ + int getRows() { + return rows; + } + + /** + * How many columns (pixels) are there in the current row + */ + int getCols() { + return cols; + } + + public int getPixelsToRead() { + return getCols(); + } + + public int getBytesToRead() { // not including filter byte + return (imi.bitspPixel * getPixelsToRead() + 7) / 8; + } + + public int getdY() { + return dY; + } + + /* + * in pixels + */ + public int getdX() { + return dX; + } + + public int getoY() { + return oY; + } + + /* + * in pixels + */ + public int getoX() { + return oX; + } + + public int getTotalRows() { + if (totalRows == 0) { // lazy compute + for (int p = 1; p <= 7; p++) { + byte[] pp = paramsForPass(p); // dx dy ox oy + int rows = imi.rows > pp[3] ? (imi.rows + pp[1] - 1 - pp[3]) / pp[1] : 0; + int cols = imi.cols > pp[2] ? (imi.cols + pp[0] - 1 - pp[2]) / pp[0] : 0; + if (rows > 0 && cols > 0) + totalRows += rows; + } + } + return totalRows; + } + + /** + * total unfiltered bytes in the image, including the filter byte + */ + public long getTotalRawBytes() { // including the filter byte + long bytes = 0; + for (int p = 1; p <= 7; p++) { + byte[] pp = paramsForPass(p); // dx dy ox oy + int rows = imi.rows > pp[3] ? (imi.rows + pp[1] - 1 - pp[3]) / pp[1] : 0; + int cols = imi.cols > pp[2] ? (imi.cols + pp[0] - 1 - pp[2]) / pp[0] : 0; + int bytesr = (imi.bitspPixel * cols + 7) / 8; // without filter byte + if (rows > 0 && cols > 0) + bytes += rows * (1 + (long) bytesr); + } + return bytes; + } + + public int getCurrRowSeq() { + return currRowSeq; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/FilterType.java b/src/js-specific/java/ar/com/hjg/pngj/FilterType.java new file mode 100644 index 00000000..66128b75 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/FilterType.java @@ -0,0 +1,124 @@ +package ar.com.hjg.pngj; + +import java.util.HashMap; + +/** + * Internal PNG predictor filter type + * + * Negative values are pseudo types, actually global strategies for writing, that (can) result on different real filters + * for different rows + */ +public enum FilterType { + /** + * No filter. + */ + FILTER_NONE(0), + /** + * SUB filter (uses same row) + */ + FILTER_SUB(1), + /** + * UP filter (uses previous row) + */ + FILTER_UP(2), + /** + * AVERAGE filter + */ + FILTER_AVERAGE(3), + /** + * PAETH predictor + */ + FILTER_PAETH(4), + /** + * Default strategy: select one of the standard filters depending on global image parameters + */ + FILTER_DEFAULT(-1), + /** + * @deprecated use #FILTER_ADAPTIVE_FAST + */ + FILTER_AGGRESSIVE(-2), + /** + * @deprecated use #FILTER_ADAPTIVE_MEDIUM or #FILTER_ADAPTIVE_FULL + */ + FILTER_VERYAGGRESSIVE(-4), + /** + * Adaptative strategy, sampling each row, or almost + */ + FILTER_ADAPTIVE_FULL(-4), + /** + * Adaptive strategy, skippping some rows + */ + FILTER_ADAPTIVE_MEDIUM(-3), // samples about 1/4 row + /** + * Adaptative strategy, skipping many rows - more speed + */ + FILTER_ADAPTIVE_FAST(-2), // samples each 8 or 16 rows + /** + * Experimental + */ + FILTER_SUPER_ADAPTIVE(-10), // + /** + * Preserves the filter passed in original row. + */ + FILTER_PRESERVE(-40), + /** + * Uses all fiters, one for lines, cyciclally. Only for tests. + */ + FILTER_CYCLIC(-50), + /** + * Not specified, placeholder for unknown or NA filters. + */ + FILTER_UNKNOWN(-100); + + public final int val; + + private FilterType(int val) { + this.val = val; + } + + private static HashMap byVal; + + static { + byVal = new HashMap(); + for (FilterType ft : values()) { + byVal.put(ft.val, ft); + } + } + + public static FilterType getByVal(int i) { + return byVal.get(i); + } + + /** only considers standard */ + public static boolean isValidStandard(int i) { + return i >= 0 && i <= 4; + } + + public static boolean isValidStandard(FilterType fy) { + return fy != null && isValidStandard(fy.val); + } + + public static boolean isAdaptive(FilterType fy) { + return fy.val <= -2 && fy.val >= -4; + } + + /** + * Returns all "standard" filters + */ + public static FilterType[] getAllStandard() { + return new FilterType[] {FILTER_NONE, FILTER_SUB, FILTER_UP, FILTER_AVERAGE, FILTER_PAETH}; + } + + public static FilterType[] getAllStandardNoneLast() { + return new FilterType[] {FILTER_SUB, FILTER_UP, FILTER_AVERAGE, FILTER_PAETH, FILTER_NONE}; + } + + public static FilterType[] getAllStandardExceptNone() { + return new FilterType[] {FILTER_SUB, FILTER_UP, FILTER_AVERAGE, FILTER_PAETH}; + } + + static FilterType[] getAllStandardForFirstRow() { + return new FilterType[] {FILTER_SUB, FILTER_NONE}; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IBytesConsumer.java b/src/js-specific/java/ar/com/hjg/pngj/IBytesConsumer.java new file mode 100644 index 00000000..b2fcde3e --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IBytesConsumer.java @@ -0,0 +1,14 @@ +package ar.com.hjg.pngj; + +/** + * Bytes consumer. Objects implementing this interface can act as bytes consumers, that are "fed" with bytes. + */ +public interface IBytesConsumer { + /** + * Eats some bytes, at most len. + *

+ * Returns bytes actually consumed. A negative return value signals that the consumer is done, it refuses to eat more + * bytes. This should only return 0 if len is 0 + */ + int consume(byte[] buf, int offset, int len); +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IChunkFactory.java b/src/js-specific/java/ar/com/hjg/pngj/IChunkFactory.java new file mode 100644 index 00000000..013f3628 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IChunkFactory.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +import ar.com.hjg.pngj.chunks.ChunkRaw; +import ar.com.hjg.pngj.chunks.PngChunk; + +/** + * Factory to create a {@link PngChunk} from a {@link ChunkRaw}. + *

+ * Used by {@link PngReader} + */ +public interface IChunkFactory { + + /** + * @param chunkRaw Chunk in raw form. Data can be null if it was skipped or processed directly (eg IDAT) + * @param imgInfo Not normally necessary, but some chunks want this info + * @return should never return null. + */ + public PngChunk createChunk(ChunkRaw chunkRaw, ImageInfo imgInfo); + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IDatChunkWriter.java b/src/js-specific/java/ar/com/hjg/pngj/IDatChunkWriter.java new file mode 100644 index 00000000..e314193c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IDatChunkWriter.java @@ -0,0 +1,129 @@ +package ar.com.hjg.pngj; + +import java.io.OutputStream; + +import ar.com.hjg.pngj.chunks.ChunkHelper; +import ar.com.hjg.pngj.chunks.ChunkRaw; + +/** + * Outputs a sequence of IDAT-like chunk, that is filled progressively until the max chunk length is reached (or until + * flush()) + */ +public class IDatChunkWriter { + + private static final int MAX_LEN_DEFAULT = 32768; // 32K rather arbitrary - data only + + private final OutputStream outputStream; + private final int maxChunkLen; + private byte[] buf; + + private int offset = 0; + private int availLen; + private long totalBytesWriten = 0; // including header+crc + private int chunksWriten = 0; + + public IDatChunkWriter(OutputStream outputStream) { + this(outputStream, 0); + } + + public IDatChunkWriter(OutputStream outputStream, int maxChunkLength) { + this.outputStream = outputStream; + this.maxChunkLen = maxChunkLength > 0 ? maxChunkLength : MAX_LEN_DEFAULT; + buf = new byte[maxChunkLen]; + availLen = maxChunkLen - offset; + postReset(); + } + + public IDatChunkWriter(OutputStream outputStream, byte[] b) { + this.outputStream = outputStream; + this.buf = b != null ? b : new byte[MAX_LEN_DEFAULT]; + this.maxChunkLen = b.length; + availLen = maxChunkLen - offset; + postReset(); + } + + protected byte[] getChunkId() { + return ChunkHelper.b_IDAT; + } + + /** + * Writes a chhunk if there is more than minLenToWrite. + * + * This is normally called internally, but can be called explicitly to force flush. + */ + public final void flush() { + if (offset > 0 && offset >= minLenToWrite()) { + ChunkRaw c = new ChunkRaw(offset, getChunkId(), false); + c.data = buf; + c.writeChunk(outputStream); + totalBytesWriten += c.len + 12; + chunksWriten++; + offset = 0; + availLen = maxChunkLen; + postReset(); + } + } + + public int getOffset() { + return offset; + } + + public int getAvailLen() { + return availLen; + } + + /** triggers an flush+reset if appropiate */ + public void incrementOffset(int n) { + offset += n; + availLen -= n; + if (availLen < 0) + throw new PngjOutputException("Anomalous situation"); + if (availLen == 0) { + flush(); + } + } + + /** + * this should rarely be used, the normal way (to avoid double copying) is to get the buffer and write directly to it + */ + public void write(byte[] b, int o, int len) { + while (len > 0) { + int n = len <= availLen ? len : availLen; + System.arraycopy(b, o, buf, offset, n); + incrementOffset(n); + len -= n; + o += n; + } + } + + /** this will be called after reset */ + protected void postReset() { + // fdat could override this (and minLenToWrite) to add a prefix + } + + protected int minLenToWrite() { + return 1; + } + + public void close() { + flush(); + offset = 0; + buf = null; + } + + /** + * You can write directly to this buffer, using {@link #getOffset()} and {@link #getAvailLen()}. You should call + * {@link #incrementOffset(int)} inmediately after. + * */ + public byte[] getBuf() { + return buf; + } + + public long getTotalBytesWriten() { + return totalBytesWriten; + } + + public int getChunksWriten() { + return chunksWriten; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IImageLine.java b/src/js-specific/java/ar/com/hjg/pngj/IImageLine.java new file mode 100644 index 00000000..f40f13d0 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IImageLine.java @@ -0,0 +1,41 @@ +package ar.com.hjg.pngj; + +/** + * General format-translated image line. + *

+ * The methods from this interface provides translation from/to PNG raw unfiltered pixel data, for each image line. This + * doesn't make any assumptions of underlying storage. + *

+ * The user of this library will not normally use this methods, but instead will cast to a more concrete implementation, + * as {@link ImageLineInt} or {@link ImageLineByte} with its methods for accessing the pixel values. + */ +public interface IImageLine { + + /** + * Extract pixels from a raw unlfilterd PNG row. Len is the total amount of bytes in the array, including the first + * byte (filter type) + * + * Arguments offset and step (0 and 1 for non interlaced) are in PIXELS. It's guaranteed that when step==1 then + * offset=0 + * + * Notice that when step!=1 the data is partial, this method will be called several times + * + * Warning: the data in array 'raw' starts at position 0 and has 'len' consecutive bytes. 'offset' and 'step' refer to + * the pixels in destination + */ + void readFromPngRaw(byte[] raw, int len, int offset, int step); + + /** + * This is called when the read for the line has been completed (eg for interlaced). It's called exactly once for each + * line. This is provided in case the class needs to to some postprocessing. + */ + void endReadFromPngRaw(); + + /** + * Writes the line to a PNG raw byte array, in the unfiltered PNG format Notice that the first byte is the filter + * type, you should write it only if you know it. + * + */ + void writeToPngRaw(byte[] raw); + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IImageLineArray.java b/src/js-specific/java/ar/com/hjg/pngj/IImageLineArray.java new file mode 100644 index 00000000..6d3d6691 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IImageLineArray.java @@ -0,0 +1,23 @@ +package ar.com.hjg.pngj; + +/** + * This interface is just for the sake of unifying some methods of {@link ImageLineHelper} that can use both + * {@link ImageLineInt} or {@link ImageLineByte}. It's not very useful outside that, and the user should not rely much + * on this. + */ +public interface IImageLineArray { + public ImageInfo getImageInfo(); + + public FilterType getFilterType(); + + /** + * length of array (should correspond to samples) + */ + public int getSize(); + + /** + * Get i-th element of array (for 0 to size-1). The meaning of this is type dependent. For ImageLineInt and + * ImageLineByte is the sample value. + */ + public int getElem(int i); +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IImageLineFactory.java b/src/js-specific/java/ar/com/hjg/pngj/IImageLineFactory.java new file mode 100644 index 00000000..1c0a4bd2 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IImageLineFactory.java @@ -0,0 +1,8 @@ +package ar.com.hjg.pngj; + +/** + * Image Line factory. + */ +public interface IImageLineFactory { + public T createImageLine(ImageInfo iminfo); +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IImageLineSet.java b/src/js-specific/java/ar/com/hjg/pngj/IImageLineSet.java new file mode 100644 index 00000000..595001aa --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IImageLineSet.java @@ -0,0 +1,53 @@ +package ar.com.hjg.pngj; + + +/** + * Set of {@link IImageLine} elements. + *

+ * This is actually a "virtual" set, it can be implemented in several ways; for example + *

    + *
  • Cursor-like: stores only one line, which is implicitly moved when requested
  • + *
  • All lines: all lines stored as an array of IImageLine
  • + *
  • + * Subset of lines: eg, only first 3 lines, or odd numbered lines. Or a band of neighbours lines that is moved like a + * cursor.
  • + * The ImageLine that PngReader returns is hosted by a IImageLineSet (this abstraction allows the implementation to deal + * with interlaced images cleanly) but the library user does not normally needs to know that (or rely on that), except + * for the {@link PngReader#readRows()} method. + *
+ */ +public interface IImageLineSet { + + /** + * Asks for imageline corresponding to row n in the original image (zero based). This can trigger side + * effects in this object (eg, advance a cursor, set current row number...) In some scenarios, this should be consider + * as alias to (pseudocode) positionAtLine(n); getCurrentLine(); + *

+ * Throws exception if not available. The caller is supposed to know what he/she is doing + **/ + public IImageLine getImageLine(int n); + + /** + * Like {@link #getImageLine(int)} but uses the raw numbering inside the LineSet This makes little sense for a cursor + * + * @param n Should normally go from 0 to {@link #size()} + * @return + */ + public IImageLine getImageLineRawNum(int n); + + + /** + * Returns true if the set contain row n (in the original image,zero based) currently allocated. + *

+ * If it's a single-cursor, this should return true only if it's positioned there. (notice that hasImageLine(n) can + * return false, but getImageLine(n) can be ok) + * + **/ + public boolean hasImageLine(int n); + + /** + * Internal size of allocated rows This is informational, it should rarely be important for the caller. + **/ + public int size(); + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IImageLineSetFactory.java b/src/js-specific/java/ar/com/hjg/pngj/IImageLineSetFactory.java new file mode 100644 index 00000000..e9aeba50 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IImageLineSetFactory.java @@ -0,0 +1,24 @@ +package ar.com.hjg.pngj; + +/** + * Factory of {@link IImageLineSet}, used by {@link PngReader}. + *

+ * + * @param Generic type of IImageLine + */ +public interface IImageLineSetFactory { + /** + * Creates a new {@link IImageLineSet} + * + * If singleCursor=true, the caller will read and write one row fully at a time, in order (it'll never try to read out + * of order lines), so the implementation can opt for allocate only one line. + * + * @param imgInfo Image info + * @param singleCursor : will read/write one row at a time + * @param nlines : how many lines we plan to read + * @param noffset : how many lines we want to skip from the original image (normally 0) + * @param step : row step (normally 1) + */ + public IImageLineSet create(ImageInfo imgInfo, boolean singleCursor, int nlines, int noffset, + int step); +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IPngWriterFactory.java b/src/js-specific/java/ar/com/hjg/pngj/IPngWriterFactory.java new file mode 100644 index 00000000..617f5855 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IPngWriterFactory.java @@ -0,0 +1,7 @@ +package ar.com.hjg.pngj; + +import java.io.OutputStream; + +public interface IPngWriterFactory { + public PngWriter createPngWriter(OutputStream outputStream, ImageInfo imgInfo); +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/IdatSet.java b/src/js-specific/java/ar/com/hjg/pngj/IdatSet.java new file mode 100644 index 00000000..82d83a11 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/IdatSet.java @@ -0,0 +1,242 @@ +package ar.com.hjg.pngj; + +import java.util.Arrays; +import java.util.zip.Checksum; +import java.util.zip.Inflater; + +/** + * This object process the concatenation of IDAT chunks. + *

+ * It extends {@link DeflatedChunksSet}, adding the intelligence to unfilter rows, and to understand row lenghts in + * terms of ImageInfo and (eventually) Deinterlacer + */ +public class IdatSet extends DeflatedChunksSet { + + protected byte rowUnfiltered[]; + protected byte rowUnfilteredPrev[]; + protected final ImageInfo imgInfo; // in the case of APNG this is the frame image + protected final Deinterlacer deinterlacer; + + final RowInfo rowinfo; // info for the last processed row, for debug + + protected int filterUseStat[] = new int[5]; // for stats + + /** + * @param id Chunk id (first chunk), should be shared by all concatenated chunks + * @param iminfo Image info + * @param deinterlacer Not null if interlaced + */ + public IdatSet(String id, ImageInfo iminfo, Deinterlacer deinterlacer) { + this(id, iminfo, deinterlacer, null, null); + } + + /** + * Special constructor with preallocated buffer. + *

+ *

+ * Same as {@link #IdatSet(String, ImageInfo, Deinterlacer)}, but you can pass a Inflater (will be reset internally), + * and a buffer (will be used only if size is enough) + */ + public IdatSet(String id, ImageInfo iminfo, Deinterlacer deinterlacer, Inflater inf, byte[] buffer) { + super(id, deinterlacer != null ? deinterlacer.getBytesToRead() + 1 : iminfo.bytesPerRow + 1, + iminfo.bytesPerRow + 1, inf, buffer); + this.imgInfo = iminfo; + this.deinterlacer = deinterlacer; + this.rowinfo = new RowInfo(iminfo, deinterlacer); + } + + /** + * Applies PNG un-filter to inflated raw line. Result in {@link #getUnfilteredRow()} {@link #getRowLen()} + */ + public void unfilterRow() { + unfilterRow(rowinfo.bytesRow); + } + + // nbytes: NOT including the filter byte. leaves result in rowUnfiltered + protected void unfilterRow(int nbytes) { + if (rowUnfiltered == null || rowUnfiltered.length < row.length) { + rowUnfiltered = new byte[row.length]; + rowUnfilteredPrev = new byte[row.length]; + } + if (rowinfo.rowNsubImg == 0) + Arrays.fill(rowUnfiltered, (byte) 0); // see swap that follows + // swap + byte[] tmp = rowUnfiltered; + rowUnfiltered = rowUnfilteredPrev; + rowUnfilteredPrev = tmp; + + int ftn = row[0]; + if (!FilterType.isValidStandard(ftn)) + throw new PngjInputException("Filter type " + ftn + " invalid"); + FilterType ft = FilterType.getByVal(ftn); + filterUseStat[ftn]++; + rowUnfiltered[0] = row[0]; // we copy the filter type, can be useful + switch (ft) { + case FILTER_NONE: + unfilterRowNone(nbytes); + break; + case FILTER_SUB: + unfilterRowSub(nbytes); + break; + case FILTER_UP: + unfilterRowUp(nbytes); + break; + case FILTER_AVERAGE: + unfilterRowAverage(nbytes); + break; + case FILTER_PAETH: + unfilterRowPaeth(nbytes); + break; + default: + throw new PngjInputException("Filter type " + ftn + " not implemented"); + } + } + + private void unfilterRowAverage(final int nbytes) { + int i, j, x; + for (j = 1 - imgInfo.bytesPixel, i = 1; i <= nbytes; i++, j++) { + x = j > 0 ? (rowUnfiltered[j] & 0xff) : 0; + rowUnfiltered[i] = (byte) (row[i] + (x + (rowUnfilteredPrev[i] & 0xFF)) / 2); + } + } + + private void unfilterRowNone(final int nbytes) { + for (int i = 1; i <= nbytes; i++) { + rowUnfiltered[i] = (byte) (row[i]); + } + } + + private void unfilterRowPaeth(final int nbytes) { + int i, j, x, y; + for (j = 1 - imgInfo.bytesPixel, i = 1; i <= nbytes; i++, j++) { + x = j > 0 ? (rowUnfiltered[j] & 0xFF) : 0; + y = j > 0 ? (rowUnfilteredPrev[j] & 0xFF) : 0; + rowUnfiltered[i] = + (byte) (row[i] + PngHelperInternal + .filterPaethPredictor(x, rowUnfilteredPrev[i] & 0xFF, y)); + } + } + + private void unfilterRowSub(final int nbytes) { + int i, j; + for (i = 1; i <= imgInfo.bytesPixel; i++) { + rowUnfiltered[i] = (byte) (row[i]); + } + for (j = 1, i = imgInfo.bytesPixel + 1; i <= nbytes; i++, j++) { + rowUnfiltered[i] = (byte) (row[i] + rowUnfiltered[j]); + } + } + + private void unfilterRowUp(final int nbytes) { + for (int i = 1; i <= nbytes; i++) { + rowUnfiltered[i] = (byte) (row[i] + rowUnfilteredPrev[i]); + } + } + + /** + * does the unfiltering of the inflated row, and updates row info + */ + @Override + protected void preProcessRow() { + super.preProcessRow(); + rowinfo.update(getRown()); + unfilterRow(); + rowinfo.updateBuf(rowUnfiltered, rowinfo.bytesRow + 1); + } + + /** + * Method for async/callback mode . + *

+ * In callback mode will be called as soon as each row is retrieved (inflated and unfiltered), after + * {@link #preProcessRow()} + *

+ * This is a dummy implementation (this normally should be overriden) that does nothing more than compute the length + * of next row. + *

+ * The return value is essential + *

+ * + * @return Length of next row, in bytes (including filter byte), non-positive if done + */ + @Override + protected int processRowCallback() { + int bytesNextRow = advanceToNextRow(); + return bytesNextRow; + } + + @Override + protected void processDoneCallback() { + super.processDoneCallback(); + } + + /** + * Signals that we are done with the previous row, begin reading the next one. + *

+ * In polled mode, calls setNextRowLen() + *

+ * Warning: after calling this, the unfilterRow is invalid! + * + * @return Returns nextRowLen + */ + public int advanceToNextRow() { + // PngHelperInternal.LOGGER.info("advanceToNextRow"); + int bytesNextRow; + if (deinterlacer == null) { + bytesNextRow = getRown() >= imgInfo.rows - 1 ? 0 : imgInfo.bytesPerRow + 1; + } else { + boolean more = deinterlacer.nextRow(); + bytesNextRow = more ? deinterlacer.getBytesToRead() + 1 : 0; + } + if (!isCallbackMode()) { // in callback mode, setNextRowLen() is called internally + prepareForNextRow(bytesNextRow); + } + return bytesNextRow; + } + + public boolean isRowReady() { + return !isWaitingForMoreInput(); + + } + + /** + * Unfiltered row. + *

+ * This should be called only if {@link #isRowReady()} returns true. + *

+ * To get real length, use {@link #getRowLen()} + *

+ * + * @return Unfiltered row, includes filter byte + */ + public byte[] getUnfilteredRow() { + return rowUnfiltered; + } + + public Deinterlacer getDeinterlacer() { + return deinterlacer; + } + + void updateCrcs(Checksum... idatCrcs) { + for (Checksum idatCrca : idatCrcs) + if (idatCrca != null)// just for testing + idatCrca.update(getUnfilteredRow(), 1, getRowFilled() - 1); + } + + @Override + public void close() { + super.close(); + rowUnfiltered = null;// not really necessary... + rowUnfilteredPrev = null; + } + + /** + * Only for debug/stats + * + * @return Array of 5 integers (sum equal numbers of rows) counting each filter use + */ + public int[] getFilterUseStat() { + return filterUseStat; + } + + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ImageInfo.java b/src/js-specific/java/ar/com/hjg/pngj/ImageInfo.java new file mode 100644 index 00000000..80758c8a --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ImageInfo.java @@ -0,0 +1,255 @@ +package ar.com.hjg.pngj; + +import java.util.zip.Checksum; + +/** + * Simple immutable wrapper for basic image info. + *

+ * Some parameters are redundant, but the constructor receives an 'orthogonal' subset. + *

+ * ref: http://www.w3.org/TR/PNG/#11IHDR + */ +public class ImageInfo { + + /** + * Absolute allowed maximum value for rows and cols (2^24 ~16 million). (bytesPerRow must fit in a 32bit integer, + * though total amount of pixels not necessarily). + */ + public static final int MAX_COLS_ROW = 16777216; + + /** + * Cols= Image width, in pixels. + */ + public final int cols; + + /** + * Rows= Image height, in pixels + */ + public final int rows; + + /** + * Bits per sample (per channel) in the buffer (1-2-4-8-16). This is 8-16 for RGB/ARGB images, 1-2-4-8 for grayscale. + * For indexed images, number of bits per palette index (1-2-4-8) + */ + public final int bitDepth; + + /** + * Number of channels, as used internally: 3 for RGB, 4 for RGBA, 2 for GA (gray with alpha), 1 for grayscale or + * indexed. + */ + public final int channels; + + /** + * Flag: true if has alpha channel (RGBA/GA) + */ + public final boolean alpha; + + /** + * Flag: true if is grayscale (G/GA) + */ + public final boolean greyscale; + + /** + * Flag: true if image is indexed, i.e., it has a palette + */ + public final boolean indexed; + + /** + * Flag: true if image internally uses less than one byte per sample (bit depth 1-2-4) + */ + public final boolean packed; + + /** + * Bits used for each pixel in the buffer: channel * bitDepth + */ + public final int bitspPixel; + + /** + * rounded up value: this is only used internally for filter + */ + public final int bytesPixel; + + /** + * ceil(bitspp*cols/8) - does not include filter + */ + public final int bytesPerRow; + + /** + * Equals cols * channels + */ + public final int samplesPerRow; + + /** + * Amount of "packed samples" : when several samples are stored in a single byte (bitdepth 1,2 4) they are counted as + * one "packed sample". This is less that samplesPerRow only when bitdepth is 1-2-4 (flag packed = true) + *

+ * This equals the number of elements in the scanline array if working with packedMode=true + *

+ * For internal use, client code should rarely access this. + */ + public final int samplesPerRowPacked; + + private long totalPixels = -1; // lazy getter + + private long totalRawBytes = -1; // lazy getter + + /** + * Short constructor: assumes truecolor (RGB/RGBA) + */ + public ImageInfo(int cols, int rows, int bitdepth, boolean alpha) { + this(cols, rows, bitdepth, alpha, false, false); + } + + /** + * Full constructor + * + * @param cols Width in pixels + * @param rows Height in pixels + * @param bitdepth Bits per sample, in the buffer : 8-16 for RGB true color and greyscale + * @param alpha Flag: has an alpha channel (RGBA or GA) + * @param grayscale Flag: is gray scale (any bitdepth, with or without alpha) + * @param indexed Flag: has palette + */ + public ImageInfo(int cols, int rows, int bitdepth, boolean alpha, boolean grayscale, + boolean indexed) { + this.cols = cols; + this.rows = rows; + this.alpha = alpha; + this.indexed = indexed; + this.greyscale = grayscale; + if (greyscale && indexed) + throw new PngjException("palette and greyscale are mutually exclusive"); + this.channels = (grayscale || indexed) ? (alpha ? 2 : 1) : (alpha ? 4 : 3); + // http://www.w3.org/TR/PNG/#11IHDR + this.bitDepth = bitdepth; + this.packed = bitdepth < 8; + this.bitspPixel = (channels * this.bitDepth); + this.bytesPixel = (bitspPixel + 7) / 8; + this.bytesPerRow = (bitspPixel * cols + 7) / 8; + this.samplesPerRow = channels * this.cols; + this.samplesPerRowPacked = packed ? bytesPerRow : samplesPerRow; + // several checks + switch (this.bitDepth) { + case 1: + case 2: + case 4: + if (!(this.indexed || this.greyscale)) + throw new PngjException("only indexed or grayscale can have bitdepth=" + this.bitDepth); + break; + case 8: + break; + case 16: + if (this.indexed) + throw new PngjException("indexed can't have bitdepth=" + this.bitDepth); + break; + default: + throw new PngjException("invalid bitdepth=" + this.bitDepth); + } + if (cols < 1 || cols > MAX_COLS_ROW) + throw new PngjException("invalid cols=" + cols + " ???"); + if (rows < 1 || rows > MAX_COLS_ROW) + throw new PngjException("invalid rows=" + rows + " ???"); + if (samplesPerRow < 1) + throw new PngjException("invalid image parameters (overflow?)"); + } + + /** + * returns a copy with different size + * + * @param cols if non-positive, the original is used + * @param rows if non-positive, the original is used + * @return a new copy with the specified size and same properties + */ + public ImageInfo withSize(int cols, int rows) { + return new ImageInfo(cols > 0 ? cols : this.cols, rows > 0 ? rows : this.rows, this.bitDepth, + this.alpha, this.greyscale, this.indexed); + } + + public long getTotalPixels() { + if (totalPixels < 0) + totalPixels = cols * (long) rows; + return totalPixels; + } + + /** + * Total uncompressed bytes in IDAT, including filter byte. This is not valid for interlaced. + */ + public long getTotalRawBytes() { + if (totalRawBytes < 0) + totalRawBytes = (bytesPerRow + 1) * (long) rows; + return totalRawBytes; + } + + @Override + public String toString() { + return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels=" + + channels + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + indexed + "]"; + } + + /** + * Brief info: COLSxROWS[dBITDEPTH][a][p][g] ( the default dBITDEPTH='d8' is ommited) + **/ + public String toStringBrief() { + return String.valueOf(cols) + "x" + rows + (bitDepth != 8 ? ("d" + bitDepth) : "") + + (alpha ? "a" : "") + (indexed ? "p" : "") + (greyscale ? "g" : ""); + } + + public String toStringDetail() { + return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels=" + + channels + ", bitspPixel=" + bitspPixel + ", bytesPixel=" + bytesPixel + ", bytesPerRow=" + + bytesPerRow + ", samplesPerRow=" + samplesPerRow + ", samplesPerRowP=" + + samplesPerRowPacked + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + + indexed + ", packed=" + packed + "]"; + } + + + void updateCrc(Checksum crc) { + crc.update((byte) rows); + crc.update((byte) (rows >> 8)); + crc.update((byte) (rows >> 16)); + crc.update((byte) cols); + crc.update((byte) (cols >> 8)); + crc.update((byte) (cols >> 16)); + crc.update((byte) (bitDepth)); + crc.update((byte) (indexed ? 1 : 2)); + crc.update((byte) (greyscale ? 3 : 4)); + crc.update((byte) (alpha ? 3 : 4)); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (alpha ? 1231 : 1237); + result = prime * result + bitDepth; + result = prime * result + cols; + result = prime * result + (greyscale ? 1231 : 1237); + result = prime * result + (indexed ? 1231 : 1237); + result = prime * result + rows; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ImageInfo other = (ImageInfo) obj; + if (alpha != other.alpha) + return false; + if (bitDepth != other.bitDepth) + return false; + if (cols != other.cols) + return false; + if (greyscale != other.greyscale) + return false; + if (indexed != other.indexed) + return false; + if (rows != other.rows) + return false; + return true; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ImageLineByte.java b/src/js-specific/java/ar/com/hjg/pngj/ImageLineByte.java new file mode 100644 index 00000000..3c1146ca --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ImageLineByte.java @@ -0,0 +1,186 @@ +package ar.com.hjg.pngj; + +/** + * Lightweight wrapper for an image scanline, used for read and write. + *

+ * This object can be (usually it is) reused while iterating over the image lines. + *

+ * See scanline field, to understand the format. + * + * Format: byte (one bytes per sample) (for 16bpp the extra byte is placed in an extra array) + */ +public class ImageLineByte implements IImageLine, IImageLineArray { + public final ImageInfo imgInfo; + + final byte[] scanline; + final byte[] scanline2; // only used for 16 bpp (less significant byte) Normally you'd prefer + // ImageLineInt in this case + + protected FilterType filterType; // informational ; only filled by the reader. not significant for + // interlaced + final int size; // = imgInfo.samplePerRowPacked, if packed:imgInfo.samplePerRow elswhere + + public ImageLineByte(ImageInfo imgInfo) { + this(imgInfo, null); + } + + public ImageLineByte(ImageInfo imgInfo, byte[] sci) { + this.imgInfo = imgInfo; + filterType = FilterType.FILTER_UNKNOWN; + size = imgInfo.samplesPerRow; + scanline = sci != null && sci.length >= size ? sci : new byte[size]; + scanline2 = imgInfo.bitDepth == 16 ? new byte[size] : null; + } + + /** + * Returns a factory for this object + */ + public static IImageLineFactory getFactory() { + return new IImageLineFactory() { + public ImageLineByte createImageLine(ImageInfo iminfo) { + return new ImageLineByte(iminfo); + } + }; + } + + public FilterType getFilterUsed() { + return filterType; + } + + /** + * One byte per sample. This can be used also for 16bpp images, but in this case this loses the less significant + * 8-bits ; see also getScanlineByte2 and getElem. + */ + public byte[] getScanlineByte() { + return scanline; + } + + /** + * only for 16bpp (less significant byte) + * + * @return null for less than 16bpp + */ + public byte[] getScanlineByte2() { + return scanline2; + } + + /** + * Basic info + */ + public String toString() { + return " cols=" + imgInfo.cols + " bpc=" + imgInfo.bitDepth + " size=" + scanline.length; + } + + public void readFromPngRaw(byte[] raw, final int len, final int offset, final int step) { + filterType = FilterType.getByVal(raw[0]); // only for non interlaced line the filter is significative + int len1 = len - 1; + int step1 = (step - 1) * imgInfo.channels; + if (imgInfo.bitDepth == 8) { + if (step == 1) {// 8bispp non-interlaced: most important case, should be optimized + System.arraycopy(raw, 1, scanline, 0, len1); + } else {// 8bispp interlaced + for (int s = 1, c = 0, i = offset * imgInfo.channels; s <= len1; s++, i++) { + scanline[i] = raw[s]; + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } + } + } else if (imgInfo.bitDepth == 16) { + if (step == 1) {// 16bispp non-interlaced + for (int i = 0, s = 1; i < imgInfo.samplesPerRow; i++) { + scanline[i] = raw[s++]; // get the first byte + scanline2[i] = raw[s++]; // get the first byte + } + } else { + for (int s = 1, c = 0, i = offset != 0 ? offset * imgInfo.channels : 0; s <= len1; i++) { + scanline[i] = raw[s++]; + scanline2[i] = raw[s++]; + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } + } + } else { // packed formats + int mask0, mask, shi, bd; + bd = imgInfo.bitDepth; + mask0 = ImageLineHelper.getMaskForPackedFormats(bd); + for (int i = offset * imgInfo.channels, r = 1, c = 0; r < len; r++) { + mask = mask0; + shi = 8 - bd; + do { + scanline[i] = (byte) ((raw[r] & mask) >> shi); + mask >>= bd; + shi -= bd; + i++; + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } while (mask != 0 && i < size); + } + } + } + + public void writeToPngRaw(byte[] raw) { + raw[0] = (byte) filterType.val; + if (imgInfo.bitDepth == 8) { + System.arraycopy(scanline, 0, raw, 1, size); + } else if (imgInfo.bitDepth == 16) { + for (int i = 0, s = 1; i < size; i++) { + raw[s++] = scanline[i]; + raw[s++] = scanline2[i]; + } + } else { // packed formats + int shi, bd, v; + bd = imgInfo.bitDepth; + shi = 8 - bd; + v = 0; + for (int i = 0, r = 1; i < size; i++) { + v |= (scanline[i] << shi); + shi -= bd; + if (shi < 0 || i == size - 1) { + raw[r++] = (byte) v; + shi = 8 - bd; + v = 0; + } + } + } + } + + public void endReadFromPngRaw() {} + + public int getSize() { + return size; + } + + public int getElem(int i) { + return scanline2 == null ? scanline[i] & 0xFF : ((scanline[i] & 0xFF) << 8) + | (scanline2[i] & 0xFF); + } + + public byte[] getScanline() { + return scanline; + } + + public ImageInfo getImageInfo() { + return imgInfo; + } + + public FilterType getFilterType() { + return filterType; + } + + /** + * This should rarely be used by client code. Only relevant if FilterPreserve==true + */ + public void setFilterType(FilterType ft) { + filterType = ft; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ImageLineHelper.java b/src/js-specific/java/ar/com/hjg/pngj/ImageLineHelper.java new file mode 100644 index 00000000..ee22d7e0 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ImageLineHelper.java @@ -0,0 +1,470 @@ +package ar.com.hjg.pngj; + +import java.util.Arrays; + +import ar.com.hjg.pngj.chunks.PngChunkPLTE; +import ar.com.hjg.pngj.chunks.PngChunkTRNS; + +/** + * Bunch of utility static methods to proces an image line at the pixel level. + *

+ * WARNING: this has little testing/optimizing, and this API is not stable. some methods will probably be changed or + * removed if future releases. + *

+ * WARNING: most methods for getting/setting values work currently only for ImageLine or ImageLineByte + */ +public class ImageLineHelper { + + static int[] DEPTH_UNPACK_1; + static int[] DEPTH_UNPACK_2; + static int[] DEPTH_UNPACK_4; + static int[][] DEPTH_UNPACK; + + static { + initDepthScale(); + } + + private static void initDepthScale() { + DEPTH_UNPACK_1 = new int[2]; + for (int i = 0; i < 2; i++) + DEPTH_UNPACK_1[i] = i * 255; + DEPTH_UNPACK_2 = new int[4]; + for (int i = 0; i < 4; i++) + DEPTH_UNPACK_2[i] = (i * 255) / 3; + DEPTH_UNPACK_4 = new int[16]; + for (int i = 0; i < 16; i++) + DEPTH_UNPACK_4[i] = (i * 255) / 15; + DEPTH_UNPACK = new int[][] {null, DEPTH_UNPACK_1, DEPTH_UNPACK_2, null, DEPTH_UNPACK_4}; + } + + /** + * When the bitdepth is less than 8, the imageLine is usually returned/expected unscaled. This method upscales it in + * place. Eg, if bitdepth=1, values 0-1 will be converted to 0-255 + */ + public static void scaleUp(IImageLineArray line) { + if (line.getImageInfo().indexed || line.getImageInfo().bitDepth >= 8) + return; + final int[] scaleArray = DEPTH_UNPACK[line.getImageInfo().bitDepth]; + if (line instanceof ImageLineInt) { + ImageLineInt iline = (ImageLineInt) line; + for (int i = 0; i < iline.getSize(); i++) + iline.scanline[i] = scaleArray[iline.scanline[i]]; + } else if (line instanceof ImageLineByte) { + ImageLineByte iline = (ImageLineByte) line; + for (int i = 0; i < iline.getSize(); i++) + iline.scanline[i] = (byte) scaleArray[iline.scanline[i]]; + } else + throw new PngjException("not implemented"); + } + + /** + * Reverse of {@link #scaleUp(IImageLineArray)} + */ + public static void scaleDown(IImageLineArray line) { + if (line.getImageInfo().indexed || line.getImageInfo().bitDepth >= 8) + return; + if (line instanceof ImageLineInt) { + final int scalefactor = 8 - line.getImageInfo().bitDepth; + if (line instanceof ImageLineInt) { + ImageLineInt iline = (ImageLineInt) line; + for (int i = 0; i < line.getSize(); i++) + iline.scanline[i] = iline.scanline[i] >> scalefactor; + } else if (line instanceof ImageLineByte) { + ImageLineByte iline = (ImageLineByte) line; + for (int i = 0; i < line.getSize(); i++) + iline.scanline[i] = (byte) ((iline.scanline[i] & 0xFF) >> scalefactor); + } + } else + throw new PngjException("not implemented"); + } + + public static byte scaleUp(int bitdepth, byte v) { + return bitdepth < 8 ? (byte) DEPTH_UNPACK[bitdepth][v] : v; + } + + public static byte scaleDown(int bitdepth, byte v) { + return bitdepth < 8 ? (byte) (v >> (8 - bitdepth)) : v; + } + + /** + * Given an indexed line with a palette, unpacks as a RGB array, or RGBA if a non nul PngChunkTRNS chunk is passed + * + * @param line ImageLine as returned from PngReader + * @param pal Palette chunk + * @param trns Transparency chunk, can be null (absent) + * @param buf Preallocated array, optional + * @return R G B (A), one sample 0-255 per array element. Ready for pngw.writeRowInt() + */ + public static int[] palette2rgb(ImageLineInt line, PngChunkPLTE pal, PngChunkTRNS trns, int[] buf) { + return palette2rgb(line, pal, trns, buf, false); + } + + /** + * Warning: the line should be upscaled, see {@link #scaleUp(IImageLineArray)} + */ + static int[] lineToARGB32(ImageLineByte line, PngChunkPLTE pal, PngChunkTRNS trns, int[] buf) { + boolean alphachannel = line.imgInfo.alpha; + int cols = line.getImageInfo().cols; + if (buf == null || buf.length < cols) + buf = new int[cols]; + int index, rgb, alpha, ga, g; + if (line.getImageInfo().indexed) {// palette + int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0; + for (int c = 0; c < cols; c++) { + index = line.scanline[c] & 0xFF; + rgb = pal.getEntry(index); + alpha = index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255; + buf[c] = (alpha << 24) | rgb; + } + } else if (line.imgInfo.greyscale) { // gray + ga = trns != null ? trns.getGray() : -1; + for (int c = 0, c2 = 0; c < cols; c++) { + g = (line.scanline[c2++] & 0xFF); + alpha = alphachannel ? line.scanline[c2++] & 0xFF : (g != ga ? 255 : 0); + buf[c] = (alpha << 24) | g | (g << 8) | (g << 16); + } + } else { // true color + ga = trns != null ? trns.getRGB888() : -1; + for (int c = 0, c2 = 0; c < cols; c++) { + rgb = + ((line.scanline[c2++] & 0xFF) << 16) | ((line.scanline[c2++] & 0xFF) << 8) + | (line.scanline[c2++] & 0xFF); + alpha = alphachannel ? line.scanline[c2++] & 0xFF : (rgb != ga ? 255 : 0); + buf[c] = (alpha << 24) | rgb; + } + } + return buf; + } + + /** + * Warning: the line should be upscaled, see {@link #scaleUp(IImageLineArray)} + */ + static byte[] lineToRGBA8888(ImageLineByte line, PngChunkPLTE pal, PngChunkTRNS trns, byte[] buf) { + boolean alphachannel = line.imgInfo.alpha; + int cols = line.imgInfo.cols; + int bytes = cols * 4; + if (buf == null || buf.length < bytes) + buf = new byte[bytes]; + int index, rgb, ga; + byte val; + if (line.imgInfo.indexed) {// palette + int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0; + for (int c = 0, b = 0; c < cols; c++) { + index = line.scanline[c] & 0xFF; + rgb = pal.getEntry(index); + buf[b++] = (byte) ((rgb >> 16) & 0xFF); + buf[b++] = (byte) ((rgb >> 8) & 0xFF); + buf[b++] = (byte) (rgb & 0xFF); + buf[b++] = (byte) (index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255); + } + } else if (line.imgInfo.greyscale) { // + ga = trns != null ? trns.getGray() : -1; + for (int c = 0, b = 0; b < bytes;) { + val = line.scanline[c++]; + buf[b++] = val; + buf[b++] = val; + buf[b++] = val; + buf[b++] = + alphachannel ? line.scanline[c++] : ((int) (val & 0xFF) == ga) ? (byte) 0 : (byte) 255; + } + } else { // true color + if (alphachannel) // same format! + System.arraycopy(line.scanline, 0, buf, 0, bytes); + else { + for (int c = 0, b = 0; b < bytes;) { + buf[b++] = line.scanline[c++]; + buf[b++] = line.scanline[c++]; + buf[b++] = line.scanline[c++]; + buf[b++] = (byte) (255); // tentative (probable) + if (trns != null && buf[b - 3] == (byte) trns.getRGB()[0] + && buf[b - 2] == (byte) trns.getRGB()[1] && buf[b - 1] == (byte) trns.getRGB()[2]) // not + // very + // efficient, + // but + // not + // frecuent + buf[b - 1] = 0; + } + } + } + return buf; + } + + static byte[] lineToRGB888(ImageLineByte line, PngChunkPLTE pal, byte[] buf) { + boolean alphachannel = line.imgInfo.alpha; + int cols = line.imgInfo.cols; + int bytes = cols * 3; + if (buf == null || buf.length < bytes) + buf = new byte[bytes]; + byte val; + int[] rgb = new int[3]; + if (line.imgInfo.indexed) {// palette + for (int c = 0, b = 0; c < cols; c++) { + pal.getEntryRgb(line.scanline[c] & 0xFF, rgb); + buf[b++] = (byte) rgb[0]; + buf[b++] = (byte) rgb[1]; + buf[b++] = (byte) rgb[2]; + } + } else if (line.imgInfo.greyscale) { // + for (int c = 0, b = 0; b < bytes;) { + val = line.scanline[c++]; + buf[b++] = val; + buf[b++] = val; + buf[b++] = val; + if (alphachannel) + c++; // skip alpha + } + } else { // true color + if (!alphachannel) // same format! + System.arraycopy(line.scanline, 0, buf, 0, bytes); + else { + for (int c = 0, b = 0; b < bytes;) { + buf[b++] = line.scanline[c++]; + buf[b++] = line.scanline[c++]; + buf[b++] = line.scanline[c++]; + c++;// skip alpha + } + } + } + return buf; + } + + /** + * Same as palette2rgbx , but returns rgba always, even if trns is null + * + * @param line ImageLine as returned from PngReader + * @param pal Palette chunk + * @param trns Transparency chunk, can be null (absent) + * @param buf Preallocated array, optional + * @return R G B (A), one sample 0-255 per array element. Ready for pngw.writeRowInt() + */ + public static int[] palette2rgba(ImageLineInt line, PngChunkPLTE pal, PngChunkTRNS trns, int[] buf) { + return palette2rgb(line, pal, trns, buf, true); + } + + public static int[] palette2rgb(ImageLineInt line, PngChunkPLTE pal, int[] buf) { + return palette2rgb(line, pal, null, buf, false); + } + + /** this is not very efficient, only for tests and troubleshooting */ + public static int[] convert2rgba(IImageLineArray line, PngChunkPLTE pal, PngChunkTRNS trns, + int[] buf) { + ImageInfo imi = line.getImageInfo(); + int nsamples = imi.cols * 4; + if (buf == null || buf.length < nsamples) + buf = new int[nsamples]; + int maxval = imi.bitDepth == 16 ? (1 << 16) - 1 : 255; + Arrays.fill(buf, maxval); + + if (imi.indexed) { + int tlen = trns != null ? trns.getPalletteAlpha().length : 0; + for (int s = 0; s < imi.cols; s++) { + int index = line.getElem(s); + pal.getEntryRgb(index, buf, s * 4); + if (index < tlen) { + buf[s * 4 + 3] = trns.getPalletteAlpha()[index]; + } + } + } else if (imi.greyscale) { + int[] unpack = null; + if (imi.bitDepth < 8) + unpack = ImageLineHelper.DEPTH_UNPACK[imi.bitDepth]; + for (int s = 0, i = 0, p = 0; p < imi.cols; p++) { + buf[s++] = unpack != null ? unpack[line.getElem(i++)] : line.getElem(i++); + buf[s] = buf[s - 1]; + s++; + buf[s] = buf[s - 1]; + s++; + if (imi.channels == 2) + buf[s++] = unpack != null ? unpack[line.getElem(i++)] : line.getElem(i++); + else + buf[s++] = maxval; + } + } else { + for (int s = 0, i = 0, p = 0; p < imi.cols; p++) { + buf[s++] = line.getElem(i++); + buf[s++] = line.getElem(i++); + buf[s++] = line.getElem(i++); + buf[s++] = imi.alpha ? line.getElem(i++) : maxval; + } + } + return buf; + } + + + + private static int[] palette2rgb(IImageLine line, PngChunkPLTE pal, PngChunkTRNS trns, int[] buf, + boolean alphaForced) { + boolean isalpha = trns != null; + int channels = isalpha ? 4 : 3; + ImageLineInt linei = (ImageLineInt) (line instanceof ImageLineInt ? line : null); + ImageLineByte lineb = (ImageLineByte) (line instanceof ImageLineByte ? line : null); + boolean isbyte = lineb != null; + int cols = linei != null ? linei.imgInfo.cols : lineb.imgInfo.cols; + int nsamples = cols * channels; + if (buf == null || buf.length < nsamples) + buf = new int[nsamples]; + int nindexesWithAlpha = trns != null ? trns.getPalletteAlpha().length : 0; + for (int c = 0; c < cols; c++) { + int index = isbyte ? (lineb.scanline[c] & 0xFF) : linei.scanline[c]; + pal.getEntryRgb(index, buf, c * channels); + if (isalpha) { + int alpha = index < nindexesWithAlpha ? trns.getPalletteAlpha()[index] : 255; + buf[c * channels + 3] = alpha; + } + } + return buf; + } + + /** + * what follows is pretty uninteresting/untested/obsolete, subject to change + */ + /** + * Just for basic info or debugging. Shows values for first and last pixel. Does not include alpha + */ + public static String infoFirstLastPixels(ImageLineInt line) { + return line.imgInfo.channels == 1 ? String.format("first=(%d) last=(%d)", line.scanline[0], + line.scanline[line.scanline.length - 1]) : String.format( + "first=(%d %d %d) last=(%d %d %d)", line.scanline[0], line.scanline[1], line.scanline[2], + line.scanline[line.scanline.length - line.imgInfo.channels], + line.scanline[line.scanline.length - line.imgInfo.channels + 1], + line.scanline[line.scanline.length - line.imgInfo.channels + 2]); + } + + /** + * integer packed R G B only for bitdepth=8! (does not check!) + * + **/ + public static int getPixelRGB8(IImageLine line, int column) { + if (line instanceof ImageLineInt) { + int offset = column * ((ImageLineInt) line).imgInfo.channels; + int[] scanline = ((ImageLineInt) line).getScanline(); + return (scanline[offset] << 16) | (scanline[offset + 1] << 8) | (scanline[offset + 2]); + } else if (line instanceof ImageLineByte) { + int offset = column * ((ImageLineByte) line).imgInfo.channels; + byte[] scanline = ((ImageLineByte) line).getScanline(); + return ((scanline[offset] & 0xff) << 16) | ((scanline[offset + 1] & 0xff) << 8) + | ((scanline[offset + 2] & 0xff)); + } else + throw new PngjException("Not supported " + line.getClass()); + } + + public static int getPixelARGB8(IImageLine line, int column) { + if (line instanceof ImageLineInt) { + int offset = column * ((ImageLineInt) line).imgInfo.channels; + int[] scanline = ((ImageLineInt) line).getScanline(); + return (scanline[offset + 3] << 24) | (scanline[offset] << 16) | (scanline[offset + 1] << 8) + | (scanline[offset + 2]); + } else if (line instanceof ImageLineByte) { + int offset = column * ((ImageLineByte) line).imgInfo.channels; + byte[] scanline = ((ImageLineByte) line).getScanline(); + return (((scanline[offset + 3] & 0xff) << 24) | ((scanline[offset] & 0xff) << 16) + | ((scanline[offset + 1] & 0xff) << 8) | ((scanline[offset + 2] & 0xff))); + } else + throw new PngjException("Not supported " + line.getClass()); + } + + public static void setPixelsRGB8(ImageLineInt line, int[] rgb) { + for (int i = 0, j = 0; i < line.imgInfo.cols; i++) { + line.scanline[j++] = ((rgb[i] >> 16) & 0xFF); + line.scanline[j++] = ((rgb[i] >> 8) & 0xFF); + line.scanline[j++] = ((rgb[i] & 0xFF)); + } + } + + public static void setPixelRGB8(ImageLineInt line, int col, int r, int g, int b) { + col *= line.imgInfo.channels; + line.scanline[col++] = r; + line.scanline[col++] = g; + line.scanline[col] = b; + } + + public static void setPixelRGB8(ImageLineInt line, int col, int rgb) { + setPixelRGB8(line, col, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); + } + + public static void setPixelsRGBA8(ImageLineInt line, int[] rgb) { + for (int i = 0, j = 0; i < line.imgInfo.cols; i++) { + line.scanline[j++] = ((rgb[i] >> 16) & 0xFF); + line.scanline[j++] = ((rgb[i] >> 8) & 0xFF); + line.scanline[j++] = ((rgb[i] & 0xFF)); + line.scanline[j++] = ((rgb[i] >> 24) & 0xFF); + } + } + + public static void setPixelRGBA8(ImageLineInt line, int col, int r, int g, int b, int a) { + col *= line.imgInfo.channels; + line.scanline[col++] = r; + line.scanline[col++] = g; + line.scanline[col++] = b; + line.scanline[col] = a; + } + + public static void setPixelRGBA8(ImageLineInt line, int col, int rgb) { + setPixelRGBA8(line, col, (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, (rgb >> 24) & 0xFF); + } + + public static void setValD(ImageLineInt line, int i, double d) { + line.scanline[i] = double2int(line, d); + } + + public static int interpol(int a, int b, int c, int d, double dx, double dy) { + // a b -> x (0-1) + // c d + double e = a * (1.0 - dx) + b * dx; + double f = c * (1.0 - dx) + d * dx; + return (int) (e * (1 - dy) + f * dy + 0.5); + } + + public static double int2double(ImageLineInt line, int p) { + return line.imgInfo.bitDepth == 16 ? p / 65535.0 : p / 255.0; + // TODO: replace my multiplication? check for other bitdepths + } + + public static double int2doubleClamped(ImageLineInt line, int p) { + // TODO: replace my multiplication? + double d = line.imgInfo.bitDepth == 16 ? p / 65535.0 : p / 255.0; + return d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d); + } + + public static int double2int(ImageLineInt line, double d) { + d = d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d); + return line.imgInfo.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); // + } + + public static int double2intClamped(ImageLineInt line, double d) { + d = d <= 0.0 ? 0 : (d >= 1.0 ? 1.0 : d); + return line.imgInfo.bitDepth == 16 ? (int) (d * 65535.0 + 0.5) : (int) (d * 255.0 + 0.5); // + } + + public static int clampTo_0_255(int i) { + return i > 255 ? 255 : (i < 0 ? 0 : i); + } + + public static int clampTo_0_65535(int i) { + return i > 65535 ? 65535 : (i < 0 ? 0 : i); + } + + public static int clampTo_128_127(int x) { + return x > 127 ? 127 : (x < -128 ? -128 : x); + } + + public static int getMaskForPackedFormats(int bitDepth) { // Utility function for pack/unpack + if (bitDepth == 4) + return 0xf0; + else if (bitDepth == 2) + return 0xc0; + else + return 0x80; // bitDepth == 1 + } + + public static int getMaskForPackedFormatsLs(int bitDepth) { // Utility function for pack/unpack + if (bitDepth == 4) + return 0x0f; + else if (bitDepth == 2) + return 0x03; + else + return 0x01; // bitDepth == 1 + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ImageLineInt.java b/src/js-specific/java/ar/com/hjg/pngj/ImageLineInt.java new file mode 100644 index 00000000..2671276e --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ImageLineInt.java @@ -0,0 +1,193 @@ +package ar.com.hjg.pngj; + +/** + * Represents an image line, integer format (one integer by sample). See {@link #scanline} to understand the format. + */ +public class ImageLineInt implements IImageLine, IImageLineArray { + public final ImageInfo imgInfo; + + /** + * The 'scanline' is an array of integers, corresponds to an image line (row). + *

+ * Each int is a "sample" (one for channel), (0-255 or 0-65535) in the corresponding PNG sequence: + * R G B R G B... or R G B A R G B A... + * or g g g ... or i i i (palette index) + *

+ * For bitdepth=1/2/4 the value is not scaled (hence, eg, if bitdepth=2 the range will be 0-4) + *

+ * To convert a indexed line to RGB values, see + * {@link ImageLineHelper#palette2rgb(ImageLineInt, ar.com.hjg.pngj.chunks.PngChunkPLTE, int[])} (you can't do the + * reverse) + */ + protected final int[] scanline; + + /** + * number of elements in the scanline + */ + protected final int size; + + /** + * informational ; only filled by the reader. not meaningful for interlaced + */ + protected FilterType filterType = FilterType.FILTER_UNKNOWN; + + /** + * @param imgInfo Inmutable ImageInfo, basic parameters of the image we are reading or writing + */ + public ImageLineInt(ImageInfo imgInfo) { + this(imgInfo, null); + } + + /** + * @param imgInfo Inmutable ImageInfo, basic parameters of the image we are reading or writing + * @param sci prealocated buffer (can be null) + */ + public ImageLineInt(ImageInfo imgInfo, int[] sci) { + this.imgInfo = imgInfo; + filterType = FilterType.FILTER_UNKNOWN; + size = imgInfo.samplesPerRow; + scanline = sci != null && sci.length >= size ? sci : new int[size]; + } + + /** + * Helper method, returns a default factory for this object + * + */ + public static IImageLineFactory getFactory() { + return new IImageLineFactory() { + public ImageLineInt createImageLine(ImageInfo iminfo) { + return new ImageLineInt(iminfo); + } + }; + } + + public FilterType getFilterType() { + return filterType; + } + + /** + * This should rarely be used by client code. Only relevant if FilterPreserve==true + */ + public void setFilterType(FilterType ft) { + filterType = ft; + } + + /** + * Basic info + */ + public String toString() { + return " cols=" + imgInfo.cols + " bpc=" + imgInfo.bitDepth + " size=" + scanline.length; + } + + public void readFromPngRaw(byte[] raw, final int len, final int offset, final int step) { + setFilterType(FilterType.getByVal(raw[0])); + int len1 = len - 1; + int step1 = (step - 1) * imgInfo.channels; + if (imgInfo.bitDepth == 8) { + if (step == 1) {// 8bispp non-interlaced: most important case, should be optimized + for (int i = 0; i < size; i++) { + scanline[i] = (raw[i + 1] & 0xff); + } + } else {// 8bispp interlaced + for (int s = 1, c = 0, i = offset * imgInfo.channels; s <= len1; s++, i++) { + scanline[i] = (raw[s] & 0xff); + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } + } + } else if (imgInfo.bitDepth == 16) { + if (step == 1) {// 16bispp non-interlaced + for (int i = 0, s = 1; i < size; i++) { + scanline[i] = ((raw[s++] & 0xFF) << 8) | (raw[s++] & 0xFF); // 16 bitspc + } + } else { + for (int s = 1, c = 0, i = offset != 0 ? offset * imgInfo.channels : 0; s <= len1; s++, i++) { + scanline[i] = ((raw[s++] & 0xFF) << 8) | (raw[s] & 0xFF); // 16 bitspc + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } + } + } else { // packed formats + int mask0, mask, shi, bd; + bd = imgInfo.bitDepth; + mask0 = ImageLineHelper.getMaskForPackedFormats(bd); + for (int i = offset * imgInfo.channels, r = 1, c = 0; r < len; r++) { + mask = mask0; + shi = 8 - bd; + do { + scanline[i++] = (raw[r] & mask) >> shi; + mask >>= bd; + shi -= bd; + c++; + if (c == imgInfo.channels) { + c = 0; + i += step1; + } + } while (mask != 0 && i < size); + } + } + } + + public void writeToPngRaw(byte[] raw) { + raw[0] = (byte) filterType.val; + if (imgInfo.bitDepth == 8) { + for (int i = 0; i < size; i++) { + raw[i + 1] = (byte) scanline[i]; + } + } else if (imgInfo.bitDepth == 16) { + for (int i = 0, s = 1; i < size; i++) { + raw[s++] = (byte) (scanline[i] >> 8); + raw[s++] = (byte) (scanline[i] & 0xff); + } + } else { // packed formats + int shi, bd, v; + bd = imgInfo.bitDepth; + shi = 8 - bd; + v = 0; + for (int i = 0, r = 1; i < size; i++) { + v |= (scanline[i] << shi); + shi -= bd; + if (shi < 0 || i == size - 1) { + raw[r++] = (byte) v; + shi = 8 - bd; + v = 0; + } + } + } + } + + /** + * Does nothing in this implementation + */ + public void endReadFromPngRaw() { + + } + + /** + * @see #size + */ + public int getSize() { + return size; + } + + public int getElem(int i) { + return scanline[i]; + } + + /** + * @return see {@link #scanline} + */ + public int[] getScanline() { + return scanline; + } + + public ImageInfo getImageInfo() { + return imgInfo; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/ImageLineSetDefault.java b/src/js-specific/java/ar/com/hjg/pngj/ImageLineSetDefault.java new file mode 100644 index 00000000..4dbdce30 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/ImageLineSetDefault.java @@ -0,0 +1,151 @@ +package ar.com.hjg.pngj; + +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation of {@link IImageLineSet}. + *

+ * Supports all modes: single cursor, full rows, or partial. This should not be used for + */ +public abstract class ImageLineSetDefault implements IImageLineSet { + + protected final ImageInfo imgInfo; + private final boolean singleCursor; + private final int nlines, offset, step; + protected List imageLines; // null if single cursor + protected T imageLine; // null unless single cursor + protected int currentRow = -1; // only relevant (and not much) for cursor + + public ImageLineSetDefault(ImageInfo imgInfo, final boolean singleCursor, final int nlinesx, + final int noffsetx, final int stepx) { + this.imgInfo = imgInfo; + this.singleCursor = singleCursor; + if (singleCursor) { + this.nlines = 1; // we store only one line, no matter how many will be read + offset = 0; + this.step = 1;// don't matter + } else { + this.nlines = nlinesx; // note that it can also be 1 + offset = noffsetx; + this.step = stepx;// don't matter + } + createImageLines(); + } + + private void createImageLines() { + if (singleCursor) + imageLine = createImageLine(); + else { + imageLines = new ArrayList(); + for (int i = 0; i < nlines; i++) + imageLines.add(createImageLine()); + } + } + + protected abstract T createImageLine(); + + /** + * Retrieves the image line + *

+ * Warning: the argument is the row number in the original image + *

+ * If this is a cursor, no check is done, always the same row is returned + */ + public T getImageLine(int n) { + currentRow = n; + if (singleCursor) + return imageLine; + else { + int r = imageRowToMatrixRowStrict(n); + if (r < 0) + throw new PngjException("Invalid row number"); + return imageLines.get(r); + } + } + + /** + * does not check for valid range + */ + public T getImageLineRawNum(int r) { + if (singleCursor) + return imageLine; + else + return imageLines.get(r); + } + + /** + * True if the set contains this image line + *

+ * Warning: the argument is the row number in the original image + *

+ * If this works as cursor, this returns true only if that is the number of its "current" line + */ + public boolean hasImageLine(int n) { + return singleCursor ? currentRow == n : imageRowToMatrixRowStrict(n) >= 0; + } + + /** + * How many lines does this object contain? + */ + public int size() { + return nlines; + } + + /** + * Same as {@link #imageRowToMatrixRow(int)}, but returns negative if invalid + */ + public int imageRowToMatrixRowStrict(int imrow) { + imrow -= offset; + int mrow = imrow >= 0 && (step == 1 || imrow % step == 0) ? imrow / step : -1; + return mrow < nlines ? mrow : -1; + } + + /** + * Converts from matrix row number (0 : nRows-1) to image row number + * + * @param mrow Matrix row number + * @return Image row number. Returns trash if mrow is invalid + */ + public int matrixRowToImageRow(int mrow) { + return mrow * step + offset; + } + + /** + * Converts from real image row to this object row number. + *

+ * Warning: this always returns a valid matrix row (clamping on 0 : nrows-1, and rounding down) + *

+ * Eg: rowOffset=4,rowStep=2 imageRowToMatrixRow(17) returns 6 , imageRowToMatrixRow(1) returns 0 + */ + public int imageRowToMatrixRow(int imrow) { + int r = (imrow - offset) / step; + return r < 0 ? 0 : (r < nlines ? r : nlines - 1); + } + + /** utility function, given a factory for one line, returns a factory for a set */ + public static IImageLineSetFactory createImageLineSetFactoryFromImageLineFactory( + final IImageLineFactory ifactory) { // ugly method must have ugly name. don't let this intimidate you + return new IImageLineSetFactory() { + public IImageLineSet create(final ImageInfo iminfo, boolean singleCursor, int nlines, + int noffset, int step) { + return new ImageLineSetDefault(iminfo, singleCursor, nlines, noffset, step) { + @Override + protected T createImageLine() { + return ifactory.createImageLine(iminfo); + } + }; + }; + }; + } + + /** utility function, returns default factory for {@link ImageLineInt} */ + public static IImageLineSetFactory getFactoryInt() { + return createImageLineSetFactoryFromImageLineFactory(ImageLineInt.getFactory()); + } + + /** utility function, returns default factory for {@link ImageLineByte} */ + public static IImageLineSetFactory getFactoryByte() { + return createImageLineSetFactoryFromImageLineFactory(ImageLineByte.getFactory()); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngHelperInternal.java b/src/js-specific/java/ar/com/hjg/pngj/PngHelperInternal.java new file mode 100644 index 00000000..6785bdd4 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngHelperInternal.java @@ -0,0 +1,329 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.logging.Logger; + +/** + * Some utility static methods for internal use. + *

+ * Client code should not normally use this class + *

+ */ +public final class PngHelperInternal { + + public static final String KEY_LOGGER = "ar.com.pngj"; + public static final Logger LOGGER = Logger.getLogger(KEY_LOGGER); + + /** + * Default charset, used internally by PNG for several things + */ + public static String charsetLatin1name = "UTF-8"; + public static Charset charsetLatin1 = Charset.forName(charsetLatin1name); + /** + * UTF-8 is only used for some chunks + */ + public static String charsetUTF8name = "UTF-8"; + public static Charset charsetUTF8 = Charset.forName(charsetUTF8name); + + private static ThreadLocal DEBUG = new ThreadLocal() { + protected Boolean initialValue() { + return Boolean.FALSE; + } + }; + + /** + * PNG magic bytes + */ + public static byte[] getPngIdSignature() { + return new byte[] {-119, 80, 78, 71, 13, 10, 26, 10}; + } + + public static int doubleToInt100000(double d) { + return (int) (d * 100000.0 + 0.5); + } + + public static double intToDouble100000(int i) { + return i / 100000.0; + } + + public static int readByte(InputStream is) { + try { + return is.read(); + } catch (IOException e) { + throw new PngjInputException("error reading byte", e); + } + } + + /** + * -1 if eof + * + * PNG uses "network byte order" + */ + public static int readInt2(InputStream is) { + try { + int b1 = is.read(); + int b2 = is.read(); + if (b1 == -1 || b2 == -1) + return -1; + return (b1 << 8) | b2; + } catch (IOException e) { + throw new PngjInputException("error reading Int2", e); + } + } + + /** + * -1 if eof + */ + public static int readInt4(InputStream is) { + try { + int b1 = is.read(); + int b2 = is.read(); + int b3 = is.read(); + int b4 = is.read(); + if (b1 == -1 || b2 == -1 || b3 == -1 || b4 == -1) + return -1; + return (b1 << 24) | (b2 << 16) | (b3 << 8) + b4; + } catch (IOException e) { + throw new PngjInputException("error reading Int4", e); + } + } + + public static int readInt1fromByte(byte[] b, int offset) { + return (b[offset] & 0xff); + } + + public static int readInt2fromBytes(byte[] b, int offset) { + return ((b[offset] & 0xff) << 8) | ((b[offset + 1] & 0xff)); + } + + public static final int readInt4fromBytes(byte[] b, int offset) { + return ((b[offset] & 0xff) << 24) | ((b[offset + 1] & 0xff) << 16) + | ((b[offset + 2] & 0xff) << 8) | (b[offset + 3] & 0xff); + } + + public static void writeByte(OutputStream os, byte b) { + try { + os.write(b); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void writeByte(OutputStream os, byte[] bs) { + try { + os.write(bs); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void writeInt2(OutputStream os, int n) { + byte[] temp = {(byte) ((n >> 8) & 0xff), (byte) (n & 0xff)}; + writeBytes(os, temp); + } + + public static void writeInt4(OutputStream os, int n) { + byte[] temp = new byte[4]; + writeInt4tobytes(n, temp, 0); + writeBytes(os, temp); + } + + public static void writeInt2tobytes(int n, byte[] b, int offset) { + b[offset] = (byte) ((n >> 8) & 0xff); + b[offset + 1] = (byte) (n & 0xff); + } + + public static void writeInt4tobytes(int n, byte[] b, int offset) { + b[offset] = (byte) ((n >> 24) & 0xff); + b[offset + 1] = (byte) ((n >> 16) & 0xff); + b[offset + 2] = (byte) ((n >> 8) & 0xff); + b[offset + 3] = (byte) (n & 0xff); + } + + + /** + * guaranteed to read exactly len bytes. throws error if it can't + */ + public static void readBytes(InputStream is, byte[] b, int offset, int len) { + if (len == 0) + return; + try { + int read = 0; + while (read < len) { + int n = is.read(b, offset + read, len - read); + if (n < 1) + throw new PngjInputException("error reading bytes, " + n + " !=" + len); + read += n; + } + } catch (IOException e) { + throw new PngjInputException("error reading", e); + } + } + + public static void skipBytes(InputStream is, long len) { + try { + while (len > 0) { + long n1 = is.skip(len); + if (n1 > 0) { + len -= n1; + } else if (n1 == 0) { // should we retry? lets read one byte + if (is.read() == -1) // EOF + break; + else + len--; + } else + // negative? this should never happen but... + throw new IOException("skip() returned a negative value ???"); + } + } catch (IOException e) { + throw new PngjInputException(e); + } + } + + public static void writeBytes(OutputStream os, byte[] b) { + try { + os.write(b); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void writeBytes(OutputStream os, byte[] b, int offset, int n) { + try { + os.write(b, offset, n); + } catch (IOException e) { + throw new PngjOutputException(e); + } + } + + public static void logdebug(String msg) { + if (isDebug()) + System.err.println("logdebug: " + msg); + } + + // / filters + public static int filterRowNone(int r) { + return (int) (r & 0xFF); + } + + public static int filterRowSub(int r, int left) { + return ((int) (r - left) & 0xFF); + } + + public static int filterRowUp(int r, int up) { + return ((int) (r - up) & 0xFF); + } + + public static int filterRowAverage(int r, int left, int up) { + return (r - (left + up) / 2) & 0xFF; + } + + public static int filterRowPaeth(int r, int left, int up, int upleft) { // a = left, b = above, c + // = upper left + return (r - filterPaethPredictor(left, up, upleft)) & 0xFF; + } + + final static int filterPaethPredictor(final int a, final int b, final int c) { // a = left, b = + // above, c = upper + // left + // from http://www.libpng.org/pub/png/spec/1.2/PNG-Filters.html + + final int p = a + b - c;// ; initial estimate + final int pa = p >= a ? p - a : a - p; + final int pb = p >= b ? p - b : b - p; + final int pc = p >= c ? p - c : c - p; + // ; return nearest of a,b,c, + // ; breaking ties in order a,b,c. + if (pa <= pb && pa <= pc) + return a; + else if (pb <= pc) + return b; + else + return c; + } + + /** + * Prits a debug message (prints class name, method and line number) + * + * @param obj : Object to print + */ + public static void debug(Object obj) { + debug(obj, 1, true); + } + + /** + * Prits a debug message (prints class name, method and line number) + * + * @param obj : Object to print + * @param offset : Offset N lines from stacktrace + */ + static void debug(Object obj, int offset) { + debug(obj, offset, true); + } + + public static InputStream istreamFromFile(File f) { + FileInputStream is; + try { + is = new FileInputStream(f); + } catch (Exception e) { + throw new PngjInputException("Could not open " + f, e); + } + return is; + } + + static OutputStream ostreamFromFile(File f) { + return ostreamFromFile(f, true); + } + + static OutputStream ostreamFromFile(File f, boolean overwrite) { + return PngHelperInternal2.ostreamFromFile(f, overwrite); + } + + /** + * Prints a debug message (prints class name, method and line number) to stderr and logFile + * + * @param obj : Object to print + * @param offset : Offset N lines from stacktrace + * @param newLine : Print a newline char at the end ('\n') + */ + static void debug(Object obj, int offset, boolean newLine) { + StackTraceElement ste = new Exception().getStackTrace()[1 + offset]; + String steStr = ste.getClassName(); + int ind = steStr.lastIndexOf('.'); + steStr = steStr.substring(ind + 1); + steStr += + "." + ste.getMethodName() + "(" + ste.getLineNumber() + "): " + + (obj == null ? null : obj.toString()); + System.err.println(steStr); + } + + /** + * Sets a global debug flag. This is bound to a thread. + */ + public static void setDebug(boolean b) { + DEBUG.set(b); + } + + public static boolean isDebug() { + return DEBUG.get().booleanValue(); + } + + public static long getDigest(PngReader pngr) { + return pngr.getSimpleDigest(); + } + + public static void initCrcForTests(PngReader pngr) { + pngr.prepareSimpleDigestComputation(); + } + + public static long getRawIdatBytes(PngReader r) { // in case of image with frames, returns the current one + return r.interlaced ? r.getChunkseq().getDeinterlacer().getTotalRawBytes() : r.getCurImgInfo() + .getTotalRawBytes(); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngHelperInternal2.java b/src/js-specific/java/ar/com/hjg/pngj/PngHelperInternal2.java new file mode 100644 index 00000000..ab34ce15 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngHelperInternal2.java @@ -0,0 +1,33 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.OutputStream; + +/** + * For organization purposes, this class is the onlt that uses classes not in GAE (Google App Engine) white list + *

+ * You should not use this class in GAE + */ +final class PngHelperInternal2 { + + /** + * WARNING: this uses FileOutputStream which is not allowed in GoogleAppEngine + * + * In GAE, dont use this + * + * @param f + * @param allowoverwrite + * @return + */ + static OutputStream ostreamFromFile(File f, boolean allowoverwrite) { + java.io.FileOutputStream os = null; // this will fail in GAE! + if (f.exists() && !allowoverwrite) + throw new PngjOutputException("File already exists: " + f); + try { + os = new java.io.FileOutputStream(f); + } catch (Exception e) { + throw new PngjInputException("Could not open for write" + f, e); + } + return os; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngReader.java b/src/js-specific/java/ar/com/hjg/pngj/PngReader.java new file mode 100644 index 00000000..6367761c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngReader.java @@ -0,0 +1,586 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.InputStream; +import java.util.zip.Adler32; +import java.util.zip.CRC32; + +import ar.com.hjg.pngj.chunks.ChunkLoadBehaviour; +import ar.com.hjg.pngj.chunks.ChunksList; +import ar.com.hjg.pngj.chunks.PngChunkFCTL; +import ar.com.hjg.pngj.chunks.PngChunkFDAT; +import ar.com.hjg.pngj.chunks.PngChunkIDAT; +import ar.com.hjg.pngj.chunks.PngMetadata; + +/** + * Reads a PNG image (pixels and/or metadata) from a file or stream. + *

+ * Each row is read as an {@link ImageLineInt} object (one int per sample), but this can be changed by setting a + * different ImageLineFactory + *

+ * Internally, this wraps a {@link ChunkSeqReaderPng} with a {@link BufferedStreamFeeder} + *

+ * The reading sequence is as follows:
+ * 1. At construction time, the header and IHDR chunk are read (basic image info)
+ * 2. Afterwards you can set some additional global options. Eg. {@link #setCrcCheckDisabled()}.
+ * 3. Optional: If you call getMetadata() or getChunksLisk() before start reading the rows, all the chunks before IDAT + * are then loaded and available
+ * 4a. The rows are read in order by calling {@link #readRow()}. You can also call {@link #readRow(int)} to skip rows + * -but you can't go backwards, at least not with this implementation. This method returns a {@link IImageLine} object + * which can be casted to the concrete class. This class returns by default a {@link ImageLineInt}, but this can be + * changed.
+ * 4b. Alternatively, you can read all rows, or a subset, in a single call: {@link #readRows()}, + * {@link #readRows(int, int, int)} ,etc. In general this consumes more memory, but for interlaced images this is + * equally efficient, and more so if reading a small subset of rows.
+ * 5. Reading of the last row automatically loads the trailing chunks, and ends the reader.
+ * 6. end() also loads the trailing chunks, if not done, and finishes cleanly the reading and closes the stream. + *

+ * See also {@link PngReaderInt} (esentially the same as this, and slightly preferred) and {@link PngReaderByte} (uses + * byte instead of int to store the samples). + */ +public class PngReader { + + // some performance/defensive limits + /** + * Defensive limit: refuse to read more than 900MB, can be changed with {@link #setMaxTotalBytesRead(long)} + */ + public static final long MAX_TOTAL_BYTES_READ_DEFAULT = 901001001L; // ~ 900MB + + /** + * Defensive limit: refuse to load more than 5MB of ancillary metadata, see {@link #setMaxBytesMetadata(long)} and + * also {@link #addChunkToSkip(String)} + */ + public static final long MAX_BYTES_METADATA_DEFAULT = 5024024; // for ancillary chunks + + /** + * Skip ancillary chunks greater than 2MB, see {@link #setSkipChunkMaxSize(long)} + */ + public static final long MAX_CHUNK_SIZE_SKIP = 2024024; // chunks exceeding this size will be skipped (nor even CRC + // checked) + + /** + * Basic image info - final and inmutable. + */ + public final ImageInfo imgInfo; // People always told me: be careful what you do, and don't go around declaring public + // fields... + /** + * flag: image was in interlaced format + */ + public final boolean interlaced; + + /** + * This object has most of the intelligence to parse the chunks and decompress the IDAT stream + */ + protected final ChunkSeqReaderPng chunkseq; + + /** + * Takes bytes from the InputStream and passes it to the ChunkSeqReaderPng. Never null. + */ + protected final BufferedStreamFeeder streamFeeder; + + /** + * @see #getMetadata() + */ + protected final PngMetadata metadata; // this a wrapper over chunks + + /** + * Current row number (reading or read), numbered from 0 + */ + protected int rowNum = -1; + + /** + * Represents the set of lines (rows) being read. Normally this works as a cursor, storing only one (the current) row. + * This stores several (perhaps all) rows only if calling {@link #readRows()} or for interlaced images (this later is + * transparent to the user) + */ + protected IImageLineSet imlinesSet; + + /** + * This factory decides the concrete type of the ImageLine that will be used. See {@link ImageLineSetDefault} for + * examples + */ + private IImageLineSetFactory imageLineSetFactory; + + CRC32 idatCrca;// for internal testing + Adler32 idatCrcb;// for internal testing + + /** + * Constructs a PngReader object from a stream, with default options. This reads the signature and the first IHDR + * chunk only. + *

+ * Warning: In case of exception the stream is NOT closed. + *

+ * Warning: By default the stream will be closed when this object is {@link #close()}d. See + * {@link #PngReader(InputStream,boolean)} or {@link #setShouldCloseStream(boolean)} + *

+ * + * @param inputStream PNG stream + */ + public PngReader(InputStream inputStream) { + this(inputStream, true); + } + + /** + * Same as {@link #PngReader(InputStream)} but allows to specify early if the stream must be closed + * + * @param inputStream + * @param shouldCloseStream The stream will be closed in case of exception (constructor included) or normal + * termination. + */ + public PngReader(InputStream inputStream, boolean shouldCloseStream) { + streamFeeder = new BufferedStreamFeeder(inputStream); + streamFeeder.setCloseStream(shouldCloseStream); + chunkseq = createChunkSeqReader(); + try { + streamFeeder.setFailIfNoFeed(true); + if (!streamFeeder.feedFixed(chunkseq, 36)) // 8+13+12=36 PNG signature+IHDR chunk + throw new PngjInputException("error reading first 21 bytes"); + imgInfo = chunkseq.getImageInfo(); + interlaced = chunkseq.getDeinterlacer() != null; + setMaxBytesMetadata(MAX_BYTES_METADATA_DEFAULT); + setMaxTotalBytesRead(MAX_TOTAL_BYTES_READ_DEFAULT); + setSkipChunkMaxSize(MAX_CHUNK_SIZE_SKIP); + chunkseq.addChunkToSkip(PngChunkFDAT.ID);// default: skip fdAT chunks! + chunkseq.addChunkToSkip(PngChunkFCTL.ID);// default: skip fctl chunks! + this.metadata = new PngMetadata(chunkseq.chunksList); + // sets a default factory (with ImageLineInt), + // this can be overwriten by a extended constructor, or by a setter + setLineSetFactory(ImageLineSetDefault.getFactoryInt()); + rowNum = -1; + } catch (RuntimeException e) { + streamFeeder.close(); + chunkseq.close(); + throw e; + } + } + + + /** + * Constructs a PngReader opening a file. Sets shouldCloseStream=true, so that the stream will be closed with + * this object. + * + * @param file PNG image file + */ + public PngReader(File file) { + this(PngHelperInternal.istreamFromFile(file), true); + } + + + /** + * Reads chunks before first IDAT. Normally this is called automatically + *

+ * Position before: after IDHR (crc included) Position after: just after the first IDAT chunk id + *

+ * This can be called several times (tentatively), it does nothing if already run + *

+ * (Note: when should this be called? in the constructor? hardly, because we loose the opportunity to call + * setChunkLoadBehaviour() and perhaps other settings before reading the first row? but sometimes we want to access + * some metadata (plte, phys) before. Because of this, this method can be called explicitly but is also called + * implicititly in some methods (getMetatada(), getChunksList()) + */ + protected void readFirstChunks() { + while (chunkseq.currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT) + if (streamFeeder.feed(chunkseq) <= 0) + throw new PngjInputException("premature ending reading first chunks"); + } + + /** + * Determines which ancillary chunks (metadata) are to be loaded and which skipped. + *

+ * Additional restrictions may apply. See also {@link #setChunksToSkip(String...)}, {@link #addChunkToSkip(String)}, + * {@link #setMaxBytesMetadata(long)}, {@link #setSkipChunkMaxSize(long)} + * + * @param chunkLoadBehaviour {@link ChunkLoadBehaviour} + */ + public void setChunkLoadBehaviour(ChunkLoadBehaviour chunkLoadBehaviour) { + this.chunkseq.setChunkLoadBehaviour(chunkLoadBehaviour); + } + + /** + * All loaded chunks (metada). If we have not yet end reading the image, this will include only the chunks before the + * pixels data (IDAT) + *

+ * Critical chunks are included, except that all IDAT chunks appearance are replaced by a single dummy-marker IDAT + * chunk. These might be copied to the PngWriter + *

+ * + * @see #getMetadata() + */ + public ChunksList getChunksList() { + return getChunksList(true); + } + + public ChunksList getChunksList(boolean forceLoadingOfFirstChunks) { + if (forceLoadingOfFirstChunks && chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + return chunkseq.chunksList; + } + + int getCurrentChunkGroup() { + return chunkseq.currentChunkGroup; + } + + /** + * High level wrapper over chunksList + * + * @see #getChunksList() + */ + public PngMetadata getMetadata() { + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + return metadata; + } + + /** + * Reads next row. + * + * The caller must know that there are more rows to read. + * + * @return Never null. Throws PngInputException if no more + */ + public IImageLine readRow() { + return readRow(rowNum + 1); + } + + /** + * True if last row has not yet been read + */ + public boolean hasMoreRows() { + return rowNum < getCurImgInfo().rows - 1; + } + + /** + * The row number is mostly meant as a check, the rows must be called in ascending order (not necessarily consecutive) + */ + public IImageLine readRow(int nrow) { + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + if (!interlaced) { + if (imlinesSet == null) + imlinesSet = createLineSet(true, -1, 0, 1); + IImageLine line = imlinesSet.getImageLine(nrow); + if (nrow == rowNum) + return line; // already read?? + else if (nrow < rowNum) + throw new PngjInputException("rows must be read in increasing order: " + nrow); + while (rowNum < nrow) { + while (!chunkseq.getIdatSet().isRowReady()) + if (streamFeeder.feed(chunkseq) < 1) + throw new PngjInputException("premature ending"); + rowNum++; + chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb); + if (rowNum == nrow) { + line.readFromPngRaw(chunkseq.getIdatSet().getUnfilteredRow(), + getCurImgInfo().bytesPerRow + 1, 0, 1); + line.endReadFromPngRaw(); + } + chunkseq.getIdatSet().advanceToNextRow(); + } + return line; + } else { // and now, for something completely different (interlaced!) + if (imlinesSet == null) { + imlinesSet = createLineSet(false, getCurImgInfo().rows, 0, 1); + loadAllInterlaced(getCurImgInfo().rows, 0, 1); + } + rowNum = nrow; + return imlinesSet.getImageLine(nrow); + } + + } + + /** + * Reads all rows in a ImageLineSet This is handy, but less memory-efficient (except for interlaced) + */ + public IImageLineSet readRows() { + return readRows(getCurImgInfo().rows, 0, 1); + } + + /** + * Reads a subset of rows. + *

+ * This method should called once, and not be mixed with {@link #readRow()} + * + * @param nRows how many rows to read (default: imageInfo.rows; negative: autocompute) + * @param rowOffset rows to skip (default:0) + * @param rowStep step between rows to load( default:1) + */ + public IImageLineSet readRows(int nRows, int rowOffset, int rowStep) { + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + if (nRows < 0) + nRows = (getCurImgInfo().rows - rowOffset) / rowStep; + if (rowStep < 1 || rowOffset < 0 || nRows == 0 + || nRows * rowStep + rowOffset > getCurImgInfo().rows) + throw new PngjInputException("bad args"); + if (rowNum >= rowOffset) + throw new PngjInputException("readRows cannot be mixed with readRow"); + imlinesSet = createLineSet(false, nRows, rowOffset, rowStep); + if (!interlaced) { + int m = -1; // last row already read in + while (m < nRows - 1) { + while (!chunkseq.getIdatSet().isRowReady()) + if (streamFeeder.feed(chunkseq) < 1) + throw new PngjInputException("Premature ending"); + rowNum++; + chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb); + m = (rowNum - rowOffset) / rowStep; + if (rowNum >= rowOffset && rowStep * m + rowOffset == rowNum) { + IImageLine line = imlinesSet.getImageLine(rowNum); + line.readFromPngRaw(chunkseq.getIdatSet().getUnfilteredRow(), + getCurImgInfo().bytesPerRow + 1, 0, 1); + line.endReadFromPngRaw(); + } + chunkseq.getIdatSet().advanceToNextRow(); + } + } else { // and now, for something completely different (interlaced) + loadAllInterlaced(nRows, rowOffset, rowStep); + } + chunkseq.getIdatSet().done(); + return imlinesSet; + } + + /** + * Sets the factory that creates the ImageLine. By default, this implementation uses ImageLineInt but this can be + * changed (at construction time or later) by calling this method. + *

+ * See also {@link #createLineSet(boolean, int, int, int)} + * + * @param factory + */ + public void setLineSetFactory(IImageLineSetFactory factory) { + imageLineSetFactory = factory; + } + + /** + * By default this uses the factory (which, by default creates ImageLineInt). You should rarely override this. + *

+ * See doc in {@link IImageLineSetFactory#create(ImageInfo, boolean, int, int, int)} + */ + protected IImageLineSet createLineSet(boolean singleCursor, int nlines, + int noffset, int step) { + return imageLineSetFactory.create(getCurImgInfo(), singleCursor, nlines, noffset, step); + } + + protected void loadAllInterlaced(int nRows, int rowOffset, int rowStep) { + IdatSet idat = chunkseq.getIdatSet(); + int nread = 0; + do { + while (!chunkseq.getIdatSet().isRowReady()) + if (streamFeeder.feed(chunkseq) <= 0) + break; + if (!chunkseq.getIdatSet().isRowReady()) + throw new PngjInputException("Premature ending?"); + chunkseq.getIdatSet().updateCrcs(idatCrca, idatCrcb); + int rowNumreal = idat.rowinfo.rowNreal; + boolean inset = imlinesSet.hasImageLine(rowNumreal); + if (inset) { + imlinesSet.getImageLine(rowNumreal).readFromPngRaw(idat.getUnfilteredRow(), + idat.rowinfo.buflen, idat.rowinfo.oX, idat.rowinfo.dX); + nread++; + } + idat.advanceToNextRow(); + } while (nread < nRows || !idat.isDone()); + idat.done(); + for (int i = 0, j = rowOffset; i < nRows; i++, j += rowStep) { + imlinesSet.getImageLine(j).endReadFromPngRaw(); + } + } + + /** + * Reads all the (remaining) file, skipping the pixels data. This is much more efficient that calling + * {@link #readRow()}, specially for big files (about 10 times faster!), because it doesn't even decompress the IDAT + * stream and disables CRC check Use this if you are not interested in reading pixels,only metadata. + */ + public void readSkippingAllRows() { + chunkseq.addChunkToSkip(PngChunkIDAT.ID); + chunkseq.addChunkToSkip(PngChunkFDAT.ID); + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + end(); + } + + /** + * Set total maximum bytes to read (0: unlimited; default: 200MB).
+ * These are the bytes read (not loaded) in the input stream. If exceeded, an exception will be thrown. + */ + public void setMaxTotalBytesRead(long maxTotalBytesToRead) { + chunkseq.setMaxTotalBytesRead(maxTotalBytesToRead); + } + + /** + * Set total maximum bytes to load from ancillary chunks (0: unlimited; default: 5Mb).
+ * If exceeded, some chunks will be skipped + */ + public void setMaxBytesMetadata(long maxBytesMetadata) { + chunkseq.setMaxBytesMetadata(maxBytesMetadata); + } + + /** + * Set maximum size in bytes for individual ancillary chunks (0: unlimited; default: 2MB).
+ * Chunks exceeding this length will be skipped (the CRC will not be checked) and the chunk will be saved as a + * PngChunkSkipped object. See also setSkipChunkIds + */ + public void setSkipChunkMaxSize(long skipChunkMaxSize) { + chunkseq.setSkipChunkMaxSize(skipChunkMaxSize); + } + + /** + * Chunks ids to be skipped.
+ * These chunks will be skipped (the CRC will not be checked) and the chunk will be saved as a PngChunkSkipped object. + * See also setSkipChunkMaxSize + */ + public void setChunksToSkip(String... chunksToSkip) { + chunkseq.setChunksToSkip(chunksToSkip); + } + + public void addChunkToSkip(String chunkToSkip) { + chunkseq.addChunkToSkip(chunkToSkip); + } + + public void dontSkipChunk(String chunkToSkip) { + chunkseq.dontSkipChunk(chunkToSkip); + } + + + /** + * if true, input stream will be closed after ending read + *

+ * default=true + */ + public void setShouldCloseStream(boolean shouldCloseStream) { + streamFeeder.setCloseStream(shouldCloseStream); + } + + /** + * Reads till end of PNG stream and call close() + * + * This should normally be called after reading the pixel data, to read the trailing chunks and close the stream. But + * it can be called at anytime. This will also read the first chunks if not still read, and skip pixels (IDAT) if + * still pending. + * + * If you want to read all metadata skipping pixels, readSkippingAllRows() is a little more efficient. + * + * If you want to abort immediately, call instead close() + */ + public void end() { + try { + if (chunkseq.firstChunksNotYetRead()) + readFirstChunks(); + if (chunkseq.getIdatSet() != null && !chunkseq.getIdatSet().isDone()) + chunkseq.getIdatSet().done(); + while (!chunkseq.isDone()) + if (streamFeeder.feed(chunkseq) <= 0) + break; + } finally { + close(); + } + } + + /** + * Releases resources, and closes stream if corresponds. Idempotent, secure, no exceptions. + * + * This can be also called for abort. It is recommended to call this in case of exceptions + */ + public void close() { + try { + if (chunkseq != null) + chunkseq.close(); + } catch (Exception e) { + PngHelperInternal.LOGGER.warning("error closing chunk sequence:" + e.getMessage()); + } + if (streamFeeder != null) + streamFeeder.close(); + } + + /** + * Interlaced PNG is accepted -though not welcomed- now... + */ + public boolean isInterlaced() { + return interlaced; + } + + /** + * Disables the CRC integrity check in IDAT chunks and ancillary chunks, this gives a slight increase in reading speed + * for big files + */ + public void setCrcCheckDisabled() { + chunkseq.setCheckCrc(false); + } + + /** + * Gets wrapped {@link ChunkSeqReaderPng} object + */ + public ChunkSeqReaderPng getChunkseq() { + return chunkseq; + } + + /** called on construction time. Override if you want an alternative class */ + protected ChunkSeqReaderPng createChunkSeqReader() { + return new ChunkSeqReaderPng(false); + } + + + /** + * Enables and prepare the simple digest computation. Must be called before reading the pixels. See + * {@link #getSimpleDigestHex()} + */ + public void prepareSimpleDigestComputation() { + if (idatCrca == null) + idatCrca = new CRC32(); + else + idatCrca.reset(); + if (idatCrcb == null) + idatCrcb = new Adler32(); + else + idatCrcb.reset(); + imgInfo.updateCrc(idatCrca); + idatCrcb.update((byte) imgInfo.rows); // not important + } + + long getSimpleDigest() { + if (idatCrca == null) + return 0; + else + return (idatCrca.getValue() ^ (idatCrcb.getValue() << 31)); + } + + /** + * Pseudo 64-bits digest computed over the basic image properties and the raw pixels data: it should coincide for + * equivalent images encoded with different filters and compressors; but will not coincide for + * interlaced/non-interlaced; also, this does not take into account the palette info. This will be valid only if + * {@link #prepareSimpleDigestComputation()} has been called, and all rows have been read. Not fool-proof, not + * cryptografically secure, only for informal testing and duplicates detection. + * + * @return A 64-digest in hexadecimal + */ + public String getSimpleDigestHex() { + return String.format("%016X", getSimpleDigest()); + } + + /** + * Basic info, for debugging. + */ + public String toString() { // basic info + return imgInfo.toString() + " interlaced=" + interlaced; + } + + /** + * Basic info, in a compact format, apt for scripting COLSxROWS[dBITDEPTH][a][p][g][i] ( the default dBITDEPTH='d8' is + * ommited) + * + */ + public String toStringCompact() { + return imgInfo.toStringBrief() + (interlaced ? "i" : ""); + } + + public ImageInfo getImgInfo() { + return imgInfo; + } + + public ImageInfo getCurImgInfo() { + return chunkseq.getCurImgInfo(); + } + + + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngReaderApng.java b/src/js-specific/java/ar/com/hjg/pngj/PngReaderApng.java new file mode 100644 index 00000000..105edab7 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngReaderApng.java @@ -0,0 +1,213 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.InputStream; +import java.util.List; + +import ar.com.hjg.pngj.chunks.PngChunk; +import ar.com.hjg.pngj.chunks.PngChunkACTL; +import ar.com.hjg.pngj.chunks.PngChunkFCTL; +import ar.com.hjg.pngj.chunks.PngChunkFDAT; +import ar.com.hjg.pngj.chunks.PngChunkIDAT; + +/** + */ +public class PngReaderApng extends PngReaderByte { + + public PngReaderApng(File file) { + super(file); + dontSkipChunk(PngChunkFCTL.ID); + } + + public PngReaderApng(InputStream inputStream) { + super(inputStream); + dontSkipChunk(PngChunkFCTL.ID); + } + + private Boolean apngKind = null; + private boolean firsIdatApngFrame = false; + protected PngChunkACTL actlChunk; // null if not APNG + private PngChunkFCTL fctlChunk; // current (null for the pseudo still frame) + + /** + * Current frame number (reading or read). First animated frame is 0. Frame -1 represents the IDAT (default image) + * when it's not part of the animation + */ + protected int frameNum = -1; // incremented after each fctl finding + + public boolean isApng() { + if (apngKind == null) { + // this triggers the loading of first chunks; + actlChunk = (PngChunkACTL) getChunksList().getById1(PngChunkACTL.ID); // null if not apng + apngKind = actlChunk != null; + firsIdatApngFrame = fctlChunk != null; + + } + return apngKind.booleanValue(); + } + + + public void advanceToFrame(int frame) { + if (frame < frameNum) + throw new PngjInputException("Cannot go backwards"); + if (frame >= getApngNumFrames()) + throw new PngjInputException("Frame out of range " + frame); + if (frame > frameNum) { + addChunkToSkip(PngChunkIDAT.ID); + addChunkToSkip(PngChunkFDAT.ID); + if (chunkseq.getIdatSet() != null && !chunkseq.getIdatSet().isDone()) + chunkseq.getIdatSet().done(); // seems to be necessary sometimes (we should check this) + while (frameNum < frame & !chunkseq.isDone()) + if (streamFeeder.feed(chunkseq) <= 0) + break; + } + if (frame == frameNum) { // prepare to read rows. at this point we have a new + dontSkipChunk(PngChunkIDAT.ID); + dontSkipChunk(PngChunkFDAT.ID); + rowNum = -1; + imlinesSet = null;// force recreation (this is slightly dirty) + // seek the next IDAT/fDAT - TODO: set the expected sequence number + while (!chunkseq.isDone() && !chunkseq.getCurChunkReader().isFromDeflatedSet()) + if (streamFeeder.feed(chunkseq) <= 0) + break; + } else { + throw new PngjInputException("unexpected error seeking from frame " + frame); + } + } + + /** + * True if it has a default image (IDAT) that is not part of the animation. In that case, we consider it as a + * pseudo-frame (number -1) + */ + public boolean hasExtraStillImage() { + return isApng() && !firsIdatApngFrame; + } + + /** + * Only counts true animation frames. + */ + public int getApngNumFrames() { + if (isApng()) + return actlChunk.getNumFrames(); + else + return 0; + } + + /** + * 0 if it's to been played infinitely. -1 if not APNG + */ + public int getApngNumPlays() { + if (isApng()) + return actlChunk.getNumPlays(); + else + return -1; + } + + @Override + public IImageLine readRow() { + // TODO Auto-generated method stub + return super.readRow(); + } + + @Override + public boolean hasMoreRows() { + // TODO Auto-generated method stub + return super.hasMoreRows(); + } + + @Override + public IImageLine readRow(int nrow) { + // TODO Auto-generated method stub + return super.readRow(nrow); + } + + @Override + public IImageLineSet readRows() { + // TODO Auto-generated method stub + return super.readRows(); + } + + @Override + public IImageLineSet readRows(int nRows, int rowOffset, int rowStep) { + // TODO Auto-generated method stub + return super.readRows(nRows, rowOffset, rowStep); + } + + @Override + public void readSkippingAllRows() { + // TODO Auto-generated method stub + super.readSkippingAllRows(); + } + + @Override + protected ChunkSeqReaderPng createChunkSeqReader() { + ChunkSeqReaderPng cr = new ChunkSeqReaderPng(false) { + + @Override + public boolean shouldSkipContent(int len, String id) { + return super.shouldSkipContent(len, id); + } + + @Override + protected boolean isIdatKind(String id) { + return id.equals(PngChunkIDAT.ID) || id.equals(PngChunkFDAT.ID); + } + + @Override + protected DeflatedChunksSet createIdatSet(String id) { + IdatSet ids = new IdatSet(id, getCurImgInfo(), deinterlacer); + ids.setCallbackMode(callbackMode); + return ids; + } + + + @Override + protected void startNewChunk(int len, String id, long offset) { + super.startNewChunk(len, id, offset); + } + + @Override + protected void postProcessChunk(ChunkReader chunkR) { + super.postProcessChunk(chunkR); + if (chunkR.getChunkRaw().id.equals(PngChunkFCTL.ID)) { + frameNum++; + List chunkslist = chunkseq.getChunks(); + fctlChunk = (PngChunkFCTL) chunkslist.get(chunkslist.size() - 1); + // as this is slightly dirty, we check + if (chunkR.getChunkRaw().getOffset() != fctlChunk.getRaw().getOffset()) + throw new PngjInputException("something went wrong"); + ImageInfo frameInfo = fctlChunk.getEquivImageInfo(); + getChunkseq().updateCurImgInfo(frameInfo); + } + } + + @Override + protected boolean countChunkTypeAsAncillary(String id) { + // we don't count fdat as ancillary data + return super.countChunkTypeAsAncillary(id) && !id.equals(id.equals(PngChunkFDAT.ID)); + } + + }; + return cr; + } + + /** + * @see #frameNum + */ + public int getFrameNum() { + return frameNum; + } + + @Override + public void end() { + // TODO Auto-generated method stub + super.end(); + } + + public PngChunkFCTL getFctl() { + return fctlChunk; + } + + + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngReaderByte.java b/src/js-specific/java/ar/com/hjg/pngj/PngReaderByte.java new file mode 100644 index 00000000..3b34fdf6 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngReaderByte.java @@ -0,0 +1,30 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.InputStream; + +/** + * Trivial extension of {@link PngReader} that uses {@link ImageLineByte} + *

+ * The factory is set at construction time. Remember that this could still be changed at runtime. + */ +public class PngReaderByte extends PngReader { + + public PngReaderByte(File file) { + super(file); + setLineSetFactory(ImageLineSetDefault.getFactoryByte()); + } + + public PngReaderByte(InputStream inputStream) { + super(inputStream); + setLineSetFactory(ImageLineSetDefault.getFactoryByte()); + } + + /** + * Utility method that casts {@link #readRow()} return to {@link ImageLineByte}. + */ + public ImageLineByte readRowByte() { + return (ImageLineByte) readRow(); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngReaderFilter.java b/src/js-specific/java/ar/com/hjg/pngj/PngReaderFilter.java new file mode 100644 index 00000000..cafe2d4c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngReaderFilter.java @@ -0,0 +1,99 @@ +package ar.com.hjg.pngj; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import ar.com.hjg.pngj.chunks.PngChunk; + +/** + * This class allows to use a simple PNG reader as an input filter, wrapping a ChunkSeqReaderPng in callback mode. + * + * In this sample implementation, all IDAT chunks are skipped and the rest are stored. An example of use, that lets us + * grab the Metadata and let the pixels go towards a BufferedImage: + * + * + *

+ * PngReaderFilter reader = new PngReaderFilter(new FileInputStream("image.png"));
+ * BufferedImage image1 = ImageIO.read(reader);
+ * reader.readUntilEndAndClose(); // in case ImageIO.read() does not read the traling chunks (it happens)
+ * System.out.println(reader.getChunksList());
+ * 
+ * + */ +public class PngReaderFilter extends FilterInputStream { + + private ChunkSeqReaderPng chunkseq; + + public PngReaderFilter(InputStream arg0) { + super(arg0); + chunkseq = createChunkSequenceReader(); + } + + protected ChunkSeqReaderPng createChunkSequenceReader() { + return new ChunkSeqReaderPng(true) { + @Override + public boolean shouldSkipContent(int len, String id) { + return super.shouldSkipContent(len, id) || id.equals("IDAT"); + } + + @Override + protected boolean shouldCheckCrc(int len, String id) { + return false; + } + + @Override + protected void postProcessChunk(ChunkReader chunkR) { + super.postProcessChunk(chunkR); + // System.out.println("processed chunk " + chunkR.getChunkRaw().id); + } + }; + } + + @Override + public void close() throws IOException { + super.close(); + chunkseq.close(); + } + + @Override + public int read() throws IOException { + int r = super.read(); + if (r > 0) + chunkseq.feedAll(new byte[] {(byte) r}, 0, 1); + return r; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int res = super.read(b, off, len); + if (res > 0) + chunkseq.feedAll(b, off, res); + return res; + } + + @Override + public int read(byte[] b) throws IOException { + int res = super.read(b); + if (res > 0) + chunkseq.feedAll(b, 0, res); + return res; + } + + public void readUntilEndAndClose() throws IOException { + BufferedStreamFeeder br = new BufferedStreamFeeder(this.in); + while ((!chunkseq.isDone()) && br.hasMoreToFeed()) + br.feed(chunkseq); + close(); + } + + public List getChunksList() { + return chunkseq.getChunks(); + } + + public ChunkSeqReaderPng getChunkseq() { + return chunkseq; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngReaderInt.java b/src/js-specific/java/ar/com/hjg/pngj/PngReaderInt.java new file mode 100644 index 00000000..9f18cfb6 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngReaderInt.java @@ -0,0 +1,37 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.InputStream; + +/** + * Trivial extension of {@link PngReader} that uses {@link ImageLineInt}. + *

+ * In the current implementation this is quite dummy/redundant, because (for backward compatibility) PngReader already + * uses a {@link ImageLineInt}. + *

+ * The factory is set at construction time. Remember that this could still be changed at runtime. + */ +public class PngReaderInt extends PngReader { + + public PngReaderInt(File file) { + super(file); // not necessary to set factory, PngReader already does that + } + + public PngReaderInt(InputStream inputStream) { + super(inputStream); + } + + /** + * Utility method that casts the IImageLine to a ImageLineInt + * + * This only make sense for this concrete class + * + */ + public ImageLineInt readRowInt() { + IImageLine line = readRow(); + if (line instanceof ImageLineInt) + return (ImageLineInt) line; + else + throw new PngjException("This is not a ImageLineInt : " + line.getClass()); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngWriter.java b/src/js-specific/java/ar/com/hjg/pngj/PngWriter.java new file mode 100644 index 00000000..c8906906 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngWriter.java @@ -0,0 +1,427 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.OutputStream; +import java.util.List; + +import ar.com.hjg.pngj.chunks.ChunkCopyBehaviour; +import ar.com.hjg.pngj.chunks.ChunkPredicate; +import ar.com.hjg.pngj.chunks.ChunksList; +import ar.com.hjg.pngj.chunks.ChunksListForWrite; +import ar.com.hjg.pngj.chunks.PngChunk; +import ar.com.hjg.pngj.chunks.PngChunkIEND; +import ar.com.hjg.pngj.chunks.PngChunkIHDR; +import ar.com.hjg.pngj.chunks.PngChunkPLTE; +import ar.com.hjg.pngj.chunks.PngMetadata; +import ar.com.hjg.pngj.pixels.PixelsWriter; +import ar.com.hjg.pngj.pixels.PixelsWriterDefault; + +/** + * Writes a PNG image, line by line. + */ +public class PngWriter { + + public final ImageInfo imgInfo; + + /** + * last writen row number, starting from 0 + */ + protected int rowNum = -1; + + private final ChunksListForWrite chunksList; + + private final PngMetadata metadata; + + /** + * Current chunk grounp, (0-6) already written or currently writing (this is advanced when just starting to write the + * new group, not when finalizing the previous) + *

+ * see {@link ChunksList} + */ + protected int currentChunkGroup = -1; + + private int passes = 1; // Some writes might require two passes (NOT USED STILL) + private int currentpass = 0; // numbered from 1 + + private boolean shouldCloseStream = true; + + private int idatMaxSize = 0; // 0=use default (PngIDatChunkOutputStream 64k) + // private PngIDatChunkOutputStream datStream; + + protected PixelsWriter pixelsWriter; + + private final OutputStream os; + + private ChunkPredicate copyFromPredicate = null; + private ChunksList copyFromList = null; + + protected StringBuilder debuginfo = new StringBuilder(); + + /** + * Opens a file for writing. + *

+ * Sets shouldCloseStream=true. For more info see {@link #PngWriter(OutputStream, ImageInfo)} + * + * @param file + * @param imgInfo + * @param allowoverwrite If false and file exists, an {@link PngjOutputException} is thrown + */ + public PngWriter(File file, ImageInfo imgInfo, boolean allowoverwrite) { + this(PngHelperInternal.ostreamFromFile(file, allowoverwrite), imgInfo); + setShouldCloseStream(true); + } + + /** + * @see #PngWriter(File, ImageInfo, boolean) (overwrite=true) + */ + public PngWriter(File file, ImageInfo imgInfo) { + this(file, imgInfo, true); + } + + /** + * Constructs a new PngWriter from a output stream. After construction nothing is writen yet. You still can set some + * parameters (compression, filters) and queue chunks before start writing the pixels. + *

+ * + * @param outputStream Open stream for binary writing + * @param imgInfo Basic image parameters + */ + public PngWriter(OutputStream outputStream, ImageInfo imgInfo) { + this.os = outputStream; + this.imgInfo = imgInfo; + // prealloc + chunksList = new ChunksListForWrite(imgInfo); + metadata = new PngMetadata(chunksList); + pixelsWriter = createPixelsWriter(imgInfo); + setCompLevel(9); + } + + private void initIdat() { // this triggers the writing of first chunks + pixelsWriter.setOs(this.os); + pixelsWriter.setIdatMaxSize(idatMaxSize); + writeSignatureAndIHDR(); + writeFirstChunks(); + } + + private void writeEndChunk() { + currentChunkGroup = ChunksList.CHUNK_GROUP_6_END; + PngChunkIEND c = new PngChunkIEND(imgInfo); + c.createRawChunk().writeChunk(os); + chunksList.getChunks().add(c); + } + + private void writeFirstChunks() { + if (currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT) + return; + int nw = 0; + currentChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + queueChunksFromOther(); + nw = chunksList.writeChunks(os, currentChunkGroup); + currentChunkGroup = ChunksList.CHUNK_GROUP_2_PLTE; + nw = chunksList.writeChunks(os, currentChunkGroup); + if (nw > 0 && imgInfo.greyscale) + throw new PngjOutputException("cannot write palette for this format"); + if (nw == 0 && imgInfo.indexed) + throw new PngjOutputException("missing palette"); + currentChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE; + nw = chunksList.writeChunks(os, currentChunkGroup); + } + + private void writeLastChunks() { // not including end + currentChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; + queueChunksFromOther(); + chunksList.writeChunks(os, currentChunkGroup); + // should not be unwriten chunks + List pending = chunksList.getQueuedChunks(); + if (!pending.isEmpty()) + throw new PngjOutputException(pending.size() + " chunks were not written! Eg: " + + pending.get(0).toString()); + } + + /** + * Write id signature and also "IHDR" chunk + */ + private void writeSignatureAndIHDR() { + PngHelperInternal.writeBytes(os, PngHelperInternal.getPngIdSignature()); // signature + currentChunkGroup = ChunksList.CHUNK_GROUP_0_IDHR; + PngChunkIHDR ihdr = new PngChunkIHDR(imgInfo); + // http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html + ihdr.createRawChunk().writeChunk(os); + chunksList.getChunks().add(ihdr); + } + + private void queueChunksFromOther() { + if (copyFromList == null || copyFromPredicate == null) + return; + boolean idatDone = currentChunkGroup >= ChunksList.CHUNK_GROUP_4_IDAT; // we assume this method is not either before + // or after the IDAT writing, not in the + // middle! + for (PngChunk chunk : copyFromList.getChunks()) { + if (chunk.getRaw().data == null) + continue; // we cannot copy skipped chunks? + int groupOri = chunk.getChunkGroup(); + if (groupOri <= ChunksList.CHUNK_GROUP_4_IDAT && idatDone) + continue; + if (groupOri >= ChunksList.CHUNK_GROUP_4_IDAT && !idatDone) + continue; + if (chunk.crit && !chunk.id.equals(PngChunkPLTE.ID)) + continue; // critical chunks (except perhaps PLTE) are never + // copied + boolean copy = copyFromPredicate.match(chunk); + if (copy) { + // but if the chunk is already queued or writen, it's ommited! + if (chunksList.getEquivalent(chunk).isEmpty() + && chunksList.getQueuedEquivalent(chunk).isEmpty()) { + chunksList.queue(chunk); + } + } + } + } + +/** + * Queues an ancillary chunk for writing. + *

+ * If a "equivalent" chunk is already queued (see {@link ChunkHelper#equivalent(PngChunk, PngChunk)), this overwrites it. + *

+ * The chunk will be written as late as possible, unless the priority is set. + * + * @param chunk + */ + public void queueChunk(PngChunk chunk) { + for (PngChunk other : chunksList.getQueuedEquivalent(chunk)) { + getChunksList().removeChunk(other); + } + chunksList.queue(chunk); + } + + /** + * Sets an origin (typically from a {@link PngReader}) of Chunks to be copied. This should be called only once, before + * starting writing the rows. It doesn't matter the current state of the PngReader reading, this is a live object and + * what matters is that when the writer writes the pixels (IDAT) the reader has already read them, and that when the + * writer ends, the reader is already ended (all this is very natural). + *

+ * Apart from the copyMask, there is some addional heuristics: + *

+ * - The chunks will be queued, but will be written as late as possible (unless you explicitly set priority=true) + *

+ * - The chunk will not be queued if an "equivalent" chunk was already queued explicitly. And it will be overwriten + * another is queued explicitly. + * + * @param chunks + * @param copyMask Some bitmask from {@link ChunkCopyBehaviour} + * + * @see #copyChunksFrom(ChunksList, ChunkPredicate) + */ + public void copyChunksFrom(ChunksList chunks, int copyMask) { + copyChunksFrom(chunks, ChunkCopyBehaviour.createPredicate(copyMask, imgInfo)); + } + + /** + * Copy all chunks from origin. See {@link #copyChunksFrom(ChunksList, int)} for more info + */ + public void copyChunksFrom(ChunksList chunks) { + copyChunksFrom(chunks, ChunkCopyBehaviour.COPY_ALL); + } + + /** + * Copy chunks from origin depending on some {@link ChunkPredicate} + * + * @param chunks + * @param predicate The chunks (ancillary or PLTE) will be copied if and only if predicate matches + * + * @see #copyChunksFrom(ChunksList, int) for more info + */ + public void copyChunksFrom(ChunksList chunks, ChunkPredicate predicate) { + if (copyFromList != null && chunks != null) + PngHelperInternal.LOGGER.warning("copyChunksFrom should only be called once"); + if (predicate == null) + throw new PngjOutputException("copyChunksFrom requires a predicate"); + this.copyFromList = chunks; + this.copyFromPredicate = predicate; + } + + /** + * Computes compressed size/raw size, approximate. + *

+ * Actually: compressed size = total size of IDAT data , raw size = uncompressed pixel bytes = rows * (bytesPerRow + + * 1). + * + * This must be called after pngw.end() + */ + public double computeCompressionRatio() { + if (currentChunkGroup < ChunksList.CHUNK_GROUP_5_AFTERIDAT) + throw new PngjOutputException("must be called after end()"); + return pixelsWriter.getCompression(); + } + + /** + * Finalizes all the steps and closes the stream. This must be called after writing the lines. Idempotent + */ + public void end() { + if (rowNum != imgInfo.rows - 1 || !pixelsWriter.isDone()) + throw new PngjOutputException("all rows have not been written"); + try { + if (pixelsWriter != null) + pixelsWriter.close(); + if (currentChunkGroup < ChunksList.CHUNK_GROUP_5_AFTERIDAT) + writeLastChunks(); + if (currentChunkGroup < ChunksList.CHUNK_GROUP_6_END) + writeEndChunk(); + } finally { + close(); + } + } + + /** + * Closes and releases resources + *

+ * This is normally called internally from {@link #end()}, you should only call this for aborting the writing and + * release resources (close the stream). + *

+ * Idempotent and secure - never throws exceptions + */ + public void close() { + if (pixelsWriter != null) + pixelsWriter.close(); + if (shouldCloseStream && os != null) + try { + os.close(); + } catch (Exception e) { + PngHelperInternal.LOGGER.warning("Error closing writer " + e.toString()); + } + } + + /** + * returns the chunks list (queued and writen chunks) + */ + public ChunksListForWrite getChunksList() { + return chunksList; + } + + /** + * Retruns a high level wrapper over for metadata handling + */ + public PngMetadata getMetadata() { + return metadata; + } + + /** + * Sets internal prediction filter type, or strategy to choose it. + *

+ * This must be called just after constructor, before starting writing. + *

+ */ + public void setFilterType(FilterType filterType) { + pixelsWriter.setFilterType(filterType); + } + + /** + * This is kept for backwards compatibility, now the PixelsWriter object should be used for setting + * compression/filtering options + * + * @see PixelsWriter#setCompressionFactor(double) + * @param compLevel between 0 (no compression, max speed) and 9 (max compression) + */ + public void setCompLevel(int complevel) { + pixelsWriter.setDeflaterCompLevel(complevel); + } + + /** + * + */ + public void setFilterPreserve(boolean filterPreserve) { + if (filterPreserve) + pixelsWriter.setFilterType(FilterType.FILTER_PRESERVE); + else if (pixelsWriter.getFilterType() == null) + pixelsWriter.setFilterType(FilterType.FILTER_DEFAULT); + } + + /** + * Sets maximum size of IDAT fragments. Incrementing this from the default has very little effect on compression and + * increments memory usage. You should rarely change this. + *

+ * + * @param idatMaxSize default=0 : use defaultSize (32K) + */ + public void setIdatMaxSize(int idatMaxSize) { + this.idatMaxSize = idatMaxSize; + } + + /** + * If true, output stream will be closed after ending write + *

+ * default=true + */ + public void setShouldCloseStream(boolean shouldCloseStream) { + this.shouldCloseStream = shouldCloseStream; + } + + /** + * Writes next row, does not check row number. + * + * @param imgline + */ + public void writeRow(IImageLine imgline) { + writeRow(imgline, rowNum + 1); + } + + /** + * Writes the full set of row. The ImageLineSet should contain (allow to acces) imgInfo.rows + */ + public void writeRows(IImageLineSet imglines) { + for (int i = 0; i < imgInfo.rows; i++) + writeRow(imglines.getImageLineRawNum(i)); + } + + public void writeRow(IImageLine imgline, int rownumber) { + rowNum++; + if (rowNum == imgInfo.rows) + rowNum = 0; + if (rownumber == imgInfo.rows) + rownumber = 0; + if (rownumber >= 0 && rowNum != rownumber) + throw new PngjOutputException("rows must be written in order: expected:" + rowNum + + " passed:" + rownumber); + if (rowNum == 0) + currentpass++; + if (rownumber == 0 && currentpass == passes) { + initIdat(); + currentChunkGroup = ChunksList.CHUNK_GROUP_4_IDAT; // we just begin writing IDAT + } + byte[] rowb = pixelsWriter.getRowb(); + imgline.writeToPngRaw(rowb); + pixelsWriter.processRow(rowb); + + } + + /** + * Utility method, uses internaly a ImageLineInt + */ + public void writeRowInt(int[] buf) { + writeRow(new ImageLineInt(imgInfo, buf)); + } + + /** + * Factory method for pixels writer. This will be called once at the moment at start writing a set of IDAT chunks + * (typically once in a normal PNG) + * + * This should be overriden if custom filtering strategies are desired. Remember to release this with close() + * + * @param imginfo Might be different than that of this object (eg: APNG with subimages) + * @param os Output stream + * @return new PixelsWriter. Don't forget to call close() when discarding it + */ + protected PixelsWriter createPixelsWriter(ImageInfo imginfo) { + PixelsWriterDefault pw = new PixelsWriterDefault(imginfo); + return pw; + } + + public final PixelsWriter getPixelsWriter() { + return pixelsWriter; + } + + public String getDebuginfo() { + return debuginfo.toString(); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngWriterHc.java b/src/js-specific/java/ar/com/hjg/pngj/PngWriterHc.java new file mode 100644 index 00000000..cc83df84 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngWriterHc.java @@ -0,0 +1,35 @@ +package ar.com.hjg.pngj; + +import java.io.File; +import java.io.OutputStream; + +import ar.com.hjg.pngj.pixels.PixelsWriter; +import ar.com.hjg.pngj.pixels.PixelsWriterMultiple; + +/** Pngwriter with High compression EXPERIMENTAL */ +public class PngWriterHc extends PngWriter { + + public PngWriterHc(File file, ImageInfo imgInfo, boolean allowoverwrite) { + super(file, imgInfo, allowoverwrite); + setFilterType(FilterType.FILTER_SUPER_ADAPTIVE); + } + + public PngWriterHc(File file, ImageInfo imgInfo) { + super(file, imgInfo); + } + + public PngWriterHc(OutputStream outputStream, ImageInfo imgInfo) { + super(outputStream, imgInfo); + } + + @Override + protected PixelsWriter createPixelsWriter(ImageInfo imginfo) { + PixelsWriterMultiple pw = new PixelsWriterMultiple(imginfo); + return pw; + } + + public PixelsWriterMultiple getPixelWriterMultiple() { + return (PixelsWriterMultiple) pixelsWriter; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngjBadCrcException.java b/src/js-specific/java/ar/com/hjg/pngj/PngjBadCrcException.java new file mode 100644 index 00000000..8e337a0c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngjBadCrcException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +/** + * Exception thrown by bad CRC check + */ +public class PngjBadCrcException extends PngjInputException { + private static final long serialVersionUID = 1L; + + public PngjBadCrcException(String message, Throwable cause) { + super(message, cause); + } + + public PngjBadCrcException(String message) { + super(message); + } + + public PngjBadCrcException(Throwable cause) { + super(cause); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngjException.java b/src/js-specific/java/ar/com/hjg/pngj/PngjException.java new file mode 100644 index 00000000..8471c70a --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngjException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +/** + * Generic exception for this library. It's a RuntimeException (unchecked) + */ +public class PngjException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public PngjException(String message, Throwable cause) { + super(message, cause); + } + + public PngjException(String message) { + super(message); + } + + public PngjException(Throwable cause) { + super(cause); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngjExceptionInternal.java b/src/js-specific/java/ar/com/hjg/pngj/PngjExceptionInternal.java new file mode 100644 index 00000000..8059e35e --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngjExceptionInternal.java @@ -0,0 +1,23 @@ +package ar.com.hjg.pngj; + +/** + * Exception for anomalous internal problems (sort of asserts) that point to some issue with the library + * + * @author Hernan J Gonzalez + * + */ +public class PngjExceptionInternal extends RuntimeException { + private static final long serialVersionUID = 1L; + + public PngjExceptionInternal(String message, Throwable cause) { + super(message, cause); + } + + public PngjExceptionInternal(String message) { + super(message); + } + + public PngjExceptionInternal(Throwable cause) { + super(cause); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngjInputException.java b/src/js-specific/java/ar/com/hjg/pngj/PngjInputException.java new file mode 100644 index 00000000..668d6b68 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngjInputException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +/** + * Exception thrown when reading a PNG. + */ +public class PngjInputException extends PngjException { + private static final long serialVersionUID = 1L; + + public PngjInputException(String message, Throwable cause) { + super(message, cause); + } + + public PngjInputException(String message) { + super(message); + } + + public PngjInputException(Throwable cause) { + super(cause); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngjOutputException.java b/src/js-specific/java/ar/com/hjg/pngj/PngjOutputException.java new file mode 100644 index 00000000..6fea798c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngjOutputException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj; + +/** + * Exception thrown by writing process + */ +public class PngjOutputException extends PngjException { + private static final long serialVersionUID = 1L; + + public PngjOutputException(String message, Throwable cause) { + super(message, cause); + } + + public PngjOutputException(String message) { + super(message); + } + + public PngjOutputException(Throwable cause) { + super(cause); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/PngjUnsupportedException.java b/src/js-specific/java/ar/com/hjg/pngj/PngjUnsupportedException.java new file mode 100644 index 00000000..46290c20 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/PngjUnsupportedException.java @@ -0,0 +1,24 @@ +package ar.com.hjg.pngj; + +/** + * Exception thrown because of some valid feature of PNG standard that this library does not support. + */ +public class PngjUnsupportedException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public PngjUnsupportedException() { + super(); + } + + public PngjUnsupportedException(String message, Throwable cause) { + super(message, cause); + } + + public PngjUnsupportedException(String message) { + super(message); + } + + public PngjUnsupportedException(Throwable cause) { + super(cause); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/RowInfo.java b/src/js-specific/java/ar/com/hjg/pngj/RowInfo.java new file mode 100644 index 00000000..e033989c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/RowInfo.java @@ -0,0 +1,55 @@ +package ar.com.hjg.pngj; + +/** + * Packs information of current row. Only used internally + */ +class RowInfo { + public final ImageInfo imgInfo; + public final Deinterlacer deinterlacer; + public final boolean imode; // Interlaced + int dY, dX, oY, oX; // current step and offset (in pixels) + int rowNseq; // row number (from 0) in sequential read order + int rowNreal; // row number in the real image + int rowNsubImg; // current row in the virtual subsampled image; this increments (by 1) from 0 to + // rows/dy 7 times + int rowsSubImg, colsSubImg; // size of current subimage , in pixels + int bytesRow; + int pass; // 1-7 + byte[] buf; // non-deep copy + int buflen; // valid bytes in buffer (include filter byte) + + public RowInfo(ImageInfo imgInfo, Deinterlacer deinterlacer) { + this.imgInfo = imgInfo; + this.deinterlacer = deinterlacer; + this.imode = deinterlacer != null; + } + + void update(int rowseq) { + rowNseq = rowseq; + if (imode) { + pass = deinterlacer.getPass(); + dX = deinterlacer.dX; + dY = deinterlacer.dY; + oX = deinterlacer.oX; + oY = deinterlacer.oY; + rowNreal = deinterlacer.getCurrRowReal(); + rowNsubImg = deinterlacer.getCurrRowSubimg(); + rowsSubImg = deinterlacer.getRows(); + colsSubImg = deinterlacer.getCols(); + bytesRow = (imgInfo.bitspPixel * colsSubImg + 7) / 8; + } else { + pass = 1; + dX = dY = 1; + oX = oY = 0; + rowNreal = rowNsubImg = rowseq; + rowsSubImg = imgInfo.rows; + colsSubImg = imgInfo.cols; + bytesRow = imgInfo.bytesPerRow; + } + } + + void updateBuf(byte[] buf, int buflen) { + this.buf = buf; + this.buflen = buflen; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkCopyBehaviour.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkCopyBehaviour.java new file mode 100644 index 00000000..9e292f50 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkCopyBehaviour.java @@ -0,0 +1,101 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngReader; +import ar.com.hjg.pngj.PngWriter; + +/** + * Chunk copy policy to apply when copyng from a {@link PngReader} to a {@link PngWriter}. + *

+ * The constants are bit-masks, they can be OR-ed + *

+ * Reference: http://www.w3.org/TR/PNG/#14
+ */ +public class ChunkCopyBehaviour { + + /** Don't copy anything */ + public static final int COPY_NONE = 0; + + /** copy the palette */ + public static final int COPY_PALETTE = 1; + + /** copy all 'safe to copy' chunks */ + public static final int COPY_ALL_SAFE = 1 << 2; + + /** + * copy all, including palette + */ + public static final int COPY_ALL = 1 << 3; // includes palette! + /** + * Copy PHYS chunk (physical resolution) + */ + public static final int COPY_PHYS = 1 << 4; // dpi + /** + * Copy al textual chunks. + */ + public static final int COPY_TEXTUAL = 1 << 5; // all textual types + /** + * Copy TRNS chunk + */ + public static final int COPY_TRANSPARENCY = 1 << 6; // + /** + * Copy unknown chunks (unknown by our factory) + */ + public static final int COPY_UNKNOWN = 1 << 7; // all unknown (by the factory!) + /** + * Copy almost all: excepts only HIST (histogram) TIME and TEXTUAL chunks + */ + public static final int COPY_ALMOSTALL = 1 << 8; + + private static boolean maskMatch(int v, int mask) { + return (v & mask) != 0; + } + + /** + * Creates a predicate equivalent to the copy mask + *

+ * Given a copy mask (see static fields) and the ImageInfo of the target PNG, returns a predicate that tells if a + * chunk should be copied. + *

+ * This is a handy helper method, you can also create and set your own predicate + */ + public static ChunkPredicate createPredicate(final int copyFromMask, final ImageInfo imgInfo) { + return new ChunkPredicate() { + public boolean match(PngChunk chunk) { + if (chunk.crit) { + if (chunk.id.equals(ChunkHelper.PLTE)) { + if (imgInfo.indexed && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_PALETTE)) + return true; + if (!imgInfo.greyscale && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALL)) + return true; + } + } else { // ancillary + boolean text = (chunk instanceof PngChunkTextVar); + boolean safe = chunk.safe; + // notice that these if are not exclusive + if (maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALL)) + return true; + if (safe && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALL_SAFE)) + return true; + if (chunk.id.equals(ChunkHelper.tRNS) + && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_TRANSPARENCY)) + return true; + if (chunk.id.equals(ChunkHelper.pHYs) + && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_PHYS)) + return true; + if (text && maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_TEXTUAL)) + return true; + if (maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_ALMOSTALL) + && !(ChunkHelper.isUnknown(chunk) || text || chunk.id.equals(ChunkHelper.hIST) || chunk.id + .equals(ChunkHelper.tIME))) + return true; + if (maskMatch(copyFromMask, ChunkCopyBehaviour.COPY_UNKNOWN) + && ChunkHelper.isUnknown(chunk)) + return true; + } + return false; + } + + }; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkFactory.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkFactory.java new file mode 100644 index 00000000..06969eca --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkFactory.java @@ -0,0 +1,107 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.IChunkFactory; +import ar.com.hjg.pngj.ImageInfo; + +/** + * Default chunk factory. + *

+ * The user that wants to parse custom chunks can extend {@link #createEmptyChunkExtended(String, ImageInfo)} + */ +public class ChunkFactory implements IChunkFactory { + + boolean parse; + + public ChunkFactory() { + this(true); + } + + public ChunkFactory(boolean parse) { + this.parse = parse; + } + + public final PngChunk createChunk(ChunkRaw chunkRaw, ImageInfo imgInfo) { + PngChunk c = createEmptyChunkKnown(chunkRaw.id, imgInfo); + if (c == null) + c = createEmptyChunkExtended(chunkRaw.id, imgInfo); + if (c == null) + c = createEmptyChunkUnknown(chunkRaw.id, imgInfo); + c.setRaw(chunkRaw); + if (parse && chunkRaw.data != null) + c.parseFromRaw(chunkRaw); + return c; + } + + protected final PngChunk createEmptyChunkKnown(String id, ImageInfo imgInfo) { + if (id.equals(ChunkHelper.IDAT)) + return new PngChunkIDAT(imgInfo); + if (id.equals(ChunkHelper.IHDR)) + return new PngChunkIHDR(imgInfo); + if (id.equals(ChunkHelper.PLTE)) + return new PngChunkPLTE(imgInfo); + if (id.equals(ChunkHelper.IEND)) + return new PngChunkIEND(imgInfo); + if (id.equals(ChunkHelper.tEXt)) + return new PngChunkTEXT(imgInfo); + if (id.equals(ChunkHelper.iTXt)) + return new PngChunkITXT(imgInfo); + if (id.equals(ChunkHelper.zTXt)) + return new PngChunkZTXT(imgInfo); + if (id.equals(ChunkHelper.bKGD)) + return new PngChunkBKGD(imgInfo); + if (id.equals(ChunkHelper.gAMA)) + return new PngChunkGAMA(imgInfo); + if (id.equals(ChunkHelper.pHYs)) + return new PngChunkPHYS(imgInfo); + if (id.equals(ChunkHelper.iCCP)) + return new PngChunkICCP(imgInfo); + if (id.equals(ChunkHelper.tIME)) + return new PngChunkTIME(imgInfo); + if (id.equals(ChunkHelper.tRNS)) + return new PngChunkTRNS(imgInfo); + if (id.equals(ChunkHelper.cHRM)) + return new PngChunkCHRM(imgInfo); + if (id.equals(ChunkHelper.sBIT)) + return new PngChunkSBIT(imgInfo); + if (id.equals(ChunkHelper.sRGB)) + return new PngChunkSRGB(imgInfo); + if (id.equals(ChunkHelper.hIST)) + return new PngChunkHIST(imgInfo); + if (id.equals(ChunkHelper.sPLT)) + return new PngChunkSPLT(imgInfo); + // apng + if (id.equals(PngChunkFDAT.ID)) + return new PngChunkFDAT(imgInfo); + if (id.equals(PngChunkACTL.ID)) + return new PngChunkACTL(imgInfo); + if (id.equals(PngChunkFCTL.ID)) + return new PngChunkFCTL(imgInfo); + return null; + } + + /** + * This is used as last resort factory method. + *

+ * It creates a {@link PngChunkUNKNOWN} chunk. + */ + protected final PngChunk createEmptyChunkUnknown(String id, ImageInfo imgInfo) { + return new PngChunkUNKNOWN(id, imgInfo); + } + + /** + * Factory for chunks that are not in the original PNG standard. This can be overriden (but dont forget to call this + * also) + * + * @param id Chunk id , 4 letters + * @param imgInfo Usually not needed + * @return null if chunk id not recognized + */ + protected PngChunk createEmptyChunkExtended(String id, ImageInfo imgInfo) { + if (id.equals(PngChunkOFFS.ID)) + return new PngChunkOFFS(imgInfo); + if (id.equals(PngChunkSTER.ID)) + return new PngChunkSTER(imgInfo); + return null; // extend! + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkHelper.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkHelper.java new file mode 100644 index 00000000..68858978 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkHelper.java @@ -0,0 +1,290 @@ +package ar.com.hjg.pngj.chunks; + +// see http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html +// http://www.w3.org/TR/PNG/#5Chunk-naming-conventions +// http://www.w3.org/TR/PNG/#table53 +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * Helper methods and constants related to Chunk processing. + *

+ * This should only be of interest to developers doing special chunk processing or extending the ChunkFactory + */ +public class ChunkHelper { + ChunkHelper() {} + + public static final String IHDR = "IHDR"; + public static final String PLTE = "PLTE"; + public static final String IDAT = "IDAT"; + public static final String IEND = "IEND"; + public static final String cHRM = "cHRM"; + public static final String gAMA = "gAMA"; + public static final String iCCP = "iCCP"; + public static final String sBIT = "sBIT"; + public static final String sRGB = "sRGB"; + public static final String bKGD = "bKGD"; + public static final String hIST = "hIST"; + public static final String tRNS = "tRNS"; + public static final String pHYs = "pHYs"; + public static final String sPLT = "sPLT"; + public static final String tIME = "tIME"; + public static final String iTXt = "iTXt"; + public static final String tEXt = "tEXt"; + public static final String zTXt = "zTXt"; + + public static final byte[] b_IHDR = toBytes(IHDR); + public static final byte[] b_PLTE = toBytes(PLTE); + public static final byte[] b_IDAT = toBytes(IDAT); + public static final byte[] b_IEND = toBytes(IEND); + + /* + * static auxiliary buffer. any method that uses this should synchronize against this + */ + private static byte[] tmpbuffer = new byte[4096]; + + /** + * Converts to bytes using Latin1 (ISO-8859-1) + */ + public static byte[] toBytes(String x) { + try { + return x.getBytes(PngHelperInternal.charsetLatin1name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to String using Latin1 (ISO-8859-1) + */ + public static String toString(byte[] x) { + try { + return new String(x, PngHelperInternal.charsetLatin1name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to String using Latin1 (ISO-8859-1) + */ + public static String toString(byte[] x, int offset, int len) { + try { + return new String(x, offset, len, PngHelperInternal.charsetLatin1name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to bytes using UTF-8 + */ + public static byte[] toBytesUTF8(String x) { + try { + return x.getBytes(PngHelperInternal.charsetUTF8name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to string using UTF-8 + */ + public static String toStringUTF8(byte[] x) { + try { + return new String(x, PngHelperInternal.charsetUTF8name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * Converts to string using UTF-8 + */ + public static String toStringUTF8(byte[] x, int offset, int len) { + try { + return new String(x, offset, len, PngHelperInternal.charsetUTF8name); + } catch (UnsupportedEncodingException e) { + throw new PngBadCharsetException(e); + } + } + + /** + * critical chunk : first letter is uppercase + */ + public static boolean isCritical(String id) { + return (Character.isUpperCase(id.charAt(0))); + } + + /** + * public chunk: second letter is uppercase + */ + public static boolean isPublic(String id) { // + return (Character.isUpperCase(id.charAt(1))); + } + + /** + * Safe to copy chunk: fourth letter is lower case + */ + public static boolean isSafeToCopy(String id) { + return (!Character.isUpperCase(id.charAt(3))); + } + + /** + * "Unknown" just means that our chunk factory (even when it has been augmented by client code) did not recognize its + * id + */ + public static boolean isUnknown(PngChunk c) { + return c instanceof PngChunkUNKNOWN; + } + + /** + * Finds position of null byte in array + * + * @param b + * @return -1 if not found + */ + public static int posNullByte(byte[] b) { + for (int i = 0; i < b.length; i++) + if (b[i] == 0) + return i; + return -1; + } + + /** + * Decides if a chunk should be loaded, according to a ChunkLoadBehaviour + * + * @param id + * @param behav + * @return true/false + */ + public static boolean shouldLoad(String id, ChunkLoadBehaviour behav) { + if (isCritical(id)) + return true; + switch (behav) { + case LOAD_CHUNK_ALWAYS: + return true; + case LOAD_CHUNK_IF_SAFE: + return isSafeToCopy(id); + case LOAD_CHUNK_NEVER: + return false; + case LOAD_CHUNK_MOST_IMPORTANT: + return id.equals(PngChunkTRNS.ID); + } + return false; // should not reach here + } + + public final static byte[] compressBytes(byte[] ori, boolean compress) { + return compressBytes(ori, 0, ori.length, compress); + } + + public static byte[] compressBytes(byte[] ori, int offset, int len, boolean compress) { + try { + ByteArrayInputStream inb = new ByteArrayInputStream(ori, offset, len); + InputStream in = compress ? inb : new InflaterInputStream(inb); + ByteArrayOutputStream outb = new ByteArrayOutputStream(); + OutputStream out = compress ? new DeflaterOutputStream(outb) : outb; + shovelInToOut(in, out); + in.close(); + out.close(); + return outb.toByteArray(); + } catch (Exception e) { + throw new PngjException(e); + } + } + + /** + * Shovels all data from an input stream to an output stream. + */ + private static void shovelInToOut(InputStream in, OutputStream out) throws IOException { + synchronized (tmpbuffer) { + int len; + while ((len = in.read(tmpbuffer)) > 0) { + out.write(tmpbuffer, 0, len); + } + } + } + + /** + * Returns only the chunks that "match" the predicate + * + * See also trimList() + */ + public static List filterList(List target, ChunkPredicate predicateKeep) { + List result = new ArrayList(); + for (PngChunk element : target) { + if (predicateKeep.match(element)) { + result.add(element); + } + } + return result; + } + + /** + * Remove (in place) the chunks that "match" the predicate + * + * See also filterList + */ + public static int trimList(List target, ChunkPredicate predicateRemove) { + Iterator it = target.iterator(); + int cont = 0; + while (it.hasNext()) { + PngChunk c = it.next(); + if (predicateRemove.match(c)) { + it.remove(); + cont++; + } + } + return cont; + } + + /** + * Adhoc criteria: two ancillary chunks are "equivalent" ("practically same type") if they have same id and (perhaps, + * if multiple are allowed) if the match also in some "internal key" (eg: key for string values, palette for sPLT, + * etc) + * + * When we use this method, we implicitly assume that we don't allow/expect two "equivalent" chunks in a single PNG + * + * Notice that the use of this is optional, and that the PNG standard actually allows text chunks that have same key + * + * @return true if "equivalent" + */ + public static final boolean equivalent(PngChunk c1, PngChunk c2) { + if (c1 == c2) + return true; + if (c1 == null || c2 == null || !c1.id.equals(c2.id)) + return false; + if (c1.crit) + return false; + // same id + if (c1.getClass() != c2.getClass()) + return false; // should not happen + if (!c2.allowsMultiple()) + return true; + if (c1 instanceof PngChunkTextVar) { + return ((PngChunkTextVar) c1).getKey().equals(((PngChunkTextVar) c2).getKey()); + } + if (c1 instanceof PngChunkSPLT) { + return ((PngChunkSPLT) c1).getPalName().equals(((PngChunkSPLT) c2).getPalName()); + } + // unknown chunks that allow multiple? consider they don't match + return false; + } + + public static boolean isText(PngChunk c) { + return c instanceof PngChunkTextVar; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkLoadBehaviour.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkLoadBehaviour.java new file mode 100644 index 00000000..84b92f3f --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkLoadBehaviour.java @@ -0,0 +1,26 @@ +package ar.com.hjg.pngj.chunks; + +/** + * What to do with ancillary (non-critical) chunks when reading. + *

+ * + */ +public enum ChunkLoadBehaviour { + /** + * All non-critical chunks are skipped + */ + LOAD_CHUNK_NEVER, + /** + * Load chunk if "safe to copy" + */ + LOAD_CHUNK_IF_SAFE, + /** + * Load only most important chunk: TRNS + */ + LOAD_CHUNK_MOST_IMPORTANT, + /** + * Load all chunks.
+ * Notice that other restrictions might apply, see PngReader.skipChunkMaxSize PngReader.skipChunkIds + */ + LOAD_CHUNK_ALWAYS; +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkPredicate.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkPredicate.java new file mode 100644 index 00000000..72b2a6f3 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkPredicate.java @@ -0,0 +1,14 @@ +package ar.com.hjg.pngj.chunks; + +/** + * Decides if another chunk "matches", according to some criterion + */ +public interface ChunkPredicate { + /** + * The other chunk matches with this one + * + * @param chunk + * @return true if match + */ + boolean match(PngChunk chunk); +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkRaw.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkRaw.java new file mode 100644 index 00000000..e02a8fb6 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunkRaw.java @@ -0,0 +1,169 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayInputStream; +import java.io.OutputStream; +import java.util.zip.CRC32; + +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjBadCrcException; +import ar.com.hjg.pngj.PngjException; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * Raw (physical) chunk. + *

+ * Short lived object, to be created while serialing/deserializing Do not reuse it for different chunks.
+ * See http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html + */ +public class ChunkRaw { + /** + * The length counts only the data field, not itself, the chunk type code, or the CRC. Zero is a valid length. + * Although encoders and decoders should treat the length as unsigned, its value must not exceed 231-1 bytes. + */ + public final int len; + + /** + * A 4-byte chunk type code. uppercase and lowercase ASCII letters + */ + public final byte[] idbytes; + public final String id; + + /** + * The data bytes appropriate to the chunk type, if any. This field can be of zero length. Does not include crc. If + * it's null, it means that the data is ot available + */ + public byte[] data = null; + /** + * @see ChunkRaw#getOffset() + */ + private long offset = 0; + + /** + * A 4-byte CRC (Cyclic Redundancy Check) calculated on the preceding bytes in the chunk, including the chunk type + * code and chunk data fields, but not including the length field. + */ + public byte[] crcval = new byte[4]; + + private CRC32 crcengine; // lazily instantiated + + public ChunkRaw(int len, String id, boolean alloc) { + this.len = len; + this.id = id; + this.idbytes = ChunkHelper.toBytes(id); + for (int i = 0; i < 4; i++) { + if (idbytes[i] < 65 || idbytes[i] > 122 || (idbytes[i] > 90 && idbytes[i] < 97)) + throw new PngjException("Bad id chunk: must be ascii letters " + id); + } + if (alloc) + allocData(); + } + + public ChunkRaw(int len, byte[] idbytes, boolean alloc) { + this(len, ChunkHelper.toString(idbytes), alloc); + } + + public void allocData() { // TODO: not public + if (data == null || data.length < len) + data = new byte[len]; + } + + /** + * this is called after setting data, before writing to os + */ + private void computeCrcForWriting() { + crcengine = new CRC32(); + crcengine.update(idbytes, 0, 4); + if (len > 0) + crcengine.update(data, 0, len); // + PngHelperInternal.writeInt4tobytes((int) crcengine.getValue(), crcval, 0); + } + + /** + * Computes the CRC and writes to the stream. If error, a PngjOutputException is thrown + * + * Note that this is only used for non idat chunks + */ + public void writeChunk(OutputStream os) { + writeChunkHeader(os); + if (len > 0) { + if (data == null) + throw new PngjOutputException("cannot write chunk, raw chunk data is null [" + id + "]"); + PngHelperInternal.writeBytes(os, data, 0, len); + } + computeCrcForWriting(); + writeChunkCrc(os); + } + + public void writeChunkHeader(OutputStream os) { + if (idbytes.length != 4) + throw new PngjOutputException("bad chunkid [" + id + "]"); + PngHelperInternal.writeInt4(os, len); + PngHelperInternal.writeBytes(os, idbytes); + } + + public void writeChunkCrc(OutputStream os) { + PngHelperInternal.writeBytes(os, crcval, 0, 4); + } + + public void checkCrc() { + int crcComputed = (int) crcengine.getValue(); + int crcExpected = PngHelperInternal.readInt4fromBytes(crcval, 0); + if (crcComputed != crcExpected) + throw new PngjBadCrcException("chunk: " + this.toString() + " expected=" + crcExpected + + " read=" + crcComputed); + } + + public void updateCrc(byte[] buf, int off, int len) { + if (crcengine == null) + crcengine = new CRC32(); + crcengine.update(buf, off, len); + } + + ByteArrayInputStream getAsByteStream() { // only the data + return new ByteArrayInputStream(data); + } + + /** + * offset in the full PNG stream, in bytes. only informational, for read chunks (0=NA) + */ + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public String toString() { + return "chunkid=" + ChunkHelper.toString(idbytes) + " len=" + len; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + (int) (offset ^ (offset >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ChunkRaw other = (ChunkRaw) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (offset != other.offset) + return false; + return true; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunksList.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunksList.java new file mode 100644 index 00000000..f028c67c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunksList.java @@ -0,0 +1,167 @@ +package ar.com.hjg.pngj.chunks; + +import java.util.ArrayList; +import java.util.List; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * All chunks that form an image, read or to be written. + *

+ * chunks include all chunks, but IDAT is a single pseudo chunk without data + **/ +public class ChunksList { + // ref: http://www.w3.org/TR/PNG/#table53 + public static final int CHUNK_GROUP_0_IDHR = 0; // required - single + public static final int CHUNK_GROUP_1_AFTERIDHR = 1; // optional - multiple + public static final int CHUNK_GROUP_2_PLTE = 2; // optional - single + public static final int CHUNK_GROUP_3_AFTERPLTE = 3; // optional - multple + public static final int CHUNK_GROUP_4_IDAT = 4; // required (single pseudo chunk) + public static final int CHUNK_GROUP_5_AFTERIDAT = 5; // optional - multple + public static final int CHUNK_GROUP_6_END = 6; // only 1 chunk - requried + + /** + * All chunks, read (or written) + * + * But IDAT is a single pseudo chunk without data + */ + List chunks = new ArrayList(); + // protected HashMap> chunksById = new HashMap>(); + // // does not include IDAT + + final ImageInfo imageInfo; // only required for writing + + boolean withPlte = false; + + public ChunksList(ImageInfo imfinfo) { + this.imageInfo = imfinfo; + } + + /** + * WARNING: this does NOT return a copy, but the list itself. The called should not modify this directly! Don't use + * this to manipulate the chunks. + */ + public List getChunks() { + return chunks; + } + + protected static List getXById(final List list, final String id, + final String innerid) { + if (innerid == null) + return ChunkHelper.filterList(list, new ChunkPredicate() { + public boolean match(PngChunk c) { + return c.id.equals(id); + } + }); + else + return ChunkHelper.filterList(list, new ChunkPredicate() { + public boolean match(PngChunk c) { + if (!c.id.equals(id)) + return false; + if (c instanceof PngChunkTextVar && !((PngChunkTextVar) c).getKey().equals(innerid)) + return false; + if (c instanceof PngChunkSPLT && !((PngChunkSPLT) c).getPalName().equals(innerid)) + return false; + return true; + } + }); + } + + /** + * Adds chunk in next position. This is used onyl by the pngReader + */ + public void appendReadChunk(PngChunk chunk, int chunkGroup) { + chunk.setChunkGroup(chunkGroup); + chunks.add(chunk); + if (chunk.id.equals(PngChunkPLTE.ID)) + withPlte = true; + } + + /** + * All chunks with this ID + * + * @param id + * @return List, empty if none + */ + public List getById(final String id) { + return getById(id, null); + } + + /** + * If innerid!=null and the chunk is PngChunkTextVar or PngChunkSPLT, it's filtered by that id + * + * @param id + * @return innerid Only used for text and SPLT chunks + * @return List, empty if none + */ + public List getById(final String id, final String innerid) { + return getXById(chunks, id, innerid); + } + + /** + * Returns only one chunk + * + * @param id + * @return First chunk found, null if not found + */ + public PngChunk getById1(final String id) { + return getById1(id, false); + } + + /** + * Returns only one chunk or null if nothing found - does not include queued + *

+ * If more than one chunk is found, then an exception is thrown (failifMultiple=true or chunk is single) or the last + * one is returned (failifMultiple=false) + **/ + public PngChunk getById1(final String id, final boolean failIfMultiple) { + return getById1(id, null, failIfMultiple); + } + + /** + * Returns only one chunk or null if nothing found - does not include queued + *

+ * If more than one chunk (after filtering by inner id) is found, then an exception is thrown (failifMultiple=true or + * chunk is single) or the last one is returned (failifMultiple=false) + **/ + public PngChunk getById1(final String id, final String innerid, final boolean failIfMultiple) { + List list = getById(id, innerid); + if (list.isEmpty()) + return null; + if (list.size() > 1 && (failIfMultiple || !list.get(0).allowsMultiple())) + throw new PngjException("unexpected multiple chunks id=" + id); + return list.get(list.size() - 1); + } + + /** + * Finds all chunks "equivalent" to this one + * + * @param c2 + * @return Empty if nothing found + */ + public List getEquivalent(final PngChunk c2) { + return ChunkHelper.filterList(chunks, new ChunkPredicate() { + public boolean match(PngChunk c) { + return ChunkHelper.equivalent(c, c2); + } + }); + } + + public String toString() { + return "ChunkList: read: " + chunks.size(); + } + + /** + * for debugging + */ + public String toStringFull() { + StringBuilder sb = new StringBuilder(toString()); + sb.append("\n Read:\n"); + for (PngChunk chunk : chunks) { + sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n"); + } + return sb.toString(); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunksListForWrite.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunksListForWrite.java new file mode 100644 index 00000000..c2e58d7f --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/ChunksListForWrite.java @@ -0,0 +1,189 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; +import ar.com.hjg.pngj.PngjOutputException; + +public class ChunksListForWrite extends ChunksList { + + /** + * chunks not yet writen - does not include IHDR, IDAT, END, perhaps yes PLTE + */ + private final List queuedChunks = new ArrayList(); + + // redundant, just for eficciency + private HashMap alreadyWrittenKeys = new HashMap(); + + public ChunksListForWrite(ImageInfo imfinfo) { + super(imfinfo); + } + + /** + * Same as getById(), but looking in the queued chunks + */ + public List getQueuedById(final String id) { + return getQueuedById(id, null); + } + + /** + * Same as getById(), but looking in the queued chunks + */ + public List getQueuedById(final String id, final String innerid) { + return getXById(queuedChunks, id, innerid); + } + + /** + * Same as getById1(), but looking in the queued chunks + **/ + public PngChunk getQueuedById1(final String id, final String innerid, final boolean failIfMultiple) { + List list = getQueuedById(id, innerid); + if (list.isEmpty()) + return null; + if (list.size() > 1 && (failIfMultiple || !list.get(0).allowsMultiple())) + throw new PngjException("unexpected multiple chunks id=" + id); + return list.get(list.size() - 1); + } + + /** + * Same as getById1(), but looking in the queued chunks + **/ + public PngChunk getQueuedById1(final String id, final boolean failIfMultiple) { + return getQueuedById1(id, null, failIfMultiple); + } + + /** + * Same as getById1(), but looking in the queued chunks + **/ + public PngChunk getQueuedById1(final String id) { + return getQueuedById1(id, false); + } + + /** + * Finds all chunks "equivalent" to this one + * + * @param c2 + * @return Empty if nothing found + */ + public List getQueuedEquivalent(final PngChunk c2) { + return ChunkHelper.filterList(queuedChunks, new ChunkPredicate() { + public boolean match(PngChunk c) { + return ChunkHelper.equivalent(c, c2); + } + }); + } + + /** + * Remove Chunk: only from queued + * + * WARNING: this depends on c.equals() implementation, which is straightforward for SingleChunks. For MultipleChunks, + * it will normally check for reference equality! + */ + public boolean removeChunk(PngChunk c) { + if (c == null) + return false; + return queuedChunks.remove(c); + } + + /** + * Adds chunk to queue + * + * If there + * + * @param c + */ + public boolean queue(PngChunk c) { + queuedChunks.add(c); + return true; + } + + /** + * this should be called only for ancillary chunks and PLTE (groups 1 - 3 - 5) + **/ + private static boolean shouldWrite(PngChunk c, int currentGroup) { + if (currentGroup == CHUNK_GROUP_2_PLTE) + return c.id.equals(ChunkHelper.PLTE); + if (currentGroup % 2 == 0) + throw new PngjOutputException("bad chunk group?"); + int minChunkGroup, maxChunkGroup; + if (c.getOrderingConstraint().mustGoBeforePLTE()) + minChunkGroup = maxChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + else if (c.getOrderingConstraint().mustGoBeforeIDAT()) { + maxChunkGroup = ChunksList.CHUNK_GROUP_3_AFTERPLTE; + minChunkGroup = + c.getOrderingConstraint().mustGoAfterPLTE() ? ChunksList.CHUNK_GROUP_3_AFTERPLTE + : ChunksList.CHUNK_GROUP_1_AFTERIDHR; + } else { + maxChunkGroup = ChunksList.CHUNK_GROUP_5_AFTERIDAT; + minChunkGroup = ChunksList.CHUNK_GROUP_1_AFTERIDHR; + } + + int preferred = maxChunkGroup; + if (c.hasPriority()) + preferred = minChunkGroup; + if (ChunkHelper.isUnknown(c) && c.getChunkGroup() > 0) + preferred = c.getChunkGroup(); + if (currentGroup == preferred) + return true; + if (currentGroup > preferred && currentGroup <= maxChunkGroup) + return true; + return false; + } + + public int writeChunks(OutputStream os, int currentGroup) { + int cont = 0; + Iterator it = queuedChunks.iterator(); + while (it.hasNext()) { + PngChunk c = it.next(); + if (!shouldWrite(c, currentGroup)) + continue; + if (ChunkHelper.isCritical(c.id) && !c.id.equals(ChunkHelper.PLTE)) + throw new PngjOutputException("bad chunk queued: " + c); + if (alreadyWrittenKeys.containsKey(c.id) && !c.allowsMultiple()) + throw new PngjOutputException("duplicated chunk does not allow multiple: " + c); + c.write(os); + chunks.add(c); + alreadyWrittenKeys.put(c.id, + alreadyWrittenKeys.containsKey(c.id) ? alreadyWrittenKeys.get(c.id) + 1 : 1); + c.setChunkGroup(currentGroup); + it.remove(); + cont++; + } + return cont; + } + + /** + * warning: this is NOT a copy, do not modify + */ + public List getQueuedChunks() { + return queuedChunks; + } + + public String toString() { + return "ChunkList: written: " + getChunks().size() + " queue: " + queuedChunks.size(); + } + + /** + * for debugging + */ + public String toStringFull() { + StringBuilder sb = new StringBuilder(toString()); + sb.append("\n Written:\n"); + for (PngChunk chunk : getChunks()) { + sb.append(chunk).append(" G=" + chunk.getChunkGroup() + "\n"); + } + if (!queuedChunks.isEmpty()) { + sb.append(" Queued:\n"); + for (PngChunk chunk : queuedChunks) { + sb.append(chunk).append("\n"); + } + + } + return sb.toString(); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngBadCharsetException.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngBadCharsetException.java new file mode 100644 index 00000000..cc41c064 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngBadCharsetException.java @@ -0,0 +1,20 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.PngjException; + +public class PngBadCharsetException extends PngjException { + private static final long serialVersionUID = 1L; + + public PngBadCharsetException(String message, Throwable cause) { + super(message, cause); + } + + public PngBadCharsetException(String message) { + super(message); + } + + public PngBadCharsetException(Throwable cause) { + super(cause); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunk.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunk.java new file mode 100644 index 00000000..16b82204 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunk.java @@ -0,0 +1,216 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.OutputStream; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjExceptionInternal; + +/** + * Represents a instance of a PNG chunk. + *

+ * See http://www + * .libpng.org/pub/png/spec/1.2/PNG-Chunks .html + *

+ * Concrete classes should extend {@link PngChunkSingle} or {@link PngChunkMultiple} + *

+ * Note that some methods/fields are type-specific (getOrderingConstraint(), allowsMultiple()),
+ * some are 'almost' type-specific (id,crit,pub,safe; the exception is PngUKNOWN),
+ * and the rest are instance-specific + */ +public abstract class PngChunk { + + /** + * Chunk-id: 4 letters + */ + public final String id; + /** + * Autocomputed at creation time + */ + public final boolean crit, pub, safe; + + protected final ImageInfo imgInfo; + + protected ChunkRaw raw; + + private boolean priority = false; // For writing. Queued chunks with high priority will be written + // as soon as + // possible + + protected int chunkGroup = -1; // chunk group where it was read or writen + + /** + * Possible ordering constraint for a PngChunk type -only relevant for ancillary chunks. Theoretically, there could be + * more general constraints, but these cover the constraints for standard chunks. + */ + public enum ChunkOrderingConstraint { + /** + * no ordering constraint + */ + NONE, + /** + * Must go before PLTE (and hence, also before IDAT) + */ + BEFORE_PLTE_AND_IDAT, + /** + * Must go after PLTE (if exists) but before IDAT + */ + AFTER_PLTE_BEFORE_IDAT, + /** + * Must go after PLTE (and it must exist) but before IDAT + */ + AFTER_PLTE_BEFORE_IDAT_PLTE_REQUIRED, + /** + * Must before IDAT (before or after PLTE) + */ + BEFORE_IDAT, + /** + * After IDAT (this restriction does not apply to the standard PNG chunks) + */ + AFTER_IDAT, + /** + * Does not apply + */ + NA; + + public boolean mustGoBeforePLTE() { + return this == BEFORE_PLTE_AND_IDAT; + } + + public boolean mustGoBeforeIDAT() { + return this == BEFORE_IDAT || this == BEFORE_PLTE_AND_IDAT || this == AFTER_PLTE_BEFORE_IDAT; + } + + /** + * after pallete, if exists + */ + public boolean mustGoAfterPLTE() { + return this == AFTER_PLTE_BEFORE_IDAT || this == AFTER_PLTE_BEFORE_IDAT_PLTE_REQUIRED; + } + + public boolean mustGoAfterIDAT() { + return this == AFTER_IDAT; + } + + public boolean isOk(int currentChunkGroup, boolean hasplte) { + if (this == NONE) + return true; + else if (this == BEFORE_IDAT) + return currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT; + else if (this == BEFORE_PLTE_AND_IDAT) + return currentChunkGroup < ChunksList.CHUNK_GROUP_2_PLTE; + else if (this == AFTER_PLTE_BEFORE_IDAT) + return hasplte ? currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT + : (currentChunkGroup < ChunksList.CHUNK_GROUP_4_IDAT && currentChunkGroup > ChunksList.CHUNK_GROUP_2_PLTE); + else if (this == AFTER_IDAT) + return currentChunkGroup > ChunksList.CHUNK_GROUP_4_IDAT; + return false; + } + } + + public PngChunk(String id, ImageInfo imgInfo) { + this.id = id; + this.imgInfo = imgInfo; + this.crit = ChunkHelper.isCritical(id); + this.pub = ChunkHelper.isPublic(id); + this.safe = ChunkHelper.isSafeToCopy(id); + } + + protected final ChunkRaw createEmptyChunk(int len, boolean alloc) { + ChunkRaw c = new ChunkRaw(len, ChunkHelper.toBytes(id), alloc); + return c; + } + + /** + * In which "chunkGroup" (see {@link ChunksList}for definition) this chunks instance was read or written. + *

+ * -1 if not read or written (eg, queued) + */ + final public int getChunkGroup() { + return chunkGroup; + } + + /** + * @see #getChunkGroup() + */ + final void setChunkGroup(int chunkGroup) { + this.chunkGroup = chunkGroup; + } + + public boolean hasPriority() { + return priority; + } + + public void setPriority(boolean priority) { + this.priority = priority; + } + + final void write(OutputStream os) { + if (raw == null || raw.data == null) + raw = createRawChunk(); + if (raw == null) + throw new PngjExceptionInternal("null chunk ! creation failed for " + this); + raw.writeChunk(os); + } + + /** + * Creates the physical chunk. This is used when writing (serialization). Each particular chunk class implements its + * own logic. + * + * @return A newly allocated and filled raw chunk + */ + public abstract ChunkRaw createRawChunk(); + + /** + * Parses raw chunk and fill inside data. This is used when reading (deserialization). Each particular chunk class + * implements its own logic. + */ + protected abstract void parseFromRaw(ChunkRaw c); + + /** + * See {@link PngChunkMultiple} and {@link PngChunkSingle} + * + * @return true if PNG accepts multiple chunks of this class + */ + protected abstract boolean allowsMultiple(); + + public ChunkRaw getRaw() { + return raw; + } + + void setRaw(ChunkRaw raw) { + this.raw = raw; + } + + /** + * @see ChunkRaw#len + */ + public int getLen() { + return raw != null ? raw.len : -1; + } + + /** + * @see ChunkRaw#getOffset() + */ + public long getOffset() { + return raw != null ? raw.getOffset() : -1; + } + + /** + * This signals that the raw chunk (serialized data) as invalid, so that it's regenerated on write. This should be + * called for the (infrequent) case of chunks that were copied from a PngReader and we want to manually modify it. + */ + public void invalidateRawData() { + raw = null; + } + + /** + * see {@link ChunkOrderingConstraint} + */ + public abstract ChunkOrderingConstraint getOrderingConstraint(); + + @Override + public String toString() { + return "chunk id= " + id + " (len=" + getLen() + " offset=" + getOffset() + ")"; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkACTL.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkACTL.java new file mode 100644 index 00000000..de08207b --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkACTL.java @@ -0,0 +1,59 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; + +/** + * acTL chunk. For APGN, not PGN standard + *

+ * see https://wiki.mozilla.org/APNG_Specification#.60acTL.60:_The_Animation_Control_Chunk + *

+ */ +public class PngChunkACTL extends PngChunkSingle { + public final static String ID = "acTL"; + private int numFrames; + private int numPlays; + + + public PngChunkACTL(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(8, true); + PngHelperInternal.writeInt4tobytes((int) numFrames, c.data, 0); + PngHelperInternal.writeInt4tobytes((int) numPlays, c.data, 4); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + numFrames = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + numPlays = PngHelperInternal.readInt4fromBytes(chunk.data, 4); + } + + public int getNumFrames() { + return numFrames; + } + + public void setNumFrames(int numFrames) { + this.numFrames = numFrames; + } + + public int getNumPlays() { + return numPlays; + } + + public void setNumPlays(int numPlays) { + this.numPlays = numPlays; + } + + + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkBKGD.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkBKGD.java new file mode 100644 index 00000000..5ef2b305 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkBKGD.java @@ -0,0 +1,112 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * bKGD Chunk. + *

+ * see {@link http://www.w3.org/TR/PNG/#11bKGD} + *

+ * This chunk structure depends on the image type + */ +public class PngChunkBKGD extends PngChunkSingle { + public final static String ID = ChunkHelper.bKGD; + // only one of these is meaningful + private int gray; + private int red, green, blue; + private int paletteIndex; + + public PngChunkBKGD(ImageInfo info) { + super(ChunkHelper.bKGD, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + if (imgInfo.greyscale) { + c = createEmptyChunk(2, true); + PngHelperInternal.writeInt2tobytes(gray, c.data, 0); + } else if (imgInfo.indexed) { + c = createEmptyChunk(1, true); + c.data[0] = (byte) paletteIndex; + } else { + c = createEmptyChunk(6, true); + PngHelperInternal.writeInt2tobytes(red, c.data, 0); + PngHelperInternal.writeInt2tobytes(green, c.data, 0); + PngHelperInternal.writeInt2tobytes(blue, c.data, 0); + } + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (imgInfo.greyscale) { + gray = PngHelperInternal.readInt2fromBytes(c.data, 0); + } else if (imgInfo.indexed) { + paletteIndex = (int) (c.data[0] & 0xff); + } else { + red = PngHelperInternal.readInt2fromBytes(c.data, 0); + green = PngHelperInternal.readInt2fromBytes(c.data, 2); + blue = PngHelperInternal.readInt2fromBytes(c.data, 4); + } + } + + /** + * Set gray value (0-255 if bitdept=8) + * + * @param gray + */ + public void setGray(int gray) { + if (!imgInfo.greyscale) + throw new PngjException("only gray images support this"); + this.gray = gray; + } + + public int getGray() { + if (!imgInfo.greyscale) + throw new PngjException("only gray images support this"); + return gray; + } + + /** + * Set pallette index + * + */ + public void setPaletteIndex(int i) { + if (!imgInfo.indexed) + throw new PngjException("only indexed (pallete) images support this"); + this.paletteIndex = i; + } + + public int getPaletteIndex() { + if (!imgInfo.indexed) + throw new PngjException("only indexed (pallete) images support this"); + return paletteIndex; + } + + /** + * Set rgb values + * + */ + public void setRGB(int r, int g, int b) { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + red = r; + green = g; + blue = b; + } + + public int[] getRGB() { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + return new int[] {red, green, blue}; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkCHRM.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkCHRM.java new file mode 100644 index 00000000..f1b28fdc --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkCHRM.java @@ -0,0 +1,75 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * cHRM chunk. + *

+ * see http://www.w3.org/TR/PNG/#11cHRM + */ +public class PngChunkCHRM extends PngChunkSingle { + public final static String ID = ChunkHelper.cHRM; + + // http://www.w3.org/TR/PNG/#11cHRM + private double whitex, whitey; + private double redx, redy; + private double greenx, greeny; + private double bluex, bluey; + + public PngChunkCHRM(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + c = createEmptyChunk(32, true); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(whitex), c.data, 0); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(whitey), c.data, 4); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(redx), c.data, 8); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(redy), c.data, 12); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(greenx), c.data, 16); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(greeny), c.data, 20); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(bluex), c.data, 24); + PngHelperInternal.writeInt4tobytes(PngHelperInternal.doubleToInt100000(bluey), c.data, 28); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (c.len != 32) + throw new PngjException("bad chunk " + c); + whitex = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 0)); + whitey = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 4)); + redx = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 8)); + redy = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 12)); + greenx = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 16)); + greeny = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 20)); + bluex = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 24)); + bluey = PngHelperInternal.intToDouble100000(PngHelperInternal.readInt4fromBytes(c.data, 28)); + } + + public void setChromaticities(double whitex, double whitey, double redx, double redy, + double greenx, double greeny, double bluex, double bluey) { + this.whitex = whitex; + this.redx = redx; + this.greenx = greenx; + this.bluex = bluex; + this.whitey = whitey; + this.redy = redy; + this.greeny = greeny; + this.bluey = bluey; + } + + public double[] getChromaticities() { + return new double[] {whitex, whitey, redx, redy, greenx, greeny, bluex, bluey}; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkFCTL.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkFCTL.java new file mode 100644 index 00000000..f55617cb --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkFCTL.java @@ -0,0 +1,158 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; + +/** + * fcTL chunk. For APGN, not PGN standard + *

+ * see https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk + *

+ */ +public class PngChunkFCTL extends PngChunkMultiple { + public final static String ID = "fcTL"; + + public final static byte APNG_DISPOSE_OP_NONE = 0; + public final static byte APNG_DISPOSE_OP_BACKGROUND = 1; + public final static byte APNG_DISPOSE_OP_PREVIOUS = 2; + public final static byte APNG_BLEND_OP_SOURCE = 0; + public final static byte APNG_BLEND_OP_OVER = 1; + + private int seqNum; + private int width, height, xOff, yOff; + private int delayNum, delayDen; + private byte disposeOp, blendOp; + + public PngChunkFCTL(ImageInfo info) { + super(ID, info); + } + + public ImageInfo getEquivImageInfo() { + return new ImageInfo(width, height, imgInfo.bitDepth, imgInfo.alpha, imgInfo.greyscale, + imgInfo.indexed); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(8, true); + int off = 0; + PngHelperInternal.writeInt4tobytes(seqNum, c.data, off); + off += 4; + PngHelperInternal.writeInt4tobytes(width, c.data, off); + off += 4; + PngHelperInternal.writeInt4tobytes(height, c.data, off); + off += 4; + PngHelperInternal.writeInt4tobytes(xOff, c.data, off); + off += 4; + PngHelperInternal.writeInt4tobytes(yOff, c.data, off); + off += 4; + PngHelperInternal.writeInt2tobytes(delayNum, c.data, off); + off += 2; + PngHelperInternal.writeInt2tobytes(delayDen, c.data, off); + off += 2; + c.data[off] = disposeOp; + off += 1; + c.data[off] = blendOp; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + int off = 0; + seqNum = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + width = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + height = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + xOff = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + yOff = PngHelperInternal.readInt4fromBytes(chunk.data, off); + off += 4; + delayNum = PngHelperInternal.readInt2fromBytes(chunk.data, off); + off += 2; + delayDen = PngHelperInternal.readInt2fromBytes(chunk.data, off); + off += 2; + disposeOp = chunk.data[off]; + off += 1; + blendOp = chunk.data[off]; + } + + public int getSeqNum() { + return seqNum; + } + + public void setSeqNum(int seqNum) { + this.seqNum = seqNum; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getxOff() { + return xOff; + } + + public void setxOff(int xOff) { + this.xOff = xOff; + } + + public int getyOff() { + return yOff; + } + + public void setyOff(int yOff) { + this.yOff = yOff; + } + + public int getDelayNum() { + return delayNum; + } + + public void setDelayNum(int delayNum) { + this.delayNum = delayNum; + } + + public int getDelayDen() { + return delayDen; + } + + public void setDelayDen(int delayDen) { + this.delayDen = delayDen; + } + + public byte getDisposeOp() { + return disposeOp; + } + + public void setDisposeOp(byte disposeOp) { + this.disposeOp = disposeOp; + } + + public byte getBlendOp() { + return blendOp; + } + + public void setBlendOp(byte blendOp) { + this.blendOp = blendOp; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkFDAT.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkFDAT.java new file mode 100644 index 00000000..16ecc7ad --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkFDAT.java @@ -0,0 +1,70 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * fdAT chunk. For APGN, not PGN standard + *

+ * see https://wiki.mozilla.org/APNG_Specification#.60fdAT.60:_The_Frame_Data_Chunk + *

+ * This implementation does not support buffering, this should be not managed similar to a IDAT chunk + * + */ +public class PngChunkFDAT extends PngChunkMultiple { + public final static String ID = "fdAT"; + private int seqNum; + private byte[] buffer; // normally not allocated - if so, it's the raw data, so it includes the 4bytes seqNum + int datalen; // length of idat data, excluding seqNUm (= chunk.len-4) + + public PngChunkFDAT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + if (buffer == null) + throw new PngjException("not buffered"); + ChunkRaw c = createEmptyChunk(datalen + 4, false); + c.data = buffer; // shallow copy! + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + seqNum = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + datalen = chunk.len - 4; + buffer = chunk.data; + } + + public int getSeqNum() { + return seqNum; + } + + public void setSeqNum(int seqNum) { + this.seqNum = seqNum; + } + + public byte[] getBuffer() { + return buffer; + } + + public void setBuffer(byte[] buffer) { + this.buffer = buffer; + } + + public int getDatalen() { + return datalen; + } + + public void setDatalen(int datalen) { + this.datalen = datalen; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkGAMA.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkGAMA.java new file mode 100644 index 00000000..d1920c88 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkGAMA.java @@ -0,0 +1,51 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * gAMA chunk. + *

+ * see http://www.w3.org/TR/PNG/#11gAMA + */ +public class PngChunkGAMA extends PngChunkSingle { + public final static String ID = ChunkHelper.gAMA; + + // http://www.w3.org/TR/PNG/#11gAMA + private double gamma; + + public PngChunkGAMA(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(4, true); + int g = (int) (gamma * 100000 + 0.5); + PngHelperInternal.writeInt4tobytes(g, c.data, 0); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 4) + throw new PngjException("bad chunk " + chunk); + int g = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + gamma = ((double) g) / 100000.0; + } + + public double getGamma() { + return gamma; + } + + public void setGamma(double gamma) { + this.gamma = gamma; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkHIST.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkHIST.java new file mode 100644 index 00000000..ebdf2355 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkHIST.java @@ -0,0 +1,58 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * hIST chunk. + *

+ * see http://www.w3.org/TR/PNG/#11hIST
+ * only for palette images + */ +public class PngChunkHIST extends PngChunkSingle { + public final static String ID = ChunkHelper.hIST; + + private int[] hist = new int[0]; // should have same lenght as palette + + public PngChunkHIST(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (!imgInfo.indexed) + throw new PngjException("only indexed images accept a HIST chunk"); + int nentries = c.data.length / 2; + hist = new int[nentries]; + for (int i = 0; i < hist.length; i++) { + hist[i] = PngHelperInternal.readInt2fromBytes(c.data, i * 2); + } + } + + @Override + public ChunkRaw createRawChunk() { + if (!imgInfo.indexed) + throw new PngjException("only indexed images accept a HIST chunk"); + ChunkRaw c = null; + c = createEmptyChunk(hist.length * 2, true); + for (int i = 0; i < hist.length; i++) { + PngHelperInternal.writeInt2tobytes(hist[i], c.data, i * 2); + } + return c; + } + + public int[] getHist() { + return hist; + } + + public void setHist(int[] hist) { + this.hist = hist; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkICCP.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkICCP.java new file mode 100644 index 00000000..b3b1109f --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkICCP.java @@ -0,0 +1,77 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * iCCP chunk. + *

+ * See {@link http://www.w3.org/TR/PNG/#11iCCP} + */ +public class PngChunkICCP extends PngChunkSingle { + public final static String ID = ChunkHelper.iCCP; + + // http://www.w3.org/TR/PNG/#11iCCP + private String profileName; + private byte[] compressedProfile; // copmression/decopmresion is done in getter/setter + + public PngChunkICCP(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(profileName.length() + compressedProfile.length + 2, true); + System.arraycopy(ChunkHelper.toBytes(profileName), 0, c.data, 0, profileName.length()); + c.data[profileName.length()] = 0; + c.data[profileName.length() + 1] = 0; + System.arraycopy(compressedProfile, 0, c.data, profileName.length() + 2, + compressedProfile.length); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + int pos0 = ChunkHelper.posNullByte(chunk.data); + profileName = ChunkHelper.toString(chunk.data, 0, pos0); + int comp = (chunk.data[pos0 + 1] & 0xff); + if (comp != 0) + throw new PngjException("bad compression for ChunkTypeICCP"); + int compdatasize = chunk.data.length - (pos0 + 2); + compressedProfile = new byte[compdatasize]; + System.arraycopy(chunk.data, pos0 + 2, compressedProfile, 0, compdatasize); + } + + /** + * The profile should be uncompressed bytes + */ + public void setProfileNameAndContent(String name, byte[] profile) { + profileName = name; + compressedProfile = ChunkHelper.compressBytes(profile, true); + } + + public void setProfileNameAndContent(String name, String profile) { + setProfileNameAndContent(name, ChunkHelper.toBytes(profile)); + } + + public String getProfileName() { + return profileName; + } + + /** + * uncompressed + **/ + public byte[] getProfile() { + return ChunkHelper.compressBytes(compressedProfile, false); + } + + public String getProfileAsString() { + return ChunkHelper.toString(getProfile()); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIDAT.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIDAT.java new file mode 100644 index 00000000..625aefaa --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIDAT.java @@ -0,0 +1,34 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * IDAT chunk. + *

+ * see http://www.w3.org/TR/PNG/#11IDAT + *

+ * This is dummy placeholder - we write/read this chunk (actually several) by special code. + */ +public class PngChunkIDAT extends PngChunkMultiple { + public final static String ID = ChunkHelper.IDAT; + + // http://www.w3.org/TR/PNG/#11IDAT + public PngChunkIDAT(ImageInfo i) { + super(ID, i); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; + } + + @Override + public ChunkRaw createRawChunk() {// does nothing + return null; + } + + @Override + public void parseFromRaw(ChunkRaw c) { // does nothing + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIEND.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIEND.java new file mode 100644 index 00000000..58073d77 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIEND.java @@ -0,0 +1,35 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * IEND chunk. + *

+ * see http://www.w3.org/TR/PNG/#11IEND + */ +public class PngChunkIEND extends PngChunkSingle { + public final static String ID = ChunkHelper.IEND; + + // http://www.w3.org/TR/PNG/#11IEND + // this is a dummy placeholder + public PngChunkIEND(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = new ChunkRaw(0, ChunkHelper.b_IEND, false); + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + // this is not used + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIHDR.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIHDR.java new file mode 100644 index 00000000..a2ea517e --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkIHDR.java @@ -0,0 +1,185 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayInputStream; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; +import ar.com.hjg.pngj.PngjInputException; + +/** + * IHDR chunk. + *

+ * see http://www.w3.org/TR/PNG/#11IHDR + *

+ * This is a special critical Chunk. + */ +public class PngChunkIHDR extends PngChunkSingle { + public final static String ID = ChunkHelper.IHDR; + + private int cols; + private int rows; + private int bitspc; + private int colormodel; + private int compmeth; + private int filmeth; + private int interlaced; + + // http://www.w3.org/TR/PNG/#11IHDR + // + public PngChunkIHDR(ImageInfo info) { // argument is normally null here, if not null is used to fill the fields + super(ID, info); + if (info != null) + fillFromInfo(info); + } + + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = new ChunkRaw(13, ChunkHelper.b_IHDR, true); + int offset = 0; + PngHelperInternal.writeInt4tobytes(cols, c.data, offset); + offset += 4; + PngHelperInternal.writeInt4tobytes(rows, c.data, offset); + offset += 4; + c.data[offset++] = (byte) bitspc; + c.data[offset++] = (byte) colormodel; + c.data[offset++] = (byte) compmeth; + c.data[offset++] = (byte) filmeth; + c.data[offset++] = (byte) interlaced; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (c.len != 13) + throw new PngjException("Bad IDHR len " + c.len); + ByteArrayInputStream st = c.getAsByteStream(); + cols = PngHelperInternal.readInt4(st); + rows = PngHelperInternal.readInt4(st); + // bit depth: number of bits per channel + bitspc = PngHelperInternal.readByte(st); + colormodel = PngHelperInternal.readByte(st); + compmeth = PngHelperInternal.readByte(st); + filmeth = PngHelperInternal.readByte(st); + interlaced = PngHelperInternal.readByte(st); + } + + public int getCols() { + return cols; + } + + public void setCols(int cols) { + this.cols = cols; + } + + public int getRows() { + return rows; + } + + public void setRows(int rows) { + this.rows = rows; + } + + public int getBitspc() { + return bitspc; + } + + public void setBitspc(int bitspc) { + this.bitspc = bitspc; + } + + public int getColormodel() { + return colormodel; + } + + public void setColormodel(int colormodel) { + this.colormodel = colormodel; + } + + public int getCompmeth() { + return compmeth; + } + + public void setCompmeth(int compmeth) { + this.compmeth = compmeth; + } + + public int getFilmeth() { + return filmeth; + } + + public void setFilmeth(int filmeth) { + this.filmeth = filmeth; + } + + public int getInterlaced() { + return interlaced; + } + + public void setInterlaced(int interlaced) { + this.interlaced = interlaced; + } + + public boolean isInterlaced() { + return getInterlaced() == 1; + } + + public void fillFromInfo(ImageInfo info) { + setCols(imgInfo.cols); + setRows(imgInfo.rows); + setBitspc(imgInfo.bitDepth); + int colormodel = 0; + if (imgInfo.alpha) + colormodel += 0x04; + if (imgInfo.indexed) + colormodel += 0x01; + if (!imgInfo.greyscale) + colormodel += 0x02; + setColormodel(colormodel); + setCompmeth(0); // compression method 0=deflate + setFilmeth(0); // filter method (0) + setInterlaced(0); // we never interlace + } + + /** throws PngInputException if unexpected values */ + public ImageInfo createImageInfo() { + check(); + boolean alpha = (getColormodel() & 0x04) != 0; + boolean palette = (getColormodel() & 0x01) != 0; + boolean grayscale = (getColormodel() == 0 || getColormodel() == 4); + // creates ImgInfo and imgLine, and allocates buffers + return new ImageInfo(getCols(), getRows(), getBitspc(), alpha, grayscale, palette); + } + + public void check() { + if (cols < 1 || rows < 1 || compmeth != 0 || filmeth != 0) + throw new PngjInputException("bad IHDR: col/row/compmethod/filmethod invalid"); + if (bitspc != 1 && bitspc != 2 && bitspc != 4 && bitspc != 8 && bitspc != 16) + throw new PngjInputException("bad IHDR: bitdepth invalid"); + if (interlaced < 0 || interlaced > 1) + throw new PngjInputException("bad IHDR: interlace invalid"); + switch (colormodel) { + case 0: + break; + case 3: + if (bitspc == 16) + throw new PngjInputException("bad IHDR: bitdepth invalid"); + break; + case 2: + case 4: + case 6: + if (bitspc != 8 && bitspc != 16) + throw new PngjInputException("bad IHDR: bitdepth invalid"); + break; + default: + throw new PngjInputException("bad IHDR: invalid colormodel"); + } + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkITXT.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkITXT.java new file mode 100644 index 00000000..f24974ac --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkITXT.java @@ -0,0 +1,111 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * iTXt chunk. + *

+ * see http://www.w3.org/TR/PNG/#11iTXt + */ +public class PngChunkITXT extends PngChunkTextVar { + public final static String ID = ChunkHelper.iTXt; + + private boolean compressed = false; + private String langTag = ""; + private String translatedTag = ""; + + // http://www.w3.org/TR/PNG/#11iTXt + public PngChunkITXT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkRaw createRawChunk() { + if (key == null || key.trim().length() == 0) + throw new PngjException("Text chunk key must be non empty"); + try { + ByteArrayOutputStream ba = new ByteArrayOutputStream(); + ba.write(ChunkHelper.toBytes(key)); + ba.write(0); // separator + ba.write(compressed ? 1 : 0); + ba.write(0); // compression method (always 0) + ba.write(ChunkHelper.toBytes(langTag)); + ba.write(0); // separator + ba.write(ChunkHelper.toBytesUTF8(translatedTag)); + ba.write(0); // separator + byte[] textbytes = ChunkHelper.toBytesUTF8(val); + if (compressed) { + textbytes = ChunkHelper.compressBytes(textbytes, true); + } + ba.write(textbytes); + byte[] b = ba.toByteArray(); + ChunkRaw chunk = createEmptyChunk(b.length, false); + chunk.data = b; + return chunk; + } catch (IOException e) { + throw new PngjException(e); + } + } + + @Override + public void parseFromRaw(ChunkRaw c) { + int nullsFound = 0; + int[] nullsIdx = new int[3]; + for (int i = 0; i < c.data.length; i++) { + if (c.data[i] != 0) + continue; + nullsIdx[nullsFound] = i; + nullsFound++; + if (nullsFound == 1) + i += 2; + if (nullsFound == 3) + break; + } + if (nullsFound != 3) + throw new PngjException("Bad formed PngChunkITXT chunk"); + key = ChunkHelper.toString(c.data, 0, nullsIdx[0]); + int i = nullsIdx[0] + 1; + compressed = c.data[i] == 0 ? false : true; + i++; + if (compressed && c.data[i] != 0) + throw new PngjException("Bad formed PngChunkITXT chunk - bad compression method "); + langTag = ChunkHelper.toString(c.data, i, nullsIdx[1] - i); + translatedTag = + ChunkHelper.toStringUTF8(c.data, nullsIdx[1] + 1, nullsIdx[2] - nullsIdx[1] - 1); + i = nullsIdx[2] + 1; + if (compressed) { + byte[] bytes = ChunkHelper.compressBytes(c.data, i, c.data.length - i, false); + val = ChunkHelper.toStringUTF8(bytes); + } else { + val = ChunkHelper.toStringUTF8(c.data, i, c.data.length - i); + } + } + + public boolean isCompressed() { + return compressed; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + + public String getLangtag() { + return langTag; + } + + public void setLangtag(String langtag) { + this.langTag = langtag; + } + + public String getTranslatedTag() { + return translatedTag; + } + + public void setTranslatedTag(String translatedTag) { + this.translatedTag = translatedTag; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkMultiple.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkMultiple.java new file mode 100644 index 00000000..8dd37524 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkMultiple.java @@ -0,0 +1,27 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * PNG chunk type (abstract) that allows multiple instances in same image. + */ +public abstract class PngChunkMultiple extends PngChunk { + + protected PngChunkMultiple(String id, ImageInfo imgInfo) { + super(id, imgInfo); + } + + @Override + public final boolean allowsMultiple() { + return true; + } + + /** + * NOTE: this chunk uses the default Object's equals() hashCode() implementation. + * + * This is the right thing to do, normally. + * + * This is important, eg see ChunkList.removeFromList() + */ + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkOFFS.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkOFFS.java new file mode 100644 index 00000000..e47cf600 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkOFFS.java @@ -0,0 +1,81 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * oFFs chunk. + *

+ * see http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.oFFs + */ +public class PngChunkOFFS extends PngChunkSingle { + public final static String ID = "oFFs"; + + // http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.oFFs + private long posX; + private long posY; + private int units; // 0: pixel 1:micrometer + + public PngChunkOFFS(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(9, true); + PngHelperInternal.writeInt4tobytes((int) posX, c.data, 0); + PngHelperInternal.writeInt4tobytes((int) posY, c.data, 4); + c.data[8] = (byte) units; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 9) + throw new PngjException("bad chunk length " + chunk); + posX = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + if (posX < 0) + posX += 0x100000000L; + posY = PngHelperInternal.readInt4fromBytes(chunk.data, 4); + if (posY < 0) + posY += 0x100000000L; + units = PngHelperInternal.readInt1fromByte(chunk.data, 8); + } + + /** + * 0: pixel, 1:micrometer + */ + public int getUnits() { + return units; + } + + /** + * 0: pixel, 1:micrometer + */ + public void setUnits(int units) { + this.units = units; + } + + public long getPosX() { + return posX; + } + + public void setPosX(long posX) { + this.posX = posX; + } + + public long getPosY() { + return posY; + } + + public void setPosY(long posY) { + this.posY = posY; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkPHYS.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkPHYS.java new file mode 100644 index 00000000..98debb1f --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkPHYS.java @@ -0,0 +1,107 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * pHYs chunk. + *

+ * see http://www.w3.org/TR/PNG/#11pHYs + */ +public class PngChunkPHYS extends PngChunkSingle { + public final static String ID = ChunkHelper.pHYs; + + // http://www.w3.org/TR/PNG/#11pHYs + private long pixelsxUnitX; + private long pixelsxUnitY; + private int units; // 0: unknown 1:metre + + public PngChunkPHYS(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(9, true); + PngHelperInternal.writeInt4tobytes((int) pixelsxUnitX, c.data, 0); + PngHelperInternal.writeInt4tobytes((int) pixelsxUnitY, c.data, 4); + c.data[8] = (byte) units; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 9) + throw new PngjException("bad chunk length " + chunk); + pixelsxUnitX = PngHelperInternal.readInt4fromBytes(chunk.data, 0); + if (pixelsxUnitX < 0) + pixelsxUnitX += 0x100000000L; + pixelsxUnitY = PngHelperInternal.readInt4fromBytes(chunk.data, 4); + if (pixelsxUnitY < 0) + pixelsxUnitY += 0x100000000L; + units = PngHelperInternal.readInt1fromByte(chunk.data, 8); + } + + public long getPixelsxUnitX() { + return pixelsxUnitX; + } + + public void setPixelsxUnitX(long pixelsxUnitX) { + this.pixelsxUnitX = pixelsxUnitX; + } + + public long getPixelsxUnitY() { + return pixelsxUnitY; + } + + public void setPixelsxUnitY(long pixelsxUnitY) { + this.pixelsxUnitY = pixelsxUnitY; + } + + public int getUnits() { + return units; + } + + public void setUnits(int units) { + this.units = units; + } + + // special getters / setters + + /** + * returns -1 if the physicial unit is unknown, or X-Y are not equal + */ + public double getAsDpi() { + if (units != 1 || pixelsxUnitX != pixelsxUnitY) + return -1; + return ((double) pixelsxUnitX) * 0.0254; + } + + /** + * returns -1 if the physicial unit is unknown + */ + public double[] getAsDpi2() { + if (units != 1) + return new double[] {-1, -1}; + return new double[] {((double) pixelsxUnitX) * 0.0254, ((double) pixelsxUnitY) * 0.0254}; + } + + public void setAsDpi(double dpi) { + units = 1; + pixelsxUnitX = (long) (dpi / 0.0254 + 0.5); + pixelsxUnitY = pixelsxUnitX; + } + + public void setAsDpi2(double dpix, double dpiy) { + units = 1; + pixelsxUnitX = (long) (dpix / 0.0254 + 0.5); + pixelsxUnitY = (long) (dpiy / 0.0254 + 0.5); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkPLTE.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkPLTE.java new file mode 100644 index 00000000..f647bde0 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkPLTE.java @@ -0,0 +1,98 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * PLTE chunk. + *

+ * see http://www.w3.org/TR/PNG/#11PLTE + *

+ * Critical chunk + */ +public class PngChunkPLTE extends PngChunkSingle { + public final static String ID = ChunkHelper.PLTE; + + // http://www.w3.org/TR/PNG/#11PLTE + private int nentries = 0; + /** + * RGB8 packed in one integer + */ + private int[] entries; + + public PngChunkPLTE(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NA; + } + + @Override + public ChunkRaw createRawChunk() { + int len = 3 * nentries; + int[] rgb = new int[3]; + ChunkRaw c = createEmptyChunk(len, true); + for (int n = 0, i = 0; n < nentries; n++) { + getEntryRgb(n, rgb); + c.data[i++] = (byte) rgb[0]; + c.data[i++] = (byte) rgb[1]; + c.data[i++] = (byte) rgb[2]; + } + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + setNentries(chunk.len / 3); + for (int n = 0, i = 0; n < nentries; n++) { + setEntry(n, (int) (chunk.data[i++] & 0xff), (int) (chunk.data[i++] & 0xff), + (int) (chunk.data[i++] & 0xff)); + } + } + + public void setNentries(int n) { + nentries = n; + if (nentries < 1 || nentries > 256) + throw new PngjException("invalid pallette - nentries=" + nentries); + if (entries == null || entries.length != nentries) { // alloc + entries = new int[nentries]; + } + } + + public int getNentries() { + return nentries; + } + + public void setEntry(int n, int r, int g, int b) { + entries[n] = ((r << 16) | (g << 8) | b); + } + + public int getEntry(int n) { + return entries[n]; + } + + public void getEntryRgb(int n, int[] rgb) { + getEntryRgb(n, rgb, 0); + } + + public void getEntryRgb(int n, int[] rgb, int offset) { + int v = entries[n]; + rgb[offset + 0] = ((v & 0xff0000) >> 16); + rgb[offset + 1] = ((v & 0xff00) >> 8); + rgb[offset + 2] = (v & 0xff); + } + + public int minBitDepth() { + if (nentries <= 2) + return 1; + else if (nentries <= 4) + return 2; + else if (nentries <= 16) + return 4; + else + return 8; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSBIT.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSBIT.java new file mode 100644 index 00000000..6c6c7a62 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSBIT.java @@ -0,0 +1,114 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * sBIT chunk. + *

+ * see http://www.w3.org/TR/PNG/#11sBIT + *

+ * this chunk structure depends on the image type + */ +public class PngChunkSBIT extends PngChunkSingle { + public final static String ID = ChunkHelper.sBIT; + // http://www.w3.org/TR/PNG/#11sBIT + + // significant bits + private int graysb, alphasb; + private int redsb, greensb, bluesb; + + public PngChunkSBIT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; + } + + private int getCLen() { + int len = imgInfo.greyscale ? 1 : 3; + if (imgInfo.alpha) + len += 1; + return len; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (c.len != getCLen()) + throw new PngjException("bad chunk length " + c); + if (imgInfo.greyscale) { + graysb = PngHelperInternal.readInt1fromByte(c.data, 0); + if (imgInfo.alpha) + alphasb = PngHelperInternal.readInt1fromByte(c.data, 1); + } else { + redsb = PngHelperInternal.readInt1fromByte(c.data, 0); + greensb = PngHelperInternal.readInt1fromByte(c.data, 1); + bluesb = PngHelperInternal.readInt1fromByte(c.data, 2); + if (imgInfo.alpha) + alphasb = PngHelperInternal.readInt1fromByte(c.data, 3); + } + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + c = createEmptyChunk(getCLen(), true); + if (imgInfo.greyscale) { + c.data[0] = (byte) graysb; + if (imgInfo.alpha) + c.data[1] = (byte) alphasb; + } else { + c.data[0] = (byte) redsb; + c.data[1] = (byte) greensb; + c.data[2] = (byte) bluesb; + if (imgInfo.alpha) + c.data[3] = (byte) alphasb; + } + return c; + } + + public void setGraysb(int gray) { + if (!imgInfo.greyscale) + throw new PngjException("only greyscale images support this"); + graysb = gray; + } + + public int getGraysb() { + if (!imgInfo.greyscale) + throw new PngjException("only greyscale images support this"); + return graysb; + } + + public void setAlphasb(int a) { + if (!imgInfo.alpha) + throw new PngjException("only images with alpha support this"); + alphasb = a; + } + + public int getAlphasb() { + if (!imgInfo.alpha) + throw new PngjException("only images with alpha support this"); + return alphasb; + } + + /** + * Set rgb values + * + */ + public void setRGB(int r, int g, int b) { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + redsb = r; + greensb = g; + bluesb = b; + } + + public int[] getRGB() { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + return new int[] {redsb, greensb, bluesb}; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSPLT.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSPLT.java new file mode 100644 index 00000000..89bd57e6 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSPLT.java @@ -0,0 +1,131 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * sPLT chunk. + *

+ * see http://www.w3.org/TR/PNG/#11sPLT + */ +public class PngChunkSPLT extends PngChunkMultiple { + public final static String ID = ChunkHelper.sPLT; + + // http://www.w3.org/TR/PNG/#11sPLT + + private String palName; + private int sampledepth; // 8/16 + private int[] palette; // 5 elements per entry + + public PngChunkSPLT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + try { + ByteArrayOutputStream ba = new ByteArrayOutputStream(); + ba.write(ChunkHelper.toBytes(palName)); + ba.write(0); // separator + ba.write((byte) sampledepth); + int nentries = getNentries(); + for (int n = 0; n < nentries; n++) { + for (int i = 0; i < 4; i++) { + if (sampledepth == 8) + PngHelperInternal.writeByte(ba, (byte) palette[n * 5 + i]); + else + PngHelperInternal.writeInt2(ba, palette[n * 5 + i]); + } + PngHelperInternal.writeInt2(ba, palette[n * 5 + 4]); + } + byte[] b = ba.toByteArray(); + ChunkRaw chunk = createEmptyChunk(b.length, false); + chunk.data = b; + return chunk; + } catch (IOException e) { + throw new PngjException(e); + } + } + + @Override + public void parseFromRaw(ChunkRaw c) { + int t = -1; + for (int i = 0; i < c.data.length; i++) { // look for first zero + if (c.data[i] == 0) { + t = i; + break; + } + } + if (t <= 0 || t > c.data.length - 2) + throw new PngjException("bad sPLT chunk: no separator found"); + palName = ChunkHelper.toString(c.data, 0, t); + sampledepth = PngHelperInternal.readInt1fromByte(c.data, t + 1); + t += 2; + int nentries = (c.data.length - t) / (sampledepth == 8 ? 6 : 10); + palette = new int[nentries * 5]; + int r, g, b, a, f, ne; + ne = 0; + for (int i = 0; i < nentries; i++) { + if (sampledepth == 8) { + r = PngHelperInternal.readInt1fromByte(c.data, t++); + g = PngHelperInternal.readInt1fromByte(c.data, t++); + b = PngHelperInternal.readInt1fromByte(c.data, t++); + a = PngHelperInternal.readInt1fromByte(c.data, t++); + } else { + r = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + g = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + b = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + a = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + } + f = PngHelperInternal.readInt2fromBytes(c.data, t); + t += 2; + palette[ne++] = r; + palette[ne++] = g; + palette[ne++] = b; + palette[ne++] = a; + palette[ne++] = f; + } + } + + public int getNentries() { + return palette.length / 5; + } + + public String getPalName() { + return palName; + } + + public void setPalName(String palName) { + this.palName = palName; + } + + public int getSampledepth() { + return sampledepth; + } + + public void setSampledepth(int sampledepth) { + this.sampledepth = sampledepth; + } + + public int[] getPalette() { + return palette; + } + + public void setPalette(int[] palette) { + this.palette = palette; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSRGB.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSRGB.java new file mode 100644 index 00000000..ff54b4c8 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSRGB.java @@ -0,0 +1,55 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * sRGB chunk. + *

+ * see http://www.w3.org/TR/PNG/#11sRGB + */ +public class PngChunkSRGB extends PngChunkSingle { + public final static String ID = ChunkHelper.sRGB; + + // http://www.w3.org/TR/PNG/#11sRGB + + public static final int RENDER_INTENT_Perceptual = 0; + public static final int RENDER_INTENT_Relative_colorimetric = 1; + public static final int RENDER_INTENT_Saturation = 2; + public static final int RENDER_INTENT_Absolute_colorimetric = 3; + + private int intent; + + public PngChunkSRGB(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_PLTE_AND_IDAT; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (c.len != 1) + throw new PngjException("bad chunk length " + c); + intent = PngHelperInternal.readInt1fromByte(c.data, 0); + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + c = createEmptyChunk(1, true); + c.data[0] = (byte) intent; + return c; + } + + public int getIntent() { + return intent; + } + + public void setIntent(int intent) { + this.intent = intent; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSTER.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSTER.java new file mode 100644 index 00000000..dd33c4d3 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSTER.java @@ -0,0 +1,54 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * sTER chunk. + *

+ * see http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.sTER + */ +public class PngChunkSTER extends PngChunkSingle { + public final static String ID = "sTER"; + + // http://www.libpng.org/pub/png/spec/register/pngext-1.3.0-pdg.html#C.sTER + private byte mode; // 0: cross-fuse layout 1: diverging-fuse layout + + public PngChunkSTER(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(1, true); + c.data[0] = (byte) mode; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 1) + throw new PngjException("bad chunk length " + chunk); + mode = chunk.data[0]; + } + + /** + * 0: cross-fuse layout 1: diverging-fuse layout + */ + public byte getMode() { + return mode; + } + + /** + * 0: cross-fuse layout 1: diverging-fuse layout + */ + public void setMode(byte mode) { + this.mode = mode; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSingle.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSingle.java new file mode 100644 index 00000000..58c23494 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkSingle.java @@ -0,0 +1,43 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * PNG chunk type (abstract) that does not allow multiple instances in same image. + */ +public abstract class PngChunkSingle extends PngChunk { + + protected PngChunkSingle(String id, ImageInfo imgInfo) { + super(id, imgInfo); + } + + public final boolean allowsMultiple() { + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PngChunkSingle other = (PngChunkSingle) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTEXT.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTEXT.java new file mode 100644 index 00000000..ea404edc --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTEXT.java @@ -0,0 +1,44 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * tEXt chunk. + *

+ * see http://www.w3.org/TR/PNG/#11tEXt + */ +public class PngChunkTEXT extends PngChunkTextVar { + public final static String ID = ChunkHelper.tEXt; + + public PngChunkTEXT(ImageInfo info) { + super(ID, info); + } + + public PngChunkTEXT(ImageInfo info, String key, String val) { + super(ID, info); + setKeyVal(key, val); + } + + @Override + public ChunkRaw createRawChunk() { + if (key == null || key.trim().length() == 0) + throw new PngjException("Text chunk key must be non empty"); + byte[] b = ChunkHelper.toBytes(key + "\0" + val); + ChunkRaw chunk = createEmptyChunk(b.length, false); + chunk.data = b; + return chunk; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + int i; + for (i = 0; i < c.data.length; i++) + if (c.data[i] == 0) + break; + key = ChunkHelper.toString(c.data, 0, i); + i++; + val = i < c.data.length ? ChunkHelper.toString(c.data, i, c.data.length - i) : ""; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTIME.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTIME.java new file mode 100644 index 00000000..21e15132 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTIME.java @@ -0,0 +1,82 @@ +package ar.com.hjg.pngj.chunks; + +import java.util.Calendar; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * tIME chunk. + *

+ * see http://www.w3.org/TR/PNG/#11tIME + */ +public class PngChunkTIME extends PngChunkSingle { + public final static String ID = ChunkHelper.tIME; + + // http://www.w3.org/TR/PNG/#11tIME + private int year, mon, day, hour, min, sec; + + public PngChunkTIME(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = createEmptyChunk(7, true); + PngHelperInternal.writeInt2tobytes(year, c.data, 0); + c.data[2] = (byte) mon; + c.data[3] = (byte) day; + c.data[4] = (byte) hour; + c.data[5] = (byte) min; + c.data[6] = (byte) sec; + return c; + } + + @Override + public void parseFromRaw(ChunkRaw chunk) { + if (chunk.len != 7) + throw new PngjException("bad chunk " + chunk); + year = PngHelperInternal.readInt2fromBytes(chunk.data, 0); + mon = PngHelperInternal.readInt1fromByte(chunk.data, 2); + day = PngHelperInternal.readInt1fromByte(chunk.data, 3); + hour = PngHelperInternal.readInt1fromByte(chunk.data, 4); + min = PngHelperInternal.readInt1fromByte(chunk.data, 5); + sec = PngHelperInternal.readInt1fromByte(chunk.data, 6); + } + + public void setNow(int secsAgo) { + Calendar d = Calendar.getInstance(); + d.setTimeInMillis(System.currentTimeMillis() - 1000 * (long) secsAgo); + year = d.get(Calendar.YEAR); + mon = d.get(Calendar.MONTH) + 1; + day = d.get(Calendar.DAY_OF_MONTH); + hour = d.get(Calendar.HOUR_OF_DAY); + min = d.get(Calendar.MINUTE); + sec = d.get(Calendar.SECOND); + } + + public void setYMDHMS(int yearx, int monx, int dayx, int hourx, int minx, int secx) { + year = yearx; + mon = monx; + day = dayx; + hour = hourx; + min = minx; + sec = secx; + } + + public int[] getYMDHMS() { + return new int[] {year, mon, day, hour, min, sec}; + } + + /** format YYYY/MM/DD HH:mm:SS */ + public String getAsString() { + return String.format("%04d/%02d/%02d %02d:%02d:%02d", year, mon, day, hour, min, sec); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTRNS.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTRNS.java new file mode 100644 index 00000000..82ad30fd --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTRNS.java @@ -0,0 +1,149 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjException; + +/** + * tRNS chunk. + *

+ * see http://www.w3.org/TR/PNG/#11tRNS + *

+ * this chunk structure depends on the image type + */ +public class PngChunkTRNS extends PngChunkSingle { + public final static String ID = ChunkHelper.tRNS; + + // http://www.w3.org/TR/PNG/#11tRNS + + // only one of these is meaningful, depending on the image type + private int gray; + private int red, green, blue; + private int[] paletteAlpha = new int[] {}; + + public PngChunkTRNS(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.AFTER_PLTE_BEFORE_IDAT; + } + + @Override + public ChunkRaw createRawChunk() { + ChunkRaw c = null; + if (imgInfo.greyscale) { + c = createEmptyChunk(2, true); + PngHelperInternal.writeInt2tobytes(gray, c.data, 0); + } else if (imgInfo.indexed) { + c = createEmptyChunk(paletteAlpha.length, true); + for (int n = 0; n < c.len; n++) { + c.data[n] = (byte) paletteAlpha[n]; + } + } else { + c = createEmptyChunk(6, true); + PngHelperInternal.writeInt2tobytes(red, c.data, 0); + PngHelperInternal.writeInt2tobytes(green, c.data, 0); + PngHelperInternal.writeInt2tobytes(blue, c.data, 0); + } + return c; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + if (imgInfo.greyscale) { + gray = PngHelperInternal.readInt2fromBytes(c.data, 0); + } else if (imgInfo.indexed) { + int nentries = c.data.length; + paletteAlpha = new int[nentries]; + for (int n = 0; n < nentries; n++) { + paletteAlpha[n] = (int) (c.data[n] & 0xff); + } + } else { + red = PngHelperInternal.readInt2fromBytes(c.data, 0); + green = PngHelperInternal.readInt2fromBytes(c.data, 2); + blue = PngHelperInternal.readInt2fromBytes(c.data, 4); + } + } + + /** + * Set rgb values + * + */ + public void setRGB(int r, int g, int b) { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + red = r; + green = g; + blue = b; + } + + public int[] getRGB() { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + return new int[] {red, green, blue}; + } + + public int getRGB888() { + if (imgInfo.greyscale || imgInfo.indexed) + throw new PngjException("only rgb or rgba images support this"); + return (red << 16) | (green << 8) | blue; + } + + public void setGray(int g) { + if (!imgInfo.greyscale) + throw new PngjException("only grayscale images support this"); + gray = g; + } + + public int getGray() { + if (!imgInfo.greyscale) + throw new PngjException("only grayscale images support this"); + return gray; + } + + /** + * Sets the length of the palette alpha. This should be followed by #setNentriesPalAlpha + * + * @param idx index inside the table + * @param val alpha value (0-255) + */ + public void setEntryPalAlpha(int idx, int val) { + paletteAlpha[idx] = val; + } + + public void setNentriesPalAlpha(int len) { + paletteAlpha = new int[len]; + } + + /** + * WARNING: non deep copy. See also {@link #setNentriesPalAlpha(int)} {@link #setEntryPalAlpha(int, int)} + */ + public void setPalAlpha(int[] palAlpha) { + if (!imgInfo.indexed) + throw new PngjException("only indexed images support this"); + paletteAlpha = palAlpha; + } + + /** + * WARNING: non deep copy + */ + public int[] getPalletteAlpha() { + return paletteAlpha; + } + + /** + * to use when only one pallete index is set as totally transparent + */ + public void setIndexEntryAsTransparent(int palAlphaIndex) { + if (!imgInfo.indexed) + throw new PngjException("only indexed images support this"); + paletteAlpha = new int[] {palAlphaIndex + 1}; + for (int i = 0; i < palAlphaIndex; i++) + paletteAlpha[i] = 255; + paletteAlpha[palAlphaIndex] = 0; + } + + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTextVar.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTextVar.java new file mode 100644 index 00000000..24ece4de --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkTextVar.java @@ -0,0 +1,60 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * Superclass (abstract) for three textual chunks (TEXT, ITXT, ZTXT) + */ +public abstract class PngChunkTextVar extends PngChunkMultiple { + protected String key; // key/val: only for tEXt. lazy computed + protected String val; + + // http://www.w3.org/TR/PNG/#11keywords + public final static String KEY_Title = "Title"; // Short (one line) title or caption for image + public final static String KEY_Author = "Author"; // Name of image's creator + public final static String KEY_Description = "Description"; // Description of image (possibly + // long) + public final static String KEY_Copyright = "Copyright"; // Copyright notice + public final static String KEY_Creation_Time = "Creation Time"; // Time of original image creation + public final static String KEY_Software = "Software"; // Software used to create the image + public final static String KEY_Disclaimer = "Disclaimer"; // Legal disclaimer + public final static String KEY_Warning = "Warning"; // Warning of nature of content + public final static String KEY_Source = "Source"; // Device used to create the image + public final static String KEY_Comment = "Comment"; // Miscellaneous comment + + protected PngChunkTextVar(String id, ImageInfo info) { + super(id, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + public static class PngTxtInfo { + public String title; + public String author; + public String description; + public String creation_time;// = (new Date()).toString(); + public String software; + public String disclaimer; + public String warning; + public String source; + public String comment; + + } + + public String getKey() { + return key; + } + + public String getVal() { + return val; + } + + public void setKeyVal(String key, String val) { + this.key = key; + this.val = val; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkUNKNOWN.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkUNKNOWN.java new file mode 100644 index 00000000..a9778f76 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkUNKNOWN.java @@ -0,0 +1,40 @@ +package ar.com.hjg.pngj.chunks; + +import ar.com.hjg.pngj.ImageInfo; + +/** + * Placeholder for UNKNOWN (custom or not) chunks. + *

+ * For PngReader, a chunk is unknown if it's not registered in the chunk factory + */ +public class PngChunkUNKNOWN extends PngChunkMultiple { // unkown, custom or not + + public PngChunkUNKNOWN(String id, ImageInfo info) { + super(id, info); + } + + @Override + public ChunkOrderingConstraint getOrderingConstraint() { + return ChunkOrderingConstraint.NONE; + } + + @Override + public ChunkRaw createRawChunk() { + return raw; + } + + @Override + public void parseFromRaw(ChunkRaw c) { + + } + + /* does not do deep copy! */ + public byte[] getData() { + return raw.data; + } + + /* does not do deep copy! */ + public void setData(byte[] data) { + raw.data = data; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkZTXT.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkZTXT.java new file mode 100644 index 00000000..6b172e62 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngChunkZTXT.java @@ -0,0 +1,62 @@ +package ar.com.hjg.pngj.chunks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjException; + +/** + * zTXt chunk. + *

+ * see http://www.w3.org/TR/PNG/#11zTXt + */ +public class PngChunkZTXT extends PngChunkTextVar { + public final static String ID = ChunkHelper.zTXt; + + // http://www.w3.org/TR/PNG/#11zTXt + public PngChunkZTXT(ImageInfo info) { + super(ID, info); + } + + @Override + public ChunkRaw createRawChunk() { + if (key == null || key.trim().length() == 0) + throw new PngjException("Text chunk key must be non empty"); + try { + ByteArrayOutputStream ba = new ByteArrayOutputStream(); + ba.write(ChunkHelper.toBytes(key)); + ba.write(0); // separator + ba.write(0); // compression method: 0 + byte[] textbytes = ChunkHelper.compressBytes(ChunkHelper.toBytes(val), true); + ba.write(textbytes); + byte[] b = ba.toByteArray(); + ChunkRaw chunk = createEmptyChunk(b.length, false); + chunk.data = b; + return chunk; + } catch (IOException e) { + throw new PngjException(e); + } + } + + @Override + public void parseFromRaw(ChunkRaw c) { + int nullsep = -1; + for (int i = 0; i < c.data.length; i++) { // look for first zero + if (c.data[i] != 0) + continue; + nullsep = i; + break; + } + if (nullsep < 0 || nullsep > c.data.length - 2) + throw new PngjException("bad zTXt chunk: no separator found"); + key = ChunkHelper.toString(c.data, 0, nullsep); + int compmet = (int) c.data[nullsep + 1]; + if (compmet != 0) + throw new PngjException("bad zTXt chunk: unknown compression method"); + byte[] uncomp = + ChunkHelper.compressBytes(c.data, nullsep + 2, c.data.length - nullsep - 2, false); // uncompress + val = ChunkHelper.toString(uncomp); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/PngMetadata.java b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngMetadata.java new file mode 100644 index 00000000..64912c45 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/PngMetadata.java @@ -0,0 +1,230 @@ +package ar.com.hjg.pngj.chunks; + +import java.util.ArrayList; +import java.util.List; + +import ar.com.hjg.pngj.PngjException; + +/** + * We consider "image metadata" every info inside the image except for the most basic image info (IHDR chunk - ImageInfo + * class) and the pixels values. + *

+ * This includes the palette (if present) and all the ancillary chunks + *

+ * This class provides a wrapper over the collection of chunks of a image (read or to write) and provides some high + * level methods to access them + */ +public class PngMetadata { + private final ChunksList chunkList; + private final boolean readonly; + + public PngMetadata(ChunksList chunks) { + this.chunkList = chunks; + if (chunks instanceof ChunksListForWrite) { + this.readonly = false; + } else { + this.readonly = true; + } + } + + /** + * Queues the chunk at the writer + *

+ * lazyOverwrite: if true, checks if there is a queued "equivalent" chunk and if so, overwrites it. However if that + * not check for already written chunks. + */ + public void queueChunk(final PngChunk c, boolean lazyOverwrite) { + ChunksListForWrite cl = getChunkListW(); + if (readonly) + throw new PngjException("cannot set chunk : readonly metadata"); + if (lazyOverwrite) { + ChunkHelper.trimList(cl.getQueuedChunks(), new ChunkPredicate() { + public boolean match(PngChunk c2) { + return ChunkHelper.equivalent(c, c2); + } + }); + } + cl.queue(c); + } + + public void queueChunk(final PngChunk c) { + queueChunk(c, true); + } + + private ChunksListForWrite getChunkListW() { + return (ChunksListForWrite) chunkList; + } + + // ///// high level utility methods follow //////////// + + // //////////// DPI + + /** + * returns -1 if not found or dimension unknown + */ + public double[] getDpi() { + PngChunk c = chunkList.getById1(ChunkHelper.pHYs, true); + if (c == null) + return new double[] {-1, -1}; + else + return ((PngChunkPHYS) c).getAsDpi2(); + } + + public void setDpi(double x) { + setDpi(x, x); + } + + public void setDpi(double x, double y) { + PngChunkPHYS c = new PngChunkPHYS(chunkList.imageInfo); + c.setAsDpi2(x, y); + queueChunk(c); + } + + // //////////// TIME + + /** + * Creates a time chunk with current time, less secsAgo seconds + *

+ * + * @return Returns the created-queued chunk, just in case you want to examine or modify it + */ + public PngChunkTIME setTimeNow(int secsAgo) { + PngChunkTIME c = new PngChunkTIME(chunkList.imageInfo); + c.setNow(secsAgo); + queueChunk(c); + return c; + } + + public PngChunkTIME setTimeNow() { + return setTimeNow(0); + } + + /** + * Creates a time chunk with diven date-time + *

+ * + * @return Returns the created-queued chunk, just in case you want to examine or modify it + */ + public PngChunkTIME setTimeYMDHMS(int yearx, int monx, int dayx, int hourx, int minx, int secx) { + PngChunkTIME c = new PngChunkTIME(chunkList.imageInfo); + c.setYMDHMS(yearx, monx, dayx, hourx, minx, secx); + queueChunk(c, true); + return c; + } + + /** + * null if not found + */ + public PngChunkTIME getTime() { + return (PngChunkTIME) chunkList.getById1(ChunkHelper.tIME); + } + + public String getTimeAsString() { + PngChunkTIME c = getTime(); + return c == null ? "" : c.getAsString(); + } + + // //////////// TEXT + + /** + * Creates a text chunk and queue it. + *

+ * + * @param k : key (latin1) + * @param val (arbitrary, should be latin1 if useLatin1) + * @param useLatin1 + * @param compress + * @return Returns the created-queued chunks, just in case you want to examine, touch it + */ + public PngChunkTextVar setText(String k, String val, boolean useLatin1, boolean compress) { + if (compress && !useLatin1) + throw new PngjException("cannot compress non latin text"); + PngChunkTextVar c; + if (useLatin1) { + if (compress) { + c = new PngChunkZTXT(chunkList.imageInfo); + } else { + c = new PngChunkTEXT(chunkList.imageInfo); + } + } else { + c = new PngChunkITXT(chunkList.imageInfo); + ((PngChunkITXT) c).setLangtag(k); // we use the same orig tag (this is not quite right) + } + c.setKeyVal(k, val); + queueChunk(c, true); + return c; + } + + public PngChunkTextVar setText(String k, String val) { + return setText(k, val, false, false); + } + + /** + * gets all text chunks with a given key + *

+ * returns null if not found + *

+ * Warning: this does not check the "lang" key of iTxt + */ + @SuppressWarnings("unchecked") + public List getTxtsForKey(String k) { + @SuppressWarnings("rawtypes") + List c = new ArrayList(); + c.addAll(chunkList.getById(ChunkHelper.tEXt, k)); + c.addAll(chunkList.getById(ChunkHelper.zTXt, k)); + c.addAll(chunkList.getById(ChunkHelper.iTXt, k)); + return c; + } + + /** + * Returns empty if not found, concatenated (with newlines) if multiple! - and trimmed + *

+ * Use getTxtsForKey() if you don't want this behaviour + */ + public String getTxtForKey(String k) { + List li = getTxtsForKey(k); + if (li.isEmpty()) + return ""; + StringBuilder t = new StringBuilder(); + for (PngChunkTextVar c : li) + t.append(c.getVal()).append("\n"); + return t.toString().trim(); + } + + /** + * Returns the palette chunk, if present + * + * @return null if not present + */ + public PngChunkPLTE getPLTE() { + return (PngChunkPLTE) chunkList.getById1(PngChunkPLTE.ID); + } + + /** + * Creates a new empty palette chunk, queues it for write and return it to the caller, who should fill its entries + */ + public PngChunkPLTE createPLTEChunk() { + PngChunkPLTE plte = new PngChunkPLTE(chunkList.imageInfo); + queueChunk(plte); + return plte; + } + + /** + * Returns the TRNS chunk, if present + * + * @return null if not present + */ + public PngChunkTRNS getTRNS() { + return (PngChunkTRNS) chunkList.getById1(PngChunkTRNS.ID); + } + + /** + * Creates a new empty TRNS chunk, queues it for write and return it to the caller, who should fill its entries + */ + public PngChunkTRNS createTRNSChunk() { + PngChunkTRNS trns = new PngChunkTRNS(chunkList.imageInfo); + queueChunk(trns); + return trns; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/chunks/package.html b/src/js-specific/java/ar/com/hjg/pngj/chunks/package.html new file mode 100644 index 00000000..13740669 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/chunks/package.html @@ -0,0 +1,9 @@ + + +

+Contains the code related to chunk management for the PNGJ library.

+

+Only needed by client code if some special chunk handling is required. +

+ + diff --git a/src/js-specific/java/ar/com/hjg/pngj/package.html b/src/js-specific/java/ar/com/hjg/pngj/package.html new file mode 100644 index 00000000..a417d56b --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/package.html @@ -0,0 +1,49 @@ + + +

+PNGJ main package +

+

+Users of this library should rarely need more than the public members of this package.
+Newcomers: start with PngReader and PngWriter. +

+

+Example of use: this code reads a true colour PNG image (RGB8 or RGBA8) +and reduces the red channel by half, increasing the green by 20. +It copies all the "safe" metadata from the original image, and adds a textual metadata. + +

+  public static void convert(String origFilename, String destFilename) {
+    // you can also use PngReader (esentially the same) or PngReaderByte 
+    PngReaderInt pngr = new PngReaderInt(new File(origFilename));  
+    System.out.println(pngr.toString());
+    int channels = pngr.imgInfo.channels;
+    if (channels < 3 || pngr.imgInfo.bitDepth != 8)
+       throw new RuntimeException("For simplicity this supports only RGB8/RGBA8 images");
+    // writer with same image properties as original
+    PngWriter pngw = new PngWriter(new File(destFilename), pngr.imgInfo, true);
+    // instruct the writer to grab all ancillary chunks from the original
+    pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_ALL_SAFE);
+    // add a textual chunk to writer
+    pngw.getMetadata().setText(PngChunkTextVar.KEY_Description, "Decreased red and increased green");
+    // also: while(pngr.hasMoreRows())
+    for (int row = 0; row < pngr.imgInfo.rows; row++) {  
+       ImageLineInt l1 = pngr.readRowInt(); // each element is a sample
+       int[] scanline = l1.getScanline(); // to save typing
+       for (int j = 0; j < pngr.imgInfo.cols; j++) {
+          scanline[j * channels] /= 2;
+          scanline[j * channels + 1] = ImageLineHelper.clampTo_0_255(scanline[j * channels + 1] + 20);
+       }
+       pngw.writeRow(l1);
+    }
+    pngr.end(); // it's recommended to end the reader first, in case there are trailing chunks to read
+    pngw.end();
+ }
+
+
+ +For more examples, see the tests and samples. + +

+ + diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStream.java b/src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStream.java new file mode 100644 index 00000000..6b4d86f3 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStream.java @@ -0,0 +1,160 @@ +package ar.com.hjg.pngj.pixels; + +import java.io.OutputStream; + +import ar.com.hjg.pngj.IDatChunkWriter; + +/** + * This is an OutputStream that compresses (via Deflater or a deflater-like object), and optionally passes the + * compressed stream to another output stream. + * + * It allows to compute in/out/ratio stats. + * + * It works as a stream (similar to DeflaterOutputStream), but it's peculiar in that it expects that each writes has a + * fixed length (other lenghts are accepted, but it's less efficient) and that the total amount of bytes is known (so it + * can close itself, but it can also be closed on demand) In PNGJ use, the block is typically a row (including filter + * byte). + * + * We use this to do the real compression (with Deflate) but also to compute tentative estimators + * + * If not closed, it can be recicled via reset() + * + * + */ +public abstract class CompressorStream extends OutputStream { + + protected IDatChunkWriter idatChunkWriter; + public final int blockLen; + public final long totalbytes; + + boolean closed = false; + protected boolean done = false; + protected long bytesIn = 0; + protected long bytesOut = 0; + protected int block = -1; + + /** optionally stores the first byte of each block (row) */ + private byte[] firstBytes; + protected boolean storeFirstByte = false; + + /** + * + * @param idatCw Can be null (if we are only interested in compute compression ratio) + * @param blockLen Estimated maximum block length. If unknown, use -1. + * @param totalbytes Expected total bytes to be fed. If unknown, use -1. + */ + public CompressorStream(IDatChunkWriter idatCw, int blockLen, long totalbytes) { + this.idatChunkWriter = idatCw; + if (blockLen < 0) + blockLen = 4096; + if (totalbytes < 0) + totalbytes = Long.MAX_VALUE; + if (blockLen < 1 || totalbytes < 1) + throw new RuntimeException(" maxBlockLen or totalLen invalid"); + this.blockLen = blockLen; + this.totalbytes = totalbytes; + } + + /** Releases resources. Idempotent. */ + @Override + public void close() { + done(); + if(idatChunkWriter!=null) idatChunkWriter.close(); + closed = true; + } + + /** + * Will be called automatically when the number of bytes reaches the total expected Can be also be called from + * outside. This should set the flag done=true + */ + public abstract void done(); + + @Override + public final void write(byte[] data) { + write(data, 0, data.length); + } + + @Override + public final void write(byte[] data, int off, int len) { + block++; + if (len <= blockLen) { // normal case + mywrite(data, off, len); + if (storeFirstByte && block < firstBytes.length) { + firstBytes[block] = data[off]; // only makes sense in this case + } + } else { + while (len > 0) { + mywrite(data, off, blockLen); + off += blockLen; + len -= blockLen; + } + } + if (bytesIn >= totalbytes) + done(); + + } + + /** + * same as write, but guarantedd to not exceed blockLen The implementation should update bytesOut and bytesInt but not + * check for totalBytes + */ + public abstract void mywrite(byte[] data, int off, int len); + + + /** + * compressed/raw. This should be called only when done + */ + public final double getCompressionRatio() { + return bytesOut == 0 ? 1.0 : bytesOut / (double) bytesIn; + } + + /** + * raw (input) bytes. This should be called only when done + */ + public final long getBytesRaw() { + return bytesIn; + } + + /** + * compressed (out) bytes. This should be called only when done + */ + public final long getBytesCompressed() { + return bytesOut; + } + + public boolean isClosed() { + return closed; + } + + public boolean isDone() { + return done; + } + + public byte[] getFirstBytes() { + return firstBytes; + } + + public void setStoreFirstByte(boolean storeFirstByte, int nblocks) { + this.storeFirstByte = storeFirstByte; + if (this.storeFirstByte) { + if (firstBytes == null || firstBytes.length < nblocks) + firstBytes = new byte[nblocks]; + } else + firstBytes = null; + } + + public void reset() { + done(); + bytesIn = 0; + bytesOut = 0; + block = -1; + done = false; + } + + @Override + public void write(int i) { // should not be used + write(new byte[] {(byte) i}); + } + + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamDeflater.java b/src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamDeflater.java new file mode 100644 index 00000000..24bb7e43 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamDeflater.java @@ -0,0 +1,104 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.zip.Deflater; + +import ar.com.hjg.pngj.IDatChunkWriter; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * CompressorStream backed by a Deflater. + * + * Note that the Deflater is not disposed after done, you should either recycle this with reset() or dispose it with + * close() + * + */ +public class CompressorStreamDeflater extends CompressorStream { + + protected Deflater deflater; + protected byte[] buf1; // temporary storage of compressed bytes: only used if idatWriter is null + protected boolean deflaterIsOwn = true; + + /** if a deflater is passed, it must be already reset. It will not be released on close */ + public CompressorStreamDeflater(IDatChunkWriter idatCw, int maxBlockLen, long totalLen, + Deflater def) { + super(idatCw, maxBlockLen, totalLen); + this.deflater = def == null ? new Deflater() : def; + this.deflaterIsOwn = def == null; + } + + public CompressorStreamDeflater(IDatChunkWriter idatCw, int maxBlockLen, long totalLen) { + this(idatCw, maxBlockLen, totalLen, null); + } + + public CompressorStreamDeflater(IDatChunkWriter idatCw, int maxBlockLen, long totalLen, + int deflaterCompLevel, int deflaterStrategy) { + this(idatCw, maxBlockLen, totalLen, new Deflater(deflaterCompLevel)); + this.deflaterIsOwn = true; + deflater.setStrategy(deflaterStrategy); + } + + @Override + public void mywrite(byte[] data, int off, final int len) { + if (deflater.finished() || done || closed) + throw new PngjOutputException("write beyond end of stream"); + deflater.setInput(data, off, len); + bytesIn += len; + while (!deflater.needsInput()) + deflate(); + } + + protected void deflate() { + byte[] buf; + int off, n; + if (idatChunkWriter != null) { + buf = idatChunkWriter.getBuf(); + off = idatChunkWriter.getOffset(); + n = idatChunkWriter.getAvailLen(); + } else { + if (buf1 == null) + buf1 = new byte[4096]; + buf = buf1; + off = 0; + n = buf1.length; + } + int len = deflater.deflate(buf, off, n); + if (len > 0) { + if (idatChunkWriter != null) + idatChunkWriter.incrementOffset(len); + bytesOut += len; + } + } + + /** automatically called when done */ + @Override + public void done() { + if (done) + return; + if (!deflater.finished()) { + deflater.finish(); + while (!deflater.finished()) + deflate(); + } + done = true; + if (idatChunkWriter != null) + idatChunkWriter.close(); + } + + public void close() { + done(); + try { + if (deflaterIsOwn) { + deflater.end(); + } + } catch (Exception e) { + } + super.close(); + } + + @Override + public void reset() { + deflater.reset(); + super.reset(); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamLz4.java b/src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamLz4.java new file mode 100644 index 00000000..299d3668 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/CompressorStreamLz4.java @@ -0,0 +1,94 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.zip.Deflater; + +import ar.com.hjg.pngj.IDatChunkWriter; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * This class uses a quick compressor to get a rough estimate of deflate compression ratio. + * + * This just ignores the outputStream, and the deflater related parameters + */ +public class CompressorStreamLz4 extends CompressorStream { + + private final DeflaterEstimatorLz4 lz4; + + private byte[] buf; // lazily allocated, only if needed + private final int buffer_size; + // bufpos=bytes in buffer yet not compressed (bytesIn include this) + private int inbuf = 0; + + private static final int MAX_BUFFER_SIZE = 16000; + + public CompressorStreamLz4(IDatChunkWriter os, int maxBlockLen, long totalLen) { + super(os, maxBlockLen, totalLen); + lz4 = new DeflaterEstimatorLz4(); + buffer_size = (int) (totalLen > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : totalLen); + } + + public CompressorStreamLz4(IDatChunkWriter os, int maxBlockLen, long totalLen, Deflater def) { + this(os, maxBlockLen, totalLen);// edlfater ignored + } + + public CompressorStreamLz4(IDatChunkWriter os, int maxBlockLen, long totalLen, + int deflaterCompLevel, int deflaterStrategy) { + this(os, maxBlockLen, totalLen); // paramters ignored + } + + @Override + public void mywrite(byte[] b, int off, int len) { + if (len == 0) + return; + if (done || closed) + throw new PngjOutputException("write beyond end of stream"); + bytesIn += len; + while (len > 0) { + if (inbuf == 0 && (len >= MAX_BUFFER_SIZE || bytesIn == totalbytes)) { + // direct copy (buffer might be null or empty) + bytesOut += lz4.compressEstim(b, off, len); + len = 0; + } else { + if (buf == null) + buf = new byte[buffer_size]; + int len1 = inbuf + len <= buffer_size ? len : buffer_size - inbuf; // to copy + if (len1 > 0) + System.arraycopy(b, off, buf, inbuf, len1); + inbuf += len1; + len -= len1; + off += len1; + if (inbuf == buffer_size) + compressFromBuffer(); + } + } + } + + void compressFromBuffer() { + if (inbuf > 0) { + bytesOut += lz4.compressEstim(buf, 0, inbuf); + inbuf = 0; + } + } + + @Override + public void done() { + if (!done) { + compressFromBuffer(); + done = true; + } + } + + @Override + public void close() { + done(); + if (!closed) { + super.close(); + buf = null; + } + } + + public void reset() { + super.reset(); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorHjg.java b/src/js-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorHjg.java new file mode 100644 index 00000000..cfd332e2 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorHjg.java @@ -0,0 +1,258 @@ +package ar.com.hjg.pngj.pixels; + + +final public class DeflaterEstimatorHjg { + + /** + * This object is stateless, it's thread safe and can be reused + */ + public DeflaterEstimatorHjg() {} + + /** + * Estimates the length of the compressed bytes, as compressed by Lz4 WARNING: if larger than LZ4_64K_LIMIT it cuts it + * in fragments + * + * WARNING: if some part of the input is discarded, this should return the proportional (so that + * returnValue/srcLen=compressionRatio) + * + * @param src + * @param srcOff + * @param srcLen + * @return length of the compressed bytes + */ + public int compressEstim(byte[] src, int srcOff, final int srcLen) { + if (srcLen < 10) + return srcLen; // too small + int stride = LZ4_64K_LIMIT - 1; + int segments = (srcLen + stride - 1) / stride; + stride = srcLen / segments; + if (stride >= LZ4_64K_LIMIT - 1 || stride * segments > srcLen || segments < 1 || stride < 1) + throw new RuntimeException("?? " + srcLen); + int bytesIn = 0; + int bytesOut = 0; + int len = srcLen; + while (len > 0) { + if (len > stride) + len = stride; + bytesOut += compress64k(src, srcOff, len); + srcOff += len; + bytesIn += len; + len = srcLen - bytesIn; + } + double ratio = bytesOut / (double) bytesIn; + return bytesIn == srcLen ? bytesOut : (int) (ratio * srcLen + 0.5); + } + + public int compressEstim(byte[] src) { + return compressEstim(src, 0, src.length); + } + + static final int MEMORY_USAGE = 14; + static final int NOT_COMPRESSIBLE_DETECTION_LEVEL = 6; // see SKIP_STRENGTH + + static final int MIN_MATCH = 4; + + static final int HASH_LOG = MEMORY_USAGE - 2; + static final int HASH_TABLE_SIZE = 1 << HASH_LOG; + + static final int SKIP_STRENGTH = Math.max(NOT_COMPRESSIBLE_DETECTION_LEVEL, 2); // 6 findMatchAttempts = + // 2^SKIP_STRENGTH+3 + static final int COPY_LENGTH = 8; + static final int LAST_LITERALS = 5; + static final int MF_LIMIT = COPY_LENGTH + MIN_MATCH; + static final int MIN_LENGTH = MF_LIMIT + 1; + + static final int MAX_DISTANCE = 1 << 16; + + static final int ML_BITS = 4; + static final int ML_MASK = (1 << ML_BITS) - 1; + static final int RUN_BITS = 8 - ML_BITS; + static final int RUN_MASK = (1 << RUN_BITS) - 1; + + static final int LZ4_64K_LIMIT = (1 << 16) + (MF_LIMIT - 1); + static final int HASH_LOG_64K = HASH_LOG + 1; + static final int HASH_TABLE_SIZE_64K = 1 << HASH_LOG_64K; + + static final int HASH_LOG_HC = 15; + static final int HASH_TABLE_SIZE_HC = 1 << HASH_LOG_HC; + static final int OPTIMAL_ML = ML_MASK - 1 + MIN_MATCH; + + static int compress64k(byte[] src, final int srcOff, final int srcLen) { + final int srcEnd = srcOff + srcLen; + final int srcLimit = srcEnd - LAST_LITERALS; + final int mflimit = srcEnd - MF_LIMIT; + + int sOff = srcOff, dOff = 0; + + int anchor = sOff; + + if (srcLen >= MIN_LENGTH) { + + final short[] hashTable = new short[HASH_TABLE_SIZE_64K]; + + ++sOff; + + main: while (true) { + + // find a match + int forwardOff = sOff; + + int ref; + int findMatchAttempts1 = (1 << SKIP_STRENGTH) + 3; // 64+3=67 + do { + sOff = forwardOff; + forwardOff += findMatchAttempts1++ >>> SKIP_STRENGTH; + + if (forwardOff > mflimit) { + break main; // ends all + } + + final int h = hash64k(readInt(src, sOff)); + ref = srcOff + readShort(hashTable, h); + writeShort(hashTable, h, sOff - srcOff); + } while (!readIntEquals(src, ref, sOff)); + + // catch up + final int excess = commonBytesBackward(src, ref, sOff, srcOff, anchor); + sOff -= excess; + ref -= excess; + // sequence == refsequence + final int runLen = sOff - anchor; + dOff++; + + if (runLen >= RUN_MASK) { + if (runLen > RUN_MASK) + dOff += (runLen - RUN_MASK) / 0xFF; + dOff++; + } + dOff += runLen; + while (true) { + // encode offset + dOff += 2; + // count nb matches + sOff += MIN_MATCH; + ref += MIN_MATCH; + final int matchLen = commonBytes(src, ref, sOff, srcLimit); + sOff += matchLen; + // encode match len + if (matchLen >= ML_MASK) { + if (matchLen >= ML_MASK + 0xFF) + dOff += (matchLen - ML_MASK) / 0xFF; + dOff++; + } + // test end of chunk + if (sOff > mflimit) { + anchor = sOff; + break main; + } + // fill table + writeShort(hashTable, hash64k(readInt(src, sOff - 2)), sOff - 2 - srcOff); + // test next position + final int h = hash64k(readInt(src, sOff)); + ref = srcOff + readShort(hashTable, h); + writeShort(hashTable, h, sOff - srcOff); + if (!readIntEquals(src, sOff, ref)) { + break; + } + dOff++; + } + // prepare next loop + anchor = sOff++; + } + } + int runLen = srcEnd - anchor; + if (runLen >= RUN_MASK + 0xFF) { + dOff += (runLen - RUN_MASK) / 0xFF; + } + dOff++; + dOff += runLen; + return dOff; + } + + static final int maxCompressedLength(int length) { + if (length < 0) { + throw new IllegalArgumentException("length must be >= 0, got " + length); + } + return length + length / 255 + 16; + } + + static int hash(int i) { + return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG); + } + + static int hash64k(int i) { + return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG_64K); + } + + static int readShortLittleEndian(byte[] buf, int i) { + return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8); + } + + static boolean readIntEquals(byte[] buf, int i, int j) { + return buf[i] == buf[j] && buf[i + 1] == buf[j + 1] && buf[i + 2] == buf[j + 2] + && buf[i + 3] == buf[j + 3]; + } + + static int commonBytes(byte[] b, int o1, int o2, int limit) { + int count = 0; + while (o2 < limit && b[o1++] == b[o2++]) { + ++count; + } + return count; + } + + static int commonBytesBackward(byte[] b, int o1, int o2, int l1, int l2) { + int count = 0; + while (o1 > l1 && o2 > l2 && b[--o1] == b[--o2]) { + ++count; + } + return count; + } + + static int readShort(short[] buf, int off) { + return buf[off] & 0xFFFF; + } + + static byte readByte(byte[] buf, int i) { + return buf[i]; + } + + static void checkRange(byte[] buf, int off) { + if (off < 0 || off >= buf.length) { + throw new ArrayIndexOutOfBoundsException(off); + } + } + + static void checkRange(byte[] buf, int off, int len) { + checkLength(len); + if (len > 0) { + checkRange(buf, off); + checkRange(buf, off + len - 1); + } + } + + static void checkLength(int len) { + if (len < 0) { + throw new IllegalArgumentException("lengths must be >= 0"); + } + } + + static int readIntBE(byte[] buf, int i) { + return ((buf[i] & 0xFF) << 24) | ((buf[i + 1] & 0xFF) << 16) | ((buf[i + 2] & 0xFF) << 8) + | (buf[i + 3] & 0xFF); + } + + static int readIntLE(byte[] buf, int i) { + return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8) | ((buf[i + 2] & 0xFF) << 16) + | ((buf[i + 3] & 0xFF) << 24); + } + + static int readInt(byte[] buf, int i) { + return readIntBE(buf, i); + } + + static void writeShort(short[] buf, int off, int v) { + buf[off] = (short) v; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorLz4.java b/src/js-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorLz4.java new file mode 100644 index 00000000..9c69d9de --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/DeflaterEstimatorLz4.java @@ -0,0 +1,272 @@ +package ar.com.hjg.pngj.pixels; + +import java.nio.ByteOrder; + +/** + * This estimator actually uses the LZ4 compression algorithm, and hopes that it's well correlated with Deflater. It's + * about 3 to 4 times faster than Deflater. + * + * This is a modified heavily trimmed version of the net.jpountz.lz4.LZ4JavaSafeCompressor class plus some methods from + * other classes from LZ4 Java library: https://github.com/jpountz/lz4-java , originally licensed under the Apache + * License 2.0 + */ +final public class DeflaterEstimatorLz4 { + + /** + * This object is stateless, it's thread safe and can be reused + */ + public DeflaterEstimatorLz4() {} + + /** + * Estimates the length of the compressed bytes, as compressed by Lz4 WARNING: if larger than LZ4_64K_LIMIT it cuts it + * in fragments + * + * WARNING: if some part of the input is discarded, this should return the proportional (so that + * returnValue/srcLen=compressionRatio) + * + * @param src + * @param srcOff + * @param srcLen + * @return length of the compressed bytes + */ + public int compressEstim(byte[] src, int srcOff, final int srcLen) { + if (srcLen < 10) + return srcLen; // too small + int stride = LZ4_64K_LIMIT - 1; + int segments = (srcLen + stride - 1) / stride; + stride = srcLen / segments; + if (stride >= LZ4_64K_LIMIT - 1 || stride * segments > srcLen || segments < 1 || stride < 1) + throw new RuntimeException("?? " + srcLen); + int bytesIn = 0; + int bytesOut = 0; + int len = srcLen; + while (len > 0) { + if (len > stride) + len = stride; + bytesOut += compress64k(src, srcOff, len); + srcOff += len; + bytesIn += len; + len = srcLen - bytesIn; + } + double ratio = bytesOut / (double) bytesIn; + return bytesIn == srcLen ? bytesOut : (int) (ratio * srcLen + 0.5); + } + + public int compressEstim(byte[] src) { + return compressEstim(src, 0, src.length); + } + + static final ByteOrder NATIVE_BYTE_ORDER = ByteOrder.nativeOrder(); + + static final int MEMORY_USAGE = 14; + static final int NOT_COMPRESSIBLE_DETECTION_LEVEL = 6; + + static final int MIN_MATCH = 4; + + static final int HASH_LOG = MEMORY_USAGE - 2; + static final int HASH_TABLE_SIZE = 1 << HASH_LOG; + + static final int SKIP_STRENGTH = Math.max(NOT_COMPRESSIBLE_DETECTION_LEVEL, 2); + static final int COPY_LENGTH = 8; + static final int LAST_LITERALS = 5; + static final int MF_LIMIT = COPY_LENGTH + MIN_MATCH; + static final int MIN_LENGTH = MF_LIMIT + 1; + + static final int MAX_DISTANCE = 1 << 16; + + static final int ML_BITS = 4; + static final int ML_MASK = (1 << ML_BITS) - 1; + static final int RUN_BITS = 8 - ML_BITS; + static final int RUN_MASK = (1 << RUN_BITS) - 1; + + static final int LZ4_64K_LIMIT = (1 << 16) + (MF_LIMIT - 1); + static final int HASH_LOG_64K = HASH_LOG + 1; + static final int HASH_TABLE_SIZE_64K = 1 << HASH_LOG_64K; + + static final int HASH_LOG_HC = 15; + static final int HASH_TABLE_SIZE_HC = 1 << HASH_LOG_HC; + static final int OPTIMAL_ML = ML_MASK - 1 + MIN_MATCH; + + static int compress64k(byte[] src, int srcOff, int srcLen) { + final int srcEnd = srcOff + srcLen; + final int srcLimit = srcEnd - LAST_LITERALS; + final int mflimit = srcEnd - MF_LIMIT; + + int sOff = srcOff, dOff = 0; + + int anchor = sOff; + + if (srcLen >= MIN_LENGTH) { + + final short[] hashTable = new short[HASH_TABLE_SIZE_64K]; + + ++sOff; + + main: while (true) { + + // find a match + int forwardOff = sOff; + + int ref; + int findMatchAttempts = (1 << SKIP_STRENGTH) + 3; + do { + sOff = forwardOff; + forwardOff += findMatchAttempts++ >>> SKIP_STRENGTH; + + if (forwardOff > mflimit) { + break main; + } + + final int h = hash64k(readInt(src, sOff)); + ref = srcOff + readShort(hashTable, h); + writeShort(hashTable, h, sOff - srcOff); + } while (!readIntEquals(src, ref, sOff)); + + // catch up + final int excess = commonBytesBackward(src, ref, sOff, srcOff, anchor); + sOff -= excess; + ref -= excess; + // sequence == refsequence + final int runLen = sOff - anchor; + dOff++; + + if (runLen >= RUN_MASK) { + if (runLen > RUN_MASK) + dOff += (runLen - RUN_MASK) / 0xFF; + dOff++; + } + dOff += runLen; + while (true) { + // encode offset + dOff += 2; + // count nb matches + sOff += MIN_MATCH; + ref += MIN_MATCH; + final int matchLen = commonBytes(src, ref, sOff, srcLimit); + sOff += matchLen; + // encode match len + if (matchLen >= ML_MASK) { + if (matchLen >= ML_MASK + 0xFF) + dOff += (matchLen - ML_MASK) / 0xFF; + dOff++; + } + // test end of chunk + if (sOff > mflimit) { + anchor = sOff; + break main; + } + // fill table + writeShort(hashTable, hash64k(readInt(src, sOff - 2)), sOff - 2 - srcOff); + // test next position + final int h = hash64k(readInt(src, sOff)); + ref = srcOff + readShort(hashTable, h); + writeShort(hashTable, h, sOff - srcOff); + if (!readIntEquals(src, sOff, ref)) { + break; + } + dOff++; + } + // prepare next loop + anchor = sOff++; + } + } + int runLen = srcEnd - anchor; + if (runLen >= RUN_MASK + 0xFF) { + dOff += (runLen - RUN_MASK) / 0xFF; + } + dOff++; + dOff += runLen; + return dOff; + } + + static final int maxCompressedLength(int length) { + if (length < 0) { + throw new IllegalArgumentException("length must be >= 0, got " + length); + } + return length + length / 255 + 16; + } + + static int hash(int i) { + return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG); + } + + static int hash64k(int i) { + return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG_64K); + } + + static int readShortLittleEndian(byte[] buf, int i) { + return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8); + } + + static boolean readIntEquals(byte[] buf, int i, int j) { + return buf[i] == buf[j] && buf[i + 1] == buf[j + 1] && buf[i + 2] == buf[j + 2] + && buf[i + 3] == buf[j + 3]; + } + + static int commonBytes(byte[] b, int o1, int o2, int limit) { + int count = 0; + while (o2 < limit && b[o1++] == b[o2++]) { + ++count; + } + return count; + } + + static int commonBytesBackward(byte[] b, int o1, int o2, int l1, int l2) { + int count = 0; + while (o1 > l1 && o2 > l2 && b[--o1] == b[--o2]) { + ++count; + } + return count; + } + + static int readShort(short[] buf, int off) { + return buf[off] & 0xFFFF; + } + + static byte readByte(byte[] buf, int i) { + return buf[i]; + } + + static void checkRange(byte[] buf, int off) { + if (off < 0 || off >= buf.length) { + throw new ArrayIndexOutOfBoundsException(off); + } + } + + static void checkRange(byte[] buf, int off, int len) { + checkLength(len); + if (len > 0) { + checkRange(buf, off); + checkRange(buf, off + len - 1); + } + } + + static void checkLength(int len) { + if (len < 0) { + throw new IllegalArgumentException("lengths must be >= 0"); + } + } + + static int readIntBE(byte[] buf, int i) { + return ((buf[i] & 0xFF) << 24) | ((buf[i + 1] & 0xFF) << 16) | ((buf[i + 2] & 0xFF) << 8) + | (buf[i + 3] & 0xFF); + } + + static int readIntLE(byte[] buf, int i) { + return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8) | ((buf[i + 2] & 0xFF) << 16) + | ((buf[i + 3] & 0xFF) << 24); + } + + static int readInt(byte[] buf, int i) { + if (NATIVE_BYTE_ORDER == ByteOrder.BIG_ENDIAN) { + return readIntBE(buf, i); + } else { + return readIntLE(buf, i); + } + } + + static void writeShort(short[] buf, int off, int v) { + buf[off] = (short) v; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/FiltersPerformance.java b/src/js-specific/java/ar/com/hjg/pngj/pixels/FiltersPerformance.java new file mode 100644 index 00000000..5a3c043c --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/FiltersPerformance.java @@ -0,0 +1,203 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.Arrays; + +import ar.com.hjg.pngj.FilterType; +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjExceptionInternal; + +/** for use in adaptative strategy */ +public class FiltersPerformance { + + private final ImageInfo iminfo; + private double memoryA = 0.7; // empirical (not very critical: 0.72) + private int lastrow = -1; + private double[] absum = new double[5];// depending on the strategy not all values might be + // computed for all + private double[] entropy = new double[5]; + private double[] cost = new double[5]; + private int[] histog = new int[256]; // temporary, not normalized + private int lastprefered = -1; + private boolean initdone = false; + private double preferenceForNone = 1.0; // higher gives more preference to NONE + + // this values are empirical (montecarlo), for RGB8 images with entropy estimator for NONE and + // memory=0.7 + // DONT MODIFY THIS + public static final double[] FILTER_WEIGHTS_DEFAULT = {0.73, 1.03, 0.97, 1.11, 1.22}; // lower is + // better! + + private double[] filter_weights = new double[] {-1, -1, -1, -1, -1}; + + private final static double LOG2NI = -1.0 / Math.log(2.0); + + public FiltersPerformance(ImageInfo imgInfo) { + this.iminfo = imgInfo; + } + + private void init() { + if (filter_weights[0] < 0) {// has not been set from outside + System.arraycopy(FILTER_WEIGHTS_DEFAULT, 0, filter_weights, 0, 5); + double wNone = filter_weights[0]; + if (iminfo.bitDepth == 16) + wNone = 1.2; + else if (iminfo.alpha) + wNone = 0.8; + else if (iminfo.indexed || iminfo.bitDepth < 8) + wNone = 0.4; // we prefer NONE strongly + wNone /= preferenceForNone; + filter_weights[0] = wNone; + } + Arrays.fill(cost, 1.0); + initdone = true; + } + + public void updateFromFiltered(FilterType ftype, byte[] rowff, int rown) { + updateFromRawOrFiltered(ftype, rowff, null, null, rown); + } + + /** alternative: computes statistic without filtering */ + public void updateFromRaw(FilterType ftype, byte[] rowb, byte[] rowbprev, int rown) { + updateFromRawOrFiltered(ftype, null, rowb, rowbprev, rown); + } + + private void updateFromRawOrFiltered(FilterType ftype, byte[] rowff, byte[] rowb, + byte[] rowbprev, int rown) { + if (!initdone) + init(); + if (rown != lastrow) { + Arrays.fill(absum, Double.NaN); + Arrays.fill(entropy, Double.NaN); + } + lastrow = rown; + if (rowff != null) + computeHistogram(rowff); + else + computeHistogramForFilter(ftype, rowb, rowbprev); + if (ftype == FilterType.FILTER_NONE) + entropy[ftype.val] = computeEntropyFromHistogram(); + else + absum[ftype.val] = computeAbsFromHistogram(); + } + + /* WARNING: this is not idempotent, call it just once per cycle (sigh) */ + public FilterType getPreferred() { + int fi = 0; + double vali = Double.MAX_VALUE, val = 0; // lower wins + for (int i = 0; i < 5; i++) { + if (!Double.isNaN(absum[i])) { + val = absum[i]; + } else if (!Double.isNaN(entropy[i])) { + val = (Math.pow(2.0, entropy[i]) - 1.0) * 0.5; + } else + continue; + val *= filter_weights[i]; + val = cost[i] * memoryA + (1 - memoryA) * val; + cost[i] = val; + if (val < vali) { + vali = val; + fi = i; + } + } + lastprefered = fi; + return FilterType.getByVal(lastprefered); + } + + public final void computeHistogramForFilter(FilterType filterType, byte[] rowb, byte[] rowbprev) { + Arrays.fill(histog, 0); + int i, j, imax = iminfo.bytesPerRow; + switch (filterType) { + case FILTER_NONE: + for (i = 1; i <= imax; i++) + histog[rowb[i] & 0xFF]++; + break; + case FILTER_PAETH: + for (i = 1; i <= imax; i++) + histog[PngHelperInternal.filterRowPaeth(rowb[i], 0, rowbprev[i] & 0xFF, 0)]++; + for (j = 1, i = iminfo.bytesPixel + 1; i <= imax; i++, j++) + histog[PngHelperInternal.filterRowPaeth(rowb[i], rowb[j] & 0xFF, rowbprev[i] & 0xFF, + rowbprev[j] & 0xFF)]++; + break; + case FILTER_SUB: + for (i = 1; i <= iminfo.bytesPixel; i++) + histog[rowb[i] & 0xFF]++; + for (j = 1, i = iminfo.bytesPixel + 1; i <= imax; i++, j++) + histog[(rowb[i] - rowb[j]) & 0xFF]++; + break; + case FILTER_UP: + for (i = 1; i <= iminfo.bytesPerRow; i++) + histog[(rowb[i] - rowbprev[i]) & 0xFF]++; + break; + case FILTER_AVERAGE: + for (i = 1; i <= iminfo.bytesPixel; i++) + histog[((rowb[i] & 0xFF) - ((rowbprev[i] & 0xFF)) / 2) & 0xFF]++; + for (j = 1, i = iminfo.bytesPixel + 1; i <= imax; i++, j++) + histog[((rowb[i] & 0xFF) - ((rowbprev[i] & 0xFF) + (rowb[j] & 0xFF)) / 2) & 0xFF]++; + break; + default: + throw new PngjExceptionInternal("Bad filter:" + filterType); + } + } + + public void computeHistogram(byte[] rowff) { + Arrays.fill(histog, 0); + for (int i = 1; i < iminfo.bytesPerRow; i++) + histog[rowff[i] & 0xFF]++; + } + + public double computeAbsFromHistogram() { + int s = 0; + for (int i = 1; i < 128; i++) + s += histog[i] * i; + for (int i = 128, j = 128; j > 0; i++, j--) + s += histog[i] * j; + return s / (double) iminfo.bytesPerRow; + } + + public final double computeEntropyFromHistogram() { + double s = 1.0 / iminfo.bytesPerRow; + double ls = Math.log(s); + + double h = 0; + for (int x : histog) { + if (x > 0) + h += (Math.log(x) + ls) * x; + } + h *= s * LOG2NI; + if (h < 0.0) + h = 0.0; + return h; + } + + /** + * If larger than 1.0, NONE will be more prefered. This must be called before init + * + * @param preferenceForNone around 1.0 (default: 1.0) + */ + public void setPreferenceForNone(double preferenceForNone) { + this.preferenceForNone = preferenceForNone; + } + + /** + * Values greater than 1.0 (towards infinite) increase the memory towards 1. Values smaller than 1.0 (towards zero) + * decreases the memory . + * + */ + public void tuneMemory(double m) { + if (m == 0) + memoryA = 0.0; + else + memoryA = Math.pow(memoryA, 1.0 / m); + } + + /** + * To set manually the filter weights. This is not recommended, unless you know what you are doing. Setting this + * ignores preferenceForNone and omits some heuristics + * + * @param weights Five doubles around 1.0, one for each filter type. Lower is preferered + */ + public void setFilterWeights(double[] weights) { + System.arraycopy(weights, 0, filter_weights, 0, 5); + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriter.java b/src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriter.java new file mode 100644 index 00000000..677cef1a --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriter.java @@ -0,0 +1,263 @@ +package ar.com.hjg.pngj.pixels; + +import java.io.OutputStream; +import java.util.zip.Deflater; + +import ar.com.hjg.pngj.FilterType; +import ar.com.hjg.pngj.IDatChunkWriter; +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngHelperInternal; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * Encodes a set of rows (pixels) as a continuous deflated stream (does not know about IDAT chunk segmentation). + *

+ * This includes the filter selection strategy, plus the filtering itself and the deflating. Only supports fixed length + * rows (no interlaced writing). + *

+ * Typically an instance of this is hold by a PngWriter - but more instances could be used (for APGN) + */ +public abstract class PixelsWriter { + + private static final int IDAT_MAX_SIZE_DEFAULT = 32000; + + protected final ImageInfo imgInfo; + /** + * row buffer length, including filter byte (imgInfo.bytesPerRow + 1) + */ + protected final int buflen; + + protected final int bytesPixel; + protected final int bytesRow; + + private CompressorStream compressorStream; // to compress the idat stream + + protected int deflaterCompLevel = 6; + protected int deflaterStrategy = Deflater.DEFAULT_STRATEGY; + + protected boolean initdone = false; + + /** + * This is the globally configured filter type - it can be a concrete type or a pseudo type (hint or strategy) + */ + protected FilterType filterType; + + // counts the filters used - just for stats + private int[] filtersUsed = new int[5]; + + // this is the raw underlying os (shared with the PngWriter) + private OutputStream os; + + private int idatMaxSize = IDAT_MAX_SIZE_DEFAULT; + + /** + * row being processed, couting from zero + */ + protected int currentRow; + + + public PixelsWriter(ImageInfo imgInfo) { + this.imgInfo = imgInfo; + bytesRow = imgInfo.bytesPerRow; + buflen = bytesRow + 1; + bytesPixel = imgInfo.bytesPixel; + currentRow = -1; + filterType = FilterType.FILTER_DEFAULT; + } + + /** + * main internal point for external call. It does the lazy initializion if necessary, sets current row, and call + * {@link #filterAndWrite(byte[])} + */ + public final void processRow(final byte[] rowb) { + if (!initdone) + init(); + currentRow++; + filterAndWrite(rowb); + } + + protected void sendToCompressedStream(byte[] rowf) { + compressorStream.write(rowf, 0, rowf.length); + filtersUsed[rowf[0]]++; + } + + /** + * This does the filtering and send to stream. Typically should decide the filtering, call + * {@link #filterRowWithFilterType(FilterType, byte[], byte[], byte[])} and and + * {@link #sendToCompressedStream(byte[])} + * + * @param rowb + */ + protected abstract void filterAndWrite(final byte[] rowb); + + /** + * Does the real filtering. This must be called with the real (standard) filterType. This should rarely be overriden. + *

+ * WARNING: look out the contract + * + * @param _filterType + * @param _rowb current row (the first byte might be modified) + * @param _rowbprev previous row (should be all zero the first time) + * @param _rowf tentative buffer to store the filtered bytes. might not be used! + * @return normally _rowf, but eventually _rowb. This MUST NOT BE MODIFIED nor reused by caller + */ + final protected byte[] filterRowWithFilterType(FilterType _filterType, byte[] _rowb, + byte[] _rowbprev, byte[] _rowf) { + // warning: some filters rely on: "previous row" (rowbprev) it must be initialized to 0 the + // first time + if (_filterType == FilterType.FILTER_NONE) + _rowf = _rowb; + _rowf[0] = (byte) _filterType.val; + int i, j; + switch (_filterType) { + case FILTER_NONE: + // we return the same original (be careful!) + break; + case FILTER_PAETH: + for (i = 1; i <= bytesPixel; i++) + _rowf[i] = (byte) PngHelperInternal.filterRowPaeth(_rowb[i], 0, _rowbprev[i] & 0xFF, 0); + for (j = 1, i = bytesPixel + 1; i <= bytesRow; i++, j++) + _rowf[i] = + (byte) PngHelperInternal.filterRowPaeth(_rowb[i], _rowb[j] & 0xFF, + _rowbprev[i] & 0xFF, _rowbprev[j] & 0xFF); + break; + case FILTER_SUB: + for (i = 1; i <= bytesPixel; i++) + _rowf[i] = (byte) _rowb[i]; + for (j = 1, i = bytesPixel + 1; i <= bytesRow; i++, j++) + _rowf[i] = (byte) (_rowb[i] - _rowb[j]); + break; + case FILTER_AVERAGE: + for (i = 1; i <= bytesPixel; i++) + _rowf[i] = (byte) (_rowb[i] - (_rowbprev[i] & 0xFF) / 2); + for (j = 1, i = bytesPixel + 1; i <= bytesRow; i++, j++) + _rowf[i] = (byte) (_rowb[i] - ((_rowbprev[i] & 0xFF) + (_rowb[j] & 0xFF)) / 2); + break; + case FILTER_UP: + for (i = 1; i <= bytesRow; i++) + _rowf[i] = (byte) (_rowb[i] - _rowbprev[i]); + break; + default: + throw new PngjOutputException("Filter type not recognized: " + _filterType); + } + return _rowf; + } + + /** + * This will be called by the PngWrite to fill the raw pixels for each row. This can change from call to call. + * Warning: this can be called before the object is init, implementations should call init() to be sure + */ + public abstract byte[] getRowb(); + + /** + * This will be called lazily just before writing row 0. Idempotent. + */ + protected final void init() { + if (!initdone) { + initParams(); + initdone = true; + } + } + + /** called by init(); override (calling this first) to do additional initialization */ + protected void initParams() { + IDatChunkWriter idatWriter = new IDatChunkWriter(os, idatMaxSize); + if (compressorStream == null) { // if not set, use the deflater + compressorStream = + new CompressorStreamDeflater(idatWriter, buflen, imgInfo.getTotalRawBytes(), + deflaterCompLevel, deflaterStrategy); + } + } + + /** cleanup. This should be called explicitly. Idempotent and secure */ + public void close() { + if (compressorStream != null) { + compressorStream.close(); + } + } + + /** + * Deflater (ZLIB) strategy. You should rarely change this from the default (Deflater.DEFAULT_STRATEGY) to + * Deflater.FILTERED (Deflater.HUFFMAN_ONLY is fast but compress poorly) + */ + public void setDeflaterStrategy(Integer deflaterStrategy) { + this.deflaterStrategy = deflaterStrategy; + } + + /** + * Deflater (ZLIB) compression level, between 0 (no compression) and 9 + */ + public void setDeflaterCompLevel(Integer deflaterCompLevel) { + this.deflaterCompLevel = deflaterCompLevel; + } + + public Integer getDeflaterCompLevel() { + return deflaterCompLevel; + } + + + public final void setOs(OutputStream datStream) { + this.os = datStream; + } + + public OutputStream getOs() { + return os; + } + + /** @see #filterType */ + final public FilterType getFilterType() { + return filterType; + } + + /** @see #filterType */ + final public void setFilterType(FilterType filterType) { + this.filterType = filterType; + } + + /* out/in This should be called only after end() to get reliable results */ + public double getCompression() { + return compressorStream.isDone() ? compressorStream.getCompressionRatio() : 1.0; + } + + public void setCompressorStream(CompressorStream compressorStream) { + this.compressorStream = compressorStream; + } + + public long getTotalBytesToWrite() { + return imgInfo.getTotalRawBytes(); + } + + public boolean isDone() { + return currentRow == imgInfo.rows - 1; + } + + /** + * computed default fixed filter type to use, if specified DEFAULT; wilde guess based on image properties + * + * @return One of the five concrete filter types + */ + protected FilterType getDefaultFilter() { + if (imgInfo.indexed || imgInfo.bitDepth < 8) + return FilterType.FILTER_NONE; + else if (imgInfo.getTotalPixels() < 1024) + return FilterType.FILTER_NONE; + else if (imgInfo.rows == 1) + return FilterType.FILTER_SUB; + else if (imgInfo.cols == 1) + return FilterType.FILTER_UP; + else + return FilterType.FILTER_PAETH; + } + + /** informational stats : filter used, in percentages */ + final public String getFiltersUsed() { + return String.format("%d,%d,%d,%d,%d", (int) (filtersUsed[0] * 100.0 / imgInfo.rows + 0.5), + (int) (filtersUsed[1] * 100.0 / imgInfo.rows + 0.5), (int) (filtersUsed[2] * 100.0 + / imgInfo.rows + 0.5), (int) (filtersUsed[3] * 100.0 / imgInfo.rows + 0.5), + (int) (filtersUsed[4] * 100.0 / imgInfo.rows + 0.5)); + } + + public void setIdatMaxSize(int idatMaxSize) { + this.idatMaxSize = idatMaxSize; + } +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterDefault.java b/src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterDefault.java new file mode 100644 index 00000000..5b8752a6 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterDefault.java @@ -0,0 +1,158 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.Arrays; + +import ar.com.hjg.pngj.FilterType; +import ar.com.hjg.pngj.ImageInfo; +import ar.com.hjg.pngj.PngjOutputException; + +/** + * Default implementation of PixelsWriter, with fixed filters and also adaptive strategies. + */ +public class PixelsWriterDefault extends PixelsWriter { + /** current raw row */ + protected byte[] rowb; + /** previous raw row */ + protected byte[] rowbprev; + /** buffer for filtered row */ + protected byte[] rowbfilter; + + /** evaluates different filters, for adaptive strategy */ + protected FiltersPerformance filtersPerformance; + + /** currently concrete selected filter type */ + protected FilterType curfilterType; + + /** parameters for adaptive strategy */ + protected int adaptMaxSkip; // set in initParams, does not change + protected int adaptSkipIncreaseSinceRow; // set in initParams, does not change + protected double adaptSkipIncreaseFactor; // set in initParams, does not change + protected int adaptNextRow = 0; + + public PixelsWriterDefault(ImageInfo imgInfo) { + super(imgInfo); + filtersPerformance = new FiltersPerformance(imgInfo); + } + + @Override + protected void initParams() { + super.initParams(); + + if (rowb == null || rowb.length < buflen) + rowb = new byte[buflen]; + if (rowbfilter == null || rowbfilter.length < buflen) + rowbfilter = new byte[buflen]; + if (rowbprev == null || rowbprev.length < buflen) + rowbprev = new byte[buflen]; + else + Arrays.fill(rowbprev, (byte) 0); + + // if adaptative but too few rows or columns, use default + if (imgInfo.cols < 3 && !FilterType.isValidStandard(filterType)) + filterType = FilterType.FILTER_DEFAULT; + if (imgInfo.rows < 3 && !FilterType.isValidStandard(filterType)) + filterType = FilterType.FILTER_DEFAULT; + + if (imgInfo.getTotalPixels() <= 1024 && !FilterType.isValidStandard(filterType)) + filterType = getDefaultFilter(); + + if (FilterType.isAdaptive(filterType)) { + // adaptCurSkip = 0; + adaptNextRow = 0; + if (filterType == FilterType.FILTER_ADAPTIVE_FAST) { + adaptMaxSkip = 200; + adaptSkipIncreaseSinceRow = 3; + adaptSkipIncreaseFactor = 1 / 4.0; // skip ~ row/3 + } else if (filterType == FilterType.FILTER_ADAPTIVE_MEDIUM) { + adaptMaxSkip = 8; + adaptSkipIncreaseSinceRow = 32; + adaptSkipIncreaseFactor = 1 / 80.0; + } else if (filterType == FilterType.FILTER_ADAPTIVE_FULL) { + adaptMaxSkip = 0; + adaptSkipIncreaseSinceRow = 128; + adaptSkipIncreaseFactor = 1 / 120.0; + } else + throw new PngjOutputException("bad filter " + filterType); + } + } + + @Override + protected void filterAndWrite(final byte[] rowb) { + if (rowb != this.rowb) + throw new RuntimeException("??"); // we rely on this + decideCurFilterType(); + byte[] filtered = filterRowWithFilterType(curfilterType, rowb, rowbprev, rowbfilter); + sendToCompressedStream(filtered); + // swap rowb <-> rowbprev + byte[] aux = this.rowb; + this.rowb = rowbprev; + rowbprev = aux; + } + + protected void decideCurFilterType() { + // decide the real filter and store in curfilterType + if (FilterType.isValidStandard(getFilterType())) { + curfilterType = getFilterType(); + } else if (getFilterType() == FilterType.FILTER_PRESERVE) { + curfilterType = FilterType.getByVal(rowb[0]); + } else if (getFilterType() == FilterType.FILTER_CYCLIC) { + curfilterType = FilterType.getByVal(currentRow % 5); + } else if (getFilterType() == FilterType.FILTER_DEFAULT) { + setFilterType(getDefaultFilter()); + curfilterType = getFilterType(); // this could be done once + } else if (FilterType.isAdaptive(getFilterType())) {// adaptive + if (currentRow == adaptNextRow) { + for (FilterType ftype : FilterType.getAllStandard()) + filtersPerformance.updateFromRaw(ftype, rowb, rowbprev, currentRow); + curfilterType = filtersPerformance.getPreferred(); + int skip = + (currentRow >= adaptSkipIncreaseSinceRow ? (int) Math + .round((currentRow - adaptSkipIncreaseSinceRow) * adaptSkipIncreaseFactor) : 0); + if (skip > adaptMaxSkip) + skip = adaptMaxSkip; + if (currentRow == 0) + skip = 0; + adaptNextRow = currentRow + 1 + skip; + } + } else { + throw new PngjOutputException("not implemented filter: " + getFilterType()); + } + if (currentRow == 0 && curfilterType != FilterType.FILTER_NONE + && curfilterType != FilterType.FILTER_SUB) + curfilterType = FilterType.FILTER_SUB; // first row should always be none or sub + } + + @Override + public byte[] getRowb() { + if (!initdone) + init(); + return rowb; + } + + @Override + public void close() { + super.close(); + } + + /** + * Only for adaptive strategies. See {@link FiltersPerformance#setPreferenceForNone(double)} + */ + public void setPreferenceForNone(double preferenceForNone) { + filtersPerformance.setPreferenceForNone(preferenceForNone); + } + + /** + * Only for adaptive strategies. See {@link FiltersPerformance#tuneMemory(double)} + */ + public void tuneMemory(double m) { + filtersPerformance.tuneMemory(m); + } + + /** + * Only for adaptive strategies. See {@link FiltersPerformance#setFilterWeights(double[])} + */ + public void setFilterWeights(double[] weights) { + filtersPerformance.setFilterWeights(weights); + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterMultiple.java b/src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterMultiple.java new file mode 100644 index 00000000..367dd981 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/PixelsWriterMultiple.java @@ -0,0 +1,241 @@ +package ar.com.hjg.pngj.pixels; + +import java.util.LinkedList; +import java.util.zip.Deflater; + +import ar.com.hjg.pngj.FilterType; +import ar.com.hjg.pngj.ImageInfo; + +/** Special pixels writer for experimental super adaptive strategy */ +public class PixelsWriterMultiple extends PixelsWriter { + /** + * unfiltered rowsperband elements, 0 is the current (rowb). This should include all rows of current band, plus one + */ + protected LinkedList rows; + + /** + * bank of compressor estimators, one for each filter and (perhaps) an adaptive strategy + */ + protected CompressorStream[] filterBank = new CompressorStream[6]; + /** + * stored filtered rows, one for each filter (0=none is not allocated but linked) + */ + protected byte[][] filteredRows = new byte[5][]; + protected byte[] filteredRowTmp; // + + protected FiltersPerformance filtersPerf; + protected int rowsPerBand = 0; // This is a 'nominal' size + protected int rowsPerBandCurrent = 0; // lastRowInThisBand-firstRowInThisBand +1 : might be + // smaller than rowsPerBand + protected int rowInBand = -1; + protected int bandNum = -1; + protected int firstRowInThisBand, lastRowInThisBand; + private boolean tryAdaptive = true; + + protected static final int HINT_MEMORY_DEFAULT_KB = 100; + // we will consume about (not more than) this memory (in buffers, not counting the compressors) + protected int hintMemoryKb = HINT_MEMORY_DEFAULT_KB; + + private int hintRowsPerBand = 1000; // default: very large number, can be changed + + private boolean useLz4 = true; + + public PixelsWriterMultiple(ImageInfo imgInfo) { + super(imgInfo); + filtersPerf = new FiltersPerformance(imgInfo); + rows = new LinkedList(); + for (int i = 0; i < 2; i++) + rows.add(new byte[buflen]); // we preallocate 2 rows (rowb and rowbprev) + filteredRowTmp = new byte[buflen]; + } + + @Override + protected void filterAndWrite(byte[] rowb) { + if (!initdone) + init(); + if (rowb != rows.get(0)) + throw new RuntimeException("?"); + setBandFromNewRown(); + byte[] rowbprev = rows.get(1); + for (FilterType ftype : FilterType.getAllStandardNoneLast()) { + // this has a special behaviour for NONE: filteredRows[0] is null, and the returned value is + // rowb + if (currentRow == 0 && ftype != FilterType.FILTER_NONE && ftype != FilterType.FILTER_SUB) + continue; + byte[] filtered = filterRowWithFilterType(ftype, rowb, rowbprev, filteredRows[ftype.val]); + filterBank[ftype.val].write(filtered); + if (currentRow == 0 && ftype == FilterType.FILTER_SUB) { // litle lie, only for first row + filterBank[FilterType.FILTER_PAETH.val].write(filtered); + filterBank[FilterType.FILTER_AVERAGE.val].write(filtered); + filterBank[FilterType.FILTER_UP.val].write(filtered); + } + // adptive: report each filterted + if (tryAdaptive) { + filtersPerf.updateFromFiltered(ftype, filtered, currentRow); + } + } + filteredRows[0] = rowb; + if (tryAdaptive) { + FilterType preferredAdaptive = filtersPerf.getPreferred(); + filterBank[5].write(filteredRows[preferredAdaptive.val]); + } + if (currentRow == lastRowInThisBand) { + int best = getBestCompressor(); + // PngHelperInternal.debug("won: " + best + " (rows: " + firstRowInThisBand + ":" + lastRowInThisBand + ")"); + // if(currentRow>90&¤tRow<100) + // PngHelperInternal.debug(String.format("row=%d ft=%s",currentRow,FilterType.getByVal(best))); + byte[] filtersAdapt = filterBank[best].getFirstBytes(); + for (int r = firstRowInThisBand, i = 0, j = lastRowInThisBand - firstRowInThisBand; r <= lastRowInThisBand; r++, j--, i++) { + int fti = filtersAdapt[i]; + byte[] filtered = null; + if (r != lastRowInThisBand) { + filtered = + filterRowWithFilterType(FilterType.getByVal(fti), rows.get(j), rows.get(j + 1), + filteredRowTmp); + } else { // no need to do this filtering, we already have it + filtered = filteredRows[fti]; + } + sendToCompressedStream(filtered); + } + } + // rotate + if (rows.size() > rowsPerBandCurrent) { + rows.addFirst(rows.removeLast()); + } else + rows.addFirst(new byte[buflen]); + } + + @Override + public byte[] getRowb() { + return rows.get(0); + } + + + private void setBandFromNewRown() { + boolean newBand = currentRow == 0 || currentRow > lastRowInThisBand; + if (currentRow == 0) + bandNum = -1; + if (newBand) { + bandNum++; + rowInBand = 0; + } else { + rowInBand++; + } + if (newBand) { + firstRowInThisBand = currentRow; + lastRowInThisBand = firstRowInThisBand + rowsPerBand - 1; + int lastRowInNextBand = firstRowInThisBand + 2 * rowsPerBand - 1; + if (lastRowInNextBand >= imgInfo.rows) // hack:make this band bigger, so we don't have a small + // last band + lastRowInThisBand = imgInfo.rows - 1; + rowsPerBandCurrent = 1 + lastRowInThisBand - firstRowInThisBand; + tryAdaptive = + rowsPerBandCurrent <= 3 || (rowsPerBandCurrent < 10 && imgInfo.bytesPerRow < 64) ? false + : true; + // rebuild bank + rebuildFiltersBank(); + } + } + + private void rebuildFiltersBank() { + long bytesPerBandCurrent = rowsPerBandCurrent * (long) buflen; + final int DEFLATER_COMP_LEVEL = 4; + for (int i = 0; i <= 5; i++) {// one for each filter plus one adaptive + CompressorStream cp = filterBank[i]; + if (cp == null || cp.totalbytes != bytesPerBandCurrent) { + if (cp != null) + cp.close(); + if (useLz4) + cp = new CompressorStreamLz4(null, buflen, bytesPerBandCurrent); + else + cp = + new CompressorStreamDeflater(null, buflen, bytesPerBandCurrent, DEFLATER_COMP_LEVEL, + Deflater.DEFAULT_STRATEGY); + filterBank[i] = cp; + } else { + cp.reset(); + } + cp.setStoreFirstByte(true, rowsPerBandCurrent); // TODO: only for adaptive? + } + } + + private int computeInitialRowsPerBand() { + // memory (only buffers) ~ (r+1+5) * bytesPerRow + int r = (int) ((hintMemoryKb * 1024.0) / (imgInfo.bytesPerRow + 1) - 5); + if (r < 1) + r = 1; + if (hintRowsPerBand > 0 && r > hintRowsPerBand) + r = hintRowsPerBand; + if (r > imgInfo.rows) + r = imgInfo.rows; + if (r > 2 && r > imgInfo.rows / 8) { // redistribute more evenly + int k = (imgInfo.rows + (r - 1)) / r; + r = (imgInfo.rows + k / 2) / k; + } + // PngHelperInternal.debug("rows :" + r + "/" + imgInfo.rows); + return r; + } + + private int getBestCompressor() { + double bestcr = Double.MAX_VALUE; + int bestb = -1; + for (int i = tryAdaptive ? 5 : 4; i >= 0; i--) { + CompressorStream fb = filterBank[i]; + double cr = fb.getCompressionRatio(); + if (cr <= bestcr) { // dirty trick, here the equality gains for row 0, so that SUB is prefered + // over PAETH, UP, AVE... + bestb = i; + bestcr = cr; + } + } + return bestb; + } + + @Override + protected void initParams() { + super.initParams(); + // if adaptative but too few rows or columns, use default + if (imgInfo.cols < 3 && !FilterType.isValidStandard(filterType)) + filterType = FilterType.FILTER_DEFAULT; + if (imgInfo.rows < 3 && !FilterType.isValidStandard(filterType)) + filterType = FilterType.FILTER_DEFAULT; + for (int i = 1; i <= 4; i++) { // element 0 is not allocated + if (filteredRows[i] == null || filteredRows[i].length < buflen) + filteredRows[i] = new byte[buflen]; + } + if (rowsPerBand == 0) + rowsPerBand = computeInitialRowsPerBand(); + } + + @Override + public void close() { + super.close(); + rows.clear(); + for (CompressorStream f : filterBank) { + f.close(); + } + } + + public void setHintMemoryKb(int hintMemoryKb) { + this.hintMemoryKb = + hintMemoryKb <= 0 ? HINT_MEMORY_DEFAULT_KB : (hintMemoryKb > 10000 ? 10000 : hintMemoryKb); + } + + public void setHintRowsPerBand(int hintRowsPerBand) { + this.hintRowsPerBand = hintRowsPerBand; + } + + public void setUseLz4(boolean lz4) { + this.useLz4 = lz4; + } + + /** for tuning memory or other parameters */ + public FiltersPerformance getFiltersPerf() { + return filtersPerf; + } + + public void setTryAdaptive(boolean tryAdaptive) { + this.tryAdaptive = tryAdaptive; + } + +} diff --git a/src/js-specific/java/ar/com/hjg/pngj/pixels/package.html b/src/js-specific/java/ar/com/hjg/pngj/pixels/package.html new file mode 100644 index 00000000..85bd6a02 --- /dev/null +++ b/src/js-specific/java/ar/com/hjg/pngj/pixels/package.html @@ -0,0 +1,14 @@ + + +

+Mostly related with logic specific to reading/writing pixels. +

+

+Includes ImageLine related classes, and rows filtering +

+

+Some classes like ImageLineInt should belong here, but we keep them in the main package for backward compatibility. + +

+ + diff --git a/src/js-specific/java/org/warp/picalculator/PlatformUtils.java b/src/js-specific/java/org/warp/picalculator/PlatformUtils.java index 2e90777d..499c05d5 100644 --- a/src/js-specific/java/org/warp/picalculator/PlatformUtils.java +++ b/src/js-specific/java/org/warp/picalculator/PlatformUtils.java @@ -2,8 +2,12 @@ package org.warp.picalculator; import java.io.PrintWriter; import java.io.StringWriter; +import java.lang.ref.WeakReference; +import org.teavm.jso.browser.Window; +import org.teavm.jso.dom.html.HTMLDocument; import org.warp.picalculator.gui.expression.blocks.Block; +import org.warp.picalculator.math.rules.RulesManager; public final class PlatformUtils { public static final boolean isJavascript = true; @@ -25,4 +29,74 @@ public final class PlatformUtils { public static String[] stacktraceToString(Error e) { return e.getMessage().toUpperCase().replace("\r", "").split("\n"); } + + public static void loadPlatformRules() { + RulesManager.addRule(new rules.functions.DivisionRule()); + RulesManager.addRule(new rules.functions.EmptyNumberRule()); + RulesManager.addRule(new rules.functions.ExpressionRule()); + RulesManager.addRule(new rules.functions.JokeRule()); + RulesManager.addRule(new rules.functions.MultiplicationRule()); + RulesManager.addRule(new rules.functions.NegativeRule()); + RulesManager.addRule(new rules.functions.NumberRule()); + RulesManager.addRule(new rules.functions.PowerRule()); + RulesManager.addRule(new rules.functions.RootRule()); + RulesManager.addRule(new rules.functions.SubtractionRule()); + RulesManager.addRule(new rules.functions.SumRule()); + RulesManager.addRule(new rules.functions.SumSubtractionRule()); + RulesManager.addRule(new rules.functions.VariableRule()); + RulesManager.addRule(new rules.ExpandRule1()); + RulesManager.addRule(new rules.ExpandRule2()); + RulesManager.addRule(new rules.ExpandRule5()); + RulesManager.addRule(new rules.ExponentRule1()); + RulesManager.addRule(new rules.ExponentRule2()); + RulesManager.addRule(new rules.ExponentRule3()); + RulesManager.addRule(new rules.ExponentRule4()); + RulesManager.addRule(new rules.ExponentRule8()); + RulesManager.addRule(new rules.ExponentRule9()); + RulesManager.addRule(new rules.ExponentRule15()); + RulesManager.addRule(new rules.ExponentRule16()); + RulesManager.addRule(new rules.ExponentRule17()); + RulesManager.addRule(new rules.FractionsRule1()); + RulesManager.addRule(new rules.FractionsRule2()); + RulesManager.addRule(new rules.FractionsRule3()); + RulesManager.addRule(new rules.FractionsRule4()); + RulesManager.addRule(new rules.FractionsRule5()); + RulesManager.addRule(new rules.FractionsRule6()); + RulesManager.addRule(new rules.FractionsRule7()); + RulesManager.addRule(new rules.FractionsRule8()); + RulesManager.addRule(new rules.FractionsRule9()); + RulesManager.addRule(new rules.FractionsRule10()); + RulesManager.addRule(new rules.FractionsRule11()); + RulesManager.addRule(new rules.FractionsRule12()); + RulesManager.addRule(new rules.FractionsRule14()); + RulesManager.addRule(new rules.NumberRule1()); + RulesManager.addRule(new rules.NumberRule2()); + RulesManager.addRule(new rules.NumberRule3()); + RulesManager.addRule(new rules.NumberRule4()); + RulesManager.addRule(new rules.NumberRule5()); + RulesManager.addRule(new rules.NumberRule7()); + RulesManager.addRule(new rules.UndefinedRule1()); + RulesManager.addRule(new rules.UndefinedRule2()); + RulesManager.addRule(new rules.VariableRule1()); + RulesManager.addRule(new rules.VariableRule2()); + RulesManager.addRule(new rules.VariableRule3()); + } + + public static void gc() { + } + + private static boolean shift, alpha; + + public static void shiftChanged(boolean shift) { + PlatformUtils.shift = shift; + HTMLDocument doc = Window.current().getDocument(); + doc.getBody().setClassName((shift ? "shift " : "") + (alpha ? "alpha": "")); + } + + public static void alphaChanged(boolean alpha) { + PlatformUtils.alpha = alpha; + HTMLDocument doc = Window.current().getDocument(); + doc.getBody().setClassName((shift ? "shift " : "") + (alpha ? "alpha": "")); + } + } diff --git a/src/js-specific/java/org/warp/picalculator/deps/DEngine.java b/src/js-specific/java/org/warp/picalculator/deps/DEngine.java index b80d8985..d0f14bc8 100644 --- a/src/js-specific/java/org/warp/picalculator/deps/DEngine.java +++ b/src/js-specific/java/org/warp/picalculator/deps/DEngine.java @@ -2,6 +2,7 @@ package org.warp.picalculator.deps; import org.warp.picalculator.ClassUtils; import org.warp.picalculator.gui.graphicengine.GraphicEngine; +import org.warp.picalculator.gui.graphicengine.html.HtmlEngine; public class DEngine { public static GraphicEngine newCPUEngine() { @@ -22,4 +23,7 @@ public class DEngine { public static GraphicEngine newFBEngine() { return null; } + public static GraphicEngine newHtmlEngine() { + return new HtmlEngine(); + } } diff --git a/src/js-specific/java/org/warp/picalculator/deps/DSemaphore.java b/src/js-specific/java/org/warp/picalculator/deps/DSemaphore.java new file mode 100644 index 00000000..1019f055 --- /dev/null +++ b/src/js-specific/java/org/warp/picalculator/deps/DSemaphore.java @@ -0,0 +1,41 @@ +package org.warp.picalculator.deps; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.Queue; + +import org.teavm.classlib.java.util.TArrayList; +import org.teavm.classlib.java.util.TQueue; + +public class DSemaphore { + + private Queue q; + + private int freePermits = 0; + + public DSemaphore(int i) { + q = new LinkedList(); + freePermits = i; + } + + public void release() { + if (q.peek() == null) { + q.poll(); + } else { + freePermits++; + } + } + + public void acquire() throws InterruptedException { + if (freePermits > 0) { + freePermits--; + } else { + Object thiz = new Object(); + q.offer(thiz); + while(q.contains(thiz)) { + Thread.sleep(500); + } + } + } + +} diff --git a/src/js-specific/java/org/warp/picalculator/deps/DStandardOpenOption.java b/src/js-specific/java/org/warp/picalculator/deps/DStandardOpenOption.java new file mode 100644 index 00000000..31bf39c9 --- /dev/null +++ b/src/js-specific/java/org/warp/picalculator/deps/DStandardOpenOption.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2007, 2009, Oracle and/or its affiliates. All rights reserved. + * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + +package org.warp.picalculator.deps; + +/** + * Defines the standard open options. + * + * @since 1.7 + */ + +public enum DStandardOpenOption { + /** + * Open for read access. + */ + READ, + + /** + * Open for write access. + */ + WRITE, + + /** + * If the file is opened for {@link #WRITE} access then bytes will be written + * to the end of the file rather than the beginning. + * + *

If the file is opened for write access by other programs, then it + * is file system specific if writing to the end of the file is atomic. + */ + APPEND, + + /** + * If the file already exists and it is opened for {@link #WRITE} + * access, then its length is truncated to 0. This option is ignored + * if the file is opened only for {@link #READ} access. + */ + TRUNCATE_EXISTING, + + /** + * Create a new file if it does not exist. + * This option is ignored if the {@link #CREATE_NEW} option is also set. + * The check for the existence of the file and the creation of the file + * if it does not exist is atomic with respect to other file system + * operations. + */ + CREATE, + + /** + * Create a new file, failing if the file already exists. + * The check for the existence of the file and the creation of the file + * if it does not exist is atomic with respect to other file system + * operations. + */ + CREATE_NEW, + + /** + * Delete on close. When this option is present then the implementation + * makes a best effort attempt to delete the file when closed + * by the appropriate {@code close} method. If the {@code close} method is + * not invoked then a best effort attempt is made to delete the + * file when the Java virtual machine terminates (either normally, as + * defined by the Java Language Specification, or where possible, abnormally). + * This option is primarily intended for use with work files that + * are used solely by a single instance of the Java virtual machine. This + * option is not recommended for use when opening files that are open + * concurrently by other entities. Many of the details as to when and how + * the file is deleted are implementation specific and therefore not + * specified. In particular, an implementation may be unable to guarantee + * that it deletes the expected file when replaced by an attacker while the + * file is open. Consequently, security sensitive applications should take + * care when using this option. + * + *

For security reasons, this option may imply the {@link + * LinkOption#NOFOLLOW_LINKS} option. In other words, if the option is present + * when opening an existing file that is a symbolic link then it may fail + * (by throwing {@link java.io.IOException}). + */ + DELETE_ON_CLOSE, + + /** + * Sparse file. When used with the {@link #CREATE_NEW} option then this + * option provides a hint that the new file will be sparse. The + * option is ignored when the file system does not support the creation of + * sparse files. + */ + SPARSE, + + /** + * Requires that every update to the file's content or metadata be written + * synchronously to the underlying storage device. + * + * @see Synchronized I/O file integrity + */ + SYNC, + + /** + * Requires that every update to the file's content be written + * synchronously to the underlying storage device. + * + * @see Synchronized I/O file integrity + */ + DSYNC; +} diff --git a/src/js-specific/java/org/warp/picalculator/deps/DURLClassLoader.java b/src/js-specific/java/org/warp/picalculator/deps/DURLClassLoader.java new file mode 100644 index 00000000..a01c9c7a --- /dev/null +++ b/src/js-specific/java/org/warp/picalculator/deps/DURLClassLoader.java @@ -0,0 +1,20 @@ +package org.warp.picalculator.deps; + +import java.net.URL; + +public class DURLClassLoader { + + public DURLClassLoader(URL[] urls) { + // TODO Auto-generated constructor stub + } + + public Class loadClass(String javaClassNameAndPath) { + return null; + } + + public void close() { + // TODO Auto-generated method stub + + } + +} diff --git a/src/js-specific/java/org/warp/picalculator/deps/StorageUtils.java b/src/js-specific/java/org/warp/picalculator/deps/StorageUtils.java index ed70ad66..5f9f7f2e 100644 --- a/src/js-specific/java/org/warp/picalculator/deps/StorageUtils.java +++ b/src/js-specific/java/org/warp/picalculator/deps/StorageUtils.java @@ -1,14 +1,216 @@ package org.warp.picalculator.deps; +import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.io.Charsets; +import org.apache.commons.io.IOUtils; +import org.teavm.classlib.java.nio.charset.TCharset; +import org.teavm.jso.browser.Window; +import org.warp.picalculator.Main; public class StorageUtils { + private static final String basepath; + static { + String fullurl = Window.current().getLocation().getFullURL(); + if (fullurl.charAt(fullurl.length()-1) == '/') { + basepath = fullurl+"resources"; + } else { + basepath = fullurl+"/resources"; + } +} + + private static Map resourcesCache = new HashMap(); + public static final boolean exists(Path f) { return f.toFile().exists(); } + + public static final boolean exists(File f) { + return f.exists(); + } - public static Path get(String path) { - return new File(path).toPath(); + public static File get(String... path) { + return new File(join(path, File.separator)); + } + + private static String join(String[] list, String conjunction) + { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (String item : list) + { + if (first) + first = false; + else + sb.append(conjunction); + sb.append(item); + } + return sb.toString(); + } + + public static File getResource(String path) throws IOException, URISyntaxException { + try { + File targetFile; + if (resourcesCache.containsKey(path)) { + if ((targetFile = resourcesCache.get(path)).exists()) { + return targetFile; + } else { + resourcesCache.remove(path); + } + } + final URL res = new URL(basepath+path); + InputStream initialStream = res.openStream(); + byte[] buffer = new byte[initialStream.available()]; + initialStream.read(buffer); + + targetFile = File.createTempFile("res", ".bin"); + targetFile.createNewFile(); + FileOutputStream outStream = new FileOutputStream(targetFile); + outStream.write(buffer); + outStream.close(); + resourcesCache.put(path, targetFile); + return targetFile; + } catch (final java.lang.IllegalArgumentException e) { + throw e; + } + } + + public static InputStream getResourceStream(String path) throws IOException, URISyntaxException { + try { + File targetFile; + if (resourcesCache.containsKey(path)) { + if ((targetFile = resourcesCache.get(path)).exists()) { + return new FileInputStream(targetFile); + } else { + resourcesCache.remove(path); + } + } + final URL res = new URL(basepath+path); + InputStream initialStream = res.openStream(); + byte[] buffer = new byte[initialStream.available()]; + initialStream.read(buffer); + + targetFile = File.createTempFile("res", ".bin"); + targetFile.createNewFile(); + FileOutputStream outStream = new FileOutputStream(targetFile); + outStream.write(buffer); + outStream.close(); + resourcesCache.put(path, targetFile); + return new FileInputStream(targetFile); + } catch (final java.lang.IllegalArgumentException e) { + throw e; + } + } + + public static List readAllLines(File file) throws IOException { + Reader reader_ = new InputStreamReader(new FileInputStream(file), Charset.defaultCharset()); + BufferedReader reader = reader_ instanceof BufferedReader ? (BufferedReader) reader_ : new BufferedReader(reader_); + List list = new ArrayList(); + String line = reader.readLine(); + while (line != null) { + list.add(line); + line = reader.readLine(); + } + reader.close(); + return list; + } + + public static String read(InputStream input) throws IOException { + return IOUtils.toString(input); + } + + public static List walk(File dir) throws IOException { + List out = new ArrayList<>(); + File[] filesList = dir.listFiles(); + if (filesList == null) { + out.add(dir); + } else { + for (File f : filesList) { + if (f.isDirectory()) { + if (f.canRead()) { + out.addAll(walk(dir)); + } + } else if (f.isFile()) { + if (f.canRead()) { + out.add(f); + } + } + } + } + return out; + } + + public static File relativize(File rulesPath, File f) { + return f; + } + + public static File resolve(File file, String string) { + return new File(file.getAbsolutePath() + File.separatorChar + string); + } + + public static File getParent(File f) { + return f.getParentFile(); + } + + public static void createDirectories(File dir) { + dir.mkdirs(); + } + + public static void write(File f, byte[] bytes, DStandardOpenOption... options) throws IOException { + boolean create = false; + for (DStandardOpenOption opt : options) { + if (opt == DStandardOpenOption.CREATE) { + create = true; + } + } + if (f.exists() == false) { + if (create) { + if (!f.createNewFile()) { + throw new IOException("File doesn't exist, can't create it!"); + } + } else { + throw new IOException("File doesn't exist."); + } + } + FileOutputStream stream = new FileOutputStream(f); + stream.write(bytes); + stream.close(); + } + + public static List readAllLines(InputStream input) throws IOException { + try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input))) { + String thisLine = null; + ArrayList output = new ArrayList<>(); + while ((thisLine = buffer.readLine()) != null) { + output.add(thisLine); + } + return output; + } } } diff --git a/src/main/java/org/warp/picalculator/Main.java b/src/main/java/org/warp/picalculator/Main.java index 87f85ed3..855a25ed 100755 --- a/src/main/java/org/warp/picalculator/Main.java +++ b/src/main/java/org/warp/picalculator/Main.java @@ -57,43 +57,46 @@ public class Main { } Utils.debugThirdScreen = StaticVars.debugOn & false; for (final String arg : args) { - if (arg.contains("2x")) { + if (arg.equalsIgnoreCase("2x")) { StaticVars.debugWindow2x = true; } - if (arg.contains("headless")) { + if (arg.equalsIgnoreCase("headless")) { Utils.headlessOverride = true; } - if (arg.contains("headless-8")) { + if (arg.equalsIgnoreCase("headless-8")) { Utils.headlessOverride = true; Utils.forceEngine = "console-8"; } - if (arg.contains("headless-256")) { + if (arg.equalsIgnoreCase("headless-256")) { Utils.headlessOverride = true; Utils.forceEngine = "console-256"; } - if (arg.contains("headless-24bit")) { + if (arg.equalsIgnoreCase("headless-24bit")) { Utils.headlessOverride = true; Utils.forceEngine = "console-24bit"; } - if (arg.contains("cpu")) { + if (arg.equalsIgnoreCase("cpu")) { Utils.forceEngine = "cpu"; } - if (arg.contains("gpu")) { + if (arg.equalsIgnoreCase("gpu")) { Utils.forceEngine = "gpu"; } - if (arg.contains("fb")) { + if (arg.equalsIgnoreCase("fb")) { Utils.forceEngine = "fb"; } - if (arg.contains("nogui")) { + if (arg.equalsIgnoreCase("nogui")) { Utils.forceEngine = "nogui"; } - if (arg.contains("verbose") || arg.contains("debug")) { + if (arg.equalsIgnoreCase("html")) { + Utils.forceEngine = "html"; + } + if (arg.equalsIgnoreCase("verbose") || arg.equalsIgnoreCase("debug")) { StaticVars.outputLevel = ConsoleUtils.OUTPUTLEVEL_DEBUG_VERBOSE; } - if (arg.contains("uncached")) { + if (arg.equalsIgnoreCase("uncached")) { Utils.debugCache = true; } - if (arg.contains("ms-dos")) { + if (arg.equalsIgnoreCase("ms-dos")) { Utils.headlessOverride = true; Utils.msDosMode = true; } diff --git a/src/main/java/org/warp/picalculator/Utils.java b/src/main/java/org/warp/picalculator/Utils.java index adefbac7..6d581582 100755 --- a/src/main/java/org/warp/picalculator/Utils.java +++ b/src/main/java/org/warp/picalculator/Utils.java @@ -9,27 +9,20 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; -import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; -import java.net.URI; import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.FileSystemAlreadyExistsException; -import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; import java.util.List; import java.util.Properties; -import java.util.stream.Collectors; import org.nevec.rjm.BigDecimalMath; import org.nevec.rjm.Rational; +import org.warp.picalculator.deps.StorageUtils; import org.warp.picalculator.gui.DisplayManager; import org.warp.picalculator.gui.graphicengine.BinaryFont; import org.warp.picalculator.math.Function; @@ -497,7 +490,7 @@ public class Utils { final int len = bytes.length; final int[] realbytes = new int[len]; for (int i = 0; i < len; i++) { - realbytes[i] = Byte.toUnsignedInt(bytes[i]); + realbytes[i] = bytes[i] & 0xFF; } return realbytes; } @@ -675,85 +668,20 @@ public class Utils { return (PlatformUtils.osName.indexOf("win") >= 0); } - public static void gc() { - Object obj = new Object(); - final WeakReference ref = new WeakReference<>(obj); - obj = null; - while (ref.get() != null) { - System.gc(); - } - } - public static ObjectArrayList newArrayList(T o) { final ObjectArrayList t = new ObjectArrayList<>(); t.add(o); return t; } - public static Path getResource(String string) throws IOException, URISyntaxException { - final URL res = Main.instance.getClass().getResource(string); - final boolean isResource = res != null; - if (isResource) { - try { - final URI uri = res.toURI(); - if (res.getProtocol().equalsIgnoreCase("jar")) { - try { - FileSystems.newFileSystem(uri, Collections.emptyMap()); - } catch (final FileSystemAlreadyExistsException e) { - FileSystems.getFileSystem(uri); - } - final Path myFolderPath = Paths.get(uri); - return myFolderPath; - } else { - return Paths.get(uri); - } - } catch (final java.lang.IllegalArgumentException e) { - throw e; - } - } else { - return Paths.get(string.substring(1)); - } - } - public static InputStream getResourceStreamSafe(String string) throws IOException, URISyntaxException { try { - return getResourceStream(string); + return StorageUtils.getResourceStream(string); } catch (final Exception ex) { return null; } } - public static InputStream getResourceStream(String string) throws IOException, URISyntaxException { - final URL res = Main.instance.getClass().getResource(string); - final boolean isResource = res != null; - if (isResource) { - try { - final URI uri = res.toURI(); - if (res.getProtocol().equalsIgnoreCase("jar")) { - try { - FileSystems.newFileSystem(uri, Collections.emptyMap()); - } catch (final FileSystemAlreadyExistsException e) { - FileSystems.getFileSystem(uri); - } - final Path myFolderPath = Paths.get(uri); - return Files.newInputStream(myFolderPath); - } else { - return Files.newInputStream(Paths.get(uri)); - } - } catch (final java.lang.IllegalArgumentException e) { - throw e; - } - } else { - return Files.newInputStream(Paths.get(string.substring(1))); - } - } - - public static String read(InputStream input) throws IOException { - try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input))) { - return buffer.lines().collect(Collectors.joining("\n")); - } - } - public static Path getJarDirectory() { return Paths.get("").toAbsolutePath(); } diff --git a/src/main/java/org/warp/picalculator/device/Keyboard.java b/src/main/java/org/warp/picalculator/device/Keyboard.java index 5f729f2f..3587644f 100755 --- a/src/main/java/org/warp/picalculator/device/Keyboard.java +++ b/src/main/java/org/warp/picalculator/device/Keyboard.java @@ -105,7 +105,7 @@ public class Keyboard { kt.start(); } - private synchronized static void debugKeyPressed(int keyCode) { + public static void debugKeyPressed(int keyCode) { switch (keyCode) { case KeyEvent.VK_ESCAPE: Keyboard.keyPressed(Key.POWEROFF); @@ -683,10 +683,12 @@ public class Keyboard { switch (k) { case SHIFT: Keyboard.shift = !Keyboard.shift; + PlatformUtils.shiftChanged(Keyboard.shift); refresh = true; break; case ALPHA: Keyboard.alpha = !Keyboard.alpha; + PlatformUtils.alphaChanged(Keyboard.alpha); refresh = true; break; default: @@ -695,9 +697,11 @@ public class Keyboard { if (StaticVars.debugOn == false) { if (k != Key.SHIFT && Keyboard.shift) { Keyboard.shift = false; + PlatformUtils.shiftChanged(Keyboard.shift); refresh = true; } else if (k != Key.ALPHA && Keyboard.alpha) { Keyboard.alpha = false; + PlatformUtils.alphaChanged(Keyboard.alpha); refresh = true; } } diff --git a/src/main/java/org/warp/picalculator/gui/DisplayManager.java b/src/main/java/org/warp/picalculator/gui/DisplayManager.java index ab53b422..4fd7f9f3 100755 --- a/src/main/java/org/warp/picalculator/gui/DisplayManager.java +++ b/src/main/java/org/warp/picalculator/gui/DisplayManager.java @@ -10,6 +10,7 @@ import org.warp.picalculator.PlatformUtils; import org.warp.picalculator.StaticVars; import org.warp.picalculator.Utils; import org.warp.picalculator.deps.DEngine; +import org.warp.picalculator.deps.DSemaphore; import org.warp.picalculator.deps.DSystem; import org.warp.picalculator.device.Keyboard; import org.warp.picalculator.gui.graphicengine.BinaryFont; @@ -40,7 +41,7 @@ public final class DisplayManager implements RenderingLoop { private Screen screen; private final HUD hud; - public Semaphore screenChange = new Semaphore(0); + public DSemaphore screenChange = new DSemaphore(0); public String displayDebugString; public ObjectArrayList errorMessages; @@ -110,43 +111,48 @@ public final class DisplayManager implements RenderingLoop { private GraphicEngine chooseGraphicEngine() { GraphicEngine d; - d = new NoGuiEngine(); - if (d.isSupported()) { - ConsoleUtils.out.println(1, "Using NoGui Graphic Engine"); - return d; - } if (!StaticVars.debugOn) { d = DEngine.newFBEngine(); - if (d.isSupported()) { + if (d != null && d.isSupported()) { ConsoleUtils.out.println(1, "Using FB Graphic Engine"); return d; } } d = DEngine.newGPUEngine(); - if (d.isSupported()) { + if (d != null && d.isSupported()) { ConsoleUtils.out.println(1, "Using GPU Graphic Engine"); return d; } d = DEngine.newCPUEngine(); - if (d.isSupported()) { + if (d != null && d.isSupported()) { ConsoleUtils.out.println(1, "Using CPU Graphic Engine"); return d; } d = DEngine.newHeadless24bitEngine(); - if (d.isSupported()) { + if (d != null && d.isSupported()) { System.err.println("Using Headless 24 bit Engine! This is a problem! No other graphic engines are available."); return d; } d = DEngine.newHeadless256Engine(); - if (d.isSupported()) { + if (d != null && d.isSupported()) { System.err.println("Using Headless 256 Engine! This is a problem! No other graphic engines are available."); return d; } d = DEngine.newHeadless8Engine(); - if (d.isSupported()) { + if (d != null && d.isSupported()) { System.err.println("Using Headless basic Engine! This is a problem! No other graphic engines are available."); return d; } + d = DEngine.newHtmlEngine(); + if (d != null && d.isSupported()) { + ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "Using Html Graphic Engine"); + return d; + } + d = new NoGuiEngine(); + if (d != null && d.isSupported()) { + ConsoleUtils.out.println(1, "Using NoGui Graphic Engine"); + return d; + } throw new UnsupportedOperationException("No graphic engines available."); } diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/Skin.java b/src/main/java/org/warp/picalculator/gui/graphicengine/Skin.java index 046cd873..36d8e696 100755 --- a/src/main/java/org/warp/picalculator/gui/graphicengine/Skin.java +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/Skin.java @@ -1,10 +1,11 @@ package org.warp.picalculator.gui.graphicengine; import java.io.IOException; +import java.net.URISyntaxException; public interface Skin { - public void load(String file) throws IOException; + public void load(String file) throws IOException, URISyntaxException; public void initialize(GraphicEngine d); diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/cpu/CPUFont.java b/src/main/java/org/warp/picalculator/gui/graphicengine/cpu/CPUFont.java index 2ac5f60b..e334a41e 100755 --- a/src/main/java/org/warp/picalculator/gui/graphicengine/cpu/CPUFont.java +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/cpu/CPUFont.java @@ -4,6 +4,8 @@ import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; import java.net.URL; import java.util.LinkedList; import java.util.logging.Level; @@ -12,7 +14,10 @@ import java.util.logging.Logger; import javax.imageio.ImageIO; import org.warp.picalculator.ConsoleUtils; +import org.warp.picalculator.PlatformUtils; import org.warp.picalculator.Utils; +import org.warp.picalculator.deps.DSystem; +import org.warp.picalculator.deps.StorageUtils; import org.warp.picalculator.gui.graphicengine.BinaryFont; import org.warp.picalculator.gui.graphicengine.GraphicEngine; @@ -95,12 +100,21 @@ public class CPUFont implements BinaryFont { } } - Utils.gc(); + PlatformUtils.gc(); } private void loadFont(String string) throws IOException { - final URL res = isResource ? this.getClass().getResource(string) : new File(string).toURI().toURL(); - final int[] file = Utils.realBytes(Utils.convertStreamToByteArray(res.openStream(), res.getFile().length())); + InputStream res; + try { + if (!string.startsWith("/")) + string = "/"+string; + res = StorageUtils.getResourceStream(string); + } catch (URISyntaxException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + final int[] file = Utils.realBytes(Utils.convertStreamToByteArray(res, res.available())); final int filelength = file.length; if (filelength >= 16) { if (file[0x0] == 114 && file[0x1] == 97 && file[0x2] == 119 && file[0x3] == 0xFF && file[0x8] == 0xFF && file[0xD] == 0xFF) { @@ -138,7 +152,7 @@ public class CPUFont implements BinaryFont { } catch (final Exception ex) { ex.printStackTrace(); System.out.println(string); - System.exit(-1); + DSystem.exit(-1); } } } else { diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/cpu/CPUSkin.java b/src/main/java/org/warp/picalculator/gui/graphicengine/cpu/CPUSkin.java index 6c82a19b..93a4de45 100755 --- a/src/main/java/org/warp/picalculator/gui/graphicengine/cpu/CPUSkin.java +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/cpu/CPUSkin.java @@ -3,12 +3,19 @@ package org.warp.picalculator.gui.graphicengine.cpu; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; import javax.imageio.ImageIO; +import org.warp.picalculator.deps.DSystem; +import org.warp.picalculator.deps.StorageUtils; import org.warp.picalculator.gui.graphicengine.GraphicEngine; import org.warp.picalculator.gui.graphicengine.Skin; +import ar.com.hjg.pngj.ImageLineInt; +import ar.com.hjg.pngj.PngReader; + public class CPUSkin implements Skin { public int[] skinData; @@ -20,28 +27,61 @@ public class CPUSkin implements Skin { load(file); } + @SuppressWarnings("unused") @Override public void load(String file) throws IOException { - final BufferedImage img = ImageIO.read(isResource ? this.getClass().getResource("/" + file) : new File(file).toURI().toURL()); - if (img == null) { - skinData = new int[0]; - skinSize = new int[] { 0, 0 }; - } else { - skinData = getMatrixOfImage(img); - skinSize = new int[] { img.getWidth(), img.getHeight() }; + if (!file.startsWith("/")) + file = "/"+file; + try { + PngReader r = new PngReader(StorageUtils.getResourceStream(file)); + if (r == null) { + skinData = new int[0]; + skinSize = new int[] { 0, 0 }; + System.err.println("ERROR WHILE LOADING SKIN " + file); + } else { + skinData = getMatrixOfImage(r); + skinSize = new int[] { r.imgInfo.cols, r.imgInfo.rows }; + } + } catch (URISyntaxException e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; } } - public static int[] getMatrixOfImage(BufferedImage bufferedImage) { - final int width = bufferedImage.getWidth(null); - final int height = bufferedImage.getHeight(null); + public static int[] getMatrixOfImage(PngReader r) { + final int width = r.imgInfo.cols; + final int height = r.imgInfo.rows; + final int channels = r.imgInfo.channels; final int[] pixels = new int[width * height]; - for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { - pixels[i + j * width] = bufferedImage.getRGB(i, j); - } - } + int pi = 0; + ImageLineInt lint; + while ( r.hasMoreRows() ) { + lint = (ImageLineInt) r.readRow(); + int[] scanLine = lint.getScanline(); + for ( int i = 0; i < width; i++ ) { + int offset = i * channels; + + // Adjust the following code depending on your source image. + // I need the to set the alpha channel to 0xFF000000 since my destination image + // is TRANSLUCENT : BufferedImage bi = CONFIG.createCompatibleImage( width, height, Transparency.TRANSLUCENT ); + // my source was 3 channels RGB without transparency + int nextPixel; + if (channels == 4) { + nextPixel = (scanLine[offset] << 16 ) | ( scanLine[offset + 1] << 8 ) | ( scanLine[offset + 2] ) | ( scanLine[offset + 3] << 24 ); + } else { + nextPixel = (scanLine[offset] << 16 ) | ( scanLine[offset + 1] << 8 ) | ( scanLine[offset + 2] ) | ( 0xFF << 24 ); + } + + // I'm placing the pixels on a memory mapped file + pixels[pi] = nextPixel; + pi++; + } + + } + + return pixels; } diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/gpu/GPUFont.java b/src/main/java/org/warp/picalculator/gui/graphicengine/gpu/GPUFont.java index cabdf261..7fbe1237 100755 --- a/src/main/java/org/warp/picalculator/gui/graphicengine/gpu/GPUFont.java +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/gpu/GPUFont.java @@ -4,6 +4,8 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; + +import org.warp.picalculator.PlatformUtils; import org.warp.picalculator.Utils; import org.warp.picalculator.gui.graphicengine.BinaryFont; import org.warp.picalculator.gui.graphicengine.GraphicEngine; @@ -64,10 +66,10 @@ public class GPUFont implements BinaryFont { intervalsTotalSize = font.intervalsTotalSize; boolean[][] rawchars = font.rawchars; font = null; - Utils.gc(); + PlatformUtils.gc(); pregenTexture(rawchars); rawchars = null; - Utils.gc(); + PlatformUtils.gc(); } public int[] getCharIndexes(String txt) { @@ -162,7 +164,7 @@ public class GPUFont implements BinaryFont { } chars = null; png.end(); - Utils.gc(); + PlatformUtils.gc(); try { memoryWidth = w; @@ -172,7 +174,7 @@ public class GPUFont implements BinaryFont { textureH = h; outputStream.flush(); outputStream.close(); - Utils.gc(); + PlatformUtils.gc(); tmpFont = f; } catch (GLException | IOException e) { e.printStackTrace(); diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/gpu/GPURenderer.java b/src/main/java/org/warp/picalculator/gui/graphicengine/gpu/GPURenderer.java index 546d4fd0..f9d4ae56 100755 --- a/src/main/java/org/warp/picalculator/gui/graphicengine/gpu/GPURenderer.java +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/gpu/GPURenderer.java @@ -9,6 +9,7 @@ import java.nio.FloatBuffer; import java.nio.file.Files; import javax.imageio.ImageIO; +import org.warp.picalculator.PlatformUtils; import org.warp.picalculator.StaticVars; import org.warp.picalculator.Utils; import org.warp.picalculator.gui.graphicengine.BinaryFont; @@ -256,7 +257,7 @@ public class GPURenderer implements Renderer { final int imgW = img.getWidth(); final int imgH = img.getHeight(); img = null; - Utils.gc(); + PlatformUtils.gc(); return new OpenedTextureData(imgW, imgH, f, isResource); } diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlEngine.java b/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlEngine.java new file mode 100644 index 00000000..deb88e44 --- /dev/null +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlEngine.java @@ -0,0 +1,274 @@ +package org.warp.picalculator.gui.graphicengine.html; + +import java.io.IOException; +import java.util.concurrent.Semaphore; + +import org.teavm.jso.JSBody; +import org.teavm.jso.JSObject; +import org.teavm.jso.browser.Window; +import org.teavm.jso.canvas.CanvasGradient; +import org.teavm.jso.canvas.CanvasRenderingContext2D; +import org.teavm.jso.dom.events.Event; +import org.teavm.jso.dom.events.EventListener; +import org.teavm.jso.dom.events.EventTarget; +import org.teavm.jso.dom.events.KeyboardEvent; +import org.teavm.jso.dom.html.HTMLButtonElement; +import org.teavm.jso.dom.html.HTMLCanvasElement; +import org.teavm.jso.dom.html.HTMLDocument; +import org.teavm.jso.dom.html.HTMLElement; +import org.teavm.jso.dom.html.HTMLInputElement; +import org.teavm.jso.dom.xml.NodeList; +import org.teavm.jso.json.JSON; +import org.warp.picalculator.PlatformUtils; +import org.warp.picalculator.Utils; +import org.warp.picalculator.deps.DSemaphore; +import org.warp.picalculator.deps.StorageUtils; +import org.warp.picalculator.device.Keyboard; +import org.warp.picalculator.gui.graphicengine.BinaryFont; +import org.warp.picalculator.gui.graphicengine.GraphicEngine; +import org.warp.picalculator.gui.graphicengine.Renderer; +import org.warp.picalculator.gui.graphicengine.RenderingLoop; +import org.warp.picalculator.gui.graphicengine.Skin; +import org.warp.picalculator.gui.graphicengine.cpu.CPUFont; +import org.warp.picalculator.gui.graphicengine.cpu.CPUSkin; + +public class HtmlEngine implements GraphicEngine { + + private boolean initialized; + public DSemaphore exitSemaphore = new DSemaphore(0); + private static final HTMLDocument document = Window.current().getDocument(); + private HTMLCanvasElement canvas; + private CanvasRenderingContext2D g; + private RenderingLoop renderingLoop; + private HtmlRenderer renderer; + private int width = -1, height = -1; + private final int frameTime = (int) (1000d/5d); + + @Override + public int[] getSize() { + return new int[] { getWidth(), getHeight() }; + } + + @Override + public boolean isInitialized() { + return initialized; + } + + @Override + public void setTitle(String title) { + HtmlEngine.setHTMLTitle(title); + } + + @JSBody(params = {"wndTitle"}, script = "document.title = wndTitle") + private static native void setHTMLTitle(String wndTitle); + + @Override + public void setResizable(boolean r) {} + + @Override + public void setDisplayMode(int ww, int wh) { + canvas.setWidth(ww); + width = ww; + canvas.setHeight(wh); + height = wh; + } + + private String previousValue=""; + + @Override + public void create(Runnable onInitialized) { + canvas = (HTMLCanvasElement) document.createElement("canvas"); + g = (CanvasRenderingContext2D ) canvas.getContext("2d"); + HTMLInputElement keyInput = (HTMLInputElement) document.createElement("input"); + keyInput.setType("text"); + keyInput.getStyle().setProperty("opacity", "0.1"); + setDisplayMode(480, 320); + document.getElementById("container").appendChild(canvas); + document.getBody().appendChild(keyInput); + keyInput.setTabIndex(0); + keyInput.addEventListener("keydown", (KeyboardEvent evt) -> { + evt.preventDefault(); + new Thread(() -> { + previousValue = keyInput.getValue(); + Keyboard.debugKeyPressed(evt.getKeyCode()); + System.out.println(evt.getKeyCode()); + System.out.println(""+(int) evt.getKey().charAt(0)); + }).start(); + }); + keyInput.addEventListener("input", (Event evt) -> { + evt.preventDefault(); + final String previousValue = this.previousValue; + final String newValue = keyInput.getValue(); + final int prevLen = previousValue.length(); + final int newLen = newValue.length(); + + new Thread(() -> { + if (newLen == prevLen) { + + } else if (newLen - prevLen == 1) { + Keyboard.debugKeyPressed((int) newValue.toUpperCase().charAt(newLen-1)); + } else if (newLen - prevLen > 1) { + for (int i = 0; i < newLen - prevLen; i++) { + Keyboard.debugKeyPressed((int) newValue.toUpperCase().charAt(prevLen + i)); + } + } else if (newLen - prevLen < 1) { + for (int i = 0; i < prevLen - newLen; i++) { + Keyboard.debugKeyPressed(8); + } + } + }).start(); + }); + canvas.addEventListener("click", (Event evt) -> { + keyInput.focus(); + }); + document.addEventListener("DOMContentLoaded", (Event e) -> { + keyInput.focus(); + }); + NodeList buttons = document.getBody().getElementsByTagName("button"); + for (int i = 0; i < buttons.getLength(); i++) { + if (buttons.item(i).hasAttribute("keycode")) { + buttons.item(i).addEventListener("click", (Event evt) -> { + evt.preventDefault(); + EventTarget target = evt.getCurrentTarget(); + HTMLButtonElement button = target.cast(); + new Thread(() -> { + try { + if (Keyboard.alpha && !Keyboard.shift) { + if (button.hasAttribute("keycodea")) { + Keyboard.debugKeyPressed(Integer.parseInt(button.getAttribute("keycodea"))); + } else { + Keyboard.debugKeyPressed(Integer.parseInt(button.getAttribute("keycode"))); + } + } else if (!Keyboard.alpha && Keyboard.shift) { + if (button.hasAttribute("keycodes")) { + Keyboard.debugKeyPressed(Integer.parseInt(button.getAttribute("keycodes"))); + } else { + Keyboard.debugKeyPressed(Integer.parseInt(button.getAttribute("keycode"))); + } + } else if (Keyboard.alpha && Keyboard.shift) { + if (button.hasAttribute("keycodesa")) { + Keyboard.debugKeyPressed(Integer.parseInt(button.getAttribute("keycodesa"))); + } else { + if (button.hasAttribute("keycodes")) { + Keyboard.debugKeyPressed(Integer.parseInt(button.getAttribute("keycodes"))); + } else { + Keyboard.debugKeyPressed(Integer.parseInt(button.getAttribute("keycode"))); + } + } + } else { + Keyboard.debugKeyPressed(Integer.parseInt(button.getAttribute("keycode"))); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + }).start(); + }); + } + } + renderer = new HtmlRenderer(this, g); + initialized = true; + if (onInitialized != null) { + onInitialized.run(); + } + } + + @Override + public boolean wasResized() { + return false; + } + + @Override + public int getWidth() { + if (width == -1) { + width = canvas.getWidth(); + } + return width; + } + + @Override + public int getHeight() { + if (height == -1) { + height = canvas.getHeight(); + } + return height; + } + + @Override + public void destroy() { + document.getBody().removeChild(canvas); + initialized = false; + exitSemaphore.release(); + } + + @Override + public void start(RenderingLoop d) { + renderingLoop = d; + final Thread th = new Thread(() -> { + try { + double extratime = 0; + while (initialized) { + final long start = System.currentTimeMillis(); + repaint(); + final long end = System.currentTimeMillis(); + final double delta = (end - start) / 1000d; + final int deltaInt = (int) Math.floor(delta); + final int extraTimeInt = (int) Math.floor(extratime); + if (extraTimeInt + deltaInt < frameTime) { + Thread.sleep(frameTime - (extraTimeInt + deltaInt)); + extratime = 0; + } else { + extratime += delta - frameTime; + } + } + } catch (final InterruptedException e) { + e.printStackTrace(); + } + }); + PlatformUtils.setThreadName(th, "Canvas rendering thread"); + PlatformUtils.setDaemon(th); + th.start(); + } + + @Override + public void repaint() { + renderingLoop.refresh(); + } + + @Override + public HtmlRenderer getRenderer() { + return renderer; + } + + @Override + public HtmlFont loadFont(String fontName) throws IOException { + return new HtmlFont(fontName); + } + + @Override + public HtmlFont loadFont(String path, String fontName) throws IOException { + return new HtmlFont(path, fontName); + } + + @Override + public HtmlSkin loadSkin(String file) throws IOException { + return new HtmlSkin(file); + } + + @Override + public void waitForExit() { + try { + exitSemaphore.acquire(); + } catch (final InterruptedException e) {} + } + + @Override + public boolean isSupported() { + return PlatformUtils.isJavascript; + } + + @Override + public boolean doesRefreshPauses() { + return true; + } + +} diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlFont.java b/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlFont.java new file mode 100644 index 00000000..7e4d755d --- /dev/null +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlFont.java @@ -0,0 +1,25 @@ +package org.warp.picalculator.gui.graphicengine.html; + +import java.io.IOException; + +import org.warp.picalculator.gui.graphicengine.GraphicEngine; +import org.warp.picalculator.gui.graphicengine.cpu.CPUFont; + +public class HtmlFont extends CPUFont { + + public HtmlFont(String fontName) throws IOException { + super(fontName); + } + + public HtmlFont(String path, String fontName) throws IOException { + super(path, fontName); + } + + @Override + public void use(GraphicEngine d) { + if (d.getRenderer() instanceof HtmlRenderer) { + ((HtmlRenderer) d.getRenderer()).f = this; + } + } + +} diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlRenderer.java b/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlRenderer.java new file mode 100644 index 00000000..1b5aa77c --- /dev/null +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlRenderer.java @@ -0,0 +1,457 @@ +package org.warp.picalculator.gui.graphicengine.html; + +import org.teavm.jso.canvas.CanvasRenderingContext2D; +import org.teavm.jso.typedarrays.Uint8ClampedArray; +import org.warp.picalculator.StaticVars; +import org.warp.picalculator.gui.graphicengine.Renderer; + +public class HtmlRenderer implements Renderer { + private static final boolean ENABLE_SUPERSAMPLING = false; + private static final boolean ENABLE_TRANSPARENCY = true; + private String currentColor = "#000000ff"; + private String clearColor = "#000000ff"; + HtmlFont f = null; + HtmlSkin currentSkin = null; + private final CanvasRenderingContext2D g; + private final HtmlEngine e; + public HtmlRenderer(HtmlEngine e, CanvasRenderingContext2D g) { + this.g = g; + this.e = e; + } + + private String toHex(int c) { + final int a = c >> 24 & 0xFF; + final int r = c >> 16 & 0xFF; + final int gg = c >> 8 & 0xFF; + final int b = c & 0xFF; + return String.format("#%02x%02x%02x%02x", r, gg, b, a); + } + + private String toHex8(int c) { + final int r = c >> 16 & 0xFF; + final int gg = c >> 8 & 0xFF; + final int b = c & 0xFF; + return String.format("#%02x%02x%02x", r, gg, b); + } + + private String toHex(int r, int g, int b) { + return String.format("#%02x%02x%02x", r, g, b); + } + + private String toHex(int r, int g, int b, int a) { + return String.format("#%02x%02x%02x%02x", r, g, b, a); + } + + @Override + public int glGetClearColor() { + return hexToInt(clearColor); + } + + private int hexToInt(String hex) { + switch (hex.length()) { + case 6: + return (0xFF << 24) | + (Integer.valueOf(hex.substring(0, 2), 16) << 16) | + (Integer.valueOf(hex.substring(2, 4), 16) << 8) | + Integer.valueOf(hex.substring(4, 6), 16); + case 6+1: + return (0xFF << 24) | + (Integer.valueOf(hex.substring(0+1, 2+1), 16) << 16) | + (Integer.valueOf(hex.substring(2+1, 4+1), 16) << 8) | + Integer.valueOf(hex.substring(4+1, 6+1), 16); + case 8: + return (Integer.valueOf(hex.substring(6, 8), 16) << 24) | + (Integer.valueOf(hex.substring(0, 2), 16) << 16) | + (Integer.valueOf(hex.substring(2, 4), 16) << 8) | + Integer.valueOf(hex.substring(4, 6), 16); + case 8+1: + return (Integer.valueOf(hex.substring(6+1, 8+1), 16) << 24) | + (Integer.valueOf(hex.substring(0+1, 2+1), 16) << 16) | + (Integer.valueOf(hex.substring(2+1, 4+1), 16) << 8) | + Integer.valueOf(hex.substring(4+1, 6+1), 16); + } + return 0xFF000000; + } + + @Override + public void glFillRect(float x, float y, float width, float height, float uvX, float uvY, float uvWidth, + float uvHeight) { + if (currentSkin != null) { + glDrawSkin((int) x, (int) y, (int) (x + width), (int) (y + height), (int) uvX, (int) uvY, (int) (uvWidth + uvX), (int) (uvHeight + uvY), true); + } else { + glFillColor(x, y, width, height); + } + } + + @SuppressWarnings("unused") + private void glDrawSkin(int x0, int y0, int x1, int y1, int s0, int t0, int s1, int t1, boolean transparent) { + final int[] size = e.getSize(); + + x0 += StaticVars.screenPos[0]; + y0 += StaticVars.screenPos[1]; + final double incrementX = Math.abs((double) (x1 - x0) / (double) (s1 - s0)); + final double incrementY = Math.abs((double) (y1 - y0) / (double) (t1 - t0)); + final boolean flippedX = (x1 - x0) / (s1 - s0) < 0; + final boolean flippedY = (y1 - y0) / (t1 - t0) < 0; + int oldColor = 0; + int newColor; + final int onex = s0 <= s1 ? 1 : -1; + final int oney = t0 <= t1 ? 1 : -1; + int width = 0; + int height = 0; + if (onex == -1) { + final int s00 = s0; + s0 = s1; + s1 = s00; + width = s1 - s0; + } + if (oney == -1) { + final int t00 = t0; + t0 = t1; + t1 = t00; + height = t1 - t0; + } + if (x0 >= size[0] || y0 >= size[0]) { + return; + } + if (x0 + width >= size[0]) { + s1 = size[0] - x0 + s0; + } + if (y0 + height >= size[1]) { + t1 = size[1] - y0 + t0; + } + if (x0 < 0) { + if (onex == -1) { + width += x0; + s1 += x0 + 1; + } else { + s0 -= x0; + } + x0 = 0; + } + if (y0 < 0) { + if (oney == -1) { + height += y0; + t1 += y0 + 1; + } else { + t0 -= y0; + } + y0 = 0; + } + Uint8ClampedArray oldColors = g.getImageData(x0, y0, x1-x0, y1-y0).getData(); + for (double pixelX = 0; pixelX < x1 - x0; pixelX++) { + for (double pixelY = 0; pixelY < y1 - y0; pixelY++) { + final int imgx = (int) (x0 + pixelX); + final int imgy = (int) (y0 + pixelY); + final int oldindex = (int) (((pixelX)+(pixelY*(x1-x0))) * 4); + final int index = imgx + imgy; + if (index >= 0 && index < size[0]*size[1] && pixelX < size[0]) { + final int texx = (int) (pixelX / incrementX); + final int texy = (int) (pixelY / incrementY); + int expX = 0; + int expY = 0; + if (incrementX < 1) { + expX = (int) Math.round(1d / incrementX / 2d); + } + if (incrementY < 1) { + expY = (int) Math.round(1d / incrementY / 2d); + } + if (ENABLE_SUPERSAMPLING) { + final int[] newColors = new int[(1 + expX * 2) * (1 + expY * 2)]; + for (int expXi = -expX; expXi <= expX; expXi++) { + for (int expYi = -expY; expYi <= expY; expYi++) { + final int skinIndex = (int) (s0 + (texx * (flippedX ? -1d : 1d) + (flippedX ? -(s0 - s1) - 1 : 0) + expXi) + (t0 + (texy * (flippedY ? -1d : 1d) + (flippedY ? -(t0 - t1) - 1 : 0) + expYi)) * currentSkin.skinSize[0]); + final int idx = (expXi + expX) + (expYi + expY) * (1 + expY * 2); + if (idx >= 0 && idx < newColors.length) { + newColors[idx] = getSkinColorAt(currentSkin.skinData, skinIndex); + } + } + } + newColor = joinColors(newColors); + } else { + final int skinIndex = (int) (s0 + (texx * (flippedX ? -1d : 1d) + (flippedX ? -(s0 - s1) - 1 : 0)) + (t0 + (texy * (flippedY ? -1d : 1d) + (flippedY ? -(t0 - t1) - 1 : 0))) * currentSkin.skinSize[0]); + newColor = getSkinColorAt(currentSkin.skinData, skinIndex); + } + + if (transparent) { + if (ENABLE_TRANSPARENCY) { + int oldColorR = oldColors.get(oldindex+0) & 0xFF; + int oldColorG = oldColors.get(oldindex+1) & 0xFF; + int oldColorB = oldColors.get(oldindex+2) & 0xFF; + int oldColorA = 0xFF; + oldColor = (oldColorA << 24) | (oldColorR << 16) | (oldColorG << 8) | (oldColorB); + final double a2 = (newColor >> 24 & 0xFF) / 255f; + final double a1 = 1f - a2; + final int r = (int) ((oldColor >> 16 & 0xFF) * a1 + (newColor >> 16 & 0xFF) * a2); + final int gg = (int) ((oldColor >> 8 & 0xFF) * a1 + (newColor >> 8 & 0xFF) * a2); + final int b = (int) ((oldColor & 0xFF) * a1 + (newColor & 0xFF) * a2); + newColor = 0xFF000000 | r << 16 | gg << 8 | b; + } + } + + if (!ENABLE_TRANSPARENCY) { + if (transparent && (((newColor >> 24) & 0xFF) < 0x02)) { + g.setFillStyle(clearColor); + } else { + g.setFillStyle(toHex8(newColor)); + } + } else { + g.setFillStyle(toHex8(stackColors(oldColor, newColor))); + } + g.fillRect(imgx, imgy, 1, 1 ); + } + } + } + } + + private int joinColors(int[] newColors) { + double a = 0; + double r = 0; + double g = 0; + double b = 0; + for (final int newColor : newColors) { + a += newColor >> 24 & 0xFF; + r += newColor >> 16 & 0xFF; + g += newColor >> 8 & 0xFF; + b += newColor & 0xFF; + } + return (int)(a / (double)newColors.length) << 24 | (int)(r / (double)newColors.length) << 16 | (int)(g / (double)newColors.length) << 8 | (int)(b / (double)newColors.length); + } + + private int getSkinColorAt(int[] skinData, int skinIndex) { + int color = hexToInt(currentColor); + int newColor = 0; + if (skinIndex >= 0 && skinIndex < skinData.length) { + newColor = skinData[skinIndex] & 0xFFFFFFFF; + final int a = (int) ((double)((newColor >> 24 & 0xFF)) * ((double) (color >> 24 & 0xFF) / (double) 0xFF)); + final int r = (int) ((double)((newColor >> 16 & 0xFF)) * ((double) (color >> 16 & 0xFF) / (double) 0xFF)); + final int g = (int) ((double)((newColor >> 8 & 0xFF)) * ((double) (color >> 8 & 0xFF) / (double) 0xFF)); + final int b = (int) ((double)((newColor & 0xFF)) * ((double) (color & 0xFF) / (double) 0xFF)); + newColor = a << 24 | r << 16 | g << 8 | b; + } + return newColor; + } + + + @Override + public void glFillColor(float x, float y, float width, float height) { + final int[] size = e.getSize(); + int color = hexToInt(currentColor); + + x += StaticVars.screenPos[0]; + y += StaticVars.screenPos[1]; + + final int ix = (int) x; + final int iy = (int) y; + final int iw = (int) width; + final int ih = (int) height; + + int x0 = ix; + int y0 = iy; + int x1 = ix + iw; + int y1 = iy + ih; + + if (ix >= size[0] || iy >= size[1]) { + return; + } + if (x0 < 0) { + x0 = 0; + } + if (x1 >= size[0]) { + x1 = size[0]; + } + if (y0 < 0) { + y0 = 0; + } + if (y1 >= size[1]) { + y1 = size[1]; + } + final int sizeW = size[0]; + for (int px = x0; px < x1; px++) { + for (int py = y0; py < y1; py++) { + final int idx = (px) + (py) * sizeW; + if (px < sizeW && idx >= 0 && idx < size[0]*size[1]) { + Uint8ClampedArray oldColor = g.getImageData(px, py, 1, 1).getData(); + int oldColorR = oldColor.get(0) & 0xFF; + int oldColorG = oldColor.get(1) & 0xFF; + int oldColorB = oldColor.get(2) & 0xFF; + int oldColorA = 0xFF; + int oldColorARGB = (oldColorA << 24) | (oldColorR << 16) | (oldColorG << 8) | (oldColorB); + g.setFillStyle(toHex(stackColors(oldColorARGB, color))); + g.fillRect( px, py, 1, 1 ); + + } + } + } + } + + @Override + public void glDrawStringRight(float x, float y, String text) { + glDrawStringLeft(x - f.getStringWidth(text), y, text); + } + + @Override + public void glDrawStringLeft(float x, float y, String textString) { + x += StaticVars.screenPos[0]; + y += StaticVars.screenPos[1]; + + final int ix = (int) x; + final int iy = (int) y; + + final int[] text = f.getCharIndexes(textString); + final int[] screenSize = e.getSize(); + final int screenLength = screenSize[0]*screenSize[1]; + int screenPos = 0; + + int currentInt; + int currentIntBitPosition; + int bitData; + int cpos; + int j; + final int l = text.length; + int color = hexToInt(currentColor); + for (int i = 0; i < l; i++) { + cpos = (i * (f.charW)); + final int charIndex = text[i]; + for (int dy = 0; dy < f.charH; dy++) { + for (int dx = 0; dx < f.charW; dx++) { + j = ix + cpos + dx; + if (j > 0 & j < screenSize[0]) { + final int bit = dx + dy * f.charW; + currentInt = (int) (Math.floor(bit) / (HtmlFont.intBits)); + currentIntBitPosition = bit - (currentInt * HtmlFont.intBits); + final int charIdx = charIndex * f.charIntCount + currentInt; + if (charIdx >= 0 && charIdx < f.chars32.length) { + bitData = (f.chars32[charIdx] >> currentIntBitPosition) & 1; + screenPos = ix + cpos + dx + (iy + dy) * screenSize[0]; + if (bitData == 1 & screenLength > screenPos & screenPos >= 0) { + Uint8ClampedArray oldColor = g.getImageData(ix+cpos+dx, iy+dy, 1, 1).getData(); + int oldColorR = oldColor.get(0) & 0xFF; + int oldColorG = oldColor.get(1) & 0xFF; + int oldColorB = oldColor.get(2) & 0xFF; + int oldColorA = 0xFF; + int oldColorARGB = (oldColorA << 24) | (oldColorR << 16) | (oldColorG << 8) | (oldColorB); + g.setFillStyle(toHex(stackColors(oldColorARGB, color))); + g.fillRect( ix+cpos+dx, iy+dy, 1, 1 ); + } + } + } + } + } + } + } + + private int stackColors(int... color) { + double a = 0; + double r = 0; + double g = 0; + double b = 0; + for (final int newColor : color) { + final double alpha = (newColor >> 24 & 0xFF) / 255d; + a = a * (1d - alpha) + (newColor >> 24 & 0xFF) * alpha; + r = r * (1d - alpha) + (newColor >> 16 & 0xFF) * alpha; + g = g * (1d - alpha) + (newColor >> 8 & 0xFF) * alpha; + b = b * (1d - alpha) + (newColor & 0xFF) * alpha; + } + return ((int) a) << 24 | ((int) r) << 16 | ((int) g) << 8 | ((int) b); + } + + @Override + public void glDrawStringCenter(float x, float y, String text) { + glDrawStringLeft(x - (f.getStringWidth(text) / 2), y, text); + } + + @Override + public void glDrawLine(float x0, float y0, float x1, float y1) { + if (x1-x0 > 0 && y1-y0 > 0) { + g.beginPath(); + g.moveTo(x0, y0); + g.lineTo(x1, y1); + g.stroke(); + } else { + g.fillRect(x0, y0, (x1-x0)+1, (y1-y0)+1); + } + } + + @Override + public void glDrawCharRight(int x, int y, char ch) { + glDrawStringRight(x, y, ch + ""); + } + + @Override + public void glDrawCharLeft(int x, int y, char ch) { + glDrawStringLeft(x, y, ch + ""); + } + + @Override + public void glDrawCharCenter(int x, int y, char ch) { + glDrawStringCenter(x, y, ch + ""); + } + + @Override + public void glColor4i(int red, int green, int blue, int alpha) { + g.setFillStyle(currentColor = toHex(red, green, blue, alpha)); + } + + @Override + public void glColor4f(float red, float green, float blue, float alpha) { + glColor4i((int) (red * 255d), (int) (green * 255d), (int) (blue * 255d), (int) (alpha * 255d)); + } + + @Override + public void glColor3i(int r, int gg, int b) { + g.setFillStyle(currentColor = toHex(r, gg, b)); + } + + @Override + public void glColor3f(float red, float green, float blue) { + glColor3i((int) (red * 255d), (int) (green * 255d), (int) (blue * 255d)); + } + + @Override + public void glColor(int c) { + final int a = c >> 24 & 0xFF; + final int r = c >> 16 & 0xFF; + final int gg = c >> 8 & 0xFF; + final int b = c & 0xFF; + g.setFillStyle(currentColor = toHex(r, gg, b, a)); + } + + @Override + public void glClearSkin() { + currentSkin = null; + } + + @Override + public void glClearColor4i(int red, int green, int blue, int alpha) { + clearColor = toHex(red, green, blue, alpha); + } + + @Override + public void glClearColor4f(float red, float green, float blue, float alpha) { + clearColor = toHex((int)(red*255), + (int)(green*255), + (int)(blue*255), + (int)(alpha*255)); + } + + @Override + public void glClearColor(int c) { + final int r = c >> 16 & 0xFF; + final int gg = c >> 8 & 0xFF; + final int b = c & 0xFF; + clearColor = toHex(r, gg, b); + } + + @Override + public void glClear(int screenWidth, int screenHeight) { + g.setFillStyle(clearColor); + g.fillRect(0, 0, screenWidth, screenHeight); + g.setFillStyle(currentColor); + } + + @Override + public HtmlFont getCurrentFont() { + return f; + } +} diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlSkin.java b/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlSkin.java new file mode 100644 index 00000000..3bcee33f --- /dev/null +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/html/HtmlSkin.java @@ -0,0 +1,20 @@ +package org.warp.picalculator.gui.graphicengine.html; + +import java.io.IOException; + +import org.warp.picalculator.gui.graphicengine.GraphicEngine; +import org.warp.picalculator.gui.graphicengine.cpu.CPUSkin; + +public class HtmlSkin extends CPUSkin { + + public HtmlSkin(String file) throws IOException { + super(file); + } + + @Override + public void use(GraphicEngine d) { + if (d instanceof HtmlEngine) { + ((HtmlEngine) d).getRenderer().currentSkin = this; + } + } +} diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/html/InputEvent.java b/src/main/java/org/warp/picalculator/gui/graphicengine/html/InputEvent.java new file mode 100644 index 00000000..bb9b99f1 --- /dev/null +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/html/InputEvent.java @@ -0,0 +1,10 @@ +package org.warp.picalculator.gui.graphicengine.html; + +import org.teavm.jso.JSProperty; +import org.teavm.jso.dom.events.Event; + +public interface InputEvent extends Event { + + @JSProperty + String getValue(); +} diff --git a/src/main/java/org/warp/picalculator/gui/graphicengine/nogui/NoGuiEngine.java b/src/main/java/org/warp/picalculator/gui/graphicengine/nogui/NoGuiEngine.java index 1381376a..b416d3b5 100644 --- a/src/main/java/org/warp/picalculator/gui/graphicengine/nogui/NoGuiEngine.java +++ b/src/main/java/org/warp/picalculator/gui/graphicengine/nogui/NoGuiEngine.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.concurrent.Semaphore; import org.warp.picalculator.Utils; +import org.warp.picalculator.deps.DSemaphore; import org.warp.picalculator.gui.graphicengine.BinaryFont; import org.warp.picalculator.gui.graphicengine.GraphicEngine; import org.warp.picalculator.gui.graphicengine.Renderer; @@ -13,7 +14,7 @@ import org.warp.picalculator.gui.graphicengine.Skin; public class NoGuiEngine implements GraphicEngine { private boolean initialized; - public Semaphore exitSemaphore = new Semaphore(0); + public DSemaphore exitSemaphore = new DSemaphore(0); @Override public int[] getSize() { @@ -274,7 +275,7 @@ public class NoGuiEngine implements GraphicEngine { @Override public boolean isSupported() { - return Utils.forceEngine != null && Utils.forceEngine.equals("nogui"); + return true; } @Override diff --git a/src/main/java/org/warp/picalculator/math/rules/RulesManager.java b/src/main/java/org/warp/picalculator/math/rules/RulesManager.java index c504ecc2..0ab00754 100644 --- a/src/main/java/org/warp/picalculator/math/rules/RulesManager.java +++ b/src/main/java/org/warp/picalculator/math/rules/RulesManager.java @@ -1,6 +1,7 @@ package org.warp.picalculator.math.rules; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -16,13 +17,17 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +import org.apache.commons.io.IOUtils; import org.warp.picalculator.ConsoleUtils; import org.warp.picalculator.Error; +import org.warp.picalculator.PlatformUtils; import org.warp.picalculator.Utils; import org.warp.picalculator.ZipUtils; import org.warp.picalculator.deps.StorageUtils; import org.warp.picalculator.deps.DJDTCompiler; +import org.warp.picalculator.deps.DStandardOpenOption; import org.warp.picalculator.deps.DSystem; +import org.warp.picalculator.deps.DURLClassLoader; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.MathContext; import org.warp.picalculator.math.functions.Expression; @@ -38,7 +43,7 @@ public class RulesManager { private RulesManager() {} - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "unused" }) public static void initialize() { ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "RulesManager", "Loading the rules"); rules = new ObjectArrayList[RuleType.values().length]; @@ -47,152 +52,182 @@ public class RulesManager { } try { boolean compiledSomething = false; - final Path defaultRulesPath = Utils.getResource("/default-rules.lst"); - if (!StorageUtils.exists(defaultRulesPath)) { + InputStream defaultRulesList; + try { + defaultRulesList = StorageUtils.getResourceStream("/default-rules.lst"); + } catch (IOException ex) { throw new FileNotFoundException("default-rules.lst not found!"); } final List ruleLines = new ArrayList<>(); - final Path rulesPath = StorageUtils.get("rules/"); - if (rulesPath.toFile().exists()) { - try (Stream paths = Files.walk(rulesPath)) { - paths.filter(Files::isRegularFile).forEach((Path p) -> { - if (p.toString().endsWith(".java")) { - String path = rulesPath.relativize(p).toString(); - path = path.substring(0, path.length() - ".java".length()); - ruleLines.add(path); - ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "RulesManager", "Found external rule: " + p.toAbsolutePath().toString()); - System.err.println(path); - } - }); - } - } - ruleLines.addAll(Files.readAllLines(defaultRulesPath)); - - boolean useCache = false; - final Path tDir = Paths.get(System.getProperty("java.io.tmpdir"), "WarpPi-Calculator").resolve("rules-rt"); -// try { -// final Path defaultResource = Utils.getResource("/math-rules-cache.zip"); -// } - final Path cacheFilePath = Utils.getResource("/math-rules-cache.zip");//Paths.get(Utils.getJarDirectory().toString()).resolve("math-rules-cache.zip").toAbsolutePath(); - if (cacheFilePath.toFile().exists()) { - try { - if (tDir.toFile().exists()) { - tDir.toFile().delete(); + final File rulesPath = StorageUtils.get("rules/"); + if (rulesPath.exists()) { + for (File f : StorageUtils.walk(rulesPath)) { + if (f.toString().endsWith(".java")) { + String path = StorageUtils.relativize(rulesPath, f).toString(); + path = path.substring(0, path.length() - ".java".length()); + ruleLines.add(path); + ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "RulesManager", "Found external rule: " + f.getAbsolutePath()); } - ZipUtils.unzip(cacheFilePath.toString(), tDir.getParent().toString(), ""); - useCache = !Utils.debugCache; - } catch (final Exception ex) { - ex.printStackTrace(); } } - for (final String rulesLine : ruleLines) { - if (rulesLine.length() > 0) { - final String[] ruleDetails = rulesLine.split(",", 1); - final String ruleName = ruleDetails[0]; - final String ruleNameEscaped = ruleName.replace(".", "_"); - ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "RulesManager", "Evaluating /rules/" + ruleNameEscaped + ".java"); - final String pathWithoutExtension = "/rules/" + ruleNameEscaped; - final String scriptFile = pathWithoutExtension + ".java"; - final InputStream resourcePath = Utils.getResourceStream(scriptFile); - if (resourcePath == null) { - System.err.println(new FileNotFoundException("/rules/" + ruleName + ".java not found!")); - } else { - Rule r = null; - if (useCache) { - try { - ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_DEBUG_MIN, "RulesManager", ruleName, "Trying to load cached rule"); - r = loadClassRuleFromSourceFile(scriptFile, tDir); - if (r != null) { - ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_DEBUG_MIN, "RulesManager", ruleName, "Loaded cached rule"); - } - } catch (final Exception e) { - e.printStackTrace(); - ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "RulesManager", ruleName, "Can't load the rule!"); - } - } - if (r == null || !useCache) { - ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_DEBUG_MIN, "RulesManager", ruleName, "This rule is not cached. Compiling"); - try { - r = compileJavaRule(scriptFile, tDir); - compiledSomething = true; - } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | IOException e) { - e.printStackTrace(); - } + ruleLines.addAll(StorageUtils.readAllLines(defaultRulesList)); + final File tDir = StorageUtils.resolve(StorageUtils.get(System.getProperty("java.io.tmpdir"), "WarpPi-Calculator"), "rules-rt"); + // try { + // final Path defaultResource = Utils.getResource("/math-rules-cache.zip"); + // } + InputStream cacheFileStream = null; + File cacheFilePath = null; + cacheFilePath = new File("math-rules-cache.zip"); + boolean cacheFileExists = false; + if (PlatformUtils.isJavascript) { + PlatformUtils.loadPlatformRules(); + } else { + if (cacheFilePath.exists()) { + cacheFileExists = true; + cacheFileStream = new FileInputStream(cacheFilePath); + } else { + try { + cacheFileStream = StorageUtils.getResourceStream("/math-rules-cache.zip");//Paths.get(Utils.getJarDirectory().toString()).resolve("math-rules-cache.zip").toAbsolutePath( + org.apache.commons.io.FileUtils.copyInputStreamToFile(cacheFileStream, cacheFilePath); + cacheFileExists = true; + } catch (IOException ex) { //File does not exists. + } + } + boolean useCache = false; + if (cacheFileExists) { + try { + if (tDir.exists()) { + tDir.delete(); } - if (r != null) { - RulesManager.addRule(r); + ZipUtils.unzip(cacheFilePath.toString(), tDir.getParent().toString(), ""); + useCache = !Utils.debugCache; + } catch (final Exception ex) { + ex.printStackTrace(); + } + } + + for (final String rulesLine : ruleLines) { + if (rulesLine.length() > 0) { + final String[] ruleDetails = rulesLine.split(",", 1); + final String ruleName = ruleDetails[0]; + final String ruleNameEscaped = ruleName.replace(".", "_"); + ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "RulesManager", "Evaluating /rules/" + ruleNameEscaped + ".java"); + final String pathWithoutExtension = "/rules/" + ruleNameEscaped; + final String scriptFile = pathWithoutExtension + ".java"; + final InputStream resourcePath = StorageUtils.getResourceStream(scriptFile); + if (resourcePath == null) { + System.err.println(new FileNotFoundException("/rules/" + ruleName + ".java not found!")); + } else { + Rule r = null; + if (useCache) { + try { + ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_DEBUG_MIN, "RulesManager", ruleName, "Trying to load cached rule"); + r = loadClassRuleFromSourceFile(scriptFile, tDir); + if (r != null) { + ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_DEBUG_MIN, "RulesManager", ruleName, "Loaded cached rule"); + } + } catch (final Exception e) { + e.printStackTrace(); + ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "RulesManager", ruleName, "Can't load the rule!"); + } + } + if (r == null || !useCache) { + ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_DEBUG_MIN, "RulesManager", ruleName, "This rule is not cached. Compiling"); + try { + r = compileJavaRule(scriptFile, tDir); + compiledSomething = true; + } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | IOException e) { + e.printStackTrace(); + } + + } + if (r != null) { + RulesManager.addRule(r); + } } } } } ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "RulesManager", "Loaded all the rules successfully"); - if (compiledSomething) { - if (cacheFilePath.toFile().exists()) { - cacheFilePath.toFile().delete(); + if (!PlatformUtils.isJavascript && compiledSomething) { + if (cacheFileExists || cacheFilePath.exists()) { + cacheFilePath.delete(); } ZipUtils.zip(tDir.toString(), cacheFilePath.toString(), ""); ConsoleUtils.out.println(ConsoleUtils.OUTPUTLEVEL_NODEBUG, "RulesManager", "Cached the compiled rules"); } + if (cacheFileStream != null) { + cacheFileStream.close(); + } } catch (URISyntaxException | IOException e) { e.printStackTrace(); DSystem.exit(1); } } - public static Rule compileJavaRule(String scriptFile, Path tDir) throws IOException, URISyntaxException, + public static Rule compileJavaRule(String scriptFile, File tDir) throws IOException, URISyntaxException, InstantiationException, IllegalAccessException, ClassNotFoundException { - final InputStream resource = Utils.getResourceStream(scriptFile); - final String text = Utils.read(resource); - final String[] textArray = text.split("\\n", 5); - final String javaClassDeclaration = textArray[2].substring(6); - int extIndex = javaClassDeclaration.lastIndexOf('.'); - final String javaClassNameOnly = javaClassDeclaration.substring(extIndex + 1, javaClassDeclaration.length()); - final String javaClassNameAndPath = new StringBuilder("org.warp.picalculator.math.rules.").append(javaClassDeclaration).toString(); - extIndex = javaClassNameAndPath.lastIndexOf('.'); - final String javaCode = new StringBuilder("package ").append(javaClassNameAndPath.substring(0, extIndex >= 0 ? extIndex : javaClassNameAndPath.length())).append(";\n").append(textArray[4]).toString(); - final Path tDirPath = tDir.resolve(javaClassNameAndPath.replace('.', File.separatorChar)).getParent(); - final Path tFileJava = tDirPath.resolve(javaClassNameOnly + ".java"); - final Path tFileClass = tDirPath.resolve(javaClassNameOnly + ".class"); - if (!tDirPath.toFile().exists()) { - Files.createDirectories(tDirPath); - } - if (tFileJava.toFile().exists()) { - tFileJava.toFile().delete(); - } - Files.write(tFileJava, javaCode.getBytes("UTF-8"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); - final boolean compiled = DJDTCompiler.compile(new String[] { "-nowarn", "-1.8", tFileJava.toString() }, new PrintWriter(System.out), new PrintWriter(System.err)); - if (Utils.debugCache) { - tFileJava.toFile().deleteOnExit(); + final InputStream resource = StorageUtils.getResourceStream(scriptFile); + final String text = StorageUtils.read(resource); + final String[] textArray = text.split("\\n", 6); + if (textArray[3].contains("PATH=")) { + final String javaClassDeclaration = textArray[3].substring(6); + int extIndex = javaClassDeclaration.lastIndexOf('.'); + final String javaClassNameOnly = javaClassDeclaration.substring(extIndex + 1, javaClassDeclaration.length()); + final String javaClassNameAndPath = new StringBuilder("org.warp.picalculator.math.rules.").append(javaClassDeclaration).toString(); + extIndex = javaClassNameAndPath.lastIndexOf('.'); + final String javaCode = new StringBuilder("package ").append(javaClassNameAndPath.substring(0, extIndex >= 0 ? extIndex : javaClassNameAndPath.length())).append(";\n").append(textArray[5]).toString(); + final File tDirPath = StorageUtils.getParent(StorageUtils.resolve(tDir, javaClassNameAndPath.replace('.', File.separatorChar))); + final File tFileJava = StorageUtils.resolve(tDirPath, javaClassNameOnly + ".java"); + final File tFileClass = StorageUtils.resolve(tDirPath, javaClassNameOnly + ".class"); + if (!tDirPath.exists()) { + StorageUtils.createDirectories(tDirPath); + } + if (tFileJava.exists()) { + tFileJava.delete(); + } + StorageUtils.write(tFileJava, javaCode.getBytes("UTF-8"), DStandardOpenOption.WRITE, DStandardOpenOption.CREATE); + final boolean compiled = DJDTCompiler.compile(new String[] { "-nowarn", "-1.8", tFileJava.toString() }, new PrintWriter(System.out), new PrintWriter(System.err)); + if (Utils.debugCache) { + tFileJava.deleteOnExit(); + } else { + tFileJava.delete(); + } + if (compiled) { + tFileClass.deleteOnExit(); + return loadClassRuleDirectly(javaClassNameAndPath, tDir); + } else { + throw new IOException("Can't build script file '" + scriptFile + "'"); + } } else { - tFileJava.toFile().delete(); - } - if (compiled) { - tFileClass.toFile().deleteOnExit(); - return loadClassRuleDirectly(javaClassNameAndPath, tDir); - } else { - throw new IOException("Can't build script file '" + scriptFile + "'"); + throw new IOException("Can't build script file '" + scriptFile + "', the header is missing or wrong."); } } - public static Rule loadClassRuleFromSourceFile(String scriptFile, Path tDir) throws IOException, URISyntaxException, + public static Rule loadClassRuleFromSourceFile(String scriptFile, File tDir) throws IOException, URISyntaxException, InstantiationException, IllegalAccessException, ClassNotFoundException { - final InputStream resource = Utils.getResourceStream(scriptFile); - final String text = Utils.read(resource); - final String[] textArray = text.split("\\n", 5); - final String javaClassName = textArray[2].substring(6); - final String javaClassNameAndPath = new StringBuilder("org.warp.picalculator.math.rules.").append(javaClassName).toString(); - try { - return loadClassRuleDirectly(javaClassNameAndPath, tDir); - } catch (final Exception ex) { - ex.printStackTrace(); - return null; + final InputStream resource = StorageUtils.getResourceStream(scriptFile); + final String text = StorageUtils.read(resource); + final String[] textArray = text.split("\\n", 6); + if (textArray[3].contains("PATH=")) { + final String javaClassName = textArray[3].substring(6); + System.err.println(javaClassName); + final String javaClassNameAndPath = new StringBuilder("org.warp.picalculator.math.rules.").append(javaClassName).toString(); + try { + return loadClassRuleDirectly(javaClassNameAndPath, tDir); + } catch (final Exception ex) { + ex.printStackTrace(); + return null; + } + } else { + throw new IOException("Can't load script file '" + scriptFile + "', the header is missing or wrong."); } } - public static Rule loadClassRuleDirectly(String javaClassNameAndPath, Path tDir) throws IOException, + public static Rule loadClassRuleDirectly(String javaClassNameAndPath, File tDir) throws IOException, URISyntaxException, InstantiationException, IllegalAccessException, ClassNotFoundException { - final URLClassLoader cl = new URLClassLoader(new URL[] { tDir.toUri().toURL() }); + final DURLClassLoader cl = new DURLClassLoader(new URL[] { tDir.toURI().toURL() }); final Class aClass = cl.loadClass(javaClassNameAndPath); cl.close(); return (Rule) aClass.newInstance(); diff --git a/src/main/resources/rules/ExpandRule1.java b/src/main/rules/rules/ExpandRule1.java similarity index 96% rename from src/main/resources/rules/ExpandRule1.java rename to src/main/rules/rules/ExpandRule1.java index 1dad8003..31f9940f 100644 --- a/src/main/resources/rules/ExpandRule1.java +++ b/src/main/rules/rules/ExpandRule1.java @@ -1,8 +1,11 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExpandRule1 */ + + import org.warp.picalculator.math.Function; import org.warp.picalculator.math.FunctionOperator; import org.warp.picalculator.math.MathContext; diff --git a/src/main/resources/rules/ExpandRule2.java b/src/main/rules/rules/ExpandRule2.java similarity index 95% rename from src/main/resources/rules/ExpandRule2.java rename to src/main/rules/rules/ExpandRule2.java index 1a9d782b..16255bdf 100644 --- a/src/main/resources/rules/ExpandRule2.java +++ b/src/main/rules/rules/ExpandRule2.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExpandRule2 diff --git a/src/main/resources/rules/ExpandRule5.java b/src/main/rules/rules/ExpandRule5.java similarity index 96% rename from src/main/resources/rules/ExpandRule5.java rename to src/main/rules/rules/ExpandRule5.java index d77c966b..cfed4b72 100644 --- a/src/main/resources/rules/ExpandRule5.java +++ b/src/main/rules/rules/ExpandRule5.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExpandRule5 diff --git a/src/main/resources/rules/ExponentRule1.java b/src/main/rules/rules/ExponentRule1.java similarity index 94% rename from src/main/resources/rules/ExponentRule1.java rename to src/main/rules/rules/ExponentRule1.java index 2c39f5eb..867e6274 100644 --- a/src/main/resources/rules/ExponentRule1.java +++ b/src/main/rules/rules/ExponentRule1.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExponentRule1 diff --git a/src/main/resources/rules/ExponentRule15.java b/src/main/rules/rules/ExponentRule15.java similarity index 95% rename from src/main/resources/rules/ExponentRule15.java rename to src/main/rules/rules/ExponentRule15.java index 62565e79..4f0519d4 100644 --- a/src/main/resources/rules/ExponentRule15.java +++ b/src/main/rules/rules/ExponentRule15.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExponentRule15 diff --git a/src/main/resources/rules/ExponentRule16.java b/src/main/rules/rules/ExponentRule16.java similarity index 96% rename from src/main/resources/rules/ExponentRule16.java rename to src/main/rules/rules/ExponentRule16.java index cd33e526..bc1c7789 100644 --- a/src/main/resources/rules/ExponentRule16.java +++ b/src/main/rules/rules/ExponentRule16.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExponentRule16 diff --git a/src/main/resources/rules/ExponentRule17.java b/src/main/rules/rules/ExponentRule17.java similarity index 95% rename from src/main/resources/rules/ExponentRule17.java rename to src/main/rules/rules/ExponentRule17.java index 3f14f070..587208cc 100644 --- a/src/main/resources/rules/ExponentRule17.java +++ b/src/main/rules/rules/ExponentRule17.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExponentRule17 diff --git a/src/main/resources/rules/ExponentRule2.java b/src/main/rules/rules/ExponentRule2.java similarity index 94% rename from src/main/resources/rules/ExponentRule2.java rename to src/main/rules/rules/ExponentRule2.java index 72361771..efe69119 100644 --- a/src/main/resources/rules/ExponentRule2.java +++ b/src/main/rules/rules/ExponentRule2.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExponentRule2 diff --git a/src/main/resources/rules/ExponentRule3.java b/src/main/rules/rules/ExponentRule3.java similarity index 94% rename from src/main/resources/rules/ExponentRule3.java rename to src/main/rules/rules/ExponentRule3.java index ed2b1ec9..9c3c7b4e 100644 --- a/src/main/resources/rules/ExponentRule3.java +++ b/src/main/rules/rules/ExponentRule3.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExponentRule3 diff --git a/src/main/resources/rules/ExponentRule4.java b/src/main/rules/rules/ExponentRule4.java similarity index 95% rename from src/main/resources/rules/ExponentRule4.java rename to src/main/rules/rules/ExponentRule4.java index 39616d19..d1e96c33 100644 --- a/src/main/resources/rules/ExponentRule4.java +++ b/src/main/rules/rules/ExponentRule4.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExponentRule4 diff --git a/src/main/resources/rules/ExponentRule8.java b/src/main/rules/rules/ExponentRule8.java similarity index 95% rename from src/main/resources/rules/ExponentRule8.java rename to src/main/rules/rules/ExponentRule8.java index 84c044f4..7a137743 100644 --- a/src/main/resources/rules/ExponentRule8.java +++ b/src/main/rules/rules/ExponentRule8.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExponentRule8 diff --git a/src/main/resources/rules/ExponentRule9.java b/src/main/rules/rules/ExponentRule9.java similarity index 95% rename from src/main/resources/rules/ExponentRule9.java rename to src/main/rules/rules/ExponentRule9.java index 525aa7e3..0e83bbf9 100644 --- a/src/main/resources/rules/ExponentRule9.java +++ b/src/main/rules/rules/ExponentRule9.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=ExponentRule9 diff --git a/src/main/resources/rules/FractionsRule1.java b/src/main/rules/rules/FractionsRule1.java similarity index 95% rename from src/main/resources/rules/FractionsRule1.java rename to src/main/rules/rules/FractionsRule1.java index dad56511..dd04a574 100644 --- a/src/main/resources/rules/FractionsRule1.java +++ b/src/main/rules/rules/FractionsRule1.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule1 diff --git a/src/main/resources/rules/FractionsRule10.java b/src/main/rules/rules/FractionsRule10.java similarity index 95% rename from src/main/resources/rules/FractionsRule10.java rename to src/main/rules/rules/FractionsRule10.java index f366a966..97f5d0a5 100644 --- a/src/main/resources/rules/FractionsRule10.java +++ b/src/main/rules/rules/FractionsRule10.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule10 diff --git a/src/main/resources/rules/FractionsRule11.java b/src/main/rules/rules/FractionsRule11.java similarity index 95% rename from src/main/resources/rules/FractionsRule11.java rename to src/main/rules/rules/FractionsRule11.java index 16570f34..e344248a 100644 --- a/src/main/resources/rules/FractionsRule11.java +++ b/src/main/rules/rules/FractionsRule11.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule11 diff --git a/src/main/resources/rules/FractionsRule12.java b/src/main/rules/rules/FractionsRule12.java similarity index 95% rename from src/main/resources/rules/FractionsRule12.java rename to src/main/rules/rules/FractionsRule12.java index 340021ba..af9397fe 100644 --- a/src/main/resources/rules/FractionsRule12.java +++ b/src/main/rules/rules/FractionsRule12.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule12 diff --git a/src/main/resources/rules/FractionsRule14.java b/src/main/rules/rules/FractionsRule14.java similarity index 96% rename from src/main/resources/rules/FractionsRule14.java rename to src/main/rules/rules/FractionsRule14.java index 15ca607a..8c45825c 100644 --- a/src/main/resources/rules/FractionsRule14.java +++ b/src/main/rules/rules/FractionsRule14.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule14 diff --git a/src/main/resources/rules/FractionsRule2.java b/src/main/rules/rules/FractionsRule2.java similarity index 94% rename from src/main/resources/rules/FractionsRule2.java rename to src/main/rules/rules/FractionsRule2.java index ac2494dc..ec097730 100644 --- a/src/main/resources/rules/FractionsRule2.java +++ b/src/main/rules/rules/FractionsRule2.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule2 diff --git a/src/main/resources/rules/FractionsRule3.java b/src/main/rules/rules/FractionsRule3.java similarity index 94% rename from src/main/resources/rules/FractionsRule3.java rename to src/main/rules/rules/FractionsRule3.java index cdb0d870..29460e90 100644 --- a/src/main/resources/rules/FractionsRule3.java +++ b/src/main/rules/rules/FractionsRule3.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule3 diff --git a/src/main/resources/rules/FractionsRule4.java b/src/main/rules/rules/FractionsRule4.java similarity index 95% rename from src/main/resources/rules/FractionsRule4.java rename to src/main/rules/rules/FractionsRule4.java index f0bee9a5..3a970b0e 100644 --- a/src/main/resources/rules/FractionsRule4.java +++ b/src/main/rules/rules/FractionsRule4.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule4 diff --git a/src/main/resources/rules/FractionsRule5.java b/src/main/rules/rules/FractionsRule5.java similarity index 96% rename from src/main/resources/rules/FractionsRule5.java rename to src/main/rules/rules/FractionsRule5.java index 8c3980e8..9f59449f 100644 --- a/src/main/resources/rules/FractionsRule5.java +++ b/src/main/rules/rules/FractionsRule5.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule5 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import java.math.BigDecimal; import org.warp.picalculator.Error; @@ -67,7 +67,7 @@ public class FractionsRule5 implements Rule { } } } - + if (isExecutable) { MathContext root = f.getMathContext(); ObjectArrayList result = new ObjectArrayList<>(); diff --git a/src/main/resources/rules/FractionsRule6.java b/src/main/rules/rules/FractionsRule6.java similarity index 95% rename from src/main/resources/rules/FractionsRule6.java rename to src/main/rules/rules/FractionsRule6.java index 488ccada..c7621d08 100644 --- a/src/main/resources/rules/FractionsRule6.java +++ b/src/main/rules/rules/FractionsRule6.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule6 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.MathContext; @@ -62,7 +62,7 @@ public class FractionsRule6 implements Rule { } } } - + if (isExecutable) { MathContext root = f.getMathContext(); ObjectArrayList result = new ObjectArrayList<>(); diff --git a/src/main/resources/rules/FractionsRule7.java b/src/main/rules/rules/FractionsRule7.java similarity index 84% rename from src/main/resources/rules/FractionsRule7.java rename to src/main/rules/rules/FractionsRule7.java index 69d0bf8c..253f60e2 100644 --- a/src/main/resources/rules/FractionsRule7.java +++ b/src/main/rules/rules/FractionsRule7.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule7 @@ -60,18 +61,18 @@ public class FractionsRule7 implements Rule { Number numb = (Number) pow.getParameter2(); if (numb.getTerm().compareTo(BigDecimal.ZERO) < 0) { ObjectArrayList result = new ObjectArrayList<>(); - Function a = new Division(root, new Number(root, 1), new Power(root, ((Power) f).getParameter1(), ((Number)((Power)f).getParameter2()).multiply(new Number(root, -1)))); + Function a = new Division(root, new Number(root, 1), new Power(root, ((Power) f).getParameter1(), ((Number) ((Power) f).getParameter2()).multiply(new Number(root, -1)))); result.add(a); return result; } - } else if (pow.getParameter2() instanceof Multiplication && ((Multiplication)pow.getParameter2()).getParameter1().equals(new Number(root, -1))) { + } else if (pow.getParameter2() instanceof Multiplication && ((Multiplication) pow.getParameter2()).getParameter1().equals(new Number(root, -1))) { ObjectArrayList result = new ObjectArrayList<>(); - Function a = new Division(root, new Number(root, 1), new Power(root, ((Power) f).getParameter1(), ((Multiplication)((Power)f).getParameter2()).getParameter2())); + Function a = new Division(root, new Number(root, 1), new Power(root, ((Power) f).getParameter1(), ((Multiplication) ((Power) f).getParameter2()).getParameter2())); result.add(a); return result; } } - + return null; } } diff --git a/src/main/resources/rules/FractionsRule8.java b/src/main/rules/rules/FractionsRule8.java similarity index 84% rename from src/main/resources/rules/FractionsRule8.java rename to src/main/rules/rules/FractionsRule8.java index 932e2fa1..e1c406a5 100644 --- a/src/main/resources/rules/FractionsRule8.java +++ b/src/main/rules/rules/FractionsRule8.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule8 @@ -55,15 +56,15 @@ public class FractionsRule8 implements Rule { if (f instanceof Division) { MathContext root = f.getMathContext(); Division div = (Division) f; - if (div.getParameter1() instanceof Multiplication && ((Multiplication)div.getParameter1()).isNegative()) { - if (div.getParameter2() instanceof Multiplication && ((Multiplication)div.getParameter2()).isNegative()) { + if (div.getParameter1() instanceof Multiplication && ((Multiplication) div.getParameter1()).isNegative()) { + if (div.getParameter2() instanceof Multiplication && ((Multiplication) div.getParameter2()).isNegative()) { ObjectArrayList result = new ObjectArrayList<>(); - result.add(new Division(root, ((Multiplication)div.getParameter1()).toPositive(), ((Multiplication)div.getParameter2()).toPositive())); + result.add(new Division(root, ((Multiplication) div.getParameter1()).toPositive(), ((Multiplication) div.getParameter2()).toPositive())); return result; } } } - + return null; } } diff --git a/src/main/resources/rules/FractionsRule9.java b/src/main/rules/rules/FractionsRule9.java similarity index 89% rename from src/main/resources/rules/FractionsRule9.java rename to src/main/rules/rules/FractionsRule9.java index 4bfe2ed5..92ca3d78 100644 --- a/src/main/resources/rules/FractionsRule9.java +++ b/src/main/rules/rules/FractionsRule9.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=FractionsRule9 @@ -55,13 +56,13 @@ public class FractionsRule9 implements Rule { if (f instanceof Division) { MathContext root = f.getMathContext(); Division div = (Division) f; - if (div.getParameter1() instanceof Multiplication && ((Multiplication)div.getParameter1()).isNegative()) { + if (div.getParameter1() instanceof Multiplication && ((Multiplication) div.getParameter1()).isNegative()) { ObjectArrayList result = new ObjectArrayList<>(); - result.add(Multiplication.newNegative(root, new Division(root, ((Multiplication)div.getParameter1()).toPositive(), div.getParameter2()))); + result.add(Multiplication.newNegative(root, new Division(root, ((Multiplication) div.getParameter1()).toPositive(), div.getParameter2()))); return result; } } - + return null; } } diff --git a/src/main/resources/rules/NumberRule1.java b/src/main/rules/rules/NumberRule1.java similarity index 95% rename from src/main/resources/rules/NumberRule1.java rename to src/main/rules/rules/NumberRule1.java index 1e8bb7b8..8587fd19 100644 --- a/src/main/resources/rules/NumberRule1.java +++ b/src/main/rules/rules/NumberRule1.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=NumberRule1 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.MathContext; @@ -66,7 +66,7 @@ public class NumberRule1 implements Rule { } } } - + if (isExecutable) { ObjectArrayList result = new ObjectArrayList<>(); result.add(new Number(f.getMathContext(), 0)); diff --git a/src/main/resources/rules/NumberRule2.java b/src/main/rules/rules/NumberRule2.java similarity index 95% rename from src/main/resources/rules/NumberRule2.java rename to src/main/rules/rules/NumberRule2.java index 6020915f..e5fd03ca 100644 --- a/src/main/resources/rules/NumberRule2.java +++ b/src/main/rules/rules/NumberRule2.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=NumberRule2 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.MathContext; @@ -66,7 +66,7 @@ public class NumberRule2 implements Rule { } } } - + if (isExecutable) { MathContext root = f.getMathContext(); ObjectArrayList result = new ObjectArrayList<>(); @@ -87,7 +87,7 @@ public class NumberRule2 implements Rule { aFound = true; } } - + result.add(a); return result; } else { diff --git a/src/main/resources/rules/NumberRule3.java b/src/main/rules/rules/NumberRule3.java similarity index 96% rename from src/main/resources/rules/NumberRule3.java rename to src/main/rules/rules/NumberRule3.java index 7800db09..351ae78a 100644 --- a/src/main/resources/rules/NumberRule3.java +++ b/src/main/rules/rules/NumberRule3.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=NumberRule3 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.MathContext; @@ -77,7 +77,7 @@ public class NumberRule3 implements Rule { isExecutable = true; } } - + if (isExecutable) { MathContext root = f.getMathContext(); ObjectArrayList result = new ObjectArrayList<>(); diff --git a/src/main/resources/rules/NumberRule4.java b/src/main/rules/rules/NumberRule4.java similarity index 95% rename from src/main/resources/rules/NumberRule4.java rename to src/main/rules/rules/NumberRule4.java index ad722b28..ec034115 100644 --- a/src/main/resources/rules/NumberRule4.java +++ b/src/main/rules/rules/NumberRule4.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=NumberRule4 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.MathContext; @@ -54,7 +54,7 @@ public class NumberRule4 implements Rule { if (f instanceof SumSubtraction) { isExecutable = true; } - + if (isExecutable) { MathContext root = f.getMathContext(); ObjectArrayList result = new ObjectArrayList<>(); diff --git a/src/main/resources/rules/NumberRule5.java b/src/main/rules/rules/NumberRule5.java similarity index 95% rename from src/main/resources/rules/NumberRule5.java rename to src/main/rules/rules/NumberRule5.java index 66aaab42..dc2c66cb 100644 --- a/src/main/resources/rules/NumberRule5.java +++ b/src/main/rules/rules/NumberRule5.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=NumberRule5 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.FunctionOperator; @@ -67,7 +67,7 @@ public class NumberRule5 implements Rule { } } } - + if (isExecutable) { MathContext root = f.getMathContext(); ObjectArrayList result = new ObjectArrayList<>(); diff --git a/src/main/resources/rules/NumberRule7.java b/src/main/rules/rules/NumberRule7.java similarity index 95% rename from src/main/resources/rules/NumberRule7.java rename to src/main/rules/rules/NumberRule7.java index 74b2be67..21242a38 100644 --- a/src/main/resources/rules/NumberRule7.java +++ b/src/main/rules/rules/NumberRule7.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=NumberRule7 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.MathContext; @@ -54,7 +54,7 @@ public class NumberRule7 implements Rule { if (f instanceof Sum) { isExecutable = ((FunctionOperator) f).getParameter1().equals(((FunctionOperator) f).getParameter2()); } - + if (isExecutable) { MathContext root = f.getMathContext(); ObjectArrayList result = new ObjectArrayList<>(); diff --git a/src/main/resources/rules/UndefinedRule1.java b/src/main/rules/rules/UndefinedRule1.java similarity index 95% rename from src/main/resources/rules/UndefinedRule1.java rename to src/main/rules/rules/UndefinedRule1.java index 29f27e52..7c681e3c 100644 --- a/src/main/resources/rules/UndefinedRule1.java +++ b/src/main/rules/rules/UndefinedRule1.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=UndefinedRule1 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.MathContext; @@ -58,7 +58,7 @@ public class UndefinedRule1 implements Rule { isExecutable = true; } } - + if (isExecutable) { MathContext root = f.getMathContext(); ObjectArrayList result = new ObjectArrayList<>(); diff --git a/src/main/resources/rules/UndefinedRule2.java b/src/main/rules/rules/UndefinedRule2.java similarity index 95% rename from src/main/resources/rules/UndefinedRule2.java rename to src/main/rules/rules/UndefinedRule2.java index 5e61c2e9..1587c857 100644 --- a/src/main/resources/rules/UndefinedRule2.java +++ b/src/main/rules/rules/UndefinedRule2.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=UndefinedRule2 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.MathContext; @@ -61,7 +61,7 @@ public class UndefinedRule2 implements Rule { } } } - + if (isExecutable) { MathContext root = f.getMathContext(); ObjectArrayList result = new ObjectArrayList<>(); diff --git a/src/main/resources/rules/VariableRule1.java b/src/main/rules/rules/VariableRule1.java similarity index 96% rename from src/main/resources/rules/VariableRule1.java rename to src/main/rules/rules/VariableRule1.java index 2729978e..649ce89a 100644 --- a/src/main/resources/rules/VariableRule1.java +++ b/src/main/rules/rules/VariableRule1.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=VariableRule1 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.FunctionOperator; @@ -62,7 +62,7 @@ public class VariableRule1 implements Rule { } } } - + if (isExecutable) { FunctionOperator fnc = (FunctionOperator) f; MathContext root = fnc.getMathContext(); @@ -81,7 +81,7 @@ public class VariableRule1 implements Rule { a = m1.getParameter2(); b = m2.getParameter2(); } - + Function rets; if (fnc instanceof Sum) { rets = new Sum(root, a, b); diff --git a/src/main/resources/rules/VariableRule2.java b/src/main/rules/rules/VariableRule2.java similarity index 95% rename from src/main/resources/rules/VariableRule2.java rename to src/main/rules/rules/VariableRule2.java index 838b0b3f..49c9c40e 100644 --- a/src/main/resources/rules/VariableRule2.java +++ b/src/main/rules/rules/VariableRule2.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=VariableRule2 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.FunctionOperator; @@ -62,7 +62,7 @@ public class VariableRule2 implements Rule { } } } - + if (isExecutable) { FunctionOperator fnc = (FunctionOperator) f; MathContext root = fnc.getMathContext(); @@ -70,7 +70,7 @@ public class VariableRule2 implements Rule { FunctionOperator m1 = (FunctionOperator) fnc.getParameter1(); Function a = m1.getParameter1(); Function x = fnc.getParameter2(); - + Function rets; if (fnc instanceof Sum) { rets = new Sum(root, a, new Number(root, 1)); diff --git a/src/main/resources/rules/VariableRule3.java b/src/main/rules/rules/VariableRule3.java similarity index 95% rename from src/main/resources/rules/VariableRule3.java rename to src/main/rules/rules/VariableRule3.java index 60557558..e2baa76f 100644 --- a/src/main/resources/rules/VariableRule3.java +++ b/src/main/rules/rules/VariableRule3.java @@ -1,3 +1,4 @@ +package rules; /* SETTINGS: (please don't move this part) PATH=VariableRule3 @@ -10,7 +11,6 @@ import org.warp.picalculator.math.FunctionSingle; import org.warp.picalculator.math.MathContext; //Imports - import org.warp.picalculator.Error; import org.warp.picalculator.math.Function; import org.warp.picalculator.math.FunctionOperator; @@ -62,7 +62,7 @@ public class VariableRule3 implements Rule { } } } - + if (isExecutable) { FunctionOperator fnc = (FunctionOperator) f; MathContext root = fnc.getMathContext(); @@ -70,14 +70,14 @@ public class VariableRule3 implements Rule { FunctionOperator m2 = (FunctionOperator) fnc.getParameter2(); Function a = m2.getParameter1(); Function x = fnc.getParameter1(); - + Function rets; if (fnc instanceof Sum) { rets = new Sum(root, new Number(root, 1), a); } else { rets = new Subtraction(root, new Number(root, 1), a); } - + Function retm = new Multiplication(root, rets, x); result.add(retm); return result; diff --git a/src/main/resources/rules/functions/DivisionRule.java b/src/main/rules/rules/functions/DivisionRule.java similarity index 82% rename from src/main/resources/rules/functions/DivisionRule.java rename to src/main/rules/rules/functions/DivisionRule.java index 421e8845..1c6bbc9b 100644 --- a/src/main/resources/rules/functions/DivisionRule.java +++ b/src/main/rules/rules/functions/DivisionRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.DivisionRule @@ -60,19 +61,19 @@ public class DivisionRule implements Rule { MathContext mathContext = f.getMathContext(); if (variable1 instanceof Number && variable2 instanceof Number) { if (mathContext.exactMode) { - if (((Number)variable1).isInteger() && ((Number)variable2).isInteger()) { + if (((Number) variable1).isInteger() && ((Number) variable2).isInteger()) { LinkedList factors1, factors2, mcm; try { - factors1 = ((Number)variable1).getFactors(); - factors2 = ((Number)variable2).getFactors(); + factors1 = ((Number) variable1).getFactors(); + factors2 = ((Number) variable2).getFactors(); mcm = ScriptUtils.mcm(factors1, factors2); } catch (Exception ex) { return null; } if (mcm.size() > 0) { //true if there is at least one common factor //divide by the common factor (ab/cb = a/c) - BigInteger nmb1 = ((Number)variable1).getTerm().toBigIntegerExact(); - BigInteger nmb2 = ((Number)variable2).getTerm().toBigIntegerExact(); + BigInteger nmb1 = ((Number) variable1).getTerm().toBigIntegerExact(); + BigInteger nmb2 = ((Number) variable2).getTerm().toBigIntegerExact(); for (BigInteger integerNumber : mcm) { nmb1 = nmb1.divide(integerNumber); nmb2 = nmb2.divide(integerNumber); @@ -83,7 +84,7 @@ public class DivisionRule implements Rule { } } else { //divide a by b (a/b = c) - result.add(((Number)variable1).divide((Number)variable2)); + result.add(((Number) variable1).divide((Number) variable2)); return result; } } diff --git a/src/main/resources/rules/functions/EmptyNumberRule.java b/src/main/rules/rules/functions/EmptyNumberRule.java similarity index 93% rename from src/main/resources/rules/functions/EmptyNumberRule.java rename to src/main/rules/rules/functions/EmptyNumberRule.java index cc16c080..6afee130 100644 --- a/src/main/resources/rules/functions/EmptyNumberRule.java +++ b/src/main/rules/rules/functions/EmptyNumberRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.EmptyNumberRule diff --git a/src/main/resources/rules/functions/ExpressionRule.java b/src/main/rules/rules/functions/ExpressionRule.java similarity index 94% rename from src/main/resources/rules/functions/ExpressionRule.java rename to src/main/rules/rules/functions/ExpressionRule.java index 77e4e23a..388b5507 100644 --- a/src/main/resources/rules/functions/ExpressionRule.java +++ b/src/main/rules/rules/functions/ExpressionRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.ExpressionRule diff --git a/src/main/resources/rules/functions/JokeRule.java b/src/main/rules/rules/functions/JokeRule.java similarity index 93% rename from src/main/resources/rules/functions/JokeRule.java rename to src/main/rules/rules/functions/JokeRule.java index 8fd724a1..ca26d8b5 100644 --- a/src/main/resources/rules/functions/JokeRule.java +++ b/src/main/rules/rules/functions/JokeRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.JokeRule diff --git a/src/main/resources/rules/functions/MultiplicationRule.java b/src/main/rules/rules/functions/MultiplicationRule.java similarity index 92% rename from src/main/resources/rules/functions/MultiplicationRule.java rename to src/main/rules/rules/functions/MultiplicationRule.java index 5c584a88..a6e3b881 100644 --- a/src/main/resources/rules/functions/MultiplicationRule.java +++ b/src/main/rules/rules/functions/MultiplicationRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.MultiplicationRule @@ -55,7 +56,7 @@ public class MultiplicationRule implements Rule { MathContext mathContext = f.getMathContext(); if (variable1 instanceof Number && variable2 instanceof Number) { //multiply a by b (a*b = c) - result.add(((Number)variable1).multiply((Number)variable2)); + result.add(((Number) variable1).multiply((Number) variable2)); return result; } } diff --git a/src/main/resources/rules/functions/NegativeRule.java b/src/main/rules/rules/functions/NegativeRule.java similarity index 89% rename from src/main/resources/rules/functions/NegativeRule.java rename to src/main/rules/rules/functions/NegativeRule.java index a0276af2..49458c65 100644 --- a/src/main/resources/rules/functions/NegativeRule.java +++ b/src/main/rules/rules/functions/NegativeRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.NegativeRule @@ -51,12 +52,12 @@ public class NegativeRule implements Rule { public ObjectArrayList execute(Function f) throws Error { if (f instanceof Negative) { ObjectArrayList result = new ObjectArrayList<>(); - Function variable = ((Negative)f).getParameter(); + Function variable = ((Negative) f).getParameter(); MathContext mathContext = f.getMathContext(); if (variable instanceof Number) { //-a = a*-1 = b try { - result.add(((Number)variable).multiply(new Number(mathContext, -1))); + result.add(((Number) variable).multiply(new Number(mathContext, -1))); } catch (Exception ex) { if (ex instanceof NullPointerException) { throw new Error(Errors.ERROR); diff --git a/src/main/resources/rules/functions/NumberRule.java b/src/main/rules/rules/functions/NumberRule.java similarity index 84% rename from src/main/resources/rules/functions/NumberRule.java rename to src/main/rules/rules/functions/NumberRule.java index 2f262ca7..f6fc8cf4 100644 --- a/src/main/resources/rules/functions/NumberRule.java +++ b/src/main/rules/rules/functions/NumberRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.NumberRule @@ -49,9 +50,9 @@ public class NumberRule implements Rule { ObjectArrayList result = new ObjectArrayList<>(); MathContext mathContext = f.getMathContext(); if (mathContext.exactMode) { - if (((Number)f).isInteger() == false) { - Number divisor = new Number(mathContext, BigInteger.TEN.pow(((Number)f).getNumberOfDecimalPlaces())); - Function number = new Number(mathContext, ((Number)f).getTerm().multiply(divisor.getTerm())); + if (((Number) f).isInteger() == false) { + Number divisor = new Number(mathContext, BigInteger.TEN.pow(((Number) f).getNumberOfDecimalPlaces())); + Function number = new Number(mathContext, ((Number) f).getTerm().multiply(divisor.getTerm())); Function div = new Division(mathContext, number, divisor); result.add(div); return result; diff --git a/src/main/resources/rules/functions/PowerRule.java b/src/main/rules/rules/functions/PowerRule.java similarity index 86% rename from src/main/resources/rules/functions/PowerRule.java rename to src/main/rules/rules/functions/PowerRule.java index 04d1e1bd..03b9a10f 100644 --- a/src/main/resources/rules/functions/PowerRule.java +++ b/src/main/rules/rules/functions/PowerRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.PowerRule @@ -51,12 +52,12 @@ public class PowerRule implements Rule { public ObjectArrayList execute(Function f) throws org.warp.picalculator.Error, InterruptedException { if (f instanceof Power) { ObjectArrayList result = new ObjectArrayList<>(); - Function variable1 = ((FunctionOperator)f).getParameter1(); - Function variable2 = ((FunctionOperator)f).getParameter2(); + Function variable1 = ((FunctionOperator) f).getParameter1(); + Function variable2 = ((FunctionOperator) f).getParameter2(); MathContext mathContext = f.getMathContext(); if (variable1 instanceof Number && variable2 instanceof Number) { //a^b = c - Number out = ((Number)variable1).pow((Number)variable2); + Number out = ((Number) variable1).pow((Number) variable2); if (mathContext.exactMode && !out.isInteger()) { return null; } diff --git a/src/main/resources/rules/functions/RootRule.java b/src/main/rules/rules/functions/RootRule.java similarity index 75% rename from src/main/resources/rules/functions/RootRule.java rename to src/main/rules/rules/functions/RootRule.java index 243c817a..2ecc5e1a 100644 --- a/src/main/resources/rules/functions/RootRule.java +++ b/src/main/rules/rules/functions/RootRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.RootRule @@ -59,20 +60,20 @@ public class RootRule implements Rule { if ((isSquare = f instanceof RootSquare) || f instanceof Root) { ObjectArrayList result = new ObjectArrayList<>(); MathContext mathContext = f.getMathContext(); - Function variable1 = ((FunctionOperator)f).getParameter1(); - Function variable2 = ((FunctionOperator)f).getParameter2(); + Function variable1 = ((FunctionOperator) f).getParameter1(); + Function variable2 = ((FunctionOperator) f).getParameter2(); boolean isSolvable = false, canBePorted = false; if (variable1 instanceof Number && variable2 instanceof Number) { if (mathContext.exactMode) { - result.add(((Number)variable1).pow(new Number(mathContext, BigDecimal.ONE).divide(((Number)variable1)))); + result.add(((Number) variable1).pow(new Number(mathContext, BigDecimal.ONE).divide(((Number) variable1)))); return result; } - isSolvable = isSolvable|!mathContext.exactMode; + isSolvable = isSolvable | !mathContext.exactMode; if (!isSolvable) { try { - Function resultVar = ((Number)variable2).pow(new Number(mathContext, BigDecimal.ONE).divide(((Number)variable1))); - Function originalVariable = ((Number)resultVar).pow(new Number(mathContext, 2)); - if ((originalVariable).equals(((FunctionOperator)f).getParameter2())) { + Function resultVar = ((Number) variable2).pow(new Number(mathContext, BigDecimal.ONE).divide(((Number) variable1))); + Function originalVariable = ((Number) resultVar).pow(new Number(mathContext, 2)); + if ((originalVariable).equals(((FunctionOperator) f).getParameter2())) { isSolvable = true; } } catch (Exception ex) { @@ -83,9 +84,9 @@ public class RootRule implements Rule { if (!isSquare && !isSolvable && variable1 instanceof Number && variable1.equals(new Number(mathContext, 2))) { canBePorted = true; } - + if (isSolvable) { - result.add(((Number)variable2).pow(new Number(mathContext, BigInteger.ONE).divide((Number)variable1))); + result.add(((Number) variable2).pow(new Number(mathContext, BigInteger.ONE).divide((Number) variable1))); return result; } if (canBePorted) { diff --git a/src/main/resources/rules/functions/SubtractionRule.java b/src/main/rules/rules/functions/SubtractionRule.java similarity index 84% rename from src/main/resources/rules/functions/SubtractionRule.java rename to src/main/rules/rules/functions/SubtractionRule.java index 483f332a..04bcc2b5 100644 --- a/src/main/resources/rules/functions/SubtractionRule.java +++ b/src/main/rules/rules/functions/SubtractionRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.SubtractionRule @@ -50,12 +51,12 @@ public class SubtractionRule implements Rule { public ObjectArrayList execute(Function f) { if (f instanceof Subtraction) { ObjectArrayList result = new ObjectArrayList<>(); - Function variable1 = ((FunctionOperator)f).getParameter1(); - Function variable2 = ((FunctionOperator)f).getParameter2(); + Function variable1 = ((FunctionOperator) f).getParameter1(); + Function variable2 = ((FunctionOperator) f).getParameter2(); MathContext mathContext = f.getMathContext(); if (variable1 instanceof Number && variable2 instanceof Number) { //a-b = a+(b*-1) = c - result.add(((Number)variable1).add(((Number)variable2).multiply(new Number(mathContext, -1)))); + result.add(((Number) variable1).add(((Number) variable2).multiply(new Number(mathContext, -1)))); return result; } } diff --git a/src/main/resources/rules/functions/SumRule.java b/src/main/rules/rules/functions/SumRule.java similarity index 85% rename from src/main/resources/rules/functions/SumRule.java rename to src/main/rules/rules/functions/SumRule.java index 6a97c737..8cc54430 100644 --- a/src/main/resources/rules/functions/SumRule.java +++ b/src/main/rules/rules/functions/SumRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.SumRule @@ -50,12 +51,12 @@ public class SumRule implements Rule { public ObjectArrayList execute(Function f) { if (f instanceof Sum) { ObjectArrayList result = new ObjectArrayList<>(); - Function variable1 = ((FunctionOperator)f).getParameter1(); - Function variable2 = ((FunctionOperator)f).getParameter2(); + Function variable1 = ((FunctionOperator) f).getParameter1(); + Function variable2 = ((FunctionOperator) f).getParameter2(); MathContext mathContext = f.getMathContext(); if (variable1 instanceof Number && variable2 instanceof Number) { //a+b = c - result.add(((Number)variable1).add(((Number)variable2))); + result.add(((Number) variable1).add(((Number) variable2))); return result; } } diff --git a/src/main/resources/rules/functions/SumSubtractionRule.java b/src/main/rules/rules/functions/SumSubtractionRule.java similarity index 81% rename from src/main/resources/rules/functions/SumSubtractionRule.java rename to src/main/rules/rules/functions/SumSubtractionRule.java index 7c78a526..2b5a3802 100644 --- a/src/main/resources/rules/functions/SumSubtractionRule.java +++ b/src/main/rules/rules/functions/SumSubtractionRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.SumSubtractionRule @@ -50,13 +51,13 @@ public class SumSubtractionRule implements Rule { public ObjectArrayList execute(Function f) { if (f instanceof SumSubtraction) { ObjectArrayList result = new ObjectArrayList<>(); - Function variable1 = ((FunctionOperator)f).getParameter1(); - Function variable2 = ((FunctionOperator)f).getParameter2(); + Function variable1 = ((FunctionOperator) f).getParameter1(); + Function variable2 = ((FunctionOperator) f).getParameter2(); MathContext mathContext = f.getMathContext(); if (variable1 instanceof Number && variable2 instanceof Number) { //a±b = c, d - result.add(((Number)variable1).add((Number)variable2)); - result.add(((Number)variable1).add(((Number)variable2).multiply(new Number(mathContext, -1)))); + result.add(((Number) variable1).add((Number) variable2)); + result.add(((Number) variable1).add(((Number) variable2).multiply(new Number(mathContext, -1)))); return result; } } diff --git a/src/main/resources/rules/functions/VariableRule.java b/src/main/rules/rules/functions/VariableRule.java similarity index 93% rename from src/main/resources/rules/functions/VariableRule.java rename to src/main/rules/rules/functions/VariableRule.java index 1044be58..9ea3ab10 100644 --- a/src/main/resources/rules/functions/VariableRule.java +++ b/src/main/rules/rules/functions/VariableRule.java @@ -1,3 +1,4 @@ +package rules.functions; /* SETTINGS: (please don't move this part) PATH=functions.VariableRule @@ -54,7 +55,7 @@ public class VariableRule implements Rule { public ObjectArrayList execute(Function f) throws Error { if (f instanceof Variable) { ObjectArrayList result = new ObjectArrayList<>(); - Character variable = ((Variable)f).getChar(); + Character variable = ((Variable) f).getChar(); MathContext mathContext = f.getMathContext(); if (mathContext.exactMode == false) { if (variable.equals(MathematicalSymbols.PI)) {