png-0.17.16/.cargo_vcs_info.json0000644000000001360000000000100117740ustar { "git": { "sha1": "fbf256669ff23594bf4c618b61fde6a52b79e088" }, "path_in_vcs": "" }png-0.17.16/CHANGES.md000064400000000000000000000201421046102023000121550ustar 00000000000000## 0.17.15 ### Added * Add a public API to advance to the next frame in APNG decoder ([#518]) * Add APIs to write ICC and EXIF chunks ([#526]) * Add support for parsing the sBIT chunk ([#524]) * Add support for parsing the bKGD chunk ([#538]) * Add support for parsing the cICP chunk ([#529]) * Add support for parsing mDCv and cLLi chunks ([#528], ([#543])) ### Changed * Improve performance of Paeth filter during decoding ([#512], [#539]) ### Fixed * Avoid an infinite loop when retrying after a fatal error using `StreamingDecoder` ([#520]) * Fixed chunk order in encoded files ([#526]) [#495]: https://github.com/image-rs/image-png/pull/495 [#496]: https://github.com/image-rs/image-png/pull/496 [#512]: https://github.com/image-rs/image-png/pull/512 [#518]: https://github.com/image-rs/image-png/pull/518 [#520]: https://github.com/image-rs/image-png/pull/520 [#524]: https://github.com/image-rs/image-png/pull/524 [#526]: https://github.com/image-rs/image-png/pull/526 [#528]: https://github.com/image-rs/image-png/pull/528 [#529]: https://github.com/image-rs/image-png/pull/529 [#538]: https://github.com/image-rs/image-png/pull/538 [#539]: https://github.com/image-rs/image-png/pull/539 [#543]: https://github.com/image-rs/image-png/pull/543 ## 0.17.14 * Updated to miniz_oxide 0.8.0. * Added public API to consume interlaced rows one by one ([#495]) * Improved support for resuming decoding after an `UnexpectedEof`, which lets you start parsing a file before it's fully received over the network ([#496]) * Fixed some broken links in documentation, improved some documentation comments [#495]: https://github.com/image-rs/image-png/pull/495 [#496]: https://github.com/image-rs/image-png/pull/496 ## 0.17.13 * Fix `Send` bound on `Reader`. ## 0.17.12 * Reject zero-sized frames. * Optimized decoding of paletted images. * Removed remaining uses of miniz_oxide for decoding. * Correct lifetime used for `Info` struct. * Fix build issue with `-Z minimal-versions`. ## 0.17.11 * Ignore subsequent iCCP chunks to match libpng behavior. * Added an option to ignore ancillary chunks with invalid CRC. * Added `new_with_info` constructor for encoder. * Removed hard-coded memory limits. * No longer allow zero sized images. * Added `Reader::finish` to read all the auxiliary chunks that comes after the image. ## 0.17.10 * Added Transformations::ALPHA * Enable encoding pixel dimensions ## 0.17.9 * Fixed a bug in ICC profile decompression. * Improved unfilter performance. ## 0.17.8 * Increased MSRV to 1.57.0. * Substantially optimized encoding and decoding: - Autovectorize filtering and unfiltering. - Make the "fast" compression preset use fdeflate. - Switch decompression to always use fdeflate. - Updated to miniz_oxide 0.7. - Added an option to ignore checksums. * Added corpus-bench example which measures the compression ratio and time to re-encode and subsequently decode a corpus of images. * More fuzz testing. ## 0.17.7 * Fixed handling broken tRNS chunk. * Updated to miniz_oxide 0.6. ## 0.17.6 * Added `Decoder::read_header_info` to query the information contained in the PNG header. * Switched to using the flate2 crate for encoding. ## 0.17.5 * Fixed a regression, introduced by chunk validation, that made the decoder sensitive to the order of `gAMA`, `cHRM`, and `sRGB` chunks. ## 0.17.4 * Added `{Decoder,StreamDecoder}::set_ignore_text_chunk` to disable decoding of ancillary text chunks during the decoding process (chunks decoded by default). * Added duplicate chunk checks. The decoder now enforces that standard chunks such as palette, gamma, … occur at most once as specified. * Added `#[forbid(unsafe_code)]` again. This may come at a minor performance cost when decoding ASCII text for now. * Fixed a bug where decoding of large chunks (>32kB) failed to produce the correct result, or fail the image decoding. As new chunk types are decoded this introduced regressions relative to previous versions. ## 0.17.3 * Fixed a bug where `Writer::finish` would not drop the underlying writer. This would fail to flush and leak memory when using a buffered file writers. * Calling `Writer::finish` will now eagerly flush the underlying writer, returning any error that this operation may result in. * Errors in inflate are now diagnosed with more details. * The color and depth combination is now checked in stream decoder. ## 0.17.2 * Added support for encoding and decoding tEXt/zTXt/iTXt chunks. * Added `Encoder::validate_sequence` to enable validation of the written frame sequence, that is, if the number of written images is consistent with the animation state. * Validation is now off by default. The basis of the new validation had been introduced in 0.17 but this fixes some cases where this validation was too aggressive compared to previous versions. * Added `Writer::finish` to fully check the write of the end of an image instead of silently ignoring potential errors in `Drop`. * The `Writer::write_chunk` method now validates that the computed chunk length does not overflow the limit set by PNG. * Fix an issue where the library would panic or even abort the process when `flush` or `write` of an underlying writer panicked, or in some other uses of `StreamWriter`. ## 0.17.1 * Fix panic in adaptive filter method `sum_buffer` ## 0.17.0 * Increased MSRV to 1.46.0 * Rework output info usage * Implement APNG encoding * Improve ergonomics of encoder set_palette and set_trns methods * Make Info struct non-exhaustive * Make encoder a core feature * Default Transformations to Identity * Add Adaptive filtering method for encoding * Fix SCREAM_CASE on ColorType variants * Forbid unsafe code ## 0.16.7 * Added `Encoder::set_trns` to register a transparency table to be written. ## 0.16.6 * Fixed silent integer overflows in buffer size calculation, resulting in panics from assertions and out-of-bounds accesses when actually decoding. This improves the stability of 32-bit and 16-bit targets and make decoding run as stable as on 64-bit. * Reject invalid color/depth combinations. Some would lead to mismatched output buffer size and panics during decoding. * Add `Clone` impl for `Info` struct. ## 0.16.5 * Decoding of APNG subframes is now officially supported and specified. Note that dispose ops and positioning in the image need to be done by the caller. * Added encoding of indexed data. * Switched to `miniz_oxide` for decompressing image data, with 30%-50% speedup in common cases and up to 200% in special ones. * Fix accepting images only with consecutive IDAT chunks, rules out data loss. ## 0.16.4 * The fdAT frames are no longer inspected when the main image is read. This would previously be the case for non-interlaced images. This would lead to incorrect failure and, e.g. an error of the form `"invalid filter method"`. * Fix always validating the last IDAT-chunks checksum, was sometimes ignored. * Prevent encoding color/bit-depth combinations forbidden by the specification. * The fixes for APNG/fdAT enable further implementation. The _next_ release is expected to officially support APNG. ## 0.16.3 * Fix encoding with filtering methods Up, Avg, Paeth * Optimize decoding throughput by up to +30% ## 0.16.2 * Added method constructing an owned stream encoder. ## 0.16.1 * Addressed files bloating the packed crate ## 0.16.0 * Fix a bug compressing images with deflate * Address use of deprecated error interfaces ## 0.15.3 * Fix panic while trying to encode empty images. Such images are no longer accepted and error when calling `write_header` before any data has been written. The specification does not permit empty images. ## 0.15.2 * Fix `EXPAND` transformation to leave bit depths above 8 unchanged ## 0.15.1 * Fix encoding writing invalid chunks. Images written can be corrected: see https://github.com/image-rs/image/issues/1074 for a recovery. * Fix a panic in bit unpacking with checked arithmetic (e.g. in debug builds) * Added better fuzzer integration * Update `term`, `rand` dev-dependency * Note: The `show` example program requires a newer compiler than 1.34.2 on some targets due to depending on `glium`. This is not considered a breaking bug. ## 0.15 Begin of changelog png-0.17.16/Cargo.lock0000644000001562270000000000100077640ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "approx" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" dependencies = [ "num-traits", ] [[package]] name = "arrayref" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "calloop" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8" dependencies = [ "bitflags 1.3.2", "log", "nix 0.25.1", "slotmap", "thiserror", "vec_map", ] [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cgl" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" dependencies = [ "libc", ] [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", "clap_derive", "clap_lex", "indexmap 1.9.3", "once_cell", "strsim", "termcolor", "textwrap", ] [[package]] name = "clap_derive" version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" dependencies = [ "heck", "proc-macro-error", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "clap_lex" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] [[package]] name = "cmake" version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" dependencies = [ "cc", ] [[package]] name = "cocoa" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", "core-foundation", "core-graphics 0.22.3", "foreign-types 0.3.2", "libc", "objc", ] [[package]] name = "cocoa" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", "core-foundation", "core-graphics 0.23.2", "foreign-types 0.5.0", "libc", "objc", ] [[package]] name = "cocoa-foundation" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ "bitflags 1.3.2", "block", "core-foundation", "core-graphics-types", "libc", "objc", ] [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-graphics-types", "foreign-types 0.3.2", "libc", ] [[package]] name = "core-graphics" version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation", "core-graphics-types", "foreign-types 0.5.0", "libc", ] [[package]] name = "core-graphics-types" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", "core-foundation", "libc", ] [[package]] name = "core-text" version = "20.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" dependencies = [ "core-foundation", "core-graphics 0.23.2", "foreign-types 0.5.0", "libc", ] [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "criterion" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" dependencies = [ "anes", "atty", "cast", "ciborium", "clap", "criterion-plot", "itertools", "lazy_static", "num-traits", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossfont" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb5a3822b594afc99b503cc1859b94686d3c3efdd60507a28587dab80ee1071" dependencies = [ "cocoa 0.25.0", "core-foundation", "core-foundation-sys", "core-graphics 0.23.2", "core-text", "dwrote", "foreign-types 0.5.0", "freetype-rs", "libc", "log", "objc", "once_cell", "pkg-config", "servo-fontconfig", "winapi", ] [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "cty" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ "darling_core", "darling_macro", ] [[package]] name = "darling_core" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", "syn 1.0.109", ] [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", "quote", "syn 1.0.109", ] [[package]] name = "dispatch" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dlib" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ "libloading 0.8.6", ] [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "dwrote" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70182709525a3632b2ba96b6569225467b18ecb4a77f46d255f713a6bebf05fd" dependencies = [ "lazy_static", "libc", "serde", "serde_derive", "winapi", "wio", ] [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "expat-sys" version = "2.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658f19728920138342f68408b7cf7644d90d4784353d8ebc32e7e8663dbe45fa" dependencies = [ "cmake", "pkg-config", ] [[package]] name = "fdeflate" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ "foreign-types-shared 0.1.1", ] [[package]] name = "foreign-types" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", "foreign-types-shared 0.3.1", ] [[package]] name = "foreign-types-macros" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", "syn 2.0.90", ] [[package]] name = "foreign-types-shared" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] name = "freetype-rs" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74eadec9d0a5c28c54bb9882e54787275152a4e36ce206b45d7451384e5bf5fb" dependencies = [ "bitflags 1.3.2", "freetype-sys", "libc", ] [[package]] name = "freetype-sys" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a37d4011c0cc628dfa766fcc195454f4b068d7afdc2adfd28861191d866e731a" dependencies = [ "cmake", "libc", "pkg-config", ] [[package]] name = "getopts" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gl_generator" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" dependencies = [ "khronos_api", "log", "xml-rs", ] [[package]] name = "glium" version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2766728ecb86014b91d3d687614b32d65aacbbdc887f424a7b03cba3ab593bf" dependencies = [ "backtrace", "fnv", "gl_generator", "glutin", "lazy_static", "memoffset", "smallvec", "takeable-option", ] [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "glutin" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444c9ad294fdcaf20ccf6726b78f380b5450275540c9b68ab62f49726ad1c713" dependencies = [ "cgl", "cocoa 0.24.1", "core-foundation", "glutin_egl_sys", "glutin_gles2_sys", "glutin_glx_sys", "glutin_wgl_sys", "libloading 0.7.4", "log", "objc", "once_cell", "osmesa-sys", "parking_lot", "raw-window-handle 0.5.2", "wayland-client", "wayland-egl", "winapi", "winit", ] [[package]] name = "glutin_egl_sys" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68900f84b471f31ea1d1355567eb865a2cf446294f06cef8d653ed7bcf5f013d" dependencies = [ "gl_generator", "winapi", ] [[package]] name = "glutin_gles2_sys" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094e708b730a7c8a1954f4f8a31880af00eb8a1c5b5bf85d28a0a3c6d69103" dependencies = [ "gl_generator", "objc", ] [[package]] name = "glutin_glx_sys" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93d0575865098580c5b3a423188cd959419912ea60b1e48e8b3b526f6d02468" dependencies = [ "gl_generator", "x11-dl", ] [[package]] name = "glutin_wgl_sys" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3da5951a1569dbab865c6f2a863efafff193a93caf05538d193e9e3816d21696" dependencies = [ "gl_generator", ] [[package]] name = "half" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "home" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", ] [[package]] name = "indexmap" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown 0.15.2", ] [[package]] name = "instant" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", "js-sys", "wasm-bindgen", "web-sys", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "khronos_api" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", "winapi", ] [[package]] name = "libloading" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", ] [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ "libc", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] [[package]] name = "memoffset" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ "autocfg", ] [[package]] name = "miniz_oxide" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi", "windows-sys 0.48.0", ] [[package]] name = "ndk" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ "bitflags 1.3.2", "jni-sys", "ndk-sys", "num_enum", "raw-window-handle 0.5.2", "thiserror", ] [[package]] name = "ndk-context" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-glue" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0434fabdd2c15e0aab768ca31d5b7b333717f03cf02037d5a0a3ff3c278ed67f" dependencies = [ "libc", "log", "ndk", "ndk-context", "ndk-macro", "ndk-sys", "once_cell", "parking_lot", ] [[package]] name = "ndk-macro" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", "proc-macro-crate", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "ndk-sys" version = "0.4.1+23.1.7779620" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" dependencies = [ "jni-sys", ] [[package]] name = "nix" version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset", ] [[package]] name = "nix" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg", "bitflags 1.3.2", "cfg-if", "libc", "memoffset", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_enum" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "objc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", ] [[package]] name = "object" version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "os_str_bytes" version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "osmesa-sys" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88cfece6e95d2e717e0872a7f53a8684712ad13822a7979bc760b9c77ec0013b" dependencies = [ "shared_library", ] [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pkg-config" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plotters" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "png" version = "0.17.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67582bd5b65bdff614270e2ea89a1cf15bef71245cc1e5f7ea126977144211d" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "png" version = "0.17.16" dependencies = [ "approx", "bitflags 1.3.2", "byteorder", "clap", "crc32fast", "criterion", "fdeflate", "flate2", "getopts", "glium", "glob", "miniz_oxide", "rand", "term", ] [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro-crate" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", "toml_edit", ] [[package]] name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", "syn 1.0.109", "version_check", ] [[package]] name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", "quote", "version_check", ] [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "raw-window-handle" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" dependencies = [ "cty", ] [[package]] name = "raw-window-handle" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "redox_syscall" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safe_arch" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05" dependencies = [ "bytemuck", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sctk-adwaita" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61270629cc6b4d77ec1907db1033d5c2e1a404c412743621981a871dc9c12339" dependencies = [ "crossfont", "log", "smithay-client-toolkit", "tiny-skia", ] [[package]] name = "serde" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", "syn 2.0.90", ] [[package]] name = "serde_json" version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "servo-fontconfig" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e3e22fe5fd73d04ebf0daa049d3efe3eae55369ce38ab16d07ddd9ac5c217c" dependencies = [ "libc", "servo-fontconfig-sys", ] [[package]] name = "servo-fontconfig-sys" version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e36b879db9892dfa40f95da1c38a835d41634b825fbd8c4c418093d53c24b388" dependencies = [ "expat-sys", "freetype-sys", "pkg-config", ] [[package]] name = "shared_library" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" dependencies = [ "lazy_static", "libc", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "slotmap" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" dependencies = [ "version_check", ] [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay-client-toolkit" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "870427e30b8f2cbe64bf43ec4b86e88fe39b0a84b3f15efd9c9c2d020bc86eb9" dependencies = [ "bitflags 1.3.2", "calloop", "dlib", "lazy_static", "log", "memmap2", "nix 0.24.3", "pkg-config", "wayland-client", "wayland-cursor", "wayland-protocols", ] [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "takeable-option" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36ae8932fcfea38b7d3883ae2ab357b0d57a02caaa18ebb4f5ece08beaec4aa0" [[package]] name = "term" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3bb6001afcea98122260987f8b7b5da969ecad46dbf0b5453702f776b491a41" dependencies = [ "home", "windows-sys 0.52.0", ] [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn 2.0.90", ] [[package]] name = "tiny-skia" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "642680569bb895b16e4b9d181c60be1ed136fa0c9c7f11d004daf053ba89bf82" dependencies = [ "arrayref", "arrayvec", "bytemuck", "cfg-if", "png 0.17.15", "safe_arch", "tiny-skia-path", ] [[package]] name = "tiny-skia-path" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c114d32f0c2ee43d585367cb013dfaba967ab9f62b90d9af0d696e955e70fa6c" dependencies = [ "arrayref", "bytemuck", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.7.0", "toml_datetime", "winnow", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wayland-client" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" dependencies = [ "bitflags 1.3.2", "downcast-rs", "libc", "nix 0.24.3", "scoped-tls", "wayland-commons", "wayland-scanner", "wayland-sys", ] [[package]] name = "wayland-commons" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" dependencies = [ "nix 0.24.3", "once_cell", "smallvec", "wayland-sys", ] [[package]] name = "wayland-cursor" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" dependencies = [ "nix 0.24.3", "wayland-client", "xcursor", ] [[package]] name = "wayland-egl" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402de949f81a012926d821a2d659f930694257e76dd92b6e0042ceb27be4107d" dependencies = [ "wayland-client", "wayland-sys", ] [[package]] name = "wayland-protocols" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" dependencies = [ "bitflags 1.3.2", "wayland-client", "wayland-commons", "wayland-scanner", ] [[package]] name = "wayland-scanner" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ "proc-macro2", "quote", "xml-rs", ] [[package]] name = "wayland-sys" version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" dependencies = [ "dlib", "lazy_static", "pkg-config", ] [[package]] name = "web-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ "windows_aarch64_msvc 0.36.1", "windows_i686_gnu 0.36.1", "windows_i686_msvc 0.36.1", "windows_x86_64_gnu 0.36.1", "windows_x86_64_msvc 0.36.1", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb796d6fbd86b2fd896c9471e6f04d39d750076ebe5680a3958f00f5ab97657c" dependencies = [ "bitflags 1.3.2", "cocoa 0.24.1", "core-foundation", "core-graphics 0.22.3", "dispatch", "instant", "libc", "log", "mio", "ndk", "ndk-glue", "objc", "once_cell", "parking_lot", "percent-encoding", "raw-window-handle 0.4.3", "raw-window-handle 0.5.2", "sctk-adwaita", "smithay-client-toolkit", "wasm-bindgen", "wayland-client", "wayland-protocols", "web-sys", "windows-sys 0.36.1", "x11-dl", ] [[package]] name = "winnow" version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] [[package]] name = "wio" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" dependencies = [ "winapi", ] [[package]] name = "x11-dl" version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" dependencies = [ "libc", "once_cell", "pkg-config", ] [[package]] name = "xcursor" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" [[package]] name = "xml-rs" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea8b391c9a790b496184c29f7f93b9ed5b16abb306c05415b68bcc16e4d06432" [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn 2.0.90", ] png-0.17.16/Cargo.toml0000644000000047770000000000100100110ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" rust-version = "1.57" name = "png" version = "0.17.16" authors = ["The image-rs Developers"] build = false include = [ "/LICENSE-MIT", "/LICENSE-APACHE", "/README.md", "/CHANGES.md", "/src/", "/examples/", "/benches/", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "PNG decoding and encoding library in pure Rust" readme = "README.md" categories = ["multimedia::images"] license = "MIT OR Apache-2.0" repository = "https://github.com/image-rs/image-png" [lib] name = "png" path = "src/lib.rs" [[example]] name = "change-png-info" path = "examples/change-png-info.rs" [[example]] name = "corpus-bench" path = "examples/corpus-bench.rs" [[example]] name = "png-generate" path = "examples/png-generate.rs" [[example]] name = "pngcheck" path = "examples/pngcheck.rs" [[example]] name = "show" path = "examples/show.rs" [[bench]] name = "decoder" path = "benches/decoder.rs" harness = false [[bench]] name = "expand_paletted" path = "benches/expand_paletted.rs" harness = false required-features = ["benchmarks"] [[bench]] name = "unfilter" path = "benches/unfilter.rs" harness = false required-features = ["benchmarks"] [dependencies.bitflags] version = "1.0" [dependencies.crc32fast] version = "1.2.0" [dependencies.fdeflate] version = "0.3.3" [dependencies.flate2] version = "1.0.11" [dependencies.miniz_oxide] version = "0.8" features = ["simd"] [dev-dependencies.approx] version = "0.5.1" [dev-dependencies.byteorder] version = "1.5.0" [dev-dependencies.clap] version = "3.0" features = ["derive"] [dev-dependencies.criterion] version = "0.4.0" [dev-dependencies.getopts] version = "0.2.14" [dev-dependencies.glium] version = "0.32" features = ["glutin"] default-features = false [dev-dependencies.glob] version = "0.3" [dev-dependencies.rand] version = "0.8.4" [dev-dependencies.term] version = "1.0.1" [features] benchmarks = [] unstable = ["crc32fast/nightly"] [lints.rust.unexpected_cfgs] level = "warn" priority = 0 check-cfg = ["cfg(fuzzing)"] png-0.17.16/Cargo.toml.orig000064400000000000000000000024301046102023000134520ustar 00000000000000[package] name = "png" version = "0.17.16" license = "MIT OR Apache-2.0" description = "PNG decoding and encoding library in pure Rust" categories = ["multimedia::images"] authors = ["The image-rs Developers"] repository = "https://github.com/image-rs/image-png" edition = "2018" rust-version = "1.57" include = [ "/LICENSE-MIT", "/LICENSE-APACHE", "/README.md", "/CHANGES.md", "/src/", "/examples/", "/benches/", ] [dependencies] bitflags = "1.0" crc32fast = "1.2.0" fdeflate = "0.3.3" flate2 = "1.0.11" miniz_oxide = { version = "0.8", features = ["simd"] } [dev-dependencies] approx = "0.5.1" byteorder = "1.5.0" clap = { version = "3.0", features = ["derive"] } criterion = "0.4.0" getopts = "0.2.14" glium = { version = "0.32", features = ["glutin"], default-features = false } glob = "0.3" rand = "0.8.4" term = "1.0.1" [features] unstable = ["crc32fast/nightly"] benchmarks = [] [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } [[bench]] path = "benches/decoder.rs" name = "decoder" harness = false [[bench]] path = "benches/unfilter.rs" name = "unfilter" harness = false required-features = ["benchmarks"] [[bench]] path = "benches/expand_paletted.rs" name = "expand_paletted" harness = false required-features = ["benchmarks"] png-0.17.16/LICENSE-APACHE000064400000000000000000000251371046102023000125200ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. png-0.17.16/LICENSE-MIT000064400000000000000000000020301046102023000122130ustar 00000000000000Copyright (c) 2015 nwin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. png-0.17.16/README.md000064400000000000000000000027671046102023000120570ustar 00000000000000# PNG Decoder/Encoder [![Build Status](https://github.com/image-rs/image-png/workflows/Rust%20CI/badge.svg)](https://github.com/image-rs/image-png/actions) [![Documentation](https://docs.rs/png/badge.svg)](https://docs.rs/png) [![Crates.io](https://img.shields.io/crates/v/png.svg)](https://crates.io/crates/png) [![License](https://img.shields.io/crates/l/png.svg)](https://github.com/image-rs/image-png) Robust and performant PNG decoder/encoder in pure Rust. Also supports [APNG](https://en.wikipedia.org/wiki/APNG). No `unsafe` code, battle-tested, and fuzzed on [OSS-fuzz](https://github.com/google/oss-fuzz). ## Performance Performance is typically on par with or better than libpng. Includes a fast encoding mode powered by [fdeflate](https://crates.io/crates/fdeflate) that is dramatically faster than the fastest mode of libpng while *simultaneously* providing better compression ratio. On nightly Rust compiler you can slightly speed up decoding of some images by enabling the `unstable` feature of this crate. ## License Licensed under either of * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. png-0.17.16/benches/README.md000064400000000000000000000002601046102023000134500ustar 00000000000000# Getting started with benchmarking To run the benchmarks you need a nightly rust toolchain. Then you launch it with rustup run nightly cargo bench --features=benchmarks png-0.17.16/benches/decoder.rs000064400000000000000000000052631046102023000141540ustar 00000000000000use std::fs; use criterion::{ criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion, Throughput, }; use png::{Decoder, Reader, Transformations}; #[path = "../src/test_utils.rs"] mod test_utils; fn load_all(c: &mut Criterion) { let mut g = c.benchmark_group("decode"); for entry in fs::read_dir("tests/benches/").unwrap().flatten() { match entry.path().extension() { Some(st) if st == "png" => {} _ => continue, } let data = fs::read(entry.path()).unwrap(); bench_file(&mut g, data, entry.file_name().into_string().unwrap()); } g.finish(); // Small IDATS let mut g = c.benchmark_group("generated-noncompressed-4k-idat"); bench_noncompressed_png(&mut g, 8, 4096); // 256 B bench_noncompressed_png(&mut g, 128, 4096); // 64 KB bench_noncompressed_png(&mut g, 2048, 4096); // 16 MB bench_noncompressed_png(&mut g, 12288, 4096); // 576 MB g.finish(); // Normal IDATS let mut g = c.benchmark_group("generated-noncompressed-64k-idat"); bench_noncompressed_png(&mut g, 128, 65536); // 64 KB bench_noncompressed_png(&mut g, 2048, 65536); // 16 MB bench_noncompressed_png(&mut g, 12288, 65536); // 576 MB g.finish(); // Large IDATS let mut g = c.benchmark_group("generated-noncompressed-2g-idat"); bench_noncompressed_png(&mut g, 2048, 0x7fffffff); // 16 MB bench_noncompressed_png(&mut g, 12288, 0x7fffffff); // 576 MB g.finish(); } criterion_group! {benches, load_all} criterion_main!(benches); fn bench_noncompressed_png(g: &mut BenchmarkGroup, size: u32, idat_bytes: usize) { let mut data = Vec::new(); test_utils::write_noncompressed_png(&mut data, size, idat_bytes); bench_file(g, data, format!("{size}x{size}.png")); } fn bench_file(g: &mut BenchmarkGroup, data: Vec, name: String) { if data.len() > 1_000_000 { g.sample_size(10); } fn create_reader(data: &[u8]) -> Reader<&[u8]> { let mut decoder = Decoder::new(data); // Cover default transformations used by the `image` crate when constructing // `image::codecs::png::PngDecoder`. decoder.set_transformations(Transformations::EXPAND); decoder.read_info().unwrap() } let mut reader = create_reader(data.as_slice()); let mut image = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut image).unwrap(); g.throughput(Throughput::Bytes(info.buffer_size() as u64)); g.bench_with_input(name, &data, |b, data| { b.iter(|| { let mut reader = create_reader(data.as_slice()); reader.next_frame(&mut image).unwrap(); }) }); } png-0.17.16/benches/expand_paletted.rs000064400000000000000000000117051046102023000157060ustar 00000000000000//! Usage example: //! //! ``` //! $ alias bench="rustup run nightly cargo bench" //! $ bench --bench=expand_paletted --features=benchmarks -- --save-baseline my_baseline //! ... tweak something ... //! $ bench --bench=expand_paletted --features=benchmarks -- --baseline my_baseline //! ``` use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use png::benchable_apis::{create_info_from_plte_trns_bitdepth, create_transform_fn, TransformFn}; use png::{Info, Transformations}; use rand::Rng; use std::fmt::{self, Display}; #[derive(Clone, Copy)] enum TrnsPresence { Present, Absent, } impl Display for TrnsPresence { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { TrnsPresence::Present => write!(f, "trns=yes"), TrnsPresence::Absent => write!(f, "trns=no"), } } } fn expand_paletted_all(c: &mut Criterion) { let trns_options = [TrnsPresence::Absent, TrnsPresence::Present]; let bit_depths = [4, 8]; let input_size = { let typical_l1_cache_size = 32 * 1024; let mut factor = 1; // input factor += 4; // RGBA output factor += 1; // other data typical_l1_cache_size / factor }; for trns in trns_options.iter().copied() { for bit_depth in bit_depths.iter().copied() { bench_expand_palette(c, trns, bit_depth, input_size); } } bench_create_fn(c, 256, 256); // Full PLTE and trNS bench_create_fn(c, 224, 32); // Partial PLTE and trNS bench_create_fn(c, 16, 1); // Guess: typical for small images? } criterion_group!(benches, expand_paletted_all); criterion_main!(benches); fn get_random_bytes(rng: &mut R, n: usize) -> Vec { use rand::Fill; let mut result = vec![0u8; n]; result.as_mut_slice().try_fill(rng).unwrap(); result } struct Input { palette: Vec, trns: Option>, src: Vec, src_bit_depth: u8, } impl Input { fn new(trns: TrnsPresence, src_bit_depth: u8, input_size_in_bytes: usize) -> Self { let mut rng = rand::thread_rng(); // We provide RGB entries for 192 out of 256 possible indices and Alpha/Transparency // entries for 32 out of 256 possible indices. Rationale for these numbers: // * Oftentimes only a handful of colors at the edges of an icon need transparency // * In general, code needs to handle out-of-bounds indices, so it seems desirable // to explicitly test this. let palette = get_random_bytes(&mut rng, 192.min(input_size_in_bytes) * 3); let trns = match trns { TrnsPresence::Absent => None, TrnsPresence::Present => Some(get_random_bytes(&mut rng, 32.min(input_size_in_bytes))), }; let src = get_random_bytes(&mut rng, input_size_in_bytes); Self { palette, trns, src, src_bit_depth, } } fn output_size_in_bytes(&self) -> usize { let output_bytes_per_input_sample = match self.trns { None => 3, Some(_) => 4, }; let samples_count_per_byte = (8 / self.src_bit_depth) as usize; let samples_count = self.src.len() * samples_count_per_byte; samples_count * output_bytes_per_input_sample } fn to_info(&self) -> Info { create_info_from_plte_trns_bitdepth(&self.palette, self.trns.as_deref(), self.src_bit_depth) } } #[inline(always)] fn create_expand_palette_fn(info: &Info) -> TransformFn { create_transform_fn(info, Transformations::EXPAND).unwrap() } fn bench_create_fn(c: &mut Criterion, plte_size: usize, trns_size: usize) { let mut group = c.benchmark_group("expand_paletted(ctor)"); group.sample_size(1000); let mut rng = rand::thread_rng(); let plte = get_random_bytes(&mut rng, 3 * plte_size as usize); let trns = get_random_bytes(&mut rng, trns_size as usize); let info = create_info_from_plte_trns_bitdepth(&plte, Some(&trns), 8); group.bench_with_input( format!("plte={plte_size}/trns={trns_size:?}"), &info, |b, info| { b.iter(|| create_expand_palette_fn(info)); }, ); } fn bench_expand_palette( c: &mut Criterion, trns: TrnsPresence, src_bit_depth: u8, input_size_in_bytes: usize, ) { let mut group = c.benchmark_group("expand_paletted(exec)"); let input = Input::new(trns, src_bit_depth, input_size_in_bytes); let transform_fn = create_expand_palette_fn(&input.to_info()); group.throughput(Throughput::Bytes(input.output_size_in_bytes() as u64)); group.sample_size(500); group.bench_with_input( format!("{trns}/src_bits={src_bit_depth}/src_size={input_size_in_bytes}"), &input, |b, input| { let mut output = vec![0; input.output_size_in_bytes()]; let info = input.to_info(); b.iter(|| { transform_fn(input.src.as_slice(), output.as_mut_slice(), &info); }); }, ); } png-0.17.16/benches/unfilter.rs000064400000000000000000000033111046102023000143670ustar 00000000000000//! Usage example: //! //! ``` //! $ alias bench="rustup run nightly cargo bench" //! $ bench --bench=unfilter --features=benchmarks,unstable -- --save-baseline my_baseline //! ... tweak something, say the Sub filter ... //! $ bench --bench=unfilter --features=benchmarks,unstable -- filter=Sub --baseline my_baseline //! ``` use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use png::benchable_apis::unfilter; use png::FilterType; use rand::Rng; fn unfilter_all(c: &mut Criterion) { let bpps = [1, 2, 3, 4, 6, 8]; let filters = [ FilterType::Sub, FilterType::Up, FilterType::Avg, FilterType::Paeth, ]; for &filter in filters.iter() { for &bpp in bpps.iter() { bench_unfilter(c, filter, bpp); } } } criterion_group!(benches, unfilter_all); criterion_main!(benches); fn bench_unfilter(c: &mut Criterion, filter: FilterType, bpp: u8) { let mut group = c.benchmark_group("unfilter"); fn get_random_bytes(rng: &mut R, n: usize) -> Vec { use rand::Fill; let mut result = vec![0u8; n]; result.as_mut_slice().try_fill(rng).unwrap(); result } let mut rng = rand::thread_rng(); let row_size = 4096 * (bpp as usize); let two_rows = get_random_bytes(&mut rng, row_size * 2); group.throughput(Throughput::Bytes(row_size as u64)); group.bench_with_input( format!("filter={filter:?}/bpp={bpp}"), &two_rows, |b, two_rows| { let (prev_row, curr_row) = two_rows.split_at(row_size); let mut curr_row = curr_row.to_vec(); b.iter(|| unfilter(filter, bpp, prev_row, curr_row.as_mut_slice())); }, ); } png-0.17.16/examples/change-png-info.rs000064400000000000000000000035311046102023000157120ustar 00000000000000/// Tests "editing"/re-encoding of an image: /// decoding, editing, re-encoding use std::fs::File; use std::io::BufWriter; use std::path::Path; pub type BoxResult = Result>; fn main() -> BoxResult<()> { // # Decode // Read test image from pngsuite let path_in = Path::new(r"./tests/pngsuite/basi0g01.png"); // The decoder is a build for reader and can be used to set various decoding options // via `Transformations`. The default output transformation is `Transformations::IDENTITY`. let decoder = png::Decoder::new(File::open(path_in)?); let mut reader = decoder.read_info()?; // Allocate the output buffer. let png_info = reader.info(); let mut buf = vec![0; reader.output_buffer_size()]; dbg!(png_info); // # Encode let path_out = Path::new(r"./target/test_modified.png"); let file = File::create(path_out)?; let ref mut w = BufWriter::new(file); // Get defaults for interlaced parameter. let mut info_out = png_info.clone(); let info_default = png::Info::default(); // Edit previous info info_out.interlaced = info_default.interlaced; let mut encoder = png::Encoder::with_info(w, info_out)?; encoder.set_depth(png_info.bit_depth); // Edit some attribute encoder.add_text_chunk( "Testing tEXt".to_string(), "This is a tEXt chunk that will appear before the IDAT chunks.".to_string(), )?; // Save picture with changed info let mut writer = encoder.write_header()?; let mut counter = 0u8; while let Ok(info) = reader.next_frame(&mut buf) { let bytes = &buf[..info.buffer_size()]; println!("{} {}", info.buffer_size(), reader.output_buffer_size()); writer.write_image_data(&bytes)?; counter += 1; println!("Written frame: {}", counter); } Ok(()) } png-0.17.16/examples/corpus-bench.rs000064400000000000000000000150071046102023000153430ustar 00000000000000use std::{fs, path::PathBuf}; use clap::Parser; use png::Decoder; #[derive(clap::ValueEnum, Clone)] enum Speed { Fast, Default, Best, } #[derive(clap::ValueEnum, Clone)] enum Filter { None, Sub, Up, Average, Paeth, Adaptive, } #[derive(clap::Parser)] struct Args { directory: Option, #[clap(short, long, value_enum, default_value_t = Speed::Fast)] speed: Speed, #[clap(short, long, value_enum, default_value_t = Filter::Adaptive)] filter: Filter, } #[inline(never)] fn run_encode( args: &Args, dimensions: (u32, u32), color_type: png::ColorType, bit_depth: png::BitDepth, image: &[u8], ) -> Vec { let mut reencoded = Vec::new(); let mut encoder = png::Encoder::new(&mut reencoded, dimensions.0, dimensions.1); encoder.set_color(color_type); encoder.set_depth(bit_depth); encoder.set_compression(match args.speed { Speed::Fast => png::Compression::Fast, Speed::Default => png::Compression::Default, Speed::Best => png::Compression::Best, }); encoder.set_filter(match args.filter { Filter::None => png::FilterType::NoFilter, Filter::Sub => png::FilterType::Sub, Filter::Up => png::FilterType::Up, Filter::Average => png::FilterType::Avg, Filter::Paeth => png::FilterType::Paeth, Filter::Adaptive => png::FilterType::Paeth, }); encoder.set_adaptive_filter(match args.filter { Filter::Adaptive => png::AdaptiveFilterType::Adaptive, _ => png::AdaptiveFilterType::NonAdaptive, }); let mut encoder = encoder.write_header().unwrap(); encoder.write_image_data(image).unwrap(); encoder.finish().unwrap(); reencoded } #[inline(never)] fn run_decode(image: &[u8], output: &mut [u8]) { let mut reader = Decoder::new(image).read_info().unwrap(); reader.next_frame(output).unwrap(); } fn main() { let mut total_uncompressed = 0; let mut total_compressed = 0; let mut total_pixels = 0; let mut total_encode_time = 0; let mut total_decode_time = 0; let args = Args::parse(); println!( "{:45} Ratio Encode Decode", "Directory" ); println!( "{:45}------- -------------------- --------------------", "---------" ); let mut image2 = Vec::new(); let mut pending = vec![args.directory.clone().unwrap_or(PathBuf::from("."))]; while let Some(directory) = pending.pop() { let mut dir_uncompressed = 0; let mut dir_compressed = 0; let mut dir_pixels = 0; let mut dir_encode_time = 0; let mut dir_decode_time = 0; for entry in fs::read_dir(&directory).unwrap().flatten() { if entry.file_type().unwrap().is_dir() { pending.push(entry.path()); continue; } match entry.path().extension() { Some(st) if st == "png" => {} _ => continue, } // Parse let data = fs::read(entry.path()).unwrap(); let mut decoder = Decoder::new(&*data); if decoder.read_header_info().ok().map(|h| h.color_type) == Some(png::ColorType::Indexed) { decoder.set_transformations( png::Transformations::EXPAND | png::Transformations::STRIP_16, ); } let mut reader = match decoder.read_info() { Ok(reader) => reader, Err(_) => continue, }; let mut image = vec![0; reader.output_buffer_size()]; let info = match reader.next_frame(&mut image) { Ok(info) => info, Err(_) => continue, }; let (width, height) = (info.width, info.height); let bit_depth = info.bit_depth; let mut color_type = info.color_type; // qoibench expands grayscale to RGB, so we do the same. if bit_depth == png::BitDepth::Eight { if color_type == png::ColorType::Grayscale { image = image.into_iter().flat_map(|v| [v, v, v, 255]).collect(); color_type = png::ColorType::Rgba; } else if color_type == png::ColorType::GrayscaleAlpha { image = image .chunks_exact(2) .flat_map(|v| [v[0], v[0], v[0], v[1]]) .collect(); color_type = png::ColorType::Rgba; } } // Re-encode let start = std::time::Instant::now(); let reencoded = run_encode(&args, (width, height), color_type, bit_depth, &image); let elapsed = start.elapsed().as_nanos() as u64; // And decode again image2.resize(image.len(), 0); let start2 = std::time::Instant::now(); run_decode(&reencoded, &mut image2); let elapsed2 = start2.elapsed().as_nanos() as u64; assert_eq!(image, image2); // Stats dir_uncompressed += image.len(); dir_compressed += reencoded.len(); dir_pixels += (width * height) as u64; dir_encode_time += elapsed; dir_decode_time += elapsed2; } if dir_uncompressed > 0 { println!( "{:45}{:6.2}%{:8} mps {:6.2} GiB/s {:8} mps {:6.2} GiB/s", directory.display(), 100.0 * dir_compressed as f64 / dir_uncompressed as f64, dir_pixels * 1000 / dir_encode_time, dir_uncompressed as f64 / (dir_encode_time as f64 * 1e-9 * (1 << 30) as f64), dir_pixels * 1000 / dir_decode_time, dir_uncompressed as f64 / (dir_decode_time as f64 * 1e-9 * (1 << 30) as f64) ); } total_uncompressed += dir_uncompressed; total_compressed += dir_compressed; total_pixels += dir_pixels; total_encode_time += dir_encode_time; total_decode_time += dir_decode_time; } println!(); println!( "{:44}{:7.3}%{:8} mps {:6.3} GiB/s {:8} mps {:6.3} GiB/s", "Total", 100.0 * total_compressed as f64 / total_uncompressed as f64, total_pixels * 1000 / total_encode_time, total_uncompressed as f64 / (total_encode_time as f64 * 1e-9 * (1 << 30) as f64), total_pixels * 1000 / total_decode_time, total_uncompressed as f64 / (total_decode_time as f64 * 1e-9 * (1 << 30) as f64) ); } png-0.17.16/examples/png-generate.rs000064400000000000000000000041341046102023000153260ustar 00000000000000// For reading and opening files use png::text_metadata::{ITXtChunk, ZTXtChunk}; use std::env; use std::fs::File; use std::io::BufWriter; fn main() { let path = env::args() .nth(1) .expect("Expected a filename to output to."); let file = File::create(path).unwrap(); let w = &mut BufWriter::new(file); let mut encoder = png::Encoder::new(w, 2, 1); // Width is 2 pixels and height is 1. encoder.set_color(png::ColorType::Rgba); encoder.set_depth(png::BitDepth::Eight); // Adding text chunks to the header encoder .add_text_chunk( "Testing tEXt".to_string(), "This is a tEXt chunk that will appear before the IDAT chunks.".to_string(), ) .unwrap(); encoder .add_ztxt_chunk( "Testing zTXt".to_string(), "This is a zTXt chunk that is compressed in the png file.".to_string(), ) .unwrap(); encoder .add_itxt_chunk( "Testing iTXt".to_string(), "iTXt chunks support all of UTF8. Example: हिंदी.".to_string(), ) .unwrap(); let mut writer = encoder.write_header().unwrap(); let data = [255, 0, 0, 255, 0, 0, 0, 255]; // An array containing a RGBA sequence. First pixel is red and second pixel is black. writer.write_image_data(&data).unwrap(); // Save // We can add a tEXt/zTXt/iTXt at any point before the encoder is dropped from scope. These chunks will be at the end of the png file. let tail_ztxt_chunk = ZTXtChunk::new( "Comment".to_string(), "A zTXt chunk after the image data.".to_string(), ); writer.write_text_chunk(&tail_ztxt_chunk).unwrap(); // The fields of the text chunk are public, so they can be mutated before being written to the file. let mut tail_itxt_chunk = ITXtChunk::new("Author".to_string(), "सायंतन खान".to_string()); tail_itxt_chunk.compressed = true; tail_itxt_chunk.language_tag = "hi".to_string(); tail_itxt_chunk.translated_keyword = "लेखक".to_string(); writer.write_text_chunk(&tail_itxt_chunk).unwrap(); } png-0.17.16/examples/pngcheck.rs000064400000000000000000000306271046102023000145420ustar 00000000000000#![allow(non_upper_case_globals)] use std::env; use std::fs::File; use std::io; use std::io::prelude::*; use std::path::Path; use getopts::{Matches, Options, ParsingStyle}; use term::{color, Attr}; fn parse_args() -> Matches { let args: Vec = env::args().collect(); let mut opts = Options::new(); opts.optflag("c", "", "colorize output (for ANSI terminals)") .optflag("q", "", "test quietly (output only errors)") .optflag( "t", "", "print contents of tEXt/zTXt/iTXt chunks (can be used with -q)", ) .optflag("v", "", "test verbosely (print most chunk data)") .parsing_style(ParsingStyle::StopAtFirstFree); if args.len() > 1 { match opts.parse(&args[1..]) { Ok(matches) => return matches, Err(err) => println!("{}", err), } } println!("{}", opts.usage("Usage: pngcheck [-cpt] [file ...]")); std::process::exit(0); } #[derive(Clone, Copy)] struct Config { quiet: bool, verbose: bool, color: bool, text: bool, } fn display_interlaced(i: bool) -> &'static str { if i { "interlaced" } else { "non-interlaced" } } fn display_image_type(bits: u8, color: png::ColorType) -> String { use png::ColorType::*; format!( "{}-bit {}", bits, match color { Grayscale => "grayscale", Rgb => "RGB", Indexed => "palette", GrayscaleAlpha => "grayscale+alpha", Rgba => "RGB+alpha", } ) } // channels after expansion of tRNS fn final_channels(c: png::ColorType, trns: bool) -> u8 { use png::ColorType::*; match c { Grayscale => 1 + u8::from(trns), Rgb => 3, Indexed => 3 + u8::from(trns), GrayscaleAlpha => 2, Rgba => 4, } } fn check_image>(c: Config, fname: P) -> io::Result<()> { // TODO improve performance by reusing allocations from decoder use png::Decoded::*; let mut t = term::stdout() .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "could not open terminal"))?; let data = &mut vec![0; 10 * 1024][..]; let mut reader = io::BufReader::new(File::open(&fname)?); let fname = fname.as_ref().to_string_lossy(); let n = reader.read(data)?; let mut buf = &data[..n]; let mut pos = 0; let mut decoder = png::StreamingDecoder::new(); // Image data let mut width = 0; let mut height = 0; let mut color = png::ColorType::Grayscale; let mut bits = 0; let mut trns = false; let mut interlaced = false; let mut compressed_size = 0; let mut n_chunks = 0; let mut have_idat = false; macro_rules! c_ratio( // TODO add palette entries to compressed_size () => ({ compressed_size as f32/( height as u64 * (width as u64 * final_channels(color, trns) as u64 * bits as u64 + 7)>>3 ) as f32 }); ); let display_error = |err| -> Result<_, io::Error> { let mut t = term::stdout() .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "could not open terminal"))?; if c.verbose { if c.color { print!(": "); t.fg(color::RED)?; writeln!(t, "{}", err)?; t.attr(Attr::Bold)?; write!(t, "ERRORS DETECTED")?; t.reset()?; } else { println!(": {}", err); print!("ERRORS DETECTED") } println!(" in {}", fname); } else { if !c.quiet { if c.color { t.fg(color::RED)?; t.attr(Attr::Bold)?; write!(t, "ERROR")?; t.reset()?; write!(t, ": ")?; t.fg(color::YELLOW)?; writeln!(t, "{}", fname)?; t.reset()?; } else { println!("ERROR: {}", fname) } } print!("{}: ", fname); if c.color { t.fg(color::RED)?; writeln!(t, "{}", err)?; t.reset()?; } else { println!("{}", err); } } Ok(()) }; if c.verbose { print!("File: "); if c.color { t.attr(Attr::Bold)?; write!(t, "{}", fname)?; t.reset()?; } else { print!("{}", fname); } print!(" ({}) bytes", data.len()) } loop { if buf.is_empty() { // circumvent borrow checker assert!(!data.is_empty()); let n = reader.read(data)?; // EOF if n == 0 { println!("ERROR: premature end of file {}", fname); break; } buf = &data[..n]; } match decoder.update(buf, &mut Vec::new()) { Ok((_, ImageEnd)) => { if !have_idat { // This isn't beautiful. But it works. display_error(png::DecodingError::IoError(io::Error::new( io::ErrorKind::InvalidData, "IDAT chunk missing", )))?; break; } if !c.verbose && !c.quiet { if c.color { t.fg(color::GREEN)?; t.attr(Attr::Bold)?; write!(t, "OK")?; t.reset()?; write!(t, ": ")?; t.fg(color::YELLOW)?; write!(t, "{}", fname)?; t.reset()?; } else { print!("OK: {}", fname) } println!( " ({}x{}, {}{}, {}, {:.1}%)", width, height, display_image_type(bits, color), (if trns { "+trns" } else { "" }), display_interlaced(interlaced), 100.0 * (1.0 - c_ratio!()) ) } else if !c.quiet { println!(); if c.color { t.fg(color::GREEN)?; t.attr(Attr::Bold)?; write!(t, "No errors detected ")?; t.reset()?; } else { print!("No errors detected "); } println!( "in {} ({} chunks, {:.1}% compression)", fname, n_chunks, 100.0 * (1.0 - c_ratio!()), ) } break; } Ok((n, res)) => { buf = &buf[n..]; pos += n; match res { Header(w, h, b, c, i) => { width = w; height = h; bits = b as u8; color = c; interlaced = i; } ChunkBegin(len, type_str) => { use png::chunk; n_chunks += 1; if c.verbose { let chunk = type_str; println!(); print!(" chunk "); if c.color { t.fg(color::YELLOW)?; write!(t, "{:?}", chunk)?; t.reset()?; } else { print!("{:?}", chunk) } print!( " at offset {:#07x}, length {}", pos - 4, // subtract chunk name length len ) } match type_str { chunk::IDAT => { have_idat = true; compressed_size += len } chunk::tRNS => { trns = true; } _ => (), } } ImageData => { //println!("got {} bytes of image data", data.len()) } ChunkComplete(_, type_str) if c.verbose => { use png::chunk::*; if type_str == IHDR { println!(); print!( " {} x {} image, {}{}, {}", width, height, display_image_type(bits, color), (if trns { "+trns" } else { "" }), display_interlaced(interlaced), ); } } AnimationControl(actl) => { println!(); print!(" {} frames, {} plays", actl.num_frames, actl.num_plays,); } FrameControl(fctl) => { println!(); println!( " sequence #{}, {} x {} pixels @ ({}, {})", fctl.sequence_number, fctl.width, fctl.height, fctl.x_offset, fctl.y_offset, /*fctl.delay_num, fctl.delay_den, fctl.dispose_op, fctl.blend_op,*/ ); print!( " {}/{} s delay, dispose: {}, blend: {}", fctl.delay_num, if fctl.delay_den == 0 { 100 } else { fctl.delay_den }, fctl.dispose_op, fctl.blend_op, ); } _ => (), } //println!("{} {:?}", n, res) } Err(err) => { let _ = display_error(err); break; } } } if c.text { println!("Parsed tEXt chunks:"); for text_chunk in &decoder.info().unwrap().uncompressed_latin1_text { println!("{:#?}", text_chunk); } println!("Parsed zTXt chunks:"); for text_chunk in &decoder.info().unwrap().compressed_latin1_text { let mut cloned_text_chunk = text_chunk.clone(); cloned_text_chunk.decompress_text()?; println!("{:#?}", cloned_text_chunk); } println!("Parsed iTXt chunks:"); for text_chunk in &decoder.info().unwrap().utf8_text { let mut cloned_text_chunk = text_chunk.clone(); cloned_text_chunk.decompress_text()?; println!("{:#?}", cloned_text_chunk); } } Ok(()) } fn main() { let m = parse_args(); let config = Config { quiet: m.opt_present("q"), verbose: m.opt_present("v"), color: m.opt_present("c"), text: m.opt_present("t"), }; for file in m.free { let result = if file.contains('*') { glob::glob(&file) .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) .and_then(|mut glob| { glob.try_for_each(|entry| { entry .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) .and_then(|file| check_image(config, file)) }) }) } else { check_image(config, &file) }; result.unwrap_or_else(|err| { println!("{}: {}", file, err); std::process::exit(1) }); } } png-0.17.16/examples/show.rs000064400000000000000000000143471046102023000137410ustar 00000000000000use glium::{ backend::glutin::Display, glutin::{ self, dpi, event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event_loop::ControlFlow, }, texture::{ClientFormat, RawImage2d}, BlitTarget, Rect, Surface, }; use std::{borrow::Cow, env, fs::File, io, path}; /// Load the image using `png` fn load_image(path: &path::PathBuf) -> io::Result> { use png::ColorType::*; let mut decoder = png::Decoder::new(File::open(path)?); decoder.set_transformations(png::Transformations::normalize_to_color8()); let mut reader = decoder.read_info()?; let mut img_data = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut img_data)?; let (data, format) = match info.color_type { Rgb => (img_data, ClientFormat::U8U8U8), Rgba => (img_data, ClientFormat::U8U8U8U8), Grayscale => ( { let mut vec = Vec::with_capacity(img_data.len() * 3); for g in img_data { vec.extend([g, g, g].iter().cloned()) } vec }, ClientFormat::U8U8U8, ), GrayscaleAlpha => ( { let mut vec = Vec::with_capacity(img_data.len() * 3); for ga in img_data.chunks(2) { let g = ga[0]; let a = ga[1]; vec.extend([g, g, g, a].iter().cloned()) } vec }, ClientFormat::U8U8U8U8, ), _ => unreachable!("uncovered color type"), }; Ok(RawImage2d { data: Cow::Owned(data), width: info.width, height: info.height, format, }) } fn main_loop(files: Vec) -> io::Result<()> { let mut files = files.into_iter(); let image = load_image(&files.next().unwrap())?; let event_loop = glutin::event_loop::EventLoop::new(); let window_builder = glutin::window::WindowBuilder::new().with_title("Show Example"); let context_builder = glutin::ContextBuilder::new().with_vsync(true); let display = glium::Display::new(window_builder, context_builder, &event_loop) .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; resize_window(&display, &image); let mut texture = glium::Texture2d::new(&display, image).unwrap(); draw(&display, &texture); event_loop.run(move |event, _, control_flow| match event { Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => exit(control_flow), Event::WindowEvent { event: WindowEvent::KeyboardInput { input: KeyboardInput { state: ElementState::Pressed, virtual_keycode: code, .. }, .. }, .. } => match code { Some(VirtualKeyCode::Escape) => exit(control_flow), Some(VirtualKeyCode::Right) => match &files.next() { Some(path) => { match load_image(path) { Ok(image) => { resize_window(&display, &image); texture = glium::Texture2d::new(&display, image).unwrap(); draw(&display, &texture); } Err(err) => { println!("Error: {}", err); exit(control_flow); } }; } None => exit(control_flow), }, _ => {} }, Event::RedrawRequested(_) => draw(&display, &texture), _ => {} }); } fn draw(display: &glium::Display, texture: &glium::Texture2d) { let frame = display.draw(); fill_v_flipped( &texture.as_surface(), &frame, glium::uniforms::MagnifySamplerFilter::Linear, ); frame.finish().unwrap(); } fn exit(control_flow: &mut ControlFlow) { *control_flow = ControlFlow::Exit; } fn fill_v_flipped(src: &S1, target: &S2, filter: glium::uniforms::MagnifySamplerFilter) where S1: Surface, S2: Surface, { let src_dim = src.get_dimensions(); let src_rect = Rect { left: 0, bottom: 0, width: src_dim.0, height: src_dim.1, }; let target_dim = target.get_dimensions(); let target_rect = BlitTarget { left: 0, bottom: target_dim.1, width: target_dim.0 as i32, height: -(target_dim.1 as i32), }; src.blit_color(&src_rect, target, &target_rect, filter); } fn resize_window(display: &Display, image: &RawImage2d<'static, u8>) { let mut width = image.width; let mut height = image.height; if width < 50 && height < 50 { width *= 10; height *= 10; } display .gl_window() .window() .set_inner_size(dpi::LogicalSize::new(f64::from(width), f64::from(height))); } fn main() { let args: Vec = env::args().collect(); if args.len() < 2 { println!("Usage: show files [...]"); } else { let mut files = vec![]; for file in args.iter().skip(1) { match if file.contains('*') { (|| -> io::Result<_> { for entry in glob::glob(file) .map_err(|err| io::Error::new(io::ErrorKind::Other, err.msg))? { files.push( entry .map_err(|_| io::Error::new(io::ErrorKind::Other, "glob error"))?, ) } Ok(()) })() } else { files.push(path::PathBuf::from(file)); Ok(()) } { Ok(_) => (), Err(err) => { println!("{}: {}", file, err); break; } } } // "tests/pngsuite/pngsuite.png" match main_loop(files) { Ok(_) => (), Err(err) => println!("Error: {}", err), } } } png-0.17.16/src/adam7.rs000064400000000000000000000365731046102023000127300ustar 00000000000000//! Utility functions related to handling of //! [the Adam7 algorithm](https://en.wikipedia.org/wiki/Adam7_algorithm). /// Describes which stage of /// [the Adam7 algorithm](https://en.wikipedia.org/wiki/Adam7_algorithm) /// applies to a decoded row. /// /// See also [Reader.next_interlaced_row](crate::decoder::Reader::next_interlaced_row). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Adam7Info { pub(crate) pass: u8, pub(crate) line: u32, pub(crate) width: u32, } impl Adam7Info { /// Creates a new `Adam7Info`. May panic if the arguments are out of range (e.g. if `pass` is /// 0 or greater than 8). /// /// * `pass` corresponds to a pass of the /// [the Adam7 algorithm](https://en.wikipedia.org/wiki/Adam7_algorithm) /// * `line` is the number of a line within a pass (starting with 0). For example, /// in an image of height 8, `line` can be beteween `0..4` in the 7th `pass` /// (those 4 interlaced rows correspond to 2nd, 4th, 6th, and 8th row of the full image). /// * `width` describes how many pixels are in an interlaced row. For example, /// in the 7th `pass`, the `width` is be the same as full image width, but in /// in the 1st `pass`, the `width` is be 1/8th of the image width (rounded up as /// necessary). /// /// Note that in typical usage, `Adam7Info`s are returned by [Reader.next_interlaced_row] /// and there is no need to create them by calling `Adam7Info::new`. `Adam7Info::new` is /// nevertheless exposed as a public API, because it helps to provide self-contained example /// usage of [expand_interlaced_row](crate::expand_interlaced_row). pub fn new(pass: u8, line: u32, width: u32) -> Self { assert!(1 <= pass && pass <= 7); assert!(width > 0); Self { pass, line, width } } } /// This iterator iterates over the different passes of an image Adam7 encoded /// PNG image /// The pattern is: /// 16462646 /// 77777777 /// 56565656 /// 77777777 /// 36463646 /// 77777777 /// 56565656 /// 77777777 /// #[derive(Clone)] pub(crate) struct Adam7Iterator { line: u32, lines: u32, line_width: u32, current_pass: u8, width: u32, height: u32, } impl Adam7Iterator { pub fn new(width: u32, height: u32) -> Adam7Iterator { let mut this = Adam7Iterator { line: 0, lines: 0, line_width: 0, current_pass: 1, width, height, }; this.init_pass(); this } /// Calculates the bounds of the current pass fn init_pass(&mut self) { let w = f64::from(self.width); let h = f64::from(self.height); let (line_width, lines) = match self.current_pass { 1 => (w / 8.0, h / 8.0), 2 => ((w - 4.0) / 8.0, h / 8.0), 3 => (w / 4.0, (h - 4.0) / 8.0), 4 => ((w - 2.0) / 4.0, h / 4.0), 5 => (w / 2.0, (h - 2.0) / 4.0), 6 => ((w - 1.0) / 2.0, h / 2.0), 7 => (w, (h - 1.0) / 2.0), _ => unreachable!(), }; self.line_width = line_width.ceil() as u32; self.lines = lines.ceil() as u32; self.line = 0; } } /// Iterates over `Adam7Info`s. impl Iterator for Adam7Iterator { type Item = Adam7Info; fn next(&mut self) -> Option { if self.line < self.lines && self.line_width > 0 { let this_line = self.line; self.line += 1; Some(Adam7Info { pass: self.current_pass, line: this_line, width: self.line_width, }) } else if self.current_pass < 7 { self.current_pass += 1; self.init_pass(); self.next() } else { None } } } fn subbyte_pixels(scanline: &[u8], bits_pp: usize) -> impl Iterator + '_ { (0..scanline.len() * 8) .step_by(bits_pp) .map(move |bit_idx| { let byte_idx = bit_idx / 8; // sub-byte samples start in the high-order bits let rem = 8 - bit_idx % 8 - bits_pp; match bits_pp { // evenly divides bytes 1 => (scanline[byte_idx] >> rem) & 1, 2 => (scanline[byte_idx] >> rem) & 3, 4 => (scanline[byte_idx] >> rem) & 15, _ => unreachable!(), } }) } /// Given `row_stride`, interlace `info`, and bits-per-pixel, produce an iterator of bit positions /// of pixels to copy from the input scanline to the image buffer. The positions are expressed as /// bit offsets from position (0,0) in the frame that is currently being decoded. fn expand_adam7_bits( row_stride_in_bytes: usize, info: &Adam7Info, bits_pp: usize, ) -> impl Iterator { let line_no = info.line as usize; let pass = info.pass; let interlaced_width = info.width as usize; let (line_mul, line_off, samp_mul, samp_off) = match pass { 1 => (8, 0, 8, 0), 2 => (8, 0, 8, 4), 3 => (8, 4, 4, 0), 4 => (4, 0, 4, 2), 5 => (4, 2, 2, 0), 6 => (2, 0, 2, 1), 7 => (2, 1, 1, 0), _ => { // `Adam7Info.pass` is a non-`pub`lic field. `InterlaceInfo` is expected // to maintain an invariant that `pass` is valid. panic!("Invalid `Adam7Info.pass`"); } }; // the equivalent line number in progressive scan let prog_line = line_mul * line_no + line_off; let line_start = prog_line * row_stride_in_bytes * 8; (0..interlaced_width) .map(move |i| i * samp_mul + samp_off) .map(move |i| i * bits_pp) .map(move |bits_offset| bits_offset + line_start) } /// Copies pixels from `interlaced_row` into the right location in `img`. /// /// First bytes of `img` should belong to the top-left corner of the currently decoded frame. /// /// `img_row_stride` specifies an offset in bytes between subsequent rows of `img`. /// This can be the width of the current frame being decoded, but this is not required - a bigger /// stride may be useful if the frame being decoded is a sub-region of `img`. /// /// `interlaced_row` and `interlace_info` typically come from /// [crate::decoder::Reader::next_interlaced_row], but this is not required. In particular, before /// calling `expand_interlaced_row` one may need to expand the decoded row, so that its format and /// `bits_per_pixel` matches that of `img`. Note that in initial Adam7 passes the `interlaced_row` /// may contain less pixels that the width of the frame being decoded (e.g. it contains only 1/8th /// of pixels in the initial pass). /// /// Example: /// /// ``` /// use png::{expand_interlaced_row, Adam7Info}; /// let info = Adam7Info::new(5, 0, 4); // 1st line of 5th pass has 4 pixels. /// let mut img = vec![0; 8 * 8]; /// let row = vec![1, 2, 3, 4]; /// expand_interlaced_row(&mut img, 8, &row, &info, 8); /// assert_eq!(&img, &[ /// 0, 0, 0, 0, 0, 0, 0, 0, /// 0, 0, 0, 0, 0, 0, 0, 0, /// 1, 0, 2, 0, 3, 0, 4, 0, // <= this is where the 1st line of 5s appears /// 0, 0, 0, 0, 0, 0, 0, 0, // in the schematic drawing of the passes at /// 0, 0, 0, 0, 0, 0, 0, 0, // https://en.wikipedia.org/wiki/Adam7_algorithm /// 0, 0, 0, 0, 0, 0, 0, 0, /// 0, 0, 0, 0, 0, 0, 0, 0, /// 0, 0, 0, 0, 0, 0, 0, 0, /// ]); /// ``` pub fn expand_pass( img: &mut [u8], img_row_stride: usize, interlaced_row: &[u8], interlace_info: &Adam7Info, bits_per_pixel: u8, ) { let bits_pp = bits_per_pixel as usize; let bit_indices = expand_adam7_bits(img_row_stride, interlace_info, bits_pp); if bits_pp < 8 { for (pos, px) in bit_indices.zip(subbyte_pixels(interlaced_row, bits_pp)) { let rem = 8 - pos % 8 - bits_pp; img[pos / 8] |= px << rem as u8; } } else { let bytes_pp = bits_pp / 8; for (bitpos, px) in bit_indices.zip(interlaced_row.chunks(bytes_pp)) { for (offset, val) in px.iter().enumerate() { img[bitpos / 8 + offset] = *val; } } } } #[test] fn test_adam7() { /* 1646 7777 5656 7777 */ let it = Adam7Iterator::new(4, 4); let passes: Vec<_> = it.collect(); assert_eq!( &*passes, &[ Adam7Info { pass: 1, line: 0, width: 1 }, Adam7Info { pass: 4, line: 0, width: 1 }, Adam7Info { pass: 5, line: 0, width: 2 }, Adam7Info { pass: 6, line: 0, width: 2 }, Adam7Info { pass: 6, line: 1, width: 2 }, Adam7Info { pass: 7, line: 0, width: 4 }, Adam7Info { pass: 7, line: 1, width: 4 } ] ); } #[test] fn test_subbyte_pixels() { let scanline = &[0b10101010, 0b10101010]; let pixels = subbyte_pixels(scanline, 1).collect::>(); assert_eq!(pixels.len(), 16); assert_eq!(pixels, [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]); } #[test] fn test_expand_adam7_bits() { let width = 32; let bits_pp = 1; let stride = width / 8; let info = |pass, line, img_width| create_adam7_info_for_tests(pass, line as u32, img_width); let expected = |offset: usize, step: usize, count: usize| { (0..count) .map(move |i| step * i + offset) .collect::>() }; for line_no in 0..8 { let start = 8 * line_no * width; assert_eq!( expand_adam7_bits(stride, &info(1, line_no, width), bits_pp).collect::>(), expected(start, 8, 4) ); let start = start + 4; assert_eq!( expand_adam7_bits(stride, &info(2, line_no, width), bits_pp).collect::>(), expected(start, 8, 4) ); let start = (8 * line_no + 4) * width; assert_eq!( expand_adam7_bits(stride, &info(3, line_no, width), bits_pp).collect::>(), expected(start, 4, 8) ); } for line_no in 0..16 { let start = 4 * line_no * width + 2; assert_eq!( expand_adam7_bits(stride, &info(4, line_no, width), bits_pp).collect::>(), expected(start, 4, 8) ); let start = (4 * line_no + 2) * width; assert_eq!( expand_adam7_bits(stride, &info(5, line_no, width), bits_pp).collect::>(), expected(start, 2, 16) ) } for line_no in 0..32 { let start = 2 * line_no * width + 1; assert_eq!( expand_adam7_bits(stride, &info(6, line_no, width), bits_pp).collect::>(), expected(start, 2, 16), "line_no: {}", line_no ); let start = (2 * line_no + 1) * width; assert_eq!( expand_adam7_bits(stride, &info(7, line_no, width), bits_pp).collect::>(), expected(start, 1, 32) ); } } #[test] fn test_expand_adam7_bits_independent_row_stride() { let pass = 1; let line_no = 1; let width = 32; let bits_pp = 8; let info = create_adam7_info_for_tests; { let stride = width; assert_eq!( expand_adam7_bits(stride, &info(pass, line_no, width), bits_pp).collect::>(), vec![2048, 2112, 2176, 2240], ); } { let stride = 10000; assert_eq!( expand_adam7_bits(stride, &info(pass, line_no, width), bits_pp).collect::>(), vec![640000, 640064, 640128, 640192], ); } } #[test] fn test_expand_pass_subbyte() { let mut img = [0u8; 8]; let width = 8; let stride = width / 8; let bits_pp = 1; let info = create_adam7_info_for_tests; expand_pass(&mut img, stride, &[0b10000000], &info(1, 0, width), bits_pp); assert_eq!(img, [0b10000000u8, 0, 0, 0, 0, 0, 0, 0]); expand_pass(&mut img, stride, &[0b10000000], &info(2, 0, width), bits_pp); assert_eq!(img, [0b10001000u8, 0, 0, 0, 0, 0, 0, 0]); expand_pass(&mut img, stride, &[0b11000000], &info(3, 0, width), bits_pp); assert_eq!(img, [0b10001000u8, 0, 0, 0, 0b10001000, 0, 0, 0]); expand_pass(&mut img, stride, &[0b11000000], &info(4, 0, width), bits_pp); assert_eq!(img, [0b10101010u8, 0, 0, 0, 0b10001000, 0, 0, 0]); expand_pass(&mut img, stride, &[0b11000000], &info(4, 1, width), bits_pp); assert_eq!(img, [0b10101010u8, 0, 0, 0, 0b10101010, 0, 0, 0]); expand_pass(&mut img, stride, &[0b11110000], &info(5, 0, width), bits_pp); assert_eq!(img, [0b10101010u8, 0, 0b10101010, 0, 0b10101010, 0, 0, 0]); expand_pass(&mut img, stride, &[0b11110000], &info(5, 1, width), bits_pp); assert_eq!( img, [0b10101010u8, 0, 0b10101010, 0, 0b10101010, 0, 0b10101010, 0] ); expand_pass(&mut img, stride, &[0b11110000], &info(6, 0, width), bits_pp); assert_eq!( img, [0b11111111u8, 0, 0b10101010, 0, 0b10101010, 0, 0b10101010, 0] ); expand_pass(&mut img, stride, &[0b11110000], &info(6, 1, width), bits_pp); assert_eq!( img, [0b11111111u8, 0, 0b11111111, 0, 0b10101010, 0, 0b10101010, 0] ); expand_pass(&mut img, stride, &[0b11110000], &info(6, 2, width), bits_pp); assert_eq!( img, [0b11111111u8, 0, 0b11111111, 0, 0b11111111, 0, 0b10101010, 0] ); expand_pass(&mut img, stride, &[0b11110000], &info(6, 3, width), bits_pp); assert_eq!( [0b11111111u8, 0, 0b11111111, 0, 0b11111111, 0, 0b11111111, 0], img ); expand_pass(&mut img, stride, &[0b11111111], &info(7, 0, width), bits_pp); assert_eq!( [ 0b11111111u8, 0b11111111, 0b11111111, 0, 0b11111111, 0, 0b11111111, 0 ], img ); expand_pass(&mut img, stride, &[0b11111111], &info(7, 1, width), bits_pp); assert_eq!( [ 0b11111111u8, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0, 0b11111111, 0 ], img ); expand_pass(&mut img, stride, &[0b11111111], &info(7, 2, width), bits_pp); assert_eq!( [ 0b11111111u8, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0 ], img ); expand_pass(&mut img, stride, &[0b11111111], &info(7, 3, width), bits_pp); assert_eq!( [ 0b11111111u8, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111 ], img ); } #[cfg(test)] fn create_adam7_info_for_tests(pass: u8, line: u32, img_width: usize) -> Adam7Info { let width = { let img_height = 8; Adam7Iterator::new(img_width as u32, img_height) .filter(|info| info.pass == pass) .map(|info| info.width) .next() .unwrap() }; Adam7Info { pass, line, width } } png-0.17.16/src/benchable_apis.rs000064400000000000000000000020121046102023000146330ustar 00000000000000//! Development-time-only helper module for exporting private APIs so that they can be benchmarked. //! This module is gated behind the "benchmarks" feature. use crate::common::BytesPerPixel; use crate::filter::FilterType; use crate::{BitDepth, ColorType, Info}; /// Re-exporting `unfilter` to make it easier to benchmark, despite some items being only /// `pub(crate)`: `fn unfilter`, `enum BytesPerPixel`. pub fn unfilter(filter: FilterType, tbpp: u8, previous: &[u8], current: &mut [u8]) { let tbpp = BytesPerPixel::from_usize(tbpp as usize); crate::filter::unfilter(filter, tbpp, previous, current) } pub use crate::decoder::transform::{create_transform_fn, TransformFn}; pub fn create_info_from_plte_trns_bitdepth<'a>( plte: &'a [u8], trns: Option<&'a [u8]>, bit_depth: u8, ) -> Info<'a> { Info { color_type: ColorType::Indexed, bit_depth: BitDepth::from_u8(bit_depth).unwrap(), palette: Some(plte.into()), trns: trns.map(Into::into), ..Info::default() } } png-0.17.16/src/chunk.rs000064400000000000000000000064701046102023000130400ustar 00000000000000//! Chunk types and functions #![allow(dead_code)] #![allow(non_upper_case_globals)] use core::fmt; #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct ChunkType(pub [u8; 4]); // -- Critical chunks -- /// Image header pub const IHDR: ChunkType = ChunkType(*b"IHDR"); /// Palette pub const PLTE: ChunkType = ChunkType(*b"PLTE"); /// Image data pub const IDAT: ChunkType = ChunkType(*b"IDAT"); /// Image trailer pub const IEND: ChunkType = ChunkType(*b"IEND"); // -- Ancillary chunks -- /// Transparency pub const tRNS: ChunkType = ChunkType(*b"tRNS"); /// Background colour pub const bKGD: ChunkType = ChunkType(*b"bKGD"); /// Image last-modification time pub const tIME: ChunkType = ChunkType(*b"tIME"); /// Physical pixel dimensions pub const pHYs: ChunkType = ChunkType(*b"pHYs"); /// Source system's pixel chromaticities pub const cHRM: ChunkType = ChunkType(*b"cHRM"); /// Source system's gamma value pub const gAMA: ChunkType = ChunkType(*b"gAMA"); /// sRGB color space chunk pub const sRGB: ChunkType = ChunkType(*b"sRGB"); /// ICC profile chunk pub const iCCP: ChunkType = ChunkType(*b"iCCP"); /// Coding-independent code points for video signal type identification chunk pub const cICP: ChunkType = ChunkType(*b"cICP"); /// Mastering Display Color Volume chunk pub const mDCV: ChunkType = ChunkType(*b"mDCV"); /// Content Light Level Information chunk pub const cLLI: ChunkType = ChunkType(*b"cLLI"); /// EXIF metadata chunk pub const eXIf: ChunkType = ChunkType(*b"eXIf"); /// Latin-1 uncompressed textual data pub const tEXt: ChunkType = ChunkType(*b"tEXt"); /// Latin-1 compressed textual data pub const zTXt: ChunkType = ChunkType(*b"zTXt"); /// UTF-8 textual data pub const iTXt: ChunkType = ChunkType(*b"iTXt"); // Significant bits pub const sBIT: ChunkType = ChunkType(*b"sBIT"); // -- Extension chunks -- /// Animation control pub const acTL: ChunkType = ChunkType(*b"acTL"); /// Frame control pub const fcTL: ChunkType = ChunkType(*b"fcTL"); /// Frame data pub const fdAT: ChunkType = ChunkType(*b"fdAT"); // -- Chunk type determination -- /// Returns true if the chunk is critical. pub fn is_critical(ChunkType(type_): ChunkType) -> bool { type_[0] & 32 == 0 } /// Returns true if the chunk is private. pub fn is_private(ChunkType(type_): ChunkType) -> bool { type_[1] & 32 != 0 } /// Checks whether the reserved bit of the chunk name is set. /// If it is set the chunk name is invalid. pub fn reserved_set(ChunkType(type_): ChunkType) -> bool { type_[2] & 32 != 0 } /// Returns true if the chunk is safe to copy if unknown. pub fn safe_to_copy(ChunkType(type_): ChunkType) -> bool { type_[3] & 32 != 0 } impl fmt::Debug for ChunkType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { struct DebugType([u8; 4]); impl fmt::Debug for DebugType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for &c in &self.0[..] { write!(f, "{}", char::from(c).escape_debug())?; } Ok(()) } } f.debug_struct("ChunkType") .field("type", &DebugType(self.0)) .field("critical", &is_critical(*self)) .field("private", &is_private(*self)) .field("reserved", &reserved_set(*self)) .field("safecopy", &safe_to_copy(*self)) .finish() } } png-0.17.16/src/common.rs000064400000000000000000001042251046102023000132150ustar 00000000000000//! Common types shared between the encoder and decoder use crate::text_metadata::{EncodableTextChunk, ITXtChunk, TEXtChunk, ZTXtChunk}; use crate::{chunk, encoder}; use io::Write; use std::{borrow::Cow, convert::TryFrom, fmt, io}; /// Describes how a pixel is encoded. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum ColorType { /// 1 grayscale sample. Grayscale = 0, /// 1 red sample, 1 green sample, 1 blue sample. Rgb = 2, /// 1 sample for the palette index. Indexed = 3, /// 1 grayscale sample, then 1 alpha sample. GrayscaleAlpha = 4, /// 1 red sample, 1 green sample, 1 blue sample, and finally, 1 alpha sample. Rgba = 6, } impl ColorType { /// Returns the number of samples used per pixel encoded in this way. pub fn samples(self) -> usize { self.samples_u8().into() } pub(crate) fn samples_u8(self) -> u8 { use self::ColorType::*; match self { Grayscale | Indexed => 1, Rgb => 3, GrayscaleAlpha => 2, Rgba => 4, } } /// u8 -> Self. Temporary solution until Rust provides a canonical one. pub fn from_u8(n: u8) -> Option { match n { 0 => Some(ColorType::Grayscale), 2 => Some(ColorType::Rgb), 3 => Some(ColorType::Indexed), 4 => Some(ColorType::GrayscaleAlpha), 6 => Some(ColorType::Rgba), _ => None, } } pub(crate) fn checked_raw_row_length(self, depth: BitDepth, width: u32) -> Option { // No overflow can occur in 64 bits, we multiply 32-bit with 5 more bits. let bits = u64::from(width) * u64::from(self.samples_u8()) * u64::from(depth.into_u8()); TryFrom::try_from(1 + (bits + 7) / 8).ok() } pub(crate) fn raw_row_length_from_width(self, depth: BitDepth, width: u32) -> usize { let samples = width as usize * self.samples(); 1 + match depth { BitDepth::Sixteen => samples * 2, BitDepth::Eight => samples, subbyte => { let samples_per_byte = 8 / subbyte as usize; let whole = samples / samples_per_byte; let fract = usize::from(samples % samples_per_byte > 0); whole + fract } } } pub(crate) fn is_combination_invalid(self, bit_depth: BitDepth) -> bool { // Section 11.2.2 of the PNG standard disallows several combinations // of bit depth and color type ((bit_depth == BitDepth::One || bit_depth == BitDepth::Two || bit_depth == BitDepth::Four) && (self == ColorType::Rgb || self == ColorType::GrayscaleAlpha || self == ColorType::Rgba)) || (bit_depth == BitDepth::Sixteen && self == ColorType::Indexed) } } /// Bit depth of the PNG file. /// Specifies the number of bits per sample. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum BitDepth { One = 1, Two = 2, Four = 4, Eight = 8, Sixteen = 16, } /// Internal count of bytes per pixel. /// This is used for filtering which never uses sub-byte units. This essentially reduces the number /// of possible byte chunk lengths to a very small set of values appropriate to be defined as an /// enum. #[derive(Debug, Clone, Copy)] #[repr(u8)] pub(crate) enum BytesPerPixel { One = 1, Two = 2, Three = 3, Four = 4, Six = 6, Eight = 8, } impl BitDepth { /// u8 -> Self. Temporary solution until Rust provides a canonical one. pub fn from_u8(n: u8) -> Option { match n { 1 => Some(BitDepth::One), 2 => Some(BitDepth::Two), 4 => Some(BitDepth::Four), 8 => Some(BitDepth::Eight), 16 => Some(BitDepth::Sixteen), _ => None, } } pub(crate) fn into_u8(self) -> u8 { self as u8 } } /// Pixel dimensions information #[derive(Clone, Copy, Debug)] pub struct PixelDimensions { /// Pixels per unit, X axis pub xppu: u32, /// Pixels per unit, Y axis pub yppu: u32, /// Either *Meter* or *Unspecified* pub unit: Unit, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] /// Physical unit of the pixel dimensions pub enum Unit { Unspecified = 0, Meter = 1, } impl Unit { /// u8 -> Self. Temporary solution until Rust provides a canonical one. pub fn from_u8(n: u8) -> Option { match n { 0 => Some(Unit::Unspecified), 1 => Some(Unit::Meter), _ => None, } } } /// How to reset buffer of an animated png (APNG) at the end of a frame. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum DisposeOp { /// Leave the buffer unchanged. None = 0, /// Clear buffer with the background color. Background = 1, /// Reset the buffer to the state before the current frame. Previous = 2, } impl DisposeOp { /// u8 -> Self. Using enum_primitive or transmute is probably the right thing but this will do for now. pub fn from_u8(n: u8) -> Option { match n { 0 => Some(DisposeOp::None), 1 => Some(DisposeOp::Background), 2 => Some(DisposeOp::Previous), _ => None, } } } impl fmt::Display for DisposeOp { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let name = match *self { DisposeOp::None => "DISPOSE_OP_NONE", DisposeOp::Background => "DISPOSE_OP_BACKGROUND", DisposeOp::Previous => "DISPOSE_OP_PREVIOUS", }; write!(f, "{}", name) } } /// How pixels are written into the buffer. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum BlendOp { /// Pixels overwrite the value at their position. Source = 0, /// The new pixels are blended into the current state based on alpha. Over = 1, } impl BlendOp { /// u8 -> Self. Using enum_primitive or transmute is probably the right thing but this will do for now. pub fn from_u8(n: u8) -> Option { match n { 0 => Some(BlendOp::Source), 1 => Some(BlendOp::Over), _ => None, } } } impl fmt::Display for BlendOp { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let name = match *self { BlendOp::Source => "BLEND_OP_SOURCE", BlendOp::Over => "BLEND_OP_OVER", }; write!(f, "{}", name) } } /// Frame control information #[derive(Clone, Copy, Debug)] pub struct FrameControl { /// Sequence number of the animation chunk, starting from 0 pub sequence_number: u32, /// Width of the following frame pub width: u32, /// Height of the following frame pub height: u32, /// X position at which to render the following frame pub x_offset: u32, /// Y position at which to render the following frame pub y_offset: u32, /// Frame delay fraction numerator pub delay_num: u16, /// Frame delay fraction denominator pub delay_den: u16, /// Type of frame area disposal to be done after rendering this frame pub dispose_op: DisposeOp, /// Type of frame area rendering for this frame pub blend_op: BlendOp, } impl Default for FrameControl { fn default() -> FrameControl { FrameControl { sequence_number: 0, width: 0, height: 0, x_offset: 0, y_offset: 0, delay_num: 1, delay_den: 30, dispose_op: DisposeOp::None, blend_op: BlendOp::Source, } } } impl FrameControl { pub fn set_seq_num(&mut self, s: u32) { self.sequence_number = s; } pub fn inc_seq_num(&mut self, i: u32) { self.sequence_number += i; } pub fn encode(self, w: &mut W) -> encoder::Result<()> { let mut data = [0u8; 26]; data[..4].copy_from_slice(&self.sequence_number.to_be_bytes()); data[4..8].copy_from_slice(&self.width.to_be_bytes()); data[8..12].copy_from_slice(&self.height.to_be_bytes()); data[12..16].copy_from_slice(&self.x_offset.to_be_bytes()); data[16..20].copy_from_slice(&self.y_offset.to_be_bytes()); data[20..22].copy_from_slice(&self.delay_num.to_be_bytes()); data[22..24].copy_from_slice(&self.delay_den.to_be_bytes()); data[24] = self.dispose_op as u8; data[25] = self.blend_op as u8; encoder::write_chunk(w, chunk::fcTL, &data) } } /// Animation control information #[derive(Clone, Copy, Debug)] pub struct AnimationControl { /// Number of frames pub num_frames: u32, /// Number of times to loop this APNG. 0 indicates infinite looping. pub num_plays: u32, } impl AnimationControl { pub fn encode(self, w: &mut W) -> encoder::Result<()> { let mut data = [0; 8]; data[..4].copy_from_slice(&self.num_frames.to_be_bytes()); data[4..].copy_from_slice(&self.num_plays.to_be_bytes()); encoder::write_chunk(w, chunk::acTL, &data) } } /// The type and strength of applied compression. #[derive(Debug, Clone, Copy)] pub enum Compression { /// Default level Default, /// Fast minimal compression Fast, /// Higher compression level /// /// Best in this context isn't actually the highest possible level /// the encoder can do, but is meant to emulate the `Best` setting in the `Flate2` /// library. Best, #[deprecated( since = "0.17.6", note = "use one of the other compression levels instead, such as 'fast'" )] Huffman, #[deprecated( since = "0.17.6", note = "use one of the other compression levels instead, such as 'fast'" )] Rle, } impl Default for Compression { fn default() -> Self { Self::Default } } /// An unsigned integer scaled version of a floating point value, /// equivalent to an integer quotient with fixed denominator (100_000)). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ScaledFloat(u32); impl ScaledFloat { const SCALING: f32 = 100_000.0; /// Gets whether the value is within the clamped range of this type. pub fn in_range(value: f32) -> bool { value >= 0.0 && (value * Self::SCALING).floor() <= u32::MAX as f32 } /// Gets whether the value can be exactly converted in round-trip. #[allow(clippy::float_cmp)] // Stupid tool, the exact float compare is _the entire point_. pub fn exact(value: f32) -> bool { let there = Self::forward(value); let back = Self::reverse(there); value == back } fn forward(value: f32) -> u32 { (value.max(0.0) * Self::SCALING).floor() as u32 } fn reverse(encoded: u32) -> f32 { encoded as f32 / Self::SCALING } /// Slightly inaccurate scaling and quantization. /// Clamps the value into the representable range if it is negative or too large. pub fn new(value: f32) -> Self { Self(Self::forward(value)) } /// Fully accurate construction from a value scaled as per specification. pub fn from_scaled(val: u32) -> Self { Self(val) } /// Get the accurate encoded value. pub fn into_scaled(self) -> u32 { self.0 } /// Get the unscaled value as a floating point. pub fn into_value(self) -> f32 { Self::reverse(self.0) } pub(crate) fn encode_gama(self, w: &mut W) -> encoder::Result<()> { encoder::write_chunk(w, chunk::gAMA, &self.into_scaled().to_be_bytes()) } } /// Chromaticities of the color space primaries #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SourceChromaticities { pub white: (ScaledFloat, ScaledFloat), pub red: (ScaledFloat, ScaledFloat), pub green: (ScaledFloat, ScaledFloat), pub blue: (ScaledFloat, ScaledFloat), } impl SourceChromaticities { pub fn new(white: (f32, f32), red: (f32, f32), green: (f32, f32), blue: (f32, f32)) -> Self { SourceChromaticities { white: (ScaledFloat::new(white.0), ScaledFloat::new(white.1)), red: (ScaledFloat::new(red.0), ScaledFloat::new(red.1)), green: (ScaledFloat::new(green.0), ScaledFloat::new(green.1)), blue: (ScaledFloat::new(blue.0), ScaledFloat::new(blue.1)), } } #[rustfmt::skip] pub fn to_be_bytes(self) -> [u8; 32] { let white_x = self.white.0.into_scaled().to_be_bytes(); let white_y = self.white.1.into_scaled().to_be_bytes(); let red_x = self.red.0.into_scaled().to_be_bytes(); let red_y = self.red.1.into_scaled().to_be_bytes(); let green_x = self.green.0.into_scaled().to_be_bytes(); let green_y = self.green.1.into_scaled().to_be_bytes(); let blue_x = self.blue.0.into_scaled().to_be_bytes(); let blue_y = self.blue.1.into_scaled().to_be_bytes(); [ white_x[0], white_x[1], white_x[2], white_x[3], white_y[0], white_y[1], white_y[2], white_y[3], red_x[0], red_x[1], red_x[2], red_x[3], red_y[0], red_y[1], red_y[2], red_y[3], green_x[0], green_x[1], green_x[2], green_x[3], green_y[0], green_y[1], green_y[2], green_y[3], blue_x[0], blue_x[1], blue_x[2], blue_x[3], blue_y[0], blue_y[1], blue_y[2], blue_y[3], ] } pub fn encode(self, w: &mut W) -> encoder::Result<()> { encoder::write_chunk(w, chunk::cHRM, &self.to_be_bytes()) } } /// The rendering intent for an sRGB image. /// /// Presence of this data also indicates that the image conforms to the sRGB color space. #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SrgbRenderingIntent { /// For images preferring good adaptation to the output device gamut at the expense of colorimetric accuracy, such as photographs. Perceptual = 0, /// For images requiring colour appearance matching (relative to the output device white point), such as logos. RelativeColorimetric = 1, /// For images preferring preservation of saturation at the expense of hue and lightness, such as charts and graphs. Saturation = 2, /// For images requiring preservation of absolute colorimetry, such as previews of images destined for a different output device (proofs). AbsoluteColorimetric = 3, } impl SrgbRenderingIntent { pub(crate) fn into_raw(self) -> u8 { self as u8 } pub(crate) fn from_raw(raw: u8) -> Option { match raw { 0 => Some(SrgbRenderingIntent::Perceptual), 1 => Some(SrgbRenderingIntent::RelativeColorimetric), 2 => Some(SrgbRenderingIntent::Saturation), 3 => Some(SrgbRenderingIntent::AbsoluteColorimetric), _ => None, } } pub fn encode(self, w: &mut W) -> encoder::Result<()> { encoder::write_chunk(w, chunk::sRGB, &[self.into_raw()]) } } /// Coding-independent code points (cICP) specify the color space (primaries), /// transfer function, matrix coefficients and scaling factor of the image using /// the code points specified in [ITU-T-H.273](https://www.itu.int/rec/T-REC-H.273). /// /// See https://www.w3.org/TR/png-3/#cICP-chunk for more details. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CodingIndependentCodePoints { /// Id number of the color primaries defined in /// [ITU-T-H.273](https://www.itu.int/rec/T-REC-H.273) in "Table 2 - /// Interpretation of colour primaries (ColourPrimaries) value". pub color_primaries: u8, /// Id number of the transfer characteristics defined in /// [ITU-T-H.273](https://www.itu.int/rec/T-REC-H.273) in "Table 3 - /// Interpretation of transfer characteristics (TransferCharacteristics) /// value". pub transfer_function: u8, /// Id number of the matrix coefficients defined in /// [ITU-T-H.273](https://www.itu.int/rec/T-REC-H.273) in "Table 4 - /// Interpretation of matrix coefficients (MatrixCoefficients) value". /// /// This field is included to faithfully replicate the base /// [ITU-T-H.273](https://www.itu.int/rec/T-REC-H.273) specification, but matrix coefficients /// will always be set to 0, because RGB is currently the only supported color mode in PNG. pub matrix_coefficients: u8, /// Whether the image is /// [a full range image](https://www.w3.org/TR/png-3/#dfn-full-range-image) /// or /// [a narrow range image](https://www.w3.org/TR/png-3/#dfn-narrow-range-image). /// /// This field is included to faithfully replicate the base /// [ITU-T-H.273](https://www.itu.int/rec/T-REC-H.273) specification, but it has limited /// practical application to PNG images, because narrow-range images are [quite /// rare](https://github.com/w3c/png/issues/312#issuecomment-2327349614) in practice. pub is_video_full_range_image: bool, } /// Mastering Display Color Volume (mDCV) used at the point of content creation, /// as specified in [SMPTE-ST-2086](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=8353899). /// /// See https://www.w3.org/TR/png-3/#mDCV-chunk for more details. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct MasteringDisplayColorVolume { /// Mastering display chromaticities. pub chromaticities: SourceChromaticities, /// Mastering display maximum luminance. /// /// The value is expressed in units of 0.0001 cd/m^2 - for example if this field /// is set to `10000000` then it indicates 1000 cd/m^2. pub max_luminance: u32, /// Mastering display minimum luminance. /// /// The value is expressed in units of 0.0001 cd/m^2 - for example if this field /// is set to `10000000` then it indicates 1000 cd/m^2. pub min_luminance: u32, } /// Content light level information of HDR content. /// /// See https://www.w3.org/TR/png-3/#cLLI-chunk for more details. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ContentLightLevelInfo { /// Maximum Content Light Level indicates the maximum light level of any /// single pixel (in cd/m^2, also known as nits) of the entire playback /// sequence. /// /// The value is expressed in units of 0.0001 cd/m^2 - for example if this field /// is set to `10000000` then it indicates 1000 cd/m^2. /// /// A value of zero means that the value is unknown or not currently calculable. pub max_content_light_level: u32, /// Maximum Frame Average Light Level indicates the maximum value of the /// frame average light level (in cd/m^2, also known as nits) of the entire /// playback sequence. It is calculated by first averaging the decoded /// luminance values of all the pixels in each frame, and then using the /// value for the frame with the highest value. /// /// The value is expressed in units of 0.0001 cd/m^2 - for example if this field /// is set to `10000000` then it indicates 1000 cd/m^2. /// /// A value of zero means that the value is unknown or not currently calculable. pub max_frame_average_light_level: u32, } /// PNG info struct #[derive(Clone, Debug)] #[non_exhaustive] pub struct Info<'a> { pub width: u32, pub height: u32, pub bit_depth: BitDepth, /// How colors are stored in the image. pub color_type: ColorType, pub interlaced: bool, /// The image's `sBIT` chunk, if present; contains significant bits of the sample. pub sbit: Option>, /// The image's `tRNS` chunk, if present; contains the alpha channel of the image's palette, 1 byte per entry. pub trns: Option>, pub pixel_dims: Option, /// The image's `PLTE` chunk, if present; contains the RGB channels (in that order) of the image's palettes, 3 bytes per entry (1 per channel). pub palette: Option>, /// The contents of the image's gAMA chunk, if present. /// Prefer `source_gamma` to also get the derived replacement gamma from sRGB chunks. pub gama_chunk: Option, /// The contents of the image's `cHRM` chunk, if present. /// Prefer `source_chromaticities` to also get the derived replacements from sRGB chunks. pub chrm_chunk: Option, /// The contents of the image's `bKGD` chunk, if present. pub bkgd: Option>, pub frame_control: Option, pub animation_control: Option, pub compression: Compression, /// Gamma of the source system. /// Set by both `gAMA` as well as to a replacement by `sRGB` chunk. pub source_gamma: Option, /// Chromaticities of the source system. /// Set by both `cHRM` as well as to a replacement by `sRGB` chunk. pub source_chromaticities: Option, /// The rendering intent of an SRGB image. /// /// Presence of this value also indicates that the image conforms to the SRGB color space. pub srgb: Option, /// The ICC profile for the image. pub icc_profile: Option>, /// The coding-independent code points for video signal type identification of the image. pub coding_independent_code_points: Option, /// The mastering display color volume for the image. pub mastering_display_color_volume: Option, /// The content light information for the image. pub content_light_level: Option, /// The EXIF metadata for the image. pub exif_metadata: Option>, /// tEXt field pub uncompressed_latin1_text: Vec, /// zTXt field pub compressed_latin1_text: Vec, /// iTXt field pub utf8_text: Vec, } impl Default for Info<'_> { fn default() -> Info<'static> { Info { width: 0, height: 0, bit_depth: BitDepth::Eight, color_type: ColorType::Grayscale, interlaced: false, palette: None, sbit: None, trns: None, gama_chunk: None, chrm_chunk: None, bkgd: None, pixel_dims: None, frame_control: None, animation_control: None, // Default to `deflate::Compression::Fast` and `filter::FilterType::Sub` // to maintain backward compatible output. compression: Compression::Fast, source_gamma: None, source_chromaticities: None, srgb: None, icc_profile: None, coding_independent_code_points: None, mastering_display_color_volume: None, content_light_level: None, exif_metadata: None, uncompressed_latin1_text: Vec::new(), compressed_latin1_text: Vec::new(), utf8_text: Vec::new(), } } } impl Info<'_> { /// A utility constructor for a default info with width and height. pub fn with_size(width: u32, height: u32) -> Self { Info { width, height, ..Default::default() } } /// Size of the image, width then height. pub fn size(&self) -> (u32, u32) { (self.width, self.height) } /// Returns true if the image is an APNG image. pub fn is_animated(&self) -> bool { self.frame_control.is_some() && self.animation_control.is_some() } /// Returns the frame control information of the image. pub fn animation_control(&self) -> Option<&AnimationControl> { self.animation_control.as_ref() } /// Returns the frame control information of the current frame pub fn frame_control(&self) -> Option<&FrameControl> { self.frame_control.as_ref() } /// Returns the number of bits per pixel. pub fn bits_per_pixel(&self) -> usize { self.color_type.samples() * self.bit_depth as usize } /// Returns the number of bytes per pixel. pub fn bytes_per_pixel(&self) -> usize { // If adjusting this for expansion or other transformation passes, remember to keep the old // implementation for bpp_in_prediction, which is internal to the png specification. self.color_type.samples() * ((self.bit_depth as usize + 7) >> 3) } /// Return the number of bytes for this pixel used in prediction. /// /// Some filters use prediction, over the raw bytes of a scanline. Where a previous pixel is /// require for such forms the specification instead references previous bytes. That is, for /// a gray pixel of bit depth 2, the pixel used in prediction is actually 4 pixels prior. This /// has the consequence that the number of possible values is rather small. To make this fact /// more obvious in the type system and the optimizer we use an explicit enum here. pub(crate) fn bpp_in_prediction(&self) -> BytesPerPixel { BytesPerPixel::from_usize(self.bytes_per_pixel()) } /// Returns the number of bytes needed for one deinterlaced image. pub fn raw_bytes(&self) -> usize { self.height as usize * self.raw_row_length() } /// Returns the number of bytes needed for one deinterlaced row. pub fn raw_row_length(&self) -> usize { self.raw_row_length_from_width(self.width) } pub(crate) fn checked_raw_row_length(&self) -> Option { self.color_type .checked_raw_row_length(self.bit_depth, self.width) } /// Returns the number of bytes needed for one deinterlaced row of width `width`. pub fn raw_row_length_from_width(&self, width: u32) -> usize { self.color_type .raw_row_length_from_width(self.bit_depth, width) } /// Mark the image data as conforming to the SRGB color space with the specified rendering intent. /// /// Any ICC profiles will be ignored. /// /// Source gamma and chromaticities will be written only if they're set to fallback /// values specified in [11.3.2.5](https://www.w3.org/TR/png-3/#sRGB-gAMA-cHRM). pub(crate) fn set_source_srgb(&mut self, rendering_intent: SrgbRenderingIntent) { self.srgb = Some(rendering_intent); self.icc_profile = None; } /// Encode this header to the writer. /// /// Note that this does _not_ include the PNG signature, it starts with the IHDR chunk and then /// includes other chunks that were added to the header. #[deprecated(note = "Use Encoder+Writer instead")] pub fn encode(&self, mut w: W) -> encoder::Result<()> { // Encode the IHDR chunk let mut data = [0; 13]; data[..4].copy_from_slice(&self.width.to_be_bytes()); data[4..8].copy_from_slice(&self.height.to_be_bytes()); data[8] = self.bit_depth as u8; data[9] = self.color_type as u8; data[12] = self.interlaced as u8; encoder::write_chunk(&mut w, chunk::IHDR, &data)?; // Encode the pHYs chunk if let Some(pd) = self.pixel_dims { let mut phys_data = [0; 9]; phys_data[0..4].copy_from_slice(&pd.xppu.to_be_bytes()); phys_data[4..8].copy_from_slice(&pd.yppu.to_be_bytes()); match pd.unit { Unit::Meter => phys_data[8] = 1, Unit::Unspecified => phys_data[8] = 0, } encoder::write_chunk(&mut w, chunk::pHYs, &phys_data)?; } // If specified, the sRGB information overrides the source gamma and chromaticities. if let Some(srgb) = &self.srgb { srgb.encode(&mut w)?; // gAMA and cHRM are optional, for backwards compatibility let srgb_gamma = crate::srgb::substitute_gamma(); if Some(srgb_gamma) == self.source_gamma { srgb_gamma.encode_gama(&mut w)? } let srgb_chromaticities = crate::srgb::substitute_chromaticities(); if Some(srgb_chromaticities) == self.source_chromaticities { srgb_chromaticities.encode(&mut w)?; } } else { if let Some(gma) = self.source_gamma { gma.encode_gama(&mut w)? } if let Some(chrms) = self.source_chromaticities { chrms.encode(&mut w)?; } if let Some(iccp) = &self.icc_profile { encoder::write_iccp_chunk(&mut w, "_", iccp)? } } if let Some(exif) = &self.exif_metadata { encoder::write_chunk(&mut w, chunk::eXIf, exif)?; } if let Some(actl) = self.animation_control { actl.encode(&mut w)?; } // The position of the PLTE chunk is important, it must come before the tRNS chunk and after // many of the other metadata chunks. if let Some(p) = &self.palette { encoder::write_chunk(&mut w, chunk::PLTE, p)?; }; if let Some(t) = &self.trns { encoder::write_chunk(&mut w, chunk::tRNS, t)?; } for text_chunk in &self.uncompressed_latin1_text { text_chunk.encode(&mut w)?; } for text_chunk in &self.compressed_latin1_text { text_chunk.encode(&mut w)?; } for text_chunk in &self.utf8_text { text_chunk.encode(&mut w)?; } Ok(()) } } impl BytesPerPixel { pub(crate) fn from_usize(bpp: usize) -> Self { match bpp { 1 => BytesPerPixel::One, 2 => BytesPerPixel::Two, 3 => BytesPerPixel::Three, 4 => BytesPerPixel::Four, 6 => BytesPerPixel::Six, // Only rgb×16bit 8 => BytesPerPixel::Eight, // Only rgba×16bit _ => unreachable!("Not a possible byte rounded pixel width"), } } pub(crate) fn into_usize(self) -> usize { self as usize } } bitflags::bitflags! { /// Output transformations /// /// Many flags from libpng are not yet supported. A PR discussing/adding them would be nice. /// #[doc = " ```c /// Discard the alpha channel const STRIP_ALPHA = 0x0002; // read only /// Expand 1; 2 and 4-bit samples to bytes const PACKING = 0x0004; // read and write /// Change order of packed pixels to LSB first const PACKSWAP = 0x0008; // read and write /// Invert monochrome images const INVERT_MONO = 0x0020; // read and write /// Normalize pixels to the sBIT depth const SHIFT = 0x0040; // read and write /// Flip RGB to BGR; RGBA to BGRA const BGR = 0x0080; // read and write /// Flip RGBA to ARGB or GA to AG const SWAP_ALPHA = 0x0100; // read and write /// Byte-swap 16-bit samples const SWAP_ENDIAN = 0x0200; // read and write /// Change alpha from opacity to transparency const INVERT_ALPHA = 0x0400; // read and write const STRIP_FILLER = 0x0800; // write only const STRIP_FILLER_BEFORE = 0x0800; // write only const STRIP_FILLER_AFTER = 0x1000; // write only const GRAY_TO_RGB = 0x2000; // read only const EXPAND_16 = 0x4000; // read only /// Similar to STRIP_16 but in libpng considering gamma? /// Not entirely sure the documentation says it is more /// accurate but doesn't say precisely how. const SCALE_16 = 0x8000; // read only ``` "] pub struct Transformations: u32 { /// No transformation const IDENTITY = 0x00000; // read and write */ /// Strip 16-bit samples to 8 bits const STRIP_16 = 0x00001; // read only */ /// Expand paletted images to RGB; expand grayscale images of /// less than 8-bit depth to 8-bit depth; and expand tRNS chunks /// to alpha channels. const EXPAND = 0x00010; // read only */ /// Expand paletted images to include an alpha channel. Implies `EXPAND`. const ALPHA = 0x10000; // read only */ } } impl Transformations { /// Transform every input to 8bit grayscale or color. /// /// This sets `EXPAND` and `STRIP_16` which is similar to the default transformation used by /// this library prior to `0.17`. pub fn normalize_to_color8() -> Transformations { Transformations::EXPAND | Transformations::STRIP_16 } } /// Instantiate the default transformations, the identity transform. impl Default for Transformations { fn default() -> Transformations { Transformations::IDENTITY } } #[derive(Debug)] pub struct ParameterError { inner: ParameterErrorKind, } #[derive(Debug)] pub(crate) enum ParameterErrorKind { /// A provided buffer must be have the exact size to hold the image data. Where the buffer can /// be allocated by the caller, they must ensure that it has a minimum size as hinted previously. /// Even though the size is calculated from image data, this does counts as a parameter error /// because they must react to a value produced by this library, which can have been subjected /// to limits. ImageBufferSize { expected: usize, actual: usize }, /// A bit like return `None` from an iterator. /// We use it to differentiate between failing to seek to the next image in a sequence and the /// absence of a next image. This is an error of the caller because they should have checked /// the number of images by inspecting the header data returned when opening the image. This /// library will perform the checks necessary to ensure that data was accurate or error with a /// format error otherwise. PolledAfterEndOfImage, /// Attempt to continue decoding after a fatal, non-resumable error was reported (e.g. after /// [`DecodingError::Format`]). The only case when it is possible to resume after an error /// is an `UnexpectedEof` scenario - see [`DecodingError::IoError`]. PolledAfterFatalError, } impl From for ParameterError { fn from(inner: ParameterErrorKind) -> Self { ParameterError { inner } } } impl fmt::Display for ParameterError { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { use ParameterErrorKind::*; match self.inner { ImageBufferSize { expected, actual } => { write!(fmt, "wrong data size, expected {} got {}", expected, actual) } PolledAfterEndOfImage => write!(fmt, "End of image has been reached"), PolledAfterFatalError => { write!(fmt, "A fatal decoding error has been encounted earlier") } } } } png-0.17.16/src/decoder/interlace_info.rs000064400000000000000000000063431046102023000163150ustar 00000000000000use std::ops::Range; use crate::adam7::{Adam7Info, Adam7Iterator}; /// Describes which interlacing algorithm applies to a decoded row. /// /// PNG (2003) specifies two interlace modes, but reserves future extensions. /// /// See also [Reader.next_interlaced_row](crate::Reader::next_interlaced_row). #[derive(Clone, Copy, Debug)] pub enum InterlaceInfo { /// The `null` method means no interlacing. Null(NullInfo), /// [The `Adam7` algorithm](https://en.wikipedia.org/wiki/Adam7_algorithm) derives its name /// from doing 7 passes over the image, only decoding a subset of all pixels in each pass. /// The following table shows pictorially what parts of each 8x8 area of the image is found in /// each pass: /// /// ```txt /// 1 6 4 6 2 6 4 6 /// 7 7 7 7 7 7 7 7 /// 5 6 5 6 5 6 5 6 /// 7 7 7 7 7 7 7 7 /// 3 6 4 6 3 6 4 6 /// 7 7 7 7 7 7 7 7 /// 5 6 5 6 5 6 5 6 /// 7 7 7 7 7 7 7 7 /// ``` Adam7(Adam7Info), } #[derive(Clone, Copy, Debug)] pub struct NullInfo { line: u32, } impl InterlaceInfo { pub(crate) fn line_number(&self) -> u32 { match self { InterlaceInfo::Null(NullInfo { line }) => *line, InterlaceInfo::Adam7(Adam7Info { line, .. }) => *line, } } pub(crate) fn get_adam7_info(&self) -> Option<&Adam7Info> { match self { InterlaceInfo::Null(_) => None, InterlaceInfo::Adam7(adam7info) => Some(adam7info), } } } pub(crate) struct InterlaceInfoIter(IterImpl); impl InterlaceInfoIter { pub fn empty() -> Self { Self(IterImpl::None(0..0)) } pub fn new(width: u32, height: u32, interlaced: bool) -> Self { if interlaced { Self(IterImpl::Adam7(Adam7Iterator::new(width, height))) } else { Self(IterImpl::None(0..height)) } } } impl Iterator for InterlaceInfoIter { type Item = InterlaceInfo; fn next(&mut self) -> Option { match self.0 { IterImpl::Adam7(ref mut adam7) => Some(InterlaceInfo::Adam7(adam7.next()?)), IterImpl::None(ref mut height) => Some(InterlaceInfo::Null(NullInfo { line: height.next()?, })), } } } enum IterImpl { None(Range), Adam7(Adam7Iterator), } #[cfg(test)] mod test { use super::*; #[test] fn null() { assert_eq!( InterlaceInfoIter::new(8, 8, false) .map(|info| info.line_number()) .collect::>(), vec![0, 1, 2, 3, 4, 5, 6, 7], ); } #[test] fn adam7() { assert_eq!( InterlaceInfoIter::new(8, 8, true) .map(|info| info.line_number()) .collect::>(), vec![ 0, // pass 1 0, // pass 2 0, // pass 3 0, 1, // pass 4 0, 1, // pass 5 0, 1, 2, 3, // pass 6 0, 1, 2, 3, // pass 7 ], ); } #[test] fn empty() { assert_eq!( InterlaceInfoIter::empty() .map(|info| info.line_number()) .collect::>(), vec![], ); } } png-0.17.16/src/decoder/mod.rs000064400000000000000000000575771046102023000141320ustar 00000000000000mod interlace_info; mod read_decoder; pub(crate) mod stream; pub(crate) mod transform; mod unfiltering_buffer; mod zlib; use self::read_decoder::{ImageDataCompletionStatus, ReadDecoder}; use self::stream::{DecodeOptions, DecodingError, FormatErrorInner, CHUNK_BUFFER_SIZE}; use self::transform::{create_transform_fn, TransformFn}; use self::unfiltering_buffer::UnfilteringBuffer; use std::io::Read; use std::mem; use crate::adam7::{self, Adam7Info}; use crate::common::{ BitDepth, BytesPerPixel, ColorType, Info, ParameterErrorKind, Transformations, }; use crate::FrameControl; pub use interlace_info::InterlaceInfo; use interlace_info::InterlaceInfoIter; /* pub enum InterlaceHandling { /// Outputs the raw rows RawRows, /// Fill missing the pixels from the existing ones Rectangle, /// Only fill the needed pixels Sparkle } */ /// Output info. /// /// This describes one particular frame of the image that was written into the output buffer. #[derive(Debug, PartialEq, Eq)] pub struct OutputInfo { /// The pixel width of this frame. pub width: u32, /// The pixel height of this frame. pub height: u32, /// The chosen output color type. pub color_type: ColorType, /// The chosen output bit depth. pub bit_depth: BitDepth, /// The byte count of each scan line in the image. pub line_size: usize, } impl OutputInfo { /// Returns the size needed to hold a decoded frame /// If the output buffer was larger then bytes after this count should be ignored. They may /// still have been changed. pub fn buffer_size(&self) -> usize { self.line_size * self.height as usize } } #[derive(Clone, Copy, Debug)] /// Limits on the resources the `Decoder` is allowed too use pub struct Limits { /// maximum number of bytes the decoder is allowed to allocate, default is 64Mib pub bytes: usize, } impl Limits { pub(crate) fn reserve_bytes(&mut self, bytes: usize) -> Result<(), DecodingError> { if self.bytes >= bytes { self.bytes -= bytes; Ok(()) } else { Err(DecodingError::LimitsExceeded) } } } impl Default for Limits { fn default() -> Limits { Limits { bytes: 1024 * 1024 * 64, } } } /// PNG Decoder pub struct Decoder { read_decoder: ReadDecoder, /// Output transformations transform: Transformations, } /// A row of data with interlace information attached. #[derive(Clone, Copy, Debug)] pub struct InterlacedRow<'data> { data: &'data [u8], interlace: InterlaceInfo, } impl<'data> InterlacedRow<'data> { pub fn data(&self) -> &'data [u8] { self.data } pub fn interlace(&self) -> &InterlaceInfo { &self.interlace } } /// A row of data without interlace information. #[derive(Clone, Copy, Debug)] pub struct Row<'data> { data: &'data [u8], } impl<'data> Row<'data> { pub fn data(&self) -> &'data [u8] { self.data } } impl Decoder { /// Create a new decoder configuration with default limits. pub fn new(r: R) -> Decoder { Decoder::new_with_limits(r, Limits::default()) } /// Create a new decoder configuration with custom limits. pub fn new_with_limits(r: R, limits: Limits) -> Decoder { let mut read_decoder = ReadDecoder::new(r); read_decoder.set_limits(limits); Decoder { read_decoder, transform: Transformations::IDENTITY, } } /// Create a new decoder configuration with custom `DecodeOptions`. pub fn new_with_options(r: R, decode_options: DecodeOptions) -> Decoder { let mut read_decoder = ReadDecoder::with_options(r, decode_options); read_decoder.set_limits(Limits::default()); Decoder { read_decoder, transform: Transformations::IDENTITY, } } /// Limit resource usage. /// /// Note that your allocations, e.g. when reading into a pre-allocated buffer, are __NOT__ /// considered part of the limits. Nevertheless, required intermediate buffers such as for /// singular lines is checked against the limit. /// /// Note that this is a best-effort basis. /// /// ``` /// use std::fs::File; /// use png::{Decoder, Limits}; /// // This image is 32×32, 1bit per pixel. The reader buffers one row which requires 4 bytes. /// let mut limits = Limits::default(); /// limits.bytes = 3; /// let mut decoder = Decoder::new_with_limits(File::open("tests/pngsuite/basi0g01.png").unwrap(), limits); /// assert!(decoder.read_info().is_err()); /// /// // This image is 32x32 pixels, so the decoder will allocate less than 10Kib /// let mut limits = Limits::default(); /// limits.bytes = 10*1024; /// let mut decoder = Decoder::new_with_limits(File::open("tests/pngsuite/basi0g01.png").unwrap(), limits); /// assert!(decoder.read_info().is_ok()); /// ``` pub fn set_limits(&mut self, limits: Limits) { self.read_decoder.set_limits(limits); } /// Read the PNG header and return the information contained within. /// /// Most image metadata will not be read until `read_info` is called, so those fields will be /// None or empty. pub fn read_header_info(&mut self) -> Result<&Info<'static>, DecodingError> { self.read_decoder.read_header_info() } /// Reads all meta data until the first IDAT chunk pub fn read_info(mut self) -> Result, DecodingError> { self.read_header_info()?; let mut reader = Reader { decoder: self.read_decoder, bpp: BytesPerPixel::One, subframe: SubframeInfo::not_yet_init(), remaining_frames: 0, // Temporary value - fixed below after reading `acTL` and `fcTL`. unfiltering_buffer: UnfilteringBuffer::new(), transform: self.transform, transform_fn: None, scratch_buffer: Vec::new(), finished: false, }; // Check if the decoding buffer of a single raw line has a valid size. if reader.info().checked_raw_row_length().is_none() { return Err(DecodingError::LimitsExceeded); } // Check if the output buffer has a valid size. let (width, height) = reader.info().size(); let (color, depth) = reader.output_color_type(); let rowlen = color .checked_raw_row_length(depth, width) .ok_or(DecodingError::LimitsExceeded)? - 1; let height: usize = std::convert::TryFrom::try_from(height).map_err(|_| DecodingError::LimitsExceeded)?; if rowlen.checked_mul(height).is_none() { return Err(DecodingError::LimitsExceeded); } reader.read_until_image_data()?; reader.remaining_frames = match reader.info().animation_control.as_ref() { None => 1, // No `acTL` => only expecting `IDAT` frame. Some(animation) => { let mut num_frames = animation.num_frames as usize; if reader.info().frame_control.is_none() { // No `fcTL` before `IDAT` => `IDAT` is not part of the animation, but // represents an *extra*, default frame for non-APNG-aware decoders. num_frames += 1; } num_frames } }; Ok(reader) } /// Set the allowed and performed transformations. /// /// A transformation is a pre-processing on the raw image data modifying content or encoding. /// Many options have an impact on memory or CPU usage during decoding. pub fn set_transformations(&mut self, transform: Transformations) { self.transform = transform; } /// Set the decoder to ignore all text chunks while parsing. /// /// eg. /// ``` /// use std::fs::File; /// use png::Decoder; /// let mut decoder = Decoder::new(File::open("tests/pngsuite/basi0g01.png").unwrap()); /// decoder.set_ignore_text_chunk(true); /// assert!(decoder.read_info().is_ok()); /// ``` pub fn set_ignore_text_chunk(&mut self, ignore_text_chunk: bool) { self.read_decoder.set_ignore_text_chunk(ignore_text_chunk); } /// Set the decoder to ignore iccp chunks while parsing. /// /// eg. /// ``` /// use std::fs::File; /// use png::Decoder; /// let mut decoder = Decoder::new(File::open("tests/iccp/broken_iccp.png").unwrap()); /// decoder.set_ignore_iccp_chunk(true); /// assert!(decoder.read_info().is_ok()); /// ``` pub fn set_ignore_iccp_chunk(&mut self, ignore_iccp_chunk: bool) { self.read_decoder.set_ignore_iccp_chunk(ignore_iccp_chunk); } /// Set the decoder to ignore and not verify the Adler-32 checksum /// and CRC code. pub fn ignore_checksums(&mut self, ignore_checksums: bool) { self.read_decoder.ignore_checksums(ignore_checksums); } } /// PNG reader (mostly high-level interface) /// /// Provides a high level that iterates over lines or whole images. pub struct Reader { decoder: ReadDecoder, bpp: BytesPerPixel, subframe: SubframeInfo, /// How many frames remain to be decoded. Decremented after each `IDAT` or `fdAT` sequence. remaining_frames: usize, /// Buffer with not-yet-`unfilter`-ed image rows unfiltering_buffer: UnfilteringBuffer, /// Output transformations transform: Transformations, /// Function that can transform decompressed, unfiltered rows into final output. /// See the `transform.rs` module for more details. transform_fn: Option, /// This buffer is only used so that `next_row` and `next_interlaced_row` can return reference /// to a byte slice. In a future version of this library, this buffer will be removed and /// `next_row` and `next_interlaced_row` will write directly into a user provided output buffer. scratch_buffer: Vec, /// Whether `ImageEnd` was already reached by `fn finish`. finished: bool, } /// The subframe specific information. /// /// In APNG the frames are constructed by combining previous frame and a new subframe (through a /// combination of `dispose_op` and `overlay_op`). These sub frames specify individual dimension /// information and reuse the global interlace options. This struct encapsulates the state of where /// in a particular IDAT-frame or subframe we are. struct SubframeInfo { width: u32, height: u32, rowlen: usize, current_interlace_info: Option, interlace_info_iter: InterlaceInfoIter, consumed_and_flushed: bool, } impl Reader { /// Advances to the start of the next animation frame and /// returns a reference to the `FrameControl` info that describes it. /// Skips and discards the image data of the previous frame if necessary. /// /// Returns a [`ParameterError`] when there are no more animation frames. /// To avoid this the caller can check if [`Info::animation_control`] exists /// and consult [`AnimationControl::num_frames`]. pub fn next_frame_info(&mut self) -> Result<&FrameControl, DecodingError> { let remaining_frames = if self.subframe.consumed_and_flushed { self.remaining_frames } else { // One remaining frame will be consumed by the `finish_decoding` call below. self.remaining_frames - 1 }; if remaining_frames == 0 { return Err(DecodingError::Parameter( ParameterErrorKind::PolledAfterEndOfImage.into(), )); } if !self.subframe.consumed_and_flushed { self.subframe.current_interlace_info = None; self.finish_decoding()?; } self.read_until_image_data()?; // The PNG standard (and `StreamingDecoder `) guarantes that there is an `fcTL` chunk // before the start of image data in a sequence of `fdAT` chunks. Therefore `unwrap` // below is guaranteed to not panic. Ok(self.info().frame_control.as_ref().unwrap()) } /// Reads all meta data until the next frame data starts. /// Requires IHDR before the IDAT and fcTL before fdAT. fn read_until_image_data(&mut self) -> Result<(), DecodingError> { self.decoder.read_until_image_data()?; self.subframe = SubframeInfo::new(self.info()); self.bpp = self.info().bpp_in_prediction(); self.unfiltering_buffer = UnfilteringBuffer::new(); // Allocate output buffer. let buflen = self.output_line_size(self.subframe.width); self.decoder.reserve_bytes(buflen)?; Ok(()) } /// Get information on the image. /// /// The structure will change as new frames of an animated image are decoded. pub fn info(&self) -> &Info<'static> { self.decoder.info().unwrap() } /// Decodes the next frame into `buf`. /// /// Note that this decodes raw subframes that need to be mixed according to blend-op and /// dispose-op by the caller. /// /// The caller must always provide a buffer large enough to hold a complete frame (the APNG /// specification restricts subframes to the dimensions given in the image header). The region /// that has been written be checked afterwards by calling `info` after a successful call and /// inspecting the `frame_control` data. This requirement may be lifted in a later version of /// `png`. /// /// Output lines will be written in row-major, packed matrix with width and height of the read /// frame (or subframe), all samples are in big endian byte order where this matters. pub fn next_frame(&mut self, buf: &mut [u8]) -> Result { if self.remaining_frames == 0 { return Err(DecodingError::Parameter( ParameterErrorKind::PolledAfterEndOfImage.into(), )); } else if self.subframe.consumed_and_flushed { // Advance until the next `fdAT` // (along the way we should encounter the fcTL for this frame). self.read_until_image_data()?; } if buf.len() < self.output_buffer_size() { return Err(DecodingError::Parameter( ParameterErrorKind::ImageBufferSize { expected: buf.len(), actual: self.output_buffer_size(), } .into(), )); } let (color_type, bit_depth) = self.output_color_type(); let output_info = OutputInfo { width: self.subframe.width, height: self.subframe.height, color_type, bit_depth, line_size: self.output_line_size(self.subframe.width), }; if self.info().interlaced { let stride = self.output_line_size(self.info().width); let samples = color_type.samples() as u8; let bits_pp = samples * (bit_depth as u8); while let Some(InterlacedRow { data: row, interlace, .. }) = self.next_interlaced_row()? { // `unwrap` won't panic, because we checked `self.info().interlaced` above. let adam7info = interlace.get_adam7_info().unwrap(); adam7::expand_pass(buf, stride, row, adam7info, bits_pp); } } else { let current_interlace_info = self.subframe.current_interlace_info.as_ref(); let already_done_rows = current_interlace_info .map(|info| info.line_number()) .unwrap_or(self.subframe.height); for row in buf .chunks_exact_mut(output_info.line_size) .take(self.subframe.height as usize) .skip(already_done_rows as usize) { self.next_interlaced_row_impl(self.subframe.rowlen, row)?; } } // Advance over the rest of data for this (sub-)frame. self.finish_decoding()?; Ok(output_info) } fn mark_subframe_as_consumed_and_flushed(&mut self) { assert!(self.remaining_frames > 0); self.remaining_frames -= 1; self.subframe.consumed_and_flushed = true; } /// Advance over the rest of data for this (sub-)frame. /// Called after decoding the last row of a frame. fn finish_decoding(&mut self) -> Result<(), DecodingError> { // Double-check that all rows of this frame have been decoded (i.e. that the potential // `finish_decoding` call below won't be discarding any data). assert!(self.subframe.current_interlace_info.is_none()); // Discard the remaining data in the current sequence of `IDAT` or `fdAT` chunks. if !self.subframe.consumed_and_flushed { self.decoder.finish_decoding_image_data()?; self.mark_subframe_as_consumed_and_flushed(); } Ok(()) } /// Returns the next processed row of the image pub fn next_row(&mut self) -> Result, DecodingError> { self.next_interlaced_row() .map(|v| v.map(|v| Row { data: v.data })) } /// Returns the next processed row of the image pub fn next_interlaced_row(&mut self) -> Result, DecodingError> { let interlace = match self.subframe.current_interlace_info.as_ref() { None => { self.finish_decoding()?; return Ok(None); } Some(interlace) => *interlace, }; if interlace.line_number() == 0 { self.unfiltering_buffer.reset_prev_row(); } let rowlen = match interlace { InterlaceInfo::Null(_) => self.subframe.rowlen, InterlaceInfo::Adam7(Adam7Info { width, .. }) => { self.info().raw_row_length_from_width(width) } }; let width = match interlace { InterlaceInfo::Adam7(Adam7Info { width, .. }) => width, InterlaceInfo::Null(_) => self.subframe.width, }; let output_line_size = self.output_line_size(width); // TODO: change the interface of `next_interlaced_row` to take an output buffer instead of // making us return a reference to a buffer that we own. let mut output_buffer = mem::take(&mut self.scratch_buffer); output_buffer.resize(output_line_size, 0u8); let ret = self.next_interlaced_row_impl(rowlen, &mut output_buffer); self.scratch_buffer = output_buffer; ret?; Ok(Some(InterlacedRow { data: &self.scratch_buffer[..output_line_size], interlace, })) } /// Read the rest of the image and chunks and finish up, including text chunks or others /// This will discard the rest of the image if the image is not read already with [`Reader::next_frame`], [`Reader::next_row`] or [`Reader::next_interlaced_row`] pub fn finish(&mut self) -> Result<(), DecodingError> { if self.finished { return Err(DecodingError::Parameter( ParameterErrorKind::PolledAfterEndOfImage.into(), )); } self.remaining_frames = 0; self.unfiltering_buffer = UnfilteringBuffer::new(); self.decoder.read_until_end_of_input()?; self.finished = true; Ok(()) } /// Fetch the next interlaced row and filter it according to our own transformations. fn next_interlaced_row_impl( &mut self, rowlen: usize, output_buffer: &mut [u8], ) -> Result<(), DecodingError> { self.next_raw_interlaced_row(rowlen)?; let row = self.unfiltering_buffer.prev_row(); assert_eq!(row.len(), rowlen - 1); // Apply transformations and write resulting data to buffer. let transform_fn = { if self.transform_fn.is_none() { self.transform_fn = Some(create_transform_fn(self.info(), self.transform)?); } self.transform_fn.as_deref().unwrap() }; transform_fn(row, output_buffer, self.info()); self.subframe.current_interlace_info = self.subframe.interlace_info_iter.next(); Ok(()) } /// Returns the color type and the number of bits per sample /// of the data returned by `Reader::next_row` and Reader::frames`. pub fn output_color_type(&self) -> (ColorType, BitDepth) { use crate::common::ColorType::*; let t = self.transform; let info = self.info(); if t == Transformations::IDENTITY { (info.color_type, info.bit_depth) } else { let bits = match info.bit_depth as u8 { 16 if t.intersects(Transformations::STRIP_16) => 8, n if n < 8 && (t.contains(Transformations::EXPAND) || t.contains(Transformations::ALPHA)) => { 8 } n => n, }; let color_type = if t.contains(Transformations::EXPAND) || t.contains(Transformations::ALPHA) { let has_trns = info.trns.is_some() || t.contains(Transformations::ALPHA); match info.color_type { Grayscale if has_trns => GrayscaleAlpha, Rgb if has_trns => Rgba, Indexed if has_trns => Rgba, Indexed => Rgb, ct => ct, } } else { info.color_type }; (color_type, BitDepth::from_u8(bits).unwrap()) } } /// Returns the number of bytes required to hold a deinterlaced image frame /// that is decoded using the given input transformations. pub fn output_buffer_size(&self) -> usize { let (width, height) = self.info().size(); let size = self.output_line_size(width); size * height as usize } /// Returns the number of bytes required to hold a deinterlaced row. pub fn output_line_size(&self, width: u32) -> usize { let (color, depth) = self.output_color_type(); color.raw_row_length_from_width(depth, width) - 1 } /// Unfilter the next raw interlaced row into `self.unfiltering_buffer`. fn next_raw_interlaced_row(&mut self, rowlen: usize) -> Result<(), DecodingError> { // Read image data until we have at least one full row (but possibly more than one). while self.unfiltering_buffer.curr_row_len() < rowlen { if self.subframe.consumed_and_flushed { return Err(DecodingError::Format( FormatErrorInner::NoMoreImageData.into(), )); } match self .decoder .decode_image_data(self.unfiltering_buffer.as_mut_vec())? { ImageDataCompletionStatus::ExpectingMoreData => (), ImageDataCompletionStatus::Done => self.mark_subframe_as_consumed_and_flushed(), } } self.unfiltering_buffer.unfilter_curr_row(rowlen, self.bpp) } } impl SubframeInfo { fn not_yet_init() -> Self { SubframeInfo { width: 0, height: 0, rowlen: 0, current_interlace_info: None, interlace_info_iter: InterlaceInfoIter::empty(), consumed_and_flushed: false, } } fn new(info: &Info) -> Self { // The apng fctnl overrides width and height. // All other data is set by the main info struct. let (width, height) = if let Some(fc) = info.frame_control { (fc.width, fc.height) } else { (info.width, info.height) }; let mut interlace_info_iter = InterlaceInfoIter::new(width, height, info.interlaced); let current_interlace_info = interlace_info_iter.next(); SubframeInfo { width, height, rowlen: info.raw_row_length_from_width(width), current_interlace_info, interlace_info_iter, consumed_and_flushed: false, } } } png-0.17.16/src/decoder/read_decoder.rs000064400000000000000000000151141046102023000157300ustar 00000000000000use super::stream::{ DecodeOptions, Decoded, DecodingError, FormatErrorInner, StreamingDecoder, CHUNK_BUFFER_SIZE, }; use super::Limits; use std::io::{BufRead, BufReader, ErrorKind, Read}; use crate::chunk; use crate::common::Info; /// Helper for encapsulating reading input from `Read` and feeding it into a `StreamingDecoder` /// while hiding low-level `Decoded` events and only exposing a few high-level reading operations /// like: /// /// * `read_header_info` - reading until `IHDR` chunk /// * `read_until_image_data` - reading until `IDAT` / `fdAT` sequence /// * `decode_image_data` - reading from `IDAT` / `fdAT` sequence into `Vec` /// * `finish_decoding_image_data()` - discarding remaining data from `IDAT` / `fdAT` sequence /// * `read_until_end_of_input()` - reading until `IEND` chunk pub(crate) struct ReadDecoder { reader: BufReader, decoder: StreamingDecoder, } impl ReadDecoder { pub fn new(r: R) -> Self { Self { reader: BufReader::with_capacity(CHUNK_BUFFER_SIZE, r), decoder: StreamingDecoder::new(), } } pub fn with_options(r: R, options: DecodeOptions) -> Self { let mut decoder = StreamingDecoder::new_with_options(options); decoder.limits = Limits::default(); Self { reader: BufReader::with_capacity(CHUNK_BUFFER_SIZE, r), decoder, } } pub fn set_limits(&mut self, limits: Limits) { self.decoder.limits = limits; } pub fn reserve_bytes(&mut self, bytes: usize) -> Result<(), DecodingError> { self.decoder.limits.reserve_bytes(bytes) } pub fn set_ignore_text_chunk(&mut self, ignore_text_chunk: bool) { self.decoder.set_ignore_text_chunk(ignore_text_chunk); } pub fn set_ignore_iccp_chunk(&mut self, ignore_iccp_chunk: bool) { self.decoder.set_ignore_iccp_chunk(ignore_iccp_chunk); } pub fn ignore_checksums(&mut self, ignore_checksums: bool) { self.decoder.set_ignore_adler32(ignore_checksums); self.decoder.set_ignore_crc(ignore_checksums); } /// Returns the next decoded chunk. If the chunk is an ImageData chunk, its contents are written /// into image_data. fn decode_next(&mut self, image_data: &mut Vec) -> Result { let (consumed, result) = { let buf = self.reader.fill_buf()?; if buf.is_empty() { return Err(DecodingError::IoError(ErrorKind::UnexpectedEof.into())); } self.decoder.update(buf, image_data)? }; self.reader.consume(consumed); Ok(result) } fn decode_next_without_image_data(&mut self) -> Result { // This is somewhat ugly. The API requires us to pass a buffer to decode_next but we // know that we will stop before reading any image data from the stream. Thus pass an // empty buffer and assert that remains empty. let mut buf = Vec::new(); let state = self.decode_next(&mut buf)?; assert!(buf.is_empty()); Ok(state) } fn decode_next_and_discard_image_data(&mut self) -> Result { let mut to_be_discarded = Vec::new(); self.decode_next(&mut to_be_discarded) } /// Reads until the end of `IHDR` chunk. /// /// Prerequisite: None (idempotent). pub fn read_header_info(&mut self) -> Result<&Info<'static>, DecodingError> { while self.info().is_none() { if let Decoded::ImageEnd = self.decode_next_without_image_data()? { unreachable!() } } Ok(self.info().unwrap()) } /// Reads until the start of the next `IDAT` or `fdAT` chunk. /// /// Prerequisite: **Not** within `IDAT` / `fdAT` chunk sequence. pub fn read_until_image_data(&mut self) -> Result<(), DecodingError> { loop { match self.decode_next_without_image_data()? { Decoded::ChunkBegin(_, chunk::IDAT) | Decoded::ChunkBegin(_, chunk::fdAT) => break, Decoded::ImageEnd => { return Err(DecodingError::Format( FormatErrorInner::MissingImageData.into(), )) } // Ignore all other chunk events. Any other chunk may be between IDAT chunks, fdAT // chunks and their control chunks. _ => {} } } Ok(()) } /// Reads `image_data` and reports whether there may be additional data afterwards (i.e. if it /// is okay to call `decode_image_data` and/or `finish_decoding_image_data` again).. /// /// Prerequisite: Input is currently positioned within `IDAT` / `fdAT` chunk sequence. pub fn decode_image_data( &mut self, image_data: &mut Vec, ) -> Result { match self.decode_next(image_data)? { Decoded::ImageData => Ok(ImageDataCompletionStatus::ExpectingMoreData), Decoded::ImageDataFlushed => Ok(ImageDataCompletionStatus::Done), // Ignore other events that may happen within an `IDAT` / `fdAT` chunks sequence. Decoded::Nothing | Decoded::ChunkComplete(_, _) | Decoded::ChunkBegin(_, _) | Decoded::PartialChunk(_) => Ok(ImageDataCompletionStatus::ExpectingMoreData), // Other kinds of events shouldn't happen, unless we have been (incorrectly) called // when outside of a sequence of `IDAT` / `fdAT` chunks. unexpected => unreachable!("{:?}", unexpected), } } /// Consumes and discards the rest of an `IDAT` / `fdAT` chunk sequence. /// /// Prerequisite: Input is currently positioned within `IDAT` / `fdAT` chunk sequence. pub fn finish_decoding_image_data(&mut self) -> Result<(), DecodingError> { loop { let mut to_be_discarded = vec![]; if let ImageDataCompletionStatus::Done = self.decode_image_data(&mut to_be_discarded)? { return Ok(()); } } } /// Reads until the `IEND` chunk. /// /// Prerequisite: `IEND` chunk hasn't been reached yet. pub fn read_until_end_of_input(&mut self) -> Result<(), DecodingError> { while !matches!( self.decode_next_and_discard_image_data()?, Decoded::ImageEnd ) {} Ok(()) } pub fn info(&self) -> Option<&Info<'static>> { self.decoder.info.as_ref() } } #[derive(Debug, Eq, PartialEq)] pub(crate) enum ImageDataCompletionStatus { ExpectingMoreData, Done, } png-0.17.16/src/decoder/stream.rs000064400000000000000000003426001046102023000146260ustar 00000000000000use std::convert::TryInto; use std::error; use std::fmt; use std::io; use std::{borrow::Cow, cmp::min}; use crc32fast::Hasher as Crc32; use super::zlib::ZlibStream; use crate::chunk::{self, ChunkType, IDAT, IEND, IHDR}; use crate::common::{ AnimationControl, BitDepth, BlendOp, ColorType, ContentLightLevelInfo, DisposeOp, FrameControl, Info, MasteringDisplayColorVolume, ParameterError, ParameterErrorKind, PixelDimensions, ScaledFloat, SourceChromaticities, Unit, }; use crate::text_metadata::{ITXtChunk, TEXtChunk, TextDecodingError, ZTXtChunk}; use crate::traits::ReadBytesExt; use crate::{CodingIndependentCodePoints, Limits}; /// TODO check if these size are reasonable pub const CHUNK_BUFFER_SIZE: usize = 32 * 1024; /// Determines if checksum checks should be disabled globally. /// /// This is used only in fuzzing. `afl` automatically adds `--cfg fuzzing` to RUSTFLAGS which can /// be used to detect that build. const CHECKSUM_DISABLED: bool = cfg!(fuzzing); /// Kind of `u32` value that is being read via `State::U32`. #[derive(Debug)] enum U32ValueKind { /// First 4 bytes of the PNG signature - see /// http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html#PNG-file-signature Signature1stU32, /// Second 4 bytes of the PNG signature - see /// http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html#PNG-file-signature Signature2ndU32, /// Chunk length - see /// http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html#Chunk-layout Length, /// Chunk type - see /// http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html#Chunk-layout Type { length: u32 }, /// Chunk checksum - see /// http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html#Chunk-layout Crc(ChunkType), /// Sequence number from an `fdAT` chunk - see /// https://wiki.mozilla.org/APNG_Specification#.60fdAT.60:_The_Frame_Data_Chunk ApngSequenceNumber, } #[derive(Debug)] enum State { /// In this state we are reading a u32 value from external input. We start with /// `accumulated_count` set to `0`. After reading or accumulating the required 4 bytes we will /// call `parse_32` which will then move onto the next state. U32 { kind: U32ValueKind, bytes: [u8; 4], accumulated_count: usize, }, /// In this state we are reading chunk data from external input, and appending it to /// `ChunkState::raw_bytes`. ReadChunkData(ChunkType), /// In this state we check if all chunk data has been already read into `ChunkState::raw_bytes` /// and if so then we parse the chunk. Otherwise, we go back to the `ReadChunkData` state. ParseChunkData(ChunkType), /// In this state we are reading image data from external input and feeding it directly into /// `StreamingDecoder::inflater`. ImageData(ChunkType), } impl State { fn new_u32(kind: U32ValueKind) -> Self { Self::U32 { kind, bytes: [0; 4], accumulated_count: 0, } } } #[derive(Debug)] /// Result of the decoding process pub enum Decoded { /// Nothing decoded yet Nothing, Header(u32, u32, BitDepth, ColorType, bool), ChunkBegin(u32, ChunkType), ChunkComplete(u32, ChunkType), PixelDimensions(PixelDimensions), AnimationControl(AnimationControl), FrameControl(FrameControl), /// Decoded raw image data. ImageData, /// The last of a consecutive chunk of IDAT was done. /// This is distinct from ChunkComplete which only marks that some IDAT chunk was completed but /// not that no additional IDAT chunk follows. ImageDataFlushed, PartialChunk(ChunkType), ImageEnd, } /// Any kind of error during PNG decoding. /// /// This enumeration provides a very rough analysis on the origin of the failure. That is, each /// variant corresponds to one kind of actor causing the error. It should not be understood as a /// direct blame but can inform the search for a root cause or if such a search is required. #[derive(Debug)] pub enum DecodingError { /// An error in IO of the underlying reader. /// /// Note that some IO errors may be recoverable - decoding may be retried after the /// error is resolved. For example, decoding from a slow stream of data (e.g. decoding from a /// network stream) may occasionally result in [std::io::ErrorKind::UnexpectedEof] kind of /// error, but decoding can resume when more data becomes available. IoError(io::Error), /// The input image was not a valid PNG. /// /// There isn't a lot that can be done here, except if the program itself was responsible for /// creating this image then investigate the generator. This is internally implemented with a /// large Enum. If You are interested in accessing some of the more exact information on the /// variant then we can discuss in an issue. Format(FormatError), /// An interface was used incorrectly. /// /// This is used in cases where it's expected that the programmer might trip up and stability /// could be affected. For example when: /// /// * The decoder is polled for more animation frames despite being done (or not being animated /// in the first place). /// * The output buffer does not have the required size. /// /// As a rough guideline for introducing new variants parts of the requirements are dynamically /// derived from the (untrusted) input data while the other half is from the caller. In the /// above cases the number of frames respectively the size is determined by the file while the /// number of calls /// /// If you're an application you might want to signal that a bug report is appreciated. Parameter(ParameterError), /// The image would have required exceeding the limits configured with the decoder. /// /// Note that Your allocations, e.g. when reading into a pre-allocated buffer, is __NOT__ /// considered part of the limits. Nevertheless, required intermediate buffers such as for /// singular lines is checked against the limit. /// /// Note that this is a best-effort basis. LimitsExceeded, } #[derive(Debug)] pub struct FormatError { inner: FormatErrorInner, } #[derive(Debug)] pub(crate) enum FormatErrorInner { /// Bad framing. CrcMismatch { /// Stored CRC32 value crc_val: u32, /// Calculated CRC32 sum crc_sum: u32, /// The chunk type that has the CRC mismatch. chunk: ChunkType, }, /// Not a PNG, the magic signature is missing. InvalidSignature, // Errors of chunk level ordering, missing etc. /// Fctl must occur if an animated chunk occurs. MissingFctl, /// Image data that was indicated in IHDR or acTL is missing. MissingImageData, /// 4.3., Must be first. ChunkBeforeIhdr { kind: ChunkType, }, /// 4.3., some chunks must be before IDAT. AfterIdat { kind: ChunkType, }, // 4.3., Some chunks must be after PLTE. BeforePlte { kind: ChunkType, }, /// 4.3., some chunks must be before PLTE. AfterPlte { kind: ChunkType, }, /// 4.3., some chunks must be between PLTE and IDAT. OutsidePlteIdat { kind: ChunkType, }, /// 4.3., some chunks must be unique. DuplicateChunk { kind: ChunkType, }, /// Specifically for fdat there is an embedded sequence number for chunks. ApngOrder { /// The sequence number in the chunk. present: u32, /// The one that should have been present. expected: u32, }, // Errors specific to particular chunk data to be validated. /// The palette did not even contain a single pixel data. ShortPalette { expected: usize, len: usize, }, /// sBIT chunk size based on color type. InvalidSbitChunkSize { color_type: ColorType, expected: usize, len: usize, }, InvalidSbit { sample_depth: BitDepth, sbit: u8, }, /// A palletized image did not have a palette. PaletteRequired, /// The color-depth combination is not valid according to Table 11.1. InvalidColorBitDepth { color_type: ColorType, bit_depth: BitDepth, }, ColorWithBadTrns(ColorType), /// The image width or height is zero. InvalidDimensions, InvalidBitDepth(u8), InvalidColorType(u8), InvalidDisposeOp(u8), InvalidBlendOp(u8), InvalidUnit(u8), /// The rendering intent of the sRGB chunk is invalid. InvalidSrgbRenderingIntent(u8), UnknownCompressionMethod(u8), UnknownFilterMethod(u8), UnknownInterlaceMethod(u8), /// The subframe is not in bounds of the image. /// TODO: fields with relevant data. BadSubFrameBounds {}, // Errors specific to the IDAT/fdAT chunks. /// The compression of the data stream was faulty. CorruptFlateStream { err: fdeflate::DecompressionError, }, /// The image data chunk was too short for the expected pixel count. NoMoreImageData, /// Bad text encoding BadTextEncoding(TextDecodingError), /// fdAT shorter than 4 bytes FdatShorterThanFourBytes, /// "11.2.4 IDAT Image data" section of the PNG spec says: There may be multiple IDAT chunks; /// if so, they shall appear consecutively with no other intervening chunks. /// `UnexpectedRestartOfDataChunkSequence{kind: IDAT}` indicates that there were "intervening /// chunks". /// /// The APNG spec doesn't directly describe an error similar to `CantInterleaveIdatChunks`, /// but we require that a new sequence of consecutive `fdAT` chunks cannot appear unless we've /// seen an `fcTL` chunk. UnexpectedRestartOfDataChunkSequence { kind: ChunkType, }, /// Failure to parse a chunk, because the chunk didn't contain enough bytes. ChunkTooShort { kind: ChunkType, }, } impl error::Error for DecodingError { fn cause(&self) -> Option<&(dyn error::Error + 'static)> { match self { DecodingError::IoError(err) => Some(err), _ => None, } } } impl fmt::Display for DecodingError { fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { use self::DecodingError::*; match self { IoError(err) => write!(fmt, "{}", err), Parameter(desc) => write!(fmt, "{}", &desc), Format(desc) => write!(fmt, "{}", desc), LimitsExceeded => write!(fmt, "limits are exceeded"), } } } impl fmt::Display for FormatError { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { use FormatErrorInner::*; match &self.inner { CrcMismatch { crc_val, crc_sum, chunk, .. } => write!( fmt, "CRC error: expected 0x{:x} have 0x{:x} while decoding {:?} chunk.", crc_val, crc_sum, chunk ), MissingFctl => write!(fmt, "fcTL chunk missing before fdAT chunk."), MissingImageData => write!(fmt, "IDAT or fdAT chunk is missing."), ChunkBeforeIhdr { kind } => write!(fmt, "{:?} chunk appeared before IHDR chunk", kind), AfterIdat { kind } => write!(fmt, "Chunk {:?} is invalid after IDAT chunk.", kind), BeforePlte { kind } => write!(fmt, "Chunk {:?} is invalid before PLTE chunk.", kind), AfterPlte { kind } => write!(fmt, "Chunk {:?} is invalid after PLTE chunk.", kind), OutsidePlteIdat { kind } => write!( fmt, "Chunk {:?} must appear between PLTE and IDAT chunks.", kind ), DuplicateChunk { kind } => write!(fmt, "Chunk {:?} must appear at most once.", kind), ApngOrder { present, expected } => write!( fmt, "Sequence is not in order, expected #{} got #{}.", expected, present, ), ShortPalette { expected, len } => write!( fmt, "Not enough palette entries, expect {} got {}.", expected, len ), InvalidSbitChunkSize {color_type, expected, len} => write!( fmt, "The size of the sBIT chunk should be {} byte(s), but {} byte(s) were provided for the {:?} color type.", expected, len, color_type ), InvalidSbit {sample_depth, sbit} => write!( fmt, "Invalid sBIT value {}. It must be greater than zero and less than the sample depth {:?}.", sbit, sample_depth ), PaletteRequired => write!(fmt, "Missing palette of indexed image."), InvalidDimensions => write!(fmt, "Invalid image dimensions"), InvalidColorBitDepth { color_type, bit_depth, } => write!( fmt, "Invalid color/depth combination in header: {:?}/{:?}", color_type, bit_depth, ), ColorWithBadTrns(color_type) => write!( fmt, "Transparency chunk found for color type {:?}.", color_type ), InvalidBitDepth(nr) => write!(fmt, "Invalid bit depth {}.", nr), InvalidColorType(nr) => write!(fmt, "Invalid color type {}.", nr), InvalidDisposeOp(nr) => write!(fmt, "Invalid dispose op {}.", nr), InvalidBlendOp(nr) => write!(fmt, "Invalid blend op {}.", nr), InvalidUnit(nr) => write!(fmt, "Invalid physical pixel size unit {}.", nr), InvalidSrgbRenderingIntent(nr) => write!(fmt, "Invalid sRGB rendering intent {}.", nr), UnknownCompressionMethod(nr) => write!(fmt, "Unknown compression method {}.", nr), UnknownFilterMethod(nr) => write!(fmt, "Unknown filter method {}.", nr), UnknownInterlaceMethod(nr) => write!(fmt, "Unknown interlace method {}.", nr), BadSubFrameBounds {} => write!(fmt, "Sub frame is out-of-bounds."), InvalidSignature => write!(fmt, "Invalid PNG signature."), NoMoreImageData => write!( fmt, "IDAT or fDAT chunk does not have enough data for image." ), CorruptFlateStream { err } => { write!(fmt, "Corrupt deflate stream. ")?; write!(fmt, "{:?}", err) } // TODO: Wrap more info in the enum variant BadTextEncoding(tde) => { match tde { TextDecodingError::Unrepresentable => { write!(fmt, "Unrepresentable data in tEXt chunk.") } TextDecodingError::InvalidKeywordSize => { write!(fmt, "Keyword empty or longer than 79 bytes.") } TextDecodingError::MissingNullSeparator => { write!(fmt, "No null separator in tEXt chunk.") } TextDecodingError::InflationError => { write!(fmt, "Invalid compressed text data.") } TextDecodingError::OutOfDecompressionSpace => { write!(fmt, "Out of decompression space. Try with a larger limit.") } TextDecodingError::InvalidCompressionMethod => { write!(fmt, "Using an unrecognized byte as compression method.") } TextDecodingError::InvalidCompressionFlag => { write!(fmt, "Using a flag that is not 0 or 255 as a compression flag for iTXt chunk.") } TextDecodingError::MissingCompressionFlag => { write!(fmt, "No compression flag in the iTXt chunk.") } } } FdatShorterThanFourBytes => write!(fmt, "fdAT chunk shorter than 4 bytes"), UnexpectedRestartOfDataChunkSequence { kind } => { write!(fmt, "Unexpected restart of {:?} chunk sequence", kind) } ChunkTooShort { kind } => { write!(fmt, "Chunk is too short: {:?}", kind) } } } } impl From for DecodingError { fn from(err: io::Error) -> DecodingError { DecodingError::IoError(err) } } impl From for DecodingError { fn from(err: FormatError) -> DecodingError { DecodingError::Format(err) } } impl From for FormatError { fn from(inner: FormatErrorInner) -> Self { FormatError { inner } } } impl From for io::Error { fn from(err: DecodingError) -> io::Error { match err { DecodingError::IoError(err) => err, err => io::Error::new(io::ErrorKind::Other, err.to_string()), } } } impl From for DecodingError { fn from(tbe: TextDecodingError) -> Self { DecodingError::Format(FormatError { inner: FormatErrorInner::BadTextEncoding(tbe), }) } } /// Decoder configuration options #[derive(Clone)] pub struct DecodeOptions { ignore_adler32: bool, ignore_crc: bool, ignore_text_chunk: bool, ignore_iccp_chunk: bool, skip_ancillary_crc_failures: bool, } impl Default for DecodeOptions { fn default() -> Self { Self { ignore_adler32: true, ignore_crc: false, ignore_text_chunk: false, ignore_iccp_chunk: false, skip_ancillary_crc_failures: true, } } } impl DecodeOptions { /// When set, the decoder will not compute and verify the Adler-32 checksum. /// /// Defaults to `true`. pub fn set_ignore_adler32(&mut self, ignore_adler32: bool) { self.ignore_adler32 = ignore_adler32; } /// When set, the decoder will not compute and verify the CRC code. /// /// Defaults to `false`. pub fn set_ignore_crc(&mut self, ignore_crc: bool) { self.ignore_crc = ignore_crc; } /// Flag to ignore computing and verifying the Adler-32 checksum and CRC /// code. pub fn set_ignore_checksums(&mut self, ignore_checksums: bool) { self.ignore_adler32 = ignore_checksums; self.ignore_crc = ignore_checksums; } /// Ignore text chunks while decoding. /// /// Defaults to `false`. pub fn set_ignore_text_chunk(&mut self, ignore_text_chunk: bool) { self.ignore_text_chunk = ignore_text_chunk; } /// Ignore ICCP chunks while decoding. /// /// Defaults to `false`. pub fn set_ignore_iccp_chunk(&mut self, ignore_iccp_chunk: bool) { self.ignore_iccp_chunk = ignore_iccp_chunk; } /// Ignore ancillary chunks if CRC fails /// /// Defaults to `true` pub fn set_skip_ancillary_crc_failures(&mut self, skip_ancillary_crc_failures: bool) { self.skip_ancillary_crc_failures = skip_ancillary_crc_failures; } } /// PNG StreamingDecoder (low-level interface) /// /// By default, the decoder does not verify Adler-32 checksum computation. To /// enable checksum verification, set it with [`StreamingDecoder::set_ignore_adler32`] /// before starting decompression. pub struct StreamingDecoder { state: Option, current_chunk: ChunkState, /// The inflater state handling consecutive `IDAT` and `fdAT` chunks. inflater: ZlibStream, /// The complete image info read from all prior chunks. pub(crate) info: Option>, /// The animation chunk sequence number. current_seq_no: Option, /// Whether we have already seen a start of an IDAT chunk. (Used to validate chunk ordering - /// some chunk types can only appear before or after an IDAT chunk.) have_idat: bool, /// Whether we are ready for a start of an `IDAT` chunk sequence. Initially `true` and set to /// `false` when the first sequence of consecutive `IDAT` chunks ends. ready_for_idat_chunks: bool, /// Whether we are ready for a start of an `fdAT` chunk sequence. Initially `false`. Set to /// `true` after encountering an `fcTL` chunk. Set to `false` when a sequence of consecutive /// `fdAT` chunks ends. ready_for_fdat_chunks: bool, /// Whether we have already seen an iCCP chunk. Used to prevent parsing of duplicate iCCP chunks. have_iccp: bool, decode_options: DecodeOptions, pub(crate) limits: Limits, } struct ChunkState { /// The type of the current chunk. /// Relevant for `IDAT` and `fdAT` which aggregate consecutive chunks of their own type. type_: ChunkType, /// Partial crc until now. crc: Crc32, /// Remaining bytes to be read. remaining: u32, /// Non-decoded bytes in the chunk. raw_bytes: Vec, } impl StreamingDecoder { /// Creates a new StreamingDecoder /// /// Allocates the internal buffers. pub fn new() -> StreamingDecoder { StreamingDecoder::new_with_options(DecodeOptions::default()) } pub fn new_with_options(decode_options: DecodeOptions) -> StreamingDecoder { let mut inflater = ZlibStream::new(); inflater.set_ignore_adler32(decode_options.ignore_adler32); StreamingDecoder { state: Some(State::new_u32(U32ValueKind::Signature1stU32)), current_chunk: ChunkState::default(), inflater, info: None, current_seq_no: None, have_idat: false, have_iccp: false, ready_for_idat_chunks: true, ready_for_fdat_chunks: false, decode_options, limits: Limits { bytes: usize::MAX }, } } /// Resets the StreamingDecoder pub fn reset(&mut self) { self.state = Some(State::new_u32(U32ValueKind::Signature1stU32)); self.current_chunk.crc = Crc32::new(); self.current_chunk.remaining = 0; self.current_chunk.raw_bytes.clear(); self.inflater.reset(); self.info = None; self.current_seq_no = None; self.have_idat = false; } /// Provides access to the inner `info` field pub fn info(&self) -> Option<&Info<'static>> { self.info.as_ref() } pub fn set_ignore_text_chunk(&mut self, ignore_text_chunk: bool) { self.decode_options.set_ignore_text_chunk(ignore_text_chunk); } pub fn set_ignore_iccp_chunk(&mut self, ignore_iccp_chunk: bool) { self.decode_options.set_ignore_iccp_chunk(ignore_iccp_chunk); } /// Return whether the decoder is set to ignore the Adler-32 checksum. pub fn ignore_adler32(&self) -> bool { self.inflater.ignore_adler32() } /// Set whether to compute and verify the Adler-32 checksum during /// decompression. Return `true` if the flag was successfully set. /// /// The decoder defaults to `true`. /// /// This flag cannot be modified after decompression has started until the /// [`StreamingDecoder`] is reset. pub fn set_ignore_adler32(&mut self, ignore_adler32: bool) -> bool { self.inflater.set_ignore_adler32(ignore_adler32) } /// Set whether to compute and verify the Adler-32 checksum during /// decompression. /// /// The decoder defaults to `false`. pub fn set_ignore_crc(&mut self, ignore_crc: bool) { self.decode_options.set_ignore_crc(ignore_crc) } /// Ignore ancillary chunks if CRC fails /// /// Defaults to `true` pub fn set_skip_ancillary_crc_failures(&mut self, skip_ancillary_crc_failures: bool) { self.decode_options .set_skip_ancillary_crc_failures(skip_ancillary_crc_failures) } /// Low level StreamingDecoder interface. /// /// Allows to stream partial data to the encoder. Returns a tuple containing the bytes that have /// been consumed from the input buffer and the current decoding result. If the decoded chunk /// was an image data chunk, it also appends the read data to `image_data`. pub fn update( &mut self, mut buf: &[u8], image_data: &mut Vec, ) -> Result<(usize, Decoded), DecodingError> { if self.state.is_none() { return Err(DecodingError::Parameter( ParameterErrorKind::PolledAfterFatalError.into(), )); } let len = buf.len(); while !buf.is_empty() { match self.next_state(buf, image_data) { Ok((bytes, Decoded::Nothing)) => buf = &buf[bytes..], Ok((bytes, result)) => { buf = &buf[bytes..]; return Ok((len - buf.len(), result)); } Err(err) => { debug_assert!(self.state.is_none()); return Err(err); } } } Ok((len - buf.len(), Decoded::Nothing)) } fn next_state( &mut self, buf: &[u8], image_data: &mut Vec, ) -> Result<(usize, Decoded), DecodingError> { use self::State::*; // Driver should ensure that state is never None let state = self.state.take().unwrap(); match state { U32 { kind, mut bytes, mut accumulated_count, } => { debug_assert!(accumulated_count <= 4); if accumulated_count == 0 && buf.len() >= 4 { // Handling these `accumulated_count` and `buf.len()` values in a separate `if` // branch is not strictly necessary - the `else` statement below is already // capable of handling these values. The main reason for special-casing these // values is that they occur fairly frequently and special-casing them results // in performance gains. const CONSUMED_BYTES: usize = 4; self.parse_u32(kind, &buf[0..4], image_data) .map(|decoded| (CONSUMED_BYTES, decoded)) } else { let remaining_count = 4 - accumulated_count; let consumed_bytes = { let available_count = min(remaining_count, buf.len()); bytes[accumulated_count..accumulated_count + available_count] .copy_from_slice(&buf[0..available_count]); accumulated_count += available_count; available_count }; if accumulated_count < 4 { self.state = Some(U32 { kind, bytes, accumulated_count, }); Ok((consumed_bytes, Decoded::Nothing)) } else { debug_assert_eq!(accumulated_count, 4); self.parse_u32(kind, &bytes, image_data) .map(|decoded| (consumed_bytes, decoded)) } } } ParseChunkData(type_str) => { debug_assert!(type_str != IDAT && type_str != chunk::fdAT); if self.current_chunk.remaining == 0 { // Got complete chunk. Ok((0, self.parse_chunk(type_str)?)) } else { // Make sure we have room to read more of the chunk. // We need it fully before parsing. self.reserve_current_chunk()?; self.state = Some(ReadChunkData(type_str)); Ok((0, Decoded::PartialChunk(type_str))) } } ReadChunkData(type_str) => { debug_assert!(type_str != IDAT && type_str != chunk::fdAT); if self.current_chunk.remaining == 0 { self.state = Some(State::new_u32(U32ValueKind::Crc(type_str))); Ok((0, Decoded::Nothing)) } else { let ChunkState { crc, remaining, raw_bytes, type_: _, } = &mut self.current_chunk; let buf_avail = raw_bytes.capacity() - raw_bytes.len(); let bytes_avail = min(buf.len(), buf_avail); let n = min(*remaining, bytes_avail as u32); if buf_avail == 0 { self.state = Some(ParseChunkData(type_str)); Ok((0, Decoded::Nothing)) } else { let buf = &buf[..n as usize]; if !self.decode_options.ignore_crc { crc.update(buf); } raw_bytes.extend_from_slice(buf); *remaining -= n; if *remaining == 0 { self.state = Some(ParseChunkData(type_str)); } else { self.state = Some(ReadChunkData(type_str)); } Ok((n as usize, Decoded::Nothing)) } } } ImageData(type_str) => { debug_assert!(type_str == IDAT || type_str == chunk::fdAT); let len = std::cmp::min(buf.len(), self.current_chunk.remaining as usize); let buf = &buf[..len]; let consumed = self.inflater.decompress(buf, image_data)?; self.current_chunk.crc.update(&buf[..consumed]); self.current_chunk.remaining -= consumed as u32; if self.current_chunk.remaining == 0 { self.state = Some(State::new_u32(U32ValueKind::Crc(type_str))); } else { self.state = Some(ImageData(type_str)); } Ok((consumed, Decoded::ImageData)) } } } fn parse_u32( &mut self, kind: U32ValueKind, u32_be_bytes: &[u8], image_data: &mut Vec, ) -> Result { debug_assert_eq!(u32_be_bytes.len(), 4); let bytes = u32_be_bytes.try_into().unwrap(); let val = u32::from_be_bytes(bytes); match kind { U32ValueKind::Signature1stU32 => { if bytes == [137, 80, 78, 71] { self.state = Some(State::new_u32(U32ValueKind::Signature2ndU32)); Ok(Decoded::Nothing) } else { Err(DecodingError::Format( FormatErrorInner::InvalidSignature.into(), )) } } U32ValueKind::Signature2ndU32 => { if bytes == [13, 10, 26, 10] { self.state = Some(State::new_u32(U32ValueKind::Length)); Ok(Decoded::Nothing) } else { Err(DecodingError::Format( FormatErrorInner::InvalidSignature.into(), )) } } U32ValueKind::Length => { self.state = Some(State::new_u32(U32ValueKind::Type { length: val })); Ok(Decoded::Nothing) } U32ValueKind::Type { length } => { let type_str = ChunkType(bytes); if self.info.is_none() && type_str != IHDR { return Err(DecodingError::Format( FormatErrorInner::ChunkBeforeIhdr { kind: type_str }.into(), )); } if type_str != self.current_chunk.type_ && (self.current_chunk.type_ == IDAT || self.current_chunk.type_ == chunk::fdAT) { self.current_chunk.type_ = type_str; self.inflater.finish_compressed_chunks(image_data)?; self.inflater.reset(); self.ready_for_idat_chunks = false; self.ready_for_fdat_chunks = false; self.state = Some(State::U32 { kind, bytes, accumulated_count: 4, }); return Ok(Decoded::ImageDataFlushed); } self.state = match type_str { chunk::fdAT => { if !self.ready_for_fdat_chunks { return Err(DecodingError::Format( FormatErrorInner::UnexpectedRestartOfDataChunkSequence { kind: chunk::fdAT, } .into(), )); } if length < 4 { return Err(DecodingError::Format( FormatErrorInner::FdatShorterThanFourBytes.into(), )); } Some(State::new_u32(U32ValueKind::ApngSequenceNumber)) } IDAT => { if !self.ready_for_idat_chunks { return Err(DecodingError::Format( FormatErrorInner::UnexpectedRestartOfDataChunkSequence { kind: IDAT, } .into(), )); } self.have_idat = true; Some(State::ImageData(type_str)) } _ => Some(State::ReadChunkData(type_str)), }; self.current_chunk.type_ = type_str; if !self.decode_options.ignore_crc { self.current_chunk.crc.reset(); self.current_chunk.crc.update(&type_str.0); } self.current_chunk.remaining = length; self.current_chunk.raw_bytes.clear(); Ok(Decoded::ChunkBegin(length, type_str)) } U32ValueKind::Crc(type_str) => { // If ignore_crc is set, do not calculate CRC. We set // sum=val so that it short-circuits to true in the next // if-statement block let sum = if self.decode_options.ignore_crc { val } else { self.current_chunk.crc.clone().finalize() }; if val == sum || CHECKSUM_DISABLED { if type_str == IEND { debug_assert!(self.state.is_none()); Ok(Decoded::ImageEnd) } else { self.state = Some(State::new_u32(U32ValueKind::Length)); Ok(Decoded::ChunkComplete(val, type_str)) } } else if self.decode_options.skip_ancillary_crc_failures && !chunk::is_critical(type_str) { // Ignore ancillary chunk with invalid CRC self.state = Some(State::new_u32(U32ValueKind::Length)); Ok(Decoded::Nothing) } else { Err(DecodingError::Format( FormatErrorInner::CrcMismatch { crc_val: val, crc_sum: sum, chunk: type_str, } .into(), )) } } U32ValueKind::ApngSequenceNumber => { debug_assert_eq!(self.current_chunk.type_, chunk::fdAT); let next_seq_no = val; // Should be verified by the FdatShorterThanFourBytes check earlier. debug_assert!(self.current_chunk.remaining >= 4); self.current_chunk.remaining -= 4; if let Some(seq_no) = self.current_seq_no { if next_seq_no != seq_no + 1 { return Err(DecodingError::Format( FormatErrorInner::ApngOrder { present: next_seq_no, expected: seq_no + 1, } .into(), )); } self.current_seq_no = Some(next_seq_no); } else { return Err(DecodingError::Format(FormatErrorInner::MissingFctl.into())); } if !self.decode_options.ignore_crc { let data = next_seq_no.to_be_bytes(); self.current_chunk.crc.update(&data); } self.state = Some(State::ImageData(chunk::fdAT)); Ok(Decoded::PartialChunk(chunk::fdAT)) } } } fn reserve_current_chunk(&mut self) -> Result<(), DecodingError> { let max = self.limits.bytes; let buffer = &mut self.current_chunk.raw_bytes; // Double if necessary, but no more than until the limit is reached. let reserve_size = max.saturating_sub(buffer.capacity()).min(buffer.len()); self.limits.reserve_bytes(reserve_size)?; buffer.reserve_exact(reserve_size); if buffer.capacity() == buffer.len() { Err(DecodingError::LimitsExceeded) } else { Ok(()) } } fn parse_chunk(&mut self, type_str: ChunkType) -> Result { self.state = Some(State::new_u32(U32ValueKind::Crc(type_str))); let parse_result = match type_str { IHDR => self.parse_ihdr(), chunk::sBIT => self.parse_sbit(), chunk::PLTE => self.parse_plte(), chunk::tRNS => self.parse_trns(), chunk::pHYs => self.parse_phys(), chunk::gAMA => self.parse_gama(), chunk::acTL => self.parse_actl(), chunk::fcTL => self.parse_fctl(), chunk::cHRM => self.parse_chrm(), chunk::sRGB => self.parse_srgb(), chunk::cICP => Ok(self.parse_cicp()), chunk::mDCV => Ok(self.parse_mdcv()), chunk::cLLI => Ok(self.parse_clli()), chunk::bKGD => Ok(self.parse_bkgd()), chunk::iCCP if !self.decode_options.ignore_iccp_chunk => self.parse_iccp(), chunk::tEXt if !self.decode_options.ignore_text_chunk => self.parse_text(), chunk::zTXt if !self.decode_options.ignore_text_chunk => self.parse_ztxt(), chunk::iTXt if !self.decode_options.ignore_text_chunk => self.parse_itxt(), _ => Ok(Decoded::PartialChunk(type_str)), }; parse_result.map_err(|e| { self.state = None; match e { // `parse_chunk` is invoked after gathering **all** bytes of a chunk, so // `UnexpectedEof` from something like `read_be` is permanent and indicates an // invalid PNG that should be represented as a `FormatError`, rather than as a // (potentially recoverable) `IoError` / `UnexpectedEof`. DecodingError::IoError(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { let fmt_err: FormatError = FormatErrorInner::ChunkTooShort { kind: type_str }.into(); fmt_err.into() } e => e, } }) } fn parse_fctl(&mut self) -> Result { let mut buf = &self.current_chunk.raw_bytes[..]; let next_seq_no = buf.read_be()?; // Assuming that fcTL is required before *every* fdAT-sequence self.current_seq_no = Some(if let Some(seq_no) = self.current_seq_no { if next_seq_no != seq_no + 1 { return Err(DecodingError::Format( FormatErrorInner::ApngOrder { expected: seq_no + 1, present: next_seq_no, } .into(), )); } next_seq_no } else { if next_seq_no != 0 { return Err(DecodingError::Format( FormatErrorInner::ApngOrder { expected: 0, present: next_seq_no, } .into(), )); } 0 }); self.inflater.reset(); self.ready_for_fdat_chunks = true; let fc = FrameControl { sequence_number: next_seq_no, width: buf.read_be()?, height: buf.read_be()?, x_offset: buf.read_be()?, y_offset: buf.read_be()?, delay_num: buf.read_be()?, delay_den: buf.read_be()?, dispose_op: { let dispose_op = buf.read_be()?; match DisposeOp::from_u8(dispose_op) { Some(dispose_op) => dispose_op, None => { return Err(DecodingError::Format( FormatErrorInner::InvalidDisposeOp(dispose_op).into(), )) } } }, blend_op: { let blend_op = buf.read_be()?; match BlendOp::from_u8(blend_op) { Some(blend_op) => blend_op, None => { return Err(DecodingError::Format( FormatErrorInner::InvalidBlendOp(blend_op).into(), )) } } }, }; self.info.as_ref().unwrap().validate(&fc)?; self.info.as_mut().unwrap().frame_control = Some(fc); Ok(Decoded::FrameControl(fc)) } fn parse_actl(&mut self) -> Result { if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::acTL }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let actl = AnimationControl { num_frames: buf.read_be()?, num_plays: buf.read_be()?, }; self.info.as_mut().unwrap().animation_control = Some(actl); Ok(Decoded::AnimationControl(actl)) } } fn parse_plte(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if info.palette.is_some() { // Only one palette is allowed Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::PLTE }.into(), )) } else { self.limits .reserve_bytes(self.current_chunk.raw_bytes.len())?; info.palette = Some(Cow::Owned(self.current_chunk.raw_bytes.clone())); Ok(Decoded::Nothing) } } fn parse_sbit(&mut self) -> Result { let mut parse = || { let info = self.info.as_mut().unwrap(); if info.palette.is_some() { return Err(DecodingError::Format( FormatErrorInner::AfterPlte { kind: chunk::sBIT }.into(), )); } if self.have_idat { return Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::sBIT }.into(), )); } if info.sbit.is_some() { return Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::sBIT }.into(), )); } let (color_type, bit_depth) = { (info.color_type, info.bit_depth) }; // The sample depth for color type 3 is fixed at eight bits. let sample_depth = if color_type == ColorType::Indexed { BitDepth::Eight } else { bit_depth }; self.limits .reserve_bytes(self.current_chunk.raw_bytes.len())?; let vec = self.current_chunk.raw_bytes.clone(); let len = vec.len(); // expected lenth of the chunk let expected = match color_type { ColorType::Grayscale => 1, ColorType::Rgb | ColorType::Indexed => 3, ColorType::GrayscaleAlpha => 2, ColorType::Rgba => 4, }; // Check if the sbit chunk size is valid. if expected != len { return Err(DecodingError::Format( FormatErrorInner::InvalidSbitChunkSize { color_type, expected, len, } .into(), )); } for sbit in &vec { if *sbit < 1 || *sbit > sample_depth as u8 { return Err(DecodingError::Format( FormatErrorInner::InvalidSbit { sample_depth, sbit: *sbit, } .into(), )); } } info.sbit = Some(Cow::Owned(vec)); Ok(Decoded::Nothing) }; parse().ok(); Ok(Decoded::Nothing) } fn parse_trns(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if info.trns.is_some() { return Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::PLTE }.into(), )); } let (color_type, bit_depth) = { (info.color_type, info.bit_depth as u8) }; self.limits .reserve_bytes(self.current_chunk.raw_bytes.len())?; let mut vec = self.current_chunk.raw_bytes.clone(); let len = vec.len(); match color_type { ColorType::Grayscale => { if len < 2 { return Err(DecodingError::Format( FormatErrorInner::ShortPalette { expected: 2, len }.into(), )); } if bit_depth < 16 { vec[0] = vec[1]; vec.truncate(1); } info.trns = Some(Cow::Owned(vec)); Ok(Decoded::Nothing) } ColorType::Rgb => { if len < 6 { return Err(DecodingError::Format( FormatErrorInner::ShortPalette { expected: 6, len }.into(), )); } if bit_depth < 16 { vec[0] = vec[1]; vec[1] = vec[3]; vec[2] = vec[5]; vec.truncate(3); } info.trns = Some(Cow::Owned(vec)); Ok(Decoded::Nothing) } ColorType::Indexed => { // The transparency chunk must be after the palette chunk and // before the data chunk. if info.palette.is_none() { return Err(DecodingError::Format( FormatErrorInner::BeforePlte { kind: chunk::tRNS }.into(), )); } else if self.have_idat { return Err(DecodingError::Format( FormatErrorInner::OutsidePlteIdat { kind: chunk::tRNS }.into(), )); } info.trns = Some(Cow::Owned(vec)); Ok(Decoded::Nothing) } c => Err(DecodingError::Format( FormatErrorInner::ColorWithBadTrns(c).into(), )), } } fn parse_phys(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::pHYs }.into(), )) } else if info.pixel_dims.is_some() { Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::pHYs }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let xppu = buf.read_be()?; let yppu = buf.read_be()?; let unit = buf.read_be()?; let unit = match Unit::from_u8(unit) { Some(unit) => unit, None => { return Err(DecodingError::Format( FormatErrorInner::InvalidUnit(unit).into(), )) } }; let pixel_dims = PixelDimensions { xppu, yppu, unit }; info.pixel_dims = Some(pixel_dims); Ok(Decoded::PixelDimensions(pixel_dims)) } } fn parse_chrm(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::cHRM }.into(), )) } else if info.chrm_chunk.is_some() { Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::cHRM }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let white_x: u32 = buf.read_be()?; let white_y: u32 = buf.read_be()?; let red_x: u32 = buf.read_be()?; let red_y: u32 = buf.read_be()?; let green_x: u32 = buf.read_be()?; let green_y: u32 = buf.read_be()?; let blue_x: u32 = buf.read_be()?; let blue_y: u32 = buf.read_be()?; let source_chromaticities = SourceChromaticities { white: ( ScaledFloat::from_scaled(white_x), ScaledFloat::from_scaled(white_y), ), red: ( ScaledFloat::from_scaled(red_x), ScaledFloat::from_scaled(red_y), ), green: ( ScaledFloat::from_scaled(green_x), ScaledFloat::from_scaled(green_y), ), blue: ( ScaledFloat::from_scaled(blue_x), ScaledFloat::from_scaled(blue_y), ), }; info.chrm_chunk = Some(source_chromaticities); // Ignore chromaticities if sRGB profile is used. if info.srgb.is_none() { info.source_chromaticities = Some(source_chromaticities); } Ok(Decoded::Nothing) } } fn parse_gama(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::gAMA }.into(), )) } else if info.gama_chunk.is_some() { Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::gAMA }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let source_gamma: u32 = buf.read_be()?; let source_gamma = ScaledFloat::from_scaled(source_gamma); info.gama_chunk = Some(source_gamma); // Ignore chromaticities if sRGB profile is used. if info.srgb.is_none() { info.source_gamma = Some(source_gamma); } Ok(Decoded::Nothing) } } fn parse_srgb(&mut self) -> Result { let info = self.info.as_mut().unwrap(); if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::acTL }.into(), )) } else if info.srgb.is_some() { Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: chunk::sRGB }.into(), )) } else { let mut buf = &self.current_chunk.raw_bytes[..]; let raw: u8 = buf.read_be()?; // BE is is nonsense for single bytes, but this way the size is checked. let rendering_intent = crate::SrgbRenderingIntent::from_raw(raw).ok_or_else(|| { FormatError::from(FormatErrorInner::InvalidSrgbRenderingIntent(raw)) })?; // Set srgb and override source gamma and chromaticities. info.srgb = Some(rendering_intent); info.source_gamma = Some(crate::srgb::substitute_gamma()); info.source_chromaticities = Some(crate::srgb::substitute_chromaticities()); Ok(Decoded::Nothing) } } // NOTE: This function cannot return `DecodingError` and handles parsing // errors or spec violations as-if the chunk was missing. See // https://github.com/image-rs/image-png/issues/525 for more discussion. fn parse_cicp(&mut self) -> Decoded { fn parse(mut buf: &[u8]) -> Result { let color_primaries: u8 = buf.read_be()?; let transfer_function: u8 = buf.read_be()?; let matrix_coefficients: u8 = buf.read_be()?; let is_video_full_range_image = { let flag: u8 = buf.read_be()?; match flag { 0 => false, 1 => true, _ => { return Err(std::io::ErrorKind::InvalidData.into()); } } }; // RGB is currently the only supported color model in PNG, and as // such Matrix Coefficients shall be set to 0. if matrix_coefficients != 0 { return Err(std::io::ErrorKind::InvalidData.into()); } if !buf.is_empty() { return Err(std::io::ErrorKind::InvalidData.into()); } Ok(CodingIndependentCodePoints { color_primaries, transfer_function, matrix_coefficients, is_video_full_range_image, }) } // The spec requires that the cICP chunk MUST come before the PLTE and IDAT chunks. // Additionally, we ignore a second, duplicated cICP chunk (if any). let info = self.info.as_mut().unwrap(); let is_before_plte_and_idat = !self.have_idat && info.palette.is_none(); if is_before_plte_and_idat && info.coding_independent_code_points.is_none() { info.coding_independent_code_points = parse(&self.current_chunk.raw_bytes[..]).ok(); } Decoded::Nothing } // NOTE: This function cannot return `DecodingError` and handles parsing // errors or spec violations as-if the chunk was missing. See // https://github.com/image-rs/image-png/issues/525 for more discussion. fn parse_mdcv(&mut self) -> Decoded { fn parse(mut buf: &[u8]) -> Result { let red_x: u16 = buf.read_be()?; let red_y: u16 = buf.read_be()?; let green_x: u16 = buf.read_be()?; let green_y: u16 = buf.read_be()?; let blue_x: u16 = buf.read_be()?; let blue_y: u16 = buf.read_be()?; let white_x: u16 = buf.read_be()?; let white_y: u16 = buf.read_be()?; fn scale(chunk: u16) -> ScaledFloat { // `ScaledFloat::SCALING` is hardcoded to 100_000, which works // well for the `cHRM` chunk where the spec says that "a value // of 0.3127 would be stored as the integer 31270". In the // `mDCV` chunk the spec says that "0.708, 0.292)" is stored as // "{ 35400, 14600 }", using a scaling factor of 50_000, so we // multiply by 2 before converting. ScaledFloat::from_scaled((chunk as u32) * 2) } let chromaticities = SourceChromaticities { white: (scale(white_x), scale(white_y)), red: (scale(red_x), scale(red_y)), green: (scale(green_x), scale(green_y)), blue: (scale(blue_x), scale(blue_y)), }; let max_luminance: u32 = buf.read_be()?; let min_luminance: u32 = buf.read_be()?; if !buf.is_empty() { return Err(std::io::ErrorKind::InvalidData.into()); } Ok(MasteringDisplayColorVolume { chromaticities, max_luminance, min_luminance, }) } // The spec requires that the mDCV chunk MUST come before the PLTE and IDAT chunks. // Additionally, we ignore a second, duplicated mDCV chunk (if any). let info = self.info.as_mut().unwrap(); let is_before_plte_and_idat = !self.have_idat && info.palette.is_none(); if is_before_plte_and_idat && info.mastering_display_color_volume.is_none() { info.mastering_display_color_volume = parse(&self.current_chunk.raw_bytes[..]).ok(); } Decoded::Nothing } // NOTE: This function cannot return `DecodingError` and handles parsing // errors or spec violations as-if the chunk was missing. See // https://github.com/image-rs/image-png/issues/525 for more discussion. fn parse_clli(&mut self) -> Decoded { fn parse(mut buf: &[u8]) -> Result { let max_content_light_level: u32 = buf.read_be()?; let max_frame_average_light_level: u32 = buf.read_be()?; if !buf.is_empty() { return Err(std::io::ErrorKind::InvalidData.into()); } Ok(ContentLightLevelInfo { max_content_light_level, max_frame_average_light_level, }) } // We ignore a second, duplicated cLLI chunk (if any). let info = self.info.as_mut().unwrap(); if info.content_light_level.is_none() { info.content_light_level = parse(&self.current_chunk.raw_bytes[..]).ok(); } Decoded::Nothing } fn parse_iccp(&mut self) -> Result { if self.have_idat { Err(DecodingError::Format( FormatErrorInner::AfterIdat { kind: chunk::iCCP }.into(), )) } else if self.have_iccp { // We have already encountered an iCCP chunk before. // // Section "4.2.2.4. iCCP Embedded ICC profile" of the spec says: // > A file should contain at most one embedded profile, // > whether explicit like iCCP or implicit like sRGB. // but // * This is a "should", not a "must" // * The spec also says that "All ancillary chunks are optional, in the sense that // [...] decoders can ignore them." // * The reference implementation (libpng) ignores the subsequent iCCP chunks // (treating them as a benign error). Ok(Decoded::Nothing) } else { self.have_iccp = true; let _ = self.parse_iccp_raw(); Ok(Decoded::Nothing) } } fn parse_iccp_raw(&mut self) -> Result<(), DecodingError> { let info = self.info.as_mut().unwrap(); let mut buf = &self.current_chunk.raw_bytes[..]; // read profile name for len in 0..=80 { let raw: u8 = buf.read_be()?; if (raw == 0 && len == 0) || (raw != 0 && len == 80) { return Err(DecodingError::from(TextDecodingError::InvalidKeywordSize)); } if raw == 0 { break; } } match buf.read_be()? { // compression method 0u8 => (), n => { return Err(DecodingError::Format( FormatErrorInner::UnknownCompressionMethod(n).into(), )) } } match fdeflate::decompress_to_vec_bounded(buf, self.limits.bytes) { Ok(profile) => { self.limits.reserve_bytes(profile.len())?; info.icc_profile = Some(Cow::Owned(profile)); } Err(fdeflate::BoundedDecompressionError::DecompressionError { inner: err }) => { return Err(DecodingError::Format( FormatErrorInner::CorruptFlateStream { err }.into(), )) } Err(fdeflate::BoundedDecompressionError::OutputTooLarge { .. }) => { return Err(DecodingError::LimitsExceeded); } } Ok(()) } fn parse_ihdr(&mut self) -> Result { if self.info.is_some() { return Err(DecodingError::Format( FormatErrorInner::DuplicateChunk { kind: IHDR }.into(), )); } let mut buf = &self.current_chunk.raw_bytes[..]; let width = buf.read_be()?; let height = buf.read_be()?; if width == 0 || height == 0 { return Err(DecodingError::Format( FormatErrorInner::InvalidDimensions.into(), )); } let bit_depth = buf.read_be()?; let bit_depth = match BitDepth::from_u8(bit_depth) { Some(bits) => bits, None => { return Err(DecodingError::Format( FormatErrorInner::InvalidBitDepth(bit_depth).into(), )) } }; let color_type = buf.read_be()?; let color_type = match ColorType::from_u8(color_type) { Some(color_type) => { if color_type.is_combination_invalid(bit_depth) { return Err(DecodingError::Format( FormatErrorInner::InvalidColorBitDepth { color_type, bit_depth, } .into(), )); } else { color_type } } None => { return Err(DecodingError::Format( FormatErrorInner::InvalidColorType(color_type).into(), )) } }; match buf.read_be()? { // compression method 0u8 => (), n => { return Err(DecodingError::Format( FormatErrorInner::UnknownCompressionMethod(n).into(), )) } } match buf.read_be()? { // filter method 0u8 => (), n => { return Err(DecodingError::Format( FormatErrorInner::UnknownFilterMethod(n).into(), )) } } let interlaced = match buf.read_be()? { 0u8 => false, 1 => true, n => { return Err(DecodingError::Format( FormatErrorInner::UnknownInterlaceMethod(n).into(), )) } }; if let Some(mut raw_row_len) = color_type.checked_raw_row_length(bit_depth, width) { if interlaced { // This overshoots, but overestimating should be fine. // TODO: Calculate **exact** IDAT size for interlaced images. raw_row_len = raw_row_len.saturating_mul(2); } self.inflater .set_max_total_output((height as usize).saturating_mul(raw_row_len)); } self.info = Some(Info { width, height, bit_depth, color_type, interlaced, ..Default::default() }); Ok(Decoded::Header( width, height, bit_depth, color_type, interlaced, )) } fn split_keyword(buf: &[u8]) -> Result<(&[u8], &[u8]), DecodingError> { let null_byte_index = buf .iter() .position(|&b| b == 0) .ok_or_else(|| DecodingError::from(TextDecodingError::MissingNullSeparator))?; if null_byte_index == 0 || null_byte_index > 79 { return Err(DecodingError::from(TextDecodingError::InvalidKeywordSize)); } Ok((&buf[..null_byte_index], &buf[null_byte_index + 1..])) } fn parse_text(&mut self) -> Result { let buf = &self.current_chunk.raw_bytes[..]; self.limits.reserve_bytes(buf.len())?; let (keyword_slice, value_slice) = Self::split_keyword(buf)?; self.info .as_mut() .unwrap() .uncompressed_latin1_text .push(TEXtChunk::decode(keyword_slice, value_slice).map_err(DecodingError::from)?); Ok(Decoded::Nothing) } fn parse_ztxt(&mut self) -> Result { let buf = &self.current_chunk.raw_bytes[..]; self.limits.reserve_bytes(buf.len())?; let (keyword_slice, value_slice) = Self::split_keyword(buf)?; let compression_method = *value_slice .first() .ok_or_else(|| DecodingError::from(TextDecodingError::InvalidCompressionMethod))?; let text_slice = &value_slice[1..]; self.info.as_mut().unwrap().compressed_latin1_text.push( ZTXtChunk::decode(keyword_slice, compression_method, text_slice) .map_err(DecodingError::from)?, ); Ok(Decoded::Nothing) } fn parse_itxt(&mut self) -> Result { let buf = &self.current_chunk.raw_bytes[..]; self.limits.reserve_bytes(buf.len())?; let (keyword_slice, value_slice) = Self::split_keyword(buf)?; let compression_flag = *value_slice .first() .ok_or_else(|| DecodingError::from(TextDecodingError::MissingCompressionFlag))?; let compression_method = *value_slice .get(1) .ok_or_else(|| DecodingError::from(TextDecodingError::InvalidCompressionMethod))?; let second_null_byte_index = value_slice[2..] .iter() .position(|&b| b == 0) .ok_or_else(|| DecodingError::from(TextDecodingError::MissingNullSeparator))? + 2; let language_tag_slice = &value_slice[2..second_null_byte_index]; let third_null_byte_index = value_slice[second_null_byte_index + 1..] .iter() .position(|&b| b == 0) .ok_or_else(|| DecodingError::from(TextDecodingError::MissingNullSeparator))? + (second_null_byte_index + 1); let translated_keyword_slice = &value_slice[second_null_byte_index + 1..third_null_byte_index]; let text_slice = &value_slice[third_null_byte_index + 1..]; self.info.as_mut().unwrap().utf8_text.push( ITXtChunk::decode( keyword_slice, compression_flag, compression_method, language_tag_slice, translated_keyword_slice, text_slice, ) .map_err(DecodingError::from)?, ); Ok(Decoded::Nothing) } // NOTE: This function cannot return `DecodingError` and handles parsing // errors or spec violations as-if the chunk was missing. See // https://github.com/image-rs/image-png/issues/525 for more discussion. fn parse_bkgd(&mut self) -> Decoded { let info = self.info.as_mut().unwrap(); if info.bkgd.is_none() && !self.have_idat { let expected = match info.color_type { ColorType::Indexed => { if info.palette.is_none() { return Decoded::Nothing; }; 1 } ColorType::Grayscale | ColorType::GrayscaleAlpha => 2, ColorType::Rgb | ColorType::Rgba => 6, }; let vec = self.current_chunk.raw_bytes.clone(); let len = vec.len(); if len == expected { info.bkgd = Some(Cow::Owned(vec)); } } Decoded::Nothing } } impl Info<'_> { fn validate(&self, fc: &FrameControl) -> Result<(), DecodingError> { if fc.width == 0 || fc.height == 0 { return Err(DecodingError::Format( FormatErrorInner::InvalidDimensions.into(), )); } // Validate mathematically: fc.width + fc.x_offset <= self.width let in_x_bounds = Some(fc.width) <= self.width.checked_sub(fc.x_offset); // Validate mathematically: fc.height + fc.y_offset <= self.height let in_y_bounds = Some(fc.height) <= self.height.checked_sub(fc.y_offset); if !in_x_bounds || !in_y_bounds { return Err(DecodingError::Format( // TODO: do we want to display the bad bounds? FormatErrorInner::BadSubFrameBounds {}.into(), )); } Ok(()) } } impl Default for StreamingDecoder { fn default() -> Self { Self::new() } } impl Default for ChunkState { fn default() -> Self { ChunkState { type_: ChunkType([0; 4]), crc: Crc32::new(), remaining: 0, raw_bytes: Vec::with_capacity(CHUNK_BUFFER_SIZE), } } } #[cfg(test)] mod tests { use super::ScaledFloat; use super::SourceChromaticities; use crate::test_utils::*; use crate::{Decoder, DecodingError, Reader}; use approx::assert_relative_eq; use byteorder::WriteBytesExt; use std::borrow::Cow; use std::cell::RefCell; use std::collections::VecDeque; use std::fs::File; use std::io::{ErrorKind, Read, Write}; use std::rc::Rc; #[test] fn image_gamma() -> Result<(), ()> { fn trial(path: &str, expected: Option) { let decoder = crate::Decoder::new(File::open(path).unwrap()); let reader = decoder.read_info().unwrap(); let actual: Option = reader.info().source_gamma; assert!(actual == expected); } trial("tests/pngsuite/f00n0g08.png", None); trial("tests/pngsuite/f00n2c08.png", None); trial("tests/pngsuite/f01n0g08.png", None); trial("tests/pngsuite/f01n2c08.png", None); trial("tests/pngsuite/f02n0g08.png", None); trial("tests/pngsuite/f02n2c08.png", None); trial("tests/pngsuite/f03n0g08.png", None); trial("tests/pngsuite/f03n2c08.png", None); trial("tests/pngsuite/f04n0g08.png", None); trial("tests/pngsuite/f04n2c08.png", None); trial("tests/pngsuite/f99n0g04.png", None); trial("tests/pngsuite/tm3n3p02.png", None); trial("tests/pngsuite/g03n0g16.png", Some(ScaledFloat::new(0.35))); trial("tests/pngsuite/g03n2c08.png", Some(ScaledFloat::new(0.35))); trial("tests/pngsuite/g03n3p04.png", Some(ScaledFloat::new(0.35))); trial("tests/pngsuite/g04n0g16.png", Some(ScaledFloat::new(0.45))); trial("tests/pngsuite/g04n2c08.png", Some(ScaledFloat::new(0.45))); trial("tests/pngsuite/g04n3p04.png", Some(ScaledFloat::new(0.45))); trial("tests/pngsuite/g05n0g16.png", Some(ScaledFloat::new(0.55))); trial("tests/pngsuite/g05n2c08.png", Some(ScaledFloat::new(0.55))); trial("tests/pngsuite/g05n3p04.png", Some(ScaledFloat::new(0.55))); trial("tests/pngsuite/g07n0g16.png", Some(ScaledFloat::new(0.7))); trial("tests/pngsuite/g07n2c08.png", Some(ScaledFloat::new(0.7))); trial("tests/pngsuite/g07n3p04.png", Some(ScaledFloat::new(0.7))); trial("tests/pngsuite/g10n0g16.png", Some(ScaledFloat::new(1.0))); trial("tests/pngsuite/g10n2c08.png", Some(ScaledFloat::new(1.0))); trial("tests/pngsuite/g10n3p04.png", Some(ScaledFloat::new(1.0))); trial("tests/pngsuite/g25n0g16.png", Some(ScaledFloat::new(2.5))); trial("tests/pngsuite/g25n2c08.png", Some(ScaledFloat::new(2.5))); trial("tests/pngsuite/g25n3p04.png", Some(ScaledFloat::new(2.5))); Ok(()) } #[test] fn image_source_chromaticities() -> Result<(), ()> { fn trial(path: &str, expected: Option) { let decoder = crate::Decoder::new(File::open(path).unwrap()); let reader = decoder.read_info().unwrap(); let actual: Option = reader.info().source_chromaticities; assert!(actual == expected); } trial( "tests/pngsuite/ccwn2c08.png", Some(SourceChromaticities::new( (0.3127, 0.3290), (0.64, 0.33), (0.30, 0.60), (0.15, 0.06), )), ); trial( "tests/pngsuite/ccwn3p08.png", Some(SourceChromaticities::new( (0.3127, 0.3290), (0.64, 0.33), (0.30, 0.60), (0.15, 0.06), )), ); trial("tests/pngsuite/basi0g01.png", None); trial("tests/pngsuite/basi0g02.png", None); trial("tests/pngsuite/basi0g04.png", None); trial("tests/pngsuite/basi0g08.png", None); trial("tests/pngsuite/basi0g16.png", None); trial("tests/pngsuite/basi2c08.png", None); trial("tests/pngsuite/basi2c16.png", None); trial("tests/pngsuite/basi3p01.png", None); trial("tests/pngsuite/basi3p02.png", None); trial("tests/pngsuite/basi3p04.png", None); trial("tests/pngsuite/basi3p08.png", None); trial("tests/pngsuite/basi4a08.png", None); trial("tests/pngsuite/basi4a16.png", None); trial("tests/pngsuite/basi6a08.png", None); trial("tests/pngsuite/basi6a16.png", None); trial("tests/pngsuite/basn0g01.png", None); trial("tests/pngsuite/basn0g02.png", None); trial("tests/pngsuite/basn0g04.png", None); trial("tests/pngsuite/basn0g08.png", None); trial("tests/pngsuite/basn0g16.png", None); trial("tests/pngsuite/basn2c08.png", None); trial("tests/pngsuite/basn2c16.png", None); trial("tests/pngsuite/basn3p01.png", None); trial("tests/pngsuite/basn3p02.png", None); trial("tests/pngsuite/basn3p04.png", None); trial("tests/pngsuite/basn3p08.png", None); trial("tests/pngsuite/basn4a08.png", None); trial("tests/pngsuite/basn4a16.png", None); trial("tests/pngsuite/basn6a08.png", None); trial("tests/pngsuite/basn6a16.png", None); trial("tests/pngsuite/bgai4a08.png", None); trial("tests/pngsuite/bgai4a16.png", None); trial("tests/pngsuite/bgan6a08.png", None); trial("tests/pngsuite/bgan6a16.png", None); trial("tests/pngsuite/bgbn4a08.png", None); trial("tests/pngsuite/bggn4a16.png", None); trial("tests/pngsuite/bgwn6a08.png", None); trial("tests/pngsuite/bgyn6a16.png", None); trial("tests/pngsuite/cdfn2c08.png", None); trial("tests/pngsuite/cdhn2c08.png", None); trial("tests/pngsuite/cdsn2c08.png", None); trial("tests/pngsuite/cdun2c08.png", None); trial("tests/pngsuite/ch1n3p04.png", None); trial("tests/pngsuite/ch2n3p08.png", None); trial("tests/pngsuite/cm0n0g04.png", None); trial("tests/pngsuite/cm7n0g04.png", None); trial("tests/pngsuite/cm9n0g04.png", None); trial("tests/pngsuite/cs3n2c16.png", None); trial("tests/pngsuite/cs3n3p08.png", None); trial("tests/pngsuite/cs5n2c08.png", None); trial("tests/pngsuite/cs5n3p08.png", None); trial("tests/pngsuite/cs8n2c08.png", None); trial("tests/pngsuite/cs8n3p08.png", None); trial("tests/pngsuite/ct0n0g04.png", None); trial("tests/pngsuite/ct1n0g04.png", None); trial("tests/pngsuite/cten0g04.png", None); trial("tests/pngsuite/ctfn0g04.png", None); trial("tests/pngsuite/ctgn0g04.png", None); trial("tests/pngsuite/cthn0g04.png", None); trial("tests/pngsuite/ctjn0g04.png", None); trial("tests/pngsuite/ctzn0g04.png", None); trial("tests/pngsuite/f00n0g08.png", None); trial("tests/pngsuite/f00n2c08.png", None); trial("tests/pngsuite/f01n0g08.png", None); trial("tests/pngsuite/f01n2c08.png", None); trial("tests/pngsuite/f02n0g08.png", None); trial("tests/pngsuite/f02n2c08.png", None); trial("tests/pngsuite/f03n0g08.png", None); trial("tests/pngsuite/f03n2c08.png", None); trial("tests/pngsuite/f04n0g08.png", None); trial("tests/pngsuite/f04n2c08.png", None); trial("tests/pngsuite/f99n0g04.png", None); trial("tests/pngsuite/g03n0g16.png", None); trial("tests/pngsuite/g03n2c08.png", None); trial("tests/pngsuite/g03n3p04.png", None); trial("tests/pngsuite/g04n0g16.png", None); trial("tests/pngsuite/g04n2c08.png", None); trial("tests/pngsuite/g04n3p04.png", None); trial("tests/pngsuite/g05n0g16.png", None); trial("tests/pngsuite/g05n2c08.png", None); trial("tests/pngsuite/g05n3p04.png", None); trial("tests/pngsuite/g07n0g16.png", None); trial("tests/pngsuite/g07n2c08.png", None); trial("tests/pngsuite/g07n3p04.png", None); trial("tests/pngsuite/g10n0g16.png", None); trial("tests/pngsuite/g10n2c08.png", None); trial("tests/pngsuite/g10n3p04.png", None); trial("tests/pngsuite/g25n0g16.png", None); trial("tests/pngsuite/g25n2c08.png", None); trial("tests/pngsuite/g25n3p04.png", None); trial("tests/pngsuite/oi1n0g16.png", None); trial("tests/pngsuite/oi1n2c16.png", None); trial("tests/pngsuite/oi2n0g16.png", None); trial("tests/pngsuite/oi2n2c16.png", None); trial("tests/pngsuite/oi4n0g16.png", None); trial("tests/pngsuite/oi4n2c16.png", None); trial("tests/pngsuite/oi9n0g16.png", None); trial("tests/pngsuite/oi9n2c16.png", None); trial("tests/pngsuite/PngSuite.png", None); trial("tests/pngsuite/pp0n2c16.png", None); trial("tests/pngsuite/pp0n6a08.png", None); trial("tests/pngsuite/ps1n0g08.png", None); trial("tests/pngsuite/ps1n2c16.png", None); trial("tests/pngsuite/ps2n0g08.png", None); trial("tests/pngsuite/ps2n2c16.png", None); trial("tests/pngsuite/s01i3p01.png", None); trial("tests/pngsuite/s01n3p01.png", None); trial("tests/pngsuite/s02i3p01.png", None); trial("tests/pngsuite/s02n3p01.png", None); trial("tests/pngsuite/s03i3p01.png", None); trial("tests/pngsuite/s03n3p01.png", None); trial("tests/pngsuite/s04i3p01.png", None); trial("tests/pngsuite/s04n3p01.png", None); trial("tests/pngsuite/s05i3p02.png", None); trial("tests/pngsuite/s05n3p02.png", None); trial("tests/pngsuite/s06i3p02.png", None); trial("tests/pngsuite/s06n3p02.png", None); trial("tests/pngsuite/s07i3p02.png", None); trial("tests/pngsuite/s07n3p02.png", None); trial("tests/pngsuite/s08i3p02.png", None); trial("tests/pngsuite/s08n3p02.png", None); trial("tests/pngsuite/s09i3p02.png", None); trial("tests/pngsuite/s09n3p02.png", None); trial("tests/pngsuite/s32i3p04.png", None); trial("tests/pngsuite/s32n3p04.png", None); trial("tests/pngsuite/s33i3p04.png", None); trial("tests/pngsuite/s33n3p04.png", None); trial("tests/pngsuite/s34i3p04.png", None); trial("tests/pngsuite/s34n3p04.png", None); trial("tests/pngsuite/s35i3p04.png", None); trial("tests/pngsuite/s35n3p04.png", None); trial("tests/pngsuite/s36i3p04.png", None); trial("tests/pngsuite/s36n3p04.png", None); trial("tests/pngsuite/s37i3p04.png", None); trial("tests/pngsuite/s37n3p04.png", None); trial("tests/pngsuite/s38i3p04.png", None); trial("tests/pngsuite/s38n3p04.png", None); trial("tests/pngsuite/s39i3p04.png", None); trial("tests/pngsuite/s39n3p04.png", None); trial("tests/pngsuite/s40i3p04.png", None); trial("tests/pngsuite/s40n3p04.png", None); trial("tests/pngsuite/tbbn0g04.png", None); trial("tests/pngsuite/tbbn2c16.png", None); trial("tests/pngsuite/tbbn3p08.png", None); trial("tests/pngsuite/tbgn2c16.png", None); trial("tests/pngsuite/tbgn3p08.png", None); trial("tests/pngsuite/tbrn2c08.png", None); trial("tests/pngsuite/tbwn0g16.png", None); trial("tests/pngsuite/tbwn3p08.png", None); trial("tests/pngsuite/tbyn3p08.png", None); trial("tests/pngsuite/tm3n3p02.png", None); trial("tests/pngsuite/tp0n0g08.png", None); trial("tests/pngsuite/tp0n2c08.png", None); trial("tests/pngsuite/tp0n3p08.png", None); trial("tests/pngsuite/tp1n3p08.png", None); trial("tests/pngsuite/z00n2c08.png", None); trial("tests/pngsuite/z03n2c08.png", None); trial("tests/pngsuite/z06n2c08.png", None); Ok(()) } #[test] fn image_source_sbit() { fn trial(path: &str, expected: Option>) { let decoder = crate::Decoder::new(File::open(path).unwrap()); let reader = decoder.read_info().unwrap(); let actual: Option> = reader.info().sbit.clone(); assert!(actual == expected); } trial("tests/sbit/g.png", Some(Cow::Owned(vec![5u8]))); trial("tests/sbit/ga.png", Some(Cow::Owned(vec![5u8, 3u8]))); trial( "tests/sbit/indexed.png", Some(Cow::Owned(vec![5u8, 6u8, 5u8])), ); trial("tests/sbit/rgb.png", Some(Cow::Owned(vec![5u8, 6u8, 5u8]))); trial( "tests/sbit/rgba.png", Some(Cow::Owned(vec![5u8, 6u8, 5u8, 8u8])), ); } /// Test handling of a PNG file that contains *two* iCCP chunks. /// This is a regression test for https://github.com/image-rs/image/issues/1825. #[test] fn test_two_iccp_chunks() { // The test file has been taken from // https://github.com/image-rs/image/issues/1825#issuecomment-1321798639, // but the 2nd iCCP chunk has been altered manually (see the 2nd comment below for more // details). let decoder = crate::Decoder::new(File::open("tests/bugfixes/issue#1825.png").unwrap()); let reader = decoder.read_info().unwrap(); let icc_profile = reader.info().icc_profile.clone().unwrap().into_owned(); // Assert that the contents of the *first* iCCP chunk are returned. // // Note that the 2nd chunk in the test file has been manually altered to have a different // content (`b"test iccp contents"`) which would have a different CRC (797351983). assert_eq!(4070462061, crc32fast::hash(&icc_profile)); } #[test] fn test_iccp_roundtrip() { let dummy_icc = b"I'm a profile"; let mut info = crate::Info::with_size(1, 1); info.icc_profile = Some(dummy_icc.into()); let mut encoded_image = Vec::new(); let enc = crate::Encoder::with_info(&mut encoded_image, info).unwrap(); let mut enc = enc.write_header().unwrap(); enc.write_image_data(&[0]).unwrap(); enc.finish().unwrap(); let dec = crate::Decoder::new(encoded_image.as_slice()); let dec = dec.read_info().unwrap(); assert_eq!(dummy_icc, &**dec.info().icc_profile.as_ref().unwrap()); } #[test] fn test_png_with_broken_iccp() { let decoder = crate::Decoder::new(File::open("tests/iccp/broken_iccp.png").unwrap()); assert!(decoder.read_info().is_ok()); let mut decoder = crate::Decoder::new(File::open("tests/iccp/broken_iccp.png").unwrap()); decoder.set_ignore_iccp_chunk(true); assert!(decoder.read_info().is_ok()); } /// Test handling of `mDCV` and `cLLI` chunks.` #[test] fn test_mdcv_and_clli_chunks() { let decoder = crate::Decoder::new(File::open("tests/bugfixes/cicp_pq.png").unwrap()); let reader = decoder.read_info().unwrap(); let info = reader.info(); let cicp = info.coding_independent_code_points.unwrap(); assert_eq!(cicp.color_primaries, 9); assert_eq!(cicp.transfer_function, 16); assert_eq!(cicp.matrix_coefficients, 0); assert!(cicp.is_video_full_range_image); let mdcv = info.mastering_display_color_volume.unwrap(); assert_relative_eq!(mdcv.chromaticities.red.0.into_value(), 0.680); assert_relative_eq!(mdcv.chromaticities.red.1.into_value(), 0.320); assert_relative_eq!(mdcv.chromaticities.green.0.into_value(), 0.265); assert_relative_eq!(mdcv.chromaticities.green.1.into_value(), 0.690); assert_relative_eq!(mdcv.chromaticities.blue.0.into_value(), 0.150); assert_relative_eq!(mdcv.chromaticities.blue.1.into_value(), 0.060); assert_relative_eq!(mdcv.chromaticities.white.0.into_value(), 0.3127); assert_relative_eq!(mdcv.chromaticities.white.1.into_value(), 0.3290); assert_relative_eq!(mdcv.min_luminance as f32 / 10_000.0, 0.01); assert_relative_eq!(mdcv.max_luminance as f32 / 10_000.0, 5000.0); let clli = info.content_light_level.unwrap(); assert_relative_eq!(clli.max_content_light_level as f32 / 10_000.0, 4000.0); assert_relative_eq!(clli.max_frame_average_light_level as f32 / 10_000.0, 2627.0); } /// Tests what happens then [`Reader.finish`] is called twice. #[test] fn test_finishing_twice() { let mut png = Vec::new(); write_noncompressed_png(&mut png, 16, 1024); let decoder = Decoder::new(png.as_slice()); let mut reader = decoder.read_info().unwrap(); // First call to `finish` - expecting success. reader.finish().unwrap(); // Second call to `finish` - expecting an error. let err = reader.finish().unwrap_err(); assert!(matches!(&err, DecodingError::Parameter(_))); assert_eq!("End of image has been reached", format!("{err}")); } /// Writes an acTL chunk. /// See https://wiki.mozilla.org/APNG_Specification#.60acTL.60:_The_Animation_Control_Chunk fn write_actl(w: &mut impl Write, animation: &crate::AnimationControl) { let mut data = Vec::new(); data.write_u32::(animation.num_frames) .unwrap(); data.write_u32::(animation.num_plays) .unwrap(); write_chunk(w, b"acTL", &data); } /// Writes an fcTL chunk. /// See https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk fn write_fctl(w: &mut impl Write, frame: &crate::FrameControl) { let mut data = Vec::new(); data.write_u32::(frame.sequence_number) .unwrap(); data.write_u32::(frame.width).unwrap(); data.write_u32::(frame.height) .unwrap(); data.write_u32::(frame.x_offset) .unwrap(); data.write_u32::(frame.y_offset) .unwrap(); data.write_u16::(frame.delay_num) .unwrap(); data.write_u16::(frame.delay_den) .unwrap(); data.write_u8(frame.dispose_op as u8).unwrap(); data.write_u8(frame.blend_op as u8).unwrap(); write_chunk(w, b"fcTL", &data); } /// Writes an fdAT chunk. /// See https://wiki.mozilla.org/APNG_Specification#.60fdAT.60:_The_Frame_Data_Chunk fn write_fdat(w: &mut impl Write, sequence_number: u32, image_data: &[u8]) { let mut data = Vec::new(); data.write_u32::(sequence_number) .unwrap(); data.write_all(image_data).unwrap(); write_chunk(w, b"fdAT", &data); } /// Writes PNG signature and chunks that can precede an fdAT chunk that is expected /// to have /// - `sequence_number` set to 0 /// - image data with rgba8 pixels in a `width` by `width` image fn write_fdat_prefix(w: &mut impl Write, num_frames: u32, width: u32) { write_png_sig(w); write_rgba8_ihdr_with_width(w, width); write_actl( w, &crate::AnimationControl { num_frames, num_plays: 0, }, ); let mut fctl = crate::FrameControl { width, height: width, ..Default::default() }; write_fctl(w, &fctl); write_rgba8_idats(w, width, 0x7fffffff); fctl.sequence_number += 1; write_fctl(w, &fctl); } #[test] fn test_fdat_chunk_payload_length_0() { let mut png = Vec::new(); write_fdat_prefix(&mut png, 2, 8); write_chunk(&mut png, b"fdAT", &[]); let decoder = Decoder::new(png.as_slice()); let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; reader.next_frame(&mut buf).unwrap(); // 0-length fdAT should result in an error. let err = reader.next_frame(&mut buf).unwrap_err(); assert!(matches!(&err, DecodingError::Format(_))); assert_eq!("fdAT chunk shorter than 4 bytes", format!("{err}")); // Calling `next_frame` again should return an error. Same error as above would be nice, // but it is probably unnecessary and infeasible (`DecodingError` can't derive `Clone` // because `std::io::Error` doesn't implement `Clone`).. But it definitely shouldn't enter // an infinite loop. let err2 = reader.next_frame(&mut buf).unwrap_err(); assert!(matches!(&err2, DecodingError::Parameter(_))); assert_eq!( "A fatal decoding error has been encounted earlier", format!("{err2}") ); } #[test] fn test_fdat_chunk_payload_length_3() { let mut png = Vec::new(); write_fdat_prefix(&mut png, 2, 8); write_chunk(&mut png, b"fdAT", &[1, 0, 0]); let decoder = Decoder::new(png.as_slice()); let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; reader.next_frame(&mut buf).unwrap(); // 3-bytes-long fdAT should result in an error. let err = reader.next_frame(&mut buf).unwrap_err(); assert!(matches!(&err, DecodingError::Format(_))); assert_eq!("fdAT chunk shorter than 4 bytes", format!("{err}")); } #[test] fn test_frame_split_across_two_fdat_chunks() { // Generate test data where the 2nd animation frame is split across 2 fdAT chunks. // // This is similar to the example given in // https://wiki.mozilla.org/APNG_Specification#Chunk_Sequence_Numbers: // // ``` // Sequence number Chunk // (none) `acTL` // 0 `fcTL` first frame // (none) `IDAT` first frame / default image // 1 `fcTL` second frame // 2 first `fdAT` for second frame // 3 second `fdAT` for second frame // ``` let png = { let mut png = Vec::new(); write_fdat_prefix(&mut png, 2, 8); let image_data = generate_rgba8_with_width_and_height(8, 8); write_fdat(&mut png, 2, &image_data[..30]); write_fdat(&mut png, 3, &image_data[30..]); write_iend(&mut png); png }; // Start decoding. let decoder = Decoder::new(png.as_slice()); let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; let Some(animation_control) = reader.info().animation_control else { panic!("No acTL"); }; assert_eq!(animation_control.num_frames, 2); // Process the 1st animation frame. let first_frame: Vec; { reader.next_frame(&mut buf).unwrap(); first_frame = buf.clone(); // Note that the doc comment of `Reader::next_frame` says that "[...] // can be checked afterwards by calling `info` **after** a successful call and // inspecting the `frame_control` data.". (Note the **emphasis** on "after".) let Some(frame_control) = reader.info().frame_control else { panic!("No fcTL (1st frame)"); }; // The sequence number is taken from the `fcTL` chunk that comes before the `IDAT` // chunk. assert_eq!(frame_control.sequence_number, 0); } // Process the 2nd animation frame. let second_frame: Vec; { reader.next_frame(&mut buf).unwrap(); second_frame = buf.clone(); // Same as above - updated `frame_control` is available *after* the `next_frame` call. let Some(frame_control) = reader.info().frame_control else { panic!("No fcTL (2nd frame)"); }; // The sequence number is taken from the `fcTL` chunk that comes before the two `fdAT` // chunks. Note that sequence numbers inside `fdAT` chunks are not publicly exposed // (but they are still checked when decoding to verify that they are sequential). assert_eq!(frame_control.sequence_number, 1); } assert_eq!(first_frame, second_frame); } #[test] fn test_idat_bigger_than_image_size_from_ihdr() { let png = { let mut png = Vec::new(); write_png_sig(&mut png); write_rgba8_ihdr_with_width(&mut png, 8); // Here we want to test an invalid image where the `IDAT` chunk contains more data // (data for 8x256 image) than declared in the `IHDR` chunk (which only describes an // 8x8 image). write_chunk( &mut png, b"IDAT", &generate_rgba8_with_width_and_height(8, 256), ); write_iend(&mut png); png }; let decoder = Decoder::new(png.as_slice()); let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; // TODO: Should this return an error instead? For now let's just have test assertions for // the current behavior. reader.next_frame(&mut buf).unwrap(); assert_eq!(3093270825, crc32fast::hash(&buf)); } #[test] fn test_only_idat_chunk_in_input_stream() { let png = { let mut png = Vec::new(); write_png_sig(&mut png); write_chunk(&mut png, b"IDAT", &[]); png }; let decoder = Decoder::new(png.as_slice()); let Err(err) = decoder.read_info() else { panic!("Expected an error") }; assert!(matches!(&err, DecodingError::Format(_))); assert_eq!( "ChunkType { type: IDAT, \ critical: true, \ private: false, \ reserved: false, \ safecopy: false \ } chunk appeared before IHDR chunk", format!("{err}"), ); } /// `StreamingInput` can be used by tests to simulate a streaming input /// (e.g. a slow http response, where all bytes are not immediately available). #[derive(Clone)] struct StreamingInput(Rc>); struct StreamingInputState { full_input: Vec, current_pos: usize, available_len: usize, } impl StreamingInput { fn new(full_input: Vec) -> Self { Self(Rc::new(RefCell::new(StreamingInputState { full_input, current_pos: 0, available_len: 0, }))) } fn with_noncompressed_png(width: u32, idat_size: usize) -> Self { let mut png = Vec::new(); write_noncompressed_png(&mut png, width, idat_size); Self::new(png) } fn expose_next_byte(&self) { let mut state = self.0.borrow_mut(); assert!(state.available_len < state.full_input.len()); state.available_len += 1; } fn stream_input_until_reader_is_available(&self) -> Reader { loop { self.0.borrow_mut().current_pos = 0; match Decoder::new(self.clone()).read_info() { Ok(reader) => { break reader; } Err(DecodingError::IoError(e)) if e.kind() == ErrorKind::UnexpectedEof => { self.expose_next_byte(); } _ => panic!("Unexpected error"), } } } fn decode_full_input(&self, f: F) -> R where F: FnOnce(Reader<&[u8]>) -> R, { let state = self.0.borrow(); let decoder = Decoder::new(state.full_input.as_slice()); f(decoder.read_info().unwrap()) } } impl Read for StreamingInput { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let mut state = self.0.borrow_mut(); let mut available_bytes = &state.full_input[state.current_pos..state.available_len]; let number_of_read_bytes = available_bytes.read(buf)?; state.current_pos += number_of_read_bytes; assert!(state.current_pos <= state.available_len); Ok(number_of_read_bytes) } } /// Test resuming/retrying `Reader.next_frame` after `UnexpectedEof`. #[test] fn test_streaming_input_and_decoding_via_next_frame() { const WIDTH: u32 = 16; const IDAT_SIZE: usize = 512; let streaming_input = StreamingInput::with_noncompressed_png(WIDTH, IDAT_SIZE); let (whole_output_info, decoded_from_whole_input) = streaming_input.decode_full_input(|mut r| { let mut buf = vec![0; r.output_buffer_size()]; let output_info = r.next_frame(&mut buf).unwrap(); (output_info, buf) }); let mut png_reader = streaming_input.stream_input_until_reader_is_available(); let mut decoded_from_streaming_input = vec![0; png_reader.output_buffer_size()]; let streaming_output_info = loop { match png_reader.next_frame(decoded_from_streaming_input.as_mut_slice()) { Ok(output_info) => break output_info, Err(DecodingError::IoError(e)) if e.kind() == ErrorKind::UnexpectedEof => { streaming_input.expose_next_byte() } e => panic!("Unexpected error: {:?}", e), } }; assert_eq!(whole_output_info, streaming_output_info); assert_eq!( crc32fast::hash(&decoded_from_whole_input), crc32fast::hash(&decoded_from_streaming_input) ); } /// Test resuming/retrying `Reader.next_row` after `UnexpectedEof`. #[test] fn test_streaming_input_and_decoding_via_next_row() { const WIDTH: u32 = 16; const IDAT_SIZE: usize = 512; let streaming_input = StreamingInput::with_noncompressed_png(WIDTH, IDAT_SIZE); let decoded_from_whole_input = streaming_input.decode_full_input(|mut r| { let mut buf = vec![0; r.output_buffer_size()]; r.next_frame(&mut buf).unwrap(); buf }); let mut png_reader = streaming_input.stream_input_until_reader_is_available(); let mut decoded_from_streaming_input = Vec::new(); loop { match png_reader.next_row() { Ok(None) => break, Ok(Some(row)) => decoded_from_streaming_input.extend_from_slice(row.data()), Err(DecodingError::IoError(e)) if e.kind() == ErrorKind::UnexpectedEof => { streaming_input.expose_next_byte() } e => panic!("Unexpected error: {:?}", e), } } assert_eq!( crc32fast::hash(&decoded_from_whole_input), crc32fast::hash(&decoded_from_streaming_input) ); } /// Test resuming/retrying `Decoder.read_header_info` after `UnexpectedEof`. #[test] fn test_streaming_input_and_reading_header_info() { const WIDTH: u32 = 16; const IDAT_SIZE: usize = 512; let streaming_input = StreamingInput::with_noncompressed_png(WIDTH, IDAT_SIZE); let info_from_whole_input = streaming_input.decode_full_input(|r| r.info().clone()); let mut decoder = Decoder::new(streaming_input.clone()); let info_from_streaming_input = loop { match decoder.read_header_info() { Ok(info) => break info.clone(), Err(DecodingError::IoError(e)) if e.kind() == ErrorKind::UnexpectedEof => { streaming_input.expose_next_byte() } e => panic!("Unexpected error: {:?}", e), } }; assert_eq!(info_from_whole_input.width, info_from_streaming_input.width); assert_eq!( info_from_whole_input.height, info_from_streaming_input.height ); assert_eq!( info_from_whole_input.bit_depth, info_from_streaming_input.bit_depth ); assert_eq!( info_from_whole_input.color_type, info_from_streaming_input.color_type ); assert_eq!( info_from_whole_input.interlaced, info_from_streaming_input.interlaced ); } /// Creates a ready-to-test [`Reader`] which decodes a PNG that contains: /// IHDR, IDAT, IEND. fn create_reader_of_ihdr_idat() -> Reader> { let mut png = VecDeque::new(); write_noncompressed_png(&mut png, /* width = */ 16, /* idat_size = */ 1024); Decoder::new(png).read_info().unwrap() } /// Creates a ready-to-test [`Reader`] which decodes an animated PNG that contains: /// IHDR, acTL, fcTL, IDAT, fcTL, fdAT, IEND. (i.e. IDAT is part of the animation) fn create_reader_of_ihdr_actl_fctl_idat_fctl_fdat() -> Reader> { let idat_width = 16; let mut fctl = crate::FrameControl { width: idat_width, height: idat_width, // same height and width ..Default::default() }; let mut png = VecDeque::new(); write_png_sig(&mut png); write_rgba8_ihdr_with_width(&mut png, idat_width); write_actl( &mut png, &crate::AnimationControl { num_frames: 2, num_plays: 0, }, ); fctl.sequence_number = 0; write_fctl(&mut png, &fctl); // Using `fctl.height + 1` means that the `IDAT` will have "left-over" data after // processing. This helps to verify that `Reader.read_until_image_data` discards the // left-over data when resetting `UnfilteredRowsBuffer`. let idat_data = generate_rgba8_with_width_and_height(fctl.width, fctl.height + 1); write_chunk(&mut png, b"IDAT", &idat_data); let fdat_width = 10; fctl.sequence_number = 1; // Using different width in `IDAT` and `fDAT` frames helps to catch problems that // may arise when `Reader.read_until_image_data` doesn't properly reset // `UnfilteredRowsBuffer`. fctl.width = fdat_width; write_fctl(&mut png, &fctl); let fdat_data = generate_rgba8_with_width_and_height(fctl.width, fctl.height); write_fdat(&mut png, 2, &fdat_data); write_iend(&mut png); Decoder::new(png).read_info().unwrap() } /// Creates a ready-to-test [`Reader`] which decodes an animated PNG that contains: IHDR, acTL, /// IDAT, fcTL, fdAT, fcTL, fdAT, IEND. (i.e. IDAT is *not* part of the animation) fn create_reader_of_ihdr_actl_idat_fctl_fdat_fctl_fdat() -> Reader> { let width = 16; let frame_data = generate_rgba8_with_width_and_height(width, width); let mut fctl = crate::FrameControl { width, height: width, ..Default::default() }; let mut png = VecDeque::new(); write_png_sig(&mut png); write_rgba8_ihdr_with_width(&mut png, width); write_actl( &mut png, &crate::AnimationControl { num_frames: 2, num_plays: 0, }, ); write_chunk(&mut png, b"IDAT", &frame_data); fctl.sequence_number = 0; write_fctl(&mut png, &fctl); write_fdat(&mut png, 1, &frame_data); fctl.sequence_number = 2; write_fctl(&mut png, &fctl); write_fdat(&mut png, 3, &frame_data); write_iend(&mut png); Decoder::new(png).read_info().unwrap() } fn get_fctl_sequence_number(reader: &Reader) -> u32 { reader .info() .frame_control .as_ref() .unwrap() .sequence_number } /// Tests that [`Reader.next_frame`] will report a `PolledAfterEndOfImage` error when called /// after already decoding a single frame in a non-animated PNG. #[test] fn test_next_frame_polling_after_end_non_animated() { let mut reader = create_reader_of_ihdr_idat(); let mut buf = vec![0; reader.output_buffer_size()]; reader .next_frame(&mut buf) .expect("Expecting no error for IDAT frame"); let err = reader .next_frame(&mut buf) .expect_err("Main test - expecting error"); assert!( matches!(&err, DecodingError::Parameter(_)), "Unexpected kind of error: {:?}", &err, ); } /// Tests that [`Reader.next_frame_info`] will report a `PolledAfterEndOfImage` error when /// called when decoding a PNG that only contains a single frame. #[test] fn test_next_frame_info_polling_after_end_non_animated() { let mut reader = create_reader_of_ihdr_idat(); let err = reader .next_frame_info() .expect_err("Main test - expecting error"); assert!( matches!(&err, DecodingError::Parameter(_)), "Unexpected kind of error: {:?}", &err, ); } /// Tests that [`Reader.next_frame`] will report a `PolledAfterEndOfImage` error when called /// after already decoding a single frame in an animated PNG where IDAT is part of the /// animation. #[test] fn test_next_frame_polling_after_end_idat_part_of_animation() { let mut reader = create_reader_of_ihdr_actl_fctl_idat_fctl_fdat(); let mut buf = vec![0; reader.output_buffer_size()]; assert_eq!(get_fctl_sequence_number(&reader), 0); reader .next_frame(&mut buf) .expect("Expecting no error for IDAT frame"); // `next_frame` doesn't advance to the next `fcTL`. assert_eq!(get_fctl_sequence_number(&reader), 0); reader .next_frame(&mut buf) .expect("Expecting no error for fdAT frame"); assert_eq!(get_fctl_sequence_number(&reader), 1); let err = reader .next_frame(&mut buf) .expect_err("Main test - expecting error"); assert!( matches!(&err, DecodingError::Parameter(_)), "Unexpected kind of error: {:?}", &err, ); } /// Tests that [`Reader.next_frame`] will report a `PolledAfterEndOfImage` error when called /// after already decoding a single frame in an animated PNG where IDAT is *not* part of the /// animation. #[test] fn test_next_frame_polling_after_end_idat_not_part_of_animation() { let mut reader = create_reader_of_ihdr_actl_idat_fctl_fdat_fctl_fdat(); let mut buf = vec![0; reader.output_buffer_size()]; assert!(reader.info().frame_control.is_none()); reader .next_frame(&mut buf) .expect("Expecting no error for IDAT frame"); // `next_frame` doesn't advance to the next `fcTL`. assert!(reader.info().frame_control.is_none()); reader .next_frame(&mut buf) .expect("Expecting no error for 1st fdAT frame"); assert_eq!(get_fctl_sequence_number(&reader), 0); reader .next_frame(&mut buf) .expect("Expecting no error for 2nd fdAT frame"); assert_eq!(get_fctl_sequence_number(&reader), 2); let err = reader .next_frame(&mut buf) .expect_err("Main test - expecting error"); assert!( matches!(&err, DecodingError::Parameter(_)), "Unexpected kind of error: {:?}", &err, ); } /// Tests that after decoding a whole frame via [`Reader.next_row`] the call to /// [`Reader.next_frame`] will decode the **next** frame. #[test] fn test_row_by_row_then_next_frame() { let mut reader = create_reader_of_ihdr_actl_fctl_idat_fctl_fdat(); let mut buf = vec![0; reader.output_buffer_size()]; assert_eq!(get_fctl_sequence_number(&reader), 0); while let Some(_) = reader.next_row().unwrap() {} assert_eq!(get_fctl_sequence_number(&reader), 0); buf.fill(0x0f); reader .next_frame(&mut buf) .expect("Expecting no error from next_frame call"); // Verify if we have read the next `fcTL` chunk + repopulated `buf`: assert_eq!(get_fctl_sequence_number(&reader), 1); assert!(buf.iter().any(|byte| *byte != 0x0f)); } /// Tests that after decoding a whole frame via [`Reader.next_row`] it is possible /// to use [`Reader.next_row`] to decode the next frame (by using the `next_frame_info` API to /// advance to the next frame when `next_row` returns `None`). #[test] fn test_row_by_row_of_two_frames() { let mut reader = create_reader_of_ihdr_actl_fctl_idat_fctl_fdat(); let mut rows_of_frame1 = 0; assert_eq!(get_fctl_sequence_number(&reader), 0); while let Some(_) = reader.next_row().unwrap() { rows_of_frame1 += 1; } assert_eq!(rows_of_frame1, 16); assert_eq!(get_fctl_sequence_number(&reader), 0); let mut rows_of_frame2 = 0; assert_eq!(reader.next_frame_info().unwrap().sequence_number, 1); assert_eq!(get_fctl_sequence_number(&reader), 1); while let Some(_) = reader.next_row().unwrap() { rows_of_frame2 += 1; } assert_eq!(rows_of_frame2, 16); assert_eq!(get_fctl_sequence_number(&reader), 1); let err = reader .next_frame_info() .expect_err("No more frames - expecting error"); assert!( matches!(&err, DecodingError::Parameter(_)), "Unexpected kind of error: {:?}", &err, ); } /// This test is similar to `test_next_frame_polling_after_end_idat_part_of_animation`, but it /// uses `next_frame_info` calls to read to the next `fcTL` earlier - before the next call to /// `next_frame` (knowing `fcTL` before calling `next_frame` may be helpful to determine the /// size of the output buffer and/or to prepare the buffer based on the `DisposeOp` of the /// previous frames). #[test] fn test_next_frame_info_after_next_frame() { let mut reader = create_reader_of_ihdr_actl_fctl_idat_fctl_fdat(); let mut buf = vec![0; reader.output_buffer_size()]; assert_eq!(get_fctl_sequence_number(&reader), 0); reader .next_frame(&mut buf) .expect("Expecting no error for IDAT frame"); // `next_frame` doesn't advance to the next `fcTL`. assert_eq!(get_fctl_sequence_number(&reader), 0); // But `next_frame_info` can be used to go to the next `fcTL`. assert_eq!(reader.next_frame_info().unwrap().sequence_number, 1); assert_eq!(get_fctl_sequence_number(&reader), 1); reader .next_frame(&mut buf) .expect("Expecting no error for fdAT frame"); assert_eq!(get_fctl_sequence_number(&reader), 1); let err = reader .next_frame_info() .expect_err("Main test - expecting error"); assert!( matches!(&err, DecodingError::Parameter(_)), "Unexpected kind of error: {:?}", &err, ); } /// This test is similar to `test_next_frame_polling_after_end_idat_not_part_of_animation`, but /// it uses `next_frame_info` to skip the `IDAT` frame entirely + to move between frames. #[test] fn test_next_frame_info_to_skip_first_frame() { let mut reader = create_reader_of_ihdr_actl_idat_fctl_fdat_fctl_fdat(); let mut buf = vec![0; reader.output_buffer_size()]; // First (IDAT) frame doesn't have frame control info, which means // that it is not part of the animation. assert!(reader.info().frame_control.is_none()); // `next_frame_info` can be used to skip the IDAT frame (without first having to separately // discard the image data - e.g. by also calling `next_frame` first). assert_eq!(reader.next_frame_info().unwrap().sequence_number, 0); assert_eq!(get_fctl_sequence_number(&reader), 0); reader .next_frame(&mut buf) .expect("Expecting no error for 1st fdAT frame"); assert_eq!(get_fctl_sequence_number(&reader), 0); // Get the `fcTL` for the 2nd frame. assert_eq!(reader.next_frame_info().unwrap().sequence_number, 2); reader .next_frame(&mut buf) .expect("Expecting no error for 2nd fdAT frame"); assert_eq!(get_fctl_sequence_number(&reader), 2); let err = reader .next_frame_info() .expect_err("Main test - expecting error"); assert!( matches!(&err, DecodingError::Parameter(_)), "Unexpected kind of error: {:?}", &err, ); } } png-0.17.16/src/decoder/transform/palette.rs000064400000000000000000000305751046102023000170110ustar 00000000000000//! Helpers for taking a slice of indices (indices into `PLTE` and/or `trNS` //! entries) and transforming this into RGB or RGBA output. //! //! # Memoization //! //! To achieve higher throughput, `create_rgba_palette` combines entries from //! `PLTE` and `trNS` chunks into a single lookup table. This is based on the //! ideas explored in . //! //! Memoization is a trade-off: //! * On one hand, memoization requires spending X ns before starting to call //! `expand_paletted_...` functions. //! * On the other hand, memoization improves the throughput of the //! `expand_paletted_...` functions - they take Y ns less to process each byte //! //! Based on X and Y, we can try to calculate the breakeven point. It seems //! that memoization is a net benefit for images bigger than around 13x13 pixels. use super::{unpack_bits, TransformFn}; use crate::{BitDepth, Info}; pub fn create_expansion_into_rgb8(info: &Info) -> TransformFn { let rgba_palette = create_rgba_palette(info); if info.bit_depth == BitDepth::Eight { Box::new(move |input, output, _info| expand_8bit_into_rgb8(input, output, &rgba_palette)) } else { Box::new(move |input, output, info| expand_into_rgb8(input, output, info, &rgba_palette)) } } pub fn create_expansion_into_rgba8(info: &Info) -> TransformFn { let rgba_palette = create_rgba_palette(info); Box::new(move |input, output, info| { expand_paletted_into_rgba8(input, output, info, &rgba_palette) }) } fn create_rgba_palette(info: &Info) -> [[u8; 4]; 256] { let palette = info.palette.as_deref().expect("Caller should verify"); let trns = info.trns.as_deref().unwrap_or(&[]); // > The tRNS chunk shall not contain more alpha values than there are palette // entries, but a tRNS chunk may contain fewer values than there are palette // entries. In this case, the alpha value for all remaining palette entries is // assumed to be 255. // // It seems, accepted reading is to fully *ignore* an invalid tRNS as if it were // completely empty / all pixels are non-transparent. let trns = if trns.len() <= palette.len() / 3 { trns } else { &[] }; // Default to black, opaque entries. let mut rgba_palette = [[0, 0, 0, 0xFF]; 256]; // Copy `palette` (RGB) entries into `rgba_palette`. This may clobber alpha // values in `rgba_palette` - we need to fix this later. { let mut palette_iter = palette; let mut rgba_iter = &mut rgba_palette[..]; while palette_iter.len() >= 4 { // Copying 4 bytes at a time is more efficient than copying 3. // OTOH, this clobbers the alpha value in `rgba_iter[0][3]` - we // need to fix this later. rgba_iter[0].copy_from_slice(&palette_iter[0..4]); palette_iter = &palette_iter[3..]; rgba_iter = &mut rgba_iter[1..]; } if !palette_iter.is_empty() { rgba_iter[0][0..3].copy_from_slice(&palette_iter[0..3]); } } // Copy `trns` (alpha) entries into `rgba_palette`. `trns.len()` may be // smaller than `palette.len()` and therefore this is not sufficient to fix // all the clobbered alpha values. for (alpha, rgba) in trns.iter().copied().zip(rgba_palette.iter_mut()) { rgba[3] = alpha; } // Unclobber the remaining alpha values. for rgba in rgba_palette[trns.len()..(palette.len() / 3)].iter_mut() { rgba[3] = 0xFF; } rgba_palette } fn expand_8bit_into_rgb8(mut input: &[u8], mut output: &mut [u8], rgba_palette: &[[u8; 4]; 256]) { while output.len() >= 4 { // Copying 4 bytes at a time is more efficient than 3. let rgba = &rgba_palette[input[0] as usize]; output[0..4].copy_from_slice(rgba); input = &input[1..]; output = &mut output[3..]; } if !output.is_empty() { let rgba = &rgba_palette[input[0] as usize]; output[0..3].copy_from_slice(&rgba[0..3]); } } fn expand_into_rgb8(row: &[u8], buffer: &mut [u8], info: &Info, rgba_palette: &[[u8; 4]; 256]) { unpack_bits(row, buffer, 3, info.bit_depth as u8, |i, chunk| { let rgba = &rgba_palette[i as usize]; chunk[0] = rgba[0]; chunk[1] = rgba[1]; chunk[2] = rgba[2]; }) } fn expand_paletted_into_rgba8( row: &[u8], buffer: &mut [u8], info: &Info, rgba_palette: &[[u8; 4]; 256], ) { unpack_bits(row, buffer, 4, info.bit_depth as u8, |i, chunk| { chunk.copy_from_slice(&rgba_palette[i as usize]); }); } #[cfg(test)] mod test { use crate::{BitDepth, ColorType, Info, Transformations}; /// Old, non-memoized version of the code is used as a test oracle. fn oracle_expand_paletted_into_rgb8(row: &[u8], buffer: &mut [u8], info: &Info) { let palette = info.palette.as_deref().expect("Caller should verify"); let black = [0, 0, 0]; super::unpack_bits(row, buffer, 3, info.bit_depth as u8, |i, chunk| { let rgb = palette .get(3 * i as usize..3 * i as usize + 3) .unwrap_or(&black); chunk[0] = rgb[0]; chunk[1] = rgb[1]; chunk[2] = rgb[2]; }) } /// Old, non-memoized version of the code is used as a test oracle. fn oracle_expand_paletted_into_rgba8(row: &[u8], buffer: &mut [u8], info: &Info) { let palette = info.palette.as_deref().expect("Caller should verify"); let trns = info.trns.as_deref().unwrap_or(&[]); let black = [0, 0, 0]; // > The tRNS chunk shall not contain more alpha values than there are palette // entries, but a tRNS chunk may contain fewer values than there are palette // entries. In this case, the alpha value for all remaining palette entries is // assumed to be 255. // // It seems, accepted reading is to fully *ignore* an invalid tRNS as if it were // completely empty / all pixels are non-transparent. let trns = if trns.len() <= palette.len() / 3 { trns } else { &[] }; super::unpack_bits(row, buffer, 4, info.bit_depth as u8, |i, chunk| { let (rgb, a) = ( palette .get(3 * i as usize..3 * i as usize + 3) .unwrap_or(&black), *trns.get(i as usize).unwrap_or(&0xFF), ); chunk[0] = rgb[0]; chunk[1] = rgb[1]; chunk[2] = rgb[2]; chunk[3] = a; }); } fn create_info<'a>(src_bit_depth: u8, palette: &'a [u8], trns: Option<&'a [u8]>) -> Info<'a> { Info { color_type: ColorType::Indexed, bit_depth: BitDepth::from_u8(src_bit_depth).unwrap(), palette: Some(palette.into()), trns: trns.map(Into::into), ..Info::default() } } fn expand_paletted( src: &[u8], src_bit_depth: u8, palette: &[u8], trns: Option<&[u8]>, ) -> Vec { let info = create_info(src_bit_depth, palette, trns); let output_bytes_per_input_sample = match trns { None => 3, Some(_) => 4, }; let samples_count_per_byte = (8 / src_bit_depth) as usize; let samples_count = src.len() * samples_count_per_byte; let mut dst = vec![0; samples_count * output_bytes_per_input_sample]; let transform_fn = super::super::create_transform_fn(&info, Transformations::EXPAND).unwrap(); transform_fn(src, dst.as_mut_slice(), &info); { // Compare the memoization-based calculations with the old, non-memoized code. let mut simple_dst = vec![0; samples_count * output_bytes_per_input_sample]; if trns.is_none() { oracle_expand_paletted_into_rgb8(src, &mut simple_dst, &info) } else { oracle_expand_paletted_into_rgba8(src, &mut simple_dst, &info) } assert_eq!(&dst, &simple_dst); } dst } #[test] fn test_expand_paletted_rgba_8bit() { let actual = expand_paletted( &[0, 1, 2, 3], // src 8, // src_bit_depth &[ // palette 0, 1, 2, // entry #0 4, 5, 6, // entry #1 8, 9, 10, // entry #2 12, 13, 14, // entry #3 ], Some(&[3, 7, 11, 15]), // trns ); assert_eq!(actual, (0..16).collect::>()); } #[test] fn test_expand_paletted_rgb_8bit() { let actual = expand_paletted( &[0, 1, 2, 3], // src 8, // src_bit_depth &[ // palette 0, 1, 2, // entry #0 3, 4, 5, // entry #1 6, 7, 8, // entry #2 9, 10, 11, // entry #3 ], None, // trns ); assert_eq!(actual, (0..12).collect::>()); } #[test] fn test_expand_paletted_rgba_4bit() { let actual = expand_paletted( &[0x01, 0x23], // src 4, // src_bit_depth &[ // palette 0, 1, 2, // entry #0 4, 5, 6, // entry #1 8, 9, 10, // entry #2 12, 13, 14, // entry #3 ], Some(&[3, 7, 11, 15]), // trns ); assert_eq!(actual, (0..16).collect::>()); } #[test] fn test_expand_paletted_rgb_4bit() { let actual = expand_paletted( &[0x01, 0x23], // src 4, // src_bit_depth &[ // palette 0, 1, 2, // entry #0 3, 4, 5, // entry #1 6, 7, 8, // entry #2 9, 10, 11, // entry #3 ], None, // trns ); assert_eq!(actual, (0..12).collect::>()); } #[test] fn test_expand_paletted_rgba_8bit_more_trns_entries_than_palette_entries() { let actual = expand_paletted( &[0, 1, 2, 3], // src 8, // src_bit_depth &[ // palette 0, 1, 2, // entry #0 4, 5, 6, // entry #1 8, 9, 10, // entry #2 12, 13, 14, // entry #3 ], Some(&[123; 5]), // trns ); // Invalid (too-long) `trns` means that we'll use 0xFF / opaque alpha everywhere. assert_eq!( actual, vec![0, 1, 2, 0xFF, 4, 5, 6, 0xFF, 8, 9, 10, 0xFF, 12, 13, 14, 0xFF], ); } #[test] fn test_expand_paletted_rgba_8bit_less_trns_entries_than_palette_entries() { let actual = expand_paletted( &[0, 1, 2, 3], // src 8, // src_bit_depth &[ // palette 0, 1, 2, // entry #0 4, 5, 6, // entry #1 8, 9, 10, // entry #2 12, 13, 14, // entry #3 ], Some(&[3, 7]), // trns ); // Too-short `trns` is treated differently from too-long - only missing entries are // replaced with 0XFF / opaque. assert_eq!( actual, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0xFF, 12, 13, 14, 0xFF], ); } #[test] fn test_create_rgba_palette() { fn create_expected_rgba_palette(plte: &[u8], trns: &[u8]) -> [[u8; 4]; 256] { let mut rgba = [[1, 2, 3, 4]; 256]; for (i, rgba) in rgba.iter_mut().enumerate() { rgba[0] = plte.get(i * 3 + 0).map(|&r| r).unwrap_or(0); rgba[1] = plte.get(i * 3 + 1).map(|&g| g).unwrap_or(0); rgba[2] = plte.get(i * 3 + 2).map(|&b| b).unwrap_or(0); rgba[3] = trns.get(i * 1 + 0).map(|&a| a).unwrap_or(0xFF); } rgba } for plte_len in 1..=32 { for trns_len in 0..=plte_len { let plte: Vec = (0..plte_len * 3).collect(); let trns: Vec = (0..trns_len).map(|alpha| alpha + 200).collect(); let info = create_info(8, &plte, Some(&trns)); let expected = create_expected_rgba_palette(&plte, &trns); let actual = super::create_rgba_palette(&info); assert_eq!(actual, expected); } } } } png-0.17.16/src/decoder/transform.rs000064400000000000000000000156331046102023000153510ustar 00000000000000//! Transforming a decompressed, unfiltered row into the final output. mod palette; use crate::{BitDepth, ColorType, DecodingError, Info, Transformations}; use super::stream::FormatErrorInner; /// Type of a function that can transform a decompressed, unfiltered row (the /// 1st argument) into the final pixels (the 2nd argument), optionally using /// image metadata (e.g. PLTE data can be accessed using the 3rd argument). /// /// TODO: If some precomputed state is needed (e.g. to make `expand_paletted...` /// faster) then consider changing this into `Box`. pub type TransformFn = Box; /// Returns a transformation function that should be applied to image rows based /// on 1) decoded image metadata (`info`) and 2) the transformations requested /// by the crate client (`transform`). pub fn create_transform_fn( info: &Info, transform: Transformations, ) -> Result { let color_type = info.color_type; let bit_depth = info.bit_depth as u8; let trns = info.trns.is_some() || transform.contains(Transformations::ALPHA); let expand = transform.contains(Transformations::EXPAND) || transform.contains(Transformations::ALPHA); let strip16 = bit_depth == 16 && transform.contains(Transformations::STRIP_16); match color_type { ColorType::Indexed if expand => { if info.palette.is_none() { Err(DecodingError::Format( FormatErrorInner::PaletteRequired.into(), )) } else if let BitDepth::Sixteen = info.bit_depth { // This should have been caught earlier but let's check again. Can't hurt. Err(DecodingError::Format( FormatErrorInner::InvalidColorBitDepth { color_type: ColorType::Indexed, bit_depth: BitDepth::Sixteen, } .into(), )) } else { Ok(if trns { palette::create_expansion_into_rgba8(info) } else { palette::create_expansion_into_rgb8(info) }) } } ColorType::Grayscale | ColorType::GrayscaleAlpha if bit_depth < 8 && expand => { Ok(Box::new(if trns { expand_gray_u8_with_trns } else { expand_gray_u8 })) } ColorType::Grayscale | ColorType::Rgb if expand && trns => { Ok(Box::new(if bit_depth == 8 { expand_trns_line } else if strip16 { expand_trns_and_strip_line16 } else { assert_eq!(bit_depth, 16); expand_trns_line16 })) } ColorType::Grayscale | ColorType::GrayscaleAlpha | ColorType::Rgb | ColorType::Rgba if strip16 => { Ok(Box::new(transform_row_strip16)) } _ => Ok(Box::new(copy_row)), } } fn copy_row(row: &[u8], output_buffer: &mut [u8], _: &Info) { output_buffer.copy_from_slice(row); } fn transform_row_strip16(row: &[u8], output_buffer: &mut [u8], _: &Info) { for i in 0..row.len() / 2 { output_buffer[i] = row[2 * i]; } } #[inline(always)] fn unpack_bits(input: &[u8], output: &mut [u8], channels: usize, bit_depth: u8, func: F) where F: Fn(u8, &mut [u8]), { // Only [1, 2, 4, 8] are valid bit depths assert!(matches!(bit_depth, 1 | 2 | 4 | 8)); // Check that `input` is capable of producing a buffer as long as `output`: // number of shift lookups per bit depth * channels * input length assert!((8 / bit_depth as usize * channels).saturating_mul(input.len()) >= output.len()); let mut buf_chunks = output.chunks_exact_mut(channels); let mut iter = input.iter(); // `shift` iterates through the corresponding bit depth sequence: // 1 => &[7, 6, 5, 4, 3, 2, 1, 0], // 2 => &[6, 4, 2, 0], // 4 => &[4, 0], // 8 => &[0], // // `(0..8).step_by(bit_depth.into()).rev()` doesn't always optimize well so // shifts are calculated instead. (2023-08, Rust 1.71) if bit_depth == 8 { for (&curr, chunk) in iter.zip(&mut buf_chunks) { func(curr, chunk); } } else { let mask = ((1u16 << bit_depth) - 1) as u8; // These variables are initialized in the loop let mut shift = -1; let mut curr = 0; for chunk in buf_chunks { if shift < 0 { shift = 8 - bit_depth as i32; curr = *iter.next().expect("input for unpack bits is not empty"); } let pixel = (curr >> shift) & mask; func(pixel, chunk); shift -= bit_depth as i32; } } } fn expand_trns_line(input: &[u8], output: &mut [u8], info: &Info) { let channels = info.color_type.samples(); let trns = info.trns.as_deref(); for (input, output) in input .chunks_exact(channels) .zip(output.chunks_exact_mut(channels + 1)) { output[..channels].copy_from_slice(input); output[channels] = if Some(input) == trns { 0 } else { 0xFF }; } } fn expand_trns_line16(input: &[u8], output: &mut [u8], info: &Info) { let channels = info.color_type.samples(); let trns = info.trns.as_deref(); for (input, output) in input .chunks_exact(channels * 2) .zip(output.chunks_exact_mut(channels * 2 + 2)) { output[..channels * 2].copy_from_slice(input); if Some(input) == trns { output[channels * 2] = 0; output[channels * 2 + 1] = 0 } else { output[channels * 2] = 0xFF; output[channels * 2 + 1] = 0xFF }; } } fn expand_trns_and_strip_line16(input: &[u8], output: &mut [u8], info: &Info) { let channels = info.color_type.samples(); let trns = info.trns.as_deref(); for (input, output) in input .chunks_exact(channels * 2) .zip(output.chunks_exact_mut(channels + 1)) { for i in 0..channels { output[i] = input[i * 2]; } output[channels] = if Some(input) == trns { 0 } else { 0xFF }; } } fn expand_gray_u8(row: &[u8], buffer: &mut [u8], info: &Info) { let scaling_factor = (255) / ((1u16 << info.bit_depth as u8) - 1) as u8; unpack_bits(row, buffer, 1, info.bit_depth as u8, |val, chunk| { chunk[0] = val * scaling_factor }); } fn expand_gray_u8_with_trns(row: &[u8], buffer: &mut [u8], info: &Info) { let scaling_factor = (255) / ((1u16 << info.bit_depth as u8) - 1) as u8; let trns = info.trns.as_deref(); unpack_bits(row, buffer, 2, info.bit_depth as u8, |pixel, chunk| { chunk[1] = if let Some(trns) = trns { if pixel == trns[0] { 0 } else { 0xFF } } else { 0xFF }; chunk[0] = pixel * scaling_factor }); } png-0.17.16/src/decoder/unfiltering_buffer.rs000064400000000000000000000106101046102023000172030ustar 00000000000000use super::stream::{DecodingError, FormatErrorInner}; use crate::common::BytesPerPixel; use crate::filter::{unfilter, FilterType}; // Buffer for temporarily holding decompressed, not-yet-`unfilter`-ed rows. pub(crate) struct UnfilteringBuffer { /// Vec containing the uncompressed image data currently being processed. data_stream: Vec, /// Index in `data_stream` where the previous row starts. /// This excludes the filter type byte - it points at the first byte of actual pixel data. /// The pixel data is already-`unfilter`-ed. /// If `prev_start == current_start` then it means that there is no previous row. prev_start: usize, /// Index in `data_stream` where the current row starts. /// This points at the filter type byte of the current row (i.e. the actual pixel data starts at `current_start + 1`) /// The pixel data is not-yet-`unfilter`-ed. current_start: usize, } impl UnfilteringBuffer { /// Asserts in debug builds that all the invariants hold. No-op in release /// builds. Intended to be called after creating or mutating `self` to /// ensure that the final state preserves the invariants. fn debug_assert_invariants(&self) { debug_assert!(self.prev_start <= self.current_start); debug_assert!(self.prev_start <= self.data_stream.len()); debug_assert!(self.current_start <= self.data_stream.len()); } pub fn new() -> Self { let result = Self { data_stream: Vec::new(), prev_start: 0, current_start: 0, }; result.debug_assert_invariants(); result } /// Called to indicate that there is no previous row (e.g. when the current /// row is the first scanline of a given Adam7 pass). pub fn reset_prev_row(&mut self) { self.prev_start = self.current_start; self.debug_assert_invariants(); } /// Returns the previous (already `unfilter`-ed) row. pub fn prev_row(&self) -> &[u8] { // No point calling this if there is no previous row. debug_assert!(self.prev_start < self.current_start); &self.data_stream[self.prev_start..self.current_start] } /// Returns how many bytes of the current row are present in the buffer. pub fn curr_row_len(&self) -> usize { self.data_stream.len() - self.current_start } /// Returns a `&mut Vec` suitable for passing to /// `ReadDecoder.decode_image_data` or `StreamingDecoder.update`. /// /// Invariants of `self` depend on the assumption that the caller will only /// append new bytes to the returned vector (which is indeed the behavior of /// `ReadDecoder` and `StreamingDecoder`). TODO: Consider protecting the /// invariants by returning an append-only view of the vector /// (`FnMut(&[u8])`??? or maybe `std::io::Write`???). pub fn as_mut_vec(&mut self) -> &mut Vec { // Opportunistically compact the current buffer by discarding bytes // before `prev_start`. if self.prev_start > 0 { self.data_stream.copy_within(self.prev_start.., 0); self.data_stream .truncate(self.data_stream.len() - self.prev_start); self.current_start -= self.prev_start; self.prev_start = 0; self.debug_assert_invariants(); } &mut self.data_stream } /// Runs `unfilter` on the current row, and then shifts rows so that the current row becomes the previous row. /// /// Will panic if `self.curr_row_len() < rowlen`. pub fn unfilter_curr_row( &mut self, rowlen: usize, bpp: BytesPerPixel, ) -> Result<(), DecodingError> { debug_assert!(rowlen >= 2); // 1 byte for `FilterType` and at least 1 byte of pixel data. let (prev, row) = self.data_stream.split_at_mut(self.current_start); let prev: &[u8] = prev; // `prev` is immutable let prev = &prev[self.prev_start..]; debug_assert!(prev.is_empty() || prev.len() == (rowlen - 1)); // Get the filter type. let filter = FilterType::from_u8(row[0]).ok_or(DecodingError::Format( FormatErrorInner::UnknownFilterMethod(row[0]).into(), ))?; let row = &mut row[1..rowlen]; unfilter(filter, bpp, prev, row); self.prev_start = self.current_start + 1; self.current_start += rowlen; self.debug_assert_invariants(); Ok(()) } } png-0.17.16/src/decoder/zlib.rs000064400000000000000000000221631046102023000142720ustar 00000000000000use super::{stream::FormatErrorInner, DecodingError, CHUNK_BUFFER_SIZE}; use fdeflate::Decompressor; /// Ergonomics wrapper around `miniz_oxide::inflate::stream` for zlib compressed data. pub(super) struct ZlibStream { /// Current decoding state. state: Box, /// If there has been a call to decompress already. started: bool, /// Remaining buffered decoded bytes. /// The decoder sometimes wants inspect some already finished bytes for further decoding. So we /// keep a total of 32KB of decoded data available as long as more data may be appended. out_buffer: Vec, /// The first index of `out_buffer` where new data can be written. out_pos: usize, /// The first index of `out_buffer` that hasn't yet been passed to our client /// (i.e. not yet appended to the `image_data` parameter of `fn decompress` or `fn /// finish_compressed_chunks`). read_pos: usize, /// Limit on how many bytes can be decompressed in total. This field is mostly used for /// performance optimizations (e.g. to avoid allocating and zeroing out large buffers when only /// a small image is being decoded). max_total_output: usize, /// Ignore and do not calculate the Adler-32 checksum. Defaults to `true`. /// /// This flag overrides `TINFL_FLAG_COMPUTE_ADLER32`. /// /// This flag should not be modified after decompression has started. ignore_adler32: bool, } impl ZlibStream { pub(crate) fn new() -> Self { ZlibStream { state: Box::new(Decompressor::new()), started: false, out_buffer: Vec::new(), out_pos: 0, read_pos: 0, max_total_output: usize::MAX, ignore_adler32: true, } } pub(crate) fn reset(&mut self) { self.started = false; self.out_buffer.clear(); self.out_pos = 0; self.read_pos = 0; self.max_total_output = usize::MAX; *self.state = Decompressor::new(); } pub(crate) fn set_max_total_output(&mut self, n: usize) { self.max_total_output = n; } /// Set the `ignore_adler32` flag and return `true` if the flag was /// successfully set. /// /// The default is `true`. /// /// This flag cannot be modified after decompression has started until the /// [ZlibStream] is reset. pub(crate) fn set_ignore_adler32(&mut self, flag: bool) -> bool { if !self.started { self.ignore_adler32 = flag; true } else { false } } /// Return the `ignore_adler32` flag. pub(crate) fn ignore_adler32(&self) -> bool { self.ignore_adler32 } /// Fill the decoded buffer as far as possible from `data`. /// On success returns the number of consumed input bytes. pub(crate) fn decompress( &mut self, data: &[u8], image_data: &mut Vec, ) -> Result { // There may be more data past the adler32 checksum at the end of the deflate stream. We // match libpng's default behavior and ignore any trailing data. In the future we may want // to add a flag to control this behavior. if self.state.is_done() { return Ok(data.len()); } self.prepare_vec_for_appending(); if !self.started && self.ignore_adler32 { self.state.ignore_adler32(); } let (in_consumed, out_consumed) = self .state .read(data, self.out_buffer.as_mut_slice(), self.out_pos, false) .map_err(|err| { DecodingError::Format(FormatErrorInner::CorruptFlateStream { err }.into()) })?; self.started = true; self.out_pos += out_consumed; self.transfer_finished_data(image_data); self.compact_out_buffer_if_needed(); Ok(in_consumed) } /// Called after all consecutive IDAT chunks were handled. /// /// The compressed stream can be split on arbitrary byte boundaries. This enables some cleanup /// within the decompressor and flushing additional data which may have been kept back in case /// more data were passed to it. pub(crate) fn finish_compressed_chunks( &mut self, image_data: &mut Vec, ) -> Result<(), DecodingError> { if !self.started { return Ok(()); } while !self.state.is_done() { self.prepare_vec_for_appending(); let (_in_consumed, out_consumed) = self .state .read(&[], self.out_buffer.as_mut_slice(), self.out_pos, true) .map_err(|err| { DecodingError::Format(FormatErrorInner::CorruptFlateStream { err }.into()) })?; self.out_pos += out_consumed; if !self.state.is_done() { let transferred = self.transfer_finished_data(image_data); assert!( transferred > 0 || out_consumed > 0, "No more forward progress made in stream decoding." ); self.compact_out_buffer_if_needed(); } } self.transfer_finished_data(image_data); self.out_buffer.clear(); Ok(()) } /// Resize the vector to allow allocation of more data. fn prepare_vec_for_appending(&mut self) { // The `debug_assert` below explains why we can use `>=` instead of `>` in the condition // that compares `self.out_post >= self.max_total_output` in the next `if` statement. debug_assert!(!self.state.is_done()); if self.out_pos >= self.max_total_output { // This can happen when the `max_total_output` was miscalculated (e.g. // because the `IHDR` chunk was malformed and didn't match the `IDAT` chunk). In // this case, let's reset `self.max_total_output` before further calculations. self.max_total_output = usize::MAX; } let current_len = self.out_buffer.len(); let desired_len = self .out_pos .saturating_add(CHUNK_BUFFER_SIZE) .min(self.max_total_output); if current_len >= desired_len { return; } let buffered_len = self.decoding_size(self.out_buffer.len()); debug_assert!(self.out_buffer.len() <= buffered_len); self.out_buffer.resize(buffered_len, 0u8); } fn decoding_size(&self, len: usize) -> usize { // Allocate one more chunk size than currently or double the length while ensuring that the // allocation is valid and that any cursor within it will be valid. len // This keeps the buffer size a power-of-two, required by miniz_oxide. .saturating_add(CHUNK_BUFFER_SIZE.max(len)) // Ensure all buffer indices are valid cursor positions. // Note: both cut off and zero extension give correct results. .min(u64::MAX as usize) // Ensure the allocation request is valid. // TODO: maximum allocation limits? .min(isize::MAX as usize) // Don't unnecessarily allocate more than `max_total_output`. .min(self.max_total_output) } fn transfer_finished_data(&mut self, image_data: &mut Vec) -> usize { let transferred = &self.out_buffer[self.read_pos..self.out_pos]; image_data.extend_from_slice(transferred); self.read_pos = self.out_pos; transferred.len() } fn compact_out_buffer_if_needed(&mut self) { // [PNG spec](https://www.w3.org/TR/2003/REC-PNG-20031110/#10Compression) says that // "deflate/inflate compression with a sliding window (which is an upper bound on the // distances appearing in the deflate stream) of at most 32768 bytes". // // `fdeflate` requires that we keep this many most recently decompressed bytes in the // `out_buffer` - this allows referring back to them when handling "length and distance // codes" in the deflate stream). const LOOKBACK_SIZE: usize = 32768; // Compact `self.out_buffer` when "needed". Doing this conditionally helps to put an upper // bound on the amortized cost of copying the data within `self.out_buffer`. // // TODO: The factor of 4 is an ad-hoc heuristic. Consider measuring and using a different // factor. (Early experiments seem to indicate that factor of 4 is faster than a factor of // 2 and 4 * `LOOKBACK_SIZE` seems like an acceptable memory trade-off. Higher factors // result in higher memory usage, but the compaction cost is lower - factor of 4 means // that 1 byte gets copied during compaction for 3 decompressed bytes.) if self.out_pos > LOOKBACK_SIZE * 4 { // Only preserve the `lookback_buffer` and "throw away" the earlier prefix. let lookback_buffer = self.out_pos.saturating_sub(LOOKBACK_SIZE)..self.out_pos; let preserved_len = lookback_buffer.len(); self.out_buffer.copy_within(lookback_buffer, 0); self.read_pos = preserved_len; self.out_pos = preserved_len; } } } png-0.17.16/src/encoder.rs000064400000000000000000002533331046102023000133510ustar 00000000000000use borrow::Cow; use io::{Read, Write}; use ops::{Deref, DerefMut}; use std::{borrow, error, fmt, io, mem, ops, result}; use crc32fast::Hasher as Crc32; use flate2::write::ZlibEncoder; use crate::chunk::{self, ChunkType}; use crate::common::{ AnimationControl, BitDepth, BlendOp, BytesPerPixel, ColorType, Compression, DisposeOp, FrameControl, Info, ParameterError, ParameterErrorKind, PixelDimensions, ScaledFloat, }; use crate::filter::{filter, AdaptiveFilterType, FilterType}; use crate::text_metadata::{ encode_iso_8859_1, EncodableTextChunk, ITXtChunk, TEXtChunk, TextEncodingError, ZTXtChunk, }; use crate::traits::WriteBytesExt; pub type Result = result::Result; #[derive(Debug)] pub enum EncodingError { IoError(io::Error), Format(FormatError), Parameter(ParameterError), LimitsExceeded, } #[derive(Debug)] pub struct FormatError { inner: FormatErrorKind, } #[derive(Debug)] enum FormatErrorKind { ZeroWidth, ZeroHeight, InvalidColorCombination(BitDepth, ColorType), NoPalette, // TODO: wait, what? WrittenTooMuch(usize), NotAnimated, OutOfBounds, EndReached, ZeroFrames, MissingFrames, MissingData(usize), Unrecoverable, BadTextEncoding(TextEncodingError), } impl error::Error for EncodingError { fn cause(&self) -> Option<&(dyn error::Error + 'static)> { match self { EncodingError::IoError(err) => Some(err), _ => None, } } } impl fmt::Display for EncodingError { fn fmt(&self, fmt: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { use self::EncodingError::*; match self { IoError(err) => write!(fmt, "{}", err), Format(desc) => write!(fmt, "{}", desc), Parameter(desc) => write!(fmt, "{}", desc), LimitsExceeded => write!(fmt, "Limits are exceeded."), } } } impl fmt::Display for FormatError { fn fmt(&self, fmt: &mut fmt::Formatter) -> result::Result<(), fmt::Error> { use FormatErrorKind::*; match self.inner { ZeroWidth => write!(fmt, "Zero width not allowed"), ZeroHeight => write!(fmt, "Zero height not allowed"), ZeroFrames => write!(fmt, "Zero frames not allowed"), InvalidColorCombination(depth, color) => write!( fmt, "Invalid combination of bit-depth '{:?}' and color-type '{:?}'", depth, color ), NoPalette => write!(fmt, "can't write indexed image without palette"), WrittenTooMuch(index) => write!(fmt, "wrong data size, got {} bytes too many", index), NotAnimated => write!(fmt, "not an animation"), OutOfBounds => write!( fmt, "the dimension and position go over the frame boundaries" ), EndReached => write!(fmt, "all the frames have been already written"), MissingFrames => write!(fmt, "there are still frames to be written"), MissingData(n) => write!(fmt, "there are still {} bytes to be written", n), Unrecoverable => write!( fmt, "a previous error put the writer into an unrecoverable state" ), BadTextEncoding(tee) => match tee { TextEncodingError::Unrepresentable => write!( fmt, "The text metadata cannot be encoded into valid ISO 8859-1" ), TextEncodingError::InvalidKeywordSize => write!(fmt, "Invalid keyword size"), TextEncodingError::CompressionError => { write!(fmt, "Unable to compress text metadata") } }, } } } impl From for EncodingError { fn from(err: io::Error) -> EncodingError { EncodingError::IoError(err) } } impl From for io::Error { fn from(err: EncodingError) -> io::Error { io::Error::new(io::ErrorKind::Other, err.to_string()) } } // Private impl. impl From for FormatError { fn from(kind: FormatErrorKind) -> Self { FormatError { inner: kind } } } impl From for EncodingError { fn from(tee: TextEncodingError) -> Self { EncodingError::Format(FormatError { inner: FormatErrorKind::BadTextEncoding(tee), }) } } /// PNG Encoder. /// /// This configures the PNG format options such as animation chunks, palette use, color types, /// auxiliary chunks etc. /// /// FIXME: Configuring APNG might be easier (less individual errors) if we had an _adapter_ which /// borrows this mutably but guarantees that `info.frame_control` is not `None`. pub struct Encoder<'a, W: Write> { w: W, info: Info<'a>, options: Options, } /// Decoding options, internal type, forwarded to the Writer. #[derive(Default)] struct Options { filter: FilterType, adaptive_filter: AdaptiveFilterType, sep_def_img: bool, validate_sequence: bool, } impl<'a, W: Write> Encoder<'a, W> { pub fn new(w: W, width: u32, height: u32) -> Encoder<'static, W> { Encoder { w, info: Info::with_size(width, height), options: Options::default(), } } pub fn with_info(w: W, info: Info<'a>) -> Result> { if info.animation_control.is_some() != info.frame_control.is_some() { return Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())); } if let Some(actl) = info.animation_control { if actl.num_frames == 0 { return Err(EncodingError::Format(FormatErrorKind::ZeroFrames.into())); } } Ok(Encoder { w, info, options: Options::default(), }) } /// Specify that the image is animated. /// /// `num_frames` controls how many frames the animation has, while /// `num_plays` controls how many times the animation should be /// repeated until it stops, if it's zero then it will repeat /// infinitely. /// /// When this method is returns successfully then the images written will be encoded as fdAT /// chunks, except for the first image that is still encoded as `IDAT`. You can control if the /// first frame should be treated as an animation frame with [`Encoder::set_sep_def_img()`]. /// /// This method returns an error if `num_frames` is 0. pub fn set_animated(&mut self, num_frames: u32, num_plays: u32) -> Result<()> { if num_frames == 0 { return Err(EncodingError::Format(FormatErrorKind::ZeroFrames.into())); } let actl = AnimationControl { num_frames, num_plays, }; let fctl = FrameControl { sequence_number: 0, width: self.info.width, height: self.info.height, ..Default::default() }; self.info.animation_control = Some(actl); self.info.frame_control = Some(fctl); Ok(()) } /// Mark the first animated frame as a 'separate default image'. /// /// In APNG each animated frame is preceded by a special control chunk, `fcTL`. It's up to the /// encoder to decide if the first image, the standard `IDAT` data, should be part of the /// animation by emitting this chunk or by not doing so. A default image that is _not_ part of /// the animation is often interpreted as a thumbnail. /// /// This method will return an error when animation control was not configured /// (which is done by calling [`Encoder::set_animated`]). pub fn set_sep_def_img(&mut self, sep_def_img: bool) -> Result<()> { if self.info.animation_control.is_some() { self.options.sep_def_img = sep_def_img; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Sets the raw byte contents of the PLTE chunk. This method accepts /// both borrowed and owned byte data. pub fn set_palette>>(&mut self, palette: T) { self.info.palette = Some(palette.into()); } /// Sets the raw byte contents of the tRNS chunk. This method accepts /// both borrowed and owned byte data. pub fn set_trns>>(&mut self, trns: T) { self.info.trns = Some(trns.into()); } /// Set the display gamma of the source system on which the image was generated or last edited. pub fn set_source_gamma(&mut self, source_gamma: ScaledFloat) { self.info.source_gamma = Some(source_gamma); } /// Set the chromaticities for the source system's display channels (red, green, blue) and the whitepoint /// of the source system on which the image was generated or last edited. pub fn set_source_chromaticities( &mut self, source_chromaticities: super::SourceChromaticities, ) { self.info.source_chromaticities = Some(source_chromaticities); } /// Mark the image data as conforming to the SRGB color space with the specified rendering intent. /// /// Matching source gamma and chromaticities chunks are added automatically. /// Any manually specified source gamma, chromaticities, or ICC profiles will be ignored. #[doc(hidden)] #[deprecated(note = "use set_source_srgb")] pub fn set_srgb(&mut self, rendering_intent: super::SrgbRenderingIntent) { self.info.set_source_srgb(rendering_intent); self.info.source_gamma = Some(crate::srgb::substitute_gamma()); self.info.source_chromaticities = Some(crate::srgb::substitute_chromaticities()); } /// Mark the image data as conforming to the SRGB color space with the specified rendering intent. /// /// Any ICC profiles will be ignored. /// /// Source gamma and chromaticities will be written only if they're set to fallback /// values specified in [11.3.2.5](https://www.w3.org/TR/png-3/#sRGB-gAMA-cHRM). pub fn set_source_srgb(&mut self, rendering_intent: super::SrgbRenderingIntent) { self.info.set_source_srgb(rendering_intent); } /// Start encoding by writing the header data. /// /// The remaining data can be supplied by methods on the returned [`Writer`]. pub fn write_header(self) -> Result> { Writer::new(self.w, PartialInfo::new(&self.info), self.options).init(&self.info) } /// Set the color of the encoded image. /// /// These correspond to the color types in the png IHDR data that will be written. The length /// of the image data that is later supplied must match the color type, otherwise an error will /// be emitted. pub fn set_color(&mut self, color: ColorType) { self.info.color_type = color; } /// Set the indicated depth of the image data. pub fn set_depth(&mut self, depth: BitDepth) { self.info.bit_depth = depth; } /// Set compression parameters. /// /// Accepts a `Compression` or any type that can transform into a `Compression`. Notably `deflate::Compression` and /// `deflate::CompressionOptions` which "just work". pub fn set_compression(&mut self, compression: Compression) { self.info.compression = compression; } /// Set the used filter type. /// /// The default filter is [`FilterType::Sub`] which provides a basic prediction algorithm for /// sample values based on the previous. For a potentially better compression ratio, at the /// cost of more complex processing, try out [`FilterType::Paeth`]. pub fn set_filter(&mut self, filter: FilterType) { self.options.filter = filter; } /// Set the adaptive filter type. /// /// Adaptive filtering attempts to select the best filter for each line /// based on heuristics which minimize the file size for compression rather /// than use a single filter for the entire image. The default method is /// [`AdaptiveFilterType::NonAdaptive`]. pub fn set_adaptive_filter(&mut self, adaptive_filter: AdaptiveFilterType) { self.options.adaptive_filter = adaptive_filter; } /// Set the fraction of time every frame is going to be displayed, in seconds. /// /// *Note that this parameter can be set for each individual frame after /// [`Encoder::write_header`] is called. (see [`Writer::set_frame_delay`])* /// /// If the denominator is 0, it is to be treated as if it were 100 /// (that is, the numerator then specifies 1/100ths of a second). /// If the value of the numerator is 0 the decoder should render the next frame /// as quickly as possible, though viewers may impose a reasonable lower bound. /// /// The default value is 0 for both the numerator and denominator. /// /// This method will return an error if the image is not animated. /// (see [`set_animated`]) /// /// [`write_header`]: Self::write_header /// [`set_animated`]: Self::set_animated pub fn set_frame_delay(&mut self, numerator: u16, denominator: u16) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { fctl.delay_den = denominator; fctl.delay_num = numerator; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the blend operation for every frame. /// /// The blend operation specifies whether the frame is to be alpha blended /// into the current output buffer content, or whether it should completely /// replace its region in the output buffer. /// /// *Note that this parameter can be set for each individual frame after /// [`write_header`] is called. (see [`Writer::set_blend_op`])* /// /// See the [`BlendOp`] documentation for the possible values and their effects. /// /// *Note that for the first frame the two blend modes are functionally /// equivalent due to the clearing of the output buffer at the beginning /// of each play.* /// /// The default value is [`BlendOp::Source`]. /// /// This method will return an error if the image is not animated. /// (see [`set_animated`]) /// /// [`write_header`]: Self::write_header /// [`set_animated`]: Self::set_animated pub fn set_blend_op(&mut self, op: BlendOp) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { fctl.blend_op = op; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the dispose operation for every frame. /// /// The dispose operation specifies how the output buffer should be changed /// at the end of the delay (before rendering the next frame) /// /// *Note that this parameter can be set for each individual frame after /// [`write_header`] is called (see [`Writer::set_dispose_op`])* /// /// See the [`DisposeOp`] documentation for the possible values and their effects. /// /// *Note that if the first frame uses [`DisposeOp::Previous`] /// it will be treated as [`DisposeOp::Background`].* /// /// The default value is [`DisposeOp::None`]. /// /// This method will return an error if the image is not animated. /// (see [`set_animated`]) /// /// [`set_animated`]: Self::set_animated /// [`write_header`]: Self::write_header pub fn set_dispose_op(&mut self, op: DisposeOp) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { fctl.dispose_op = op; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } pub fn set_pixel_dims(&mut self, pixel_dims: Option) { self.info.pixel_dims = pixel_dims } /// Convenience function to add tEXt chunks to [`Info`] struct pub fn add_text_chunk(&mut self, keyword: String, text: String) -> Result<()> { let text_chunk = TEXtChunk::new(keyword, text); self.info.uncompressed_latin1_text.push(text_chunk); Ok(()) } /// Convenience function to add zTXt chunks to [`Info`] struct pub fn add_ztxt_chunk(&mut self, keyword: String, text: String) -> Result<()> { let text_chunk = ZTXtChunk::new(keyword, text); self.info.compressed_latin1_text.push(text_chunk); Ok(()) } /// Convenience function to add iTXt chunks to [`Info`] struct /// /// This function only sets the `keyword` and `text` field of the iTXt chunk. /// To set the other fields, create a [`ITXtChunk`] directly, and then encode it to the output stream. pub fn add_itxt_chunk(&mut self, keyword: String, text: String) -> Result<()> { let text_chunk = ITXtChunk::new(keyword, text); self.info.utf8_text.push(text_chunk); Ok(()) } /// Validate the written image sequence. /// /// When validation is turned on (it's turned off by default) then attempts to write more than /// one `IDAT` image or images beyond the number of frames indicated in the animation control /// chunk will fail and return an error result instead. Attempts to [finish][finish] the image /// with missing frames will also return an error. /// /// [finish]: StreamWriter::finish /// /// (It's possible to circumvent these checks by writing raw chunks instead.) pub fn validate_sequence(&mut self, validate: bool) { self.options.validate_sequence = validate; } } /// PNG writer /// /// Progresses through the image by writing images, frames, or raw individual chunks. This is /// constructed through [`Encoder::write_header()`]. /// /// FIXME: Writing of animated chunks might be clearer if we had an _adapter_ that you would call /// to guarantee the next image to be prefaced with a fcTL-chunk, and all other chunks would be /// guaranteed to be `IDAT`/not affected by APNG's frame control. pub struct Writer { /// The underlying writer. w: W, /// The local version of the `Info` struct. info: PartialInfo, /// Global encoding options. options: Options, /// The total number of image frames, counting all consecutive IDAT and fdAT chunks. images_written: u64, /// The total number of animation frames, that is equivalent to counting fcTL chunks. animation_written: u32, /// A flag to note when the IEND chunk was already added. /// This is only set on code paths that drop `Self` to control the destructor. iend_written: bool, } /// Contains the subset of attributes of [Info] needed for [Writer] to function struct PartialInfo { width: u32, height: u32, bit_depth: BitDepth, color_type: ColorType, frame_control: Option, animation_control: Option, compression: Compression, has_palette: bool, } impl PartialInfo { fn new(info: &Info) -> Self { PartialInfo { width: info.width, height: info.height, bit_depth: info.bit_depth, color_type: info.color_type, frame_control: info.frame_control, animation_control: info.animation_control, compression: info.compression, has_palette: info.palette.is_some(), } } fn bpp_in_prediction(&self) -> BytesPerPixel { // Passthrough self.to_info().bpp_in_prediction() } fn raw_row_length(&self) -> usize { // Passthrough self.to_info().raw_row_length() } fn raw_row_length_from_width(&self, width: u32) -> usize { // Passthrough self.to_info().raw_row_length_from_width(width) } /// Converts this partial info to an owned Info struct, /// setting missing values to their defaults fn to_info(&self) -> Info<'static> { Info { width: self.width, height: self.height, bit_depth: self.bit_depth, color_type: self.color_type, frame_control: self.frame_control, animation_control: self.animation_control, compression: self.compression, ..Default::default() } } } const DEFAULT_BUFFER_LENGTH: usize = 4 * 1024; pub(crate) fn write_chunk(mut w: W, name: chunk::ChunkType, data: &[u8]) -> Result<()> { w.write_be(data.len() as u32)?; w.write_all(&name.0)?; w.write_all(data)?; let mut crc = Crc32::new(); crc.update(&name.0); crc.update(data); w.write_be(crc.finalize())?; Ok(()) } impl Writer { fn new(w: W, info: PartialInfo, options: Options) -> Writer { Writer { w, info, options, images_written: 0, animation_written: 0, iend_written: false, } } fn init(mut self, info: &Info<'_>) -> Result { if self.info.width == 0 { return Err(EncodingError::Format(FormatErrorKind::ZeroWidth.into())); } if self.info.height == 0 { return Err(EncodingError::Format(FormatErrorKind::ZeroHeight.into())); } if self .info .color_type .is_combination_invalid(self.info.bit_depth) { return Err(EncodingError::Format( FormatErrorKind::InvalidColorCombination(self.info.bit_depth, self.info.color_type) .into(), )); } self.w.write_all(&[137, 80, 78, 71, 13, 10, 26, 10])?; // PNG signature #[allow(deprecated)] info.encode(&mut self.w)?; Ok(self) } /// Write a raw chunk of PNG data. /// /// The chunk will have its CRC calculated and correctly. The data is not filtered in any way, /// but the chunk needs to be short enough to have its length encoded correctly. pub fn write_chunk(&mut self, name: ChunkType, data: &[u8]) -> Result<()> { use std::convert::TryFrom; if u32::try_from(data.len()).map_or(true, |length| length > i32::MAX as u32) { let kind = FormatErrorKind::WrittenTooMuch(data.len() - i32::MAX as usize); return Err(EncodingError::Format(kind.into())); } write_chunk(&mut self.w, name, data) } pub fn write_text_chunk(&mut self, text_chunk: &T) -> Result<()> { text_chunk.encode(&mut self.w) } /// Check if we should allow writing another image. fn validate_new_image(&self) -> Result<()> { if !self.options.validate_sequence { return Ok(()); } match self.info.animation_control { None => { if self.images_written == 0 { Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::EndReached.into())) } } Some(_) => { if self.info.frame_control.is_some() { Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::EndReached.into())) } } } } fn validate_sequence_done(&self) -> Result<()> { if !self.options.validate_sequence { return Ok(()); } if (self.info.animation_control.is_some() && self.info.frame_control.is_some()) || self.images_written == 0 { Err(EncodingError::Format(FormatErrorKind::MissingFrames.into())) } else { Ok(()) } } const MAX_IDAT_CHUNK_LEN: u32 = u32::MAX >> 1; #[allow(non_upper_case_globals)] const MAX_fdAT_CHUNK_LEN: u32 = (u32::MAX >> 1) - 4; /// Writes the next image data. pub fn write_image_data(&mut self, data: &[u8]) -> Result<()> { if self.info.color_type == ColorType::Indexed && !self.info.has_palette { return Err(EncodingError::Format(FormatErrorKind::NoPalette.into())); } self.validate_new_image()?; let width: usize; let height: usize; if let Some(ref mut fctl) = self.info.frame_control { width = fctl.width as usize; height = fctl.height as usize; } else { width = self.info.width as usize; height = self.info.height as usize; } let in_len = self.info.raw_row_length_from_width(width as u32) - 1; let data_size = in_len * height; if data_size != data.len() { return Err(EncodingError::Parameter( ParameterErrorKind::ImageBufferSize { expected: data_size, actual: data.len(), } .into(), )); } let prev = vec![0; in_len]; let mut prev = prev.as_slice(); let bpp = self.info.bpp_in_prediction(); let filter_method = self.options.filter; let adaptive_method = self.options.adaptive_filter; let zlib_encoded = match self.info.compression { Compression::Fast => { let mut compressor = fdeflate::Compressor::new(std::io::Cursor::new(Vec::new()))?; let mut current = vec![0; in_len + 1]; for line in data.chunks(in_len) { let filter_type = filter( filter_method, adaptive_method, bpp, prev, line, &mut current[1..], ); current[0] = filter_type as u8; compressor.write_data(¤t)?; prev = line; } let compressed = compressor.finish()?.into_inner(); if compressed.len() > fdeflate::StoredOnlyCompressor::<()>::compressed_size((in_len + 1) * height) { // Write uncompressed data since the result from fast compression would take // more space than that. // // We always use FilterType::NoFilter here regardless of the filter method // requested by the user. Doing filtering again would only add performance // cost for both encoding and subsequent decoding, without improving the // compression ratio. let mut compressor = fdeflate::StoredOnlyCompressor::new(std::io::Cursor::new(Vec::new()))?; for line in data.chunks(in_len) { compressor.write_data(&[0])?; compressor.write_data(line)?; } compressor.finish()?.into_inner() } else { compressed } } _ => { let mut current = vec![0; in_len]; let mut zlib = ZlibEncoder::new(Vec::new(), self.info.compression.to_options()); for line in data.chunks(in_len) { let filter_type = filter( filter_method, adaptive_method, bpp, prev, line, &mut current, ); zlib.write_all(&[filter_type as u8])?; zlib.write_all(¤t)?; prev = line; } zlib.finish()? } }; match self.info.frame_control { None => { self.write_zlib_encoded_idat(&zlib_encoded)?; } Some(_) if self.should_skip_frame_control_on_default_image() => { self.write_zlib_encoded_idat(&zlib_encoded)?; } Some(ref mut fctl) => { fctl.encode(&mut self.w)?; fctl.sequence_number = fctl.sequence_number.wrapping_add(1); self.animation_written += 1; // If the default image is the first frame of an animation, it's still an IDAT. if self.images_written == 0 { self.write_zlib_encoded_idat(&zlib_encoded)?; } else { let buff_size = zlib_encoded.len().min(Self::MAX_fdAT_CHUNK_LEN as usize); let mut alldata = vec![0u8; 4 + buff_size]; for chunk in zlib_encoded.chunks(Self::MAX_fdAT_CHUNK_LEN as usize) { alldata[..4].copy_from_slice(&fctl.sequence_number.to_be_bytes()); alldata[4..][..chunk.len()].copy_from_slice(chunk); write_chunk(&mut self.w, chunk::fdAT, &alldata[..4 + chunk.len()])?; fctl.sequence_number = fctl.sequence_number.wrapping_add(1); } } } } self.increment_images_written(); Ok(()) } fn increment_images_written(&mut self) { self.images_written = self.images_written.saturating_add(1); if let Some(actl) = self.info.animation_control { if actl.num_frames <= self.animation_written { // If we've written all animation frames, all following will be normal image chunks. self.info.frame_control = None; } } } fn write_iend(&mut self) -> Result<()> { self.iend_written = true; self.write_chunk(chunk::IEND, &[]) } fn should_skip_frame_control_on_default_image(&self) -> bool { self.options.sep_def_img && self.images_written == 0 } fn write_zlib_encoded_idat(&mut self, zlib_encoded: &[u8]) -> Result<()> { for chunk in zlib_encoded.chunks(Self::MAX_IDAT_CHUNK_LEN as usize) { self.write_chunk(chunk::IDAT, chunk)?; } Ok(()) } /// Set the used filter type for the following frames. /// /// The default filter is [`FilterType::Sub`] which provides a basic prediction algorithm for /// sample values based on the previous. For a potentially better compression ratio, at the /// cost of more complex processing, try out [`FilterType::Paeth`]. pub fn set_filter(&mut self, filter: FilterType) { self.options.filter = filter; } /// Set the adaptive filter type for the following frames. /// /// Adaptive filtering attempts to select the best filter for each line /// based on heuristics which minimize the file size for compression rather /// than use a single filter for the entire image. The default method is /// [`AdaptiveFilterType::NonAdaptive`]. pub fn set_adaptive_filter(&mut self, adaptive_filter: AdaptiveFilterType) { self.options.adaptive_filter = adaptive_filter; } /// Set the fraction of time the following frames are going to be displayed, /// in seconds /// /// If the denominator is 0, it is to be treated as if it were 100 /// (that is, the numerator then specifies 1/100ths of a second). /// If the value of the numerator is 0 the decoder should render the next frame /// as quickly as possible, though viewers may impose a reasonable lower bound. /// /// This method will return an error if the image is not animated. pub fn set_frame_delay(&mut self, numerator: u16, denominator: u16) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { fctl.delay_den = denominator; fctl.delay_num = numerator; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the dimension of the following frames. /// /// This function will return an error when: /// - The image is not an animated; /// /// - The selected dimension, considering also the current frame position, /// goes outside the image boundaries; /// /// - One or both the width and height are 0; /// // ??? TODO ??? // - The next frame is the default image pub fn set_frame_dimension(&mut self, width: u32, height: u32) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { if Some(width) > self.info.width.checked_sub(fctl.x_offset) || Some(height) > self.info.height.checked_sub(fctl.y_offset) { return Err(EncodingError::Format(FormatErrorKind::OutOfBounds.into())); } else if width == 0 { return Err(EncodingError::Format(FormatErrorKind::ZeroWidth.into())); } else if height == 0 { return Err(EncodingError::Format(FormatErrorKind::ZeroHeight.into())); } fctl.width = width; fctl.height = height; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the position of the following frames. /// /// An error will be returned if: /// - The image is not animated; /// /// - The selected position, considering also the current frame dimension, /// goes outside the image boundaries; /// // ??? TODO ??? // - The next frame is the default image pub fn set_frame_position(&mut self, x: u32, y: u32) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { if Some(x) > self.info.width.checked_sub(fctl.width) || Some(y) > self.info.height.checked_sub(fctl.height) { return Err(EncodingError::Format(FormatErrorKind::OutOfBounds.into())); } fctl.x_offset = x; fctl.y_offset = y; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the frame dimension to occupy all the image, starting from /// the current position. /// /// To reset the frame to the full image size [`reset_frame_position`] /// should be called first. /// /// This method will return an error if the image is not animated. /// /// [`reset_frame_position`]: Writer::reset_frame_position pub fn reset_frame_dimension(&mut self) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { fctl.width = self.info.width - fctl.x_offset; fctl.height = self.info.height - fctl.y_offset; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the frame position to (0, 0). /// /// Equivalent to calling [`set_frame_position(0, 0)`]. /// /// This method will return an error if the image is not animated. /// /// [`set_frame_position(0, 0)`]: Writer::set_frame_position pub fn reset_frame_position(&mut self) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { fctl.x_offset = 0; fctl.y_offset = 0; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the blend operation for the following frames. /// /// The blend operation specifies whether the frame is to be alpha blended /// into the current output buffer content, or whether it should completely /// replace its region in the output buffer. /// /// See the [`BlendOp`] documentation for the possible values and their effects. /// /// *Note that for the first frame the two blend modes are functionally /// equivalent due to the clearing of the output buffer at the beginning /// of each play.* /// /// This method will return an error if the image is not animated. pub fn set_blend_op(&mut self, op: BlendOp) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { fctl.blend_op = op; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the dispose operation for the following frames. /// /// The dispose operation specifies how the output buffer should be changed /// at the end of the delay (before rendering the next frame) /// /// See the [`DisposeOp`] documentation for the possible values and their effects. /// /// *Note that if the first frame uses [`DisposeOp::Previous`] /// it will be treated as [`DisposeOp::Background`].* /// /// This method will return an error if the image is not animated. pub fn set_dispose_op(&mut self, op: DisposeOp) -> Result<()> { if let Some(ref mut fctl) = self.info.frame_control { fctl.dispose_op = op; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Create a stream writer. /// /// This allows you to create images that do not fit in memory. The default /// chunk size is 4K, use `stream_writer_with_size` to set another chunk /// size. /// /// This borrows the writer which allows for manually appending additional /// chunks after the image data has been written. pub fn stream_writer(&mut self) -> Result> { self.stream_writer_with_size(DEFAULT_BUFFER_LENGTH) } /// Create a stream writer with custom buffer size. /// /// See [`stream_writer`]. /// /// [`stream_writer`]: Self::stream_writer pub fn stream_writer_with_size(&mut self, size: usize) -> Result> { StreamWriter::new(ChunkOutput::Borrowed(self), size) } /// Turn this into a stream writer for image data. /// /// This allows you to create images that do not fit in memory. The default /// chunk size is 4K, use [`stream_writer_with_size`] to set another chunk /// size. /// /// [`stream_writer_with_size`]: Self::stream_writer_with_size pub fn into_stream_writer(self) -> Result> { self.into_stream_writer_with_size(DEFAULT_BUFFER_LENGTH) } /// Turn this into a stream writer with custom buffer size. /// /// See [`into_stream_writer`]. /// /// [`into_stream_writer`]: Self::into_stream_writer pub fn into_stream_writer_with_size(self, size: usize) -> Result> { StreamWriter::new(ChunkOutput::Owned(self), size) } /// Consume the stream writer with validation. /// /// Unlike a simple drop this ensures that the final chunk was written correctly. When other /// validation options (chunk sequencing) had been turned on in the configuration then it will /// also do a check on their correctness _before_ writing the final chunk. pub fn finish(mut self) -> Result<()> { self.validate_sequence_done()?; self.write_iend()?; self.w.flush()?; // Explicitly drop `self` just for clarity. drop(self); Ok(()) } } impl Drop for Writer { fn drop(&mut self) { if !self.iend_written { let _ = self.write_iend(); } } } // This should be moved to Writer after `Info::encoding` is gone pub(crate) fn write_iccp_chunk( w: &mut W, profile_name: &str, icc_profile: &[u8], ) -> Result<()> { let profile_name = encode_iso_8859_1(profile_name)?; if profile_name.len() < 1 || profile_name.len() > 79 { return Err(TextEncodingError::InvalidKeywordSize.into()); } let estimated_compressed_size = icc_profile.len() * 3 / 4; let chunk_size = profile_name .len() .checked_add(2) // string NUL + compression type. Checked add optimizes out later Vec reallocations. .and_then(|s| s.checked_add(estimated_compressed_size)) .ok_or(EncodingError::LimitsExceeded)?; let mut data = Vec::new(); data.try_reserve_exact(chunk_size) .map_err(|_| EncodingError::LimitsExceeded)?; data.extend(profile_name.into_iter().chain([0, 0])); let mut encoder = ZlibEncoder::new(data, flate2::Compression::default()); encoder.write_all(icc_profile)?; write_chunk(w, chunk::iCCP, &encoder.finish()?) } enum ChunkOutput<'a, W: Write> { Borrowed(&'a mut Writer), Owned(Writer), } // opted for deref for practical reasons impl<'a, W: Write> Deref for ChunkOutput<'a, W> { type Target = Writer; fn deref(&self) -> &Self::Target { match self { ChunkOutput::Borrowed(writer) => writer, ChunkOutput::Owned(writer) => writer, } } } impl<'a, W: Write> DerefMut for ChunkOutput<'a, W> { fn deref_mut(&mut self) -> &mut Self::Target { match self { ChunkOutput::Borrowed(writer) => writer, ChunkOutput::Owned(writer) => writer, } } } /// This writer is used between the actual writer and the /// ZlibEncoder and has the job of packaging the compressed /// data into a PNG chunk, based on the image metadata /// /// Currently the way it works is that the specified buffer /// will hold one chunk at the time and buffer the incoming /// data until `flush` is called or the maximum chunk size /// is reached. /// /// The maximum chunk is the smallest between the selected buffer size /// and `u32::MAX >> 1` (`0x7fffffff` or `2147483647` dec) /// /// When a chunk has to be flushed the length (that is now known) /// and the CRC will be written at the correct locations in the chunk. struct ChunkWriter<'a, W: Write> { writer: ChunkOutput<'a, W>, buffer: Vec, /// keeps track of where the last byte was written index: usize, curr_chunk: ChunkType, } impl<'a, W: Write> ChunkWriter<'a, W> { fn new(writer: ChunkOutput<'a, W>, buf_len: usize) -> ChunkWriter<'a, W> { // currently buf_len will determine the size of each chunk // the len is capped to the maximum size every chunk can hold // (this wont ever overflow an u32) // // TODO (maybe): find a way to hold two chunks at a time if `usize` // is 64 bits. const CAP: usize = u32::MAX as usize >> 1; let curr_chunk = if writer.images_written == 0 { chunk::IDAT } else { chunk::fdAT }; ChunkWriter { writer, buffer: vec![0; CAP.min(buf_len)], index: 0, curr_chunk, } } /// Returns the size of each scanline for the next frame /// paired with the size of the whole frame /// /// This is used by the `StreamWriter` to know when the scanline ends /// so it can filter compress it and also to know when to start /// the next one fn next_frame_info(&self) -> (usize, usize) { let wrt = self.writer.deref(); let width: usize; let height: usize; if let Some(fctl) = wrt.info.frame_control { width = fctl.width as usize; height = fctl.height as usize; } else { width = wrt.info.width as usize; height = wrt.info.height as usize; } let in_len = wrt.info.raw_row_length_from_width(width as u32) - 1; let data_size = in_len * height; (in_len, data_size) } /// NOTE: this bypasses the internal buffer so the flush method should be called before this /// in the case there is some data left in the buffer when this is called, it will panic fn write_header(&mut self) -> Result<()> { assert_eq!(self.index, 0, "Called when not flushed"); let wrt = self.writer.deref_mut(); self.curr_chunk = if wrt.images_written == 0 { chunk::IDAT } else { chunk::fdAT }; match wrt.info.frame_control { Some(_) if wrt.should_skip_frame_control_on_default_image() => {} Some(ref mut fctl) => { fctl.encode(&mut wrt.w)?; fctl.sequence_number += 1; } _ => {} } Ok(()) } /// Set the [`FrameControl`] for the following frame /// /// It will ignore the `sequence_number` of the parameter /// as it is updated internally. fn set_fctl(&mut self, f: FrameControl) { if let Some(ref mut fctl) = self.writer.info.frame_control { // Ignore the sequence number *fctl = FrameControl { sequence_number: fctl.sequence_number, ..f }; } else { panic!("This function must be called on an animated PNG") } } /// Flushes the current chunk fn flush_inner(&mut self) -> io::Result<()> { if self.index > 0 { // flush the chunk and reset everything write_chunk( &mut self.writer.w, self.curr_chunk, &self.buffer[..self.index], )?; self.index = 0; } Ok(()) } } impl<'a, W: Write> Write for ChunkWriter<'a, W> { fn write(&mut self, mut data: &[u8]) -> io::Result { if data.is_empty() { return Ok(0); } // index == 0 means a chunk has been flushed out if self.index == 0 { let wrt = self.writer.deref_mut(); // Prepare the next animated frame, if any. let no_fctl = wrt.should_skip_frame_control_on_default_image(); if wrt.info.frame_control.is_some() && !no_fctl { let fctl = wrt.info.frame_control.as_mut().unwrap(); self.buffer[0..4].copy_from_slice(&fctl.sequence_number.to_be_bytes()); fctl.sequence_number += 1; self.index = 4; } } // Cap the buffer length to the maximum number of bytes that can't still // be added to the current chunk let written = data.len().min(self.buffer.len() - self.index); data = &data[..written]; self.buffer[self.index..][..written].copy_from_slice(data); self.index += written; // if the maximum data for this chunk as been reached it needs to be flushed if self.index == self.buffer.len() { self.flush_inner()?; } Ok(written) } fn flush(&mut self) -> io::Result<()> { self.flush_inner() } } impl Drop for ChunkWriter<'_, W> { fn drop(&mut self) { let _ = self.flush(); } } // TODO: find a better name // /// This enum is used to be allow the `StreamWriter` to keep /// its inner `ChunkWriter` without wrapping it inside a /// `ZlibEncoder`. This is used in the case that between the /// change of state that happens when the last write of a frame /// is performed an error occurs, which obviously has to be returned. /// This creates the problem of where to store the writer before /// exiting the function, and this is where `Wrapper` comes in. /// /// Unfortunately the `ZlibWriter` can't be used because on the /// write following the error, `finish` would be called and that /// would write some data even if 0 bytes where compressed. /// /// If the `finish` function fails then there is nothing much to /// do as the `ChunkWriter` would get lost so the `Unrecoverable` /// variant is used to signal that. enum Wrapper<'a, W: Write> { Chunk(ChunkWriter<'a, W>), Zlib(ZlibEncoder>), Unrecoverable, /// This is used in-between, should never be matched None, } impl<'a, W: Write> Wrapper<'a, W> { /// Like `Option::take` this returns the `Wrapper` contained /// in `self` and replaces it with `Wrapper::None` fn take(&mut self) -> Wrapper<'a, W> { let mut swap = Wrapper::None; mem::swap(self, &mut swap); swap } } /// Streaming PNG writer /// /// This may silently fail in the destructor, so it is a good idea to call /// [`finish`] or [`flush`] before dropping. /// /// [`finish`]: Self::finish /// [`flush`]: Write::flush pub struct StreamWriter<'a, W: Write> { /// The option here is needed in order to access the inner `ChunkWriter` in-between /// each frame, which is needed for writing the fcTL chunks between each frame writer: Wrapper<'a, W>, prev_buf: Vec, curr_buf: Vec, /// Amount of data already written index: usize, /// length of the current scanline line_len: usize, /// size of the frame (width * height * sample_size) to_write: usize, width: u32, height: u32, bpp: BytesPerPixel, filter: FilterType, adaptive_filter: AdaptiveFilterType, fctl: Option, compression: Compression, } impl<'a, W: Write> StreamWriter<'a, W> { fn new(writer: ChunkOutput<'a, W>, buf_len: usize) -> Result> { let PartialInfo { width, height, frame_control: fctl, compression, .. } = writer.info; let bpp = writer.info.bpp_in_prediction(); let in_len = writer.info.raw_row_length() - 1; let filter = writer.options.filter; let adaptive_filter = writer.options.adaptive_filter; let prev_buf = vec![0; in_len]; let curr_buf = vec![0; in_len]; let mut chunk_writer = ChunkWriter::new(writer, buf_len); let (line_len, to_write) = chunk_writer.next_frame_info(); chunk_writer.write_header()?; let zlib = ZlibEncoder::new(chunk_writer, compression.to_options()); Ok(StreamWriter { writer: Wrapper::Zlib(zlib), index: 0, prev_buf, curr_buf, bpp, filter, width, height, adaptive_filter, line_len, to_write, fctl, compression, }) } /// Set the used filter type for the next frame. /// /// The default filter is [`FilterType::Sub`] which provides a basic prediction algorithm for /// sample values based on the previous. /// /// For optimal compression ratio you should enable adaptive filtering /// instead of setting a single filter for the entire image, see /// [set_adaptive_filter](Self::set_adaptive_filter). pub fn set_filter(&mut self, filter: FilterType) { self.filter = filter; } /// Set the adaptive filter type for the next frame. /// /// Adaptive filtering attempts to select the best filter for each line /// based on heuristics which minimize the file size for compression rather /// than use a single filter for the entire image. /// /// The default method is [`AdaptiveFilterType::NonAdaptive`]. pub fn set_adaptive_filter(&mut self, adaptive_filter: AdaptiveFilterType) { self.adaptive_filter = adaptive_filter; } /// Set the fraction of time the following frames are going to be displayed, /// in seconds /// /// If the denominator is 0, it is to be treated as if it were 100 /// (that is, the numerator then specifies 1/100ths of a second). /// If the value of the numerator is 0 the decoder should render the next frame /// as quickly as possible, though viewers may impose a reasonable lower bound. /// /// This method will return an error if the image is not animated. pub fn set_frame_delay(&mut self, numerator: u16, denominator: u16) -> Result<()> { if let Some(ref mut fctl) = self.fctl { fctl.delay_den = denominator; fctl.delay_num = numerator; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the dimension of the following frames. /// /// This function will return an error when: /// - The image is not an animated; /// /// - The selected dimension, considering also the current frame position, /// goes outside the image boundaries; /// /// - One or both the width and height are 0; /// pub fn set_frame_dimension(&mut self, width: u32, height: u32) -> Result<()> { if let Some(ref mut fctl) = self.fctl { if Some(width) > self.width.checked_sub(fctl.x_offset) || Some(height) > self.height.checked_sub(fctl.y_offset) { return Err(EncodingError::Format(FormatErrorKind::OutOfBounds.into())); } else if width == 0 { return Err(EncodingError::Format(FormatErrorKind::ZeroWidth.into())); } else if height == 0 { return Err(EncodingError::Format(FormatErrorKind::ZeroHeight.into())); } fctl.width = width; fctl.height = height; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the position of the following frames. /// /// An error will be returned if: /// - The image is not animated; /// /// - The selected position, considering also the current frame dimension, /// goes outside the image boundaries; /// pub fn set_frame_position(&mut self, x: u32, y: u32) -> Result<()> { if let Some(ref mut fctl) = self.fctl { if Some(x) > self.width.checked_sub(fctl.width) || Some(y) > self.height.checked_sub(fctl.height) { return Err(EncodingError::Format(FormatErrorKind::OutOfBounds.into())); } fctl.x_offset = x; fctl.y_offset = y; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the frame dimension to occupy all the image, starting from /// the current position. /// /// To reset the frame to the full image size [`reset_frame_position`] /// should be called first. /// /// This method will return an error if the image is not animated. /// /// [`reset_frame_position`]: Writer::reset_frame_position pub fn reset_frame_dimension(&mut self) -> Result<()> { if let Some(ref mut fctl) = self.fctl { fctl.width = self.width - fctl.x_offset; fctl.height = self.height - fctl.y_offset; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the frame position to (0, 0). /// /// Equivalent to calling [`set_frame_position(0, 0)`]. /// /// This method will return an error if the image is not animated. /// /// [`set_frame_position(0, 0)`]: Writer::set_frame_position pub fn reset_frame_position(&mut self) -> Result<()> { if let Some(ref mut fctl) = self.fctl { fctl.x_offset = 0; fctl.y_offset = 0; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the blend operation for the following frames. /// /// The blend operation specifies whether the frame is to be alpha blended /// into the current output buffer content, or whether it should completely /// replace its region in the output buffer. /// /// See the [`BlendOp`] documentation for the possible values and their effects. /// /// *Note that for the first frame the two blend modes are functionally /// equivalent due to the clearing of the output buffer at the beginning /// of each play.* /// /// This method will return an error if the image is not animated. pub fn set_blend_op(&mut self, op: BlendOp) -> Result<()> { if let Some(ref mut fctl) = self.fctl { fctl.blend_op = op; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Set the dispose operation for the following frames. /// /// The dispose operation specifies how the output buffer should be changed /// at the end of the delay (before rendering the next frame) /// /// See the [`DisposeOp`] documentation for the possible values and their effects. /// /// *Note that if the first frame uses [`DisposeOp::Previous`] /// it will be treated as [`DisposeOp::Background`].* /// /// This method will return an error if the image is not animated. pub fn set_dispose_op(&mut self, op: DisposeOp) -> Result<()> { if let Some(ref mut fctl) = self.fctl { fctl.dispose_op = op; Ok(()) } else { Err(EncodingError::Format(FormatErrorKind::NotAnimated.into())) } } /// Consume the stream writer with validation. /// /// Unlike a simple drop this ensures that the all data was written correctly. When other /// validation options (chunk sequencing) had been turned on in the configuration of inner /// [`Writer`], then it will also do a check on their correctness. Differently from /// [`Writer::finish`], this just `flush`es, returns error if some data is abandoned. pub fn finish(mut self) -> Result<()> { if self.to_write > 0 { let err = FormatErrorKind::MissingData(self.to_write).into(); return Err(EncodingError::Format(err)); } // TODO: call `writer.finish` somehow? self.flush()?; if let Wrapper::Chunk(wrt) = self.writer.take() { wrt.writer.validate_sequence_done()?; } Ok(()) } /// Flushes the buffered chunk, checks if it was the last frame, /// writes the next frame header and gets the next frame scanline size /// and image size. /// NOTE: This method must only be called when the writer is the variant Chunk(_) fn new_frame(&mut self) -> Result<()> { let wrt = match &mut self.writer { Wrapper::Chunk(wrt) => wrt, Wrapper::Unrecoverable => { let err = FormatErrorKind::Unrecoverable.into(); return Err(EncodingError::Format(err)); } Wrapper::Zlib(_) => unreachable!("never called on a half-finished frame"), Wrapper::None => unreachable!(), }; wrt.flush()?; wrt.writer.validate_new_image()?; if let Some(fctl) = self.fctl { wrt.set_fctl(fctl); } let (scansize, size) = wrt.next_frame_info(); self.line_len = scansize; self.to_write = size; wrt.write_header()?; wrt.writer.increment_images_written(); // now it can be taken because the next statements cannot cause any errors match self.writer.take() { Wrapper::Chunk(wrt) => { let encoder = ZlibEncoder::new(wrt, self.compression.to_options()); self.writer = Wrapper::Zlib(encoder); } _ => unreachable!(), }; Ok(()) } } impl<'a, W: Write> Write for StreamWriter<'a, W> { fn write(&mut self, mut data: &[u8]) -> io::Result { if let Wrapper::Unrecoverable = self.writer { let err = FormatErrorKind::Unrecoverable.into(); return Err(EncodingError::Format(err).into()); } if data.is_empty() { return Ok(0); } if self.to_write == 0 { match self.writer.take() { Wrapper::Zlib(wrt) => match wrt.finish() { Ok(chunk) => self.writer = Wrapper::Chunk(chunk), Err(err) => { self.writer = Wrapper::Unrecoverable; return Err(err); } }, chunk @ Wrapper::Chunk(_) => self.writer = chunk, Wrapper::Unrecoverable => unreachable!(), Wrapper::None => unreachable!(), }; // Transition Wrapper::Chunk to Wrapper::Zlib. self.new_frame()?; } let written = data.read(&mut self.curr_buf[..self.line_len][self.index..])?; self.index += written; self.to_write -= written; if self.index == self.line_len { // TODO: reuse this buffer between rows. let mut filtered = vec![0; self.curr_buf.len()]; let filter_type = filter( self.filter, self.adaptive_filter, self.bpp, &self.prev_buf, &self.curr_buf, &mut filtered, ); // This can't fail as the other variant is used only to allow the zlib encoder to finish let wrt = match &mut self.writer { Wrapper::Zlib(wrt) => wrt, _ => unreachable!(), }; wrt.write_all(&[filter_type as u8])?; wrt.write_all(&filtered)?; mem::swap(&mut self.prev_buf, &mut self.curr_buf); self.index = 0; } Ok(written) } fn flush(&mut self) -> io::Result<()> { match &mut self.writer { Wrapper::Zlib(wrt) => wrt.flush()?, Wrapper::Chunk(wrt) => wrt.flush()?, // This handles both the case where we entered an unrecoverable state after zlib // decoding failure and after a panic while we had taken the chunk/zlib reader. Wrapper::Unrecoverable | Wrapper::None => { let err = FormatErrorKind::Unrecoverable.into(); return Err(EncodingError::Format(err).into()); } } if self.index > 0 { let err = FormatErrorKind::WrittenTooMuch(self.index).into(); return Err(EncodingError::Format(err).into()); } Ok(()) } } impl Drop for StreamWriter<'_, W> { fn drop(&mut self) { let _ = self.flush(); } } /// Mod to encapsulate the converters depending on the `deflate` crate. /// /// Since this only contains trait impls, there is no need to make this public, they are simply /// available when the mod is compiled as well. impl Compression { fn to_options(self) -> flate2::Compression { #[allow(deprecated)] match self { Compression::Default => flate2::Compression::default(), Compression::Fast => flate2::Compression::fast(), Compression::Best => flate2::Compression::best(), #[allow(deprecated)] Compression::Huffman => flate2::Compression::none(), #[allow(deprecated)] Compression::Rle => flate2::Compression::none(), } } } #[cfg(test)] mod tests { use super::*; use crate::Decoder; use rand::{thread_rng, Rng}; use std::cmp; use std::fs::File; use std::io::Cursor; #[test] fn roundtrip() { // More loops = more random testing, but also more test wait time for _ in 0..10 { for path in glob::glob("tests/pngsuite/*.png") .unwrap() .map(|r| r.unwrap()) { if path.file_name().unwrap().to_str().unwrap().starts_with('x') { // x* files are expected to fail to decode continue; } eprintln!("{}", path.display()); // Decode image let decoder = Decoder::new(File::open(path).unwrap()); let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).unwrap(); // Encode decoded image let mut out = Vec::new(); { let mut wrapper = RandomChunkWriter { rng: thread_rng(), w: &mut out, }; let mut encoder = Encoder::new(&mut wrapper, info.width, info.height); encoder.set_color(info.color_type); encoder.set_depth(info.bit_depth); if let Some(palette) = &reader.info().palette { encoder.set_palette(palette.clone()); } let mut encoder = encoder.write_header().unwrap(); encoder.write_image_data(&buf).unwrap(); } // Decode encoded decoded image let decoder = Decoder::new(&*out); let mut reader = decoder.read_info().unwrap(); let mut buf2 = vec![0; reader.output_buffer_size()]; reader.next_frame(&mut buf2).unwrap(); // check if the encoded image is ok: assert_eq!(buf, buf2); } } } #[test] fn roundtrip_stream() { // More loops = more random testing, but also more test wait time for _ in 0..10 { for path in glob::glob("tests/pngsuite/*.png") .unwrap() .map(|r| r.unwrap()) { if path.file_name().unwrap().to_str().unwrap().starts_with('x') { // x* files are expected to fail to decode continue; } // Decode image let decoder = Decoder::new(File::open(path).unwrap()); let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf).unwrap(); // Encode decoded image let mut out = Vec::new(); { let mut wrapper = RandomChunkWriter { rng: thread_rng(), w: &mut out, }; let mut encoder = Encoder::new(&mut wrapper, info.width, info.height); encoder.set_color(info.color_type); encoder.set_depth(info.bit_depth); if let Some(palette) = &reader.info().palette { encoder.set_palette(palette.clone()); } let mut encoder = encoder.write_header().unwrap(); let mut stream_writer = encoder.stream_writer().unwrap(); let mut outer_wrapper = RandomChunkWriter { rng: thread_rng(), w: &mut stream_writer, }; outer_wrapper.write_all(&buf).unwrap(); } // Decode encoded decoded image let decoder = Decoder::new(&*out); let mut reader = decoder.read_info().unwrap(); let mut buf2 = vec![0; reader.output_buffer_size()]; reader.next_frame(&mut buf2).unwrap(); // check if the encoded image is ok: assert_eq!(buf, buf2); } } } #[test] fn image_palette() -> Result<()> { for &bit_depth in &[1u8, 2, 4, 8] { // Do a reference decoding, choose a fitting palette image from pngsuite let path = format!("tests/pngsuite/basn3p0{}.png", bit_depth); let decoder = Decoder::new(File::open(&path).unwrap()); let mut reader = decoder.read_info().unwrap(); let mut decoded_pixels = vec![0; reader.output_buffer_size()]; let info = reader.info(); assert_eq!( info.width as usize * info.height as usize * usize::from(bit_depth), decoded_pixels.len() * 8 ); let info = reader.next_frame(&mut decoded_pixels).unwrap(); let indexed_data = decoded_pixels; let palette = reader.info().palette.as_ref().unwrap(); let mut out = Vec::new(); { let mut encoder = Encoder::new(&mut out, info.width, info.height); encoder.set_depth(BitDepth::from_u8(bit_depth).unwrap()); encoder.set_color(ColorType::Indexed); encoder.set_palette(palette.as_ref()); let mut writer = encoder.write_header().unwrap(); writer.write_image_data(&indexed_data).unwrap(); } // Decode re-encoded image let decoder = Decoder::new(&*out); let mut reader = decoder.read_info().unwrap(); let mut redecoded = vec![0; reader.output_buffer_size()]; reader.next_frame(&mut redecoded).unwrap(); // check if the encoded image is ok: assert_eq!(indexed_data, redecoded); } Ok(()) } #[test] fn expect_error_on_wrong_image_len() -> Result<()> { let width = 10; let height = 10; let output = vec![0u8; 1024]; let writer = Cursor::new(output); let mut encoder = Encoder::new(writer, width as u32, height as u32); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Rgb); let mut png_writer = encoder.write_header()?; let correct_image_size = width * height * 3; let image = vec![0u8; correct_image_size + 1]; let result = png_writer.write_image_data(image.as_ref()); assert!(result.is_err()); Ok(()) } #[test] fn expect_error_on_empty_image() -> Result<()> { let output = vec![0u8; 1024]; let mut writer = Cursor::new(output); let encoder = Encoder::new(&mut writer, 0, 0); assert!(encoder.write_header().is_err()); let encoder = Encoder::new(&mut writer, 100, 0); assert!(encoder.write_header().is_err()); let encoder = Encoder::new(&mut writer, 0, 100); assert!(encoder.write_header().is_err()); Ok(()) } #[test] fn expect_error_on_invalid_bit_depth_color_type_combination() -> Result<()> { let output = vec![0u8; 1024]; let mut writer = Cursor::new(output); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::One); encoder.set_color(ColorType::Rgb); assert!(encoder.write_header().is_err()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::One); encoder.set_color(ColorType::GrayscaleAlpha); assert!(encoder.write_header().is_err()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::One); encoder.set_color(ColorType::Rgba); assert!(encoder.write_header().is_err()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Two); encoder.set_color(ColorType::Rgb); assert!(encoder.write_header().is_err()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Two); encoder.set_color(ColorType::GrayscaleAlpha); assert!(encoder.write_header().is_err()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Two); encoder.set_color(ColorType::Rgba); assert!(encoder.write_header().is_err()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Four); encoder.set_color(ColorType::Rgb); assert!(encoder.write_header().is_err()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Four); encoder.set_color(ColorType::GrayscaleAlpha); assert!(encoder.write_header().is_err()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Four); encoder.set_color(ColorType::Rgba); assert!(encoder.write_header().is_err()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Sixteen); encoder.set_color(ColorType::Indexed); assert!(encoder.write_header().is_err()); Ok(()) } #[test] fn can_write_header_with_valid_bit_depth_color_type_combination() -> Result<()> { let output = vec![0u8; 1024]; let mut writer = Cursor::new(output); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::One); encoder.set_color(ColorType::Grayscale); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::One); encoder.set_color(ColorType::Indexed); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Two); encoder.set_color(ColorType::Grayscale); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Two); encoder.set_color(ColorType::Indexed); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Four); encoder.set_color(ColorType::Grayscale); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Four); encoder.set_color(ColorType::Indexed); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Rgb); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Indexed); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::GrayscaleAlpha); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Rgba); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Sixteen); encoder.set_color(ColorType::Grayscale); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Sixteen); encoder.set_color(ColorType::Rgb); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Sixteen); encoder.set_color(ColorType::GrayscaleAlpha); assert!(encoder.write_header().is_ok()); let mut encoder = Encoder::new(&mut writer, 1, 1); encoder.set_depth(BitDepth::Sixteen); encoder.set_color(ColorType::Rgba); assert!(encoder.write_header().is_ok()); Ok(()) } #[test] fn all_filters_roundtrip() -> io::Result<()> { let pixel: Vec<_> = (0..48).collect(); let roundtrip = |filter: FilterType| -> io::Result<()> { let mut buffer = vec![]; let mut encoder = Encoder::new(&mut buffer, 4, 4); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Rgb); encoder.set_filter(filter); encoder.write_header()?.write_image_data(&pixel)?; let decoder = crate::Decoder::new(Cursor::new(buffer)); let mut reader = decoder.read_info()?; let info = reader.info(); assert_eq!(info.width, 4); assert_eq!(info.height, 4); let mut dest = vec![0; pixel.len()]; reader.next_frame(&mut dest)?; assert_eq!(dest, pixel, "Deviation with filter type {:?}", filter); Ok(()) }; roundtrip(FilterType::NoFilter)?; roundtrip(FilterType::Sub)?; roundtrip(FilterType::Up)?; roundtrip(FilterType::Avg)?; roundtrip(FilterType::Paeth)?; Ok(()) } #[test] fn some_gamma_roundtrip() -> io::Result<()> { let pixel: Vec<_> = (0..48).collect(); let roundtrip = |gamma: Option| -> io::Result<()> { let mut buffer = vec![]; let mut encoder = Encoder::new(&mut buffer, 4, 4); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Rgb); encoder.set_filter(FilterType::Avg); if let Some(gamma) = gamma { encoder.set_source_gamma(gamma); } encoder.write_header()?.write_image_data(&pixel)?; let decoder = crate::Decoder::new(Cursor::new(buffer)); let mut reader = decoder.read_info()?; assert_eq!( reader.info().source_gamma, gamma, "Deviation with gamma {:?}", gamma ); let mut dest = vec![0; pixel.len()]; let info = reader.next_frame(&mut dest)?; assert_eq!(info.width, 4); assert_eq!(info.height, 4); Ok(()) }; roundtrip(None)?; roundtrip(Some(ScaledFloat::new(0.35)))?; roundtrip(Some(ScaledFloat::new(0.45)))?; roundtrip(Some(ScaledFloat::new(0.55)))?; roundtrip(Some(ScaledFloat::new(0.7)))?; roundtrip(Some(ScaledFloat::new(1.0)))?; roundtrip(Some(ScaledFloat::new(2.5)))?; Ok(()) } #[test] fn write_image_chunks_beyond_first() -> Result<()> { let width = 10; let height = 10; let output = vec![0u8; 1024]; let writer = Cursor::new(output); // Not an animation but we should still be able to write multiple images // See issue: // This is technically all valid png so there is no issue with correctness. let mut encoder = Encoder::new(writer, width, height); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); let mut png_writer = encoder.write_header()?; for _ in 0..3 { let correct_image_size = (width * height) as usize; let image = vec![0u8; correct_image_size]; png_writer.write_image_data(image.as_ref())?; } Ok(()) } #[test] fn image_validate_sequence_without_animation() -> Result<()> { let width = 10; let height = 10; let output = vec![0u8; 1024]; let writer = Cursor::new(output); let mut encoder = Encoder::new(writer, width, height); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); encoder.validate_sequence(true); let mut png_writer = encoder.write_header()?; let correct_image_size = (width * height) as usize; let image = vec![0u8; correct_image_size]; png_writer.write_image_data(image.as_ref())?; assert!(png_writer.write_image_data(image.as_ref()).is_err()); Ok(()) } #[test] fn image_validate_animation() -> Result<()> { let width = 10; let height = 10; let output = vec![0u8; 1024]; let writer = Cursor::new(output); let correct_image_size = (width * height) as usize; let image = vec![0u8; correct_image_size]; let mut encoder = Encoder::new(writer, width, height); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); encoder.set_animated(1, 0)?; encoder.validate_sequence(true); let mut png_writer = encoder.write_header()?; png_writer.write_image_data(image.as_ref())?; Ok(()) } #[test] fn image_validate_animation2() -> Result<()> { let width = 10; let height = 10; let output = vec![0u8; 1024]; let writer = Cursor::new(output); let correct_image_size = (width * height) as usize; let image = vec![0u8; correct_image_size]; let mut encoder = Encoder::new(writer, width, height); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); encoder.set_animated(2, 0)?; encoder.validate_sequence(true); let mut png_writer = encoder.write_header()?; png_writer.write_image_data(image.as_ref())?; png_writer.write_image_data(image.as_ref())?; png_writer.finish()?; Ok(()) } #[test] fn image_validate_animation_sep_def_image() -> Result<()> { let width = 10; let height = 10; let output = vec![0u8; 1024]; let writer = Cursor::new(output); let correct_image_size = (width * height) as usize; let image = vec![0u8; correct_image_size]; let mut encoder = Encoder::new(writer, width, height); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); encoder.set_animated(1, 0)?; encoder.set_sep_def_img(true)?; encoder.validate_sequence(true); let mut png_writer = encoder.write_header()?; png_writer.write_image_data(image.as_ref())?; png_writer.write_image_data(image.as_ref())?; png_writer.finish()?; Ok(()) } #[test] fn image_validate_missing_image() -> Result<()> { let width = 10; let height = 10; let output = vec![0u8; 1024]; let writer = Cursor::new(output); let mut encoder = Encoder::new(writer, width, height); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); encoder.validate_sequence(true); let png_writer = encoder.write_header()?; assert!(png_writer.finish().is_err()); Ok(()) } #[test] fn image_validate_missing_animated_frame() -> Result<()> { let width = 10; let height = 10; let output = vec![0u8; 1024]; let writer = Cursor::new(output); let correct_image_size = (width * height) as usize; let image = vec![0u8; correct_image_size]; let mut encoder = Encoder::new(writer, width, height); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); encoder.set_animated(2, 0)?; encoder.validate_sequence(true); let mut png_writer = encoder.write_header()?; png_writer.write_image_data(image.as_ref())?; assert!(png_writer.finish().is_err()); Ok(()) } #[test] fn issue_307_stream_validation() -> Result<()> { let output = vec![0u8; 1024]; let mut cursor = Cursor::new(output); let encoder = Encoder::new(&mut cursor, 1, 1); // Create a 1-pixel image let mut writer = encoder.write_header()?; let mut stream = writer.stream_writer()?; let written = stream.write(&[1, 2, 3, 4])?; assert_eq!(written, 1); stream.finish()?; drop(writer); { cursor.set_position(0); let mut decoder = Decoder::new(cursor).read_info().expect("A valid image"); let mut buffer = [0u8; 1]; decoder.next_frame(&mut buffer[..]).expect("Valid read"); assert_eq!(buffer, [1]); } Ok(()) } #[test] fn stream_filtering() -> Result<()> { let output = vec![0u8; 1024]; let mut cursor = Cursor::new(output); let mut encoder = Encoder::new(&mut cursor, 8, 8); encoder.set_color(ColorType::Rgba); encoder.set_filter(FilterType::Paeth); let mut writer = encoder.write_header()?; let mut stream = writer.stream_writer()?; for _ in 0..8 { let written = stream.write(&[1; 32])?; assert_eq!(written, 32); } stream.finish()?; drop(writer); { cursor.set_position(0); let mut decoder = Decoder::new(cursor).read_info().expect("A valid image"); let mut buffer = [0u8; 256]; decoder.next_frame(&mut buffer[..]).expect("Valid read"); assert_eq!(buffer, [1; 256]); } Ok(()) } #[test] #[cfg(all(unix, not(target_pointer_width = "32")))] fn exper_error_on_huge_chunk() -> Result<()> { // Okay, so we want a proper 4 GB chunk but not actually spend the memory for reserving it. // Let's rely on overcommit? Otherwise we got the rather dumb option of mmap-ing /dev/zero. let empty = vec![0; 1usize << 31]; let writer = Cursor::new(vec![0u8; 1024]); let mut encoder = Encoder::new(writer, 10, 10); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); let mut png_writer = encoder.write_header()?; assert!(png_writer.write_chunk(chunk::fdAT, &empty).is_err()); Ok(()) } #[test] #[cfg(all(unix, not(target_pointer_width = "32")))] fn exper_error_on_non_u32_chunk() -> Result<()> { // Okay, so we want a proper 4 GB chunk but not actually spend the memory for reserving it. // Let's rely on overcommit? Otherwise we got the rather dumb option of mmap-ing /dev/zero. let empty = vec![0; 1usize << 32]; let writer = Cursor::new(vec![0u8; 1024]); let mut encoder = Encoder::new(writer, 10, 10); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); let mut png_writer = encoder.write_header()?; assert!(png_writer.write_chunk(chunk::fdAT, &empty).is_err()); Ok(()) } #[test] fn finish_drops_inner_writer() -> Result<()> { struct NoWriter<'flag>(&'flag mut bool); impl Write for NoWriter<'_> { fn write(&mut self, buf: &[u8]) -> io::Result { Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } impl Drop for NoWriter<'_> { fn drop(&mut self) { *self.0 = true; } } let mut flag = false; { let mut encoder = Encoder::new(NoWriter(&mut flag), 10, 10); encoder.set_depth(BitDepth::Eight); encoder.set_color(ColorType::Grayscale); let mut writer = encoder.write_header()?; writer.write_image_data(&[0; 100])?; writer.finish()?; } assert!(flag, "PNG finished but writer was not dropped"); Ok(()) } /// A Writer that only writes a few bytes at a time struct RandomChunkWriter { rng: R, w: W, } impl Write for RandomChunkWriter { fn write(&mut self, buf: &[u8]) -> io::Result { // choose a random length to write let len = cmp::min(self.rng.gen_range(1..50), buf.len()); self.w.write(&buf[0..len]) } fn flush(&mut self) -> io::Result<()> { self.w.flush() } } } png-0.17.16/src/filter.rs000064400000000000000000001361211046102023000132120ustar 00000000000000use core::convert::TryInto; use crate::common::BytesPerPixel; /// SIMD helpers for `fn unfilter` /// /// TODO(https://github.com/rust-lang/rust/issues/86656): Stop gating this module behind the /// "unstable" feature of the `png` crate. This should be possible once the "portable_simd" /// feature of Rust gets stabilized. /// /// This is only known to help on x86, with no change measured on most benchmarks on ARM, /// and even severely regressing some of them. /// So despite the code being portable, we only enable this for x86. /// We can add more platforms once this code is proven to be beneficial for them. #[cfg(all(feature = "unstable", target_arch = "x86_64"))] mod simd { use std::simd::num::{SimdInt, SimdUint}; use std::simd::{u8x4, u8x8, LaneCount, Simd, SimdElement, SupportedLaneCount}; /// Scalar Paeth function wrapped in SIMD scaffolding. /// /// This is needed because simply running the function on the inputs /// makes the compiler think our inputs are too short /// to benefit from vectorization. /// Putting it in SIMD scaffolding fixes that. /// https://github.com/image-rs/image-png/issues/511 /// /// Funnily, the autovectorizer does a better job here /// than a handwritten algorithm using std::simd! /// We used to have a handwritten one but this is just faster. fn paeth_predictor( a: Simd, b: Simd, c: Simd, ) -> Simd where LaneCount: SupportedLaneCount, { let mut out = [0; N]; for i in 0..N { out[i] = super::filter_paeth_stbi_i16(a[i].into(), b[i].into(), c[i].into()); } out.into() } /// Functionally equivalent to `simd::paeth_predictor` but does not temporarily convert /// the SIMD elements to `i16`. fn paeth_predictor_u8( a: Simd, b: Simd, c: Simd, ) -> Simd where LaneCount: SupportedLaneCount, { let mut out = [0; N]; for i in 0..N { out[i] = super::filter_paeth_stbi(a[i].into(), b[i].into(), c[i].into()); } out.into() } /// Memory of previous pixels (as needed to unfilter `FilterType::Paeth`). /// See also https://www.w3.org/TR/png/#filter-byte-positions #[derive(Default)] struct PaethState where T: SimdElement, LaneCount: SupportedLaneCount, { /// Previous pixel in the previous row. c: Simd, /// Previous pixel in the current row. a: Simd, } /// Mutates `x` as needed to unfilter `FilterType::Paeth`. /// /// `b` is the current pixel in the previous row. `x` is the current pixel in the current row. /// See also https://www.w3.org/TR/png/#filter-byte-positions fn paeth_step( state: &mut PaethState, b: Simd, x: &mut Simd, ) where LaneCount: SupportedLaneCount, { // Storing the inputs. let b = b.cast::(); // Calculating the new value of the current pixel. let predictor = paeth_predictor(state.a, b, state.c); *x += predictor.cast::(); // Preparing for the next step. state.c = b; state.a = x.cast::(); } /// Computes the Paeth predictor without converting `u8` to `i16`. /// /// See `simd::paeth_step`. fn paeth_step_u8( state: &mut PaethState, b: Simd, x: &mut Simd, ) where LaneCount: SupportedLaneCount, { // Calculating the new value of the current pixel. *x += paeth_predictor_u8(state.a, b, state.c); // Preparing for the next step. state.c = b; state.a = *x; } fn load3(src: &[u8]) -> u8x4 { u8x4::from_array([src[0], src[1], src[2], 0]) } fn store3(src: u8x4, dest: &mut [u8]) { dest[0..3].copy_from_slice(&src.to_array()[0..3]) } /// Undoes `FilterType::Paeth` for `BytesPerPixel::Three`. pub fn unfilter_paeth3(mut prev_row: &[u8], mut curr_row: &mut [u8]) { debug_assert_eq!(prev_row.len(), curr_row.len()); debug_assert_eq!(prev_row.len() % 3, 0); let mut state = PaethState::::default(); while prev_row.len() >= 4 { // `u8x4` requires working with `[u8;4]`, but we can just load and ignore the first // byte from the next triple. This optimization technique mimics the algorithm found // in // https://github.com/glennrp/libpng/blob/f8e5fa92b0e37ab597616f554bee254157998227/intel/filter_sse2_intrinsics.c#L130-L131 let b = u8x4::from_slice(prev_row); let mut x = u8x4::from_slice(curr_row); paeth_step(&mut state, b, &mut x); // We can speculate that writing 4 bytes might be more efficient (just as with using // `u8x4::from_slice` above), but we can't use that here, because we can't clobber the // first byte of the next pixel in the `curr_row`. store3(x, curr_row); prev_row = &prev_row[3..]; curr_row = &mut curr_row[3..]; } // Can't use `u8x4::from_slice` for the last `[u8;3]`. let b = load3(prev_row); let mut x = load3(curr_row); paeth_step(&mut state, b, &mut x); store3(x, curr_row); } /// Undoes `FilterType::Paeth` for `BytesPerPixel::Four` and `BytesPerPixel::Eight`. /// /// This function calculates the Paeth predictor entirely in `Simd` /// without converting to an intermediate `Simd`. Doing so avoids /// paying a small performance penalty converting between types. pub fn unfilter_paeth_u8(prev_row: &[u8], curr_row: &mut [u8]) where LaneCount: SupportedLaneCount, { debug_assert_eq!(prev_row.len(), curr_row.len()); debug_assert_eq!(prev_row.len() % N, 0); assert!(matches!(N, 4 | 8)); let mut state = PaethState::::default(); for (prev_row, curr_row) in prev_row.chunks_exact(N).zip(curr_row.chunks_exact_mut(N)) { let b = Simd::from_slice(prev_row); let mut x = Simd::from_slice(curr_row); paeth_step_u8(&mut state, b, &mut x); curr_row[..N].copy_from_slice(&x.to_array()[..N]); } } fn load6(src: &[u8]) -> u8x8 { u8x8::from_array([src[0], src[1], src[2], src[3], src[4], src[5], 0, 0]) } fn store6(src: u8x8, dest: &mut [u8]) { dest[0..6].copy_from_slice(&src.to_array()[0..6]) } /// Undoes `FilterType::Paeth` for `BytesPerPixel::Six`. pub fn unfilter_paeth6(mut prev_row: &[u8], mut curr_row: &mut [u8]) { debug_assert_eq!(prev_row.len(), curr_row.len()); debug_assert_eq!(prev_row.len() % 6, 0); let mut state = PaethState::::default(); while prev_row.len() >= 8 { // `u8x8` requires working with `[u8;8]`, but we can just load and ignore the first two // bytes from the next pixel. This optimization technique mimics the algorithm found // in // https://github.com/glennrp/libpng/blob/f8e5fa92b0e37ab597616f554bee254157998227/intel/filter_sse2_intrinsics.c#L130-L131 let b = u8x8::from_slice(prev_row); let mut x = u8x8::from_slice(curr_row); paeth_step(&mut state, b, &mut x); // We can speculate that writing 8 bytes might be more efficient (just as with using // `u8x8::from_slice` above), but we can't use that here, because we can't clobber the // first bytes of the next pixel in the `curr_row`. store6(x, curr_row); prev_row = &prev_row[6..]; curr_row = &mut curr_row[6..]; } // Can't use `u8x8::from_slice` for the last `[u8;6]`. let b = load6(prev_row); let mut x = load6(curr_row); paeth_step(&mut state, b, &mut x); store6(x, curr_row); } } /// The byte level filter applied to scanlines to prepare them for compression. /// /// Compression in general benefits from repetitive data. The filter is a content-aware method of /// compressing the range of occurring byte values to help the compression algorithm. Note that /// this does not operate on pixels but on raw bytes of a scanline. /// /// Details on how each filter works can be found in the [PNG Book](http://www.libpng.org/pub/png/book/chapter09.html). #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum FilterType { NoFilter = 0, Sub = 1, Up = 2, Avg = 3, Paeth = 4, } impl Default for FilterType { fn default() -> Self { FilterType::Sub } } impl FilterType { /// u8 -> Self. Temporary solution until Rust provides a canonical one. pub fn from_u8(n: u8) -> Option { match n { 0 => Some(FilterType::NoFilter), 1 => Some(FilterType::Sub), 2 => Some(FilterType::Up), 3 => Some(FilterType::Avg), 4 => Some(FilterType::Paeth), _ => None, } } } /// Adaptive filtering tries every possible filter for each row and uses a heuristic to select the best one. /// This improves compression ratio, but makes encoding slightly slower. /// /// It is recommended to use `Adaptive` whenever you care about compression ratio. /// Filtering is quite cheap compared to other parts of encoding, but can contribute /// to the compression ratio significantly. /// /// `NonAdaptive` filtering is the default. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum AdaptiveFilterType { Adaptive, NonAdaptive, } impl Default for AdaptiveFilterType { fn default() -> Self { AdaptiveFilterType::NonAdaptive } } fn filter_paeth(a: u8, b: u8, c: u8) -> u8 { // On ARM this algorithm performs much better than the one above adapted from stb, // and this is the better-studied algorithm we've always used here, // so we default to it on all non-x86 platforms. let pa = (i16::from(b) - i16::from(c)).abs(); let pb = (i16::from(a) - i16::from(c)).abs(); let pc = ((i16::from(a) - i16::from(c)) + (i16::from(b) - i16::from(c))).abs(); let mut out = a; let mut min = pa; if pb < min { min = pb; out = b; } if pc < min { out = c; } out } fn filter_paeth_stbi(a: u8, b: u8, c: u8) -> u8 { // Decoding optimizes better with this algorithm than with `filter_paeth` // // This formulation looks very different from the reference in the PNG spec, but is // actually equivalent and has favorable data dependencies and admits straightforward // generation of branch-free code, which helps performance significantly. // // Adapted from public domain PNG implementation: // https://github.com/nothings/stb/blob/5c205738c191bcb0abc65c4febfa9bd25ff35234/stb_image.h#L4657-L4668 let thresh = i16::from(c) * 3 - (i16::from(a) + i16::from(b)); let lo = a.min(b); let hi = a.max(b); let t0 = if hi as i16 <= thresh { lo } else { c }; let t1 = if thresh <= lo as i16 { hi } else { t0 }; return t1; } #[cfg(any(test, all(feature = "unstable", target_arch = "x86_64")))] fn filter_paeth_stbi_i16(a: i16, b: i16, c: i16) -> i16 { // Like `filter_paeth_stbi` but vectorizes better when wrapped in SIMD types. // Used for bpp=3 and bpp=6 let thresh = c * 3 - (a + b); let lo = a.min(b); let hi = a.max(b); let t0 = if hi <= thresh { lo } else { c }; let t1 = if thresh <= lo { hi } else { t0 }; return t1; } fn filter_paeth_fpnge(a: u8, b: u8, c: u8) -> u8 { // This is an optimized version of the paeth filter from the PNG specification, proposed by // Luca Versari for [FPNGE](https://www.lucaversari.it/FJXL_and_FPNGE.pdf). It operates // entirely on unsigned 8-bit quantities, making it more conducive to vectorization. // // p = a + b - c // pa = |p - a| = |a + b - c - a| = |b - c| = max(b, c) - min(b, c) // pb = |p - b| = |a + b - c - b| = |a - c| = max(a, c) - min(a, c) // pc = |p - c| = |a + b - c - c| = |(b - c) + (a - c)| = ... // // Further optimizing the calculation of `pc` a bit tricker. However, notice that: // // a > c && b > c // ==> (a - c) > 0 && (b - c) > 0 // ==> pc > (a - c) && pc > (b - c) // ==> pc > |a - c| && pc > |b - c| // ==> pc > pb && pc > pa // // Meaning that if `c` is smaller than `a` and `b`, the value of `pc` is irrelevant. Similar // reasoning applies if `c` is larger than the other two inputs. Assuming that `c >= b` and // `c <= b` or vice versa: // // pc = ||b - c| - |a - c|| = |pa - pb| = max(pa, pb) - min(pa, pb) // let pa = b.max(c) - c.min(b); let pb = a.max(c) - c.min(a); let pc = if (a < c) == (c < b) { pa.max(pb) - pa.min(pb) } else { 255 }; if pa <= pb && pa <= pc { a } else if pb <= pc { b } else { c } } pub(crate) fn unfilter( mut filter: FilterType, tbpp: BytesPerPixel, previous: &[u8], current: &mut [u8], ) { use self::FilterType::*; // If the previous row is empty, then treat it as if it were filled with zeros. if previous.is_empty() { if filter == Paeth { filter = Sub; } else if filter == Up { filter = NoFilter; } } // [2023/01 @okaneco] - Notes on optimizing decoding filters // // Links: // [PR]: https://github.com/image-rs/image-png/pull/382 // [SWAR]: http://aggregate.org/SWAR/over.html // [AVG]: http://aggregate.org/MAGIC/#Average%20of%20Integers // // #382 heavily refactored and optimized the following filters making the // implementation nonobvious. These comments function as a summary of that // PR with an explanation of the choices made below. // // #382 originally started with trying to optimize using a technique called // SWAR, SIMD Within a Register. SWAR uses regular integer types like `u32` // and `u64` as SIMD registers to perform vertical operations in parallel, // usually involving bit-twiddling. This allowed each `BytesPerPixel` (bpp) // pixel to be decoded in parallel: 3bpp and 4bpp in a `u32`, 6bpp and 8pp // in a `u64`. The `Sub` filter looked like the following code block, `Avg` // was similar but used a bitwise average method from [AVG]: // ``` // // See "Unpartitioned Operations With Correction Code" from [SWAR] // fn swar_add_u32(x: u32, y: u32) -> u32 { // // 7-bit addition so there's no carry over the most significant bit // let n = (x & 0x7f7f7f7f) + (y & 0x7f7f7f7f); // 0x7F = 0b_0111_1111 // // 1-bit parity/XOR addition to fill in the missing MSB // n ^ (x ^ y) & 0x80808080 // 0x80 = 0b_1000_0000 // } // // let mut prev = // u32::from_ne_bytes([current[0], current[1], current[2], current[3]]); // for chunk in current[4..].chunks_exact_mut(4) { // let cur = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); // let new_chunk = swar_add_u32(cur, prev); // chunk.copy_from_slice(&new_chunk.to_ne_bytes()); // prev = new_chunk; // } // ``` // While this provided a measurable increase, @fintelia found that this idea // could be taken even further by unrolling the chunks component-wise and // avoiding unnecessary byte-shuffling by using byte arrays instead of // `u32::from|to_ne_bytes`. The bitwise operations were no longer necessary // so they were reverted to their obvious arithmetic equivalent. Lastly, // `TryInto` was used instead of `copy_from_slice`. The `Sub` code now // looked like this (with asserts to remove `0..bpp` bounds checks): // ``` // assert!(len > 3); // let mut prev = [current[0], current[1], current[2], current[3]]; // for chunk in current[4..].chunks_exact_mut(4) { // let new_chunk = [ // chunk[0].wrapping_add(prev[0]), // chunk[1].wrapping_add(prev[1]), // chunk[2].wrapping_add(prev[2]), // chunk[3].wrapping_add(prev[3]), // ]; // *TryInto::<&mut [u8; 4]>::try_into(chunk).unwrap() = new_chunk; // prev = new_chunk; // } // ``` // The compiler was able to optimize the code to be even faster and this // method even sped up Paeth filtering! Assertions were experimentally // added within loop bodies which produced better instructions but no // difference in speed. Finally, the code was refactored to remove manual // slicing and start the previous pixel chunks with arrays of `[0; N]`. // ``` // let mut prev = [0; 4]; // for chunk in current.chunks_exact_mut(4) { // let new_chunk = [ // chunk[0].wrapping_add(prev[0]), // chunk[1].wrapping_add(prev[1]), // chunk[2].wrapping_add(prev[2]), // chunk[3].wrapping_add(prev[3]), // ]; // *TryInto::<&mut [u8; 4]>::try_into(chunk).unwrap() = new_chunk; // prev = new_chunk; // } // ``` // While we're not manually bit-twiddling anymore, a possible takeaway from // this is to "think in SWAR" when dealing with small byte arrays. Unrolling // array operations and performing them component-wise may unlock previously // unavailable optimizations from the compiler, even when using the // `chunks_exact` methods for their potential auto-vectorization benefits. match filter { NoFilter => {} Sub => match tbpp { BytesPerPixel::One => { current.iter_mut().reduce(|&mut prev, curr| { *curr = curr.wrapping_add(prev); curr }); } BytesPerPixel::Two => { let mut prev = [0; 2]; for chunk in current.chunks_exact_mut(2) { let new_chunk = [ chunk[0].wrapping_add(prev[0]), chunk[1].wrapping_add(prev[1]), ]; *TryInto::<&mut [u8; 2]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } BytesPerPixel::Three => { let mut prev = [0; 3]; for chunk in current.chunks_exact_mut(3) { let new_chunk = [ chunk[0].wrapping_add(prev[0]), chunk[1].wrapping_add(prev[1]), chunk[2].wrapping_add(prev[2]), ]; *TryInto::<&mut [u8; 3]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } BytesPerPixel::Four => { let mut prev = [0; 4]; for chunk in current.chunks_exact_mut(4) { let new_chunk = [ chunk[0].wrapping_add(prev[0]), chunk[1].wrapping_add(prev[1]), chunk[2].wrapping_add(prev[2]), chunk[3].wrapping_add(prev[3]), ]; *TryInto::<&mut [u8; 4]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } BytesPerPixel::Six => { let mut prev = [0; 6]; for chunk in current.chunks_exact_mut(6) { let new_chunk = [ chunk[0].wrapping_add(prev[0]), chunk[1].wrapping_add(prev[1]), chunk[2].wrapping_add(prev[2]), chunk[3].wrapping_add(prev[3]), chunk[4].wrapping_add(prev[4]), chunk[5].wrapping_add(prev[5]), ]; *TryInto::<&mut [u8; 6]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } BytesPerPixel::Eight => { let mut prev = [0; 8]; for chunk in current.chunks_exact_mut(8) { let new_chunk = [ chunk[0].wrapping_add(prev[0]), chunk[1].wrapping_add(prev[1]), chunk[2].wrapping_add(prev[2]), chunk[3].wrapping_add(prev[3]), chunk[4].wrapping_add(prev[4]), chunk[5].wrapping_add(prev[5]), chunk[6].wrapping_add(prev[6]), chunk[7].wrapping_add(prev[7]), ]; *TryInto::<&mut [u8; 8]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } }, Up => { for (curr, &above) in current.iter_mut().zip(previous) { *curr = curr.wrapping_add(above); } } Avg if previous.is_empty() => match tbpp { BytesPerPixel::One => { current.iter_mut().reduce(|&mut prev, curr| { *curr = curr.wrapping_add(prev / 2); curr }); } BytesPerPixel::Two => { let mut prev = [0; 2]; for chunk in current.chunks_exact_mut(2) { let new_chunk = [ chunk[0].wrapping_add(prev[0] / 2), chunk[1].wrapping_add(prev[1] / 2), ]; *TryInto::<&mut [u8; 2]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } BytesPerPixel::Three => { let mut prev = [0; 3]; for chunk in current.chunks_exact_mut(3) { let new_chunk = [ chunk[0].wrapping_add(prev[0] / 2), chunk[1].wrapping_add(prev[1] / 2), chunk[2].wrapping_add(prev[2] / 2), ]; *TryInto::<&mut [u8; 3]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } BytesPerPixel::Four => { let mut prev = [0; 4]; for chunk in current.chunks_exact_mut(4) { let new_chunk = [ chunk[0].wrapping_add(prev[0] / 2), chunk[1].wrapping_add(prev[1] / 2), chunk[2].wrapping_add(prev[2] / 2), chunk[3].wrapping_add(prev[3] / 2), ]; *TryInto::<&mut [u8; 4]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } BytesPerPixel::Six => { let mut prev = [0; 6]; for chunk in current.chunks_exact_mut(6) { let new_chunk = [ chunk[0].wrapping_add(prev[0] / 2), chunk[1].wrapping_add(prev[1] / 2), chunk[2].wrapping_add(prev[2] / 2), chunk[3].wrapping_add(prev[3] / 2), chunk[4].wrapping_add(prev[4] / 2), chunk[5].wrapping_add(prev[5] / 2), ]; *TryInto::<&mut [u8; 6]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } BytesPerPixel::Eight => { let mut prev = [0; 8]; for chunk in current.chunks_exact_mut(8) { let new_chunk = [ chunk[0].wrapping_add(prev[0] / 2), chunk[1].wrapping_add(prev[1] / 2), chunk[2].wrapping_add(prev[2] / 2), chunk[3].wrapping_add(prev[3] / 2), chunk[4].wrapping_add(prev[4] / 2), chunk[5].wrapping_add(prev[5] / 2), chunk[6].wrapping_add(prev[6] / 2), chunk[7].wrapping_add(prev[7] / 2), ]; *TryInto::<&mut [u8; 8]>::try_into(chunk).unwrap() = new_chunk; prev = new_chunk; } } }, Avg => match tbpp { BytesPerPixel::One => { let mut lprev = [0; 1]; for (chunk, above) in current.chunks_exact_mut(1).zip(previous.chunks_exact(1)) { let new_chunk = [chunk[0].wrapping_add(((above[0] as u16 + lprev[0] as u16) / 2) as u8)]; *TryInto::<&mut [u8; 1]>::try_into(chunk).unwrap() = new_chunk; lprev = new_chunk; } } BytesPerPixel::Two => { let mut lprev = [0; 2]; for (chunk, above) in current.chunks_exact_mut(2).zip(previous.chunks_exact(2)) { let new_chunk = [ chunk[0].wrapping_add(((above[0] as u16 + lprev[0] as u16) / 2) as u8), chunk[1].wrapping_add(((above[1] as u16 + lprev[1] as u16) / 2) as u8), ]; *TryInto::<&mut [u8; 2]>::try_into(chunk).unwrap() = new_chunk; lprev = new_chunk; } } BytesPerPixel::Three => { let mut lprev = [0; 3]; for (chunk, above) in current.chunks_exact_mut(3).zip(previous.chunks_exact(3)) { let new_chunk = [ chunk[0].wrapping_add(((above[0] as u16 + lprev[0] as u16) / 2) as u8), chunk[1].wrapping_add(((above[1] as u16 + lprev[1] as u16) / 2) as u8), chunk[2].wrapping_add(((above[2] as u16 + lprev[2] as u16) / 2) as u8), ]; *TryInto::<&mut [u8; 3]>::try_into(chunk).unwrap() = new_chunk; lprev = new_chunk; } } BytesPerPixel::Four => { let mut lprev = [0; 4]; for (chunk, above) in current.chunks_exact_mut(4).zip(previous.chunks_exact(4)) { let new_chunk = [ chunk[0].wrapping_add(((above[0] as u16 + lprev[0] as u16) / 2) as u8), chunk[1].wrapping_add(((above[1] as u16 + lprev[1] as u16) / 2) as u8), chunk[2].wrapping_add(((above[2] as u16 + lprev[2] as u16) / 2) as u8), chunk[3].wrapping_add(((above[3] as u16 + lprev[3] as u16) / 2) as u8), ]; *TryInto::<&mut [u8; 4]>::try_into(chunk).unwrap() = new_chunk; lprev = new_chunk; } } BytesPerPixel::Six => { let mut lprev = [0; 6]; for (chunk, above) in current.chunks_exact_mut(6).zip(previous.chunks_exact(6)) { let new_chunk = [ chunk[0].wrapping_add(((above[0] as u16 + lprev[0] as u16) / 2) as u8), chunk[1].wrapping_add(((above[1] as u16 + lprev[1] as u16) / 2) as u8), chunk[2].wrapping_add(((above[2] as u16 + lprev[2] as u16) / 2) as u8), chunk[3].wrapping_add(((above[3] as u16 + lprev[3] as u16) / 2) as u8), chunk[4].wrapping_add(((above[4] as u16 + lprev[4] as u16) / 2) as u8), chunk[5].wrapping_add(((above[5] as u16 + lprev[5] as u16) / 2) as u8), ]; *TryInto::<&mut [u8; 6]>::try_into(chunk).unwrap() = new_chunk; lprev = new_chunk; } } BytesPerPixel::Eight => { let mut lprev = [0; 8]; for (chunk, above) in current.chunks_exact_mut(8).zip(previous.chunks_exact(8)) { let new_chunk = [ chunk[0].wrapping_add(((above[0] as u16 + lprev[0] as u16) / 2) as u8), chunk[1].wrapping_add(((above[1] as u16 + lprev[1] as u16) / 2) as u8), chunk[2].wrapping_add(((above[2] as u16 + lprev[2] as u16) / 2) as u8), chunk[3].wrapping_add(((above[3] as u16 + lprev[3] as u16) / 2) as u8), chunk[4].wrapping_add(((above[4] as u16 + lprev[4] as u16) / 2) as u8), chunk[5].wrapping_add(((above[5] as u16 + lprev[5] as u16) / 2) as u8), chunk[6].wrapping_add(((above[6] as u16 + lprev[6] as u16) / 2) as u8), chunk[7].wrapping_add(((above[7] as u16 + lprev[7] as u16) / 2) as u8), ]; *TryInto::<&mut [u8; 8]>::try_into(chunk).unwrap() = new_chunk; lprev = new_chunk; } } }, #[allow(unreachable_code)] Paeth => { // Select the fastest Paeth filter implementation based on the target architecture. let filter_paeth_decode = if cfg!(target_arch = "x86_64") { filter_paeth_stbi } else { filter_paeth }; // Paeth filter pixels: // C B D // A X match tbpp { BytesPerPixel::One => { let mut a_bpp = [0; 1]; let mut c_bpp = [0; 1]; for (chunk, b_bpp) in current.chunks_exact_mut(1).zip(previous.chunks_exact(1)) { let new_chunk = [chunk[0] .wrapping_add(filter_paeth_decode(a_bpp[0], b_bpp[0], c_bpp[0]))]; *TryInto::<&mut [u8; 1]>::try_into(chunk).unwrap() = new_chunk; a_bpp = new_chunk; c_bpp = b_bpp.try_into().unwrap(); } } BytesPerPixel::Two => { let mut a_bpp = [0; 2]; let mut c_bpp = [0; 2]; for (chunk, b_bpp) in current.chunks_exact_mut(2).zip(previous.chunks_exact(2)) { let new_chunk = [ chunk[0] .wrapping_add(filter_paeth_decode(a_bpp[0], b_bpp[0], c_bpp[0])), chunk[1] .wrapping_add(filter_paeth_decode(a_bpp[1], b_bpp[1], c_bpp[1])), ]; *TryInto::<&mut [u8; 2]>::try_into(chunk).unwrap() = new_chunk; a_bpp = new_chunk; c_bpp = b_bpp.try_into().unwrap(); } } BytesPerPixel::Three => { // Do not enable this algorithm on ARM, that would be a big performance hit #[cfg(all(feature = "unstable", target_arch = "x86_64"))] { simd::unfilter_paeth3(previous, current); return; } let mut a_bpp = [0; 3]; let mut c_bpp = [0; 3]; for (chunk, b_bpp) in current.chunks_exact_mut(3).zip(previous.chunks_exact(3)) { let new_chunk = [ chunk[0] .wrapping_add(filter_paeth_decode(a_bpp[0], b_bpp[0], c_bpp[0])), chunk[1] .wrapping_add(filter_paeth_decode(a_bpp[1], b_bpp[1], c_bpp[1])), chunk[2] .wrapping_add(filter_paeth_decode(a_bpp[2], b_bpp[2], c_bpp[2])), ]; *TryInto::<&mut [u8; 3]>::try_into(chunk).unwrap() = new_chunk; a_bpp = new_chunk; c_bpp = b_bpp.try_into().unwrap(); } } BytesPerPixel::Four => { #[cfg(all(feature = "unstable", target_arch = "x86_64"))] { simd::unfilter_paeth_u8::<4>(previous, current); return; } let mut a_bpp = [0; 4]; let mut c_bpp = [0; 4]; for (chunk, b_bpp) in current.chunks_exact_mut(4).zip(previous.chunks_exact(4)) { let new_chunk = [ chunk[0] .wrapping_add(filter_paeth_decode(a_bpp[0], b_bpp[0], c_bpp[0])), chunk[1] .wrapping_add(filter_paeth_decode(a_bpp[1], b_bpp[1], c_bpp[1])), chunk[2] .wrapping_add(filter_paeth_decode(a_bpp[2], b_bpp[2], c_bpp[2])), chunk[3] .wrapping_add(filter_paeth_decode(a_bpp[3], b_bpp[3], c_bpp[3])), ]; *TryInto::<&mut [u8; 4]>::try_into(chunk).unwrap() = new_chunk; a_bpp = new_chunk; c_bpp = b_bpp.try_into().unwrap(); } } BytesPerPixel::Six => { #[cfg(all(feature = "unstable", target_arch = "x86_64"))] { simd::unfilter_paeth6(previous, current); return; } let mut a_bpp = [0; 6]; let mut c_bpp = [0; 6]; for (chunk, b_bpp) in current.chunks_exact_mut(6).zip(previous.chunks_exact(6)) { let new_chunk = [ chunk[0] .wrapping_add(filter_paeth_decode(a_bpp[0], b_bpp[0], c_bpp[0])), chunk[1] .wrapping_add(filter_paeth_decode(a_bpp[1], b_bpp[1], c_bpp[1])), chunk[2] .wrapping_add(filter_paeth_decode(a_bpp[2], b_bpp[2], c_bpp[2])), chunk[3] .wrapping_add(filter_paeth_decode(a_bpp[3], b_bpp[3], c_bpp[3])), chunk[4] .wrapping_add(filter_paeth_decode(a_bpp[4], b_bpp[4], c_bpp[4])), chunk[5] .wrapping_add(filter_paeth_decode(a_bpp[5], b_bpp[5], c_bpp[5])), ]; *TryInto::<&mut [u8; 6]>::try_into(chunk).unwrap() = new_chunk; a_bpp = new_chunk; c_bpp = b_bpp.try_into().unwrap(); } } BytesPerPixel::Eight => { #[cfg(all(feature = "unstable", target_arch = "x86_64"))] { simd::unfilter_paeth_u8::<8>(previous, current); return; } let mut a_bpp = [0; 8]; let mut c_bpp = [0; 8]; for (chunk, b_bpp) in current.chunks_exact_mut(8).zip(previous.chunks_exact(8)) { let new_chunk = [ chunk[0] .wrapping_add(filter_paeth_decode(a_bpp[0], b_bpp[0], c_bpp[0])), chunk[1] .wrapping_add(filter_paeth_decode(a_bpp[1], b_bpp[1], c_bpp[1])), chunk[2] .wrapping_add(filter_paeth_decode(a_bpp[2], b_bpp[2], c_bpp[2])), chunk[3] .wrapping_add(filter_paeth_decode(a_bpp[3], b_bpp[3], c_bpp[3])), chunk[4] .wrapping_add(filter_paeth_decode(a_bpp[4], b_bpp[4], c_bpp[4])), chunk[5] .wrapping_add(filter_paeth_decode(a_bpp[5], b_bpp[5], c_bpp[5])), chunk[6] .wrapping_add(filter_paeth_decode(a_bpp[6], b_bpp[6], c_bpp[6])), chunk[7] .wrapping_add(filter_paeth_decode(a_bpp[7], b_bpp[7], c_bpp[7])), ]; *TryInto::<&mut [u8; 8]>::try_into(chunk).unwrap() = new_chunk; a_bpp = new_chunk; c_bpp = b_bpp.try_into().unwrap(); } } } } } } fn filter_internal( method: FilterType, bpp: usize, len: usize, previous: &[u8], current: &[u8], output: &mut [u8], ) -> FilterType { use self::FilterType::*; // This value was chosen experimentally based on what achieved the best performance. The // Rust compiler does auto-vectorization, and 32-bytes per loop iteration seems to enable // the fastest code when doing so. const CHUNK_SIZE: usize = 32; match method { NoFilter => { output.copy_from_slice(current); NoFilter } Sub => { let mut out_chunks = output[bpp..].chunks_exact_mut(CHUNK_SIZE); let mut cur_chunks = current[bpp..].chunks_exact(CHUNK_SIZE); let mut prev_chunks = current[..len - bpp].chunks_exact(CHUNK_SIZE); for ((out, cur), prev) in (&mut out_chunks).zip(&mut cur_chunks).zip(&mut prev_chunks) { for i in 0..CHUNK_SIZE { out[i] = cur[i].wrapping_sub(prev[i]); } } for ((out, cur), &prev) in out_chunks .into_remainder() .iter_mut() .zip(cur_chunks.remainder()) .zip(prev_chunks.remainder()) { *out = cur.wrapping_sub(prev); } output[..bpp].copy_from_slice(¤t[..bpp]); Sub } Up => { let mut out_chunks = output.chunks_exact_mut(CHUNK_SIZE); let mut cur_chunks = current.chunks_exact(CHUNK_SIZE); let mut prev_chunks = previous.chunks_exact(CHUNK_SIZE); for ((out, cur), prev) in (&mut out_chunks).zip(&mut cur_chunks).zip(&mut prev_chunks) { for i in 0..CHUNK_SIZE { out[i] = cur[i].wrapping_sub(prev[i]); } } for ((out, cur), &prev) in out_chunks .into_remainder() .iter_mut() .zip(cur_chunks.remainder()) .zip(prev_chunks.remainder()) { *out = cur.wrapping_sub(prev); } Up } Avg => { let mut out_chunks = output[bpp..].chunks_exact_mut(CHUNK_SIZE); let mut cur_chunks = current[bpp..].chunks_exact(CHUNK_SIZE); let mut cur_minus_bpp_chunks = current[..len - bpp].chunks_exact(CHUNK_SIZE); let mut prev_chunks = previous[bpp..].chunks_exact(CHUNK_SIZE); for (((out, cur), cur_minus_bpp), prev) in (&mut out_chunks) .zip(&mut cur_chunks) .zip(&mut cur_minus_bpp_chunks) .zip(&mut prev_chunks) { for i in 0..CHUNK_SIZE { // Bitwise average of two integers without overflow and // without converting to a wider bit-width. See: // http://aggregate.org/MAGIC/#Average%20of%20Integers // If this is unrolled by component, consider reverting to // `((cur_minus_bpp[i] as u16 + prev[i] as u16) / 2) as u8` out[i] = cur[i].wrapping_sub( (cur_minus_bpp[i] & prev[i]) + ((cur_minus_bpp[i] ^ prev[i]) >> 1), ); } } for (((out, cur), &cur_minus_bpp), &prev) in out_chunks .into_remainder() .iter_mut() .zip(cur_chunks.remainder()) .zip(cur_minus_bpp_chunks.remainder()) .zip(prev_chunks.remainder()) { *out = cur.wrapping_sub((cur_minus_bpp & prev) + ((cur_minus_bpp ^ prev) >> 1)); } for i in 0..bpp { output[i] = current[i].wrapping_sub(previous[i] / 2); } Avg } Paeth => { let mut out_chunks = output[bpp..].chunks_exact_mut(CHUNK_SIZE); let mut cur_chunks = current[bpp..].chunks_exact(CHUNK_SIZE); let mut a_chunks = current[..len - bpp].chunks_exact(CHUNK_SIZE); let mut b_chunks = previous[bpp..].chunks_exact(CHUNK_SIZE); let mut c_chunks = previous[..len - bpp].chunks_exact(CHUNK_SIZE); for ((((out, cur), a), b), c) in (&mut out_chunks) .zip(&mut cur_chunks) .zip(&mut a_chunks) .zip(&mut b_chunks) .zip(&mut c_chunks) { for i in 0..CHUNK_SIZE { out[i] = cur[i].wrapping_sub(filter_paeth_fpnge(a[i], b[i], c[i])); } } for ((((out, cur), &a), &b), &c) in out_chunks .into_remainder() .iter_mut() .zip(cur_chunks.remainder()) .zip(a_chunks.remainder()) .zip(b_chunks.remainder()) .zip(c_chunks.remainder()) { *out = cur.wrapping_sub(filter_paeth_fpnge(a, b, c)); } for i in 0..bpp { output[i] = current[i].wrapping_sub(filter_paeth_fpnge(0, previous[i], 0)); } Paeth } } } pub(crate) fn filter( method: FilterType, adaptive: AdaptiveFilterType, bpp: BytesPerPixel, previous: &[u8], current: &[u8], output: &mut [u8], ) -> FilterType { use FilterType::*; let bpp = bpp.into_usize(); let len = current.len(); match adaptive { AdaptiveFilterType::NonAdaptive => { filter_internal(method, bpp, len, previous, current, output) } AdaptiveFilterType::Adaptive => { let mut min_sum: u64 = u64::MAX; let mut filter_choice = FilterType::NoFilter; for &filter in [Sub, Up, Avg, Paeth].iter() { filter_internal(filter, bpp, len, previous, current, output); let sum = sum_buffer(output); if sum <= min_sum { min_sum = sum; filter_choice = filter; } } if filter_choice != Paeth { filter_internal(filter_choice, bpp, len, previous, current, output); } filter_choice } } } // Helper function for Adaptive filter buffer summation fn sum_buffer(buf: &[u8]) -> u64 { const CHUNK_SIZE: usize = 32; let mut buf_chunks = buf.chunks_exact(CHUNK_SIZE); let mut sum = 0_u64; for chunk in &mut buf_chunks { // At most, `acc` can be `32 * (i8::MIN as u8) = 32 * 128 = 4096`. let mut acc = 0; for &b in chunk { acc += u64::from((b as i8).unsigned_abs()); } sum = sum.saturating_add(acc); } let mut acc = 0; for &b in buf_chunks.remainder() { acc += u64::from((b as i8).unsigned_abs()); } sum.saturating_add(acc) } #[cfg(test)] mod test { use super::*; use core::iter; #[test] fn roundtrip() { // A multiple of 8, 6, 4, 3, 2, 1 const LEN: u8 = 240; let previous: Vec<_> = iter::repeat(1).take(LEN.into()).collect(); let current: Vec<_> = (0..LEN).collect(); let expected = current.clone(); let adaptive = AdaptiveFilterType::NonAdaptive; let roundtrip = |kind, bpp: BytesPerPixel| { let mut output = vec![0; LEN.into()]; filter(kind, adaptive, bpp, &previous, ¤t, &mut output); unfilter(kind, bpp, &previous, &mut output); assert_eq!( output, expected, "Filtering {:?} with {:?} does not roundtrip", bpp, kind ); }; let filters = [ FilterType::NoFilter, FilterType::Sub, FilterType::Up, FilterType::Avg, FilterType::Paeth, ]; let bpps = [ BytesPerPixel::One, BytesPerPixel::Two, BytesPerPixel::Three, BytesPerPixel::Four, BytesPerPixel::Six, BytesPerPixel::Eight, ]; for &filter in filters.iter() { for &bpp in bpps.iter() { roundtrip(filter, bpp); } } } #[test] #[ignore] // takes ~20s without optimizations fn paeth_impls_are_equivalent() { for a in 0..=255 { for b in 0..=255 { for c in 0..=255 { let baseline = filter_paeth(a, b, c); let fpnge = filter_paeth_fpnge(a, b, c); let stbi = filter_paeth_stbi(a, b, c); let stbi_i16 = filter_paeth_stbi_i16(a as i16, b as i16, c as i16); assert_eq!(baseline, fpnge); assert_eq!(baseline, stbi); assert_eq!(baseline as i16, stbi_i16); } } } } #[test] fn roundtrip_ascending_previous_line() { // A multiple of 8, 6, 4, 3, 2, 1 const LEN: u8 = 240; let previous: Vec<_> = (0..LEN).collect(); let current: Vec<_> = (0..LEN).collect(); let expected = current.clone(); let adaptive = AdaptiveFilterType::NonAdaptive; let roundtrip = |kind, bpp: BytesPerPixel| { let mut output = vec![0; LEN.into()]; filter(kind, adaptive, bpp, &previous, ¤t, &mut output); unfilter(kind, bpp, &previous, &mut output); assert_eq!( output, expected, "Filtering {:?} with {:?} does not roundtrip", bpp, kind ); }; let filters = [ FilterType::NoFilter, FilterType::Sub, FilterType::Up, FilterType::Avg, FilterType::Paeth, ]; let bpps = [ BytesPerPixel::One, BytesPerPixel::Two, BytesPerPixel::Three, BytesPerPixel::Four, BytesPerPixel::Six, BytesPerPixel::Eight, ]; for &filter in filters.iter() { for &bpp in bpps.iter() { roundtrip(filter, bpp); } } } #[test] // This tests that converting u8 to i8 doesn't overflow when taking the // absolute value for adaptive filtering: -128_i8.abs() will panic in debug // or produce garbage in release mode. The sum of 0..=255u8 should equal the // sum of the absolute values of -128_i8..=127, or abs(-128..=0) + 1..=127. fn sum_buffer_test() { let sum = (0..=128).sum::() + (1..=127).sum::(); let buf: Vec = (0_u8..=255).collect(); assert_eq!(sum, crate::filter::sum_buffer(&buf)); } } png-0.17.16/src/lib.rs000064400000000000000000000062021046102023000124670ustar 00000000000000//! # PNG encoder and decoder //! //! This crate contains a PNG encoder and decoder. It supports reading of single lines or whole frames. //! //! ## The decoder //! //! The most important types for decoding purposes are [`Decoder`] and //! [`Reader`]. They both wrap a [`std::io::Read`]. //! `Decoder` serves as a builder for `Reader`. Calling [`Decoder::read_info`] reads from the `Read` until the //! image data is reached. //! //! ### Using the decoder //! ``` //! use std::fs::File; //! // The decoder is a build for reader and can be used to set various decoding options //! // via `Transformations`. The default output transformation is `Transformations::IDENTITY`. //! let decoder = png::Decoder::new(File::open("tests/pngsuite/basi0g01.png").unwrap()); //! let mut reader = decoder.read_info().unwrap(); //! // Allocate the output buffer. //! let mut buf = vec![0; reader.output_buffer_size()]; //! // Read the next frame. An APNG might contain multiple frames. //! let info = reader.next_frame(&mut buf).unwrap(); //! // Grab the bytes of the image. //! let bytes = &buf[..info.buffer_size()]; //! // Inspect more details of the last read frame. //! let in_animation = reader.info().frame_control.is_some(); //! ``` //! //! ## Encoder //! ### Using the encoder //! //! ```no_run //! // For reading and opening files //! use std::path::Path; //! use std::fs::File; //! use std::io::BufWriter; //! //! let path = Path::new(r"/path/to/image.png"); //! let file = File::create(path).unwrap(); //! let ref mut w = BufWriter::new(file); //! //! let mut encoder = png::Encoder::new(w, 2, 1); // Width is 2 pixels and height is 1. //! encoder.set_color(png::ColorType::Rgba); //! encoder.set_depth(png::BitDepth::Eight); //! encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455)); // 1.0 / 2.2, scaled by 100000 //! encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2)); // 1.0 / 2.2, unscaled, but rounded //! let source_chromaticities = png::SourceChromaticities::new( // Using unscaled instantiation here //! (0.31270, 0.32900), //! (0.64000, 0.33000), //! (0.30000, 0.60000), //! (0.15000, 0.06000) //! ); //! encoder.set_source_chromaticities(source_chromaticities); //! let mut writer = encoder.write_header().unwrap(); //! //! let data = [255, 0, 0, 255, 0, 0, 0, 255]; // An array containing a RGBA sequence. First pixel is red and second pixel is black. //! writer.write_image_data(&data).unwrap(); // Save //! ``` //! #![cfg_attr(feature = "unstable", feature(portable_simd))] #![forbid(unsafe_code)] mod adam7; pub mod chunk; mod common; mod decoder; mod encoder; mod filter; mod srgb; pub mod text_metadata; mod traits; pub use crate::adam7::expand_pass as expand_interlaced_row; pub use crate::adam7::Adam7Info; pub use crate::common::*; pub use crate::decoder::stream::{DecodeOptions, Decoded, DecodingError, StreamingDecoder}; pub use crate::decoder::{Decoder, InterlaceInfo, InterlacedRow, Limits, OutputInfo, Reader}; pub use crate::encoder::{Encoder, EncodingError, StreamWriter, Writer}; pub use crate::filter::{AdaptiveFilterType, FilterType}; #[cfg(test)] pub(crate) mod test_utils; #[cfg(feature = "benchmarks")] pub mod benchable_apis; png-0.17.16/src/srgb.rs000064400000000000000000000020211046102023000126510ustar 00000000000000use crate::{ScaledFloat, SourceChromaticities}; /// Get the gamma that should be substituted for images conforming to the sRGB color space. pub fn substitute_gamma() -> ScaledFloat { // Value taken from https://www.w3.org/TR/2003/REC-PNG-20031110/#11sRGB ScaledFloat::from_scaled(45455) } /// Get the chromaticities that should be substituted for images conforming to the sRGB color space. pub fn substitute_chromaticities() -> SourceChromaticities { // Values taken from https://www.w3.org/TR/2003/REC-PNG-20031110/#11sRGB SourceChromaticities { white: ( ScaledFloat::from_scaled(31270), ScaledFloat::from_scaled(32900), ), red: ( ScaledFloat::from_scaled(64000), ScaledFloat::from_scaled(33000), ), green: ( ScaledFloat::from_scaled(30000), ScaledFloat::from_scaled(60000), ), blue: ( ScaledFloat::from_scaled(15000), ScaledFloat::from_scaled(6000), ), } } png-0.17.16/src/test_utils.rs000064400000000000000000000105161046102023000141230ustar 00000000000000//! A set of test utilities. //! //! There is some overlap between this module and `src/encoder.rs` module, but: //! //! * This module (unlike `src/encoder.rs`) performs no validation of the data being written - this //! allows building testcases that use arbitrary, potentially invalid PNGs as input. //! * This module can be reused from `benches/decoder.rs` (a separate crate). use byteorder::WriteBytesExt; use std::io::Write; /// Generates a store-only, non-compressed image: /// /// * `00` compression mode (i.e.`BTYPE` = `00` = no compression) is used /// * No filter is applied to the image rows /// /// Currently the image always has the following properties: /// /// * Single `IDAT` chunk /// * Zlib chunks of maximum possible size /// * 8-bit RGBA /// /// These images are somewhat artificial, but may be useful for benchmarking performance of parts /// outside of `fdeflate` crate and/or the `unfilter` function (e.g. these images were originally /// used to evaluate changes to minimize copying of image pixels between various buffers - see /// [this /// discussion](https://github.com/image-rs/image-png/discussions/416#discussioncomment-7436871) /// for more details). pub fn write_noncompressed_png(w: &mut impl Write, size: u32, idat_bytes: usize) { write_png_sig(w); write_rgba8_ihdr_with_width(w, size); write_rgba8_idats(w, size, idat_bytes); write_iend(w); } /// Writes PNG signature. /// See http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html#PNG-file-signature pub fn write_png_sig(w: &mut impl Write) { const SIG: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10]; w.write_all(&SIG).unwrap(); } /// Writes an arbitrary PNG chunk. pub fn write_chunk(w: &mut impl Write, chunk_type: &[u8], data: &[u8]) { assert_eq!(chunk_type.len(), 4); let crc = { let input = chunk_type .iter() .copied() .chain(data.iter().copied()) .collect::>(); crc32fast::hash(input.as_slice()) }; w.write_u32::(data.len() as u32) .unwrap(); w.write_all(chunk_type).unwrap(); w.write_all(data).unwrap(); w.write_u32::(crc).unwrap(); } /// Writes an IHDR chunk that indicates a non-interlaced RGBA8 that uses the same height and /// `width`. See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.IHDR pub fn write_rgba8_ihdr_with_width(w: &mut impl Write, width: u32) { let mut data = Vec::new(); data.write_u32::(width).unwrap(); data.write_u32::(width).unwrap(); // height data.write_u8(8).unwrap(); // bit depth = always 8-bits per channel data.write_u8(6).unwrap(); // color type = color + alpha data.write_u8(0).unwrap(); // compression method (0 is the only allowed value) data.write_u8(0).unwrap(); // filter method (0 is the only allowed value) data.write_u8(0).unwrap(); // interlace method = no interlacing write_chunk(w, b"IHDR", &data); } /// Generates RGBA8 `width` x `height` image and wraps it in a store-only zlib container. pub fn generate_rgba8_with_width_and_height(width: u32, height: u32) -> Vec { // Generate arbitrary test pixels. let image_pixels = { let mut row = Vec::new(); row.write_u8(0).unwrap(); // filter = no filter let row_pixels = (0..width).flat_map(|i| { let color: u8 = (i * 255 / width) as u8; let alpha: u8 = 0xff; [color, 255 - color, color / 2, alpha] }); row.extend(row_pixels); std::iter::repeat(row) .take(height as usize) .flatten() .collect::>() }; let mut zlib_data = Vec::new(); let mut store_only_compressor = fdeflate::StoredOnlyCompressor::new(std::io::Cursor::new(&mut zlib_data)).unwrap(); store_only_compressor.write_data(&image_pixels).unwrap(); store_only_compressor.finish().unwrap(); zlib_data } /// Writes an IDAT chunk. pub fn write_rgba8_idats(w: &mut impl Write, size: u32, idat_bytes: usize) { let data = generate_rgba8_with_width_and_height(size, size); for chunk in data.chunks(idat_bytes) { write_chunk(w, b"IDAT", chunk); } } /// Writes an IEND chunk. /// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.IEND pub fn write_iend(w: &mut impl Write) { write_chunk(w, b"IEND", &[]); } png-0.17.16/src/text_metadata.rs000064400000000000000000000534021046102023000145510ustar 00000000000000//! # Text chunks (tEXt/zTXt/iTXt) structs and functions //! //! The [PNG spec](https://www.w3.org/TR/2003/REC-PNG-20031110/#11textinfo) optionally allows for //! embedded text chunks in the file. They may appear either before or after the image data //! chunks. There are three kinds of text chunks. //! - `tEXt`: This has a `keyword` and `text` field, and is ISO 8859-1 encoded. //! - `zTXt`: This is semantically the same as `tEXt`, i.e. it has the same fields and //! encoding, but the `text` field is compressed before being written into the PNG file. //! - `iTXt`: This chunk allows for its `text` field to be any valid UTF-8, and supports //! compression of the text field as well. //! //! The `ISO 8859-1` encoding technically doesn't allow any control characters //! to be used, but in practice these values are encountered anyway. This can //! either be the extended `ISO-8859-1` encoding with control characters or the //! `Windows-1252` encoding. This crate assumes the `ISO-8859-1` encoding is //! used. //! //! ## Reading text chunks //! //! As a PNG is decoded, any text chunk encountered is appended the //! [`Info`](`crate::common::Info`) struct, in the `uncompressed_latin1_text`, //! `compressed_latin1_text`, and the `utf8_text` fields depending on whether the encountered //! chunk is `tEXt`, `zTXt`, or `iTXt`. //! //! ``` //! use std::fs::File; //! use std::iter::FromIterator; //! use std::path::PathBuf; //! //! // Opening a png file that has a zTXt chunk //! let decoder = png::Decoder::new( //! File::open(PathBuf::from_iter([ //! "tests", //! "text_chunk_examples", //! "ztxt_example.png", //! ])) //! .unwrap(), //! ); //! let mut reader = decoder.read_info().unwrap(); //! // If the text chunk is before the image data frames, `reader.info()` already contains the text. //! for text_chunk in &reader.info().compressed_latin1_text { //! println!("{:?}", text_chunk.keyword); // Prints the keyword //! println!("{:#?}", text_chunk); // Prints out the text chunk. //! // To get the uncompressed text, use the `get_text` method. //! println!("{}", text_chunk.get_text().unwrap()); //! } //! ``` //! //! ## Writing text chunks //! //! There are two ways to write text chunks: the first is to add the appropriate text structs directly to the encoder header before the header is written to file. //! To add a text chunk at any point in the stream, use the `write_text_chunk` method. //! //! ``` //! # use png::text_metadata::{ITXtChunk, ZTXtChunk}; //! # use std::env; //! # use std::fs::File; //! # use std::io::BufWriter; //! # use std::iter::FromIterator; //! # use std::path::PathBuf; //! # let file = File::create(PathBuf::from_iter(["target", "text_chunk.png"])).unwrap(); //! # let ref mut w = BufWriter::new(file); //! let mut encoder = png::Encoder::new(w, 2, 1); // Width is 2 pixels and height is 1. //! encoder.set_color(png::ColorType::Rgba); //! encoder.set_depth(png::BitDepth::Eight); //! // Adding text chunks to the header //! encoder //! .add_text_chunk( //! "Testing tEXt".to_string(), //! "This is a tEXt chunk that will appear before the IDAT chunks.".to_string(), //! ) //! .unwrap(); //! encoder //! .add_ztxt_chunk( //! "Testing zTXt".to_string(), //! "This is a zTXt chunk that is compressed in the png file.".to_string(), //! ) //! .unwrap(); //! encoder //! .add_itxt_chunk( //! "Testing iTXt".to_string(), //! "iTXt chunks support all of UTF8. Example: हिंदी.".to_string(), //! ) //! .unwrap(); //! //! let mut writer = encoder.write_header().unwrap(); //! //! let data = [255, 0, 0, 255, 0, 0, 0, 255]; // An array containing a RGBA sequence. First pixel is red and second pixel is black. //! writer.write_image_data(&data).unwrap(); // Save //! //! // We can add a tEXt/zTXt/iTXt at any point before the encoder is dropped from scope. These chunks will be at the end of the png file. //! let tail_ztxt_chunk = ZTXtChunk::new("Comment".to_string(), "A zTXt chunk after the image data.".to_string()); //! writer.write_text_chunk(&tail_ztxt_chunk).unwrap(); //! //! // The fields of the text chunk are public, so they can be mutated before being written to the file. //! let mut tail_itxt_chunk = ITXtChunk::new("Author".to_string(), "सायंतन खान".to_string()); //! tail_itxt_chunk.compressed = true; //! tail_itxt_chunk.language_tag = "hi".to_string(); //! tail_itxt_chunk.translated_keyword = "लेखक".to_string(); //! writer.write_text_chunk(&tail_itxt_chunk).unwrap(); //! ``` #![warn(missing_docs)] use crate::{chunk, encoder, DecodingError, EncodingError}; use fdeflate::BoundedDecompressionError; use flate2::write::ZlibEncoder; use flate2::Compression; use std::{convert::TryFrom, io::Write}; /// Default decompression limit for compressed text chunks. pub const DECOMPRESSION_LIMIT: usize = 2097152; // 2 MiB /// Text encoding errors that is wrapped by the standard EncodingError type #[derive(Debug, Clone, Copy)] pub(crate) enum TextEncodingError { /// Unrepresentable characters in string Unrepresentable, /// Keyword longer than 79 bytes or empty InvalidKeywordSize, /// Error encountered while compressing text CompressionError, } /// Text decoding error that is wrapped by the standard DecodingError type #[derive(Debug, Clone, Copy)] pub(crate) enum TextDecodingError { /// Unrepresentable characters in string Unrepresentable, /// Keyword longer than 79 bytes or empty InvalidKeywordSize, /// Missing null separator MissingNullSeparator, /// Compressed text cannot be uncompressed InflationError, /// Needs more space to decompress OutOfDecompressionSpace, /// Using an unspecified value for the compression method InvalidCompressionMethod, /// Using a byte that is not 0 or 255 as compression flag in iTXt chunk InvalidCompressionFlag, /// Missing the compression flag MissingCompressionFlag, } /// A generalized text chunk trait pub trait EncodableTextChunk { /// Encode text chunk as `Vec` to a `Write` fn encode(&self, w: &mut W) -> Result<(), EncodingError>; } /// Struct representing a tEXt chunk #[derive(Clone, Debug, PartialEq, Eq)] pub struct TEXtChunk { /// Keyword field of the tEXt chunk. Needs to be between 1-79 bytes when encoded as Latin-1. pub keyword: String, /// Text field of tEXt chunk. Can be at most 2GB. pub text: String, } fn decode_iso_8859_1(text: &[u8]) -> String { text.iter().map(|&b| b as char).collect() } pub(crate) fn encode_iso_8859_1(text: &str) -> Result, TextEncodingError> { encode_iso_8859_1_iter(text).collect() } fn encode_iso_8859_1_into(buf: &mut Vec, text: &str) -> Result<(), TextEncodingError> { for b in encode_iso_8859_1_iter(text) { buf.push(b?); } Ok(()) } fn encode_iso_8859_1_iter(text: &str) -> impl Iterator> + '_ { text.chars() .map(|c| u8::try_from(c as u32).map_err(|_| TextEncodingError::Unrepresentable)) } fn decode_ascii(text: &[u8]) -> Result<&str, TextDecodingError> { if text.is_ascii() { // `from_utf8` cannot panic because we're already checked that `text` is ASCII-7. // And this is the only safe way to get ASCII-7 string from `&[u8]`. Ok(std::str::from_utf8(text).expect("unreachable")) } else { Err(TextDecodingError::Unrepresentable) } } impl TEXtChunk { /// Constructs a new TEXtChunk. /// Not sure whether it should take &str or String. pub fn new(keyword: impl Into, text: impl Into) -> Self { Self { keyword: keyword.into(), text: text.into(), } } /// Decodes a slice of bytes to a String using Latin-1 decoding. /// The decoder runs in strict mode, and any decoding errors are passed along to the caller. pub(crate) fn decode( keyword_slice: &[u8], text_slice: &[u8], ) -> Result { if keyword_slice.is_empty() || keyword_slice.len() > 79 { return Err(TextDecodingError::InvalidKeywordSize); } Ok(Self { keyword: decode_iso_8859_1(keyword_slice), text: decode_iso_8859_1(text_slice), }) } } impl EncodableTextChunk for TEXtChunk { /// Encodes TEXtChunk to a Writer. The keyword and text are separated by a byte of zeroes. fn encode(&self, w: &mut W) -> Result<(), EncodingError> { let mut data = encode_iso_8859_1(&self.keyword)?; if data.is_empty() || data.len() > 79 { return Err(TextEncodingError::InvalidKeywordSize.into()); } data.push(0); encode_iso_8859_1_into(&mut data, &self.text)?; encoder::write_chunk(w, chunk::tEXt, &data) } } /// Struct representing a zTXt chunk #[derive(Clone, Debug, PartialEq, Eq)] pub struct ZTXtChunk { /// Keyword field of the tEXt chunk. Needs to be between 1-79 bytes when encoded as Latin-1. pub keyword: String, /// Text field of zTXt chunk. It is compressed by default, but can be uncompressed if necessary. text: OptCompressed, } /// Private enum encoding the compressed and uncompressed states of zTXt/iTXt text field. #[derive(Clone, Debug, PartialEq, Eq)] enum OptCompressed { /// Compressed version of text field. Can be at most 2GB. Compressed(Vec), /// Uncompressed text field. Uncompressed(String), } impl ZTXtChunk { /// Creates a new ZTXt chunk. pub fn new(keyword: impl Into, text: impl Into) -> Self { Self { keyword: keyword.into(), text: OptCompressed::Uncompressed(text.into()), } } pub(crate) fn decode( keyword_slice: &[u8], compression_method: u8, text_slice: &[u8], ) -> Result { if keyword_slice.is_empty() || keyword_slice.len() > 79 { return Err(TextDecodingError::InvalidKeywordSize); } if compression_method != 0 { return Err(TextDecodingError::InvalidCompressionMethod); } Ok(Self { keyword: decode_iso_8859_1(keyword_slice), text: OptCompressed::Compressed(text_slice.to_vec()), }) } /// Decompresses the inner text, mutating its own state. Can only handle decompressed text up to `DECOMPRESSION_LIMIT` bytes. pub fn decompress_text(&mut self) -> Result<(), DecodingError> { self.decompress_text_with_limit(DECOMPRESSION_LIMIT) } /// Decompresses the inner text, mutating its own state. Can only handle decompressed text up to `limit` bytes. pub fn decompress_text_with_limit(&mut self, limit: usize) -> Result<(), DecodingError> { match &self.text { OptCompressed::Compressed(v) => { let uncompressed_raw = match fdeflate::decompress_to_vec_bounded(&v[..], limit) { Ok(s) => s, Err(BoundedDecompressionError::OutputTooLarge { .. }) => { return Err(DecodingError::from( TextDecodingError::OutOfDecompressionSpace, )); } Err(_) => { return Err(DecodingError::from(TextDecodingError::InflationError)); } }; self.text = OptCompressed::Uncompressed(decode_iso_8859_1(&uncompressed_raw)); } OptCompressed::Uncompressed(_) => {} }; Ok(()) } /// Decompresses the inner text, and returns it as a `String`. /// If decompression uses more the 2MiB, first call decompress with limit, and then this method. pub fn get_text(&self) -> Result { match &self.text { OptCompressed::Compressed(v) => { let uncompressed_raw = fdeflate::decompress_to_vec(v) .map_err(|_| DecodingError::from(TextDecodingError::InflationError))?; Ok(decode_iso_8859_1(&uncompressed_raw)) } OptCompressed::Uncompressed(s) => Ok(s.clone()), } } /// Compresses the inner text, mutating its own state. pub fn compress_text(&mut self) -> Result<(), EncodingError> { match &self.text { OptCompressed::Uncompressed(s) => { let uncompressed_raw = encode_iso_8859_1(s)?; let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); encoder .write_all(&uncompressed_raw) .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; self.text = OptCompressed::Compressed( encoder .finish() .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?, ); } OptCompressed::Compressed(_) => {} } Ok(()) } } impl EncodableTextChunk for ZTXtChunk { fn encode(&self, w: &mut W) -> Result<(), EncodingError> { let mut data = encode_iso_8859_1(&self.keyword)?; if data.is_empty() || data.len() > 79 { return Err(TextEncodingError::InvalidKeywordSize.into()); } // Null separator data.push(0); // Compression method: the only valid value is 0, as of 2021. data.push(0); match &self.text { OptCompressed::Compressed(v) => { data.extend_from_slice(&v[..]); } OptCompressed::Uncompressed(s) => { // This code may have a bug. Check for correctness. let uncompressed_raw = encode_iso_8859_1(s)?; let mut encoder = ZlibEncoder::new(data, Compression::fast()); encoder .write_all(&uncompressed_raw) .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; data = encoder .finish() .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; } }; encoder::write_chunk(w, chunk::zTXt, &data) } } /// Struct encoding an iTXt chunk #[derive(Clone, Debug, PartialEq, Eq)] pub struct ITXtChunk { /// The keyword field. This needs to be between 1-79 bytes when encoded as Latin-1. pub keyword: String, /// Indicates whether the text will be (or was) compressed in the PNG. pub compressed: bool, /// A hyphen separated list of languages that the keyword is translated to. This is ASCII-7 encoded. pub language_tag: String, /// Translated keyword. This is UTF-8 encoded. pub translated_keyword: String, /// Text field of iTXt chunk. It is compressed by default, but can be uncompressed if necessary. text: OptCompressed, } impl ITXtChunk { /// Constructs a new iTXt chunk. Leaves all but keyword and text to default values. pub fn new(keyword: impl Into, text: impl Into) -> Self { Self { keyword: keyword.into(), compressed: false, language_tag: "".to_string(), translated_keyword: "".to_string(), text: OptCompressed::Uncompressed(text.into()), } } pub(crate) fn decode( keyword_slice: &[u8], compression_flag: u8, compression_method: u8, language_tag_slice: &[u8], translated_keyword_slice: &[u8], text_slice: &[u8], ) -> Result { if keyword_slice.is_empty() || keyword_slice.len() > 79 { return Err(TextDecodingError::InvalidKeywordSize); } let keyword = decode_iso_8859_1(keyword_slice); let compressed = match compression_flag { 0 => false, 1 => true, _ => return Err(TextDecodingError::InvalidCompressionFlag), }; if compressed && compression_method != 0 { return Err(TextDecodingError::InvalidCompressionMethod); } let language_tag = decode_ascii(language_tag_slice)?.to_owned(); let translated_keyword = std::str::from_utf8(translated_keyword_slice) .map_err(|_| TextDecodingError::Unrepresentable)? .to_string(); let text = if compressed { OptCompressed::Compressed(text_slice.to_vec()) } else { OptCompressed::Uncompressed( String::from_utf8(text_slice.to_vec()) .map_err(|_| TextDecodingError::Unrepresentable)?, ) }; Ok(Self { keyword, compressed, language_tag, translated_keyword, text, }) } /// Decompresses the inner text, mutating its own state. Can only handle decompressed text up to `DECOMPRESSION_LIMIT` bytes. pub fn decompress_text(&mut self) -> Result<(), DecodingError> { self.decompress_text_with_limit(DECOMPRESSION_LIMIT) } /// Decompresses the inner text, mutating its own state. Can only handle decompressed text up to `limit` bytes. pub fn decompress_text_with_limit(&mut self, limit: usize) -> Result<(), DecodingError> { match &self.text { OptCompressed::Compressed(v) => { let uncompressed_raw = match fdeflate::decompress_to_vec_bounded(v, limit) { Ok(s) => s, Err(BoundedDecompressionError::OutputTooLarge { .. }) => { return Err(DecodingError::from( TextDecodingError::OutOfDecompressionSpace, )); } Err(_) => { return Err(DecodingError::from(TextDecodingError::InflationError)); } }; self.text = OptCompressed::Uncompressed( String::from_utf8(uncompressed_raw) .map_err(|_| TextDecodingError::Unrepresentable)?, ); } OptCompressed::Uncompressed(_) => {} }; Ok(()) } /// Decompresses the inner text, and returns it as a `String`. /// If decompression takes more than 2 MiB, try `decompress_text_with_limit` followed by this method. pub fn get_text(&self) -> Result { match &self.text { OptCompressed::Compressed(v) => { let uncompressed_raw = fdeflate::decompress_to_vec(v) .map_err(|_| DecodingError::from(TextDecodingError::InflationError))?; String::from_utf8(uncompressed_raw) .map_err(|_| TextDecodingError::Unrepresentable.into()) } OptCompressed::Uncompressed(s) => Ok(s.clone()), } } /// Compresses the inner text, mutating its own state. pub fn compress_text(&mut self) -> Result<(), EncodingError> { match &self.text { OptCompressed::Uncompressed(s) => { let uncompressed_raw = s.as_bytes(); let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); encoder .write_all(uncompressed_raw) .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; self.text = OptCompressed::Compressed( encoder .finish() .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?, ); } OptCompressed::Compressed(_) => {} } Ok(()) } } impl EncodableTextChunk for ITXtChunk { fn encode(&self, w: &mut W) -> Result<(), EncodingError> { // Keyword let mut data = encode_iso_8859_1(&self.keyword)?; if data.is_empty() || data.len() > 79 { return Err(TextEncodingError::InvalidKeywordSize.into()); } // Null separator data.push(0); // Compression flag if self.compressed { data.push(1); } else { data.push(0); } // Compression method data.push(0); // Language tag if !self.language_tag.is_ascii() { return Err(EncodingError::from(TextEncodingError::Unrepresentable)); } data.extend(self.language_tag.as_bytes()); // Null separator data.push(0); // Translated keyword data.extend_from_slice(self.translated_keyword.as_bytes()); // Null separator data.push(0); // Text if self.compressed { match &self.text { OptCompressed::Compressed(v) => { data.extend_from_slice(&v[..]); } OptCompressed::Uncompressed(s) => { let uncompressed_raw = s.as_bytes(); let mut encoder = ZlibEncoder::new(data, Compression::fast()); encoder .write_all(uncompressed_raw) .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; data = encoder .finish() .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; } } } else { match &self.text { OptCompressed::Compressed(v) => { let uncompressed_raw = fdeflate::decompress_to_vec(v) .map_err(|_| EncodingError::from(TextEncodingError::CompressionError))?; data.extend_from_slice(&uncompressed_raw[..]); } OptCompressed::Uncompressed(s) => { data.extend_from_slice(s.as_bytes()); } } } encoder::write_chunk(w, chunk::iTXt, &data) } } png-0.17.16/src/traits.rs000064400000000000000000000023231046102023000132270ustar 00000000000000use std::io; macro_rules! read_bytes_ext { ($output_type:ty) => { impl ReadBytesExt<$output_type> for W { #[inline] fn read_be(&mut self) -> io::Result<$output_type> { let mut bytes = [0u8; std::mem::size_of::<$output_type>()]; self.read_exact(&mut bytes)?; Ok(<$output_type>::from_be_bytes(bytes)) } } }; } macro_rules! write_bytes_ext { ($input_type:ty) => { impl WriteBytesExt<$input_type> for W { #[inline] fn write_be(&mut self, n: $input_type) -> io::Result<()> { self.write_all(&n.to_be_bytes()) } } }; } /// Read extension to read big endian data pub trait ReadBytesExt: io::Read { /// Read `T` from a bytes stream. Most significant byte first. fn read_be(&mut self) -> io::Result; } /// Write extension to write big endian data pub trait WriteBytesExt: io::Write { /// Writes `T` to a bytes stream. Most significant byte first. fn write_be(&mut self, _: T) -> io::Result<()>; } read_bytes_ext!(u8); read_bytes_ext!(u16); read_bytes_ext!(u32); write_bytes_ext!(u32);