image-extras-0.1.0/.cargo_vcs_info.json0000644000000001360000000000100134200ustar { "git": { "sha1": "bb1aa4f5a30298ccdef3bce49b6eb9c85a0a3d0d" }, "path_in_vcs": "" }image-extras-0.1.0/Cargo.lock0000644000000320330000000000100113740ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aliasable" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder-lite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "derive_arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "fax" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" dependencies = [ "fax_derive", ] [[package]] name = "fax_derive" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", "syn", ] [[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.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", "miniz_oxide", ] [[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", "zerocopy", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "image" version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", "moxcms", "num-traits", "png", "tiff", ] [[package]] name = "image-extras" version = "0.1.0" dependencies = [ "image", "ouroboros", "pcx", "walkdir", "wbmp", "zip", ] [[package]] name = "indexmap" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "libz-rs-sys" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" dependencies = [ "zlib-rs", ] [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "moxcms" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" dependencies = [ "num-traits", "pxfm", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "ouroboros" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" dependencies = [ "aliasable", "ouroboros_macro", "static_assertions", ] [[package]] name = "ouroboros_macro" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" dependencies = [ "heck", "proc-macro2", "proc-macro2-diagnostics", "quote", "syn", ] [[package]] name = "pcx" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c49a81cdd56136bf6a20df44c8d67453eca545ed82f716e8cd1c25fa0a1d621" dependencies = [ "byteorder", ] [[package]] name = "png" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ "bitflags", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "proc-macro2-diagnostics" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", "syn", "version_check", "yansi", ] [[package]] name = "pxfm" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" dependencies = [ "num-traits", ] [[package]] name = "quick-error" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[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 = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tiff" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" dependencies = [ "fax", "flate2", "half", "quick-error", "weezl", "zune-jpeg", ] [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[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 = "wbmp" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8225fa9cacbdd3c0eccaf5cab3f1920b70b02447ff67b4af1ca15873d2fa445f" [[package]] name = "weezl" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zip" version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532" dependencies = [ "arbitrary", "crc32fast", "flate2", "indexmap", "memchr", "zopfli", ] [[package]] name = "zlib-rs" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", "log", "simd-adler32", ] [[package]] name = "zune-core" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] image-extras-0.1.0/Cargo.toml0000644000000032510000000000100114170ustar # 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 = "2021" rust-version = "1.89.0" name = "image-extras" version = "0.1.0" build = false include = [ "src", "LICENSE-APACHE", "LICENSE-MIT", "README.md", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Additional image format decoders for the image crate" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/image-rs/image-extras" [features] default = [ "ora", "otb", "pcx", "sgi", "xbm", "xpm", "wbmp", ] ora = [ "image/png", "dep:zip", "dep:ouroboros", ] otb = [] pcx = ["dep:pcx"] sgi = [] wbmp = ["dep:wbmp"] xbm = [] xpm = [] [lib] name = "image_extras" path = "src/lib.rs" [dependencies.image] version = "0.25.8" default-features = false [dependencies.ouroboros] version = "0.18.5" optional = true [dependencies.pcx] version = "0.2.4" optional = true [dependencies.wbmp] version = "0.1.2" optional = true [dependencies.zip] version = "5.1.1" features = ["deflate"] optional = true default-features = false [dev-dependencies.image] version = "0.25.8" features = [ "png", "tiff", ] default-features = false [dev-dependencies.walkdir] version = "2.5.0" image-extras-0.1.0/Cargo.toml.orig000064400000000000000000000020671046102023000151040ustar 00000000000000[package] name = "image-extras" version = "0.1.0" edition = "2021" rust-version = "1.89.0" # MSRV Policy: N-2 license = "MIT OR Apache-2.0" description = "Additional image format decoders for the image crate" repository = "https://github.com/image-rs/image-extras" include = ["src", "LICENSE-APACHE", "LICENSE-MIT", "README.md"] [features] default = ["ora", "otb", "pcx", "sgi", "xbm", "xpm", "wbmp"] # Each feature below is the file extension for the file format encoder/decoder # it enables. ora = ["image/png", "dep:zip", "dep:ouroboros"] otb = [] sgi = [] pcx = ["dep:pcx"] wbmp = ["dep:wbmp"] xbm = [] xpm = [] [dependencies] image = { version = "0.25.8", default-features = false } # Optional dependencies ouroboros = { version = "0.18.5", optional = true } pcx = { version = "0.2.4", optional = true } wbmp = { version = "0.1.2", optional = true } zip = { version = "5.1.1", default-features = false, features = ["deflate"], optional = true } [dev-dependencies] image = { version = "0.25.8", default-features = false, features = ["png", "tiff"] } walkdir = "2.5.0" image-extras-0.1.0/LICENSE-APACHE000064400000000000000000000236761046102023000141520ustar 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 image-extras-0.1.0/LICENSE-MIT000064400000000000000000000020141046102023000136410ustar 00000000000000MIT License 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. image-extras-0.1.0/README.md000064400000000000000000000054031046102023000134710ustar 00000000000000# image-extras Support for additional image formats beyond those provided by the [`image`](https://crates.io/crates/image) crate. ## Usage Call the `register` function at program startup: ```rust fn main() { image_extras::register(); // Now you can use the image crate as normal let img = image::open("path/to/image.pcx").unwrap(); } ``` By default, all supported formats are enabled. For finer control, enable individual formats via Cargo features: ```toml [dependencies] image_extras = { version = "0.1", features = ["pcx"], default-features = false } ``` ## Supported Formats | Feature | Format | ------- | ------ | `ora` | OpenRaster [\[spec\]](https://www.openraster.org/) | `otb` | OTA Bitmap (Over The Air Bitmap) [\[desc\]](https://en.wikipedia.org/wiki/OTA_bitmap) | `pcx` | PCX (ZSoft Paintbrush bitmap/PiCture eXchange) [\[desc\]](https://en.wikipedia.org/wiki/PCX#PCX_file_format) | `sgi` | SGI (Silicon Graphics Image) [\[spec\]](https://web.archive.org/web/20010413154909/https://reality.sgi.com/grafica/sgiimage.html) | `wbmp` | Wireless Bitmap [\[spec\]](https://www.wapforum.org/what/technical/SPEC-WAESpec-19990524.pdf) | `xbm` | X BitMap [\[spec\]](https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#Manipulating_Bitmaps) | `xpm` | X PixMap [\[spec\]](https://www.x.org/docs/XPM/xpm.pdf) By default, `image-extras` enables support for all included formats. This is convenient for prototyping, but for other uses you are encouraged to evaluate the individual implementations and enable only the ones that meet your quality/robustness requirements. ### New Formats We welcome PRs to add support for additional image formats. #### Required criteria - [ ] Must be one of the raster image formats [recognized by ImageMagick](https://imagemagick.org/script/formats.php). - [ ] No patent or licensing restrictions. - [ ] Specification or sufficiently detailed file format description freely available online. - [ ] Must include multiple test images, and their source/license should be mentioned in the PR description. - [ ] Implementation must be entirely in Rust. #### Additional nice-to-haves - [ ] Minimal or no dependencies on external libraries. - [ ] No use of unsafe code. ## Version Compatibility | `image` crate version | Compatible `image-extras` versions | | --------------------- | ---------------------------------- | | 0.25.x | 0.1.x | ## Fuzzing Fuzzing is not a priority for this crate and decoders may panic or worse on malformed input. Please do not open issues for crashes found by fuzzing, unless they are memory safety violations, though PRs fixing them are welcome. This is an intentional tradeoff to balance the inclusion criteria for new formats with maintainer time and effort. image-extras-0.1.0/src/lib.rs000064400000000000000000000114411046102023000141140ustar 00000000000000//! This crate provides additional formats for the image crate. //! //! Call the [`register`] function at program startup: //! //! ```rust,no_run //! image_extras::register(); //! //! // Now you can use the image crate as normal //! let img = image::open("path/to/image.pcx").unwrap(); //! ``` //! //! By default, all supported formats are enabled. For finer control, enable //! individual formats via Cargo features. #![forbid(unsafe_code)] #[cfg(feature = "ora")] pub mod ora; #[cfg(feature = "otb")] pub mod otb; #[cfg(feature = "pcx")] pub mod pcx; #[cfg(feature = "sgi")] pub mod sgi; #[cfg(feature = "wbmp")] pub mod wbmp; #[cfg(feature = "xbm")] pub mod xbm; #[cfg(feature = "xpm")] pub mod xpm; #[allow(unused_imports)] use image::hooks::{register_decoding_hook, register_format_detection_hook}; static REGISTER: std::sync::Once = std::sync::Once::new(); /// Register all enabled extra formats with the image crate. /// /// Calling this function more than once is allowed but has no additional /// effect. pub fn register() { REGISTER.call_once(|| { // OpenRaster images are ZIP files and have no simple signature to distinguish them // from ZIP files containing other content #[cfg(feature = "ora")] register_decoding_hook( "ora".into(), Box::new(|r| { Ok(Box::new(ora::OpenRasterDecoder::with_limits( r, image::Limits::no_limits(), )?)) }), ); #[cfg(feature = "otb")] image::hooks::register_decoding_hook( "otb".into(), Box::new(|r| Ok(Box::new(otb::OtbDecoder::new(r)?))), ); #[cfg(feature = "pcx")] if register_decoding_hook( "pcx".into(), Box::new(|r| Ok(Box::new(pcx::PCXDecoder::new(r)?))), ) { register_format_detection_hook("pcx".into(), &[0x0a, 0x0], Some(b"\xFF\xF8")); } // SGI RGB images generally show up with a .rgb ending (whether or not they // have 3 channels), and sometimes .bw (when grayscale) and .rgba. The // extensions .sgi and .iris, while unambiguous, do not seem to have been // used much. The extension .rgb is also used for a variety of other files, // including bare image data, so to be sure it would be best to check both // extension and leading bytes #[cfg(feature = "sgi")] { let hook: for<'a> fn( image::hooks::GenericReader<'a>, ) -> image::ImageResult> = |r| Ok(Box::new(sgi::SgiDecoder::new(r)?)); image::hooks::register_decoding_hook("bw".into(), Box::new(hook)); image::hooks::register_decoding_hook("rgb".into(), Box::new(hook)); image::hooks::register_decoding_hook("rgba".into(), Box::new(hook)); image::hooks::register_decoding_hook("iris".into(), Box::new(hook)); if image::hooks::register_decoding_hook("sgi".into(), Box::new(hook)) { // The main signature bytes are technically just 01 da, but this is short // and the following storage and bpc fields are constrained well enough to // efficiently match them as well image::hooks::register_format_detection_hook( "sgi".into(), b"\x01\xda\x00\x01", None, ); image::hooks::register_format_detection_hook( "sgi".into(), b"\x01\xda\x01\x01", None, ); image::hooks::register_format_detection_hook( "sgi".into(), b"\x01\xda\x00\x02", None, ); image::hooks::register_format_detection_hook( "sgi".into(), b"\x01\xda\x01\x02", None, ); } } #[cfg(feature = "wbmp")] image::hooks::register_decoding_hook( "wbmp".into(), Box::new(|r| Ok(Box::new(wbmp::WbmpDecoder::new(r)?))), ); #[cfg(feature = "xbm")] { register_decoding_hook( "xbm".into(), Box::new(|r| Ok(Box::new(xbm::XbmDecoder::new(r)?))), ); register_decoding_hook( "bm".into(), Box::new(|r| Ok(Box::new(xbm::XbmDecoder::new(r)?))), ); } #[cfg(feature = "xpm")] if register_decoding_hook( "xpm".into(), Box::new(|r| Ok(Box::new(xpm::XpmDecoder::new(r)?))), ) { register_format_detection_hook("xpm".into(), b"/* XPM */", None); } }); } image-extras-0.1.0/src/ora.rs000064400000000000000000000215401046102023000141300ustar 00000000000000//! Decoding of OpenRaster Images (*.ora) //! //! OpenRaster is an a file format used to communicate layered images; the //! decoder provided herein only extracts and displays the final merged raster //! image cached by the OpenRaster file, and does not expose the details of //! layers (which may be either raster or vector graphics) or render the merged //! image itself. //! //! # Related Links //! * - The OpenRaster format on Wikipedia //! * - OpenRaster specification use image::codecs::png::PngDecoder; use image::error::{DecodingError, ImageFormatHint, UnsupportedError}; use image::metadata::Orientation; use image::{ColorType, ExtendedColorType, ImageDecoder, ImageError, ImageResult, Limits}; use ouroboros::self_referencing; use std::io::{self, BufReader, Read, Seek}; use std::marker::PhantomData; use zip::read::{ZipArchive, ZipFile}; pub struct OpenRasterDecoder<'a, R> where R: Read + Seek + 'a, { mergedimg_decoder: PngDecoder>>, } fn openraster_format_hint() -> ImageFormatHint { ImageFormatHint::Name("OpenRaster".into()) } /// Adjust the format of the PngDecoder's errors to indicate OpenRaster instead fn set_ora_image_type(err: ImageError) -> ImageError { match err { ImageError::Decoding(e) => { // DecodingError does not directly expose the underlying type, // so nest the error ImageError::Decoding(DecodingError::new(openraster_format_hint(), e)) } ImageError::Encoding(_) => { // Should not be encoding any files unreachable!(); } ImageError::Parameter(e) => ImageError::Parameter(e), ImageError::Limits(e) => ImageError::Limits(e), ImageError::Unsupported(e) => ImageError::Unsupported( UnsupportedError::from_format_and_kind(openraster_format_hint(), e.kind()), ), ImageError::IoError(e) => ImageError::IoError(e), } } #[self_referencing] struct SeekableArchiveCore<'a, R: Read + Seek + 'a> { archive: ZipArchive, #[covariant] #[borrows(mut archive)] file: ZipFile<'this, R>, lifetime_helper: PhantomData<&'a R>, } /// The zip crate does not provide a seekable reader that works on compressed /// entries, while png::Decoder requires the Seek bound (but does not currently /// use it). This structure implements Seek by reopening and reading the zip /// archive entry whenever it seeks backwards. struct SeekableArchiveFile<'a, R: Read + Seek + 'a> { core: Option>, file_index: usize, position: u64, file_size: u64, } impl<'a, R: Read + Seek + 'a> SeekableArchiveFile<'a, R> { fn new( archive: ZipArchive, file_index: usize, ) -> Result, io::Error> { let core = SeekableArchiveCore::try_new(archive, |x| x.by_index(file_index), PhantomData) .map_err(|x| io::Error::other(format!("failed to open: {:?}", x)))?; let file_size = core.with_file(|file| file.size()); Ok(SeekableArchiveFile { core: Some(core), file_index, position: 0, file_size, }) } } impl Read for SeekableArchiveFile<'_, R> { fn read(&mut self, buf: &mut [u8]) -> io::Result { let res = self .core .as_mut() .unwrap() .with_file_mut(|file| file.read(buf)); let nread = res?; self.position .checked_add(nread as u64) .ok_or_else(|| io::Error::other("seek position overflow"))?; Ok(nread) } } impl Seek for SeekableArchiveFile<'_, R> { fn seek(&mut self, pos: io::SeekFrom) -> io::Result { let target_pos = match pos { io::SeekFrom::Start(offset) => offset, io::SeekFrom::End(offset) => self .file_size .checked_add_signed(offset) .ok_or_else(|| io::Error::other("seek position over or underflow"))?, io::SeekFrom::Current(offset) => self .position .checked_add_signed(offset) .ok_or_else(|| io::Error::other("seek position over or underflow"))?, }; if target_pos < self.position { let core = self.core.take(); let archive = core.unwrap().into_heads().archive; self.core = Some( SeekableArchiveCore::try_new(archive, |x| x.by_index(self.file_index), PhantomData) .map_err(|x| io::Error::other(format!("failed to reopen: {:?}", x)))?, ); } while self.position < target_pos { const TMP_LEN: usize = 1024; let mut tmp = [0_u8; TMP_LEN]; let cur_pos = self.position; let nr = self .read(&mut tmp[..std::cmp::min(TMP_LEN as u64, target_pos - cur_pos) as usize])?; if nr == 0 { return Err(io::Error::other("unexpected eof when seeking")); } self.position += nr as u64; } Ok(0) } } impl<'a, R> OpenRasterDecoder<'a, R> where R: Read + Seek + 'a, { /// Create a new `OpenRasterDecoder` with the provided limits. /// /// (Limits need to be specified in advance, because determining the /// minimum information needed for the ImageDecoder trait (image size and /// color type) may require reading through and remembering image-dependent /// amount of data.) /// /// Warning: While decoding limits apply to the header parsing and decoding /// of the merged imaged component (a PNG file inside the ZIP archive that /// forms an OpenRaster file), memory constraints on the ZIP file decoding /// process have not yet been implemented; input ZIP files with very many /// entries may require significant amounts of memory to read. pub fn with_limits(r: R, limits: Limits) -> Result, ImageError> { let mut archive = ZipArchive::new(r) .map_err(|e| ImageError::Decoding(DecodingError::new(openraster_format_hint(), e)))?; /* Verify that this _is_ an OpenRaster file, and not some unrelated ZIP archive */ let mimetype_index = archive.index_for_name("mimetype").ok_or_else(|| { ImageError::Decoding(DecodingError::new( openraster_format_hint(), "OpenRaster images should contain a mimetype subfile", )) })?; let mut mimetype_file = archive .by_index(mimetype_index) .map_err(|x| ImageError::Decoding(DecodingError::new(openraster_format_hint(), x)))?; const EXPECTED_MIMETYPE: &str = "image/openraster"; let mut tmp = [0u8; EXPECTED_MIMETYPE.len()]; mimetype_file.read_exact(&mut tmp)?; if tmp != EXPECTED_MIMETYPE.as_bytes() || mimetype_file.size() != EXPECTED_MIMETYPE.len() as u64 { return Err(ImageError::Decoding(DecodingError::new( openraster_format_hint(), "Image did not have correct mimetype subentry to be identified as OpenRaster", ))); } drop(mimetype_file); let mergedimage_index = archive.index_for_name("mergedimage.png").ok_or_else(|| { ImageError::Decoding(DecodingError::new( openraster_format_hint(), "OpenRaster image missing mergedimage.png entry", )) })?; let file = SeekableArchiveFile::new(archive, mergedimage_index)?; let decoder = PngDecoder::with_limits(BufReader::new(file), limits).map_err(set_ora_image_type)?; Ok(OpenRasterDecoder { mergedimg_decoder: decoder, }) } } impl<'a, R: Read + Seek + 'a> ImageDecoder for OpenRasterDecoder<'a, R> { fn dimensions(&self) -> (u32, u32) { self.mergedimg_decoder.dimensions() } fn color_type(&self) -> ColorType { self.mergedimg_decoder.color_type() } fn original_color_type(&self) -> ExtendedColorType { self.mergedimg_decoder.original_color_type() } fn set_limits(&mut self, limits: Limits) -> ImageResult<()> { // Warning: this does not account for any ZIP reading overhead self.mergedimg_decoder.set_limits(limits) } fn icc_profile(&mut self) -> ImageResult>> { self.mergedimg_decoder.icc_profile() } fn exif_metadata(&mut self) -> ImageResult>> { self.mergedimg_decoder.exif_metadata() } fn orientation(&mut self) -> ImageResult { self.mergedimg_decoder.orientation() } fn read_image(self, buf: &mut [u8]) -> ImageResult<()> { self.mergedimg_decoder.read_image(buf) } fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { (*self).read_image(buf) } } image-extras-0.1.0/src/otb.rs000064400000000000000000000371711046102023000141420ustar 00000000000000//! Decoding of OTB Images //! //! OTB (Over The air Bitmap) Format is an image format from Nokia's Smart Messaging specification. //! //! # Related Links //! * - OTA bitmap on Wikipedia //! * - Specification use std::fmt::{self, Display}; use std::io::{BufRead, Seek, Write}; use image::error::{DecodingError, EncodingError, ImageFormatHint, LimitError, LimitErrorKind}; use image::{ColorType, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageResult}; /// All errors that can occur when attempting to encode an image to OTB format #[derive(Debug, Clone)] enum EncoderError { /// Specified image does not fit into OTB width / height constraints ImageTooLarge, /// ColorType of image is not supported UnsupportedColorType, } impl Display for EncoderError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { EncoderError::ImageTooLarge => f.write_fmt(format_args!( "Specified image is too large for the OTB format (Max 255x255)" )), EncoderError::UnsupportedColorType => f.write_fmt(format_args!( "Color type of specified image is not supported" )), } } } impl std::error::Error for EncoderError {} impl From for ImageError { fn from(e: EncoderError) -> ImageError { match e { EncoderError::ImageTooLarge => { ImageError::Limits(LimitError::from_kind(LimitErrorKind::DimensionError)) } _ => ImageError::Encoding(EncodingError::new(ImageFormatHint::Name("otb".into()), e)), } } } /// Encoder for Otb images. pub struct OtbEncoder { writer: W, threshold: u8, } impl OtbEncoder { pub fn new(writer: W) -> Self { Self { writer, threshold: 127_u8, } } pub fn with_threshold(mut self, threshold: u8) -> Self { self.threshold = threshold; self } } impl ImageEncoder for OtbEncoder { fn write_image( mut self, buf: &[u8], width: u32, height: u32, color_type: ExtendedColorType, ) -> std::result::Result<(), ImageError> { if width > 0xFF || height > 0xFF { return Err(EncoderError::ImageTooLarge.into()); } if color_type != ExtendedColorType::L8 { return Err(EncoderError::UnsupportedColorType.into()); } // Write Headers let _ = self .writer .write(&[0x00, width as u8, height as u8, 0x01])?; // Write the encoded image let mut current_byte = 0_u8; let mut bit = 0; for buf_idx in 0..(width * height) { if buf[buf_idx as usize] < self.threshold { current_byte |= 1 << (7 - bit); } bit += 1; if bit == 8 { self.writer.write_all(&[current_byte])?; current_byte = 0_u8; bit = 0; }; } if bit != 0 { self.writer.write_all(&[current_byte])?; } Ok(()) } } /// All errors that can occur when attempting to parse an OTB image #[derive(Debug, Clone)] enum DecoderError { /// Info field in OTB image headers is unsupported UnsupportedInfoField(u8), /// Width in OTB image headers is zero WidthZero, /// Height in OTB image headers is zero HeightZero, /// Specified color depth is not supported UnsupportedColorDepth(u8), } impl Display for DecoderError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::UnsupportedInfoField(info) => { f.write_fmt(format_args!("Unsupported value in info field {info:08b}")) } Self::WidthZero => f.write_fmt(format_args!("Width cannot be zero")), Self::HeightZero => f.write_fmt(format_args!("Height cannot be zero")), Self::UnsupportedColorDepth(depth) => f.write_fmt(format_args!( "Unsupported color depth value in headers {depth}" )), } } } impl std::error::Error for DecoderError {} impl From for ImageError { fn from(e: DecoderError) -> ImageError { ImageError::Decoding(DecodingError::new(ImageFormatHint::Name("otb".into()), e)) } } /// Decoder for Otb images. pub struct OtbDecoder { reader: R, dimensions: (u32, u32), } impl OtbDecoder where R: BufRead + Seek, { /// Create a new `OtbDecoder`. pub fn new(reader: R) -> Result, ImageError> { let mut decoder = Self::new_decoder(reader); decoder.read_metadata()?; Ok(decoder) } fn new_decoder(reader: R) -> OtbDecoder { Self { reader, dimensions: (0, 0), } } fn read_metadata(&mut self) -> Result<(), ImageError> { let mut header_buf = [0_u8; 4]; self.reader.read_exact(&mut header_buf)?; let [info_field, width, height, depth] = header_buf; // InfoField - 00 for single byte width/height values if info_field != 0 { return Err(DecoderError::UnsupportedInfoField(info_field).into()); } // Width if width == 0 { return Err(DecoderError::WidthZero.into()); } // Height if height == 0 { return Err(DecoderError::HeightZero.into()); } // Depth if depth != 1 { return Err(DecoderError::UnsupportedColorDepth(depth).into()); } self.dimensions = (width as u32, height as u32); Ok(()) } } impl ImageDecoder for OtbDecoder { fn dimensions(&self) -> (u32, u32) { self.dimensions } fn color_type(&self) -> ColorType { ColorType::L8 } fn original_color_type(&self) -> ExtendedColorType { ExtendedColorType::L1 } fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { let (width, height) = (self.dimensions.0 as usize, self.dimensions.1 as usize); assert_eq!(buf.len(), width * height, "Invalid buffer length"); // Read entire image data into a buffer let mut byte_buf = vec![0_u8; (width * height).div_ceil(8)].into_boxed_slice(); self.reader.read_exact(&mut byte_buf)?; // Set a byte in buf for every bit in the image data for (i, &byte) in byte_buf.iter().enumerate() { for bit in 0..8 { let buf_idx = 8 * i + bit; if buf_idx >= buf.len() { break; } buf[buf_idx] = if (byte >> (7 - bit)) & 1 == 0 { 0xFF } else { 0x00 }; } } Ok(()) } fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { (*self).read_image(buf) } } mod test { #[test] fn test_decode_image() { use image::ImageDecoder; use std::io::Cursor; let otb_data = vec![ // Headers 0x00, 0x48, 0x1C, 0x01, // End Headers // Image Data 0x7F, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xFB, 0xFF, 0xFE, 0x40, 0x3F, 0xE8, 0x38, 0x2F, 0xFF, 0xFB, 0xFF, 0xFE, 0x48, 0x3F, 0xA8, 0x38, 0x2F, 0x9F, 0xFB, 0xFF, 0xFE, 0x4C, 0xFF, 0xA9, 0xFF, 0x2F, 0x8F, 0xFA, 0xDA, 0xDA, 0x4E, 0xFF, 0x29, 0x01, 0x2F, 0x80, 0xFA, 0x52, 0x52, 0x5E, 0x7F, 0x69, 0x31, 0x2F, 0xBF, 0x7B, 0x07, 0x06, 0x4F, 0xFF, 0x69, 0x79, 0x2F, 0xBE, 0xFB, 0x77, 0x76, 0x47, 0xFF, 0x69, 0x79, 0x2F, 0xBE, 0x7B, 0x07, 0x06, 0x47, 0xFE, 0xEF, 0x7D, 0xEF, 0xBE, 0x7B, 0xFF, 0xFE, 0x47, 0xFC, 0xEF, 0x7D, 0xE7, 0xBC, 0xF1, 0xFF, 0xFC, 0x40, 0xF0, 0xEF, 0x7D, 0xE7, 0x7C, 0xF1, 0xED, 0xBC, 0x21, 0xE7, 0xC9, 0x79, 0x27, 0x98, 0xF1, 0xE5, 0x3C, 0x21, 0xE7, 0xC9, 0x39, 0x27, 0xC8, 0xF1, 0xF0, 0x7C, 0x16, 0x6F, 0x89, 0x39, 0x23, 0xE6, 0xE0, 0xF7, 0x78, 0x15, 0x2F, 0x88, 0x82, 0x23, 0xF3, 0xE0, 0xF0, 0x78, 0x08, 0x3F, 0x04, 0x44, 0x43, 0xD7, 0xE0, 0xFF, 0xF8, 0x04, 0x3E, 0x02, 0x28, 0x81, 0xEF, 0xC0, 0x7F, 0xF0, 0x02, 0x3C, 0x01, 0x39, 0x00, 0xFF, 0x80, 0x3F, 0xE0, 0x01, 0x38, 0x00, 0xBA, 0x00, 0x7F, 0x00, 0x1F, 0xC0, 0x00, 0xF0, 0x00, 0x7C, 0x00, 0x3E, 0x00, 0x0F, 0x80, 0xFF, 0xC0, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x07, 0xFF, 0x55, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAA, 0x2A, 0xF3, 0x87, 0x87, 0x3F, 0x1E, 0x67, 0x0F, 0x54, 0x15, 0xF3, 0x93, 0x9F, 0x3E, 0x4E, 0x27, 0x27, 0xA8, 0x2A, 0xF3, 0x87, 0x8F, 0x3E, 0x4E, 0x07, 0x27, 0x54, 0x55, 0xF3, 0x93, 0x9F, 0x3E, 0x0E, 0x47, 0x27, 0xAA, 0xFF, 0xF3, 0x9B, 0x87, 0x0E, 0x4E, 0x67, 0x0F, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // End Image Data ]; let decoder = crate::otb::OtbDecoder::new(Cursor::new(otb_data)).unwrap(); let (width, height) = decoder.dimensions(); assert!(width == 0x48); assert!(height == 0x1C); let mut img_bytes = vec![0; 2016]; decoder.read_image(&mut img_bytes).unwrap(); } #[test] fn test_decoder_irregular_width() { use image::ImageDecoder; use std::io::Cursor; let expected_data = [ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // row0 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, // row1 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, // row2 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, // row3 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, // row4 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, // row5 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, // row6 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, // row7 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, // row8 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // row9 ]; #[allow(clippy::unusual_byte_groupings)] let image_data: [u8; 17] = [ 0, 10, 10, 1, // headers 0b_00000000, 0b00_000011, 0b0000_0001, 0b001000_00, 0b10000100_, 0b_01000000, 0b10_010000, 0b0010_0010, 0b000100_00, 0b01001000_, 0b_00001100, 0b00_000000, 0b0000_0000, ]; let decoder = crate::otb::OtbDecoder::new(Cursor::new(image_data)).unwrap(); let (width, height) = decoder.dimensions(); assert!(width == 10); assert!(height == 10); let mut img_bytes = vec![0; 100]; decoder.read_image(&mut img_bytes).unwrap(); img_bytes.iter().enumerate().for_each(|(i, byte)| { assert_eq!(*byte, expected_data[i]); }); } #[test] fn test_decoder() { use image::ImageDecoder; use std::io::Cursor; let expected_data = [ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, // row1 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, // row2 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, // row3 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // row4 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // row5 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, // row6 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, // row7 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, // row8 ]; let image_data: [u8; 12] = [ 0, 8, 8, 1, // headers 0b00011000, // row1 0b00100100, // row2 0b01000010, // row3 0b10000001, // row4 0b10000001, // row5 0b01000010, // row6 0b00100100, // row7 0b00011000, // row8 ]; let decoder = crate::otb::OtbDecoder::new(Cursor::new(image_data)).unwrap(); let (width, height) = decoder.dimensions(); assert!(width == 8); assert!(height == 8); let mut img_bytes = vec![0; 64]; decoder.read_image(&mut img_bytes).unwrap(); img_bytes.iter().enumerate().for_each(|(i, byte)| { assert_eq!(*byte, expected_data[i]); }); } #[test] fn test_encoder() { use image::ImageEncoder; let img_data = [ 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, // row1 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, // row2 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, // row3 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // row4 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, // row5 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, // row6 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, // row7 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, // row8 ]; let expected_data: [u8; 12] = [ 0, 8, 8, 1, // headers 0b00011000, // row1 0b00100100, // row2 0b01000010, // row3 0b10000001, // row4 0b10000001, // row5 0b01000010, // row6 0b00100100, // row7 0b00011000, // row8 ]; let mut encoded_data = Vec::::with_capacity(expected_data.len()); let encoder = crate::otb::OtbEncoder::new(&mut encoded_data); encoder .write_image(&img_data, 8, 8, image::ExtendedColorType::L8) .unwrap(); encoded_data.iter().enumerate().for_each(|(i, byte)| { assert_eq!(*byte, expected_data[i]); }); } #[test] fn test_encoder_irregular_width() { use image::ImageEncoder; let img_data = [ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // row0 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, // row1 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, // row2 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, // row3 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, // row4 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, // row5 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, // row6 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, // row7 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, // row8 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // row9 ]; #[allow(clippy::unusual_byte_groupings)] let expected_data: [u8; 17] = [ 0, 10, 10, 1, // headers 0b_00000000, 0b00_000011, 0b0000_0001, 0b001000_00, 0b10000100_, 0b_01000000, 0b10_010000, 0b0010_0010, 0b000100_00, 0b01001000_, 0b_00001100, 0b00_000000, 0b0000_0000, ]; let mut encoded_data = Vec::::with_capacity(expected_data.len()); let encoder = crate::otb::OtbEncoder::new(&mut encoded_data); encoder .write_image(&img_data, 10, 10, image::ExtendedColorType::L8) .unwrap(); encoded_data.iter().enumerate().for_each(|(i, byte)| { assert_eq!(*byte, expected_data[i]); }); } } image-extras-0.1.0/src/pcx.rs000064400000000000000000000102351046102023000141400ustar 00000000000000//! Decoding of PCX Images //! //! PCX (PiCture eXchange) Format is an obsolete image format from the 1980s. //! //! # Related Links //! * - The PCX format on Wikipedia use std::io::{self, BufRead, Read, Seek}; use std::iter; use image::{ColorType, ExtendedColorType, ImageDecoder, ImageError, ImageResult}; /// Decoder for PCX images. pub struct PCXDecoder where R: Read, { dimensions: (u32, u32), inner: pcx::Reader, } impl PCXDecoder where R: BufRead + Seek, { /// Create a new `PCXDecoder`. pub fn new(r: R) -> Result, ImageError> { let inner = pcx::Reader::new(r).map_err(convert_pcx_decode_error)?; let dimensions = (u32::from(inner.width()), u32::from(inner.height())); Ok(PCXDecoder { dimensions, inner }) } } fn convert_pcx_decode_error(err: io::Error) -> ImageError { ImageError::IoError(err) } impl ImageDecoder for PCXDecoder { fn dimensions(&self) -> (u32, u32) { self.dimensions } fn color_type(&self) -> ColorType { ColorType::Rgb8 } fn original_color_type(&self) -> ExtendedColorType { if self.inner.is_paletted() { return ExtendedColorType::Unknown(self.inner.header.bit_depth); } match ( self.inner.header.number_of_color_planes, self.inner.header.bit_depth, ) { (1, 1) => ExtendedColorType::L1, (1, 2) => ExtendedColorType::L2, (1, 4) => ExtendedColorType::L4, (1, 8) => ExtendedColorType::L8, (3, 1) => ExtendedColorType::Rgb1, (3, 2) => ExtendedColorType::Rgb2, (3, 4) => ExtendedColorType::Rgb4, (3, 8) => ExtendedColorType::Rgb8, (4, 1) => ExtendedColorType::Rgba1, (4, 2) => ExtendedColorType::Rgba2, (4, 4) => ExtendedColorType::Rgba4, (4, 8) => ExtendedColorType::Rgba8, (_, _) => unreachable!(), } } fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); let height = self.inner.height() as usize; let width = self.inner.width() as usize; match self.inner.palette_length() { // No palette to interpret, so we can just write directly to buf None => { for i in 0..height { let offset = i * 3 * width; self.inner .next_row_rgb(&mut buf[offset..offset + (width * 3)]) .map_err(convert_pcx_decode_error)?; } } // We need to convert from the palette colours to RGB values inline, // but the pcx crate can't give us the palette first. Work around it // by taking the paletted image into a buffer, then converting it to // RGB8 after. Some(palette_length) => { let mut pal_buf: Vec = iter::repeat_n(0, height * width).collect(); for i in 0..height { let offset = i * width; self.inner .next_row_paletted(&mut pal_buf[offset..offset + width]) .map_err(convert_pcx_decode_error)?; } let mut palette: Vec = std::iter::repeat_n(0, 3 * palette_length as usize).collect(); self.inner .read_palette(&mut palette[..]) .map_err(convert_pcx_decode_error)?; for i in 0..height { for j in 0..width { let pixel = pal_buf[i * width + j] as usize; let offset = i * width * 3 + j * 3; buf[offset] = palette[pixel * 3]; buf[offset + 1] = palette[pixel * 3 + 1]; buf[offset + 2] = palette[pixel * 3 + 2]; } } } } Ok(()) } fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { (*self).read_image(buf) } } image-extras-0.1.0/src/sgi.rs000064400000000000000000000641531046102023000141400ustar 00000000000000/*! Decoding of SGI Image File Format (.rgb) * * The SGI Image File format (often referred to as .rgb) is an obsolete * file format which has uncompressed and run-length encoded modes, * supports a variable number of color channels, and both 8-bit and * 16-bit precisions. * * This decoder does not support: * - Images with ≥ 5 color channels (zsize). (While theoretically supported * by the format, we haven't found any existing images that do this.) * * - Colormaps: Not supported, only the NORMAL=0 mode. The other operations * (DITHERED=1, SCREEN=2, COLORMAP=3) were obsolete at the time the spec * was written. * * The format specification does not explain how the alpha channel is to * be interpreted. Existing decoders appear to assume straight alpha, like * PNG. * * Specification: * - * - * */ use core::ffi::CStr; use std::fmt; use std::io::BufRead; use image::error::{ DecodingError, ImageError, ImageFormatHint, ImageResult, LimitError, LimitErrorKind, }; use image::{ColorType, ExtendedColorType, ImageDecoder, LimitSupport, Limits}; /// The length of the SGI .rgb file header, including padding const HEADER_FULL_LENGTH: usize = 512; /// Important information from an SGI Image header. /// This _excludes_: /// - The name field (returned separately) /// - the pixmin/pixmax fields, which different encoders generate /// inconsistently and are easy to compute if needed /// - colormap field: only colormap NORMAL=0 is accepted, not obsolete modes /// #[derive(Clone, Copy)] struct SgiRgbHeaderInfo { xsize: u16, ysize: u16, color_type: ColorType, is_rle: bool, } /// Errors which can occur while parsing an SGI .rgb file #[derive(Debug)] enum SgiRgbDecodeError { BadMagic, HeaderError, RLERowInvalid(u32, u32), // Invalid RLE row specification UnexpectedColormap(u32), UnexpectedZSize(u16), ZeroSize(u16, u16, u16), ScanlineOverflow, ScanlineUnderflow, ScanlineTooShort, ScanlineTooLong, InvalidName, EarlyEOF, } impl fmt::Display for SgiRgbDecodeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::BadMagic => f.write_str("Incorrect magic bytes, not an SGI image file"), Self::HeaderError => f.write_str("Error parsing header"), Self::UnexpectedColormap(code) => { f.write_fmt(format_args!("Unexpected color map value ({})", code)) } Self::UnexpectedZSize(dim) => { f.write_fmt(format_args!("Unexpected Z dimension is >= 5 ({})", dim)) } Self::ZeroSize(x, y, z) => f.write_fmt(format_args!( "Image has zero size: x*y*z={}*{}*{}=0", x, y, z )), Self::RLERowInvalid(offset, length) => { f.write_fmt(format_args!("Bad RLE row info (offset={}, length={}); empty or not in image data region", offset, length)) } Self::InvalidName => { f.write_str("Invalid name field (not null terminated or not ASCII)") } Self::ScanlineOverflow => { f.write_str("An RLE-encoded scanline contained more data than the image width") } Self::ScanlineUnderflow => { f.write_str("An RLE-encoded scanline contained less data than the image width") } Self::ScanlineTooShort => { f.write_str("An RLE-encoded scanline stopped (reached a zero counter) before its stated length") } Self::ScanlineTooLong => { f.write_str("An RLE-encoded scanline did not stop at its stated length (missing trailing zero counter).") } Self::EarlyEOF => { f.write_str("File ended before all scanlines were read.") } } } } impl std::error::Error for SgiRgbDecodeError {} impl From for ImageError { fn from(e: SgiRgbDecodeError) -> ImageError { ImageError::Decoding(DecodingError::new(ImageFormatHint::Name("SGI".into()), e)) } } /// Parse the image header, returning key metadata and the name field if successful fn parse_header( header: &[u8; HEADER_FULL_LENGTH], ) -> Result<(SgiRgbHeaderInfo, &CStr), SgiRgbDecodeError> { if header[..2] != *b"\x01\xda" { return Err(SgiRgbDecodeError::BadMagic); } let storage = header[2]; let bpc = header[3]; let dimension = u16::from_be_bytes(header[4..6].try_into().unwrap()); let xsize = u16::from_be_bytes(header[6..8].try_into().unwrap()); let ysize = u16::from_be_bytes(header[8..10].try_into().unwrap()); let zsize = u16::from_be_bytes(header[10..12].try_into().unwrap()); let _pixmin = u32::from_be_bytes(header[12..16].try_into().unwrap()); let _pixmax = u32::from_be_bytes(header[16..20].try_into().unwrap()); /* Dummy bytes -- formerly, this was a 'wastebytes' field, so old * images may not set this to zero. */ let _dummy1 = u32::from_be_bytes(header[20..24].try_into().unwrap()); let imagename: &[u8] = &header[24..104]; let colormap = u32::from_be_bytes(header[104..108].try_into().unwrap()); /* More dummy bytes -- old libraries appear to have used the same struct * for header data and, following it, internal image parsing state; and * when writing the header just wrote whatever was in the struct. As a * result, the following bytes may contain random counters, pointer data, * etc. in old images. Recently (post 2000) written encoders tend to * follow the file format specification and write zeros here. */ let _dummy2 = &header[108..]; if storage >= 2 { return Err(SgiRgbDecodeError::HeaderError); } if bpc == 0 { return Err(SgiRgbDecodeError::HeaderError); } if colormap != 0 { return Err(SgiRgbDecodeError::UnexpectedColormap(colormap)); } // Header fields `ysize`/`zsize` are only validated if used let (xsize, ysize, zsize) = match dimension { 0 | 4.. => { return Err(SgiRgbDecodeError::HeaderError); } 1 => (xsize, 1, 1), 2 => (xsize, ysize, 1), 3 => (xsize, ysize, zsize), }; if xsize == 0 || ysize == 0 || zsize == 0 { return Err(SgiRgbDecodeError::ZeroSize(xsize, ysize, zsize)); } let name = CStr::from_bytes_until_nul(imagename).map_err(|_| SgiRgbDecodeError::InvalidName)?; if name.to_bytes().iter().any(|x| !x.is_ascii()) { return Err(SgiRgbDecodeError::InvalidName); } let color_type = match (bpc, zsize) { (1, 1) => ColorType::L8, (1, 2) => ColorType::La8, (1, 3) => ColorType::Rgb8, (1, 4) => ColorType::Rgba8, (2, 1) => ColorType::L16, (2, 2) => ColorType::La16, (2, 3) => ColorType::Rgb16, (2, 4) => ColorType::Rgba16, _ => { if zsize >= 5 { return Err(SgiRgbDecodeError::UnexpectedZSize(zsize)); } else { return Err(SgiRgbDecodeError::HeaderError); } } }; Ok(( SgiRgbHeaderInfo { is_rle: storage == 1, xsize, ysize, color_type, }, name, )) } /// Decoder for SGI (.rgb) images. pub struct SgiDecoder { info: SgiRgbHeaderInfo, reader: R, } impl SgiDecoder where R: BufRead, { /// Create a new `SgiDecoder`. Assumes `r` starts at seek position 0. pub fn new(mut r: R) -> Result, ImageError> { let mut header = [0u8; HEADER_FULL_LENGTH]; r.read_exact(&mut header)?; let (header_info, _name) = parse_header(&header)?; Ok(SgiDecoder { info: header_info, reader: r, }) } } /** State associated with the processing of a scan line */ #[derive(Clone, Copy)] struct SgiRgbScanlineState { /// Offset of the encoded row in the input file offset: u32, /// Length of the encoded row in the file, including terminator length: u32, /// Current row number in 0..ysize row_id: u16, /// Current position to write in the current image row, range 0..=xsize position: u16, /// Index of the preceding line which contains the current file position /// (or u32::MAX if there is no such line.) Other than u32::MAX, values /// range from 0 to 2^18 - 1 prec_active_line: u32, /// Color plane of row. Values in 0..4 plane: u8, /// The most recently read counter byte; for copy operations, this may be /// modified as the copy progresses counter: u8, /// If 16-bit depth used, last high byte read data_char_hi: u8, /// If true, will next read a counter field at_counter: bool, /// If true, will next read a high byte high_byte: bool, } /** State associated with the processing of the image data for RLE type images */ struct SgiRgbDecodeState { /// Number of bytes in the stream read so far, including header. In practice, /// this can be as large as the max end of a valid encoded scanline (2^32 + 2^18 - 3) pos: u64, /// Maximum index of a row which contains pos, if one exists. max_active_row: Option, /// Index of the next row in the sequence which has offset >= pos, or rle_table.len() if no such row cursor: usize, } /** Apply a byte of the input to a scanline state machine. * Returns `Ok(true)` if this byte completes an end of line marker. * Returns Err(true) on line overflow, Err(false) on line underflow. */ fn apply_rle16_byte( row: &mut SgiRgbScanlineState, buf_row: &mut [u8], byte: u8, xsize: u16, channels: u8, ) -> Result { let mut eor = false; if row.high_byte { row.data_char_hi = byte; row.high_byte = false; } else { if row.at_counter { row.counter = byte; row.at_counter = false; let count = row.counter & (!0x80); if count == 0 { // End of line if row.position != xsize { return Err(false); } else { eor = true; } } } else { let data = u16::from_be_bytes([row.data_char_hi, byte]); let mut count = row.counter & (!0x80); /* Have checked that count != 0 */ if count == 0 { unreachable!(); } else if row.counter & 0x80 == 0 { // Expand data to `count` repeated u16s let overflows_row = (count as u16) > xsize || row.position > xsize - (count as u16); if overflows_row { return Err(true); } // The calculated write_start should be <= buf.len() and hence not overflow let write_start: usize = (row.position as usize) * (channels as usize); for i in 0..count { let o = write_start + (row.plane as usize) + (channels as usize) * (i as usize); buf_row[2 * o..2 * o + 2].copy_from_slice(&u16::to_ne_bytes(data)); } row.position += count as u16; row.at_counter = true; } else { // Copy the next `count` u16s of data let overflows_row = row.position > xsize - 1; if overflows_row { return Err(true); } let write_start: usize = (row.position as usize) * (channels as usize) + (row.plane as usize); buf_row[2 * write_start..2 * write_start + 2] .copy_from_slice(&u16::to_ne_bytes(data)); count -= 1; row.position += 1; row.counter = 0x80 | count; row.at_counter = count == 0; } } row.high_byte = !row.high_byte; } Ok(eor) } /** Apply a byte of the input to a scanline state machine. * Returns Ok(true) if this byte completes an end of line marker. * Returns Err(true) on line overflow, Err(false) on line underflow. */ fn apply_rle8_byte( row: &mut SgiRgbScanlineState, buf_row: &mut [u8], byte: u8, xsize: u16, channels: u8, ) -> Result { let mut eor = false; if row.at_counter { row.counter = byte; row.at_counter = false; let count = row.counter & (!0x80); if count == 0 { // End of line if row.position != xsize { return Err(false); } else { eor = true; } } } else { let mut count = row.counter & (!0x80); /* Have checked that count != 0 */ if count == 0 { unreachable!(); } else if row.counter & 0x80 == 0 { // Expand data to `count` repeated u16s let overflows_row = (count as u16) > xsize || row.position > xsize - (count as u16); if overflows_row { return Err(true); } // The calculated write_start should be <= buf.len() and hence not overflow let write_start: usize = (row.position as usize) * (channels as usize); for i in 0..count { let o = write_start + (row.plane as usize) + (channels as usize) * (i as usize); buf_row[o] = byte; } row.position += count as u16; row.at_counter = true; } else { // Copy the next `count` u8s of data let overflows_row = row.position > xsize - 1; if overflows_row { return Err(true); } let write_start: usize = (row.position as usize) * (channels as usize) + (row.plane as usize); buf_row[write_start] = byte; count -= 1; row.position += 1; row.counter = 0x80 | count; row.at_counter = count == 0; } } Ok(eor) } /** For the given RLE scanline, process the bytes in `segment`. * Parameter `ending` is true iff this segment ends the scanline. */ fn process_rle_segment( buf_row: &mut [u8], row: &mut SgiRgbScanlineState, xsize: u16, channels: u8, segment: &[u8], ending: bool, ) -> Result<(), SgiRgbDecodeError> { for (i, byte) in segment.iter().enumerate() { let res = if DEEP { apply_rle16_byte(row, buf_row, *byte, xsize, channels) } else { apply_rle8_byte(row, buf_row, *byte, xsize, channels) }; let eor = res.map_err(|overflow| { if overflow { SgiRgbDecodeError::ScanlineOverflow } else { SgiRgbDecodeError::ScanlineUnderflow } })?; let expecting_end = i + 1 == segment.len() && ending; if eor != expecting_end { if eor { // Row ended earlier than expected return Err(SgiRgbDecodeError::ScanlineTooShort); } else { // At end of scanline data, did not process a row end marker return Err(SgiRgbDecodeError::ScanlineTooLong); } } } Ok(()) } /** Process the next region of the RLE-encoded image data section of the SGI image file */ fn process_data_segment( buf: &mut [u8], info: SgiRgbHeaderInfo, mut state: SgiRgbDecodeState, rle_table: &mut [SgiRgbScanlineState], segment: &[u8], ) -> Result<(SgiRgbDecodeState, bool), SgiRgbDecodeError> { let channels = info.color_type.channel_count(); let start_pos = state.pos; let end_pos = state.pos + segment.len() as u64; // Add new encoded lines that intersect `segment` to the active set while state.cursor < rle_table.len() && rle_table[state.cursor].offset as u64 <= end_pos { rle_table[state.cursor].prec_active_line = state.max_active_row.unwrap_or(u32::MAX); state.max_active_row = Some(state.cursor as u32); state.cursor += 1; } let mut prev_row: Option = None; let Some(mut row_index) = state.max_active_row else { // No rows are active, nothing to do state.pos = end_pos; return Ok((state, false)); }; loop { let row = &mut rle_table[row_index as usize]; let row_end = row.offset as u64 + row.length as u64; debug_assert!(row.offset as u64 <= end_pos && row_end > start_pos); let prev_active_row = row.prec_active_line; // Intersect the segment being processed with the row extents, and then // run the RLE state machine over the resulting intersected segment let rs_start: usize = if row.offset as u64 > start_pos { ((row.offset as u64) - start_pos) as usize } else { 0 }; let rs_end: usize = if row_end <= end_pos { (row_end - start_pos) as usize } else { segment.len() }; let has_ending = row_end <= end_pos; let row_input = &segment[rs_start..rs_end]; let bpp = if DEEP { 2 } else { 1 }; let stride = (info.xsize as usize) * (channels as usize) * bpp; let buf_row = &mut buf[((info.ysize - 1 - row.row_id) as usize) * stride ..((info.ysize - 1 - row.row_id) as usize) * stride + stride]; process_rle_segment::(buf_row, row, info.xsize, channels, row_input, has_ending)?; if has_ending { // Mark this row as not having a preceding row, and remove // it from the singly linked list row.prec_active_line = u32::MAX; if let Some(r) = prev_row { rle_table[r as usize].prec_active_line = prev_active_row; } else if prev_active_row == u32::MAX { state.max_active_row = None; } else { state.max_active_row = Some(prev_active_row); } } else { prev_row = Some(row_index); } if prev_active_row != u32::MAX { row_index = prev_active_row; } else { break; } } state.pos = end_pos; let done = state.max_active_row.is_none() && state.cursor == rle_table.len(); Ok((state, done)) } impl ImageDecoder for SgiDecoder { fn dimensions(&self) -> (u32, u32) { (self.info.xsize as u32, self.info.ysize as u32) } fn color_type(&self) -> ColorType { self.info.color_type } fn original_color_type(&self) -> ExtendedColorType { self.info.color_type.into() } fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); let channels = self.info.color_type.channel_count(); let deep = self.info.color_type.bytes_per_pixel() > channels; if self.info.is_rle { /* Tricky case: need to read the RLE offset tables to determine * where to find the scanlines for each plane. Images can place * the scanlines whereever, and processing them in logical order * can require a lot of seeking. * * This decoder does not have fine grained control over the * input stream buffering, so each seek operation could make * the input stream read 8 kB and/or make a syscall if the * input stream is a default BufReader. An image could trigger * one seek operation per 8 marginal bytes of data. To avoid * this unfortunate interaction, we load the O(height) bytes * of RLE tables into memory and then process the rest of the * image in a single pass. */ /* `rle_offset_entries` has maximum value `4 * (2^16-1)` and will not overflow */ let rle_offset_entries = (channels as u32) * (self.info.ysize as u32); let mut rle_table: Vec = Vec::new(); /* Tiny invalid images can trigger medium-size 4 * (2^16-1) allocations here; * this can be avoided by dynamically resizing the vector as it is read. */ rle_table .try_reserve_exact(rle_offset_entries as usize) .map_err(|_| ImageError::Limits(LimitErrorKind::InsufficientMemory.into()))?; rle_table.resize( rle_offset_entries as usize, SgiRgbScanlineState { offset: 0, length: 0, row_id: 0, position: 0, plane: 0, counter: 0, data_char_hi: 0, prec_active_line: 0, high_byte: deep, at_counter: true, }, ); // Read offset table for plane in 0..channels { for y in 0..self.info.ysize { let idx = (plane as usize) * (self.info.ysize as usize) + (y as usize); let mut tmp = [0u8; 4]; self.reader.read_exact(&mut tmp)?; rle_table[idx].offset = u32::from_be_bytes(tmp); rle_table[idx].row_id = y; rle_table[idx].plane = plane; } } // Read length table, and validate (offset, length) pairs for plane in 0..channels { for y in 0..self.info.ysize { let idx = (plane as usize) * (self.info.ysize as usize) + (y as usize); let mut tmp = [0u8; 4]; self.reader.read_exact(&mut tmp)?; rle_table[idx].length = u32::from_be_bytes(tmp); // Per spec, image data follows the offset tables. // (although other decoders will probably read inside the offset tables // if asked.) let scanline_too_early = rle_table[idx].offset < (HEADER_FULL_LENGTH as u32) + rle_offset_entries * 8; let zero_length = rle_table[idx].length == 0; if scanline_too_early || zero_length { return Err(SgiRgbDecodeError::RLERowInvalid( rle_table[idx].offset, rle_table[idx].length, ) .into()); } } } // Sort rows by their starting position in the stream, breaking // ties by their position in the buffer. rle_table.sort_unstable_by_key(|f| { (f.offset as u64) << 32 | (f.row_id as u64) << 16 | f.plane as u64 }); // Use explicit state for linear processing let mut rle_state = SgiRgbDecodeState { cursor: 0, max_active_row: None, pos: (HEADER_FULL_LENGTH as u64) + (rle_table.len() as u64) * 8, }; loop { let buffer = self.reader.fill_buf()?; if buffer.is_empty() { /* Unexpected EOF . */ return Err(SgiRgbDecodeError::EarlyEOF.into()); } let (new_state, done) = if deep { process_data_segment::(buf, self.info, rle_state, &mut rle_table, buffer)? } else { process_data_segment::( buf, self.info, rle_state, &mut rle_table, buffer, )? }; if done { return Ok(()); } rle_state = new_state; let buffer_length = buffer.len(); self.reader.consume(buffer_length); } } else { /* Easy case: packed images by scanline, plane by plane */ if deep { let bpp = 2 * channels; // `width` will be at most `(2^16-1) * 8`, so there is never overflow let width = (bpp as u32) * (self.info.xsize as u32); for plane in 0..channels as usize { for row in buf.chunks_exact_mut(width as usize).rev() { for px in row.chunks_exact_mut(bpp as usize) { let mut tmp = [0_u8; 2]; self.reader.read_exact(&mut tmp)?; px[2 * plane..2 * plane + 2] .copy_from_slice(&u16::to_ne_bytes(u16::from_be_bytes(tmp))); } } } } else { let width = (channels as u32) * (self.info.xsize as u32); for plane in 0..channels as usize { for row in buf.chunks_exact_mut(width as usize).rev() { for px in row.chunks_exact_mut(channels as usize) { self.reader.read_exact(&mut px[plane..plane + 1])?; } } } } Ok(()) } } fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { (*self).read_image(buf) } fn set_limits(&mut self, limits: Limits) -> ImageResult<()> { limits.check_support(&LimitSupport::default())?; let (width, height) = self.dimensions(); limits.check_dimensions(width, height)?; // This will not overflow, because 8 * (2^16-1) * (2^16-1) ≤ 2^35 let max_image_bytes = 8 * (self.info.xsize as u64) * (self.info.ysize as u64); // This will not overflow, because even 1 KB * (2^16-1) ≤ 2^35 ⪡ 2^64-1 let max_table_bytes = (self.info.ysize as u64) * (std::mem::size_of::() as u64); // This will not overflow, because it is ≤ 2^36 ⪡ 2^64-1 let max_bytes = max_image_bytes + max_table_bytes; let max_alloc = limits.max_alloc.unwrap_or(u64::MAX); if max_alloc < max_bytes { return Err(ImageError::Limits(LimitError::from_kind( LimitErrorKind::InsufficientMemory, ))); } Ok(()) } } image-extras-0.1.0/src/wbmp.rs000064400000000000000000000077131046102023000143220ustar 00000000000000//! Encoding and Decoding of WBMP Images //! //! WBMP (Wireless BitMaP) Format is an image format used by the WAP protocol. //! //! # Related Links //! * - The WBMP format on Wikipedia //! * - The WAP Specification use std::io::{BufRead, Seek, Write}; use image::error::{ DecodingError, EncodingError, ImageFormatHint, UnsupportedError, UnsupportedErrorKind, }; use image::{ColorType, ExtendedColorType, ImageDecoder, ImageEncoder, ImageError, ImageResult}; /// Encoder for Wbmp images. pub struct WbmpEncoder { writer: W, threshold: u8, } impl WbmpEncoder { pub fn new(writer: W) -> Self { Self { writer, threshold: 127_u8, } } pub fn with_threshold(mut self, threshold: u8) -> Self { self.threshold = threshold; self } } impl ImageEncoder for WbmpEncoder { fn write_image( mut self, buf: &[u8], width: u32, height: u32, color_type: ExtendedColorType, ) -> std::result::Result<(), ImageError> { let color = match color_type { ExtendedColorType::L8 => wbmp::ColorType::Luma8, ExtendedColorType::Rgba8 => wbmp::ColorType::Rgba8, _ => { return Err(ImageError::Encoding(EncodingError::new( format_hint(), "Unsupported ColorType".to_string(), ))); } }; wbmp::Encoder::new(&mut self.writer) .with_threshold(self.threshold) .encode(buf, width, height, color) .map_err(convert_wbmp_error) } } /// Decoder for Wbmp images. pub struct WbmpDecoder { dimensions: (u32, u32), inner: wbmp::Decoder, } impl WbmpDecoder where R: BufRead + Seek, { /// Create a new `WbmpDecoder`. pub fn new(r: R) -> Result, ImageError> { let inner = wbmp::Decoder::new(r).map_err(convert_wbmp_error)?; let dimensions = inner.dimensions(); Ok(WbmpDecoder { dimensions, inner }) } } impl ImageDecoder for WbmpDecoder { fn dimensions(&self) -> (u32, u32) { self.dimensions } fn color_type(&self) -> ColorType { ColorType::L8 } fn original_color_type(&self) -> ExtendedColorType { ExtendedColorType::L1 } fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { let (width, height) = self.dimensions; assert_eq!(buf.len(), (width * height) as usize, "Invalid buffer size"); self.inner.read_image_data(buf).map_err(convert_wbmp_error) } fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { (*self).read_image(buf) } } fn convert_wbmp_error(err: wbmp::error::WbmpError) -> ImageError { use wbmp::error::WbmpError; match err { WbmpError::IoError(inner) => ImageError::IoError(inner), WbmpError::UnsupportedType(inner) => { ImageError::Unsupported(UnsupportedError::from_format_and_kind( format_hint(), UnsupportedErrorKind::GenericFeature(format!( "type {inner} is not supported for wbmp images" )), )) } WbmpError::UnsupportedHeaders => { ImageError::Unsupported(UnsupportedError::from_format_and_kind( format_hint(), UnsupportedErrorKind::GenericFeature( "Extension headers are not supported for wbmp images".to_string(), ), )) } WbmpError::InvalidImageData => ImageError::Encoding(EncodingError::new(format_hint(), err)), WbmpError::UsageError(_) => ImageError::Decoding(DecodingError::new(format_hint(), err)), } } fn format_hint() -> ImageFormatHint { ImageFormatHint::Name("WBMP".to_string()) } image-extras-0.1.0/src/xbm.rs000064400000000000000000000525571046102023000141510ustar 00000000000000//! Decoding of X BitMap (.xbm) Images //! //! XBM (X BitMap) Format is a plain text image format, sometimes used to store //! cursor and icon data. XBM images can be valid C code (although a noticeable //! fraction of historical images includes a name field which is not a valid C //! identifier, and need not even be either of pure-ASCII or UTF-8). //! //! # Related Links //! * - The XBM format specification //! * - The XBM format on wikipedia use std::fmt; use std::io::{BufRead, Bytes}; use image::error::{DecodingError, ImageFormatHint, ParameterError, ParameterErrorKind}; use image::{ColorType, ExtendedColorType, ImageDecoder, ImageError, ImageResult}; /// Location of a byte in the input stream. /// /// Includes byte offset (for format debugging with hex editor) and /// line:column offset (for format debugging with text editor) #[derive(Clone, Copy, Debug)] struct TextLocation { byte: u64, line: u64, column: u64, } /// A peekable reader which tracks location information struct TextReader { inner: R, current: Option, location: TextLocation, } impl TextReader where R: Iterator, { /// Initialize a TextReader fn new(mut r: R) -> TextReader { let current = r.next(); TextReader { inner: r, current, location: TextLocation { byte: 0, line: 1, column: 0, }, } } /// Consume the next byte. On EOF, will return None fn next(&mut self) -> Option { self.current?; let mut current = self.inner.next(); std::mem::swap(&mut self.current, &mut current); self.location.byte += 1; self.location.column += 1; if let Some(b'\n') = current { self.location.line += 1; self.location.column = 0; } current } /// Peek at the next byte. On EOF, will return None fn peek(&self) -> Option { self.current } /// The location of the last byte returned by [Self::next] fn loc(&self) -> TextLocation { self.location } } /// Properties of an XBM image (excluding the rarely useful `name` field.) struct XbmHeaderData { width: u32, height: u32, hotspot: Option<(i32, i32)>, } /// XBM stream decoder (works in no_std, has the natural streaming API for the uncompressed text structure of XBM) /// /// To properly validate the image trailer, invoke `next_byte()` again after reading the last byte of content; if /// the trailer is valid it should return Ok(None). struct XbmStreamDecoder { r: TextReader, current_position: u64, // Note: technically this includes header metadata that isn't _needed_ when parsing header: XbmHeaderData, } /// Helper struct to project BufRead down to Iterator. Costs of this simple /// lifetime-free abstraction include that the struct requires space to store the /// error value, and that code using this must eventually check the error field. struct IoAdapter { reader: Bytes, error: Option, } impl Iterator for IoAdapter where R: BufRead, { type Item = u8; #[inline(always)] fn next(&mut self) -> Option { if self.error.is_some() { return None; } match self.reader.next() { None => None, Some(Ok(v)) => Some(v), Some(Err(e)) => { self.error = Some(e); None } } } } /// XBM decoder (usable wrapper of XbmStreamDecoder that handles IO errors) pub struct XbmDecoder { base: XbmStreamDecoder>, } /// Part of the XBM file in which a parse error occurs #[derive(Debug, Clone, Copy)] enum XbmPart { Width, Height, HotspotX, HotspotY, Array, Data, ArrayEnd, Trailing, } /// Error that can occur while parsing an XBM file #[derive(Debug)] enum XbmDecodeError { Parse(XbmPart, TextLocation), DecodeInteger(XbmPart), ZeroWidth, ZeroHeight, } impl fmt::Display for TextLocation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_fmt(format_args!( "byte={},line={}:col={}", self.byte, self.line, self.column )) } } impl fmt::Display for XbmPart { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { XbmPart::Width => f.write_str("#define for image width"), XbmPart::Height => f.write_str("#define for image height"), XbmPart::HotspotX => f.write_str("#define for hotspot x coordinate"), XbmPart::HotspotY => f.write_str("#define for hotspot y coordinate"), XbmPart::Array => f.write_str("array definition"), XbmPart::Data => f.write_str("array content"), XbmPart::ArrayEnd => f.write_str("array end"), XbmPart::Trailing => f.write_str("end of file"), } } } impl fmt::Display for XbmDecodeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { XbmDecodeError::Parse(part, loc) => f.write_fmt(format_args!( "Failed to parse {}, unexpected character or eof at {}", part, loc )), XbmDecodeError::DecodeInteger(part) => { f.write_fmt(format_args!("Failed to parse integer for {}", part)) } XbmDecodeError::ZeroWidth => f.write_str("Invalid image width: should not be zero"), XbmDecodeError::ZeroHeight => f.write_str("Invalid image height: should not be zero"), } } } impl std::error::Error for XbmDecodeError {} impl From for ImageError { fn from(e: XbmDecodeError) -> ImageError { ImageError::Decoding(DecodingError::new(ImageFormatHint::Name("XBM".into()), e)) } } /// Helper trait for the pattern in which, after calling a function returning a Result, /// one wishes to use an error from a different source. trait XbmDecoderIoInjectionExt { type Value; fn apply_after(self, err: &mut Option) -> Result; } impl XbmDecoderIoInjectionExt for Result { type Value = X; fn apply_after(self, err: &mut Option) -> Result { if let Some(err) = err.take() { return Err(ImageError::IoError(err)); } match self { Self::Ok(x) => Ok(x), Self::Err(e) => Err(ImageError::Decoding(DecodingError::new( ImageFormatHint::Name("XBM".into()), e, ))), } } } /// A limit on the length of a #define symbol (containing `name` + '_width') in an image. /// Names are typically valid C identifiers, and a 255 char limit is common, /// so XBM files exceeding this are unlikely to work anyway. const MAX_IDENTIFIER_LENGTH: usize = 256; /// Read precisely the string `s` from `r`, or error. fn read_fixed_string>( r: &mut TextReader, s: &[u8], part: XbmPart, ) -> Result<(), XbmDecodeError> { for c in s { if let Some(b) = r.next() { if b != *c { return Err(XbmDecodeError::Parse(part, r.loc())); } } else { return Err(XbmDecodeError::Parse(part, r.loc())); }; } Ok(()) } // Read a single byte fn read_byte>( r: &mut TextReader, part: XbmPart, ) -> Result { match r.next() { None => Err(XbmDecodeError::Parse(part, r.loc())), Some(b) => Ok(b), } } /// Read a mixture of ' ' and '\t'. At least one character must be read. // Other whitespace characters are not permitted. fn read_whitespace_gap>( r: &mut TextReader, part: XbmPart, ) -> Result<(), XbmDecodeError> { let b = read_byte(r, part)?; if !(b == b' ' || b == b'\t') { return Err(XbmDecodeError::Parse(part, r.loc())); } while let Some(b) = r.peek() { if b == b' ' || b == b'\t' { r.next(); continue; } else { return Ok(()); } } Ok(()) } /// Read a mixture of ' ', '\t', and '\n'. Other whitespace characters are not permitted. fn read_optional_whitespace>( r: &mut TextReader, ) -> Result<(), XbmDecodeError> { while let Some(b) = r.peek() { if b == b' ' || b == b'\t' || b == b'\n' { r.next(); continue; } else { break; } } Ok(()) } /// Read a mixture of ' ' and '\t', until reading '\n'. fn read_to_newline>( r: &mut TextReader, part: XbmPart, ) -> Result<(), XbmDecodeError> { while let Some(b) = r.peek() { if b == b' ' || b == b'\t' { r.next(); continue; } else { break; } } if read_byte(r, part)? != b'\n' { Err(XbmDecodeError::Parse(part, r.loc())) } else { Ok(()) } } /// Read token into the buffer until the buffer size is exceeded, or ' ' or '\t' or '\n' is found /// Returns the length of the data read. fn read_until_whitespace<'a, R: Iterator>( r: &mut TextReader, buf: &'a mut [u8], part: XbmPart, ) -> Result<&'a [u8], XbmDecodeError> { let mut len = 0; while let Some(b) = r.peek() { if b == b' ' || b == b'\t' || b == b'\n' { return Ok(&buf[..len]); } else { if len >= buf.len() { // identifier is too long return Err(XbmDecodeError::Parse(part, r.loc())); } buf[len] = b; len += 1; r.next(); } } Ok(&buf[..len]) } /// Read a single hex digit, either upper or lower case fn read_hex_digit>( r: &mut TextReader, part: XbmPart, ) -> Result { let b = read_byte(r, part)?; match b { b'0'..=b'9' => Ok(b - b'0'), b'A'..=b'F' => Ok(b - b'A' + 10), b'a'..=b'f' => Ok(b - b'a' + 10), _ => Err(XbmDecodeError::Parse(part, r.loc())), } } /// Read a hex-encoded byte (e.g.: 0xA1) fn read_hex_byte>( r: &mut TextReader, part: XbmPart, ) -> Result { if read_byte(r, part)? != b'0' { return Err(XbmDecodeError::Parse(part, r.loc())); } let x = read_byte(r, part)?; if !(x == b'x' || x == b'X') { return Err(XbmDecodeError::Parse(part, r.loc())); } let mut v = read_hex_digit(r, part)? << 4; v += read_hex_digit(r, part)?; Ok(v) } /// Parse string into signed integer, rejecting leading + and leading zeros /// (i32::from_str_radix accepts '014' as 14, but in C is it octal and has value 12) fn parse_i32(data: &[u8]) -> Option { if data.starts_with(b"-") { (-(parse_u32(&data[1..])? as i64)).try_into().ok() } else { parse_u32(data)?.try_into().ok() } } /// Parse string into unsigned integer, rejecting leading + and leading zeros /// (u32::from_str_radix accepts '014' as 14, but in C is it octal and has value 12) fn parse_u32(data: &[u8]) -> Option { let Some(c1) = data.first() else { // Reject empty string return None; }; if *c1 == b'0' && data.len() > 1 { // Nonzero integers may not have leading zeros return None; } let mut x: u32 = 0; for c in data { if b'0' <= *c && *c <= b'9' { x = x.checked_mul(10)?.checked_add((*c - b'0') as u32)?; } else { return None; } } Some(x) } /// Read the XBM file header up to and including the first opening brace fn read_xbm_header<'a, R: Iterator>( r: &mut TextReader, name_width_buf: &'a mut [u8], ) -> Result<(&'a [u8], XbmHeaderData), XbmDecodeError> { // The header consists of three to five lines. Lines 3-4 may be skipped // In practice, the name may be empty or UTF-8. // // #define _width // #define _height // #define _x_hot // #define _y_hot // static _bits[] = { ... let mut int_buf = [0u8; 11]; // -2^31 and 2^32 fit in 11 bytes // Read width field and acquire name. read_fixed_string(r, b"#define", XbmPart::Width)?; read_whitespace_gap(r, XbmPart::Width)?; let name_width = read_until_whitespace(r, name_width_buf, XbmPart::Width)?; if !name_width.ends_with(b"_width") { return Err(XbmDecodeError::Parse(XbmPart::Width, r.loc())); } let name = &name_width[..name_width.len() - b"_width".len()]; read_whitespace_gap(r, XbmPart::Width)?; let int = read_until_whitespace(r, &mut int_buf, XbmPart::Width)?; read_to_newline(r, XbmPart::Width)?; let width = parse_u32(int).ok_or(XbmDecodeError::DecodeInteger(XbmPart::Width))?; if width == 0 { return Err(XbmDecodeError::ZeroWidth); } // Read height field, checking that the name matches read_fixed_string(r, b"#define", XbmPart::Height)?; read_whitespace_gap(r, XbmPart::Height)?; read_fixed_string(r, name, XbmPart::Height)?; read_fixed_string(r, b"_height", XbmPart::Height)?; read_whitespace_gap(r, XbmPart::Height)?; let int = read_until_whitespace(r, &mut int_buf, XbmPart::Height)?; read_to_newline(r, XbmPart::Height)?; let height = parse_u32(int).ok_or(XbmDecodeError::DecodeInteger(XbmPart::Height))?; if height == 0 { return Err(XbmDecodeError::ZeroHeight); } let hotspot = match r.peek() { Some(b'#') => { // Parse hotspot lines read_fixed_string(r, b"#define", XbmPart::HotspotX)?; read_whitespace_gap(r, XbmPart::HotspotX)?; read_fixed_string(r, name, XbmPart::HotspotX)?; read_fixed_string(r, b"_x_hot", XbmPart::HotspotX)?; read_whitespace_gap(r, XbmPart::HotspotX)?; let int = read_until_whitespace(r, &mut int_buf, XbmPart::HotspotX)?; read_to_newline(r, XbmPart::HotspotX)?; let hotspot_x = parse_i32(int).ok_or(XbmDecodeError::DecodeInteger(XbmPart::HotspotX))?; read_fixed_string(r, b"#define", XbmPart::HotspotY)?; read_whitespace_gap(r, XbmPart::HotspotY)?; read_fixed_string(r, name, XbmPart::HotspotY)?; read_fixed_string(r, b"_y_hot", XbmPart::HotspotY)?; read_whitespace_gap(r, XbmPart::HotspotY)?; let int = read_until_whitespace(r, &mut int_buf, XbmPart::HotspotY)?; read_to_newline(r, XbmPart::HotspotY)?; let hotspot_y = parse_i32(int).ok_or(XbmDecodeError::DecodeInteger(XbmPart::HotspotY))?; Some((hotspot_x, hotspot_y)) } Some(b's') => None, _ => { r.next(); return Err(XbmDecodeError::Parse(XbmPart::Array, r.loc())); } }; read_fixed_string(r, b"static", XbmPart::Array)?; read_whitespace_gap(r, XbmPart::Array)?; match r.peek() { Some(b'c') => { read_fixed_string(r, b"char", XbmPart::Array)?; } Some(b'u') => { read_fixed_string(r, b"unsigned", XbmPart::Array)?; read_whitespace_gap(r, XbmPart::Array)?; read_fixed_string(r, b"char", XbmPart::Array)?; } _ => { r.next(); return Err(XbmDecodeError::Parse(XbmPart::Array, r.loc())); } } read_whitespace_gap(r, XbmPart::Array)?; read_fixed_string(r, name, XbmPart::Array)?; read_fixed_string(r, b"_bits[]", XbmPart::Array)?; read_whitespace_gap(r, XbmPart::Array)?; read_fixed_string(r, b"=", XbmPart::Array)?; read_whitespace_gap(r, XbmPart::Array)?; read_fixed_string(r, b"{", XbmPart::Array)?; Ok(( name, XbmHeaderData { width, height, hotspot, }, )) } impl XbmStreamDecoder where R: Iterator, { /// Create a new `XbmStreamDecoder` or error if the header failed to parse. pub fn new(reader: R) -> Result, (R, XbmDecodeError)> { let mut r = TextReader::new(reader); let mut name_width_buf = [0u8; MAX_IDENTIFIER_LENGTH]; match read_xbm_header(&mut r, &mut name_width_buf) { Err(e) => Err((r.inner, e)), Ok((_name, header)) => Ok(XbmStreamDecoder { r, current_position: 0, header, }), } } /// Read the next byte of the raw image data. The XBM image is organized /// in row major order with rows containing ceil(width / 8) bytes, so that /// the `i`th pixel in a row is the `(i%8)`th least significant /// bit of the `(i/8)`th byte in the row. Bit value 1 = black, 0 = white. pub fn next_byte(&mut self) -> Result, XbmDecodeError> { let data_size = (self.header.width.div_ceil(8) as u64) * (self.header.height as u64); if self.current_position < data_size { let first = self.current_position == 0; self.current_position += 1; if !first { read_optional_whitespace(&mut self.r)?; read_fixed_string(&mut self.r, b",", XbmPart::Data)?; } read_optional_whitespace(&mut self.r)?; Ok(Some(read_hex_byte(&mut self.r, XbmPart::Data)?)) } else { // Read optional comma, followed by final }; read_optional_whitespace(&mut self.r)?; match self.r.peek() { Some(b',') => { read_fixed_string(&mut self.r, b",", XbmPart::Data)?; read_optional_whitespace(&mut self.r)?; } Some(b'}') => (), _ => { self.r.next(); return Err(XbmDecodeError::Parse(XbmPart::ArrayEnd, self.r.loc())); } } read_fixed_string(&mut self.r, b"}", XbmPart::ArrayEnd)?; read_optional_whitespace(&mut self.r)?; read_fixed_string(&mut self.r, b";", XbmPart::ArrayEnd)?; read_optional_whitespace(&mut self.r)?; if self.r.next().is_some() { // File has unexpected trailing contents return Err(XbmDecodeError::Parse(XbmPart::Trailing, self.r.loc())); }; Ok(None) } } } impl XbmDecoder where R: BufRead, { /// Create a new `XBMDecoder`. pub fn new(reader: R) -> Result, ImageError> { match XbmStreamDecoder::new(IoAdapter { reader: reader.bytes(), error: None, }) { Err((mut r, e)) => Err(e).apply_after(&mut r.error), Ok(x) => Ok(XbmDecoder { base: x }), } } /// Returns the (x,y) hotspot coordinates of the image, if the image provides them. pub fn hotspot(&self) -> Option<(i32, i32)> { self.base.header.hotspot } } impl ImageDecoder for XbmDecoder { fn dimensions(&self) -> (u32, u32) { (self.base.header.width, self.base.header.height) } fn color_type(&self) -> ColorType { ColorType::L8 } fn original_color_type(&self) -> ExtendedColorType { ExtendedColorType::L1 } fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> where Self: Sized, { for row in buf.chunks_exact_mut(self.base.header.width as usize) { // The XBM format discards the last `8 * ceil(self.width / 8) - self.width` bits in each row for chunk in row.chunks_mut(8) { let nxt = self .base .next_byte() .apply_after(&mut self.base.r.inner.error)?; let val = nxt.ok_or_else(|| { ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::DimensionMismatch, )) })?; for (i, p) in chunk.iter_mut().enumerate() { // Set bits correspond to black, unset bits to white *p = if val & (1 << i) == 0 { 0xff } else { 0 }; } } } let val = self .base .next_byte() .apply_after(&mut self.base.r.inner.error)?; if val.is_some() { return Err(ImageError::Parameter(ParameterError::from_kind( ParameterErrorKind::DimensionMismatch, ))); } Ok(()) } fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { (*self).read_image(buf) } } #[cfg(test)] mod tests { use super::*; use std::fs::File; use std::io::BufReader; #[test] fn image_without_hotspot() { let decoder = XbmDecoder::new(BufReader::new( File::open("tests/images/xbm/1x1.xbm").unwrap(), )) .expect("Unable to read XBM file"); assert_eq!((1, 1), decoder.dimensions()); assert_eq!(None, decoder.hotspot()); } #[test] fn image_with_hotspot() { let decoder = XbmDecoder::new(BufReader::new( File::open("tests/images/xbm/hotspot.xbm").unwrap(), )) .expect("Unable to read XBM file"); assert_eq!((5, 5), decoder.dimensions()); assert_eq!(Some((-1, 2)), decoder.hotspot()); } } image-extras-0.1.0/src/xpm/mod.rs000064400000000000000000001132461046102023000147370ustar 00000000000000//! Decoding of XPM Images //! //! XPM (X PixMap) Format is a plain text image format, originally designed to store //! cursor and icon data. XPM images are valid C code. //! //! (This format is obsolete and nobody should make new images in it. If you need to //! include an image in a C program, use `xxd -i` or #embed.) //! //! The XPM format allows for encoding an image which can be expressed differently //! depending on the display capabilities (X11 visual), providing specialized versions //! for color, grayscale, black and white, etc. output in the same image. In practice, //! most XPM images created after the mid 1990s only provide a variant for the color //! visual. As a result, this decoder implementation only outputs the color version //! of the input image. //! //! A number of features of the original libXpm are not supported (because they appear to very //! rarely have been used): //! - XPMEXT extensions //! - HSV color specifications //! - Output for non-color visuals //! - More relaxed header comment parsing (allowing different whitespace around `XPM` in `/* XPM */`) //! - Loading with a different color table //! //! This is a somewhat strict decoder and will reject many broken image files, including: //! - those using the XPM2 header or `static char ** name = {` array string //! - those missing a trailing "," on lines, or which use ";" instead of "," //! - those with color data lines that are too long //! - those which have content after the final semicolon which is not a C comment //! //! Note: color values for the X11 color name table were _changed_ for the X11R4 release //! in Dec 1989; since then there have only been additions. //! //! This overlaps with XPM version development: XPMv1 in Feb 1989, XPMv2 in Feb-August 1990, //! and XPMv3 in April 1991. Therefore, if you _do_ see an ancient XPMv1 or XPMv2 file //! somewhere, it may be using different color name values. //! //! This decoder uses the X11 color name table as of X11R6 (May 1994); the only additions since //! then, in 2014 to add some CSS color names, are _not_ included, to preserve compatibility //! with other XPM parsers. //! //! # Related Links //! * - XPM Manual version 3.4i, which specifies the format //! * - XPM Paper //! * - The XPM format on wikipedia //! * - XPM format history //! * - X color names //! * - Introduction of modern X11 color name table //! * - more historical XPM material mod x11r6colors; use std::cmp::Ordering; use std::fmt; use std::io::{BufRead, Bytes}; use image::error::{ DecodingError, ImageError, ImageFormatHint, ImageResult, LimitError, LimitErrorKind, }; use image::{ColorType, ImageDecoder, LimitSupport, Limits}; /// Maximum length of an X11/CSS/etc. color name is 20; and of an RGB color is 13 const MAX_COLOR_NAME_LEN: usize = 32; /// Location of a byte in the input stream. /// /// Includes byte offset (for format debugging with hex editor) and /// line:column offset (for format debugging with text editor) #[derive(Clone, Copy, Debug)] struct TextLocation { byte: u64, line: u64, column: u64, } /// A peekable reader which tracks location information struct TextReader { inner: R, current: Option, location: TextLocation, } impl TextReader where R: Iterator, { /// Initialize a TextReader fn new(mut r: R) -> TextReader { let current = r.next(); TextReader { inner: r, current, location: TextLocation { byte: 0, line: 1, column: 0, }, } } /// Consume the next byte. On EOF, will return None fn next(&mut self) -> Option { self.current?; let mut current = self.inner.next(); std::mem::swap(&mut self.current, &mut current); self.location.byte += 1; self.location.column += 1; if let Some(b'\n') = current { self.location.line += 1; self.location.column = 0; } current } /// Peek at the next byte. On EOF, will return None fn peek(&self) -> Option { self.current } /// The location of the last byte returned by [Self::next] fn loc(&self) -> TextLocation { self.location } } /// Helper struct to project BufRead down to Iterator. Costs of this simple /// lifetime-free abstraction include that the struct requires space to store the /// error value, and that code using this must eventually check the error field. struct IoAdapter { reader: Bytes, error: Option, } impl Iterator for IoAdapter where R: BufRead, { type Item = u8; #[inline(always)] fn next(&mut self) -> Option { if self.error.is_some() { return None; } match self.reader.next() { None => None, Some(Ok(v)) => Some(v), Some(Err(e)) => { self.error = Some(e); None } } } } /// XPM decoder pub struct XpmDecoder { r: TextReader>, info: XpmHeaderInfo, } /// Key XPM file properties determined from first line struct XpmHeaderInfo { width: u32, height: u32, ncolors: u32, /// characters per pixel cpp: u32, } /// XPM color palette storage struct XpmPalette { /// Sorted table of color code entries. There are many possible ways to store /// this, and the fastest approach depends on the image structure, number of pixels, /// and number of colors. While not as efficient to construct as an unsorted list, /// or as efficient to look values up in as a perfect hash table, the sorted table /// performs decently well as long as the palette is small enough to fit in CPU caches. table: Vec, } /// Pixel code and value read from the Colors section of an XPM file struct XpmColorCodeEntry { code: u64, /// channel order: R,G,B,A value: [u16; 4], } #[derive(Debug, Clone, Copy)] enum XpmPart { Header, ArrayStart, FirstLine, Palette, Body, Trailing, AfterEnd, } #[derive(Debug)] enum XpmDecodeError { Parse(XpmPart, TextLocation), ZeroWidth, ZeroHeight, ZeroColors, BadCharsPerColor(u32), // A color with the given name is not available. // Name provided in buffer, length format, and should be alphanumeric ASCII UnknownColor(([u8; MAX_COLOR_NAME_LEN], u8)), // Palette entry is missing 'c'-type color specification NoColorModeColorSpecified, BadHexColor, DuplicateCode, UnknownCode, TwoKeysInARow, MissingEntry, MissingColorAfterKey, MissingKeyBeforeColor, InvalidColorName, ColorNameTooLong, } /// Types of visuals for which a color should be used #[derive(Debug)] enum XpmVisual { Mono, Symbolic, Grayscale4, Grayscale, Color, } impl fmt::Display for TextLocation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_fmt(format_args!( "byte={},line={}:col={}", self.byte, self.line, self.column )) } } impl fmt::Display for XpmPart { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Header => f.write_str("header"), Self::ArrayStart => f.write_str("array definition"), Self::FirstLine => f.write_str(" section"), Self::Palette => f.write_str(" section"), Self::Body => f.write_str(" section"), Self::Trailing => f.write_str("array end"), Self::AfterEnd => f.write_str("after final semicolon"), } } } impl fmt::Display for XpmDecodeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Parse(part, loc) => f.write_fmt(format_args!("Failed to parse {}, at {}", part, loc)), Self::ZeroWidth => f.write_str("Invalid (zero) image width"), Self::ZeroHeight => f.write_str("Invalid (zero) image height"), Self::ZeroColors => f.write_str("Invalid (zero) number of colors"), Self::BadCharsPerColor(c) => f.write_fmt(format_args!( "Invalid number of characters per color: {} is not in [1,8]", c )), Self::UnknownColor((buf, len)) => { let s = std::str::from_utf8(&buf[..*len as usize]).ok().unwrap_or(""); assert!(s.chars().all(|x| x.is_ascii_alphanumeric())); f.write_fmt(format_args!("Unknown color name \"{}\"; is not an X11R6 color.", s)) } Self::NoColorModeColorSpecified => { f.write_str("Color entry has no specified value for color visual") } Self::BadHexColor => f.write_str("Invalid hex RGB color"), Self::DuplicateCode => f.write_str("Duplicate color code"), Self::UnknownCode => f.write_str("Unknown color code"), Self::ColorNameTooLong => f.write_str("Invalid color name, too long"), Self::TwoKeysInARow => f.write_str("Invalid color specification, two keys in a row"), Self::MissingEntry => f.write_str("Invalid color specification, must contain at least one key-color pair"), Self::MissingColorAfterKey => f.write_str("Invalid color specification, no color name after key"), Self::MissingKeyBeforeColor => f.write_str("Invalid color specification, no key before color name or could not parse value as key (m|s|g4|g|c)"), Self::InvalidColorName => f.write_str("Invalid color name, contains non-alphanumeric or non-whitespace characters"), } } } impl std::error::Error for XpmDecodeError {} impl From for ImageError { fn from(e: XpmDecodeError) -> ImageError { ImageError::Decoding(DecodingError::new(ImageFormatHint::Name("XPM".into()), e)) } } /// Helper trait for the pattern in which, after calling a function returning a Result, /// one wishes to use an error from a different source. trait XpmDecoderIoInjectionExt { type Value; fn apply_after(self, err: &mut Option) -> Result; } impl XpmDecoderIoInjectionExt for Result { type Value = X; fn apply_after(self, err: &mut Option) -> Result { if let Some(err) = err.take() { return Err(ImageError::IoError(err)); } match self { Self::Ok(x) => Ok(x), Self::Err(e) => Err(e.into()), } } } /// Is x a valid character to use in a word of a color name fn valid_name_char(x: u8) -> bool { // underscore: used in some symbolic names matches!(x, b'#' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_') } /// Replace upper case by lower case ASCII letters fn fold_to_lower(x: u8) -> u8 { match x { b'A'..=b'Z' => (x - b'A') + b'a', _ => x, } } /// Read precisely the string `s` from `r`, or error. fn read_fixed_string>( r: &mut TextReader, s: &[u8], part: XpmPart, ) -> Result<(), XpmDecodeError> { for c in s { if let Some(b) = r.next() { if b != *c { return Err(XpmDecodeError::Parse(part, r.loc())); } } else { return Err(XpmDecodeError::Parse(part, r.loc())); }; } Ok(()) } // Read a single byte fn read_byte>( r: &mut TextReader, part: XpmPart, ) -> Result { match r.next() { None => Err(XpmDecodeError::Parse(part, r.loc())), Some(b) => Ok(b), } } /// Read a mixture of ' ' and '\t'. At least one character must be read. // Other whitespace characters are not permitted. fn read_whitespace_gap>( r: &mut TextReader, part: XpmPart, ) -> Result<(), XpmDecodeError> { let b = read_byte(r, part)?; if !(b == b' ' || b == b'\t') { return Err(XpmDecodeError::Parse(part, r.loc())); } while let Some(b) = r.peek() { if b == b' ' || b == b'\t' { r.next(); continue; } else { return Ok(()); } } Ok(()) } /// Read a mixture of ' ', '\t', '\n', and C-style /* comments */. /// This will error if it sees a / without following * fn skip_whitespace_and_comments>( r: &mut TextReader, part: XpmPart, ) -> Result { let mut nbytes = 0; // `has_first_char`: If out of comment, has / ; if in comment, has * let mut has_first_char = false; let mut in_comment = false; while let Some(b) = r.peek() { if !in_comment { if has_first_char { if b != b'*' { return Err(XpmDecodeError::Parse(part, r.loc())); } else { in_comment = true; has_first_char = false; } } if b == b'/' { has_first_char = true; } } if b == b' ' || b == b'\t' || b == b'\n' || b == b'/' || in_comment { if in_comment { if has_first_char && b == b'/' { in_comment = false; } has_first_char = b == b'*'; } nbytes += 1; r.next(); continue; } else { break; } } if !in_comment && has_first_char { // Parsed up to a / but did not find * return Err(XpmDecodeError::Parse(part, r.loc())); } Ok(nbytes) } fn skip_spaces_and_tabs>( r: &mut TextReader, ) -> Result { let mut nbytes = 0; while let Some(b) = r.peek() { if b == b' ' || b == b'\t' { nbytes += 1; r.next(); continue; } else { break; } } Ok(nbytes) } /// Read a mixture of ' ' and '\t', until reading '\n'. fn read_to_newline>( r: &mut TextReader, part: XpmPart, ) -> Result<(), XpmDecodeError> { while let Some(b) = r.peek() { if b == b' ' || b == b'\t' { r.next(); continue; } else { break; } } if read_byte(r, part)? != b'\n' { Err(XpmDecodeError::Parse(part, r.loc())) } else { Ok(()) } } /// Read token into the buffer until the buffer size is exceeded, or ' ' or '\t' or '"' is found /// \ characters are forbidden. Returns the region of data read. fn read_until_whitespace_or_eos<'a, R: Iterator>( r: &mut TextReader, buf: &'a mut [u8], part: XpmPart, ) -> Result<&'a mut [u8], XpmDecodeError> { let mut len = 0; while let Some(b) = r.peek() { if b == b' ' || b == b'\t' || b == b'"' { return Ok(&mut buf[..len]); } else if b == b'\\' { r.next(); return Err(XpmDecodeError::Parse(part, r.loc())); } else { if len >= buf.len() { // identifier is too long return Err(XpmDecodeError::Parse(part, r.loc())); } buf[len] = b; len += 1; r.next(); } } Ok(&mut buf[..len]) } /// Read fixed length token into the buffer. Errors if file ends, or " or \ is found. fn read_all_except_eos>( r: &mut TextReader, buf: &mut [u8], part: XpmPart, ) -> Result<(), XpmDecodeError> { let mut len = 0; while let Some(b) = r.peek() { if b == b'"' || b == b'\\' { r.next(); return Err(XpmDecodeError::Parse(part, r.loc())); } else { buf[len] = b; len += 1; r.next(); if len >= buf.len() { return Ok(()); } } } Err(XpmDecodeError::Parse(part, r.loc())) } /// Read the name portion of the file (but do not validate it, because some old files /// may put invalid characters here (like "." and "-") or use 8-bit character sets instead /// of Unicode.) fn read_name>( r: &mut TextReader, part: XpmPart, ) -> Result<(), XpmDecodeError> { let mut empty = true; while let Some(b) = r.peek() { match b { b'/' | b' ' | b'\t' | b'\n' | b'[' => { break; } _ => (), } r.next(); empty = false; } if empty { return Err(XpmDecodeError::Parse(part, r.loc())); } Ok(()) } /// Parse string into integer, rejecting leading + and leading zeros fn parse_i32(data: &[u8]) -> Option { if data.starts_with(b"-") { (-(parse_u32(&data[1..])? as i64)).try_into().ok() } else { parse_u32(data)?.try_into().ok() } } /// Parse string into unsigned integer, rejecting leading + and leading zeros fn parse_u32(data: &[u8]) -> Option { let Some(c1) = data.first() else { // Reject empty string return None; }; if *c1 == b'0' && data.len() > 1 { // Reject leading zeros unless value is exactly zero return None; } let mut x: u32 = 0; for c in data { if b'0' <= *c && *c <= b'9' { x = x.checked_mul(10)?.checked_add((*c - b'0') as u32)?; } else { return None; } } Some(x) } fn parse_hex(b: u8) -> Option { match b { b'0'..=b'9' => Some(b - b'0'), b'A'..=b'F' => Some(b - b'A' + 10), b'a'..=b'f' => Some(b - b'a' + 10), _ => None, } } fn parse_hex1(x1: u8) -> Option { let x = parse_hex(x1)? as u16; Some(x | (x << 4) | (x << 8) | (x << 12)) } fn parse_hex2(x2: u8, x1: u8) -> Option { let x = ((parse_hex(x2)? as u16) << 4) | (parse_hex(x1)? as u16); Some(x | (x << 8)) } fn parse_hex3(x3: u8, x2: u8, x1: u8) -> Option { let x = ((parse_hex(x3)? as u16) << 8) | ((parse_hex(x2)? as u16) << 4) | (parse_hex(x1)? as u16); // There are four reasonable approaches to converting 12-bit to 16-bit, // round down, round nearest, round up, and round fast // (x*65535)/4095, (x*65535+2047)/4095, (x*65535+4094)/4095, and (x<<4)|(x>>8). Some((((x as u32) * 65535 + 2047) / 4095) as u16) } fn parse_hex4(x4: u8, x3: u8, x2: u8, x1: u8) -> Option { Some( (parse_hex(x1)? as u16) | ((parse_hex(x2)? as u16) << 4) | ((parse_hex(x3)? as u16) << 8) | ((parse_hex(x4)? as u16) << 12), ) } fn scale_u8_to_u16(x: u8) -> u16 { (x as u16) << 8 | (x as u16) } /// Parse an #RGB-style color. /// Note: this deviates from XParseColor in order to sensibly interpret #aabbcc as #aaaabbbbcccc /// instead of #aa00bb00cc00. fn parse_hex_color(data: &[u8]) -> Option<[u16; 4]> { Some(match data { [r, g, b] => [parse_hex1(*r)?, parse_hex1(*g)?, parse_hex1(*b)?, 0xffff], [r2, r1, g2, g1, b2, b1] => [ parse_hex2(*r2, *r1)?, parse_hex2(*g2, *g1)?, parse_hex2(*b2, *b1)?, 0xffff, ], [r3, r2, r1, g3, g2, g1, b3, b2, b1] => [ parse_hex3(*r3, *r2, *r1)?, parse_hex3(*g3, *g2, *g1)?, parse_hex3(*b3, *b2, *b1)?, 0xffff, ], [r4, r3, r2, r1, g4, g3, g2, g1, b4, b3, b2, b1] => [ parse_hex4(*r4, *r3, *r2, *r1)?, parse_hex4(*g4, *g3, *g2, *g1)?, parse_hex4(*b4, *b3, *b2, *b1)?, 0xffff, ], _ => { return None; } }) } fn parse_color(data: &[u8]) -> Result<[u16; 4], XpmDecodeError> { if data.starts_with(b"#") { parse_hex_color(&data[1..]).ok_or(XpmDecodeError::BadHexColor) } else { if data == b"none" { return Ok([0, 0, 0, 0]); } if let Ok(idx) = x11r6colors::COLORS.binary_search_by(|entry| entry.0.as_bytes().cmp(data)) { let entry = x11r6colors::COLORS[idx]; Ok([ scale_u8_to_u16(entry.1), scale_u8_to_u16(entry.2), scale_u8_to_u16(entry.3), 0xffff, ]) } else { // At this point, `data` has been validated as alphanumeric ASCII; read_xpm_palette // should ensure its length is <= MAX_COLOR_NAME_LEN assert!(data.len() <= MAX_COLOR_NAME_LEN); let mut tmp = [0u8; MAX_COLOR_NAME_LEN]; tmp[..data.len()].copy_from_slice(data); Err(XpmDecodeError::UnknownColor((tmp, data.len() as u8))) } } } /// Read the header of the XPM image and first line fn read_xpm_header>( r: &mut TextReader, ) -> Result { // Note: XPM3 header is `/* XPM */` read_fixed_string(r, b"/* XPM */", XpmPart::Header)?; read_to_newline(r, XpmPart::Header)?; skip_whitespace_and_comments(r, XpmPart::ArrayStart)?; read_fixed_string(r, b"static", XpmPart::ArrayStart)?; if skip_whitespace_and_comments(r, XpmPart::ArrayStart)? == 0 { /* need a space or other char between 'static' and 'char' */ return Err(XpmDecodeError::Parse(XpmPart::ArrayStart, r.loc())); } read_fixed_string(r, b"char", XpmPart::ArrayStart)?; skip_whitespace_and_comments(r, XpmPart::ArrayStart)?; read_fixed_string(r, b"*", XpmPart::ArrayStart)?; skip_whitespace_and_comments(r, XpmPart::ArrayStart)?; read_name(r, XpmPart::ArrayStart)?; skip_whitespace_and_comments(r, XpmPart::ArrayStart)?; read_fixed_string(r, b"[", XpmPart::ArrayStart)?; skip_whitespace_and_comments(r, XpmPart::ArrayStart)?; read_fixed_string(r, b"]", XpmPart::ArrayStart)?; skip_whitespace_and_comments(r, XpmPart::ArrayStart)?; read_fixed_string(r, b"=", XpmPart::ArrayStart)?; skip_whitespace_and_comments(r, XpmPart::ArrayStart)?; read_fixed_string(r, b"{", XpmPart::ArrayStart)?; skip_whitespace_and_comments(r, XpmPart::ArrayStart)?; /* next: read \" */ read_fixed_string(r, b"\"", XpmPart::FirstLine)?; // Inside strings, only spaces are allowed for separators let mut int_buf = [0u8; 10]; // 2^32 fits in 10 bytes skip_spaces_and_tabs(r)?; // words separated by space & tabulation chars -- so skip both? let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?; let width = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?; if width == 0 { return Err(XpmDecodeError::ZeroWidth); } read_whitespace_gap(r, XpmPart::FirstLine)?; let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?; let height = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?; if height == 0 { return Err(XpmDecodeError::ZeroHeight); } read_whitespace_gap(r, XpmPart::FirstLine)?; let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?; let ncolors = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?; read_whitespace_gap(r, XpmPart::FirstLine)?; let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?; let cpp = parse_u32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?; skip_spaces_and_tabs(r)?; let _hotspot = if let Some(b'"') = r.peek() { // Done None } else { let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?; let hotspot_x = parse_i32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?; read_whitespace_gap(r, XpmPart::FirstLine)?; let int = read_until_whitespace_or_eos(r, &mut int_buf, XpmPart::FirstLine)?; let hotspot_y = parse_i32(int).ok_or(XpmDecodeError::Parse(XpmPart::FirstLine, r.loc()))?; skip_spaces_and_tabs(r)?; // Parse hotspot now. Some((hotspot_x, hotspot_y)) }; // XPMEXT tags are not supported -- they were essentially never used in practice. read_fixed_string(r, b"\"", XpmPart::FirstLine)?; skip_whitespace_and_comments(r, XpmPart::FirstLine)?; read_fixed_string(r, b",", XpmPart::FirstLine)?; skip_whitespace_and_comments(r, XpmPart::FirstLine)?; if ncolors == 0 { return Err(XpmDecodeError::ZeroColors); } if cpp == 0 || cpp > 8 { /* cpp larger than 8 is pointless and would not be made by sane encoders: * with hex encoding, it would allow 2^32 distinct colors. */ return Err(XpmDecodeError::BadCharsPerColor(cpp)); } Ok(XpmHeaderInfo { width, height, ncolors, cpp, }) } /// Read the palette portion of the XPM image, stopping just before the first pixel fn read_xpm_palette>( r: &mut TextReader, info: &XpmHeaderInfo, ) -> Result { assert!(1 <= info.cpp && info.cpp <= 8); // Check that color table is sorted assert!(x11r6colors::COLORS.windows(2).all(|p| p[0].0 < p[1].0)); // Even though the file provides a value for `ncolors`, and memory limits are validated, // do NOT reserve the suggested memory in advance. Dynamically resizing the vector // is negligibly slower, but ensures that the amount of memory allocated is always // bounded by a multiple of the actual file size. Kernel virtual memory optimizations // may hide the performance cost of allocating a 100MB color table from the // application, but such allocations are still expensive even if mostly unused. let mut color_table: Vec = Vec::new(); for _col in 0..info.ncolors { read_fixed_string(r, b"\"", XpmPart::Palette)?; let mut code = [0_u8; 8]; read_all_except_eos(r, &mut code[..info.cpp as usize], XpmPart::Palette)?; read_whitespace_gap(r, XpmPart::Palette)?; // Color parsing: XPM color specifications have the form { }+ // This is tricky to parse correctly as color names may contain spaces. // Fortunately, the key values are "m", "s", "g4", "g", "c", which will // never be a word within a color name, so one can acquire the entire color // name by parsing until the next key appears or until '"' arrives. // Like the X server, this parser does a case-insensitive match on color names. // Unfortunately, there is no general way to handle spaces in names: the color // name database includes variants with spaces for multi-word names that do not // end in a number; e.g. "antiquewhite" has a split variation "antique white", // but "antiquewhite3" does not. let mut color_name_buf = [0_u8; MAX_COLOR_NAME_LEN]; let mut color_name_len = 0; let mut next_buf = [0_u8; MAX_COLOR_NAME_LEN]; let mut key: Option = None; let mut cvis_color = None; loop { if r.peek().unwrap_or(b'"') == b'"' { let Some(ref k) = key else { // At end of line, must have read a key return Err(XpmDecodeError::MissingEntry); }; if color_name_len == 0 { // At end of line, must also have read a color to process return Err(XpmDecodeError::MissingColorAfterKey); } let color = handle_key_color(k, &color_name_buf[..color_name_len])?; cvis_color = color.or(cvis_color); break; } let next = read_until_whitespace_or_eos(r, &mut next_buf, XpmPart::Palette)?; skip_spaces_and_tabs(r)?; let this_key = match &next[..] { b"m" => Some(XpmVisual::Mono), b"s" => Some(XpmVisual::Symbolic), b"g4" => Some(XpmVisual::Grayscale4), b"g" => Some(XpmVisual::Grayscale), b"c" => Some(XpmVisual::Color), _ => None, }; let Some(ref k) = key else { // No key has been set, is first key-color pair in the line if this_key.is_none() { // Error: processing non-key value with no preceding key return Err(XpmDecodeError::MissingKeyBeforeColor); }; key = this_key; continue; }; if this_key.is_some() { // End of preceding segment if color_name_len == 0 { return Err(XpmDecodeError::TwoKeysInARow); } let color = handle_key_color(k, &color_name_buf[..color_name_len])?; cvis_color = color.or(cvis_color); color_name_len = 0; key = this_key; continue; } // Validate word, case fold it, and concatenate it with the preceding word, // adding a space betweeen words if color_name_len > 0 { if color_name_len < MAX_COLOR_NAME_LEN { color_name_buf[color_name_len] = b' '; color_name_len += 1; } else { return Err(XpmDecodeError::ColorNameTooLong); } } for c in next { if !valid_name_char(*c) { return Err(XpmDecodeError::InvalidColorName); } // Reduce to lowercase, matching the color name database, to // make regular string comparisons be case-insensitive if color_name_len < MAX_COLOR_NAME_LEN { color_name_buf[color_name_len] = fold_to_lower(*c); color_name_len += 1; } else { return Err(XpmDecodeError::ColorNameTooLong); } } } let Some(color) = cvis_color else { return Err(XpmDecodeError::NoColorModeColorSpecified); }; color_table.push(XpmColorCodeEntry { code: u64::from_le_bytes(code), value: color, }); read_fixed_string(r, b"\"", XpmPart::Palette)?; skip_whitespace_and_comments(r, XpmPart::Palette)?; read_fixed_string(r, b",", XpmPart::Palette)?; skip_whitespace_and_comments(r, XpmPart::Palette)?; } // Sort table and check for duplicates color_table.sort_unstable_by(|x, y| x.code.cmp(&y.code)); for w in color_table.windows(2) { if w[0].code.cmp(&w[1].code) != Ordering::Less { return Err(XpmDecodeError::DuplicateCode); } } read_fixed_string(r, b"\"", XpmPart::Body)?; Ok(XpmPalette { table: color_table }) } /// Read a single pixel from within the main image area fn read_xpm_pixel>( r: &mut TextReader, info: &XpmHeaderInfo, palette: &XpmPalette, chunk: &mut [u8; 8], ) -> Result<(), XpmDecodeError> { let mut code = [0_u8; 8]; read_all_except_eos(r, &mut code[..info.cpp as usize], XpmPart::Palette)?; let code = u64::from_le_bytes(code); let Ok(index) = palette .table .binary_search_by(|entry| entry.code.cmp(&code)) else { return Err(XpmDecodeError::UnknownCode); }; let color = palette.table[index].value; // ColorType::Rgba16 is currently native endian, R,G,B,A channel order chunk[0..2].copy_from_slice(&color[0].to_ne_bytes()); chunk[2..4].copy_from_slice(&color[1].to_ne_bytes()); chunk[4..6].copy_from_slice(&color[2].to_ne_bytes()); chunk[6..8].copy_from_slice(&color[3].to_ne_bytes()); Ok(()) } /// Read the end of this row of the XPM image body and the start of the next. /// Should only be called between rows, and not after the last one fn read_xpm_row_transition>( r: &mut TextReader, ) -> Result<(), XpmDecodeError> { // End of this line read_fixed_string(r, b"\"", XpmPart::Body)?; skip_whitespace_and_comments(r, XpmPart::Body)?; read_fixed_string(r, b",", XpmPart::Body)?; skip_whitespace_and_comments(r, XpmPart::Body)?; // Start of next line read_fixed_string(r, b"\"", XpmPart::Body)?; Ok(()) } /// Read the end of the XPM image fn read_xpm_trailing>(r: &mut TextReader) -> Result<(), XpmDecodeError> { // Read end of last line read_fixed_string(r, b"\"", XpmPart::Body)?; // Read optional comma, followed by final }; skip_whitespace_and_comments(r, XpmPart::Trailing)?; let next = read_byte(r, XpmPart::Trailing)?; if next == b',' { skip_whitespace_and_comments(r, XpmPart::Trailing)?; read_fixed_string(r, b"}", XpmPart::Trailing)?; } else if next != b'}' { return Err(XpmDecodeError::Parse(XpmPart::Trailing, r.loc())); } skip_whitespace_and_comments(r, XpmPart::Trailing)?; read_fixed_string(r, b";", XpmPart::Trailing)?; skip_whitespace_and_comments(r, XpmPart::AfterEnd)?; if r.next().is_some() { // File has unexpected trailing contents. Err(XpmDecodeError::Parse(XpmPart::AfterEnd, r.loc())) } else { Ok(()) } } impl XpmDecoder where R: BufRead, { /// Create a new [XpmDecoder]. pub fn new(reader: R) -> Result, ImageError> { let mut r = TextReader::new(IoAdapter { reader: reader.bytes(), error: None, }); let info = read_xpm_header(&mut r).apply_after(&mut r.inner.error)?; Ok(XpmDecoder { r, info }) } } /// Parse color, returning it if the key is also XpmVisual::Color fn handle_key_color(key: &XpmVisual, color: &[u8]) -> Result, XpmDecodeError> { if matches!(key, XpmVisual::Symbolic) { return Ok(None); } let color = parse_color(color)?; if matches!(key, XpmVisual::Color) { Ok(Some(color)) } else { Ok(None) } } impl ImageDecoder for XpmDecoder { fn dimensions(&self) -> (u32, u32) { (self.info.width, self.info.height) } fn color_type(&self) -> ColorType { // note: some images specify 16-bpc colors, and fully transparent pixels are possible, // so RGBA16 is needed to handle all possible cases ColorType::Rgba16 } fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> where Self: Sized, { assert!(1 <= self.info.cpp && self.info.cpp <= 8); let palette = read_xpm_palette(&mut self.r, &self.info).apply_after(&mut self.r.inner.error)?; // Read main image contents let stride = (self.info.width as usize).checked_mul(8).unwrap(); for (i, row) in buf.chunks_exact_mut(stride).enumerate() { for chunk in row.chunks_exact_mut(8) { read_xpm_pixel(&mut self.r, &self.info, &palette, chunk.try_into().unwrap()) .apply_after(&mut self.r.inner.error)?; } if i >= (self.info.height - 1) as usize { // Last row, } else { read_xpm_row_transition(&mut self.r).apply_after(&mut self.r.inner.error)?; } } read_xpm_trailing(&mut self.r).apply_after(&mut self.r.inner.error)?; Ok(()) } fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { (*self).read_image(buf) } fn set_limits(&mut self, limits: Limits) -> ImageResult<()> { limits.check_support(&LimitSupport::default())?; let (width, height) = self.dimensions(); limits.check_dimensions(width, height)?; let max_pixels = u64::from(self.info.width) * u64::from(self.info.height); let max_image_bytes = max_pixels .checked_mul(8) .ok_or(ImageError::Limits(LimitError::from_kind( LimitErrorKind::DimensionError, )))?; let max_table_bytes = (self.info.ncolors as u64) * (size_of::() as u64); let max_bytes = max_image_bytes .checked_add(max_table_bytes) .ok_or(ImageError::Limits(LimitError::from_kind( LimitErrorKind::InsufficientMemory, )))?; let max_alloc = limits.max_alloc.unwrap_or(u64::MAX); if max_alloc < max_bytes { return Err(ImageError::Limits(LimitError::from_kind( LimitErrorKind::InsufficientMemory, ))); } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn image_missing_body() { let data = b"/* XPM */ static char *test[] = { \"20 5 10 1\", }; "; let decoder = XpmDecoder::new(&data[..]).unwrap(); let mut image = vec![0; decoder.total_bytes() as usize]; assert!(decoder.read_image(&mut image).is_err()); } #[test] fn invalid_color_name() { let data = b"/* XPM */ static char *test[] = { \"1 1 1 1\", \" c Antique White1\", \" \", };"; let decoder = XpmDecoder::new(&data[..]).unwrap(); let mut image = vec![0; decoder.total_bytes() as usize]; assert!(decoder.read_image(&mut image).is_err()); } #[test] fn trailing_semicolon_required() { let data = b"/* XPM */ static char *test[] = { \"1 1 1 1\", \" c none\", \" \", };"; let decoder = XpmDecoder::new(&data[..data.len() - 1]).unwrap(); let mut image = vec![0; decoder.total_bytes() as usize]; assert!(decoder.read_image(&mut image).is_err()); let decoder = XpmDecoder::new(&data[..]).unwrap(); let mut image = vec![0; decoder.total_bytes() as usize]; assert!(decoder.read_image(&mut image).is_ok()); } } image-extras-0.1.0/src/xpm/x11r6colors.rs000064400000000000000000000647131046102023000162670ustar 00000000000000//! # The colors from X11R6. //! //! This mini-library contains the color names database (`rgb.txt`) provided by //! X11R6 (the sixth release of the X Window System, Version 11), with names //! folded to lowercase because X does case insensitive string comparisons. //! //! X11R6 was relased in May of 1994. Since then, the X11 color list was //! modified in June 2014 to add CSS level 4 colors and add entries //! distinguishing certain CSS from X11 colors. There have been no other //! material changes. //! //! The color values have been mostly stable since X11R4 (released Dec 1989); //! X11R5 slightly modified the colors 'gray' and 'grey', and X11R6 only added //! colors. //! //! ## License //! //! It is possible that databases of colors with names are copyrightable and //! therefore subject to the X11 license: //! //! ```text //! Copyright (C) 1994 X Consortium //! //! 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 //! X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN //! AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC- //! TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //! //! Except as contained in this notice, the name of the X Consortium shall not //! be used in advertising or otherwise to promote the sale, use or other deal- //! ings in this Software without prior written authorization from the X Consor- //! tium. //! ``` /// Table of colors, sorted by name. /// Table entries are (name, R, G, B) tuples. /// Names are lowercase. /// Some but not all multi-word names have an additional form with spaces between words. pub const COLORS: &[(&str, u8, u8, u8)] = &[ ("alice blue", 240, 248, 255), ("aliceblue", 240, 248, 255), ("antique white", 250, 235, 215), ("antiquewhite", 250, 235, 215), ("antiquewhite1", 255, 239, 219), ("antiquewhite2", 238, 223, 204), ("antiquewhite3", 205, 192, 176), ("antiquewhite4", 139, 131, 120), ("aquamarine", 127, 255, 212), ("aquamarine1", 127, 255, 212), ("aquamarine2", 118, 238, 198), ("aquamarine3", 102, 205, 170), ("aquamarine4", 69, 139, 116), ("azure", 240, 255, 255), ("azure1", 240, 255, 255), ("azure2", 224, 238, 238), ("azure3", 193, 205, 205), ("azure4", 131, 139, 139), ("beige", 245, 245, 220), ("bisque", 255, 228, 196), ("bisque1", 255, 228, 196), ("bisque2", 238, 213, 183), ("bisque3", 205, 183, 158), ("bisque4", 139, 125, 107), ("black", 0, 0, 0), ("blanched almond", 255, 235, 205), ("blanchedalmond", 255, 235, 205), ("blue", 0, 0, 255), ("blue violet", 138, 43, 226), ("blue1", 0, 0, 255), ("blue2", 0, 0, 238), ("blue3", 0, 0, 205), ("blue4", 0, 0, 139), ("blueviolet", 138, 43, 226), ("brown", 165, 42, 42), ("brown1", 255, 64, 64), ("brown2", 238, 59, 59), ("brown3", 205, 51, 51), ("brown4", 139, 35, 35), ("burlywood", 222, 184, 135), ("burlywood1", 255, 211, 155), ("burlywood2", 238, 197, 145), ("burlywood3", 205, 170, 125), ("burlywood4", 139, 115, 85), ("cadet blue", 95, 158, 160), ("cadetblue", 95, 158, 160), ("cadetblue1", 152, 245, 255), ("cadetblue2", 142, 229, 238), ("cadetblue3", 122, 197, 205), ("cadetblue4", 83, 134, 139), ("chartreuse", 127, 255, 0), ("chartreuse1", 127, 255, 0), ("chartreuse2", 118, 238, 0), ("chartreuse3", 102, 205, 0), ("chartreuse4", 69, 139, 0), ("chocolate", 210, 105, 30), ("chocolate1", 255, 127, 36), ("chocolate2", 238, 118, 33), ("chocolate3", 205, 102, 29), ("chocolate4", 139, 69, 19), ("coral", 255, 127, 80), ("coral1", 255, 114, 86), ("coral2", 238, 106, 80), ("coral3", 205, 91, 69), ("coral4", 139, 62, 47), ("cornflower blue", 100, 149, 237), ("cornflowerblue", 100, 149, 237), ("cornsilk", 255, 248, 220), ("cornsilk1", 255, 248, 220), ("cornsilk2", 238, 232, 205), ("cornsilk3", 205, 200, 177), ("cornsilk4", 139, 136, 120), ("cyan", 0, 255, 255), ("cyan1", 0, 255, 255), ("cyan2", 0, 238, 238), ("cyan3", 0, 205, 205), ("cyan4", 0, 139, 139), ("dark blue", 0, 0, 139), ("dark cyan", 0, 139, 139), ("dark goldenrod", 184, 134, 11), ("dark gray", 169, 169, 169), ("dark green", 0, 100, 0), ("dark grey", 169, 169, 169), ("dark khaki", 189, 183, 107), ("dark magenta", 139, 0, 139), ("dark olive green", 85, 107, 47), ("dark orange", 255, 140, 0), ("dark orchid", 153, 50, 204), ("dark red", 139, 0, 0), ("dark salmon", 233, 150, 122), ("dark sea green", 143, 188, 143), ("dark slate blue", 72, 61, 139), ("dark slate gray", 47, 79, 79), ("dark slate grey", 47, 79, 79), ("dark turquoise", 0, 206, 209), ("dark violet", 148, 0, 211), ("darkblue", 0, 0, 139), ("darkcyan", 0, 139, 139), ("darkgoldenrod", 184, 134, 11), ("darkgoldenrod1", 255, 185, 15), ("darkgoldenrod2", 238, 173, 14), ("darkgoldenrod3", 205, 149, 12), ("darkgoldenrod4", 139, 101, 8), ("darkgray", 169, 169, 169), ("darkgreen", 0, 100, 0), ("darkgrey", 169, 169, 169), ("darkkhaki", 189, 183, 107), ("darkmagenta", 139, 0, 139), ("darkolivegreen", 85, 107, 47), ("darkolivegreen1", 202, 255, 112), ("darkolivegreen2", 188, 238, 104), ("darkolivegreen3", 162, 205, 90), ("darkolivegreen4", 110, 139, 61), ("darkorange", 255, 140, 0), ("darkorange1", 255, 127, 0), ("darkorange2", 238, 118, 0), ("darkorange3", 205, 102, 0), ("darkorange4", 139, 69, 0), ("darkorchid", 153, 50, 204), ("darkorchid1", 191, 62, 255), ("darkorchid2", 178, 58, 238), ("darkorchid3", 154, 50, 205), ("darkorchid4", 104, 34, 139), ("darkred", 139, 0, 0), ("darksalmon", 233, 150, 122), ("darkseagreen", 143, 188, 143), ("darkseagreen1", 193, 255, 193), ("darkseagreen2", 180, 238, 180), ("darkseagreen3", 155, 205, 155), ("darkseagreen4", 105, 139, 105), ("darkslateblue", 72, 61, 139), ("darkslategray", 47, 79, 79), ("darkslategray1", 151, 255, 255), ("darkslategray2", 141, 238, 238), ("darkslategray3", 121, 205, 205), ("darkslategray4", 82, 139, 139), ("darkslategrey", 47, 79, 79), ("darkturquoise", 0, 206, 209), ("darkviolet", 148, 0, 211), ("deep pink", 255, 20, 147), ("deep sky blue", 0, 191, 255), ("deeppink", 255, 20, 147), ("deeppink1", 255, 20, 147), ("deeppink2", 238, 18, 137), ("deeppink3", 205, 16, 118), ("deeppink4", 139, 10, 80), ("deepskyblue", 0, 191, 255), ("deepskyblue1", 0, 191, 255), ("deepskyblue2", 0, 178, 238), ("deepskyblue3", 0, 154, 205), ("deepskyblue4", 0, 104, 139), ("dim gray", 105, 105, 105), ("dim grey", 105, 105, 105), ("dimgray", 105, 105, 105), ("dimgrey", 105, 105, 105), ("dodger blue", 30, 144, 255), ("dodgerblue", 30, 144, 255), ("dodgerblue1", 30, 144, 255), ("dodgerblue2", 28, 134, 238), ("dodgerblue3", 24, 116, 205), ("dodgerblue4", 16, 78, 139), ("firebrick", 178, 34, 34), ("firebrick1", 255, 48, 48), ("firebrick2", 238, 44, 44), ("firebrick3", 205, 38, 38), ("firebrick4", 139, 26, 26), ("floral white", 255, 250, 240), ("floralwhite", 255, 250, 240), ("forest green", 34, 139, 34), ("forestgreen", 34, 139, 34), ("gainsboro", 220, 220, 220), ("ghost white", 248, 248, 255), ("ghostwhite", 248, 248, 255), ("gold", 255, 215, 0), ("gold1", 255, 215, 0), ("gold2", 238, 201, 0), ("gold3", 205, 173, 0), ("gold4", 139, 117, 0), ("goldenrod", 218, 165, 32), ("goldenrod1", 255, 193, 37), ("goldenrod2", 238, 180, 34), ("goldenrod3", 205, 155, 29), ("goldenrod4", 139, 105, 20), ("gray", 190, 190, 190), ("gray0", 0, 0, 0), ("gray1", 3, 3, 3), ("gray10", 26, 26, 26), ("gray100", 255, 255, 255), ("gray11", 28, 28, 28), ("gray12", 31, 31, 31), ("gray13", 33, 33, 33), ("gray14", 36, 36, 36), ("gray15", 38, 38, 38), ("gray16", 41, 41, 41), ("gray17", 43, 43, 43), ("gray18", 46, 46, 46), ("gray19", 48, 48, 48), ("gray2", 5, 5, 5), ("gray20", 51, 51, 51), ("gray21", 54, 54, 54), ("gray22", 56, 56, 56), ("gray23", 59, 59, 59), ("gray24", 61, 61, 61), ("gray25", 64, 64, 64), ("gray26", 66, 66, 66), ("gray27", 69, 69, 69), ("gray28", 71, 71, 71), ("gray29", 74, 74, 74), ("gray3", 8, 8, 8), ("gray30", 77, 77, 77), ("gray31", 79, 79, 79), ("gray32", 82, 82, 82), ("gray33", 84, 84, 84), ("gray34", 87, 87, 87), ("gray35", 89, 89, 89), ("gray36", 92, 92, 92), ("gray37", 94, 94, 94), ("gray38", 97, 97, 97), ("gray39", 99, 99, 99), ("gray4", 10, 10, 10), ("gray40", 102, 102, 102), ("gray41", 105, 105, 105), ("gray42", 107, 107, 107), ("gray43", 110, 110, 110), ("gray44", 112, 112, 112), ("gray45", 115, 115, 115), ("gray46", 117, 117, 117), ("gray47", 120, 120, 120), ("gray48", 122, 122, 122), ("gray49", 125, 125, 125), ("gray5", 13, 13, 13), ("gray50", 127, 127, 127), ("gray51", 130, 130, 130), ("gray52", 133, 133, 133), ("gray53", 135, 135, 135), ("gray54", 138, 138, 138), ("gray55", 140, 140, 140), ("gray56", 143, 143, 143), ("gray57", 145, 145, 145), ("gray58", 148, 148, 148), ("gray59", 150, 150, 150), ("gray6", 15, 15, 15), ("gray60", 153, 153, 153), ("gray61", 156, 156, 156), ("gray62", 158, 158, 158), ("gray63", 161, 161, 161), ("gray64", 163, 163, 163), ("gray65", 166, 166, 166), ("gray66", 168, 168, 168), ("gray67", 171, 171, 171), ("gray68", 173, 173, 173), ("gray69", 176, 176, 176), ("gray7", 18, 18, 18), ("gray70", 179, 179, 179), ("gray71", 181, 181, 181), ("gray72", 184, 184, 184), ("gray73", 186, 186, 186), ("gray74", 189, 189, 189), ("gray75", 191, 191, 191), ("gray76", 194, 194, 194), ("gray77", 196, 196, 196), ("gray78", 199, 199, 199), ("gray79", 201, 201, 201), ("gray8", 20, 20, 20), ("gray80", 204, 204, 204), ("gray81", 207, 207, 207), ("gray82", 209, 209, 209), ("gray83", 212, 212, 212), ("gray84", 214, 214, 214), ("gray85", 217, 217, 217), ("gray86", 219, 219, 219), ("gray87", 222, 222, 222), ("gray88", 224, 224, 224), ("gray89", 227, 227, 227), ("gray9", 23, 23, 23), ("gray90", 229, 229, 229), ("gray91", 232, 232, 232), ("gray92", 235, 235, 235), ("gray93", 237, 237, 237), ("gray94", 240, 240, 240), ("gray95", 242, 242, 242), ("gray96", 245, 245, 245), ("gray97", 247, 247, 247), ("gray98", 250, 250, 250), ("gray99", 252, 252, 252), ("green", 0, 255, 0), ("green yellow", 173, 255, 47), ("green1", 0, 255, 0), ("green2", 0, 238, 0), ("green3", 0, 205, 0), ("green4", 0, 139, 0), ("greenyellow", 173, 255, 47), ("grey", 190, 190, 190), ("grey0", 0, 0, 0), ("grey1", 3, 3, 3), ("grey10", 26, 26, 26), ("grey100", 255, 255, 255), ("grey11", 28, 28, 28), ("grey12", 31, 31, 31), ("grey13", 33, 33, 33), ("grey14", 36, 36, 36), ("grey15", 38, 38, 38), ("grey16", 41, 41, 41), ("grey17", 43, 43, 43), ("grey18", 46, 46, 46), ("grey19", 48, 48, 48), ("grey2", 5, 5, 5), ("grey20", 51, 51, 51), ("grey21", 54, 54, 54), ("grey22", 56, 56, 56), ("grey23", 59, 59, 59), ("grey24", 61, 61, 61), ("grey25", 64, 64, 64), ("grey26", 66, 66, 66), ("grey27", 69, 69, 69), ("grey28", 71, 71, 71), ("grey29", 74, 74, 74), ("grey3", 8, 8, 8), ("grey30", 77, 77, 77), ("grey31", 79, 79, 79), ("grey32", 82, 82, 82), ("grey33", 84, 84, 84), ("grey34", 87, 87, 87), ("grey35", 89, 89, 89), ("grey36", 92, 92, 92), ("grey37", 94, 94, 94), ("grey38", 97, 97, 97), ("grey39", 99, 99, 99), ("grey4", 10, 10, 10), ("grey40", 102, 102, 102), ("grey41", 105, 105, 105), ("grey42", 107, 107, 107), ("grey43", 110, 110, 110), ("grey44", 112, 112, 112), ("grey45", 115, 115, 115), ("grey46", 117, 117, 117), ("grey47", 120, 120, 120), ("grey48", 122, 122, 122), ("grey49", 125, 125, 125), ("grey5", 13, 13, 13), ("grey50", 127, 127, 127), ("grey51", 130, 130, 130), ("grey52", 133, 133, 133), ("grey53", 135, 135, 135), ("grey54", 138, 138, 138), ("grey55", 140, 140, 140), ("grey56", 143, 143, 143), ("grey57", 145, 145, 145), ("grey58", 148, 148, 148), ("grey59", 150, 150, 150), ("grey6", 15, 15, 15), ("grey60", 153, 153, 153), ("grey61", 156, 156, 156), ("grey62", 158, 158, 158), ("grey63", 161, 161, 161), ("grey64", 163, 163, 163), ("grey65", 166, 166, 166), ("grey66", 168, 168, 168), ("grey67", 171, 171, 171), ("grey68", 173, 173, 173), ("grey69", 176, 176, 176), ("grey7", 18, 18, 18), ("grey70", 179, 179, 179), ("grey71", 181, 181, 181), ("grey72", 184, 184, 184), ("grey73", 186, 186, 186), ("grey74", 189, 189, 189), ("grey75", 191, 191, 191), ("grey76", 194, 194, 194), ("grey77", 196, 196, 196), ("grey78", 199, 199, 199), ("grey79", 201, 201, 201), ("grey8", 20, 20, 20), ("grey80", 204, 204, 204), ("grey81", 207, 207, 207), ("grey82", 209, 209, 209), ("grey83", 212, 212, 212), ("grey84", 214, 214, 214), ("grey85", 217, 217, 217), ("grey86", 219, 219, 219), ("grey87", 222, 222, 222), ("grey88", 224, 224, 224), ("grey89", 227, 227, 227), ("grey9", 23, 23, 23), ("grey90", 229, 229, 229), ("grey91", 232, 232, 232), ("grey92", 235, 235, 235), ("grey93", 237, 237, 237), ("grey94", 240, 240, 240), ("grey95", 242, 242, 242), ("grey96", 245, 245, 245), ("grey97", 247, 247, 247), ("grey98", 250, 250, 250), ("grey99", 252, 252, 252), ("honeydew", 240, 255, 240), ("honeydew1", 240, 255, 240), ("honeydew2", 224, 238, 224), ("honeydew3", 193, 205, 193), ("honeydew4", 131, 139, 131), ("hot pink", 255, 105, 180), ("hotpink", 255, 105, 180), ("hotpink1", 255, 110, 180), ("hotpink2", 238, 106, 167), ("hotpink3", 205, 96, 144), ("hotpink4", 139, 58, 98), ("indian red", 205, 92, 92), ("indianred", 205, 92, 92), ("indianred1", 255, 106, 106), ("indianred2", 238, 99, 99), ("indianred3", 205, 85, 85), ("indianred4", 139, 58, 58), ("ivory", 255, 255, 240), ("ivory1", 255, 255, 240), ("ivory2", 238, 238, 224), ("ivory3", 205, 205, 193), ("ivory4", 139, 139, 131), ("khaki", 240, 230, 140), ("khaki1", 255, 246, 143), ("khaki2", 238, 230, 133), ("khaki3", 205, 198, 115), ("khaki4", 139, 134, 78), ("lavender", 230, 230, 250), ("lavender blush", 255, 240, 245), ("lavenderblush", 255, 240, 245), ("lavenderblush1", 255, 240, 245), ("lavenderblush2", 238, 224, 229), ("lavenderblush3", 205, 193, 197), ("lavenderblush4", 139, 131, 134), ("lawn green", 124, 252, 0), ("lawngreen", 124, 252, 0), ("lemon chiffon", 255, 250, 205), ("lemonchiffon", 255, 250, 205), ("lemonchiffon1", 255, 250, 205), ("lemonchiffon2", 238, 233, 191), ("lemonchiffon3", 205, 201, 165), ("lemonchiffon4", 139, 137, 112), ("light blue", 173, 216, 230), ("light coral", 240, 128, 128), ("light cyan", 224, 255, 255), ("light goldenrod", 238, 221, 130), ("light goldenrod yellow", 250, 250, 210), ("light gray", 211, 211, 211), ("light green", 144, 238, 144), ("light grey", 211, 211, 211), ("light pink", 255, 182, 193), ("light salmon", 255, 160, 122), ("light sea green", 32, 178, 170), ("light sky blue", 135, 206, 250), ("light slate blue", 132, 112, 255), ("light slate gray", 119, 136, 153), ("light slate grey", 119, 136, 153), ("light steel blue", 176, 196, 222), ("light yellow", 255, 255, 224), ("lightblue", 173, 216, 230), ("lightblue1", 191, 239, 255), ("lightblue2", 178, 223, 238), ("lightblue3", 154, 192, 205), ("lightblue4", 104, 131, 139), ("lightcoral", 240, 128, 128), ("lightcyan", 224, 255, 255), ("lightcyan1", 224, 255, 255), ("lightcyan2", 209, 238, 238), ("lightcyan3", 180, 205, 205), ("lightcyan4", 122, 139, 139), ("lightgoldenrod", 238, 221, 130), ("lightgoldenrod1", 255, 236, 139), ("lightgoldenrod2", 238, 220, 130), ("lightgoldenrod3", 205, 190, 112), ("lightgoldenrod4", 139, 129, 76), ("lightgoldenrodyellow", 250, 250, 210), ("lightgray", 211, 211, 211), ("lightgreen", 144, 238, 144), ("lightgrey", 211, 211, 211), ("lightpink", 255, 182, 193), ("lightpink1", 255, 174, 185), ("lightpink2", 238, 162, 173), ("lightpink3", 205, 140, 149), ("lightpink4", 139, 95, 101), ("lightsalmon", 255, 160, 122), ("lightsalmon1", 255, 160, 122), ("lightsalmon2", 238, 149, 114), ("lightsalmon3", 205, 129, 98), ("lightsalmon4", 139, 87, 66), ("lightseagreen", 32, 178, 170), ("lightskyblue", 135, 206, 250), ("lightskyblue1", 176, 226, 255), ("lightskyblue2", 164, 211, 238), ("lightskyblue3", 141, 182, 205), ("lightskyblue4", 96, 123, 139), ("lightslateblue", 132, 112, 255), ("lightslategray", 119, 136, 153), ("lightslategrey", 119, 136, 153), ("lightsteelblue", 176, 196, 222), ("lightsteelblue1", 202, 225, 255), ("lightsteelblue2", 188, 210, 238), ("lightsteelblue3", 162, 181, 205), ("lightsteelblue4", 110, 123, 139), ("lightyellow", 255, 255, 224), ("lightyellow1", 255, 255, 224), ("lightyellow2", 238, 238, 209), ("lightyellow3", 205, 205, 180), ("lightyellow4", 139, 139, 122), ("lime green", 50, 205, 50), ("limegreen", 50, 205, 50), ("linen", 250, 240, 230), ("magenta", 255, 0, 255), ("magenta1", 255, 0, 255), ("magenta2", 238, 0, 238), ("magenta3", 205, 0, 205), ("magenta4", 139, 0, 139), ("maroon", 176, 48, 96), ("maroon1", 255, 52, 179), ("maroon2", 238, 48, 167), ("maroon3", 205, 41, 144), ("maroon4", 139, 28, 98), ("medium aquamarine", 102, 205, 170), ("medium blue", 0, 0, 205), ("medium orchid", 186, 85, 211), ("medium purple", 147, 112, 219), ("medium sea green", 60, 179, 113), ("medium slate blue", 123, 104, 238), ("medium spring green", 0, 250, 154), ("medium turquoise", 72, 209, 204), ("medium violet red", 199, 21, 133), ("mediumaquamarine", 102, 205, 170), ("mediumblue", 0, 0, 205), ("mediumorchid", 186, 85, 211), ("mediumorchid1", 224, 102, 255), ("mediumorchid2", 209, 95, 238), ("mediumorchid3", 180, 82, 205), ("mediumorchid4", 122, 55, 139), ("mediumpurple", 147, 112, 219), ("mediumpurple1", 171, 130, 255), ("mediumpurple2", 159, 121, 238), ("mediumpurple3", 137, 104, 205), ("mediumpurple4", 93, 71, 139), ("mediumseagreen", 60, 179, 113), ("mediumslateblue", 123, 104, 238), ("mediumspringgreen", 0, 250, 154), ("mediumturquoise", 72, 209, 204), ("mediumvioletred", 199, 21, 133), ("midnight blue", 25, 25, 112), ("midnightblue", 25, 25, 112), ("mint cream", 245, 255, 250), ("mintcream", 245, 255, 250), ("misty rose", 255, 228, 225), ("mistyrose", 255, 228, 225), ("mistyrose1", 255, 228, 225), ("mistyrose2", 238, 213, 210), ("mistyrose3", 205, 183, 181), ("mistyrose4", 139, 125, 123), ("moccasin", 255, 228, 181), ("navajo white", 255, 222, 173), ("navajowhite", 255, 222, 173), ("navajowhite1", 255, 222, 173), ("navajowhite2", 238, 207, 161), ("navajowhite3", 205, 179, 139), ("navajowhite4", 139, 121, 94), ("navy", 0, 0, 128), ("navy blue", 0, 0, 128), ("navyblue", 0, 0, 128), ("old lace", 253, 245, 230), ("oldlace", 253, 245, 230), ("olive drab", 107, 142, 35), ("olivedrab", 107, 142, 35), ("olivedrab1", 192, 255, 62), ("olivedrab2", 179, 238, 58), ("olivedrab3", 154, 205, 50), ("olivedrab4", 105, 139, 34), ("orange", 255, 165, 0), ("orange red", 255, 69, 0), ("orange1", 255, 165, 0), ("orange2", 238, 154, 0), ("orange3", 205, 133, 0), ("orange4", 139, 90, 0), ("orangered", 255, 69, 0), ("orangered1", 255, 69, 0), ("orangered2", 238, 64, 0), ("orangered3", 205, 55, 0), ("orangered4", 139, 37, 0), ("orchid", 218, 112, 214), ("orchid1", 255, 131, 250), ("orchid2", 238, 122, 233), ("orchid3", 205, 105, 201), ("orchid4", 139, 71, 137), ("pale goldenrod", 238, 232, 170), ("pale green", 152, 251, 152), ("pale turquoise", 175, 238, 238), ("pale violet red", 219, 112, 147), ("palegoldenrod", 238, 232, 170), ("palegreen", 152, 251, 152), ("palegreen1", 154, 255, 154), ("palegreen2", 144, 238, 144), ("palegreen3", 124, 205, 124), ("palegreen4", 84, 139, 84), ("paleturquoise", 175, 238, 238), ("paleturquoise1", 187, 255, 255), ("paleturquoise2", 174, 238, 238), ("paleturquoise3", 150, 205, 205), ("paleturquoise4", 102, 139, 139), ("palevioletred", 219, 112, 147), ("palevioletred1", 255, 130, 171), ("palevioletred2", 238, 121, 159), ("palevioletred3", 205, 104, 137), ("palevioletred4", 139, 71, 93), ("papaya whip", 255, 239, 213), ("papayawhip", 255, 239, 213), ("peach puff", 255, 218, 185), ("peachpuff", 255, 218, 185), ("peachpuff1", 255, 218, 185), ("peachpuff2", 238, 203, 173), ("peachpuff3", 205, 175, 149), ("peachpuff4", 139, 119, 101), ("peru", 205, 133, 63), ("pink", 255, 192, 203), ("pink1", 255, 181, 197), ("pink2", 238, 169, 184), ("pink3", 205, 145, 158), ("pink4", 139, 99, 108), ("plum", 221, 160, 221), ("plum1", 255, 187, 255), ("plum2", 238, 174, 238), ("plum3", 205, 150, 205), ("plum4", 139, 102, 139), ("powder blue", 176, 224, 230), ("powderblue", 176, 224, 230), ("purple", 160, 32, 240), ("purple1", 155, 48, 255), ("purple2", 145, 44, 238), ("purple3", 125, 38, 205), ("purple4", 85, 26, 139), ("red", 255, 0, 0), ("red1", 255, 0, 0), ("red2", 238, 0, 0), ("red3", 205, 0, 0), ("red4", 139, 0, 0), ("rosy brown", 188, 143, 143), ("rosybrown", 188, 143, 143), ("rosybrown1", 255, 193, 193), ("rosybrown2", 238, 180, 180), ("rosybrown3", 205, 155, 155), ("rosybrown4", 139, 105, 105), ("royal blue", 65, 105, 225), ("royalblue", 65, 105, 225), ("royalblue1", 72, 118, 255), ("royalblue2", 67, 110, 238), ("royalblue3", 58, 95, 205), ("royalblue4", 39, 64, 139), ("saddle brown", 139, 69, 19), ("saddlebrown", 139, 69, 19), ("salmon", 250, 128, 114), ("salmon1", 255, 140, 105), ("salmon2", 238, 130, 98), ("salmon3", 205, 112, 84), ("salmon4", 139, 76, 57), ("sandy brown", 244, 164, 96), ("sandybrown", 244, 164, 96), ("sea green", 46, 139, 87), ("seagreen", 46, 139, 87), ("seagreen1", 84, 255, 159), ("seagreen2", 78, 238, 148), ("seagreen3", 67, 205, 128), ("seagreen4", 46, 139, 87), ("seashell", 255, 245, 238), ("seashell1", 255, 245, 238), ("seashell2", 238, 229, 222), ("seashell3", 205, 197, 191), ("seashell4", 139, 134, 130), ("sienna", 160, 82, 45), ("sienna1", 255, 130, 71), ("sienna2", 238, 121, 66), ("sienna3", 205, 104, 57), ("sienna4", 139, 71, 38), ("sky blue", 135, 206, 235), ("skyblue", 135, 206, 235), ("skyblue1", 135, 206, 255), ("skyblue2", 126, 192, 238), ("skyblue3", 108, 166, 205), ("skyblue4", 74, 112, 139), ("slate blue", 106, 90, 205), ("slate gray", 112, 128, 144), ("slate grey", 112, 128, 144), ("slateblue", 106, 90, 205), ("slateblue1", 131, 111, 255), ("slateblue2", 122, 103, 238), ("slateblue3", 105, 89, 205), ("slateblue4", 71, 60, 139), ("slategray", 112, 128, 144), ("slategray1", 198, 226, 255), ("slategray2", 185, 211, 238), ("slategray3", 159, 182, 205), ("slategray4", 108, 123, 139), ("slategrey", 112, 128, 144), ("snow", 255, 250, 250), ("snow1", 255, 250, 250), ("snow2", 238, 233, 233), ("snow3", 205, 201, 201), ("snow4", 139, 137, 137), ("spring green", 0, 255, 127), ("springgreen", 0, 255, 127), ("springgreen1", 0, 255, 127), ("springgreen2", 0, 238, 118), ("springgreen3", 0, 205, 102), ("springgreen4", 0, 139, 69), ("steel blue", 70, 130, 180), ("steelblue", 70, 130, 180), ("steelblue1", 99, 184, 255), ("steelblue2", 92, 172, 238), ("steelblue3", 79, 148, 205), ("steelblue4", 54, 100, 139), ("tan", 210, 180, 140), ("tan1", 255, 165, 79), ("tan2", 238, 154, 73), ("tan3", 205, 133, 63), ("tan4", 139, 90, 43), ("thistle", 216, 191, 216), ("thistle1", 255, 225, 255), ("thistle2", 238, 210, 238), ("thistle3", 205, 181, 205), ("thistle4", 139, 123, 139), ("tomato", 255, 99, 71), ("tomato1", 255, 99, 71), ("tomato2", 238, 92, 66), ("tomato3", 205, 79, 57), ("tomato4", 139, 54, 38), ("turquoise", 64, 224, 208), ("turquoise1", 0, 245, 255), ("turquoise2", 0, 229, 238), ("turquoise3", 0, 197, 205), ("turquoise4", 0, 134, 139), ("violet", 238, 130, 238), ("violet red", 208, 32, 144), ("violetred", 208, 32, 144), ("violetred1", 255, 62, 150), ("violetred2", 238, 58, 140), ("violetred3", 205, 50, 120), ("violetred4", 139, 34, 82), ("wheat", 245, 222, 179), ("wheat1", 255, 231, 186), ("wheat2", 238, 216, 174), ("wheat3", 205, 186, 150), ("wheat4", 139, 126, 102), ("white", 255, 255, 255), ("white smoke", 245, 245, 245), ("whitesmoke", 245, 245, 245), ("yellow", 255, 255, 0), ("yellow green", 154, 205, 50), ("yellow1", 255, 255, 0), ("yellow2", 238, 238, 0), ("yellow3", 205, 205, 0), ("yellow4", 139, 139, 0), ("yellowgreen", 154, 205, 50), ];