qoi-0.4.1/.cargo_vcs_info.json0000644000000001360000000000100116260ustar { "git": { "sha1": "e97077e527618a07413f7895a3792a6859afac59" }, "path_in_vcs": "" }qoi-0.4.1/.github/workflows/ci.yml000064400000000000000000000020031046102023000151240ustar 00000000000000on: push: branches: [master] pull_request: branches: [master] name: CI env: CARGO_TERM_COLOR: always HOST: x86_64-unknown-linux-gnu RUSTFLAGS: "-D warnings" jobs: tests: runs-on: ubuntu-latest strategy: matrix: rust: [stable, beta, nightly, 1.61.0] # MSRV=1.61 steps: - uses: actions/checkout@v2 with: {submodules: true} - uses: actions-rs/toolchain@v1 with: {profile: minimal, toolchain: '${{ matrix.rust }}', override: true} - run: cargo test reference: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: {submodules: true} - uses: actions-rs/toolchain@v1 with: {profile: minimal, toolchain: stable, override: true} - run: cargo test --features=reference clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: {profile: minimal, toolchain: beta, override: true, components: clippy} - run: cargo clippy qoi-0.4.1/.gitignore000064400000000000000000000001101046102023000123760ustar 00000000000000**/target/ **/Cargo.lock /.idea .DS_Store /suite/ /tmp/ /fuzz/coverage/ qoi-0.4.1/.gitmodules000064400000000000000000000001221046102023000125660ustar 00000000000000[submodule "ext/qoi"] path = ext/qoi url = https://github.com/phoboslab/qoi.git qoi-0.4.1/Cargo.toml0000644000000026270000000000100076330ustar # 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.61.0" name = "qoi" version = "0.4.1" authors = ["Ivan Smirnov "] exclude = ["assets/*"] description = "VERY fast encoder/decoder for QOI (Quite Okay Image) format" homepage = "https://github.com/aldanor/qoi-rust" documentation = "https://docs.rs/qoi" readme = "README.md" keywords = [ "qoi", "graphics", "image", "encoding", ] categories = [ "multimedia::images", "multimedia::encoding", ] license = "MIT/Apache-2.0" repository = "https://github.com/aldanor/qoi-rust" [profile.test] opt-level = 3 [lib] name = "qoi" path = "src/lib.rs" doctest = false [dependencies.bytemuck] version = "1.12" [dev-dependencies.anyhow] version = "1.0" [dev-dependencies.cfg-if] version = "1.0" [dev-dependencies.png] version = "0.17" [dev-dependencies.rand] version = "0.8" [dev-dependencies.walkdir] version = "2.3" [features] alloc = [] default = ["std"] reference = [] std = [] qoi-0.4.1/Cargo.toml.orig0000644000000021330000000000100105620ustar [package] name = "qoi" version = "0.4.1" description = "VERY fast encoder/decoder for QOI (Quite Okay Image) format" authors = ["Ivan Smirnov "] edition = "2021" readme = "README.md" license = "MIT/Apache-2.0" repository = "https://github.com/aldanor/qoi-rust" homepage = "https://github.com/aldanor/qoi-rust" documentation = "https://docs.rs/qoi" categories = ["multimedia::images", "multimedia::encoding"] keywords = ["qoi", "graphics", "image", "encoding"] exclude = [ "assets/*", ] rust-version = "1.61.0" [features] default = ["std"] alloc = [] # provides access to `Vec` without enabling `std` mode std = [] # std mode (enabled by default) - provides access to `std::io`, `Error` and `Vec` reference = [] # follows reference encoder implementation precisely, but may be slightly slower [dependencies] bytemuck = "1.12" [workspace] members = ["libqoi", "bench"] [dev-dependencies] anyhow = "1.0" png = "0.17" walkdir = "2.3" cfg-if = "1.0" rand = "0.8" libqoi = { path = "libqoi"} [lib] name = "qoi" path = "src/lib.rs" doctest = false [profile.test] opt-level = 3 qoi-0.4.1/Cargo.toml.orig000064400000000000000000000021331046102023000133040ustar 00000000000000[package] name = "qoi" version = "0.4.1" description = "VERY fast encoder/decoder for QOI (Quite Okay Image) format" authors = ["Ivan Smirnov "] edition = "2021" readme = "README.md" license = "MIT/Apache-2.0" repository = "https://github.com/aldanor/qoi-rust" homepage = "https://github.com/aldanor/qoi-rust" documentation = "https://docs.rs/qoi" categories = ["multimedia::images", "multimedia::encoding"] keywords = ["qoi", "graphics", "image", "encoding"] exclude = [ "assets/*", ] rust-version = "1.61.0" [features] default = ["std"] alloc = [] # provides access to `Vec` without enabling `std` mode std = [] # std mode (enabled by default) - provides access to `std::io`, `Error` and `Vec` reference = [] # follows reference encoder implementation precisely, but may be slightly slower [dependencies] bytemuck = "1.12" [workspace] members = ["libqoi", "bench"] [dev-dependencies] anyhow = "1.0" png = "0.17" walkdir = "2.3" cfg-if = "1.0" rand = "0.8" libqoi = { path = "libqoi"} [lib] name = "qoi" path = "src/lib.rs" doctest = false [profile.test] opt-level = 3 qoi-0.4.1/LICENSE-APACHE000064400000000000000000000251421046102023000123460ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. qoi-0.4.1/LICENSE-MIT000064400000000000000000000020401046102023000120460ustar 00000000000000Copyright (c) 2022 Ivan Smirnov 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. qoi-0.4.1/README.md000064400000000000000000000053221046102023000116770ustar 00000000000000# [qoi](https://crates.io/crates/qoi) [![Build](https://github.com/aldanor/qoi-rust/workflows/CI/badge.svg)](https://github.com/aldanor/qoi-rust/actions?query=branch%3Amaster) [![Latest Version](https://img.shields.io/crates/v/qoi.svg)](https://crates.io/crates/qoi) [![Documentation](https://img.shields.io/docsrs/qoi)](https://docs.rs/qoi) [![Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance) Fast encoder/decoder for [QOI image format](https://qoiformat.org/), implemented in pure and safe Rust. - One of the [fastest](#benchmarks) QOI encoders/decoders out there. - Compliant with the [latest](https://qoiformat.org/qoi-specification.pdf) QOI format specification. - Zero unsafe code. - Supports decoding from / encoding to `std::io` streams directly. - `no_std` support. - Roundtrip-tested vs the reference C implementation; fuzz-tested. ### Examples ```rust use qoi::{encode_to_vec, decode_to_vec}; let encoded = encode_to_vec(&pixels, width, height)?; let (header, decoded) = decode_to_vec(&encoded)?; assert_eq!(header.width, width); assert_eq!(header.height, height); assert_eq!(decoded, pixels); ``` ### Benchmarks ``` decode:Mp/s encode:Mp/s decode:MB/s encode:MB/s qoi.h 282.9 225.3 978.3 778.9 qoi-rust 427.4 290.0 1477.7 1002.9 ``` - Reference C implementation: [phoboslab/qoi@00e34217](https://github.com/phoboslab/qoi/commit/00e34217). - Benchmark timings were collected on an Apple M1 laptop. - 2846 images from the suite provided upstream ([tarball](https://phoboslab.org/files/qoibench/qoi_benchmark_suite.tar)): all pngs except two with broken checksums. - 1.32 GPixels in total with 4.46 GB of raw pixel data. Benchmarks have also been run for all of the other Rust implementations of QOI for comparison purposes and, at the time of writing this document, this library proved to be the fastest one by a noticeable margin. ### Rust version The minimum required Rust version for the latest crate version is 1.61.0. ### `no_std` This crate supports `no_std` mode. By default, std is enabled via the `std` feature. You can deactivate the `default-features` to target core instead. In that case anything related to `std::io`, `std::error::Error` and heap allocations is disabled. There is an additional `alloc` feature that can be activated to bring back the support for heap allocations. ### License This project is dual-licensed under MIT and Apache 2.0. qoi-0.4.1/doc/qoi-specification.pdf000064400000000000000000001147151046102023000152750ustar 00000000000000%PDF-1.5 % 4 0 obj << /Length 5 0 R /Filter /FlateDecode >> stream x\{8ߟXp">DQm &smvtȶƏNOf~Umٖ#-w2Y,ֻȢ'%)_1PL:an* "S 8Ƈq$~T}>#şBuV*2F) #CӪDc DO@ASV"0HEª k'bBK TѲaDȪ71A55 ASàw%xhO汼8D`%R60~=WGl\HCX EpJDǀH OXG-?؄hM%&Z.ga 1Ug@h L@*ZP@Ҵ e]uX@*d$3*/(NJB<HPW cMZ%46Dh"sJ,&IxIOQZIeB pTAc &(le!^ Sf" mXlfq2@trAhb"W =5C p萘 V:- x1 Mt"~`MD-QC,eRGeS n R)cX("a!j+LA- (H mY޶{ ;8bD= ND1X#%P('ÐEC IjZ P;v7^@J;"kK`6$t!܀$ZIViP ,J06ƠXA Z7J^WBV  ?!P36V:<4ٶP3h$(ᑡx#_? 7CYo+'DOLa!Q@46N4,(+-uNz$d$@8OL>]Ik?`RH&l6NRޗ=᭾}ہ,z T&Bi8vza/};뾽O|KL|mw^Hם l߼~#= C߶u"n[`+57ȒKok(C\BlaYQb  ugW *0,ŝRV<|4/`iwi>ϿIX~I6;qTip_~|#F$E)# iz g0+nh>쳘- 4ĞxtNzp},`/z C3<ǬO-}A!mħCN&kL+`;`xqmEIxJXԶجEBIdXAò&s,p^S:9gݧ9iX` e޳a6W:BB.C6CQI bb1I-=P?_,S 'jPQ?-C.DH k[)D b@\PWQDֻZoŘw#g9ŘZEӮiɔ')qp;ՈPdȲ%>%j&} $zf,.dyQJRWzoU8r$Lj! $X`)+a N dZ>F)^ڑbK'uғ30I~$82f7d(z+HϒܩBDo#2G#,i5># dzmz%{ 7Mi<-Y׶gcIӲ\N CŒLH.'aB2wqOX6kSv%W`0!q?w7!X6gVRl9#9o.5϶CVO]׬-t nHjJ!0:e 4՛ E3{2u:3/Pjb+>/+5T jЇyS*qU4lV:űofP봨y5ˤ^e.7ăe[@r{8?|z6ƹ}C!Hپc71+%4__7|>V)-Lv ҋҗwR/9kB\Ƙ;̐ Oy#"ND@c-N&DL& X| QOYLNL&r!JG B2T*orA6d'I˝FRt:Z:1]f+=` Atc8GNPYOv1dפ@yiSgE Ⱥ5c톱06^6ZrUhGvV४] =w4P<9ַ-L~uw4/Py_\?ހh~ԡiyjpxiX+྄gB |a:mѱ jOf^{ʟ!NNA^Z߶0_1]6?B nҏӎ OF)~n?Otw]ˉfz q` P/OxӤgUG/÷wкt.I}tߧ_H񑚖 g1ޮK69uݺd$ Nҫɞ`ktլeSp C;s ߤoB(f]ر+4>@'@2);6#s#b4 ?yæS6K%:|&uzl\Uo\۹nXVY@X=.vִYmG_` Sqj,LPc͚gKXtsC[χmWq1/K>Tt^"lꇍT;y^͐D1'\Thf^`E#H:u9ݎ|﯐|qSrV\l@;mӼȰ8)LOo3/6N6cTGV5o::R 1ʪjfC[dlTC$sL3o엘o:vD* lI!+&Vv^@YEW!Ix6j&EOSh~SLV.'\ c`2c(:{CU^c߽y8/;F 2q$tUE'29_$vXV2+9T+åD$Ik|`2Tu[b^pX|6}b]DS.n%Tҍ㚷sUA Q܁^yvGnG.2BwovtoeV]WQ<8U҉8TTԭadnډNj %|]bt0++`.  FM,6 "I,JdS}͗=GYiǯ}tޫvd/{ESaT3u9Hlƭ+adupX!Q|af?ڵ W;$S5rtkp9^o Nk@nL 'pyA|< q{,Be1H0 k͐"[7Z}*^Us$NѵaݿAw7X^)gD7AZ" raBk2M?_?mFCxi6ZKuk/ hscz]WKmpt&X߁/0_V.)W|R/Q*:h&gr8VTOR;$UW!CPm5}.dXuXjON6!}Wö%;{*_a ~s2R])v>ެ7b6ڵAjo9ط—cM÷fG.b&^vA`\i ~~w]dw!ȿ}|gW^4,Vn}u'6́8;j𫫘#8M㠚q endstream endobj 5 0 obj 4571 endobj 3 0 obj << /ExtGState << /a0 << /CA 1 /ca 1 >> >> /Font << /f-0-0 6 0 R /f-0-1 7 0 R /f-1-0 8 0 R /f-2-0 9 0 R /f-1-1 10 0 R >> >> endobj 11 0 obj << /Type /ObjStm /Length 12 0 R /N 1 /First 4 /Filter /FlateDecode >> stream x=ͽ 0Oq禵B A&%^J$GۛDx{+8Gy_ aR<*0 zc?xDb845kcݡ)Ⱥ-3iK.+m5u,I ]¶9$mތ2 endstream endobj 12 0 obj 148 endobj 14 0 obj << /Length 15 0 R /Filter /FlateDecode /Length1 6444 >> stream xX XUU~!8??5jѠ":ΔWQT1EQ+$#3=Gr¿ЩNt+1'Rk`q}j<}{Z#Tw]KKZi/\y0QR:a/QvՒs#f["7P_l-)j'-M/P*gjinxjqJJS[zϼґO1/ JD\·:0߀ JI3ׁzLGFgl1CQ~=J N?K+L Xi#<,C3]A 68}!C=TJjJ [ B9}DOt_n6td68@+aX4ZRmO4Ρvud^#Gu\k٣29+d찁 l7(8tPhK9&mCZwx{w讛W|e=J$p2J*| كqNI$16b!ۯu\L|W몋C>˗q|.]nb\"isQv$iN`S!+tC顓+O_x|7),<6ҝ6l!l83Hvx婽4W &p`*E0leyt۬lƘ'k-nK\ qYTݤYW-/ϦBHIfTVf&>Ieiw D7ö6DO>3^{ח-a#-"(>A6 `ql|+vl!nuK<鲛3Ik4.ɄqN/y~x]|)HB#lM$s@[M/w{vv޺vƒWYM#%M-R)/Bę^qzkT]CӞYGtE+Ksix4<k|lY_m4 znհ>2Ftdk2Ћw-Q#&ll1+&:tv[u5_=y?IE5b = 0ClD1aB[}g'f7i~ı^?]UζŠ$B#\ ۸u6Eh,8G비/dEA6ipEC(@ft:O=LأGRIdɠA\),7oKKrio_ AX7Qx2X#%[aXPy |b^j~+5&ܢl×uG#$|_!ePXF`>m|]crw_tgvw }ۉ';! /HdkR:}F^ QbG X2x"^÷r% ӚsQ,%@nťs]^ 5Ǫ:EJMQ0.QvW=lQ8"+TT.+I%$au~#fg/1}fgŏ < W/MvFRݶ_]w\{ >H#@rmsJj%mk1ڙAc@RԙL-H b.1mqwg2 o/#!?9 7 $ o 4y]VV$KO?BS-K=R_S$aBR,_v\착5>7`0yUck7Lz橢}RVIŜs{Sn~dm^W>:thCrrWlɳV&Dxbe E`h`b08450;6Ej-33lv"QUN}EFןaUɗ009;|]4-$j'ɣpێvذqGPٍSM}yoޞ=yAmm'},΋K.KCzsNlĜ } H ؕr8y^ϙakOB?RpqguJM b|Cwn_6dH%d9=b ڝcc<H̏ RW4T+MJK2RUcھUbr4*ohܵq}c.._T\a6|ɓ?:qfqBtOϣC_Db4a5hhJ5wGΜ[#c${1=l~&q`ExkCU2~dmt6$CQd5In$Qxܣ&Gv2pֱ!fMOrM8$ZtAj[ !nzvc~¶~B}?D)} E;<͜;]IaI/?/-?wŅW*:yzSY ~47<{?#ͱV=tROLګ԰pd;Yx!7 VGֵ6lvt*K\2{QB]wY[kc|/XX9YŊSsEi3O>Vޭgf֋[?a/|ll]iYe P4c].p85*n]\}g3lF/3 /NRұc/lmƔ[ٍNڐDnev6"ùY??9В2CÏfpN;#h_df&!Xz4J!`'cRw7;KѾMeE4o<mlcTN4.+"|7{jjk×*Ru}fpu"FU. Ahr`3I.zi5VKiԷ[f5b l"dQ/9.56yy:EyW*iȕr\TdAY,vѐ 5*H[R4zӰ"?M(};Y5xcgIVt%i42#)HzkN4~6\RUtخ.v,:PoXr<jz-EPCko(%l 鮕 mui\\Ck4=JSIhXQIl&IQMi>[FZ,lXHO@B W_s {LNH2Z Z:(1U$3}iCKӫMPdY ̐&߄~}O?H2`{옾Ui3QkQ76m~P䝻F]8b:oD@38o?9xa u8˷n4t%\D˷;XgDBK =(Pnrj&Bn';म޻e&8)e8Pʴy}᱀j=i-:pH>E` *pOT>YOTE$$pO . x ,p\-1oM8 8 pGE8j DH 'px:0'sT607fY0:x 3M<Â&fCUL3LL881Ŏ|Ѝǁ~~& L>gGo1z:M*ex`N0c+0RhRT,::F v66eQVs4(4 4huTQADEA,r) YX ? endstream endobj 15 0 obj 4513 endobj 16 0 obj << /Length 17 0 R /Filter /FlateDecode >> stream x]Rn0+"IL#!*pCTeȁ7J`ǻ3jLrk74B?:p t8Ne9ѬS|*!q-+NgU|pY';w y }]i5WpBB ,dvBHx_['sy> endobj 6 0 obj << /Type /Font /Subtype /TrueType /BaseFont /PUPNAP+DejaVuSans /FirstChar 32 /LastChar 117 /FontDescriptor 18 0 R /Encoding /WinAnsiEncoding /Widths [ 317.871094 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 575.195312 0 0 294.921875 0 655.761719 0 0 0 787.109375 0 787.109375 0 634.765625 610.839844 0 0 0 0 0 0 0 0 0 0 0 0 612.792969 0 549.804688 0 615.234375 0 634.765625 633.789062 277.832031 0 0 0 974.121094 633.789062 611.816406 634.765625 0 411.132812 0 392.089844 633.789062 ] /ToUnicode 16 0 R >> endobj 19 0 obj << /Length 20 0 R /Filter /FlateDecode /Length1 2600 >> stream xTktTw{fLfrgBL<b d"hIŀD0 1EJ%$!ji!T[JK NVjv{=~|ιQ4([M%!Ł-| W!>.7os,H!8\g-^hG-(#i7 ]4gb¼Dp`|*[5"UN& pP=bdE&2N<ӗO_Ǒ8R:uW""-/?[j"—ovQ<%l#^ңc9=!/s>2Kxپ5*-#CbY{j$hTH=NO͜iz~+[ZC_i Mn7q;g46L;x,jvS Htjaڵt)M:i)RDktH8h.mŬ>C?[Wz^eb_5'MNzQv*.!G MP0 .0̸4#Ƈ^\P.qA /3a?1yG{w}5ζw3b~]'jwT'$bkƯA>Ph_zZzcN&81~8)(qāֵhinS-% 5W=p}bAˌ%?^jMN쓤<';nz΋vȏF1i YƮgb.3ع#QcvCHvb4ckЦ26l-AldWc? 㩆6D5FңKPӟ`#cQ'eMǭj [NX+ZA1> stream x]j0 ~ Cqslrfhd8d7t0-$}?Yڿ2Ov >c\a)j.{Uo;öd{Q- ^\@C4:> endobj 24 0 obj << /Type /Font /Subtype /CIDFontType2 /BaseFont /VYJTSO+DejaVuSans /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor 23 0 R /W [0 [ 600.097656 629.882812 ]] >> endobj 7 0 obj << /Type /Font /Subtype /Type0 /BaseFont /VYJTSO+DejaVuSans /Encoding /Identity-H /DescendantFonts [ 24 0 R] /ToUnicode 21 0 R >> endobj 25 0 obj << /Length 26 0 R /Filter /FlateDecode /Length1 12856 >> stream xz{xTz{=k&יdB CB2PJ 1!$ w0P5R)""Ŕ(ĚVX>kc"d{ z8<';ڳ.^ (da)we$N|C'!=j 5jϞWP<ܫW?0O #z?!ht9!Bz60>+:{A=kz<1GюMx=Vۇ}uSkG# %Q$D8[ID:z $v/i?ӞmAHl>K7׿r bkxHdv$Qb DI I"Tѣ^٢7E3 "Ȁ I}Ir8 U|Wg'H|wB&D̸BP(PI*Wt^>MTXB$.s#q5lQLOyVrc;-Yc6u՗[af/Ώ>0~`T2hج_\ _@ S L;A7߂S?6:_;PԳ'T1hͪ0c%@\oniv2Y!Nm0nQL\3maKWϴيՉA|T3PB~;IXʍ?J^~tNgL:P5#= d?(!/ElfeS2AYCWFg'jA7zdo Iutngfa+2.~'q^mv\%i3j~t̃=@*#y>,ϕs\C1הk9C^ܴ܌\o?+U 6m.f)2sl Cȍ‚*kP.O=3{ʻyퟟy.RPX<,̜ٯl EKdڳwuȋӈ,$21Q7-mT n(:LI[[ 6ߡ _tGcfK ?/TV^Dw0i*MQsQDue#}ۘA%ڑ%C\ $IhtF}[ HW:W7^z .` `*\cvHl$ u 8~X@gj\A*۪'Żo̝\o񌧖u.VNhzwǝf ^ёzY/jz[Lc*TM?#P0"v?Q1_xfgdVB4M[%qCVd{X:_dH$dxY*,9eGhB>$B)-eBX*JrRjMFH:FG#цrZʅ2L*ӕrְJt5FIvNvw;k*}*U~Uy*J.]u ]bԥw]JM$9 'IvR8)NNO'V [rjȪJЬ+nwLfUfE5 mVȤ~oAɓM?#U2*kXLO6ʊvP'%bAV[oEW{'mO[[qB* : PcvAAϤtpJ L\+6 i+6-]lYƓ\"E>'$Χ>gBPRJC!o7KmRGԕZ &Cvwڈs{|#lkٗ un,-/`} Be` =})t9;tF; ¢ 9[,[sKn_n.ʤ-mqlѭOP X%j*'͛7tr,i㉪<aMznۡ+OMS^ o({z&mp. UB,K"YڊwTaľ č''PbA1F񂉙00 3E`52b h4[c3%(iԫTH2 hKuАw?w;N~A#O[/be` *Z,_"K?[q%ON{p 0_bf! dFE#$ :P$XՉ^hS;~ ʹ{ |Wr6Zy]08>)PPꋱogk m|Oˍ{:ˆSsU }wNeR=dpll7`hmE{'gT=8\ mBEA!IgLl!фW& 4d+1lٰj69U5[9UR=h8f?}{K}hbyG'[?o1:*x\-99< vy*i1KA$:F6E^;˨ ֎0ri T<{/a!՗b+KK5S\N-F5@)ˆI b,8|;Q!o9f=zsySXG?,onZFWlS9 9@rO$Mjo!Pf\ljksh6$#ۖl2׈ v$#ߊtZ&4r ;UܱcNCÜߙm7L}}My=g0z}}+)iorvC}NT|W!< Q$IjE4 %ŶSs"fF E$3p%V]{\~;aۀVQ[D 8HF5CլYO,-X Ak.-GNœ'X8(w#9(+ ͙EFk׉(Fbb *"ez&-Ѡ`( AGBBR ZE|߮̃Q2q*%^R/KS4 }KG Hn=`|<ۤm&/7چ/|:9{ha{lfzwQo1YGwƫumIX5Ew nwSV`Q^4%BVFG$ej~ 2]%|u+bUeR2َ21"D?EESd5 AA(-"EൄW/Q7Tr9oE:M2vY*3Qɲe2U; >WDŽ:{uMuݍf"]$zhd ,)1š8֞bz>0sv0|Ȫ.P6{l}[[Sp/\3tjQLz7lҤq-L~כ8 g$I_+ FV],Z ͨMIv#<lWZZ&]"qO 5  cSـorR]B?پ2rY_?+/%]>LH =F$GZ{}NF]N%Y&zi؄En&KqGjI,^#K7=N`O0S+OۗNlKpֿ#mù??dWL^i+#4Z 1 I1%+QgyjBT%VdʴHDX`YPBHbVq՚k')`I1)~e23:؆I*NZS}͝1/C$a髪 >a5CzYǰ|7:ol]LߔN ִW:gOeaϞ=Y˅(㙚SPRk9 J6!7iD"hh[62jaAPbmڜ;g۠zO<0jȋU{4p̱үw7Oo]-+31?`.IEM PZV;H }(nT@(-?R#{2TtPmV78颰 &E.Nb1|wFgSr9~8",,h>|]^GSBH q!8F͈n$ 3i$p"Xx6FjAY"}TѢO5-c$yU*YJ-%]uf VhAXjo`Yw+-s[ڒ([Z^]oɃC {.rό/1z f67wܵnl h62"j200ϱ %HXlx. 6]-ژ@lbNo(mlAQQ0r?فG^86VÚ$THa$A%iLFYƠU~":㻰ةiEFc]]5ujU/ = }~o\zի7_{_Cv}~ئ3IP*iɰ,k9,f0#q.0suʿJ x OK}yd` oyuhcrXJ-a&|~Ub/va^K!պHeg01_\OLĸÿAmW[SZa}b@H- $*XD1!=RvY?u+ҙ~<{_.#W$ jVU!(⻣!mW n`>AMh6l]p+g,AGT74T_7:OcE8n+OKa\N1Nb)(f ^]6IWODwԼ5^jmR% ïyaX׌cٛo o okߐiK]8{L{Gb(AgKفg97@ *0 ~ۄbAA? |T&ݑ0՟E_ɡ04ȘuzL2rBIc(K5 1D* w=zl"v %zNJNCMTrQQ0`:^ #^MWDD3RzE2&*/j]$ExS8~ˠEvp\|+~QI/fz&d"FVKhb_4!Ec-\_:gO߈ 12Q㌷m}ҏN&SrY^,8LN雙걐=;w­oG׮=vlڣPZ\VVQYum!HQ [){zQ-r*\:4 5b+|JZXOFN^!oO*ۥ/Nr TqLi0`ß 1Lϡ[3vΣ;7`ZHV"2r` ԣ 53lf Uؾ4[p x4'_zt!g6E-!}H,UILz;}?# $=H)WEӡ'H!=|vBJB2!YH #(ԉ7EuQC4N#^|1>QS?SqlF$ v[rO cKId̎c[?Ƒ#^7 Fv kn>[|IF9Jm8i!W!O߂ %; ~o,(PL1jEv_a$2_ o!}H:ijGvY灀LHN]W6 o6=4%TiFDPȧYn'sqUv>+FD P) 'g`D%΢ G hvIfcpZ#?u3島}5-1w&{ޭϪ󵐲SHWWY*NhZXP"]޽FUZZl\^w5\kSm_UI듁OZGff7YN6N7ٓFa1qgU돱+uOul]Dx"֮k [Vq[f+;5l-kbKkqI-Y8[0$7y}:aZ9s8h8BxtFP|=faX73 w5l:װi8Ҵ 6j8{jΦ,eUMF})N!'fz nVf g쇜3V6Ǝqcؘ28ʬl4g_#>a8و&v{sݽ5!޵ wxP3+ !66,dA,1A68IĊ$VB``"BQXT{XaG, 9d1\bs_y"n18 yX|'e{Y#4^y:B 4ywZ񮧤=ͬXyPzXXHXO, Ya'Lsa!̇}̛),O+ad)1OmBJMLq1ƒ9=(x,i$scw&*b" qf ̊0+DRf;s3a7 f4XDc3X:0}|:\t eAeQRt"Qf dAh($ d}@#vGff_;>D endstream endobj 26 0 obj 9041 endobj 27 0 obj << /Length 28 0 R /Filter /FlateDecode >> stream x]Tn0+xLdr)&`H.>Y$_N=QّV0O~?͎y9rx[tIfk!E[~.VZxi?5Y;Z!]׮Oɴu8M5%iCSu]6AqYL=8<#p"NZqYLwRqxr<8NfP'j>ӀQ#{x~I0"u"t9662ۈl=}z{դD͇9D͡f/'߃ٯGAkA, 򏬍UGz=NG::l } |л3œE{ y9}?8zxp_أ@~P>9e}L s9-2qTNs~ˊ* endstream endobj 28 0 obj 528 endobj 29 0 obj << /Type /FontDescriptor /FontName /OJFBYP+DejaVuSansMono-Bold /FontFamily (DejaVu Sans Mono) /Flags 32 /FontBBox [ -446 -394 730 1041 ] /ItalicAngle 0 /Ascent 928 /Descent -235 /CapHeight 1041 /StemV 80 /StemH 80 /FontFile2 25 0 R >> endobj 8 0 obj << /Type /Font /Subtype /TrueType /BaseFont /OJFBYP+DejaVuSansMono-Bold /FirstChar 32 /LastChar 125 /FontDescriptor 29 0 R /Encoding /WinAnsiEncoding /Widths [ 602.050781 0 602.050781 0 0 602.050781 0 0 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 0 602.050781 602.050781 0 602.050781 0 0 0 602.050781 602.050781 0 602.050781 602.050781 602.050781 602.050781 0 602.050781 0 0 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 0 0 602.050781 602.050781 0 602.050781 0 0 602.050781 0 602.050781 0 602.050781 0 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 0 0 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 0 602.050781 602.050781 602.050781 0 602.050781 0 602.050781 ] /ToUnicode 27 0 R >> endobj 30 0 obj << /Length 31 0 R /Filter /FlateDecode /Length1 2580 >> stream xUklT>s9z6`ֱ$~`XHqL 4؍I <@\S 3t>S{]|:E0-jh=Fid;D;EjXh&Eݠ{|Ґ>mQZd-n ͔Tό0Zq\=KWm1nK޽2jnS/6ChTEƬ9RyT-y;2PdqDG-Sw=K6֐QwŌ@}8jj%?LQc,c?>1جgpKgpt646cזN.kߪfy,qYm8 Cu`!W2 \/HScp-P% ۻYƻ|{JccOM}ُSZii9dmcLYbimjS MVE!++\R#tmjbd>Hu|erAF,K=|i.zpwM''NΝ=LϞi᳇q_yϴLH]abhNic =xj)R<䓛p"&~~fh~@ԇ#.>҇V6x.ّoq(A#8Яk?`^'^'D}Eow<.E_]^Q u!NvcGxm-[&E=}+#=~r֢+7mB#x94@DD(0:$RG]`bu}h3jE/DKYd9k(טx!oX*W* +yeM.nF 400h0\ރgK}XVe}>,ׂxƍ%3yOLInU>j@e+}"80 <߇yP6ϫEs57en.BYRtiK($VIq&8QW*rq&?F?Հ'ӀB" Cj }(≙^~"|%?=˳s'CYBL/fzx Hѹ9n w2sȱrZ}^Lo,ΎbZ5<$dw.A]A83هL2!2yHOsrzҜHÞ'ڥB{:V))lK-B5B9ZG#'=h> stream x]j >mw!Cд$+4*5ldf1:V`Fy%B6 SvN,*Q_al Wa}mU8kP8^;MFE]uc!2$u}gFdUW WШ%A:O2ũ5HT$:ʬS$z$:dSr钴3U܆z\em9κ-EB݆x endstream endobj 33 0 obj 265 endobj 34 0 obj << /Type /FontDescriptor /FontName /EELPZR+DejaVuSansMono-Bold /FontFamily (DejaVu Sans Mono) /Flags 4 /FontBBox [ -446 -394 730 1041 ] /ItalicAngle 0 /Ascent 928 /Descent -235 /CapHeight 1041 /StemV 80 /StemH 80 /FontFile2 30 0 R >> endobj 35 0 obj << /Type /Font /Subtype /CIDFontType2 /BaseFont /EELPZR+DejaVuSansMono-Bold /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor 34 0 R /W [0 [ 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 ]] >> endobj 10 0 obj << /Type /Font /Subtype /Type0 /BaseFont /EELPZR+DejaVuSansMono-Bold /Encoding /Identity-H /DescendantFonts [ 35 0 R] /ToUnicode 32 0 R >> endobj 36 0 obj << /Length 37 0 R /Filter /FlateDecode /Length1 12256 >> stream xzy|TEoUNoNwtB&!@&YdADL d 21рdAǰ8)޹D|^ԽRu|9uC(!D%H™Gcx$'3*e!ޥXOX#𭭏=1g5z _;'$xG R:O Jz\`Z_OL(W=]JHMXOXo D%LdqJ+w@DT1d4jHZN#mI6_-i@3 6?^~ZJw9vfFxl@D>[4 Hz-x`q/ wZB#HODwt b}$[mFT:*RɈUeT&lkt$-WZ:\\99"ٗLLG;x%9>)8lt&{#8s7ug^Uȡ| 5(TSSsvon/c}Ur05R51+=Jw+%d') ghS3H`!dRd2 cɣy,<1OʓHb˲!&7#sP0s@]M5!,_@vę֒ a,XO m^z! ]B[F6GՒVBx׺of9+vYƇ ,M ĹM(ʜ&ICWZ-lurr9cp+[jeJJ/|{î]ǧ459ݺ|ԥFaCs-$b0/@Tf'ez{`>.mi-&Rي^Zq]?n:sG?IiZ;pzJb C Z%eOb 0RI@z{sK{hIfq⥄~BO"~& ~'$1Z>E JivoȤ9S'Ӯ܉؟k>BjB7v)9|lBy0ֽTwFVe#T dUbjhB`rXՖHiOkV-@~k6 mѴμDyEElݵÞE= s:֓¸3$ ql3 I&RvAI|$;MeP/*Ey@h{20@L/Jo=]$ޮ#݃v|{K&|2?/EWƼS BX,JX-6G1q/>h`{$ kW7_?vUc>2i$O~ 7}}tT:dHм/yW?!;fQ@-}J %s\ޠ*_3#f)mlnjy܉ Ư|+~=߾h'6(C']e}FGۓr~⼆bMP90!躋t 뤤f{PWe?Bd˗.XRwk.^lx"'>;Ϳ`\lm+njT }x0_QЊRa)Hb!QXvn99aաIl$ymf%iH;ɭ#oqaړkx更/,1`GaEE Tf7S)Hd@f~ -MOn!B쮩O-,e믱~ŷ_o]"2n9|_4ĘJ#ZIiި2*&n"RN_dzCCLB;;܅lCx>*ѮVp]mUa0\S0WQGG&95'MCz|(-j]#|:9RO;I>hKJo-: .;:PdKo#^8I]ЂSf ǐ_;~`(J !" ZHH[ ZBmP *\[3zV.PݘjزbGF_Ǎko؎,ZnVIB~ \9O%3_\azq!{:8I)+8J{e6WA@ɽچ\(ʯjhwa]4f_t7d0O :ި-[کW^pBڿ~^o}v9aՂ?oKڂ١@2(P.̥ 1LtI&@<}eUR(JRO˄G 6yk D1VY'қMb3ٳ"RDKpRVBV݄.bEi8LHS90[#͑㐌#l4@Ty[/ye#86򑭽YBL&lEW0+\A|0(X$ZTP5\IRhPdI3L 2T0=aS u+BM1*rF&I0Ȓ.c] IC.$Mn5oW'[>(z[xF;Zi<_F_xro x6Kl%8+ PeV홦L)s;"RfNb+']r r9FqzC%몖,Z$b.yUBN h 4Zk5dJ T:+,UnIl.Oy]}-,뭭4y؅F \^LKCb?./JT*%vLL+ce2ZhN#ý 9TssAM0N&O$?@# x1ʕ"EPJ< YLm`ivv2BM 4 0@$;'I<ܺD~ǃYzZG! Y(b掵 T[hW7̀Y ??,,D_a'Ձ^.s.`,e`F#,.9"Ă2yͼ/fDhQ]{EjɎnfbsc)]:V{):"Ŝbj+GD%fڎfkSl;aEht=܁׍iW!.cd=y-#1hݤɡxYCqH1efGRe/#ٍ}Z;.®By^8E{Fu*6~}!볧?% }GuOa]_{jES6|6- I8/e+xrdPѸj9 -"ԛuy6?X5N;mm"o+ydPI_>uzwZ}H@!9-*]ߣ,B= 9K2D*SJOzSmjsc1x$%At ^h.Fs~pD5lUkJZÏ,\y+Vl༹@ )f*/,[ v46X3)І#Wg? C@oD>I\,i<'\\Qa w )&Cӏvj7vGxc!khUt?G,Q}W:NڵK7TEXh=,CE Wáh\8"/L MvQ&j@5Dc4 Zm<֗tM?p3uH.কVRiXj F<,:|NH|}0d$ _,|y2~#bF%%6??[}T[Ul m$[WB "}0(F/գ?ͣRH9mLd }jm_ߘso__ޯadH !`֓2C墙-!>uI鰻), ,YrJ̻}YЂvSo+kԨ`7bpew:l(jl]g-74i$FET*:HY&}4$w"g[b/>g!5CHjXn#)X#n8 w)ʢٔDѢ(@Ldm& mKJakœLiPc4v{+'DW Vuʨ k/CR"e)F&r;C_Ҙh&g^|`'?4YTK #*q ZZT$G'!! SC)YBPˏZϿǏM`;֮[W~Wp !pv/Tm[4c^۶$&ykަМ/4"Ā-RG2S O_h_Vg E8 !EH;1=2 e3=r̹+~si,E*cl<͕UBӤ޴@F%-ՊL0yK)?E_l%N8~3 el|oFLeŨBZ+|)&|##/EHnRѸͷ}`t-)l8hD3bQ,FI¼3f߱pD6Yo;7ڽh_:3ރ>z'ܪtvH2%fto(a$-h]8}o#Yz;9w q&aٻ]Iz6eFԆx-sD܃>DbH;MI2qOGOoWRHsSGꥆ$@VYQyN&|Wϡd$&?KuC(4mҎh'y!OZ"xYp3TrBḽ\ri#ӷy,a!#֓P ɆŁ8Ixm/%+97P+}ސw ZLE2 id"P@ʹg0(gPӠ'$/^"%tAOfhD oˡr>Ѻ xhDRz_Trv2x{Ĝĺ[bŸS >ߺy.}먫WpċCVxWOvч'Nܺܛ:17]gL6QWICgV*+DIr׾)\~!և32Yh44bY5v!|mYOcQEXR,G:|_k K rXCYC^x2\>2c) =O`9a2*tY2"KNxܘB^B'9,̈́`*#jpB XVsj؎Ɋ >G3c`p͎}ڏ ޏsG!N(NXЛK `Dwg&Oce4è SգqI*X@ގqܪ-H:@yĎ=|4ǚ ]P#:R@23O)H`@e\ҟsA_8 Unr ?·]!^+k+)p5 \I_}.gqÿ2CKݭ->pBx.·:Vױ<s_Fnp qs8٧N30:q.4a)Nq8* >pbM<URx!9|~8a/{LPn{  wnaWԕPÛfv hru;pxmY|-6նٔ.e>lJW8lgB'!d?E@b5 k[kvÚbaK>qhXVqX_^A9*}+P V6Xa)%Jl{8`_ 9φpx~>sa.Y083( 13{v=9TVO Em gTS2a xl7L0C! =UG90ØFqF¨CVqH# ;`9.6Z]&kwC/7 =6~;_" QGb-gfb b h P(^НC!S];ƈ]Cbg?F!ow dY:!}:ujGB{M =pi,WLЖC* E_/hc/dEL搔.&͇tH!C6C,Jl4p\!ʙ.FaB$>;o`Ñ=Xm` b6 XB3G"Pw*Dv &4luTd6-qaptbbei:+:ZXGo '& endstream endobj 37 0 obj 8553 endobj 38 0 obj << /Length 39 0 R /Filter /FlateDecode >> stream x]S0,/A2'@0\y N>@)Xh߇s)l!~=}9-yiWӼĜn=I 2/A8~|~t=-Zu۞s\RJsLy^.뙡}kZvTǣi*۷T-ϧX\1~=|(i\cmr\R5Quts-Si|sP,7MF .K'S/X7Sp ''e)%n=zxf:c-VbqNӃþ^#vW@:u@ i;`˳[g7lY _j zͭ%F\\\'ǀ{ ?M:s雁o^i55[Щ顆 /qN{9ܲf> endobj 9 0 obj << /Type /Font /Subtype /TrueType /BaseFont /TIKZHK+DejaVuSansMono /FirstChar 32 /LastChar 150 /FontDescriptor 40 0 R /Encoding /WinAnsiEncoding /Widths [ 602.050781 0 0 0 0 0 0 602.050781 602.050781 602.050781 0 0 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 0 602.050781 602.050781 602.050781 0 602.050781 0 0 0 0 0 0 602.050781 0 0 602.050781 602.050781 0 0 0 602.050781 0 0 0 0 602.050781 602.050781 602.050781 602.050781 0 602.050781 602.050781 0 602.050781 0 602.050781 0 0 0 0 0 0 602.050781 0 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 0 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 602.050781 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 602.050781 602.050781 602.050781 602.050781 ] /ToUnicode 38 0 R >> endobj 13 0 obj << /Type /ObjStm /Length 43 0 R /N 3 /First 17 /Filter /FlateDecode >> stream x5= 0۪LT)qѭnC!$.7+ O%"4Ϣet`վᙬέsM:jlT;PAD6ŸɎp>㵊ͽY_K!<BSQIڸ`5% endstream endobj 43 0 obj 163 endobj 44 0 obj << /Type /XRef /Length 162 /Filter /FlateDecode /Size 45 /W [1 2 2] /Root 42 0 R /Info 41 0 R >> stream x= AqO +\Ƞ eF+-[lVe\,Ng8=(p`2Rx?RI7d%ɥu䒒':}74I)V-"qR'^Cq,uu'zքpd endstream endobj startxref 39040 %%EOF qoi-0.4.1/rustfmt.toml000064400000000000000000000002741046102023000130220ustar 00000000000000use_small_heuristics = "Max" use_field_init_shorthand = true use_try_shorthand = true empty_item_single_line = true edition = "2018" unstable_features = true fn_args_layout = "Compressed" qoi-0.4.1/src/consts.rs000064400000000000000000000011321046102023000130610ustar 00000000000000pub const QOI_OP_INDEX: u8 = 0x00; // 00xxxxxx pub const QOI_OP_DIFF: u8 = 0x40; // 01xxxxxx pub const QOI_OP_LUMA: u8 = 0x80; // 10xxxxxx pub const QOI_OP_RUN: u8 = 0xc0; // 11xxxxxx pub const QOI_OP_RGB: u8 = 0xfe; // 11111110 pub const QOI_OP_RGBA: u8 = 0xff; // 11111111 pub const QOI_MASK_2: u8 = 0xc0; // (11)000000 pub const QOI_HEADER_SIZE: usize = 14; pub const QOI_PADDING: [u8; 8] = [0, 0, 0, 0, 0, 0, 0, 0x01]; // 7 zeros and one 0x01 marker pub const QOI_PADDING_SIZE: usize = 8; pub const QOI_MAGIC: u32 = u32::from_be_bytes(*b"qoif"); pub const QOI_PIXELS_MAX: usize = 400_000_000; qoi-0.4.1/src/decode.rs000064400000000000000000000305301046102023000127770ustar 00000000000000#[cfg(any(feature = "std", feature = "alloc"))] use alloc::{vec, vec::Vec}; #[cfg(feature = "std")] use std::io::Read; // TODO: can be removed once https://github.com/rust-lang/rust/issues/74985 is stable use bytemuck::{cast_slice_mut, Pod}; use crate::consts::{ QOI_HEADER_SIZE, QOI_OP_DIFF, QOI_OP_INDEX, QOI_OP_LUMA, QOI_OP_RGB, QOI_OP_RGBA, QOI_OP_RUN, QOI_PADDING, QOI_PADDING_SIZE, }; use crate::error::{Error, Result}; use crate::header::Header; use crate::pixel::{Pixel, SupportedChannels}; use crate::types::Channels; use crate::utils::{cold, unlikely}; const QOI_OP_INDEX_END: u8 = QOI_OP_INDEX | 0x3f; const QOI_OP_RUN_END: u8 = QOI_OP_RUN | 0x3d; // <- note, 0x3d (not 0x3f) const QOI_OP_DIFF_END: u8 = QOI_OP_DIFF | 0x3f; const QOI_OP_LUMA_END: u8 = QOI_OP_LUMA | 0x3f; #[inline] fn decode_impl_slice(data: &[u8], out: &mut [u8]) -> Result where Pixel: SupportedChannels, [u8; N]: Pod, { let mut pixels = cast_slice_mut::<_, [u8; N]>(out); let data_len = data.len(); let mut data = data; let mut index = [Pixel::<4>::new(); 256]; let mut px = Pixel::::new().with_a(0xff); let mut px_rgba: Pixel<4>; while let [px_out, ptail @ ..] = pixels { pixels = ptail; match data { [b1 @ QOI_OP_INDEX..=QOI_OP_INDEX_END, dtail @ ..] => { px_rgba = index[*b1 as usize]; px.update(px_rgba); *px_out = px.into(); data = dtail; continue; } [QOI_OP_RGB, r, g, b, dtail @ ..] => { px.update_rgb(*r, *g, *b); data = dtail; } [QOI_OP_RGBA, r, g, b, a, dtail @ ..] if RGBA => { px.update_rgba(*r, *g, *b, *a); data = dtail; } [b1 @ QOI_OP_RUN..=QOI_OP_RUN_END, dtail @ ..] => { *px_out = px.into(); let run = ((b1 & 0x3f) as usize).min(pixels.len()); let (phead, ptail) = pixels.split_at_mut(run); // can't panic phead.fill(px.into()); pixels = ptail; data = dtail; continue; } [b1 @ QOI_OP_DIFF..=QOI_OP_DIFF_END, dtail @ ..] => { px.update_diff(*b1); data = dtail; } [b1 @ QOI_OP_LUMA..=QOI_OP_LUMA_END, b2, dtail @ ..] => { px.update_luma(*b1, *b2); data = dtail; } _ => { cold(); if unlikely(data.len() < QOI_PADDING_SIZE) { return Err(Error::UnexpectedBufferEnd); } } } px_rgba = px.as_rgba(0xff); index[px_rgba.hash_index() as usize] = px_rgba; *px_out = px.into(); } if unlikely(data.len() < QOI_PADDING_SIZE) { return Err(Error::UnexpectedBufferEnd); } else if unlikely(data[..QOI_PADDING_SIZE] != QOI_PADDING) { return Err(Error::InvalidPadding); } Ok(data_len.saturating_sub(data.len()).saturating_sub(QOI_PADDING_SIZE)) } #[inline] fn decode_impl_slice_all( data: &[u8], out: &mut [u8], channels: u8, src_channels: u8, ) -> Result { match (channels, src_channels) { (3, 3) => decode_impl_slice::<3, false>(data, out), (3, 4) => decode_impl_slice::<3, true>(data, out), (4, 3) => decode_impl_slice::<4, false>(data, out), (4, 4) => decode_impl_slice::<4, true>(data, out), _ => { cold(); Err(Error::InvalidChannels { channels }) } } } /// Decode the image into a pre-allocated buffer. /// /// Note: the resulting number of channels will match the header. In order to change /// the number of channels, use [`Decoder::with_channels`]. #[inline] pub fn decode_to_buf(buf: impl AsMut<[u8]>, data: impl AsRef<[u8]>) -> Result
{ let mut decoder = Decoder::new(&data)?; decoder.decode_to_buf(buf)?; Ok(*decoder.header()) } /// Decode the image into a newly allocated vector. /// /// Note: the resulting number of channels will match the header. In order to change /// the number of channels, use [`Decoder::with_channels`]. #[cfg(any(feature = "std", feature = "alloc"))] #[inline] pub fn decode_to_vec(data: impl AsRef<[u8]>) -> Result<(Header, Vec)> { let mut decoder = Decoder::new(&data)?; let out = decoder.decode_to_vec()?; Ok((*decoder.header(), out)) } /// Decode the image header from a slice of bytes. #[inline] pub fn decode_header(data: impl AsRef<[u8]>) -> Result
{ Header::decode(data) } #[cfg(any(feature = "std"))] #[inline] fn decode_impl_stream( data: &mut R, out: &mut [u8], ) -> Result<()> where Pixel: SupportedChannels, [u8; N]: Pod, { let mut pixels = cast_slice_mut::<_, [u8; N]>(out); let mut index = [Pixel::::new(); 256]; let mut px = Pixel::::new().with_a(0xff); while let [px_out, ptail @ ..] = pixels { pixels = ptail; let mut p = [0]; data.read_exact(&mut p)?; let [b1] = p; match b1 { QOI_OP_INDEX..=QOI_OP_INDEX_END => { px = index[b1 as usize]; *px_out = px.into(); continue; } QOI_OP_RGB => { let mut p = [0; 3]; data.read_exact(&mut p)?; px.update_rgb(p[0], p[1], p[2]); } QOI_OP_RGBA if RGBA => { let mut p = [0; 4]; data.read_exact(&mut p)?; px.update_rgba(p[0], p[1], p[2], p[3]); } QOI_OP_RUN..=QOI_OP_RUN_END => { *px_out = px.into(); let run = ((b1 & 0x3f) as usize).min(pixels.len()); let (phead, ptail) = pixels.split_at_mut(run); // can't panic phead.fill(px.into()); pixels = ptail; continue; } QOI_OP_DIFF..=QOI_OP_DIFF_END => { px.update_diff(b1); } QOI_OP_LUMA..=QOI_OP_LUMA_END => { let mut p = [0]; data.read_exact(&mut p)?; let [b2] = p; px.update_luma(b1, b2); } _ => { cold(); } } index[px.hash_index() as usize] = px; *px_out = px.into(); } let mut p = [0_u8; QOI_PADDING_SIZE]; data.read_exact(&mut p)?; if unlikely(p != QOI_PADDING) { return Err(Error::InvalidPadding); } Ok(()) } #[cfg(feature = "std")] #[inline] fn decode_impl_stream_all( data: &mut R, out: &mut [u8], channels: u8, src_channels: u8, ) -> Result<()> { match (channels, src_channels) { (3, 3) => decode_impl_stream::<_, 3, false>(data, out), (3, 4) => decode_impl_stream::<_, 3, true>(data, out), (4, 3) => decode_impl_stream::<_, 4, false>(data, out), (4, 4) => decode_impl_stream::<_, 4, true>(data, out), _ => { cold(); Err(Error::InvalidChannels { channels }) } } } #[doc(hidden)] pub trait Reader: Sized { fn decode_header(&mut self) -> Result
; fn decode_image(&mut self, out: &mut [u8], channels: u8, src_channels: u8) -> Result<()>; } pub struct Bytes<'a>(&'a [u8]); impl<'a> Bytes<'a> { #[inline] pub const fn new(buf: &'a [u8]) -> Self { Self(buf) } #[inline] pub const fn as_slice(&self) -> &[u8] { self.0 } } impl<'a> Reader for Bytes<'a> { #[inline] fn decode_header(&mut self) -> Result
{ let header = Header::decode(self.0)?; self.0 = &self.0[QOI_HEADER_SIZE..]; // can't panic Ok(header) } #[inline] fn decode_image(&mut self, out: &mut [u8], channels: u8, src_channels: u8) -> Result<()> { let n_read = decode_impl_slice_all(self.0, out, channels, src_channels)?; self.0 = &self.0[n_read..]; Ok(()) } } #[cfg(feature = "std")] impl Reader for R { #[inline] fn decode_header(&mut self) -> Result
{ let mut b = [0; QOI_HEADER_SIZE]; self.read_exact(&mut b)?; Header::decode(b) } #[inline] fn decode_image(&mut self, out: &mut [u8], channels: u8, src_channels: u8) -> Result<()> { decode_impl_stream_all(self, out, channels, src_channels) } } /// Decode QOI images from slices or from streams. #[derive(Clone)] pub struct Decoder { reader: R, header: Header, channels: Channels, } impl<'a> Decoder> { /// Creates a new decoder from a slice of bytes. /// /// The header will be decoded immediately upon construction. /// /// Note: this provides the most efficient decoding, but requires the source data to /// be loaded in memory in order to decode it. In order to decode from a generic /// stream, use [`Decoder::from_stream`] instead. #[inline] pub fn new(data: &'a (impl AsRef<[u8]> + ?Sized)) -> Result { Self::new_impl(Bytes::new(data.as_ref())) } /// Returns the undecoded tail of the input slice of bytes. #[inline] pub const fn data(&self) -> &[u8] { self.reader.as_slice() } } #[cfg(feature = "std")] impl Decoder { /// Creates a new decoder from a generic reader that implements [`Read`](std::io::Read). /// /// The header will be decoded immediately upon construction. /// /// Note: while it's possible to pass a `&[u8]` slice here since it implements `Read`, it /// would be more efficient to use a specialized constructor instead: [`Decoder::new`]. #[inline] pub fn from_stream(reader: R) -> Result { Self::new_impl(reader) } /// Returns an immutable reference to the underlying reader. #[inline] pub const fn reader(&self) -> &R { &self.reader } /// Consumes the decoder and returns the underlying reader back. #[inline] #[allow(clippy::missing_const_for_fn)] pub fn into_reader(self) -> R { self.reader } } impl Decoder { #[inline] fn new_impl(mut reader: R) -> Result { let header = reader.decode_header()?; Ok(Self { reader, header, channels: header.channels }) } /// Returns a new decoder with modified number of channels. /// /// By default, the number of channels in the decoded image will be equal /// to whatever is specified in the header. However, it is also possible /// to decode RGB into RGBA (in which case the alpha channel will be set /// to 255), and vice versa (in which case the alpha channel will be ignored). #[inline] pub const fn with_channels(mut self, channels: Channels) -> Self { self.channels = channels; self } /// Returns the number of channels in the decoded image. /// /// Note: this may differ from the number of channels specified in the header. #[inline] pub const fn channels(&self) -> Channels { self.channels } /// Returns the decoded image header. #[inline] pub const fn header(&self) -> &Header { &self.header } /// The number of bytes the decoded image will take. /// /// Can be used to pre-allocate the buffer to decode the image into. #[inline] pub const fn required_buf_len(&self) -> usize { self.header.n_pixels().saturating_mul(self.channels.as_u8() as usize) } /// Decodes the image to a pre-allocated buffer and returns the number of bytes written. /// /// The minimum size of the buffer can be found via [`Decoder::required_buf_len`]. #[inline] pub fn decode_to_buf(&mut self, mut buf: impl AsMut<[u8]>) -> Result { let buf = buf.as_mut(); let size = self.required_buf_len(); if unlikely(buf.len() < size) { return Err(Error::OutputBufferTooSmall { size: buf.len(), required: size }); } self.reader.decode_image(buf, self.channels.as_u8(), self.header.channels.as_u8())?; Ok(size) } /// Decodes the image into a newly allocated vector of bytes and returns it. #[cfg(any(feature = "std", feature = "alloc"))] #[inline] pub fn decode_to_vec(&mut self) -> Result> { let mut out = vec![0; self.header.n_pixels() * self.channels.as_u8() as usize]; let _ = self.decode_to_buf(&mut out)?; Ok(out) } } qoi-0.4.1/src/encode.rs000064400000000000000000000165601046102023000130200ustar 00000000000000#[cfg(any(feature = "std", feature = "alloc"))] use alloc::{vec, vec::Vec}; use core::convert::TryFrom; #[cfg(feature = "std")] use std::io::Write; use bytemuck::Pod; use crate::consts::{QOI_HEADER_SIZE, QOI_OP_INDEX, QOI_OP_RUN, QOI_PADDING, QOI_PADDING_SIZE}; use crate::error::{Error, Result}; use crate::header::Header; use crate::pixel::{Pixel, SupportedChannels}; use crate::types::{Channels, ColorSpace}; #[cfg(feature = "std")] use crate::utils::GenericWriter; use crate::utils::{unlikely, BytesMut, Writer}; #[allow(clippy::cast_possible_truncation, unused_assignments, unused_variables)] fn encode_impl(mut buf: W, data: &[u8]) -> Result where Pixel: SupportedChannels, [u8; N]: Pod, { let cap = buf.capacity(); let mut index = [Pixel::new(); 256]; let mut px_prev = Pixel::new().with_a(0xff); let mut hash_prev = px_prev.hash_index(); let mut run = 0_u8; let mut px = Pixel::::new().with_a(0xff); let mut index_allowed = false; let n_pixels = data.len() / N; for (i, chunk) in data.chunks_exact(N).enumerate() { px.read(chunk); if px == px_prev { run += 1; if run == 62 || unlikely(i == n_pixels - 1) { buf = buf.write_one(QOI_OP_RUN | (run - 1))?; run = 0; } } else { if run != 0 { #[cfg(not(feature = "reference"))] { // credits for the original idea: @zakarumych (had to be fixed though) buf = buf.write_one(if run == 1 && index_allowed { QOI_OP_INDEX | hash_prev } else { QOI_OP_RUN | (run - 1) })?; } #[cfg(feature = "reference")] { buf = buf.write_one(QOI_OP_RUN | (run - 1))?; } run = 0; } index_allowed = true; let px_rgba = px.as_rgba(0xff); hash_prev = px_rgba.hash_index(); let index_px = &mut index[hash_prev as usize]; if *index_px == px_rgba { buf = buf.write_one(QOI_OP_INDEX | hash_prev)?; } else { *index_px = px_rgba; buf = px.encode_into(px_prev, buf)?; } px_prev = px; } } buf = buf.write_many(&QOI_PADDING)?; Ok(cap.saturating_sub(buf.capacity())) } #[inline] fn encode_impl_all(out: W, data: &[u8], channels: Channels) -> Result { match channels { Channels::Rgb => encode_impl::<_, 3>(out, data), Channels::Rgba => encode_impl::<_, 4>(out, data), } } /// The maximum number of bytes the encoded image will take. /// /// Can be used to pre-allocate the buffer to encode the image into. #[inline] pub fn encode_max_len(width: u32, height: u32, channels: impl Into) -> usize { let (width, height) = (width as usize, height as usize); let n_pixels = width.saturating_mul(height); QOI_HEADER_SIZE + n_pixels.saturating_mul(channels.into() as usize) + n_pixels + QOI_PADDING_SIZE } /// Encode the image into a pre-allocated buffer. /// /// Returns the total number of bytes written. #[inline] pub fn encode_to_buf( buf: impl AsMut<[u8]>, data: impl AsRef<[u8]>, width: u32, height: u32, ) -> Result { Encoder::new(&data, width, height)?.encode_to_buf(buf) } /// Encode the image into a newly allocated vector. #[cfg(any(feature = "alloc", feature = "std"))] #[inline] pub fn encode_to_vec(data: impl AsRef<[u8]>, width: u32, height: u32) -> Result> { Encoder::new(&data, width, height)?.encode_to_vec() } /// Encode QOI images into buffers or into streams. pub struct Encoder<'a> { data: &'a [u8], header: Header, } impl<'a> Encoder<'a> { /// Creates a new encoder from a given array of pixel data and image dimensions. /// /// The number of channels will be inferred automatically (the valid values /// are 3 or 4). The color space will be set to sRGB by default. #[inline] #[allow(clippy::cast_possible_truncation)] pub fn new(data: &'a (impl AsRef<[u8]> + ?Sized), width: u32, height: u32) -> Result { let data = data.as_ref(); let mut header = Header::try_new(width, height, Channels::default(), ColorSpace::default())?; let size = data.len(); let n_channels = size / header.n_pixels(); if header.n_pixels() * n_channels != size { return Err(Error::InvalidImageLength { size, width, height }); } header.channels = Channels::try_from(n_channels.min(0xff) as u8)?; Ok(Self { data, header }) } /// Returns a new encoder with modified color space. /// /// Note: the color space doesn't affect encoding or decoding in any way, it's /// a purely informative field that's stored in the image header. #[inline] pub const fn with_colorspace(mut self, colorspace: ColorSpace) -> Self { self.header = self.header.with_colorspace(colorspace); self } /// Returns the inferred number of channels. #[inline] pub const fn channels(&self) -> Channels { self.header.channels } /// Returns the header that will be stored in the encoded image. #[inline] pub const fn header(&self) -> &Header { &self.header } /// The maximum number of bytes the encoded image will take. /// /// Can be used to pre-allocate the buffer to encode the image into. #[inline] pub fn required_buf_len(&self) -> usize { self.header.encode_max_len() } /// Encodes the image to a pre-allocated buffer and returns the number of bytes written. /// /// The minimum size of the buffer can be found via [`Encoder::required_buf_len`]. #[inline] pub fn encode_to_buf(&self, mut buf: impl AsMut<[u8]>) -> Result { let buf = buf.as_mut(); let size_required = self.required_buf_len(); if unlikely(buf.len() < size_required) { return Err(Error::OutputBufferTooSmall { size: buf.len(), required: size_required }); } let (head, tail) = buf.split_at_mut(QOI_HEADER_SIZE); // can't panic head.copy_from_slice(&self.header.encode()); let n_written = encode_impl_all(BytesMut::new(tail), self.data, self.header.channels)?; Ok(QOI_HEADER_SIZE + n_written) } /// Encodes the image into a newly allocated vector of bytes and returns it. #[cfg(any(feature = "alloc", feature = "std"))] #[inline] pub fn encode_to_vec(&self) -> Result> { let mut out = vec![0_u8; self.required_buf_len()]; let size = self.encode_to_buf(&mut out)?; out.truncate(size); Ok(out) } /// Encodes the image directly to a generic writer that implements [`Write`](std::io::Write). /// /// Note: while it's possible to pass a `&mut [u8]` slice here since it implements `Write`, /// it would more effficient to use a specialized method instead: [`Encoder::encode_to_buf`]. #[cfg(feature = "std")] #[inline] pub fn encode_to_stream(&self, writer: &mut W) -> Result { writer.write_all(&self.header.encode())?; let n_written = encode_impl_all(GenericWriter::new(writer), self.data, self.header.channels)?; Ok(n_written + QOI_HEADER_SIZE) } } qoi-0.4.1/src/error.rs000064400000000000000000000057161046102023000127150ustar 00000000000000use core::convert::Infallible; use core::fmt::{self, Display}; use crate::consts::QOI_MAGIC; /// Errors that can occur during encoding or decoding. #[derive(Debug)] pub enum Error { /// Leading 4 magic bytes don't match when decoding InvalidMagic { magic: u32 }, /// Invalid number of channels: expected 3 or 4 InvalidChannels { channels: u8 }, /// Invalid color space: expected 0 or 1 InvalidColorSpace { colorspace: u8 }, /// Invalid image dimensions: can't be empty or larger than 400Mp InvalidImageDimensions { width: u32, height: u32 }, /// Image dimensions are inconsistent with image buffer length InvalidImageLength { size: usize, width: u32, height: u32 }, /// Output buffer is too small to fit encoded/decoded image OutputBufferTooSmall { size: usize, required: usize }, /// Input buffer ended unexpectedly before decoding was finished UnexpectedBufferEnd, /// Invalid stream end marker encountered when decoding InvalidPadding, #[cfg(feature = "std")] /// Generic I/O error from the wrapped reader/writer IoError(std::io::Error), } /// Alias for [`Result`](std::result::Result) with the error type of [`Error`]. pub type Result = core::result::Result; impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Self::InvalidMagic { magic } => { write!(f, "invalid magic: expected {:?}, got {:?}", QOI_MAGIC, magic.to_be_bytes()) } Self::InvalidChannels { channels } => { write!(f, "invalid number of channels: {}", channels) } Self::InvalidColorSpace { colorspace } => { write!(f, "invalid color space: {} (expected 0 or 1)", colorspace) } Self::InvalidImageDimensions { width, height } => { write!(f, "invalid image dimensions: {}x{}", width, height) } Self::InvalidImageLength { size, width, height } => { write!(f, "invalid image length: {} bytes for {}x{}", size, width, height) } Self::OutputBufferTooSmall { size, required } => { write!(f, "output buffer size too small: {} (required: {})", size, required) } Self::UnexpectedBufferEnd => { write!(f, "unexpected input buffer end while decoding") } Self::InvalidPadding => { write!(f, "invalid padding (stream end marker mismatch)") } #[cfg(feature = "std")] Self::IoError(ref err) => { write!(f, "i/o error: {}", err) } } } } #[cfg(feature = "std")] impl std::error::Error for Error {} impl From for Error { fn from(_: Infallible) -> Self { unreachable!() } } #[cfg(feature = "std")] impl From for Error { fn from(err: std::io::Error) -> Self { Self::IoError(err) } } qoi-0.4.1/src/header.rs000064400000000000000000000074711046102023000130140ustar 00000000000000use core::convert::TryInto; use bytemuck::cast_slice; use crate::consts::{QOI_HEADER_SIZE, QOI_MAGIC, QOI_PIXELS_MAX}; use crate::encode_max_len; use crate::error::{Error, Result}; use crate::types::{Channels, ColorSpace}; use crate::utils::unlikely; /// Image header: dimensions, channels, color space. /// /// ### Notes /// A valid image header must satisfy the following conditions: /// * Both width and height must be non-zero. /// * Maximum number of pixels is 400Mp (=4e8 pixels). #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct Header { /// Image width in pixels pub width: u32, /// Image height in pixels pub height: u32, /// Number of 8-bit channels per pixel pub channels: Channels, /// Color space (informative field, doesn't affect encoding) pub colorspace: ColorSpace, } impl Default for Header { #[inline] fn default() -> Self { Self { width: 1, height: 1, channels: Channels::default(), colorspace: ColorSpace::default(), } } } impl Header { /// Creates a new header and validates image dimensions. #[inline] pub const fn try_new( width: u32, height: u32, channels: Channels, colorspace: ColorSpace, ) -> Result { let n_pixels = (width as usize).saturating_mul(height as usize); if unlikely(n_pixels == 0 || n_pixels > QOI_PIXELS_MAX) { return Err(Error::InvalidImageDimensions { width, height }); } Ok(Self { width, height, channels, colorspace }) } /// Creates a new header with modified channels. #[inline] pub const fn with_channels(mut self, channels: Channels) -> Self { self.channels = channels; self } /// Creates a new header with modified color space. #[inline] pub const fn with_colorspace(mut self, colorspace: ColorSpace) -> Self { self.colorspace = colorspace; self } /// Serializes the header into a bytes array. #[inline] pub(crate) fn encode(&self) -> [u8; QOI_HEADER_SIZE] { let mut out = [0; QOI_HEADER_SIZE]; out[..4].copy_from_slice(&QOI_MAGIC.to_be_bytes()); out[4..8].copy_from_slice(&self.width.to_be_bytes()); out[8..12].copy_from_slice(&self.height.to_be_bytes()); out[12] = self.channels.into(); out[13] = self.colorspace.into(); out } /// Deserializes the header from a byte array. #[inline] pub(crate) fn decode(data: impl AsRef<[u8]>) -> Result { let data = data.as_ref(); if unlikely(data.len() < QOI_HEADER_SIZE) { return Err(Error::UnexpectedBufferEnd); } let v = cast_slice::<_, [u8; 4]>(&data[..12]); let magic = u32::from_be_bytes(v[0]); let width = u32::from_be_bytes(v[1]); let height = u32::from_be_bytes(v[2]); let channels = data[12].try_into()?; let colorspace = data[13].try_into()?; if unlikely(magic != QOI_MAGIC) { return Err(Error::InvalidMagic { magic }); } Self::try_new(width, height, channels, colorspace) } /// Returns a number of pixels in the image. #[inline] pub const fn n_pixels(&self) -> usize { (self.width as usize).saturating_mul(self.height as usize) } /// Returns the total number of bytes in the raw pixel array. /// /// This may come useful when pre-allocating a buffer to decode the image into. #[inline] pub const fn n_bytes(&self) -> usize { self.n_pixels() * self.channels.as_u8() as usize } /// The maximum number of bytes the encoded image will take. /// /// Can be used to pre-allocate the buffer to encode the image into. #[inline] pub fn encode_max_len(&self) -> usize { encode_max_len(self.width, self.height, self.channels) } } qoi-0.4.1/src/lib.rs000064400000000000000000000064221046102023000123250ustar 00000000000000//! Fast encoder/decoder for [QOI image format](https://qoiformat.org/), implemented in pure and safe Rust. //! //! - One of the [fastest](#benchmarks) QOI encoders/decoders out there. //! - Compliant with the [latest](https://qoiformat.org/qoi-specification.pdf) QOI format specification. //! - Zero unsafe code. //! - Supports decoding from / encoding to `std::io` streams directly. //! - `no_std` support. //! - Roundtrip-tested vs the reference C implementation; fuzz-tested. //! //! ### Examples //! //! ```rust //! use qoi::{encode_to_vec, decode_to_vec}; //! //! let encoded = encode_to_vec(&pixels, width, height)?; //! let (header, decoded) = decode_to_vec(&encoded)?; //! //! assert_eq!(header.width, width); //! assert_eq!(header.height, height); //! assert_eq!(decoded, pixels); //! ``` //! //! ### Benchmarks //! //! ``` //! decode:Mp/s encode:Mp/s decode:MB/s encode:MB/s //! qoi.h 282.9 225.3 978.3 778.9 //! qoi-rust 427.4 290.0 1477.7 1002.9 //! ``` //! //! - Reference C implementation: //! [phoboslab/qoi@00e34217](https://github.com/phoboslab/qoi/commit/00e34217). //! - Benchmark timings were collected on an Apple M1 laptop. //! - 2846 images from the suite provided upstream //! ([tarball](https://phoboslab.org/files/qoibench/qoi_benchmark_suite.tar)): //! all pngs except two with broken checksums. //! - 1.32 GPixels in total with 4.46 GB of raw pixel data. //! //! Benchmarks have also been run for all of the other Rust implementations //! of QOI for comparison purposes and, at the time of writing this document, //! this library proved to be the fastest one by a noticeable margin. //! //! ### Rust version //! //! The minimum supported Rust version is 1.51.0 (any changes to this would be //! considered to be a breaking change). //! //! ### `no_std` //! //! This crate supports `no_std` mode. By default, std is enabled via the `std` //! feature. You can deactivate the `default-features` to target core instead. //! In that case anything related to `std::io`, `std::error::Error` and heap //! allocations is disabled. There is an additional `alloc` feature that can //! be activated to bring back the support for heap allocations. #![forbid(unsafe_code)] #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] #![allow( clippy::inline_always, clippy::similar_names, clippy::missing_errors_doc, clippy::must_use_candidate, clippy::module_name_repetitions, clippy::cargo_common_metadata, clippy::doc_markdown, clippy::return_self_not_must_use, )] #![cfg_attr(not(any(feature = "std", test)), no_std)] #[cfg(all(feature = "alloc", not(any(feature = "std", test))))] extern crate alloc; #[cfg(any(feature = "std", test))] extern crate std as alloc; mod decode; mod encode; mod error; mod header; mod pixel; mod types; mod utils; #[doc(hidden)] pub mod consts; #[cfg(any(feature = "alloc", feature = "std"))] pub use crate::decode::decode_to_vec; pub use crate::decode::{decode_header, decode_to_buf, Decoder}; #[cfg(any(feature = "alloc", feature = "std"))] pub use crate::encode::encode_to_vec; pub use crate::encode::{encode_max_len, encode_to_buf, Encoder}; pub use crate::error::{Error, Result}; pub use crate::header::Header; pub use crate::types::{Channels, ColorSpace}; qoi-0.4.1/src/pixel.rs000064400000000000000000000117431046102023000127020ustar 00000000000000use crate::consts::{QOI_OP_DIFF, QOI_OP_LUMA, QOI_OP_RGB, QOI_OP_RGBA}; use crate::error::Result; use crate::utils::Writer; use bytemuck::{cast, Pod}; #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[repr(transparent)] pub struct Pixel([u8; N]); impl Pixel { #[inline] pub const fn new() -> Self { Self([0; N]) } #[inline] pub fn read(&mut self, s: &[u8]) { if s.len() == N { let mut i = 0; while i < N { self.0[i] = s[i]; i += 1; } } else { unreachable!(); } } #[inline] pub fn update(&mut self, px: Pixel) { let mut i = 0; while i < M && i < N { self.0[i] = px.0[i]; i += 1; } } #[inline] pub fn update_rgb(&mut self, r: u8, g: u8, b: u8) { self.0[0] = r; self.0[1] = g; self.0[2] = b; } #[inline] pub fn update_rgba(&mut self, r: u8, g: u8, b: u8, a: u8) { self.0[0] = r; self.0[1] = g; self.0[2] = b; if N >= 4 { self.0[3] = a; } } #[inline] pub fn update_diff(&mut self, b1: u8) { self.0[0] = self.0[0].wrapping_add((b1 >> 4) & 0x03).wrapping_sub(2); self.0[1] = self.0[1].wrapping_add((b1 >> 2) & 0x03).wrapping_sub(2); self.0[2] = self.0[2].wrapping_add(b1 & 0x03).wrapping_sub(2); } #[inline] pub fn update_luma(&mut self, b1: u8, b2: u8) { let vg = (b1 & 0x3f).wrapping_sub(32); let vg_8 = vg.wrapping_sub(8); let vr = vg_8.wrapping_add((b2 >> 4) & 0x0f); let vb = vg_8.wrapping_add(b2 & 0x0f); self.0[0] = self.0[0].wrapping_add(vr); self.0[1] = self.0[1].wrapping_add(vg); self.0[2] = self.0[2].wrapping_add(vb); } #[inline] pub const fn as_rgba(self, with_a: u8) -> Pixel<4> { let mut i = 0; let mut out = Pixel::new(); while i < N { out.0[i] = self.0[i]; i += 1; } if N < 4 { out.0[3] = with_a; } out } #[inline] pub const fn r(self) -> u8 { self.0[0] } #[inline] pub const fn g(self) -> u8 { self.0[1] } #[inline] pub const fn b(self) -> u8 { self.0[2] } #[inline] pub const fn with_a(mut self, value: u8) -> Self { if N >= 4 { self.0[3] = value; } self } #[inline] pub const fn a_or(self, value: u8) -> u8 { if N < 4 { value } else { self.0[3] } } #[inline] #[allow(clippy::cast_lossless, clippy::cast_possible_truncation)] pub fn hash_index(self) -> u8 where [u8; N]: Pod, { // credits for the initial idea: @zakarumych let v = if N == 4 { u32::from_ne_bytes(cast(self.0)) } else { u32::from_ne_bytes([self.0[0], self.0[1], self.0[2], 0xff]) } as u64; let s = ((v & 0xff00_ff00) << 32) | (v & 0x00ff_00ff); s.wrapping_mul(0x0300_0700_0005_000b_u64).to_le().swap_bytes() as u8 & 63 } #[inline] pub fn rgb_add(&mut self, r: u8, g: u8, b: u8) { self.0[0] = self.0[0].wrapping_add(r); self.0[1] = self.0[1].wrapping_add(g); self.0[2] = self.0[2].wrapping_add(b); } #[inline] pub fn encode_into(&self, px_prev: Self, buf: W) -> Result { if N == 3 || self.a_or(0) == px_prev.a_or(0) { let vg = self.g().wrapping_sub(px_prev.g()); let vg_32 = vg.wrapping_add(32); if vg_32 | 63 == 63 { let vr = self.r().wrapping_sub(px_prev.r()); let vb = self.b().wrapping_sub(px_prev.b()); let vg_r = vr.wrapping_sub(vg); let vg_b = vb.wrapping_sub(vg); let (vr_2, vg_2, vb_2) = (vr.wrapping_add(2), vg.wrapping_add(2), vb.wrapping_add(2)); if vr_2 | vg_2 | vb_2 | 3 == 3 { buf.write_one(QOI_OP_DIFF | vr_2 << 4 | vg_2 << 2 | vb_2) } else { let (vg_r_8, vg_b_8) = (vg_r.wrapping_add(8), vg_b.wrapping_add(8)); if vg_r_8 | vg_b_8 | 15 == 15 { buf.write_many(&[QOI_OP_LUMA | vg_32, vg_r_8 << 4 | vg_b_8]) } else { buf.write_many(&[QOI_OP_RGB, self.r(), self.g(), self.b()]) } } } else { buf.write_many(&[QOI_OP_RGB, self.r(), self.g(), self.b()]) } } else { buf.write_many(&[QOI_OP_RGBA, self.r(), self.g(), self.b(), self.a_or(0xff)]) } } } impl From> for [u8; N] { #[inline(always)] fn from(px: Pixel) -> Self { px.0 } } pub trait SupportedChannels {} impl SupportedChannels for Pixel<3> {} impl SupportedChannels for Pixel<4> {} qoi-0.4.1/src/types.rs000064400000000000000000000050601046102023000127200ustar 00000000000000use core::convert::TryFrom; use crate::error::{Error, Result}; use crate::utils::unlikely; /// Image color space. /// /// Note: the color space is purely informative. Although it is saved to the /// file header, it does not affect encoding/decoding in any way. #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] #[repr(u8)] pub enum ColorSpace { /// sRGB with linear alpha Srgb = 0, /// All channels are linear Linear = 1, } impl ColorSpace { /// Returns true if the color space is sRGB with linear alpha. pub const fn is_srgb(self) -> bool { matches!(self, Self::Srgb) } /// Returns true is all channels are linear. pub const fn is_linear(self) -> bool { matches!(self, Self::Linear) } /// Converts to an integer (0 if sRGB, 1 if all linear). pub const fn as_u8(self) -> u8 { self as u8 } } impl Default for ColorSpace { fn default() -> Self { Self::Srgb } } impl From for u8 { #[inline] fn from(colorspace: ColorSpace) -> Self { colorspace as Self } } impl TryFrom for ColorSpace { type Error = Error; #[inline] fn try_from(colorspace: u8) -> Result { if unlikely(colorspace | 1 != 1) { Err(Error::InvalidColorSpace { colorspace }) } else { Ok(if colorspace == 0 { Self::Srgb } else { Self::Linear }) } } } /// Number of 8-bit channels in a pixel. #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] #[repr(u8)] pub enum Channels { /// Three 8-bit channels (RGB) Rgb = 3, /// Four 8-bit channels (RGBA) Rgba = 4, } impl Channels { /// Returns true if there are 3 channels (RGB). pub const fn is_rgb(self) -> bool { matches!(self, Self::Rgb) } /// Returns true if there are 4 channels (RGBA). pub const fn is_rgba(self) -> bool { matches!(self, Self::Rgba) } /// Converts to an integer (3 if RGB, 4 if RGBA). pub const fn as_u8(self) -> u8 { self as u8 } } impl Default for Channels { fn default() -> Self { Self::Rgb } } impl From for u8 { #[inline] fn from(channels: Channels) -> Self { channels as Self } } impl TryFrom for Channels { type Error = Error; #[inline] fn try_from(channels: u8) -> Result { if unlikely(channels != 3 && channels != 4) { Err(Error::InvalidChannels { channels }) } else { Ok(if channels == 3 { Self::Rgb } else { Self::Rgba }) } } } qoi-0.4.1/src/utils.rs000064400000000000000000000042231046102023000127140ustar 00000000000000#[cfg(feature = "std")] use std::io::Write; use crate::error::Result; #[inline(always)] #[cold] pub const fn cold() {} #[inline(always)] #[allow(unused)] pub const fn likely(b: bool) -> bool { if !b { cold(); } b } #[inline(always)] pub const fn unlikely(b: bool) -> bool { if b { cold(); } b } pub trait Writer: Sized { fn write_one(self, v: u8) -> Result; fn write_many(self, v: &[u8]) -> Result; fn capacity(&self) -> usize; } pub struct BytesMut<'a>(&'a mut [u8]); impl<'a> BytesMut<'a> { pub fn new(buf: &'a mut [u8]) -> Self { Self(buf) } #[inline] pub fn write_one(self, v: u8) -> Self { if let Some((first, tail)) = self.0.split_first_mut() { *first = v; Self(tail) } else { unreachable!() } } #[inline] pub fn write_many(self, v: &[u8]) -> Self { if v.len() <= self.0.len() { let (head, tail) = self.0.split_at_mut(v.len()); head.copy_from_slice(v); Self(tail) } else { unreachable!() } } } impl<'a> Writer for BytesMut<'a> { #[inline] fn write_one(self, v: u8) -> Result { Ok(BytesMut::write_one(self, v)) } #[inline] fn write_many(self, v: &[u8]) -> Result { Ok(BytesMut::write_many(self, v)) } #[inline] fn capacity(&self) -> usize { self.0.len() } } #[cfg(feature = "std")] pub struct GenericWriter { writer: W, n_written: usize, } #[cfg(feature = "std")] impl GenericWriter { pub const fn new(writer: W) -> Self { Self { writer, n_written: 0 } } } #[cfg(feature = "std")] impl Writer for GenericWriter { fn write_one(mut self, v: u8) -> Result { self.n_written += 1; self.writer.write_all(&[v]).map(|_| self).map_err(Into::into) } fn write_many(mut self, v: &[u8]) -> Result { self.n_written += v.len(); self.writer.write_all(v).map(|_| self).map_err(Into::into) } fn capacity(&self) -> usize { usize::MAX - self.n_written } } qoi-0.4.1/tests/common.rs000064400000000000000000000005541046102023000134220ustar 00000000000000#[allow(unused)] pub fn hash(px: [u8; N]) -> u8 { let r = px[0]; let g = px[1]; let b = px[2]; let a = if N >= 4 { px[3] } else { 0xff }; let rm = r.wrapping_mul(3); let gm = g.wrapping_mul(5); let bm = b.wrapping_mul(7); let am = a.wrapping_mul(11); rm.wrapping_add(gm).wrapping_add(bm).wrapping_add(am) % 64 } qoi-0.4.1/tests/test_chunks.rs000064400000000000000000000142451046102023000144660ustar 00000000000000mod common; use bytemuck::{cast_slice, Pod}; use qoi::consts::{ QOI_HEADER_SIZE, QOI_OP_DIFF, QOI_OP_INDEX, QOI_OP_LUMA, QOI_OP_RGB, QOI_OP_RGBA, QOI_OP_RUN, QOI_PADDING_SIZE, }; use qoi::{decode_to_vec, encode_to_vec}; use self::common::hash; fn test_chunk(pixels: P, expected: E) where P: AsRef<[[u8; N]]>, E: AsRef<[u8]>, [u8; N]: Pod, { let pixels = pixels.as_ref(); let expected = expected.as_ref(); let pixels_raw = cast_slice::<_, u8>(pixels); let encoded = encode_to_vec(pixels_raw, pixels.len() as _, 1).unwrap(); let decoded = decode_to_vec(&encoded).unwrap().1; assert_eq!(pixels_raw, decoded.as_slice(), "roundtrip failed (encoded={:?}))", encoded); assert!(encoded.len() >= expected.len() + QOI_HEADER_SIZE + QOI_PADDING_SIZE); assert_eq!(&encoded[QOI_HEADER_SIZE..][..expected.len()], expected); } #[test] fn test_encode_rgb_3ch() { test_chunk([[11, 121, 231]], [QOI_OP_RGB, 11, 121, 231]); } #[test] fn test_encode_rgb_4ch() { test_chunk([[11, 121, 231, 0xff]], [QOI_OP_RGB, 11, 121, 231]); } #[test] fn test_encode_rgba() { test_chunk([[11, 121, 231, 55]], [QOI_OP_RGBA, 11, 121, 231, 55]); } #[test] fn test_encode_run_start_len1to62_3ch() { for n in 1..=62 { let mut v = vec![[0, 0, 0]; n]; v.push([11, 22, 33]); test_chunk(v, [QOI_OP_RUN | (n as u8 - 1), QOI_OP_RGB]); } } #[test] fn test_encode_run_start_len1to62_4ch() { for n in 1..=62 { let mut v = vec![[0, 0, 0, 0xff]; n]; v.push([11, 22, 33, 44]); test_chunk(v, [QOI_OP_RUN | (n as u8 - 1), QOI_OP_RGBA]); } } #[test] fn test_encode_run_start_63to124_3ch() { for n in 63..=124 { let mut v = vec![[0, 0, 0]; n]; v.push([11, 22, 33]); test_chunk(v, [QOI_OP_RUN | 61, QOI_OP_RUN | (n as u8 - 63), QOI_OP_RGB]); } } #[test] fn test_encode_run_start_len63to124_4ch() { for n in 63..=124 { let mut v = vec![[0, 0, 0, 0xff]; n]; v.push([11, 22, 33, 44]); test_chunk(v, [QOI_OP_RUN | 61, QOI_OP_RUN | (n as u8 - 63), QOI_OP_RGBA]); } } #[test] fn test_encode_run_end_3ch() { let px = [11, 33, 55]; test_chunk( [[1, 99, 2], px, px, px], [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGB, px[0], px[1], px[2], QOI_OP_RUN | 1], ); } #[test] fn test_encode_run_end_4ch() { let px = [11, 33, 55, 77]; test_chunk( [[1, 99, 2, 3], px, px, px], [QOI_OP_RGBA, 1, 99, 2, 3, QOI_OP_RGBA, px[0], px[1], px[2], px[3], QOI_OP_RUN | 1], ); } #[test] fn test_encode_run_mid_3ch() { let px = [11, 33, 55]; test_chunk( [[1, 99, 2], px, px, px, [1, 2, 3]], [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGB, px[0], px[1], px[2], QOI_OP_RUN | 1], ); } #[test] fn test_encode_run_mid_4ch() { let px = [11, 33, 55, 77]; test_chunk( [[1, 99, 2, 3], px, px, px, [1, 2, 3, 4]], [QOI_OP_RGBA, 1, 99, 2, 3, QOI_OP_RGBA, px[0], px[1], px[2], px[3], QOI_OP_RUN | 1], ); } #[test] fn test_encode_index_3ch() { let px = [101, 102, 103]; test_chunk( [px, [1, 2, 3], px], [QOI_OP_RGB, 101, 102, 103, QOI_OP_RGB, 1, 2, 3, QOI_OP_INDEX | hash(px)], ); } #[test] fn test_encode_index_4ch() { let px = [101, 102, 103, 104]; test_chunk( [px, [1, 2, 3, 4], px], [QOI_OP_RGBA, 101, 102, 103, 104, QOI_OP_RGBA, 1, 2, 3, 4, QOI_OP_INDEX | hash(px)], ); } #[test] fn test_encode_index_zero_3ch() { let px = [0, 0, 0]; test_chunk([[101, 102, 103], px], [QOI_OP_RGB, 101, 102, 103, QOI_OP_RGB, 0, 0, 0]); } #[test] fn test_encode_index_zero_0x00_4ch() { let px = [0, 0, 0, 0]; test_chunk( [[101, 102, 103, 104], px], [QOI_OP_RGBA, 101, 102, 103, 104, QOI_OP_INDEX | hash(px)], ); } #[test] fn test_encode_index_zero_0xff_4ch() { let px = [0, 0, 0, 0xff]; test_chunk( [[101, 102, 103, 104], px], [QOI_OP_RGBA, 101, 102, 103, 104, QOI_OP_RGBA, 0, 0, 0, 0xff], ); } #[test] fn test_encode_diff() { for x in 0..8_u8 { let x = [x.wrapping_sub(5), x.wrapping_sub(4), x.wrapping_sub(3)]; for dr in 0..3 { for dg in 0..3 { for db in 0..3 { if dr != 2 || dg != 2 || db != 2 { let r = x[0].wrapping_add(dr).wrapping_sub(2); let g = x[1].wrapping_add(dg).wrapping_sub(2); let b = x[2].wrapping_add(db).wrapping_sub(2); let d = QOI_OP_DIFF | dr << 4 | dg << 2 | db; test_chunk( [[1, 99, 2], x, [r, g, b]], [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGB, x[0], x[1], x[2], d], ); test_chunk( [[1, 99, 2, 0xff], [x[0], x[1], x[2], 9], [r, g, b, 9]], [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGBA, x[0], x[1], x[2], 9, d], ); } } } } } } #[test] fn test_encode_luma() { for x in (0..200_u8).step_by(4) { let x = [x.wrapping_mul(3), x.wrapping_sub(5), x.wrapping_sub(7)]; for dr_g in (0..16).step_by(4) { for dg in (0..64).step_by(8) { for db_g in (0..16).step_by(4) { if dr_g != 8 || dg != 32 || db_g != 8 { let r = x[0].wrapping_add(dr_g).wrapping_add(dg).wrapping_sub(40); let g = x[1].wrapping_add(dg).wrapping_sub(32); let b = x[2].wrapping_add(db_g).wrapping_add(dg).wrapping_sub(40); let d1 = QOI_OP_LUMA | dg; let d2 = (dr_g << 4) | db_g; test_chunk( [[1, 99, 2], x, [r, g, b]], [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGB, x[0], x[1], x[2], d1, d2], ); test_chunk( [[1, 99, 2, 0xff], [x[0], x[1], x[2], 9], [r, g, b, 9]], [QOI_OP_RGB, 1, 99, 2, QOI_OP_RGBA, x[0], x[1], x[2], 9, d1, d2], ); } } } } } } qoi-0.4.1/tests/test_gen.rs000064400000000000000000000224001046102023000137340ustar 00000000000000mod common; use bytemuck::cast_slice; use std::borrow::Cow; use std::fmt::Debug; use cfg_if::cfg_if; use rand::{ distributions::{Distribution, Standard}, rngs::StdRng, Rng, SeedableRng, }; use libqoi::{qoi_decode, qoi_encode}; use qoi::consts::{ QOI_HEADER_SIZE, QOI_MASK_2, QOI_OP_DIFF, QOI_OP_INDEX, QOI_OP_LUMA, QOI_OP_RGB, QOI_OP_RGBA, QOI_OP_RUN, QOI_PADDING_SIZE, }; use qoi::{decode_header, decode_to_vec, encode_to_vec}; use self::common::hash; struct GenState { index: [[u8; N]; 64], pixels: Vec, prev: [u8; N], len: usize, } impl GenState { pub fn with_capacity(capacity: usize) -> Self { Self { index: [[0; N]; 64], pixels: Vec::with_capacity(capacity * N), prev: Self::zero(), len: 0, } } pub fn write(&mut self, px: [u8; N]) { self.index[hash(px) as usize] = px; for i in 0..N { self.pixels.push(px[i]); } self.prev = px; self.len += 1; } pub fn pick_from_index(&self, rng: &mut impl Rng) -> [u8; N] { self.index[rng.gen_range(0_usize..64)] } pub fn zero() -> [u8; N] { let mut px = [0; N]; if N >= 4 { px[3] = 0xff; } px } } struct ImageGen { p_new: f64, p_index: f64, p_repeat: f64, p_diff: f64, p_luma: f64, } impl ImageGen { pub fn new_random(rng: &mut impl Rng) -> Self { let p: [f64; 6] = rng.gen(); let t = p.iter().sum::(); Self { p_new: p[0] / t, p_index: p[1] / t, p_repeat: p[2] / t, p_diff: p[3] / t, p_luma: p[4] / t, } } pub fn generate(&self, rng: &mut impl Rng, channels: usize, min_len: usize) -> Vec { match channels { 3 => self.generate_const::<_, 3>(rng, min_len), 4 => self.generate_const::<_, 4>(rng, min_len), _ => panic!(), } } fn generate_const(&self, rng: &mut R, min_len: usize) -> Vec where Standard: Distribution<[u8; N]>, { let mut s = GenState::::with_capacity(min_len); let zero = GenState::::zero(); while s.len < min_len { let mut p = rng.gen_range(0.0..1.0); if p < self.p_new { s.write(rng.gen()); continue; } p -= self.p_new; if p < self.p_index { let px = s.pick_from_index(rng); s.write(px); continue; } p -= self.p_index; if p < self.p_repeat { let px = s.prev; let n_repeat = rng.gen_range(1_usize..=70); for _ in 0..n_repeat { s.write(px); } continue; } p -= self.p_repeat; if p < self.p_diff { let mut px = s.prev; px[0] = px[0].wrapping_add(rng.gen_range(0_u8..4).wrapping_sub(2)); px[1] = px[1].wrapping_add(rng.gen_range(0_u8..4).wrapping_sub(2)); px[2] = px[2].wrapping_add(rng.gen_range(0_u8..4).wrapping_sub(2)); s.write(px); continue; } p -= self.p_diff; if p < self.p_luma { let mut px = s.prev; let vg = rng.gen_range(0_u8..64).wrapping_sub(32); let vr = rng.gen_range(0_u8..16).wrapping_sub(8).wrapping_add(vg); let vb = rng.gen_range(0_u8..16).wrapping_sub(8).wrapping_add(vg); px[0] = px[0].wrapping_add(vr); px[1] = px[1].wrapping_add(vg); px[2] = px[2].wrapping_add(vb); s.write(px); continue; } s.write(zero); } s.pixels } } fn format_encoded(encoded: &[u8]) -> String { let header = decode_header(encoded).unwrap(); let mut data = &encoded[QOI_HEADER_SIZE..encoded.len() - QOI_PADDING_SIZE]; let mut s = format!("{}x{}:{} = [", header.width, header.height, header.channels.as_u8()); while !data.is_empty() { let b1 = data[0]; data = &data[1..]; match b1 { QOI_OP_RGB => { s.push_str(&format!("rgb({},{},{})", data[0], data[1], data[2])); data = &data[3..]; } QOI_OP_RGBA => { s.push_str(&format!("rgba({},{},{},{})", data[0], data[1], data[2], data[3])); data = &data[4..]; } _ => match b1 & QOI_MASK_2 { QOI_OP_INDEX => s.push_str(&format!("index({})", b1 & 0x3f)), QOI_OP_RUN => s.push_str(&format!("run({})", b1 & 0x3f)), QOI_OP_DIFF => s.push_str(&format!( "diff({},{},{})", (b1 >> 4) & 0x03, (b1 >> 2) & 0x03, b1 & 0x03 )), QOI_OP_LUMA => { let b2 = data[0]; data = &data[1..]; s.push_str(&format!("luma({},{},{})", (b2 >> 4) & 0x0f, b1 & 0x3f, b2 & 0x0f)) } _ => {} }, } s.push_str(", "); } s.pop().unwrap(); s.pop().unwrap(); s.push(']'); s } fn check_roundtrip( msg: &str, mut data: &[u8], channels: usize, encode: E, decode: D, ) where E: Fn(&[u8], u32) -> Result, D: Fn(&[u8]) -> Result, VE: AsRef<[u8]>, VD: AsRef<[u8]>, EE: Debug, ED: Debug, { macro_rules! rt { ($data:expr, $n:expr) => { decode(encode($data, $n as _).unwrap().as_ref()).unwrap() }; } macro_rules! fail { ($msg:expr, $data:expr, $decoded:expr, $encoded:expr, $channels:expr) => { assert!( false, "{} roundtrip failed\n\n image: {:?}\ndecoded: {:?}\nencoded: {}", $msg, cast_slice::<_, [u8; $channels]>($data.as_ref()), cast_slice::<_, [u8; $channels]>($decoded.as_ref()), format_encoded($encoded.as_ref()), ); }; } let mut n_pixels = data.len() / channels; assert_eq!(n_pixels * channels, data.len()); // if all ok, return // ... but if roundtrip check fails, try to reduce the example to the smallest we can find if rt!(data, n_pixels).as_ref() == data { return; } // try removing pixels from the beginning while n_pixels > 1 { let slice = &data[..data.len() - channels]; if rt!(slice, n_pixels - 1).as_ref() != slice { data = slice; n_pixels -= 1; } else { break; } } // try removing pixels from the end while n_pixels > 1 { let slice = &data[channels..]; if rt!(slice, n_pixels - 1).as_ref() != slice { data = slice; n_pixels -= 1; } else { break; } } // try removing pixels from the middle let mut data = Cow::from(data); let mut pos = 1; while n_pixels > 1 && pos < n_pixels - 1 { let mut vec = data.to_vec(); for _ in 0..channels { vec.remove(pos * channels); } if rt!(vec.as_slice(), n_pixels - 1).as_ref() != vec.as_slice() { data = Cow::from(vec); n_pixels -= 1; } else { pos += 1; } } let encoded = encode(data.as_ref(), n_pixels as _).unwrap(); let decoded = decode(encoded.as_ref()).unwrap(); assert_ne!(decoded.as_ref(), data.as_ref()); if channels == 3 { fail!(msg, data, decoded, encoded, 3); } else { fail!(msg, data, decoded, encoded, 4); } } #[test] fn test_generated() { let mut rng = StdRng::seed_from_u64(0); let mut n_pixels = 0; while n_pixels < 20_000_000 { let min_len = rng.gen_range(1..=5000); let channels = rng.gen_range(3..=4); let gen = ImageGen::new_random(&mut rng); let img = gen.generate(&mut rng, channels, min_len); let encode = |data: &[u8], size| encode_to_vec(data, size, 1); let decode = |data: &[u8]| decode_to_vec(data).map(|r| r.1); let encode_c = |data: &[u8], size| qoi_encode(data, size, 1, channels as _); let decode_c = |data: &[u8]| qoi_decode(data, channels as _).map(|r| r.1); check_roundtrip("qoi-rust -> qoi-rust", &img, channels as _, encode, decode); check_roundtrip("qoi-rust -> qoi.h", &img, channels as _, encode, decode_c); check_roundtrip("qoi.h -> qoi-rust", &img, channels as _, encode_c, decode); let size = (img.len() / channels) as u32; let encoded = encode(&img, size).unwrap(); let encoded_c = encode_c(&img, size).unwrap(); cfg_if! { if #[cfg(feature = "reference")] { let eq = encoded.as_slice() == encoded_c.as_ref(); assert!(eq, "qoi-rust [reference mode] doesn't match qoi.h"); } else { let eq = encoded.len() == encoded_c.len(); assert!(eq, "qoi-rust [non-reference mode] length doesn't match qoi.h"); } } n_pixels += size; } } qoi-0.4.1/tests/test_misc.rs000064400000000000000000000002271046102023000141210ustar 00000000000000#[test] fn test_new_encoder() { // this used to fail due to `Bytes` not being `pub` let arr = [0u8]; let _ = qoi::Decoder::new(&arr[..]); }qoi-0.4.1/tests/test_ref.rs000064400000000000000000000067311046102023000137500ustar 00000000000000use std::fs::{self, File}; use std::path::{Path, PathBuf}; use anyhow::{bail, Result}; use cfg_if::cfg_if; use walkdir::{DirEntry, WalkDir}; use qoi::{decode_to_vec, encode_to_vec}; fn find_qoi_png_pairs(root: impl AsRef) -> Vec<(PathBuf, PathBuf)> { let root = root.as_ref(); let get_ext = |path: &Path| path.extension().unwrap_or_default().to_string_lossy().to_ascii_lowercase(); let check_qoi_png_pair = |path: &Path| { let (qoi, png) = (path.to_path_buf(), path.with_extension("png")); if qoi.is_file() && get_ext(&qoi) == "qoi" && png.is_file() { Some((qoi, png)) } else { None } }; let mut out = vec![]; if let Some(pair) = check_qoi_png_pair(root) { out.push(pair); } else if root.is_dir() { out.extend( WalkDir::new(root) .follow_links(true) .into_iter() .filter_map(Result::ok) .map(DirEntry::into_path) .filter_map(|p| check_qoi_png_pair(&p)), ) } out } struct Image { pub width: u32, pub height: u32, pub channels: u8, pub data: Vec, } impl Image { fn from_png(filename: &Path) -> Result { let decoder = png::Decoder::new(File::open(filename)?); let mut reader = decoder.read_info()?; let mut buf = vec![0; reader.output_buffer_size()]; let info = reader.next_frame(&mut buf)?; let bytes = &buf[..info.buffer_size()]; Ok(Self { width: info.width, height: info.height, channels: info.color_type.samples() as u8, data: bytes.to_vec(), }) } } fn compare_slices(name: &str, desc: &str, result: &[u8], expected: &[u8]) -> Result<()> { if result == expected { Ok(()) } else { if let Some(i) = (0..result.len().min(expected.len())).position(|i| result[i] != expected[i]) { bail!( "{}: {} mismatch at byte {}: expected {:?}, got {:?}", name, desc, i, &expected[i..(i + 4).min(expected.len())], &result[i..(i + 4).min(result.len())], ); } else { bail!( "{}: {} length mismatch: expected {}, got {}", name, desc, expected.len(), result.len() ); } } } #[test] fn test_reference_images() -> Result<()> { let pairs = find_qoi_png_pairs("assets"); assert!(!pairs.is_empty()); for (qoi_path, png_path) in &pairs { let png_name = png_path.file_name().unwrap_or_default().to_string_lossy(); let img = Image::from_png(png_path)?; println!("{} {} {} {}", png_name, img.width, img.height, img.channels); let encoded = encode_to_vec(&img.data, img.width, img.height)?; let expected = fs::read(qoi_path)?; assert_eq!(encoded.len(), expected.len()); // this should match regardless cfg_if! { if #[cfg(feature = "reference")] { compare_slices(&png_name, "encoding", &encoded, &expected)?; } } let (_header1, decoded1) = decode_to_vec(&encoded)?; let (_header2, decoded2) = decode_to_vec(&expected)?; compare_slices(&png_name, "decoding [1]", &decoded1, &img.data)?; compare_slices(&png_name, "decoding [2]", &decoded2, &img.data)?; } Ok(()) }