├── .cargo └── config.toml ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── deploy.yml │ ├── docker.yml │ └── oxipng.yml ├── .gitignore ├── .pre-commit-hooks.yaml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── MANUAL.txt ├── README.md ├── SECURITY.md ├── benches ├── deflate.rs ├── filters.rs ├── interlacing.rs ├── reductions.rs ├── strategies.rs └── zopfli.rs ├── scripts ├── compare.sh └── manual.sh ├── src ├── apng.rs ├── atomicmin.rs ├── cli.rs ├── colors.rs ├── deflate │ ├── deflater.rs │ ├── mod.rs │ └── zopfli_oxipng.rs ├── display_chunks.rs ├── error.rs ├── evaluate.rs ├── filters.rs ├── headers.rs ├── interlace.rs ├── lib.rs ├── main.rs ├── options.rs ├── png │ ├── mod.rs │ └── scan_lines.rs ├── rayon.rs ├── reduction │ ├── alpha.rs │ ├── bit_depth.rs │ ├── color.rs │ ├── mod.rs │ └── palette.rs └── sanity_checks.rs ├── tests ├── files │ ├── apng_file.png │ ├── badsrgb.png │ ├── c2pa-signed.png │ ├── corrupted_header.png │ ├── filter_0_for_grayscale_16.png │ ├── filter_0_for_grayscale_8.png │ ├── filter_0_for_grayscale_alpha_16.png │ ├── filter_0_for_grayscale_alpha_8.png │ ├── filter_0_for_palette_1.png │ ├── filter_0_for_palette_2.png │ ├── filter_0_for_palette_4.png │ ├── filter_0_for_rgb_16.png │ ├── filter_0_for_rgb_8.png │ ├── filter_0_for_rgba_16.png │ ├── filter_0_for_rgba_8.png │ ├── filter_1_for_grayscale_16.png │ ├── filter_1_for_grayscale_8.png │ ├── filter_1_for_grayscale_alpha_16.png │ ├── filter_1_for_grayscale_alpha_8.png │ ├── filter_1_for_palette_1.png │ ├── filter_1_for_palette_2.png │ ├── filter_1_for_palette_4.png │ ├── filter_1_for_rgb_16.png │ ├── filter_1_for_rgb_8.png │ ├── filter_1_for_rgba_16.png │ ├── filter_1_for_rgba_8.png │ ├── filter_2_for_grayscale_16.png │ ├── filter_2_for_grayscale_8.png │ ├── filter_2_for_grayscale_alpha_16.png │ ├── filter_2_for_grayscale_alpha_8.png │ ├── filter_2_for_palette_1.png │ ├── filter_2_for_palette_2.png │ ├── filter_2_for_palette_4.png │ ├── filter_2_for_rgb_16.png │ ├── filter_2_for_rgb_8.png │ ├── filter_2_for_rgba_16.png │ ├── filter_2_for_rgba_8.png │ ├── filter_3_for_grayscale_16.png │ ├── filter_3_for_grayscale_8.png │ ├── filter_3_for_grayscale_alpha_16.png │ ├── filter_3_for_grayscale_alpha_8.png │ ├── filter_3_for_palette_1.png │ ├── filter_3_for_palette_2.png │ ├── filter_3_for_palette_4.png │ ├── filter_3_for_rgb_16.png │ ├── filter_3_for_rgb_8.png │ ├── filter_3_for_rgba_16.png │ ├── filter_3_for_rgba_8.png │ ├── filter_4_for_grayscale_16.png │ ├── filter_4_for_grayscale_8.png │ ├── filter_4_for_grayscale_alpha_16.png │ ├── filter_4_for_grayscale_alpha_8.png │ ├── filter_4_for_palette_1.png │ ├── filter_4_for_palette_2.png │ ├── filter_4_for_palette_4.png │ ├── filter_4_for_rgb_16.png │ ├── filter_4_for_rgb_8.png │ ├── filter_4_for_rgba_16.png │ ├── filter_4_for_rgba_8.png │ ├── filter_5_for_grayscale_16.png │ ├── filter_5_for_grayscale_8.png │ ├── filter_5_for_grayscale_alpha_16.png │ ├── filter_5_for_grayscale_alpha_8.png │ ├── filter_5_for_palette_1.png │ ├── filter_5_for_palette_2.png │ ├── filter_5_for_palette_4.png │ ├── filter_5_for_rgb_16.png │ ├── filter_5_for_rgb_8.png │ ├── filter_5_for_rgba_16.png │ ├── filter_5_for_rgba_8.png │ ├── fix_errors.png │ ├── fully_optimized.png │ ├── grayscale_16_should_be_grayscale_1.png │ ├── grayscale_16_should_be_grayscale_16.png │ ├── grayscale_16_should_be_grayscale_8.png │ ├── grayscale_2_should_be_grayscale_1.png │ ├── grayscale_4_should_be_grayscale_1.png │ ├── grayscale_4_should_be_grayscale_2.png │ ├── grayscale_8_should_be_grayscale_1.png │ ├── grayscale_8_should_be_grayscale_2.png │ ├── grayscale_8_should_be_grayscale_4.png │ ├── grayscale_8_should_be_grayscale_8.png │ ├── grayscale_8_should_be_palette_1.png │ ├── grayscale_8_should_be_palette_2.png │ ├── grayscale_8_should_be_palette_4.png │ ├── grayscale_8_should_be_palette_8.png │ ├── grayscale_alpha_16_reduce_alpha.png │ ├── grayscale_alpha_16_should_be_grayscale_16.png │ ├── grayscale_alpha_16_should_be_grayscale_8.png │ ├── grayscale_alpha_16_should_be_grayscale_alpha_16.png │ ├── grayscale_alpha_16_should_be_grayscale_alpha_8.png │ ├── grayscale_alpha_16_should_be_grayscale_trns_16.png │ ├── grayscale_alpha_8_reduce_alpha.png │ ├── grayscale_alpha_8_should_be_grayscale_8.png │ ├── grayscale_alpha_8_should_be_grayscale_alpha_8.png │ ├── grayscale_alpha_8_should_be_grayscale_trns_1.png │ ├── grayscale_alpha_8_should_be_grayscale_trns_8.png │ ├── grayscale_alpha_8_should_be_palette_8.png │ ├── grayscale_trns_8_should_be_grayscale_1.png │ ├── interlaced_0_to_1_other_filter_mode.png │ ├── interlaced_grayscale_16_should_be_grayscale_16.png │ ├── interlaced_grayscale_16_should_be_grayscale_8.png │ ├── interlaced_grayscale_8_should_be_grayscale_8.png │ ├── interlaced_grayscale_alpha_16_should_be_grayscale_16.png │ ├── interlaced_grayscale_alpha_16_should_be_grayscale_8.png │ ├── interlaced_grayscale_alpha_16_should_be_grayscale_alpha_16.png │ ├── interlaced_grayscale_alpha_16_should_be_grayscale_alpha_8.png │ ├── interlaced_grayscale_alpha_8_should_be_grayscale_8.png │ ├── interlaced_grayscale_alpha_8_should_be_grayscale_alpha_8.png │ ├── interlaced_odd_width.png │ ├── interlaced_palette_1_should_be_palette_1.png │ ├── interlaced_palette_2_should_be_palette_1.png │ ├── interlaced_palette_2_should_be_palette_2.png │ ├── interlaced_palette_4_should_be_palette_1.png │ ├── interlaced_palette_4_should_be_palette_2.png │ ├── interlaced_palette_4_should_be_palette_4.png │ ├── interlaced_palette_8_should_be_grayscale_8.png │ ├── interlaced_palette_8_should_be_palette_1.png │ ├── interlaced_palette_8_should_be_palette_2.png │ ├── interlaced_palette_8_should_be_palette_4.png │ ├── interlaced_palette_8_should_be_palette_8.png │ ├── interlaced_rgb_16_should_be_grayscale_16.png │ ├── interlaced_rgb_16_should_be_grayscale_8.png │ ├── interlaced_rgb_16_should_be_palette_1.png │ ├── interlaced_rgb_16_should_be_palette_2.png │ ├── interlaced_rgb_16_should_be_palette_4.png │ ├── interlaced_rgb_16_should_be_palette_8.png │ ├── interlaced_rgb_16_should_be_rgb_16.png │ ├── interlaced_rgb_16_should_be_rgb_8.png │ ├── interlaced_rgb_8_should_be_grayscale_8.png │ ├── interlaced_rgb_8_should_be_palette_1.png │ ├── interlaced_rgb_8_should_be_palette_2.png │ ├── interlaced_rgb_8_should_be_palette_4.png │ ├── interlaced_rgb_8_should_be_palette_8.png │ ├── interlaced_rgb_8_should_be_rgb_8.png │ ├── interlaced_rgba_16_should_be_grayscale_16.png │ ├── interlaced_rgba_16_should_be_grayscale_8.png │ ├── interlaced_rgba_16_should_be_grayscale_alpha_16.png │ ├── interlaced_rgba_16_should_be_grayscale_alpha_8.png │ ├── interlaced_rgba_16_should_be_palette_1.png │ ├── interlaced_rgba_16_should_be_palette_2.png │ ├── interlaced_rgba_16_should_be_palette_4.png │ ├── interlaced_rgba_16_should_be_palette_8.png │ ├── interlaced_rgba_16_should_be_rgb_16.png │ ├── interlaced_rgba_16_should_be_rgb_8.png │ ├── interlaced_rgba_16_should_be_rgba_16.png │ ├── interlaced_rgba_16_should_be_rgba_8.png │ ├── interlaced_rgba_8_should_be_grayscale_8.png │ ├── interlaced_rgba_8_should_be_grayscale_alpha_8.png │ ├── interlaced_rgba_8_should_be_palette_1.png │ ├── interlaced_rgba_8_should_be_palette_2.png │ ├── interlaced_rgba_8_should_be_palette_4.png │ ├── interlaced_rgba_8_should_be_palette_8.png │ ├── interlaced_rgba_8_should_be_rgb_8.png │ ├── interlaced_rgba_8_should_be_rgba_8.png │ ├── interlaced_small_files.png │ ├── interlaced_vertical_filters.png │ ├── interlacing_0_to_1.png │ ├── interlacing_0_to_1_small_files.png │ ├── interlacing_1_to_0.png │ ├── interlacing_1_to_0_small_files.png │ ├── issue-140.png │ ├── issue-171.png │ ├── issue-175.png │ ├── issue-182.png │ ├── issue-42.png │ ├── issue-56.png │ ├── issue-58.png │ ├── issue-59.png │ ├── issue-60.png │ ├── issue-89.png │ ├── palette_1_should_be_palette_1.png │ ├── palette_2_should_be_grayscale_alpha_8.png │ ├── palette_2_should_be_palette_1.png │ ├── palette_2_should_be_palette_2.png │ ├── palette_4_should_be_palette_1.png │ ├── palette_4_should_be_palette_2.png │ ├── palette_4_should_be_palette_4.png │ ├── palette_8_should_be_grayscale_8.png │ ├── palette_8_should_be_palette_1.png │ ├── palette_8_should_be_palette_2.png │ ├── palette_8_should_be_palette_4.png │ ├── palette_8_should_be_palette_8.png │ ├── palette_8_should_be_rgb.png │ ├── palette_8_should_be_rgba.png │ ├── palette_should_be_reduced_with_bkgd.png │ ├── palette_should_be_reduced_with_both.png │ ├── palette_should_be_reduced_with_dupes.png │ ├── palette_should_be_reduced_with_missing.png │ ├── palette_should_be_reduced_with_unused.png │ ├── preserve_attrs.png │ ├── profile_adobe_rgb_disallow_gray.png │ ├── profile_gray_disallow_color.png │ ├── profile_srgb_allow_gray.png │ ├── profile_srgb_no_strip_disallow_gray.png │ ├── quiet_mode.png │ ├── raw_api.png │ ├── rgb_16_should_be_grayscale_16.png │ ├── rgb_16_should_be_grayscale_8.png │ ├── rgb_16_should_be_palette_1.png │ ├── rgb_16_should_be_palette_2.png │ ├── rgb_16_should_be_palette_4.png │ ├── rgb_16_should_be_palette_8.png │ ├── rgb_16_should_be_rgb_16.png │ ├── rgb_16_should_be_rgb_8.png │ ├── rgb_8_should_be_grayscale_8.png │ ├── rgb_8_should_be_palette_1.png │ ├── rgb_8_should_be_palette_2.png │ ├── rgb_8_should_be_palette_4.png │ ├── rgb_8_should_be_palette_8.png │ ├── rgb_8_should_be_rgb_8.png │ ├── rgb_trns_8_should_be_palette_8.png │ ├── rgba_16_reduce_alpha.png │ ├── rgba_16_should_be_grayscale_16.png │ ├── rgba_16_should_be_grayscale_8.png │ ├── rgba_16_should_be_grayscale_alpha_16.png │ ├── rgba_16_should_be_grayscale_alpha_8.png │ ├── rgba_16_should_be_palette_1.png │ ├── rgba_16_should_be_palette_2.png │ ├── rgba_16_should_be_palette_4.png │ ├── rgba_16_should_be_palette_8.png │ ├── rgba_16_should_be_rgb_16.png │ ├── rgba_16_should_be_rgb_8.png │ ├── rgba_16_should_be_rgb_trns_16.png │ ├── rgba_16_should_be_rgba_16.png │ ├── rgba_16_should_be_rgba_8.png │ ├── rgba_8_reduce_alpha.png │ ├── rgba_8_should_be_grayscale_8.png │ ├── rgba_8_should_be_grayscale_alpha_8.png │ ├── rgba_8_should_be_palette_1.png │ ├── rgba_8_should_be_palette_2.png │ ├── rgba_8_should_be_palette_4.png │ ├── rgba_8_should_be_palette_8.png │ ├── rgba_8_should_be_rgb_8.png │ ├── rgba_8_should_be_rgb_trns_8.png │ ├── rgba_8_should_be_rgba_8.png │ ├── small_files.png │ ├── strip_chunks_all.png │ ├── strip_chunks_list.png │ ├── strip_chunks_none.png │ ├── strip_chunks_safe.png │ ├── verbose_mode.png │ └── zopfli_mode.png ├── filters.rs ├── flags.rs ├── interlaced.rs ├── interlacing.rs ├── lib.rs ├── raw.rs ├── reduction.rs ├── regression.rs └── strategies.rs └── xtask ├── Cargo.lock ├── Cargo.toml └── src └── main.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --manifest-path xtask/Cargo.toml --" 3 | 4 | [target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'] 5 | runner = "qemu-aarch64" # May need to remove this if targeting AArch64 from an AArch64 Linux box 6 | 7 | [target.aarch64-unknown-linux-gnu] 8 | linker = "aarch64-linux-gnu-gcc" 9 | 10 | [target.aarch64-unknown-linux-musl] 11 | linker = "aarch64-linux-musl-gcc" 12 | # AArch64 Linux musl targets are repeatedly affected under varying 13 | # circumstances by fixedn't linking issues to compiler builtin symbols. 14 | # Linking to libgcc, though arguably an inelegant hack that's only portable 15 | # to our glibc from musl crosscompilation scenario, is effective to reliably 16 | # provide an implementation of those fundamental symbols over time, without 17 | # user-visible impacts on the final executables. See: 18 | # https://github.com/rust-lang/rust/issues/46651 19 | # https://github.com/rust-lang/compiler-builtins/issues/201 20 | # https://github.com/rust-lang/rust/issues/128401 (the issue most relevant to 21 | # our observed failures) 22 | # Note that these flags are not enforced on CI because CI overrides them through 23 | # the RUSTFLAG environment variable. Check out the CI workflow definition file at 24 | # .github/workflows/oxipng.yml for more info 25 | rustflags = ["-Clink-args=-lgcc"] 26 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | scripts 2 | .github 3 | .editorconfig 4 | .pre-commit-hooks.yaml 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.rs] 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | permissions: 9 | actions: read 10 | contents: write 11 | 12 | jobs: 13 | deploy: 14 | name: Deploy release 15 | 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 30 18 | 19 | # Prevent job from running on forks 20 | if: ${{ !github.event.repository.fork }} 21 | 22 | strategy: 23 | # Execute one job at a time to avoid potential race conditions in the 24 | # GitHub release management APIs. See: 25 | # https://github.com/softprops/action-gh-release/issues/445#issuecomment-2407940052 26 | max-parallel: 1 27 | matrix: 28 | target: 29 | - x86_64-unknown-linux-gnu 30 | - x86_64-unknown-linux-musl 31 | - aarch64-unknown-linux-gnu 32 | - aarch64-unknown-linux-musl 33 | - x86_64-pc-windows-msvc 34 | - i686-pc-windows-msvc 35 | - x86_64-apple-darwin 36 | - aarch64-apple-darwin 37 | 38 | steps: 39 | - name: Checkout source 40 | uses: actions/checkout@v4 41 | 42 | - name: Install Rust toolchain 43 | uses: actions-rust-lang/setup-rust-toolchain@v1 44 | with: 45 | toolchain: nightly 46 | cache-bin: false 47 | cache-shared-key: cache 48 | 49 | - name: Install cargo-deb 50 | if: endsWith(matrix.target, '-linux-gnu') 51 | uses: taiki-e/install-action@v2 52 | with: 53 | tool: cargo-deb 54 | 55 | - name: Get the Oxipng version 56 | id: oxipngMeta 57 | run: 58 | echo "version=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "oxipng").version')" 59 | >> "$GITHUB_OUTPUT" 60 | 61 | - name: Retrieve ${{ matrix.target }} binary 62 | uses: dawidd6/action-download-artifact@v10 63 | with: 64 | workflow: oxipng.yml 65 | commit: ${{ env.GITHUB_SHA }} 66 | name: Oxipng binary (${{ matrix.target }}) 67 | path: target 68 | 69 | - name: Generate up to date manual 70 | run: scripts/manual.sh 71 | 72 | - name: Build archives 73 | working-directory: target 74 | run: | 75 | ARCHIVE_NAME="oxipng-${{ steps.oxipngMeta.outputs.version }}-${{ matrix.target }}" 76 | 77 | mkdir "$ARCHIVE_NAME" 78 | cp ../CHANGELOG.md ../README.md ../MANUAL.txt "$ARCHIVE_NAME" 79 | 80 | case '${{ matrix.target }}' in 81 | *-windows-*) 82 | cp ../LICENSE "$ARCHIVE_NAME/LICENSE.txt" 83 | cp oxipng.exe "$ARCHIVE_NAME" 84 | zip "${ARCHIVE_NAME}.zip" "$ARCHIVE_NAME"/*;; 85 | *) 86 | cp ../LICENSE "$ARCHIVE_NAME" 87 | cp oxipng "$ARCHIVE_NAME" 88 | # Execute permissions are not stored in artifact files, 89 | # so make the binary world-executable to meet user 90 | # expectations set by preceding releases. 91 | # Related issue: 92 | # https://github.com/shssoichiro/oxipng/issues/575 93 | chmod ugo+x "$ARCHIVE_NAME"/oxipng 94 | tar -vczf "${ARCHIVE_NAME}.tar.gz" "$ARCHIVE_NAME"/*;; 95 | esac 96 | 97 | - name: Install AArch64 libc components 98 | if: matrix.target == 'aarch64-unknown-linux-gnu' 99 | run: | 100 | sudo apt-get -yq update 101 | # The shared libc AArch64 libraries are needed for cargo deb below 102 | # to be able to infer package requirements with dpkg-shlibdeps 103 | # properly 104 | sudo apt-get -yq install libc6-arm64-cross libgcc-s1-arm64-cross 105 | 106 | - name: Build Debian packages 107 | if: endsWith(matrix.target, '-linux-gnu') 108 | env: 109 | # The *-arm64-cross packages above install AArch64 libraries in 110 | # /usr//lib instead of /usr/lib/, as expected by 111 | # cargo-deb and dpkg-shlibdeps to find such shared libraries. 112 | # Make both of them visible to such commands by adding that directory 113 | # to the dynamic linker's library search path. See: 114 | # - ("Errors" section) 115 | # - 116 | LD_LIBRARY_PATH: /usr/aarch64-linux-gnu/lib 117 | run: | 118 | mkdir -p "target/${{ matrix.target }}/release" 119 | mv target/oxipng "target/${{ matrix.target }}/release" 120 | cargo deb --target "${{ matrix.target }}" --no-build --no-strip 121 | 122 | - name: Create release notes 123 | run: tail -n +3 CHANGELOG.md | sed -e '/^$/,$d' > RELEASE_NOTES.txt 124 | 125 | - name: Create release 126 | uses: softprops/action-gh-release@v2 127 | with: 128 | name: v${{ steps.oxipngMeta.outputs.version }} 129 | body_path: RELEASE_NOTES.txt 130 | files: | 131 | target/*.zip 132 | target/*.tar.gz 133 | target/${{ matrix.target }}/debian/*.deb 134 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | tags: 7 | - 'v*.*.*' 8 | pull_request: 9 | types: 10 | - opened 11 | - synchronize 12 | workflow_dispatch: 13 | inputs: 14 | use_cache: 15 | description: "Use build cache" 16 | required: true 17 | type: boolean 18 | default: true 19 | 20 | env: 21 | REGISTRY: ghcr.io 22 | # ghcr.io/OWNER/REPO 23 | IMAGE_NAME: ${{ github.repository }} 24 | 25 | jobs: 26 | build: 27 | runs-on: ubuntu-latest 28 | permissions: 29 | contents: read 30 | packages: write 31 | id-token: write 32 | attestations: write 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | 38 | - name: Set up QEMU 39 | uses: docker/setup-qemu-action@v3 40 | 41 | # Workaround: https://github.com/docker/build-push-action/issues/461 42 | - name: Setup Docker buildx 43 | uses: docker/setup-buildx-action@v3 44 | 45 | # Login against a Docker registry except on PR 46 | # https://github.com/docker/login-action 47 | - name: Log into registry ${{ env.REGISTRY }} 48 | if: github.event_name != 'pull_request' 49 | uses: docker/login-action@v3 50 | with: 51 | registry: ${{ env.REGISTRY }} 52 | username: ${{ github.actor }} 53 | password: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | # Extract metadata (tags, labels) for Docker 56 | # For some reason the title have to be set manually 57 | # https://github.com/docker/metadata-action 58 | - name: Extract Docker metadata 59 | id: meta 60 | uses: docker/metadata-action@v5 61 | with: 62 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 63 | labels: | 64 | org.opencontainers.image.title=Oxipng 65 | annotations: | 66 | org.opencontainers.image.title=Oxipng 67 | 68 | # Build and push Docker image with Buildx (don't push on PR) 69 | # Cache isn't used for tags and on workflow_dispatch if specified 70 | # https://github.com/docker/build-push-action 71 | - name: Build and push Docker image 72 | id: build-and-push 73 | uses: docker/build-push-action@v6 74 | with: 75 | context: . 76 | platforms: linux/amd64,linux/arm64 77 | push: ${{ github.event_name != 'pull_request' }} 78 | tags: ${{ steps.meta.outputs.tags }} 79 | labels: ${{ steps.meta.outputs.labels }} 80 | cache-from: type=gha 81 | cache-to: type=gha,mode=max 82 | no-cache: ${{ (github.event_name == 'workflow_dispatch' && !inputs.use_cache) || startsWith(github.ref, 'refs/tags/') }} 83 | 84 | # Attest the build provenance 85 | # TODO: enable push to registry when referrers API will be supported by ghcr.io 86 | - name: Attest Build Provenance 87 | if: github.event_name != 'pull_request' 88 | uses: actions/attest-build-provenance@v2 89 | with: 90 | subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 91 | subject-digest: ${{ steps.build-and-push.outputs.digest }} 92 | push-to-registry: false 93 | -------------------------------------------------------------------------------- /.github/workflows/oxipng.yml: -------------------------------------------------------------------------------- 1 | name: oxipng 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - synchronize 9 | workflow_dispatch: 10 | 11 | jobs: 12 | ci: 13 | name: CI 14 | 15 | runs-on: ${{ matrix.os }} 16 | timeout-minutes: 60 17 | 18 | # Prevent tags and in-repo PRs from triggering this workflow more than once for a commit 19 | if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork) 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | target: 25 | - x86_64-unknown-linux-gnu 26 | - x86_64-unknown-linux-musl 27 | - aarch64-unknown-linux-gnu 28 | - aarch64-unknown-linux-musl 29 | - x86_64-pc-windows-msvc 30 | - i686-pc-windows-msvc 31 | - x86_64-apple-darwin 32 | - aarch64-apple-darwin 33 | 34 | include: 35 | - target: x86_64-unknown-linux-gnu 36 | os: ubuntu-22.04 37 | target-apt-arch: amd64 38 | - target: x86_64-unknown-linux-musl 39 | os: ubuntu-22.04 40 | target-apt-arch: amd64 41 | - target: aarch64-unknown-linux-gnu 42 | os: ubuntu-22.04 43 | target-apt-arch: arm64 44 | - target: aarch64-unknown-linux-musl 45 | os: ubuntu-22.04 46 | target-apt-arch: arm64 47 | - target: x86_64-pc-windows-msvc 48 | os: windows-latest 49 | - target: i686-pc-windows-msvc 50 | os: windows-latest 51 | - target: x86_64-apple-darwin 52 | os: macos-13 # x86_64 runner 53 | - target: aarch64-apple-darwin 54 | os: macos-14 # ARM64 runner 55 | 56 | env: 57 | CARGO_BUILD_TARGET: ${{ matrix.target }} 58 | # Hopefully temporary workaround for https://github.com/rust-lang/rust/issues/128401 59 | # See the comments for similar rustflag settings at .cargo/config.toml for more context 60 | RUSTFLAGS: ${{ matrix.target == 'aarch64-unknown-linux-musl' && '-Zlocation-detail=none -Clink-args=-lgcc' || '-Zlocation-detail=none' }} 61 | 62 | steps: 63 | - name: Checkout source 64 | uses: actions/checkout@v4 65 | with: 66 | persist-credentials: false 67 | 68 | - name: Set up Ubuntu multiarch 69 | if: startsWith(matrix.os, 'ubuntu') && matrix.target-apt-arch != 'amd64' 70 | run: | 71 | readonly DISTRO_CODENAME=jammy 72 | sudo dpkg --add-architecture "${{ matrix.target-apt-arch }}" 73 | sudo sed -i "s/^deb http/deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH)] http/" /etc/apt/sources.list 74 | sudo sed -i "s/^deb mirror/deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH)] mirror/" /etc/apt/sources.list 75 | for suite in '' '-updates' '-backports' '-security'; do 76 | echo "deb [arch=${{ matrix.target-apt-arch }}] http://ports.ubuntu.com/ $DISTRO_CODENAME$suite main universe multiverse" | \ 77 | sudo tee -a /etc/apt/sources.list >/dev/null 78 | done 79 | 80 | - name: Install musl development files 81 | if: endsWith(matrix.target, '-musl') 82 | run: | 83 | sudo apt-get -yq update 84 | sudo apt-get -yq install musl-tools musl-dev:${{ matrix.target-apt-arch }} 85 | 86 | - name: Install QEMU and AArch64 cross compiler 87 | if: startsWith(matrix.target, 'aarch64-unknown-linux') 88 | run: | 89 | sudo apt-get -yq update 90 | # libc6 must be present to run executables dynamically linked 91 | # against glibc for the target architecture 92 | sudo apt-get -yq install qemu-user gcc-aarch64-linux-gnu libc6:arm64 93 | 94 | - name: Install Rust toolchain 95 | uses: actions-rust-lang/setup-rust-toolchain@v1 96 | with: 97 | toolchain: nightly 98 | target: ${{ env.CARGO_BUILD_TARGET }} 99 | components: clippy, rustfmt 100 | cache-bin: false 101 | cache-shared-key: cache 102 | 103 | - name: Install nextest 104 | uses: taiki-e/install-action@nextest 105 | 106 | - name: Install cargo-hack 107 | if: matrix.target == 'x86_64-unknown-linux-gnu' 108 | uses: taiki-e/install-action@cargo-hack 109 | 110 | - name: Install clippy-sarif 111 | if: matrix.target == 'x86_64-unknown-linux-gnu' 112 | uses: taiki-e/install-action@v2 113 | with: 114 | tool: clippy-sarif 115 | 116 | - name: Install sarif-fmt 117 | if: matrix.target == 'x86_64-unknown-linux-gnu' 118 | uses: taiki-e/install-action@v2 119 | with: 120 | tool: sarif-fmt 121 | 122 | - name: Run rustfmt 123 | if: matrix.target == 'x86_64-unknown-linux-gnu' 124 | run: cargo fmt --check 125 | 126 | - name: Run Clippy for all feature combinations 127 | if: matrix.target == 'x86_64-unknown-linux-gnu' 128 | run: > 129 | set -o pipefail; 130 | cargo hack clippy --no-deps --all-targets --feature-powerset \ 131 | --exclude-features sanity-checks,system-libdeflate \ 132 | --message-format=json -- -D warnings \ 133 | | clippy-sarif 134 | | tee clippy-results.sarif 135 | | sarif-fmt 136 | 137 | - name: Run tests 138 | run: | 139 | cargo nextest run --release --features sanity-checks 140 | cargo test --doc --release --features sanity-checks 141 | 142 | - name: Build benchmarks 143 | run: cargo bench --no-run 144 | 145 | - name: Build docs 146 | if: matrix.target == 'x86_64-unknown-linux-gnu' 147 | run: cargo doc --release --no-deps 148 | 149 | - name: Build CLI binary 150 | run: cargo build --release 151 | 152 | - name: Upload CLI binary as artifact 153 | uses: actions/upload-artifact@v4 154 | with: 155 | name: Oxipng binary (${{ matrix.target }}) 156 | path: | 157 | target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng 158 | target/${{ env.CARGO_BUILD_TARGET }}/release/oxipng.exe 159 | 160 | - name: Upload analysis results to GitHub 161 | uses: github/codeql-action/upload-sarif@v3 162 | if: always() && matrix.target == 'x86_64-unknown-linux-gnu' 163 | continue-on-error: true 164 | with: 165 | sarif_file: clippy-results.sarif 166 | category: clippy 167 | 168 | msrv-check: 169 | name: MSRV check 170 | 171 | runs-on: ubuntu-latest 172 | timeout-minutes: 30 173 | 174 | # Prevent tags and in-repo PRs from triggering this workflow more than once for a commit 175 | if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork) 176 | 177 | steps: 178 | - name: Checkout source 179 | uses: actions/checkout@v4 180 | with: 181 | persist-credentials: false 182 | 183 | - name: Install MSRV Rust toolchain 184 | uses: actions-rust-lang/setup-rust-toolchain@v1 185 | with: 186 | toolchain: 1.74.0 187 | cache-bin: false 188 | cache-shared-key: cache 189 | 190 | - name: Install nextest 191 | uses: taiki-e/install-action@nextest 192 | 193 | - name: Run tests 194 | run: | 195 | cargo nextest run --release --features sanity-checks 196 | cargo test --doc --release --features sanity-checks 197 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.bk 3 | .DS_Store 4 | *.out.png 5 | /.idea 6 | /node_modules 7 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: oxipng 2 | name: oxipng 3 | description: 'Multithreaded PNG optimizer written in Rust.' 4 | entry: oxipng 5 | language: rust 6 | types: [png] 7 | require_serial: true 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Joshua Holmer "] 3 | categories = ["command-line-utilities", "compression"] 4 | description = "A lossless PNG compression optimizer" 5 | keywords = ["png", "image-compression", "optimization", "multi-threading", "lossless"] 6 | documentation = "https://docs.rs/oxipng" 7 | edition = "2021" 8 | exclude = [ 9 | ".editorconfig", 10 | ".gitattributes", 11 | ".github/*", 12 | ".gitignore", 13 | ".pre-commit-hooks.yaml", 14 | "Dockerfile", 15 | "scripts/*", 16 | "tests/*", 17 | "xtask/*", 18 | ] 19 | homepage = "https://github.com/shssoichiro/oxipng" 20 | license = "MIT" 21 | name = "oxipng" 22 | repository = "https://github.com/shssoichiro/oxipng" 23 | version = "9.1.5" 24 | rust-version = "1.74.0" 25 | 26 | [badges] 27 | travis-ci = { repository = "shssoichiro/oxipng", branch = "master" } 28 | maintenance = { status = "actively-developed" } 29 | 30 | [[bin]] 31 | doc = false 32 | name = "oxipng" 33 | path = "src/main.rs" 34 | required-features = ["binary"] 35 | 36 | [[bench]] 37 | name = "zopfli" 38 | required-features = ["zopfli"] 39 | 40 | [dependencies] 41 | zopfli = { version = "0.8.2", optional = true, default-features = false, features = ["std", "zlib"] } 42 | rgb = "0.8.50" 43 | indexmap = "2.9.0" 44 | libdeflater = "1.23.1" 45 | log = "0.4.27" 46 | bitvec = "1.0.1" 47 | rustc-hash = "2.1.1" 48 | 49 | [dependencies.env_logger] 50 | optional = true 51 | default-features = false 52 | features = ["auto-color"] 53 | version = "0.11.8" 54 | 55 | [dependencies.crossbeam-channel] 56 | optional = true 57 | version = "0.5.15" 58 | 59 | [dependencies.filetime] 60 | optional = true 61 | version = "0.2.25" 62 | 63 | [dependencies.rayon] 64 | optional = true 65 | version = "1.10.0" 66 | 67 | [dependencies.clap] 68 | optional = true 69 | version = "4.5.36" 70 | features = ["wrap_help"] 71 | 72 | [target.'cfg(windows)'.dependencies.glob] 73 | optional = true 74 | version = "0.3.2" 75 | 76 | [dependencies.image] 77 | optional = true 78 | default-features = false 79 | features = ["png"] 80 | version = "0.25.6" 81 | 82 | [features] 83 | binary = ["dep:clap", "dep:glob", "dep:env_logger"] 84 | default = ["binary", "parallel", "zopfli", "filetime"] 85 | parallel = ["dep:rayon", "indexmap/rayon", "dep:crossbeam-channel"] 86 | freestanding = ["libdeflater/freestanding"] 87 | sanity-checks = ["dep:image"] 88 | zopfli = ["dep:zopfli"] 89 | filetime = ["dep:filetime"] 90 | system-libdeflate = ["libdeflater/dynamic"] 91 | 92 | [lib] 93 | name = "oxipng" 94 | path = "src/lib.rs" 95 | 96 | [profile.dev] 97 | opt-level = 2 98 | 99 | [profile.release] 100 | lto = "fat" 101 | strip = "symbols" 102 | panic = "abort" 103 | 104 | [package.metadata.deb] 105 | assets = [ 106 | ["target/release/oxipng", "usr/bin/", "755"], 107 | ["target/xtask/mangen/manpages/oxipng.1", "usr/share/man/man1/", "644"], 108 | ["README.md", "usr/share/doc/oxipng/", "644"], 109 | ["CHANGELOG.md", "usr/share/doc/oxipng/", "644"], 110 | ] 111 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # check=error=true 3 | FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx 4 | 5 | FROM --platform=$BUILDPLATFORM rust:1.74-alpine AS base 6 | 7 | RUN apk update && \ 8 | apk add \ 9 | gcc \ 10 | g++ \ 11 | clang 12 | 13 | COPY --from=xx / / 14 | 15 | ARG TARGETPLATFORM 16 | RUN xx-info env 17 | 18 | RUN xx-apk add \ 19 | gcc \ 20 | musl-dev \ 21 | libdeflate 22 | 23 | WORKDIR /src 24 | 25 | COPY . . 26 | 27 | RUN --mount=type=cache,target=/root/.cargo/git/db \ 28 | --mount=type=cache,target=/root/.cargo/registry/cache \ 29 | --mount=type=cache,target=/root/.cargo/registry/index \ 30 | xx-cargo build --release && \ 31 | xx-verify /src/target/$(xx-cargo --print-target-triple)/release/oxipng && \ 32 | cp /src/target/$(xx-cargo --print-target-triple)/release/oxipng /src/target/oxipng 33 | 34 | FROM scratch AS tool 35 | 36 | LABEL org.opencontainers.image.title="Oxipng" 37 | LABEL org.opencontainers.image.description="Multithreaded PNG optimizer written in Rust" 38 | LABEL org.opencontainers.image.authors="Joshua Holmer " 39 | LABEL org.opencontainers.image.licenses="MIT" 40 | LABEL org.opencontainers.image.source="https://github.com/shssoichiro/oxipng" 41 | 42 | COPY --from=base /src/target/oxipng /usr/local/bin/oxipng 43 | 44 | WORKDIR /work 45 | ENTRYPOINT [ "oxipng" ] 46 | CMD [ "--help" ] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Joshua Holmer 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANUAL.txt: -------------------------------------------------------------------------------- 1 | oxipng 9.1.5 2 | Losslessly improve compression of PNG files 3 | 4 | Usage: oxipng [OPTIONS] ... 5 | 6 | Arguments: 7 | ... 8 | File(s) to compress (use '-' for stdin) 9 | 10 | Options: 11 | -o, --opt 12 | Set the optimization level preset. The default level 2 is quite fast and provides good 13 | compression. Lower levels are faster, higher levels provide better compression, though 14 | with increasingly diminishing returns. 15 | 16 | 0 => --zc 5 --fast (1 trial, determined heuristically) 17 | 1 => --zc 10 --fast (1 trial, determined heuristically) 18 | 2 => --zc 11 -f 0,1,6,7 --fast (4 fast trials, 1 main trial) 19 | 3 => --zc 11 -f 0,7,8,9 (4 trials) 20 | 4 => --zc 12 -f 0,7,8,9 (4 trials) 21 | 5 => --zc 12 -f 0,1,2,5,6,7,8,9 (8 trials) 22 | 6 => --zc 12 -f 0-9 (10 trials) 23 | max => (stable alias for the max level) 24 | 25 | Manually specifying a compression option (zc, f, etc.) will override the optimization 26 | preset, regardless of the order you write the arguments. 27 | 28 | [default: 2] 29 | 30 | -r, --recursive 31 | When directories are given as input, traverse the directory trees and optimize all PNG 32 | files found (files with “.png” or “.apng” extension). 33 | 34 | --dir 35 | Write output file(s) to . If the directory does not exist, it will be created. 36 | Note that this will not preserve the directory structure of the input files when used with 37 | '--recursive'. 38 | 39 | --out 40 | Write output file to 41 | 42 | --stdout 43 | Write output to stdout 44 | 45 | -p, --preserve 46 | Preserve file permissions and timestamps if possible 47 | 48 | -P, --pretend 49 | Do not write any files, only show compression results 50 | 51 | -s 52 | Strip safely-removable chunks, same as '--strip safe' 53 | 54 | --strip 55 | Strip metadata chunks, where is one of: 56 | 57 | safe => Strip all non-critical chunks, except for the following: 58 | cICP, iCCP, sRGB, pHYs, acTL, fcTL, fdAT 59 | all => Strip all non-critical chunks 60 | => Strip chunks in the comma-separated list, e.g. 'bKGD,cHRM' 61 | 62 | CAUTION: 'all' will convert APNGs to standard PNGs. 63 | 64 | Note that 'bKGD', 'sBIT' and 'hIST' will be forcibly stripped if the color type or bit 65 | depth is changed, regardless of any options set. 66 | 67 | The default when --strip is not passed is to keep all metadata. 68 | 69 | --keep 70 | Strip all metadata chunks except those in the comma-separated list. The special value 71 | 'display' includes chunks that affect the image appearance, equivalent to '--strip safe'. 72 | 73 | E.g. '--keep eXIf,display' will strip chunks, keeping only eXIf and those that affect the 74 | image appearance. 75 | 76 | -a, --alpha 77 | Perform additional optimization on images with an alpha channel, by altering the color 78 | values of fully transparent pixels. This is generally recommended for better compression, 79 | but take care as while this is “visually lossless”, it is technically a lossy 80 | transformation and may be unsuitable for some applications. 81 | 82 | -i, --interlace 83 | Set the PNG interlacing type, where is one of: 84 | 85 | 0 => Remove interlacing from all images that are processed 86 | 1 => Apply Adam7 interlacing on all images that are processed 87 | keep => Keep the existing interlacing type of each image 88 | 89 | Note that interlacing can add 25-50% to the size of an optimized image. Only use it if you 90 | believe the benefits outweigh the costs for your use case. 91 | 92 | [default: 0] 93 | 94 | --scale16 95 | Forcibly reduce images with 16 bits per channel to 8 bits per channel. This is a lossy 96 | operation but can provide significant savings when you have no need for higher depth. 97 | Reduction is performed by scaling the values such that, e.g. 0x00FF is reduced to 0x01 98 | rather than 0x00. 99 | 100 | Without this flag, 16-bit images will only be reduced in depth if it can be done 101 | losslessly. 102 | 103 | -v, --verbose... 104 | Run in verbose mode (use twice to increase verbosity) 105 | 106 | -q, --quiet 107 | Run in quiet mode 108 | 109 | -f, --filters 110 | Perform compression trials with each of the given filter types. You can specify a 111 | comma-separated list, or a range of values. E.g. '-f 0-3' is the same as '-f 0,1,2,3'. 112 | 113 | PNG delta filters (apply the same filter to every line) 114 | 0 => None (recommended to always include this filter) 115 | 1 => Sub 116 | 2 => Up 117 | 3 => Average 118 | 4 => Paeth 119 | 120 | Heuristic strategies (try to find the best delta filter for each line) 121 | 5 => MinSum Minimum sum of absolute differences 122 | 6 => Entropy Highest Shannon entropy 123 | 7 => Bigrams Lowest count of distinct bigrams 124 | 8 => BigEnt Highest Shannon entropy of bigrams 125 | 9 => Brute Smallest compressed size (slow) 126 | 127 | The default value depends on the optimization level preset. 128 | 129 | --fast 130 | Perform a fast compression evaluation of each enabled filter, followed by a single main 131 | compression trial of the best result. Recommended if you have more filters enabled than 132 | CPU cores. 133 | 134 | --zc 135 | Deflate compression level (0-12) for main compression trials. The levels here are defined 136 | by the libdeflate compression library. 137 | 138 | The default value depends on the optimization level preset. 139 | 140 | --nb 141 | Do not change bit depth 142 | 143 | --nc 144 | Do not change color type 145 | 146 | --np 147 | Do not change color palette 148 | 149 | --ng 150 | Do not change to or from grayscale 151 | 152 | --nx 153 | Do not perform any transformations and do not deinterlace by default. 154 | 155 | --nz 156 | Do not recompress IDAT unless required due to transformations. Recompression of other 157 | compressed chunks (such as iCCP) will also be disabled. Note that the combination of 158 | '--nx' and '--nz' will fully disable all optimization. 159 | 160 | --fix 161 | Do not perform checksum validation of PNG chunks. This may allow some files with errors to 162 | be processed successfully. 163 | 164 | --force 165 | Write the output even if it is larger than the input 166 | 167 | -Z, --zopfli 168 | Use the much slower but stronger Zopfli compressor for main compression trials. 169 | Recommended use is with '-o max' and '--fast'. 170 | 171 | --zi 172 | Set the number of iterations to use for Zopfli compression. Using fewer iterations may 173 | speed up compression for large files. This option requires '--zopfli' to be set. 174 | 175 | [default: 15] 176 | 177 | --timeout 178 | Maximum amount of time, in seconds, to spend on optimizations. Oxipng will check the 179 | timeout before each transformation or compression trial, and will stop trying to optimize 180 | the file if the timeout is exceeded. Note that this does not cut short any operations that 181 | are already in progress, so it is currently of limited effectiveness for large files with 182 | high compression levels. 183 | 184 | -t, --threads 185 | Set the maximum number of threads to use. Oxipng uses multithreading to evaluate multiple 186 | optimizations on the same file in parallel as well as process multiple files in parallel. 187 | You can set this to a lower value if you need to limit memory or CPU usage. 188 | 189 | [default: num logical CPUs] 190 | 191 | --sequential 192 | Process multiple files sequentially rather than in parallel. Use this if you need 193 | determinism in the processing order. Note this is not necessary if using '--threads 1'. 194 | 195 | -h, --help 196 | Print help (see a summary with '-h') 197 | 198 | -V, --version 199 | Print version 200 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest version will be supported with security updates. We will not maintain old branches. 6 | For this reason, you should attempt to always use the latest released version. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | To privately report a vulnerability in oxipng, please visit [our advisories page](https://github.com/shssoichiro/oxipng/security/advisories) and use the button to report a vulnerability. 11 | We will review it as soon as possible. 12 | 13 | For vulnerabilities in dependencies, please open a pull request updating the dependency to a patched version. 14 | -------------------------------------------------------------------------------- /benches/deflate.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate oxipng; 4 | extern crate test; 5 | 6 | use std::path::PathBuf; 7 | 8 | use oxipng::{internal_tests::*, *}; 9 | use test::Bencher; 10 | 11 | #[bench] 12 | fn deflate_16_bits(b: &mut Bencher) { 13 | let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); 14 | let png = PngData::new(&input, &Options::default()).unwrap(); 15 | 16 | b.iter(|| deflate(png.raw.data.as_ref(), 12, None)); 17 | } 18 | 19 | #[bench] 20 | fn deflate_8_bits(b: &mut Bencher) { 21 | let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); 22 | let png = PngData::new(&input, &Options::default()).unwrap(); 23 | 24 | b.iter(|| deflate(png.raw.data.as_ref(), 12, None)); 25 | } 26 | 27 | #[bench] 28 | fn deflate_4_bits(b: &mut Bencher) { 29 | let input = test::black_box(PathBuf::from( 30 | "tests/files/palette_4_should_be_palette_4.png", 31 | )); 32 | let png = PngData::new(&input, &Options::default()).unwrap(); 33 | 34 | b.iter(|| deflate(png.raw.data.as_ref(), 12, None)); 35 | } 36 | 37 | #[bench] 38 | fn deflate_2_bits(b: &mut Bencher) { 39 | let input = test::black_box(PathBuf::from( 40 | "tests/files/palette_2_should_be_palette_2.png", 41 | )); 42 | let png = PngData::new(&input, &Options::default()).unwrap(); 43 | 44 | b.iter(|| deflate(png.raw.data.as_ref(), 12, None)); 45 | } 46 | 47 | #[bench] 48 | fn deflate_1_bits(b: &mut Bencher) { 49 | let input = test::black_box(PathBuf::from( 50 | "tests/files/palette_1_should_be_palette_1.png", 51 | )); 52 | let png = PngData::new(&input, &Options::default()).unwrap(); 53 | 54 | b.iter(|| deflate(png.raw.data.as_ref(), 12, None)); 55 | } 56 | 57 | #[bench] 58 | fn inflate_generic(b: &mut Bencher) { 59 | let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); 60 | let png = PngData::new(&input, &Options::default()).unwrap(); 61 | 62 | b.iter(|| inflate(png.idat_data.as_ref(), png.raw.ihdr.raw_data_size())); 63 | } 64 | -------------------------------------------------------------------------------- /benches/interlacing.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate oxipng; 4 | extern crate test; 5 | 6 | use std::path::PathBuf; 7 | 8 | use oxipng::{internal_tests::*, *}; 9 | use test::Bencher; 10 | 11 | #[bench] 12 | fn interlacing_16_bits(b: &mut Bencher) { 13 | let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); 14 | let png = PngData::new(&input, &Options::default()).unwrap(); 15 | 16 | b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); 17 | } 18 | 19 | #[bench] 20 | fn interlacing_8_bits(b: &mut Bencher) { 21 | let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); 22 | let png = PngData::new(&input, &Options::default()).unwrap(); 23 | 24 | b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); 25 | } 26 | 27 | #[bench] 28 | fn interlacing_4_bits(b: &mut Bencher) { 29 | let input = test::black_box(PathBuf::from( 30 | "tests/files/palette_4_should_be_palette_4.png", 31 | )); 32 | let png = PngData::new(&input, &Options::default()).unwrap(); 33 | 34 | b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); 35 | } 36 | 37 | #[bench] 38 | fn interlacing_2_bits(b: &mut Bencher) { 39 | let input = test::black_box(PathBuf::from( 40 | "tests/files/palette_2_should_be_palette_2.png", 41 | )); 42 | let png = PngData::new(&input, &Options::default()).unwrap(); 43 | 44 | b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); 45 | } 46 | 47 | #[bench] 48 | fn interlacing_1_bits(b: &mut Bencher) { 49 | let input = test::black_box(PathBuf::from( 50 | "tests/files/palette_1_should_be_palette_1.png", 51 | )); 52 | let png = PngData::new(&input, &Options::default()).unwrap(); 53 | 54 | b.iter(|| png.raw.change_interlacing(Interlacing::Adam7)); 55 | } 56 | 57 | #[bench] 58 | fn deinterlacing_16_bits(b: &mut Bencher) { 59 | let input = test::black_box(PathBuf::from( 60 | "tests/files/interlaced_rgb_16_should_be_rgb_16.png", 61 | )); 62 | let png = PngData::new(&input, &Options::default()).unwrap(); 63 | 64 | b.iter(|| png.raw.change_interlacing(Interlacing::None)); 65 | } 66 | 67 | #[bench] 68 | fn deinterlacing_8_bits(b: &mut Bencher) { 69 | let input = test::black_box(PathBuf::from( 70 | "tests/files/interlaced_rgb_8_should_be_rgb_8.png", 71 | )); 72 | let png = PngData::new(&input, &Options::default()).unwrap(); 73 | 74 | b.iter(|| png.raw.change_interlacing(Interlacing::None)); 75 | } 76 | 77 | #[bench] 78 | fn deinterlacing_4_bits(b: &mut Bencher) { 79 | let input = test::black_box(PathBuf::from( 80 | "tests/files/interlaced_palette_4_should_be_palette_4.png", 81 | )); 82 | let png = PngData::new(&input, &Options::default()).unwrap(); 83 | 84 | b.iter(|| png.raw.change_interlacing(Interlacing::None)); 85 | } 86 | 87 | #[bench] 88 | fn deinterlacing_2_bits(b: &mut Bencher) { 89 | let input = test::black_box(PathBuf::from( 90 | "tests/files/interlaced_palette_2_should_be_palette_2.png", 91 | )); 92 | let png = PngData::new(&input, &Options::default()).unwrap(); 93 | 94 | b.iter(|| png.raw.change_interlacing(Interlacing::None)); 95 | } 96 | 97 | #[bench] 98 | fn deinterlacing_1_bits(b: &mut Bencher) { 99 | let input = test::black_box(PathBuf::from( 100 | "tests/files/interlaced_palette_1_should_be_palette_1.png", 101 | )); 102 | let png = PngData::new(&input, &Options::default()).unwrap(); 103 | 104 | b.iter(|| png.raw.change_interlacing(Interlacing::None)); 105 | } 106 | -------------------------------------------------------------------------------- /benches/strategies.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate oxipng; 4 | extern crate test; 5 | 6 | use std::path::PathBuf; 7 | 8 | use oxipng::{internal_tests::*, *}; 9 | use test::Bencher; 10 | 11 | #[bench] 12 | fn filters_minsum(b: &mut Bencher) { 13 | let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); 14 | let png = PngData::new(&input, &Options::default()).unwrap(); 15 | 16 | b.iter(|| png.raw.filter_image(RowFilter::MinSum, false)); 17 | } 18 | 19 | #[bench] 20 | fn filters_entropy(b: &mut Bencher) { 21 | let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); 22 | let png = PngData::new(&input, &Options::default()).unwrap(); 23 | 24 | b.iter(|| png.raw.filter_image(RowFilter::Entropy, false)); 25 | } 26 | 27 | #[bench] 28 | fn filters_bigrams(b: &mut Bencher) { 29 | let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); 30 | let png = PngData::new(&input, &Options::default()).unwrap(); 31 | 32 | b.iter(|| png.raw.filter_image(RowFilter::Bigrams, false)); 33 | } 34 | 35 | #[bench] 36 | fn filters_bigent(b: &mut Bencher) { 37 | let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); 38 | let png = PngData::new(&input, &Options::default()).unwrap(); 39 | 40 | b.iter(|| png.raw.filter_image(RowFilter::BigEnt, false)); 41 | } 42 | 43 | #[bench] 44 | fn filters_brute(b: &mut Bencher) { 45 | let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); 46 | let png = PngData::new(&input, &Options::default()).unwrap(); 47 | 48 | b.iter(|| png.raw.filter_image(RowFilter::Brute, false)); 49 | } 50 | -------------------------------------------------------------------------------- /benches/zopfli.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate oxipng; 4 | extern crate test; 5 | 6 | use std::{num::NonZeroU8, path::PathBuf}; 7 | 8 | use oxipng::{internal_tests::*, *}; 9 | use test::Bencher; 10 | 11 | // SAFETY: trivially safe. Stopgap solution until const unwrap is stabilized. 12 | const DEFAULT_ZOPFLI_ITERATIONS: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(15) }; 13 | 14 | #[bench] 15 | fn zopfli_16_bits_strategy_0(b: &mut Bencher) { 16 | let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); 17 | let png = PngData::new(&input, &Options::default()).unwrap(); 18 | 19 | b.iter(|| { 20 | zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); 21 | }); 22 | } 23 | 24 | #[bench] 25 | fn zopfli_8_bits_strategy_0(b: &mut Bencher) { 26 | let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_rgb_8.png")); 27 | let png = PngData::new(&input, &Options::default()).unwrap(); 28 | 29 | b.iter(|| { 30 | zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); 31 | }); 32 | } 33 | 34 | #[bench] 35 | fn zopfli_4_bits_strategy_0(b: &mut Bencher) { 36 | let input = test::black_box(PathBuf::from( 37 | "tests/files/palette_4_should_be_palette_4.png", 38 | )); 39 | let png = PngData::new(&input, &Options::default()).unwrap(); 40 | 41 | b.iter(|| { 42 | zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); 43 | }); 44 | } 45 | 46 | #[bench] 47 | fn zopfli_2_bits_strategy_0(b: &mut Bencher) { 48 | let input = test::black_box(PathBuf::from( 49 | "tests/files/palette_2_should_be_palette_2.png", 50 | )); 51 | let png = PngData::new(&input, &Options::default()).unwrap(); 52 | 53 | b.iter(|| { 54 | zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); 55 | }); 56 | } 57 | 58 | #[bench] 59 | fn zopfli_1_bits_strategy_0(b: &mut Bencher) { 60 | let input = test::black_box(PathBuf::from( 61 | "tests/files/palette_1_should_be_palette_1.png", 62 | )); 63 | let png = PngData::new(&input, &Options::default()).unwrap(); 64 | 65 | b.iter(|| { 66 | zopfli_deflate(png.raw.data.as_ref(), DEFAULT_ZOPFLI_ITERATIONS).ok(); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /scripts/compare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cargo build --release 3 | sed -i.bak '/## Benchmarks/,$d' README.md 4 | rm README.md.bak 5 | 6 | CORES=$(sysctl -n hw.ncpu 2>/dev/null || grep -c ^processor /proc/cpuinfo) 7 | CPU=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || grep '^model name' /proc/cpuinfo | sed 's/model name.\+: //g' | head -n 1) 8 | OXIPNG_VERSION=$(./target/release/oxipng -V) 9 | OPTIPNG_VERSION=$(optipng -v | head -n 1) 10 | RUST_VERSION=$(rustc -V) 11 | echo -e '## Benchmarks\n' >> README.md 12 | echo "Tested $OXIPNG_VERSION (compiled on $RUST_VERSION) against $OPTIPNG_VERSION on $CPU with $CORES logical cores" >> README.md 13 | echo -e '\n```\n' >> README.md 14 | 15 | hyperfine --style basic --warmup 5 './target/release/oxipng -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' >> README.md 16 | echo -e '\n\n' >> README.md 17 | hyperfine --style basic --warmup 5 './target/release/oxipng -o4 -P ./tests/files/rgb_16_should_be_grayscale_8.png' 'optipng -o 4 -simulate ./tests/files/rgb_16_should_be_grayscale_8.png' >> README.md 18 | 19 | echo -e '\n```' >> README.md 20 | -------------------------------------------------------------------------------- /scripts/manual.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | cargo build 3 | cargo xtask mangen 4 | 5 | ./target/debug/oxipng -V > MANUAL.txt 6 | #Redirect all streams to prevent detection of the terminal width and force an internal default of 100 7 | ./target/debug/oxipng --help >> MANUAL.txt 2>/dev/null , 30 | } 31 | 32 | impl Frame { 33 | /// Construct a new Frame from the data in a fcTL chunk 34 | pub fn from_fctl_data(byte_data: &[u8]) -> PngResult { 35 | if byte_data.len() < 26 { 36 | return Err(PngError::TruncatedData); 37 | } 38 | Ok(Frame { 39 | width: read_be_u32(&byte_data[4..8]), 40 | height: read_be_u32(&byte_data[8..12]), 41 | x_offset: read_be_u32(&byte_data[12..16]), 42 | y_offset: read_be_u32(&byte_data[16..20]), 43 | delay_num: read_be_u16(&byte_data[20..22]), 44 | delay_den: read_be_u16(&byte_data[22..24]), 45 | dispose_op: byte_data[24], 46 | blend_op: byte_data[25], 47 | data: vec![], 48 | }) 49 | } 50 | 51 | /// Construct the data for a fcTL chunk using the given sequence number 52 | #[must_use] 53 | pub fn fctl_data(&self, sequence_number: u32) -> Vec { 54 | let mut byte_data = Vec::with_capacity(26); 55 | byte_data.write_all(&sequence_number.to_be_bytes()).unwrap(); 56 | byte_data.write_all(&self.width.to_be_bytes()).unwrap(); 57 | byte_data.write_all(&self.height.to_be_bytes()).unwrap(); 58 | byte_data.write_all(&self.x_offset.to_be_bytes()).unwrap(); 59 | byte_data.write_all(&self.y_offset.to_be_bytes()).unwrap(); 60 | byte_data.write_all(&self.delay_num.to_be_bytes()).unwrap(); 61 | byte_data.write_all(&self.delay_den.to_be_bytes()).unwrap(); 62 | byte_data.push(self.dispose_op); 63 | byte_data.push(self.blend_op); 64 | byte_data 65 | } 66 | 67 | /// Construct the data for a fdAT chunk using the given sequence number 68 | #[must_use] 69 | pub fn fdat_data(&self, sequence_number: u32) -> Vec { 70 | let mut byte_data = Vec::with_capacity(4 + self.data.len()); 71 | byte_data.write_all(&sequence_number.to_be_bytes()).unwrap(); 72 | byte_data.write_all(&self.data).unwrap(); 73 | byte_data 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/atomicmin.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; 2 | 3 | #[derive(Debug)] 4 | pub struct AtomicMin { 5 | val: AtomicUsize, 6 | } 7 | 8 | impl AtomicMin { 9 | #[must_use] 10 | pub fn new(init: Option) -> Self { 11 | Self { 12 | val: AtomicUsize::new(init.unwrap_or(usize::MAX)), 13 | } 14 | } 15 | 16 | pub fn get(&self) -> Option { 17 | let val = self.val.load(SeqCst); 18 | if val == usize::MAX { 19 | None 20 | } else { 21 | Some(val) 22 | } 23 | } 24 | 25 | /// Try a new value, returning true if it is the new minimum 26 | pub fn set_min(&self, new_val: usize) -> bool { 27 | new_val < self.val.fetch_min(new_val, SeqCst) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/colors.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, fmt::Display}; 2 | 3 | use rgb::{RGB16, RGBA8}; 4 | 5 | use crate::PngError; 6 | 7 | #[derive(Debug, PartialEq, Eq, Clone)] 8 | /// The color type used to represent this image 9 | pub enum ColorType { 10 | /// Grayscale, with one color channel 11 | Grayscale { 12 | /// Optional shade of gray that should be rendered as transparent 13 | transparent_shade: Option, 14 | }, 15 | /// RGB, with three color channels 16 | RGB { 17 | /// Optional color value that should be rendered as transparent 18 | transparent_color: Option, 19 | }, 20 | /// Indexed, with one byte per pixel representing a color from the palette 21 | Indexed { 22 | /// The palette containing the colors used, up to 256 entries 23 | palette: Vec, 24 | }, 25 | /// Grayscale + Alpha, with two color channels 26 | GrayscaleAlpha, 27 | /// RGBA, with four color channels 28 | RGBA, 29 | } 30 | 31 | impl Display for ColorType { 32 | #[inline] 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | match self { 35 | Self::Grayscale { .. } => write!(f, "Grayscale"), 36 | Self::RGB { .. } => write!(f, "RGB"), 37 | Self::Indexed { palette } => write!(f, "Indexed ({} colors)", palette.len()), 38 | Self::GrayscaleAlpha => write!(f, "Grayscale + Alpha"), 39 | Self::RGBA => write!(f, "RGB + Alpha"), 40 | } 41 | } 42 | } 43 | 44 | impl ColorType { 45 | /// Get the code used by the PNG specification to denote this color type 46 | #[inline] 47 | #[must_use] 48 | pub const fn png_header_code(&self) -> u8 { 49 | match self { 50 | Self::Grayscale { .. } => 0, 51 | Self::RGB { .. } => 2, 52 | Self::Indexed { .. } => 3, 53 | Self::GrayscaleAlpha => 4, 54 | Self::RGBA => 6, 55 | } 56 | } 57 | 58 | #[inline] 59 | pub(crate) const fn channels_per_pixel(&self) -> u8 { 60 | match self { 61 | Self::Grayscale { .. } | Self::Indexed { .. } => 1, 62 | Self::GrayscaleAlpha => 2, 63 | Self::RGB { .. } => 3, 64 | Self::RGBA => 4, 65 | } 66 | } 67 | 68 | #[inline] 69 | pub(crate) const fn is_rgb(&self) -> bool { 70 | matches!(self, Self::RGB { .. } | Self::RGBA) 71 | } 72 | 73 | #[inline] 74 | pub(crate) const fn is_gray(&self) -> bool { 75 | matches!(self, Self::Grayscale { .. } | Self::GrayscaleAlpha) 76 | } 77 | 78 | #[inline] 79 | pub(crate) const fn has_alpha(&self) -> bool { 80 | matches!(self, Self::GrayscaleAlpha | Self::RGBA) 81 | } 82 | 83 | #[inline] 84 | pub(crate) const fn has_trns(&self) -> bool { 85 | match self { 86 | Self::Grayscale { transparent_shade } => transparent_shade.is_some(), 87 | Self::RGB { transparent_color } => transparent_color.is_some(), 88 | _ => false, 89 | } 90 | } 91 | } 92 | 93 | #[repr(u8)] 94 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] 95 | /// The number of bits to be used per channel per pixel 96 | pub enum BitDepth { 97 | /// One bit per channel per pixel 98 | One = 1, 99 | /// Two bits per channel per pixel 100 | Two = 2, 101 | /// Four bits per channel per pixel 102 | Four = 4, 103 | /// Eight bits per channel per pixel 104 | Eight = 8, 105 | /// Sixteen bits per channel per pixel 106 | Sixteen = 16, 107 | } 108 | 109 | impl TryFrom for BitDepth { 110 | type Error = PngError; 111 | 112 | fn try_from(value: u8) -> Result { 113 | match value { 114 | 1 => Ok(Self::One), 115 | 2 => Ok(Self::Two), 116 | 4 => Ok(Self::Four), 117 | 8 => Ok(Self::Eight), 118 | 16 => Ok(Self::Sixteen), 119 | _ => Err(PngError::new("Unexpected bit depth")), 120 | } 121 | } 122 | } 123 | 124 | impl Display for BitDepth { 125 | #[inline] 126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 | Display::fmt(&(*self as u8).to_string(), f) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/deflate/deflater.rs: -------------------------------------------------------------------------------- 1 | use libdeflater::*; 2 | 3 | use crate::{PngError, PngResult}; 4 | 5 | pub fn deflate(data: &[u8], level: u8, max_size: Option) -> PngResult> { 6 | let mut compressor = Compressor::new(CompressionLvl::new(level.into()).unwrap()); 7 | let capacity = max_size.unwrap_or_else(|| compressor.zlib_compress_bound(data.len())); 8 | let mut dest = vec![0; capacity]; 9 | let len = compressor 10 | .zlib_compress(data, &mut dest) 11 | .map_err(|err| match err { 12 | CompressionError::InsufficientSpace => PngError::DeflatedDataTooLong(capacity), 13 | })?; 14 | dest.truncate(len); 15 | Ok(dest) 16 | } 17 | 18 | pub fn inflate(data: &[u8], out_size: usize) -> PngResult> { 19 | let mut decompressor = Decompressor::new(); 20 | let mut dest = vec![0; out_size]; 21 | let len = decompressor 22 | .zlib_decompress(data, &mut dest) 23 | .map_err(|err| match err { 24 | DecompressionError::BadData => PngError::InvalidData, 25 | DecompressionError::InsufficientSpace => PngError::new("inflated data too long"), 26 | })?; 27 | dest.truncate(len); 28 | Ok(dest) 29 | } 30 | 31 | #[must_use] 32 | pub fn crc32(data: &[u8]) -> u32 { 33 | let mut crc = Crc::new(); 34 | crc.update(data); 35 | crc.sum() 36 | } 37 | -------------------------------------------------------------------------------- /src/deflate/mod.rs: -------------------------------------------------------------------------------- 1 | mod deflater; 2 | #[cfg(feature = "zopfli")] 3 | use std::num::NonZeroU8; 4 | use std::{fmt, fmt::Display}; 5 | 6 | pub use deflater::{crc32, deflate, inflate}; 7 | 8 | use crate::{PngError, PngResult}; 9 | #[cfg(feature = "zopfli")] 10 | mod zopfli_oxipng; 11 | #[cfg(feature = "zopfli")] 12 | pub use zopfli_oxipng::deflate as zopfli_deflate; 13 | 14 | /// DEFLATE algorithms supported by oxipng (for use in [`Options`][crate::Options]) 15 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 16 | pub enum Deflaters { 17 | /// Use libdeflater. 18 | Libdeflater { 19 | /// Which compression level to use on the file (0-12) 20 | compression: u8, 21 | }, 22 | #[cfg(feature = "zopfli")] 23 | /// Use the better but slower Zopfli implementation 24 | Zopfli { 25 | /// The number of compression iterations to do. 15 iterations are fine 26 | /// for small files, but bigger files will need to be compressed with 27 | /// less iterations, or else they will be too slow. 28 | iterations: NonZeroU8, 29 | }, 30 | } 31 | 32 | impl Deflaters { 33 | pub(crate) fn deflate(self, data: &[u8], max_size: Option) -> PngResult> { 34 | let compressed = match self { 35 | Self::Libdeflater { compression } => deflate(data, compression, max_size)?, 36 | #[cfg(feature = "zopfli")] 37 | Self::Zopfli { iterations } => zopfli_deflate(data, iterations)?, 38 | }; 39 | if let Some(max) = max_size { 40 | if compressed.len() > max { 41 | return Err(PngError::DeflatedDataTooLong(max)); 42 | } 43 | } 44 | Ok(compressed) 45 | } 46 | } 47 | 48 | impl Display for Deflaters { 49 | #[inline] 50 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 51 | match self { 52 | Self::Libdeflater { compression } => write!(f, "zc = {compression}"), 53 | #[cfg(feature = "zopfli")] 54 | Self::Zopfli { iterations } => write!(f, "zopfli, zi = {iterations}"), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/deflate/zopfli_oxipng.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroU8; 2 | 3 | use crate::{PngError, PngResult}; 4 | 5 | pub fn deflate(data: &[u8], iterations: NonZeroU8) -> PngResult> { 6 | let mut output = Vec::with_capacity(data.len()); 7 | let options = zopfli::Options { 8 | iteration_count: iterations.into(), 9 | ..Default::default() 10 | }; 11 | // Since Rust v1.74, passing &[u8] directly into zopfli causes a regression in compressed size 12 | // for some files. Wrapping the slice in another Read implementer such as Box fixes it for now. 13 | match zopfli::compress(options, zopfli::Format::Zlib, Box::new(data), &mut output) { 14 | Ok(_) => (), 15 | Err(_) => return Err(PngError::new("Failed to compress in zopfli")), 16 | }; 17 | output.shrink_to_fit(); 18 | Ok(output) 19 | } 20 | -------------------------------------------------------------------------------- /src/display_chunks.rs: -------------------------------------------------------------------------------- 1 | /// List of chunks that affect image display and will be kept when using the `Safe` chunk strip option 2 | pub const DISPLAY_CHUNKS: [[u8; 4]; 7] = [ 3 | *b"cICP", *b"iCCP", *b"sRGB", *b"pHYs", *b"acTL", *b"fcTL", *b"fdAT", 4 | ]; 5 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use crate::colors::{BitDepth, ColorType}; 4 | 5 | #[derive(Debug, Clone)] 6 | #[non_exhaustive] 7 | pub enum PngError { 8 | DeflatedDataTooLong(usize), 9 | TimedOut, 10 | NotPNG, 11 | APNGNotSupported, 12 | APNGOutOfOrder, 13 | InvalidData, 14 | TruncatedData, 15 | ChunkMissing(&'static str), 16 | InvalidDepthForType(BitDepth, ColorType), 17 | IncorrectDataLength(usize, usize), 18 | C2PAMetadataPreventsChanges, 19 | Other(Box), 20 | } 21 | 22 | impl Error for PngError {} 23 | 24 | impl fmt::Display for PngError { 25 | #[inline] 26 | #[cold] 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | match *self { 29 | PngError::DeflatedDataTooLong(_) => f.write_str("deflated data too long"), 30 | PngError::TimedOut => f.write_str("timed out"), 31 | PngError::NotPNG => f.write_str("Invalid header detected; Not a PNG file"), 32 | PngError::InvalidData => f.write_str("Invalid data found; unable to read PNG file"), 33 | PngError::TruncatedData => { 34 | f.write_str("Missing data in the file; the file is truncated") 35 | } 36 | PngError::APNGNotSupported => f.write_str("APNG files are not (yet) supported"), 37 | PngError::APNGOutOfOrder => f.write_str("APNG chunks are out of order"), 38 | PngError::ChunkMissing(s) => write!(f, "Chunk {s} missing or empty"), 39 | PngError::InvalidDepthForType(d, ref c) => { 40 | write!(f, "Invalid bit depth {d} for color type {c}") 41 | } 42 | PngError::IncorrectDataLength(l1, l2) => write!( 43 | f, 44 | "Data length {l1} does not match the expected length {l2}" 45 | ), 46 | PngError::C2PAMetadataPreventsChanges => f.write_str( 47 | "The image contains C2PA manifest that would be invalidated by any file changes", 48 | ), 49 | PngError::Other(ref s) => f.write_str(s), 50 | } 51 | } 52 | } 53 | 54 | impl PngError { 55 | #[cold] 56 | #[must_use] 57 | pub fn new(description: &str) -> PngError { 58 | PngError::Other(description.into()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/evaluate.rs: -------------------------------------------------------------------------------- 1 | //! Check if a reduction makes file smaller, and keep best reductions. 2 | //! Works asynchronously when possible 3 | 4 | #[cfg(not(feature = "parallel"))] 5 | use std::cell::RefCell; 6 | use std::sync::{ 7 | atomic::{AtomicUsize, Ordering::*}, 8 | Arc, 9 | }; 10 | 11 | #[cfg(feature = "parallel")] 12 | use crossbeam_channel::{unbounded, Receiver, Sender}; 13 | use deflate::Deflaters; 14 | use indexmap::IndexSet; 15 | use log::trace; 16 | use rayon::prelude::*; 17 | 18 | #[cfg(not(feature = "parallel"))] 19 | use crate::rayon; 20 | use crate::{atomicmin::AtomicMin, deflate, filters::RowFilter, png::PngImage, Deadline, PngError}; 21 | 22 | pub(crate) struct Candidate { 23 | pub image: Arc, 24 | pub data: Vec, 25 | pub data_is_compressed: bool, 26 | pub estimated_output_size: usize, 27 | pub filter: RowFilter, 28 | // For determining tie-breaker 29 | nth: usize, 30 | } 31 | 32 | impl Candidate { 33 | fn cmp_key(&self) -> impl Ord { 34 | ( 35 | self.estimated_output_size, 36 | self.image.data.len(), 37 | self.filter, 38 | // Prefer the later image added (e.g. baseline, which is always added last) 39 | usize::MAX - self.nth, 40 | ) 41 | } 42 | } 43 | 44 | /// Collect image versions and pick one that compresses best 45 | pub(crate) struct Evaluator { 46 | deadline: Arc, 47 | filters: IndexSet, 48 | deflater: Deflaters, 49 | optimize_alpha: bool, 50 | final_round: bool, 51 | nth: AtomicUsize, 52 | executed: Arc, 53 | best_candidate_size: Arc, 54 | /// images are sent to the caller thread for evaluation 55 | #[cfg(feature = "parallel")] 56 | eval_channel: (Sender, Receiver), 57 | // in non-parallel mode, images are evaluated synchronously 58 | #[cfg(not(feature = "parallel"))] 59 | eval_best_candidate: RefCell>, 60 | } 61 | 62 | impl Evaluator { 63 | pub fn new( 64 | deadline: Arc, 65 | filters: IndexSet, 66 | deflater: Deflaters, 67 | optimize_alpha: bool, 68 | final_round: bool, 69 | ) -> Self { 70 | #[cfg(feature = "parallel")] 71 | let eval_channel = unbounded(); 72 | Self { 73 | deadline, 74 | filters, 75 | deflater, 76 | optimize_alpha, 77 | final_round, 78 | nth: AtomicUsize::new(0), 79 | executed: Arc::new(AtomicUsize::new(0)), 80 | best_candidate_size: Arc::new(AtomicMin::new(None)), 81 | #[cfg(feature = "parallel")] 82 | eval_channel, 83 | #[cfg(not(feature = "parallel"))] 84 | eval_best_candidate: RefCell::new(None), 85 | } 86 | } 87 | 88 | /// Wait for all evaluations to finish and return smallest reduction 89 | /// Or `None` if the queue is empty. 90 | #[cfg(feature = "parallel")] 91 | pub fn get_best_candidate(self) -> Option { 92 | let (eval_send, eval_recv) = self.eval_channel; 93 | // Disconnect the sender, breaking the loop in the thread 94 | drop(eval_send); 95 | let nth = self.nth.load(SeqCst); 96 | // Yield to ensure all evaluations are executed 97 | // This can prevent deadlocks when run within an existing rayon thread pool 98 | while self.executed.load(Relaxed) < nth { 99 | rayon::yield_local(); 100 | } 101 | eval_recv.into_iter().min_by_key(Candidate::cmp_key) 102 | } 103 | 104 | #[cfg(not(feature = "parallel"))] 105 | pub fn get_best_candidate(self) -> Option { 106 | self.eval_best_candidate.into_inner() 107 | } 108 | 109 | /// Set best size, if known in advance 110 | pub fn set_best_size(&self, size: usize) { 111 | self.best_candidate_size.set_min(size); 112 | } 113 | 114 | /// Check if the image is smaller than others 115 | pub fn try_image(&self, image: Arc) { 116 | let description = format!("{}", image.ihdr.color_type); 117 | self.try_image_with_description(image, &description); 118 | } 119 | 120 | /// Check if the image is smaller than others, with a description for verbose mode 121 | pub fn try_image_with_description(&self, image: Arc, description: &str) { 122 | let nth = self.nth.fetch_add(1, SeqCst); 123 | // These clones are only cheap refcounts 124 | let deadline = self.deadline.clone(); 125 | let filters = self.filters.clone(); 126 | let deflater = self.deflater; 127 | let optimize_alpha = self.optimize_alpha; 128 | let final_round = self.final_round; 129 | let executed = self.executed.clone(); 130 | let best_candidate_size = self.best_candidate_size.clone(); 131 | let description = description.to_string(); 132 | // sends it off asynchronously for compression, 133 | // but results will be collected via the message queue 134 | #[cfg(feature = "parallel")] 135 | let eval_send = self.eval_channel.0.clone(); 136 | rayon::spawn(move || { 137 | executed.fetch_add(1, Relaxed); 138 | let filters_iter = filters.par_iter().with_max_len(1); 139 | 140 | // Updating of best result inside the parallel loop would require locks, 141 | // which are dangerous to do in side Rayon's loop. 142 | // Instead, only update (atomic) best size in real time, 143 | // and the best result later without need for locks. 144 | filters_iter.for_each(|&filter| { 145 | if deadline.passed() { 146 | return; 147 | } 148 | let filtered = image.filter_image(filter, optimize_alpha); 149 | let idat_data = deflater.deflate(&filtered, best_candidate_size.get()); 150 | if let Ok(idat_data) = idat_data { 151 | let estimated_output_size = image.estimated_output_size(&idat_data); 152 | // For the final round we need the IDAT data, otherwise the filtered data 153 | let new = Candidate { 154 | image: image.clone(), 155 | data: if final_round { idat_data } else { filtered }, 156 | data_is_compressed: final_round, 157 | estimated_output_size, 158 | filter, 159 | nth, 160 | }; 161 | best_candidate_size.set_min(estimated_output_size); 162 | trace!( 163 | "Eval: {}-bit {:23} {:8} {} bytes", 164 | image.ihdr.bit_depth, 165 | description, 166 | filter, 167 | estimated_output_size 168 | ); 169 | 170 | #[cfg(feature = "parallel")] 171 | { 172 | eval_send.send(new).expect("send"); 173 | } 174 | 175 | #[cfg(not(feature = "parallel"))] 176 | { 177 | match &mut *self.eval_best_candidate.borrow_mut() { 178 | Some(prev) if prev.cmp_key() < new.cmp_key() => {} 179 | best => *best = Some(new), 180 | } 181 | } 182 | } else if let Err(PngError::DeflatedDataTooLong(size)) = idat_data { 183 | trace!( 184 | "Eval: {}-bit {:23} {:8} >{} bytes", 185 | image.ihdr.bit_depth, 186 | description, 187 | filter, 188 | size 189 | ); 190 | } 191 | }); 192 | }); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | path::{Path, PathBuf}, 4 | time::Duration, 5 | }; 6 | 7 | use indexmap::{indexset, IndexSet}; 8 | use log::warn; 9 | 10 | use crate::{deflate::Deflaters, filters::RowFilter, headers::StripChunks, interlace::Interlacing}; 11 | 12 | /// Write destination for [`optimize`][crate::optimize]. 13 | /// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O. 14 | #[derive(Clone, Debug)] 15 | pub enum OutFile { 16 | /// Don't actually write any output, just calculate the best results. 17 | None, 18 | /// Write output to a file. 19 | /// 20 | /// * `path`: Path to write the output file. `None` means same as input. 21 | /// * `preserve_attrs`: Ensure the output file has the same permissions & timestamps as the input file. 22 | Path { 23 | path: Option, 24 | preserve_attrs: bool, 25 | }, 26 | /// Write to standard output. 27 | StdOut, 28 | } 29 | 30 | impl OutFile { 31 | /// Construct a new `OutFile` with the given path. 32 | /// 33 | /// This is a convenience method for `OutFile::Path { path: Some(path), preserve_attrs: false }`. 34 | #[must_use] 35 | pub fn from_path(path: PathBuf) -> Self { 36 | OutFile::Path { 37 | path: Some(path), 38 | preserve_attrs: false, 39 | } 40 | } 41 | 42 | #[must_use] 43 | pub fn path(&self) -> Option<&Path> { 44 | match *self { 45 | Self::Path { 46 | path: Some(ref p), .. 47 | } => Some(p.as_path()), 48 | _ => None, 49 | } 50 | } 51 | } 52 | 53 | /// Where to read images from in [`optimize`][crate::optimize]. 54 | /// You can use [`optimize_from_memory`](crate::optimize_from_memory) to avoid external I/O. 55 | #[derive(Clone, Debug)] 56 | pub enum InFile { 57 | Path(PathBuf), 58 | StdIn, 59 | } 60 | 61 | impl InFile { 62 | #[must_use] 63 | pub fn path(&self) -> Option<&Path> { 64 | match *self { 65 | Self::Path(ref p) => Some(p.as_path()), 66 | Self::StdIn => None, 67 | } 68 | } 69 | } 70 | 71 | impl fmt::Display for InFile { 72 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 73 | match *self { 74 | Self::Path(ref p) => write!(f, "{}", p.display()), 75 | Self::StdIn => f.write_str("stdin"), 76 | } 77 | } 78 | } 79 | 80 | impl> From for InFile { 81 | fn from(s: T) -> Self { 82 | Self::Path(s.into()) 83 | } 84 | } 85 | 86 | #[derive(Clone, Debug)] 87 | /// Options controlling the output of the `optimize` function 88 | pub struct Options { 89 | /// Attempt to fix errors when decoding the input file rather than returning an `Err`. 90 | /// 91 | /// Default: `false` 92 | pub fix_errors: bool, 93 | /// Write to output even if there was no improvement in compression. 94 | /// 95 | /// Default: `false` 96 | pub force: bool, 97 | /// Which `RowFilters` to try on the file 98 | /// 99 | /// Default: `None,Sub,Entropy,Bigrams` 100 | pub filter: IndexSet, 101 | /// Whether to change the interlacing type of the file. 102 | /// 103 | /// These are the interlacing types avaliable: 104 | /// - `None` will not change the current interlacing type. 105 | /// - `Some(x)` will change the file to interlacing mode `x`. 106 | /// See [`Interlacing`] for the possible interlacing types. 107 | /// 108 | /// Default: `Some(Interlacing::None)` 109 | pub interlace: Option, 110 | /// Whether to allow transparent pixels to be altered to improve compression. 111 | /// 112 | /// Default: `false` 113 | pub optimize_alpha: bool, 114 | /// Whether to attempt bit depth reduction 115 | /// 116 | /// Default: `true` 117 | pub bit_depth_reduction: bool, 118 | /// Whether to attempt color type reduction 119 | /// 120 | /// Default: `true` 121 | pub color_type_reduction: bool, 122 | /// Whether to attempt palette reduction 123 | /// 124 | /// Default: `true` 125 | pub palette_reduction: bool, 126 | /// Whether to attempt grayscale reduction 127 | /// 128 | /// Default: `true` 129 | pub grayscale_reduction: bool, 130 | /// Whether to perform recoding of IDAT and other compressed chunks 131 | /// 132 | /// If any type of reduction is performed, IDAT recoding will be performed 133 | /// regardless of this setting 134 | /// 135 | /// Default: `true` 136 | pub idat_recoding: bool, 137 | /// Whether to forcibly reduce 16-bit to 8-bit by scaling 138 | /// 139 | /// Default: `false` 140 | pub scale_16: bool, 141 | /// Which chunks to strip from the PNG file, if any 142 | /// 143 | /// Default: `None` 144 | pub strip: StripChunks, 145 | /// Which DEFLATE (zlib) algorithm to use 146 | #[cfg_attr(feature = "zopfli", doc = "(e.g. Zopfli)")] 147 | /// 148 | /// Default: `Libdeflater` 149 | pub deflate: Deflaters, 150 | /// Whether to use fast evaluation to pick the best filter 151 | /// 152 | /// Default: `true` 153 | pub fast_evaluation: bool, 154 | /// Maximum amount of time to spend on optimizations. 155 | /// Further potential optimizations are skipped if the timeout is exceeded. 156 | /// 157 | /// Default: `None` 158 | pub timeout: Option, 159 | } 160 | 161 | impl Options { 162 | #[must_use] 163 | pub fn from_preset(level: u8) -> Self { 164 | let opts = Self::default(); 165 | match level { 166 | 0 => opts.apply_preset_0(), 167 | 1 => opts.apply_preset_1(), 168 | 2 => opts.apply_preset_2(), 169 | 3 => opts.apply_preset_3(), 170 | 4 => opts.apply_preset_4(), 171 | 5 => opts.apply_preset_5(), 172 | 6 => opts.apply_preset_6(), 173 | _ => { 174 | warn!("Level 7 and above don't exist yet and are identical to level 6"); 175 | opts.apply_preset_6() 176 | } 177 | } 178 | } 179 | 180 | #[must_use] 181 | pub fn max_compression() -> Self { 182 | Self::from_preset(6) 183 | } 184 | 185 | // The following methods make assumptions that they are operating 186 | // on an `Options` struct generated by the `default` method. 187 | fn apply_preset_0(mut self) -> Self { 188 | self.filter.clear(); 189 | if let Deflaters::Libdeflater { compression } = &mut self.deflate { 190 | *compression = 5; 191 | } 192 | self 193 | } 194 | 195 | fn apply_preset_1(mut self) -> Self { 196 | self.filter.clear(); 197 | if let Deflaters::Libdeflater { compression } = &mut self.deflate { 198 | *compression = 10; 199 | } 200 | self 201 | } 202 | 203 | fn apply_preset_2(self) -> Self { 204 | self 205 | } 206 | 207 | fn apply_preset_3(mut self) -> Self { 208 | self.fast_evaluation = false; 209 | self.filter = indexset! { 210 | RowFilter::None, 211 | RowFilter::Bigrams, 212 | RowFilter::BigEnt, 213 | RowFilter::Brute 214 | }; 215 | self 216 | } 217 | 218 | fn apply_preset_4(mut self) -> Self { 219 | if let Deflaters::Libdeflater { compression } = &mut self.deflate { 220 | *compression = 12; 221 | } 222 | self.apply_preset_3() 223 | } 224 | 225 | fn apply_preset_5(mut self) -> Self { 226 | self.fast_evaluation = false; 227 | self.filter.insert(RowFilter::Up); 228 | self.filter.insert(RowFilter::MinSum); 229 | self.filter.insert(RowFilter::BigEnt); 230 | self.filter.insert(RowFilter::Brute); 231 | if let Deflaters::Libdeflater { compression } = &mut self.deflate { 232 | *compression = 12; 233 | } 234 | self 235 | } 236 | 237 | fn apply_preset_6(mut self) -> Self { 238 | self.filter.insert(RowFilter::Average); 239 | self.filter.insert(RowFilter::Paeth); 240 | self.apply_preset_5() 241 | } 242 | } 243 | 244 | impl Default for Options { 245 | fn default() -> Self { 246 | // Default settings based on -o 2 from the CLI interface 247 | Self { 248 | fix_errors: false, 249 | force: false, 250 | filter: indexset! {RowFilter::None, RowFilter::Sub, RowFilter::Entropy, RowFilter::Bigrams}, 251 | interlace: Some(Interlacing::None), 252 | optimize_alpha: false, 253 | bit_depth_reduction: true, 254 | color_type_reduction: true, 255 | palette_reduction: true, 256 | grayscale_reduction: true, 257 | idat_recoding: true, 258 | scale_16: false, 259 | strip: StripChunks::None, 260 | deflate: Deflaters::Libdeflater { compression: 11 }, 261 | fast_evaluation: true, 262 | timeout: None, 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/png/scan_lines.rs: -------------------------------------------------------------------------------- 1 | use crate::{interlace::Interlacing, png::PngImage}; 2 | 3 | /// An iterator over the scan lines of a PNG image 4 | #[derive(Debug, Clone)] 5 | pub struct ScanLines<'a> { 6 | iter: ScanLineRanges, 7 | /// A reference to the PNG image being iterated upon 8 | raw_data: &'a [u8], 9 | /// Whether the raw data contains filter bytes 10 | has_filter: bool, 11 | } 12 | 13 | impl<'a> ScanLines<'a> { 14 | #[must_use] 15 | pub fn new(png: &'a PngImage, has_filter: bool) -> Self { 16 | Self { 17 | iter: ScanLineRanges::new(png, has_filter), 18 | raw_data: &png.data, 19 | has_filter, 20 | } 21 | } 22 | } 23 | 24 | impl<'a> Iterator for ScanLines<'a> { 25 | type Item = ScanLine<'a>; 26 | 27 | #[inline] 28 | fn next(&mut self) -> Option { 29 | let (len, pass, num_pixels) = self.iter.next()?; 30 | debug_assert!(self.raw_data.len() >= len); 31 | debug_assert!(!self.has_filter || len > 1); 32 | // The data length should always be correct here but this check assures 33 | // the compiler that it doesn't need to account for a potential panic 34 | if self.raw_data.len() < len { 35 | return None; 36 | } 37 | let (data, rest) = self.raw_data.split_at(len); 38 | self.raw_data = rest; 39 | let (&filter, data) = if self.has_filter { 40 | data.split_first().unwrap() 41 | } else { 42 | (&0, data) 43 | }; 44 | Some(ScanLine { 45 | filter, 46 | data, 47 | pass, 48 | num_pixels, 49 | }) 50 | } 51 | } 52 | 53 | #[derive(Debug, Clone)] 54 | /// An iterator over the scan line locations of a PNG image 55 | struct ScanLineRanges { 56 | /// Current pass number, and 0-indexed row within the pass 57 | pass: Option<(u8, u32)>, 58 | bits_per_pixel: usize, 59 | width: u32, 60 | height: u32, 61 | left: usize, 62 | has_filter: bool, 63 | } 64 | 65 | impl ScanLineRanges { 66 | pub fn new(png: &PngImage, has_filter: bool) -> Self { 67 | Self { 68 | bits_per_pixel: png.ihdr.bpp(), 69 | width: png.ihdr.width, 70 | height: png.ihdr.height, 71 | left: png.data.len(), 72 | pass: if png.ihdr.interlaced == Interlacing::Adam7 { 73 | Some((1, 0)) 74 | } else { 75 | None 76 | }, 77 | has_filter, 78 | } 79 | } 80 | } 81 | 82 | impl Iterator for ScanLineRanges { 83 | type Item = (usize, Option, usize); 84 | 85 | fn next(&mut self) -> Option { 86 | if self.left == 0 { 87 | return None; 88 | } 89 | let (pixels_per_line, current_pass) = if let Some(ref mut pass) = self.pass { 90 | // Scanlines for interlaced PNG files 91 | // Handle edge cases for images smaller than 5 pixels in either direction 92 | // No extra case needed for skipping pass 7 as this is already handled by the 93 | // self.left == 0 check above 94 | if self.width < 5 && pass.0 == 2 { 95 | pass.0 = 3; 96 | pass.1 = 4; 97 | } 98 | // Intentionally keep these separate so that they can be applied one after another 99 | if self.height < 5 && pass.0 == 3 { 100 | pass.0 = 4; 101 | pass.1 = 0; 102 | } 103 | if self.width < 3 && pass.0 == 4 { 104 | pass.0 = 5; 105 | pass.1 = 2; 106 | } 107 | if self.height < 3 && pass.0 == 5 { 108 | pass.0 = 6; 109 | pass.1 = 0; 110 | } 111 | if self.width == 1 && pass.0 == 6 { 112 | pass.0 = 7; 113 | pass.1 = 1; 114 | } 115 | let (pixels_factor, y_steps) = match pass.0 { 116 | 1 | 2 => (8, 8), 117 | 3 => (4, 8), 118 | 4 => (4, 4), 119 | 5 => (2, 4), 120 | 6 => (2, 2), 121 | 7 => (1, 2), 122 | _ => unreachable!(), 123 | }; 124 | let mut pixels_per_line = self.width / pixels_factor; 125 | // Determine whether to add pixels if there is a final, incomplete 8x8 block 126 | let gap = self.width % pixels_factor; 127 | match pass.0 { 128 | 1 | 3 | 5 if gap > 0 => { 129 | pixels_per_line += 1; 130 | } 131 | 2 if gap >= 5 => { 132 | pixels_per_line += 1; 133 | } 134 | 4 if gap >= 3 => { 135 | pixels_per_line += 1; 136 | } 137 | 6 if gap >= 2 => { 138 | pixels_per_line += 1; 139 | } 140 | _ => (), 141 | }; 142 | let current_pass = Some(pass.0); 143 | if pass.1 + y_steps >= self.height { 144 | pass.0 += 1; 145 | pass.1 = match pass.0 { 146 | 3 => 4, 147 | 5 => 2, 148 | 7 => 1, 149 | _ => 0, 150 | }; 151 | } else { 152 | pass.1 += y_steps; 153 | } 154 | (pixels_per_line, current_pass) 155 | } else { 156 | // Standard, non-interlaced PNG scanlines 157 | (self.width, None) 158 | }; 159 | let bits_per_line = pixels_per_line as usize * self.bits_per_pixel; 160 | let mut len = bits_per_line.div_ceil(8); 161 | if self.has_filter { 162 | len += 1; 163 | } 164 | self.left = self.left.checked_sub(len)?; 165 | Some((len, current_pass, pixels_per_line as usize)) 166 | } 167 | } 168 | 169 | #[derive(Debug, Clone)] 170 | /// A scan line in a PNG image 171 | pub struct ScanLine<'a> { 172 | /// The filter type used to encode the current scan line (0-4) 173 | pub filter: u8, 174 | /// The byte data for the current scan line, encoded with the filter specified in the `filter` field 175 | pub data: &'a [u8], 176 | /// The current pass if the image is interlaced 177 | pub pass: Option, 178 | /// The number of pixels in the current scan line 179 | pub num_pixels: usize, 180 | } 181 | -------------------------------------------------------------------------------- /src/rayon.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | pub mod prelude { 4 | pub use super::*; 5 | } 6 | 7 | pub trait ParallelIterator: Iterator + Sized { 8 | fn with_max_len(self, _l: usize) -> Self { 9 | self 10 | } 11 | fn reduce_with(mut self, op: OP) -> Option 12 | where 13 | OP: Fn(Self::Item, Self::Item) -> Self::Item + Sync, 14 | { 15 | self.next().map(|a| self.fold(a, op)) 16 | } 17 | } 18 | 19 | pub trait IntoParallelIterator { 20 | type Iter: Iterator; 21 | type Item: Send; 22 | fn into_par_iter(self) -> Self::Iter; 23 | } 24 | 25 | pub trait IntoParallelRefIterator<'data> { 26 | type Iter: Iterator; 27 | type Item: Send + 'data; 28 | fn par_iter(&'data self) -> Self::Iter; 29 | } 30 | 31 | pub trait IntoParallelRefMutIterator<'data> { 32 | type Iter: ParallelIterator; 33 | type Item: Send + 'data; 34 | fn par_iter_mut(&'data mut self) -> Self::Iter; 35 | } 36 | 37 | impl IntoParallelIterator for I 38 | where 39 | I::Item: Send, 40 | { 41 | type Iter = I::IntoIter; 42 | type Item = I::Item; 43 | 44 | fn into_par_iter(self) -> Self::Iter { 45 | self.into_iter() 46 | } 47 | } 48 | 49 | impl<'data, I: 'data + ?Sized> IntoParallelRefIterator<'data> for I 50 | where 51 | &'data I: IntoParallelIterator, 52 | { 53 | type Iter = <&'data I as IntoParallelIterator>::Iter; 54 | type Item = <&'data I as IntoParallelIterator>::Item; 55 | 56 | fn par_iter(&'data self) -> Self::Iter { 57 | self.into_par_iter() 58 | } 59 | } 60 | 61 | impl<'data, I: 'data + ?Sized> IntoParallelRefMutIterator<'data> for I 62 | where 63 | &'data mut I: IntoParallelIterator, 64 | { 65 | type Iter = <&'data mut I as IntoParallelIterator>::Iter; 66 | type Item = <&'data mut I as IntoParallelIterator>::Item; 67 | 68 | fn par_iter_mut(&'data mut self) -> Self::Iter { 69 | self.into_par_iter() 70 | } 71 | } 72 | 73 | impl ParallelIterator for I {} 74 | 75 | pub fn join(a: impl FnOnce() -> A, b: impl FnOnce() -> B) -> (A, B) { 76 | (a(), b()) 77 | } 78 | 79 | pub fn spawn(a: impl FnOnce() -> A) -> A { 80 | a() 81 | } 82 | -------------------------------------------------------------------------------- /src/reduction/alpha.rs: -------------------------------------------------------------------------------- 1 | use rgb::RGB16; 2 | 3 | use crate::{ 4 | colors::{BitDepth, ColorType}, 5 | headers::IhdrData, 6 | png::PngImage, 7 | }; 8 | 9 | /// Clean the alpha channel by setting the color of all fully transparent pixels to black 10 | #[must_use] 11 | pub fn cleaned_alpha_channel(png: &PngImage) -> Option { 12 | if !png.ihdr.color_type.has_alpha() { 13 | return None; 14 | } 15 | let byte_depth = png.bytes_per_channel(); 16 | let bpp = png.channels_per_pixel() * byte_depth; 17 | let colored_bytes = bpp - byte_depth; 18 | 19 | let mut reduced = Vec::with_capacity(png.data.len()); 20 | for pixel in png.data.chunks_exact(bpp) { 21 | if pixel.iter().skip(colored_bytes).all(|b| *b == 0) { 22 | reduced.resize(reduced.len() + bpp, 0); 23 | } else { 24 | reduced.extend_from_slice(pixel); 25 | } 26 | } 27 | 28 | Some(PngImage { 29 | data: reduced, 30 | ihdr: png.ihdr.clone(), 31 | }) 32 | } 33 | 34 | #[must_use] 35 | pub fn reduced_alpha_channel(png: &PngImage, optimize_alpha: bool) -> Option { 36 | if !png.ihdr.color_type.has_alpha() { 37 | return None; 38 | } 39 | let byte_depth = png.bytes_per_channel(); 40 | let bpp = png.channels_per_pixel() * byte_depth; 41 | let colored_bytes = bpp - byte_depth; 42 | 43 | // If alpha optimisation is enabled, see if the image contains only fully opaque and fully transparent pixels. 44 | // In case this occurs, we want to try and find an unused color we can use for the tRNS chunk. 45 | // Rather than an exhaustive search, we will just keep track of 256 shades of gray, which should cover many cases. 46 | let mut has_transparency = false; 47 | let mut used_colors = vec![false; 256]; 48 | 49 | for pixel in png.data.chunks_exact(bpp) { 50 | if optimize_alpha && pixel.iter().skip(colored_bytes).all(|b| *b == 0) { 51 | // Fully transparent, we may be able to reduce with tRNS 52 | has_transparency = true; 53 | } else if pixel.iter().skip(colored_bytes).any(|b| *b != 255) { 54 | // Partially transparent, the image is not reducible 55 | return None; 56 | } else if optimize_alpha && pixel.iter().take(colored_bytes).all(|b| *b == pixel[0]) { 57 | // Opaque shade of gray, we can't use this color for tRNS 58 | used_colors[pixel[0] as usize] = true; 59 | } 60 | } 61 | 62 | let transparency_pixel = if has_transparency { 63 | // For grayscale, start by checking 4 specific values in the hope that we may reduce depth 64 | let unused = match png.ihdr.color_type { 65 | ColorType::GrayscaleAlpha => [0x00, 0xFF, 0x55, 0xAA] 66 | .into_iter() 67 | .find(|&v| !used_colors[v as usize]), 68 | _ => None, 69 | } 70 | .or_else(|| used_colors.iter().position(|&u| !u).map(|v| v as u8)); 71 | // If no unused color was found we will have to fail here 72 | Some(unused?) 73 | } else { 74 | None 75 | }; 76 | 77 | let mut raw_data = Vec::with_capacity(png.data.len()); 78 | for pixel in png.data.chunks_exact(bpp) { 79 | match transparency_pixel { 80 | Some(trns) if pixel.iter().skip(colored_bytes).all(|b| *b == 0) => { 81 | raw_data.resize(raw_data.len() + colored_bytes, trns); 82 | } 83 | _ => raw_data.extend_from_slice(&pixel[0..colored_bytes]), 84 | }; 85 | } 86 | 87 | // Construct the color type with appropriate transparency data 88 | let transparent = transparency_pixel.map(|trns| match png.ihdr.bit_depth { 89 | BitDepth::Sixteen => (u16::from(trns) << 8) | u16::from(trns), 90 | _ => u16::from(trns), 91 | }); 92 | let target_color_type = match png.ihdr.color_type { 93 | ColorType::GrayscaleAlpha => ColorType::Grayscale { 94 | transparent_shade: transparent, 95 | }, 96 | _ => ColorType::RGB { 97 | transparent_color: transparent.map(|t| RGB16::new(t, t, t)), 98 | }, 99 | }; 100 | 101 | Some(PngImage { 102 | data: raw_data, 103 | ihdr: IhdrData { 104 | color_type: target_color_type, 105 | ..png.ihdr 106 | }, 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /src/reduction/bit_depth.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | colors::{BitDepth, ColorType}, 3 | headers::IhdrData, 4 | png::PngImage, 5 | }; 6 | 7 | /// Attempt to reduce a 16-bit image to 8-bit, returning the reduced image if successful 8 | #[must_use] 9 | pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option { 10 | if png.ihdr.bit_depth != BitDepth::Sixteen { 11 | return None; 12 | } 13 | 14 | if force_scale { 15 | return scaled_bit_depth_16_to_8(png); 16 | } 17 | 18 | // Reduce from 16 to 8 bits per channel per pixel 19 | if png.data.chunks_exact(2).any(|pair| pair[0] != pair[1]) { 20 | // Can't reduce 21 | return None; 22 | } 23 | 24 | Some(PngImage { 25 | data: png.data.chunks_exact(2).map(|pair| pair[0]).collect(), 26 | ihdr: IhdrData { 27 | color_type: png.ihdr.color_type.clone(), 28 | bit_depth: BitDepth::Eight, 29 | ..png.ihdr 30 | }, 31 | }) 32 | } 33 | 34 | /// Forcibly reduce a 16-bit image to 8-bit by scaling, returning the reduced image if successful 35 | #[must_use] 36 | pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option { 37 | if png.ihdr.bit_depth != BitDepth::Sixteen { 38 | return None; 39 | } 40 | 41 | // Reduce from 16 to 8 bits per channel per pixel by scaling when necessary 42 | let data = png 43 | .data 44 | .chunks_exact(2) 45 | .map(|pair| { 46 | if pair[0] == pair[1] { 47 | return pair[0]; 48 | } 49 | // See: http://www.libpng.org/pub/png/spec/1.2/PNG-Decoders.html#D.Sample-depth-rescaling 50 | // This allows values such as 0x00FF to be rounded to 0x01 rather than truncated to 0x00 51 | let val = f32::from(u16::from_be_bytes([pair[0], pair[1]])); 52 | (val * (255.0 / 65535.0)).round() as u8 53 | }) 54 | .collect(); 55 | 56 | Some(PngImage { 57 | data, 58 | ihdr: IhdrData { 59 | color_type: png.ihdr.color_type.clone(), 60 | bit_depth: BitDepth::Eight, 61 | ..png.ihdr 62 | }, 63 | }) 64 | } 65 | 66 | /// Attempt to reduce an 8-bit image to a lower bit depth, returning the reduced image if successful 67 | #[must_use] 68 | pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option { 69 | if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() != 1 { 70 | return None; 71 | } 72 | 73 | let mut minimum_bits = 1; 74 | 75 | if let ColorType::Indexed { palette } = &png.ihdr.color_type { 76 | // We can easily determine minimum depth by the palette size 77 | minimum_bits = match palette.len() { 78 | 0..=2 => 1, 79 | 3..=4 => 2, 80 | 5..=16 => 4, 81 | _ => return None, 82 | }; 83 | } else { 84 | // Finding minimum depth for grayscale is much more complicated 85 | let mut mask = 1; 86 | let mut divisions = 1..8; 87 | for &b in &png.data { 88 | if b == 0 || b == 255 { 89 | continue; 90 | } 91 | 'try_depth: loop { 92 | // Align the first pixel division with the mask 93 | let mut byte = b.rotate_left(minimum_bits as u32); 94 | // Each potential division of this pixel must be identical to successfully reduce 95 | let compare = byte & mask; 96 | for _ in divisions.clone() { 97 | // Align the next division with the mask 98 | byte = byte.rotate_left(minimum_bits as u32); 99 | if byte & mask != compare { 100 | // This depth is not possible, try the next one up 101 | minimum_bits <<= 1; 102 | if minimum_bits == 8 { 103 | return None; 104 | } 105 | mask = (1 << minimum_bits) - 1; 106 | divisions = 1..(8 / minimum_bits); 107 | continue 'try_depth; 108 | } 109 | } 110 | break; 111 | } 112 | } 113 | } 114 | 115 | let mut reduced = Vec::with_capacity(png.data.len()); 116 | let mask = (1 << minimum_bits) - 1; 117 | for line in png.scan_lines(false) { 118 | // Loop over the data in chunks that will produce 1 byte of output 119 | for chunk in line.data.chunks(8 / minimum_bits) { 120 | let mut new_byte = 0; 121 | let mut shift = 8; 122 | for byte in chunk { 123 | shift -= minimum_bits; 124 | // Take the low bits of the pixel and shift them into the output byte 125 | new_byte |= (byte & mask) << shift; 126 | } 127 | reduced.push(new_byte); 128 | } 129 | } 130 | 131 | // If the image is grayscale we also need to reduce the transparency pixel 132 | let color_type = if let ColorType::Grayscale { 133 | transparent_shade: Some(trans), 134 | } = png.ihdr.color_type 135 | { 136 | let reduced_trans = (trans & 0xFF) >> (8 - minimum_bits); 137 | // Verify the reduction is valid by restoring back to original bit depth 138 | let mut check = reduced_trans; 139 | let mut bits = minimum_bits; 140 | while bits < 8 { 141 | check = (check << bits) | check; 142 | bits <<= 1; 143 | } 144 | // If the transparency doesn't fit the new bit depth it is therefore unused - set it to None 145 | ColorType::Grayscale { 146 | transparent_shade: if trans == check { 147 | Some(reduced_trans) 148 | } else { 149 | None 150 | }, 151 | } 152 | } else { 153 | png.ihdr.color_type.clone() 154 | }; 155 | 156 | Some(PngImage { 157 | data: reduced, 158 | ihdr: IhdrData { 159 | color_type, 160 | bit_depth: (minimum_bits as u8).try_into().unwrap(), 161 | ..png.ihdr 162 | }, 163 | }) 164 | } 165 | 166 | /// Expand a 1/2/4-bit image to 8-bit, returning the expanded image if successful 167 | #[must_use] 168 | pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option { 169 | let bit_depth = png.ihdr.bit_depth as u32; 170 | if bit_depth >= 8 { 171 | return None; 172 | } 173 | // Calculate the current number of pixels per byte 174 | let ppb = 8 / bit_depth; 175 | let is_gray = matches!(png.ihdr.color_type, ColorType::Grayscale { .. }); 176 | 177 | let mut reduced = Vec::with_capacity((png.ihdr.width * png.ihdr.height) as usize); 178 | let mut length = 0; 179 | let mask = (1 << bit_depth) - 1; 180 | for line in png.scan_lines(false) { 181 | for &(mut byte) in line.data { 182 | // Loop over each pixel in the byte 183 | for _ in 0..ppb { 184 | // Align the current pixel with the mask 185 | byte = byte.rotate_left(bit_depth); 186 | let mut val = byte & mask; 187 | if is_gray { 188 | // Expand gray by repeating the bits 189 | let mut bits = bit_depth; 190 | while bits < 8 { 191 | val = (val << bits) | val; 192 | bits <<= 1; 193 | } 194 | } 195 | reduced.push(val); 196 | } 197 | } 198 | // Trim any overflow 199 | length += line.num_pixels; 200 | reduced.truncate(length); 201 | } 202 | 203 | // If the image is grayscale we also need to expand the transparency pixel 204 | let color_type = if let ColorType::Grayscale { 205 | transparent_shade: Some(mut trans), 206 | } = png.ihdr.color_type 207 | { 208 | let mut bits = bit_depth; 209 | while bits < 8 { 210 | trans = (trans << bits) | trans; 211 | bits <<= 1; 212 | } 213 | ColorType::Grayscale { 214 | transparent_shade: Some(trans), 215 | } 216 | } else { 217 | png.ihdr.color_type.clone() 218 | }; 219 | 220 | Some(PngImage { 221 | data: reduced, 222 | ihdr: IhdrData { 223 | color_type, 224 | bit_depth: BitDepth::Eight, 225 | ..png.ihdr 226 | }, 227 | }) 228 | } 229 | -------------------------------------------------------------------------------- /src/reduction/color.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{BuildHasherDefault, Hash}; 2 | 3 | use indexmap::IndexSet; 4 | use rgb::{alt::Gray, ComponentMap, ComponentSlice, FromSlice, RGB, RGBA}; 5 | use rustc_hash::FxHasher; 6 | 7 | use crate::{ 8 | colors::{BitDepth, ColorType}, 9 | headers::IhdrData, 10 | png::PngImage, 11 | }; 12 | 13 | type FxIndexSet = IndexSet>; 14 | 15 | /// Maximum size difference between indexed and channels to consider a candidate for evaluation 16 | pub const INDEXED_MAX_DIFF: usize = 20000; 17 | 18 | fn build_palette( 19 | iter: impl IntoIterator, 20 | reduced: &mut Vec, 21 | ) -> Option> 22 | where 23 | T: Eq + Hash, 24 | { 25 | let mut palette = FxIndexSet::default(); 26 | palette.reserve(257); 27 | for pixel in iter { 28 | let (idx, _) = palette.insert_full(pixel); 29 | if idx == 256 { 30 | return None; 31 | } 32 | reduced.push(idx as u8); 33 | } 34 | Some(palette) 35 | } 36 | 37 | #[must_use] 38 | pub fn reduced_to_indexed(png: &PngImage, allow_grayscale: bool) -> Option { 39 | if png.ihdr.bit_depth != BitDepth::Eight { 40 | return None; 41 | } 42 | if matches!(png.ihdr.color_type, ColorType::Indexed { .. }) { 43 | return None; 44 | } 45 | if !allow_grayscale && png.ihdr.color_type.is_gray() { 46 | return None; 47 | } 48 | 49 | let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel()); 50 | let palette: Vec<_> = match png.ihdr.color_type { 51 | ColorType::Grayscale { transparent_shade } => { 52 | let pmap = build_palette(png.data.as_gray().iter().copied(), &mut raw_data)?; 53 | // Convert the Gray16 transparency to Gray8 54 | let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8)); 55 | pmap.into_iter() 56 | .map(|px| { 57 | RGB::from(px).with_alpha(if Some(px) != transparency_pixel { 58 | 255 59 | } else { 60 | 0 61 | }) 62 | }) 63 | .collect() 64 | } 65 | ColorType::RGB { transparent_color } => { 66 | let pmap = build_palette(png.data.as_rgb().iter().copied(), &mut raw_data)?; 67 | // Convert the RGB16 transparency to RGB8 68 | let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8)); 69 | pmap.into_iter() 70 | .map(|px| { 71 | px.with_alpha(if Some(px) != transparency_pixel { 72 | 255 73 | } else { 74 | 0 75 | }) 76 | }) 77 | .collect() 78 | } 79 | ColorType::GrayscaleAlpha => { 80 | let pmap = build_palette(png.data.as_gray_alpha().iter().copied(), &mut raw_data)?; 81 | pmap.into_iter().map(RGBA::from).collect() 82 | } 83 | ColorType::RGBA => { 84 | let pmap = build_palette(png.data.as_rgba().iter().copied(), &mut raw_data)?; 85 | pmap.into_iter().collect() 86 | } 87 | _ => return None, 88 | }; 89 | 90 | Some(PngImage { 91 | data: raw_data, 92 | ihdr: IhdrData { 93 | color_type: ColorType::Indexed { palette }, 94 | ..png.ihdr 95 | }, 96 | }) 97 | } 98 | 99 | #[must_use] 100 | pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option { 101 | if !png.ihdr.color_type.is_rgb() { 102 | return None; 103 | } 104 | 105 | let mut reduced = Vec::with_capacity(png.data.len()); 106 | let byte_depth = png.bytes_per_channel(); 107 | let bpp = png.channels_per_pixel() * byte_depth; 108 | let last_color = 2 * byte_depth; 109 | for pixel in png.data.chunks_exact(bpp) { 110 | if byte_depth == 1 { 111 | if pixel[0] != pixel[1] || pixel[1] != pixel[2] { 112 | return None; 113 | } 114 | } else if pixel[0..2] != pixel[2..4] || pixel[2..4] != pixel[4..6] { 115 | return None; 116 | } 117 | reduced.extend_from_slice(&pixel[last_color..]); 118 | } 119 | 120 | let color_type = match png.ihdr.color_type { 121 | ColorType::RGB { transparent_color } => ColorType::Grayscale { 122 | // Copy the transparent component if it is also gray 123 | transparent_shade: transparent_color 124 | .filter(|t| t.r == t.g && t.g == t.b) 125 | .map(|t| t.r), 126 | }, 127 | _ => ColorType::GrayscaleAlpha, 128 | }; 129 | 130 | Some(PngImage { 131 | data: reduced, 132 | ihdr: IhdrData { 133 | color_type, 134 | ..png.ihdr 135 | }, 136 | }) 137 | } 138 | 139 | /// Attempt to convert indexed to a different color type, returning the resulting image if successful 140 | #[must_use] 141 | pub fn indexed_to_channels( 142 | png: &PngImage, 143 | allow_grayscale: bool, 144 | optimize_alpha: bool, 145 | ) -> Option { 146 | if png.ihdr.bit_depth != BitDepth::Eight { 147 | return None; 148 | } 149 | let mut palette = match &png.ihdr.color_type { 150 | ColorType::Indexed { palette } => palette.clone(), 151 | _ => return None, 152 | }; 153 | 154 | // Ensure fully transparent colors are black, which can help with grayscale conversion 155 | if optimize_alpha { 156 | for color in &mut palette { 157 | if color.a == 0 { 158 | color.r = 0; 159 | color.g = 0; 160 | color.b = 0; 161 | } 162 | } 163 | } 164 | 165 | // Determine which channels are required 166 | let is_gray = if allow_grayscale { 167 | palette.iter().all(|c| c.r == c.g && c.g == c.b) 168 | } else { 169 | false 170 | }; 171 | let has_alpha = palette.iter().any(|c| c.a != 255); 172 | let color_type = match (is_gray, has_alpha) { 173 | (false, true) => ColorType::RGBA, 174 | (false, false) => ColorType::RGB { 175 | transparent_color: None, 176 | }, 177 | (true, true) => ColorType::GrayscaleAlpha, 178 | (true, false) => ColorType::Grayscale { 179 | transparent_shade: None, 180 | }, 181 | }; 182 | 183 | // Don't proceed if output would be too much larger 184 | let out_size = color_type.channels_per_pixel() as usize * png.data.len(); 185 | if out_size - png.data.len() > INDEXED_MAX_DIFF { 186 | return None; 187 | } 188 | 189 | // Construct the new data 190 | let black = RGBA::new(0, 0, 0, 255); 191 | let ch_start = if is_gray { 2 } else { 0 }; 192 | let ch_end = if has_alpha { 3 } else { 2 }; 193 | let mut data = Vec::with_capacity(out_size); 194 | for b in &png.data { 195 | let color = palette.get(*b as usize).unwrap_or(&black); 196 | data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]); 197 | } 198 | 199 | Some(PngImage { 200 | ihdr: IhdrData { 201 | color_type, 202 | ..png.ihdr 203 | }, 204 | data, 205 | }) 206 | } 207 | -------------------------------------------------------------------------------- /src/reduction/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{evaluate::Evaluator, png::PngImage, ColorType, Deadline, Deflaters, Options}; 4 | 5 | pub mod alpha; 6 | use crate::alpha::*; 7 | pub mod bit_depth; 8 | use crate::bit_depth::*; 9 | pub mod color; 10 | use crate::color::*; 11 | pub mod palette; 12 | use crate::palette::*; 13 | 14 | pub(crate) fn perform_reductions( 15 | mut png: Arc, 16 | opts: &Options, 17 | deadline: &Deadline, 18 | eval: &Evaluator, 19 | ) -> Arc { 20 | let mut evaluation_added = false; 21 | 22 | // At low compression levels, skip some transformations which are less likely to be effective 23 | // This currently affects optimization presets 0-2 24 | let cheap = match opts.deflate { 25 | Deflaters::Libdeflater { compression } => compression < 12 && opts.fast_evaluation, 26 | _ => false, 27 | }; 28 | 29 | // Interlacing must be processed first in order to evaluate the rest correctly 30 | if let Some(interlacing) = opts.interlace { 31 | if let Some(reduced) = png.change_interlacing(interlacing) { 32 | png = Arc::new(reduced); 33 | } 34 | } 35 | 36 | // If alpha optimization is enabled, clean the alpha channel before continuing 37 | // This can allow some color type reductions which may not have been possible otherwise 38 | if opts.optimize_alpha && !deadline.passed() { 39 | if let Some(reduced) = cleaned_alpha_channel(&png) { 40 | png = Arc::new(reduced); 41 | } 42 | } 43 | 44 | // Attempt to reduce 16-bit to 8-bit 45 | // This is just removal of bytes and does not need to be evaluated 46 | if opts.bit_depth_reduction && !deadline.passed() { 47 | if let Some(reduced) = reduced_bit_depth_16_to_8(&png, opts.scale_16) { 48 | png = Arc::new(reduced); 49 | } 50 | } 51 | 52 | // Attempt to reduce RGB to grayscale 53 | // This is just removal of bytes and does not need to be evaluated 54 | if opts.color_type_reduction && opts.grayscale_reduction && !deadline.passed() { 55 | if let Some(reduced) = reduced_rgb_to_grayscale(&png) { 56 | png = Arc::new(reduced); 57 | } 58 | } 59 | 60 | // Attempt to expand the bit depth to 8 61 | // This does need to be evaluated but will be done so later when it gets reduced again 62 | if opts.bit_depth_reduction && !deadline.passed() { 63 | if let Some(reduced) = expanded_bit_depth_to_8(&png) { 64 | png = Arc::new(reduced); 65 | } 66 | } 67 | 68 | // Now retain the current png for the evaluator baseline 69 | // It will only be entered into the evaluator if there are also others to evaluate 70 | let mut baseline = png.clone(); 71 | 72 | // Attempt to reduce and sort the palette 73 | if opts.palette_reduction && !deadline.passed() { 74 | if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) { 75 | png = Arc::new(reduced); 76 | // If the palette was reduced but the data is unchanged then this should become the baseline 77 | if png.data == baseline.data { 78 | baseline = png.clone(); 79 | } 80 | } 81 | if let Some(reduced) = sorted_palette(&png) { 82 | png = Arc::new(reduced); 83 | } 84 | // If either action changed the data then enter this into the evaluator 85 | if !Arc::ptr_eq(&png, &baseline) { 86 | eval.try_image_with_description(png.clone(), "Indexed (luma sort)"); 87 | evaluation_added = true; 88 | } 89 | } 90 | 91 | // Attempt alpha removal 92 | if opts.color_type_reduction && !deadline.passed() { 93 | if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) { 94 | png = Arc::new(reduced); 95 | // For small differences, if a tRNS chunk is required then enter this into the evaluator 96 | // Otherwise it is mostly just removal of bytes and should become the baseline 97 | if png.ihdr.color_type.has_trns() && baseline.data.len() - png.data.len() <= 1000 { 98 | eval.try_image(png.clone()); 99 | evaluation_added = true; 100 | } else { 101 | baseline = png.clone(); 102 | } 103 | } 104 | } 105 | 106 | // Attempt to convert from indexed to channels 107 | // This may give a better result due to dropping the PLTE chunk 108 | if !cheap && opts.color_type_reduction && !deadline.passed() { 109 | if let Some(reduced) = 110 | indexed_to_channels(&png, opts.grayscale_reduction, opts.optimize_alpha) 111 | { 112 | // This result should not be passed on to subsequent reductions 113 | eval.try_image(Arc::new(reduced)); 114 | evaluation_added = true; 115 | } 116 | } 117 | 118 | // Attempt to reduce to indexed 119 | // Keep the existing `png` var in case it is grayscale - we can test both for depth reduction later 120 | let mut indexed = None; 121 | if opts.color_type_reduction && !deadline.passed() { 122 | if let Some(reduced) = reduced_to_indexed(&png, opts.grayscale_reduction) { 123 | // Make sure the palette gets sorted (but don't bother evaluating both results) 124 | let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced)); 125 | // For relatively small differences, enter this into the evaluator 126 | // Otherwise we're confident enough for it to become the baseline 127 | if png.data.len() - new.data.len() <= INDEXED_MAX_DIFF { 128 | eval.try_image_with_description(new.clone(), "Indexed (luma sort)"); 129 | evaluation_added = true; 130 | } else { 131 | baseline = new.clone(); 132 | } 133 | indexed = Some(new); 134 | } 135 | } 136 | 137 | // Attempt additional palette sorting techniques 138 | if !cheap && opts.palette_reduction { 139 | // Collect a list of palettes so we can avoid evaluating the same one twice 140 | let mut palettes = Vec::new(); 141 | if let ColorType::Indexed { palette } = &baseline.ihdr.color_type { 142 | palettes.push(palette.clone()); 143 | } 144 | // Make sure we use the `indexed` var as input if it exists 145 | // This one doesn't need to be kept in the palette list as the sorters will fail if there's no change 146 | let input = indexed.as_ref().unwrap_or(&png); 147 | 148 | // Attempt to sort the palette using the battiato method 149 | if !deadline.passed() { 150 | if let Some(reduced) = sorted_palette_battiato(input) { 151 | if let ColorType::Indexed { palette } = &reduced.ihdr.color_type { 152 | if !palettes.contains(palette) { 153 | palettes.push(palette.clone()); 154 | eval.try_image_with_description( 155 | Arc::new(reduced), 156 | "Indexed (battiato sort)", 157 | ); 158 | evaluation_added = true; 159 | } 160 | } 161 | } 162 | } 163 | 164 | // Attempt to sort the palette using the mzeng method 165 | if !deadline.passed() { 166 | if let Some(reduced) = sorted_palette_mzeng(input) { 167 | if let ColorType::Indexed { palette } = &reduced.ihdr.color_type { 168 | if !palettes.contains(palette) { 169 | palettes.push(palette.clone()); 170 | eval.try_image_with_description(Arc::new(reduced), "Indexed (mzeng sort)"); 171 | evaluation_added = true; 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | // Attempt to reduce to a lower bit depth 179 | if opts.bit_depth_reduction && !deadline.passed() { 180 | // First try the `png` var 181 | let reduced = reduced_bit_depth_8_or_less(&png); 182 | // Then try the `indexed` var, unless we're doing cheap evaluations and already have a reduction 183 | if (!cheap || reduced.is_none()) && !deadline.passed() { 184 | if let Some(indexed) = indexed.and_then(|png| reduced_bit_depth_8_or_less(&png)) { 185 | // Only evaluate this if it's different from the first result (which must be grayscale if it exists) 186 | if reduced.as_ref().map_or(true, |r| r.data != indexed.data) { 187 | eval.try_image(Arc::new(indexed)); 188 | evaluation_added = true; 189 | } 190 | } 191 | } 192 | // Enter the first result into the evaluator 193 | if let Some(reduced) = reduced { 194 | eval.try_image(Arc::new(reduced)); 195 | evaluation_added = true; 196 | } 197 | } 198 | 199 | if evaluation_added { 200 | eval.try_image(baseline.clone()); 201 | } 202 | baseline 203 | } 204 | -------------------------------------------------------------------------------- /src/sanity_checks.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | 3 | use image::{codecs::png::PngDecoder, *}; 4 | use log::{error, warn}; 5 | 6 | #[cfg(not(feature = "parallel"))] 7 | use crate::rayon; 8 | 9 | /// Validate that the output png data still matches the original image 10 | pub fn validate_output(output: &[u8], original_data: &[u8]) -> bool { 11 | let (old_frames, new_frames) = rayon::join( 12 | || load_png_image_from_memory(original_data), 13 | || load_png_image_from_memory(output), 14 | ); 15 | 16 | match (new_frames, old_frames) { 17 | (Err(new_err), _) => { 18 | error!("Failed to read output image for validation: {}", new_err); 19 | false 20 | } 21 | (_, Err(old_err)) => { 22 | // The original image might be invalid if, for example, there is a CRC error, 23 | // and we set fix_errors to true. In that case, all we can do is check that the 24 | // new image is decodable. 25 | warn!("Failed to read input image for validation: {}", old_err); 26 | true 27 | } 28 | (Ok(new_frames), Ok(old_frames)) if new_frames.len() != old_frames.len() => false, 29 | (Ok(new_frames), Ok(old_frames)) => { 30 | for (a, b) in old_frames.iter().zip(new_frames) { 31 | if !images_equal(a, &b) { 32 | return false; 33 | } 34 | } 35 | true 36 | } 37 | } 38 | } 39 | 40 | /// Loads a PNG image from memory to frames of [RgbaImage] 41 | fn load_png_image_from_memory(png_data: &[u8]) -> Result, image::ImageError> { 42 | let decoder = PngDecoder::new(Cursor::new(png_data))?; 43 | if decoder.is_apng()? { 44 | decoder 45 | .apng()? 46 | .into_frames() 47 | .map(|f| f.map(|f| f.into_buffer())) 48 | .collect() 49 | } else { 50 | DynamicImage::from_decoder(decoder).map(|i| vec![i.into_rgba8()]) 51 | } 52 | } 53 | 54 | /// Compares images pixel by pixel for equivalent content 55 | fn images_equal(old_png: &RgbaImage, new_png: &RgbaImage) -> bool { 56 | let a = old_png.pixels().filter(|x| { 57 | let p = x.channels(); 58 | !(p.len() == 4 && p[3] == 0) 59 | }); 60 | let b = new_png.pixels().filter(|x| { 61 | let p = x.channels(); 62 | !(p.len() == 4 && p[3] == 0) 63 | }); 64 | a.eq(b) 65 | } 66 | -------------------------------------------------------------------------------- /tests/files/apng_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/apng_file.png -------------------------------------------------------------------------------- /tests/files/badsrgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/badsrgb.png -------------------------------------------------------------------------------- /tests/files/c2pa-signed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/c2pa-signed.png -------------------------------------------------------------------------------- /tests/files/corrupted_header.png: -------------------------------------------------------------------------------- 1 | abcdefghjik 2 | -------------------------------------------------------------------------------- /tests/files/filter_0_for_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_palette_1.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_palette_2.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_palette_4.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_rgb_16.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_rgb_8.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_rgba_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_rgba_16.png -------------------------------------------------------------------------------- /tests/files/filter_0_for_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_0_for_rgba_8.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_palette_1.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_palette_2.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_palette_4.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_rgb_16.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_rgb_8.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_rgba_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_rgba_16.png -------------------------------------------------------------------------------- /tests/files/filter_1_for_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_1_for_rgba_8.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_palette_1.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_palette_2.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_palette_4.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_rgb_16.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_rgb_8.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_rgba_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_rgba_16.png -------------------------------------------------------------------------------- /tests/files/filter_2_for_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_2_for_rgba_8.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_palette_1.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_palette_2.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_palette_4.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_rgb_16.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_rgb_8.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_rgba_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_rgba_16.png -------------------------------------------------------------------------------- /tests/files/filter_3_for_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_3_for_rgba_8.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_palette_1.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_palette_2.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_palette_4.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_rgb_16.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_rgb_8.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_rgba_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_rgba_16.png -------------------------------------------------------------------------------- /tests/files/filter_4_for_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_4_for_rgba_8.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_palette_1.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_palette_2.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_palette_4.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_rgb_16.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_rgb_8.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_rgba_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_rgba_16.png -------------------------------------------------------------------------------- /tests/files/filter_5_for_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/filter_5_for_rgba_8.png -------------------------------------------------------------------------------- /tests/files/fix_errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/fix_errors.png -------------------------------------------------------------------------------- /tests/files/fully_optimized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/fully_optimized.png -------------------------------------------------------------------------------- /tests/files/grayscale_16_should_be_grayscale_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_16_should_be_grayscale_1.png -------------------------------------------------------------------------------- /tests/files/grayscale_16_should_be_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_16_should_be_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/grayscale_16_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_16_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/grayscale_2_should_be_grayscale_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_2_should_be_grayscale_1.png -------------------------------------------------------------------------------- /tests/files/grayscale_4_should_be_grayscale_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_4_should_be_grayscale_1.png -------------------------------------------------------------------------------- /tests/files/grayscale_4_should_be_grayscale_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_4_should_be_grayscale_2.png -------------------------------------------------------------------------------- /tests/files/grayscale_8_should_be_grayscale_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_8_should_be_grayscale_1.png -------------------------------------------------------------------------------- /tests/files/grayscale_8_should_be_grayscale_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_8_should_be_grayscale_2.png -------------------------------------------------------------------------------- /tests/files/grayscale_8_should_be_grayscale_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_8_should_be_grayscale_4.png -------------------------------------------------------------------------------- /tests/files/grayscale_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/grayscale_8_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_8_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/grayscale_8_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_8_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/grayscale_8_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_8_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/grayscale_8_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_8_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_16_reduce_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_16_reduce_alpha.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_16_should_be_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_16_should_be_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_16_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_16_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_16_should_be_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_16_should_be_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_16_should_be_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_16_should_be_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_16_should_be_grayscale_trns_16.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_8_reduce_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_8_reduce_alpha.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_8_should_be_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_8_should_be_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_8_should_be_grayscale_trns_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_8_should_be_grayscale_trns_1.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_8_should_be_grayscale_trns_8.png -------------------------------------------------------------------------------- /tests/files/grayscale_alpha_8_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_alpha_8_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/grayscale_trns_8_should_be_grayscale_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/grayscale_trns_8_should_be_grayscale_1.png -------------------------------------------------------------------------------- /tests/files/interlaced_0_to_1_other_filter_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_0_to_1_other_filter_mode.png -------------------------------------------------------------------------------- /tests/files/interlaced_grayscale_16_should_be_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_grayscale_16_should_be_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/interlaced_grayscale_16_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_grayscale_16_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_grayscale_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_grayscale_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_grayscale_alpha_16_should_be_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_grayscale_alpha_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_grayscale_alpha_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_grayscale_alpha_8_should_be_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_grayscale_alpha_8_should_be_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_odd_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_odd_width.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_1_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_1_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_2_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_2_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_2_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_2_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_4_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_4_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_4_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_4_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_4_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_4_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_8_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_8_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_8_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_8_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_8_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_8_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/interlaced_palette_8_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_palette_8_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_16_should_be_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_16_should_be_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_16_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_16_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_16_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_16_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_16_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_16_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_16_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_16_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_16_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_16_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_16_should_be_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_16_should_be_rgb_16.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_16_should_be_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_16_should_be_rgb_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_8_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_8_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_8_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_8_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_8_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_8_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_8_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_8_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgb_8_should_be_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgb_8_should_be_rgb_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_rgb_16.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_rgb_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_rgba_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_rgba_16.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_16_should_be_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_16_should_be_rgba_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_8_should_be_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_8_should_be_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_8_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_8_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_8_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_8_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_8_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_8_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_8_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_8_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_8_should_be_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_8_should_be_rgb_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_rgba_8_should_be_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_rgba_8_should_be_rgba_8.png -------------------------------------------------------------------------------- /tests/files/interlaced_small_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_small_files.png -------------------------------------------------------------------------------- /tests/files/interlaced_vertical_filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlaced_vertical_filters.png -------------------------------------------------------------------------------- /tests/files/interlacing_0_to_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlacing_0_to_1.png -------------------------------------------------------------------------------- /tests/files/interlacing_0_to_1_small_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlacing_0_to_1_small_files.png -------------------------------------------------------------------------------- /tests/files/interlacing_1_to_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlacing_1_to_0.png -------------------------------------------------------------------------------- /tests/files/interlacing_1_to_0_small_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/interlacing_1_to_0_small_files.png -------------------------------------------------------------------------------- /tests/files/issue-140.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-140.png -------------------------------------------------------------------------------- /tests/files/issue-171.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-171.png -------------------------------------------------------------------------------- /tests/files/issue-175.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-175.png -------------------------------------------------------------------------------- /tests/files/issue-182.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-182.png -------------------------------------------------------------------------------- /tests/files/issue-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-42.png -------------------------------------------------------------------------------- /tests/files/issue-56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-56.png -------------------------------------------------------------------------------- /tests/files/issue-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-58.png -------------------------------------------------------------------------------- /tests/files/issue-59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-59.png -------------------------------------------------------------------------------- /tests/files/issue-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-60.png -------------------------------------------------------------------------------- /tests/files/issue-89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/issue-89.png -------------------------------------------------------------------------------- /tests/files/palette_1_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_1_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/palette_2_should_be_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_2_should_be_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/palette_2_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_2_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/palette_2_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_2_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/palette_4_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_4_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/palette_4_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_4_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/palette_4_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_4_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/palette_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/palette_8_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_8_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/palette_8_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_8_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/palette_8_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_8_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/palette_8_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_8_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/palette_8_should_be_rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_8_should_be_rgb.png -------------------------------------------------------------------------------- /tests/files/palette_8_should_be_rgba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_8_should_be_rgba.png -------------------------------------------------------------------------------- /tests/files/palette_should_be_reduced_with_bkgd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_should_be_reduced_with_bkgd.png -------------------------------------------------------------------------------- /tests/files/palette_should_be_reduced_with_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_should_be_reduced_with_both.png -------------------------------------------------------------------------------- /tests/files/palette_should_be_reduced_with_dupes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_should_be_reduced_with_dupes.png -------------------------------------------------------------------------------- /tests/files/palette_should_be_reduced_with_missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_should_be_reduced_with_missing.png -------------------------------------------------------------------------------- /tests/files/palette_should_be_reduced_with_unused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/palette_should_be_reduced_with_unused.png -------------------------------------------------------------------------------- /tests/files/preserve_attrs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/preserve_attrs.png -------------------------------------------------------------------------------- /tests/files/profile_adobe_rgb_disallow_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/profile_adobe_rgb_disallow_gray.png -------------------------------------------------------------------------------- /tests/files/profile_gray_disallow_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/profile_gray_disallow_color.png -------------------------------------------------------------------------------- /tests/files/profile_srgb_allow_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/profile_srgb_allow_gray.png -------------------------------------------------------------------------------- /tests/files/profile_srgb_no_strip_disallow_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/profile_srgb_no_strip_disallow_gray.png -------------------------------------------------------------------------------- /tests/files/quiet_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/quiet_mode.png -------------------------------------------------------------------------------- /tests/files/raw_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/raw_api.png -------------------------------------------------------------------------------- /tests/files/rgb_16_should_be_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_16_should_be_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/rgb_16_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_16_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/rgb_16_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_16_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/rgb_16_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_16_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/rgb_16_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_16_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/rgb_16_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_16_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/rgb_16_should_be_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_16_should_be_rgb_16.png -------------------------------------------------------------------------------- /tests/files/rgb_16_should_be_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_16_should_be_rgb_8.png -------------------------------------------------------------------------------- /tests/files/rgb_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/rgb_8_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_8_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/rgb_8_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_8_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/rgb_8_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_8_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/rgb_8_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_8_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/rgb_8_should_be_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_8_should_be_rgb_8.png -------------------------------------------------------------------------------- /tests/files/rgb_trns_8_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgb_trns_8_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/rgba_16_reduce_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_reduce_alpha.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_grayscale_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_grayscale_16.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_grayscale_alpha_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_grayscale_alpha_16.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_rgb_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_rgb_16.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_rgb_8.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_rgb_trns_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_rgb_trns_16.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_rgba_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_rgba_16.png -------------------------------------------------------------------------------- /tests/files/rgba_16_should_be_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_16_should_be_rgba_8.png -------------------------------------------------------------------------------- /tests/files/rgba_8_reduce_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_reduce_alpha.png -------------------------------------------------------------------------------- /tests/files/rgba_8_should_be_grayscale_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_should_be_grayscale_8.png -------------------------------------------------------------------------------- /tests/files/rgba_8_should_be_grayscale_alpha_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_should_be_grayscale_alpha_8.png -------------------------------------------------------------------------------- /tests/files/rgba_8_should_be_palette_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_should_be_palette_1.png -------------------------------------------------------------------------------- /tests/files/rgba_8_should_be_palette_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_should_be_palette_2.png -------------------------------------------------------------------------------- /tests/files/rgba_8_should_be_palette_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_should_be_palette_4.png -------------------------------------------------------------------------------- /tests/files/rgba_8_should_be_palette_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_should_be_palette_8.png -------------------------------------------------------------------------------- /tests/files/rgba_8_should_be_rgb_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_should_be_rgb_8.png -------------------------------------------------------------------------------- /tests/files/rgba_8_should_be_rgb_trns_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_should_be_rgb_trns_8.png -------------------------------------------------------------------------------- /tests/files/rgba_8_should_be_rgba_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/rgba_8_should_be_rgba_8.png -------------------------------------------------------------------------------- /tests/files/small_files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/small_files.png -------------------------------------------------------------------------------- /tests/files/strip_chunks_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/strip_chunks_all.png -------------------------------------------------------------------------------- /tests/files/strip_chunks_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/strip_chunks_list.png -------------------------------------------------------------------------------- /tests/files/strip_chunks_none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/strip_chunks_none.png -------------------------------------------------------------------------------- /tests/files/strip_chunks_safe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/strip_chunks_safe.png -------------------------------------------------------------------------------- /tests/files/verbose_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/verbose_mode.png -------------------------------------------------------------------------------- /tests/files/zopfli_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shssoichiro/oxipng/54a31a65a70905f5bc7f74e2c402ab052413d5f8/tests/files/zopfli_mode.png -------------------------------------------------------------------------------- /tests/interlacing.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::remove_file, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use oxipng::{internal_tests::*, *}; 7 | 8 | const RGB: u8 = 2; 9 | const INDEXED: u8 = 3; 10 | 11 | fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { 12 | let mut options = oxipng::Options { 13 | force: true, 14 | ..Default::default() 15 | }; 16 | let mut filter = IndexSet::new(); 17 | filter.insert(RowFilter::None); 18 | options.filter = filter; 19 | 20 | (OutFile::from_path(input.with_extension("out.png")), options) 21 | } 22 | 23 | fn test_it_converts( 24 | input: &str, 25 | interlace: Interlacing, 26 | color_type_in: u8, 27 | bit_depth_in: BitDepth, 28 | color_type_out: u8, 29 | bit_depth_out: BitDepth, 30 | ) { 31 | let input = PathBuf::from(input); 32 | let (output, mut opts) = get_opts(&input); 33 | let png = PngData::new(&input, &opts).unwrap(); 34 | opts.interlace = Some(interlace); 35 | assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); 36 | assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); 37 | assert_eq!( 38 | png.raw.ihdr.interlaced, 39 | if interlace == Interlacing::Adam7 { 40 | Interlacing::None 41 | } else { 42 | Interlacing::Adam7 43 | } 44 | ); 45 | 46 | match oxipng::optimize(&InFile::Path(input), &output, &opts) { 47 | Ok(_) => (), 48 | Err(x) => panic!("{}", x), 49 | }; 50 | let output = output.path().unwrap(); 51 | assert!(output.exists()); 52 | 53 | let png = match PngData::new(output, &opts) { 54 | Ok(x) => x, 55 | Err(x) => { 56 | remove_file(output).ok(); 57 | panic!("{}", x) 58 | } 59 | }; 60 | 61 | assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out); 62 | assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); 63 | 64 | remove_file(output).ok(); 65 | } 66 | 67 | #[test] 68 | fn deinterlace_rgb_16() { 69 | test_it_converts( 70 | "tests/files/interlaced_rgb_16_should_be_rgb_16.png", 71 | Interlacing::None, 72 | RGB, 73 | BitDepth::Sixteen, 74 | RGB, 75 | BitDepth::Sixteen, 76 | ); 77 | } 78 | 79 | #[test] 80 | fn deinterlace_rgb_8() { 81 | test_it_converts( 82 | "tests/files/interlaced_rgb_8_should_be_rgb_8.png", 83 | Interlacing::None, 84 | RGB, 85 | BitDepth::Eight, 86 | RGB, 87 | BitDepth::Eight, 88 | ); 89 | } 90 | 91 | #[test] 92 | fn deinterlace_palette_8() { 93 | test_it_converts( 94 | "tests/files/interlaced_palette_8_should_be_palette_8.png", 95 | Interlacing::None, 96 | INDEXED, 97 | BitDepth::Eight, 98 | INDEXED, 99 | BitDepth::Eight, 100 | ); 101 | } 102 | 103 | #[test] 104 | fn deinterlace_palette_4() { 105 | test_it_converts( 106 | "tests/files/interlaced_palette_4_should_be_palette_4.png", 107 | Interlacing::None, 108 | INDEXED, 109 | BitDepth::Four, 110 | INDEXED, 111 | BitDepth::Four, 112 | ); 113 | } 114 | 115 | #[test] 116 | fn deinterlace_palette_2() { 117 | test_it_converts( 118 | "tests/files/interlaced_palette_2_should_be_palette_2.png", 119 | Interlacing::None, 120 | INDEXED, 121 | BitDepth::Two, 122 | INDEXED, 123 | BitDepth::Two, 124 | ); 125 | } 126 | 127 | #[test] 128 | fn deinterlace_palette_1() { 129 | test_it_converts( 130 | "tests/files/interlaced_palette_1_should_be_palette_1.png", 131 | Interlacing::None, 132 | INDEXED, 133 | BitDepth::One, 134 | INDEXED, 135 | BitDepth::One, 136 | ); 137 | } 138 | 139 | #[test] 140 | fn interlace_rgb_16() { 141 | test_it_converts( 142 | "tests/files/rgb_16_should_be_rgb_16.png", 143 | Interlacing::Adam7, 144 | RGB, 145 | BitDepth::Sixteen, 146 | RGB, 147 | BitDepth::Sixteen, 148 | ); 149 | } 150 | 151 | #[test] 152 | fn interlace_rgb_8() { 153 | test_it_converts( 154 | "tests/files/rgb_8_should_be_rgb_8.png", 155 | Interlacing::Adam7, 156 | RGB, 157 | BitDepth::Eight, 158 | RGB, 159 | BitDepth::Eight, 160 | ); 161 | } 162 | 163 | #[test] 164 | fn interlace_palette_8() { 165 | test_it_converts( 166 | "tests/files/palette_8_should_be_palette_8.png", 167 | Interlacing::Adam7, 168 | INDEXED, 169 | BitDepth::Eight, 170 | INDEXED, 171 | BitDepth::Eight, 172 | ); 173 | } 174 | 175 | #[test] 176 | fn interlace_palette_4() { 177 | test_it_converts( 178 | "tests/files/palette_4_should_be_palette_4.png", 179 | Interlacing::Adam7, 180 | INDEXED, 181 | BitDepth::Four, 182 | INDEXED, 183 | BitDepth::Four, 184 | ); 185 | } 186 | 187 | #[test] 188 | fn interlace_palette_2() { 189 | test_it_converts( 190 | "tests/files/palette_2_should_be_palette_2.png", 191 | Interlacing::Adam7, 192 | INDEXED, 193 | BitDepth::Two, 194 | INDEXED, 195 | BitDepth::Two, 196 | ); 197 | } 198 | 199 | #[test] 200 | fn interlace_palette_1() { 201 | test_it_converts( 202 | "tests/files/palette_1_should_be_palette_1.png", 203 | Interlacing::Adam7, 204 | INDEXED, 205 | BitDepth::One, 206 | INDEXED, 207 | BitDepth::One, 208 | ); 209 | } 210 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, fs::File, io::prelude::*}; 2 | 3 | use oxipng::*; 4 | 5 | #[test] 6 | fn optimize_from_memory() { 7 | let mut in_file = File::open("tests/files/fully_optimized.png").unwrap(); 8 | let mut in_file_buf: Vec = Vec::new(); 9 | in_file.read_to_end(&mut in_file_buf).unwrap(); 10 | 11 | let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default()); 12 | assert!(result.is_ok()); 13 | } 14 | 15 | #[test] 16 | fn optimize_from_memory_corrupted() { 17 | let mut in_file = File::open("tests/files/corrupted_header.png").unwrap(); 18 | let mut in_file_buf: Vec = Vec::new(); 19 | in_file.read_to_end(&mut in_file_buf).unwrap(); 20 | 21 | let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default()); 22 | assert!(result.is_err()); 23 | } 24 | 25 | #[test] 26 | fn optimize_from_memory_apng() { 27 | let mut in_file = File::open("tests/files/apng_file.png").unwrap(); 28 | let mut in_file_buf: Vec = Vec::new(); 29 | in_file.read_to_end(&mut in_file_buf).unwrap(); 30 | 31 | let result = oxipng::optimize_from_memory(&in_file_buf, &Options::default()); 32 | assert!(result.is_ok()); 33 | } 34 | 35 | #[test] 36 | fn optimize() { 37 | let result = oxipng::optimize( 38 | &"tests/files/fully_optimized.png".into(), 39 | &OutFile::None, 40 | &Options::default(), 41 | ); 42 | assert!(result.is_ok()); 43 | } 44 | 45 | #[test] 46 | fn skip_c2pa() { 47 | let result = oxipng::optimize( 48 | &"tests/files/c2pa-signed.png".into(), 49 | &OutFile::None, 50 | &Options { 51 | strip: StripChunks::Keep(indexset! {*b"caBX"}), 52 | ..Options::default() 53 | }, 54 | ); 55 | assert!(matches!(result, Err(PngError::C2PAMetadataPreventsChanges))); 56 | } 57 | 58 | #[test] 59 | fn optimize_corrupted() { 60 | let result = oxipng::optimize( 61 | &"tests/files/corrupted_header.png".into(), 62 | &OutFile::None, 63 | &Options::default(), 64 | ); 65 | assert!(result.is_err()); 66 | } 67 | 68 | #[test] 69 | fn optimize_apng() { 70 | let result = oxipng::optimize( 71 | &"tests/files/apng_file.png".into(), 72 | &OutFile::None, 73 | &Options::from_preset(0), 74 | ); 75 | assert!(result.is_ok()); 76 | } 77 | 78 | #[test] 79 | fn optimize_srgb_icc() { 80 | let file = fs::read("tests/files/badsrgb.png").unwrap(); 81 | let mut opts = Options::default(); 82 | 83 | let result = oxipng::optimize_from_memory(&file, &opts); 84 | assert!(result.unwrap().len() > 1000); 85 | 86 | opts.strip = StripChunks::Safe; 87 | let result = oxipng::optimize_from_memory(&file, &opts); 88 | assert!(result.unwrap().len() < 1000); 89 | } 90 | -------------------------------------------------------------------------------- /tests/raw.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::Arc}; 2 | 3 | use oxipng::{internal_tests::*, *}; 4 | 5 | fn get_opts() -> Options { 6 | Options { 7 | force: true, 8 | filter: indexset! { RowFilter::None }, 9 | ..Default::default() 10 | } 11 | } 12 | 13 | fn test_it_converts(input: &str) { 14 | let input = PathBuf::from(input); 15 | let opts = get_opts(); 16 | 17 | let original_data = PngData::read_file(&input).unwrap(); 18 | let image = PngData::from_slice(&original_data, &opts).unwrap(); 19 | let png = Arc::try_unwrap(image.raw).unwrap(); 20 | 21 | let num_chunks = image.aux_chunks.len(); 22 | assert!(num_chunks > 0); 23 | 24 | let mut raw = RawImage::new( 25 | png.ihdr.width, 26 | png.ihdr.height, 27 | png.ihdr.color_type, 28 | png.ihdr.bit_depth, 29 | png.data, 30 | ) 31 | .unwrap(); 32 | 33 | for chunk in image.aux_chunks { 34 | raw.add_png_chunk(chunk.name, chunk.data); 35 | } 36 | 37 | let output = raw.create_optimized_png(&opts).unwrap(); 38 | 39 | let new = PngData::from_slice(&output, &opts).unwrap(); 40 | assert!(new.aux_chunks.len() == num_chunks); 41 | 42 | #[cfg(feature = "sanity-checks")] 43 | assert!(validate_output(&output, &original_data)); 44 | } 45 | 46 | #[test] 47 | fn from_file() { 48 | test_it_converts("tests/files/raw_api.png"); 49 | } 50 | 51 | #[test] 52 | fn custom_indexed() { 53 | let opts = get_opts(); 54 | 55 | let raw = RawImage::new( 56 | 4, 57 | 4, 58 | ColorType::Indexed { 59 | palette: vec![ 60 | RGBA8::new(255, 255, 255, 255), 61 | RGBA8::new(255, 0, 0, 255), 62 | RGBA8::new(0, 255, 0, 255), 63 | RGBA8::new(0, 0, 255, 255), 64 | ], 65 | }, 66 | BitDepth::Eight, 67 | vec![0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 3, 3], 68 | ) 69 | .unwrap(); 70 | 71 | raw.create_optimized_png(&opts).unwrap(); 72 | } 73 | 74 | #[test] 75 | fn invalid_depth() { 76 | RawImage::new( 77 | 2, 78 | 2, 79 | ColorType::RGBA, 80 | BitDepth::Four, 81 | vec![0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 3, 3, 2, 2, 3, 3], 82 | ) 83 | .expect_err("Expected invalid depth for color type"); 84 | } 85 | 86 | #[test] 87 | fn incorrect_length() { 88 | RawImage::new( 89 | 2, 90 | 2, 91 | ColorType::RGBA, 92 | BitDepth::Eight, 93 | vec![0, 0, 1, 1, 0, 0, 1, 1], 94 | ) 95 | .expect_err("Expected incorrect data length"); 96 | } 97 | -------------------------------------------------------------------------------- /tests/regression.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::remove_file, 3 | path::{Path, PathBuf}, 4 | sync::Arc, 5 | }; 6 | 7 | use oxipng::{internal_tests::*, *}; 8 | 9 | const GRAYSCALE: u8 = 0; 10 | const INDEXED: u8 = 3; 11 | const GRAYSCALE_ALPHA: u8 = 4; 12 | const RGBA: u8 = 6; 13 | 14 | fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { 15 | let mut options = oxipng::Options { 16 | force: true, 17 | ..Default::default() 18 | }; 19 | let mut filter = IndexSet::new(); 20 | filter.insert(RowFilter::None); 21 | options.filter = filter; 22 | 23 | (OutFile::from_path(input.with_extension("out.png")), options) 24 | } 25 | 26 | fn test_it_converts( 27 | input: &str, 28 | custom: Option<(OutFile, oxipng::Options)>, 29 | color_type_in: u8, 30 | bit_depth_in: BitDepth, 31 | color_type_out: u8, 32 | bit_depth_out: BitDepth, 33 | ) -> PngImage { 34 | let input = PathBuf::from(input); 35 | let (output, opts) = custom.unwrap_or_else(|| get_opts(&input)); 36 | let png = PngData::new(&input, &opts).unwrap(); 37 | 38 | assert_eq!( 39 | png.raw.ihdr.color_type.png_header_code(), 40 | color_type_in, 41 | "test file is broken" 42 | ); 43 | assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in, "test file is broken"); 44 | 45 | match oxipng::optimize(&InFile::Path(input), &output, &opts) { 46 | Ok(_) => (), 47 | Err(x) => panic!("{}", x), 48 | }; 49 | let output = output.path().unwrap(); 50 | assert!(output.exists()); 51 | 52 | let png = match PngData::new(output, &opts) { 53 | Ok(x) => x, 54 | Err(x) => { 55 | remove_file(output).ok(); 56 | panic!("{}", x) 57 | } 58 | }; 59 | 60 | assert_eq!( 61 | png.raw.ihdr.color_type.png_header_code(), 62 | color_type_out, 63 | "optimized to wrong color type" 64 | ); 65 | assert_eq!( 66 | png.raw.ihdr.bit_depth, bit_depth_out, 67 | "optimized to wrong bit depth" 68 | ); 69 | if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { 70 | assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8)); 71 | } 72 | 73 | remove_file(output).ok(); 74 | Arc::try_unwrap(png.raw).unwrap() 75 | } 76 | 77 | #[test] 78 | fn issue_42() { 79 | let input = "tests/files/issue-42.png"; 80 | let (output, mut opts) = get_opts(Path::new(input)); 81 | opts.interlace = Some(Interlacing::Adam7); 82 | test_it_converts( 83 | input, 84 | Some((output, opts)), 85 | GRAYSCALE_ALPHA, 86 | BitDepth::Eight, 87 | GRAYSCALE_ALPHA, 88 | BitDepth::Eight, 89 | ); 90 | } 91 | 92 | #[test] 93 | fn issue_56() { 94 | test_it_converts( 95 | "tests/files/issue-56.png", 96 | None, 97 | INDEXED, 98 | BitDepth::Four, 99 | INDEXED, 100 | BitDepth::Four, 101 | ); 102 | } 103 | 104 | #[test] 105 | fn issue_58() { 106 | test_it_converts( 107 | "tests/files/issue-58.png", 108 | None, 109 | INDEXED, 110 | BitDepth::Four, 111 | INDEXED, 112 | BitDepth::Four, 113 | ); 114 | } 115 | 116 | #[test] 117 | fn issue_59() { 118 | test_it_converts( 119 | "tests/files/issue-59.png", 120 | None, 121 | RGBA, 122 | BitDepth::Eight, 123 | INDEXED, 124 | BitDepth::Eight, 125 | ); 126 | } 127 | 128 | #[test] 129 | fn issue_60() { 130 | test_it_converts( 131 | "tests/files/issue-60.png", 132 | None, 133 | RGBA, 134 | BitDepth::Eight, 135 | GRAYSCALE_ALPHA, 136 | BitDepth::Eight, 137 | ); 138 | } 139 | 140 | #[test] 141 | fn issue_89() { 142 | test_it_converts( 143 | "tests/files/issue-89.png", 144 | None, 145 | RGBA, 146 | BitDepth::Eight, 147 | GRAYSCALE, 148 | BitDepth::Eight, 149 | ); 150 | } 151 | 152 | #[test] 153 | fn issue_140() { 154 | test_it_converts( 155 | "tests/files/issue-140.png", 156 | None, 157 | GRAYSCALE, 158 | BitDepth::Two, 159 | GRAYSCALE, 160 | BitDepth::Two, 161 | ); 162 | } 163 | 164 | #[test] 165 | fn issue_171() { 166 | test_it_converts( 167 | "tests/files/issue-171.png", 168 | None, 169 | GRAYSCALE, 170 | BitDepth::Eight, 171 | GRAYSCALE, 172 | BitDepth::Eight, 173 | ); 174 | } 175 | 176 | #[test] 177 | fn issue_175() { 178 | test_it_converts( 179 | "tests/files/issue-175.png", 180 | None, 181 | GRAYSCALE, 182 | BitDepth::One, 183 | GRAYSCALE, 184 | BitDepth::One, 185 | ); 186 | } 187 | 188 | #[test] 189 | fn issue_182() { 190 | let input = "tests/files/issue-182.png"; 191 | let (output, mut opts) = get_opts(Path::new(input)); 192 | opts.interlace = Some(Interlacing::Adam7); 193 | 194 | test_it_converts( 195 | input, 196 | Some((output, opts)), 197 | GRAYSCALE, 198 | BitDepth::One, 199 | GRAYSCALE, 200 | BitDepth::One, 201 | ); 202 | } 203 | -------------------------------------------------------------------------------- /tests/strategies.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::remove_file, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use oxipng::{internal_tests::*, *}; 7 | 8 | const GRAYSCALE: u8 = 0; 9 | const RGB: u8 = 2; 10 | const INDEXED: u8 = 3; 11 | const RGBA: u8 = 6; 12 | 13 | fn get_opts(input: &Path) -> (OutFile, oxipng::Options) { 14 | let mut options = oxipng::Options { 15 | force: true, 16 | ..Default::default() 17 | }; 18 | let mut filter = IndexSet::new(); 19 | filter.insert(RowFilter::None); 20 | options.filter = filter; 21 | 22 | (OutFile::from_path(input.with_extension("out.png")), options) 23 | } 24 | 25 | fn test_it_converts( 26 | input: &str, 27 | filter: RowFilter, 28 | color_type_in: u8, 29 | bit_depth_in: BitDepth, 30 | color_type_out: u8, 31 | bit_depth_out: BitDepth, 32 | ) { 33 | let input = PathBuf::from(input); 34 | 35 | let (output, mut opts) = get_opts(&input); 36 | let png = PngData::new(&input, &opts).unwrap(); 37 | opts.filter = IndexSet::new(); 38 | opts.filter.insert(filter); 39 | assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_in); 40 | assert_eq!(png.raw.ihdr.bit_depth, bit_depth_in); 41 | 42 | match oxipng::optimize(&InFile::Path(input), &output, &opts) { 43 | Ok(_) => (), 44 | Err(x) => panic!("{}", x), 45 | }; 46 | let output = output.path().unwrap(); 47 | assert!(output.exists()); 48 | 49 | let png = match PngData::new(output, &opts) { 50 | Ok(x) => x, 51 | Err(x) => { 52 | remove_file(output).ok(); 53 | panic!("{}", x) 54 | } 55 | }; 56 | 57 | assert_eq!(png.raw.ihdr.color_type.png_header_code(), color_type_out); 58 | assert_eq!(png.raw.ihdr.bit_depth, bit_depth_out); 59 | if let ColorType::Indexed { palette } = &png.raw.ihdr.color_type { 60 | assert!(palette.len() <= 1 << (png.raw.ihdr.bit_depth as u8)); 61 | } 62 | 63 | remove_file(output).ok(); 64 | } 65 | 66 | #[test] 67 | fn filter_minsum() { 68 | test_it_converts( 69 | "tests/files/rgb_16_should_be_rgb_16.png", 70 | RowFilter::MinSum, 71 | RGB, 72 | BitDepth::Sixteen, 73 | RGB, 74 | BitDepth::Sixteen, 75 | ); 76 | } 77 | 78 | #[test] 79 | fn filter_entropy() { 80 | test_it_converts( 81 | "tests/files/rgb_8_should_be_rgb_8.png", 82 | RowFilter::Entropy, 83 | RGB, 84 | BitDepth::Eight, 85 | RGB, 86 | BitDepth::Eight, 87 | ); 88 | } 89 | 90 | #[test] 91 | fn filter_bigrams() { 92 | test_it_converts( 93 | "tests/files/rgba_8_should_be_rgba_8.png", 94 | RowFilter::Bigrams, 95 | RGBA, 96 | BitDepth::Eight, 97 | RGBA, 98 | BitDepth::Eight, 99 | ); 100 | } 101 | 102 | #[test] 103 | fn filter_bigent() { 104 | test_it_converts( 105 | "tests/files/grayscale_8_should_be_grayscale_8.png", 106 | RowFilter::BigEnt, 107 | GRAYSCALE, 108 | BitDepth::Eight, 109 | GRAYSCALE, 110 | BitDepth::Eight, 111 | ); 112 | } 113 | 114 | #[test] 115 | fn filter_brute() { 116 | test_it_converts( 117 | "tests/files/palette_8_should_be_palette_8.png", 118 | RowFilter::Brute, 119 | INDEXED, 120 | BitDepth::Eight, 121 | INDEXED, 122 | BitDepth::Eight, 123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /xtask/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys", 52 | ] 53 | 54 | [[package]] 55 | name = "clap" 56 | version = "4.5.21" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" 59 | dependencies = [ 60 | "clap_builder", 61 | ] 62 | 63 | [[package]] 64 | name = "clap_builder" 65 | version = "4.5.21" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" 68 | dependencies = [ 69 | "anstream", 70 | "anstyle", 71 | "clap_lex", 72 | "strsim", 73 | ] 74 | 75 | [[package]] 76 | name = "clap_lex" 77 | version = "0.7.3" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" 80 | 81 | [[package]] 82 | name = "clap_mangen" 83 | version = "0.2.24" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf" 86 | dependencies = [ 87 | "clap", 88 | "roff", 89 | ] 90 | 91 | [[package]] 92 | name = "colorchoice" 93 | version = "1.0.3" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 96 | 97 | [[package]] 98 | name = "is_terminal_polyfill" 99 | version = "1.70.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 102 | 103 | [[package]] 104 | name = "roff" 105 | version = "0.2.2" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" 108 | 109 | [[package]] 110 | name = "strsim" 111 | version = "0.11.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 114 | 115 | [[package]] 116 | name = "utf8parse" 117 | version = "0.2.2" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 120 | 121 | [[package]] 122 | name = "windows-sys" 123 | version = "0.59.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 126 | dependencies = [ 127 | "windows-targets", 128 | ] 129 | 130 | [[package]] 131 | name = "windows-targets" 132 | version = "0.52.6" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 135 | dependencies = [ 136 | "windows_aarch64_gnullvm", 137 | "windows_aarch64_msvc", 138 | "windows_i686_gnu", 139 | "windows_i686_gnullvm", 140 | "windows_i686_msvc", 141 | "windows_x86_64_gnu", 142 | "windows_x86_64_gnullvm", 143 | "windows_x86_64_msvc", 144 | ] 145 | 146 | [[package]] 147 | name = "windows_aarch64_gnullvm" 148 | version = "0.52.6" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 151 | 152 | [[package]] 153 | name = "windows_aarch64_msvc" 154 | version = "0.52.6" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 157 | 158 | [[package]] 159 | name = "windows_i686_gnu" 160 | version = "0.52.6" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 163 | 164 | [[package]] 165 | name = "windows_i686_gnullvm" 166 | version = "0.52.6" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 169 | 170 | [[package]] 171 | name = "windows_i686_msvc" 172 | version = "0.52.6" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 175 | 176 | [[package]] 177 | name = "windows_x86_64_gnu" 178 | version = "0.52.6" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 181 | 182 | [[package]] 183 | name = "windows_x86_64_gnullvm" 184 | version = "0.52.6" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 187 | 188 | [[package]] 189 | name = "windows_x86_64_msvc" 190 | version = "0.52.6" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 193 | 194 | [[package]] 195 | name = "xtask" 196 | version = "0.1.0" 197 | dependencies = [ 198 | "clap", 199 | "clap_mangen", 200 | ] 201 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | description = "xtasks for the Oxipng project: https://github.com/matklad/cargo-xtask" 4 | version = "0.1.0" 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | clap = "4.5.21" 10 | clap_mangen = "0.2.24" 11 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, error::Error, fs, fs::File, io::BufWriter}; 2 | 3 | use clap_mangen::Man; 4 | 5 | include!("../../src/cli.rs"); 6 | 7 | fn main() -> Result<(), Box> { 8 | match &*env::args().nth(1).ok_or("No xtask to run provided")? { 9 | "mangen" => build_manpages(), 10 | _ => Err("Unknown xtask".into()), 11 | } 12 | } 13 | 14 | fn build_manpages() -> Result<(), Box> { 15 | // Put manpages in /target/xtask/mangen/manpages. Our working directory is 16 | // expected to be the root of the repository due to the xtask invocation alias 17 | let manpages_dir = env::current_dir()?.join("target/xtask/mangen/manpages"); 18 | fs::create_dir_all(&manpages_dir)?; 19 | 20 | let mut man_file = BufWriter::new(File::create(manpages_dir.join("oxipng.1"))?); 21 | Man::new(build_command()).render(&mut man_file)?; 22 | 23 | println!("Manpages generated in {}", manpages_dir.display()); 24 | 25 | Ok(()) 26 | } 27 | --------------------------------------------------------------------------------