From 241ba344a70d7bc7df9ae8d96691497ddd59ba3b Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:28:34 +0700 Subject: [PATCH 1/6] chore(repo): move the CLI into an npm workspace --- .dockerignore | 13 +- .githooks/pre-commit | 2 +- .githooks/pre-push | 2 +- .github/workflows/ci-nodejs.yml | 4 +- .github/workflows/publish-npm.yml | 4 +- Dockerfile | 2 +- README.md | 183 +---------------- package-lock.json | 61 +++--- package.json | 68 +------ .../optimizt/.optimiztrc.cjs | 0 .../optimizt/CHANGELOG.md | 0 packages/optimizt/LICENSE | 21 ++ .../optimizt/MIGRATION.md | 0 packages/optimizt/README.md | 187 ++++++++++++++++++ cli.js => packages/optimizt/cli.js | 0 convert.js => packages/optimizt/convert.js | 0 .../optimizt/eslint.config.js | 0 index.js => packages/optimizt/index.js | 0 .../optimizt/jsconfig.json | 0 .../optimizt/lib}/atomic-write.js | 0 .../optimizt/lib}/calculate-ratio.js | 0 {lib => packages/optimizt/lib}/colorize.js | 0 {lib => packages/optimizt/lib}/constants.js | 0 .../lib}/create-progress-bar-container.js | 0 .../optimizt/lib}/describe-codec-failure.js | 0 .../optimizt/lib}/find-config-file-path.js | 0 .../optimizt/lib}/format-bytes.js | 0 {lib => packages/optimizt/lib}/get-plural.js | 0 .../optimizt/lib}/get-relative-path.js | 0 {lib => packages/optimizt/lib}/lifecycle.js | 0 {lib => packages/optimizt/lib}/log.js | 0 .../optimizt/lib}/options-to-arguments.js | 0 .../optimizt/lib}/outcome-status.js | 0 .../optimizt/lib}/parse-image-metadata.js | 0 .../optimizt/lib}/prepare-operation-plan.js | 0 .../optimizt/lib}/program-options.js | 0 {lib => packages/optimizt/lib}/show-total.js | 0 .../optimizt/lib}/stream-capabilities.js | 0 optimize.js => packages/optimizt/optimize.js | 0 packages/optimizt/package.json | 69 +++++++ .../optimizt/tests}/calculate-ratio.test.js | 0 .../optimizt/tests}/colorize.test.js | 0 .../create-progress-bar-container.test.js | 0 .../optimizt/tests}/filesystem-safety.test.js | 0 .../optimizt/tests}/format-bytes.test.js | 0 .../optimizt/tests}/formats.test.js | 0 .../optimizt/tests}/get-plural.test.js | 0 .../optimizt/tests}/helpers/cli.js | 0 .../optimizt/tests}/helpers/platform.js | 0 .../tests}/images/gif-not-optimized.gif | Bin .../tests}/images/jpeg-low-quality.jpg | Bin .../tests}/images/jpeg-not-optimized.jpeg | Bin .../optimizt/tests}/images/jpeg-one-pixel.jpg | Bin .../tests}/images/png-not-optimized.png | Bin .../subdirectory/file-in-subdirectory.jpg | Bin .../tests}/images/svg-not-optimized.svg | 0 .../optimizt/tests}/images/svg-optimized.svg | 0 .../optimizt/tests}/interruption.test.js | 0 .../optimizt/tests}/lifecycle.test.js | 0 .../optimizt/tests}/log.test.js | 0 .../tests}/operation-planning.test.js | 0 .../tests}/parse-image-metadata.test.js | 0 .../optimizt/tests}/process-contract.test.js | 0 .../optimizt/tests}/show-total.test.js | 0 .../optimizt/vitest.config.js | 0 65 files changed, 338 insertions(+), 278 deletions(-) rename .optimiztrc.cjs => packages/optimizt/.optimiztrc.cjs (100%) rename CHANGELOG.md => packages/optimizt/CHANGELOG.md (100%) create mode 100644 packages/optimizt/LICENSE rename MIGRATION.md => packages/optimizt/MIGRATION.md (100%) create mode 100644 packages/optimizt/README.md rename cli.js => packages/optimizt/cli.js (100%) rename convert.js => packages/optimizt/convert.js (100%) rename eslint.config.js => packages/optimizt/eslint.config.js (100%) rename index.js => packages/optimizt/index.js (100%) rename jsconfig.json => packages/optimizt/jsconfig.json (100%) rename {lib => packages/optimizt/lib}/atomic-write.js (100%) rename {lib => packages/optimizt/lib}/calculate-ratio.js (100%) rename {lib => packages/optimizt/lib}/colorize.js (100%) rename {lib => packages/optimizt/lib}/constants.js (100%) rename {lib => packages/optimizt/lib}/create-progress-bar-container.js (100%) rename {lib => packages/optimizt/lib}/describe-codec-failure.js (100%) rename {lib => packages/optimizt/lib}/find-config-file-path.js (100%) rename {lib => packages/optimizt/lib}/format-bytes.js (100%) rename {lib => packages/optimizt/lib}/get-plural.js (100%) rename {lib => packages/optimizt/lib}/get-relative-path.js (100%) rename {lib => packages/optimizt/lib}/lifecycle.js (100%) rename {lib => packages/optimizt/lib}/log.js (100%) rename {lib => packages/optimizt/lib}/options-to-arguments.js (100%) rename {lib => packages/optimizt/lib}/outcome-status.js (100%) rename {lib => packages/optimizt/lib}/parse-image-metadata.js (100%) rename {lib => packages/optimizt/lib}/prepare-operation-plan.js (100%) rename {lib => packages/optimizt/lib}/program-options.js (100%) rename {lib => packages/optimizt/lib}/show-total.js (100%) rename {lib => packages/optimizt/lib}/stream-capabilities.js (100%) rename optimize.js => packages/optimizt/optimize.js (100%) create mode 100644 packages/optimizt/package.json rename {tests => packages/optimizt/tests}/calculate-ratio.test.js (100%) rename {tests => packages/optimizt/tests}/colorize.test.js (100%) rename {tests => packages/optimizt/tests}/create-progress-bar-container.test.js (100%) rename {tests => packages/optimizt/tests}/filesystem-safety.test.js (100%) rename {tests => packages/optimizt/tests}/format-bytes.test.js (100%) rename {tests => packages/optimizt/tests}/formats.test.js (100%) rename {tests => packages/optimizt/tests}/get-plural.test.js (100%) rename {tests => packages/optimizt/tests}/helpers/cli.js (100%) rename {tests => packages/optimizt/tests}/helpers/platform.js (100%) rename {tests => packages/optimizt/tests}/images/gif-not-optimized.gif (100%) rename {tests => packages/optimizt/tests}/images/jpeg-low-quality.jpg (100%) rename {tests => packages/optimizt/tests}/images/jpeg-not-optimized.jpeg (100%) rename {tests => packages/optimizt/tests}/images/jpeg-one-pixel.jpg (100%) rename {tests => packages/optimizt/tests}/images/png-not-optimized.png (100%) rename {tests => packages/optimizt/tests}/images/subdirectory/file-in-subdirectory.jpg (100%) rename {tests => packages/optimizt/tests}/images/svg-not-optimized.svg (100%) rename {tests => packages/optimizt/tests}/images/svg-optimized.svg (100%) rename {tests => packages/optimizt/tests}/interruption.test.js (100%) rename {tests => packages/optimizt/tests}/lifecycle.test.js (100%) rename {tests => packages/optimizt/tests}/log.test.js (100%) rename {tests => packages/optimizt/tests}/operation-planning.test.js (100%) rename {tests => packages/optimizt/tests}/parse-image-metadata.test.js (100%) rename {tests => packages/optimizt/tests}/process-contract.test.js (100%) rename {tests => packages/optimizt/tests}/show-total.test.js (100%) rename vitest.config.js => packages/optimizt/vitest.config.js (100%) diff --git a/.dockerignore b/.dockerignore index 6d54d6c..7a116ad 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,6 @@ * -!lib/ -!.optimiztrc.cjs -!cli.js -!convert.js -!index.js -!LICENSE -!optimize.js -!package*.json +!package.json +!package-lock.json +!packages/ +!packages/optimizt/ +!packages/optimizt/** diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 3b608ae..2aba984 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,3 +1,3 @@ #!/bin/sh -npx lint-staged +npx lint-staged --cwd packages/optimizt diff --git a/.githooks/pre-push b/.githooks/pre-push index b1aa1fc..118d159 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,3 +1,3 @@ #!/bin/sh -npm test && npm run license-check +npm run check diff --git a/.github/workflows/ci-nodejs.yml b/.github/workflows/ci-nodejs.yml index a0d8290..4bbc278 100644 --- a/.github/workflows/ci-nodejs.yml +++ b/.github/workflows/ci-nodejs.yml @@ -34,5 +34,5 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm ci - - name: Run tests - run: npm test + - name: Validate packages + run: npm run check diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 6db3f28..0da4b9f 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -26,7 +26,7 @@ jobs: id: check run: | LATEST_VERSION=$(npm view . version) - CURRENT_VERSION=$(node -p "require('./package.json').version") + CURRENT_VERSION=$(node -p "require('./packages/optimizt/package.json').version") if [ "$LATEST_VERSION" != "$CURRENT_VERSION" ]; then echo "Version changed from $LATEST_VERSION to $CURRENT_VERSION" echo "VERSION_CHANGED=true" >> "$GITHUB_OUTPUT" @@ -39,4 +39,4 @@ jobs: run: npm ci - name: Publish to npm if: ${{ steps.check.outputs.VERSION_CHANGED == 'true' }} - run: npm publish --access public + run: npm publish --workspace @343dev/optimizt --access public diff --git a/Dockerfile b/Dockerfile index 12e7ebb..1ac797c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ COPY . . ENV NODE_ENV="production" RUN npm ci \ - && npm link \ + && npm link --workspace @343dev/optimizt \ && npm cache clean --force WORKDIR /src diff --git a/README.md b/README.md index 871e9bb..2e18da2 100644 --- a/README.md +++ b/README.md @@ -1,185 +1,12 @@ -# @343dev/optimizt +# Optimizt monorepo -Optimizt logo: OK hand sign with Mona Lisa image between the fingers - -[![NPM Downloads](https://img.shields.io/npm/dw/%40343dev%2Foptimizt)](https://www.npmjs.com/package/@343dev/optimizt) -[![npm](https://img.shields.io/npm/v/@343dev/optimizt.svg)](https://www.npmjs.com/package/@343dev/optimizt) -[![Docker](https://img.shields.io/docker/v/343dev/optimizt?label=Docker)](https://hub.docker.com/r/343dev/optimizt) - -Optimizt is a command-line tool for compressing images and converting them to AVIF and WebP. - -It compresses PNG, JPEG, GIF, and SVG files. You can process individual files or entire directories, including their subdirectories. - -## Why Optimizt? - -I built Optimizt while working on images for the web. I kept switching between tools to compress images and convert formats. I wanted one command-line tool for both. - -## Usage - -Install Optimizt: - -```sh -npm install -g @343dev/optimizt -``` - -Optimize an image in place: - -```sh -optimizt path/to/picture.jpg -``` - -This command can replace the original file. To keep it, use `--output` with a separate, existing directory. - -## Command-line options - -| Option | Description | -| --- | --- | -| `--avif` | Create AVIF versions of images. | -| `--webp` | Create WebP versions of images. | -| `-f, --force` | Replace existing AVIF and WebP versions. | -| `-l, --lossless` | Use the lossless profile. JPEG compression is still lossy. | -| `-v, --verbose` | Show detailed output, including skipped files. | -| `-c, --config ` | Use a custom configuration file instead of the bundled defaults. | -| `-o, --output ` | Write results to an existing directory. | -| `-p, --prefix ` | Add a prefix to output file names. | -| `-s, --suffix ` | Add a suffix to output file names. | -| `-V, --version` | Show the version. | -| `-h, --help` | Show help. | - -## Examples - -```bash -# optimize a single image -optimizt path/to/picture.jpg - -# optimize multiple images with the lossless profile -optimizt --lossless path/to/picture.jpg path/to/another/picture.png - -# recursively create AVIF and WebP versions for all images in a directory -optimizt --avif --webp path/to/directory - -# recursively optimize JPEG files in the current directory -find . -iname \*.jpg -exec optimizt {} + -``` - -## Lossy and lossless modes - -### Lossy (default) - -Optimizt uses lossy compression to reduce file size, with some loss of image quality. SVG settings are the same in both modes. - -### Lossless (`--lossless`) - -With the bundled settings: - -- **AVIF/WebP/PNG/GIF**: Uses lossless compression. -- **JPEG**: Uses [Guetzli](https://github.com/google/guetzli) for higher-quality lossy compression. Despite the profile name, JPEG compression is not lossless, and repeated optimization may reduce quality. -- **SVG**: Uses the same settings as the default mode. - -Files may be larger than those produced by the lossy profile. - -## How files are written - -Optimizt writes each result to a temporary file in the destination directory and synchronizes it to disk. It then replaces the destination file in one step, so other programs see either the old file or the complete new file, never a partially written image. - -Optimizt does not restore files it has already replaced if the run fails or stops. - -- When replacing a file, Optimizt keeps its permissions. It also keeps the file's owner if the operating system allows it. -- Optimizt does not replace files with multiple hard links, because the other links would still point to the old file. Use a separate output path instead. -- When optimizing a symbolic link in place, Optimizt replaces the file it points to and keeps the link itself. -- When converting an image through a symbolic link, Optimizt writes the AVIF or WebP version next to the link. Use `--output` to write it to a different directory. - -> [!NOTE] -> Atomic replacement protects against partially written files. It does not guarantee that the directory entry itself survives sudden power loss, and it does not preserve file timestamps. - -## Configuration - -Optimizt uses: - -- [sharp](https://github.com/lovell/sharp) for [JPEG](https://sharp.pixelplumbing.com/api-output#jpeg), [PNG](https://sharp.pixelplumbing.com/api-output#png), [WebP](https://sharp.pixelplumbing.com/api-output#webp), and [AVIF](https://sharp.pixelplumbing.com/api-output#avif). -- [svgo](https://github.com/svg/svgo) for SVG. -- [gifsicle](https://github.com/kohler/gifsicle) for GIF. - -For JPEG in the lossless profile, Optimizt uses [Guetzli](https://github.com/google/guetzli) instead of sharp for the final compression step. - -See [.optimiztrc.cjs](./.optimiztrc.cjs) for the default settings. For available options and accepted values, check the documentation for each image processor linked above. - -Use `--config path/to/.optimiztrc.cjs` to load your own settings instead of the bundled configuration. Without `--config`, Optimizt looks for `.optimiztrc.cjs` in the current directory, then in each parent directory. It uses the first file it finds, or the bundled defaults if it finds none. - -> [!WARNING] -> `.optimiztrc.cjs` is executable code and runs with your permissions. This applies whether Optimizt finds the file automatically or you select it with `--config`. Use Optimizt only in repositories you trust. +This repository contains the [`@343dev/optimizt`](packages/optimizt) command-line image optimizer as an npm workspace. ## Development -After cloning the repository, enable Git hooks once: - ```sh -npm run enable-git-hooks +npm ci +npm run check ``` -This tells Git to use the hooks stored in the [.githooks](./.githooks) directory. - -## Docker - -### Pre-built image - -```bash -# pull the latest image -docker pull 343dev/optimizt - -# pull a specific version -docker pull 343dev/optimizt:9.0.2 -``` - -### Build the image - -```bash -# clone the repository -git clone https://github.com/343dev/optimizt.git -cd optimizt - -# build the image -docker build --tag 343dev/optimizt . -``` - -Alternatively: - -```bash -# build directly from GitHub -# ignores .dockerignore (see: https://github.com/docker/cli/issues/2827) -docker build --tag 343dev/optimizt https://github.com/343dev/optimizt.git -``` - -### Run the container - -```bash -# mount the current directory at /src in the container -docker run --rm --user "$(id -u):$(id -g)" --volume "$(pwd):/src" 343dev/optimizt --webp ./image.png -``` - -## Integrations - -Use these guides to run Optimizt from your editor or GitHub Actions: - -- [JetBrains IDEs](./docs/jetbrains.md) -- [Visual Studio Code](./docs/vscode.md) -- [Sublime Text 3](./docs/sublime-text.md) -- [GitHub Actions](./docs/github.md) - -## Articles - -- [anuwong.com](https://anuwong.com/blog/2023-08-21-save-tons-of-gbs-with-optimizt/). An article in Thai about compressing files before uploading them. -- [Linux Format, Issue 277 (July 2021)](https://www.linuxformat.com/archives?issue=277#:~:text=Kitchen%20Tales%2C%20zFRAG%2C-,Optimizt,-and%20SingleFileZ.). An article about reducing image file sizes with Optimizt. - -## Credits - -[Igor Garybaldi](http://pandabanda.com/) created the logo. - -## Other projects - -- [harold](https://github.com/343dev/harold) compares frontend project bundle sizes between snapshots from the command line. -- [jailbot](https://github.com/343dev/jailbot) wraps Docker containers and automatically mounts filesystem paths. -- [markdown-lint](https://github.com/343dev/markdown-lint) checks Markdown style with Prettier, Remark, and Typograf. -- [languagetool-node](https://github.com/343dev/languagetool-node) checks spelling and grammar from the command line with LanguageTool. +Only `@343dev/optimizt` is published. diff --git a/package-lock.json b/package-lock.json index 62bbcfd..24f6910 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,33 +1,14 @@ { - "name": "@343dev/optimizt", + "name": "@343dev/optimizt-monorepo", "version": "13.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@343dev/optimizt", - "version": "13.0.0", - "license": "MIT", - "dependencies": { - "@343dev/gifsicle": "1.2.0", - "@343dev/guetzli": "1.3.0", - "cli-progress": "3.12.0", - "commander": "15.0.0", - "p-limit": "7.3.0", - "sharp": "0.35.2", - "svgo": "4.0.1", - "write-file-atomic": "7.0.1" - }, - "bin": { - "optimizt": "cli.js" - }, - "devDependencies": { - "@343dev/eslint-config": "5.0.0", - "@vitest/coverage-v8": "4.1.9", - "eslint": "10.5.0", - "lint-staged": "17.0.7", - "vitest": "4.1.9" - }, + "name": "@343dev/optimizt-monorepo", + "workspaces": [ + "packages/*" + ], "engines": { "node": ">=22.22.1" } @@ -79,6 +60,10 @@ "node": ">=18.17" } }, + "node_modules/@343dev/optimizt": { + "resolved": "packages/optimizt", + "link": true + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -4496,6 +4481,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "packages/optimizt": { + "name": "@343dev/optimizt", + "version": "13.0.0", + "license": "MIT", + "dependencies": { + "@343dev/gifsicle": "1.2.0", + "@343dev/guetzli": "1.3.0", + "cli-progress": "3.12.0", + "commander": "15.0.0", + "p-limit": "7.3.0", + "sharp": "0.35.2", + "svgo": "4.0.1", + "write-file-atomic": "7.0.1" + }, + "bin": { + "optimizt": "cli.js" + }, + "devDependencies": { + "@343dev/eslint-config": "5.0.0", + "@vitest/coverage-v8": "4.1.9", + "eslint": "10.5.0", + "lint-staged": "17.0.7", + "vitest": "4.1.9" + }, + "engines": { + "node": ">=22.22.1" + } } } } diff --git a/package.json b/package.json index 436aa1f..e6f2fe1 100644 --- a/package.json +++ b/package.json @@ -1,71 +1,17 @@ { - "name": "@343dev/optimizt", - "version": "13.0.0", - "description": "Optimize images in place or create AVIF and WebP variants", - "keywords": [ - "svg", - "png", - "jpeg", - "gif", - "webp", - "avif", - "image-compression", - "sharp", - "svgo", - "gifsicle", - "guetzli" - ], - "license": "MIT", - "author": "Andrey Warkentin (https://github.com/343dev)", - "repository": "343dev/optimizt", - "homepage": "https://github.com/343dev/optimizt#readme", - "bugs": { - "url": "https://github.com/343dev/optimizt/issues" - }, - "exports": {}, - "bin": { - "optimizt": "cli.js" - }, - "files": [ - "lib/", - ".optimiztrc.cjs", - "cli.js", - "convert.js", - "index.js", - "MIGRATION.md", - "optimize.js" + "name": "@343dev/optimizt-monorepo", + "private": true, + "workspaces": [ + "packages/*" ], "scripts": { + "check": "npm run lint && npm test && npm run license-check", + "lint": "npm run lint --workspaces --if-present", + "test": "npm test --workspaces --if-present", "license-check": "npx license-compliance@3.0.1 --report detailed --allow '0BSD;Apache-2.0;Artistic-2.0;BlueOak-1.0.0;BSD-2-Clause;BSD-3-Clause;BSD-4-Clause;CC0-1.0;CC-BY-4.0;ISC;LGPL-2.1-or-later;LGPL-3.0-or-later;MIT;MPL-1.1;MPL-2.0;Python-2.0;Unlicense;W3C;W3C-20150513;X11;Zlib' --exclude 'spdx-exceptions;@343dev/gifsicle'", - "lint": "eslint ./", - "test": "vitest run", - "test-coverage": "vitest run --coverage", "enable-git-hooks": "git config core.hooksPath .githooks" }, - "dependencies": { - "@343dev/gifsicle": "1.2.0", - "@343dev/guetzli": "1.3.0", - "cli-progress": "3.12.0", - "commander": "15.0.0", - "p-limit": "7.3.0", - "sharp": "0.35.2", - "svgo": "4.0.1", - "write-file-atomic": "7.0.1" - }, - "devDependencies": { - "@343dev/eslint-config": "5.0.0", - "@vitest/coverage-v8": "4.1.9", - "eslint": "10.5.0", - "lint-staged": "17.0.7", - "vitest": "4.1.9" - }, "engines": { "node": ">=22.22.1" - }, - "type": "module", - "lint-staged": { - "*.js": [ - "eslint --fix" - ] } } diff --git a/.optimiztrc.cjs b/packages/optimizt/.optimiztrc.cjs similarity index 100% rename from .optimiztrc.cjs rename to packages/optimizt/.optimiztrc.cjs diff --git a/CHANGELOG.md b/packages/optimizt/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/optimizt/CHANGELOG.md diff --git a/packages/optimizt/LICENSE b/packages/optimizt/LICENSE new file mode 100644 index 0000000..e44c0af --- /dev/null +++ b/packages/optimizt/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Andrey Warkentin and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MIGRATION.md b/packages/optimizt/MIGRATION.md similarity index 100% rename from MIGRATION.md rename to packages/optimizt/MIGRATION.md diff --git a/packages/optimizt/README.md b/packages/optimizt/README.md new file mode 100644 index 0000000..4c483fd --- /dev/null +++ b/packages/optimizt/README.md @@ -0,0 +1,187 @@ +# @343dev/optimizt + +> This package lives in the `packages/optimizt` workspace of the Optimizt monorepo. + +Optimizt logo: OK hand sign with Mona Lisa image between the fingers + +[![NPM Downloads](https://img.shields.io/npm/dw/%40343dev%2Foptimizt)](https://www.npmjs.com/package/@343dev/optimizt) +[![npm](https://img.shields.io/npm/v/@343dev/optimizt.svg)](https://www.npmjs.com/package/@343dev/optimizt) +[![Docker](https://img.shields.io/docker/v/343dev/optimizt?label=Docker)](https://hub.docker.com/r/343dev/optimizt) + +Optimizt is a command-line tool for compressing images and converting them to AVIF and WebP. + +It compresses PNG, JPEG, GIF, and SVG files. You can process individual files or entire directories, including their subdirectories. + +## Why Optimizt? + +I built Optimizt while working on images for the web. I kept switching between tools to compress images and convert formats. I wanted one command-line tool for both. + +## Usage + +Install Optimizt: + +```sh +npm install -g @343dev/optimizt +``` + +Optimize an image in place: + +```sh +optimizt path/to/picture.jpg +``` + +This command can replace the original file. To keep it, use `--output` with a separate, existing directory. + +## Command-line options + +| Option | Description | +| --- | --- | +| `--avif` | Create AVIF versions of images. | +| `--webp` | Create WebP versions of images. | +| `-f, --force` | Replace existing AVIF and WebP versions. | +| `-l, --lossless` | Use the lossless profile. JPEG compression is still lossy. | +| `-v, --verbose` | Show detailed output, including skipped files. | +| `-c, --config ` | Use a custom configuration file instead of the bundled defaults. | +| `-o, --output ` | Write results to an existing directory. | +| `-p, --prefix ` | Add a prefix to output file names. | +| `-s, --suffix ` | Add a suffix to output file names. | +| `-V, --version` | Show the version. | +| `-h, --help` | Show help. | + +## Examples + +```bash +# optimize a single image +optimizt path/to/picture.jpg + +# optimize multiple images with the lossless profile +optimizt --lossless path/to/picture.jpg path/to/another/picture.png + +# recursively create AVIF and WebP versions for all images in a directory +optimizt --avif --webp path/to/directory + +# recursively optimize JPEG files in the current directory +find . -iname \*.jpg -exec optimizt {} + +``` + +## Lossy and lossless modes + +### Lossy (default) + +Optimizt uses lossy compression to reduce file size, with some loss of image quality. SVG settings are the same in both modes. + +### Lossless (`--lossless`) + +With the bundled settings: + +- **AVIF/WebP/PNG/GIF**: Uses lossless compression. +- **JPEG**: Uses [Guetzli](https://github.com/google/guetzli) for higher-quality lossy compression. Despite the profile name, JPEG compression is not lossless, and repeated optimization may reduce quality. +- **SVG**: Uses the same settings as the default mode. + +Files may be larger than those produced by the lossy profile. + +## How files are written + +Optimizt writes each result to a temporary file in the destination directory and synchronizes it to disk. It then replaces the destination file in one step, so other programs see either the old file or the complete new file, never a partially written image. + +Optimizt does not restore files it has already replaced if the run fails or stops. + +- When replacing a file, Optimizt keeps its permissions. It also keeps the file's owner if the operating system allows it. +- Optimizt does not replace files with multiple hard links, because the other links would still point to the old file. Use a separate output path instead. +- When optimizing a symbolic link in place, Optimizt replaces the file it points to and keeps the link itself. +- When converting an image through a symbolic link, Optimizt writes the AVIF or WebP version next to the link. Use `--output` to write it to a different directory. + +> [!NOTE] +> Atomic replacement protects against partially written files. It does not guarantee that the directory entry itself survives sudden power loss, and it does not preserve file timestamps. + +## Configuration + +Optimizt uses: + +- [sharp](https://github.com/lovell/sharp) for [JPEG](https://sharp.pixelplumbing.com/api-output#jpeg), [PNG](https://sharp.pixelplumbing.com/api-output#png), [WebP](https://sharp.pixelplumbing.com/api-output#webp), and [AVIF](https://sharp.pixelplumbing.com/api-output#avif). +- [svgo](https://github.com/svg/svgo) for SVG. +- [gifsicle](https://github.com/kohler/gifsicle) for GIF. + +For JPEG in the lossless profile, Optimizt uses [Guetzli](https://github.com/google/guetzli) instead of sharp for the final compression step. + +See [.optimiztrc.cjs](./.optimiztrc.cjs) for the default settings. For available options and accepted values, check the documentation for each image processor linked above. + +Use `--config path/to/.optimiztrc.cjs` to load your own settings instead of the bundled configuration. Without `--config`, Optimizt looks for `.optimiztrc.cjs` in the current directory, then in each parent directory. It uses the first file it finds, or the bundled defaults if it finds none. + +> [!WARNING] +> `.optimiztrc.cjs` is executable code and runs with your permissions. This applies whether Optimizt finds the file automatically or you select it with `--config`. Use Optimizt only in repositories you trust. + +## Development + +After cloning the repository, enable Git hooks once: + +```sh +npm run enable-git-hooks +``` + +This tells Git to use the hooks stored in the [.githooks](./.githooks) directory. + +## Docker + +### Pre-built image + +```bash +# pull the latest image +docker pull 343dev/optimizt + +# pull a specific version +docker pull 343dev/optimizt:9.0.2 +``` + +### Build the image + +```bash +# clone the repository +git clone https://github.com/343dev/optimizt.git +cd optimizt + +# build the image +docker build --tag 343dev/optimizt . +``` + +Alternatively: + +```bash +# build directly from GitHub +# ignores .dockerignore (see: https://github.com/docker/cli/issues/2827) +docker build --tag 343dev/optimizt https://github.com/343dev/optimizt.git +``` + +### Run the container + +```bash +# mount the current directory at /src in the container +docker run --rm --user "$(id -u):$(id -g)" --volume "$(pwd):/src" 343dev/optimizt --webp ./image.png +``` + +## Integrations + +Use these guides to run Optimizt from your editor or GitHub Actions: + +- [JetBrains IDEs](https://github.com/343dev/optimizt/blob/main/docs/jetbrains.md) +- [Visual Studio Code](https://github.com/343dev/optimizt/blob/main/docs/vscode.md) +- [Sublime Text 3](https://github.com/343dev/optimizt/blob/main/docs/sublime-text.md) +- [GitHub Actions](https://github.com/343dev/optimizt/blob/main/docs/github.md) + +## Articles + +- [anuwong.com](https://anuwong.com/blog/2023-08-21-save-tons-of-gbs-with-optimizt/). An article in Thai about compressing files before uploading them. +- [Linux Format, Issue 277 (July 2021)](https://www.linuxformat.com/archives?issue=277#:~:text=Kitchen%20Tales%2C%20zFRAG%2C-,Optimizt,-and%20SingleFileZ.). An article about reducing image file sizes with Optimizt. + +## Credits + +[Igor Garybaldi](http://pandabanda.com/) created the logo. + +## Other projects + +- [harold](https://github.com/343dev/harold) compares frontend project bundle sizes between snapshots from the command line. +- [jailbot](https://github.com/343dev/jailbot) wraps Docker containers and automatically mounts filesystem paths. +- [markdown-lint](https://github.com/343dev/markdown-lint) checks Markdown style with Prettier, Remark, and Typograf. +- [languagetool-node](https://github.com/343dev/languagetool-node) checks spelling and grammar from the command line with LanguageTool. diff --git a/cli.js b/packages/optimizt/cli.js similarity index 100% rename from cli.js rename to packages/optimizt/cli.js diff --git a/convert.js b/packages/optimizt/convert.js similarity index 100% rename from convert.js rename to packages/optimizt/convert.js diff --git a/eslint.config.js b/packages/optimizt/eslint.config.js similarity index 100% rename from eslint.config.js rename to packages/optimizt/eslint.config.js diff --git a/index.js b/packages/optimizt/index.js similarity index 100% rename from index.js rename to packages/optimizt/index.js diff --git a/jsconfig.json b/packages/optimizt/jsconfig.json similarity index 100% rename from jsconfig.json rename to packages/optimizt/jsconfig.json diff --git a/lib/atomic-write.js b/packages/optimizt/lib/atomic-write.js similarity index 100% rename from lib/atomic-write.js rename to packages/optimizt/lib/atomic-write.js diff --git a/lib/calculate-ratio.js b/packages/optimizt/lib/calculate-ratio.js similarity index 100% rename from lib/calculate-ratio.js rename to packages/optimizt/lib/calculate-ratio.js diff --git a/lib/colorize.js b/packages/optimizt/lib/colorize.js similarity index 100% rename from lib/colorize.js rename to packages/optimizt/lib/colorize.js diff --git a/lib/constants.js b/packages/optimizt/lib/constants.js similarity index 100% rename from lib/constants.js rename to packages/optimizt/lib/constants.js diff --git a/lib/create-progress-bar-container.js b/packages/optimizt/lib/create-progress-bar-container.js similarity index 100% rename from lib/create-progress-bar-container.js rename to packages/optimizt/lib/create-progress-bar-container.js diff --git a/lib/describe-codec-failure.js b/packages/optimizt/lib/describe-codec-failure.js similarity index 100% rename from lib/describe-codec-failure.js rename to packages/optimizt/lib/describe-codec-failure.js diff --git a/lib/find-config-file-path.js b/packages/optimizt/lib/find-config-file-path.js similarity index 100% rename from lib/find-config-file-path.js rename to packages/optimizt/lib/find-config-file-path.js diff --git a/lib/format-bytes.js b/packages/optimizt/lib/format-bytes.js similarity index 100% rename from lib/format-bytes.js rename to packages/optimizt/lib/format-bytes.js diff --git a/lib/get-plural.js b/packages/optimizt/lib/get-plural.js similarity index 100% rename from lib/get-plural.js rename to packages/optimizt/lib/get-plural.js diff --git a/lib/get-relative-path.js b/packages/optimizt/lib/get-relative-path.js similarity index 100% rename from lib/get-relative-path.js rename to packages/optimizt/lib/get-relative-path.js diff --git a/lib/lifecycle.js b/packages/optimizt/lib/lifecycle.js similarity index 100% rename from lib/lifecycle.js rename to packages/optimizt/lib/lifecycle.js diff --git a/lib/log.js b/packages/optimizt/lib/log.js similarity index 100% rename from lib/log.js rename to packages/optimizt/lib/log.js diff --git a/lib/options-to-arguments.js b/packages/optimizt/lib/options-to-arguments.js similarity index 100% rename from lib/options-to-arguments.js rename to packages/optimizt/lib/options-to-arguments.js diff --git a/lib/outcome-status.js b/packages/optimizt/lib/outcome-status.js similarity index 100% rename from lib/outcome-status.js rename to packages/optimizt/lib/outcome-status.js diff --git a/lib/parse-image-metadata.js b/packages/optimizt/lib/parse-image-metadata.js similarity index 100% rename from lib/parse-image-metadata.js rename to packages/optimizt/lib/parse-image-metadata.js diff --git a/lib/prepare-operation-plan.js b/packages/optimizt/lib/prepare-operation-plan.js similarity index 100% rename from lib/prepare-operation-plan.js rename to packages/optimizt/lib/prepare-operation-plan.js diff --git a/lib/program-options.js b/packages/optimizt/lib/program-options.js similarity index 100% rename from lib/program-options.js rename to packages/optimizt/lib/program-options.js diff --git a/lib/show-total.js b/packages/optimizt/lib/show-total.js similarity index 100% rename from lib/show-total.js rename to packages/optimizt/lib/show-total.js diff --git a/lib/stream-capabilities.js b/packages/optimizt/lib/stream-capabilities.js similarity index 100% rename from lib/stream-capabilities.js rename to packages/optimizt/lib/stream-capabilities.js diff --git a/optimize.js b/packages/optimizt/optimize.js similarity index 100% rename from optimize.js rename to packages/optimizt/optimize.js diff --git a/packages/optimizt/package.json b/packages/optimizt/package.json new file mode 100644 index 0000000..9c8d5fc --- /dev/null +++ b/packages/optimizt/package.json @@ -0,0 +1,69 @@ +{ + "name": "@343dev/optimizt", + "version": "13.0.0", + "description": "Optimize images in place or create AVIF and WebP variants", + "keywords": [ + "svg", + "png", + "jpeg", + "gif", + "webp", + "avif", + "image-compression", + "sharp", + "svgo", + "gifsicle", + "guetzli" + ], + "license": "MIT", + "author": "Andrey Warkentin (https://github.com/343dev)", + "repository": "343dev/optimizt", + "homepage": "https://github.com/343dev/optimizt#readme", + "bugs": { + "url": "https://github.com/343dev/optimizt/issues" + }, + "exports": {}, + "bin": { + "optimizt": "cli.js" + }, + "files": [ + "lib/", + ".optimiztrc.cjs", + "cli.js", + "convert.js", + "index.js", + "MIGRATION.md", + "optimize.js" + ], + "scripts": { + "lint": "eslint ./", + "test": "vitest run", + "test-coverage": "vitest run --coverage" + }, + "dependencies": { + "@343dev/gifsicle": "1.2.0", + "@343dev/guetzli": "1.3.0", + "cli-progress": "3.12.0", + "commander": "15.0.0", + "p-limit": "7.3.0", + "sharp": "0.35.2", + "svgo": "4.0.1", + "write-file-atomic": "7.0.1" + }, + "devDependencies": { + "@343dev/eslint-config": "5.0.0", + "@vitest/coverage-v8": "4.1.9", + "eslint": "10.5.0", + "lint-staged": "17.0.7", + "vitest": "4.1.9" + }, + "engines": { + "node": ">=22.22.1" + }, + "type": "module", + "lint-staged": { + "*.js": [ + "eslint --fix" + ] + } +} diff --git a/tests/calculate-ratio.test.js b/packages/optimizt/tests/calculate-ratio.test.js similarity index 100% rename from tests/calculate-ratio.test.js rename to packages/optimizt/tests/calculate-ratio.test.js diff --git a/tests/colorize.test.js b/packages/optimizt/tests/colorize.test.js similarity index 100% rename from tests/colorize.test.js rename to packages/optimizt/tests/colorize.test.js diff --git a/tests/create-progress-bar-container.test.js b/packages/optimizt/tests/create-progress-bar-container.test.js similarity index 100% rename from tests/create-progress-bar-container.test.js rename to packages/optimizt/tests/create-progress-bar-container.test.js diff --git a/tests/filesystem-safety.test.js b/packages/optimizt/tests/filesystem-safety.test.js similarity index 100% rename from tests/filesystem-safety.test.js rename to packages/optimizt/tests/filesystem-safety.test.js diff --git a/tests/format-bytes.test.js b/packages/optimizt/tests/format-bytes.test.js similarity index 100% rename from tests/format-bytes.test.js rename to packages/optimizt/tests/format-bytes.test.js diff --git a/tests/formats.test.js b/packages/optimizt/tests/formats.test.js similarity index 100% rename from tests/formats.test.js rename to packages/optimizt/tests/formats.test.js diff --git a/tests/get-plural.test.js b/packages/optimizt/tests/get-plural.test.js similarity index 100% rename from tests/get-plural.test.js rename to packages/optimizt/tests/get-plural.test.js diff --git a/tests/helpers/cli.js b/packages/optimizt/tests/helpers/cli.js similarity index 100% rename from tests/helpers/cli.js rename to packages/optimizt/tests/helpers/cli.js diff --git a/tests/helpers/platform.js b/packages/optimizt/tests/helpers/platform.js similarity index 100% rename from tests/helpers/platform.js rename to packages/optimizt/tests/helpers/platform.js diff --git a/tests/images/gif-not-optimized.gif b/packages/optimizt/tests/images/gif-not-optimized.gif similarity index 100% rename from tests/images/gif-not-optimized.gif rename to packages/optimizt/tests/images/gif-not-optimized.gif diff --git a/tests/images/jpeg-low-quality.jpg b/packages/optimizt/tests/images/jpeg-low-quality.jpg similarity index 100% rename from tests/images/jpeg-low-quality.jpg rename to packages/optimizt/tests/images/jpeg-low-quality.jpg diff --git a/tests/images/jpeg-not-optimized.jpeg b/packages/optimizt/tests/images/jpeg-not-optimized.jpeg similarity index 100% rename from tests/images/jpeg-not-optimized.jpeg rename to packages/optimizt/tests/images/jpeg-not-optimized.jpeg diff --git a/tests/images/jpeg-one-pixel.jpg b/packages/optimizt/tests/images/jpeg-one-pixel.jpg similarity index 100% rename from tests/images/jpeg-one-pixel.jpg rename to packages/optimizt/tests/images/jpeg-one-pixel.jpg diff --git a/tests/images/png-not-optimized.png b/packages/optimizt/tests/images/png-not-optimized.png similarity index 100% rename from tests/images/png-not-optimized.png rename to packages/optimizt/tests/images/png-not-optimized.png diff --git a/tests/images/subdirectory/file-in-subdirectory.jpg b/packages/optimizt/tests/images/subdirectory/file-in-subdirectory.jpg similarity index 100% rename from tests/images/subdirectory/file-in-subdirectory.jpg rename to packages/optimizt/tests/images/subdirectory/file-in-subdirectory.jpg diff --git a/tests/images/svg-not-optimized.svg b/packages/optimizt/tests/images/svg-not-optimized.svg similarity index 100% rename from tests/images/svg-not-optimized.svg rename to packages/optimizt/tests/images/svg-not-optimized.svg diff --git a/tests/images/svg-optimized.svg b/packages/optimizt/tests/images/svg-optimized.svg similarity index 100% rename from tests/images/svg-optimized.svg rename to packages/optimizt/tests/images/svg-optimized.svg diff --git a/tests/interruption.test.js b/packages/optimizt/tests/interruption.test.js similarity index 100% rename from tests/interruption.test.js rename to packages/optimizt/tests/interruption.test.js diff --git a/tests/lifecycle.test.js b/packages/optimizt/tests/lifecycle.test.js similarity index 100% rename from tests/lifecycle.test.js rename to packages/optimizt/tests/lifecycle.test.js diff --git a/tests/log.test.js b/packages/optimizt/tests/log.test.js similarity index 100% rename from tests/log.test.js rename to packages/optimizt/tests/log.test.js diff --git a/tests/operation-planning.test.js b/packages/optimizt/tests/operation-planning.test.js similarity index 100% rename from tests/operation-planning.test.js rename to packages/optimizt/tests/operation-planning.test.js diff --git a/tests/parse-image-metadata.test.js b/packages/optimizt/tests/parse-image-metadata.test.js similarity index 100% rename from tests/parse-image-metadata.test.js rename to packages/optimizt/tests/parse-image-metadata.test.js diff --git a/tests/process-contract.test.js b/packages/optimizt/tests/process-contract.test.js similarity index 100% rename from tests/process-contract.test.js rename to packages/optimizt/tests/process-contract.test.js diff --git a/tests/show-total.test.js b/packages/optimizt/tests/show-total.test.js similarity index 100% rename from tests/show-total.test.js rename to packages/optimizt/tests/show-total.test.js diff --git a/vitest.config.js b/packages/optimizt/vitest.config.js similarity index 100% rename from vitest.config.js rename to packages/optimizt/vitest.config.js From 060a5a78aaaf9a3fa933fc7d95d73ad51bca56bf Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:29:28 +0700 Subject: [PATCH 2/6] feat(cli)!: run on a vendored WASM-only Sharp runtime Replace native Sharp installations with a reproducibly generated WebAssembly-only distribution that is vendored into the published CLI package.\n\nBREAKING CHANGE: the package no longer installs platform-specific Sharp binaries. --- .dockerignore | 6 + .githooks/pre-commit | 3 + .gitignore | 6 + Dockerfile | 8 +- README.md | 7 +- package-lock.json | 664 +++--------------- package.json | 7 +- packages/optimizt-sharp/LICENSE | 191 +++++ packages/optimizt-sharp/NOTICE | 7 + packages/optimizt-sharp/README.md | 31 + packages/optimizt-sharp/eslint.config.js | 1 + packages/optimizt-sharp/package.json | 72 ++ packages/optimizt-sharp/scripts/build.mjs | 83 +++ packages/optimizt-sharp/test/package.test.js | 57 ++ packages/optimizt-sharp/vitest.config.js | 8 + packages/optimizt/convert.js | 201 +----- packages/optimizt/eslint.config.js | 1 + packages/optimizt/lib/parse-image-metadata.js | 2 +- packages/optimizt/lib/sharp.js | 1 + packages/optimizt/optimize.js | 300 ++------ packages/optimizt/package.json | 23 +- packages/optimizt/scripts/vendor-sharp.mjs | 41 ++ packages/optimizt/tests/package.test.js | 22 + 23 files changed, 744 insertions(+), 998 deletions(-) create mode 100644 packages/optimizt-sharp/LICENSE create mode 100644 packages/optimizt-sharp/NOTICE create mode 100644 packages/optimizt-sharp/README.md create mode 100644 packages/optimizt-sharp/eslint.config.js create mode 100644 packages/optimizt-sharp/package.json create mode 100644 packages/optimizt-sharp/scripts/build.mjs create mode 100644 packages/optimizt-sharp/test/package.test.js create mode 100644 packages/optimizt-sharp/vitest.config.js create mode 100644 packages/optimizt/lib/sharp.js create mode 100644 packages/optimizt/scripts/vendor-sharp.mjs create mode 100644 packages/optimizt/tests/package.test.js diff --git a/.dockerignore b/.dockerignore index 7a116ad..57118d7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,9 @@ !packages/ !packages/optimizt/ !packages/optimizt/** +!packages/optimizt-sharp/ +!packages/optimizt-sharp/** +packages/optimizt-sharp/dist/ +packages/optimizt/vendor/ +node_modules/ +coverage/ diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 2aba984..d63ee5b 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,3 +1,6 @@ #!/bin/sh +# lint-staged resolves each workspace's eslint.config.js from its own directory, +# so it must run once per workspace rather than from the repository root. npx lint-staged --cwd packages/optimizt +npx lint-staged --cwd packages/optimizt-sharp diff --git a/.gitignore b/.gitignore index b509c88..47ef484 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ coverage/ node_modules/ +.cache/ +.DS_Store +packages/optimizt-sharp/dist/ +packages/optimizt/vendor/ + +artifacts/ diff --git a/Dockerfile b/Dockerfile index 1ac797c..362119a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,12 +5,14 @@ WORKDIR /app COPY . . -ENV NODE_ENV="production" - -RUN npm ci \ +RUN npm ci --include=dev \ + && npm run build \ + && npm prune --omit=dev \ && npm link --workspace @343dev/optimizt \ && npm cache clean --force +ENV NODE_ENV="production" + WORKDIR /src ENTRYPOINT ["optimizt"] diff --git a/README.md b/README.md index 2e18da2..6bcf8ca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Optimizt monorepo -This repository contains the [`@343dev/optimizt`](packages/optimizt) command-line image optimizer as an npm workspace. +This repository contains two workspaces: + +- [`@343dev/optimizt`](packages/optimizt) — the published Optimizt command-line image optimizer. +- [`@343dev/optimizt-sharp`](packages/optimizt-sharp) — a private build workspace that reproducibly generates the WASM-only Sharp distribution vendored into Optimizt. ## Development @@ -9,4 +12,4 @@ npm ci npm run check ``` -Only `@343dev/optimizt` is published. +`npm run build` downloads the exact upstream Sharp tarball pinned by `packages/optimizt-sharp/package.json`, verifies its integrity, generates the wrapper, and copies it into the ignored `packages/optimizt/vendor` directory. Only `@343dev/optimizt` is published. diff --git a/package-lock.json b/package-lock.json index 24f6910..6d44d2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,10 @@ "resolved": "packages/optimizt", "link": true }, + "node_modules/@343dev/optimizt-sharp": { + "resolved": "packages/optimizt-sharp", + "link": true + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -333,459 +337,11 @@ "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", - "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.1" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", - "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.1" - } - }, - "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", - "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "dependencies": { - "@img/sharp-wasm32": "0.35.2" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", - "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", - "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", - "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", - "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", - "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", - "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", - "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", - "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", - "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", - "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", - "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.1" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", - "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.1" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", - "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.1" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", - "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.1" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", - "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", - "cpu": [ - "s390x" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.1" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", - "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.1" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", - "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", - "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.1" - } - }, "node_modules/@img/sharp-wasm32": { "version": "0.35.2", "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, "dependencies": { "@emnapi/runtime": "^1.11.1" }, @@ -801,85 +357,21 @@ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", - "optional": true, "dependencies": { "tslib": "^2.4.0" } }, - "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", - "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0", - "optional": true, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", "dependencies": { - "@img/sharp-wasm32": "0.35.2" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", - "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", - "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.9.0" + "minipass": "^7.0.4" }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", - "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1675,6 +1167,16 @@ "dev": true, "license": "MIT" }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/ci-info": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", @@ -3382,6 +2884,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3794,50 +3319,6 @@ "node": ">=10" } }, - "node_modules/sharp": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", - "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.1.0", - "detect-libc": "^2.1.2", - "semver": "^7.8.4" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.2", - "@img/sharp-darwin-x64": "0.35.2", - "@img/sharp-freebsd-wasm32": "0.35.2", - "@img/sharp-libvips-darwin-arm64": "1.3.1", - "@img/sharp-libvips-darwin-x64": "1.3.1", - "@img/sharp-libvips-linux-arm": "1.3.1", - "@img/sharp-libvips-linux-arm64": "1.3.1", - "@img/sharp-libvips-linux-ppc64": "1.3.1", - "@img/sharp-libvips-linux-riscv64": "1.3.1", - "@img/sharp-libvips-linux-s390x": "1.3.1", - "@img/sharp-libvips-linux-x64": "1.3.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", - "@img/sharp-libvips-linuxmusl-x64": "1.3.1", - "@img/sharp-linux-arm": "0.35.2", - "@img/sharp-linux-arm64": "0.35.2", - "@img/sharp-linux-ppc64": "0.35.2", - "@img/sharp-linux-riscv64": "0.35.2", - "@img/sharp-linux-s390x": "0.35.2", - "@img/sharp-linux-x64": "0.35.2", - "@img/sharp-linuxmusl-arm64": "0.35.2", - "@img/sharp-linuxmusl-x64": "0.35.2", - "@img/sharp-webcontainers-wasm32": "0.35.2", - "@img/sharp-win32-arm64": "0.35.2", - "@img/sharp-win32-ia32": "0.35.2", - "@img/sharp-win32-x64": "0.35.2" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4077,6 +3558,23 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4125,8 +3623,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", @@ -4453,6 +3950,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -4489,10 +3996,13 @@ "dependencies": { "@343dev/gifsicle": "1.2.0", "@343dev/guetzli": "1.3.0", + "@img/colour": "^1.1.0", + "@img/sharp-wasm32": "0.35.2", "cli-progress": "3.12.0", "commander": "15.0.0", + "detect-libc": "^2.1.2", "p-limit": "7.3.0", - "sharp": "0.35.2", + "semver": "^7.8.4", "svgo": "4.0.1", "write-file-atomic": "7.0.1" }, @@ -4506,6 +4016,26 @@ "lint-staged": "17.0.7", "vitest": "4.1.9" }, + "engines": { + "node": ">=24.0.0" + } + }, + "packages/optimizt-sharp": { + "name": "@343dev/optimizt-sharp", + "version": "0.35.2-0", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "@img/sharp-wasm32": "0.35.2", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "devDependencies": { + "@343dev/eslint-config": "5.0.0", + "eslint": "10.5.0", + "tar": "7.5.22", + "vitest": "4.1.9" + }, "engines": { "node": ">=22.22.1" } diff --git a/package.json b/package.json index e6f2fe1..44c79be 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,12 @@ "packages/*" ], "scripts": { - "check": "npm run lint && npm test && npm run license-check", + "check": "npm run build && npm run lint && npm test && npm run license-check", "lint": "npm run lint --workspaces --if-present", "test": "npm test --workspaces --if-present", - "license-check": "npx license-compliance@3.0.1 --report detailed --allow '0BSD;Apache-2.0;Artistic-2.0;BlueOak-1.0.0;BSD-2-Clause;BSD-3-Clause;BSD-4-Clause;CC0-1.0;CC-BY-4.0;ISC;LGPL-2.1-or-later;LGPL-3.0-or-later;MIT;MPL-1.1;MPL-2.0;Python-2.0;Unlicense;W3C;W3C-20150513;X11;Zlib' --exclude 'spdx-exceptions;@343dev/gifsicle'", - "enable-git-hooks": "git config core.hooksPath .githooks" + "license-check": "npx license-compliance@3.0.1 --report detailed --allow '0BSD;Apache-2.0;Artistic-2.0;BlueOak-1.0.0;BSD-2-Clause;BSD-3-Clause;BSD-4-Clause;CC0-1.0;CC-BY-4.0;ISC;LGPL-2.1-or-later;LGPL-3.0-or-later;MIT;MPL-1.1;MPL-2.0;Python-2.0;Unlicense;W3C;W3C-20150513;X11;Zlib' --exclude 'spdx-exceptions'", + "enable-git-hooks": "git config core.hooksPath .githooks", + "build": "npm run build --workspace @343dev/optimizt" }, "engines": { "node": ">=22.22.1" diff --git a/packages/optimizt-sharp/LICENSE b/packages/optimizt-sharp/LICENSE new file mode 100644 index 0000000..37ec93a --- /dev/null +++ b/packages/optimizt-sharp/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/optimizt-sharp/NOTICE b/packages/optimizt-sharp/NOTICE new file mode 100644 index 0000000..547ec3b --- /dev/null +++ b/packages/optimizt-sharp/NOTICE @@ -0,0 +1,7 @@ +This package contains a generated distribution derived from sharp 0.35.2. + +sharp copyright 2013 Lovell Fuller and others. +https://github.com/lovell/sharp + +The derived sharp JavaScript files are licensed under Apache License 2.0. +The @img/sharp-wasm32 runtime contains third-party libraries under the licenses declared by that package. diff --git a/packages/optimizt-sharp/README.md b/packages/optimizt-sharp/README.md new file mode 100644 index 0000000..6bc2c3c --- /dev/null +++ b/packages/optimizt-sharp/README.md @@ -0,0 +1,31 @@ +# @343dev/optimizt-sharp + +An internal workspace that builds the WebAssembly-only [`sharp`](https://sharp.pixelplumbing.com/) distribution vendored into the published Optimizt package. This workspace is private and is not published separately. + +The generated distribution preserves sharp's JavaScript interface but always loads `@img/sharp-wasm32`. It does not declare sharp's platform-specific native packages. + +```js +import sharp from '@343dev/optimizt-sharp'; + +const output = await sharp(input).rotate().webp().toBuffer(); +``` + +## Reproducible build + +The exact upstream package, version, tarball URL, and Subresource Integrity digest are pinned in `package.json` under `optimiztSharp.upstream`. + +```sh +npm run build --workspace @343dev/optimizt-sharp +``` + +The build downloads the pinned npm tarball, verifies its digest and package identity, copies its published `dist` directory, and replaces only the binding loader with a WASM-only loader. Generated files are excluded from Git. The Optimizt build copies them to `packages/optimizt/vendor/sharp`, which is included in the Optimizt tarball. + +The wrapper version starts with the corresponding upstream sharp version. `0.35.2-0` is wrapper revision 0 based on sharp 0.35.2. + +## Runtime limitations + +The same WebAssembly limitations documented by sharp apply: browsers, single-threaded runtimes, native text rendering, and tile-based output are unsupported. + +## License + +The derived sharp JavaScript code is licensed under Apache-2.0. The WASM dependency includes libraries under the licenses reported by `@img/sharp-wasm32`. diff --git a/packages/optimizt-sharp/eslint.config.js b/packages/optimizt-sharp/eslint.config.js new file mode 100644 index 0000000..98b9bb9 --- /dev/null +++ b/packages/optimizt-sharp/eslint.config.js @@ -0,0 +1 @@ +export { default } from '@343dev/eslint-config'; diff --git a/packages/optimizt-sharp/package.json b/packages/optimizt-sharp/package.json new file mode 100644 index 0000000..7f1180c --- /dev/null +++ b/packages/optimizt-sharp/package.json @@ -0,0 +1,72 @@ +{ + "name": "@343dev/optimizt-sharp", + "version": "0.35.2-0", + "private": true, + "description": "Internal build workspace for Optimizt's vendored WASM-only sharp distribution", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/343dev/optimizt.git", + "directory": "packages/optimizt-sharp" + }, + "homepage": "https://github.com/343dev/optimizt/tree/main/packages/optimizt-sharp#readme", + "files": [ + "dist/", + "LICENSE", + "NOTICE", + "README.md" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "sideEffects": true, + "type": "module", + "config": { + "libvips": ">=8.18.3" + }, + "scripts": { + "build": "node scripts/build.mjs", + "lint": "eslint ./scripts ./test vitest.config.js", + "test": "npm run build && vitest run --config vitest.config.js" + }, + "dependencies": { + "@img/colour": "^1.1.0", + "@img/sharp-wasm32": "0.35.2", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "devDependencies": { + "@343dev/eslint-config": "5.0.0", + "eslint": "10.5.0", + "tar": "7.5.22", + "vitest": "4.1.9" + }, + "lint-staged": { + "*.js": [ + "eslint --fix" + ] + }, + "engines": { + "node": ">=22.22.1" + }, + "optimiztSharp": { + "upstream": { + "package": "sharp", + "version": "0.35.2", + "tarball": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", + "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==" + } + } +} diff --git a/packages/optimizt-sharp/scripts/build.mjs b/packages/optimizt-sharp/scripts/build.mjs new file mode 100644 index 0000000..8239c92 --- /dev/null +++ b/packages/optimizt-sharp/scripts/build.mjs @@ -0,0 +1,83 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs/promises'; +import https from 'node:https'; +import path from 'node:path'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { fileURLToPath } from 'node:url'; + +import { x as extractTar } from 'tar'; + +const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const packageJson = JSON.parse(await fs.readFile(path.join(packageDirectory, 'package.json'), 'utf8')); +const { integrity, package: upstreamName, tarball, version: upstreamVersion } = packageJson.optimiztSharp.upstream; + +assertPinnedUpstream(); + +const archive = await download(tarball); +verifyIntegrity(archive, integrity); + +const temporaryRoot = await fs.realpath(process.env.RUNNER_TEMP || process.env.TMPDIR || '/tmp'); +const temporaryDirectory = await fs.mkdtemp(path.join(temporaryRoot, 'optimizt-sharp-')); +try { + await pipeline(Readable.from(archive), extractTar({ cwd: temporaryDirectory, strip: 1 })); + + const upstreamPackage = JSON.parse(await fs.readFile(path.join(temporaryDirectory, 'package.json'), 'utf8')); + if (upstreamPackage.name !== upstreamName || upstreamPackage.version !== upstreamVersion) { + throw new Error(`Expected ${upstreamName}@${upstreamVersion}, received ${upstreamPackage.name}@${upstreamPackage.version}`); + } + + const destinationDirectory = path.join(packageDirectory, 'dist'); + await fs.rm(destinationDirectory, { force: true, recursive: true }); + await fs.cp(path.join(temporaryDirectory, 'dist'), destinationDirectory, { recursive: true }); + await Promise.all([ + writeLoader(path.join(destinationDirectory, 'sharp.cjs'), 'commonjs'), + writeLoader(path.join(destinationDirectory, 'sharp.mjs'), 'module'), + ]); +} finally { + await fs.rm(temporaryDirectory, { force: true, recursive: true }); +} + +async function writeLoader(filePath, format) { + const banner = `/*\n * Derived from sharp ${upstreamVersion}.\n * Copyright 2013 Lovell Fuller and others. SPDX-License-Identifier: Apache-2.0.\n */\n`; + const source = format === 'module' + ? `${banner}import { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\nconst sharp = require('@img/sharp-wasm32/sharp.node');\n\nexport default sharp;\n` + : `${banner}module.exports = require('@img/sharp-wasm32/sharp.node');\n`; + await fs.writeFile(filePath, source); +} + +function assertPinnedUpstream() { + const upstreamFields = Object.entries({ integrity, upstreamName, tarball, upstreamVersion }); + for (const [key, value] of upstreamFields) { + if (typeof value !== 'string' || value.length === 0) throw new Error(`Missing pinned upstream ${key}`); + } + if (!tarball.endsWith(`/${upstreamName}-${upstreamVersion}.tgz`)) { + throw new Error('Upstream tarball does not match the pinned package and version'); + } +} + +function verifyIntegrity(buffer, expectedIntegrity) { + const separator = expectedIntegrity.indexOf('-'); + const algorithm = expectedIntegrity.slice(0, separator); + const expectedDigest = expectedIntegrity.slice(separator + 1); + const actualDigest = createHash(algorithm).update(buffer).digest('base64'); + if (actualDigest !== expectedDigest) throw new Error(`Upstream integrity mismatch: expected ${expectedIntegrity}`); +} + +function download(url) { + return new Promise((resolve, reject) => { + https.get(url, (response) => { + if (response.statusCode !== 200) { + response.resume(); + reject(new Error(`Unable to download upstream tarball: HTTP ${response.statusCode}`)); + return; + } + const chunks = []; + response.on('data', (chunk) => { + chunks.push(chunk); + }); + response.on('end', () => resolve(Buffer.concat(chunks))); + response.on('error', reject); + }).on('error', reject); + }); +} diff --git a/packages/optimizt-sharp/test/package.test.js b/packages/optimizt-sharp/test/package.test.js new file mode 100644 index 0000000..dda0758 --- /dev/null +++ b/packages/optimizt-sharp/test/package.test.js @@ -0,0 +1,57 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, test } from 'vitest'; + +import sharp from '../dist/index.mjs'; + +const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const fixturePath = path.resolve(packageDirectory, '..', 'optimizt', 'tests', 'images', 'jpeg-low-quality.jpg'); + +describe('@343dev/optimizt-sharp', () => { + test('uses the WebAssembly backend', () => { + expect(sharp.versions.emscripten).toBeTypeOf('string'); + }); + + test.each(['jpeg', 'png', 'webp', 'avif'])('encodes %s', async (format) => { + const input = await fs.readFile(fixturePath); + const output = await sharp(input).rotate()[format]().toBuffer(); + const metadata = await sharp(output).metadata(); + + expect(output.length).toBeGreaterThan(0); + expect(metadata.format).toBe(format === 'avif' ? 'heif' : format); + }); + + test('keeps runtime packages compatible with the published Optimizt package', async () => { + const packageJson = JSON.parse(await fs.readFile(path.join(packageDirectory, 'package.json'), 'utf8')); + const optimiztPackage = JSON.parse(await fs.readFile(path.join(packageDirectory, '..', 'optimizt', 'package.json'), 'utf8')); + const dependencies = Object.keys(packageJson.dependencies ?? {}); + + expect(packageJson.private).toBe(true); + expect(packageJson.optionalDependencies).toBeUndefined(); + expect(dependencies).toContain('@img/sharp-wasm32'); + expect(dependencies.filter(name => /^@img\/sharp-(?!wasm32$)/.test(name))).toEqual([]); + for (const dependency of dependencies) { + expect(optimiztPackage.dependencies[dependency]).toBe(packageJson.dependencies[dependency]); + } + }); + + test('rebuilds byte-for-byte from the pinned upstream tarball', async () => { + const before = await snapshotDistribution(); + const { spawn } = await import('node:child_process'); + const child = spawn(process.execPath, ['scripts/build.mjs'], { cwd: packageDirectory, stdio: 'inherit' }); + await new Promise((resolve, reject) => { + child.once('error', reject); + child.once('close', code => code === 0 ? resolve() : reject(new Error(`Build exited with ${code}`))); + }); + expect(await snapshotDistribution()).toEqual(before); + }); +}); + +async function snapshotDistribution() { + const directory = path.join(packageDirectory, 'dist'); + const entries = await fs.readdir(directory); + const names = entries.toSorted((left, right) => left.localeCompare(right)); + return Promise.all(names.map(async name => [name, await fs.readFile(path.join(directory, name), 'base64')])); +} diff --git a/packages/optimizt-sharp/vitest.config.js b/packages/optimizt-sharp/vitest.config.js new file mode 100644 index 0000000..755777b --- /dev/null +++ b/packages/optimizt-sharp/vitest.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/**/*.test.js'], + testTimeout: 60_000, + }, +}); diff --git a/packages/optimizt/convert.js b/packages/optimizt/convert.js index 074be35..26f6497 100644 --- a/packages/optimizt/convert.js +++ b/packages/optimizt/convert.js @@ -1,176 +1,43 @@ -import fs from 'node:fs'; -import os from 'node:os'; - -import pLimit from 'p-limit'; -import sharp from 'sharp'; +import fs from 'node:fs/promises'; import { atomicWrite } from './lib/atomic-write.js'; import { calculateRatio } from './lib/calculate-ratio.js'; -import { createProgressBarContainer } from './lib/create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import { describeCodecFailure } from './lib/describe-codec-failure.js'; -import { formatBytes } from './lib/format-bytes.js'; -import { getPlural } from './lib/get-plural.js'; -import { getRelativePath } from './lib/get-relative-path.js'; -import { isInterrupted } from './lib/lifecycle.js'; -import { - LOG_TYPES, - log, - logProgress, - logProgressVerbose, -} from './lib/log.js'; -import { OUTCOME_STATUS } from './lib/outcome-status.js'; +import { createOperationRuntime } from './lib/operation-runtime.js'; import { parseImageMetadata } from './lib/parse-image-metadata.js'; -import { programOptions } from './lib/program-options.js'; -import { showTotal } from './lib/show-total.js'; - -export async function convert({ operations, config, configPath }) { - const { isLossless } = programOptions; - const filePathsCount = new Set(operations.map(operation => operation.input)).size; - - if (!filePathsCount) { - return; - } - - log(`Converting ${filePathsCount} ${getPlural(filePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})`); - - const progressBarTotal = operations.length; - const progressBarContainer = createProgressBarContainer(progressBarTotal); - const progressBar = progressBarContainer.create(progressBarTotal, 0); - - const totalSize = { before: 0, after: 0 }; - - const getConfig = format => config?.[format]?.[isLossless ? 'lossless' : 'lossy']; - - const avifConfig = getConfig('avif'); - const webpConfig = getConfig('webp'); - - const cpuCount = os.cpus().length; - const tasksSimultaneousLimit = pLimit(cpuCount); - - const outcomes = await Promise.all(operations.map((operation, planIndex) => tasksSimultaneousLimit(() => { - if (isInterrupted()) return { planIndex, status: OUTCOME_STATUS.UNSTARTED }; - const isAvif = operation.format === 'avif'; - return processFile({ - config: (isAvif ? avifConfig : webpConfig) || {}, - configPath, - filePath: { input: operation.input, output: operation.output }, - format: isAvif ? 'AVIF' : 'WebP', - isLossless, - processFunction: isAvif ? processAvif : processWebp, - progressBar, - progressBarContainer, - planIndex, - skipReason: operation.skipReason, - totalSize, - }); - }))); - - progressBarContainer.update(); // Prevent logs lost. See: https://github.com/npkgz/cli-progress/issues/145#issuecomment-1859594159 - progressBarContainer.stop(); - - showTotal(totalSize.before, totalSize.after, outcomes, { conversion: true }); - return { failed: outcomes.filter(outcome => outcome.status === OUTCOME_STATUS.FAILED).length, outcomes }; -} - -async function processFile({ - filePath, - config, - configPath, - isLossless, - progressBarContainer, - progressBar, - planIndex, - totalSize, - skipReason, - format, - processFunction, -}) { - const outputFilePath = filePath.output; - - try { - if (skipReason) { - logProgressVerbose(getRelativePath(outputFilePath), { - description: `Output file already exists: ${outputFilePath}. Use --force to replace it.`, - progressBarContainer, - }); - - return { planIndex, status: OUTCOME_STATUS.SKIPPED }; +import sharp from './lib/sharp.js'; + +export async function convert({ operations, config, configPath, lifecycle, options, reporter }) { + const runtime = createOperationRuntime({ + detect: (_operation, input) => parseImageMetadata(input), + lifecycle, + read: operation => fs.readFile(operation.input), + writer: { write: atomicWrite }, + }); + return runtime.execute({ + onCompleted: reporter.operationCompleted, + operations, + policy: { + complete({ encoded, input }) { + return { after: encoded.length, before: input.length, ratio: calculateRatio(input.length, encoded.length), write: true }; + }, + }, + selectCodec(metadata, operation) { + if (!metadata.format) throw new Error('Unable to read the image format. Check that the file is a valid, supported image.'); + if (!SUPPORTED_FILE_TYPES.CONVERT.includes(metadata.format)) throw new Error(`Unsupported image format: "${metadata.format}"`); + return { encode: input => encode(input, metadata, operation.format) }; + }, + }); + + async function encode(input, metadata, format) { + if (format === 'avif' && metadata.pages > 1) throw new Error('Unable to create an animated AVIF. Use WebP or provide a non-animated image.'); + const codecConfig = config?.[format]?.[options.isLossless ? 'lossless' : 'lossy'] || {}; + try { + const image = sharp(input, { animated: format === 'webp' && metadata.pages > 1 }).rotate(); + return await image[format](codecConfig).toBuffer(); + } catch (error) { + throw describeCodecFailure({ configPath, error, format, isLossless: options.isLossless, mode: 'convert' }); } - - const fileBuffer = await fs.promises.readFile(filePath.input); - const processedFileBuffer = await processFunction({ fileBuffer, config, configPath, isLossless }); - - await atomicWrite(outputFilePath, processedFileBuffer); - - const fileSize = fileBuffer.length; - const processedFileSize = processedFileBuffer.length; - - totalSize.before += fileSize; - totalSize.after += Math.min(fileSize, processedFileSize); - - const ratio = calculateRatio(fileSize, processedFileSize); - const before = formatBytes(fileSize); - const after = formatBytes(processedFileSize); - - logProgress(getRelativePath(outputFilePath), { - type: LOG_TYPES.SUCCESS, - description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, - progressBarContainer, - }); - return { after: processedFileSize, before: fileSize, planIndex, status: OUTCOME_STATUS.PROCESSED }; - } catch (error) { - // Work abandoned during shutdown is left undone by the interruption, not failed. - if (isInterrupted()) return { planIndex, status: OUTCOME_STATUS.UNSTARTED }; - return { error, output: outputFilePath, planIndex, status: OUTCOME_STATUS.FAILED }; - } finally { - progressBar.increment(); - } -} - -async function processAvif({ fileBuffer, config, configPath, isLossless }) { - const imageMetadata = await parseImageMetadata(fileBuffer); - checkImageFormat(imageMetadata.format); - - const isAnimated = imageMetadata.pages > 1; - - if (isAnimated) { - throw new Error('Unable to create an animated AVIF. Use WebP or provide a non-animated image.'); // See: https://github.com/strukturag/libheif/issues/377 - } - - // Only the codec call is enriched, so detection and support errors keep speaking for themselves. - try { - return await sharp(fileBuffer) - .rotate() // Rotate image using information from EXIF Orientation tag - .avif(config) - .toBuffer(); - } catch (error) { - throw describeCodecFailure({ configPath, error, format: 'avif', isLossless, mode: 'convert' }); - } -} - -async function processWebp({ fileBuffer, config, configPath, isLossless }) { - const imageMetadata = await parseImageMetadata(fileBuffer); - checkImageFormat(imageMetadata.format); - - const isAnimated = imageMetadata.pages > 1; - - try { - return await sharp(fileBuffer, { animated: isAnimated }) - .rotate() // Rotate image using information from EXIF Orientation tag - .webp(config) - .toBuffer(); - } catch (error) { - throw describeCodecFailure({ configPath, error, format: 'webp', isLossless, mode: 'convert' }); - } -} - -function checkImageFormat(imageFormat) { - if (!imageFormat) { - throw new Error('Unable to read the image format. Check that the file is a valid, supported image.'); - } - - if (!SUPPORTED_FILE_TYPES.CONVERT.includes(imageFormat)) { - throw new Error(`Unsupported image format: "${imageFormat}"`); } } diff --git a/packages/optimizt/eslint.config.js b/packages/optimizt/eslint.config.js index 4038ff8..dbe59d1 100644 --- a/packages/optimizt/eslint.config.js +++ b/packages/optimizt/eslint.config.js @@ -6,6 +6,7 @@ export default [ { ignores: [ 'coverage/', + 'vendor/', '.optimiztrc.cjs', ], }, diff --git a/packages/optimizt/lib/parse-image-metadata.js b/packages/optimizt/lib/parse-image-metadata.js index a7a4b35..9c63dc3 100644 --- a/packages/optimizt/lib/parse-image-metadata.js +++ b/packages/optimizt/lib/parse-image-metadata.js @@ -1,4 +1,4 @@ -import sharp from 'sharp'; +import sharp from './sharp.js'; export async function parseImageMetadata(buffer) { try { diff --git a/packages/optimizt/lib/sharp.js b/packages/optimizt/lib/sharp.js new file mode 100644 index 0000000..00ab0ea --- /dev/null +++ b/packages/optimizt/lib/sharp.js @@ -0,0 +1 @@ +export { default } from '../vendor/sharp/dist/index.mjs'; diff --git a/packages/optimizt/optimize.js b/packages/optimizt/optimize.js index 62cf1b4..8193d2b 100644 --- a/packages/optimizt/optimize.js +++ b/packages/optimizt/optimize.js @@ -1,258 +1,64 @@ -import { spawn } from 'node:child_process'; -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; +import fs from 'node:fs/promises'; -import gifsicle from '@343dev/gifsicle'; -import guetzli from '@343dev/guetzli'; -import pLimit from 'p-limit'; -import sharp from 'sharp'; import { optimize as svgoOptimize } from 'svgo'; import { atomicWrite } from './lib/atomic-write.js'; import { calculateRatio } from './lib/calculate-ratio.js'; -import { createProgressBarContainer } from './lib/create-progress-bar-container.js'; import { describeCodecFailure } from './lib/describe-codec-failure.js'; -import { formatBytes } from './lib/format-bytes.js'; -import { getPlural } from './lib/get-plural.js'; -import { getRelativePath } from './lib/get-relative-path.js'; -import { isInterrupted, registerChild } from './lib/lifecycle.js'; -import { - LOG_TYPES, - log, - logProgress, - logProgressVerbose, -} from './lib/log.js'; -import { optionsToArguments } from './lib/options-to-arguments.js'; -import { OUTCOME_STATUS } from './lib/outcome-status.js'; +import { createGifOperation } from './lib/gifsicle.js'; +import { encodeWithGuetzli } from './lib/guetzli.js'; +import { createOperationRuntime } from './lib/operation-runtime.js'; import { parseImageMetadata } from './lib/parse-image-metadata.js'; -import { programOptions } from './lib/program-options.js'; -import { showTotal } from './lib/show-total.js'; - -export async function optimize({ operations, config, configPath }) { - const { isLossless } = programOptions; - const filePaths = operations.map(operation => ({ input: operation.input, output: operation.output })); - - const filePathsCount = filePaths.length; - - if (filePathsCount <= 0) { - return; - } - - log(`Optimizing ${filePathsCount} ${getPlural(filePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})`); - - const progressBarContainer = createProgressBarContainer(filePathsCount); - const progressBar = progressBarContainer.create(filePathsCount, 0); - - const totalSize = { before: 0, after: 0 }; - - const cpuCount = os.cpus().length; - const tasksSimultaneousLimit = pLimit(cpuCount); - const guetzliTasksSimultaneousLimit = pLimit(1); // Guetzli uses a large amount of memory and a significant amount of CPU time. To reduce system load, we only allow one instance of guetzli to run at the same time. - - const outcomes = await Promise.all( - filePaths.map((filePath, planIndex) => { - const extension = path.extname(filePath.input).toLowerCase(); - const isJpeg = extension === '.jpg' || extension === '.jpeg'; - - const limit = isJpeg && isLossless - ? guetzliTasksSimultaneousLimit - : tasksSimultaneousLimit; - - return limit(() => isInterrupted() - ? { planIndex, status: OUTCOME_STATUS.UNSTARTED } - : processFile({ - filePath, - config, - configPath, - progressBarContainer, - progressBar, - totalSize, - isLossless, - planIndex, - })); - }), - ); - - progressBarContainer.update(); // Prevent logs lost. See: https://github.com/npkgz/cli-progress/issues/145#issuecomment-1859594159 - progressBarContainer.stop(); - - showTotal(totalSize.before, totalSize.after, outcomes); - return { failed: outcomes.filter(outcome => outcome.status === OUTCOME_STATUS.FAILED).length, outcomes }; -} - -async function processFile({ - filePath, - config, - configPath, - progressBarContainer, - progressBar, - totalSize, - isLossless, - planIndex, -}) { - try { - const fileBuffer = await fs.promises.readFile(filePath.input); - const processedFileBuffer = await processFileByFormat({ fileBuffer, config, configPath, isLossless }); - - const fileSize = fileBuffer.length; - const processedFileSize = processedFileBuffer.length; - - totalSize.before += fileSize; - totalSize.after += Math.min(fileSize, processedFileSize); - - const ratio = calculateRatio(fileSize, processedFileSize); - - const isOptimized = ratio > 0; - const isChanged = !fileBuffer.equals(processedFileBuffer); - const isSvg = path.extname(filePath.input).toLowerCase() === '.svg'; - - if (!isOptimized && (!isChanged || !isSvg)) { - logProgressVerbose(getRelativePath(filePath.output), { - description: `${(isChanged ? 'File size increased' : 'Nothing changed')}. Skipped`, - progressBarContainer, - }); - - return { planIndex, status: OUTCOME_STATUS.SKIPPED }; - } - - await atomicWrite(filePath.output, processedFileBuffer); - - const before = formatBytes(fileSize); - const after = formatBytes(processedFileSize); - - logProgress(getRelativePath(filePath.output), { - type: isOptimized ? LOG_TYPES.SUCCESS : LOG_TYPES.WARNING, - description: `${before} → ${after}. Ratio: ${ratio}%`, - progressBarContainer, - }); - return { after: processedFileSize, before: fileSize, planIndex, status: OUTCOME_STATUS.PROCESSED }; - } catch (error) { - // Work abandoned during shutdown is left undone by the interruption, not failed. - if (isInterrupted()) return { planIndex, status: OUTCOME_STATUS.UNSTARTED }; - return { error, output: filePath.output, planIndex, status: OUTCOME_STATUS.FAILED }; - } finally { - progressBar.increment(); - } -} - -async function processFileByFormat({ fileBuffer, config, configPath, isLossless }) { - const imageMetadata = await parseImageMetadata(fileBuffer); - const format = imageMetadata.format; - - if (!format) { - throw new Error('Unable to read the image format. Check that the file is a valid, supported image.'); - } - - const processByFormat = PROCESS_BY_FORMAT.get(format); - if (!processByFormat) { - throw new Error(`Unsupported image format: "${format}"`); - } - - // Only the codec call is enriched, so filesystem and detection errors keep speaking - // for themselves. - try { - return await processByFormat({ fileBuffer, config, isLossless }); - } catch (error) { - throw describeCodecFailure({ configPath, error, format, isLossless, mode: 'optimize' }); - } -} - -const PROCESS_BY_FORMAT = new Map([ - ['gif', processGif], - ['jpeg', processJpeg], - ['png', processPng], - ['svg', processSvg], -]); - -async function processJpeg({ fileBuffer, config, isLossless }) { - const sharpImage = sharp(fileBuffer) - .rotate(); // Rotate image using information from EXIF Orientation tag - - if (!isLossless) { - return sharpImage - .jpeg(config?.jpeg?.lossy || {}) - .toBuffer(); - } - - const inputBuffer = await sharpImage - .toColorspace('srgb') // Replace colorspace (guetzli works only with sRGB) - .jpeg({ quality: 100, optimizeCoding: false }) // Applying maximum quality to minimize losses during image processing with sharp - .toBuffer(); - - const commandOptions = [ - ...optionsToArguments({ - options: config?.jpeg?.lossless || {}, - }), - '-', - '-', - ]; - - return pipe({ - command: guetzli, - commandOptions, - inputBuffer, +import sharp from './lib/sharp.js'; + +export async function optimize({ operations, config, configPath, lifecycle, options, reporter }) { + const runtime = createOperationRuntime({ + detect: (_operation, input) => parseImageMetadata(input), + lifecycle, + read: operation => fs.readFile(operation.input), + writer: { write: atomicWrite }, }); -} - -function processPng({ fileBuffer, config, isLossless }) { - return sharp(fileBuffer) - .png(isLossless ? config?.png?.lossless : config?.png?.lossy || {}) - .toBuffer(); -} - -function processGif({ fileBuffer, config, isLossless }) { - const commandOptions = [ - ...optionsToArguments({ - options: (isLossless ? config?.gif?.lossless : config?.gif?.lossy) || {}, - concat: true, - }), - `--threads=${os.cpus().length}`, - '--no-warnings', - '-', - ]; - - return pipe({ - command: gifsicle, - commandOptions, - inputBuffer: fileBuffer, + return runtime.execute({ + onCompleted: reporter.operationCompleted, + operations, + policy: { + complete({ encoded, input, metadata }) { + const ratio = calculateRatio(input.length, encoded.length); + const isChanged = !input.equals(encoded); + const isWrite = ratio > 0 || (metadata.format === 'svg' && isChanged); + return { before: input.length, after: isWrite ? encoded.length : input.length, ratio, write: isWrite }; // eslint-disable-line unicorn/prefer-minimal-ternary + }, + }, + selectCodec(metadata) { + const codec = CODECS.get(metadata.format); + if (!metadata.format) throw new Error('Unable to read the image format. Check that the file is a valid, supported image.'); + if (!codec) throw new Error(`Unsupported image format: "${metadata.format}"`); + return { + resource: metadata.format === 'jpeg' && options.isLossless ? 'guetzli' : undefined, + async encode(input) { + try { + return await codec({ config, input, isLossless: options.isLossless, lifecycle }); + } catch (error) { + throw describeCodecFailure({ configPath, error, format: metadata.format, isLossless: options.isLossless, mode: 'optimize' }); + } + }, + }; + }, }); } -function processSvg({ fileBuffer, config }) { - return Buffer.from( - svgoOptimize( - fileBuffer, - config.svg, - ).data, - ); -} - -function pipe({ command, commandOptions, inputBuffer }) { - return new Promise((resolve, reject) => { - const process = spawn(command, commandOptions); - registerChild(process); - - process.stdin.write(inputBuffer); - process.stdin.end(); - - const stdoutChunks = []; - process.stdout.on('data', (chunk) => { - stdoutChunks.push(chunk); - }); - - process.on('error', (error) => { - reject(new Error(`Unable to optimize the image: ${error.message}`)); - }); - - process.on('close', (code) => { - if (code !== 0) { - reject(new Error(`Unable to optimize the image. The encoder exited with code ${code}.`)); - return; - } - - const processedFileBuffer = Buffer.concat(stdoutChunks); - resolve(processedFileBuffer); - }); - }); -} +const CODECS = new Map([ + ['gif', ({ config, input, isLossless, lifecycle }) => { + const operation = createGifOperation(input, (isLossless ? config?.gif?.lossless : config?.gif?.lossy) || {}); + lifecycle.registerCancellable(operation.terminate, operation.promise); + return operation.promise; + }], + ['jpeg', async ({ config, input, isLossless, lifecycle }) => { + const image = sharp(input).rotate(); + if (!isLossless) return image.jpeg(config?.jpeg?.lossy || {}).toBuffer(); + const prepared = await image.toColorspace('srgb').jpeg({ quality: 100, optimizeCoding: false }).toBuffer(); + return encodeWithGuetzli(prepared, config?.jpeg?.lossless || {}, lifecycle); + }], + ['png', ({ config, input, isLossless }) => sharp(input).png(isLossless ? config?.png?.lossless : config?.png?.lossy || {}).toBuffer()], + ['svg', ({ config, input }) => Buffer.from(svgoOptimize(input, config.svg).data)], +]); diff --git a/packages/optimizt/package.json b/packages/optimizt/package.json index 9c8d5fc..ba29a15 100644 --- a/packages/optimizt/package.json +++ b/packages/optimizt/package.json @@ -33,22 +33,29 @@ "convert.js", "index.js", "MIGRATION.md", - "optimize.js" + "optimize.js", + "vendor/" ], "scripts": { + "build": "node ../optimizt-sharp/scripts/build.mjs && node scripts/vendor-sharp.mjs", "lint": "eslint ./", - "test": "vitest run", - "test-coverage": "vitest run --coverage" + "prepack": "npm run build", + "test": "npm run build && vitest run", + "test-coverage": "npm run build && vitest run --coverage", + "enable-git-hooks": "git config core.hooksPath .githooks" }, "dependencies": { - "@343dev/gifsicle": "1.2.0", - "@343dev/guetzli": "1.3.0", + "@img/colour": "^1.1.0", + "@img/sharp-wasm32": "0.35.2", "cli-progress": "3.12.0", "commander": "15.0.0", + "detect-libc": "^2.1.2", "p-limit": "7.3.0", - "sharp": "0.35.2", + "semver": "^7.8.4", "svgo": "4.0.1", - "write-file-atomic": "7.0.1" + "write-file-atomic": "7.0.1", + "@343dev/gifsicle": "1.2.0", + "@343dev/guetzli": "1.3.0" }, "devDependencies": { "@343dev/eslint-config": "5.0.0", @@ -58,7 +65,7 @@ "vitest": "4.1.9" }, "engines": { - "node": ">=22.22.1" + "node": ">=24.0.0" }, "type": "module", "lint-staged": { diff --git a/packages/optimizt/scripts/vendor-sharp.mjs b/packages/optimizt/scripts/vendor-sharp.mjs new file mode 100644 index 0000000..3cbca1b --- /dev/null +++ b/packages/optimizt/scripts/vendor-sharp.mjs @@ -0,0 +1,41 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const workspaceDirectory = path.resolve(packageDirectory, '..', 'optimizt-sharp'); +const sharpDistributionDirectory = path.join(workspaceDirectory, 'dist'); +const destinationDirectory = path.join(packageDirectory, 'vendor', 'sharp'); + +try { + await fs.access(sharpDistributionDirectory); +} catch { + throw new Error(`Missing ${sharpDistributionDirectory}. Run "node ../optimizt-sharp/scripts/build.mjs" before vendoring sharp.`); +} + +await fs.rm(destinationDirectory, { force: true, recursive: true }); +await fs.mkdir(destinationDirectory, { recursive: true }); +await Promise.all([ + fs.cp(sharpDistributionDirectory, path.join(destinationDirectory, 'dist'), { recursive: true }), + fs.copyFile(path.join(workspaceDirectory, 'LICENSE'), path.join(destinationDirectory, 'LICENSE')), + fs.copyFile(path.join(workspaceDirectory, 'NOTICE'), path.join(destinationDirectory, 'NOTICE')), + writePackageJson(), +]); + +async function writePackageJson() { + const workspacePackage = JSON.parse(await fs.readFile(path.join(workspaceDirectory, 'package.json'), 'utf8')); + const optimiztPackage = JSON.parse(await fs.readFile(path.join(packageDirectory, 'package.json'), 'utf8')); + for (const [name, version] of Object.entries(workspacePackage.dependencies)) { + if (optimiztPackage.dependencies[name] !== version) { + throw new Error(`Dependency "${name}@${version}" required by vendored sharp must be declared by @343dev/optimizt with the same version`); + } + } + const vendorPackage = { + name: '@343dev/optimizt-vendored-sharp', + private: true, + version: workspacePackage.version, + type: 'module', + config: workspacePackage.config, + }; + await fs.writeFile(path.join(destinationDirectory, 'package.json'), `${JSON.stringify(vendorPackage, undefined, '\t')}\n`); +} diff --git a/packages/optimizt/tests/package.test.js b/packages/optimizt/tests/package.test.js new file mode 100644 index 0000000..9b83982 --- /dev/null +++ b/packages/optimizt/tests/package.test.js @@ -0,0 +1,22 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, test } from 'vitest'; + +const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +describe('published package', () => { + test('vendors the generated sharp distribution', async () => { + const packageJson = JSON.parse(await fs.readFile(path.join(packageDirectory, 'package.json'), 'utf8')); + const vendorPackage = JSON.parse(await fs.readFile(path.join(packageDirectory, 'vendor', 'sharp', 'package.json'), 'utf8')); + const vendorFiles = await fs.readdir(path.join(packageDirectory, 'vendor', 'sharp', 'dist')); + + expect(packageJson.files).toContain('vendor/'); + expect(packageJson.dependencies['@343dev/optimizt-sharp']).toBeUndefined(); + expect(packageJson.dependencies['@img/sharp-wasm32']).toBeTypeOf('string'); + expect(vendorPackage.private).toBe(true); + expect(vendorFiles).toContain('index.mjs'); + expect(vendorFiles).toContain('sharp.mjs'); + }); +}); From cf5142ed059dcfbe16bd7da219e7f073da2aa218 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:29:51 +0700 Subject: [PATCH 3/6] feat(wasm)!: add a WASM-only Guetzli runtime Vendor a reproducible wasm64 Guetzli build, its provenance sources, parity fixtures, and the CLI adapter. Raise the runtime requirement to Node.js 24 for WebAssembly memory64 support.\n\nBREAKING CHANGE: Optimizt now requires Node.js 24.0.0 or newer. --- .dockerignore | 2 + .githooks/pre-commit | 2 + .github/workflows/ci-nodejs.yml | 31 +- README.md | 5 +- docs/github.md | 2 +- package-lock.json | 31 +- package.json | 9 +- packages/optimizt-guetzli/.gitattributes | 6 + packages/optimizt-guetzli/LICENSE | 201 +++ packages/optimizt-guetzli/MUSL-COPYRIGHT | 193 ++ packages/optimizt-guetzli/README.md | 57 + .../optimizt-guetzli/THIRD_PARTY_NOTICES.md | 57 + packages/optimizt-guetzli/UPSTREAM.md | 24 + packages/optimizt-guetzli/cli.js | 79 + packages/optimizt-guetzli/dist/guetzli.mjs | 2 + packages/optimizt-guetzli/dist/guetzli.wasm | Bin 0 -> 199991 bytes packages/optimizt-guetzli/eslint.config.js | 29 + packages/optimizt-guetzli/package.json | 30 + .../optimizt-guetzli/scripts/benchmark.js | 132 ++ .../optimizt-guetzli/scripts/build-native.sh | 61 + .../optimizt-guetzli/scripts/build-wasm.sh | 108 ++ .../scripts/generate-benchmark-fixtures.py | 58 + .../scripts/generate-fixtures.py | 86 + .../scripts/native-reference.cc | 91 + .../optimizt-guetzli/scripts/setup-emsdk.sh | 44 + .../optimizt-guetzli/scripts/verify-dist.sh | 57 + .../scripts/verify-native-parity.js | 68 + .../scripts/verify-upstream.js | 49 + packages/optimizt-guetzli/src/wasm/bridge.cc | 62 + .../src/wasm/guetzli/debug_print.h | 23 + packages/optimizt-guetzli/test/PARITY.md | 16 + .../optimizt-guetzli/test/fixtures/README.md | 45 + .../fixtures/floating-point-boundary-444.jpg | Bin 0 -> 4520 bytes .../fixtures/grayscale-single-component.jpg | Bin 0 -> 2928 bytes .../test/fixtures/grayscale-ycbcr.jpg | Bin 0 -> 3244 bytes .../test/fixtures/invalid.jpg | 1 + .../test/fixtures/metadata-444.jpg | Bin 0 -> 7671 bytes .../test/fixtures/progressive-420.jpg | Bin 0 -> 3402 bytes .../test/fixtures/rgb-420.jpg | Bin 0 -> 4475 bytes .../test/fixtures/rgb-444.jpg | Bin 0 -> 7608 bytes .../test/parity-manifest.json | 56 + packages/optimizt-guetzli/test/parity.test.js | 63 + .../optimizt-guetzli/upstream/guetzli.sha256 | 73 + .../upstream/guetzli/.gitignore | 17 + .../optimizt-guetzli/upstream/guetzli/BUILD | 27 + .../upstream/guetzli/CONTRIBUTING.md | 27 + .../optimizt-guetzli/upstream/guetzli/LICENSE | 202 +++ .../upstream/guetzli/Makefile | 45 + .../upstream/guetzli/README.md | 96 + .../upstream/guetzli/WORKSPACE | 25 + .../upstream/guetzli/guetzli.make | 213 +++ .../upstream/guetzli/guetzli.sln | 26 + .../upstream/guetzli/guetzli.vcxproj | 220 +++ .../upstream/guetzli/guetzli.vcxproj.filters | 162 ++ .../guetzli/guetzli/butteraugli_comparator.cc | 231 +++ .../guetzli/guetzli/butteraugli_comparator.h | 74 + .../guetzli/guetzli/color_transform.h | 223 +++ .../upstream/guetzli/guetzli/comparator.h | 90 + .../upstream/guetzli/guetzli/dct_double.cc | 87 + .../upstream/guetzli/guetzli/dct_double.h | 32 + .../upstream/guetzli/guetzli/debug_print.cc | 30 + .../upstream/guetzli/guetzli/debug_print.h | 46 + .../guetzli/guetzli/entropy_encode.cc | 147 ++ .../upstream/guetzli/guetzli/entropy_encode.h | 62 + .../upstream/guetzli/guetzli/fast_log.h | 40 + .../upstream/guetzli/guetzli/fdct.cc | 242 +++ .../upstream/guetzli/guetzli/fdct.h | 31 + .../upstream/guetzli/guetzli/gamma_correct.cc | 40 + .../upstream/guetzli/guetzli/gamma_correct.h | 26 + .../upstream/guetzli/guetzli/guetzli.cc | 300 ++++ .../upstream/guetzli/guetzli/idct.cc | 163 ++ .../upstream/guetzli/guetzli/idct.h | 31 + .../guetzli/guetzli/jpeg_bit_writer.h | 113 ++ .../upstream/guetzli/guetzli/jpeg_data.cc | 104 ++ .../upstream/guetzli/guetzli/jpeg_data.h | 211 +++ .../guetzli/guetzli/jpeg_data_decoder.cc | 56 + .../guetzli/guetzli/jpeg_data_decoder.h | 41 + .../guetzli/guetzli/jpeg_data_encoder.cc | 138 ++ .../guetzli/guetzli/jpeg_data_encoder.h | 41 + .../guetzli/guetzli/jpeg_data_reader.cc | 1076 +++++++++++ .../guetzli/guetzli/jpeg_data_reader.h | 50 + .../guetzli/guetzli/jpeg_data_writer.cc | 568 ++++++ .../guetzli/guetzli/jpeg_data_writer.h | 105 ++ .../upstream/guetzli/guetzli/jpeg_error.h | 71 + .../guetzli/guetzli/jpeg_huffman_decode.cc | 122 ++ .../guetzli/guetzli/jpeg_huffman_decode.h | 51 + .../upstream/guetzli/guetzli/order.inc | 391 ++++ .../upstream/guetzli/guetzli/output_image.cc | 451 +++++ .../upstream/guetzli/guetzli/output_image.h | 157 ++ .../guetzli/guetzli/preprocess_downsample.cc | 478 +++++ .../guetzli/guetzli/preprocess_downsample.h | 40 + .../upstream/guetzli/guetzli/processor.cc | 889 +++++++++ .../upstream/guetzli/guetzli/processor.h | 62 + .../upstream/guetzli/guetzli/quality.cc | 87 + .../upstream/guetzli/guetzli/quality.h | 26 + .../upstream/guetzli/guetzli/quantize.cc | 32 + .../upstream/guetzli/guetzli/quantize.h | 35 + .../upstream/guetzli/guetzli/score.cc | 43 + .../upstream/guetzli/guetzli/score.h | 28 + .../upstream/guetzli/guetzli/stats.h | 44 + .../upstream/guetzli/png.BUILD | 33 + .../upstream/guetzli/premake5.lua | 38 + .../upstream/guetzli/snapcraft.yaml | 22 + .../guetzli/third_party/butteraugli/BUILD | 24 + .../guetzli/third_party/butteraugli/LICENSE | 201 +++ .../guetzli/third_party/butteraugli/README.md | 68 + .../guetzli/third_party/butteraugli/WORKSPACE | 25 + .../butteraugli/butteraugli/Makefile | 7 + .../butteraugli/butteraugli/butteraugli.cc | 1591 +++++++++++++++++ .../butteraugli/butteraugli/butteraugli.h | 560 ++++++ .../butteraugli/butteraugli_main.cc | 441 +++++ .../third_party/butteraugli/jpeg.BUILD | 89 + .../guetzli/third_party/butteraugli/png.BUILD | 33 + .../third_party/butteraugli/zlib.BUILD | 36 + .../upstream/guetzli/tools/guetzli-compare.py | 49 + .../upstream/guetzli/zlib.BUILD | 36 + packages/optimizt/.optimiztrc.cjs | 6 +- packages/optimizt/CHANGELOG.md | 2 + packages/optimizt/MIGRATION.md | 4 +- packages/optimizt/lib/guetzli.js | 61 + packages/optimizt/package.json | 5 +- packages/optimizt/scripts/vendor-guetzli.mjs | 18 + 122 files changed, 13636 insertions(+), 31 deletions(-) create mode 100644 packages/optimizt-guetzli/.gitattributes create mode 100644 packages/optimizt-guetzli/LICENSE create mode 100644 packages/optimizt-guetzli/MUSL-COPYRIGHT create mode 100644 packages/optimizt-guetzli/README.md create mode 100644 packages/optimizt-guetzli/THIRD_PARTY_NOTICES.md create mode 100644 packages/optimizt-guetzli/UPSTREAM.md create mode 100755 packages/optimizt-guetzli/cli.js create mode 100644 packages/optimizt-guetzli/dist/guetzli.mjs create mode 100755 packages/optimizt-guetzli/dist/guetzli.wasm create mode 100644 packages/optimizt-guetzli/eslint.config.js create mode 100644 packages/optimizt-guetzli/package.json create mode 100644 packages/optimizt-guetzli/scripts/benchmark.js create mode 100755 packages/optimizt-guetzli/scripts/build-native.sh create mode 100755 packages/optimizt-guetzli/scripts/build-wasm.sh create mode 100644 packages/optimizt-guetzli/scripts/generate-benchmark-fixtures.py create mode 100755 packages/optimizt-guetzli/scripts/generate-fixtures.py create mode 100644 packages/optimizt-guetzli/scripts/native-reference.cc create mode 100755 packages/optimizt-guetzli/scripts/setup-emsdk.sh create mode 100755 packages/optimizt-guetzli/scripts/verify-dist.sh create mode 100644 packages/optimizt-guetzli/scripts/verify-native-parity.js create mode 100644 packages/optimizt-guetzli/scripts/verify-upstream.js create mode 100644 packages/optimizt-guetzli/src/wasm/bridge.cc create mode 100644 packages/optimizt-guetzli/src/wasm/guetzli/debug_print.h create mode 100644 packages/optimizt-guetzli/test/PARITY.md create mode 100644 packages/optimizt-guetzli/test/fixtures/README.md create mode 100644 packages/optimizt-guetzli/test/fixtures/floating-point-boundary-444.jpg create mode 100644 packages/optimizt-guetzli/test/fixtures/grayscale-single-component.jpg create mode 100644 packages/optimizt-guetzli/test/fixtures/grayscale-ycbcr.jpg create mode 100644 packages/optimizt-guetzli/test/fixtures/invalid.jpg create mode 100644 packages/optimizt-guetzli/test/fixtures/metadata-444.jpg create mode 100644 packages/optimizt-guetzli/test/fixtures/progressive-420.jpg create mode 100644 packages/optimizt-guetzli/test/fixtures/rgb-420.jpg create mode 100644 packages/optimizt-guetzli/test/fixtures/rgb-444.jpg create mode 100644 packages/optimizt-guetzli/test/parity-manifest.json create mode 100644 packages/optimizt-guetzli/test/parity.test.js create mode 100644 packages/optimizt-guetzli/upstream/guetzli.sha256 create mode 100644 packages/optimizt-guetzli/upstream/guetzli/.gitignore create mode 100644 packages/optimizt-guetzli/upstream/guetzli/BUILD create mode 100644 packages/optimizt-guetzli/upstream/guetzli/CONTRIBUTING.md create mode 100644 packages/optimizt-guetzli/upstream/guetzli/LICENSE create mode 100644 packages/optimizt-guetzli/upstream/guetzli/Makefile create mode 100644 packages/optimizt-guetzli/upstream/guetzli/README.md create mode 100644 packages/optimizt-guetzli/upstream/guetzli/WORKSPACE create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli.make create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli.sln create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli.vcxproj create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli.vcxproj.filters create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/butteraugli_comparator.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/butteraugli_comparator.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/color_transform.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/comparator.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/dct_double.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/dct_double.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/debug_print.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/debug_print.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/entropy_encode.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/entropy_encode.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/fast_log.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/fdct.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/fdct.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/gamma_correct.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/gamma_correct.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/guetzli.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/idct.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/idct.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_bit_writer.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_decoder.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_decoder.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_encoder.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_encoder.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_reader.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_reader.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_writer.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_writer.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_error.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_huffman_decode.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_huffman_decode.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/order.inc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/output_image.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/output_image.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/preprocess_downsample.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/preprocess_downsample.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/processor.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/processor.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/quality.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/quality.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/quantize.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/quantize.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/score.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/score.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/guetzli/stats.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/png.BUILD create mode 100644 packages/optimizt-guetzli/upstream/guetzli/premake5.lua create mode 100644 packages/optimizt-guetzli/upstream/guetzli/snapcraft.yaml create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/BUILD create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/LICENSE create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/README.md create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/WORKSPACE create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/Makefile create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli.h create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/jpeg.BUILD create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/png.BUILD create mode 100644 packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/zlib.BUILD create mode 100644 packages/optimizt-guetzli/upstream/guetzli/tools/guetzli-compare.py create mode 100644 packages/optimizt-guetzli/upstream/guetzli/zlib.BUILD create mode 100644 packages/optimizt/lib/guetzli.js create mode 100644 packages/optimizt/scripts/vendor-guetzli.mjs diff --git a/.dockerignore b/.dockerignore index 57118d7..85314b9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,8 @@ !packages/optimizt/** !packages/optimizt-sharp/ !packages/optimizt-sharp/** +!packages/optimizt-guetzli/ +!packages/optimizt-guetzli/** packages/optimizt-sharp/dist/ packages/optimizt/vendor/ node_modules/ diff --git a/.githooks/pre-commit b/.githooks/pre-commit index d63ee5b..5044342 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -4,3 +4,5 @@ # so it must run once per workspace rather than from the repository root. npx lint-staged --cwd packages/optimizt npx lint-staged --cwd packages/optimizt-sharp + +npx lint-staged --cwd packages/optimizt-guetzli diff --git a/.github/workflows/ci-nodejs.yml b/.github/workflows/ci-nodejs.yml index 4bbc278..dba7cb7 100644 --- a/.github/workflows/ci-nodejs.yml +++ b/.github/workflows/ci-nodejs.yml @@ -9,11 +9,38 @@ permissions: contents: read jobs: + provenance: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + persist-credentials: false + - id: changes + run: | + if git diff --name-only "${{ github.event.pull_request.base.sha }}...HEAD" | grep -Eq '^packages/optimizt-(gifsicle|guetzli)/'; then + echo "required=true" >> "$GITHUB_OUTPUT" + else + echo "required=false" >> "$GITHUB_OUTPUT" + fi + - name: Use Node.js + if: steps.changes.outputs.required == 'true' + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 24.18.0 + - if: steps.changes.outputs.required == 'true' + run: npm ci + - if: steps.changes.outputs.required == 'true' + run: npm run provenance:setup + - if: steps.changes.outputs.required == 'true' + run: npm run provenance:verify + tests: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [22.22.1, latest] + node-version: [24.18.0, latest] os: [ ubuntu-latest, # x64 ubuntu-24.04-arm, # arm64 @@ -22,7 +49,7 @@ jobs: ] include: - os: windows-latest - node-version: 22.22.1 + node-version: 24.18.0 steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 diff --git a/README.md b/README.md index 6bcf8ca..ac07cf4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # Optimizt monorepo -This repository contains two workspaces: +This repository contains three workspaces: - [`@343dev/optimizt`](packages/optimizt) — the published Optimizt command-line image optimizer. - [`@343dev/optimizt-sharp`](packages/optimizt-sharp) — a private build workspace that reproducibly generates the WASM-only Sharp distribution vendored into Optimizt. +- [`@343dev/optimizt-guetzli`](packages/optimizt-guetzli) — the private Guetzli WebAssembly build and provenance workspace. ## Development @@ -12,4 +13,4 @@ npm ci npm run check ``` -`npm run build` downloads the exact upstream Sharp tarball pinned by `packages/optimizt-sharp/package.json`, verifies its integrity, generates the wrapper, and copies it into the ignored `packages/optimizt/vendor` directory. Only `@343dev/optimizt` is published. +`npm run build` downloads the exact upstream Sharp tarball pinned by `packages/optimizt-sharp/package.json`, verifies its integrity, generates the wrapper, and copies both codec runtimes into the ignored `packages/optimizt/vendor` directory. Only `@343dev/optimizt` is published. diff --git a/docs/github.md b/docs/github.md index bd6595f..39ff599 100644 --- a/docs/github.md +++ b/docs/github.md @@ -31,7 +31,7 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '22.22.1' + node-version: '24.18.0' - run: npm install --global @343dev/optimizt diff --git a/package-lock.json b/package-lock.json index 6d44d2c..8bbe3fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "packages/*" ], "engines": { - "node": ">=22.22.1" + "node": ">=24.0.0" } }, "node_modules/@343dev/eslint-config": { @@ -48,22 +48,14 @@ "node": ">=18.17" } }, - "node_modules/@343dev/guetzli": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@343dev/guetzli/-/guetzli-1.3.0.tgz", - "integrity": "sha512-vIOlor+UtNf22IxMj9sQPc/BAwxdpiDVDg7a8kUCkgjuokcHlfYDlS0DeDsB5Nf75HtDXub6zDfPWVUNzNHwIQ==", - "license": "Apache-2.0", - "bin": { - "guetzli": "cli.js" - }, - "engines": { - "node": ">=18.17" - } - }, "node_modules/@343dev/optimizt": { "resolved": "packages/optimizt", "link": true }, + "node_modules/@343dev/optimizt-guetzli": { + "resolved": "packages/optimizt-guetzli", + "link": true + }, "node_modules/@343dev/optimizt-sharp": { "resolved": "packages/optimizt-sharp", "link": true @@ -3995,7 +3987,6 @@ "license": "MIT", "dependencies": { "@343dev/gifsicle": "1.2.0", - "@343dev/guetzli": "1.3.0", "@img/colour": "^1.1.0", "@img/sharp-wasm32": "0.35.2", "cli-progress": "3.12.0", @@ -4020,6 +4011,18 @@ "node": ">=24.0.0" } }, + "packages/optimizt-guetzli": { + "name": "@343dev/optimizt-guetzli", + "version": "1.0.1-0", + "license": "Apache-2.0", + "devDependencies": { + "@343dev/eslint-config": "5.0.0", + "eslint": "10.5.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, "packages/optimizt-sharp": { "name": "@343dev/optimizt-sharp", "version": "0.35.2-0", diff --git a/package.json b/package.json index 44c79be..14727a1 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,15 @@ "packages/*" ], "scripts": { + "build": "npm run build --workspace @343dev/optimizt", "check": "npm run build && npm run lint && npm test && npm run license-check", "lint": "npm run lint --workspaces --if-present", "test": "npm test --workspaces --if-present", - "license-check": "npx license-compliance@3.0.1 --report detailed --allow '0BSD;Apache-2.0;Artistic-2.0;BlueOak-1.0.0;BSD-2-Clause;BSD-3-Clause;BSD-4-Clause;CC0-1.0;CC-BY-4.0;ISC;LGPL-2.1-or-later;LGPL-3.0-or-later;MIT;MPL-1.1;MPL-2.0;Python-2.0;Unlicense;W3C;W3C-20150513;X11;Zlib' --exclude 'spdx-exceptions'", - "enable-git-hooks": "git config core.hooksPath .githooks", - "build": "npm run build --workspace @343dev/optimizt" + "license-check": "npx license-compliance@3.0.1 --report detailed --allow \"0BSD;Apache-2.0;Artistic-2.0;BlueOak-1.0.0;BSD-2-Clause;BSD-3-Clause;BSD-4-Clause;CC0-1.0;CC-BY-4.0;ISC;LGPL-2.1-or-later;LGPL-3.0-or-later;MIT;MPL-1.1;MPL-2.0;Python-2.0;Unlicense;W3C;W3C-20150513;X11;Zlib\"", + "provenance:setup": "npm run setup-emsdk --workspace @343dev/optimizt-guetzli && EMSDK_INSTALL_DIR=$PWD/packages/optimizt-guetzli/.cache/emsdk npm run setup-emsdk --workspace @343dev/optimizt-gifsicle", + "provenance:verify": "EMSDK_INSTALL_DIR=$PWD/packages/optimizt-guetzli/.cache/emsdk npm run verify:upstream --workspace @343dev/optimizt-guetzli && EMSDK_INSTALL_DIR=$PWD/packages/optimizt-guetzli/.cache/emsdk npm run verify:dist --workspace @343dev/optimizt-guetzli && EMSDK_INSTALL_DIR=$PWD/packages/optimizt-guetzli/.cache/emsdk npm run verify:upstream --workspace @343dev/optimizt-gifsicle && EMSDK_INSTALL_DIR=$PWD/packages/optimizt-guetzli/.cache/emsdk npm run verify:patch --workspace @343dev/optimizt-gifsicle && EMSDK_INSTALL_DIR=$PWD/packages/optimizt-guetzli/.cache/emsdk npm run verify:dist --workspace @343dev/optimizt-gifsicle && EMSDK_INSTALL_DIR=$PWD/packages/optimizt-guetzli/.cache/emsdk npm run verify:parity --workspace @343dev/optimizt-gifsicle" }, "engines": { - "node": ">=22.22.1" + "node": ">=24.0.0" } } diff --git a/packages/optimizt-guetzli/.gitattributes b/packages/optimizt-guetzli/.gitattributes new file mode 100644 index 0000000..908a5ea --- /dev/null +++ b/packages/optimizt-guetzli/.gitattributes @@ -0,0 +1,6 @@ +# Keep source files consistent across platforms. In particular, the CLI +# hashbang must end with LF for ESLint and Unix execution. +* text=auto eol=lf + +# Preserve the exact bytes of the vendored upstream snapshot. +/upstream/guetzli/** -text -eol diff --git a/packages/optimizt-guetzli/LICENSE b/packages/optimizt-guetzli/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/packages/optimizt-guetzli/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/optimizt-guetzli/MUSL-COPYRIGHT b/packages/optimizt-guetzli/MUSL-COPYRIGHT new file mode 100644 index 0000000..2f15edc --- /dev/null +++ b/packages/optimizt-guetzli/MUSL-COPYRIGHT @@ -0,0 +1,193 @@ +musl as a whole is licensed under the following standard MIT license: + +---------------------------------------------------------------------- +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------- + +Authors/contributors include: + +A. Wilcox +Ada Worcester +Alex Dowad +Alex Suykov +Alexander Monakov +Andre McCurdy +Andrew Kelley +Anthony G. Basile +Aric Belsito +Arvid Picciani +Bartosz Brachaczek +Benjamin Peterson +Bobby Bingham +Boris Brezillon +Brent Cook +Chris Spiegel +Clément Vasseur +Daniel Micay +Daniel Sabogal +Daurnimator +David Carlier +David Edelsohn +Denys Vlasenko +Dmitry Ivanov +Dmitry V. Levin +Drew DeVault +Emil Renner Berthing +Fangrui Song +Felix Fietkau +Felix Janda +Gianluca Anzolin +Hauke Mehrtens +He X +Hiltjo Posthuma +Isaac Dunham +Jaydeep Patil +Jens Gustedt +Jeremy Huntwork +Jo-Philipp Wich +Joakim Sindholt +John Spencer +Julien Ramseier +Justin Cormack +Kaarle Ritvanen +Khem Raj +Kylie McClain +Leah Neukirchen +Luca Barbato +Luka Perkov +Lynn Ochs +M Farkas-Dyck (Strake) +Mahesh Bodapati +Markus Wichmann +Masanori Ogino +Michael Clark +Michael Forney +Mikhail Kremnyov +Natanael Copa +Nicholas J. Kain +orc +Pascal Cuoq +Patrick Oppenlander +Petr Hosek +Petr Skocik +Pierre Carrier +Reini Urban +Rich Felker +Richard Pennington +Ryan Fairfax +Samuel Holland +Segev Finer +Shiz +sin +Solar Designer +Stefan Kristiansson +Stefan O'Rear +Szabolcs Nagy +Timo Teräs +Trutz Behn +Will Dietz +William Haddon +William Pitcock + +Portions of this software are derived from third-party works licensed +under terms compatible with the above MIT license: + +The TRE regular expression implementation (src/regex/reg* and +src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed +under a 2-clause BSD license (license text in the source files). The +included version has been heavily modified by Rich Felker in 2012, in +the interests of size, simplicity, and namespace cleanliness. + +Much of the math library code (src/math/* and src/complex/*) is +Copyright © 1993,2004 Sun Microsystems or +Copyright © 2003-2011 David Schultz or +Copyright © 2003-2009 Steven G. Kargl or +Copyright © 2003-2009 Bruce D. Evans or +Copyright © 2008 Stephen L. Moshier or +Copyright © 2017-2018 Arm Limited +and labelled as such in comments in the individual source files. All +have been licensed under extremely permissive terms. + +The ARM memcpy code (src/string/arm/memcpy.S) is Copyright © 2008 +The Android Open Source Project and is licensed under a two-clause BSD +license. It was taken from Bionic libc, used on Android. + +The AArch64 memcpy and memset code (src/string/aarch64/*) are +Copyright © 1999-2019, Arm Limited. + +The implementation of DES for crypt (src/crypt/crypt_des.c) is +Copyright © 1994 David Burren. It is licensed under a BSD license. + +The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was +originally written by Solar Designer and placed into the public +domain. The code also comes with a fallback permissive license for use +in jurisdictions that may not recognize the public domain. + +The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011 +Lynn Ochs and is licensed under an MIT-style license. + +The x86_64 port was written by Nicholas J. Kain and is licensed under +the standard MIT terms. + +The mips and microblaze ports were originally written by Richard +Pennington for use in the ellcc project. The original code was adapted +by Rich Felker for build system and code conventions during upstream +integration. It is licensed under the standard MIT terms. + +The mips64 port was contributed by Imagination Technologies and is +licensed under the standard MIT terms. + +The powerpc port was also originally written by Richard Pennington, +and later supplemented and integrated by John Spencer. It is licensed +under the standard MIT terms. + +All other files which have no copyright comments are original works +produced specifically for use as part of this library, written either +by Rich Felker, the main author of the library, or by one or more +contibutors listed above. Details on authorship of individual files +can be found in the git version control history of the project. The +omission of copyright and license comments in each file is in the +interest of source tree size. + +In addition, permission is hereby granted for all public header files +(include/* and arch/*/bits/*) and crt files intended to be linked into +applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit +the copyright notice and permission notice otherwise required by the +license, and to use these files without any requirement of +attribution. These files include substantial contributions from: + +Bobby Bingham +John Spencer +Nicholas J. Kain +Rich Felker +Richard Pennington +Stefan Kristiansson +Szabolcs Nagy + +all of whom have explicitly granted such permission. + +This file previously contained text expressing a belief that most of +the files covered by the above exception were sufficiently trivial not +to be subject to copyright, resulting in confusion over whether it +negated the permissions granted in the license. In the spirit of +permissive licensing, and of not having licensing issues being an +obstacle to adoption, that text has been removed. diff --git a/packages/optimizt-guetzli/README.md b/packages/optimizt-guetzli/README.md new file mode 100644 index 0000000..4c0db8a --- /dev/null +++ b/packages/optimizt-guetzli/README.md @@ -0,0 +1,57 @@ +# Guetzli WebAssembly for Optimizt + +Google Guetzli v1.0.1 compiled to WebAssembly for exclusive use inside the +Optimizt monorepo. This directory is not a public package. + +The runtime is intentionally minimal: + +- JPEG input is read from standard input. +- JPEG output is written to standard output. +- `--quality Q` accepts an integer from 84 to 110 and defaults to 90. +- There is no JavaScript API, filesystem interface, Worker, verbose mode, or + application-level memory limit. + +Optimizt must launch `cli.js` as a child Node.js process. Process isolation is +the memory-safety boundary: if Guetzli exhausts available memory, the runtime or +operating system may terminate only that child process. + +```sh +node cli.js --quality 90 < input.jpg > output.jpg +``` + +## Resource use + +Guetzli is deliberately CPU- and memory-intensive. Upstream estimates +approximately 300 MiB per megapixel and about one minute of CPU time per +megapixel. The integration does not estimate or limit memory before encoding. +WebAssembly's wasm64 address-space ceiling and the host operating system are the +effective limits. The Guetzli runtime requires Node.js 24.0.0 or newer. + +Guetzli expects high-quality sRGB JPEG input with gamma 2.2. It ignores embedded +color-profile metadata and produces sequential JPEG output. A true +single-component grayscale JPEG is not supported by upstream Guetzli; +three-component YCbCr JPEGs with grayscale-looking pixels are supported. + +See [UPSTREAM.md](UPSTREAM.md) for source and toolchain provenance. + +## Development + +Install the monorepo dependencies from the repository root: + +```sh +npm ci --ignore-scripts +``` + +Before completing a change, run from the repository root: + +```sh +npm run lint --workspace @343dev/optimizt-guetzli +npm test --workspace @343dev/optimizt-guetzli +``` + +Changes to the bridge, build scripts, Emscripten version, or committed +distribution also require: + +```sh +npm run verify:dist +``` diff --git a/packages/optimizt-guetzli/THIRD_PARTY_NOTICES.md b/packages/optimizt-guetzli/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..4651510 --- /dev/null +++ b/packages/optimizt-guetzli/THIRD_PARTY_NOTICES.md @@ -0,0 +1,57 @@ +# Third-party notices + +This package contains software compiled from third-party source code. + +## Guetzli and Butteraugli + +Copyright 2016 Google Inc. + +Google Guetzli v1.0.1, including its bundled Butteraugli source, is licensed under the Apache License, Version 2.0. The complete license text is included in this package as `LICENSE`. Source provenance and the exact upstream revision are documented in `UPSTREAM.md`. + +## Emscripten runtime + +The generated files in `dist/` contain Emscripten runtime portions. Emscripten 6.0.9 is used as the build tool and is not otherwise distributed in this package. + +Emscripten is available under the MIT License and the University of Illinois/NCSA Open Source License. + +Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal with the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution. + +Neither the names of Mozilla, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## musl libc runtime + +The WebAssembly binary links Emscripten's musl libc implementation. The +complete copyright and license notices from Emscripten 6.0.9's +`system/lib/libc/musl/COPYRIGHT` are reproduced verbatim in `MUSL-COPYRIGHT`. +They cover musl and the separately attributed Arm Limited `exp` and `pow` +implementations and data linked into the binary. + +## Node.js runtime portions + +The Emscripten runtime uses portions of Node.js source code under the MIT License. + +Copyright Joyent, Inc. and other Node contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/optimizt-guetzli/UPSTREAM.md b/packages/optimizt-guetzli/UPSTREAM.md new file mode 100644 index 0000000..986423b --- /dev/null +++ b/packages/optimizt-guetzli/UPSTREAM.md @@ -0,0 +1,24 @@ +# Upstream provenance + +## Google Guetzli + +- Project: [Google Guetzli](https://github.com/google/guetzli) +- Version: 1.0.1 +- Commit: `a0f47a297f802630f937a3091964838eaf3b87d8` +- License: Apache-2.0 +- Source archive SHA-256: `98fe2f9befc06eb1796a8906b3e940eccd8c4a634ee05ddf3b36c59c15504fc7` +- Official Linux x64 release binary SHA-256: `44d32910aa2a0e5a5076c12ba7115ff5d2bd1192afdda0f8fe55421c374cba18` + +`upstream/guetzli/` is an unmodified snapshot of the commit above. `upstream/guetzli.sha256` records every vendored file. WebAssembly integration is kept outside that directory. + +The official release binary is corroborating evidence only. The source commit is authoritative. The native reference used by parity tests is compiled from the vendored source for Linux x64 with GCC 14.2.0 and the upstream release flags. + +## WebAssembly toolchain + +- Emscripten SDK: 6.0.9 +- emsdk commit: `5eb0bde7585670252e8ba05e9d361627bffd08b5` +- Emscripten compiler revision: `4e4223852a0835923411059a3929907d7df1232e` +- WebAssembly target: `wasm64` (`-m64`), with a 16 GiB maximum linear memory +- Exported pointer signatures use Emscripten `SIGNATURE_CONVERSIONS`; the JavaScript API continues to use numeric pointers + +The WebAssembly distribution is built from the unmodified Guetzli library sources plus `src/wasm/bridge.cc`. The original filesystem CLI and PNG reader are not compiled. The internal Optimizt integration intentionally supports JPEG input only and applies no preflight memory limit. diff --git a/packages/optimizt-guetzli/cli.js b/packages/optimizt-guetzli/cli.js new file mode 100755 index 0000000..6e82ac6 --- /dev/null +++ b/packages/optimizt-guetzli/cli.js @@ -0,0 +1,79 @@ +import process from 'node:process'; + +import createModule from './dist/guetzli.mjs'; + +const defaultQuality = 90; +const minimumQuality = 84; +const maximumQuality = 110; + +function fail(message) { + process.stderr.write(`${message}\n`); + process.exitCode = 1; +} + +function parseQuality(arguments_) { + if (arguments_.length === 0) { + return defaultQuality; + } + if (arguments_.length !== 2 || arguments_[0] !== '--quality') { + throw new Error( + `Usage: guetzli-runner [--quality ${minimumQuality}-${maximumQuality}]`, + ); + } + const quality = Number(arguments_[1]); + if (!Number.isInteger(quality) || quality < minimumQuality || quality > maximumQuality) { + throw new Error(`quality must be an integer between ${minimumQuality} and ${maximumQuality}`); + } + return quality; +} + +async function readInput() { + const chunks = await Array.fromAsync(process.stdin); + return Buffer.concat(chunks); +} + +try { + const quality = parseQuality(process.argv.slice(2)); + const input = await readInput(); + const module = await createModule(); + const inputPointer = module._malloc(input.byteLength); + if (inputPointer === 0) { + throw new Error('Could not allocate WebAssembly input memory'); + } + + let status; + try { + module.HEAPU8.set(input, inputPointer); + status = module._guetzli_encode(inputPointer, input.byteLength, quality); + } finally { + module._free(inputPointer); + } + if (status !== 0) { + throw new Error('Guetzli could not process the JPEG input'); + } + + const outputPointer = module._guetzli_output_data(); + const outputSize = module._guetzli_output_size(); + // Encoding may grow memory, so reacquire the current heap view here. + const output = module.HEAPU8.subarray(outputPointer, outputPointer + outputSize); + await new Promise((resolve, reject) => { + let settled = false; + const rejectOnce = (error) => { + if (!settled) { + settled = true; + reject(error); + } + }; + process.stdout.once('error', rejectOnce); + process.stdout.write(output, (error) => { + if (error) { + rejectOnce(error); + } else if (!settled) { + settled = true; + resolve(); + } + }); + }); +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} diff --git a/packages/optimizt-guetzli/dist/guetzli.mjs b/packages/optimizt-guetzli/dist/guetzli.mjs new file mode 100644 index 0000000..cd35df7 --- /dev/null +++ b/packages/optimizt-guetzli/dist/guetzli.mjs @@ -0,0 +1,2 @@ +async function Module(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_NODE=true;if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var programArgs=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}programArgs=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){}function initRuntime(){runtimeInitialized=true;wasmExports["f"]()}function postRun(){}function abort(what){what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("guetzli.wasm")}return new URL("guetzli.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;wasmExports=applySignatureConversions(wasmExports);assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var __abort_js=()=>abort("");var getHeapMax=()=>17179869184;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(BigInt(pages));updateMemoryViews();return 1}catch(e){}};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);var HEAPU8;function _emscripten_resize_heap(requestedSize){requestedSize=bigintToI53Checked(requestedSize);var oldSize=HEAPU8.length;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false}var keepRuntimeAlive=()=>true;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var HEAPU64;function _fd_write(fd,iov,iovcnt,pnum){iov=bigintToI53Checked(iov);iovcnt=bigintToI53Checked(iovcnt);pnum=bigintToI53Checked(pnum);var num=0;for(var i=0;i(a0,a1,a2)=>f(BigInt(a0),BigInt(a1),a2);var makeWrapper_p=f=>()=>Number(f());var makeWrapper_pp=f=>a0=>Number(f(BigInt(a0)));var makeWrapper__p=f=>a0=>f(BigInt(a0));wasmExports["g"]=makeWrapper__pp_(wasmExports["g"]);wasmExports["h"]=makeWrapper_p(wasmExports["h"]);wasmExports["i"]=makeWrapper_p(wasmExports["i"]);wasmExports["j"]=makeWrapper_pp(wasmExports["j"]);wasmExports["k"]=makeWrapper__p(wasmExports["k"]);wasmExports["_emscripten_stack_restore"]=makeWrapper__p(wasmExports["_emscripten_stack_restore"]);wasmExports["_emscripten_stack_alloc"]=makeWrapper_pp(wasmExports["_emscripten_stack_alloc"]);wasmExports["emscripten_stack_get_current"]=makeWrapper_p(wasmExports["emscripten_stack_get_current"]);return wasmExports}async function run(){preRun();if(ABORT)return;initRuntime();postRun()}var wasmExports;wasmExports=await createWasm();await run(); +;return Module}export default Module; diff --git a/packages/optimizt-guetzli/dist/guetzli.wasm b/packages/optimizt-guetzli/dist/guetzli.wasm new file mode 100755 index 0000000000000000000000000000000000000000..68f8c778e4a4e7132fcaddbd66acc70a1f3d7fa2 GIT binary patch literal 199991 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 literal 0 HcmV?d00001 diff --git a/packages/optimizt-guetzli/eslint.config.js b/packages/optimizt-guetzli/eslint.config.js new file mode 100644 index 0000000..a9cf6f1 --- /dev/null +++ b/packages/optimizt-guetzli/eslint.config.js @@ -0,0 +1,29 @@ +import config from '@343dev/eslint-config'; + +export default [ + { + ignores: [ + '.cache/**', + 'dist/**', + 'upstream/**', + ], + }, + ...config, + { + rules: { + 'unicorn/consistent-boolean-name': 'off', + 'unicorn/no-computed-property-existence-check': 'off', + 'unicorn/no-negated-array-predicate': 'off', + 'unicorn/no-return-array-push': 'off', + 'unicorn/no-top-level-assignment-in-function': 'off', + 'unicorn/prefer-await': 'off', + 'unicorn/prefer-continue': 'off', + 'unicorn/prefer-early-return': 'off', + 'unicorn/prefer-iterator-to-array': 'off', + 'unicorn/prefer-minimal-ternary': 'off', + 'unicorn/prefer-number-is-safe-integer': 'off', + 'unicorn/prefer-promise-with-resolvers': 'off', + 'unicorn/require-array-sort-compare': 'off', + }, + }, +]; diff --git a/packages/optimizt-guetzli/package.json b/packages/optimizt-guetzli/package.json new file mode 100644 index 0000000..15cf00c --- /dev/null +++ b/packages/optimizt-guetzli/package.json @@ -0,0 +1,30 @@ +{ + "name": "@343dev/optimizt-guetzli", + "version": "1.0.1-0", + "private": true, + "description": "Internal Guetzli WebAssembly runner for Optimizt", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/343dev/optimizt.git", + "directory": "packages/optimizt-guetzli" + }, + "type": "module", + "scripts": { + "setup-emsdk": "./scripts/setup-emsdk.sh", + "build:wasm": "./scripts/build-wasm.sh", + "build:native": "./scripts/build-native.sh", + "benchmark": "node ./scripts/benchmark.js", + "lint": "npx eslint ./", + "test": "node --test --test-concurrency=1", + "verify:upstream": "node ./scripts/verify-upstream.js", + "verify:dist": "./scripts/verify-dist.sh" + }, + "devDependencies": { + "@343dev/eslint-config": "5.0.0", + "eslint": "10.5.0" + }, + "engines": { + "node": ">=24.0.0" + } +} diff --git a/packages/optimizt-guetzli/scripts/benchmark.js b/packages/optimizt-guetzli/scripts/benchmark.js new file mode 100644 index 0000000..82c691e --- /dev/null +++ b/packages/optimizt-guetzli/scripts/benchmark.js @@ -0,0 +1,132 @@ +import { spawn } from 'node:child_process'; +import { mkdtemp, readFile, rm, stat } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import createModule from '../dist/guetzli.mjs'; + +const quality = 95; + +function usage() { + return 'Usage: node scripts/benchmark.js <1mp.jpg> <4mp.jpg> [8mp.jpg]\n'; +} + +function enforceThresholds(results) { + const failed = results.filter(result => result.wasmToNativeRatio > 2); + if (failed.length > 0) { + throw new Error( + `WebAssembly/native duration exceeds 2x for ${failed.map(result => result.fixture).join(', ')}`, + ); + } +} + +async function medianOperation(operation) { + await operation(); + const measurements = []; + for (let index = 0; index < 3; index += 1) { + const started = process.hrtime.bigint(); + const result = await operation(); + measurements.push({ + milliseconds: Number(process.hrtime.bigint() - started) / 1e6, + ...result, + }); + } + return measurements.toSorted( + (left, right) => left.milliseconds - right.milliseconds, + )[1]; +} + +async function wasmEncode(module, input) { + const inputPointer = module._malloc(input.byteLength); + if (inputPointer === 0) { + throw new Error('Could not allocate WebAssembly input memory'); + } + try { + module.HEAPU8.set(input, inputPointer); + if (module._guetzli_encode(inputPointer, input.byteLength, quality) !== 0) { + throw new Error('Guetzli encoding failed'); + } + } finally { + module._free(inputPointer); + } + return module._guetzli_output_size(); +} + +async function nativeEncode(binary, quality, inputPath, outputPath) { + await new Promise((resolve, reject) => { + const child = spawn(binary, [String(quality), inputPath, outputPath], { stdio: 'ignore' }); + child.once('error', reject); + child.once('exit', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Native reference exited with status ${code}`)); + } + }); + }); + const outputStatistics = await stat(outputPath); + return { outputBytes: outputStatistics.size }; +} + +async function measure(inputPath) { + const defaultBinary = process.env.NATIVE_OUTPUT_DIR + ? path.join(process.env.NATIVE_OUTPUT_DIR, 'guetzli-native-reference') + : fileURLToPath(new URL( + '../.cache/native-reference/guetzli-native-reference', + import.meta.url, + )); + const binary = process.env.GUETZLI_NATIVE_REFERENCE ?? defaultBinary; + const directory = await mkdtemp(path.join(tmpdir(), 'guetzli-benchmark-')); + const nativeOutput = path.join(directory, 'native.jpg'); + try { + const input = await readFile(inputPath); + const initialization = await medianOperation(async () => { + const module = await import(`../dist/guetzli.mjs?benchmark=${Math.random()}`); + await module.default(); + return {}; + }); + const module = await createModule(); + const native = await medianOperation( + async () => nativeEncode( + binary, + quality, + inputPath, + nativeOutput, + ), + ); + const wasm = await medianOperation(async () => ({ + outputBytes: await wasmEncode(module, input), + })); + return { + fixture: path.basename(inputPath), + inputBytes: input.byteLength, + nativeMilliseconds: native.milliseconds, + nativeOutputBytes: native.outputBytes, + wasmMilliseconds: wasm.milliseconds, + wasmOutputBytes: wasm.outputBytes, + wasmToNativeRatio: wasm.milliseconds / native.milliseconds, + wasmInitializationMilliseconds: initialization.milliseconds, + }; + } finally { + await rm(directory, { recursive: true, force: true }); + } +} + +if (process.argv.length < 4 || process.argv.length > 5) { + process.stderr.write(usage()); + process.exitCode = 2; +} else { + const results = []; + for (const inputPath of process.argv.slice(2)) { + results.push(await measure(inputPath)); + } + process.stdout.write(`${JSON.stringify({ + node: process.version, + platform: `${process.platform}-${process.arch}`, + quality, + results, + }, undefined, '\t')}\n`); + enforceThresholds(results); +} diff --git a/packages/optimizt-guetzli/scripts/build-native.sh b/packages/optimizt-guetzli/scripts/build-native.sh new file mode 100755 index 0000000..75eb212 --- /dev/null +++ b/packages/optimizt-guetzli/scripts/build-native.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly UPSTREAM="$ROOT/upstream/guetzli" +readonly OUTPUT_DIR="${NATIVE_OUTPUT_DIR:-$ROOT/.cache/native-reference}" +readonly CXX="${CXX:-g++}" +readonly EXPECTED_VERSION='14.2.0' + +actual_version="$($CXX -dumpfullversion -dumpversion)" +if [[ "$actual_version" != "$EXPECTED_VERSION" ]]; then + printf 'Expected GCC %s, got %s from %s\n' \ + "$EXPECTED_VERSION" "$actual_version" "$CXX" >&2 + exit 1 +fi + +for source_dir in \ + "$UPSTREAM/guetzli" \ + "$UPSTREAM/third_party/butteraugli"; do + if [[ ! -d "$source_dir" ]]; then + printf 'Missing source directory %s\n' "$source_dir" >&2 + exit 1 + fi +done + +sources=() +mapfile -d '' sources < <( + find "$UPSTREAM/guetzli" "$UPSTREAM/third_party/butteraugli" \ + -type f -name '*.cc' \ + ! -name 'guetzli.cc' \ + ! -name 'butteraugli_main.cc' \ + -print0 | sort -z +) +if [[ ${#sources[@]} -eq 0 ]]; then + printf 'No Guetzli sources found under %s\n' "$UPSTREAM" >&2 + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" + +"$CXX" \ + -O3 \ + -std=c++11 \ + -Wall \ + -Wextra \ + -Werror \ + -I"$UPSTREAM" \ + -I"$UPSTREAM/third_party/butteraugli" \ + -c "$ROOT/scripts/native-reference.cc" \ + -o "$OUTPUT_DIR/native-reference.o" + +"$CXX" \ + -O3 \ + -std=c++11 \ + -I"$UPSTREAM" \ + -I"$UPSTREAM/third_party/butteraugli" \ + "$OUTPUT_DIR/native-reference.o" \ + "${sources[@]}" \ + -o "$OUTPUT_DIR/guetzli-native-reference" + +printf 'Built %s\n' "$OUTPUT_DIR/guetzli-native-reference" diff --git a/packages/optimizt-guetzli/scripts/build-wasm.sh b/packages/optimizt-guetzli/scripts/build-wasm.sh new file mode 100755 index 0000000..a55c003 --- /dev/null +++ b/packages/optimizt-guetzli/scripts/build-wasm.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly UPSTREAM="$ROOT/upstream/guetzli" +readonly OUTPUT_DIR="${WASM_OUTPUT_DIR:-$ROOT/dist}" +readonly EXPECTED_VERSION='6.0.9' +readonly SDK_DIR="${EMSDK_INSTALL_DIR:-$ROOT/.cache/emsdk}" +readonly EMXX="$SDK_DIR/upstream/emscripten/em++" + +if [[ ! -x "$EMXX" ]]; then + printf 'Emscripten is not installed at %s. Run npm run setup-emsdk.\n' \ + "$SDK_DIR" >&2 + exit 1 +fi + +actual_version="$($EMXX --version | head -n 1)" +if [[ "$actual_version" != *" $EXPECTED_VERSION "* ]]; then + printf 'Expected Emscripten %s, got: %s\n' \ + "$EXPECTED_VERSION" "$actual_version" >&2 + exit 1 +fi + +for source_dir in \ + "$UPSTREAM/guetzli" \ + "$UPSTREAM/third_party/butteraugli"; do + if [[ ! -d "$source_dir" ]]; then + printf 'Missing source directory %s\n' "$source_dir" >&2 + exit 1 + fi +done + +sources=() +mapfile -d '' sources < <( + find "$UPSTREAM/guetzli" "$UPSTREAM/third_party/butteraugli" \ + -type f -name '*.cc' \ + ! -name 'guetzli.cc' \ + ! -name 'butteraugli_main.cc' \ + ! -name 'debug_print.cc' \ + -print0 | sort -z +) +if [[ ${#sources[@]} -eq 0 ]]; then + printf 'No Guetzli sources found under %s\n' "$UPSTREAM" >&2 + exit 1 +fi + +mkdir -p "$OUTPUT_DIR" + +# Compile the integration code separately so new warnings fail the build while +# warning-only issues in the unmodified 2017 upstream snapshot remain visible. +"$EMXX" \ + -m64 \ + -O3 \ + -DNDEBUG \ + -std=c++11 \ + -Wall \ + -Wextra \ + -Werror \ + -fno-exceptions \ + -I"$ROOT/src/wasm" \ + -I"$UPSTREAM" \ + -I"$UPSTREAM/third_party/butteraugli" \ + -c "$ROOT/src/wasm/bridge.cc" \ + -o "$OUTPUT_DIR/bridge.o" + +objects=("$OUTPUT_DIR/bridge.o") +for source in "${sources[@]}"; do + relative="${source#"$UPSTREAM/"}" + object="$OUTPUT_DIR/upstream/${relative%.cc}.o" + mkdir -p "$(dirname "$object")" + "$EMXX" \ + -m64 \ + -O3 \ + -DNDEBUG \ + -std=c++11 \ + -fno-exceptions \ + -I"$ROOT/src/wasm" \ + -I"$UPSTREAM" \ + -I"$UPSTREAM/third_party/butteraugli" \ + -c "$source" \ + -o "$object" + objects+=("$object") +done + +"$EMXX" \ + -m64 \ + -O3 \ + -DNDEBUG \ + -std=c++11 \ + -fno-exceptions \ + "${objects[@]}" \ + -sMODULARIZE=1 \ + -sEXPORT_ES6=1 \ + -sENVIRONMENT=node \ + -sALLOW_MEMORY_GROWTH=1 \ + -sMAXIMUM_MEMORY=16GB \ + -sSIGNATURE_CONVERSIONS=guetzli_encode:_pp_,guetzli_output_data:p,guetzli_output_size:p \ + -sFILESYSTEM=0 \ + -sINCOMING_MODULE_JS_API= \ + -sEXPORTED_FUNCTIONS=_malloc,_free,_guetzli_encode,_guetzli_output_data,_guetzli_output_size \ + -sEXPORTED_RUNTIME_METHODS=HEAPU8,UTF8ToString \ + -o "$OUTPUT_DIR/guetzli.mjs" + +rm -f "$OUTPUT_DIR/bridge.o" +rm -rf "$OUTPUT_DIR/upstream" + +printf 'Built %s and %s\n' \ + "$OUTPUT_DIR/guetzli.mjs" "$OUTPUT_DIR/guetzli.wasm" diff --git a/packages/optimizt-guetzli/scripts/generate-benchmark-fixtures.py b/packages/optimizt-guetzli/scripts/generate-benchmark-fixtures.py new file mode 100644 index 0000000..4cab932 --- /dev/null +++ b/packages/optimizt-guetzli/scripts/generate-benchmark-fixtures.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Generate local JPEG fixtures used by scripts/benchmark.js. + +Requires Pillow 12.3.0. Output is intentionally outside the repository. +""" + +import sys +from pathlib import Path + +import PIL +from PIL import Image, features + +SIZES = { + "1mp": (1000, 1000), + "4mp": (2000, 2000), + "8mp": (2828, 2828), +} + + +def assert_toolchain() -> None: + versions = { + "Pillow": PIL.__version__, + "libjpeg-turbo": features.version("libjpeg_turbo"), + } + expected = {"Pillow": "12.3.0", "libjpeg-turbo": "3.1.4.1"} + if versions != expected: + raise RuntimeError(f"benchmark toolchain must be {expected}, got {versions}") + + +def image(width: int, height: int) -> Image.Image: + result = Image.new("RGB", (width, height)) + pixels = result.load() + for y in range(height): + for x in range(width): + pixels[x, y] = ( + (x * 3 + y) % 256, + (y * 5 + x) % 256, + ((x ^ y) * 7) % 256, + ) + return result + + +def main() -> None: + assert_toolchain() + output = Path(sys.argv[1] if len(sys.argv) > 1 else ".cache/benchmarks") + output.mkdir(parents=True, exist_ok=True) + for name, dimensions in SIZES.items(): + image(*dimensions).save( + output / f"{name}.jpg", + quality=98, + subsampling=0, + optimize=False, + progressive=False, + ) + + +if __name__ == "__main__": + main() diff --git a/packages/optimizt-guetzli/scripts/generate-fixtures.py b/packages/optimizt-guetzli/scripts/generate-fixtures.py new file mode 100755 index 0000000..f19da89 --- /dev/null +++ b/packages/optimizt-guetzli/scripts/generate-fixtures.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Generate the reproducible JPEG parity fixtures. + +Requires Pillow 12.3.0. The immutable floating-point boundary fixture is not +created here; see test/fixtures/README.md. +""" + +from pathlib import Path + +import PIL +from PIL import Image, features + +WIDTH = 96 +HEIGHT = 64 +FIXTURES = Path(__file__).resolve().parents[1] / "test" / "fixtures" + + +def assert_toolchain() -> None: + versions = { + "Pillow": PIL.__version__, + "libjpeg-turbo": features.version("libjpeg_turbo"), + } + expected = {"Pillow": "12.3.0", "libjpeg-turbo": "3.1.4.1"} + if versions != expected: + raise RuntimeError(f"fixture toolchain must be {expected}, got {versions}") + + +def rgb_image() -> Image.Image: + image = Image.new("RGB", (WIDTH, HEIGHT)) + pixels = image.load() + for y in range(HEIGHT): + for x in range(WIDTH): + pixels[x, y] = ((x * 3) % 256, (y * 5) % 256, ((x ^ y) * 7) % 256) + return image + + +def grayscale_image() -> Image.Image: + image = Image.new("RGB", (WIDTH, HEIGHT)) + pixels = image.load() + for y in range(HEIGHT): + for x in range(WIDTH): + value = (x * 3 + y * 5) % 256 + pixels[x, y] = (value, value, value) + return image + + +def save(image: Image.Image, name: str, **options: object) -> None: + image.save( + FIXTURES / name, + quality=98, + optimize=False, + progressive=False, + **options, + ) + + +def main() -> None: + assert_toolchain() + FIXTURES.mkdir(parents=True, exist_ok=True) + + rgb = rgb_image() + save(rgb, "rgb-444.jpg", subsampling=0) + save(rgb, "rgb-420.jpg", subsampling=2) + rgb.save( + FIXTURES / "progressive-420.jpg", + quality=98, + subsampling=2, + optimize=False, + progressive=True, + ) + save( + rgb, + "metadata-444.jpg", + subsampling=0, + exif=b"Exif\0\0fixture metadata", + icc_profile=b"fixture ICC profile", + ) + + grayscale = grayscale_image() + save(grayscale, "grayscale-ycbcr.jpg", subsampling=2) + save(grayscale.convert("L"), "grayscale-single-component.jpg") + (FIXTURES / "invalid.jpg").write_bytes(b"not a jpeg") + + +if __name__ == "__main__": + main() diff --git a/packages/optimizt-guetzli/scripts/native-reference.cc b/packages/optimizt-guetzli/scripts/native-reference.cc new file mode 100644 index 0000000..57aeb65 --- /dev/null +++ b/packages/optimizt-guetzli/scripts/native-reference.cc @@ -0,0 +1,91 @@ +/* + * Copyright 2026 343dev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include + +#include "guetzli/processor.h" +#include "guetzli/quality.h" +#include "guetzli/stats.h" + +int main(int argc, char** argv) { + if (argc != 4) { + std::cerr << "Usage: native-reference QUALITY INPUT OUTPUT\n"; + return 2; + } + + const char* quality_text = argv[1]; + bool digits_only = *quality_text != '\0'; + for (const char* character = quality_text; + digits_only && *character != '\0'; ++character) { + digits_only = + std::isdigit(static_cast(*character)) != 0; + } + if (!digits_only) { + std::cerr << "QUALITY must be an integer\n"; + return 2; + } + + int quality; + try { + quality = std::stoi(quality_text); + } catch (const std::exception&) { + std::cerr << "QUALITY must be an integer\n"; + return 2; + } + if (quality < 84 || quality > 110) { + std::cerr << "QUALITY must be between 84 and 110\n"; + return 2; + } + + std::ifstream input_file(argv[2], std::ios::binary); + if (!input_file) { + std::cerr << "Could not open input\n"; + return 3; + } + const std::string input((std::istreambuf_iterator(input_file)), + std::istreambuf_iterator()); + if (input_file.bad()) { + std::cerr << "Could not read input\n"; + return 3; + } + + guetzli::Params params; + params.butteraugli_target = static_cast( + guetzli::ButteraugliScoreForQuality(quality)); + guetzli::ProcessStats stats; + std::string output; + if (!guetzli::Process(params, &stats, input, &output)) { + std::cerr << "Guetzli processing failed\n"; + return 1; + } + + std::ofstream output_file(argv[3], std::ios::binary | std::ios::trunc); + if (!output_file) { + std::cerr << "Could not open output\n"; + return 3; + } + output_file.write(output.data(), output.size()); + if (!output_file) { + std::cerr << "Could not write output\n"; + return 3; + } + return 0; +} diff --git a/packages/optimizt-guetzli/scripts/setup-emsdk.sh b/packages/optimizt-guetzli/scripts/setup-emsdk.sh new file mode 100755 index 0000000..f998c31 --- /dev/null +++ b/packages/optimizt-guetzli/scripts/setup-emsdk.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly SDK_DIR="${EMSDK_INSTALL_DIR:-$ROOT/.cache/emsdk}" +readonly EMSDK_VERSION='6.0.9' +readonly EMSDK_COMMIT='5eb0bde7585670252e8ba05e9d361627bffd08b5' + +if [[ -e "$SDK_DIR" && ! -d "$SDK_DIR/.git" ]]; then + printf '%s exists but is not an emsdk Git checkout\n' "$SDK_DIR" >&2 + exit 1 +fi + +if [[ ! -d "$SDK_DIR/.git" ]]; then + mkdir -p "$(dirname "$SDK_DIR")" + git clone --filter=blob:none --no-checkout \ + https://github.com/emscripten-core/emsdk.git "$SDK_DIR" + git -C "$SDK_DIR" fetch --depth 1 origin "$EMSDK_COMMIT" + git -C "$SDK_DIR" checkout --detach "$EMSDK_COMMIT" +fi + +actual_commit="$(git -C "$SDK_DIR" rev-parse HEAD 2>/dev/null || true)" +if [[ "$actual_commit" != "$EMSDK_COMMIT" ]]; then + printf 'Expected emsdk commit %s, found %s in %s\n' \ + "$EMSDK_COMMIT" "$actual_commit" "$SDK_DIR" >&2 + printf 'Remove that checkout or choose another EMSDK_INSTALL_DIR.\n' >&2 + exit 1 +fi + +if [[ ! -x "$SDK_DIR/emsdk" ]]; then + git -C "$SDK_DIR" reset --hard "$EMSDK_COMMIT" +fi + +"$SDK_DIR/emsdk" install "$EMSDK_VERSION" +"$SDK_DIR/emsdk" activate "$EMSDK_VERSION" + +actual_version="$($SDK_DIR/upstream/emscripten/em++ --version | head -n 1)" +if [[ "$actual_version" != *" $EMSDK_VERSION "* ]]; then + printf 'Expected Emscripten %s, got: %s\n' \ + "$EMSDK_VERSION" "$actual_version" >&2 + exit 1 +fi + +printf 'Emscripten %s is ready at %s\n' "$EMSDK_VERSION" "$SDK_DIR" diff --git a/packages/optimizt-guetzli/scripts/verify-dist.sh b/packages/optimizt-guetzli/scripts/verify-dist.sh new file mode 100755 index 0000000..beca937 --- /dev/null +++ b/packages/optimizt-guetzli/scripts/verify-dist.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly TEMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TEMP_DIR"' EXIT + +WASM_OUTPUT_DIR="$TEMP_DIR" "$ROOT/scripts/build-wasm.sh" + +for artifact in guetzli.mjs guetzli.wasm; do + if ! cmp -s "$ROOT/dist/$artifact" "$TEMP_DIR/$artifact"; then + printf 'Committed dist/%s differs from a canonical rebuild.\n' \ + "$artifact" >&2 + exit 1 + fi +done + +if grep -aF "$ROOT" "$ROOT/dist/guetzli.mjs" "$ROOT/dist/guetzli.wasm" >/dev/null; then + printf 'Generated distribution contains the repository absolute path.\n' >&2 + exit 1 +fi + +( + cd /tmp + GUETZLI_VERIFY_ROOT="$ROOT" node --input-type=module <<'EOF' +import { pathToFileURL } from 'node:url'; + +const modulePath = `${process.env.GUETZLI_VERIFY_ROOT}/dist/guetzli.mjs`; +const createModule = (await import(pathToFileURL(modulePath).href)).default; +const module = await createModule(); +const largeAllocation = module._malloc(5 * 1024 ** 3); +if (largeAllocation === 0) { + throw new Error('WebAssembly distribution cannot allocate beyond 4 GiB'); +} +const probe = largeAllocation + 4 * 1024 ** 3; +module.HEAPU8[probe] = 123; +if (module.HEAPU8[probe] !== 123) { + throw new Error('WebAssembly distribution cannot address memory beyond 4 GiB'); +} +module._free(largeAllocation); +const expected = [ + '_free', + '_guetzli_encode', + '_guetzli_output_data', + '_guetzli_output_size', + '_malloc', +]; +const actual = Object.keys(module) + .filter(name => name.startsWith('_') && typeof module[name] === 'function') + .toSorted(); +if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`Unexpected runtime functions: ${actual.join(', ')}`); +} +EOF +) + +printf 'Verified committed WebAssembly distribution.\n' diff --git a/packages/optimizt-guetzli/scripts/verify-native-parity.js b/packages/optimizt-guetzli/scripts/verify-native-parity.js new file mode 100644 index 0000000..b49028d --- /dev/null +++ b/packages/optimizt-guetzli/scripts/verify-native-parity.js @@ -0,0 +1,68 @@ +import { spawn } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import manifest from '../test/parity-manifest.json' with { type: 'json' }; + +const binary = process.env.GUETZLI_NATIVE_REFERENCE; +if (!binary) { + throw new Error('GUETZLI_NATIVE_REFERENCE must point to the native reference binary'); +} + +function sha256(value) { + return createHash('sha256').update(value).digest('hex'); +} + +async function encode(inputPath, quality, outputPath) { + await new Promise((resolve, reject) => { + const child = spawn(binary, [String(quality), inputPath, outputPath], { + stdio: 'inherit', + }); + child.once('error', reject); + child.once('exit', (code, signal) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error( + signal + ? `Native reference was terminated by ${signal}` + : `Native reference exited with status ${code}`, + )); + }); + }); +} + +const cases = Object.entries(manifest.cases ?? {}); +if (cases.length === 0) { + throw new Error('Parity manifest contains no cases to verify'); +} + +const directory = await mkdtemp(path.join(tmpdir(), 'guetzli-native-parity-')); +try { + // Keep encodings serial: each Guetzli process can consume hundreds of MiB. + for (const [fixture, qualities] of cases) { + const inputPath = fileURLToPath(new URL(`../test/fixtures/${fixture}`, import.meta.url)); + for (const [qualityText, expectation] of Object.entries(qualities)) { + const outputPath = path.join(directory, `${fixture}-${qualityText}.jpg`); + await encode(inputPath, Number(qualityText), outputPath); + const output = await readFile(outputPath); + const actualSha256 = sha256(output); + if (output.byteLength !== expectation.size || actualSha256 !== expectation.sha256) { + throw new Error( + `Native parity mismatch for ${fixture} at quality ${qualityText}: ` + + `expected size ${expectation.size}, sha256 ${expectation.sha256}; ` + + `actual size ${output.byteLength}, sha256 ${actualSha256}`, + ); + } + } + } +} finally { + await rm(directory, { recursive: true, force: true }); +} + +process.stdout.write('Verified native parity baselines.\n'); diff --git a/packages/optimizt-guetzli/scripts/verify-upstream.js b/packages/optimizt-guetzli/scripts/verify-upstream.js new file mode 100644 index 0000000..6d7a6ee --- /dev/null +++ b/packages/optimizt-guetzli/scripts/verify-upstream.js @@ -0,0 +1,49 @@ +import { createHash } from 'node:crypto'; +import { readFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; + +const root = new URL('../', import.meta.url); +const upstreamRoot = new URL('../upstream/guetzli/', import.meta.url); +const manifestUrl = new URL('../upstream/guetzli.sha256', import.meta.url); + +async function listFiles(directory, prefix = '') { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const relative = path.posix.join(prefix, entry.name); + if (entry.isDirectory()) { + files.push(...await listFiles(new URL(`${relative}/`, upstreamRoot), relative)); + } else if (entry.isFile()) { + files.push(relative); + } else { + throw new Error(`Unexpected non-file in upstream snapshot: ${relative}`); + } + } + return files; +} + +const manifest = await readFile(manifestUrl, 'utf8'); +const expected = new Map(); +for (const line of manifest.trimEnd().split('\n')) { + const match = /^(?[0-9a-f]{64}) {2}\.\/(?.+)$/.exec(line); + if (!match) { + throw new Error(`Malformed upstream manifest line: ${line}`); + } + expected.set(match.groups.file, match.groups.hash); +} + +const discoveredFiles = await listFiles(upstreamRoot); +const files = discoveredFiles.toSorted(); +if (files.join('\n') !== [...expected.keys()].join('\n')) { + throw new Error('Upstream snapshot file list differs from upstream/guetzli.sha256'); +} + +for (const file of files) { + const bytes = await readFile(new URL(`upstream/guetzli/${file}`, root)); + const actual = createHash('sha256').update(bytes).digest('hex'); + if (actual !== expected.get(file)) { + throw new Error(`Upstream hash mismatch: ${file}`); + } +} + +console.log(`Verified ${files.length} upstream Guetzli files.`); diff --git a/packages/optimizt-guetzli/src/wasm/bridge.cc b/packages/optimizt-guetzli/src/wasm/bridge.cc new file mode 100644 index 0000000..cf37105 --- /dev/null +++ b/packages/optimizt-guetzli/src/wasm/bridge.cc @@ -0,0 +1,62 @@ +/* + * Copyright 2026 343dev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "guetzli/processor.h" +#include "guetzli/quality.h" +#include "guetzli/stats.h" + +namespace { + +std::string output; + +} // namespace + +extern "C" { + +int guetzli_encode(const std::uint8_t* input, + std::size_t input_size, + int quality) { + output.clear(); + if (input == nullptr || input_size == 0 || quality < 84 || quality > 110) { + return 1; + } + + const std::string input_data(reinterpret_cast(input), + input_size); + guetzli::Params parameters; + parameters.butteraugli_target = static_cast( + guetzli::ButteraugliScoreForQuality(quality)); + guetzli::ProcessStats statistics; + if (!guetzli::Process(parameters, &statistics, input_data, &output)) { + output.clear(); + return 1; + } + return 0; +} + +const std::uint8_t* guetzli_output_data() { + return reinterpret_cast(output.data()); +} + +std::size_t guetzli_output_size() { + return output.size(); +} + +} // extern "C" diff --git a/packages/optimizt-guetzli/src/wasm/guetzli/debug_print.h b/packages/optimizt-guetzli/src/wasm/guetzli/debug_print.h new file mode 100644 index 0000000..19f8f07 --- /dev/null +++ b/packages/optimizt-guetzli/src/wasm/guetzli/debug_print.h @@ -0,0 +1,23 @@ +/* + * Copyright 2026 343dev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_DEBUG_PRINT_H_ +#define GUETZLI_DEBUG_PRINT_H_ + +#define GUETZLI_LOG(stats, ...) do { } while (0) +#define GUETZLI_LOG_QUANT(stats, q) do { } while (0) + +#endif // GUETZLI_DEBUG_PRINT_H_ diff --git a/packages/optimizt-guetzli/test/PARITY.md b/packages/optimizt-guetzli/test/PARITY.md new file mode 100644 index 0000000..87eff0b --- /dev/null +++ b/packages/optimizt-guetzli/test/PARITY.md @@ -0,0 +1,16 @@ +# Native and WebAssembly parity + +`test/parity-manifest.json` records the output of the canonical Linux x64 native reference for each fixture and quality. Every WebAssembly result must match those bytes except for the single case below. A new mismatch is a release blocker; it must not be accepted automatically from a perceptual threshold. + +## Accepted equivalent case + +`floating-point-boundary-444.jpg` at quality 95 crosses a floating-point decision boundary. The canonical native result is 2,507 bytes, while wasm32 produces 2,505 bytes. The outputs decode to the same dimensions and were investigated with these results: + +- 260 of 6,144 pixels differ; +- maximum channel delta: 12; +- mean absolute channel delta: 0.05398; +- RMSE: 0.41855; +- Butteraugli distance: 0.66740; +- upstream “good” threshold: 0.76548. + +This exception applies only to that immutable input hash and quality 95. Its native hash remains in the manifest; the test also pins the accepted WebAssembly hash. diff --git a/packages/optimizt-guetzli/test/fixtures/README.md b/packages/optimizt-guetzli/test/fixtures/README.md new file mode 100644 index 0000000..0b6093d --- /dev/null +++ b/packages/optimizt-guetzli/test/fixtures/README.md @@ -0,0 +1,45 @@ +# JPEG fixtures + +The fixtures are intentionally small because Guetzli is CPU- and memory-intensive. Their byte hashes are part of `test/parity-manifest.json`. + +## Reproducible fixtures + +Run the generator with the exact toolchain: + +```sh +python3 -m venv .cache/fixtures-venv +.cache/fixtures-venv/bin/pip install Pillow==12.3.0 +.cache/fixtures-venv/bin/python scripts/generate-fixtures.py +``` + +The generator rejects other versions. The pinned wheel used for the baseline contains Pillow 12.3.0 and libjpeg-turbo 3.1.4.1. + +| Fixture | Purpose | +| --- | --- | +| `rgb-444.jpg` | Baseline YCbCr 4:4:4 JPEG | +| `rgb-420.jpg` | Baseline YCbCr 4:2:0 JPEG | +| `progressive-420.jpg` | Progressive YCbCr 4:2:0 JPEG | +| `metadata-444.jpg` | 4:4:4 JPEG with EXIF and ICC APP segments; Guetzli removes them | +| `grayscale-ycbcr.jpg` | Supported three-component YCbCr JPEG with grayscale pixel content | +| `grayscale-single-component.jpg` | Well-formed but unsupported single-component grayscale JPEG | +| `invalid.jpg` | Malformed input | + +The generated pixel patterns are defined in `scripts/generate-fixtures.py`. Do not edit the JPEG bytes manually. + +## Floating-point boundary fixture + +`floating-point-boundary-444.jpg` is an immutable regression artifact. It was produced on Linux arm64 from the same 96×64 RGB pixel pattern as the baseline RGB fixture, using: + +- sharp 0.35.4; +- libvips 8.18.6; +- mozjpeg revision `0826579`; +- JPEG quality 98; +- 4:4:4 chroma subsampling. + +Its SHA-256 is: + +```text +5a9d67c90a42940b4159e4c99423436af00c4bcb43d03fae09071b31561dba25 +``` + +At Guetzli quality 95, this input causes a small, investigated floating-point divergence between the canonical native reference and WebAssembly. It is retained specifically to prevent that difference from being hidden by fixtures that happen to produce byte parity. Do not regenerate it with a newer sharp or libvips release; changing the input requires a new parity investigation. diff --git a/packages/optimizt-guetzli/test/fixtures/floating-point-boundary-444.jpg b/packages/optimizt-guetzli/test/fixtures/floating-point-boundary-444.jpg new file mode 100644 index 0000000000000000000000000000000000000000..079dda0b5cb36eabb8bd67c62f7eb4efe69be2f0 GIT binary patch literal 4520 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 literal 0 HcmV?d00001 diff --git a/packages/optimizt-guetzli/test/fixtures/grayscale-single-component.jpg b/packages/optimizt-guetzli/test/fixtures/grayscale-single-component.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8adbd3a945d7b114b3a843ca4171f5ded73f589e GIT binary patch literal 2928 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 literal 0 HcmV?d00001 diff --git a/packages/optimizt-guetzli/test/fixtures/invalid.jpg b/packages/optimizt-guetzli/test/fixtures/invalid.jpg new file mode 100644 index 0000000..9743eac --- /dev/null +++ b/packages/optimizt-guetzli/test/fixtures/invalid.jpg @@ -0,0 +1 @@ +not a jpeg \ No newline at end of file diff --git a/packages/optimizt-guetzli/test/fixtures/metadata-444.jpg b/packages/optimizt-guetzli/test/fixtures/metadata-444.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fca0c3e2c13d377868a227318ae587f7ed47731d GIT binary patch literal 7671 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 XRe8_tcI9?*OPl+dH5@0C`L6!~^#_M& literal 0 HcmV?d00001 diff --git a/packages/optimizt-guetzli/test/fixtures/rgb-420.jpg b/packages/optimizt-guetzli/test/fixtures/rgb-420.jpg new file mode 100644 index 0000000000000000000000000000000000000000..76d57347cd141c91cb87e720a93288a3b49ece46 GIT binary patch literal 4475 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 fpiaB { + const module = await createModule(); + const input = await readFile(new URL('fixtures/rgb-444.jpg', import.meta.url)); + await encode(module, input, 95); + + const invalid = await readFile(new URL('fixtures/invalid.jpg', import.meta.url)); + const inputPointer = module._malloc(invalid.byteLength); + assert.notEqual(inputPointer, 0); + try { + module.HEAPU8.set(invalid, inputPointer); + assert.notEqual(module._guetzli_encode(inputPointer, invalid.byteLength, 95), 0); + } finally { + module._free(inputPointer); + } + assert.equal(module._guetzli_output_size(), 0); +}); + +for (const [fixture, qualities] of Object.entries(manifest.cases)) { + test(`matches the native reference for ${fixture}`, async () => { + const module = await createModule(); + const input = await readFile(new URL(`fixtures/${fixture}`, import.meta.url)); + for (const [qualityText, expectation] of Object.entries(qualities)) { + const quality = Number(qualityText); + const output = await encode(module, input, quality); + if (expectation.parity === 'byte') { + assert.equal(output.length, expectation.size, `quality ${quality} size`); + assert.equal(sha256(output), expectation.sha256, `quality ${quality} hash`); + continue; + } + + assert.equal(expectation.parity, 'equivalent'); + assert.equal(sha256(input), expectation.inputSha256); + assert.equal(output.length, expectation.wasmSize); + assert.equal(sha256(output), expectation.wasmSha256); + } + }); +} diff --git a/packages/optimizt-guetzli/upstream/guetzli.sha256 b/packages/optimizt-guetzli/upstream/guetzli.sha256 new file mode 100644 index 0000000..f4f8ad3 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli.sha256 @@ -0,0 +1,73 @@ +dbe86138e6a880b3a741728e18d52307c83c1a302eb27868b6cea407ffe59531 ./.gitignore +7321cfc7998f6fe9015866ed12685aa637b0ae384dd451a59d8fcb9d6dc48ff7 ./BUILD +d070fce965bcdc24f31758a6e24006f716220b04d81092c55bca08531013a5eb ./CONTRIBUTING.md +cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 ./LICENSE +912d00f2110306754ac1c58b60a09604f7934af33857aa3ccbbd4a1120226c9b ./Makefile +b257b397725f9b5c89525a2c038788c134f63be1cf93396c0266b15872238ace ./README.md +cab774757a7a5ebd7f624847e265b895e1f5c45891f519b1ba068f8375540094 ./WORKSPACE +a4e0318e8ae47ee37cdb3f606d281e541f39fb756a942fe44bdb99907b116c45 ./guetzli.make +3dfd05457e02da274419798b1a880c634fe6b4d38495dc67847f6efffb5d4110 ./guetzli.sln +59279d79535eed8b9bf137db8ef867f2de742e54087a3000d7256754bc927bad ./guetzli.vcxproj +013aade3e7238eae4d2b72a86ef2b4cec6f30abc5c3b5bd1d11163b0aa3d6111 ./guetzli.vcxproj.filters +7ab87c805efc1cd4273752bcaaf38730f0b760ef20581dbfc6babafdb7fb8762 ./guetzli/butteraugli_comparator.cc +c6941ff6907c2dbbded7f113c8abde43cb8c7f19c06972a774a64418e2bf48f8 ./guetzli/butteraugli_comparator.h +07b81b66aeb7a43770331facda734780fdd9bf46d8a3afd341adafa29692278f ./guetzli/color_transform.h +e58611ca751f0f506de44147c14c43da14fedd947a93044127a439a0f29b7885 ./guetzli/comparator.h +959d1c80e9bae54a3ae1dc3b585603e72eb8faf449ee3b292b8ac95f11978d79 ./guetzli/dct_double.cc +cdc2a5ed33baf803aca0eff427b026bd7b03e381e36cfd0f064be8d0bbe28bd6 ./guetzli/dct_double.h +36a3ebf01107b17535711b647f2a227942ab8c5d4b6af40c7d6e9b686bae1758 ./guetzli/debug_print.cc +3e4bfe0457962b394d9ec5d7b8cfdf0a69b836b8296340a4fd23de4d5e4713ef ./guetzli/debug_print.h +d51feea107ebbdc13b033c8865a4eeae9538b099e2487becca8a6429f64744ed ./guetzli/entropy_encode.cc +3dbcdccf536439e4bab62d0d2896c0b99d2695a85bd8152c9e9d35852ddc4c6a ./guetzli/entropy_encode.h +961dace1c5d09b3bb54346306558f942dbd10d38602fa203cbaed5c7a66ac7c2 ./guetzli/fast_log.h +7472dd57e060bce59927bd144eb0f04649b16c369ec351371a6f0dc492f7a4a4 ./guetzli/fdct.cc +4c3b694f4064a351068df082dd982a0f48eb51b297c598b24fd0c747a10c7b84 ./guetzli/fdct.h +4fb621bb4ecb98eb0ae2bb2023ee35fe1f3ac439eb5e487f87228499bb86d92a ./guetzli/gamma_correct.cc +269e57ba48d76fdc0b87cbd3e4618f80add34f3dceb615f9c9d2dd5200cbe63b ./guetzli/gamma_correct.h +3e35abf463956250d0c04c8254e90b1ab75f46319eaa6087840837da943d8e9a ./guetzli/guetzli.cc +9e0ebb04c85e374fe936c6a5b1e2cdb00e8e89ea82bdc48978311d4c27191774 ./guetzli/idct.cc +e7ed8747c988e13b484731048068d380d87dce2544ff4f2f4d8e5866b0e66056 ./guetzli/idct.h +6488b65a47bba12f259880ba8b6fedfe357686c38a221bc1163b815d77a34690 ./guetzli/jpeg_bit_writer.h +00fa2dd9a09a17a1ec51024f4db20e5e8c6d80f3851abd7c6929799c0c9c6072 ./guetzli/jpeg_data.cc +03ad3e39aa4d92675a7ad15b9046bf02413348c3ab6281d21bad4ff7ad2e45e7 ./guetzli/jpeg_data.h +85221fbaf69180b9afc46883cb3c2def2a1b38fb7f5290af16db375680d3c2e6 ./guetzli/jpeg_data_decoder.cc +9fc01962cc4ac24e2a2d95090dea2b105250f9fa58ce3ad71c2601461400adf7 ./guetzli/jpeg_data_decoder.h +b1bb18b704b6668258db3a06074445967a2aa3d0302c55cef59769fe9a2075ec ./guetzli/jpeg_data_encoder.cc +9ecf04a04510e9f7ef7b99b7268e7956b4e57dce40a24a254f3ea49034613199 ./guetzli/jpeg_data_encoder.h +6269af626ed4fb2cf2c929cf7e93d3c4eb1e89390f07e4bcfea810b03075d3d0 ./guetzli/jpeg_data_reader.cc +73195ff127f98b1ef651fb7260be023efb023eb9ba6b9bcf883b2f2d9cf058af ./guetzli/jpeg_data_reader.h +8d3b8caa2845d7b77503673e6a72d5a01958d4537e2f67863f654953a8e18899 ./guetzli/jpeg_data_writer.cc +332c82342d5572cb2eb9930988c46698cbdda90ba8cc6ef031c060f3bbb75ef9 ./guetzli/jpeg_data_writer.h +8d9763b69d843339d2b6b331105d41341dd9258a0a10c62e84d4f389784f3568 ./guetzli/jpeg_error.h +33cec5cc38b6dc26b871dd2de48647d2f410fdcfb97d73eefe6297f7dedecb16 ./guetzli/jpeg_huffman_decode.cc +ab27b72b79e3079c0ee05ac16dc6b6080747715704ef4cbc94e5ce8e68172ef5 ./guetzli/jpeg_huffman_decode.h +c1c942cfe50e293c5e3f3710210e29671ed12d18b6ac68a7534a9202ed265d29 ./guetzli/order.inc +bdb96baeeb6fc98ddd7419419ca6c6dc62f26c901bd9be6384a7ebbda635fc68 ./guetzli/output_image.cc +08944c14a7d42001a5dda3b0225f5dd827a24f6a1871d1d8076521cf62759733 ./guetzli/output_image.h +d4a242e427f14a1d076213670c6eb7b05977a1e2848ce67bf628c9aad8e75135 ./guetzli/preprocess_downsample.cc +8e067fbc1f984f2887e886d3fb5737898d808f5c686016201e4b86ba925c6e28 ./guetzli/preprocess_downsample.h +1b8a3a528cf95025c4441e1f5b93204bea968037bab67222058fdd97dde1eed7 ./guetzli/processor.cc +cb903e647b9b48918a6df3fc3d57d078510f41610d80b85a5132367d8d367977 ./guetzli/processor.h +9990ebf52de2cf51f0deb21b6f9d09ad0ef0f79d39a65cd5f2afce10d01714d6 ./guetzli/quality.cc +e2cd7059df270f69d91ee5ca0062dd49fbfa104b7e3dfab762cec034673b17e6 ./guetzli/quality.h +f5d001ca7b17364826f98cc90bb13740306bee28b390534f44b12034b2c24748 ./guetzli/quantize.cc +04c36560b83fc8e99e0fd610584f1ac452484b8bf66a63f021d0581842d402d1 ./guetzli/quantize.h +e1a9f999ad8b2f8ab294c9c81b3f3f7f86c58a990184baa21714ba9fecce5e0e ./guetzli/score.cc +3938335f8e5aa2ddf0600a67885ad1da73e44371f0f4df7fec455516468c484e ./guetzli/score.h +a21077d74f0ba8a6f9ac4d8f71886d033e40e0f852b32c5201068a6ba6b0d4d5 ./guetzli/stats.h +970066cce1ed3d038d908076603387c533bd44cd726a1afee8f21d2adf8269c8 ./png.BUILD +9ff181d63a9ffcc4d1c3bbc0f44461689b6f1845a4d4cdf398c329d3d98f832f ./premake5.lua +35bd067115d5027ba08ad2ebefbae8e61ea79ab094db6a4ea10629858cbc2ae1 ./snapcraft.yaml +3ace3886e2541275cfefbd02fdeaebbe52b2c296b3e6ad4ba9269f261118d625 ./third_party/butteraugli/BUILD +c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 ./third_party/butteraugli/LICENSE +b542269416b966e6b07f899c1c300d05a7c07a46816b27502fd7dfe18bc8be33 ./third_party/butteraugli/README.md +d281fc0edda6e6b0d6566a3fde80fffbbadf17b2150d66575464b6f81f7079da ./third_party/butteraugli/WORKSPACE +de619abe0ac9bfd67e5f26314eed2b842f2e192e3e511a1bc02e8238cfc294b2 ./third_party/butteraugli/butteraugli/Makefile +d7ce753740b52e4ee1bb9b4a85ef19af26fbcb6b8b1849a239893395242eea35 ./third_party/butteraugli/butteraugli/butteraugli.cc +697bbb02cd7c502e1179770e42a6db3d7768c1f21881e89bb0f60e03d1e3bc4b ./third_party/butteraugli/butteraugli/butteraugli.h +4b536e8a3be43f7ba1aeb6b215ae539b122223cc81a0ebf9ac69ac690e6d2bcf ./third_party/butteraugli/butteraugli/butteraugli_main.cc +b108208499f5553a5233169f4ad4d6ae77652ef02f45ab9187f90f610ae9f37b ./third_party/butteraugli/jpeg.BUILD +970066cce1ed3d038d908076603387c533bd44cd726a1afee8f21d2adf8269c8 ./third_party/butteraugli/png.BUILD +a6f68834ee37fd3f728ea5370d816e3e60849f971fa636eabe408bcc719421db ./third_party/butteraugli/zlib.BUILD +7896c1d71030dfa3812bdc18cff31cc9a8912dddc7182f6470dda3b98f440af8 ./tools/guetzli-compare.py +a6f68834ee37fd3f728ea5370d816e3e60849f971fa636eabe408bcc719421db ./zlib.BUILD diff --git a/packages/optimizt-guetzli/upstream/guetzli/.gitignore b/packages/optimizt-guetzli/upstream/guetzli/.gitignore new file mode 100644 index 0000000..dd10da5 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/.gitignore @@ -0,0 +1,17 @@ +bin/ +obj/ +/bazel-* + +# Visual Studio 2015 cache/options directory +.vs/ + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb diff --git a/packages/optimizt-guetzli/upstream/guetzli/BUILD b/packages/optimizt-guetzli/upstream/guetzli/BUILD new file mode 100644 index 0000000..05bfc0d --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/BUILD @@ -0,0 +1,27 @@ +# Description: +# Bazel build file for Guetzli. + +cc_library( + name = "guetzli_lib", + srcs = glob( + [ + "guetzli/*.h", + "guetzli/*.cc", + "guetzli/*.inc", + ], + exclude = ["guetzli/guetzli.cc"], + ), + copts = [ "-Wno-sign-compare" ], + deps = [ + "@butteraugli//:butteraugli_lib", + ], +) + +cc_binary( + name = "guetzli", + srcs = ["guetzli/guetzli.cc"], + deps = [ + ":guetzli_lib", + "@png_archive//:png", + ], +) diff --git a/packages/optimizt-guetzli/upstream/guetzli/CONTRIBUTING.md b/packages/optimizt-guetzli/upstream/guetzli/CONTRIBUTING.md new file mode 100644 index 0000000..ff0a831 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/CONTRIBUTING.md @@ -0,0 +1,27 @@ +Want to contribute bugfixes or non-output-modifying changes (e.g. speedups)? +Great! First, read this page (including the small print at the end). Want to +contribute changes that modify the generated JPEG file? Talk to us first. + +### Before you contribute +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +### Code reviews +All submissions, including submissions by project members, require review. We +use Github pull requests for this purpose. + +### The small print +Contributions made by corporations are covered by a different agreement than +the one above, the +[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). diff --git a/packages/optimizt-guetzli/upstream/guetzli/LICENSE b/packages/optimizt-guetzli/upstream/guetzli/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/optimizt-guetzli/upstream/guetzli/Makefile b/packages/optimizt-guetzli/upstream/guetzli/Makefile new file mode 100644 index 0000000..52d56bd --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/Makefile @@ -0,0 +1,45 @@ +# GNU Make workspace makefile autogenerated by Premake + +ifndef config + config=release +endif + +ifndef verbose + SILENT = @ +endif + +ifeq ($(config),release) + guetzli_config = release +endif +ifeq ($(config),debug) + guetzli_config = debug +endif + +PROJECTS := guetzli + +.PHONY: all clean help $(PROJECTS) + +all: $(PROJECTS) + +guetzli: +ifneq (,$(guetzli_config)) + @echo "==== Building guetzli ($(guetzli_config)) ====" + @${MAKE} --no-print-directory -C . -f guetzli.make config=$(guetzli_config) +endif + +clean: + @${MAKE} --no-print-directory -C . -f guetzli.make clean + +help: + @echo "Usage: make [config=name] [target]" + @echo "" + @echo "CONFIGURATIONS:" + @echo " release" + @echo " debug" + @echo "" + @echo "TARGETS:" + @echo " all (default)" + @echo " clean" + @echo " guetzli" + @echo "" + @echo "For more information, see http://industriousone.com/premake/quick-start" \ No newline at end of file diff --git a/packages/optimizt-guetzli/upstream/guetzli/README.md b/packages/optimizt-guetzli/upstream/guetzli/README.md new file mode 100644 index 0000000..3e99dd4 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/README.md @@ -0,0 +1,96 @@ +# Introduction + +Guetzli is a JPEG encoder that aims for excellent compression density at high +visual quality. Guetzli-generated images are typically 20-30% smaller than +images of equivalent quality generated by libjpeg. Guetzli generates only +sequential (nonprogressive) JPEGs due to faster decompression speeds they offer. + +# Building + +## On POSIX systems + +1. Get a copy of the source code, either by cloning this repository, or by + downloading an + [archive](https://github.com/google/guetzli/archive/master.zip) and + unpacking it. +2. Install [libpng](http://www.libpng.org/pub/png/libpng.html). + If using your operating system + package manager, install development versions of the packages if the + distinction exists. + * On Ubuntu, do `apt-get install libpng-dev`. + * On Fedora, do `dnf install libpng-devel`. + * On Arch Linux, do `pacman -S libpng`. +3. Run `make` and expect the binary to be created in `bin/Release/guetzli`. + +## On Windows + +1. Get a copy of the source code, either by cloning this repository, or by + downloading an + [archive](https://github.com/google/guetzli/archive/master.zip) and + unpacking it. +2. Install [Visual Studio 2015](https://www.visualstudio.com) and + [vcpkg](https://github.com/Microsoft/vcpkg) +3. Install `libpng` using vcpkg: `.\vcpkg install `libpng. +4. Cause the installed packages to be available system-wide: `.\vcpkg integrate + install`. If you prefer not to do this, refer to [vcpkg's + documentation](https://github.com/Microsoft/vcpkg/blob/master/docs/EXAMPLES.md#example-1-2). +5. Open the Visual Studio project enclosed in the repository and build it. + +## On macOS + +To install using [Homebrew](https://brew.sh/): +1. Install [Homebrew](https://brew.sh/) +2. `brew install guetzli` + +To install using the repository: +1. Get a copy of the source code, either by cloning this repository, or by + downloading an + [archive](https://github.com/google/guetzli/archive/master.zip) and + unpacking it. +2. Install [Homebrew](https://brew.sh/) or [MacPorts](https://www.macports.org/) +3. Install `libpng` + * Using [Homebrew](https://brew.sh/): `brew install libpng`. + * Using [MacPorts](https://www.macports.org/): `port install libpng` (You may need to use `sudo`). +4. Run the following command to build the binary in `bin/Release/guetzli`. + * If you installed using [Homebrew](https://brew.sh/) simply use `make` + * If you installed using [MacPorts](https://www.macports.org/) use `CFLAGS='-I/opt/local/include' LDFLAGS='-L/opt/local/lib' make` + +## With Bazel + +There's also a [Bazel](https://bazel.build) build configuration provided. If you +have Bazel installed, you can also compile Guetzli by running `bazel build -c opt //:guetzli`. + +# Using + +**Note:** Guetzli uses a large amount of memory. You should provide 300MB of +memory per 1MPix of the input image. + +**Note:** Guetzli uses a significant amount of CPU time. You should count on +using about 1 minute of CPU per 1 MPix of input image. + +**Note:** Guetzli assumes that input is in **sRGB profile** with a **gamma of +2.2**. Guetzli will ignore any color-profile metadata in the image. + +To try out Guetzli you need to [build](#building) or +[download](https://github.com/google/guetzli/releases) the Guetzli binary. The +binary reads a PNG or JPEG image and creates an optimized JPEG image: + +```bash +guetzli [--quality Q] [--verbose] original.png output.jpg +guetzli [--quality Q] [--verbose] original.jpg output.jpg +``` + +Note that Guetzli is designed to work on high quality images. You should always +prefer providing uncompressed input images (e.g. that haven't been already +compressed with any JPEG encoders, including Guetzli). While it will work on other +images too, results will be poorer. You can try compressing an enclosed [sample +high quality +image](https://github.com/google/guetzli/releases/download/v0/bees.png). + +You can pass a `--quality Q` parameter to set quality in units equivalent to +libjpeg quality. You can also pass a `--verbose` flag to see a trace of encoding +attempts made. + +Please note that JPEG images do not support alpha channel (transparency). If the +input is a PNG with an alpha channel, it will be overlaid on black background +before encoding. diff --git a/packages/optimizt-guetzli/upstream/guetzli/WORKSPACE b/packages/optimizt-guetzli/upstream/guetzli/WORKSPACE new file mode 100644 index 0000000..0fb9778 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/WORKSPACE @@ -0,0 +1,25 @@ +# Description: +# Bazel workspace file for Guetzli. + +workspace(name = "guetzli") + +new_http_archive( + name = "png_archive", + build_file = "png.BUILD", + sha256 = "a941dc09ca00148fe7aaf4ecdd6a67579c293678ed1e1cf633b5ffc02f4f8cf7", + strip_prefix = "libpng-1.2.57", + url = "http://github.com/glennrp/libpng/archive/v1.2.57.zip", +) + +new_http_archive( + name = "zlib_archive", + build_file = "zlib.BUILD", + sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017", + strip_prefix = "zlib-1.2.10", + url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz", +) + +local_repository( + name = "butteraugli", + path = "third_party/butteraugli/", +) diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli.make b/packages/optimizt-guetzli/upstream/guetzli/guetzli.make new file mode 100644 index 0000000..d4df6ad --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli.make @@ -0,0 +1,213 @@ +# GNU Make project makefile autogenerated by Premake + +ifndef config + config=release +endif + +ifndef verbose + SILENT = @ +endif + +.PHONY: clean prebuild prelink + +ifeq ($(config),release) + RESCOMP = windres + TARGETDIR = bin/Release + TARGET = $(TARGETDIR)/guetzli + OBJDIR = obj/Release + DEFINES += + INCLUDES += -I. -Ithird_party/butteraugli + FORCE_INCLUDE += + ALL_CPPFLAGS += $(CPPFLAGS) -MMD -MP $(DEFINES) $(INCLUDES) + ALL_CFLAGS += $(CFLAGS) $(ALL_CPPFLAGS) -O3 -g `pkg-config --cflags libpng` + ALL_CXXFLAGS += $(CXXFLAGS) $(ALL_CPPFLAGS) -O3 -g -std=c++11 `pkg-config --cflags libpng` + ALL_RESFLAGS += $(RESFLAGS) $(DEFINES) $(INCLUDES) + LIBS += + LDDEPS += + ALL_LDFLAGS += $(LDFLAGS) `pkg-config --libs libpng` + LINKCMD = $(CXX) -o "$@" $(OBJECTS) $(RESOURCES) $(ALL_LDFLAGS) $(LIBS) + define PREBUILDCMDS + endef + define PRELINKCMDS + endef + define POSTBUILDCMDS + endef +all: $(TARGETDIR) $(OBJDIR) prebuild prelink $(TARGET) + @: + +endif + +ifeq ($(config),debug) + RESCOMP = windres + TARGETDIR = bin/Debug + TARGET = $(TARGETDIR)/guetzli + OBJDIR = obj/Debug + DEFINES += + INCLUDES += -I. -Ithird_party/butteraugli + FORCE_INCLUDE += + ALL_CPPFLAGS += $(CPPFLAGS) -MMD -MP $(DEFINES) $(INCLUDES) + ALL_CFLAGS += $(CFLAGS) $(ALL_CPPFLAGS) -g `pkg-config --cflags libpng` + ALL_CXXFLAGS += $(CXXFLAGS) $(ALL_CPPFLAGS) -g -std=c++11 `pkg-config --cflags libpng` + ALL_RESFLAGS += $(RESFLAGS) $(DEFINES) $(INCLUDES) + LIBS += + LDDEPS += + ALL_LDFLAGS += $(LDFLAGS) `pkg-config --libs libpng` + LINKCMD = $(CXX) -o "$@" $(OBJECTS) $(RESOURCES) $(ALL_LDFLAGS) $(LIBS) + define PREBUILDCMDS + endef + define PRELINKCMDS + endef + define POSTBUILDCMDS + endef +all: $(TARGETDIR) $(OBJDIR) prebuild prelink $(TARGET) + @: + +endif + +OBJECTS := \ + $(OBJDIR)/butteraugli_comparator.o \ + $(OBJDIR)/dct_double.o \ + $(OBJDIR)/debug_print.o \ + $(OBJDIR)/entropy_encode.o \ + $(OBJDIR)/fdct.o \ + $(OBJDIR)/gamma_correct.o \ + $(OBJDIR)/guetzli.o \ + $(OBJDIR)/idct.o \ + $(OBJDIR)/jpeg_data.o \ + $(OBJDIR)/jpeg_data_decoder.o \ + $(OBJDIR)/jpeg_data_encoder.o \ + $(OBJDIR)/jpeg_data_reader.o \ + $(OBJDIR)/jpeg_data_writer.o \ + $(OBJDIR)/jpeg_huffman_decode.o \ + $(OBJDIR)/output_image.o \ + $(OBJDIR)/preprocess_downsample.o \ + $(OBJDIR)/processor.o \ + $(OBJDIR)/quality.o \ + $(OBJDIR)/quantize.o \ + $(OBJDIR)/score.o \ + $(OBJDIR)/butteraugli.o \ + +RESOURCES := \ + +CUSTOMFILES := \ + +SHELLTYPE := msdos +ifeq (,$(ComSpec)$(COMSPEC)) + SHELLTYPE := posix +endif +ifeq (/bin,$(findstring /bin,$(SHELL))) + SHELLTYPE := posix +endif + +$(TARGET): $(GCH) ${CUSTOMFILES} $(OBJECTS) $(LDDEPS) $(RESOURCES) + @echo Linking guetzli + $(SILENT) $(LINKCMD) + $(POSTBUILDCMDS) + +$(TARGETDIR): + @echo Creating $(TARGETDIR) +ifeq (posix,$(SHELLTYPE)) + $(SILENT) mkdir -p $(TARGETDIR) +else + $(SILENT) mkdir $(subst /,\\,$(TARGETDIR)) +endif + +$(OBJDIR): + @echo Creating $(OBJDIR) +ifeq (posix,$(SHELLTYPE)) + $(SILENT) mkdir -p $(OBJDIR) +else + $(SILENT) mkdir $(subst /,\\,$(OBJDIR)) +endif + +clean: + @echo Cleaning guetzli +ifeq (posix,$(SHELLTYPE)) + $(SILENT) rm -f $(TARGET) + $(SILENT) rm -rf $(OBJDIR) +else + $(SILENT) if exist $(subst /,\\,$(TARGET)) del $(subst /,\\,$(TARGET)) + $(SILENT) if exist $(subst /,\\,$(OBJDIR)) rmdir /s /q $(subst /,\\,$(OBJDIR)) +endif + +prebuild: + $(PREBUILDCMDS) + +prelink: + $(PRELINKCMDS) + +ifneq (,$(PCH)) +$(OBJECTS): $(GCH) $(PCH) +$(GCH): $(PCH) + @echo $(notdir $<) + $(SILENT) $(CXX) -x c++-header $(ALL_CXXFLAGS) -o "$@" -MF "$(@:%.gch=%.d)" -c "$<" +endif + +$(OBJDIR)/butteraugli_comparator.o: guetzli/butteraugli_comparator.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/dct_double.o: guetzli/dct_double.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/debug_print.o: guetzli/debug_print.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/entropy_encode.o: guetzli/entropy_encode.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/fdct.o: guetzli/fdct.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/gamma_correct.o: guetzli/gamma_correct.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/guetzli.o: guetzli/guetzli.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/idct.o: guetzli/idct.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/jpeg_data.o: guetzli/jpeg_data.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/jpeg_data_decoder.o: guetzli/jpeg_data_decoder.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/jpeg_data_encoder.o: guetzli/jpeg_data_encoder.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/jpeg_data_reader.o: guetzli/jpeg_data_reader.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/jpeg_data_writer.o: guetzli/jpeg_data_writer.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/jpeg_huffman_decode.o: guetzli/jpeg_huffman_decode.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/output_image.o: guetzli/output_image.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/preprocess_downsample.o: guetzli/preprocess_downsample.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/processor.o: guetzli/processor.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/quality.o: guetzli/quality.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/quantize.o: guetzli/quantize.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/score.o: guetzli/score.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" +$(OBJDIR)/butteraugli.o: third_party/butteraugli/butteraugli/butteraugli.cc + @echo $(notdir $<) + $(SILENT) $(CXX) $(ALL_CXXFLAGS) $(FORCE_INCLUDE) -o "$@" -MF "$(@:%.o=%.d)" -c "$<" + +-include $(OBJECTS:%.o=%.d) +ifneq (,$(PCH)) + -include $(OBJDIR)/$(notdir $(PCH)).d +endif \ No newline at end of file diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli.sln b/packages/optimizt-guetzli/upstream/guetzli/guetzli.sln new file mode 100644 index 0000000..0cb3249 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2015 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "guetzli", "guetzli.vcxproj", "{C9FCBE14-35DC-3DB0-3EF4-C886AA52A411}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411}.Debug|Win32.ActiveCfg = Debug|Win32 + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411}.Debug|Win32.Build.0 = Debug|Win32 + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411}.Debug|x64.ActiveCfg = Debug|x64 + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411}.Debug|x64.Build.0 = Debug|x64 + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411}.Release|Win32.ActiveCfg = Release|Win32 + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411}.Release|Win32.Build.0 = Release|Win32 + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411}.Release|x64.ActiveCfg = Release|x64 + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli.vcxproj b/packages/optimizt-guetzli/upstream/guetzli/guetzli.vcxproj new file mode 100644 index 0000000..d14db62 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli.vcxproj @@ -0,0 +1,220 @@ + + + + + Release + x64 + + + Release + Win32 + + + Debug + x64 + + + Debug + Win32 + + + + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411} + true + Win32Proj + guetzli + + + + Application + false + Unicode + v140 + + + Application + false + Unicode + v140 + + + Application + true + Unicode + v140 + + + Application + true + Unicode + v140 + + + + + + + + + + + + + + + + + + + false + bin\x86_64\Release\ + obj\x86_64\Release\ + guetzli + .exe + + + false + bin\x86\Release\ + obj\x86\Release\ + guetzli + .exe + + + true + bin\x86_64\Debug\ + obj\x86_64\Debug\ + guetzli + .exe + + + true + bin\x86\Debug\ + obj\x86\Debug\ + guetzli + .exe + + + + NotUsing + Level3 + .;third_party\butteraugli;%(AdditionalIncludeDirectories) + Full + true + true + false + true + + + Console + false + true + true + shlwapi.lib;%(AdditionalDependencies) + mainCRTStartup + + + + + NotUsing + Level3 + .;third_party\butteraugli;%(AdditionalIncludeDirectories) + Full + true + true + false + true + + + Console + false + true + true + shlwapi.lib;%(AdditionalDependencies) + mainCRTStartup + + + + + NotUsing + Level3 + .;third_party\butteraugli;%(AdditionalIncludeDirectories) + ProgramDatabase + Disabled + + + Console + true + shlwapi.lib;%(AdditionalDependencies) + mainCRTStartup + + + + + NotUsing + Level3 + .;third_party\butteraugli;%(AdditionalIncludeDirectories) + EditAndContinue + Disabled + + + Console + true + shlwapi.lib;%(AdditionalDependencies) + mainCRTStartup + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli.vcxproj.filters b/packages/optimizt-guetzli/upstream/guetzli/guetzli.vcxproj.filters new file mode 100644 index 0000000..da2297c --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli.vcxproj.filters @@ -0,0 +1,162 @@ + + + + + {C9FCBE14-35DC-3DB0-3EF4-C886AA52A411} + + + {0FB18DF1-7B66-06E7-045B-00BE700FFDEA} + + + {468B2B32-B2C2-73C9-BBCC-D7EC27839AC2} + + + {FD6FCB41-6929-36EC-F288-50C65E41EC5B} + + + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + third_party\butteraugli\butteraugli + + + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + guetzli + + + third_party\butteraugli\butteraugli + + + \ No newline at end of file diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/butteraugli_comparator.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/butteraugli_comparator.cc new file mode 100644 index 0000000..7c8cefe --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/butteraugli_comparator.cc @@ -0,0 +1,231 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/butteraugli_comparator.h" + +#include + +#include "guetzli/debug_print.h" +#include "guetzli/gamma_correct.h" +#include "guetzli/score.h" + +namespace guetzli { + +namespace { +using ::butteraugli::ConstRestrict; +using ::butteraugli::ImageF; +using ::butteraugli::CreatePlanes; +using ::butteraugli::PlanesFromPacked; +using ::butteraugli::PackedFromPlanes; + +std::vector LinearRgb(const size_t xsize, const size_t ysize, + const std::vector& rgb) { + const double* lut = Srgb8ToLinearTable(); + std::vector planes = CreatePlanes(xsize, ysize, 3); + for (int c = 0; c < 3; ++c) { + for (size_t y = 0; y < ysize; ++y) { + ConstRestrict row_in = &rgb[3 * xsize * y]; + ConstRestrict row_out = planes[c].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_out[x] = lut[row_in[3 * x + c]]; + } + } + } + return planes; +} + +} // namespace + +ButteraugliComparator::ButteraugliComparator(const int width, const int height, + const std::vector& rgb, + const float target_distance, + ProcessStats* stats) + : width_(width), + height_(height), + target_distance_(target_distance), + comparator_(width_, height_, kButteraugliStep), + distance_(0.0), + distmap_(width_, height_), + stats_(stats) { + rgb_linear_pregamma_ = LinearRgb(width, height, rgb); + const int block_w = (width_ + 7) / 8; + const int block_h = (height_ + 7) / 8; + const int nblocks = block_w * block_h; + per_block_pregamma_.resize(nblocks); + for (int block_y = 0, bx = 0; block_y < block_h; ++block_y) { + for (int block_x = 0; block_x < block_w; ++block_x, ++bx) { + per_block_pregamma_[bx].resize(3, std::vector(kDCTBlockSize)); + for (int iy = 0, i = 0; iy < 8; ++iy) { + for (int ix = 0; ix < 8; ++ix, ++i) { + int x = std::min(8 * block_x + ix, width_ - 1); + int y = std::min(8 * block_y + iy, height_ - 1); + for (int c = 0; c < 3; ++c) { + ConstRestrict row_linear = + rgb_linear_pregamma_[c].Row(y); + per_block_pregamma_[bx][c][i] = row_linear[x]; + } + } + } + ::butteraugli::OpsinDynamicsImage(8, 8, per_block_pregamma_[bx]); + } + } + std::vector> pregamma = + PackedFromPlanes(rgb_linear_pregamma_); + ::butteraugli::OpsinDynamicsImage(width_, height_, pregamma); + rgb_linear_pregamma_ = PlanesFromPacked(width_, height_, pregamma); + std::vector > dummy(3); + ::butteraugli::Mask(pregamma, pregamma, width_, height_, + &mask_xyz_, &dummy); +} + +void ButteraugliComparator::Compare(const OutputImage& img) { + std::vector > rgb(3, std::vector(width_ * height_)); + img.ToLinearRGB(&rgb); + ::butteraugli::OpsinDynamicsImage(width_, height_, rgb); + ImageF distmap; + const std::vector rgb_planes = PlanesFromPacked(width_, height_, rgb); + comparator_.DiffmapOpsinDynamicsImage(rgb_linear_pregamma_, + rgb_planes, distmap); + distmap_.resize(width_ * height_); + CopyToPacked(distmap, &distmap_); + distance_ = ::butteraugli::ButteraugliScoreFromDiffmap(distmap); + GUETZLI_LOG(stats_, " BA[100.00%%] D[%6.4f]", distance_); +} + +double ButteraugliComparator::CompareBlock( + const OutputImage& img, int block_x, int block_y) const { + int xmin = 8 * block_x; + int ymin = 8 * block_y; + int block_ix = block_y * ((width_ + 7) / 8) + block_x; + const std::vector >& rgb0_c = + per_block_pregamma_[block_ix]; + + std::vector > rgb1_c(3, std::vector(kDCTBlockSize)); + img.ToLinearRGB(xmin, ymin, 8, 8, &rgb1_c); + ::butteraugli::OpsinDynamicsImage(8, 8, rgb1_c); + + std::vector > rgb0 = rgb0_c; + std::vector > rgb1 = rgb1_c; + + ::butteraugli::MaskHighIntensityChange(8, 8, rgb0_c, rgb1_c, rgb0, rgb1); + + double b0[3 * kDCTBlockSize]; + double b1[3 * kDCTBlockSize]; + for (int c = 0; c < 3; ++c) { + for (int ix = 0; ix < kDCTBlockSize; ++ix) { + b0[c * kDCTBlockSize + ix] = rgb0[c][ix]; + b1[c * kDCTBlockSize + ix] = rgb1[c][ix]; + } + } + double diff_xyz_dc[3] = { 0.0 }; + double diff_xyz_ac[3] = { 0.0 }; + double diff_xyz_edge_dc[3] = { 0.0 }; + ::butteraugli::ButteraugliBlockDiff( + b0, b1, diff_xyz_dc, diff_xyz_ac, diff_xyz_edge_dc); + + double scale[3]; + for (int c = 0; c < 3; ++c) { + scale[c] = mask_xyz_[c][ymin * width_ + xmin]; + } + + static const double kEdgeWeight = 0.05; + + double diff = 0.0; + double diff_edge = 0.0; + for (int c = 0; c < 3; ++c) { + diff += diff_xyz_dc[c] * scale[c]; + diff += diff_xyz_ac[c] * scale[c]; + diff_edge += diff_xyz_edge_dc[c] * scale[c]; + } + return sqrt((1 - kEdgeWeight) * diff + kEdgeWeight * diff_edge); +} + +float ButteraugliComparator::BlockErrorLimit() const { + return target_distance_; +} + +void ButteraugliComparator::ComputeBlockErrorAdjustmentWeights( + int direction, + int max_block_dist, + double target_mul, + int factor_x, int factor_y, + const std::vector& distmap, + std::vector* block_weight) { + const double target_distance = target_distance_ * target_mul; + const int sizex = 8 * factor_x; + const int sizey = 8 * factor_y; + const int block_width = (width_ + sizex - 1) / sizex; + const int block_height = (height_ + sizey - 1) / sizey; + std::vector max_dist_per_block(block_width * block_height); + for (int block_y = 0; block_y < block_height; ++block_y) { + for (int block_x = 0; block_x < block_width; ++block_x) { + int block_ix = block_y * block_width + block_x; + int x_max = std::min(width_, sizex * (block_x + 1)); + int y_max = std::min(height_, sizey * (block_y + 1)); + float max_dist = 0.0; + for (int y = sizey * block_y; y < y_max; ++y) { + for (int x = sizex * block_x; x < x_max; ++x) { + max_dist = std::max(max_dist, distmap[y * width_ + x]); + } + } + max_dist_per_block[block_ix] = max_dist; + } + } + for (int block_y = 0; block_y < block_height; ++block_y) { + for (int block_x = 0; block_x < block_width; ++block_x) { + int block_ix = block_y * block_width + block_x; + float max_local_dist = target_distance; + int x_min = std::max(0, block_x - max_block_dist); + int y_min = std::max(0, block_y - max_block_dist); + int x_max = std::min(block_width, block_x + 1 + max_block_dist); + int y_max = std::min(block_height, block_y + 1 + max_block_dist); + for (int y = y_min; y < y_max; ++y) { + for (int x = x_min; x < x_max; ++x) { + max_local_dist = + std::max(max_local_dist, max_dist_per_block[y * block_width + x]); + } + } + if (direction > 0) { + if (max_dist_per_block[block_ix] <= target_distance && + max_local_dist <= 1.1 * target_distance) { + (*block_weight)[block_ix] = 1.0; + } + } else { + constexpr double kLocalMaxWeight = 0.5; + if (max_dist_per_block[block_ix] <= + (1 - kLocalMaxWeight) * target_distance + + kLocalMaxWeight * max_local_dist) { + continue; + } + for (int y = y_min; y < y_max; ++y) { + for (int x = x_min; x < x_max; ++x) { + int d = std::max(std::abs(y - block_y), std::abs(x - block_x)); + int ix = y * block_width + x; + (*block_weight)[ix] = std::max( + (*block_weight)[ix], 1.0 / (d + 1.0)); + } + } + } + } + } +} + +double ButteraugliComparator::ScoreOutputSize(int size) const { + return ScoreJPEG(distance_, size, target_distance_); +} + + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/butteraugli_comparator.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/butteraugli_comparator.h new file mode 100644 index 0000000..d9ae0b0 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/butteraugli_comparator.h @@ -0,0 +1,74 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_BUTTERAUGLI_COMPARATOR_H_ +#define GUETZLI_BUTTERAUGLI_COMPARATOR_H_ + +#include + +#include "butteraugli/butteraugli.h" +#include "guetzli/comparator.h" +#include "guetzli/jpeg_data.h" +#include "guetzli/output_image.h" +#include "guetzli/stats.h" + +namespace guetzli { + +constexpr int kButteraugliStep = 3; + +class ButteraugliComparator : public Comparator { + public: + ButteraugliComparator(const int width, const int height, + const std::vector& rgb, + const float target_distance, ProcessStats* stats); + + void Compare(const OutputImage& img) override; + + double CompareBlock(const OutputImage& img, + int block_x, int block_y) const override; + + double ScoreOutputSize(int size) const override; + + bool DistanceOK(double target_mul) const override { + return distance_ <= target_mul * target_distance_; + } + + const std::vector distmap() const override { return distmap_; } + float distmap_aggregate() const override { return distance_; } + + float BlockErrorLimit() const override; + + void ComputeBlockErrorAdjustmentWeights( + int direction, int max_block_dist, double target_mul, int factor_x, + int factor_y, const std::vector& distmap, + std::vector* block_weight) override; + + private: + const int width_; + const int height_; + const float target_distance_; + std::vector<::butteraugli::ImageF> rgb_linear_pregamma_; + std::vector> mask_xyz_; + std::vector>> per_block_pregamma_; + ::butteraugli::ButteraugliComparator comparator_; + float distance_; + std::vector distmap_; + ProcessStats* stats_; +}; + +} // namespace guetzli + +#endif // GUETZLI_BUTTERAUGLI_COMPARATOR_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/color_transform.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/color_transform.h new file mode 100644 index 0000000..850b2d0 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/color_transform.h @@ -0,0 +1,223 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_COLOR_TRANSFORM_H_ +#define GUETZLI_COLOR_TRANSFORM_H_ + +namespace guetzli { + +static const int kCrToRedTable[256] = { + -179, -178, -177, -175, -174, -172, -171, -170, -168, -167, -165, -164, + -163, -161, -160, -158, -157, -156, -154, -153, -151, -150, -149, -147, + -146, -144, -143, -142, -140, -139, -137, -136, -135, -133, -132, -130, + -129, -128, -126, -125, -123, -122, -121, -119, -118, -116, -115, -114, + -112, -111, -109, -108, -107, -105, -104, -102, -101, -100, -98, -97, + -95, -94, -93, -91, -90, -88, -87, -86, -84, -83, -81, -80, + -79, -77, -76, -74, -73, -72, -70, -69, -67, -66, -64, -63, + -62, -60, -59, -57, -56, -55, -53, -52, -50, -49, -48, -46, + -45, -43, -42, -41, -39, -38, -36, -35, -34, -32, -31, -29, + -28, -27, -25, -24, -22, -21, -20, -18, -17, -15, -14, -13, + -11, -10, -8, -7, -6, -4, -3, -1, 0, 1, 3, 4, + 6, 7, 8, 10, 11, 13, 14, 15, 17, 18, 20, 21, + 22, 24, 25, 27, 28, 29, 31, 32, 34, 35, 36, 38, + 39, 41, 42, 43, 45, 46, 48, 49, 50, 52, 53, 55, + 56, 57, 59, 60, 62, 63, 64, 66, 67, 69, 70, 72, + 73, 74, 76, 77, 79, 80, 81, 83, 84, 86, 87, 88, + 90, 91, 93, 94, 95, 97, 98, 100, 101, 102, 104, 105, + 107, 108, 109, 111, 112, 114, 115, 116, 118, 119, 121, 122, + 123, 125, 126, 128, 129, 130, 132, 133, 135, 136, 137, 139, + 140, 142, 143, 144, 146, 147, 149, 150, 151, 153, 154, 156, + 157, 158, 160, 161, 163, 164, 165, 167, 168, 170, 171, 172, + 174, 175, 177, 178 +}; + +static const int kCbToBlueTable[256] = { + -227, -225, -223, -222, -220, -218, -216, -214, -213, -211, -209, -207, + -206, -204, -202, -200, -198, -197, -195, -193, -191, -190, -188, -186, + -184, -183, -181, -179, -177, -175, -174, -172, -170, -168, -167, -165, + -163, -161, -159, -158, -156, -154, -152, -151, -149, -147, -145, -144, + -142, -140, -138, -136, -135, -133, -131, -129, -128, -126, -124, -122, + -120, -119, -117, -115, -113, -112, -110, -108, -106, -105, -103, -101, + -99, -97, -96, -94, -92, -90, -89, -87, -85, -83, -82, -80, + -78, -76, -74, -73, -71, -69, -67, -66, -64, -62, -60, -58, + -57, -55, -53, -51, -50, -48, -46, -44, -43, -41, -39, -37, + -35, -34, -32, -30, -28, -27, -25, -23, -21, -19, -18, -16, + -14, -12, -11, -9, -7, -5, -4, -2, 0, 2, 4, 5, + 7, 9, 11, 12, 14, 16, 18, 19, 21, 23, 25, 27, + 28, 30, 32, 34, 35, 37, 39, 41, 43, 44, 46, 48, + 50, 51, 53, 55, 57, 58, 60, 62, 64, 66, 67, 69, + 71, 73, 74, 76, 78, 80, 82, 83, 85, 87, 89, 90, + 92, 94, 96, 97, 99, 101, 103, 105, 106, 108, 110, 112, + 113, 115, 117, 119, 120, 122, 124, 126, 128, 129, 131, 133, + 135, 136, 138, 140, 142, 144, 145, 147, 149, 151, 152, 154, + 156, 158, 159, 161, 163, 165, 167, 168, 170, 172, 174, 175, + 177, 179, 181, 183, 184, 186, 188, 190, 191, 193, 195, 197, + 198, 200, 202, 204, 206, 207, 209, 211, 213, 214, 216, 218, + 220, 222, 223, 225, +}; + +static const int kCrToGreenTable[256] = { + 5990656, 5943854, 5897052, 5850250, 5803448, 5756646, 5709844, 5663042, + 5616240, 5569438, 5522636, 5475834, 5429032, 5382230, 5335428, 5288626, + 5241824, 5195022, 5148220, 5101418, 5054616, 5007814, 4961012, 4914210, + 4867408, 4820606, 4773804, 4727002, 4680200, 4633398, 4586596, 4539794, + 4492992, 4446190, 4399388, 4352586, 4305784, 4258982, 4212180, 4165378, + 4118576, 4071774, 4024972, 3978170, 3931368, 3884566, 3837764, 3790962, + 3744160, 3697358, 3650556, 3603754, 3556952, 3510150, 3463348, 3416546, + 3369744, 3322942, 3276140, 3229338, 3182536, 3135734, 3088932, 3042130, + 2995328, 2948526, 2901724, 2854922, 2808120, 2761318, 2714516, 2667714, + 2620912, 2574110, 2527308, 2480506, 2433704, 2386902, 2340100, 2293298, + 2246496, 2199694, 2152892, 2106090, 2059288, 2012486, 1965684, 1918882, + 1872080, 1825278, 1778476, 1731674, 1684872, 1638070, 1591268, 1544466, + 1497664, 1450862, 1404060, 1357258, 1310456, 1263654, 1216852, 1170050, + 1123248, 1076446, 1029644, 982842, 936040, 889238, 842436, 795634, + 748832, 702030, 655228, 608426, 561624, 514822, 468020, 421218, + 374416, 327614, 280812, 234010, 187208, 140406, 93604, 46802, + 0, -46802, -93604, -140406, -187208, -234010, -280812, -327614, + -374416, -421218, -468020, -514822, -561624, -608426, -655228, -702030, + -748832, -795634, -842436, -889238, -936040, -982842, -1029644, -1076446, + -1123248, -1170050, -1216852, -1263654, -1310456, -1357258, -1404060, -1450862, + -1497664, -1544466, -1591268, -1638070, -1684872, -1731674, -1778476, -1825278, + -1872080, -1918882, -1965684, -2012486, -2059288, -2106090, -2152892, -2199694, + -2246496, -2293298, -2340100, -2386902, -2433704, -2480506, -2527308, -2574110, + -2620912, -2667714, -2714516, -2761318, -2808120, -2854922, -2901724, -2948526, + -2995328, -3042130, -3088932, -3135734, -3182536, -3229338, -3276140, -3322942, + -3369744, -3416546, -3463348, -3510150, -3556952, -3603754, -3650556, -3697358, + -3744160, -3790962, -3837764, -3884566, -3931368, -3978170, -4024972, -4071774, + -4118576, -4165378, -4212180, -4258982, -4305784, -4352586, -4399388, -4446190, + -4492992, -4539794, -4586596, -4633398, -4680200, -4727002, -4773804, -4820606, + -4867408, -4914210, -4961012, -5007814, -5054616, -5101418, -5148220, -5195022, + -5241824, -5288626, -5335428, -5382230, -5429032, -5475834, -5522636, -5569438, + -5616240, -5663042, -5709844, -5756646, -5803448, -5850250, -5897052, -5943854, +}; + +static const int kCbToGreenTable[256] = { + 2919680, 2897126, 2874572, 2852018, 2829464, 2806910, 2784356, 2761802, + 2739248, 2716694, 2694140, 2671586, 2649032, 2626478, 2603924, 2581370, + 2558816, 2536262, 2513708, 2491154, 2468600, 2446046, 2423492, 2400938, + 2378384, 2355830, 2333276, 2310722, 2288168, 2265614, 2243060, 2220506, + 2197952, 2175398, 2152844, 2130290, 2107736, 2085182, 2062628, 2040074, + 2017520, 1994966, 1972412, 1949858, 1927304, 1904750, 1882196, 1859642, + 1837088, 1814534, 1791980, 1769426, 1746872, 1724318, 1701764, 1679210, + 1656656, 1634102, 1611548, 1588994, 1566440, 1543886, 1521332, 1498778, + 1476224, 1453670, 1431116, 1408562, 1386008, 1363454, 1340900, 1318346, + 1295792, 1273238, 1250684, 1228130, 1205576, 1183022, 1160468, 1137914, + 1115360, 1092806, 1070252, 1047698, 1025144, 1002590, 980036, 957482, + 934928, 912374, 889820, 867266, 844712, 822158, 799604, 777050, + 754496, 731942, 709388, 686834, 664280, 641726, 619172, 596618, + 574064, 551510, 528956, 506402, 483848, 461294, 438740, 416186, + 393632, 371078, 348524, 325970, 303416, 280862, 258308, 235754, + 213200, 190646, 168092, 145538, 122984, 100430, 77876, 55322, + 32768, 10214, -12340, -34894, -57448, -80002, -102556, -125110, + -147664, -170218, -192772, -215326, -237880, -260434, -282988, -305542, + -328096, -350650, -373204, -395758, -418312, -440866, -463420, -485974, + -508528, -531082, -553636, -576190, -598744, -621298, -643852, -666406, + -688960, -711514, -734068, -756622, -779176, -801730, -824284, -846838, + -869392, -891946, -914500, -937054, -959608, -982162, -1004716, -1027270, + -1049824, -1072378, -1094932, -1117486, -1140040, -1162594, -1185148, -1207702, + -1230256, -1252810, -1275364, -1297918, -1320472, -1343026, -1365580, -1388134, + -1410688, -1433242, -1455796, -1478350, -1500904, -1523458, -1546012, -1568566, + -1591120, -1613674, -1636228, -1658782, -1681336, -1703890, -1726444, -1748998, + -1771552, -1794106, -1816660, -1839214, -1861768, -1884322, -1906876, -1929430, + -1951984, -1974538, -1997092, -2019646, -2042200, -2064754, -2087308, -2109862, + -2132416, -2154970, -2177524, -2200078, -2222632, -2245186, -2267740, -2290294, + -2312848, -2335402, -2357956, -2380510, -2403064, -2425618, -2448172, -2470726, + -2493280, -2515834, -2538388, -2560942, -2583496, -2606050, -2628604, -2651158, + -2673712, -2696266, -2718820, -2741374, -2763928, -2786482, -2809036, -2831590, +}; + +static const uint8_t kRangeLimitLut[4 * 256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, + 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, + 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +}; + +static const uint8_t* kRangeLimit = kRangeLimitLut + 384; + +inline void ColorTransformYCbCrToRGB(uint8_t* pixel) { + int y = pixel[0]; + int cb = pixel[1]; + int cr = pixel[2]; + pixel[0] = kRangeLimit[y + kCrToRedTable[cr]]; + pixel[1] = kRangeLimit[y + + ((kCrToGreenTable[cr] + kCbToGreenTable[cb]) >> 16)]; + pixel[2] = kRangeLimit[y + kCbToBlueTable[cb]]; +} + +} // namespace guetzli + +#endif // GUETZLI_COLOR_TRANSFORM_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/comparator.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/comparator.h new file mode 100644 index 0000000..6c49caf --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/comparator.h @@ -0,0 +1,90 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_COMPARATOR_H_ +#define GUETZLI_COMPARATOR_H_ + +#include + +#include "guetzli/output_image.h" +#include "guetzli/stats.h" + +namespace guetzli { + +// Represents a baseline image, a comparison metric and an image acceptance +// criteria based on this metric. +class Comparator { + public: + Comparator() {} + virtual ~Comparator() {} + + // Compares img with the baseline image and saves the resulting distance map + // inside the object. The provided image must have the same dimensions as the + // baseline image. + virtual void Compare(const OutputImage& img) = 0; + + // Compares an 8x8 block of the baseline image with the same block of img and + // returns the resulting per-block distance. The interpretation of the + // returned distance depends on the comparator used. + virtual double CompareBlock(const OutputImage& img, + int block_x, int block_y) const = 0; + + // Returns the combined score of the output image in the last Compare() call + // (or the baseline image, if Compare() was not called yet), based on output + // size and the similarity metric. + virtual double ScoreOutputSize(int size) const = 0; + + // Returns true if the argument of the last Compare() call (or the baseline + // image, if Compare() was not called yet) meets the image acceptance + // criteria. The target_mul modifies the acceptance criteria used in this call + // the following way: + // = 1.0 : the original acceptance criteria is used, + // < 1.0 : a more strict acceptance criteria is used, + // > 1.0 : a less strict acceptance criteria is used. + virtual bool DistanceOK(double target_mul) const = 0; + + // Returns the distance map between the baseline image and the image in the + // last Compare() call (or the baseline image, if Compare() was not called + // yet). + // The dimensions of the distance map are the same as the baseline image. + // The interpretation of the distance values depend on the comparator used. + virtual const std::vector distmap() const = 0; + + // Returns an aggregate distance or similarity value between the baseline + // image and the image in the last Compare() call (or the baseline image, if + // Compare() was not called yet). + // The interpretation of this aggregate value depends on the comparator used. + virtual float distmap_aggregate() const = 0; + + // Returns a heuristic cutoff on block errors in the sense that we won't + // consider distortions where a block error is greater than this. + virtual float BlockErrorLimit() const = 0; + // Given the search direction (+1 for upwards and -1 for downwards) and the + // current distance map, fills in *block_weight image with the relative block + // error adjustment weights. + // The target_mul param has the same semantics as in DistanceOK(). + // Note that this is essentially a static function in the sense that it does + // not depend on the last Compare() call. + virtual void ComputeBlockErrorAdjustmentWeights( + int direction, int max_block_dist, double target_mul, int factor_x, + int factor_y, const std::vector& distmap, + std::vector* block_weight) = 0; +}; + + +} // namespace guetzli + +#endif // GUETZLI_COMPARATOR_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/dct_double.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/dct_double.cc new file mode 100644 index 0000000..037381d --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/dct_double.cc @@ -0,0 +1,87 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/dct_double.h" + +#include +#include + +namespace guetzli { + +namespace { + +// kDCTMatrix[8*u+x] = 0.5*alpha(u)*cos((2*x+1)*u*M_PI/16), +// where alpha(0) = 1/sqrt(2) and alpha(u) = 1 for u > 0. +static const double kDCTMatrix[64] = { + 0.3535533906, 0.3535533906, 0.3535533906, 0.3535533906, + 0.3535533906, 0.3535533906, 0.3535533906, 0.3535533906, + 0.4903926402, 0.4157348062, 0.2777851165, 0.0975451610, + -0.0975451610, -0.2777851165, -0.4157348062, -0.4903926402, + 0.4619397663, 0.1913417162, -0.1913417162, -0.4619397663, + -0.4619397663, -0.1913417162, 0.1913417162, 0.4619397663, + 0.4157348062, -0.0975451610, -0.4903926402, -0.2777851165, + 0.2777851165, 0.4903926402, 0.0975451610, -0.4157348062, + 0.3535533906, -0.3535533906, -0.3535533906, 0.3535533906, + 0.3535533906, -0.3535533906, -0.3535533906, 0.3535533906, + 0.2777851165, -0.4903926402, 0.0975451610, 0.4157348062, + -0.4157348062, -0.0975451610, 0.4903926402, -0.2777851165, + 0.1913417162, -0.4619397663, 0.4619397663, -0.1913417162, + -0.1913417162, 0.4619397663, -0.4619397663, 0.1913417162, + 0.0975451610, -0.2777851165, 0.4157348062, -0.4903926402, + 0.4903926402, -0.4157348062, 0.2777851165, -0.0975451610, +}; + +void DCT1d(const double* in, int stride, double* out) { + for (int x = 0; x < 8; ++x) { + out[x * stride] = 0.0; + for (int u = 0; u < 8; ++u) { + out[x * stride] += kDCTMatrix[8 * x + u] * in[u * stride]; + } + } +} + +void IDCT1d(const double* in, int stride, double* out) { + for (int x = 0; x < 8; ++x) { + out[x * stride] = 0.0; + for (int u = 0; u < 8; ++u) { + out[x * stride] += kDCTMatrix[8 * u + x] * in[u * stride]; + } + } +} + +typedef void (*Transform1d)(const double* in, int stride, double* out); + +void TransformBlock(double block[64], Transform1d f) { + double tmp[64]; + for (int x = 0; x < 8; ++x) { + f(&block[x], 8, &tmp[x]); + } + for (int y = 0; y < 8; ++y) { + f(&tmp[8 * y], 1, &block[8 * y]); + } +} + +} // namespace + +void ComputeBlockDCTDouble(double block[64]) { + TransformBlock(block, DCT1d); +} + +void ComputeBlockIDCTDouble(double block[64]) { + TransformBlock(block, IDCT1d); +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/dct_double.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/dct_double.h new file mode 100644 index 0000000..636b9f9 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/dct_double.h @@ -0,0 +1,32 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_DCT_DOUBLE_H_ +#define GUETZLI_DCT_DOUBLE_H_ + +namespace guetzli { + +// Performs in-place floating point 8x8 DCT on block[0..63]. +// Note that the DCT used here is the DCT-2 with the first term multiplied by +// 1/sqrt(2) and the result scaled by 1/2. +void ComputeBlockDCTDouble(double block[64]); + +// Performs in-place floating point 8x8 inverse DCT on block[0..63]. +void ComputeBlockIDCTDouble(double block[64]); + +} // namespace guetzli + +#endif // GUETZLI_DCT_DOUBLE_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/debug_print.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/debug_print.cc new file mode 100644 index 0000000..b4fbf8c --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/debug_print.cc @@ -0,0 +1,30 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/debug_print.h" + +namespace guetzli { + +void PrintDebug(ProcessStats* stats, std::string s) { + if (stats->debug_output) { + stats->debug_output->append(s); + } + if (stats->debug_output_file) { + fprintf(stats->debug_output_file, "%s", s.c_str()); + } +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/debug_print.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/debug_print.h new file mode 100644 index 0000000..5ba0135 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/debug_print.h @@ -0,0 +1,46 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_DEBUG_PRINT_H_ +#define GUETZLI_DEBUG_PRINT_H_ + +#include "guetzli/stats.h" + +namespace guetzli { + +void PrintDebug(ProcessStats* stats, std::string s); + +} // namespace guetzli + +#define GUETZLI_LOG(stats, ...) \ + do { \ + char debug_string[1024]; \ + snprintf(debug_string, sizeof(debug_string), __VA_ARGS__); \ + debug_string[sizeof(debug_string) - 1] = '\0'; \ + ::guetzli::PrintDebug( \ + stats, std::string(debug_string)); \ + } while (0) +#define GUETZLI_LOG_QUANT(stats, q) \ + for (int y = 0; y < 8; ++y) { \ + for (int c = 0; c < 3; ++c) { \ + for (int x = 0; x < 8; ++x) \ + GUETZLI_LOG(stats, " %2d", (q)[c][8 * y + x]); \ + GUETZLI_LOG(stats, " "); \ + } \ + GUETZLI_LOG(stats, "\n"); \ + } + +#endif // GUETZLI_DEBUG_PRINT_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/entropy_encode.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/entropy_encode.cc new file mode 100644 index 0000000..632fd21 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/entropy_encode.cc @@ -0,0 +1,147 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Entropy encoding (Huffman) utilities. + +#include "guetzli/entropy_encode.h" + +#include +#include + +namespace guetzli { + +bool SetDepth(int p0, HuffmanTree *pool, uint8_t *depth, int max_depth) { + int stack[17]; + int level = 0; + int p = p0; + assert(max_depth <= 16); + stack[0] = -1; + while (true) { + if (pool[p].index_left_ >= 0) { + level++; + if (level > max_depth) return false; + stack[level] = pool[p].index_right_or_value_; + p = pool[p].index_left_; + continue; + } else { + depth[pool[p].index_right_or_value_] = static_cast(level); + } + while (level >= 0 && stack[level] == -1) level--; + if (level < 0) return true; + p = stack[level]; + stack[level] = -1; + } +} + +// Sort the root nodes, least popular first. +static inline bool SortHuffmanTree(const HuffmanTree& v0, + const HuffmanTree& v1) { + if (v0.total_count_ != v1.total_count_) { + return v0.total_count_ < v1.total_count_; + } + return v0.index_right_or_value_ > v1.index_right_or_value_; +} + +// This function will create a Huffman tree. +// +// The catch here is that the tree cannot be arbitrarily deep. +// Brotli specifies a maximum depth of 15 bits for "code trees" +// and 7 bits for "code length code trees." +// +// count_limit is the value that is to be faked as the minimum value +// and this minimum value is raised until the tree matches the +// maximum length requirement. +// +// This algorithm is not of excellent performance for very long data blocks, +// especially when population counts are longer than 2**tree_limit, but +// we are not planning to use this with extremely long blocks. +// +// See http://en.wikipedia.org/wiki/Huffman_coding +void CreateHuffmanTree(const uint32_t *data, + const size_t length, + const int tree_limit, + HuffmanTree* tree, + uint8_t *depth) { + // For block sizes below 64 kB, we never need to do a second iteration + // of this loop. Probably all of our block sizes will be smaller than + // that, so this loop is mostly of academic interest. If we actually + // would need this, we would be better off with the Katajainen algorithm. + for (uint32_t count_limit = 1; ; count_limit *= 2) { + size_t n = 0; + for (size_t i = length; i != 0;) { + --i; + if (data[i]) { + const uint32_t count = std::max(data[i], count_limit); + tree[n++] = HuffmanTree(count, -1, static_cast(i)); + } + } + + if (n == 1) { + depth[tree[0].index_right_or_value_] = 1; // Only one element. + break; + } + + std::sort(tree, tree + n, SortHuffmanTree); + + // The nodes are: + // [0, n): the sorted leaf nodes that we start with. + // [n]: we add a sentinel here. + // [n + 1, 2n): new parent nodes are added here, starting from + // (n+1). These are naturally in ascending order. + // [2n]: we add a sentinel at the end as well. + // There will be (2n+1) elements at the end. + const HuffmanTree sentinel(~static_cast(0), -1, -1); + tree[n] = sentinel; + tree[n + 1] = sentinel; + + size_t i = 0; // Points to the next leaf node. + size_t j = n + 1; // Points to the next non-leaf node. + for (size_t k = n - 1; k != 0; --k) { + size_t left, right; + if (tree[i].total_count_ <= tree[j].total_count_) { + left = i; + ++i; + } else { + left = j; + ++j; + } + if (tree[i].total_count_ <= tree[j].total_count_) { + right = i; + ++i; + } else { + right = j; + ++j; + } + + // The sentinel node becomes the parent node. + size_t j_end = 2 * n - k; + tree[j_end].total_count_ = + tree[left].total_count_ + tree[right].total_count_; + tree[j_end].index_left_ = static_cast(left); + tree[j_end].index_right_or_value_ = static_cast(right); + + // Add back the last sentinel node. + tree[j_end + 1] = sentinel; + } + if (SetDepth(static_cast(2 * n - 1), &tree[0], depth, tree_limit)) { + /* We need to pack the Huffman tree in tree_limit bits. If this was not + successful, add fake entities to the lowest values and retry. */ + break; + } + } +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/entropy_encode.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/entropy_encode.h new file mode 100644 index 0000000..1b6b84c --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/entropy_encode.h @@ -0,0 +1,62 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Entropy encoding (Huffman) utilities. + +#ifndef GUETZLI_ENTROPY_ENCODE_H_ +#define GUETZLI_ENTROPY_ENCODE_H_ + +#include +#include + +namespace guetzli { + +// A node of a Huffman tree. +struct HuffmanTree { + HuffmanTree() {} + HuffmanTree(uint32_t count, int16_t left, int16_t right) + : total_count_(count), + index_left_(left), + index_right_or_value_(right) { + } + uint32_t total_count_; + int16_t index_left_; + int16_t index_right_or_value_; +}; + +bool SetDepth(int p, HuffmanTree *pool, uint8_t *depth, int max_depth); + +// This function will create a Huffman tree. +// +// The (data,length) contains the population counts. +// The tree_limit is the maximum bit depth of the Huffman codes. +// +// The depth contains the tree, i.e., how many bits are used for +// the symbol. +// +// The actual Huffman tree is constructed in the tree[] array, which has to +// be at least 2 * length + 1 long. +// +// See http://en.wikipedia.org/wiki/Huffman_coding +void CreateHuffmanTree(const uint32_t *data, + const size_t length, + const int tree_limit, + HuffmanTree* tree, + uint8_t *depth); + +} // namespace guetzli + +#endif // GUETZLI_ENTROPY_ENCODE_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/fast_log.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/fast_log.h new file mode 100644 index 0000000..48677b7 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/fast_log.h @@ -0,0 +1,40 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_FAST_LOG_H_ +#define GUETZLI_FAST_LOG_H_ + +#include + +namespace guetzli { + +inline int Log2FloorNonZero(uint32_t n) { +#ifdef __GNUC__ + return 31 ^ __builtin_clz(n); +#else + unsigned int result = 0; + while (n >>= 1) result++; + return result; +#endif +} + +inline int Log2Floor(uint32_t n) { + return n == 0 ? -1 : Log2FloorNonZero(n); +} + +} // namespace guetzli + +#endif // GUETZLI_FAST_LOG_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/fdct.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/fdct.cc new file mode 100644 index 0000000..74e7a9a --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/fdct.cc @@ -0,0 +1,242 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Integer implementation of the Discrete Cosine Transform (DCT) +// +// Note! DCT output is kept scaled by 16, to retain maximum 16bit precision + +#include "guetzli/fdct.h" + +namespace guetzli { + +namespace { + +/////////////////////////////////////////////////////////////////////////////// +// Cosine table: C(k) = cos(k.pi/16)/sqrt(2), k = 1..7 using 15 bits signed +const coeff_t kTable04[7] = { 22725, 21407, 19266, 16384, 12873, 8867, 4520 }; +// rows #1 and #7 are pre-multiplied by 2.C(1) before the 2nd pass. +// This multiply is merged in the table of constants used during 1st pass: +const coeff_t kTable17[7] = { 31521, 29692, 26722, 22725, 17855, 12299, 6270 }; +// rows #2 and #6 are pre-multiplied by 2.C(2): +const coeff_t kTable26[7] = { 29692, 27969, 25172, 21407, 16819, 11585, 5906 }; +// rows #3 and #5 are pre-multiplied by 2.C(3): +const coeff_t kTable35[7] = { 26722, 25172, 22654, 19266, 15137, 10426, 5315 }; + +/////////////////////////////////////////////////////////////////////////////// +// Constants (15bit precision) and C macros for IDCT vertical pass + +#define kTan1 (13036) // = tan(pi/16) +#define kTan2 (27146) // = tan(2.pi/16) = sqrt(2) - 1. +#define kTan3m1 (-21746) // = tan(3.pi/16) - 1 +#define k2Sqrt2 (23170) // = 1 / 2.sqrt(2) + + // performs: {a,b} <- {a-b, a+b}, without saturation +#define BUTTERFLY(a, b) do { \ + SUB((a), (b)); \ + ADD((b), (b)); \ + ADD((b), (a)); \ +} while (0) + +/////////////////////////////////////////////////////////////////////////////// +// Constants for DCT horizontal pass + +// Note about the CORRECT_LSB macro: +// using 16bit fixed-point constants, we often compute products like: +// p = (A*x + B*y + 32768) >> 16 by adding two sub-terms q = (A*x) >> 16 +// and r = (B*y) >> 16 together. Statistically, we have p = q + r + 1 +// in 3/4 of the cases. This can be easily seen from the relation: +// (a + b + 1) >> 1 = (a >> 1) + (b >> 1) + ((a|b)&1) +// The approximation we are doing is replacing ((a|b)&1) by 1. +// In practice, this is a slightly more involved because the constants A and B +// have also been rounded compared to their exact floating point value. +// However, all in all the correction is quite small, and CORRECT_LSB can +// be defined empty if needed. + +#define COLUMN_DCT8(in) do { \ + LOAD(m0, (in)[0 * 8]); \ + LOAD(m2, (in)[2 * 8]); \ + LOAD(m7, (in)[7 * 8]); \ + LOAD(m5, (in)[5 * 8]); \ + \ + BUTTERFLY(m0, m7); \ + BUTTERFLY(m2, m5); \ + \ + LOAD(m3, (in)[3 * 8]); \ + LOAD(m4, (in)[4 * 8]); \ + BUTTERFLY(m3, m4); \ + \ + LOAD(m6, (in)[6 * 8]); \ + LOAD(m1, (in)[1 * 8]); \ + BUTTERFLY(m1, m6); \ + BUTTERFLY(m7, m4); \ + BUTTERFLY(m6, m5); \ + \ + /* RowIdct() needs 15bits fixed-point input, when the output from */ \ + /* ColumnIdct() would be 12bits. We are better doing the shift by 3 */ \ + /* now instead of in RowIdct(), because we have some multiplies to */ \ + /* perform, that can take advantage of the extra 3bits precision. */ \ + LSHIFT(m4, 3); \ + LSHIFT(m5, 3); \ + BUTTERFLY(m4, m5); \ + STORE16((in)[0 * 8], m5); \ + STORE16((in)[4 * 8], m4); \ + \ + LSHIFT(m7, 3); \ + LSHIFT(m6, 3); \ + LSHIFT(m3, 3); \ + LSHIFT(m0, 3); \ + \ + LOAD_CST(m4, kTan2); \ + m5 = m4; \ + MULT(m4, m7); \ + MULT(m5, m6); \ + SUB(m4, m6); \ + ADD(m5, m7); \ + STORE16((in)[2 * 8], m5); \ + STORE16((in)[6 * 8], m4); \ + \ + /* We should be multiplying m6 by C4 = 1/sqrt(2) here, but we only have */ \ + /* the k2Sqrt2 = 1/(2.sqrt(2)) constant that fits into 15bits. So we */ \ + /* shift by 4 instead of 3 to compensate for the additional 1/2 factor. */ \ + LOAD_CST(m6, k2Sqrt2); \ + LSHIFT(m2, 3 + 1); \ + LSHIFT(m1, 3 + 1); \ + BUTTERFLY(m1, m2); \ + MULT(m2, m6); \ + MULT(m1, m6); \ + BUTTERFLY(m3, m1); \ + BUTTERFLY(m0, m2); \ + \ + LOAD_CST(m4, kTan3m1); \ + LOAD_CST(m5, kTan1); \ + m7 = m3; \ + m6 = m1; \ + MULT(m3, m4); \ + MULT(m1, m5); \ + \ + ADD(m3, m7); \ + ADD(m1, m2); \ + CORRECT_LSB(m1); \ + CORRECT_LSB(m3); \ + MULT(m4, m0); \ + MULT(m5, m2); \ + ADD(m4, m0); \ + SUB(m0, m3); \ + ADD(m7, m4); \ + SUB(m5, m6); \ + \ + STORE16((in)[1 * 8], m1); \ + STORE16((in)[3 * 8], m0); \ + STORE16((in)[5 * 8], m7); \ + STORE16((in)[7 * 8], m5); \ +} while (0) + + +// these are the macro required by COLUMN_* +#define LOAD_CST(dst, src) (dst) = (src) +#define LOAD(dst, src) (dst) = (src) +#define MULT(a, b) (a) = (((a) * (b)) >> 16) +#define ADD(a, b) (a) = (a) + (b) +#define SUB(a, b) (a) = (a) - (b) +#define LSHIFT(a, n) (a) = ((a) << (n)) +#define STORE16(a, b) (a) = (b) +#define CORRECT_LSB(a) (a) += 1 + +// DCT vertical pass + +inline void ColumnDct(coeff_t* in) { + for (int i = 0; i < 8; ++i) { + int m0, m1, m2, m3, m4, m5, m6, m7; + COLUMN_DCT8(in + i); + } +} + +// DCT horizontal pass + +// We don't really need to round before descaling, since we +// still have 4 bits of precision left as final scaled output. +#define DESCALE(a) static_cast((a) >> 16) + +void RowDct(coeff_t* in, const coeff_t* table) { + // The Fourier transform is an unitary operator, so we're basically + // doing the transpose of RowIdct() + const int a0 = in[0] + in[7]; + const int b0 = in[0] - in[7]; + const int a1 = in[1] + in[6]; + const int b1 = in[1] - in[6]; + const int a2 = in[2] + in[5]; + const int b2 = in[2] - in[5]; + const int a3 = in[3] + in[4]; + const int b3 = in[3] - in[4]; + + // even part + const int C2 = table[1]; + const int C4 = table[3]; + const int C6 = table[5]; + const int c0 = a0 + a3; + const int c1 = a0 - a3; + const int c2 = a1 + a2; + const int c3 = a1 - a2; + + in[0] = DESCALE(C4 * (c0 + c2)); + in[4] = DESCALE(C4 * (c0 - c2)); + in[2] = DESCALE(C2 * c1 + C6 * c3); + in[6] = DESCALE(C6 * c1 - C2 * c3); + + // odd part + const int C1 = table[0]; + const int C3 = table[2]; + const int C5 = table[4]; + const int C7 = table[6]; + in[1] = DESCALE(C1 * b0 + C3 * b1 + C5 * b2 + C7 * b3); + in[3] = DESCALE(C3 * b0 - C7 * b1 - C1 * b2 - C5 * b3); + in[5] = DESCALE(C5 * b0 - C1 * b1 + C7 * b2 + C3 * b3); + in[7] = DESCALE(C7 * b0 - C5 * b1 + C3 * b2 - C1 * b3); +} +#undef DESCALE +#undef LOAD_CST +#undef LOAD +#undef MULT +#undef ADD +#undef SUB +#undef LSHIFT +#undef STORE16 +#undef CORRECT_LSB +#undef kTan1 +#undef kTan2 +#undef kTan3m1 +#undef k2Sqrt2 +#undef BUTTERFLY +#undef COLUMN_DCT8 + +} // namespace + +/////////////////////////////////////////////////////////////////////////////// +// visible FDCT callable functions + +void ComputeBlockDCT(coeff_t* coeffs) { + ColumnDct(coeffs); + RowDct(coeffs + 0 * 8, kTable04); + RowDct(coeffs + 1 * 8, kTable17); + RowDct(coeffs + 2 * 8, kTable26); + RowDct(coeffs + 3 * 8, kTable35); + RowDct(coeffs + 4 * 8, kTable04); + RowDct(coeffs + 5 * 8, kTable35); + RowDct(coeffs + 6 * 8, kTable26); + RowDct(coeffs + 7 * 8, kTable17); +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/fdct.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/fdct.h new file mode 100644 index 0000000..76d9d39 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/fdct.h @@ -0,0 +1,31 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_FDCT_H_ +#define GUETZLI_FDCT_H_ + +#include "guetzli/jpeg_data.h" + +namespace guetzli { + +// Computes the DCT (Discrete Cosine Transform) of the 8x8 array in 'block', +// scaled up by a factor of 16. The values in 'block' are laid out row-by-row +// and the result is written to the same memory area. +void ComputeBlockDCT(coeff_t* block); + +} // namespace guetzli + +#endif // GUETZLI_FDCT_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/gamma_correct.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/gamma_correct.cc new file mode 100644 index 0000000..82c17b0 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/gamma_correct.cc @@ -0,0 +1,40 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/gamma_correct.h" + +#include + +namespace guetzli { + +const double* NewSrgb8ToLinearTable() { + double* table = new double[256]; + int i = 0; + for (; i < 11; ++i) { + table[i] = i / 12.92; + } + for (; i < 256; ++i) { + table[i] = 255.0 * std::pow(((i / 255.0) + 0.055) / 1.055, 2.4); + } + return table; +} + +const double* Srgb8ToLinearTable() { + static const double* const kSrgb8ToLinearTable = NewSrgb8ToLinearTable(); + return kSrgb8ToLinearTable; +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/gamma_correct.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/gamma_correct.h new file mode 100644 index 0000000..7dffef6 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/gamma_correct.h @@ -0,0 +1,26 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_GAMMA_CORRECT_H_ +#define GUETZLI_GAMMA_CORRECT_H_ + +namespace guetzli { + +const double* Srgb8ToLinearTable(); + +} // namespace guetzli + +#endif // GUETZLI_GAMMA_CORRECT_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/guetzli.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/guetzli.cc new file mode 100644 index 0000000..aaab1c8 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/guetzli.cc @@ -0,0 +1,300 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include "png.h" +#include "guetzli/jpeg_data.h" +#include "guetzli/jpeg_data_reader.h" +#include "guetzli/processor.h" +#include "guetzli/quality.h" +#include "guetzli/stats.h" + +namespace { + +// An upper estimate of memory usage of Guetzli. The bound is +// max(kLowerMemusaeMB * 1<<20, pixel_count * kBytesPerPixel) +constexpr int kBytesPerPixel = 300; +constexpr int kLowestMemusageMB = 100; // in MB + +constexpr int kDefaultMemlimitMB = 6000; // in MB + +inline uint8_t BlendOnBlack(const uint8_t val, const uint8_t alpha) { + return (static_cast(val) * static_cast(alpha) + 128) / 255; +} + +bool ReadPNG(FILE* f, int* xsize, int* ysize, + std::vector* rgb) { + png_structp png_ptr = + png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png_ptr) { + return false; + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + return false; + } + + if (setjmp(png_jmpbuf(png_ptr)) != 0) { + // Ok we are here because of the setjmp. + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return false; + } + + rewind(f); + png_init_io(png_ptr, f); + + // The png_transforms flags are as follows: + // packing == convert 1,2,4 bit images, + // strip == 16 -> 8 bits / channel, + // shift == use sBIT dynamics, and + // expand == palettes -> rgb, grayscale -> 8 bit images, tRNS -> alpha. + const unsigned int png_transforms = + PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16; + + png_read_png(png_ptr, info_ptr, png_transforms, nullptr); + + png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr); + + *xsize = png_get_image_width(png_ptr, info_ptr); + *ysize = png_get_image_height(png_ptr, info_ptr); + rgb->resize(3 * (*xsize) * (*ysize)); + + const int components = png_get_channels(png_ptr, info_ptr); + switch (components) { + case 1: { + // GRAYSCALE + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; + for (int x = 0; x < *xsize; ++x) { + const uint8_t gray = row_in[x]; + row_out[3 * x + 0] = gray; + row_out[3 * x + 1] = gray; + row_out[3 * x + 2] = gray; + } + } + break; + } + case 2: { + // GRAYSCALE + ALPHA + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; + for (int x = 0; x < *xsize; ++x) { + const uint8_t gray = BlendOnBlack(row_in[2 * x], row_in[2 * x + 1]); + row_out[3 * x + 0] = gray; + row_out[3 * x + 1] = gray; + row_out[3 * x + 2] = gray; + } + } + break; + } + case 3: { + // RGB + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; + memcpy(row_out, row_in, 3 * (*xsize)); + } + break; + } + case 4: { + // RGBA + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; + for (int x = 0; x < *xsize; ++x) { + const uint8_t alpha = row_in[4 * x + 3]; + row_out[3 * x + 0] = BlendOnBlack(row_in[4 * x + 0], alpha); + row_out[3 * x + 1] = BlendOnBlack(row_in[4 * x + 1], alpha); + row_out[3 * x + 2] = BlendOnBlack(row_in[4 * x + 2], alpha); + } + } + break; + } + default: + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return false; + } + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return true; +} + +std::string ReadFileOrDie(FILE* f) { + if (fseek(f, 0, SEEK_END) != 0) { + perror("fseek"); + exit(1); + } + off_t size = ftell(f); + if (size < 0) { + perror("ftell"); + exit(1); + } + if (fseek(f, 0, SEEK_SET) != 0) { + perror("fseek"); + exit(1); + } + std::unique_ptr buf(new char[size]); + if (fread(buf.get(), 1, size, f) != (size_t)size) { + perror("fread"); + exit(1); + } + std::string result(buf.get(), size); + return result; +} + +void WriteFileOrDie(FILE* f, const std::string& contents) { + if (fwrite(contents.data(), 1, contents.size(), f) != contents.size()) { + perror("fwrite"); + exit(1); + } + if (fclose(f) < 0) { + perror("fclose"); + exit(1); + } +} + +void TerminateHandler() { + fprintf(stderr, "Unhandled exception. Most likely insufficient memory available.\n" + "Make sure that there is 300MB/MPix of memory available.\n"); + exit(1); +} + +void Usage() { + fprintf(stderr, + "Guetzli JPEG compressor. Usage: \n" + "guetzli [flags] input_filename output_filename\n" + "\n" + "Flags:\n" + " --verbose - Print a verbose trace of all attempts to standard output.\n" + " --quality Q - Visual quality to aim for, expressed as a JPEG quality value.\n" + " --memlimit M - Memory limit in MB. Guetzli will fail if unable to stay under\n" + " the limit. Default is %d MB\n" + " --nomemlimit - Do not limit memory usage.\n", kDefaultMemlimitMB); + exit(1); +} + +} // namespace + +int main(int argc, char** argv) { + std::set_terminate(TerminateHandler); + + int verbose = 0; + int quality = 95; + int memlimit_mb = kDefaultMemlimitMB; + + int opt_idx = 1; + for(;opt_idx < argc;opt_idx++) { + if (argv[opt_idx][0] != '-') + break; + if (!strcmp(argv[opt_idx], "--verbose")) { + verbose = 1; + } else if (!strcmp(argv[opt_idx], "--quality")) { + opt_idx++; + quality = atoi(argv[opt_idx]); + } else if (!strcmp(argv[opt_idx], "--memlimit")) { + opt_idx++; + memlimit_mb = atoi(argv[opt_idx]); + } else if (!strcmp(argv[opt_idx], "--nomemlimit")) { + memlimit_mb = -1; + } else { + fprintf(stderr, "Unknown commandline flag: %s\n", argv[opt_idx]); + Usage(); + } + } + + if (argc - opt_idx != 2) { + Usage(); + } + + FILE* fin = fopen(argv[opt_idx], "rb"); + if (!fin) { + fprintf(stderr, "Can't open input file\n"); + return 1; + } + + std::string in_data = ReadFileOrDie(fin); + std::string out_data; + + guetzli::Params params; + params.butteraugli_target = + guetzli::ButteraugliScoreForQuality(quality); + + guetzli::ProcessStats stats; + + if (verbose) { + stats.debug_output_file = stdout; + } + + static const unsigned char kPNGMagicBytes[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', + }; + if (in_data.size() >= 8 && + memcmp(in_data.data(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) { + int xsize, ysize; + std::vector rgb; + if (!ReadPNG(fin, &xsize, &ysize, &rgb)) { + fprintf(stderr, "Error reading PNG data from input file\n"); + return 1; + } + int pixels = xsize * ysize; + if (memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > memlimit_mb + || memlimit_mb < kLowestMemusageMB)) { + fprintf(stderr, "Memory limit would be exceeded. Failing.\n"); + return 1; + } + if (!guetzli::Process(params, &stats, rgb, xsize, ysize, &out_data)) { + fprintf(stderr, "Guetzli processing failed\n"); + return 1; + } + } else { + guetzli::JPEGData jpg_header; + if (!guetzli::ReadJpeg(in_data, guetzli::JPEG_READ_HEADER, &jpg_header)) { + fprintf(stderr, "Error reading JPG data from input file\n"); + return 1; + } + int pixels = jpg_header.width * jpg_header.height; + if (memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > memlimit_mb + || memlimit_mb < kLowestMemusageMB)) { + fprintf(stderr, "Memory limit would be exceeded. Failing.\n"); + return 1; + } + if (!guetzli::Process(params, &stats, in_data, &out_data)) { + fprintf(stderr, "Guetzli processing failed\n"); + return 1; + } + } + + fclose(fin); + + FILE* fout = fopen(argv[opt_idx + 1], "wb"); + if (!fout) { + fprintf(stderr, "Can't open output file for writing\n"); + return 1; + } + + WriteFileOrDie(fout, out_data); + return 0; +} diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/idct.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/idct.cc new file mode 100644 index 0000000..266a896 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/idct.cc @@ -0,0 +1,163 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Integer implementation of the Inverse Discrete Cosine Transform (IDCT). + +#include "guetzli/idct.h" + +#include +#include + +namespace guetzli { + +// kIDCTMatrix[8*x+u] = alpha(u)*cos((2*x+1)*u*M_PI/16)*sqrt(2), with fixed 13 +// bit precision, where alpha(0) = 1/sqrt(2) and alpha(u) = 1 for u > 0. +// Some coefficients are off by +-1 to mimick libjpeg's behaviour. +static const int kIDCTMatrix[kDCTBlockSize] = { + 8192, 11363, 10703, 9633, 8192, 6437, 4433, 2260, + 8192, 9633, 4433, -2259, -8192, -11362, -10704, -6436, + 8192, 6437, -4433, -11362, -8192, 2261, 10704, 9633, + 8192, 2260, -10703, -6436, 8192, 9633, -4433, -11363, + 8192, -2260, -10703, 6436, 8192, -9633, -4433, 11363, + 8192, -6437, -4433, 11362, -8192, -2261, 10704, -9633, + 8192, -9633, 4433, 2259, -8192, 11362, -10704, 6436, + 8192, -11363, 10703, -9633, 8192, -6437, 4433, -2260, +}; + +// Computes out[x] = sum{kIDCTMatrix[8*x+u]*in[u*stride]; for u in [0..7]} +inline void Compute1dIDCT(const coeff_t* in, const int stride, int out[8]) { + int tmp0, tmp1, tmp2, tmp3, tmp4; + + tmp1 = kIDCTMatrix[0] * in[0]; + out[0] = out[1] = out[2] = out[3] = out[4] = out[5] = out[6] = out[7] = tmp1; + + tmp0 = in[stride]; + tmp1 = kIDCTMatrix[ 1] * tmp0; + tmp2 = kIDCTMatrix[ 9] * tmp0; + tmp3 = kIDCTMatrix[17] * tmp0; + tmp4 = kIDCTMatrix[25] * tmp0; + out[0] += tmp1; + out[1] += tmp2; + out[2] += tmp3; + out[3] += tmp4; + out[4] -= tmp4; + out[5] -= tmp3; + out[6] -= tmp2; + out[7] -= tmp1; + + tmp0 = in[2 * stride]; + tmp1 = kIDCTMatrix[ 2] * tmp0; + tmp2 = kIDCTMatrix[10] * tmp0; + out[0] += tmp1; + out[1] += tmp2; + out[2] -= tmp2; + out[3] -= tmp1; + out[4] -= tmp1; + out[5] -= tmp2; + out[6] += tmp2; + out[7] += tmp1; + + tmp0 = in[3 * stride]; + tmp1 = kIDCTMatrix[ 3] * tmp0; + tmp2 = kIDCTMatrix[11] * tmp0; + tmp3 = kIDCTMatrix[19] * tmp0; + tmp4 = kIDCTMatrix[27] * tmp0; + out[0] += tmp1; + out[1] += tmp2; + out[2] += tmp3; + out[3] += tmp4; + out[4] -= tmp4; + out[5] -= tmp3; + out[6] -= tmp2; + out[7] -= tmp1; + + tmp0 = in[4 * stride]; + tmp1 = kIDCTMatrix[ 4] * tmp0; + out[0] += tmp1; + out[1] -= tmp1; + out[2] -= tmp1; + out[3] += tmp1; + out[4] += tmp1; + out[5] -= tmp1; + out[6] -= tmp1; + out[7] += tmp1; + + tmp0 = in[5 * stride]; + tmp1 = kIDCTMatrix[ 5] * tmp0; + tmp2 = kIDCTMatrix[13] * tmp0; + tmp3 = kIDCTMatrix[21] * tmp0; + tmp4 = kIDCTMatrix[29] * tmp0; + out[0] += tmp1; + out[1] += tmp2; + out[2] += tmp3; + out[3] += tmp4; + out[4] -= tmp4; + out[5] -= tmp3; + out[6] -= tmp2; + out[7] -= tmp1; + + tmp0 = in[6 * stride]; + tmp1 = kIDCTMatrix[ 6] * tmp0; + tmp2 = kIDCTMatrix[14] * tmp0; + out[0] += tmp1; + out[1] += tmp2; + out[2] -= tmp2; + out[3] -= tmp1; + out[4] -= tmp1; + out[5] -= tmp2; + out[6] += tmp2; + out[7] += tmp1; + + tmp0 = in[7 * stride]; + tmp1 = kIDCTMatrix[ 7] * tmp0; + tmp2 = kIDCTMatrix[15] * tmp0; + tmp3 = kIDCTMatrix[23] * tmp0; + tmp4 = kIDCTMatrix[31] * tmp0; + out[0] += tmp1; + out[1] += tmp2; + out[2] += tmp3; + out[3] += tmp4; + out[4] -= tmp4; + out[5] -= tmp3; + out[6] -= tmp2; + out[7] -= tmp1; +} + +void ComputeBlockIDCT(const coeff_t* block, uint8_t* out) { + coeff_t colidcts[kDCTBlockSize]; + const int kColScale = 11; + const int kColRound = 1 << (kColScale - 1); + for (int x = 0; x < 8; ++x) { + int colbuf[8] = { 0 }; + Compute1dIDCT(&block[x], 8, colbuf); + for (int y = 0; y < 8; ++y) { + colidcts[8 * y + x] = (colbuf[y] + kColRound) >> kColScale; + } + } + const int kRowScale = 18; + const int kRowRound = 257 << (kRowScale - 1); // includes offset by 128 + for (int y = 0; y < 8; ++y) { + const int rowidx = 8 * y; + int rowbuf[8] = { 0 }; + Compute1dIDCT(&colidcts[rowidx], 1, rowbuf); + for (int x = 0; x < 8; ++x) { + out[rowidx + x] = + std::max(0, std::min(255, (rowbuf[x] + kRowRound) >> kRowScale)); + } + } +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/idct.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/idct.h new file mode 100644 index 0000000..3e6c436 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/idct.h @@ -0,0 +1,31 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_IDCT_H_ +#define GUETZLI_IDCT_H_ + +#include "guetzli/jpeg_data.h" + +namespace guetzli { + +// Fills in 'result' with the inverse DCT of 'block'. +// The arguments 'block' and 'result' point to 8x8 arrays that are arranged in +// a row-by-row memory layout. +void ComputeBlockIDCT(const coeff_t* block, uint8_t* result); + +} // namespace guetzli + +#endif // GUETZLI_IDCT_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_bit_writer.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_bit_writer.h new file mode 100644 index 0000000..3a53137 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_bit_writer.h @@ -0,0 +1,113 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_JPEG_BIT_WRITER_H_ +#define GUETZLI_JPEG_BIT_WRITER_H_ + +#include +#include + +namespace guetzli { + +// Returns non-zero if and only if x has a zero byte, i.e. one of +// x & 0xff, x & 0xff00, ..., x & 0xff00000000000000 is zero. +inline uint64_t HasZeroByte(uint64_t x) { + return (x - 0x0101010101010101ULL) & ~x & 0x8080808080808080ULL; +} + +// Handles the packing of bits into output bytes. +struct BitWriter { + explicit BitWriter(size_t length) : len(length), + data(new uint8_t[len]), + pos(0), + put_buffer(0), + put_bits(64), + overflow(false) {} + + void WriteBits(int nbits, uint64_t bits) { + put_bits -= nbits; + put_buffer |= (bits << put_bits); + if (put_bits <= 16) { + // At this point we are ready to emit the most significant 6 bytes of + // put_buffer_ to the output. + // The JPEG format requires that after every 0xff byte in the entropy + // coded section, there is a zero byte, therefore we first check if any of + // the 6 most significant bytes of put_buffer_ is 0xff. + if (HasZeroByte(~put_buffer | 0xffff)) { + // We have a 0xff byte somewhere, examine each byte and append a zero + // byte if necessary. + EmitByte((put_buffer >> 56) & 0xff); + EmitByte((put_buffer >> 48) & 0xff); + EmitByte((put_buffer >> 40) & 0xff); + EmitByte((put_buffer >> 32) & 0xff); + EmitByte((put_buffer >> 24) & 0xff); + EmitByte((put_buffer >> 16) & 0xff); + } else if (pos + 6 < len) { + // We don't have any 0xff bytes, output all 6 bytes without checking. + data[pos] = (put_buffer >> 56) & 0xff; + data[pos + 1] = (put_buffer >> 48) & 0xff; + data[pos + 2] = (put_buffer >> 40) & 0xff; + data[pos + 3] = (put_buffer >> 32) & 0xff; + data[pos + 4] = (put_buffer >> 24) & 0xff; + data[pos + 5] = (put_buffer >> 16) & 0xff; + pos += 6; + } else { + overflow = true; + } + put_buffer <<= 48; + put_bits += 48; + } + } + + // Writes the given byte to the output, writes an extra zero if byte is 0xff. + void EmitByte(int byte) { + if (pos < len) { + data[pos++] = byte; + } else { + overflow = true; + } + if (byte == 0xff) { + EmitByte(0); + } + } + + void JumpToByteBoundary() { + while (put_bits <= 56) { + int c = (put_buffer >> 56) & 0xff; + EmitByte(c); + put_buffer <<= 8; + put_bits += 8; + } + if (put_bits < 64) { + int padmask = 0xff >> (64 - put_bits); + int c = ((put_buffer >> 56) & ~padmask) | padmask; + EmitByte(c); + } + put_buffer = 0; + put_bits = 64; + } + + size_t len; + std::unique_ptr data; + int pos; + uint64_t put_buffer; + int put_bits; + bool overflow; +}; + +} // namespace guetzli + +#endif // GUETZLI_JPEG_BIT_WRITER_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data.cc new file mode 100644 index 0000000..587251e --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data.cc @@ -0,0 +1,104 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/jpeg_data.h" + +#include +#include + +namespace guetzli { + +bool JPEGData::Is420() const { + return (components.size() == 3 && + max_h_samp_factor == 2 && + max_v_samp_factor == 2 && + components[0].h_samp_factor == 2 && + components[0].v_samp_factor == 2 && + components[1].h_samp_factor == 1 && + components[1].v_samp_factor == 1 && + components[2].h_samp_factor == 1 && + components[2].v_samp_factor == 1); +} + +bool JPEGData::Is444() const { + return (components.size() == 3 && + max_h_samp_factor == 1 && + max_v_samp_factor == 1 && + components[0].h_samp_factor == 1 && + components[0].v_samp_factor == 1 && + components[1].h_samp_factor == 1 && + components[1].v_samp_factor == 1 && + components[2].h_samp_factor == 1 && + components[2].v_samp_factor == 1); +} + +void InitJPEGDataForYUV444(int w, int h, JPEGData* jpg) { + jpg->width = w; + jpg->height = h; + jpg->max_h_samp_factor = 1; + jpg->max_v_samp_factor = 1; + jpg->MCU_rows = (h + 7) >> 3; + jpg->MCU_cols = (w + 7) >> 3; + jpg->quant.resize(3); + jpg->components.resize(3); + for (int i = 0; i < 3; ++i) { + JPEGComponent* c = &jpg->components[i]; + c->id = i; + c->h_samp_factor = 1; + c->v_samp_factor = 1; + c->quant_idx = i; + c->width_in_blocks = jpg->MCU_cols; + c->height_in_blocks = jpg->MCU_rows; + c->num_blocks = c->width_in_blocks * c->height_in_blocks; + c->coeffs.resize(c->num_blocks * kDCTBlockSize); + } +} + +void SaveQuantTables(const int q[3][kDCTBlockSize], JPEGData* jpg) { + const size_t kTableSize = kDCTBlockSize * sizeof(q[0][0]); + jpg->quant.clear(); + int num_tables = 0; + for (int i = 0; i < jpg->components.size(); ++i) { + JPEGComponent* comp = &jpg->components[i]; + // Check if we have this quant table already. + bool found = false; + for (int j = 0; j < num_tables; ++j) { + if (memcmp(&q[i][0], &jpg->quant[j].values[0], kTableSize) == 0) { + comp->quant_idx = j; + found = true; + break; + } + } + if (!found) { + JPEGQuantTable table; + memcpy(&table.values[0], &q[i][0], kTableSize); + table.precision = 0; + for (int k = 0; k < kDCTBlockSize; ++k) { + assert(table.values[k] >= 0); + assert(table.values[k] < (1 << 16)); + if (table.values[k] > 0xff) { + table.precision = 1; + } + } + table.index = num_tables; + comp->quant_idx = num_tables; + jpg->quant.push_back(table); + ++num_tables; + } + } +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data.h new file mode 100644 index 0000000..5623e51 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data.h @@ -0,0 +1,211 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Data structures that represent the contents of a jpeg file. + +#ifndef GUETZLI_JPEG_DATA_H_ +#define GUETZLI_JPEG_DATA_H_ + +#include +#include +#include +#include + +#include "guetzli/jpeg_error.h" + +namespace guetzli { + +static const int kDCTBlockSize = 64; +static const int kMaxComponents = 4; +static const int kMaxQuantTables = 4; +static const int kMaxHuffmanTables = 4; +static const int kJpegHuffmanMaxBitLength = 16; +static const int kJpegHuffmanAlphabetSize = 256; +static const int kJpegDCAlphabetSize = 12; +static const int kMaxDHTMarkers = 512; + +static const uint8_t kDefaultQuantMatrix[2][64] = { + { 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, + 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, + 72, 92, 95, 98, 112, 100, 103, 99 }, + { 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 } +}; + +const int kJPEGNaturalOrder[80] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + // extra entries for safety in decoder + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63, 63 +}; + +const int kJPEGZigZagOrder[64] = { + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63 +}; + +// Quantization values for an 8x8 pixel block. +struct JPEGQuantTable { + JPEGQuantTable() : values(kDCTBlockSize), precision(0), + index(0), is_last(true) {} + + std::vector values; + int precision; + // The index of this quantization table as it was parsed from the input JPEG. + // Each DQT marker segment contains an 'index' field, and we save this index + // here. Valid values are 0 to 3. + int index; + // Set to true if this table is the last one within its marker segment. + bool is_last; +}; + +// Huffman code and decoding lookup table used for DC and AC coefficients. +struct JPEGHuffmanCode { + JPEGHuffmanCode() : counts(kJpegHuffmanMaxBitLength + 1), + values(kJpegHuffmanAlphabetSize + 1), + slot_id(0), + is_last(true) {} + + // Bit length histogram. + std::vector counts; + // Symbol values sorted by increasing bit lengths. + std::vector values; + // The index of the Huffman code in the current set of Huffman codes. For AC + // component Huffman codes, 0x10 is added to the index. + int slot_id; + // Set to true if this Huffman code is the last one within its marker segment. + bool is_last; +}; + +// Huffman table indexes used for one component of one scan. +struct JPEGComponentScanInfo { + int comp_idx; + int dc_tbl_idx; + int ac_tbl_idx; +}; + +// Contains information that is used in one scan. +struct JPEGScanInfo { + // Parameters used for progressive scans (named the same way as in the spec): + // Ss : Start of spectral band in zig-zag sequence. + // Se : End of spectral band in zig-zag sequence. + // Ah : Successive approximation bit position, high. + // Al : Successive approximation bit position, low. + int Ss; + int Se; + int Ah; + int Al; + std::vector components; +}; + +typedef int16_t coeff_t; + +// Represents one component of a jpeg file. +struct JPEGComponent { + JPEGComponent() : id(0), + h_samp_factor(1), + v_samp_factor(1), + quant_idx(0), + width_in_blocks(0), + height_in_blocks(0) {} + + // One-byte id of the component. + int id; + // Horizontal and vertical sampling factors. + // In interleaved mode, each minimal coded unit (MCU) has + // h_samp_factor x v_samp_factor DCT blocks from this component. + int h_samp_factor; + int v_samp_factor; + // The index of the quantization table used for this component. + int quant_idx; + // The dimensions of the component measured in 8x8 blocks. + int width_in_blocks; + int height_in_blocks; + int num_blocks; + // The DCT coefficients of this component, laid out block-by-block, divided + // through the quantization matrix values. + std::vector coeffs; +}; + +// Represents a parsed jpeg file. +struct JPEGData { + JPEGData() : width(0), + height(0), + version(0), + max_h_samp_factor(1), + max_v_samp_factor(1), + MCU_rows(0), + MCU_cols(0), + restart_interval(0), + original_jpg(NULL), + original_jpg_size(0), + error(JPEG_OK) {} + + bool Is420() const; + bool Is444() const; + + int width; + int height; + int version; + int max_h_samp_factor; + int max_v_samp_factor; + int MCU_rows; + int MCU_cols; + int restart_interval; + std::vector app_data; + std::vector com_data; + std::vector quant; + std::vector huffman_code; + std::vector components; + std::vector scan_info; + std::vector marker_order; + std::vector inter_marker_data; + std::string tail_data; + const uint8_t* original_jpg; + size_t original_jpg_size; + JPEGReadError error; +}; + +void InitJPEGDataForYUV444(int w, int h, JPEGData* jpg); +void SaveQuantTables(const int q[3][kDCTBlockSize], JPEGData* jpg); + +} // namespace guetzli + +#endif // GUETZLI_JPEG_DATA_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_decoder.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_decoder.cc new file mode 100644 index 0000000..98f9f4c --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_decoder.cc @@ -0,0 +1,56 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/jpeg_data_decoder.h" + +#include "guetzli/output_image.h" + +namespace guetzli { + +// Mimic libjpeg's heuristics to guess jpeg color space. +// Requires that the jpg has 3 components. +bool HasYCbCrColorSpace(const JPEGData& jpg) { + bool has_Adobe_marker = false; + uint8_t Adobe_transform = 0; + for (const std::string& app : jpg.app_data) { + if (static_cast(app[0]) == 0xe0) { + return true; + } else if (static_cast(app[0]) == 0xee && app.size() >= 15) { + has_Adobe_marker = true; + Adobe_transform = app[14]; + } + } + if (has_Adobe_marker) { + return (Adobe_transform != 0); + } + const int cid0 = jpg.components[0].id; + const int cid1 = jpg.components[1].id; + const int cid2 = jpg.components[2].id; + return (cid0 != 'R' || cid1 != 'G' || cid2 != 'B'); +} + +std::vector DecodeJpegToRGB(const JPEGData& jpg) { + if (jpg.components.size() == 1 || + (jpg.components.size() == 3 && + HasYCbCrColorSpace(jpg) && (jpg.Is420() || jpg.Is444()))) { + OutputImage img(jpg.width, jpg.height); + img.CopyFromJpegData(jpg); + return img.ToSRGB(); + } + return std::vector(); +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_decoder.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_decoder.h new file mode 100644 index 0000000..11679dc --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_decoder.h @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Library to decode jpeg coefficients into an RGB image. + +#ifndef GUETZLI_JPEG_DATA_DECODER_H_ +#define GUETZLI_JPEG_DATA_DECODER_H_ + +#include + +#include "guetzli/jpeg_data.h" + +namespace guetzli { + +// Decodes the parsed jpeg coefficients into an RGB image. +// There can be only either 1 or 3 image components, in either case, an RGB +// output image will be generated. +// Only YUV420 and YUV444 sampling factors are supported. +// Vector will be empty if a decoding error occurred. +std::vector DecodeJpegToRGB(const JPEGData& jpg); + +// Mimic libjpeg's heuristics to guess jpeg color space. +// Requires that the jpg has 3 components. +bool HasYCbCrColorSpace(const JPEGData& jpg); + +} // namespace guetzli + +#endif // GUETZLI_JPEG_DATA_DECODER_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_encoder.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_encoder.cc new file mode 100644 index 0000000..1895d22 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_encoder.cc @@ -0,0 +1,138 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/jpeg_data_encoder.h" + +#include +#include + +#include "guetzli/fdct.h" + +namespace guetzli { + +namespace { + +static const int kIQuantBits = 16; +// Output of the DCT is upscaled by 16. +static const int kDCTBits = kIQuantBits + 4; +static const int kBias = 0x80 << (kDCTBits - 8); + +void Quantize(coeff_t* v, int iquant) { + *v = (*v * iquant + kBias) >> kDCTBits; +} + +// Single pixel rgb to 16-bit yuv conversion. +// The returned yuv values are signed integers in the +// range [-128, 127] inclusive. +inline static void RGBToYUV16(const uint8_t* const rgb, + coeff_t *out) { + enum { FRAC = 16, HALF = 1 << (FRAC - 1) }; + const int r = rgb[0]; + const int g = rgb[1]; + const int b = rgb[2]; + out[0] = (19595 * r + 38469 * g + 7471 * b - (128 << 16) + HALF) >> FRAC; + out[64] = (-11059 * r - 21709 * g + 32768 * b + HALF - 1) >> FRAC; + out[128] = (32768 * r - 27439 * g - 5329 * b + HALF - 1) >> FRAC; +} + +} // namespace + +void AddApp0Data(JPEGData* jpg) { + const unsigned char kApp0Data[] = { + 0xe0, 0x00, 0x10, // APP0 + 0x4a, 0x46, 0x49, 0x46, 0x00, // 'JFIF' + 0x01, 0x01, // v1.01 + 0x00, 0x00, 0x01, 0x00, 0x01, // aspect ratio = 1:1 + 0x00, 0x00 // thumbnail width/height + }; + jpg->app_data.push_back( + std::string(reinterpret_cast(kApp0Data), + sizeof(kApp0Data))); +} + +bool EncodeRGBToJpeg(const std::vector& rgb, int w, int h, + const int* quant, JPEGData* jpg) { + if (w < 0 || w >= 1 << 16 || h < 0 || h >= 1 << 16 || + rgb.size() != 3 * w * h) { + return false; + } + InitJPEGDataForYUV444(w, h, jpg); + AddApp0Data(jpg); + + int iquant[3 * kDCTBlockSize]; + int idx = 0; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < kDCTBlockSize; ++j) { + int v = quant[idx]; + jpg->quant[i].values[j] = v; + iquant[idx++] = ((1 << kIQuantBits) + 1) / v; + } + } + + // Compute YUV444 DCT coefficients. + int block_ix = 0; + for (int block_y = 0; block_y < jpg->MCU_rows; ++block_y) { + for (int block_x = 0; block_x < jpg->MCU_cols; ++block_x) { + coeff_t block[3 * kDCTBlockSize]; + // RGB->YUV transform. + for (int iy = 0; iy < 8; ++iy) { + for (int ix = 0; ix < 8; ++ix) { + int y = std::min(h - 1, 8 * block_y + iy); + int x = std::min(w - 1, 8 * block_x + ix); + int p = y * w + x; + RGBToYUV16(&rgb[3 * p], &block[8 * iy + ix]); + } + } + // DCT + for (int i = 0; i < 3; ++i) { + ComputeBlockDCT(&block[i * kDCTBlockSize]); + } + // Quantization + for (int i = 0; i < 3 * 64; ++i) { + Quantize(&block[i], iquant[i]); + } + // Copy the resulting coefficients to *jpg. + for (int i = 0; i < 3; ++i) { + memcpy(&jpg->components[i].coeffs[block_ix * kDCTBlockSize], + &block[i * kDCTBlockSize], kDCTBlockSize * sizeof(block[0])); + } + ++block_ix; + } + } + + return true; +} + +bool EncodeRGBToJpeg(const std::vector& rgb, int w, int h, + JPEGData* jpg) { + static const int quant[3 * kDCTBlockSize] = { + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + }; + return EncodeRGBToJpeg(rgb, w, h, quant, jpg); +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_encoder.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_encoder.h new file mode 100644 index 0000000..06b3315 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_encoder.h @@ -0,0 +1,41 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_JPEG_DATA_ENCODER_H_ +#define GUETZLI_JPEG_DATA_ENCODER_H_ + +#include + +#include "guetzli/jpeg_data.h" + +namespace guetzli { + + +// Adds APP0 header data. +void AddApp0Data(JPEGData* jpg); + +// Creates a JPEG from the rgb pixel data. Returns true on success. +bool EncodeRGBToJpeg(const std::vector& rgb, int w, int h, + JPEGData* jpg); + +// Creates a JPEG from the rgb pixel data. Returns true on success. The given +// quantization table must have 3 * kDCTBlockSize values. +bool EncodeRGBToJpeg(const std::vector& rgb, int w, int h, + const int* quant, JPEGData* jpg); + +} // namespace guetzli + +#endif // GUETZLI_JPEG_DATA_ENCODER_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_reader.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_reader.cc new file mode 100644 index 0000000..97021e7 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_reader.cc @@ -0,0 +1,1076 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/jpeg_data_reader.h" + +#include +#include +#include + +#include "guetzli/jpeg_huffman_decode.h" + +namespace guetzli { + +namespace { + +// Macros for commonly used error conditions. + +#define VERIFY_LEN(n) \ + if (*pos + (n) > len) { \ + fprintf(stderr, "Unexpected end of input: pos=%d need=%d len=%d\n", \ + static_cast(*pos), static_cast(n), \ + static_cast(len)); \ + jpg->error = JPEG_UNEXPECTED_EOF; \ + return false; \ + } + +#define VERIFY_INPUT(var, low, high, code) \ + if (var < low || var > high) { \ + fprintf(stderr, "Invalid %s: %d\n", #var, static_cast(var)); \ + jpg->error = JPEG_INVALID_ ## code; \ + return false; \ + } + +#define VERIFY_MARKER_END() \ + if (start_pos + marker_len != *pos) { \ + fprintf(stderr, "Invalid marker length: declared=%d actual=%d\n", \ + static_cast(marker_len), \ + static_cast(*pos - start_pos)); \ + jpg->error = JPEG_WRONG_MARKER_SIZE; \ + return false; \ + } + +#define EXPECT_MARKER() \ + if (pos + 2 > len || data[pos] != 0xff) { \ + fprintf(stderr, "Marker byte (0xff) expected, found: %d " \ + "pos=%d len=%d\n", \ + (pos < len ? data[pos] : 0), static_cast(pos), \ + static_cast(len)); \ + jpg->error = JPEG_MARKER_BYTE_NOT_FOUND; \ + return false; \ + } + +// Returns ceil(a/b). +inline int DivCeil(int a, int b) { + return (a + b - 1) / b; +} + +inline int ReadUint8(const uint8_t* data, size_t* pos) { + return data[(*pos)++]; +} + +inline int ReadUint16(const uint8_t* data, size_t* pos) { + int v = (data[*pos] << 8) + data[*pos + 1]; + *pos += 2; + return v; +} + +// Reads the Start of Frame (SOF) marker segment and fills in *jpg with the +// parsed data. +bool ProcessSOF(const uint8_t* data, const size_t len, + JpegReadMode mode, size_t* pos, JPEGData* jpg) { + if (jpg->width != 0) { + fprintf(stderr, "Duplicate SOF marker.\n"); + jpg->error = JPEG_DUPLICATE_SOF; + return false; + } + const size_t start_pos = *pos; + VERIFY_LEN(8); + size_t marker_len = ReadUint16(data, pos); + int precision = ReadUint8(data, pos); + int height = ReadUint16(data, pos); + int width = ReadUint16(data, pos); + int num_components = ReadUint8(data, pos); + VERIFY_INPUT(precision, 8, 8, PRECISION); + VERIFY_INPUT(height, 1, 65535, HEIGHT); + VERIFY_INPUT(width, 1, 65535, WIDTH); + VERIFY_INPUT(num_components, 1, kMaxComponents, NUMCOMP); + VERIFY_LEN(3 * num_components); + jpg->height = height; + jpg->width = width; + jpg->components.resize(num_components); + + // Read sampling factors and quant table index for each component. + std::vector ids_seen(256, false); + for (int i = 0; i < jpg->components.size(); ++i) { + const int id = ReadUint8(data, pos); + if (ids_seen[id]) { // (cf. section B.2.2, syntax of Ci) + fprintf(stderr, "Duplicate ID %d in SOF.\n", id); + jpg->error = JPEG_DUPLICATE_COMPONENT_ID; + return false; + } + ids_seen[id] = true; + jpg->components[i].id = id; + int factor = ReadUint8(data, pos); + int h_samp_factor = factor >> 4; + int v_samp_factor = factor & 0xf; + VERIFY_INPUT(h_samp_factor, 1, 15, SAMP_FACTOR); + VERIFY_INPUT(v_samp_factor, 1, 15, SAMP_FACTOR); + jpg->components[i].h_samp_factor = h_samp_factor; + jpg->components[i].v_samp_factor = v_samp_factor; + jpg->components[i].quant_idx = ReadUint8(data, pos); + jpg->max_h_samp_factor = std::max(jpg->max_h_samp_factor, h_samp_factor); + jpg->max_v_samp_factor = std::max(jpg->max_v_samp_factor, v_samp_factor); + } + + // We have checked above that none of the sampling factors are 0, so the max + // sampling factors can not be 0. + jpg->MCU_rows = DivCeil(jpg->height, jpg->max_v_samp_factor * 8); + jpg->MCU_cols = DivCeil(jpg->width, jpg->max_h_samp_factor * 8); + // Compute the block dimensions for each component. + if (mode == JPEG_READ_ALL) { + for (int i = 0; i < jpg->components.size(); ++i) { + JPEGComponent* c = &jpg->components[i]; + if (jpg->max_h_samp_factor % c->h_samp_factor != 0 || + jpg->max_v_samp_factor % c->v_samp_factor != 0) { + fprintf(stderr, "Non-integral subsampling ratios.\n"); + jpg->error = JPEG_INVALID_SAMPLING_FACTORS; + return false; + } + c->width_in_blocks = jpg->MCU_cols * c->h_samp_factor; + c->height_in_blocks = jpg->MCU_rows * c->v_samp_factor; + const uint64_t num_blocks = + static_cast(c->width_in_blocks) * c->height_in_blocks; + if (num_blocks > (1ull << 21)) { + // Refuse to allocate more than 1 GB of memory for the coefficients, + // that is 2M blocks x 64 coeffs x 2 bytes per coeff x max 4 components. + // TODO(user) Add this limit to a GuetzliParams struct. + fprintf(stderr, "Image too large.\n"); + jpg->error = JPEG_IMAGE_TOO_LARGE; + return false; + } + c->num_blocks = static_cast(num_blocks); + c->coeffs.resize(c->num_blocks * kDCTBlockSize); + } + } + VERIFY_MARKER_END(); + return true; +} + +// Reads the Start of Scan (SOS) marker segment and fills in *scan_info with the +// parsed data. +bool ProcessSOS(const uint8_t* data, const size_t len, size_t* pos, + JPEGData* jpg) { + const size_t start_pos = *pos; + VERIFY_LEN(3); + size_t marker_len = ReadUint16(data, pos); + int comps_in_scan = ReadUint8(data, pos); + VERIFY_INPUT(comps_in_scan, 1, jpg->components.size(), COMPS_IN_SCAN); + + JPEGScanInfo scan_info; + scan_info.components.resize(comps_in_scan); + VERIFY_LEN(2 * comps_in_scan); + std::vector ids_seen(256, false); + for (int i = 0; i < comps_in_scan; ++i) { + int id = ReadUint8(data, pos); + if (ids_seen[id]) { // (cf. section B.2.3, regarding CSj) + fprintf(stderr, "Duplicate ID %d in SOS.\n", id); + jpg->error = JPEG_DUPLICATE_COMPONENT_ID; + return false; + } + ids_seen[id] = true; + bool found_index = false; + for (int j = 0; j < jpg->components.size(); ++j) { + if (jpg->components[j].id == id) { + scan_info.components[i].comp_idx = j; + found_index = true; + } + } + if (!found_index) { + fprintf(stderr, "SOS marker: Could not find component with id %d\n", id); + jpg->error = JPEG_COMPONENT_NOT_FOUND; + return false; + } + int c = ReadUint8(data, pos); + int dc_tbl_idx = c >> 4; + int ac_tbl_idx = c & 0xf; + VERIFY_INPUT(dc_tbl_idx, 0, 3, HUFFMAN_INDEX); + VERIFY_INPUT(ac_tbl_idx, 0, 3, HUFFMAN_INDEX); + scan_info.components[i].dc_tbl_idx = dc_tbl_idx; + scan_info.components[i].ac_tbl_idx = ac_tbl_idx; + } + VERIFY_LEN(3); + scan_info.Ss = ReadUint8(data, pos); + scan_info.Se = ReadUint8(data, pos); + VERIFY_INPUT(scan_info.Ss, 0, 63, START_OF_SCAN); + VERIFY_INPUT(scan_info.Se, scan_info.Ss, 63, END_OF_SCAN); + int c = ReadUint8(data, pos); + scan_info.Ah = c >> 4; + scan_info.Al = c & 0xf; + // Check that all the Huffman tables needed for this scan are defined. + for (int i = 0; i < comps_in_scan; ++i) { + bool found_dc_table = false; + bool found_ac_table = false; + for (int j = 0; j < jpg->huffman_code.size(); ++j) { + int slot_id = jpg->huffman_code[j].slot_id; + if (slot_id == scan_info.components[i].dc_tbl_idx) { + found_dc_table = true; + } else if (slot_id == scan_info.components[i].ac_tbl_idx + 16) { + found_ac_table = true; + } + } + if (scan_info.Ss == 0 && !found_dc_table) { + fprintf(stderr, "SOS marker: Could not find DC Huffman table with index " + "%d\n", scan_info.components[i].dc_tbl_idx); + jpg->error = JPEG_HUFFMAN_TABLE_NOT_FOUND; + return false; + } + if (scan_info.Se > 0 && !found_ac_table) { + fprintf(stderr, "SOS marker: Could not find AC Huffman table with index " + "%d\n", scan_info.components[i].ac_tbl_idx); + jpg->error = JPEG_HUFFMAN_TABLE_NOT_FOUND; + return false; + } + } + jpg->scan_info.push_back(scan_info); + VERIFY_MARKER_END(); + return true; +} + +// Reads the Define Huffman Table (DHT) marker segment and fills in *jpg with +// the parsed data. Builds the Huffman decoding table in either dc_huff_lut or +// ac_huff_lut, depending on the type and solt_id of Huffman code being read. +bool ProcessDHT(const uint8_t* data, const size_t len, + JpegReadMode mode, + std::vector* dc_huff_lut, + std::vector* ac_huff_lut, + size_t* pos, + JPEGData* jpg) { + const size_t start_pos = *pos; + VERIFY_LEN(2); + size_t marker_len = ReadUint16(data, pos); + if (marker_len == 2) { + fprintf(stderr, "DHT marker: no Huffman table found\n"); + jpg->error = JPEG_EMPTY_DHT; + return false; + } + while (*pos < start_pos + marker_len) { + VERIFY_LEN(1 + kJpegHuffmanMaxBitLength); + JPEGHuffmanCode huff; + huff.slot_id = ReadUint8(data, pos); + int huffman_index = huff.slot_id; + int is_ac_table = (huff.slot_id & 0x10) != 0; + HuffmanTableEntry* huff_lut; + if (is_ac_table) { + huffman_index -= 0x10; + VERIFY_INPUT(huffman_index, 0, 3, HUFFMAN_INDEX); + huff_lut = &(*ac_huff_lut)[huffman_index * kJpegHuffmanLutSize]; + } else { + VERIFY_INPUT(huffman_index, 0, 3, HUFFMAN_INDEX); + huff_lut = &(*dc_huff_lut)[huffman_index * kJpegHuffmanLutSize]; + } + huff.counts[0] = 0; + int total_count = 0; + int space = 1 << kJpegHuffmanMaxBitLength; + int max_depth = 1; + for (int i = 1; i <= kJpegHuffmanMaxBitLength; ++i) { + int count = ReadUint8(data, pos); + if (count != 0) { + max_depth = i; + } + huff.counts[i] = count; + total_count += count; + space -= count * (1 << (kJpegHuffmanMaxBitLength - i)); + } + if (is_ac_table) { + VERIFY_INPUT(total_count, 0, kJpegHuffmanAlphabetSize, HUFFMAN_CODE); + } else { + VERIFY_INPUT(total_count, 0, kJpegDCAlphabetSize, HUFFMAN_CODE); + } + VERIFY_LEN(total_count); + std::vector values_seen(256, false); + for (int i = 0; i < total_count; ++i) { + uint8_t value = ReadUint8(data, pos); + if (!is_ac_table) { + VERIFY_INPUT(value, 0, kJpegDCAlphabetSize - 1, HUFFMAN_CODE); + } + if (values_seen[value]) { + fprintf(stderr, "Duplicate Huffman code value %d\n", value); + jpg->error = JPEG_INVALID_HUFFMAN_CODE; + return false; + } + values_seen[value] = true; + huff.values[i] = value; + } + // Add an invalid symbol that will have the all 1 code. + ++huff.counts[max_depth]; + huff.values[total_count] = kJpegHuffmanAlphabetSize; + space -= (1 << (kJpegHuffmanMaxBitLength - max_depth)); + if (space < 0) { + fprintf(stderr, "Invalid Huffman code lengths.\n"); + jpg->error = JPEG_INVALID_HUFFMAN_CODE; + return false; + } else if (space > 0 && huff_lut[0].value != 0xffff) { + // Re-initialize the values to an invalid symbol so that we can recognize + // it when reading the bit stream using a Huffman code with space > 0. + for (int i = 0; i < kJpegHuffmanLutSize; ++i) { + huff_lut[i].bits = 0; + huff_lut[i].value = 0xffff; + } + } + huff.is_last = (*pos == start_pos + marker_len); + if (mode == JPEG_READ_ALL && + !BuildJpegHuffmanTable(&huff.counts[0], &huff.values[0], huff_lut)) { + fprintf(stderr, "Failed to build Huffman table.\n"); + jpg->error = JPEG_INVALID_HUFFMAN_CODE; + return false; + } + jpg->huffman_code.push_back(huff); + } + VERIFY_MARKER_END(); + return true; +} + +// Reads the Define Quantization Table (DQT) marker segment and fills in *jpg +// with the parsed data. +bool ProcessDQT(const uint8_t* data, const size_t len, size_t* pos, + JPEGData* jpg) { + const size_t start_pos = *pos; + VERIFY_LEN(2); + size_t marker_len = ReadUint16(data, pos); + if (marker_len == 2) { + fprintf(stderr, "DQT marker: no quantization table found\n"); + jpg->error = JPEG_EMPTY_DQT; + return false; + } + while (*pos < start_pos + marker_len && jpg->quant.size() < kMaxQuantTables) { + VERIFY_LEN(1); + int quant_table_index = ReadUint8(data, pos); + int quant_table_precision = quant_table_index >> 4; + quant_table_index &= 0xf; + VERIFY_INPUT(quant_table_index, 0, 3, QUANT_TBL_INDEX); + VERIFY_LEN((quant_table_precision ? 2 : 1) * kDCTBlockSize); + JPEGQuantTable table; + table.index = quant_table_index; + table.precision = quant_table_precision; + for (int i = 0; i < kDCTBlockSize; ++i) { + int quant_val = quant_table_precision ? + ReadUint16(data, pos) : + ReadUint8(data, pos); + VERIFY_INPUT(quant_val, 1, 65535, QUANT_VAL); + table.values[kJPEGNaturalOrder[i]] = quant_val; + } + table.is_last = (*pos == start_pos + marker_len); + jpg->quant.push_back(table); + } + VERIFY_MARKER_END(); + return true; +} + +// Reads the DRI marker and saved the restart interval into *jpg. +bool ProcessDRI(const uint8_t* data, const size_t len, size_t* pos, + JPEGData* jpg) { + if (jpg->restart_interval > 0) { + fprintf(stderr, "Duplicate DRI marker.\n"); + jpg->error = JPEG_DUPLICATE_DRI; + return false; + } + const size_t start_pos = *pos; + VERIFY_LEN(4); + size_t marker_len = ReadUint16(data, pos); + int restart_interval = ReadUint16(data, pos); + jpg->restart_interval = restart_interval; + VERIFY_MARKER_END(); + return true; +} + +// Saves the APP marker segment as a string to *jpg. +bool ProcessAPP(const uint8_t* data, const size_t len, size_t* pos, + JPEGData* jpg) { + VERIFY_LEN(2); + size_t marker_len = ReadUint16(data, pos); + VERIFY_INPUT(marker_len, 2, 65535, MARKER_LEN); + VERIFY_LEN(marker_len - 2); + // Save the marker type together with the app data. + std::string app_str(reinterpret_cast( + &data[*pos - 3]), marker_len + 1); + *pos += marker_len - 2; + jpg->app_data.push_back(app_str); + return true; +} + +// Saves the COM marker segment as a string to *jpg. +bool ProcessCOM(const uint8_t* data, const size_t len, size_t* pos, + JPEGData* jpg) { + VERIFY_LEN(2); + size_t marker_len = ReadUint16(data, pos); + VERIFY_INPUT(marker_len, 2, 65535, MARKER_LEN); + VERIFY_LEN(marker_len - 2); + std::string com_str(reinterpret_cast( + &data[*pos - 2]), marker_len); + *pos += marker_len - 2; + jpg->com_data.push_back(com_str); + return true; +} + +// Helper structure to read bits from the entropy coded data segment. +struct BitReaderState { + BitReaderState(const uint8_t* data, const size_t len, size_t pos) + : data_(data), len_(len) { + Reset(pos); + } + + void Reset(size_t pos) { + pos_ = pos; + val_ = 0; + bits_left_ = 0; + next_marker_pos_ = len_ - 2; + FillBitWindow(); + } + + // Returns the next byte and skips the 0xff/0x00 escape sequences. + uint8_t GetNextByte() { + if (pos_ >= next_marker_pos_) { + ++pos_; + return 0; + } + uint8_t c = data_[pos_++]; + if (c == 0xff) { + uint8_t escape = data_[pos_]; + if (escape == 0) { + ++pos_; + } else { + // 0xff was followed by a non-zero byte, which means that we found the + // start of the next marker segment. + next_marker_pos_ = pos_ - 1; + } + } + return c; + } + + void FillBitWindow() { + if (bits_left_ <= 16) { + while (bits_left_ <= 56) { + val_ <<= 8; + val_ |= (uint64_t)GetNextByte(); + bits_left_ += 8; + } + } + } + + int ReadBits(int nbits) { + FillBitWindow(); + uint64_t val = (val_ >> (bits_left_ - nbits)) & ((1ULL << nbits) - 1); + bits_left_ -= nbits; + return val; + } + + // Sets *pos to the next stream position where parsing should continue. + // Returns false if the stream ended too early. + bool FinishStream(size_t* pos) { + // Give back some bytes that we did not use. + int unused_bytes_left = bits_left_ >> 3; + while (unused_bytes_left-- > 0) { + --pos_; + // If we give back a 0 byte, we need to check if it was a 0xff/0x00 escape + // sequence, and if yes, we need to give back one more byte. + if (pos_ < next_marker_pos_ && + data_[pos_] == 0 && data_[pos_ - 1] == 0xff) { + --pos_; + } + } + if (pos_ > next_marker_pos_) { + // Data ran out before the scan was complete. + fprintf(stderr, "Unexpected end of scan.\n"); + return false; + } + *pos = pos_; + return true; + } + + const uint8_t* data_; + const size_t len_; + size_t pos_; + uint64_t val_; + int bits_left_; + size_t next_marker_pos_; +}; + +// Returns the next Huffman-coded symbol. +int ReadSymbol(const HuffmanTableEntry* table, BitReaderState* br) { + int nbits; + br->FillBitWindow(); + int val = (br->val_ >> (br->bits_left_ - 8)) & 0xff; + table += val; + nbits = table->bits - 8; + if (nbits > 0) { + br->bits_left_ -= 8; + table += table->value; + val = (br->val_ >> (br->bits_left_ - nbits)) & ((1 << nbits) - 1); + table += val; + } + br->bits_left_ -= table->bits; + return table->value; +} + +// Returns the DC diff or AC value for extra bits value x and prefix code s. +// See Tables F.1 and F.2 of the spec. +int HuffExtend(int x, int s) { + return (x < (1 << (s - 1)) ? x + ((-1) << s ) + 1 : x); +} + +// Decodes one 8x8 block of DCT coefficients from the bit stream. +bool DecodeDCTBlock(const HuffmanTableEntry* dc_huff, + const HuffmanTableEntry* ac_huff, + int Ss, int Se, int Al, + int* eobrun, + BitReaderState* br, + JPEGData* jpg, + coeff_t* last_dc_coeff, + coeff_t* coeffs) { + int s; + int r; + bool eobrun_allowed = Ss > 0; + if (Ss == 0) { + s = ReadSymbol(dc_huff, br); + if (s >= kJpegDCAlphabetSize) { + fprintf(stderr, "Invalid Huffman symbol %d for DC coefficient.\n", s); + jpg->error = JPEG_INVALID_SYMBOL; + return false; + } + if (s > 0) { + r = br->ReadBits(s); + s = HuffExtend(r, s); + } + s += *last_dc_coeff; + const int dc_coeff = s << Al; + coeffs[0] = dc_coeff; + if (dc_coeff != coeffs[0]) { + fprintf(stderr, "Invalid DC coefficient %d\n", dc_coeff); + jpg->error = JPEG_NON_REPRESENTABLE_DC_COEFF; + return false; + } + *last_dc_coeff = s; + ++Ss; + } + if (Ss > Se) { + return true; + } + if (*eobrun > 0) { + --(*eobrun); + return true; + } + for (int k = Ss; k <= Se; k++) { + s = ReadSymbol(ac_huff, br); + if (s >= kJpegHuffmanAlphabetSize) { + fprintf(stderr, "Invalid Huffman symbol %d for AC coefficient %d\n", + s, k); + jpg->error = JPEG_INVALID_SYMBOL; + return false; + } + r = s >> 4; + s &= 15; + if (s > 0) { + k += r; + if (k > Se) { + fprintf(stderr, "Out-of-band coefficient %d band was %d-%d\n", + k, Ss, Se); + jpg->error = JPEG_OUT_OF_BAND_COEFF; + return false; + } + if (s + Al >= kJpegDCAlphabetSize) { + fprintf(stderr, "Out of range AC coefficient value: s=%d Al=%d k=%d\n", + s, Al, k); + jpg->error = JPEG_NON_REPRESENTABLE_AC_COEFF; + return false; + } + r = br->ReadBits(s); + s = HuffExtend(r, s); + coeffs[kJPEGNaturalOrder[k]] = s << Al; + } else if (r == 15) { + k += 15; + } else { + *eobrun = 1 << r; + if (r > 0) { + if (!eobrun_allowed) { + fprintf(stderr, "End-of-block run crossing DC coeff.\n"); + jpg->error = JPEG_EOB_RUN_TOO_LONG; + return false; + } + *eobrun += br->ReadBits(r); + } + break; + } + } + --(*eobrun); + return true; +} + +bool RefineDCTBlock(const HuffmanTableEntry* ac_huff, + int Ss, int Se, int Al, + int* eobrun, + BitReaderState* br, + JPEGData* jpg, + coeff_t* coeffs) { + bool eobrun_allowed = Ss > 0; + if (Ss == 0) { + int s = br->ReadBits(1); + coeff_t dc_coeff = coeffs[0]; + dc_coeff |= s << Al; + coeffs[0] = dc_coeff; + ++Ss; + } + if (Ss > Se) { + return true; + } + int p1 = 1 << Al; + int m1 = (-1) << Al; + int k = Ss; + int r; + int s; + bool in_zero_run = false; + if (*eobrun <= 0) { + for (; k <= Se; k++) { + s = ReadSymbol(ac_huff, br); + if (s >= kJpegHuffmanAlphabetSize) { + fprintf(stderr, "Invalid Huffman symbol %d for AC coefficient %d\n", + s, k); + jpg->error = JPEG_INVALID_SYMBOL; + return false; + } + r = s >> 4; + s &= 15; + if (s) { + if (s != 1) { + fprintf(stderr, "Invalid Huffman symbol %d for AC coefficient %d\n", + s, k); + jpg->error = JPEG_INVALID_SYMBOL; + return false; + } + s = br->ReadBits(1) ? p1 : m1; + in_zero_run = false; + } else { + if (r != 15) { + *eobrun = 1 << r; + if (r > 0) { + if (!eobrun_allowed) { + fprintf(stderr, "End-of-block run crossing DC coeff.\n"); + jpg->error = JPEG_EOB_RUN_TOO_LONG; + return false; + } + *eobrun += br->ReadBits(r); + } + break; + } + in_zero_run = true; + } + do { + coeff_t thiscoef = coeffs[kJPEGNaturalOrder[k]]; + if (thiscoef != 0) { + if (br->ReadBits(1)) { + if ((thiscoef & p1) == 0) { + if (thiscoef >= 0) { + thiscoef += p1; + } else { + thiscoef += m1; + } + } + } + coeffs[kJPEGNaturalOrder[k]] = thiscoef; + } else { + if (--r < 0) { + break; + } + } + k++; + } while (k <= Se); + if (s) { + if (k > Se) { + fprintf(stderr, "Out-of-band coefficient %d band was %d-%d\n", + k, Ss, Se); + jpg->error = JPEG_OUT_OF_BAND_COEFF; + return false; + } + coeffs[kJPEGNaturalOrder[k]] = s; + } + } + } + if (in_zero_run) { + fprintf(stderr, "Extra zero run before end-of-block.\n"); + jpg->error = JPEG_EXTRA_ZERO_RUN; + return false; + } + if (*eobrun > 0) { + for (; k <= Se; k++) { + coeff_t thiscoef = coeffs[kJPEGNaturalOrder[k]]; + if (thiscoef != 0) { + if (br->ReadBits(1)) { + if ((thiscoef & p1) == 0) { + if (thiscoef >= 0) { + thiscoef += p1; + } else { + thiscoef += m1; + } + } + } + coeffs[kJPEGNaturalOrder[k]] = thiscoef; + } + } + } + --(*eobrun); + return true; +} + +bool ProcessRestart(const uint8_t* data, const size_t len, + int* next_restart_marker, BitReaderState* br, + JPEGData* jpg) { + size_t pos = 0; + if (!br->FinishStream(&pos)) { + jpg->error = JPEG_INVALID_SCAN; + return false; + } + int expected_marker = 0xd0 + *next_restart_marker; + EXPECT_MARKER(); + int marker = data[pos + 1]; + if (marker != expected_marker) { + fprintf(stderr, "Did not find expected restart marker %d actual=%d\n", + expected_marker, marker); + jpg->error = JPEG_WRONG_RESTART_MARKER; + return false; + } + br->Reset(pos + 2); + *next_restart_marker += 1; + *next_restart_marker &= 0x7; + return true; +} + +bool ProcessScan(const uint8_t* data, const size_t len, + const std::vector& dc_huff_lut, + const std::vector& ac_huff_lut, + uint16_t scan_progression[kMaxComponents][kDCTBlockSize], + bool is_progressive, + size_t* pos, + JPEGData* jpg) { + if (!ProcessSOS(data, len, pos, jpg)) { + return false; + } + JPEGScanInfo* scan_info = &jpg->scan_info.back(); + bool is_interleaved = (scan_info->components.size() > 1); + int MCUs_per_row; + int MCU_rows; + if (is_interleaved) { + MCUs_per_row = jpg->MCU_cols; + MCU_rows = jpg->MCU_rows; + } else { + const JPEGComponent& c = jpg->components[scan_info->components[0].comp_idx]; + MCUs_per_row = + DivCeil(jpg->width * c.h_samp_factor, 8 * jpg->max_h_samp_factor); + MCU_rows = + DivCeil(jpg->height * c.v_samp_factor, 8 * jpg->max_v_samp_factor); + } + coeff_t last_dc_coeff[kMaxComponents] = {0}; + BitReaderState br(data, len, *pos); + int restarts_to_go = jpg->restart_interval; + int next_restart_marker = 0; + int eobrun = -1; + int block_scan_index = 0; + const int Al = is_progressive ? scan_info->Al : 0; + const int Ah = is_progressive ? scan_info->Ah : 0; + const int Ss = is_progressive ? scan_info->Ss : 0; + const int Se = is_progressive ? scan_info->Se : 63; + const uint16_t scan_bitmask = Ah == 0 ? (0xffff << Al) : (1u << Al); + const uint16_t refinement_bitmask = (1 << Al) - 1; + for (int i = 0; i < scan_info->components.size(); ++i) { + int comp_idx = scan_info->components[i].comp_idx; + for (int k = Ss; k <= Se; ++k) { + if (scan_progression[comp_idx][k] & scan_bitmask) { + fprintf(stderr, "Overlapping scans: component=%d k=%d prev_mask=%d " + "cur_mask=%d\n", comp_idx, k, scan_progression[i][k], + scan_bitmask); + jpg->error = JPEG_OVERLAPPING_SCANS; + return false; + } + if (scan_progression[comp_idx][k] & refinement_bitmask) { + fprintf(stderr, "Invalid scan order, a more refined scan was already " + "done: component=%d k=%d prev_mask=%d cur_mask=%d\n", comp_idx, + k, scan_progression[i][k], scan_bitmask); + jpg->error = JPEG_INVALID_SCAN_ORDER; + return false; + } + scan_progression[comp_idx][k] |= scan_bitmask; + } + } + if (Al > 10) { + fprintf(stderr, "Scan parameter Al=%d is not supported in guetzli.\n", Al); + jpg->error = JPEG_NON_REPRESENTABLE_AC_COEFF; + return false; + } + for (int mcu_y = 0; mcu_y < MCU_rows; ++mcu_y) { + for (int mcu_x = 0; mcu_x < MCUs_per_row; ++mcu_x) { + // Handle the restart intervals. + if (jpg->restart_interval > 0) { + if (restarts_to_go == 0) { + if (ProcessRestart(data, len, + &next_restart_marker, &br, jpg)) { + restarts_to_go = jpg->restart_interval; + memset(last_dc_coeff, 0, sizeof(last_dc_coeff)); + if (eobrun > 0) { + fprintf(stderr, "End-of-block run too long.\n"); + jpg->error = JPEG_EOB_RUN_TOO_LONG; + return false; + } + eobrun = -1; // fresh start + } else { + return false; + } + } + --restarts_to_go; + } + // Decode one MCU. + for (int i = 0; i < scan_info->components.size(); ++i) { + JPEGComponentScanInfo* si = &scan_info->components[i]; + JPEGComponent* c = &jpg->components[si->comp_idx]; + const HuffmanTableEntry* dc_lut = + &dc_huff_lut[si->dc_tbl_idx * kJpegHuffmanLutSize]; + const HuffmanTableEntry* ac_lut = + &ac_huff_lut[si->ac_tbl_idx * kJpegHuffmanLutSize]; + int nblocks_y = is_interleaved ? c->v_samp_factor : 1; + int nblocks_x = is_interleaved ? c->h_samp_factor : 1; + for (int iy = 0; iy < nblocks_y; ++iy) { + for (int ix = 0; ix < nblocks_x; ++ix) { + int block_y = mcu_y * nblocks_y + iy; + int block_x = mcu_x * nblocks_x + ix; + int block_idx = block_y * c->width_in_blocks + block_x; + coeff_t* coeffs = &c->coeffs[block_idx * kDCTBlockSize]; + if (Ah == 0) { + if (!DecodeDCTBlock(dc_lut, ac_lut, Ss, Se, Al, &eobrun, &br, jpg, + &last_dc_coeff[si->comp_idx], coeffs)) { + return false; + } + } else { + if (!RefineDCTBlock(ac_lut, Ss, Se, Al, + &eobrun, &br, jpg, coeffs)) { + return false; + } + } + ++block_scan_index; + } + } + } + } + } + if (eobrun > 0) { + fprintf(stderr, "End-of-block run too long.\n"); + jpg->error = JPEG_EOB_RUN_TOO_LONG; + return false; + } + if (!br.FinishStream(pos)) { + jpg->error = JPEG_INVALID_SCAN; + return false; + } + if (*pos > len) { + fprintf(stderr, "Unexpected end of file during scan. pos=%d len=%d\n", + static_cast(*pos), static_cast(len)); + jpg->error = JPEG_UNEXPECTED_EOF; + return false; + } + return true; +} + +// Changes the quant_idx field of the components to refer to the index of the +// quant table in the jpg->quant array. +bool FixupIndexes(JPEGData* jpg) { + for (int i = 0; i < jpg->components.size(); ++i) { + JPEGComponent* c = &jpg->components[i]; + bool found_index = false; + for (int j = 0; j < jpg->quant.size(); ++j) { + if (jpg->quant[j].index == c->quant_idx) { + c->quant_idx = j; + found_index = true; + break; + } + } + if (!found_index) { + fprintf(stderr, "Quantization table with index %d not found\n", + c->quant_idx); + jpg->error = JPEG_QUANT_TABLE_NOT_FOUND; + return false; + } + } + return true; +} + +size_t FindNextMarker(const uint8_t* data, const size_t len, size_t pos) { + // kIsValidMarker[i] == 1 means (0xc0 + i) is a valid marker. + static const uint8_t kIsValidMarker[] = { + 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + }; + size_t num_skipped = 0; + while (pos + 1 < len && + (data[pos] != 0xff || data[pos + 1] < 0xc0 || + !kIsValidMarker[data[pos + 1] - 0xc0])) { + ++pos; + ++num_skipped; + } + return num_skipped; +} + +} // namespace + +bool ReadJpeg(const uint8_t* data, const size_t len, JpegReadMode mode, + JPEGData* jpg) { + size_t pos = 0; + // Check SOI marker. + EXPECT_MARKER(); + int marker = data[pos + 1]; + pos += 2; + if (marker != 0xd8) { + fprintf(stderr, "Did not find expected SOI marker, actual=%d\n", marker); + jpg->error = JPEG_SOI_NOT_FOUND; + return false; + } + int lut_size = kMaxHuffmanTables * kJpegHuffmanLutSize; + std::vector dc_huff_lut(lut_size); + std::vector ac_huff_lut(lut_size); + bool found_sof = false; + uint16_t scan_progression[kMaxComponents][kDCTBlockSize] = { { 0 } }; + + bool is_progressive = false; // default + do { + // Read next marker. + size_t num_skipped = FindNextMarker(data, len, pos); + if (num_skipped > 0) { + // Add a fake marker to indicate arbitrary in-between-markers data. + jpg->marker_order.push_back(0xff); + jpg->inter_marker_data.push_back( + std::string(reinterpret_cast(&data[pos]), + num_skipped)); + pos += num_skipped; + } + EXPECT_MARKER(); + marker = data[pos + 1]; + pos += 2; + bool ok = true; + switch (marker) { + case 0xc0: + case 0xc1: + case 0xc2: + is_progressive = (marker == 0xc2); + ok = ProcessSOF(data, len, mode, &pos, jpg); + found_sof = true; + break; + case 0xc4: + ok = ProcessDHT(data, len, mode, &dc_huff_lut, &ac_huff_lut, &pos, jpg); + break; + case 0xd0: + case 0xd1: + case 0xd2: + case 0xd3: + case 0xd4: + case 0xd5: + case 0xd6: + case 0xd7: + // RST markers do not have any data. + break; + case 0xd9: + // Found end marker. + break; + case 0xda: + if (mode == JPEG_READ_ALL) { + ok = ProcessScan(data, len, dc_huff_lut, ac_huff_lut, + scan_progression, is_progressive, &pos, jpg); + } + break; + case 0xdb: + ok = ProcessDQT(data, len, &pos, jpg); + break; + case 0xdd: + ok = ProcessDRI(data, len, &pos, jpg); + break; + case 0xe0: + case 0xe1: + case 0xe2: + case 0xe3: + case 0xe4: + case 0xe5: + case 0xe6: + case 0xe7: + case 0xe8: + case 0xe9: + case 0xea: + case 0xeb: + case 0xec: + case 0xed: + case 0xee: + case 0xef: + if (mode != JPEG_READ_TABLES) { + ok = ProcessAPP(data, len, &pos, jpg); + } + break; + case 0xfe: + if (mode != JPEG_READ_TABLES) { + ok = ProcessCOM(data, len, &pos, jpg); + } + break; + default: + fprintf(stderr, "Unsupported marker: %d pos=%d len=%d\n", + marker, static_cast(pos), static_cast(len)); + jpg->error = JPEG_UNSUPPORTED_MARKER; + ok = false; + break; + } + if (!ok) { + return false; + } + jpg->marker_order.push_back(marker); + if (mode == JPEG_READ_HEADER && found_sof) { + break; + } + } while (marker != 0xd9); + + if (!found_sof) { + fprintf(stderr, "Missing SOF marker.\n"); + jpg->error = JPEG_SOF_NOT_FOUND; + return false; + } + + // Supplemental checks. + if (mode == JPEG_READ_ALL) { + if (pos < len) { + jpg->tail_data.assign(reinterpret_cast(&data[pos]), + len - pos); + } + if (!FixupIndexes(jpg)) { + return false; + } + if (jpg->huffman_code.size() == 0) { + // Section B.2.4.2: "If a table has never been defined for a particular + // destination, then when this destination is specified in a scan header, + // the results are unpredictable." + fprintf(stderr, "Need at least one Huffman code table.\n"); + jpg->error = JPEG_HUFFMAN_TABLE_ERROR; + return false; + } + if (jpg->huffman_code.size() >= kMaxDHTMarkers) { + fprintf(stderr, "Too many Huffman tables.\n"); + jpg->error = JPEG_HUFFMAN_TABLE_ERROR; + return false; + } + } + return true; +} + +bool ReadJpeg(const std::string& data, JpegReadMode mode, + JPEGData* jpg) { + return ReadJpeg(reinterpret_cast(data.data()), + static_cast(data.size()), + mode, jpg); +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_reader.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_reader.h new file mode 100644 index 0000000..32142cf --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_reader.h @@ -0,0 +1,50 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Functions for reading a jpeg byte stream into a JPEGData object. + +#ifndef GUETZLI_JPEG_DATA_READER_H_ +#define GUETZLI_JPEG_DATA_READER_H_ + +#include +#include + +#include + +#include "guetzli/jpeg_data.h" + +namespace guetzli { + +enum JpegReadMode { + JPEG_READ_HEADER, // only basic headers + JPEG_READ_TABLES, // headers and tables (quant, Huffman, ...) + JPEG_READ_ALL, // everything +}; + +// Parses the jpeg stream contained in data[*pos ... len) and fills in *jpg with +// the parsed information. +// If mode is JPEG_READ_HEADER, it fills in only the image dimensions in *jpg. +// Returns false if the data is not valid jpeg, or if it contains an unsupported +// jpeg feature. +bool ReadJpeg(const uint8_t* data, const size_t len, JpegReadMode mode, + JPEGData* jpg); +// string variant +bool ReadJpeg(const std::string& data, JpegReadMode mode, + JPEGData* jpg); + +} // namespace guetzli + +#endif // GUETZLI_JPEG_DATA_READER_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_writer.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_writer.cc new file mode 100644 index 0000000..4f91189 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_writer.cc @@ -0,0 +1,568 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/jpeg_data_writer.h" + +#include +#include +#include + +#include "guetzli/entropy_encode.h" +#include "guetzli/fast_log.h" +#include "guetzli/jpeg_bit_writer.h" + +namespace guetzli { + +namespace { + +static const int kJpegPrecision = 8; + +// Writes len bytes from buf, using the out callback. +inline bool JPEGWrite(JPEGOutput out, const uint8_t* buf, size_t len) { + static const size_t kBlockSize = 1u << 30; + size_t pos = 0; + while (len - pos > kBlockSize) { + if (!out.Write(buf + pos, kBlockSize)) { + return false; + } + pos += kBlockSize; + } + return out.Write(buf + pos, len - pos); +} + +// Writes a string using the out callback. +inline bool JPEGWrite(JPEGOutput out, const std::string& s) { + const uint8_t* data = reinterpret_cast(&s[0]); + return JPEGWrite(out, data, s.size()); +} + +bool EncodeMetadata(const JPEGData& jpg, bool strip_metadata, JPEGOutput out) { + if (strip_metadata) { + const uint8_t kApp0Data[] = { + 0xff, 0xe0, 0x00, 0x10, // APP0 + 0x4a, 0x46, 0x49, 0x46, 0x00, // 'JFIF' + 0x01, 0x01, // v1.01 + 0x00, 0x00, 0x01, 0x00, 0x01, // aspect ratio = 1:1 + 0x00, 0x00 // thumbnail width/height + }; + return JPEGWrite(out, kApp0Data, sizeof(kApp0Data)); + } + bool ok = true; + for (int i = 0; i < jpg.app_data.size(); ++i) { + uint8_t data[1] = { 0xff }; + ok = ok && JPEGWrite(out, data, sizeof(data)); + ok = ok && JPEGWrite(out, jpg.app_data[i]); + } + for (int i = 0; i < jpg.com_data.size(); ++i) { + uint8_t data[2] = { 0xff, 0xfe }; + ok = ok && JPEGWrite(out, data, sizeof(data)); + ok = ok && JPEGWrite(out, jpg.com_data[i]); + } + return ok; +} + +bool EncodeDQT(const std::vector& quant, JPEGOutput out) { + int marker_len = 2; + for (int i = 0; i < quant.size(); ++i) { + marker_len += 1 + (quant[i].precision ? 2 : 1) * kDCTBlockSize; + } + std::vector data(marker_len + 2); + size_t pos = 0; + data[pos++] = 0xff; + data[pos++] = 0xdb; + data[pos++] = marker_len >> 8; + data[pos++] = marker_len & 0xff; + for (int i = 0; i < quant.size(); ++i) { + const JPEGQuantTable& table = quant[i]; + data[pos++] = (table.precision << 4) + table.index; + for (int k = 0; k < kDCTBlockSize; ++k) { + int val = table.values[kJPEGNaturalOrder[k]]; + if (table.precision) { + data[pos++] = val >> 8; + } + data[pos++] = val & 0xff; + } + } + return JPEGWrite(out, &data[0], pos); +} + +bool EncodeSOF(const JPEGData& jpg, JPEGOutput out) { + const size_t ncomps = jpg.components.size(); + const size_t marker_len = 8 + 3 * ncomps; + std::vector data(marker_len + 2); + size_t pos = 0; + data[pos++] = 0xff; + data[pos++] = 0xc1; + data[pos++] = marker_len >> 8; + data[pos++] = marker_len & 0xff; + data[pos++] = kJpegPrecision; + data[pos++] = jpg.height >> 8; + data[pos++] = jpg.height & 0xff; + data[pos++] = jpg.width >> 8; + data[pos++] = jpg.width & 0xff; + data[pos++] = ncomps; + for (size_t i = 0; i < ncomps; ++i) { + data[pos++] = jpg.components[i].id; + data[pos++] = ((jpg.components[i].h_samp_factor << 4) | + (jpg.components[i].v_samp_factor)); + const int quant_idx = jpg.components[i].quant_idx; + if (quant_idx >= jpg.quant.size()) { + return false; + } + data[pos++] = jpg.quant[quant_idx].index; + } + return JPEGWrite(out, &data[0], pos); +} + +// Builds a JPEG-style huffman code from the given bit depths. +void BuildHuffmanCode(uint8_t* depth, int* counts, int* values) { + for (int i = 0; i < JpegHistogram::kSize; ++i) { + if (depth[i] > 0) { + ++counts[depth[i]]; + } + } + int offset[kJpegHuffmanMaxBitLength + 1] = { 0 }; + for (int i = 1; i <= kJpegHuffmanMaxBitLength; ++i) { + offset[i] = offset[i - 1] + counts[i - 1]; + } + for (int i = 0; i < JpegHistogram::kSize; ++i) { + if (depth[i] > 0) { + values[offset[depth[i]]++] = i; + } + } +} + +void BuildHuffmanCodeTable(const int* counts, const int* values, + HuffmanCodeTable* table) { + int huffcode[256]; + int huffsize[256]; + int p = 0; + for (int l = 1; l <= kJpegHuffmanMaxBitLength; ++l) { + int i = counts[l]; + while (i--) huffsize[p++] = l; + } + + if (p == 0) + return; + + huffsize[p - 1] = 0; + int lastp = p - 1; + + int code = 0; + int si = huffsize[0]; + p = 0; + while (huffsize[p]) { + while ((huffsize[p]) == si) { + huffcode[p++] = code; + code++; + } + code <<= 1; + si++; + } + for (p = 0; p < lastp; p++) { + int i = values[p]; + table->depth[i] = huffsize[p]; + table->code[i] = huffcode[p]; + } +} + +} // namespace + +// Updates ac_histogram with the counts of the AC symbols that will be added by +// a sequential jpeg encoder for this block. Every symbol is counted twice so +// that we can add a fake symbol at the end with count 1 to be the last (least +// frequent) symbol with the all 1 code. +void UpdateACHistogramForDCTBlock(const coeff_t* coeffs, + JpegHistogram* ac_histogram) { + int r = 0; + for (int k = 1; k < 64; ++k) { + coeff_t coeff = coeffs[kJPEGNaturalOrder[k]]; + if (coeff == 0) { + r++; + continue; + } + while (r > 15) { + ac_histogram->Add(0xf0); + r -= 16; + } + int nbits = Log2FloorNonZero(std::abs(coeff)) + 1; + int symbol = (r << 4) + nbits; + ac_histogram->Add(symbol); + r = 0; + } + if (r > 0) { + ac_histogram->Add(0); + } +} + +size_t HistogramHeaderCost(const JpegHistogram& histo) { + size_t header_bits = 17 * 8; + for (int i = 0; i + 1 < JpegHistogram::kSize; ++i) { + if (histo.counts[i] > 0) { + header_bits += 8; + } + } + return header_bits; +} + +size_t HistogramEntropyCost(const JpegHistogram& histo, + const uint8_t depths[256]) { + size_t bits = 0; + for (int i = 0; i + 1 < JpegHistogram::kSize; ++i) { + // JpegHistogram::Add() counts every symbol twice, so we have to divide by + // two here. + bits += (histo.counts[i] / 2) * (depths[i] + (i & 0xf)); + } + // Estimate escape byte rate to be 0.75/256. + bits += (bits * 3 + 512) >> 10; + return bits; +} + +void BuildDCHistograms(const JPEGData& jpg, JpegHistogram* histo) { + for (int i = 0; i < jpg.components.size(); ++i) { + const JPEGComponent& c = jpg.components[i]; + JpegHistogram* dc_histogram = &histo[i]; + coeff_t last_dc_coeff = 0; + for (int mcu_y = 0; mcu_y < jpg.MCU_rows; ++mcu_y) { + for (int mcu_x = 0; mcu_x < jpg.MCU_cols; ++mcu_x) { + for (int iy = 0; iy < c.v_samp_factor; ++iy) { + for (int ix = 0; ix < c.h_samp_factor; ++ix) { + int block_y = mcu_y * c.v_samp_factor + iy; + int block_x = mcu_x * c.h_samp_factor + ix; + int block_idx = block_y * c.width_in_blocks + block_x; + coeff_t dc_coeff = c.coeffs[block_idx << 6]; + int diff = std::abs(dc_coeff - last_dc_coeff); + int nbits = Log2Floor(diff) + 1; + dc_histogram->Add(nbits); + last_dc_coeff = dc_coeff; + } + } + } + } + } +} + +void BuildACHistograms(const JPEGData& jpg, JpegHistogram* histo) { + for (int i = 0; i < jpg.components.size(); ++i) { + const JPEGComponent& c = jpg.components[i]; + JpegHistogram* ac_histogram = &histo[i]; + for (int j = 0; j < c.coeffs.size(); j += kDCTBlockSize) { + UpdateACHistogramForDCTBlock(&c.coeffs[j], ac_histogram); + } + } +} + +// Size of everything except the Huffman codes and the entropy coded data. +size_t JpegHeaderSize(const JPEGData& jpg, bool strip_metadata) { + size_t num_bytes = 0; + num_bytes += 2; // SOI + if (strip_metadata) { + num_bytes += 18; // APP0 + } else { + for (int i = 0; i < jpg.app_data.size(); ++i) { + num_bytes += 1 + jpg.app_data[i].size(); + } + for (int i = 0; i < jpg.com_data.size(); ++i) { + num_bytes += 2 + jpg.com_data[i].size(); + } + } + // DQT + num_bytes += 4; + for (int i = 0; i < jpg.quant.size(); ++i) { + num_bytes += 1 + (jpg.quant[i].precision ? 2 : 1) * kDCTBlockSize; + } + num_bytes += 10 + 3 * jpg.components.size(); // SOF + num_bytes += 4; // DHT (w/o actual Huffman code data) + num_bytes += 8 + 2 * jpg.components.size(); // SOS + num_bytes += 2; // EOI + num_bytes += jpg.tail_data.size(); + return num_bytes; +} + +size_t ClusterHistograms(JpegHistogram* histo, size_t* num, + int* histo_indexes, uint8_t* depth) { + memset(depth, 0, *num * JpegHistogram::kSize); + size_t costs[kMaxComponents]; + for (size_t i = 0; i < *num; ++i) { + histo_indexes[i] = i; + std::vector tree(2 * JpegHistogram::kSize + 1); + CreateHuffmanTree(histo[i].counts, JpegHistogram::kSize, + kJpegHuffmanMaxBitLength, &tree[0], + &depth[i * JpegHistogram::kSize]); + costs[i] = (HistogramHeaderCost(histo[i]) + + HistogramEntropyCost(histo[i], + &depth[i * JpegHistogram::kSize])); + } + const size_t orig_num = *num; + while (*num > 1) { + size_t last = *num - 1; + size_t second_last = *num - 2; + JpegHistogram combined(histo[last]); + combined.AddHistogram(histo[second_last]); + std::vector tree(2 * JpegHistogram::kSize + 1); + uint8_t depth_combined[JpegHistogram::kSize] = { 0 }; + CreateHuffmanTree(combined.counts, JpegHistogram::kSize, + kJpegHuffmanMaxBitLength, &tree[0], depth_combined); + size_t cost_combined = (HistogramHeaderCost(combined) + + HistogramEntropyCost(combined, depth_combined)); + if (cost_combined < costs[last] + costs[second_last]) { + histo[second_last] = combined; + histo[last] = JpegHistogram(); + costs[second_last] = cost_combined; + memcpy(&depth[second_last * JpegHistogram::kSize], depth_combined, + sizeof(depth_combined)); + for (size_t i = 0; i < orig_num; ++i) { + if (histo_indexes[i] == last) { + histo_indexes[i] = second_last; + } + } + --(*num); + } else { + break; + } + } + size_t total_cost = 0; + for (int i = 0; i < *num; ++i) { + total_cost += costs[i]; + } + return (total_cost + 7) / 8; +} + +size_t EstimateJpegDataSize(const int num_components, + const std::vector& histograms) { + assert(histograms.size() == 2 * num_components); + std::vector clustered = histograms; + size_t num_dc = num_components; + size_t num_ac = num_components; + int indexes[kMaxComponents]; + uint8_t depth[kMaxComponents * JpegHistogram::kSize]; + return (ClusterHistograms(&clustered[0], &num_dc, indexes, depth) + + ClusterHistograms(&clustered[num_components], &num_ac, indexes, + depth)); +} + +namespace { + +// Writes DHT and SOS marker segments to out and fills in DC/AC Huffman tables +// for each component of the image. +bool BuildAndEncodeHuffmanCodes(const JPEGData& jpg, JPEGOutput out, + std::vector* dc_huff_tables, + std::vector* ac_huff_tables) { + const int ncomps = jpg.components.size(); + dc_huff_tables->resize(ncomps); + ac_huff_tables->resize(ncomps); + + // Build separate DC histograms for each component. + std::vector histograms(ncomps); + BuildDCHistograms(jpg, &histograms[0]); + + // Cluster DC histograms. + size_t num_dc_histo = ncomps; + int dc_histo_indexes[kMaxComponents]; + std::vector depths(ncomps * JpegHistogram::kSize); + ClusterHistograms(&histograms[0], &num_dc_histo, dc_histo_indexes, + &depths[0]); + + // Build separate AC histograms for each component. + histograms.resize(num_dc_histo + ncomps); + depths.resize((num_dc_histo + ncomps) * JpegHistogram::kSize); + BuildACHistograms(jpg, &histograms[num_dc_histo]); + + // Cluster AC histograms. + size_t num_ac_histo = ncomps; + int ac_histo_indexes[kMaxComponents]; + ClusterHistograms(&histograms[num_dc_histo], &num_ac_histo, ac_histo_indexes, + &depths[num_dc_histo * JpegHistogram::kSize]); + + // Compute DHT and SOS marker data sizes and start emitting DHT marker. + int num_histo = num_dc_histo + num_ac_histo; + histograms.resize(num_histo); + int total_count = 0; + for (int i = 0; i < histograms.size(); ++i) { + total_count += histograms[i].NumSymbols(); + } + const size_t dht_marker_len = + 2 + num_histo * (kJpegHuffmanMaxBitLength + 1) + total_count; + const size_t sos_marker_len = 6 + 2 * ncomps; + std::vector data(dht_marker_len + sos_marker_len + 4); + size_t pos = 0; + data[pos++] = 0xff; + data[pos++] = 0xc4; + data[pos++] = dht_marker_len >> 8; + data[pos++] = dht_marker_len & 0xff; + + // Compute Huffman codes for each histograms. + for (size_t i = 0; i < num_histo; ++i) { + const bool is_dc = i < num_dc_histo; + const int idx = is_dc ? i : i - num_dc_histo; + int counts[kJpegHuffmanMaxBitLength + 1] = { 0 }; + int values[JpegHistogram::kSize] = { 0 }; + BuildHuffmanCode(&depths[i * JpegHistogram::kSize], counts, values); + HuffmanCodeTable table; + for (int j = 0; j < 256; ++j) table.depth[j] = 255; + BuildHuffmanCodeTable(counts, values, &table); + for (int c = 0; c < ncomps; ++c) { + if (is_dc) { + if (dc_histo_indexes[c] == idx) (*dc_huff_tables)[c] = table; + } else { + if (ac_histo_indexes[c] == idx) (*ac_huff_tables)[c] = table; + } + } + int max_length = kJpegHuffmanMaxBitLength; + while (max_length > 0 && counts[max_length] == 0) --max_length; + --counts[max_length]; + int total_count = 0; + for (int j = 0; j <= max_length; ++j) total_count += counts[j]; + data[pos++] = is_dc ? i : i - num_dc_histo + 0x10; + for (size_t j = 1; j <= kJpegHuffmanMaxBitLength; ++j) { + data[pos++] = counts[j]; + } + for (size_t j = 0; j < total_count; ++j) { + data[pos++] = values[j]; + } + } + + // Emit SOS marker data. + data[pos++] = 0xff; + data[pos++] = 0xda; + data[pos++] = sos_marker_len >> 8; + data[pos++] = sos_marker_len & 0xff; + data[pos++] = ncomps; + for (int i = 0; i < ncomps; ++i) { + data[pos++] = jpg.components[i].id; + data[pos++] = (dc_histo_indexes[i] << 4) | ac_histo_indexes[i]; + } + data[pos++] = 0; + data[pos++] = 63; + data[pos++] = 0; + assert(pos == data.size()); + return JPEGWrite(out, &data[0], data.size()); +} + +void EncodeDCTBlockSequential(const coeff_t* coeffs, + const HuffmanCodeTable& dc_huff, + const HuffmanCodeTable& ac_huff, + coeff_t* last_dc_coeff, + BitWriter* bw) { + coeff_t temp2; + coeff_t temp; + temp2 = coeffs[0]; + temp = temp2 - *last_dc_coeff; + *last_dc_coeff = temp2; + temp2 = temp; + if (temp < 0) { + temp = -temp; + temp2--; + } + int nbits = Log2Floor(temp) + 1; + bw->WriteBits(dc_huff.depth[nbits], dc_huff.code[nbits]); + if (nbits > 0) { + bw->WriteBits(nbits, temp2 & ((1 << nbits) - 1)); + } + int r = 0; + for (int k = 1; k < 64; ++k) { + if ((temp = coeffs[kJPEGNaturalOrder[k]]) == 0) { + r++; + continue; + } + if (temp < 0) { + temp = -temp; + temp2 = ~temp; + } else { + temp2 = temp; + } + while (r > 15) { + bw->WriteBits(ac_huff.depth[0xf0], ac_huff.code[0xf0]); + r -= 16; + } + int nbits = Log2FloorNonZero(temp) + 1; + int symbol = (r << 4) + nbits; + bw->WriteBits(ac_huff.depth[symbol], ac_huff.code[symbol]); + bw->WriteBits(nbits, temp2 & ((1 << nbits) - 1)); + r = 0; + } + if (r > 0) { + bw->WriteBits(ac_huff.depth[0], ac_huff.code[0]); + } +} + +bool EncodeScan(const JPEGData& jpg, + const std::vector& dc_huff_table, + const std::vector& ac_huff_table, + JPEGOutput out) { + coeff_t last_dc_coeff[kMaxComponents] = { 0 }; + BitWriter bw(1 << 17); + for (int mcu_y = 0; mcu_y < jpg.MCU_rows; ++mcu_y) { + for (int mcu_x = 0; mcu_x < jpg.MCU_cols; ++mcu_x) { + // Encode one MCU + for (int i = 0; i < jpg.components.size(); ++i) { + const JPEGComponent& c = jpg.components[i]; + int nblocks_y = c.v_samp_factor; + int nblocks_x = c.h_samp_factor; + for (int iy = 0; iy < nblocks_y; ++iy) { + for (int ix = 0; ix < nblocks_x; ++ix) { + int block_y = mcu_y * nblocks_y + iy; + int block_x = mcu_x * nblocks_x + ix; + int block_idx = block_y * c.width_in_blocks + block_x; + const coeff_t* coeffs = &c.coeffs[block_idx << 6]; + EncodeDCTBlockSequential(coeffs, dc_huff_table[i], ac_huff_table[i], + &last_dc_coeff[i], &bw); + } + } + } + if (bw.pos > (1 << 16)) { + if (!JPEGWrite(out, bw.data.get(), bw.pos)) { + return false; + } + bw.pos = 0; + } + } + } + bw.JumpToByteBoundary(); + return !bw.overflow && JPEGWrite(out, bw.data.get(), bw.pos); +} + +} // namespace + +bool WriteJpeg(const JPEGData& jpg, bool strip_metadata, JPEGOutput out) { + static const uint8_t kSOIMarker[2] = { 0xff, 0xd8 }; + static const uint8_t kEOIMarker[2] = { 0xff, 0xd9 }; + std::vector dc_codes; + std::vector ac_codes; + return (JPEGWrite(out, kSOIMarker, sizeof(kSOIMarker)) && + EncodeMetadata(jpg, strip_metadata, out) && + EncodeDQT(jpg.quant, out) && + EncodeSOF(jpg, out) && + BuildAndEncodeHuffmanCodes(jpg, out, &dc_codes, &ac_codes) && + EncodeScan(jpg, dc_codes, ac_codes, out) && + JPEGWrite(out, kEOIMarker, sizeof(kEOIMarker)) && + (strip_metadata || JPEGWrite(out, jpg.tail_data))); +} + +int NullOut(void* data, const uint8_t* buf, size_t count) { + return count; +} + +void BuildSequentialHuffmanCodes( + const JPEGData& jpg, + std::vector* dc_huffman_code_tables, + std::vector* ac_huffman_code_tables) { + JPEGOutput out(NullOut, nullptr); + BuildAndEncodeHuffmanCodes(jpg, out, dc_huffman_code_tables, + ac_huffman_code_tables); +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_writer.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_writer.h new file mode 100644 index 0000000..941118b --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_data_writer.h @@ -0,0 +1,105 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Functions for writing a JPEGData object into a jpeg byte stream. + +#ifndef GUETZLI_JPEG_DATA_WRITER_H_ +#define GUETZLI_JPEG_DATA_WRITER_H_ + +#include +#include +#include + +#include "guetzli/jpeg_data.h" + +namespace guetzli { + +// Function pointer type used to write len bytes into buf. Returns the +// number of bytes written or -1 on error. +typedef int (*JPEGOutputHook)(void* data, const uint8_t* buf, size_t len); + +// Output callback function with associated data. +struct JPEGOutput { + JPEGOutput(JPEGOutputHook cb, void* data) : cb(cb), data(data) {} + bool Write(const uint8_t* buf, size_t len) const { + return (len == 0) || (cb(data, buf, len) == len); + } + private: + JPEGOutputHook cb; + void* data; +}; + +bool WriteJpeg(const JPEGData& jpg, bool strip_metadata, JPEGOutput out); + +struct HuffmanCodeTable { + uint8_t depth[256]; + int code[256]; +}; + +void BuildSequentialHuffmanCodes( + const JPEGData& jpg, std::vector* dc_huffman_code_tables, + std::vector* ac_huffman_code_tables); + +struct JpegHistogram { + static const int kSize = kJpegHuffmanAlphabetSize + 1; + + JpegHistogram() { Clear(); } + void Clear() { + memset(counts, 0, sizeof(counts)); + counts[kSize - 1] = 1; + } + void Add(int symbol) { + counts[symbol] += 2; + } + void Add(int symbol, int weight) { + counts[symbol] += 2 * weight; + } + void AddHistogram(const JpegHistogram& other) { + for (int i = 0; i + 1 < kSize; ++i) { + counts[i] += other.counts[i]; + } + counts[kSize - 1] = 1; + } + int NumSymbols() const { + int n = 0; + for (int i = 0; i + 1 < kSize; ++i) { + n += (counts[i] > 0 ? 1 : 0); + } + return n; + } + + uint32_t counts[kSize]; +}; + +void BuildDCHistograms(const JPEGData& jpg, JpegHistogram* histo); +void BuildACHistograms(const JPEGData& jpg, JpegHistogram* histo); +size_t JpegHeaderSize(const JPEGData& jpg, bool strip_metadata); +size_t EstimateJpegDataSize(const int num_components, + const std::vector& histograms); + +size_t HistogramEntropyCost(const JpegHistogram& histo, + const uint8_t depths[256]); +size_t HistogramHeaderCost(const JpegHistogram& histo); + +void UpdateACHistogramForDCTBlock(const coeff_t* coeffs, + JpegHistogram* ac_histogram); + +size_t ClusterHistograms(JpegHistogram* histo, size_t* num, int* histo_indexes, + uint8_t* depths); + +} // namespace guetzli + +#endif // GUETZLI_JPEG_DATA_WRITER_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_error.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_error.h new file mode 100644 index 0000000..94cb9be --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_error.h @@ -0,0 +1,71 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Definition of error codes for parsing jpeg files. + +#ifndef GUETZLI_JPEG_ERROR_H_ +#define GUETZLI_JPEG_ERROR_H_ + +namespace guetzli { + +enum JPEGReadError { + JPEG_OK = 0, + JPEG_SOI_NOT_FOUND, + JPEG_SOF_NOT_FOUND, + JPEG_UNEXPECTED_EOF, + JPEG_MARKER_BYTE_NOT_FOUND, + JPEG_UNSUPPORTED_MARKER, + JPEG_WRONG_MARKER_SIZE, + JPEG_INVALID_PRECISION, + JPEG_INVALID_WIDTH, + JPEG_INVALID_HEIGHT, + JPEG_INVALID_NUMCOMP, + JPEG_INVALID_SAMP_FACTOR, + JPEG_INVALID_START_OF_SCAN, + JPEG_INVALID_END_OF_SCAN, + JPEG_INVALID_SCAN_BIT_POSITION, + JPEG_INVALID_COMPS_IN_SCAN, + JPEG_INVALID_HUFFMAN_INDEX, + JPEG_INVALID_QUANT_TBL_INDEX, + JPEG_INVALID_QUANT_VAL, + JPEG_INVALID_MARKER_LEN, + JPEG_INVALID_SAMPLING_FACTORS, + JPEG_INVALID_HUFFMAN_CODE, + JPEG_INVALID_SYMBOL, + JPEG_NON_REPRESENTABLE_DC_COEFF, + JPEG_NON_REPRESENTABLE_AC_COEFF, + JPEG_INVALID_SCAN, + JPEG_OVERLAPPING_SCANS, + JPEG_INVALID_SCAN_ORDER, + JPEG_EXTRA_ZERO_RUN, + JPEG_DUPLICATE_DRI, + JPEG_DUPLICATE_SOF, + JPEG_WRONG_RESTART_MARKER, + JPEG_DUPLICATE_COMPONENT_ID, + JPEG_COMPONENT_NOT_FOUND, + JPEG_HUFFMAN_TABLE_NOT_FOUND, + JPEG_HUFFMAN_TABLE_ERROR, + JPEG_QUANT_TABLE_NOT_FOUND, + JPEG_EMPTY_DHT, + JPEG_EMPTY_DQT, + JPEG_OUT_OF_BAND_COEFF, + JPEG_EOB_RUN_TOO_LONG, + JPEG_IMAGE_TOO_LARGE, +}; + +} // namespace guetzli + +#endif // GUETZLI_JPEG_ERROR_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_huffman_decode.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_huffman_decode.cc new file mode 100644 index 0000000..e5378a6 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_huffman_decode.cc @@ -0,0 +1,122 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/jpeg_huffman_decode.h" + +#include +#include +#include +#include + +#include "guetzli/jpeg_data.h" + +namespace guetzli { + +// Returns the table width of the next 2nd level table, count is the histogram +// of bit lengths for the remaining symbols, len is the code length of the next +// processed symbol. +static inline int NextTableBitSize(const int* count, int len) { + int left = 1 << (len - kJpegHuffmanRootTableBits); + while (len < kJpegHuffmanMaxBitLength) { + left -= count[len]; + if (left <= 0) break; + ++len; + left <<= 1; + } + return len - kJpegHuffmanRootTableBits; +} + +int BuildJpegHuffmanTable(const int* count_in, const int* symbols, + HuffmanTableEntry* lut) { + HuffmanTableEntry code; // current table entry + HuffmanTableEntry* table; // next available space in table + int len; // current code length + int idx; // symbol index + int key; // prefix code + int reps; // number of replicate key values in current table + int low; // low bits for current root entry + int table_bits; // key length of current table + int table_size; // size of current table + int total_size; // sum of root table size and 2nd level table sizes + + // Make a local copy of the input bit length histogram. + int count[kJpegHuffmanMaxBitLength + 1] = { 0 }; + int total_count = 0; + for (len = 1; len <= kJpegHuffmanMaxBitLength; ++len) { + count[len] = count_in[len]; + total_count += count[len]; + } + + table = lut; + table_bits = kJpegHuffmanRootTableBits; + table_size = 1 << table_bits; + total_size = table_size; + + // Special case code with only one value. + if (total_count == 1) { + code.bits = 0; + code.value = symbols[0]; + for (key = 0; key < total_size; ++key) { + table[key] = code; + } + return total_size; + } + + // Fill in root table. + key = 0; + idx = 0; + for (len = 1; len <= kJpegHuffmanRootTableBits; ++len) { + for (; count[len] > 0; --count[len]) { + code.bits = len; + code.value = symbols[idx++]; + reps = 1 << (kJpegHuffmanRootTableBits - len); + while (reps--) { + table[key++] = code; + } + } + } + + // Fill in 2nd level tables and add pointers to root table. + table += table_size; + table_size = 0; + low = 0; + for (len = kJpegHuffmanRootTableBits + 1; + len <= kJpegHuffmanMaxBitLength; ++len) { + for (; count[len] > 0; --count[len]) { + // Start a new sub-table if the previous one is full. + if (low >= table_size) { + table += table_size; + table_bits = NextTableBitSize(count, len); + table_size = 1 << table_bits; + total_size += table_size; + low = 0; + lut[key].bits = table_bits + kJpegHuffmanRootTableBits; + lut[key].value = (table - lut) - key; + ++key; + } + code.bits = len - kJpegHuffmanRootTableBits; + code.value = symbols[idx++]; + reps = 1 << (table_bits - code.bits); + while (reps--) { + table[low++] = code; + } + } + } + + return total_size; +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_huffman_decode.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_huffman_decode.h new file mode 100644 index 0000000..0deefa8 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/jpeg_huffman_decode.h @@ -0,0 +1,51 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Utility function for building a Huffman lookup table for the jpeg decoder. + +#ifndef GUETZLI_JPEG_HUFFMAN_DECODE_H_ +#define GUETZLI_JPEG_HUFFMAN_DECODE_H_ + +#include + +namespace guetzli { + +static const int kJpegHuffmanRootTableBits = 8; +// Maximum huffman lookup table size. +// According to zlib/examples/enough.c, 758 entries are always enough for +// an alphabet of 257 symbols (256 + 1 special symbol for the all 1s code) and +// max bit length 16 if the root table has 8 bits. +static const int kJpegHuffmanLutSize = 758; + +struct HuffmanTableEntry { + // Initialize the value to an invalid symbol so that we can recognize it + // when reading the bit stream using a Huffman code with space > 0. + HuffmanTableEntry() : bits(0), value(0xffff) {} + + uint8_t bits; // number of bits used for this symbol + uint16_t value; // symbol value or table offset +}; + +// Builds jpeg-style Huffman lookup table from the given symbols. +// The symbols are in order of increasing bit lengths. The number of symbols +// with bit length n is given in counts[n] for each n >= 1. +// Returns the size of the lookup table. +int BuildJpegHuffmanTable(const int* counts, const int* symbols, + HuffmanTableEntry* lut); + +} // namespace guetzli + +#endif // GUETZLI_JPEG_HUFFMAN_DECODE_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/order.inc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/order.inc new file mode 100644 index 0000000..f65fdfc --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/order.inc @@ -0,0 +1,391 @@ +// Automatically generated by guetzli/order:update_c_code + +static const float csf[192] = { + 0.0, + 1.71014, + 0.298711, + 0.233709, + 0.223126, + 0.207072, + 0.192775, + 0.161201, + 2.05807, + 0.222927, + 0.203406, + 0.188465, + 0.184668, + 0.169993, + 0.159142, + 0.130155, + 0.430518, + 0.204939, + 0.206655, + 0.192231, + 0.182941, + 0.169455, + 0.157599, + 0.127153, + 0.234757, + 0.191098, + 0.192698, + 0.17425, + 0.166503, + 0.142154, + 0.126182, + 0.104196, + 0.226117, + 0.185373, + 0.183825, + 0.166643, + 0.159414, + 0.12636, + 0.108696, + 0.0911974, + 0.207463, + 0.171517, + 0.170124, + 0.141582, + 0.126213, + 0.103627, + 0.0882436, + 0.0751848, + 0.196436, + 0.161947, + 0.159271, + 0.126938, + 0.109125, + 0.0878027, + 0.0749842, + 0.0633859, + 0.165232, + 0.132905, + 0.128679, + 0.105766, + 0.0906087, + 0.0751544, + 0.0641187, + 0.0529921, + 0.0, + 0.147235, + 0.11264, + 0.0757892, + 0.0493929, + 0.0280663, + 0.0075012, + -0.000945567, + 0.149251, + 0.0964806, + 0.0786224, + 0.05206, + 0.0292758, + 0.00353094, + -0.00277912, + -0.00404481, + 0.115551, + 0.0793142, + 0.0623735, + 0.0405019, + 0.0152656, + -0.00145742, + -0.00370369, + -0.00375106, + 0.0791547, + 0.0537506, + 0.0413634, + 0.0193486, + 0.000609066, + -0.00510923, + -0.0046452, + -0.00385187, + 0.0544534, + 0.0334066, + 0.0153899, + 0.000539088, + -0.00356085, + -0.00535661, + -0.00429145, + -0.00343131, + 0.0356439, + 0.00865645, + 0.00165229, + -0.00425931, + -0.00507324, + -0.00459083, + -0.003703, + -0.00310327, + 0.0121926, + -0.0009259, + -0.00330991, + -0.00499378, + -0.00437381, + -0.00377427, + -0.00311731, + -0.00255125, + -0.000320593, + -0.00426043, + -0.00416549, + -0.00419364, + -0.00365418, + -0.00317499, + -0.00255932, + -0.00217917, + 0.0, + 0.143471, + 0.124336, + 0.0947465, + 0.0814066, + 0.0686776, + 0.0588122, + 0.0374415, + 0.146315, + 0.105334, + 0.0949415, + 0.0784241, + 0.0689064, + 0.0588304, + 0.0495961, + 0.0202342, + 0.123818, + 0.0952654, + 0.0860556, + 0.0724158, + 0.0628307, + 0.0529965, + 0.0353941, + 0.00815821, + 0.097054, + 0.080422, + 0.0731085, + 0.0636154, + 0.055606, + 0.0384127, + 0.0142879, + 0.00105195, + 0.0849312, + 0.071115, + 0.0631183, + 0.0552972, + 0.0369221, + 0.00798314, + 0.000716374, + -0.00200948, + 0.0722298, + 0.0599559, + 0.054841, + 0.0387529, + 0.0107262, + 0.000355315, + -0.00244803, + -0.00335222, + 0.0635335, + 0.0514196, + 0.0406309, + 0.0125833, + 0.00151305, + -0.00140269, + -0.00362547, + -0.00337649, + 0.0472024, + 0.0198725, + 0.0113437, + 0.00266305, + -0.00137183, + -0.00354158, + -0.00341292, + -0.00290074 +}; + +static const float bias[192] = { + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 +}; diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/output_image.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/output_image.cc new file mode 100644 index 0000000..fc2546b --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/output_image.cc @@ -0,0 +1,451 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/output_image.h" + +#include +#include +#include +#include +#include +#include + +#include "guetzli/idct.h" +#include "guetzli/color_transform.h" +#include "guetzli/dct_double.h" +#include "guetzli/gamma_correct.h" +#include "guetzli/preprocess_downsample.h" +#include "guetzli/quantize.h" + +namespace guetzli { + +OutputImageComponent::OutputImageComponent(int w, int h) + : width_(w), height_(h) { + Reset(1, 1); +} + +void OutputImageComponent::Reset(int factor_x, int factor_y) { + factor_x_ = factor_x; + factor_y_ = factor_y; + width_in_blocks_ = (width_ + 8 * factor_x_ - 1) / (8 * factor_x_); + height_in_blocks_ = (height_ + 8 * factor_y_ - 1) / (8 * factor_y_); + num_blocks_ = width_in_blocks_ * height_in_blocks_; + coeffs_ = std::vector(num_blocks_ * kDCTBlockSize); + pixels_ = std::vector(width_ * height_, 128 << 4); + for (int i = 0; i < kDCTBlockSize; ++i) quant_[i] = 1; +} + +bool OutputImageComponent::IsAllZero() const { + int numcoeffs = num_blocks_ * kDCTBlockSize; + for (int i = 0; i < numcoeffs; ++i) { + if (coeffs_[i] != 0) return false; + } + return true; +} + +void OutputImageComponent::GetCoeffBlock(int block_x, int block_y, + coeff_t block[kDCTBlockSize]) const { + assert(block_x < width_in_blocks_); + assert(block_y < height_in_blocks_); + int offset = (block_y * width_in_blocks_ + block_x) * kDCTBlockSize; + memcpy(block, &coeffs_[offset], kDCTBlockSize * sizeof(coeffs_[0])); +} + +void OutputImageComponent::ToPixels(int xmin, int ymin, int xsize, int ysize, + uint8_t* out, int stride) const { + assert(xmin >= 0); + assert(ymin >= 0); + assert(xmin < width_); + assert(ymin < height_); + const int yend1 = ymin + ysize; + const int yend0 = std::min(yend1, height_); + int y = ymin; + for (; y < yend0; ++y) { + const int xend1 = xmin + xsize; + const int xend0 = std::min(xend1, width_); + int x = xmin; + int px = y * width_ + xmin; + for (; x < xend0; ++x, ++px, out += stride) { + *out = static_cast((pixels_[px] + 8 - (x & 1)) >> 4); + } + const int offset = -stride; + for (; x < xend1; ++x) { + *out = out[offset]; + out += stride; + } + } + for (; y < yend1; ++y) { + const int offset = -stride * xsize; + for (int x = 0; x < xsize; ++x) { + *out = out[offset]; + out += stride; + } + } +} + +void OutputImageComponent::ToFloatPixels(float* out, int stride) const { + assert(factor_x_ == 1); + assert(factor_y_ == 1); + for (int block_y = 0; block_y < height_in_blocks_; ++block_y) { + for (int block_x = 0; block_x < width_in_blocks_; ++block_x) { + coeff_t block[kDCTBlockSize]; + GetCoeffBlock(block_x, block_y, block); + double blockd[kDCTBlockSize]; + for (int k = 0; k < kDCTBlockSize; ++k) { + blockd[k] = block[k]; + } + ComputeBlockIDCTDouble(blockd); + for (int iy = 0; iy < 8; ++iy) { + for (int ix = 0; ix < 8; ++ix) { + int y = block_y * 8 + iy; + int x = block_x * 8 + ix; + if (y >= height_ || x >= width_) continue; + out[(y * width_ + x) * stride] = blockd[8 * iy + ix] + 128.0; + } + } + } + } +} + +void OutputImageComponent::SetCoeffBlock(int block_x, int block_y, + const coeff_t block[kDCTBlockSize]) { + assert(block_x < width_in_blocks_); + assert(block_y < height_in_blocks_); + int offset = (block_y * width_in_blocks_ + block_x) * kDCTBlockSize; + memcpy(&coeffs_[offset], block, kDCTBlockSize * sizeof(coeffs_[0])); + uint8_t idct[kDCTBlockSize]; + ComputeBlockIDCT(&coeffs_[offset], idct); + UpdatePixelsForBlock(block_x, block_y, idct); +} + +void OutputImageComponent::UpdatePixelsForBlock( + int block_x, int block_y, const uint8_t idct[kDCTBlockSize]) { + if (factor_x_ == 1 && factor_y_ == 1) { + for (int iy = 0; iy < 8; ++iy) { + for (int ix = 0; ix < 8; ++ix) { + int x = 8 * block_x + ix; + int y = 8 * block_y + iy; + if (x >= width_ || y >= height_) continue; + int p = y * width_ + x; + pixels_[p] = idct[8 * iy + ix] << 4; + } + } + } else if (factor_x_ == 2 && factor_y_ == 2) { + // Fill in the 10x10 pixel area in the subsampled image that will be the + // basis of the upsampling. This area is enough to hold the 3x3 kernel of + // the fancy upsampler around each pixel. + static const int kSubsampledEdgeSize = 10; + uint16_t subsampled[kSubsampledEdgeSize * kSubsampledEdgeSize]; + for (int j = 0; j < kSubsampledEdgeSize; ++j) { + // The order we fill in the rows is: + // 8 rows intersecting the block, row below, row above + const int y0 = block_y * 16 + (j < 9 ? j * 2 : -2); + for (int i = 0; i < kSubsampledEdgeSize; ++i) { + // The order we fill in each row is: + // 8 pixels within the block, left edge, right edge + const int ix = ((j < 9 ? (j + 1) * kSubsampledEdgeSize : 0) + + (i < 9 ? i + 1 : 0)); + const int x0 = block_x * 16 + (i < 9 ? i * 2 : -2); + if (x0 < 0) { + subsampled[ix] = subsampled[ix + 1]; + } else if (y0 < 0) { + subsampled[ix] = subsampled[ix + kSubsampledEdgeSize]; + } else if (x0 >= width_) { + subsampled[ix] = subsampled[ix - 1]; + } else if (y0 >= height_) { + subsampled[ix] = subsampled[ix - kSubsampledEdgeSize]; + } else if (i < 8 && j < 8) { + subsampled[ix] = idct[j * 8 + i] << 4; + } else { + // Reconstruct the subsampled pixels around the edge of the current + // block by computing the inverse of the fancy upsampler. + const int y1 = std::max(y0 - 1, 0); + const int x1 = std::max(x0 - 1, 0); + subsampled[ix] = (pixels_[y0 * width_ + x0] * 9 + + pixels_[y1 * width_ + x1] + + pixels_[y0 * width_ + x1] * -3 + + pixels_[y1 * width_ + x0] * -3) >> 2; + } + } + } + + // Determine area to update. + int xmin = std::max(block_x * 16 - 1, 0); + int xmax = std::min(block_x * 16 + 16, width_ - 1); + int ymin = std::max(block_y * 16 - 1, 0); + int ymax = std::min(block_y * 16 + 16, height_ - 1); + + // Apply the fancy upsampler on the subsampled block. + for (int y = ymin; y <= ymax; ++y) { + const int y0 = ((y & ~1) / 2 - block_y * 8 + 1) * kSubsampledEdgeSize; + const int dy = ((y & 1) * 2 - 1) * kSubsampledEdgeSize; + uint16_t* rowptr = &pixels_[y * width_]; + for (int x = xmin; x <= xmax; ++x) { + const int x0 = (x & ~1) / 2 - block_x * 8 + 1; + const int dx = (x & 1) * 2 - 1; + const int ix = x0 + y0; + rowptr[x] = (subsampled[ix] * 9 + subsampled[ix + dy] * 3 + + subsampled[ix + dx] * 3 + subsampled[ix + dx + dy]) >> 4; + } + } + } else { + printf("Sampling ratio not supported: factor_x = %d factor_y = %d\n", + factor_x_, factor_y_); + exit(1); + } +} + +void OutputImageComponent::CopyFromJpegComponent(const JPEGComponent& comp, + int factor_x, int factor_y, + const int* quant) { + Reset(factor_x, factor_y); + assert(width_in_blocks_ <= comp.width_in_blocks); + assert(height_in_blocks_ <= comp.height_in_blocks); + const size_t src_row_size = comp.width_in_blocks * kDCTBlockSize; + for (int block_y = 0; block_y < height_in_blocks_; ++block_y) { + const coeff_t* src_coeffs = &comp.coeffs[block_y * src_row_size]; + for (int block_x = 0; block_x < width_in_blocks_; ++block_x) { + coeff_t block[kDCTBlockSize]; + for (int i = 0; i < kDCTBlockSize; ++i) { + block[i] = src_coeffs[i] * quant[i]; + } + SetCoeffBlock(block_x, block_y, block); + src_coeffs += kDCTBlockSize; + } + } + memcpy(quant_, quant, sizeof(quant_)); +} + +void OutputImageComponent::ApplyGlobalQuantization(const int q[kDCTBlockSize]) { + for (int block_y = 0; block_y < height_in_blocks_; ++block_y) { + for (int block_x = 0; block_x < width_in_blocks_; ++block_x) { + coeff_t block[kDCTBlockSize]; + GetCoeffBlock(block_x, block_y, block); + if (QuantizeBlock(block, q)) { + SetCoeffBlock(block_x, block_y, block); + } + } + } + memcpy(quant_, q, sizeof(quant_)); +} + +OutputImage::OutputImage(int w, int h) + : width_(w), + height_(h), + components_(3, OutputImageComponent(w, h)) {} + +void OutputImage::CopyFromJpegData(const JPEGData& jpg) { + for (int i = 0; i < jpg.components.size(); ++i) { + const JPEGComponent& comp = jpg.components[i]; + assert(jpg.max_h_samp_factor % comp.h_samp_factor == 0); + assert(jpg.max_v_samp_factor % comp.v_samp_factor == 0); + int factor_x = jpg.max_h_samp_factor / comp.h_samp_factor; + int factor_y = jpg.max_v_samp_factor / comp.v_samp_factor; + assert(comp.quant_idx < jpg.quant.size()); + components_[i].CopyFromJpegComponent(comp, factor_x, factor_y, + &jpg.quant[comp.quant_idx].values[0]); + } +} + +namespace { + +void SetDownsampledCoefficients(const std::vector& pixels, + int factor_x, int factor_y, + OutputImageComponent* comp) { + assert(pixels.size() == comp->width() * comp->height()); + comp->Reset(factor_x, factor_y); + for (int block_y = 0; block_y < comp->height_in_blocks(); ++block_y) { + for (int block_x = 0; block_x < comp->width_in_blocks(); ++block_x) { + double blockd[kDCTBlockSize]; + int x0 = 8 * block_x * factor_x; + int y0 = 8 * block_y * factor_y; + assert(x0 < comp->width()); + assert(y0 < comp->height()); + for (int iy = 0; iy < 8; ++iy) { + for (int ix = 0; ix < 8; ++ix) { + float avg = 0.0; + for (int j = 0; j < factor_y; ++j) { + for (int i = 0; i < factor_x; ++i) { + int x = std::min(x0 + ix * factor_x + i, comp->width() - 1); + int y = std::min(y0 + iy * factor_y + j, comp->height() - 1); + avg += pixels[y * comp->width() + x]; + } + } + avg /= factor_x * factor_y; + blockd[iy * 8 + ix] = avg; + } + } + ComputeBlockDCTDouble(blockd); + blockd[0] -= 1024.0; + coeff_t block[kDCTBlockSize]; + for (int k = 0; k < kDCTBlockSize; ++k) { + block[k] = static_cast(std::round(blockd[k])); + } + comp->SetCoeffBlock(block_x, block_y, block); + } + } +} + +} // namespace + +void OutputImage::Downsample(const DownsampleConfig& cfg) { + if (components_[1].IsAllZero() && components_[2].IsAllZero()) { + // If the image is already grayscale, nothing to do. + return; + } + if (cfg.use_silver_screen && + cfg.u_factor_x == 2 && cfg.u_factor_y == 2 && + cfg.v_factor_x == 2 && cfg.v_factor_y == 2) { + std::vector rgb = ToSRGB(); + std::vector > yuv = RGBToYUV420(rgb, width_, height_); + SetDownsampledCoefficients(yuv[0], 1, 1, &components_[0]); + SetDownsampledCoefficients(yuv[1], 2, 2, &components_[1]); + SetDownsampledCoefficients(yuv[2], 2, 2, &components_[2]); + return; + } + // Get the floating-point precision YUV array represented by the set of + // DCT coefficients. + std::vector > yuv(3, std::vector(width_ * height_)); + for (int c = 0; c < 3; ++c) { + components_[c].ToFloatPixels(&yuv[c][0], 1); + } + + yuv = PreProcessChannel(width_, height_, 2, 1.3, 0.5, + cfg.u_sharpen, cfg.u_blur, yuv); + yuv = PreProcessChannel(width_, height_, 1, 1.3, 0.5, + cfg.v_sharpen, cfg.v_blur, yuv); + + // Do the actual downsampling (averaging) and forward-DCT. + if (cfg.u_factor_x != 1 || cfg.u_factor_y != 1) { + SetDownsampledCoefficients(yuv[1], cfg.u_factor_x, cfg.u_factor_y, + &components_[1]); + } + if (cfg.v_factor_x != 1 || cfg.v_factor_y != 1) { + SetDownsampledCoefficients(yuv[2], cfg.v_factor_x, cfg.v_factor_y, + &components_[2]); + } +} + +void OutputImage::ApplyGlobalQuantization(const int q[3][kDCTBlockSize]) { + for (int c = 0; c < 3; ++c) { + components_[c].ApplyGlobalQuantization(&q[c][0]); + } +} + +void OutputImage::SaveToJpegData(JPEGData* jpg) const { + assert(components_[0].factor_x() == 1); + assert(components_[0].factor_y() == 1); + jpg->width = width_; + jpg->height = height_; + jpg->max_h_samp_factor = 1; + jpg->max_v_samp_factor = 1; + jpg->MCU_cols = components_[0].width_in_blocks(); + jpg->MCU_rows = components_[0].height_in_blocks(); + int ncomp = components_[1].IsAllZero() && components_[2].IsAllZero() ? 1 : 3; + for (int i = 1; i < ncomp; ++i) { + jpg->max_h_samp_factor = std::max(jpg->max_h_samp_factor, + components_[i].factor_x()); + jpg->max_v_samp_factor = std::max(jpg->max_h_samp_factor, + components_[i].factor_y()); + jpg->MCU_cols = std::min(jpg->MCU_cols, components_[i].width_in_blocks()); + jpg->MCU_rows = std::min(jpg->MCU_rows, components_[i].height_in_blocks()); + } + jpg->components.resize(ncomp); + int q[3][kDCTBlockSize]; + for (int c = 0; c < 3; ++c) { + memcpy(&q[c][0], components_[c].quant(), kDCTBlockSize * sizeof(q[0][0])); + } + for (int c = 0; c < ncomp; ++c) { + JPEGComponent* comp = &jpg->components[c]; + assert(jpg->max_h_samp_factor % components_[c].factor_x() == 0); + assert(jpg->max_v_samp_factor % components_[c].factor_y() == 0); + comp->id = c; + comp->h_samp_factor = jpg->max_h_samp_factor / components_[c].factor_x(); + comp->v_samp_factor = jpg->max_v_samp_factor / components_[c].factor_y(); + comp->width_in_blocks = jpg->MCU_cols * comp->h_samp_factor; + comp->height_in_blocks = jpg->MCU_rows * comp->v_samp_factor; + comp->num_blocks = comp->width_in_blocks * comp->height_in_blocks; + comp->coeffs.resize(kDCTBlockSize * comp->num_blocks); + + int last_dc = 0; + const coeff_t* src_coeffs = components_[c].coeffs(); + coeff_t* dest_coeffs = &comp->coeffs[0]; + for (int block_y = 0; block_y < comp->height_in_blocks; ++block_y) { + for (int block_x = 0; block_x < comp->width_in_blocks; ++block_x) { + if (block_y >= components_[c].height_in_blocks() || + block_x >= components_[c].width_in_blocks()) { + dest_coeffs[0] = last_dc; + for (int k = 1; k < kDCTBlockSize; ++k) { + dest_coeffs[k] = 0; + } + } else { + for (int k = 0; k < kDCTBlockSize; ++k) { + const int quant = q[c][k]; + int coeff = src_coeffs[k]; + assert(coeff % quant == 0); + dest_coeffs[k] = coeff / quant; + } + src_coeffs += kDCTBlockSize; + } + last_dc = dest_coeffs[0]; + dest_coeffs += kDCTBlockSize; + } + } + } + SaveQuantTables(q, jpg); +} + +std::vector OutputImage::ToSRGB(int xmin, int ymin, + int xsize, int ysize) const { + std::vector rgb(xsize * ysize * 3); + for (int c = 0; c < 3; ++c) { + components_[c].ToPixels(xmin, ymin, xsize, ysize, &rgb[c], 3); + } + for (int p = 0; p < rgb.size(); p += 3) { + ColorTransformYCbCrToRGB(&rgb[p]); + } + return rgb; +} + +std::vector OutputImage::ToSRGB() const { + return ToSRGB(0, 0, width_, height_); +} + +void OutputImage::ToLinearRGB(int xmin, int ymin, int xsize, int ysize, + std::vector >* rgb) const { + const double* lut = Srgb8ToLinearTable(); + std::vector rgb_pixels = ToSRGB(xmin, ymin, xsize, ysize); + for (int p = 0; p < xsize * ysize; ++p) { + for (int i = 0; i < 3; ++i) { + (*rgb)[i][p] = lut[rgb_pixels[3 * p + i]]; + } + } +} + +void OutputImage::ToLinearRGB(std::vector >* rgb) const { + ToLinearRGB(0, 0, width_, height_, rgb); +} + +std::string OutputImage::FrameTypeStr() const { + char buf[128]; + int len = snprintf(buf, sizeof(buf), "f%d%d%d%d%d%d", + component(0).factor_x(), component(0).factor_y(), + component(1).factor_x(), component(1).factor_y(), + component(2).factor_x(), component(2).factor_y()); + return std::string(buf, len); +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/output_image.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/output_image.h new file mode 100644 index 0000000..1018eea --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/output_image.h @@ -0,0 +1,157 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_OUTPUT_IMAGE_H_ +#define GUETZLI_OUTPUT_IMAGE_H_ + +#include +#include + +#include "guetzli/jpeg_data.h" + +namespace guetzli { + +class OutputImageComponent { + public: + OutputImageComponent(int w, int h); + + void Reset(int factor_x, int factor_y); + + int width() const { return width_; } + int height() const { return height_; } + int factor_x() const { return factor_x_; } + int factor_y() const { return factor_y_; } + int width_in_blocks() const { return width_in_blocks_; } + int height_in_blocks() const { return height_in_blocks_; } + const coeff_t* coeffs() const { return &coeffs_[0]; } + const int* quant() const { return &quant_[0]; } + bool IsAllZero() const; + + // Fills in block[] with the 8x8 coefficient block with block coordinates + // (block_x, block_y). + // NOTE: If the component is 2x2 subsampled, this corresponds to the 16x16 + // pixel area with upper-left corner (16 * block_x, 16 * block_y). + void GetCoeffBlock(int block_x, int block_y, + coeff_t block[kDCTBlockSize]) const; + + // Fills in out[] array with the 8-bit pixel view of this component cropped + // to the specified window. The window's upper-left corner, (xmin, ymin) must + // be within the image, but the window may extend past the image. In that + // case the edge pixels are duplicated. + void ToPixels(int xmin, int ymin, int xsize, int ysize, + uint8_t* out, int stride) const; + + // Fills in out[] array with the floating-point precision pixel view of the + // component. + // REQUIRES: factor_x() == 1 and factor_y() == 1. + void ToFloatPixels(float* out, int stride) const; + + // Sets the 8x8 coefficient block with block coordinates (block_x, block_y) + // to block[]. + // NOTE: If the component is 2x2 subsampled, this corresponds to the 16x16 + // pixel area with upper-left corner (16 * block_x, 16 * block_y). + // REQUIRES: block[k] % quant()[k] == 0 for each coefficient index k. + void SetCoeffBlock(int block_x, int block_y, + const coeff_t block[kDCTBlockSize]); + + // Requires that comp is not downsampled. + void CopyFromJpegComponent(const JPEGComponent& comp, + int factor_x, int factor_y, + const int* quant); + + void ApplyGlobalQuantization(const int q[kDCTBlockSize]); + + private: + void UpdatePixelsForBlock(int block_x, int block_y, + const uint8_t idct[kDCTBlockSize]); + + const int width_; + const int height_; + int factor_x_; + int factor_y_; + int width_in_blocks_; + int height_in_blocks_; + int num_blocks_; + std::vector coeffs_; + std::vector pixels_; + // Same as last argument of ApplyGlobalQuantization() (default is all 1s). + int quant_[kDCTBlockSize]; +}; + +class OutputImage { + public: + OutputImage(int w, int h); + + int width() const { return width_; } + int height() const { return height_; } + + OutputImageComponent& component(int c) { return components_[c]; } + const OutputImageComponent& component(int c) const { return components_[c]; } + + // Requires that jpg is in YUV444 format. + void CopyFromJpegData(const JPEGData& jpg); + + void ApplyGlobalQuantization(const int q[3][kDCTBlockSize]); + + // If sharpen or blur are enabled, preprocesses image before downsampling U or + // V to improve butteraugli score and/or reduce file size. + // u_sharpen: sharpen the u channel in red areas to improve score (not as + // effective as v_sharpen, blue is not so important) + // u_blur: blur the u channel in some areas to reduce file size + // v_sharpen: sharpen the v channel in red areas to improve score + // v_blur: blur the v channel in some areas to reduce file size + struct DownsampleConfig { + // Default is YUV420. + DownsampleConfig() : u_factor_x(2), u_factor_y(2), + v_factor_x(2), v_factor_y(2), + u_sharpen(true), u_blur(true), + v_sharpen(true), v_blur(true), + use_silver_screen(false) {} + int u_factor_x; + int u_factor_y; + int v_factor_x; + int v_factor_y; + bool u_sharpen; + bool u_blur; + bool v_sharpen; + bool v_blur; + bool use_silver_screen; + }; + + void Downsample(const DownsampleConfig& cfg); + + void SaveToJpegData(JPEGData* jpg) const; + + std::vector ToSRGB() const; + + std::vector ToSRGB(int xmin, int ymin, int xsize, int ysize) const; + + void ToLinearRGB(std::vector >* rgb) const; + + void ToLinearRGB(int xmin, int ymin, int xsize, int ysize, + std::vector >* rgb) const; + + std::string FrameTypeStr() const; + + private: + const int width_; + const int height_; + std::vector components_; +}; + +} // namespace guetzli + +#endif // GUETZLI_OUTPUT_IMAGE_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/preprocess_downsample.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/preprocess_downsample.cc new file mode 100644 index 0000000..a07c36a --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/preprocess_downsample.cc @@ -0,0 +1,478 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/preprocess_downsample.h" + +#include +#include +#include +#include + +using std::size_t; + +namespace { + +// convolve with size*size kernel +std::vector Convolve2D(const std::vector& image, int w, int h, + const double* kernel, int size) { + auto result = image; + int size2 = size / 2; + for (int i = 0; i < image.size(); i++) { + int x = i % w; + int y = i / w; + // Avoid non-normalized results at boundary by skipping edges. + if (x < size2 || x + size - size2 - 1 >= w + || y < size2 || y + size - size2 - 1 >= h) { + continue; + } + float v = 0; + for (int j = 0; j < size * size; j++) { + int x2 = x + j % size - size2; + int y2 = y + j / size - size2; + v += kernel[j] * image[y2 * w + x2]; + } + result[i] = v; + } + return result; +} + +// convolve horizontally and vertically with 1D kernel +std::vector Convolve2X(const std::vector& image, int w, int h, + const double* kernel, int size, double mul) { + auto temp = image; + int size2 = size / 2; + for (int i = 0; i < image.size(); i++) { + int x = i % w; + int y = i / w; + // Avoid non-normalized results at boundary by skipping edges. + if (x < size2 || x + size - size2 - 1 >= w) continue; + float v = 0; + for (int j = 0; j < size; j++) { + int x2 = x + j - size2; + v += kernel[j] * image[y * w + x2]; + } + temp[i] = v * mul; + } + auto result = temp; + for (int i = 0; i < temp.size(); i++) { + int x = i % w; + int y = i / w; + // Avoid non-normalized results at boundary by skipping edges. + if (y < size2 || y + size - size2 - 1 >= h) continue; + float v = 0; + for (int j = 0; j < size; j++) { + int y2 = y + j - size2; + v += kernel[j] * temp[y2 * w + x]; + } + result[i] = v * mul; + } + return result; +} + +double Normal(double x, double sigma) { + static const double kInvSqrt2Pi = 0.3989422804014327; + return std::exp(-x * x / (2 * sigma * sigma)) * kInvSqrt2Pi / sigma; +} + +std::vector Sharpen(const std::vector& image, int w, int h, + float sigma, float amount) { + // This is only made for small sigma, e.g. 1.3. + std::vector kernel(5); + for (int i = 0; i < kernel.size(); i++) { + kernel[i] = Normal(1.0 * i - kernel.size() / 2, sigma); + } + + double sum = 0; + for (int i = 0; i < kernel.size(); i++) sum += kernel[i]; + const double mul = 1.0 / sum; + + std::vector result = + Convolve2X(image, w, h, kernel.data(), kernel.size(), mul); + for (size_t i = 0; i < image.size(); i++) { + result[i] = image[i] + (image[i] - result[i]) * amount; + } + return result; +} + +void Erode(int w, int h, std::vector* image) { + std::vector temp = *image; + for (int y = 1; y + 1 < h; y++) { + for (int x = 1; x + 1 < w; x++) { + size_t index = y * w + x; + if (!(temp[index] && temp[index - 1] && temp[index + 1] + && temp[index - w] && temp[index + w])) { + (*image)[index] = 0; + } + } + } +} + +void Dilate(int w, int h, std::vector* image) { + std::vector temp = *image; + for (int y = 1; y + 1 < h; y++) { + for (int x = 1; x + 1 < w; x++) { + size_t index = y * w + x; + if (temp[index] || temp[index - 1] || temp[index + 1] + || temp[index - w] || temp[index + w]) { + (*image)[index] = 1; + } + } + } +} + +std::vector Blur(const std::vector& image, int w, int h) { + // This is only made for small sigma, e.g. 1.3. + static const double kSigma = 1.3; + std::vector kernel(5); + for (int i = 0; i < kernel.size(); i++) { + kernel[i] = Normal(1.0 * i - kernel.size() / 2, kSigma); + } + + double sum = 0; + for (int i = 0; i < kernel.size(); i++) sum += kernel[i]; + const double mul = 1.0 / sum; + + return Convolve2X(image, w, h, kernel.data(), kernel.size(), mul); +} + +} // namespace + +namespace guetzli { + +// Do the sharpening to the v channel, but only in areas where it will help +// channel should be 2 for v sharpening, or 1 for less effective u sharpening +std::vector> PreProcessChannel( + int w, int h, int channel, float sigma, float amount, bool blur, + bool sharpen, const std::vector>& image) { + if (!blur && !sharpen) return image; + + // Bring in range 0.0-1.0 for Y, -0.5 - 0.5 for U and V + auto yuv = image; + for (int i = 0; i < yuv[0].size(); i++) { + yuv[0][i] /= 255.0; + yuv[1][i] = yuv[1][i] / 255.0 - 0.5; + yuv[2][i] = yuv[2][i] / 255.0 - 0.5; + } + + // Map of areas where the image is not too bright to apply the effect. + std::vector darkmap(image[0].size(), false); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + size_t index = y * w + x; + float y = yuv[0][index]; + float u = yuv[1][index]; + float v = yuv[2][index]; + + float r = y + 1.402 * v; + float g = y - 0.34414 * u - 0.71414 * v; + float b = y + 1.772 * u; + + // Parameters tuned to avoid sharpening in too bright areas, where the + // effect makes it worse instead of better. + if (channel == 2 && g < 0.85 && b < 0.85 && r < 0.9) { + darkmap[index] = true; + } + if (channel == 1 && r < 0.85 && g < 0.85 && b < 0.9) { + darkmap[index] = true; + } + } + } + + Erode(w, h, &darkmap); + Erode(w, h, &darkmap); + Erode(w, h, &darkmap); + + // Map of areas where the image is red enough (blue in case of u channel). + std::vector redmap(image[0].size(), false); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + size_t index = y * w + x; + float u = yuv[1][index]; + float v = yuv[2][index]; + + // Parameters tuned to allow only colors on which sharpening is useful. + if (channel == 2 && 2.116 * v > -0.34414 * u + 0.2 + && 1.402 * v > 1.772 * u + 0.2) { + redmap[index] = true; + } + if (channel == 1 && v < 1.263 * u - 0.1 && u > -0.33741 * v) { + redmap[index] = true; + } + } + } + + Dilate(w, h, &redmap); + Dilate(w, h, &redmap); + Dilate(w, h, &redmap); + + // Map of areas where to allow sharpening by combining red and dark areas + std::vector sharpenmap(image[0].size(), 0); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + size_t index = y * w + x; + sharpenmap[index] = redmap[index] && darkmap[index]; + } + } + + // Threshold for where considered an edge. + const double threshold = (channel == 2 ? 0.02 : 1.0) * 127.5; + + static const double kEdgeMatrix[9] = { + 0, -1, 0, + -1, 4, -1, + 0, -1, 0 + }; + + // Map of areas where to allow blurring, only where it is not too sharp + std::vector blurmap(image[0].size(), false); + std::vector edge = Convolve2D(yuv[channel], w, h, kEdgeMatrix, 3); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + size_t index = y * w + x; + float u = yuv[1][index]; + float v = yuv[2][index]; + if (sharpenmap[index]) continue; + if (!darkmap[index]) continue; + if (fabs(edge[index]) < threshold && v < -0.162 * u) { + blurmap[index] = true; + } + } + } + Erode(w, h, &blurmap); + Erode(w, h, &blurmap); + + // Choose sharpened, blurred or original per pixel + std::vector sharpened = Sharpen(yuv[channel], w, h, sigma, amount); + std::vector blurred = Blur(yuv[channel], w, h); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + size_t index = y * w + x; + + if (sharpenmap[index] > 0) { + if (sharpen) yuv[channel][index] = sharpened[index]; + } else if (blurmap[index] > 0) { + if (blur) yuv[channel][index] = blurred[index]; + } + } + } + + // Bring back to range 0-255 + for (int i = 0; i < yuv[0].size(); i++) { + yuv[0][i] *= 255.0; + yuv[1][i] = (yuv[1][i] + 0.5) * 255.0; + yuv[2][i] = (yuv[2][i] + 0.5) * 255.0; + } + return yuv; +} + +namespace { + +inline float Clip(float val) { + return std::max(0.0f, std::min(255.0f, val)); +} + +inline float RGBToY(float r, float g, float b) { + return 0.299f * r + 0.587f * g + 0.114f * b; +} + +inline float RGBToU(float r, float g, float b) { + return -0.16874f * r - 0.33126f * g + 0.5f * b + 128.0; +} + +inline float RGBToV(float r, float g, float b) { + return 0.5f * r - 0.41869f * g - 0.08131f * b + 128.0; +} + +inline float YUVToR(float y, float u, float v) { + return y + 1.402 * (v - 128.0); +} + +inline float YUVToG(float y, float u, float v) { + return y - 0.344136 * (u - 128.0) - 0.714136 * (v - 128.0); +} + +inline float YUVToB(float y, float u, float v) { + return y + 1.772 * (u - 128.0); +} + +// TODO(user) Use SRGB->linear conversion and a lookup-table. +inline float GammaToLinear(float x) { + return std::pow(x / 255.0, 2.2); +} + +// TODO(user) Use linear->SRGB conversion and a lookup-table. +inline float LinearToGamma(float x) { + return 255.0 * std::pow(x, 1.0 / 2.2); +} + +std::vector LinearlyAveragedLuma(const std::vector& rgb) { + assert(rgb.size() % 3 == 0); + std::vector y(rgb.size() / 3); + for (int i = 0, p = 0; p < rgb.size(); ++i, p += 3) { + y[i] = LinearToGamma(RGBToY(GammaToLinear(rgb[p + 0]), + GammaToLinear(rgb[p + 1]), + GammaToLinear(rgb[p + 2]))); + } + return y; +} + +std::vector LinearlyDownsample2x2(const std::vector& rgb_in, + const int width, const int height) { + assert(rgb_in.size() == 3 * width * height); + int w = (width + 1) / 2; + int h = (height + 1) / 2; + std::vector rgb_out(3 * w * h); + for (int y = 0, p = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + for (int i = 0; i < 3; ++i, ++p) { + rgb_out[p] = 0.0; + for (int iy = 0; iy < 2; ++iy) { + for (int ix = 0; ix < 2; ++ix) { + int yy = std::min(height - 1, 2 * y + iy); + int xx = std::min(width - 1, 2 * x + ix); + rgb_out[p] += GammaToLinear(rgb_in[3 * (yy * width + xx) + i]); + } + } + rgb_out[p] = LinearToGamma(0.25 * rgb_out[p]); + } + } + } + return rgb_out; +} + +std::vector > RGBToYUV(const std::vector& rgb) { + std::vector > yuv(3, std::vector(rgb.size() / 3)); + for (int i = 0, p = 0; p < rgb.size(); ++i, p += 3) { + const float r = rgb[p + 0]; + const float g = rgb[p + 1]; + const float b = rgb[p + 2]; + yuv[0][i] = RGBToY(r, g, b); + yuv[1][i] = RGBToU(r, g, b); + yuv[2][i] = RGBToV(r, g, b); + } + return yuv; +} + +std::vector YUVToRGB(const std::vector >& yuv) { + std::vector rgb(3 * yuv[0].size()); + for (int i = 0, p = 0; p < rgb.size(); ++i, p += 3) { + const float y = yuv[0][i]; + const float u = yuv[1][i]; + const float v = yuv[2][i]; + rgb[p + 0] = Clip(YUVToR(y, u, v)); + rgb[p + 1] = Clip(YUVToG(y, u, v)); + rgb[p + 2] = Clip(YUVToB(y, u, v)); + } + return rgb; +} + +// Upsamples img_in with a box-filter, and returns an image with output +// dimensions width x height. +std::vector Upsample2x2(const std::vector& img_in, + const int width, const int height) { + int w = (width + 1) / 2; + int h = (height + 1) / 2; + assert(img_in.size() == w * h); + std::vector img_out(width * height); + for (int y = 0, p = 0; y < h; ++y) { + for (int x = 0; x < w; ++x, ++p) { + for (int iy = 0; iy < 2; ++iy) { + for (int ix = 0; ix < 2; ++ix) { + int yy = std::min(height - 1, 2 * y + iy); + int xx = std::min(width - 1, 2 * x + ix); + img_out[yy * width + xx] = img_in[p]; + } + } + } + } + return img_out; +} + +// Apply the "fancy upsample" filter used by libjpeg. +std::vector Blur(const std::vector& img, + const int width, const int height) { + std::vector img_out(width * height); + for (int y0 = 0; y0 < height; y0 += 2) { + for (int x0 = 0; x0 < width; x0 += 2) { + for (int iy = 0; iy < 2 && y0 + iy < height; ++iy) { + for (int ix = 0; ix < 2 && x0 + ix < width; ++ix) { + int dy = 4 * iy - 2; + int dx = 4 * ix - 2; + int x1 = std::min(width - 1, std::max(0, x0 + dx)); + int y1 = std::min(height - 1, std::max(0, y0 + dy)); + img_out[(y0 + iy) * width + x0 + ix] = + (9.0 * img[y0 * width + x0] + + 3.0 * img[y0 * width + x1] + + 3.0 * img[y1 * width + x0] + + 1.0 * img[y1 * width + x1]) / 16.0; + } + } + } + } + return img_out; +} + +std::vector YUV420ToRGB(const std::vector >& yuv420, + const int width, const int height) { + std::vector > yuv; + yuv.push_back(yuv420[0]); + std::vector u = Upsample2x2(yuv420[1], width, height); + std::vector v = Upsample2x2(yuv420[2], width, height); + yuv.push_back(Blur(u, width, height)); + yuv.push_back(Blur(v, width, height)); + return YUVToRGB(yuv); +} + +void UpdateGuess(const std::vector& target, + const std::vector& reconstructed, + std::vector* guess) { + assert(reconstructed.size() == guess->size()); + assert(target.size() == guess->size()); + for (int i = 0; i < guess->size(); ++i) { + // TODO(user): Evaluate using a decaying constant here. + (*guess)[i] = Clip((*guess)[i] - (reconstructed[i] - target[i])); + } +} + +} // namespace + +std::vector > RGBToYUV420( + const std::vector& rgb_in, const int width, const int height) { + std::vector rgbf(rgb_in.size()); + for (int i = 0; i < rgb_in.size(); ++i) { + rgbf[i] = static_cast(rgb_in[i]); + } + std::vector y_target = LinearlyAveragedLuma(rgbf); + std::vector > yuv_target = + RGBToYUV(LinearlyDownsample2x2(rgbf, width, height)); + std::vector > yuv_guess = yuv_target; + yuv_guess[0] = Upsample2x2(yuv_guess[0], width, height); + // TODO(user): Stop early if the error is small enough. + for (int iter = 0; iter < 20; ++iter) { + std::vector rgb_rec = YUV420ToRGB(yuv_guess, width, height); + std::vector y_rec = LinearlyAveragedLuma(rgb_rec); + std::vector > yuv_rec = + RGBToYUV(LinearlyDownsample2x2(rgb_rec, width, height)); + UpdateGuess(y_target, y_rec, &yuv_guess[0]); + UpdateGuess(yuv_target[1], yuv_rec[1], &yuv_guess[1]); + UpdateGuess(yuv_target[2], yuv_rec[2], &yuv_guess[2]); + } + yuv_guess[1] = Upsample2x2(yuv_guess[1], width, height); + yuv_guess[2] = Upsample2x2(yuv_guess[2], width, height); + return yuv_guess; +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/preprocess_downsample.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/preprocess_downsample.h new file mode 100644 index 0000000..bd3d86a --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/preprocess_downsample.h @@ -0,0 +1,40 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Preprocesses U and V channel for better results after downsampling. + +#ifndef GUETZLI_PREPROCESS_DOWNSAMPLE_H_ +#define GUETZLI_PREPROCESS_DOWNSAMPLE_H_ + +#include +#include + +namespace guetzli { + +// Preprocesses the u (1) or v (2) channel of the given YUV image (range 0-255). +std::vector> PreProcessChannel( + int w, int h, int channel, float sigma, float amount, bool blur, + bool sharpen, const std::vector>& image); + +// Gamma-compensated chroma subsampling. +// Returns Y, U, V image planes, each with width x height dimensions, but the +// U and V planes are composed of 2x2 blocks with the same values. +std::vector > RGBToYUV420( + const std::vector& rgb_in, const int width, const int height); + +} // namespace guetzli + +#endif // GUETZLI_PREPROCESS_DOWNSAMPLE_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/processor.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/processor.cc new file mode 100644 index 0000000..f608e40 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/processor.cc @@ -0,0 +1,889 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/processor.h" + +#include +#include +#include +#include + +#include "guetzli/butteraugli_comparator.h" +#include "guetzli/comparator.h" +#include "guetzli/debug_print.h" +#include "guetzli/fast_log.h" +#include "guetzli/jpeg_data_decoder.h" +#include "guetzli/jpeg_data_encoder.h" +#include "guetzli/jpeg_data_reader.h" +#include "guetzli/jpeg_data_writer.h" +#include "guetzli/output_image.h" +#include "guetzli/quantize.h" + +namespace guetzli { + +namespace { + +static const size_t kBlockSize = 3 * kDCTBlockSize; + +struct CoeffData { + int idx; + float block_err; +}; +struct QuantData { + int q[3][kDCTBlockSize]; + bool dist_ok; + GuetzliOutput out; +}; +class Processor { + public: + bool ProcessJpegData(const Params& params, const JPEGData& jpg_in, + Comparator* comparator, GuetzliOutput* out, + ProcessStats* stats); + + private: + void SelectFrequencyMasking(const JPEGData& jpg, OutputImage* img, + const uint8_t comp_mask, const double target_mul, + bool stop_early); + void ComputeBlockZeroingOrder( + const coeff_t block[kBlockSize], const coeff_t orig_block[kBlockSize], + const int block_x, const int block_y, const int factor_x, + const int factor_y, const uint8_t comp_mask, OutputImage* img, + std::vector* output_order); + bool SelectQuantMatrix(const JPEGData& jpg_in, const bool downsample, + int best_q[3][kDCTBlockSize], + GuetzliOutput* quantized_out); + QuantData TryQuantMatrix(const JPEGData& jpg_in, + const float target_mul, + int q[3][kDCTBlockSize]); + void MaybeOutput(const std::string& encoded_jpg); + void DownsampleImage(OutputImage* img); + void OutputJpeg(const JPEGData& in, std::string* out); + + Params params_; + Comparator* comparator_; + GuetzliOutput* final_output_; + ProcessStats* stats_; +}; + +void RemoveOriginalQuantization(JPEGData* jpg, int q_in[3][kDCTBlockSize]) { + for (int i = 0; i < 3; ++i) { + JPEGComponent& c = jpg->components[i]; + const int* q = &jpg->quant[c.quant_idx].values[0]; + memcpy(&q_in[i][0], q, kDCTBlockSize * sizeof(q[0])); + for (int j = 0; j < c.coeffs.size(); ++j) { + c.coeffs[j] *= q[j % kDCTBlockSize]; + } + } + int q[3][kDCTBlockSize]; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < kDCTBlockSize; ++j) q[i][j] = 1; + SaveQuantTables(q, jpg); +} + +void Processor::DownsampleImage(OutputImage* img) { + if (img->component(1).factor_x() > 1 || img->component(1).factor_y() > 1) { + return; + } + OutputImage::DownsampleConfig cfg; + cfg.use_silver_screen = params_.use_silver_screen; + img->Downsample(cfg); +} + +} // namespace + +int GuetzliStringOut(void* data, const uint8_t* buf, size_t count) { + std::string* sink = + reinterpret_cast(data); + sink->append(reinterpret_cast(buf), count); + return count; +} + +void Processor::OutputJpeg(const JPEGData& jpg, + std::string* out) { + out->clear(); + JPEGOutput output(GuetzliStringOut, out); + if (!WriteJpeg(jpg, params_.clear_metadata, output)) { + assert(0); + } +} + +void Processor::MaybeOutput(const std::string& encoded_jpg) { + double score = comparator_->ScoreOutputSize(encoded_jpg.size()); + GUETZLI_LOG(stats_, " Score[%.4f]", score); + if (score < final_output_->score || final_output_->score < 0) { + final_output_->jpeg_data = encoded_jpg; + final_output_->distmap = comparator_->distmap(); + final_output_->distmap_aggregate = comparator_->distmap_aggregate(); + final_output_->score = score; + GUETZLI_LOG(stats_, " (*)"); + } + GUETZLI_LOG(stats_, "\n"); +} + +bool CompareQuantData(const QuantData& a, const QuantData& b) { + if (a.dist_ok && !b.dist_ok) return true; + if (!a.dist_ok && b.dist_ok) return false; + return a.out.jpeg_data.size() < b.out.jpeg_data.size(); +} + +// Compares a[0..kBlockSize) and b[0..kBlockSize) vectors, and returns +// 0 : if they are equal +// -1 : if a is everywhere <= than b and in at least one coordinate < +// 1 : if a is everywhere >= than b and in at least one coordinate > +// 2 : if a and b are uncomparable (some coordinate smaller and some greater) +int CompareQuantMatrices(const int* a, const int* b) { + int i = 0; + while (i < kBlockSize && a[i] == b[i]) ++i; + if (i == kBlockSize) { + return 0; + } + if (a[i] < b[i]) { + for (++i; i < kBlockSize; ++i) { + if (a[i] > b[i]) return 2; + } + return -1; + } else { + for (++i; i < kBlockSize; ++i) { + if (a[i] < b[i]) return 2; + } + return 1; + } +} + +double ContrastSensitivity(int k) { + return 1.0 / (1.0 + kJPEGZigZagOrder[k] / 2.0); +} + +double QuantMatrixHeuristicScore(const int q[3][kDCTBlockSize]) { + double score = 0.0; + for (int c = 0; c < 3; ++c) { + for (int k = 0; k < kDCTBlockSize; ++k) { + score += 0.5 * (q[c][k] - 1.0) * ContrastSensitivity(k); + } + } + return score; +} + +class QuantMatrixGenerator { + public: + QuantMatrixGenerator(bool downsample, ProcessStats* stats) + : downsample_(downsample), hscore_a_(-1.0), hscore_b_(-1.0), + total_csf_(0.0), stats_(stats) { + for (int k = 0; k < kDCTBlockSize; ++k) { + total_csf_ += 3.0 * ContrastSensitivity(k); + } + } + + bool GetNext(int q[3][kDCTBlockSize]) { + // This loop should terminate by return. This 1000 iteration limit is just a + // precaution. + for (int iter = 0; iter < 1000; iter++) { + double hscore; + if (hscore_b_ == -1.0) { + if (hscore_a_ == -1.0) { + hscore = downsample_ ? 0.0 : total_csf_; + } else { + hscore = hscore_a_ + total_csf_; + } + if (hscore > 100 * total_csf_) { + // We could not find a quantization matrix that creates enough + // butteraugli error. This can happen if all dct coefficients are + // close to zero in the original image. + return false; + } + } else if (hscore_b_ == 0.0) { + return false; + } else if (hscore_a_ == -1.0) { + hscore = 0.0; + } else { + int lower_q[3][kDCTBlockSize]; + int upper_q[3][kDCTBlockSize]; + constexpr double kEps = 0.05; + GetQuantMatrixWithHeuristicScore( + (1 - kEps) * hscore_a_ + kEps * 0.5 * (hscore_a_ + hscore_b_), + lower_q); + GetQuantMatrixWithHeuristicScore( + (1 - kEps) * hscore_b_ + kEps * 0.5 * (hscore_a_ + hscore_b_), + upper_q); + if (CompareQuantMatrices(&lower_q[0][0], &upper_q[0][0]) == 0) + return false; + hscore = (hscore_a_ + hscore_b_) * 0.5; + } + GetQuantMatrixWithHeuristicScore(hscore, q); + bool retry = false; + for (int i = 0; i < quants_.size(); ++i) { + if (CompareQuantMatrices(&q[0][0], &quants_[i].q[0][0]) == 0) { + if (quants_[i].dist_ok) { + hscore_a_ = hscore; + } else { + hscore_b_ = hscore; + } + retry = true; + break; + } + } + if (!retry) return true; + } + return false; + } + + void Add(const QuantData& data) { + quants_.push_back(data); + double hscore = QuantMatrixHeuristicScore(data.q); + if (data.dist_ok) { + hscore_a_ = std::max(hscore_a_, hscore); + } else { + hscore_b_ = hscore_b_ == -1.0 ? hscore : std::min(hscore_b_, hscore); + } + } + + private: + void GetQuantMatrixWithHeuristicScore(double score, + int q[3][kDCTBlockSize]) const { + int level = static_cast(score / total_csf_); + score -= level * total_csf_; + for (int k = kDCTBlockSize - 1; k >= 0; --k) { + for (int c = 0; c < 3; ++c) { + q[c][kJPEGNaturalOrder[k]] = 2 * level + (score > 0.0 ? 3 : 1); + } + score -= 3.0 * ContrastSensitivity(kJPEGNaturalOrder[k]); + } + } + + const bool downsample_; + // Lower bound for quant matrix heuristic score used in binary search. + double hscore_a_; + // Upper bound for quant matrix heuristic score used in binary search, or 0.0 + // if no upper bound is found yet. + double hscore_b_; + // Cached value of the sum of all ContrastSensitivity() values over all + // quant matrix elements. + double total_csf_; + std::vector quants_; + + ProcessStats* stats_; +}; + +QuantData Processor::TryQuantMatrix(const JPEGData& jpg_in, + const float target_mul, + int q[3][kDCTBlockSize]) { + QuantData data; + memcpy(data.q, q, sizeof(data.q)); + OutputImage img(jpg_in.width, jpg_in.height); + img.CopyFromJpegData(jpg_in); + img.ApplyGlobalQuantization(data.q); + JPEGData jpg_out = jpg_in; + img.SaveToJpegData(&jpg_out); + std::string encoded_jpg; + OutputJpeg(jpg_out, &encoded_jpg); + GUETZLI_LOG(stats_, "Iter %2d: %s quantization matrix:\n", + stats_->counters[kNumItersCnt] + 1, + img.FrameTypeStr().c_str()); + GUETZLI_LOG_QUANT(stats_, q); + GUETZLI_LOG(stats_, "Iter %2d: %s GQ[%5.2f] Out[%7zd]", + stats_->counters[kNumItersCnt] + 1, + img.FrameTypeStr().c_str(), + QuantMatrixHeuristicScore(q), encoded_jpg.size()); + ++stats_->counters[kNumItersCnt]; + comparator_->Compare(img); + data.dist_ok = comparator_->DistanceOK(target_mul); + data.out.jpeg_data = encoded_jpg; + data.out.distmap = comparator_->distmap(); + data.out.distmap_aggregate = comparator_->distmap_aggregate(); + data.out.score = comparator_->ScoreOutputSize(encoded_jpg.size()); + MaybeOutput(encoded_jpg); + return data; +} + +bool Processor::SelectQuantMatrix(const JPEGData& jpg_in, const bool downsample, + int best_q[3][kDCTBlockSize], + GuetzliOutput* quantized_out) { + QuantMatrixGenerator qgen(downsample, stats_); + // Don't try to go up to exactly the target distance when selecting a + // quantization matrix, since we will need some slack to do the frequency + // masking later. + const float target_mul_high = 0.97; + const float target_mul_low = 0.95; + + QuantData best = TryQuantMatrix(jpg_in, target_mul_high, best_q); + for (;;) { + int q_next[3][kDCTBlockSize]; + if (!qgen.GetNext(q_next)) { + break; + } + + QuantData data = + TryQuantMatrix(jpg_in, target_mul_high, q_next); + qgen.Add(data); + if (CompareQuantData(data, best)) { + best = data; + if (data.dist_ok && !comparator_->DistanceOK(target_mul_low)) { + break; + } + } + } + + memcpy(&best_q[0][0], &best.q[0][0], kBlockSize * sizeof(best_q[0][0])); + *quantized_out = best.out; + GUETZLI_LOG(stats_, "\n%s selected quantization matrix:\n", + downsample ? "YUV420" : "YUV444"); + GUETZLI_LOG_QUANT(stats_, best_q); + return best.dist_ok; +} + + +// REQUIRES: block[c*64...(c*64+63)] is all zero if (comp_mask & (1<* output_order) { + static const uint8_t oldCsf[kDCTBlockSize] = { + 10, 10, 20, 40, 60, 70, 80, 90, + 10, 20, 30, 60, 70, 80, 90, 90, + 20, 30, 60, 70, 80, 90, 90, 90, + 40, 60, 70, 80, 90, 90, 90, 90, + 60, 70, 80, 90, 90, 90, 90, 90, + 70, 80, 90, 90, 90, 90, 90, 90, + 80, 90, 90, 90, 90, 90, 90, 90, + 90, 90, 90, 90, 90, 90, 90, 90, + }; + static const double kWeight[3] = { 1.0, 0.22, 0.20 }; +#include "guetzli/order.inc" + std::vector > input_order; + for (int c = 0; c < 3; ++c) { + if (!(comp_mask & (1 << c))) continue; + for (int k = 1; k < kDCTBlockSize; ++k) { + int idx = c * kDCTBlockSize + k; + if (block[idx] != 0) { + float score; + if (params_.new_zeroing_model) { + score = std::abs(orig_block[idx]) * csf[idx] + bias[idx]; + } else { + score = (std::abs(orig_block[idx]) - kJPEGZigZagOrder[k] / 64.0) * + kWeight[c] / oldCsf[k]; + } + input_order.push_back(std::make_pair(idx, score)); + } + } + } + std::sort(input_order.begin(), input_order.end(), + [](const std::pair& a, const std::pair& b) { + return a.second < b.second; }); + coeff_t processed_block[kBlockSize]; + memcpy(processed_block, block, sizeof(processed_block)); + while (!input_order.empty()) { + float best_err = 1e17; + int best_i = 0; + for (int i = 0; i < std::min(params_.zeroing_greedy_lookahead, + input_order.size()); + ++i) { + coeff_t candidate_block[kBlockSize]; + memcpy(candidate_block, processed_block, sizeof(candidate_block)); + const int idx = input_order[i].first; + candidate_block[idx] = 0; + for (int c = 0; c < 3; ++c) { + if (comp_mask & (1 << c)) { + img->component(c).SetCoeffBlock( + block_x, block_y, &candidate_block[c * kDCTBlockSize]); + } + } + float max_err = 0; + for (int iy = 0; iy < factor_y; ++iy) { + for (int ix = 0; ix < factor_x; ++ix) { + int block_xx = block_x * factor_x + ix; + int block_yy = block_y * factor_y + iy; + if (8 * block_xx < img->width() && 8 * block_yy < img->height()) { + float err = comparator_->CompareBlock(*img, block_xx, block_yy); + max_err = std::max(max_err, err); + } + } + } + if (max_err < best_err) { + best_err = max_err; + best_i = i; + } + } + int idx = input_order[best_i].first; + processed_block[idx] = 0; + input_order.erase(input_order.begin() + best_i); + output_order->push_back({idx, best_err}); + for (int c = 0; c < 3; ++c) { + if (comp_mask & (1 << c)) { + img->component(c).SetCoeffBlock( + block_x, block_y, &processed_block[c * kDCTBlockSize]); + } + } + } + // Make the block error values monotonic. + float min_err = 1e10; + for (int i = output_order->size() - 1; i >= 0; --i) { + min_err = std::min(min_err, (*output_order)[i].block_err); + (*output_order)[i].block_err = min_err; + } + // Cut off at the block error limit. + int num = 0; + while (num < output_order->size() && + (*output_order)[num].block_err <= comparator_->BlockErrorLimit()) { + ++num; + } + output_order->resize(num); + // Restore *img to the same state as it was at the start of this function. + for (int c = 0; c < 3; ++c) { + if (comp_mask & (1 << c)) { + img->component(c).SetCoeffBlock( + block_x, block_y, &block[c * kDCTBlockSize]); + } + } +} + +namespace { + +void UpdateACHistogram(const int weight, + const coeff_t* coeffs, + const int* q, + JpegHistogram* ac_histogram) { + int r = 0; + for (int k = 1; k < 64; ++k) { + const int k_nat = kJPEGNaturalOrder[k]; + coeff_t coeff = coeffs[k_nat]; + if (coeff == 0) { + r++; + continue; + } + while (r > 15) { + ac_histogram->Add(0xf0, weight); + r -= 16; + } + int nbits = Log2FloorNonZero(std::abs(coeff / q[k_nat])) + 1; + int symbol = (r << 4) + nbits; + ac_histogram->Add(symbol, weight); + r = 0; + } + if (r > 0) { + ac_histogram->Add(0, weight); + } +} + +size_t ComputeEntropyCodes(const std::vector& histograms, + std::vector* depths) { + std::vector clustered = histograms; + size_t num = histograms.size(); + std::vector indexes(histograms.size()); + std::vector clustered_depths( + histograms.size() * JpegHistogram::kSize); + ClusterHistograms(&clustered[0], &num, &indexes[0], &clustered_depths[0]); + depths->resize(clustered_depths.size()); + for (int i = 0; i < histograms.size(); ++i) { + memcpy(&(*depths)[i * JpegHistogram::kSize], + &clustered_depths[indexes[i] * JpegHistogram::kSize], + JpegHistogram::kSize); + } + size_t histogram_size = 0; + for (int i = 0; i < num; ++i) { + histogram_size += HistogramHeaderCost(clustered[i]) / 8; + } + return histogram_size; +} + +size_t EntropyCodedDataSize(const std::vector& histograms, + const std::vector& depths) { + size_t numbits = 0; + for (int i = 0; i < histograms.size(); ++i) { + numbits += HistogramEntropyCost( + histograms[i], &depths[i * JpegHistogram::kSize]); + } + return (numbits + 7) / 8; +} + +size_t EstimateDCSize(const JPEGData& jpg) { + std::vector histograms(jpg.components.size()); + BuildDCHistograms(jpg, &histograms[0]); + size_t num = histograms.size(); + std::vector indexes(num); + std::vector depths(num * JpegHistogram::kSize); + return ClusterHistograms(&histograms[0], &num, &indexes[0], &depths[0]); +} + +} // namespace + +void Processor::SelectFrequencyMasking(const JPEGData& jpg, OutputImage* img, + const uint8_t comp_mask, + const double target_mul, + bool stop_early) { + const int width = img->width(); + const int height = img->height(); + const int last_c = Log2FloorNonZero(comp_mask); + if (last_c >= jpg.components.size()) return; + const int factor_x = img->component(last_c).factor_x(); + const int factor_y = img->component(last_c).factor_y(); + const int block_width = (width + 8 * factor_x - 1) / (8 * factor_x); + const int block_height = (height + 8 * factor_y - 1) / (8 * factor_y); + const int num_blocks = block_width * block_height; + + std::vector > orders(num_blocks); + for (int block_y = 0, block_ix = 0; block_y < block_height; ++block_y) { + for (int block_x = 0; block_x < block_width; ++block_x, ++block_ix) { + coeff_t block[kBlockSize] = { 0 }; + coeff_t orig_block[kBlockSize] = { 0 }; + for (int c = 0; c < 3; ++c) { + if (comp_mask & (1 << c)) { + assert(img->component(c).factor_x() == factor_x); + assert(img->component(c).factor_y() == factor_y); + img->component(c).GetCoeffBlock(block_x, block_y, + &block[c * kDCTBlockSize]); + const JPEGComponent& comp = jpg.components[c]; + int jpg_block_ix = block_y * comp.width_in_blocks + block_x; + memcpy(&orig_block[c * kDCTBlockSize], + &comp.coeffs[jpg_block_ix * kDCTBlockSize], + kDCTBlockSize * sizeof(orig_block[0])); + } + } + ComputeBlockZeroingOrder(block, orig_block, block_x, block_y, factor_x, + factor_y, comp_mask, img, + &orders[block_ix]); + } + } + + JPEGData jpg_out = jpg; + img->SaveToJpegData(&jpg_out); + const int jpg_header_size = JpegHeaderSize(jpg_out, params_.clear_metadata); + const int dc_size = EstimateDCSize(jpg_out); + std::vector ac_histograms(jpg_out.components.size()); + BuildACHistograms(jpg_out, &ac_histograms[0]); + std::vector ac_depths; + int ac_histogram_size = ComputeEntropyCodes(ac_histograms, &ac_depths); + int base_size = jpg_header_size + dc_size + ac_histogram_size + + EntropyCodedDataSize(ac_histograms, ac_depths); + int prev_size = base_size; + + std::vector max_block_error(num_blocks); + std::vector last_indexes(num_blocks); + std::vector distmap(width * height); + + bool first_up_iter = true; + for (int direction : {1, -1}) { + for (;;) { + if (stop_early && direction == -1) { + if (prev_size > 1.01 * final_output_->jpeg_data.size()) { + // If we are down-adjusting the error, the output size will only keep + // increasing. + // TODO(user): Do this check always by comparing only the size + // of the currently processed components. + break; + } + } + std::vector > global_order; + int blocks_to_change; + std::vector block_weight; + for (int rblock = 1; rblock <= 4; ++rblock) { + block_weight = std::vector(num_blocks); + comparator_->ComputeBlockErrorAdjustmentWeights( + direction, rblock, target_mul, factor_x, factor_y, distmap, + &block_weight); + global_order.clear(); + blocks_to_change = 0; + for (int block_y = 0, block_ix = 0; block_y < block_height; ++block_y) { + for (int block_x = 0; block_x < block_width; ++block_x, ++block_ix) { + const int last_index = last_indexes[block_ix]; + const std::vector& order = orders[block_ix]; + const float max_err = max_block_error[block_ix]; + if (block_weight[block_ix] == 0) { + continue; + } + if (direction > 0) { + for (int i = last_index; i < order.size(); ++i) { + float val = ((order[i].block_err - max_err) / + block_weight[block_ix]); + global_order.push_back(std::make_pair(block_ix, val)); + } + blocks_to_change += (last_index < order.size() ? 1 : 0); + } else { + for (int i = last_index - 1; i >= 0; --i) { + float val = ((max_err - order[i].block_err) / + block_weight[block_ix]); + global_order.push_back(std::make_pair(block_ix, val)); + } + blocks_to_change += (last_index > 0 ? 1 : 0); + } + } + } + if (!global_order.empty()) { + // If we found something to adjust with the current block adjustment + // radius, we can stop and adjust the blocks we have. + break; + } + } + + if (global_order.empty()) { + break; + } + + std::sort(global_order.begin(), global_order.end(), + [](const std::pair& a, + const std::pair& b) { + return a.second < b.second; }); + + double rel_size_delta = direction > 0 ? 0.01 : 0.0005; + if (direction > 0 && comparator_->DistanceOK(1.0)) { + rel_size_delta = 0.05; + } + size_t min_size_delta = base_size * rel_size_delta; + + float coeffs_to_change_per_block = + direction > 0 ? 2.0 : factor_x * factor_y * 0.2; + int min_coeffs_to_change = coeffs_to_change_per_block * blocks_to_change; + + if (first_up_iter) { + const float limit = 0.75 * comparator_->BlockErrorLimit(); + auto it = std::partition_point(global_order.begin(), global_order.end(), + [=](const std::pair& a) { + return a.second < limit; }); + min_coeffs_to_change = std::max(min_coeffs_to_change, + it - global_order.begin()); + first_up_iter = false; + } + + std::set changed_blocks; + float val_threshold = 0.0; + int changed_coeffs = 0; + int est_jpg_size = prev_size; + for (int i = 0; i < global_order.size(); ++i) { + const int block_ix = global_order[i].first; + const int block_x = block_ix % block_width; + const int block_y = block_ix / block_width; + const int last_idx = last_indexes[block_ix]; + const std::vector& order = orders[block_ix]; + const int idx = order[last_idx + std::min(direction, 0)].idx; + const int c = idx / kDCTBlockSize; + const int k = idx % kDCTBlockSize; + const int* quant = img->component(c).quant(); + const JPEGComponent& comp = jpg.components[c]; + const int jpg_block_ix = block_y * comp.width_in_blocks + block_x; + const int newval = direction > 0 ? 0 : Quantize( + comp.coeffs[jpg_block_ix * kDCTBlockSize + k], quant[k]); + coeff_t block[kDCTBlockSize] = { 0 }; + img->component(c).GetCoeffBlock(block_x, block_y, block); + UpdateACHistogram(-1, block, quant, &ac_histograms[c]); + block[k] = newval; + UpdateACHistogram(1, block, quant, &ac_histograms[c]); + img->component(c).SetCoeffBlock(block_x, block_y, block); + last_indexes[block_ix] += direction; + changed_blocks.insert(block_ix); + val_threshold = global_order[i].second; + ++changed_coeffs; + static const int kEntropyCodeUpdateFreq = 10; + if (i % kEntropyCodeUpdateFreq == 0) { + ac_histogram_size = ComputeEntropyCodes(ac_histograms, &ac_depths); + } + est_jpg_size = jpg_header_size + dc_size + ac_histogram_size + + EntropyCodedDataSize(ac_histograms, ac_depths); + if (changed_coeffs > min_coeffs_to_change && + std::abs(est_jpg_size - prev_size) > min_size_delta) { + break; + } + } + + for (int i = 0; i < num_blocks; ++i) { + max_block_error[i] += block_weight[i] * val_threshold * direction; + } + + ++stats_->counters[kNumItersCnt]; + ++stats_->counters[direction > 0 ? kNumItersUpCnt : kNumItersDownCnt]; + JPEGData jpg_out = jpg; + img->SaveToJpegData(&jpg_out); + std::string encoded_jpg; + OutputJpeg(jpg_out, &encoded_jpg); + GUETZLI_LOG(stats_, + "Iter %2d: %s(%d) %s Coeffs[%d/%zd] " + "Blocks[%zd/%d/%d] ValThres[%.4f] Out[%7zd] EstErr[%.2f%%]", + stats_->counters[kNumItersCnt], img->FrameTypeStr().c_str(), + comp_mask, direction > 0 ? "up" : "down", changed_coeffs, + global_order.size(), changed_blocks.size(), + blocks_to_change, num_blocks, val_threshold, + encoded_jpg.size(), + 100.0 - (100.0 * est_jpg_size) / encoded_jpg.size()); + comparator_->Compare(*img); + MaybeOutput(encoded_jpg); + distmap = comparator_->distmap(); + prev_size = est_jpg_size; + } + } +} + +bool IsGrayscale(const JPEGData& jpg) { + for (int c = 1; c < 3; ++c) { + const JPEGComponent& comp = jpg.components[c]; + for (size_t i = 0; i < comp.coeffs.size(); ++i) { + if (comp.coeffs[i] != 0) return false; + } + } + return true; +} + +bool Processor::ProcessJpegData(const Params& params, const JPEGData& jpg_in, + Comparator* comparator, GuetzliOutput* out, + ProcessStats* stats) { + params_ = params; + comparator_ = comparator; + final_output_ = out; + stats_ = stats; + + if (params.butteraugli_target > 2.0f) { + fprintf(stderr, + "Guetzli should be called with quality >= 84, otherwise the\n" + "output will have noticeable artifacts. If you want to\n" + "proceed anyway, please edit the source code.\n"); + return false; + } + if (jpg_in.components.size() != 3 || !HasYCbCrColorSpace(jpg_in)) { + fprintf(stderr, "Only YUV color space input jpeg is supported\n"); + return false; + } + bool input_is_420; + if (jpg_in.Is444()) { + input_is_420 = false; + } else if (jpg_in.Is420()) { + input_is_420 = true; + } else { + fprintf(stderr, "Unsupported sampling factors:"); + for (int i = 0; i < jpg_in.components.size(); ++i) { + fprintf(stderr, " %dx%d", jpg_in.components[i].h_samp_factor, + jpg_in.components[i].v_samp_factor); + } + fprintf(stderr, "\n"); + return false; + } + JPEGData jpg = jpg_in; + int q_in[3][kDCTBlockSize]; + // Output the original image, in case we do not manage to create anything + // with a good enough quality. + std::string encoded_jpg; + OutputJpeg(jpg, &encoded_jpg); + final_output_->score = -1; + GUETZLI_LOG(stats, "Original Out[%7zd]", encoded_jpg.size()); + if (comparator_ == nullptr) { + GUETZLI_LOG(stats, " \n"); + final_output_->jpeg_data = encoded_jpg; + final_output_->distmap = std::vector(jpg.width * jpg.height, 0.0); + final_output_->distmap_aggregate = 0; + final_output_->score = encoded_jpg.size(); + // Butteraugli doesn't work with images this small. + return true; + } + RemoveOriginalQuantization(&jpg, q_in); + OutputImage img(jpg.width, jpg.height); + img.CopyFromJpegData(jpg); + comparator_->Compare(img); + MaybeOutput(encoded_jpg); + int try_420 = (input_is_420 || params_.force_420 || + (params_.try_420 && !IsGrayscale(jpg))) ? 1 : 0; + int force_420 = (input_is_420 || params_.force_420) ? 1 : 0; + for (int downsample = force_420; downsample <= try_420; ++downsample) { + OutputImage img(jpg.width, jpg.height); + img.CopyFromJpegData(jpg); + JPEGData tmp_jpg = jpg; + if (downsample) { + DownsampleImage(&img); + img.SaveToJpegData(&tmp_jpg); + } + int best_q[3][kDCTBlockSize]; + memcpy(best_q, q_in, sizeof(best_q)); + GuetzliOutput quantized_out; + if (!SelectQuantMatrix(tmp_jpg, downsample, best_q, &quantized_out)) { + for (int c = 0; c < 3; ++c) { + for (int i = 0; i < kDCTBlockSize; ++i) { + best_q[c][i] = 1; + } + } + } + img.ApplyGlobalQuantization(best_q); + + if (!downsample) { + SelectFrequencyMasking(tmp_jpg, &img, 7, 1.0, false); + } else { + const float ymul = tmp_jpg.components.size() == 1 ? 1.0 : 0.97; + SelectFrequencyMasking(tmp_jpg, &img, 1, ymul, false); + SelectFrequencyMasking(tmp_jpg, &img, 6, 1.0, true); + } + } + + return true; +} + +bool ProcessJpegData(const Params& params, const JPEGData& jpg_in, + Comparator* comparator, GuetzliOutput* out, + ProcessStats* stats) { + Processor processor; + return processor.ProcessJpegData(params, jpg_in, comparator, out, stats); +} + +bool Process(const Params& params, ProcessStats* stats, + const std::string& data, + std::string* jpg_out) { + JPEGData jpg; + if (!ReadJpeg(data, JPEG_READ_ALL, &jpg)) { + fprintf(stderr, "Can't read jpg data from input file\n"); + return false; + } + std::vector rgb = DecodeJpegToRGB(jpg); + if (rgb.empty()) { + fprintf(stderr, "Unsupported input JPEG file (e.g. unsupported " + "downsampling mode).\nPlease provide the input image as " + "a PNG file.\n"); + return false; + } + GuetzliOutput out; + ProcessStats dummy_stats; + if (stats == nullptr) { + stats = &dummy_stats; + } + std::unique_ptr comparator; + if (jpg.width >= 32 && jpg.height >= 32) { + comparator.reset( + new ButteraugliComparator(jpg.width, jpg.height, rgb, + params.butteraugli_target, stats)); + } + bool ok = ProcessJpegData(params, jpg, comparator.get(), &out, stats); + *jpg_out = out.jpeg_data; + return ok; +} + +bool Process(const Params& params, ProcessStats* stats, + const std::vector& rgb, int w, int h, + std::string* jpg_out) { + JPEGData jpg; + if (!EncodeRGBToJpeg(rgb, w, h, &jpg)) { + fprintf(stderr, "Could not create jpg data from rgb pixels\n"); + return false; + } + GuetzliOutput out; + ProcessStats dummy_stats; + if (stats == nullptr) { + stats = &dummy_stats; + } + std::unique_ptr comparator; + if (jpg.width >= 32 && jpg.height >= 32) { + comparator.reset( + new ButteraugliComparator(jpg.width, jpg.height, rgb, + params.butteraugli_target, stats)); + } + bool ok = ProcessJpegData(params, jpg, comparator.get(), &out, stats); + *jpg_out = out.jpeg_data; + return ok; +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/processor.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/processor.h new file mode 100644 index 0000000..c2beb7e --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/processor.h @@ -0,0 +1,62 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_PROCESSOR_H_ +#define GUETZLI_PROCESSOR_H_ + +#include +#include + +#include "guetzli/comparator.h" +#include "guetzli/jpeg_data.h" +#include "guetzli/stats.h" + +namespace guetzli { + +struct Params { + float butteraugli_target = 1.0; + bool clear_metadata = true; + bool try_420 = false; + bool force_420 = false; + bool use_silver_screen = false; + int zeroing_greedy_lookahead = 3; + bool new_zeroing_model = true; +}; + +bool Process(const Params& params, ProcessStats* stats, + const std::string& in_data, + std::string* out_data); + +struct GuetzliOutput { + std::string jpeg_data; + std::vector distmap; + double distmap_aggregate; + double score; +}; + +bool ProcessJpegData(const Params& params, const JPEGData& jpg_in, + Comparator* comparator, GuetzliOutput* out, + ProcessStats* stats); + +// Sets *out to a jpeg encoded string that will decode to an image that is +// visually indistinguishable from the input rgb image. +bool Process(const Params& params, ProcessStats* stats, + const std::vector& rgb, int w, int h, + std::string* out); + +} // namespace guetzli + +#endif // GUETZLI_PROCESSOR_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/quality.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/quality.cc new file mode 100644 index 0000000..681c0cb --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/quality.cc @@ -0,0 +1,87 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/quality.h" + +namespace guetzli { + +namespace { + +constexpr int kLowestQuality = 70; +constexpr int kHighestQuality = 110; + +// Butteraugli scores that correspond to JPEG quality levels, starting at +// kLowestQuality. They were computed by taking median BA scores of JPEGs +// generated using libjpeg-turbo at given quality from a set of PNGs. +// The scores above quality level 100 are just linearly decreased so that score +// for 110 is 90% of the score for 100. +const double kScoreForQuality[] = { + 2.810761, // 70 + 2.729300, + 2.689687, + 2.636811, + 2.547863, + 2.525400, + 2.473416, + 2.366133, + 2.338078, + 2.318654, + 2.201674, // 80 + 2.145517, + 2.087322, + 2.009328, + 1.945456, + 1.900112, + 1.805701, + 1.750194, + 1.644175, + 1.562165, + 1.473608, // 90 + 1.382021, + 1.294298, + 1.185402, + 1.066781, + 0.971769, // 95 + 0.852901, + 0.724544, + 0.611302, + 0.443185, + 0.211578, // 100 + 0.209462, + 0.207346, + 0.205230, + 0.203114, + 0.200999, // 105 + 0.198883, + 0.196767, + 0.194651, + 0.192535, + 0.190420, // 110 + 0.190420, +}; + +} // namespace + +double ButteraugliScoreForQuality(double quality) { + if (quality < kLowestQuality) quality = kLowestQuality; + if (quality > kHighestQuality) quality = kHighestQuality; + int index = static_cast(quality); + double mix = quality - index; + return kScoreForQuality[index - kLowestQuality] * (1 - mix) + + kScoreForQuality[index - kLowestQuality + 1] * mix; +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/quality.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/quality.h new file mode 100644 index 0000000..a7ede82 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/quality.h @@ -0,0 +1,26 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_QUALITY_H_ +#define GUETZLI_QUALITY_H_ + +namespace guetzli { + +double ButteraugliScoreForQuality(double quality); + +} // namespace guetzli + +#endif // GUETZLI_QUALITY_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/quantize.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/quantize.cc new file mode 100644 index 0000000..f8c25ac --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/quantize.cc @@ -0,0 +1,32 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/quantize.h" + +namespace guetzli { + +bool QuantizeBlock(coeff_t block[kDCTBlockSize], + const int q[kDCTBlockSize]) { + bool changed = false; + for (int k = 0; k < kDCTBlockSize; ++k) { + coeff_t coeff = Quantize(block[k], q[k]); + changed = changed || (coeff != block[k]); + block[k] = coeff; + } + return changed; +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/quantize.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/quantize.h new file mode 100644 index 0000000..2e9a189 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/quantize.h @@ -0,0 +1,35 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_QUANTIZE_H_ +#define GUETZLI_QUANTIZE_H_ + +#include "guetzli/jpeg_data.h" + +namespace guetzli { + +inline coeff_t Quantize(coeff_t raw_coeff, int quant) { + const int r = raw_coeff % quant; + const coeff_t delta = + 2 * r > quant ? quant - r : (-2) * r > quant ? -quant - r : -r; + return raw_coeff + delta; +} + +bool QuantizeBlock(coeff_t block[kDCTBlockSize], const int q[kDCTBlockSize]); + +} // namespace guetzli + +#endif // GUETZLI_QUANTIZE_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/score.cc b/packages/optimizt-guetzli/upstream/guetzli/guetzli/score.cc new file mode 100644 index 0000000..542cfc6 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/score.cc @@ -0,0 +1,43 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "guetzli/score.h" + +#include + +namespace guetzli { + +double ScoreJPEG(double butteraugli_distance, int size, + double butteraugli_target) { + constexpr double kScale = 50; + constexpr double kMaxExponent = 10; + constexpr double kLargeSize = 1e30; + // TODO(user): The score should also depend on distance below target (and be + // smooth). + double diff = butteraugli_distance - butteraugli_target; + if (diff <= 0.0) { + return size; + } else { + double exponent = kScale * diff; + if (exponent > kMaxExponent) { + return kLargeSize * std::exp(kMaxExponent) * diff + size; + } else { + return std::exp(exponent) * size; + } + } +} + +} // namespace guetzli diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/score.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/score.h new file mode 100644 index 0000000..e4207a9 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/score.h @@ -0,0 +1,28 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_SCORE_H_ +#define GUETZLI_SCORE_H_ + +#include + +namespace guetzli { + +double ScoreJPEG(double butteraugli_distance, int size, + double butteraugli_target); + +} // namespace guetzli +#endif // GUETZLI_SCORE_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/guetzli/stats.h b/packages/optimizt-guetzli/upstream/guetzli/guetzli/stats.h new file mode 100644 index 0000000..8d2565d --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/guetzli/stats.h @@ -0,0 +1,44 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef GUETZLI_STATS_H_ +#define GUETZLI_STATS_H_ + +#include +#include +#include +#include +#include + + +namespace guetzli { + +static const char* const kNumItersCnt = "number of iterations"; +static const char* const kNumItersUpCnt = "number of iterations up"; +static const char* const kNumItersDownCnt = "number of iterations down"; + +struct ProcessStats { + ProcessStats() {} + std::map counters; + std::string* debug_output = nullptr; + FILE* debug_output_file = nullptr; + + std::string filename; +}; + +} // namespace guetzli + +#endif // GUETZLI_STATS_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/png.BUILD b/packages/optimizt-guetzli/upstream/guetzli/png.BUILD new file mode 100644 index 0000000..9ff982b --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/png.BUILD @@ -0,0 +1,33 @@ +# Description: +# libpng is the official PNG reference library. + +licenses(["notice"]) # BSD/MIT-like license + +cc_library( + name = "png", + srcs = [ + "png.c", + "pngerror.c", + "pngget.c", + "pngmem.c", + "pngpread.c", + "pngread.c", + "pngrio.c", + "pngrtran.c", + "pngrutil.c", + "pngset.c", + "pngtrans.c", + "pngwio.c", + "pngwrite.c", + "pngwtran.c", + "pngwutil.c", + ], + hdrs = [ + "png.h", + "pngconf.h", + ], + includes = ["."], + linkopts = ["-lm"], + visibility = ["//visibility:public"], + deps = ["@zlib_archive//:zlib"], +) diff --git a/packages/optimizt-guetzli/upstream/guetzli/premake5.lua b/packages/optimizt-guetzli/upstream/guetzli/premake5.lua new file mode 100644 index 0000000..9fd79e0 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/premake5.lua @@ -0,0 +1,38 @@ +workspace "guetzli" + configurations { "Release", "Debug" } + filter "action:vs*" + platforms { "x86_64", "x86" } + + flags { "C++11" } + + -- workaround for #41 + filter "action:gmake" + symbols "On" + + filter "configurations:Debug" + symbols "On" + filter "configurations:Release" + optimize "Full" + + project "guetzli" + kind "ConsoleApp" + language "C++" + flags "C++11" + includedirs { ".", "third_party/butteraugli" } + filter "action:gmake" + linkoptions { "`pkg-config --libs libpng`" } + buildoptions { "`pkg-config --cflags libpng`" } + filter "action:vs*" + links { "shlwapi" } + filter "platforms:x86" + architecture "x86" + filter "platforms:x86_64" + architecture "x86_64" + filter {} + files + { + "guetzli/*.cc", + "guetzli/*.h", + "third_party/butteraugli/butteraugli/butteraugli.cc", + "third_party/butteraugli/butteraugli/butteraugli.h" + } diff --git a/packages/optimizt-guetzli/upstream/guetzli/snapcraft.yaml b/packages/optimizt-guetzli/upstream/guetzli/snapcraft.yaml new file mode 100644 index 0000000..ccd1b18 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/snapcraft.yaml @@ -0,0 +1,22 @@ +name: guetzli +version: master +summary: JPEG encoder for excellent compression density at high visual quality +description: | + Guetzli-generated images are typically 20-30% smaller than images of + equivalent quality generated by libjpeg. Guetzli generates only sequential + (nonprogressive) JPEGs due to faster decompression speeds they offer. + +grade: devel +confinement: strict + +apps: + guetzli: + command: bin/Release/guetzli + plugs: [home] + +parts: + guetzli: + source: . + plugin: make + build-packages: [libpng12-dev, libgflags-dev] + artifacts: [bin/Release/guetzli] diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/BUILD b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/BUILD new file mode 100644 index 0000000..5695064 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/BUILD @@ -0,0 +1,24 @@ +cc_library( + name = "butteraugli_lib", + srcs = [ + "butteraugli/butteraugli.cc", + "butteraugli/butteraugli.h", + ], + hdrs = [ + "butteraugli/butteraugli.h", + ], + copts = ["-Wno-sign-compare"], + visibility = ["//visibility:public"], +) + +cc_binary( + name = "butteraugli", + srcs = ["butteraugli/butteraugli_main.cc"], + copts = ["-Wno-sign-compare"], + visibility = ["//visibility:public"], + deps = [ + ":butteraugli_lib", + "@jpeg_archive//:jpeg", + "@png_archive//:png", + ], +) diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/LICENSE b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/README.md b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/README.md new file mode 100644 index 0000000..126fa5f --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/README.md @@ -0,0 +1,68 @@ +# butteraugli + +> A tool for measuring perceived differences between images + +## Introduction + +Butteraugli is a project that estimates the psychovisual similarity of two +images. It gives a score for the images that is reliable in the domain of barely +noticeable differences. Butteraugli not only gives a scalar score, but also +computes a spatial map of the level of differences. + +One of the main motivations for this project is the statistical differences in +location and density of different color receptors, particularly the low density +of blue cones in the fovea. Another motivation comes from more accurate modeling +of ganglion cells, particularly the frequency space inhibition. + +## Use + +Butteraugli can work as a quality metric for lossy image and video compression. +On our small test corpus butteraugli performs better than our implementations of +the reference methods, psnrhsv-m, ssim, and our yuv-color-space variant of ssim. +One possible use is to define the quality level setting used in a jpeg +compressor, or to compare two or more compression methods at the same level of +psychovisual differences. + +Butteraugli is intended to be a research tool more than a practical tool for +choosing compression formats. We don't know how well butteraugli performs with +major deformations -- we have mostly tuned it within a small range of quality, +roughly corresponding to jpeg qualities 90 to 95. + +## Interface + +Only a C++ interface is provided. The interface takes two images and outputs a +map together with a scalar value defining the difference. The scalar value can +be compared to two reference values that divide the value space into three +experience classes: 'great', 'acceptable' and 'not acceptable'. + +## Build instructions + +Install [Bazel](http://bazel.io) by following the +[instructions](https://www.bazel.io/docs/install.html). Run `bazel build -c opt +//:butteraugli` in the directory that contains this README file to build the +[command-line utility](#cmdline-tool). If you want to use Butteraugli as a +library, depend on the `//:butteraugli_lib` target. + +Alternatively, you can use the Makefile provided in the `butteraugli` directory, +after ensuring that [libpng](http://www.libpng.org/) and +[libjpeg](http://ijg.org/) are installed. On some systems you might need to also +install corresponding `-dev` packages. + +The code is portable and also compiles on Windows after defining +`_CRT_SECURE_NO_WARNINGS` in the project settings. + +## Command-line utility {#cmdline-tool} + +Butteraugli, apart from the library, comes bundled with a comparison tool. The +comparison tool supports PNG and JPG images as inputs. To compare images, run: + +``` +butteraugli image1.{png|jpg} image2.{png|jpg} +``` + +The tool can also produce a heatmap of differences between images. The heatmap +will be output as a PNM image. To produce one, run: + +``` +butteraugli image1.{png|jpg} image2.{png|jpg} heatmap.pnm +``` diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/WORKSPACE b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/WORKSPACE new file mode 100644 index 0000000..530c2ac --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/WORKSPACE @@ -0,0 +1,25 @@ +workspace(name = "butteraugli") + +new_http_archive( + name = "png_archive", + url = "http://github.com/glennrp/libpng/archive/v1.2.53.zip", + sha256 = "c35bcc6387495ee6e757507a68ba036d38ad05b415c2553b3debe2a57647a692", + strip_prefix = "libpng-1.2.53", + build_file = "png.BUILD", +) + +new_http_archive( + name = "zlib_archive", + url = "http://zlib.net/zlib-1.2.8.tar.gz", + sha256 = "36658cb768a54c1d4dec43c3116c27ed893e88b02ecfcb44f2166f9c0b7f2a0d", + strip_prefix = "zlib-1.2.8", + build_file = "zlib.BUILD", +) + +new_http_archive( + name = "jpeg_archive", + url = "http://www.ijg.org/files/jpegsrc.v9a.tar.gz", + sha256 = "3a753ea48d917945dd54a2d97de388aa06ca2eb1066cbfdc6652036349fe05a7", + strip_prefix = "jpeg-9a", + build_file = "jpeg.BUILD", +) diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/Makefile b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/Makefile new file mode 100644 index 0000000..76b3a9b --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/Makefile @@ -0,0 +1,7 @@ +LDLIBS += -lpng -ljpeg +CXXFLAGS += -std=c++11 -I.. +LINK.o = $(LINK.cc) + +all: butteraugli.o butteraugli_main.o butteraugli + +butteraugli: butteraugli.o butteraugli_main.o diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc new file mode 100644 index 0000000..1401fed --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc @@ -0,0 +1,1591 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) +// +// The physical architecture of butteraugli is based on the following naming +// convention: +// * Opsin - dynamics of the photosensitive chemicals in the retina +// with their immediate electrical processing +// * Xyb - hybrid opponent/trichromatic color space +// x is roughly red-subtract-green. +// y is yellow. +// b is blue. +// Xyb values are computed from Opsin mixing, not directly from rgb. +// * Mask - for visual masking +// * Hf - color modeling for spatially high-frequency features +// * Lf - color modeling for spatially low-frequency features +// * Diffmap - to cluster and build an image of error between the images +// * Blur - to hold the smoothing code + +#include "butteraugli/butteraugli.h" + +#include +#include +#include +#include +#include + +#include +#include + +// Restricted pointers speed up Convolution(); MSVC uses a different keyword. +#ifdef _MSC_VER +#define __restrict__ __restrict +#endif + +namespace butteraugli { + +void *CacheAligned::Allocate(const size_t bytes) { + char *const allocated = static_cast(malloc(bytes + kCacheLineSize)); + if (allocated == nullptr) { + return nullptr; + } + const uintptr_t misalignment = + reinterpret_cast(allocated) & (kCacheLineSize - 1); + // malloc is at least kPointerSize aligned, so we can store the "allocated" + // pointer immediately before the aligned memory. + assert(misalignment % kPointerSize == 0); + char *const aligned = allocated + kCacheLineSize - misalignment; + memcpy(aligned - kPointerSize, &allocated, kPointerSize); + return aligned; +} + +void CacheAligned::Free(void *aligned_pointer) { + if (aligned_pointer == nullptr) { + return; + } + char *const aligned = static_cast(aligned_pointer); + assert(reinterpret_cast(aligned) % kCacheLineSize == 0); + char *allocated; + memcpy(&allocated, aligned - kPointerSize, kPointerSize); + assert(allocated <= aligned - kPointerSize); + assert(allocated >= aligned - kCacheLineSize); + free(allocated); +} + +static inline bool IsNan(const float x) { + uint32_t bits; + memcpy(&bits, &x, sizeof(bits)); + const uint32_t bitmask_exp = 0x7F800000; + return (bits & bitmask_exp) == bitmask_exp && (bits & 0x7FFFFF); +} + +static inline bool IsNan(const double x) { + uint64_t bits; + memcpy(&bits, &x, sizeof(bits)); + return (0x7ff0000000000001ULL <= bits && bits <= 0x7fffffffffffffffULL) || + (0xfff0000000000001ULL <= bits && bits <= 0xffffffffffffffffULL); +} + +static inline void CheckImage(const ImageF &image, const char *name) { + for (size_t y = 0; y < image.ysize(); ++y) { + ConstRestrict row = image.Row(y); + for (size_t x = 0; x < image.xsize(); ++x) { + if (IsNan(row[x])) { + printf("Image %s @ %lu,%lu (of %lu,%lu)\n", name, x, y, image.xsize(), + image.ysize()); + exit(1); + } + } + } +} + +#if BUTTERAUGLI_ENABLE_CHECKS + +#define CHECK_NAN(x, str) \ + do { \ + if (IsNan(x)) { \ + printf("%d: %s\n", __LINE__, str); \ + abort(); \ + } \ + } while (0) + +#define CHECK_IMAGE(image, name) CheckImage(image, name) + +#else + +#define CHECK_NAN(x, str) +#define CHECK_IMAGE(image, name) + +#endif + + +static const double kInternalGoodQualityThreshold = 14.921561160295326; +static const double kGlobalScale = 1.0 / kInternalGoodQualityThreshold; + +inline double DotProduct(const double u[3], const double v[3]) { + return u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; +} + +inline double DotProduct(const float u[3], const double v[3]) { + return u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; +} + +// Computes a horizontal convolution and transposes the result. +static void Convolution(size_t xsize, size_t ysize, + size_t xstep, + size_t len, size_t offset, + const float* __restrict__ multipliers, + const float* __restrict__ inp, + double border_ratio, + float* __restrict__ result) { + PROFILER_FUNC; + double weight_no_border = 0; + for (int j = 0; j <= 2 * offset; ++j) { + weight_no_border += multipliers[j]; + } + for (size_t x = 0, ox = 0; x < xsize; x += xstep, ox++) { + int minx = x < offset ? 0 : x - offset; + int maxx = std::min(xsize, x + len - offset) - 1; + double weight = 0.0; + for (int j = minx; j <= maxx; ++j) { + weight += multipliers[j - x + offset]; + } + // Interpolate linearly between the no-border scaling and border scaling. + weight = (1.0 - border_ratio) * weight + border_ratio * weight_no_border; + double scale = 1.0 / weight; + for (size_t y = 0; y < ysize; ++y) { + double sum = 0.0; + for (int j = minx; j <= maxx; ++j) { + sum += inp[y * xsize + j] * multipliers[j - x + offset]; + } + result[ox * ysize + y] = sum * scale; + } + } +} + +void Blur(size_t xsize, size_t ysize, float* channel, double sigma, + double border_ratio) { + PROFILER_FUNC; + double m = 2.25; // Accuracy increases when m is increased. + const double scaler = -1.0 / (2 * sigma * sigma); + // For m = 9.0: exp(-scaler * diff * diff) < 2^ {-52} + const int diff = std::max(1, m * fabs(sigma)); + const int expn_size = 2 * diff + 1; + std::vector expn(expn_size); + for (int i = -diff; i <= diff; ++i) { + expn[i + diff] = exp(scaler * i * i); + } + const int xstep = std::max(1, int(sigma / 3)); + const int ystep = xstep; + int dxsize = (xsize + xstep - 1) / xstep; + int dysize = (ysize + ystep - 1) / ystep; + std::vector tmp(dxsize * ysize); + std::vector downsampled_output(dxsize * dysize); + Convolution(xsize, ysize, xstep, expn_size, diff, expn.data(), channel, + border_ratio, + tmp.data()); + Convolution(ysize, dxsize, ystep, expn_size, diff, expn.data(), tmp.data(), + border_ratio, + downsampled_output.data()); + for (int y = 0; y < ysize; y++) { + for (int x = 0; x < xsize; x++) { + // TODO: Use correct rounding. + channel[y * xsize + x] = + downsampled_output[(y / ystep) * dxsize + (x / xstep)]; + } + } +} + +// To change this to n, add the relevant FFTn function and kFFTnMapIndexTable. +constexpr size_t kBlockEdge = 8; +constexpr size_t kBlockSize = kBlockEdge * kBlockEdge; +constexpr size_t kBlockEdgeHalf = kBlockEdge / 2; +constexpr size_t kBlockHalf = kBlockEdge * kBlockEdgeHalf; + +// Contrast sensitivity related weights. +static const double *GetContrastSensitivityMatrix() { + static double csf8x8[kBlockHalf + kBlockEdgeHalf + 1] = { + 5.28270670524, + 0.0, + 0.0, + 0.0, + 0.3831134973, + 0.676303603859, + 3.58927792424, + 18.6104367002, + 18.6104367002, + 3.09093131948, + 1.0, + 0.498250875965, + 0.36198671102, + 0.308982169883, + 0.1312701920435, + 2.37370549629, + 3.58927792424, + 1.0, + 2.37370549629, + 0.991205724152, + 1.05178802919, + 0.627264168628, + 0.4, + 0.1312701920435, + 0.676303603859, + 0.498250875965, + 0.991205724152, + 0.5, + 0.3831134973, + 0.349686450518, + 0.627264168628, + 0.308982169883, + 0.3831134973, + 0.36198671102, + 1.05178802919, + 0.3831134973, + 0.12, + }; + return &csf8x8[0]; +} + +std::array MakeHighFreqColorDiffDx() { + std::array lut; + static const double off = 11.38708334481672; + static const double inc = 14.550189611520716; + lut[0] = 0.0; + lut[1] = off; + for (int i = 2; i < 21; ++i) { + lut[i] = lut[i - 1] + inc; + } + return lut; +} + +const double *GetHighFreqColorDiffDx() { + static const std::array kLut = MakeHighFreqColorDiffDx(); + return kLut.data(); +} + +std::array MakeHighFreqColorDiffDy() { + std::array lut; + static const double off = 1.4103373714040413; + static const double inc = 0.7084088867024; + lut[0] = 0.0; + lut[1] = off; + for (int i = 2; i < 21; ++i) { + lut[i] = lut[i - 1] + inc; + } + return lut; +} + +const double *GetHighFreqColorDiffDy() { + static const std::array kLut = MakeHighFreqColorDiffDy(); + return kLut.data(); +} + +std::array MakeLowFreqColorDiffDy() { + std::array lut; + static const double inc = 5.2511644570349185; + lut[0] = 0.0; + for (int i = 1; i < 21; ++i) { + lut[i] = lut[i - 1] + inc; + } + return lut; +} + +const double *GetLowFreqColorDiffDy() { + static const std::array kLut = MakeLowFreqColorDiffDy(); + return kLut.data(); +} + +inline double Interpolate(const double *array, int size, double sx) { + double ix = fabs(sx); + assert(ix < 10000); + int baseix = static_cast(ix); + double res; + if (baseix >= size - 1) { + res = array[size - 1]; + } else { + double mix = ix - baseix; + int nextix = baseix + 1; + res = array[baseix] + mix * (array[nextix] - array[baseix]); + } + if (sx < 0) res = -res; + return res; +} + +inline double InterpolateClampNegative(const double *array, + int size, double sx) { + if (sx < 0) { + sx = 0; + } + double ix = fabs(sx); + int baseix = static_cast(ix); + double res; + if (baseix >= size - 1) { + res = array[size - 1]; + } else { + double mix = ix - baseix; + int nextix = baseix + 1; + res = array[baseix] + mix * (array[nextix] - array[baseix]); + } + return res; +} + +void RgbToXyb(double r, double g, double b, + double *valx, double *valy, double *valz) { + static const double a0 = 1.01611726948; + static const double a1 = 0.982482243696; + static const double a2 = 1.43571362627; + static const double a3 = 0.896039849412; + *valx = a0 * r - a1 * g; + *valy = a2 * r + a3 * g; + *valz = b; +} + +static inline void XybToVals(double x, double y, double z, + double *valx, double *valy, double *valz) { + static const double xmul = 0.758304045695; + static const double ymul = 2.28148649801; + static const double zmul = 1.87816926918; + *valx = Interpolate(GetHighFreqColorDiffDx(), 21, x * xmul); + *valy = Interpolate(GetHighFreqColorDiffDy(), 21, y * ymul); + *valz = zmul * z; +} + +// Rough psychovisual distance to gray for low frequency colors. +static void XybLowFreqToVals(double x, double y, double z, + double *valx, double *valy, double *valz) { + static const double xmul = 6.64482198135; + static const double ymul = 0.837846224276; + static const double zmul = 7.34905756986; + static const double y_to_z_mul = 0.0812519812628; + z += y_to_z_mul * y; + *valz = z * zmul; + *valx = x * xmul; + *valy = Interpolate(GetLowFreqColorDiffDy(), 21, y * ymul); +} + +double RemoveRangeAroundZero(double v, double range) { + if (v >= -range && v < range) { + return 0; + } + if (v < 0) { + return v + range; + } else { + return v - range; + } +} + +void XybDiffLowFreqSquaredAccumulate(double r0, double g0, double b0, + double r1, double g1, double b1, + double factor, double res[3]) { + double valx0, valy0, valz0; + double valx1, valy1, valz1; + XybLowFreqToVals(r0, g0, b0, &valx0, &valy0, &valz0); + if (r1 == 0.0 && g1 == 0.0 && b1 == 0.0) { + PROFILER_ZONE("XybDiff r1=g1=b1=0"); + res[0] += factor * valx0 * valx0; + res[1] += factor * valy0 * valy0; + res[2] += factor * valz0 * valz0; + return; + } + XybLowFreqToVals(r1, g1, b1, &valx1, &valy1, &valz1); + // Approximate the distance of the colors by their respective distances + // to gray. + double valx = valx0 - valx1; + double valy = valy0 - valy1; + double valz = valz0 - valz1; + res[0] += factor * valx * valx; + res[1] += factor * valy * valy; + res[2] += factor * valz * valz; +} + +struct Complex { + public: + double real; + double imag; +}; + +inline double abssq(const Complex& c) { + return c.real * c.real + c.imag * c.imag; +} + +static void TransposeBlock(Complex data[kBlockSize]) { + for (int i = 0; i < kBlockEdge; i++) { + for (int j = 0; j < i; j++) { + std::swap(data[kBlockEdge * i + j], data[kBlockEdge * j + i]); + } + } +} + +// D. J. Bernstein's Fast Fourier Transform algorithm on 4 elements. +inline void FFT4(Complex* a) { + double t1, t2, t3, t4, t5, t6, t7, t8; + t5 = a[2].real; + t1 = a[0].real - t5; + t7 = a[3].real; + t5 += a[0].real; + t3 = a[1].real - t7; + t7 += a[1].real; + t8 = t5 + t7; + a[0].real = t8; + t5 -= t7; + a[1].real = t5; + t6 = a[2].imag; + t2 = a[0].imag - t6; + t6 += a[0].imag; + t5 = a[3].imag; + a[2].imag = t2 + t3; + t2 -= t3; + a[3].imag = t2; + t4 = a[1].imag - t5; + a[3].real = t1 + t4; + t1 -= t4; + a[2].real = t1; + t5 += a[1].imag; + a[0].imag = t6 + t5; + t6 -= t5; + a[1].imag = t6; +} + +static const double kSqrtHalf = 0.70710678118654752440084436210484903; + +// D. J. Bernstein's Fast Fourier Transform algorithm on 8 elements. +void FFT8(Complex* a) { + double t1, t2, t3, t4, t5, t6, t7, t8; + + t7 = a[4].imag; + t4 = a[0].imag - t7; + t7 += a[0].imag; + a[0].imag = t7; + + t8 = a[6].real; + t5 = a[2].real - t8; + t8 += a[2].real; + a[2].real = t8; + + t7 = a[6].imag; + a[6].imag = t4 - t5; + t4 += t5; + a[4].imag = t4; + + t6 = a[2].imag - t7; + t7 += a[2].imag; + a[2].imag = t7; + + t8 = a[4].real; + t3 = a[0].real - t8; + t8 += a[0].real; + a[0].real = t8; + + a[4].real = t3 - t6; + t3 += t6; + a[6].real = t3; + + t7 = a[5].real; + t3 = a[1].real - t7; + t7 += a[1].real; + a[1].real = t7; + + t8 = a[7].imag; + t6 = a[3].imag - t8; + t8 += a[3].imag; + a[3].imag = t8; + t1 = t3 - t6; + t3 += t6; + + t7 = a[5].imag; + t4 = a[1].imag - t7; + t7 += a[1].imag; + a[1].imag = t7; + + t8 = a[7].real; + t5 = a[3].real - t8; + t8 += a[3].real; + a[3].real = t8; + + t2 = t4 - t5; + t4 += t5; + + t6 = t1 - t4; + t8 = kSqrtHalf; + t6 *= t8; + a[5].real = a[4].real - t6; + t1 += t4; + t1 *= t8; + a[5].imag = a[4].imag - t1; + t6 += a[4].real; + a[4].real = t6; + t1 += a[4].imag; + a[4].imag = t1; + + t5 = t2 - t3; + t5 *= t8; + a[7].imag = a[6].imag - t5; + t2 += t3; + t2 *= t8; + a[7].real = a[6].real - t2; + t2 += a[6].real; + a[6].real = t2; + t5 += a[6].imag; + a[6].imag = t5; + + FFT4(a); + + // Reorder to the correct output order. + // TODO: Modify the above computation so that this is not needed. + Complex tmp = a[2]; + a[2] = a[3]; + a[3] = a[5]; + a[5] = a[7]; + a[7] = a[4]; + a[4] = a[1]; + a[1] = a[6]; + a[6] = tmp; +} + +// Same as FFT8, but all inputs are real. +// TODO: Since this does not need to be in-place, maybe there is a +// faster FFT than this one, which is derived from DJB's in-place complex FFT. +void RealFFT8(const double* in, Complex* out) { + double t1, t2, t3, t5, t6, t7, t8; + t8 = in[6]; + t5 = in[2] - t8; + t8 += in[2]; + out[2].real = t8; + out[6].imag = -t5; + out[4].imag = t5; + t8 = in[4]; + t3 = in[0] - t8; + t8 += in[0]; + out[0].real = t8; + out[4].real = t3; + out[6].real = t3; + t7 = in[5]; + t3 = in[1] - t7; + t7 += in[1]; + out[1].real = t7; + t8 = in[7]; + t5 = in[3] - t8; + t8 += in[3]; + out[3].real = t8; + t2 = -t5; + t6 = t3 - t5; + t8 = kSqrtHalf; + t6 *= t8; + out[5].real = out[4].real - t6; + t1 = t3 + t5; + t1 *= t8; + out[5].imag = out[4].imag - t1; + t6 += out[4].real; + out[4].real = t6; + t1 += out[4].imag; + out[4].imag = t1; + t5 = t2 - t3; + t5 *= t8; + out[7].imag = out[6].imag - t5; + t2 += t3; + t2 *= t8; + out[7].real = out[6].real - t2; + t2 += out[6].real; + out[6].real = t2; + t5 += out[6].imag; + out[6].imag = t5; + t5 = out[2].real; + t1 = out[0].real - t5; + t7 = out[3].real; + t5 += out[0].real; + t3 = out[1].real - t7; + t7 += out[1].real; + t8 = t5 + t7; + out[0].real = t8; + t5 -= t7; + out[1].real = t5; + out[2].imag = t3; + out[3].imag = -t3; + out[3].real = t1; + out[2].real = t1; + out[0].imag = 0; + out[1].imag = 0; + + // Reorder to the correct output order. + // TODO: Modify the above computation so that this is not needed. + Complex tmp = out[2]; + out[2] = out[3]; + out[3] = out[5]; + out[5] = out[7]; + out[7] = out[4]; + out[4] = out[1]; + out[1] = out[6]; + out[6] = tmp; +} + +// Fills in block[kBlockEdgeHalf..(kBlockHalf+kBlockEdgeHalf)], and leaves the +// rest unmodified. +void ButteraugliFFTSquared(double block[kBlockSize]) { + double global_mul = 0.000064; + Complex block_c[kBlockSize]; + assert(kBlockEdge == 8); + for (int y = 0; y < kBlockEdge; ++y) { + RealFFT8(block + y * kBlockEdge, block_c + y * kBlockEdge); + } + TransposeBlock(block_c); + double r0[kBlockEdge]; + double r1[kBlockEdge]; + for (int x = 0; x < kBlockEdge; ++x) { + r0[x] = block_c[x].real; + r1[x] = block_c[kBlockHalf + x].real; + } + RealFFT8(r0, block_c); + RealFFT8(r1, block_c + kBlockHalf); + for (int y = 1; y < kBlockEdgeHalf; ++y) { + FFT8(block_c + y * kBlockEdge); + } + for (int i = kBlockEdgeHalf; i < kBlockHalf + kBlockEdgeHalf + 1; ++i) { + block[i] = abssq(block_c[i]); + block[i] *= global_mul; + } +} + +// Computes 8x8 FFT of each channel of xyb0 and xyb1 and adds the total squared +// 3-dimensional xybdiff of the two blocks to diff_xyb_{dc,ac} and the average +// diff on the edges to diff_xyb_edge_dc. +void ButteraugliBlockDiff(double xyb0[3 * kBlockSize], + double xyb1[3 * kBlockSize], + double diff_xyb_dc[3], + double diff_xyb_ac[3], + double diff_xyb_edge_dc[3]) { + PROFILER_FUNC; + const double *csf8x8 = GetContrastSensitivityMatrix(); + + double avgdiff_xyb[3] = {0.0}; + double avgdiff_edge[3][4] = { {0.0} }; + for (int i = 0; i < 3 * kBlockSize; ++i) { + const double diff_xyb = xyb0[i] - xyb1[i]; + const int c = i / kBlockSize; + avgdiff_xyb[c] += diff_xyb / kBlockSize; + const int k = i % kBlockSize; + const int kx = k % kBlockEdge; + const int ky = k / kBlockEdge; + const int h_edge_idx = ky == 0 ? 1 : ky == 7 ? 3 : -1; + const int v_edge_idx = kx == 0 ? 0 : kx == 7 ? 2 : -1; + if (h_edge_idx >= 0) { + avgdiff_edge[c][h_edge_idx] += diff_xyb / kBlockEdge; + } + if (v_edge_idx >= 0) { + avgdiff_edge[c][v_edge_idx] += diff_xyb / kBlockEdge; + } + } + XybDiffLowFreqSquaredAccumulate(avgdiff_xyb[0], + avgdiff_xyb[1], + avgdiff_xyb[2], + 0, 0, 0, csf8x8[0], + diff_xyb_dc); + for (int i = 0; i < 4; ++i) { + XybDiffLowFreqSquaredAccumulate(avgdiff_edge[0][i], + avgdiff_edge[1][i], + avgdiff_edge[2][i], + 0, 0, 0, csf8x8[0], + diff_xyb_edge_dc); + } + + double* xyb_avg = xyb0; + double* xyb_halfdiff = xyb1; + for(int i = 0; i < 3 * kBlockSize; ++i) { + double avg = (xyb0[i] + xyb1[i])/2; + double halfdiff = (xyb0[i] - xyb1[i])/2; + xyb_avg[i] = avg; + xyb_halfdiff[i] = halfdiff; + } + double *y_avg = &xyb_avg[kBlockSize]; + double *x_halfdiff_squared = &xyb_halfdiff[0]; + double *y_halfdiff = &xyb_halfdiff[kBlockSize]; + double *z_halfdiff_squared = &xyb_halfdiff[2 * kBlockSize]; + ButteraugliFFTSquared(y_avg); + ButteraugliFFTSquared(x_halfdiff_squared); + ButteraugliFFTSquared(y_halfdiff); + ButteraugliFFTSquared(z_halfdiff_squared); + + static const double xmul = 64.8; + static const double ymul = 1.753123908348329; + static const double ymul2 = 1.51983458269; + static const double zmul = 2.4; + + for (size_t i = kBlockEdgeHalf; i < kBlockHalf + kBlockEdgeHalf + 1; ++i) { + double d = csf8x8[i]; + diff_xyb_ac[0] += d * xmul * x_halfdiff_squared[i]; + diff_xyb_ac[2] += d * zmul * z_halfdiff_squared[i]; + + y_avg[i] = sqrt(y_avg[i]); + y_halfdiff[i] = sqrt(y_halfdiff[i]); + double y0 = y_avg[i] - y_halfdiff[i]; + double y1 = y_avg[i] + y_halfdiff[i]; + // Remove the impact of small absolute values. + // This improves the behavior with flat noise. + static const double ylimit = 0.04; + y0 = RemoveRangeAroundZero(y0, ylimit); + y1 = RemoveRangeAroundZero(y1, ylimit); + if (y0 != y1) { + double valy0 = Interpolate(GetHighFreqColorDiffDy(), 21, y0 * ymul2); + double valy1 = Interpolate(GetHighFreqColorDiffDy(), 21, y1 * ymul2); + double valy = ymul * (valy0 - valy1); + diff_xyb_ac[1] += d * valy * valy; + } + } +} + +// Low frequency edge detectors. +// Two edge detectors are applied in each corner of the 8x8 square. +// The squared 3-dimensional error vector is added to diff_xyb. +void Butteraugli8x8CornerEdgeDetectorDiff( + const size_t pos_x, + const size_t pos_y, + const size_t xsize, + const size_t ysize, + const std::vector > &blurred0, + const std::vector > &blurred1, + double diff_xyb[3]) { + PROFILER_FUNC; + int local_count = 0; + double local_xyb[3] = { 0 }; + static const double w = 0.711100840192; + for (int k = 0; k < 4; ++k) { + size_t step = 3; + size_t offset[4][2] = { { 0, 0 }, { 0, 7 }, { 7, 0 }, { 7, 7 } }; + size_t x = pos_x + offset[k][0]; + size_t y = pos_y + offset[k][1]; + if (x >= step && x + step < xsize) { + size_t ix = y * xsize + (x - step); + size_t ix2 = ix + 2 * step; + XybDiffLowFreqSquaredAccumulate( + w * (blurred0[0][ix] - blurred0[0][ix2]), + w * (blurred0[1][ix] - blurred0[1][ix2]), + w * (blurred0[2][ix] - blurred0[2][ix2]), + w * (blurred1[0][ix] - blurred1[0][ix2]), + w * (blurred1[1][ix] - blurred1[1][ix2]), + w * (blurred1[2][ix] - blurred1[2][ix2]), + 1.0, local_xyb); + ++local_count; + } + if (y >= step && y + step < ysize) { + size_t ix = (y - step) * xsize + x; + size_t ix2 = ix + 2 * step * xsize; + XybDiffLowFreqSquaredAccumulate( + w * (blurred0[0][ix] - blurred0[0][ix2]), + w * (blurred0[1][ix] - blurred0[1][ix2]), + w * (blurred0[2][ix] - blurred0[2][ix2]), + w * (blurred1[0][ix] - blurred1[0][ix2]), + w * (blurred1[1][ix] - blurred1[1][ix2]), + w * (blurred1[2][ix] - blurred1[2][ix2]), + 1.0, local_xyb); + ++local_count; + } + } + static const double weight = 0.01617112696; + const double mul = weight * 8.0 / local_count; + for (int i = 0; i < 3; ++i) { + diff_xyb[i] += mul * local_xyb[i]; + } +} + +// https://en.wikipedia.org/wiki/Photopsin absordance modeling. +const double *GetOpsinAbsorbance() { + static const double kMix[12] = { + 0.348036746003, + 0.577814843137, + 0.0544556093735, + 0.774145581713, + 0.26922717275, + 0.767247733938, + 0.0366922708552, + 0.920130265014, + 0.0882062883536, + 0.158581714673, + 0.712857943858, + 10.6524069248, + }; + return &kMix[0]; +} + +void OpsinAbsorbance(const double in[3], double out[3]) { + const double *mix = GetOpsinAbsorbance(); + out[0] = mix[0] * in[0] + mix[1] * in[1] + mix[2] * in[2] + mix[3]; + out[1] = mix[4] * in[0] + mix[5] * in[1] + mix[6] * in[2] + mix[7]; + out[2] = mix[8] * in[0] + mix[9] * in[1] + mix[10] * in[2] + mix[11]; +} + +double GammaMinArg() { + double in[3] = { 0.0, 0.0, 0.0 }; + double out[3]; + OpsinAbsorbance(in, out); + return std::min(out[0], std::min(out[1], out[2])); +} + +double GammaMaxArg() { + double in[3] = { 255.0, 255.0, 255.0 }; + double out[3]; + OpsinAbsorbance(in, out); + return std::max(out[0], std::max(out[1], out[2])); +} + +ButteraugliComparator::ButteraugliComparator( + size_t xsize, size_t ysize, int step) + : xsize_(xsize), + ysize_(ysize), + num_pixels_(xsize * ysize), + step_(step), + res_xsize_((xsize + step - 1) / step), + res_ysize_((ysize + step - 1) / step) { + assert(step <= 4); +} + +void MaskHighIntensityChange( + size_t xsize, size_t ysize, + const std::vector > &c0, + const std::vector > &c1, + std::vector > &xyb0, + std::vector > &xyb1) { + PROFILER_FUNC; + for (int y = 0; y < ysize; ++y) { + for (int x = 0; x < xsize; ++x) { + int ix = y * xsize + x; + const double ave[3] = { + (c0[0][ix] + c1[0][ix]) * 0.5, + (c0[1][ix] + c1[1][ix]) * 0.5, + (c0[2][ix] + c1[2][ix]) * 0.5, + }; + double sqr_max_diff = -1; + { + int offset[4] = + { -1, 1, -static_cast(xsize), static_cast(xsize) }; + int border[4] = + { x == 0, x + 1 == xsize, y == 0, y + 1 == ysize }; + for (int dir = 0; dir < 4; ++dir) { + if (border[dir]) { + continue; + } + const int ix2 = ix + offset[dir]; + double diff = 0.5 * (c0[1][ix2] + c1[1][ix2]) - ave[1]; + diff *= diff; + if (sqr_max_diff < diff) { + sqr_max_diff = diff; + } + } + } + static const double kReductionX = 275.19165240059317; + static const double kReductionY = 18599.41286306991; + static const double kReductionZ = 410.8995306951065; + static const double kChromaBalance = 106.95800948271017; + double chroma_scale = kChromaBalance / (ave[1] + kChromaBalance); + + const double mix[3] = { + chroma_scale * kReductionX / (sqr_max_diff + kReductionX), + kReductionY / (sqr_max_diff + kReductionY), + chroma_scale * kReductionZ / (sqr_max_diff + kReductionZ), + }; + // Interpolate lineraly between the average color and the actual + // color -- to reduce the importance of this pixel. + for (int i = 0; i < 3; ++i) { + xyb0[i][ix] = mix[i] * c0[i][ix] + (1 - mix[i]) * ave[i]; + xyb1[i][ix] = mix[i] * c1[i][ix] + (1 - mix[i]) * ave[i]; + } + } + } +} + +double SimpleGamma(double v) { + static const double kGamma = 0.387494322593; + static const double limit = 43.01745241042018; + double bright = v - limit; + if (bright >= 0) { + static const double mul = 0.0383723643799; + v -= bright * mul; + } + static const double limit2 = 94.68634353321337; + double bright2 = v - limit2; + if (bright2 >= 0) { + static const double mul = 0.22885405968; + v -= bright2 * mul; + } + static const double offset = 0.156775786057; + static const double scale = 8.898059160493739; + double retval = scale * (offset + pow(v, kGamma)); + return retval; +} + +static inline double Gamma(double v) { + // return SimpleGamma(v); + return GammaPolynomial(v); +} + +void OpsinDynamicsImage(size_t xsize, size_t ysize, + std::vector > &rgb) { + PROFILER_FUNC; + std::vector > blurred = rgb; + static const double kSigma = 1.1; + for (int i = 0; i < 3; ++i) { + Blur(xsize, ysize, blurred[i].data(), kSigma, 0.0); + } + for (int i = 0; i < rgb[0].size(); ++i) { + double sensitivity[3]; + { + // Calculate sensitivity[3] based on the smoothed image gamma derivative. + double pre_rgb[3] = { blurred[0][i], blurred[1][i], blurred[2][i] }; + double pre_mixed[3]; + OpsinAbsorbance(pre_rgb, pre_mixed); + sensitivity[0] = Gamma(pre_mixed[0]) / pre_mixed[0]; + sensitivity[1] = Gamma(pre_mixed[1]) / pre_mixed[1]; + sensitivity[2] = Gamma(pre_mixed[2]) / pre_mixed[2]; + } + double cur_rgb[3] = { rgb[0][i], rgb[1][i], rgb[2][i] }; + double cur_mixed[3]; + OpsinAbsorbance(cur_rgb, cur_mixed); + cur_mixed[0] *= sensitivity[0]; + cur_mixed[1] *= sensitivity[1]; + cur_mixed[2] *= sensitivity[2]; + double x, y, z; + RgbToXyb(cur_mixed[0], cur_mixed[1], cur_mixed[2], &x, &y, &z); + rgb[0][i] = x; + rgb[1][i] = y; + rgb[2][i] = z; + } +} + +static void ScaleImage(double scale, std::vector *result) { + PROFILER_FUNC; + for (size_t i = 0; i < result->size(); ++i) { + (*result)[i] *= scale; + } +} + +// Making a cluster of local errors to be more impactful than +// just a single error. +void CalculateDiffmap(const size_t xsize, const size_t ysize, + const int step, + std::vector* diffmap) { + PROFILER_FUNC; + // Shift the diffmap more correctly above the pixels, from 2.5 pixels to 0.5 + // pixels distance over the original image. The border of 2 pixels on top and + // left side and 3 pixels on right and bottom side are zeroed, but these + // values have no meaning, they only exist to keep the result map the same + // size as the input images. + int s2 = (8 - step) / 2; + // Upsample and take square root. + std::vector diffmap_out(xsize * ysize); + const size_t res_xsize = (xsize + step - 1) / step; + for (size_t res_y = 0; res_y + 8 - step < ysize; res_y += step) { + for (size_t res_x = 0; res_x + 8 - step < xsize; res_x += step) { + size_t res_ix = (res_y * res_xsize + res_x) / step; + float orig_val = (*diffmap)[res_ix]; + constexpr float kInitialSlope = 100; + // TODO(b/29974893): Until that is fixed do not call sqrt on very small + // numbers. + double val = orig_val < (1.0 / (kInitialSlope * kInitialSlope)) + ? kInitialSlope * orig_val + : std::sqrt(orig_val); + for (size_t off_y = 0; off_y < step; ++off_y) { + for (size_t off_x = 0; off_x < step; ++off_x) { + diffmap_out[(res_y + off_y + s2) * xsize + res_x + off_x + s2] = val; + } + } + } + } + *diffmap = diffmap_out; + { + static const double kSigma = 8.8510880283; + static const double mul1 = 24.8235314874; + static const double scale = 1.0 / (1.0 + mul1); + const int s = 8 - step; + std::vector blurred((xsize - s) * (ysize - s)); + for (int y = 0; y < ysize - s; ++y) { + for (int x = 0; x < xsize - s; ++x) { + blurred[y * (xsize - s) + x] = (*diffmap)[(y + s2) * xsize + x + s2]; + } + } + static const double border_ratio = 0.03027655136; + Blur(xsize - s, ysize - s, blurred.data(), kSigma, border_ratio); + for (int y = 0; y < ysize - s; ++y) { + for (int x = 0; x < xsize - s; ++x) { + (*diffmap)[(y + s2) * xsize + x + s2] + += mul1 * blurred[y * (xsize - s) + x]; + } + } + ScaleImage(scale, diffmap); + } +} + +void ButteraugliComparator::Diffmap(const std::vector &rgb0_arg, + const std::vector &rgb1_arg, + ImageF &result) { + result = ImageF(xsize_, ysize_); + if (xsize_ < 8 || ysize_ < 8) return; + std::vector> rgb0_c = PackedFromPlanes(rgb0_arg); + std::vector> rgb1_c = PackedFromPlanes(rgb1_arg); + OpsinDynamicsImage(xsize_, ysize_, rgb0_c); + OpsinDynamicsImage(xsize_, ysize_, rgb1_c); + std::vector pg0 = PlanesFromPacked(xsize_, ysize_, rgb0_c); + std::vector pg1 = PlanesFromPacked(xsize_, ysize_, rgb1_c); + DiffmapOpsinDynamicsImage(pg0, pg1, result); +} + +void ButteraugliComparator::DiffmapOpsinDynamicsImage( + const std::vector &xyb0_arg, const std::vector &xyb1_arg, + ImageF &result) { + result = ImageF(xsize_, ysize_); + if (xsize_ < 8 || ysize_ < 8) return; + std::vector> xyb0 = PackedFromPlanes(xyb0_arg); + std::vector> xyb1 = PackedFromPlanes(xyb1_arg); + auto xyb0_c = xyb0; + auto xyb1_c = xyb1; + + MaskHighIntensityChange(xsize_, ysize_, xyb0_c, xyb1_c, xyb0, xyb1); + assert(8 <= xsize_); + for (int i = 0; i < 3; i++) { + assert(xyb0[i].size() == num_pixels_); + assert(xyb1[i].size() == num_pixels_); + } + std::vector > mask_xyb(3); + std::vector > mask_xyb_dc(3); + std::vector block_diff_dc(3 * res_xsize_ * res_ysize_); + std::vector block_diff_ac(3 * res_xsize_ * res_ysize_); + std::vector edge_detector_map(3 * res_xsize_ * res_ysize_); + std::vector packed_result; + BlockDiffMap(xyb0, xyb1, &block_diff_dc, &block_diff_ac); + EdgeDetectorMap(xyb0, xyb1, &edge_detector_map); + EdgeDetectorLowFreq(xyb0, xyb1, &block_diff_ac); + Mask(xyb0, xyb1, xsize_, ysize_, &mask_xyb, &mask_xyb_dc); + CombineChannels(mask_xyb, mask_xyb_dc, block_diff_dc, block_diff_ac, + edge_detector_map, &packed_result); + CalculateDiffmap(xsize_, ysize_, step_, &packed_result); + CopyFromPacked(packed_result, &result); +} + +void ButteraugliComparator::BlockDiffMap( + const std::vector > &xyb0, + const std::vector > &xyb1, + std::vector* block_diff_dc, + std::vector* block_diff_ac) { + PROFILER_FUNC; + for (size_t res_y = 0; res_y + (kBlockEdge - step_ - 1) < ysize_; + res_y += step_) { + for (size_t res_x = 0; res_x + (kBlockEdge - step_ - 1) < xsize_; + res_x += step_) { + size_t res_ix = (res_y * res_xsize_ + res_x) / step_; + size_t offset = (std::min(res_y, ysize_ - 8) * xsize_ + + std::min(res_x, xsize_ - 8)); + double block0[3 * kBlockEdge * kBlockEdge]; + double block1[3 * kBlockEdge * kBlockEdge]; + for (int i = 0; i < 3; ++i) { + double *m0 = &block0[i * kBlockEdge * kBlockEdge]; + double *m1 = &block1[i * kBlockEdge * kBlockEdge]; + for (size_t y = 0; y < kBlockEdge; y++) { + for (size_t x = 0; x < kBlockEdge; x++) { + m0[kBlockEdge * y + x] = xyb0[i][offset + y * xsize_ + x]; + m1[kBlockEdge * y + x] = xyb1[i][offset + y * xsize_ + x]; + } + } + } + double diff_xyb_dc[3] = { 0.0 }; + double diff_xyb_ac[3] = { 0.0 }; + double diff_xyb_edge_dc[3] = { 0.0 }; + ButteraugliBlockDiff(block0, block1, + diff_xyb_dc, diff_xyb_ac, diff_xyb_edge_dc); + for (int i = 0; i < 3; ++i) { + (*block_diff_dc)[3 * res_ix + i] = diff_xyb_dc[i]; + (*block_diff_ac)[3 * res_ix + i] = diff_xyb_ac[i]; + } + } + } +} + +void ButteraugliComparator::EdgeDetectorMap( + const std::vector > &xyb0, + const std::vector > &xyb1, + std::vector* edge_detector_map) { + PROFILER_FUNC; + static const double kSigma[3] = { + 1.5, + 0.586, + 0.4, + }; + std::vector > blurred0(xyb0); + std::vector > blurred1(xyb1); + for (int i = 0; i < 3; i++) { + Blur(xsize_, ysize_, blurred0[i].data(), kSigma[i], 0.0); + Blur(xsize_, ysize_, blurred1[i].data(), kSigma[i], 0.0); + } + for (size_t res_y = 0; res_y + (8 - step_) < ysize_; res_y += step_) { + for (size_t res_x = 0; res_x + (8 - step_) < xsize_; res_x += step_) { + size_t res_ix = (res_y * res_xsize_ + res_x) / step_; + double diff_xyb[3] = { 0.0 }; + Butteraugli8x8CornerEdgeDetectorDiff(std::min(res_x, xsize_ - 8), + std::min(res_y, ysize_ - 8), + xsize_, ysize_, + blurred0, blurred1, + diff_xyb); + for (int i = 0; i < 3; ++i) { + (*edge_detector_map)[3 * res_ix + i] = diff_xyb[i]; + } + } + } +} + +void ButteraugliComparator::EdgeDetectorLowFreq( + const std::vector > &xyb0, + const std::vector > &xyb1, + std::vector* block_diff_ac) { + PROFILER_FUNC; + static const double kSigma = 14; + static const double kMul = 10; + std::vector > blurred0(xyb0); + std::vector > blurred1(xyb1); + for (int i = 0; i < 3; i++) { + Blur(xsize_, ysize_, blurred0[i].data(), kSigma, 0.0); + Blur(xsize_, ysize_, blurred1[i].data(), kSigma, 0.0); + } + const int step = 8; + for (int y = 0; y + step < ysize_; y += step_) { + int resy = y / step_; + int resx = step / step_; + for (int x = 0; x + step < xsize_; x += step_, resx++) { + const int ix = y * xsize_ + x; + const int res_ix = resy * res_xsize_ + resx; + double diff[4][3]; + for (int i = 0; i < 3; ++i) { + int ix2 = ix + 8; + diff[0][i] = + ((blurred1[i][ix] - blurred0[i][ix]) + + (blurred0[i][ix2] - blurred1[i][ix2])); + ix2 = ix + 8 * xsize_; + diff[1][i] = + ((blurred1[i][ix] - blurred0[i][ix]) + + (blurred0[i][ix2] - blurred1[i][ix2])); + ix2 = ix + 6 * xsize_ + 6; + diff[2][i] = + ((blurred1[i][ix] - blurred0[i][ix]) + + (blurred0[i][ix2] - blurred1[i][ix2])); + ix2 = ix + 6 * xsize_ - 6; + diff[3][i] = x < step ? 0 : + ((blurred1[i][ix] - blurred0[i][ix]) + + (blurred0[i][ix2] - blurred1[i][ix2])); + } + double max_diff_xyb[3] = { 0 }; + for (int k = 0; k < 4; ++k) { + double diff_xyb[3] = { 0 }; + XybDiffLowFreqSquaredAccumulate(diff[k][0], diff[k][1], diff[k][2], + 0, 0, 0, 1.0, + diff_xyb); + for (int i = 0; i < 3; ++i) { + max_diff_xyb[i] = std::max(max_diff_xyb[i], diff_xyb[i]); + } + } + for (int i = 0; i < 3; ++i) { + (*block_diff_ac)[3 * res_ix + i] += kMul * max_diff_xyb[i]; + } + } + } +} + +void ButteraugliComparator::CombineChannels( + const std::vector >& mask_xyb, + const std::vector >& mask_xyb_dc, + const std::vector& block_diff_dc, + const std::vector& block_diff_ac, + const std::vector& edge_detector_map, + std::vector* result) { + PROFILER_FUNC; + result->resize(res_xsize_ * res_ysize_); + for (size_t res_y = 0; res_y + (8 - step_) < ysize_; res_y += step_) { + for (size_t res_x = 0; res_x + (8 - step_) < xsize_; res_x += step_) { + size_t res_ix = (res_y * res_xsize_ + res_x) / step_; + double mask[3]; + double dc_mask[3]; + for (int i = 0; i < 3; ++i) { + mask[i] = mask_xyb[i][(res_y + 3) * xsize_ + (res_x + 3)]; + dc_mask[i] = mask_xyb_dc[i][(res_y + 3) * xsize_ + (res_x + 3)]; + } + (*result)[res_ix] = + (DotProduct(&block_diff_dc[3 * res_ix], dc_mask) + + DotProduct(&block_diff_ac[3 * res_ix], mask) + + DotProduct(&edge_detector_map[3 * res_ix], mask)); + } + } +} + +double ButteraugliScoreFromDiffmap(const ImageF& diffmap) { + PROFILER_FUNC; + float retval = 0.0f; + for (size_t y = 0; y < diffmap.ysize(); ++y) { + ConstRestrict row = diffmap.Row(y); + for (size_t x = 0; x < diffmap.xsize(); ++x) { + retval = std::max(retval, row[x]); + } + } + return retval; +} + +static std::array MakeMask( + double extmul, double extoff, + double mul, double offset, + double scaler) { + std::array lut; + for (int i = 0; i < lut.size(); ++i) { + const double c = mul / ((0.01 * scaler * i) + offset); + lut[i] = 1.0 + extmul * (c + extoff); + assert(lut[i] >= 0.0); + lut[i] *= lut[i]; + } + return lut; +} + +double MaskX(double delta) { + PROFILER_FUNC; + static const double extmul = 0.975741017749; + static const double extoff = -4.25328244168; + static const double offset = 0.454909521427; + static const double scaler = 0.0738288224836; + static const double mul = 20.8029176447; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskY(double delta) { + PROFILER_FUNC; + static const double extmul = 0.373995618954; + static const double extoff = 1.5307267433; + static const double offset = 0.911952641929; + static const double scaler = 1.1731667845; + static const double mul = 16.2447033988; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskB(double delta) { + PROFILER_FUNC; + static const double extmul = 0.61582234137; + static const double extoff = -4.25376118646; + static const double offset = 1.05105070921; + static const double scaler = 0.47434643535; + static const double mul = 31.1444967089; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskDcX(double delta) { + PROFILER_FUNC; + static const double extmul = 1.79116943438; + static const double extoff = -3.86797479189; + static const double offset = 0.670960225853; + static const double scaler = 0.486575865525; + static const double mul = 20.4563479139; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskDcY(double delta) { + PROFILER_FUNC; + static const double extmul = 0.212223514236; + static const double extoff = -3.65647120524; + static const double offset = 1.73396799447; + static const double scaler = 0.170392660501; + static const double mul = 21.6566724788; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskDcB(double delta) { + PROFILER_FUNC; + static const double extmul = 0.349376011816; + static const double extoff = -0.894711072781; + static const double offset = 0.901647926679; + static const double scaler = 0.380086095024; + static const double mul = 18.0373825149; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +// Replaces values[x + y * xsize] with the minimum of the values in the +// square_size square with coordinates +// x - offset .. x + square_size - offset - 1, +// y - offset .. y + square_size - offset - 1. +void MinSquareVal(size_t square_size, size_t offset, + size_t xsize, size_t ysize, + float *values) { + PROFILER_FUNC; + // offset is not negative and smaller than square_size. + assert(offset < square_size); + std::vector tmp(xsize * ysize); + for (size_t y = 0; y < ysize; ++y) { + const size_t minh = offset > y ? 0 : y - offset; + const size_t maxh = std::min(ysize, y + square_size - offset); + for (size_t x = 0; x < xsize; ++x) { + double min = values[x + minh * xsize]; + for (size_t j = minh + 1; j < maxh; ++j) { + min = fmin(min, values[x + j * xsize]); + } + tmp[x + y * xsize] = min; + } + } + for (size_t x = 0; x < xsize; ++x) { + const size_t minw = offset > x ? 0 : x - offset; + const size_t maxw = std::min(xsize, x + square_size - offset); + for (size_t y = 0; y < ysize; ++y) { + double min = tmp[minw + y * xsize]; + for (size_t j = minw + 1; j < maxw; ++j) { + min = fmin(min, tmp[j + y * xsize]); + } + values[x + y * xsize] = min; + } + } +} + +// ===== Functions used by Mask only ===== +void Average5x5(int xsize, int ysize, std::vector* diffs) { + PROFILER_FUNC; + if (xsize < 4 || ysize < 4) { + // TODO: Make this work for small dimensions as well. + return; + } + static const float w = 0.679144890667; + static const float scale = 1.0 / (5.0 + 4 * w); + std::vector result = *diffs; + std::vector tmp0 = *diffs; + std::vector tmp1 = *diffs; + ScaleImage(w, &tmp1); + for (int y = 0; y < ysize; y++) { + const int row0 = y * xsize; + result[row0 + 1] += tmp0[row0]; + result[row0 + 0] += tmp0[row0 + 1]; + result[row0 + 2] += tmp0[row0 + 1]; + for (int x = 2; x < xsize - 2; ++x) { + result[row0 + x - 1] += tmp0[row0 + x]; + result[row0 + x + 1] += tmp0[row0 + x]; + } + result[row0 + xsize - 3] += tmp0[row0 + xsize - 2]; + result[row0 + xsize - 1] += tmp0[row0 + xsize - 2]; + result[row0 + xsize - 2] += tmp0[row0 + xsize - 1]; + if (y > 0) { + const int rowd1 = row0 - xsize; + result[rowd1 + 1] += tmp1[row0]; + result[rowd1 + 0] += tmp0[row0]; + for (int x = 1; x < xsize - 1; ++x) { + result[rowd1 + x + 1] += tmp1[row0 + x]; + result[rowd1 + x + 0] += tmp0[row0 + x]; + result[rowd1 + x - 1] += tmp1[row0 + x]; + } + result[rowd1 + xsize - 1] += tmp0[row0 + xsize - 1]; + result[rowd1 + xsize - 2] += tmp1[row0 + xsize - 1]; + } + if (y + 1 < ysize) { + const int rowu1 = row0 + xsize; + result[rowu1 + 1] += tmp1[row0]; + result[rowu1 + 0] += tmp0[row0]; + for (int x = 1; x < xsize - 1; ++x) { + result[rowu1 + x + 1] += tmp1[row0 + x]; + result[rowu1 + x + 0] += tmp0[row0 + x]; + result[rowu1 + x - 1] += tmp1[row0 + x]; + } + result[rowu1 + xsize - 1] += tmp0[row0 + xsize - 1]; + result[rowu1 + xsize - 2] += tmp1[row0 + xsize - 1]; + } + } + *diffs = result; + ScaleImage(scale, diffs); +} + +void DiffPrecompute( + const std::vector > &xyb0, + const std::vector > &xyb1, + size_t xsize, size_t ysize, + std::vector > *mask) { + PROFILER_FUNC; + mask->resize(3, std::vector(xyb0[0].size())); + double valsh0[3] = { 0.0 }; + double valsv0[3] = { 0.0 }; + double valsh1[3] = { 0.0 }; + double valsv1[3] = { 0.0 }; + int ix2; + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + size_t ix = x + xsize * y; + if (x + 1 < xsize) { + ix2 = ix + 1; + } else { + ix2 = ix - 1; + } + { + double x0 = (xyb0[0][ix] - xyb0[0][ix2]); + double y0 = (xyb0[1][ix] - xyb0[1][ix2]); + double z0 = (xyb0[2][ix] - xyb0[2][ix2]); + XybToVals(x0, y0, z0, &valsh0[0], &valsh0[1], &valsh0[2]); + double x1 = (xyb1[0][ix] - xyb1[0][ix2]); + double y1 = (xyb1[1][ix] - xyb1[1][ix2]); + double z1 = (xyb1[2][ix] - xyb1[2][ix2]); + XybToVals(x1, y1, z1, &valsh1[0], &valsh1[1], &valsh1[2]); + } + if (y + 1 < ysize) { + ix2 = ix + xsize; + } else { + ix2 = ix - xsize; + } + { + double x0 = (xyb0[0][ix] - xyb0[0][ix2]); + double y0 = (xyb0[1][ix] - xyb0[1][ix2]); + double z0 = (xyb0[2][ix] - xyb0[2][ix2]); + XybToVals(x0, y0, z0, &valsv0[0], &valsv0[1], &valsv0[2]); + double x1 = (xyb1[0][ix] - xyb1[0][ix2]); + double y1 = (xyb1[1][ix] - xyb1[1][ix2]); + double z1 = (xyb1[2][ix] - xyb1[2][ix2]); + XybToVals(x1, y1, z1, &valsv1[0], &valsv1[1], &valsv1[2]); + } + for (int i = 0; i < 3; ++i) { + double sup0 = fabs(valsh0[i]) + fabs(valsv0[i]); + double sup1 = fabs(valsh1[i]) + fabs(valsv1[i]); + double m = std::min(sup0, sup1); + (*mask)[i][ix] = m; + } + } + } +} + +void Mask(const std::vector > &xyb0, + const std::vector > &xyb1, + size_t xsize, size_t ysize, + std::vector > *mask, + std::vector > *mask_dc) { + PROFILER_FUNC; + mask->resize(3); + mask_dc->resize(3); + for (int i = 0; i < 3; ++i) { + (*mask)[i].resize(xsize * ysize); + (*mask_dc)[i].resize(xsize * ysize); + } + DiffPrecompute(xyb0, xyb1, xsize, ysize, mask); + for (int i = 0; i < 3; ++i) { + Average5x5(xsize, ysize, &(*mask)[i]); + MinSquareVal(4, 0, xsize, ysize, (*mask)[i].data()); + static const double sigma[3] = { + 9.65781083553, + 14.2644604355, + 4.53358927369, + }; + Blur(xsize, ysize, (*mask)[i].data(), sigma[i], 0.0); + } + static const double w00 = 232.206464018; + static const double w11 = 22.9455222245; + static const double w22 = 503.962310606; + + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + const size_t idx = y * xsize + x; + const double s0 = (*mask)[0][idx]; + const double s1 = (*mask)[1][idx]; + const double s2 = (*mask)[2][idx]; + const double p0 = w00 * s0; + const double p1 = w11 * s1; + const double p2 = w22 * s2; + + (*mask)[0][idx] = MaskX(p0); + (*mask)[1][idx] = MaskY(p1); + (*mask)[2][idx] = MaskB(p2); + (*mask_dc)[0][idx] = MaskDcX(p0); + (*mask_dc)[1][idx] = MaskDcY(p1); + (*mask_dc)[2][idx] = MaskDcB(p2); + } + } + for (int i = 0; i < 3; ++i) { + ScaleImage(kGlobalScale * kGlobalScale, &(*mask)[i]); + ScaleImage(kGlobalScale * kGlobalScale, &(*mask_dc)[i]); + } +} + +void ButteraugliDiffmap(const std::vector &rgb0_image, + const std::vector &rgb1_image, + ImageF &result_image) { + const size_t xsize = rgb0_image[0].xsize(); + const size_t ysize = rgb0_image[0].ysize(); + ButteraugliComparator butteraugli(xsize, ysize, 3); + butteraugli.Diffmap(rgb0_image, rgb1_image, result_image); +} + +bool ButteraugliInterface(const std::vector &rgb0, + const std::vector &rgb1, + ImageF &diffmap, + double &diffvalue) { + const size_t xsize = rgb0[0].xsize(); + const size_t ysize = rgb0[0].ysize(); + if (xsize < 1 || ysize < 1) { + // Butteraugli values for small (where xsize or ysize is smaller + // than 8 pixels) images are non-sensical, but most likely it is + // less disruptive to try to compute something than just give up. + return false; // No image. + } + for (int i = 1; i < 3; i++) { + if (rgb0[i].xsize() != xsize || rgb0[i].ysize() != ysize || + rgb1[i].xsize() != xsize || rgb1[i].ysize() != ysize) { + return false; // Image planes must have same dimensions. + } + } + if (xsize < 8 || ysize < 8) { + for (int y = 0; y < ysize; ++y) { + for (int x = 0; x < xsize; ++x) { + diffmap.Row(y)[x] = 0; + } + } + diffvalue = 0; + return true; + } + ButteraugliDiffmap(rgb0, rgb1, diffmap); + diffvalue = ButteraugliScoreFromDiffmap(diffmap); + return true; +} + +bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize, + const std::vector > &rgb, std::vector &quant) { + if (xsize < 16 || ysize < 16) { + return false; // Butteraugli is undefined for small images. + } + size_t size = xsize * ysize; + + std::vector > scale_xyb(3); + std::vector > scale_xyb_dc(3); + Mask(rgb, rgb, xsize, ysize, &scale_xyb, &scale_xyb_dc); + quant.resize(size); + + // Mask gives us values in 3 color channels, but for now we take only + // the intensity channel. + for (size_t i = 0; i < size; i++) { + quant[i] = scale_xyb[1][i]; + } + return true; +} + +double ButteraugliFuzzyClass(double score) { + static const double fuzzy_width_up = 10.287189655; + static const double fuzzy_width_down = 6.97490803335; + static const double m0 = 2.0; + double fuzzy_width = score < 1.0 ? fuzzy_width_down : fuzzy_width_up; + return m0 / (1.0 + exp((score - 1.0) * fuzzy_width)); +} + +double ButteraugliFuzzyInverse(double seek) { + double pos = 0; + for (double range = 1.0; range >= 1e-10; range *= 0.5) { + double cur = ButteraugliFuzzyClass(pos); + if (cur < seek) { + pos -= range; + } else { + pos += range; + } + } + return pos; +} + +} // namespace butteraugli diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli.h b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli.h new file mode 100644 index 0000000..7b80551 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli.h @@ -0,0 +1,560 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Disclaimer: This is not an official Google product. +// +// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) + +#ifndef BUTTERAUGLI_BUTTERAUGLI_H_ +#define BUTTERAUGLI_BUTTERAUGLI_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PROFILER_ENABLED +#define PROFILER_ENABLED 0 +#endif +#if PROFILER_ENABLED +#else +#define PROFILER_FUNC +#define PROFILER_ZONE(name) +#endif + +#define BUTTERAUGLI_ENABLE_CHECKS 0 + +// This is the main interface to butteraugli image similarity +// analysis function. + +namespace butteraugli { + +template +class Image; + +using Image8 = Image; +using ImageF = Image; +using ImageD = Image; + +// ButteraugliInterface defines the public interface for butteraugli. +// +// It calculates the difference between rgb0 and rgb1. +// +// rgb0 and rgb1 contain the images. rgb0[c][px] and rgb1[c][px] contains +// the red image for c == 0, green for c == 1, blue for c == 2. Location index +// px is calculated as y * xsize + x. +// +// Value of pixels of images rgb0 and rgb1 need to be represented as raw +// intensity. Most image formats store gamma corrected intensity in pixel +// values. This gamma correction has to be removed, by applying the following +// function: +// butteraugli_val = 255.0 * pow(png_val / 255.0, gamma); +// A typical value of gamma is 2.2. It is usually stored in the image header. +// Take care not to confuse that value with its inverse. The gamma value should +// be always greater than one. +// Butteraugli does not work as intended if the caller does not perform +// gamma correction. +// +// diffmap will contain an image of the size xsize * ysize, containing +// localized differences for values px (indexed with the px the same as rgb0 +// and rgb1). diffvalue will give a global score of similarity. +// +// A diffvalue smaller than kButteraugliGood indicates that images can be +// observed as the same image. +// diffvalue larger than kButteraugliBad indicates that a difference between +// the images can be observed. +// A diffvalue between kButteraugliGood and kButteraugliBad indicates that +// a subtle difference can be observed between the images. +// +// Returns true on success. + +bool ButteraugliInterface(const std::vector &rgb0, + const std::vector &rgb1, + ImageF &diffmap, + double &diffvalue); + +const double kButteraugliQuantLow = 0.26; +const double kButteraugliQuantHigh = 1.454; + +// Converts the butteraugli score into fuzzy class values that are continuous +// at the class boundary. The class boundary location is based on human +// raters, but the slope is arbitrary. Particularly, it does not reflect +// the expectation value of probabilities of the human raters. It is just +// expected that a smoother class boundary will allow for higher-level +// optimization algorithms to work faster. +// +// Returns 2.0 for a perfect match, and 1.0 for 'ok', 0.0 for bad. Because the +// scoring is fuzzy, a butteraugli score of 0.96 would return a class of +// around 1.9. +double ButteraugliFuzzyClass(double score); + +// Input values should be in range 0 (bad) to 2 (good). Use +// kButteraugliNormalization as normalization. +double ButteraugliFuzzyInverse(double seek); + +// Returns a map which can be used for adaptive quantization. Values can +// typically range from kButteraugliQuantLow to kButteraugliQuantHigh. Low +// values require coarse quantization (e.g. near random noise), high values +// require fine quantization (e.g. in smooth bright areas). +bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize, + const std::vector > &rgb, std::vector &quant); + +// Implementation details, don't use anything below or your code will +// break in the future. + +#ifdef _MSC_VER +#define BUTTERAUGLI_RESTRICT +#else +#define BUTTERAUGLI_RESTRICT __restrict__ +#endif + +#ifdef _MSC_VER +#define BUTTERAUGLI_CACHE_ALIGNED_RETURN /* not supported */ +#else +#define BUTTERAUGLI_CACHE_ALIGNED_RETURN __attribute__((assume_aligned(64))) +#endif + +// Alias for unchangeable, non-aliased pointers. T is a pointer type, +// possibly to a const type. Example: ConstRestrict ptr = nullptr. +// The conventional syntax uint8_t* const RESTRICT is more confusing - it is +// not immediately obvious that the pointee is non-const. +template +using ConstRestrict = T const BUTTERAUGLI_RESTRICT; + +// Functions that depend on the cache line size. +class CacheAligned { + public: + static constexpr size_t kPointerSize = sizeof(void *); + static constexpr size_t kCacheLineSize = 64; + + // The aligned-return annotation is only allowed on function declarations. + static void *Allocate(const size_t bytes) BUTTERAUGLI_CACHE_ALIGNED_RETURN; + static void Free(void *aligned_pointer); +}; + +template +using CacheAlignedUniquePtrT = std::unique_ptr; + +using CacheAlignedUniquePtr = CacheAlignedUniquePtrT; + +template +static inline CacheAlignedUniquePtrT Allocate(const size_t entries) { + return CacheAlignedUniquePtrT( + static_cast>( + CacheAligned::Allocate(entries * sizeof(T))), + CacheAligned::Free); +} + +// Returns the smallest integer not less than "amount" that is divisible by +// "multiple", which must be a power of two. +template +static inline size_t Align(const size_t amount) { + static_assert(multiple != 0 && ((multiple & (multiple - 1)) == 0), + "Align<> argument must be a power of two"); + return (amount + multiple - 1) & ~(multiple - 1); +} + +// Single channel, contiguous (cache-aligned) rows separated by padding. +// T must be POD. +// +// Rationale: vectorization benefits from aligned operands - unaligned loads and +// especially stores are expensive when the address crosses cache line +// boundaries. Introducing padding after each row ensures the start of a row is +// aligned, and that row loops can process entire vectors (writes to the padding +// are allowed and ignored). +// +// We prefer a planar representation, where channels are stored as separate +// 2D arrays, because that simplifies vectorization (repeating the same +// operation on multiple adjacent components) without the complexity of a +// hybrid layout (8 R, 8 G, 8 B, ...). In particular, clients can easily iterate +// over all components in a row and Image requires no knowledge of the pixel +// format beyond the component type "T". The downside is that we duplicate the +// xsize/ysize members for each channel. +// +// This image layout could also be achieved with a vector and a row accessor +// function, but a class wrapper with support for "deleter" allows wrapping +// existing memory allocated by clients without copying the pixels. It also +// provides convenient accessors for xsize/ysize, which shortens function +// argument lists. Supports move-construction so it can be stored in containers. +template +class Image { + // Returns cache-aligned row stride, being careful to avoid 2K aliasing. + static size_t BytesPerRow(const size_t xsize) { + // Allow reading one extra AVX-2 vector on the right margin. + const size_t row_size = xsize * sizeof(T) + 32; + const size_t align = CacheAligned::kCacheLineSize; + size_t bytes_per_row = (row_size + align - 1) & ~(align - 1); + // During the lengthy window before writes are committed to memory, CPUs + // guard against read after write hazards by checking the address, but + // only the lower 11 bits. We avoid a false dependency between writes to + // consecutive rows by ensuring their sizes are not multiples of 2 KiB. + if (bytes_per_row % 2048 == 0) { + bytes_per_row += align; + } + return bytes_per_row; + } + + public: + using T = ComponentType; + + Image() : xsize_(0), ysize_(0), bytes_per_row_(0), bytes_(static_cast(nullptr), Ignore) {} + + Image(const size_t xsize, const size_t ysize) + : xsize_(xsize), + ysize_(ysize), + bytes_per_row_(BytesPerRow(xsize)), + bytes_(Allocate(bytes_per_row_ * ysize)) {} + + Image(const size_t xsize, const size_t ysize, ConstRestrict bytes, + const size_t bytes_per_row) + : xsize_(xsize), + ysize_(ysize), + bytes_per_row_(bytes_per_row), + bytes_(bytes, Ignore) {} + + // Move constructor (required for returning Image from function) + Image(Image &&other) + : xsize_(other.xsize_), + ysize_(other.ysize_), + bytes_per_row_(other.bytes_per_row_), + bytes_(std::move(other.bytes_)) {} + + // Move assignment (required for std::vector) + Image &operator=(Image &&other) { + xsize_ = other.xsize_; + ysize_ = other.ysize_; + bytes_per_row_ = other.bytes_per_row_; + bytes_ = std::move(other.bytes_); + return *this; + } + + void Swap(Image &other) { + std::swap(xsize_, other.xsize_); + std::swap(ysize_, other.ysize_); + std::swap(bytes_per_row_, other.bytes_per_row_); + std::swap(bytes_, other.bytes_); + } + + // How many pixels. + size_t xsize() const { return xsize_; } + size_t ysize() const { return ysize_; } + + ConstRestrict Row(const size_t y) BUTTERAUGLI_CACHE_ALIGNED_RETURN { +#ifdef BUTTERAUGLI_ENABLE_CHECKS + if (y >= ysize_) { + printf("Row %zu out of bounds (ysize=%zu)\n", y, ysize_); + abort(); + } +#endif + return reinterpret_cast(bytes_.get() + y * bytes_per_row_); + } + + ConstRestrict Row(const size_t y) const + BUTTERAUGLI_CACHE_ALIGNED_RETURN { +#ifdef BUTTERAUGLI_ENABLE_CHECKS + if (y >= ysize_) { + printf("Const row %zu out of bounds (ysize=%zu)\n", y, ysize_); + abort(); + } +#endif + return reinterpret_cast(bytes_.get() + y * bytes_per_row_); + } + + // Raw access to byte contents, for interfacing with other libraries. + // Unsigned char instead of char to avoid surprises (sign extension). + ConstRestrict bytes() { return bytes_.get(); } + ConstRestrict bytes() const { return bytes_.get(); } + size_t bytes_per_row() const { return bytes_per_row_; } + + // Returns number of pixels (some of which are padding) per row. Useful for + // computing other rows via pointer arithmetic. + intptr_t PixelsPerRow() const { + static_assert(CacheAligned::kCacheLineSize % sizeof(T) == 0, + "Padding must be divisible by the pixel size."); + return static_cast(bytes_per_row_ / sizeof(T)); + } + + private: + // Deleter used when bytes are not owned. + static void Ignore(void *ptr) {} + + // (Members are non-const to enable assignment during move-assignment.) + size_t xsize_; // original intended pixels, not including any padding. + size_t ysize_; + size_t bytes_per_row_; // [bytes] including padding. + CacheAlignedUniquePtr bytes_; +}; + +// Returns newly allocated planes of the given dimensions. +template +static inline std::vector> CreatePlanes(const size_t xsize, + const size_t ysize, + const size_t num_planes) { + std::vector> planes; + planes.reserve(num_planes); + for (size_t i = 0; i < num_planes; ++i) { + planes.emplace_back(xsize, ysize); + } + return planes; +} + +// Returns a new image with the same dimensions and pixel values. +template +static inline Image CopyPixels(const Image &other) { + Image copy(other.xsize(), other.ysize()); + const void *BUTTERAUGLI_RESTRICT from = other.bytes(); + void *BUTTERAUGLI_RESTRICT to = copy.bytes(); + memcpy(to, from, other.ysize() * other.bytes_per_row()); + return copy; +} + +// Returns new planes with the same dimensions and pixel values. +template +static inline std::vector> CopyPlanes( + const std::vector> &planes) { + std::vector> copy; + copy.reserve(planes.size()); + for (const Image &plane : planes) { + copy.push_back(CopyPixels(plane)); + } + return copy; +} + +// Compacts a padded image into a preallocated packed vector. +template +static inline void CopyToPacked(const Image &from, std::vector *to) { + const size_t xsize = from.xsize(); + const size_t ysize = from.ysize(); +#if BUTTERAUGLI_ENABLE_CHECKS + if (to->size() < xsize * ysize) { + printf("%zu x %zu exceeds %zu capacity\n", xsize, ysize, to->size()); + abort(); + } +#endif + for (size_t y = 0; y < ysize; ++y) { + ConstRestrict row_from = from.Row(y); + ConstRestrict row_to = to->data() + y * xsize; + memcpy(row_to, row_from, xsize * sizeof(T)); + } +} + +// Expands a packed vector into a preallocated padded image. +template +static inline void CopyFromPacked(const std::vector &from, Image *to) { + const size_t xsize = to->xsize(); + const size_t ysize = to->ysize(); + assert(from.size() == xsize * ysize); + for (size_t y = 0; y < ysize; ++y) { + ConstRestrict row_from = from.data() + y * xsize; + ConstRestrict row_to = to->Row(y); + memcpy(row_to, row_from, xsize * sizeof(T)); + } +} + +template +static inline std::vector> PlanesFromPacked( + const size_t xsize, const size_t ysize, + const std::vector> &packed) { + std::vector> planes; + planes.reserve(packed.size()); + for (const std::vector &p : packed) { + planes.push_back(Image(xsize, ysize)); + CopyFromPacked(p, &planes.back()); + } + return planes; +} + +template +static inline std::vector> PackedFromPlanes( + const std::vector> &planes) { + assert(!planes.empty()); + const size_t num_pixels = planes[0].xsize() * planes[0].ysize(); + std::vector> packed; + packed.reserve(planes.size()); + for (const Image &image : planes) { + packed.push_back(std::vector(num_pixels)); + CopyToPacked(image, &packed.back()); + } + return packed; +} + +class ButteraugliComparator { + public: + ButteraugliComparator(size_t xsize, size_t ysize, int step); + + // Computes the butteraugli map between rgb0 and rgb1 and updates result. + void Diffmap(const std::vector &rgb0, + const std::vector &rgb1, + ImageF &result); + + // Same as above, but OpsinDynamicsImage() was already applied to + // rgb0 and rgb1. + void DiffmapOpsinDynamicsImage(const std::vector &rgb0, + const std::vector &rgb1, + ImageF &result); + + private: + void BlockDiffMap(const std::vector > &rgb0, + const std::vector > &rgb1, + std::vector* block_diff_dc, + std::vector* block_diff_ac); + + + void EdgeDetectorMap(const std::vector > &rgb0, + const std::vector > &rgb1, + std::vector* edge_detector_map); + + void EdgeDetectorLowFreq(const std::vector > &rgb0, + const std::vector > &rgb1, + std::vector* block_diff_ac); + + void CombineChannels(const std::vector >& scale_xyb, + const std::vector >& scale_xyb_dc, + const std::vector& block_diff_dc, + const std::vector& block_diff_ac, + const std::vector& edge_detector_map, + std::vector* result); + + const size_t xsize_; + const size_t ysize_; + const size_t num_pixels_; + const int step_; + const size_t res_xsize_; + const size_t res_ysize_; +}; + +void ButteraugliDiffmap(const std::vector &rgb0, + const std::vector &rgb1, + ImageF &diffmap); + +double ButteraugliScoreFromDiffmap(const ImageF& distmap); + +// Compute values of local frequency and dc masking based on the activity +// in the two images. +void Mask(const std::vector > &rgb0, + const std::vector > &rgb1, + size_t xsize, size_t ysize, + std::vector > *mask, + std::vector > *mask_dc); + +// Computes difference metrics for one 8x8 block. +void ButteraugliBlockDiff(double rgb0[192], + double rgb1[192], + double diff_xyb_dc[3], + double diff_xyb_ac[3], + double diff_xyb_edge_dc[3]); + +void OpsinAbsorbance(const double in[3], double out[3]); + +void OpsinDynamicsImage(size_t xsize, size_t ysize, + std::vector > &rgb); + +void MaskHighIntensityChange( + size_t xsize, size_t ysize, + const std::vector > &c0, + const std::vector > &c1, + std::vector > &rgb0, + std::vector > &rgb1); + +void Blur(size_t xsize, size_t ysize, float* channel, double sigma, + double border_ratio = 0.0); + +void RgbToXyb(double r, double g, double b, + double *valx, double *valy, double *valz); + +double SimpleGamma(double v); + +double GammaMinArg(); +double GammaMaxArg(); + +// Polynomial evaluation via Clenshaw's scheme (similar to Horner's). +// Template enables compile-time unrolling of the recursion, but must reside +// outside of a class due to the specialization. +template +static inline void ClenshawRecursion(const double x, const double *coefficients, + double *b1, double *b2) { + const double x_b1 = x * (*b1); + const double t = (x_b1 + x_b1) - (*b2) + coefficients[INDEX]; + *b2 = *b1; + *b1 = t; + + ClenshawRecursion(x, coefficients, b1, b2); +} + +// Base case +template <> +inline void ClenshawRecursion<0>(const double x, const double *coefficients, + double *b1, double *b2) { + const double x_b1 = x * (*b1); + // The final iteration differs - no 2 * x_b1 here. + *b1 = x_b1 - (*b2) + coefficients[0]; +} + +// Rational polynomial := dividing two polynomial evaluations. These are easier +// to find than minimax polynomials. +struct RationalPolynomial { + template + static double EvaluatePolynomial(const double x, + const double (&coefficients)[N]) { + double b1 = 0.0; + double b2 = 0.0; + ClenshawRecursion(x, coefficients, &b1, &b2); + return b1; + } + + // Evaluates the polynomial at x (in [min_value, max_value]). + inline double operator()(const float x) const { + // First normalize to [0, 1]. + const double x01 = (x - min_value) / (max_value - min_value); + // And then to [-1, 1] domain of Chebyshev polynomials. + const double xc = 2.0 * x01 - 1.0; + + const double yp = EvaluatePolynomial(xc, p); + const double yq = EvaluatePolynomial(xc, q); + if (yq == 0.0) return 0.0; + return static_cast(yp / yq); + } + + // Domain of the polynomials; they are undefined elsewhere. + double min_value; + double max_value; + + // Coefficients of T_n (Chebyshev polynomials of the first kind). + // Degree 5/5 is a compromise between accuracy (0.1%) and numerical stability. + double p[5 + 1]; + double q[5 + 1]; +}; + +static inline float GammaPolynomial(float value) { + // Generated by gamma_polynomial.m from equispaced x/gamma(x) samples. + static const RationalPolynomial r = { + 0.770000000000000, 274.579999999999984, + { + 881.979476556478289, 1496.058452015812463, 908.662212739659481, + 373.566100223287378, 85.840860336314364, 6.683258861509244, + }, + { + 12.262350348616792, 20.557285797683576, 12.161463238367844, + 4.711532733641639, 0.899112889751053, 0.035662329617191, + }}; + return r(value); +} + +} // namespace butteraugli + +#endif // BUTTERAUGLI_BUTTERAUGLI_H_ diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc new file mode 100644 index 0000000..f20abb2 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc @@ -0,0 +1,441 @@ +#include +#include +#include +#include +#include "butteraugli/butteraugli.h" + +extern "C" { +#include "png.h" +#include "jpeglib.h" +} + +namespace butteraugli { +namespace { + +// "rgb": cleared and filled with same-sized image planes (one per channel); +// either RGB, or RGBA if the PNG contains an alpha channel. +bool ReadPNG(FILE* f, std::vector* rgb) { + png_structp png_ptr = + png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png_ptr) { + return false; + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, NULL, NULL); + return false; + } + + if (setjmp(png_jmpbuf(png_ptr)) != 0) { + // Ok we are here because of the setjmp. + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return false; + } + + rewind(f); + png_init_io(png_ptr, f); + + // The png_transforms flags are as follows: + // packing == convert 1,2,4 bit images, + // strip == 16 -> 8 bits / channel, + // shift == use sBIT dynamics, and + // expand == palettes -> rgb, grayscale -> 8 bit images, tRNS -> alpha. + const unsigned int png_transforms = + PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16; + + png_read_png(png_ptr, info_ptr, png_transforms, NULL); + + png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr); + + const int xsize = png_get_image_width(png_ptr, info_ptr); + const int ysize = png_get_image_height(png_ptr, info_ptr); + const int components = png_get_channels(png_ptr, info_ptr); + + *rgb = CreatePlanes(xsize, ysize, 3); + + switch (components) { + case 1: { + // GRAYSCALE + for (int y = 0; y < ysize; ++y) { + ConstRestrict row = row_pointers[y]; + ConstRestrict row0 = (*rgb)[0].Row(y); + ConstRestrict row1 = (*rgb)[1].Row(y); + ConstRestrict row2 = (*rgb)[2].Row(y); + + for (int x = 0; x < xsize; ++x) { + const uint8_t gray = row[x]; + row0[x] = row1[x] = row2[x] = gray; + } + } + break; + } + case 2: { + // GRAYSCALE_ALPHA + rgb->push_back(Image8(xsize, ysize)); + for (int y = 0; y < ysize; ++y) { + ConstRestrict row = row_pointers[y]; + ConstRestrict row0 = (*rgb)[0].Row(y); + ConstRestrict row1 = (*rgb)[1].Row(y); + ConstRestrict row2 = (*rgb)[2].Row(y); + ConstRestrict row3 = (*rgb)[3].Row(y); + + for (int x = 0; x < xsize; ++x) { + const uint8_t gray = row[2 * x + 0]; + const uint8_t alpha = row[2 * x + 1]; + row0[x] = gray; + row1[x] = gray; + row2[x] = gray; + row3[x] = alpha; + } + } + break; + } + case 3: { + // RGB + for (int y = 0; y < ysize; ++y) { + ConstRestrict row = row_pointers[y]; + ConstRestrict row0 = (*rgb)[0].Row(y); + ConstRestrict row1 = (*rgb)[1].Row(y); + ConstRestrict row2 = (*rgb)[2].Row(y); + + for (int x = 0; x < xsize; ++x) { + row0[x] = row[3 * x + 0]; + row1[x] = row[3 * x + 1]; + row2[x] = row[3 * x + 2]; + } + } + break; + } + case 4: { + // RGBA + rgb->push_back(Image8(xsize, ysize)); + for (int y = 0; y < ysize; ++y) { + ConstRestrict row = row_pointers[y]; + ConstRestrict row0 = (*rgb)[0].Row(y); + ConstRestrict row1 = (*rgb)[1].Row(y); + ConstRestrict row2 = (*rgb)[2].Row(y); + ConstRestrict row3 = (*rgb)[3].Row(y); + + for (int x = 0; x < xsize; ++x) { + row0[x] = row[4 * x + 0]; + row1[x] = row[4 * x + 1]; + row2[x] = row[4 * x + 2]; + row3[x] = row[4 * x + 3]; + } + } + break; + } + default: + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return false; + } + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return true; +} + +const double* NewSrgbToLinearTable() { + double* table = new double[256]; + for (int i = 0; i < 256; ++i) { + const double srgb = i / 255.0; + table[i] = + 255.0 * (srgb <= 0.04045 ? srgb / 12.92 + : std::pow((srgb + 0.055) / 1.055, 2.4)); + } + return table; +} + +void jpeg_catch_error(j_common_ptr cinfo) { + (*cinfo->err->output_message) (cinfo); + jmp_buf* jpeg_jmpbuf = (jmp_buf*) cinfo->client_data; + jpeg_destroy(cinfo); + longjmp(*jpeg_jmpbuf, 1); +} + +// "rgb": cleared and filled with same-sized image planes (one per channel); +// either RGB, or RGBA if the PNG contains an alpha channel. +bool ReadJPEG(FILE* f, std::vector* rgb) { + rewind(f); + + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jmp_buf jpeg_jmpbuf; + cinfo.client_data = &jpeg_jmpbuf; + jerr.error_exit = jpeg_catch_error; + if (setjmp(jpeg_jmpbuf)) { + return false; + } + + jpeg_create_decompress(&cinfo); + + jpeg_stdio_src(&cinfo, f); + jpeg_read_header(&cinfo, TRUE); + jpeg_start_decompress(&cinfo); + + int row_stride = cinfo.output_width * cinfo.output_components; + JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray) + ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1); + + const size_t xsize = cinfo.output_width; + const size_t ysize = cinfo.output_height; + + *rgb = CreatePlanes(xsize, ysize, 3); + + switch (cinfo.out_color_space) { + case JCS_GRAYSCALE: + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + + ConstRestrict row = buffer[0]; + ConstRestrict row0 = (*rgb)[0].Row(cinfo.output_scanline - 1); + ConstRestrict row1 = (*rgb)[1].Row(cinfo.output_scanline - 1); + ConstRestrict row2 = (*rgb)[2].Row(cinfo.output_scanline - 1); + + for (int x = 0; x < xsize; x++) { + const uint8_t gray = row[x]; + row0[x] = row1[x] = row2[x] = gray; + } + } + break; + + case JCS_RGB: + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + + ConstRestrict row = buffer[0]; + ConstRestrict row0 = (*rgb)[0].Row(cinfo.output_scanline - 1); + ConstRestrict row1 = (*rgb)[1].Row(cinfo.output_scanline - 1); + ConstRestrict row2 = (*rgb)[2].Row(cinfo.output_scanline - 1); + + for (int x = 0; x < xsize; x++) { + row0[x] = row[3 * x + 0]; + row1[x] = row[3 * x + 1]; + row2[x] = row[3 * x + 2]; + } + } + break; + + default: + jpeg_destroy_decompress(&cinfo); + return false; + } + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + return true; +} + +// Translate R, G, B channels from sRGB to linear space. If an alpha channel +// is present, overlay the image over a black or white background. Overlaying +// is done in the sRGB space; while technically incorrect, this is aligned with +// many other software (web browsers, WebP near lossless). +void FromSrgbToLinear(const std::vector& rgb, + std::vector& linear, int background) { + const size_t xsize = rgb[0].xsize(); + const size_t ysize = rgb[0].ysize(); + static const double* const kSrgbToLinearTable = NewSrgbToLinearTable(); + + if (rgb.size() == 3) { // RGB + for (int c = 0; c < 3; c++) { + linear.push_back(ImageF(xsize, ysize)); + for (int y = 0; y < ysize; ++y) { + ConstRestrict row_rgb = rgb[c].Row(y); + ConstRestrict row_linear = linear[c].Row(y); + for (size_t x = 0; x < xsize; x++) { + const int value = row_rgb[x]; + row_linear[x] = kSrgbToLinearTable[value]; + } + } + } + } else { // RGBA + for (int c = 0; c < 3; c++) { + linear.push_back(ImageF(xsize, ysize)); + for (int y = 0; y < ysize; ++y) { + ConstRestrict row_rgb = rgb[c].Row(y); + ConstRestrict row_linear = linear[c].Row(y); + ConstRestrict row_alpha = rgb[3].Row(y); + for (size_t x = 0; x < xsize; x++) { + int value; + if (row_alpha[x] == 255) { + value = row_rgb[x]; + } else if (row_alpha[x] == 0) { + value = background; + } else { + const int fg_weight = row_alpha[x]; + const int bg_weight = 255 - fg_weight; + value = + (row_rgb[x] * fg_weight + background * bg_weight + 127) / 255; + } + row_linear[x] = kSrgbToLinearTable[value]; + } + } + } + } +} + +std::vector ReadImageOrDie(const char* filename) { + std::vector rgb; + FILE* f = fopen(filename, "r"); + if (!f) { + fprintf(stderr, "Cannot open %s\n", filename); + exit(1); + } + unsigned char magic[2]; + if (fread(magic, 1, 2, f) != 2) { + fprintf(stderr, "Cannot read from %s\n", filename); + exit(1); + } + if (magic[0] == 0xFF && magic[1] == 0xD8) { + if (!ReadJPEG(f, &rgb)) { + fprintf(stderr, "File %s is a malformed JPEG.\n", filename); + exit(1); + } + } else { + if (!ReadPNG(f, &rgb)) { + fprintf(stderr, "File %s is neither a valid JPEG nor a valid PNG.\n", + filename); + exit(1); + } + } + fclose(f); + return rgb; +} + +void CreateHeatMapImage(const ImageF& distmap, double good_threshold, + double bad_threshold, size_t xsize, size_t ysize, + std::vector* heatmap) { + heatmap->resize(3 * xsize * ysize); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + int px = xsize * y + x; + double d = distmap.Row(y)[x]; + uint8_t* rgb = &(*heatmap)[3 * px]; + if (d < 0.5 * good_threshold) { + rgb[0] = 0; + rgb[1] = d * (255 - 153) / (0.5 * good_threshold) + 153; + rgb[2] = 0; + } else if (d < good_threshold) { + d -= 0.5 * good_threshold; + rgb[0] = d * 255 / (0.5 * good_threshold); + rgb[1] = 255; + rgb[2] = 0; + } else if (d < bad_threshold) { + d -= good_threshold; + rgb[0] = 255; + rgb[1] = 255 - d * 255 / (bad_threshold - good_threshold); + rgb[2] = 0; + } else if (d < 5 * bad_threshold) { + rgb[0] = 255; + rgb[1] = 0; + rgb[2] = 255 * (d - bad_threshold) / (4 * bad_threshold); + } else if (d < 10 * bad_threshold) { + rgb[0] = 255; + rgb[1] = 255 * (d - 5 * bad_threshold) / (5 * bad_threshold); + rgb[2] = 255; + } else { + rgb[0] = 255; + rgb[1] = 255; + rgb[2] = 255; + } + } + } +} + +// main() function, within butteraugli namespace for convenience. +int Run(int argc, char* argv[]) { + if (argc != 3 && argc != 4) { + fprintf(stderr, + "Usage: %s {image1.(png|jpg|jpeg)} {image2.(png|jpg|jpeg)} " + "[heatmap.ppm]\n", + argv[0]); + return 1; + } + + std::vector rgb1 = ReadImageOrDie(argv[1]); + std::vector rgb2 = ReadImageOrDie(argv[2]); + + if (rgb1.size() != rgb2.size()) { + fprintf(stderr, "Different number of channels: %lu vs %lu\n", rgb1.size(), + rgb2.size()); + exit(1); + } + + for (size_t c = 0; c < rgb1.size(); ++c) { + if (rgb1[c].xsize() != rgb2[c].xsize() || + rgb1[c].ysize() != rgb2[c].ysize()) { + fprintf( + stderr, "The images are not equal in size: (%lu,%lu) vs (%lu,%lu)\n", + rgb1[c].xsize(), rgb2[c].xsize(), rgb1[c].ysize(), rgb2[c].ysize()); + return 1; + } + } + + // TODO: Figure out if it is a good idea to fetch the gamma from the image + // instead of applying sRGB conversion. + std::vector linear1, linear2; + // Overlay the image over a black background. + FromSrgbToLinear(rgb1, linear1, 0); + FromSrgbToLinear(rgb2, linear2, 0); + ImageF diff_map, diff_map_on_white; + double diff_value; + if (!butteraugli::ButteraugliInterface(linear1, linear2, diff_map, + diff_value)) { + fprintf(stderr, "Butteraugli comparison failed\n"); + return 1; + } + ImageF* diff_map_ptr = &diff_map; + if (rgb1.size() == 4 || rgb2.size() == 4) { + // If the alpha channel is present, overlay the image over a white + // background as well. + FromSrgbToLinear(rgb1, linear1, 255); + FromSrgbToLinear(rgb2, linear2, 255); + double diff_value_on_white; + if (!butteraugli::ButteraugliInterface(linear1, linear2, diff_map_on_white, + diff_value_on_white)) { + fprintf(stderr, "Butteraugli comparison failed\n"); + return 1; + } + if (diff_value_on_white > diff_value) { + diff_value = diff_value_on_white; + diff_map_ptr = &diff_map_on_white; + } + } + printf("%lf\n", diff_value); + + if (argc == 4) { + const double good_quality = ::butteraugli::ButteraugliFuzzyInverse(1.5); + const double bad_quality = ::butteraugli::ButteraugliFuzzyInverse(0.5); + std::vector rgb; + CreateHeatMapImage(*diff_map_ptr, good_quality, bad_quality, + rgb1[0].xsize(), rgb2[0].ysize(), &rgb); + FILE* const fmap = fopen(argv[3], "wb"); + if (fmap == NULL) { + fprintf(stderr, "Cannot open %s\n", argv[3]); + perror("fopen"); + return 1; + } + bool ok = true; + if (fprintf(fmap, "P6\n%lu %lu\n255\n", + rgb1[0].xsize(), rgb1[0].ysize()) < 0){ + perror("fprintf"); + ok = false; + } + if (ok && fwrite(rgb.data(), 1, rgb.size(), fmap) != rgb.size()) { + perror("fwrite"); + ok = false; + } + if (fclose(fmap) != 0) { + perror("fclose"); + ok = false; + } + if (!ok) return 1; + } + + return 0; +} + +} // namespace +} // namespace butteraugli + +int main(int argc, char** argv) { return butteraugli::Run(argc, argv); } diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/jpeg.BUILD b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/jpeg.BUILD new file mode 100644 index 0000000..92c9ddc --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/jpeg.BUILD @@ -0,0 +1,89 @@ +# Description: +# The Independent JPEG Group's JPEG runtime library. + +licenses(["notice"]) # custom notice-style license, see LICENSE + +cc_library( + name = "jpeg", + srcs = [ + "cderror.h", + "cdjpeg.h", + "jaricom.c", + "jcapimin.c", + "jcapistd.c", + "jcarith.c", + "jccoefct.c", + "jccolor.c", + "jcdctmgr.c", + "jchuff.c", + "jcinit.c", + "jcmainct.c", + "jcmarker.c", + "jcmaster.c", + "jcomapi.c", + "jconfig.h", + "jcparam.c", + "jcprepct.c", + "jcsample.c", + "jctrans.c", + "jdapimin.c", + "jdapistd.c", + "jdarith.c", + "jdatadst.c", + "jdatasrc.c", + "jdcoefct.c", + "jdcolor.c", + "jdct.h", + "jddctmgr.c", + "jdhuff.c", + "jdinput.c", + "jdmainct.c", + "jdmarker.c", + "jdmaster.c", + "jdmerge.c", + "jdpostct.c", + "jdsample.c", + "jdtrans.c", + "jerror.c", + "jfdctflt.c", + "jfdctfst.c", + "jfdctint.c", + "jidctflt.c", + "jidctfst.c", + "jidctint.c", + "jinclude.h", + "jmemmgr.c", + "jmemnobs.c", + "jmemsys.h", + "jmorecfg.h", + "jquant1.c", + "jquant2.c", + "jutils.c", + "jversion.h", + "transupp.h", + ], + hdrs = [ + "jerror.h", + "jpegint.h", + "jpeglib.h", + ], + includes = ["."], + visibility = ["//visibility:public"], +) + +genrule( + name = "configure", + outs = ["jconfig.h"], + cmd = "cat <$@\n" + + "#define HAVE_PROTOTYPES 1\n" + + "#define HAVE_UNSIGNED_CHAR 1\n" + + "#define HAVE_UNSIGNED_SHORT 1\n" + + "#define HAVE_STDDEF_H 1\n" + + "#define HAVE_STDLIB_H 1\n" + + "#ifdef WIN32\n" + + "#define INLINE __inline\n" + + "#else\n" + + "#define INLINE __inline__\n" + + "#endif\n" + + "EOF\n", +) diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/png.BUILD b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/png.BUILD new file mode 100644 index 0000000..9ff982b --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/png.BUILD @@ -0,0 +1,33 @@ +# Description: +# libpng is the official PNG reference library. + +licenses(["notice"]) # BSD/MIT-like license + +cc_library( + name = "png", + srcs = [ + "png.c", + "pngerror.c", + "pngget.c", + "pngmem.c", + "pngpread.c", + "pngread.c", + "pngrio.c", + "pngrtran.c", + "pngrutil.c", + "pngset.c", + "pngtrans.c", + "pngwio.c", + "pngwrite.c", + "pngwtran.c", + "pngwutil.c", + ], + hdrs = [ + "png.h", + "pngconf.h", + ], + includes = ["."], + linkopts = ["-lm"], + visibility = ["//visibility:public"], + deps = ["@zlib_archive//:zlib"], +) diff --git a/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/zlib.BUILD b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/zlib.BUILD new file mode 100644 index 0000000..edb77fd --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/third_party/butteraugli/zlib.BUILD @@ -0,0 +1,36 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # BSD/MIT-like license (for zlib) + +cc_library( + name = "zlib", + srcs = [ + "adler32.c", + "compress.c", + "crc32.c", + "crc32.h", + "deflate.c", + "deflate.h", + "gzclose.c", + "gzguts.h", + "gzlib.c", + "gzread.c", + "gzwrite.c", + "infback.c", + "inffast.c", + "inffast.h", + "inffixed.h", + "inflate.c", + "inflate.h", + "inftrees.c", + "inftrees.h", + "trees.c", + "trees.h", + "uncompr.c", + "zconf.h", + "zutil.c", + "zutil.h", + ], + hdrs = ["zlib.h"], + includes = ["."], +) diff --git a/packages/optimizt-guetzli/upstream/guetzli/tools/guetzli-compare.py b/packages/optimizt-guetzli/upstream/guetzli/tools/guetzli-compare.py new file mode 100644 index 0000000..3837c74 --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/tools/guetzli-compare.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +from __future__ import print_function +import os +import glob +import sys +import subprocess + +BA_CMDLINE = './butteraugli {0} {1} {2}' +GUETZLI_CMDLINE = './guetzli -quality {2} {0} {1}' +OTHER_CMDLINE = sys.argv[3] + +def run(cmdline): + print('running {}'.format(cmdline), file=sys.stderr) + return subprocess.check_output(cmdline, shell=True) + +def size(filename): + return os.stat(filename).st_size + +def ba_distance(orig, compressed): + return float(run(BA_CMDLINE.format(orig, compressed, compressed + ".diffmap.pnm"))) + +def handle_png(png): + other_jpeg = png + "." + sys.argv[1] + ".other.jpg" + guetzli_jpeg = png + "." + sys.argv[1] + ".guetzli.jpg" + run(OTHER_CMDLINE.format(png, other_jpeg)) + other_distance = ba_distance(png, other_jpeg) + left = 84.0 + right = 110.0 + while right - left > 0.05: + q = (left + right) / 2 + run(GUETZLI_CMDLINE.format(png, guetzli_jpeg, q)) + guetzli_distance = ba_distance(png, guetzli_jpeg) + if guetzli_distance < other_distance: + right = q + else: + left = q + run(GUETZLI_CMDLINE.format(png, guetzli_jpeg, right)) + guetzli_distance = ba_distance(png, guetzli_jpeg) + assert guetzli_distance < other_distance + return (size(guetzli_jpeg), size(other_jpeg)) + +pngs = glob.glob(sys.argv[2]) +sizes = (0, 0) +for png in pngs: + this_size = handle_png(png) + print(png, this_size) + sizes = (sizes[0] + this_size[0], sizes[1] + this_size[1]) +print(sizes) diff --git a/packages/optimizt-guetzli/upstream/guetzli/zlib.BUILD b/packages/optimizt-guetzli/upstream/guetzli/zlib.BUILD new file mode 100644 index 0000000..edb77fd --- /dev/null +++ b/packages/optimizt-guetzli/upstream/guetzli/zlib.BUILD @@ -0,0 +1,36 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # BSD/MIT-like license (for zlib) + +cc_library( + name = "zlib", + srcs = [ + "adler32.c", + "compress.c", + "crc32.c", + "crc32.h", + "deflate.c", + "deflate.h", + "gzclose.c", + "gzguts.h", + "gzlib.c", + "gzread.c", + "gzwrite.c", + "infback.c", + "inffast.c", + "inffast.h", + "inffixed.h", + "inflate.c", + "inflate.h", + "inftrees.c", + "inftrees.h", + "trees.c", + "trees.h", + "uncompr.c", + "zconf.h", + "zutil.c", + "zutil.h", + ], + hdrs = ["zlib.h"], + includes = ["."], +) diff --git a/packages/optimizt/.optimiztrc.cjs b/packages/optimizt/.optimiztrc.cjs index 148dcd4..acc581a 100644 --- a/packages/optimizt/.optimiztrc.cjs +++ b/packages/optimizt/.optimiztrc.cjs @@ -15,9 +15,7 @@ module.exports = { }, // https://github.com/google/guetzli lossless: { - quality: 90, // visual quality to aim for, expressed as a JPEG quality value; should be >= 84, otherwise the output will have noticeable artifacts - memlimit: 6000, // memory limit in MB; guetzli will fail if unable to stay under the limit - nomemlimit: false, // do not limit memory usage + quality: 90, // visual quality to aim for, expressed as a JPEG quality value; must be between 84 and 110 }, }, @@ -55,7 +53,7 @@ module.exports = { gamma: 1, // set the color space/gamma correction to gamma, which can be a real number, ‘srgb’ (the default), or ‘oklab’ }, lossless: { - optimize: 0, + optimize: 3, careful: true, colors: 256, lossy: 0, diff --git a/packages/optimizt/CHANGELOG.md b/packages/optimizt/CHANGELOG.md index 253da3f..d718751 100644 --- a/packages/optimizt/CHANGELOG.md +++ b/packages/optimizt/CHANGELOG.md @@ -10,9 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `--no-color` to disable colored output and `--debug` to include stack traces and version details in errors. +- Added Guetzli WebAssembly memory64 support for JPEG processing beyond the 4 GiB wasm32 address-space limit. ### Changed +- **BREAKING:** Guetzli now uses wasm64 and requires Node.js 24.0.0 or newer. - **BREAKING:** Processing now exits with status `1` when argument validation, filesystem preflight, configuration loading, or any image operation fails. A valid run with no eligible images still exits `0`. - **BREAKING:** Status, progress, warnings, errors, and summaries now use `stderr`. `stdout` is reserved for help and version output and remains empty during image processing. - **BREAKING:** Missing or inaccessible explicit operands and explicitly supplied files with unsupported extensions now fail instead of being silently ignored. Unsupported files found while traversing directories remain ignored. diff --git a/packages/optimizt/MIGRATION.md b/packages/optimizt/MIGRATION.md index 3b10e6f..a81db2d 100644 --- a/packages/optimizt/MIGRATION.md +++ b/packages/optimizt/MIGRATION.md @@ -2,7 +2,9 @@ ## 13.0.0 → Unreleased -Optimizt now treats the exit status and files on disk as the command result. Review scripts and custom configuration for the changes below. +Optimizt now requires Node.js 24.0.0 or newer because its bundled Guetzli runtime uses WebAssembly memory64. + +It also treats the exit status and files on disk as the command result. Review scripts and custom configuration for the changes below. ### Use the exit status, not log text diff --git a/packages/optimizt/lib/guetzli.js b/packages/optimizt/lib/guetzli.js new file mode 100644 index 0000000..e5595b5 --- /dev/null +++ b/packages/optimizt/lib/guetzli.js @@ -0,0 +1,61 @@ +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { optionsToArguments } from './options-to-arguments.js'; + +const DIAGNOSTIC_LIMIT = 64 * 1024; + +export const nodeExecutable = process.execPath; +export const guetzliRunner = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../vendor/guetzli/cli.js', +); + +export function encodeWithGuetzli(input, options, lifecycle, { + command = nodeExecutable, + runner = guetzliRunner, +} = {}) { + return new Promise((resolve, reject) => { + const child = spawn(command, [runner, ...optionsToArguments({ options })]); + lifecycle.registerChild(child); + const stdout = []; + let diagnostic = Buffer.alloc(0); + let isTruncated = false; + let isSettled = false; + + child.stdout.on('data', (chunk) => { + stdout.push(chunk); + }); + child.stderr.on('data', (chunk) => { + diagnostic = Buffer.concat([diagnostic, chunk]); + if (diagnostic.length > DIAGNOSTIC_LIMIT) { + diagnostic = diagnostic.subarray(diagnostic.length - DIAGNOSTIC_LIMIT); + isTruncated = true; + } + }); + child.once('error', error => rejectOnce(new Error(`Guetzli failed to start: ${error.message}`, { cause: error }))); + child.stdin.on('error', (error) => { + if (error.code !== 'EPIPE') rejectOnce(new Error(`Could not send input to Guetzli: ${error.message}`, { cause: error })); + }); + child.once('close', (code, signal) => { + if (isSettled) return; + if (code === 0) { + isSettled = true; + resolve(Buffer.concat(stdout)); + return; + } + const reason = diagnostic.toString('utf8').trim(); + const exit = signal ? `terminated by signal ${signal}` : `exited with code ${code}`; + const prefix = isTruncated ? '[stderr truncated] ' : ''; + rejectOnce(new Error(`${prefix}${reason || 'Guetzli failed'} (${exit})`)); + }); + child.stdin.end(input); + + function rejectOnce(error) { + if (isSettled) return; + isSettled = true; + reject(error); + } + }); +} diff --git a/packages/optimizt/package.json b/packages/optimizt/package.json index ba29a15..6bbf6a6 100644 --- a/packages/optimizt/package.json +++ b/packages/optimizt/package.json @@ -37,7 +37,7 @@ "vendor/" ], "scripts": { - "build": "node ../optimizt-sharp/scripts/build.mjs && node scripts/vendor-sharp.mjs", + "build": "node ../optimizt-sharp/scripts/build.mjs && node scripts/vendor-sharp.mjs && node scripts/vendor-guetzli.mjs", "lint": "eslint ./", "prepack": "npm run build", "test": "npm run build && vitest run", @@ -54,8 +54,7 @@ "semver": "^7.8.4", "svgo": "4.0.1", "write-file-atomic": "7.0.1", - "@343dev/gifsicle": "1.2.0", - "@343dev/guetzli": "1.3.0" + "@343dev/gifsicle": "1.2.0" }, "devDependencies": { "@343dev/eslint-config": "5.0.0", diff --git a/packages/optimizt/scripts/vendor-guetzli.mjs b/packages/optimizt/scripts/vendor-guetzli.mjs new file mode 100644 index 0000000..41c855f --- /dev/null +++ b/packages/optimizt/scripts/vendor-guetzli.mjs @@ -0,0 +1,18 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const workspaceDirectory = path.resolve(packageDirectory, '..', 'optimizt-guetzli'); +const destinationDirectory = path.join(packageDirectory, 'vendor', 'guetzli'); + +await fs.rm(destinationDirectory, { force: true, recursive: true }); +await fs.mkdir(destinationDirectory, { recursive: true }); +await Promise.all([ + fs.cp(path.join(workspaceDirectory, 'dist'), path.join(destinationDirectory, 'dist'), { recursive: true }), + fs.copyFile(path.join(workspaceDirectory, 'cli.js'), path.join(destinationDirectory, 'cli.js')), + fs.copyFile(path.join(workspaceDirectory, 'LICENSE'), path.join(destinationDirectory, 'LICENSE')), + fs.copyFile(path.join(workspaceDirectory, 'MUSL-COPYRIGHT'), path.join(destinationDirectory, 'MUSL-COPYRIGHT')), + fs.copyFile(path.join(workspaceDirectory, 'THIRD_PARTY_NOTICES.md'), path.join(destinationDirectory, 'THIRD_PARTY_NOTICES.md')), + fs.copyFile(path.join(workspaceDirectory, 'UPSTREAM.md'), path.join(destinationDirectory, 'UPSTREAM.md')), +]); From bcfc284ae84d6f9276688ec45df2db36a51a03cb Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:30:18 +0700 Subject: [PATCH 4/6] feat(wasm): add a WASM-only Gifsicle runtime Vendor a reproducible wasm64 Gifsicle build with Worker-based cancellation, provenance sources, parity evidence, and codec-specific license notices. --- .dockerignore | 2 + .githooks/pre-commit | 2 - README.md | 5 +- package-lock.json | 29 +- packages/optimizt-gifsicle/.gitattributes | 7 + .../optimizt-gifsicle/GIFSICLE-LICENSE.md | 27 + packages/optimizt-gifsicle/LICENSE | 339 ++ packages/optimizt-gifsicle/MUSL-COPYRIGHT | 193 + packages/optimizt-gifsicle/README.md | 46 + .../optimizt-gifsicle/THIRD_PARTY_NOTICES.md | 134 + packages/optimizt-gifsicle/UPSTREAM.md | 50 + packages/optimizt-gifsicle/dist/gifsicle.mjs | 2 + packages/optimizt-gifsicle/dist/gifsicle.wasm | Bin 0 -> 132971 bytes packages/optimizt-gifsicle/eslint.config.js | 13 + packages/optimizt-gifsicle/index.js | 2 + packages/optimizt-gifsicle/lib/errors.js | 17 + packages/optimizt-gifsicle/lib/optimize.js | 137 + packages/optimizt-gifsicle/lib/options.js | 71 + packages/optimizt-gifsicle/lib/worker.js | 107 + packages/optimizt-gifsicle/package.json | 34 + .../optimizt-gifsicle/scripts/benchmark.js | 105 + .../scripts/build-native-container.sh | 15 + .../optimizt-gifsicle/scripts/build-native.sh | 80 + .../optimizt-gifsicle/scripts/build-wasm.sh | 116 + .../scripts/emsdk-version.sh | 3 + .../scripts/native-reference.js | 64 + .../optimizt-gifsicle/scripts/setup-emsdk.sh | 45 + .../optimizt-gifsicle/scripts/verify-dist.sh | 52 + .../scripts/verify-native-sanitizers.sh | 26 + .../scripts/verify-parity.js | 57 + .../optimizt-gifsicle/scripts/verify-patch.sh | 15 + .../scripts/verify-upstream.js | 84 + packages/optimizt-gifsicle/src/wasm/config.h | 57 + .../src/wasm/native-driver.c | 74 + .../optimizt-gifsicle/src/wasm/stable-qsort.c | 76 + packages/optimizt-gifsicle/test/api.test.js | 189 + .../test/fixtures/animated.gif | Bin 0 -> 8703 bytes .../test/fixtures/empty-transparent.gif | Bin 0 -> 219 bytes .../test/fixtures/extensions.gif | Bin 0 -> 856 bytes .../test/fixtures/local-palette.gif | Bin 0 -> 821 bytes .../test/fixtures/megapixel.gif | Bin 0 -> 1790 bytes .../fixtures/recoverable-trailing-garbage.gif | Bin 0 -> 66 bytes .../test/fixtures/single.gif | Bin 0 -> 821 bytes .../test/fixtures/transparent.gif | Bin 0 -> 242 bytes .../test/fixtures/zero-dimensions.gif | Bin 0 -> 35 bytes .../optimizt-gifsicle/test/limits.test.js | 89 + .../test/native-reference.test.js | 58 + .../upstream/gifsicle-wasm.patch | 771 ++++ .../optimizt-gifsicle/upstream/gifsicle.modes | 59 + .../upstream/gifsicle.sha256 | 59 + .../upstream/gifsicle/.appveyor.yml | 10 + .../gifsicle/.github/workflows/tests.yml | 26 + .../upstream/gifsicle/.gitignore | 30 + .../upstream/gifsicle/.travis.yml | 37 + .../upstream/gifsicle/COPYING | 339 ++ .../upstream/gifsicle/INSTALL | 176 + .../upstream/gifsicle/Makefile.am | 50 + .../upstream/gifsicle/NEWS.md | 1207 +++++++ .../upstream/gifsicle/README.md | 135 + .../upstream/gifsicle/bootstrap.sh | 2 + .../upstream/gifsicle/configure.ac | 317 ++ .../upstream/gifsicle/gifdiff.1 | 97 + .../upstream/gifsicle/gifsicle.1 | 1404 ++++++++ .../upstream/gifsicle/gifview.1 | 312 ++ .../upstream/gifsicle/include/lcdf/clp.h | 337 ++ .../upstream/gifsicle/include/lcdf/inttypes.h | 31 + .../upstream/gifsicle/include/lcdfgif/gif.h | 320 ++ .../upstream/gifsicle/include/lcdfgif/gifx.h | 88 + .../upstream/gifsicle/logo.gif | Bin 0 -> 8703 bytes .../upstream/gifsicle/logo1.gif | Bin 0 -> 821 bytes .../upstream/gifsicle/src/Makefile.am | 35 + .../upstream/gifsicle/src/Makefile.bcc | 80 + .../upstream/gifsicle/src/Makefile.mingw | 66 + .../upstream/gifsicle/src/Makefile.w32 | 62 + .../upstream/gifsicle/src/clp.c | 2512 +++++++++++++ .../upstream/gifsicle/src/fmalloc.c | 39 + .../upstream/gifsicle/src/gifdiff.c | 632 ++++ .../upstream/gifsicle/src/giffunc.c | 844 +++++ .../upstream/gifsicle/src/gifread.c | 979 +++++ .../upstream/gifsicle/src/gifsicle.c | 2236 ++++++++++++ .../upstream/gifsicle/src/gifsicle.h | 350 ++ .../upstream/gifsicle/src/giftoc.c | 222 ++ .../upstream/gifsicle/src/gifunopt.c | 238 ++ .../upstream/gifsicle/src/gifview.c | 1419 ++++++++ .../upstream/gifsicle/src/gifwrite.c | 1214 +++++++ .../upstream/gifsicle/src/gifx.c | 852 +++++ .../upstream/gifsicle/src/kcolor.c | 708 ++++ .../upstream/gifsicle/src/kcolor.h | 354 ++ .../upstream/gifsicle/src/merge.c | 403 +++ .../upstream/gifsicle/src/optimize.c | 467 +++ .../upstream/gifsicle/src/opttemplate.c | 960 +++++ .../upstream/gifsicle/src/quantize.c | 1477 ++++++++ .../upstream/gifsicle/src/strerror.c | 25 + .../upstream/gifsicle/src/support.c | 1783 ++++++++++ .../upstream/gifsicle/src/ungifwrt.c | 847 +++++ .../upstream/gifsicle/src/win32cfg.h | 203 ++ .../upstream/gifsicle/src/xform.c | 1405 ++++++++ .../gifsicle/test/001-transexpand.testie | 47 + .../gifsicle/test/002-cropresize.testie | 505 +++ .../upstream/gifsicle/test/003-bgopt.testie | 3140 +++++++++++++++++ .../upstream/gifsicle/test/004-careful.testie | 538 +++ .../upstream/gifsicle/test/005-resize.testie | 62 + .../gifsicle/test/006-unoptdisposal.testie | 12 + .../gifsicle/test/007-alltransp.testie | 25 + .../gifsicle/test/008-resizemix.testie | 29 + .../gifsicle/test/009-zerowidth.testie | 68 + .../gifsicle/test/010-warnings.testie | 20 + .../gifsicle/test/011-resizemix.testie | 8 + .../upstream/gifsicle/test/testie | 1937 ++++++++++ .../verification/benchmark.json | 98 + .../verification/native-reference.md | 27 + .../verification/parity-manifest.json | 373 ++ packages/optimizt/CHANGELOG.md | 2 +- packages/optimizt/MIGRATION.md | 2 +- packages/optimizt/convert.js | 199 +- packages/optimizt/lib/gifsicle.js | 1 + packages/optimizt/lib/guetzli.js | 53 - packages/optimizt/lib/lifecycle.js | 24 +- packages/optimizt/optimize.js | 288 +- packages/optimizt/package.json | 5 +- packages/optimizt/scripts/vendor-gifsicle.mjs | 80 + packages/optimizt/tests/lifecycle.test.js | 73 + 122 files changed, 36095 insertions(+), 162 deletions(-) create mode 100644 packages/optimizt-gifsicle/.gitattributes create mode 100644 packages/optimizt-gifsicle/GIFSICLE-LICENSE.md create mode 100644 packages/optimizt-gifsicle/LICENSE create mode 100644 packages/optimizt-gifsicle/MUSL-COPYRIGHT create mode 100644 packages/optimizt-gifsicle/README.md create mode 100644 packages/optimizt-gifsicle/THIRD_PARTY_NOTICES.md create mode 100644 packages/optimizt-gifsicle/UPSTREAM.md create mode 100644 packages/optimizt-gifsicle/dist/gifsicle.mjs create mode 100644 packages/optimizt-gifsicle/dist/gifsicle.wasm create mode 100644 packages/optimizt-gifsicle/eslint.config.js create mode 100644 packages/optimizt-gifsicle/index.js create mode 100644 packages/optimizt-gifsicle/lib/errors.js create mode 100644 packages/optimizt-gifsicle/lib/optimize.js create mode 100644 packages/optimizt-gifsicle/lib/options.js create mode 100644 packages/optimizt-gifsicle/lib/worker.js create mode 100644 packages/optimizt-gifsicle/package.json create mode 100644 packages/optimizt-gifsicle/scripts/benchmark.js create mode 100755 packages/optimizt-gifsicle/scripts/build-native-container.sh create mode 100755 packages/optimizt-gifsicle/scripts/build-native.sh create mode 100755 packages/optimizt-gifsicle/scripts/build-wasm.sh create mode 100644 packages/optimizt-gifsicle/scripts/emsdk-version.sh create mode 100644 packages/optimizt-gifsicle/scripts/native-reference.js create mode 100755 packages/optimizt-gifsicle/scripts/setup-emsdk.sh create mode 100755 packages/optimizt-gifsicle/scripts/verify-dist.sh create mode 100755 packages/optimizt-gifsicle/scripts/verify-native-sanitizers.sh create mode 100644 packages/optimizt-gifsicle/scripts/verify-parity.js create mode 100755 packages/optimizt-gifsicle/scripts/verify-patch.sh create mode 100644 packages/optimizt-gifsicle/scripts/verify-upstream.js create mode 100644 packages/optimizt-gifsicle/src/wasm/config.h create mode 100644 packages/optimizt-gifsicle/src/wasm/native-driver.c create mode 100644 packages/optimizt-gifsicle/src/wasm/stable-qsort.c create mode 100644 packages/optimizt-gifsicle/test/api.test.js create mode 100644 packages/optimizt-gifsicle/test/fixtures/animated.gif create mode 100644 packages/optimizt-gifsicle/test/fixtures/empty-transparent.gif create mode 100644 packages/optimizt-gifsicle/test/fixtures/extensions.gif create mode 100644 packages/optimizt-gifsicle/test/fixtures/local-palette.gif create mode 100644 packages/optimizt-gifsicle/test/fixtures/megapixel.gif create mode 100644 packages/optimizt-gifsicle/test/fixtures/recoverable-trailing-garbage.gif create mode 100644 packages/optimizt-gifsicle/test/fixtures/single.gif create mode 100644 packages/optimizt-gifsicle/test/fixtures/transparent.gif create mode 100644 packages/optimizt-gifsicle/test/fixtures/zero-dimensions.gif create mode 100644 packages/optimizt-gifsicle/test/limits.test.js create mode 100644 packages/optimizt-gifsicle/test/native-reference.test.js create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle-wasm.patch create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle.modes create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle.sha256 create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/.appveyor.yml create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/.github/workflows/tests.yml create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/.gitignore create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/.travis.yml create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/COPYING create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/INSTALL create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/Makefile.am create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/NEWS.md create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/README.md create mode 100755 packages/optimizt-gifsicle/upstream/gifsicle/bootstrap.sh create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/configure.ac create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/gifdiff.1 create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/gifsicle.1 create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/gifview.1 create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/include/lcdf/clp.h create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/include/lcdf/inttypes.h create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/include/lcdfgif/gif.h create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/include/lcdfgif/gifx.h create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/logo.gif create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/logo1.gif create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.am create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.bcc create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.mingw create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.w32 create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/clp.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/fmalloc.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/gifdiff.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/giffunc.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/gifread.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/gifsicle.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/gifsicle.h create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/giftoc.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/gifunopt.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/gifview.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/gifwrite.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/gifx.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/kcolor.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/kcolor.h create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/merge.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/optimize.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/opttemplate.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/quantize.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/strerror.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/support.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/ungifwrt.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/win32cfg.h create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/src/xform.c create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/001-transexpand.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/002-cropresize.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/003-bgopt.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/004-careful.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/005-resize.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/006-unoptdisposal.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/007-alltransp.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/008-resizemix.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/009-zerowidth.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/010-warnings.testie create mode 100644 packages/optimizt-gifsicle/upstream/gifsicle/test/011-resizemix.testie create mode 100755 packages/optimizt-gifsicle/upstream/gifsicle/test/testie create mode 100644 packages/optimizt-gifsicle/verification/benchmark.json create mode 100644 packages/optimizt-gifsicle/verification/native-reference.md create mode 100644 packages/optimizt-gifsicle/verification/parity-manifest.json create mode 100644 packages/optimizt/lib/gifsicle.js create mode 100644 packages/optimizt/scripts/vendor-gifsicle.mjs diff --git a/.dockerignore b/.dockerignore index 85314b9..0fa7a69 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,8 @@ !packages/optimizt-sharp/** !packages/optimizt-guetzli/ !packages/optimizt-guetzli/** +!packages/optimizt-gifsicle/ +!packages/optimizt-gifsicle/** packages/optimizt-sharp/dist/ packages/optimizt/vendor/ node_modules/ diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 5044342..d63ee5b 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -4,5 +4,3 @@ # so it must run once per workspace rather than from the repository root. npx lint-staged --cwd packages/optimizt npx lint-staged --cwd packages/optimizt-sharp - -npx lint-staged --cwd packages/optimizt-guetzli diff --git a/README.md b/README.md index ac07cf4..e6b1611 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Optimizt monorepo -This repository contains three workspaces: +This repository contains four workspaces: - [`@343dev/optimizt`](packages/optimizt) — the published Optimizt command-line image optimizer. - [`@343dev/optimizt-sharp`](packages/optimizt-sharp) — a private build workspace that reproducibly generates the WASM-only Sharp distribution vendored into Optimizt. - [`@343dev/optimizt-guetzli`](packages/optimizt-guetzli) — the private Guetzli WebAssembly build and provenance workspace. +- [`@343dev/optimizt-gifsicle`](packages/optimizt-gifsicle) — the private Gifsicle WebAssembly build and provenance workspace. ## Development @@ -13,4 +14,4 @@ npm ci npm run check ``` -`npm run build` downloads the exact upstream Sharp tarball pinned by `packages/optimizt-sharp/package.json`, verifies its integrity, generates the wrapper, and copies both codec runtimes into the ignored `packages/optimizt/vendor` directory. Only `@343dev/optimizt` is published. +`npm run build` downloads the exact upstream Sharp tarball pinned by `packages/optimizt-sharp/package.json`, verifies its integrity, generates the wrapper, and copies all three codec runtimes into the ignored `packages/optimizt/vendor` directory. Only `@343dev/optimizt` is published. diff --git a/package-lock.json b/package-lock.json index 8bbe3fc..67c7c39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,22 +36,14 @@ "eslint": "^10.5.0" } }, - "node_modules/@343dev/gifsicle": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@343dev/gifsicle/-/gifsicle-1.2.0.tgz", - "integrity": "sha512-sJMxT5SCdnAgf3j7GIXTYecRLPG/NYsCdPpjYhoXesVr2NqdH4HZsl6+j5M9Q5HtA/Ee4AOkTQRd98hlYHFW6A==", - "license": "GPL-2.0-only", - "bin": { - "gifsicle": "cli.js" - }, - "engines": { - "node": ">=18.17" - } - }, "node_modules/@343dev/optimizt": { "resolved": "packages/optimizt", "link": true }, + "node_modules/@343dev/optimizt-gifsicle": { + "resolved": "packages/optimizt-gifsicle", + "link": true + }, "node_modules/@343dev/optimizt-guetzli": { "resolved": "packages/optimizt-guetzli", "link": true @@ -3986,7 +3978,6 @@ "version": "13.0.0", "license": "MIT", "dependencies": { - "@343dev/gifsicle": "1.2.0", "@img/colour": "^1.1.0", "@img/sharp-wasm32": "0.35.2", "cli-progress": "3.12.0", @@ -4011,6 +4002,18 @@ "node": ">=24.0.0" } }, + "packages/optimizt-gifsicle": { + "name": "@343dev/optimizt-gifsicle", + "version": "1.96.0-0", + "license": "SEE LICENSE IN GIFSICLE-LICENSE.md", + "devDependencies": { + "@343dev/eslint-config": "5.0.0", + "eslint": "10.5.0" + }, + "engines": { + "node": ">=24.0.0" + } + }, "packages/optimizt-guetzli": { "name": "@343dev/optimizt-guetzli", "version": "1.0.1-0", diff --git a/packages/optimizt-gifsicle/.gitattributes b/packages/optimizt-gifsicle/.gitattributes new file mode 100644 index 0000000..e2322d1 --- /dev/null +++ b/packages/optimizt-gifsicle/.gitattributes @@ -0,0 +1,7 @@ +# Keep project text files consistent across platforms. +* text=auto eol=lf + +# The upstream provenance manifests and snapshot must retain repository bytes. +upstream/gifsicle.sha256 text eol=lf +upstream/gifsicle.modes text eol=lf +upstream/gifsicle/** -text diff --git a/packages/optimizt-gifsicle/GIFSICLE-LICENSE.md b/packages/optimizt-gifsicle/GIFSICLE-LICENSE.md new file mode 100644 index 0000000..5dd50e4 --- /dev/null +++ b/packages/optimizt-gifsicle/GIFSICLE-LICENSE.md @@ -0,0 +1,27 @@ +Copyright/License +----------------- + +All source code is Copyright (C) 1997-2025 Eddie Kohler. + +IF YOU PLAN TO USE GIFSICLE ONLY TO CREATE OR MODIFY GIF IMAGES, DON'T +WORRY ABOUT THE REST OF THIS SECTION. Anyone can use Gifsicle however +they wish; the license applies only to those who plan to copy, +distribute, or alter its code. If you use Gifsicle for an +organizational or commercial Web site, I would appreciate a link to +the Gifsicle home page on any 'About This Server' page, but it's not +required. + +This code is distributed under the GNU General Public License, Version +2 (and only Version 2). The GNU General Public License is available +via the Web at or in the +'COPYING' file in this directory. + +The following alternative license may be used at your discretion. + +Permission is granted to copy, distribute, or alter Gifsicle, whole or +in part, as long as source code copyright notices are kept intact, +with the following restriction: Developers or distributors who plan to +use Gifsicle code, whole or in part, in a product whose source code +will not be made available to the end user -- more precisely, in a +context which would violate the GPL -- MUST contact the author and +obtain permission before doing so. diff --git a/packages/optimizt-gifsicle/LICENSE b/packages/optimizt-gifsicle/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/packages/optimizt-gifsicle/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/packages/optimizt-gifsicle/MUSL-COPYRIGHT b/packages/optimizt-gifsicle/MUSL-COPYRIGHT new file mode 100644 index 0000000..2f15edc --- /dev/null +++ b/packages/optimizt-gifsicle/MUSL-COPYRIGHT @@ -0,0 +1,193 @@ +musl as a whole is licensed under the following standard MIT license: + +---------------------------------------------------------------------- +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------- + +Authors/contributors include: + +A. Wilcox +Ada Worcester +Alex Dowad +Alex Suykov +Alexander Monakov +Andre McCurdy +Andrew Kelley +Anthony G. Basile +Aric Belsito +Arvid Picciani +Bartosz Brachaczek +Benjamin Peterson +Bobby Bingham +Boris Brezillon +Brent Cook +Chris Spiegel +Clément Vasseur +Daniel Micay +Daniel Sabogal +Daurnimator +David Carlier +David Edelsohn +Denys Vlasenko +Dmitry Ivanov +Dmitry V. Levin +Drew DeVault +Emil Renner Berthing +Fangrui Song +Felix Fietkau +Felix Janda +Gianluca Anzolin +Hauke Mehrtens +He X +Hiltjo Posthuma +Isaac Dunham +Jaydeep Patil +Jens Gustedt +Jeremy Huntwork +Jo-Philipp Wich +Joakim Sindholt +John Spencer +Julien Ramseier +Justin Cormack +Kaarle Ritvanen +Khem Raj +Kylie McClain +Leah Neukirchen +Luca Barbato +Luka Perkov +Lynn Ochs +M Farkas-Dyck (Strake) +Mahesh Bodapati +Markus Wichmann +Masanori Ogino +Michael Clark +Michael Forney +Mikhail Kremnyov +Natanael Copa +Nicholas J. Kain +orc +Pascal Cuoq +Patrick Oppenlander +Petr Hosek +Petr Skocik +Pierre Carrier +Reini Urban +Rich Felker +Richard Pennington +Ryan Fairfax +Samuel Holland +Segev Finer +Shiz +sin +Solar Designer +Stefan Kristiansson +Stefan O'Rear +Szabolcs Nagy +Timo Teräs +Trutz Behn +Will Dietz +William Haddon +William Pitcock + +Portions of this software are derived from third-party works licensed +under terms compatible with the above MIT license: + +The TRE regular expression implementation (src/regex/reg* and +src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed +under a 2-clause BSD license (license text in the source files). The +included version has been heavily modified by Rich Felker in 2012, in +the interests of size, simplicity, and namespace cleanliness. + +Much of the math library code (src/math/* and src/complex/*) is +Copyright © 1993,2004 Sun Microsystems or +Copyright © 2003-2011 David Schultz or +Copyright © 2003-2009 Steven G. Kargl or +Copyright © 2003-2009 Bruce D. Evans or +Copyright © 2008 Stephen L. Moshier or +Copyright © 2017-2018 Arm Limited +and labelled as such in comments in the individual source files. All +have been licensed under extremely permissive terms. + +The ARM memcpy code (src/string/arm/memcpy.S) is Copyright © 2008 +The Android Open Source Project and is licensed under a two-clause BSD +license. It was taken from Bionic libc, used on Android. + +The AArch64 memcpy and memset code (src/string/aarch64/*) are +Copyright © 1999-2019, Arm Limited. + +The implementation of DES for crypt (src/crypt/crypt_des.c) is +Copyright © 1994 David Burren. It is licensed under a BSD license. + +The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was +originally written by Solar Designer and placed into the public +domain. The code also comes with a fallback permissive license for use +in jurisdictions that may not recognize the public domain. + +The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011 +Lynn Ochs and is licensed under an MIT-style license. + +The x86_64 port was written by Nicholas J. Kain and is licensed under +the standard MIT terms. + +The mips and microblaze ports were originally written by Richard +Pennington for use in the ellcc project. The original code was adapted +by Rich Felker for build system and code conventions during upstream +integration. It is licensed under the standard MIT terms. + +The mips64 port was contributed by Imagination Technologies and is +licensed under the standard MIT terms. + +The powerpc port was also originally written by Richard Pennington, +and later supplemented and integrated by John Spencer. It is licensed +under the standard MIT terms. + +All other files which have no copyright comments are original works +produced specifically for use as part of this library, written either +by Rich Felker, the main author of the library, or by one or more +contibutors listed above. Details on authorship of individual files +can be found in the git version control history of the project. The +omission of copyright and license comments in each file is in the +interest of source tree size. + +In addition, permission is hereby granted for all public header files +(include/* and arch/*/bits/*) and crt files intended to be linked into +applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit +the copyright notice and permission notice otherwise required by the +license, and to use these files without any requirement of +attribution. These files include substantial contributions from: + +Bobby Bingham +John Spencer +Nicholas J. Kain +Rich Felker +Richard Pennington +Stefan Kristiansson +Szabolcs Nagy + +all of whom have explicitly granted such permission. + +This file previously contained text expressing a belief that most of +the files covered by the above exception were sufficiently trivial not +to be subject to copyright, resulting in confusion over whether it +negated the permissions granted in the license. In the spirit of +permissive licensing, and of not having licensing issues being an +obstacle to adoption, that text has been removed. diff --git a/packages/optimizt-gifsicle/README.md b/packages/optimizt-gifsicle/README.md new file mode 100644 index 0000000..73de26a --- /dev/null +++ b/packages/optimizt-gifsicle/README.md @@ -0,0 +1,46 @@ +# Gifsicle WebAssembly for Optimizt + +Gifsicle v1.96 compiled to WebAssembly for exclusive use inside the Optimizt +monorepo. This directory is not a public package. + +The internal runtime accepts a `Buffer` or `Uint8Array` containing one GIF and +returns a `Promise`. Every operation uses a fresh Worker Thread and +WebAssembly instance. The module uses wasm64 linear memory with a 16 GiB maximum +and requires Node.js 24.0.0 or newer. It has no filesystem interface, runtime +dependencies, native executables, install scripts, platform allowlist, or +first-run downloads. + +Supported options are: + +- `optimize`: `false` or an integer from 0 through 3 +- `careful`: a boolean +- `colors`: `false` or an integer from 2 through 256 +- `lossy`: `false` or an integer from 0 through 2,147,483,647 +- `gamma`: `false`, a positive finite number, `srgb`, or `oklab` + +Encoded input is limited to 128 MiB. Parsing also limits GIFs to 100,000 frames, +a 134,217,728-pixel logical canvas, and 134,217,728 total frame pixels. + +See [UPSTREAM.md](UPSTREAM.md) for source and toolchain provenance. + +## Development + +Install the monorepo dependencies from the repository root: + +```sh +npm ci --ignore-scripts +``` + +Before completing a change, run from the repository root: + +```sh +npm run lint --workspace @343dev/optimizt-gifsicle +npm test --workspace @343dev/optimizt-gifsicle +``` + +Changes to the bridge, build scripts, Emscripten version, or committed +distribution also require: + +```sh +npm run verify:dist --workspace @343dev/optimizt-gifsicle +``` diff --git a/packages/optimizt-gifsicle/THIRD_PARTY_NOTICES.md b/packages/optimizt-gifsicle/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..d65343f --- /dev/null +++ b/packages/optimizt-gifsicle/THIRD_PARTY_NOTICES.md @@ -0,0 +1,134 @@ +# Third-party notices + +This package contains software compiled from third-party source code. + +## Gifsicle + +Gifsicle 1.96 and the LCDF GIF library + +Copyright (C) 1997-2025 Eddie Kohler. + +Upstream Gifsicle offers the GNU General Public License, Version 2 (and only +Version 2), or the alternative license offered by its author. The bundled +Gifsicle in this Optimizt distribution is distributed under that alternative +license. The original upstream licensing text, including both options, is +reproduced verbatim in `GIFSICLE-LICENSE.md`. The complete GNU General Public +License Version 2 text remains available in `LICENSE` as the upstream GPLv2 +licensing option. + +In the vendored runtime distributed with Optimizt, `SOURCE.md` identifies the +exact corresponding source for the WebAssembly binary. That source includes +the pristine Gifsicle source, Optimizt modifications and patch, the WebAssembly +bridge, build scripts, and build configuration. + +Upstream project: + +## Emscripten runtime + +The generated files in `dist/` contain Emscripten runtime portions. Emscripten +6.0.9 is used as the build tool and is not otherwise distributed in the npm +package. + +Emscripten is available under 2 licenses, the MIT license and the +University of Illinois/NCSA Open Source License. + +Both are permissive open source licenses, with little if any +practical difference between them. + +The reason for offering both is that (1) the MIT license is +well-known, while (2) the University of Illinois/NCSA Open Source +License allows Emscripten's code to be integrated upstream into +LLVM, which uses that license, should the opportunity arise. + +The full text of both licenses follows. + +============================================================================== + +Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +============================================================================== + +Copyright (c) 2010-2014 Emscripten authors, see AUTHORS file. +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimers. + + Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimers + in the documentation and/or other materials provided with the + distribution. + + Neither the names of Mozilla, + nor the names of its contributors may be used to endorse + or promote products derived from this Software without specific prior + written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. + +============================================================================== + +This program uses portions of Node.js source code located in src/library_path.js, +in accordance with the terms of the MIT license. Node's license follows: + + """ + Copyright Joyent, Inc. and other Node contributors. All rights reserved. + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + """ + +## musl libc runtime + +The WebAssembly binary links Emscripten's musl libc implementation. The +complete copyright and license notices from Emscripten 6.0.9's +`system/lib/libc/musl/COPYRIGHT` are reproduced verbatim in `MUSL-COPYRIGHT`. +They cover musl and the separately attributed math implementations linked into +the binary, including Sun Microsystems' `cbrtf` and Arm Limited's `exp2` and +`pow` implementations and data. diff --git a/packages/optimizt-gifsicle/UPSTREAM.md b/packages/optimizt-gifsicle/UPSTREAM.md new file mode 100644 index 0000000..6c8d60f --- /dev/null +++ b/packages/optimizt-gifsicle/UPSTREAM.md @@ -0,0 +1,50 @@ +# Upstream source and provenance + +This package embeds the complete source snapshot for Gifsicle **1.96** from +. + +- Git tag: `v1.96` +- Git commit: `a08e0f6686d467bb8b9e4715b1f1835f12984fb0` +- Snapshot directory: `upstream/gifsicle/` +- File checksums: `upstream/gifsicle.sha256` +- File modes: `upstream/gifsicle.modes` +- Local integration changes: `upstream/gifsicle-wasm.patch` + +The snapshot was exported from the exact Git commit rather than reconstructed +from generated release files. The checksum and mode manifests are the +canonical evidence for the committed snapshot. Run `npm run verify:upstream` +to verify every file and `npm run verify:patch` to prove that the integration +patch applies exactly, without offsets or fuzz. + +The source was acquired from GitHub's `v1.96` tag archive endpoint: + +```text +https://github.com/kohler/gifsicle/archive/refs/tags/v1.96.tar.gz +``` + +The acquired archive had SHA-256 +`1104b338745f466bdb6b739b152c42a5dfe2fb50a4c21e3bcd78446766f007ea`. +The tag was resolved to the commit recorded above before export. GitHub-generated, +codeload-generated, and local `git archive` files are not assumed to have +interchangeable bytes; the committed per-file manifest is therefore the +authoritative verification input for this snapshot. + +The build copies the pristine snapshot to temporary storage, applies the patch, +and compiles that copy. Files under `upstream/gifsicle/` must never be edited +for integration purposes. + +Gifsicle's optimization code uses C-library `qsort()` comparators that can compare +distinct tied candidates as equal. Different libc implementations can therefore +produce equivalent palette permutations but different compressed bytes. Both the +native reference and WebAssembly build redirect those calls to the stable mergesort +in `src/wasm/stable-qsort.c`. This compatibility layer preserves upstream comparison +and optimization logic while making exact cross-platform parity deterministic. + +The exact native/WebAssembly cases and hashes are retained in +`verification/parity-manifest.json` and checked by `npm run verify:parity`. The +canonical native toolchain is documented in `verification/native-reference.md`. + +The WebAssembly build targets wasm64 with Emscripten's `-m64` option and allows +linear memory to grow to 16 GiB. Pointer-bearing exported functions use +Emscripten signature conversions so the existing JavaScript API continues to +receive numeric pointers. diff --git a/packages/optimizt-gifsicle/dist/gifsicle.mjs b/packages/optimizt-gifsicle/dist/gifsicle.mjs new file mode 100644 index 0000000..42746a6 --- /dev/null +++ b/packages/optimizt-gifsicle/dist/gifsicle.mjs @@ -0,0 +1,2 @@ +async function Module(moduleArg={}){var Module=moduleArg;var ENVIRONMENT_IS_NODE=true;if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var programArgs=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}programArgs=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){}function initRuntime(){runtimeInitialized=true;wasmExports["p"]()}function postRun(){}function abort(what){what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("gifsicle.wasm")}return new URL("gifsicle.wasm",import.meta.url).href}function getBinarySync(file){if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;wasmExports=applySignatureConversions(wasmExports);assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP8;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __emscripten_throw_longjmp=()=>{throw new EmscriptenSjLj};var getHeapMax=()=>17179869184;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(BigInt(pages));updateMemoryViews();return 1}catch(e){}};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);var HEAPU8;function _emscripten_resize_heap(requestedSize){requestedSize=bigintToI53Checked(requestedSize);var oldSize=HEAPU8.length;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false}var keepRuntimeAlive=()=>true;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var _fd_close=fd=>52;function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);newOffset=bigintToI53Checked(newOffset);return 70}var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var HEAPU64;function _fd_write(fd,iov,iovcnt,pnum){iov=bigintToI53Checked(iov);iovcnt=bigintToI53Checked(iovcnt);pnum=bigintToI53Checked(pnum);var num=0;for(var i=0;i{funcPtr=Number(funcPtr);var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(BigInt(funcPtr))}return func};{if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"]}Module["UTF8ToString"]=UTF8ToString;var _free,_malloc,_gifsicle_reset,_gifsicle_optimize,_gifsicle_output_data,_gifsicle_output_size,_gifsicle_error_message,_setThrew,__emscripten_stack_restore,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_free=Module["_free"]=wasmExports["r"];_malloc=Module["_malloc"]=wasmExports["s"];_gifsicle_reset=Module["_gifsicle_reset"]=wasmExports["t"];_gifsicle_optimize=Module["_gifsicle_optimize"]=wasmExports["u"];_gifsicle_output_data=Module["_gifsicle_output_data"]=wasmExports["v"];_gifsicle_output_size=Module["_gifsicle_output_size"]=wasmExports["w"];_gifsicle_error_message=Module["_gifsicle_error_message"]=wasmExports["x"];_setThrew=wasmExports["y"];__emscripten_stack_restore=wasmExports["z"];_emscripten_stack_get_current=wasmExports["A"];memory=wasmMemory=wasmExports["o"];__indirect_function_table=wasmTable=wasmExports["q"]}var wasmImports={k:__emscripten_throw_longjmp,l:_emscripten_resize_heap,b:_exit,n:_fd_close,m:_fd_seek,c:_fd_write,d:invoke_ijjj,h:invoke_jjiijij,j:invoke_jjijjj,i:invoke_jjjjji,f:invoke_vid,a:invoke_vj,e:invoke_vjii,g:invoke_vjiii};function invoke_vj(index,a1){var sp=stackSave();try{getWasmTableEntry(Number(index))(a1)}catch(e){stackRestore(sp);if(!(e instanceof EmscriptenEH))throw e;_setThrew(1,0)}}function invoke_jjijjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(Number(index))(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(!(e instanceof EmscriptenEH))throw e;_setThrew(1,0);return 0n}}function invoke_jjjjji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(Number(index))(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(!(e instanceof EmscriptenEH))throw e;_setThrew(1,0);return 0n}}function invoke_jjiijij(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(Number(index))(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(!(e instanceof EmscriptenEH))throw e;_setThrew(1,0);return 0n}}function invoke_vjiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(Number(index))(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(!(e instanceof EmscriptenEH))throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(Number(index))(a1,a2)}catch(e){stackRestore(sp);if(!(e instanceof EmscriptenEH))throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(Number(index))(a1,a2,a3)}catch(e){stackRestore(sp);if(!(e instanceof EmscriptenEH))throw e;_setThrew(1,0)}}function invoke_ijjj(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(Number(index))(a1,a2,a3)}catch(e){stackRestore(sp);if(!(e instanceof EmscriptenEH))throw e;_setThrew(1,0)}}function applySignatureConversions(wasmExports){wasmExports=Object.assign({},wasmExports);var makeWrapper__p=f=>a0=>f(BigInt(a0));var makeWrapper_pp=f=>a0=>Number(f(BigInt(a0)));var makeWrapper__p_______=f=>(a0,a1,a2,a3,a4,a5,a6,a7)=>f(BigInt(a0),a1,a2,a3,a4,a5,a6,a7);var makeWrapper_p=f=>()=>Number(f());var makeWrapper__p_=f=>(a0,a1)=>f(BigInt(a0),a1);wasmExports["r"]=makeWrapper__p(wasmExports["r"]);wasmExports["s"]=makeWrapper_pp(wasmExports["s"]);wasmExports["u"]=makeWrapper__p_______(wasmExports["u"]);wasmExports["v"]=makeWrapper_p(wasmExports["v"]);wasmExports["x"]=makeWrapper_p(wasmExports["x"]);wasmExports["y"]=makeWrapper__p_(wasmExports["y"]);wasmExports["z"]=makeWrapper__p(wasmExports["z"]);wasmExports["_emscripten_stack_alloc"]=makeWrapper_pp(wasmExports["_emscripten_stack_alloc"]);wasmExports["A"]=makeWrapper_p(wasmExports["A"]);return wasmExports}async function run(){preRun();if(ABORT)return;initRuntime();postRun()}var wasmExports;wasmExports=await createWasm();await run(); +;return Module}export default Module; diff --git a/packages/optimizt-gifsicle/dist/gifsicle.wasm b/packages/optimizt-gifsicle/dist/gifsicle.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7822a5482a04a72b95671d42792e6cd6b387a10d GIT binary patch literal 132971 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

=Joew4XL$QVeR=iXXu(HL%m< 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 literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/eslint.config.js b/packages/optimizt-gifsicle/eslint.config.js new file mode 100644 index 0000000..4fc3e59 --- /dev/null +++ b/packages/optimizt-gifsicle/eslint.config.js @@ -0,0 +1,13 @@ +import config from '@343dev/eslint-config'; + +export default [ + { + ignores: [ + '.cache/**', + '.claude/**', + 'dist/**', + 'upstream/gifsicle/**', + ], + }, + ...config, +]; diff --git a/packages/optimizt-gifsicle/index.js b/packages/optimizt-gifsicle/index.js new file mode 100644 index 0000000..513a106 --- /dev/null +++ b/packages/optimizt-gifsicle/index.js @@ -0,0 +1,2 @@ +export { createOperation, optimize as default, optimize } from './lib/optimize.js'; +export { errorCodes } from './lib/errors.js'; diff --git a/packages/optimizt-gifsicle/lib/errors.js b/packages/optimizt-gifsicle/lib/errors.js new file mode 100644 index 0000000..d47a83c --- /dev/null +++ b/packages/optimizt-gifsicle/lib/errors.js @@ -0,0 +1,17 @@ +export const errorCodes = Object.freeze({ + INVALID_INPUT: 'INVALID_INPUT', + INVALID_OPTIONS: 'INVALID_OPTIONS', + PROCESSING_FAILED: 'PROCESSING_FAILED', + WASM_OUT_OF_MEMORY: 'WASM_OUT_OF_MEMORY', + WORKER_FAILED: 'WORKER_FAILED', +}); + +export const outOfMemoryPattern = /ERR_WORKER_OUT_OF_MEMORY|cannot enlarge memory|out of memory|bad_alloc|\bOOM\b/i; + +export function createError(code, message, cause) { + const error = cause === undefined + ? new Error(message) + : new Error(message, { cause }); + error.code = code; + return error; +} diff --git a/packages/optimizt-gifsicle/lib/optimize.js b/packages/optimizt-gifsicle/lib/optimize.js new file mode 100644 index 0000000..9d755ef --- /dev/null +++ b/packages/optimizt-gifsicle/lib/optimize.js @@ -0,0 +1,137 @@ +import { Worker } from 'node:worker_threads'; + +import { createError, errorCodes, outOfMemoryPattern } from './errors.js'; +import { normalizeOptions } from './options.js'; + +export const maximumInputSize = 128 * 1024 * 1024; + +const workerUrl = new URL('worker.js', import.meta.url); + +function snapshotInput(input) { + if (!(input instanceof Uint8Array)) { + throw createError( + errorCodes.INVALID_INPUT, + 'input must be a Buffer or Uint8Array', + ); + } + if (input.byteLength === 0) { + throw createError(errorCodes.INVALID_INPUT, 'GIF input is empty'); + } + if (input.byteLength > maximumInputSize) { + throw createError( + errorCodes.INVALID_INPUT, + `GIF input exceeds the ${maximumInputSize}-byte limit`, + ); + } + return Uint8Array.from(input); +} + +function deserializeError(serialized) { + const code = Object.values(errorCodes).includes(serialized?.code) + ? serialized.code + : errorCodes.WORKER_FAILED; + const details = serialized?.details?.trim(); + const cause = details ? new Error(details) : undefined; + return createError(code, serialized?.message || 'Gifsicle Worker failed', cause); +} + +function classifyWorkerFailure(error) { + const text = `${error?.code ?? ''} ${error?.message ?? ''}`; + const code = outOfMemoryPattern.test(text) + ? errorCodes.WASM_OUT_OF_MEMORY + : errorCodes.WORKER_FAILED; + const message = code === errorCodes.WASM_OUT_OF_MEMORY + ? 'WebAssembly ran out of memory while optimizing the GIF' + : 'Gifsicle Worker failed'; + return createError(code, message, error); +} + +export function createOperation(input, options) { + let snapshot; + let normalizedOptions; + try { + snapshot = snapshotInput(input); + normalizedOptions = normalizeOptions(options); + } catch (error) { + return { + promise: Promise.reject(error), + terminate: async () => {}, + }; + } + + let worker; + let isSettled = false; + let isReceivedTermination = false; + let message; + let workerError; + + const promise = new Promise((resolve, reject) => { + try { + worker = new Worker(workerUrl, { + execArgv: [], + name: 'gifsicle-optimize', + workerData: { + input: snapshot.buffer, + options: normalizedOptions, + }, + transferList: [snapshot.buffer], + }); + } catch (error) { + reject(classifyWorkerFailure(error)); + return; + } + + worker.once('message', (value) => { + message = value; + }); + worker.once('error', (error) => { + workerError = error; + }); + worker.once('exit', (exitCode) => { + isSettled = true; + if (isReceivedTermination) { + reject(createError(errorCodes.WORKER_FAILED, 'Gifsicle Worker was terminated')); + return; + } + if (workerError) { + reject(classifyWorkerFailure(workerError)); + return; + } + if (exitCode !== 0) { + reject(createError( + errorCodes.WORKER_FAILED, + `Gifsicle Worker exited unexpectedly (exit code ${exitCode})`, + )); + return; + } + if (!message) { + reject(createError( + errorCodes.WORKER_FAILED, + `Gifsicle Worker exited without a result (exit code ${exitCode})`, + )); + return; + } + if (!message.ok) { + reject(deserializeError(message.error)); + return; + } + resolve(Buffer.from(message.output)); + }); + }); + + return { + promise, + terminate: async () => { + if (!worker || isSettled) { + return; + } + + isReceivedTermination = true; + await worker.terminate(); + }, + }; +} + +export function optimize(input, options) { + return createOperation(input, options).promise; +} diff --git a/packages/optimizt-gifsicle/lib/options.js b/packages/optimizt-gifsicle/lib/options.js new file mode 100644 index 0000000..2ebc632 --- /dev/null +++ b/packages/optimizt-gifsicle/lib/options.js @@ -0,0 +1,71 @@ +import { createError, errorCodes } from './errors.js'; + +const optionNames = new Set(['optimize', 'careful', 'colors', 'lossy', 'gamma']); + +function invalid(message) { + throw createError(errorCodes.INVALID_OPTIONS, message); +} + +function isPlainObject(value) { + if (typeof value !== 'object' || value === null) { + return false; + } + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function optionalInteger(name, value, minimum, maximum) { + if (value === undefined || value === false) { + return 0; + } + if (!Number.isSafeInteger(value) || value < minimum || value > maximum) { + invalid(`${name} must be false or an integer between ${minimum} and ${maximum}`); + } + return value; +} + +export function normalizeOptions(options) { + if (options === undefined) { + options = {}; + } else if (!isPlainObject(options)) { + invalid('options must be a plain object'); + } + + for (const name of Object.keys(options)) { + if (!optionNames.has(name)) { + invalid(`Unknown option: ${name}`); + } + } + + if (options.careful !== undefined && typeof options.careful !== 'boolean') { + invalid('careful must be a boolean'); + } + + let gammaType = 0; + let gamma = 2.2; + if (options.gamma !== undefined && options.gamma !== false) { + if (options.gamma === 'srgb') { + gammaType = 0; + } else if (options.gamma === 'oklab') { + gammaType = 2; + } else if ( + typeof options.gamma === 'number' + && Number.isFinite(options.gamma) + && options.gamma > 0 + ) { + gammaType = 1; + gamma = options.gamma; + } else { + invalid('gamma must be false, a positive finite number, "srgb", or "oklab"'); + } + } + + return { + optimize: optionalInteger('optimize', options.optimize, 0, 3), + careful: options.careful ?? false, + colors: optionalInteger('colors', options.colors, 2, 256), + lossy: optionalInteger('lossy', options.lossy, 0, 2_147_483_647), + gammaType, + gamma, + }; +} diff --git a/packages/optimizt-gifsicle/lib/worker.js b/packages/optimizt-gifsicle/lib/worker.js new file mode 100644 index 0000000..791782c --- /dev/null +++ b/packages/optimizt-gifsicle/lib/worker.js @@ -0,0 +1,107 @@ +import { parentPort, workerData } from 'node:worker_threads'; + +import createModule from '../dist/gifsicle.mjs'; +import { errorCodes, outOfMemoryPattern } from './errors.js'; + +if (parentPort === null) { + throw new Error('The Gifsicle runtime must run in a Worker Thread'); +} + +const statuses = { + 1: errorCodes.INVALID_INPUT, + 2: errorCodes.PROCESSING_FAILED, + 3: errorCodes.WASM_OUT_OF_MEMORY, +}; + +function postFailure(code, message, details) { + parentPort.postMessage({ + ok: false, + error: { code, message, details }, + }); +} + +async function optimize() { + const diagnostics = []; + const module = await createModule({ + print(line) { + diagnostics.push(line); + }, + printErr(line) { + diagnostics.push(line); + }, + }); + const input = new Uint8Array(workerData.input); + const pointer = module._malloc(input.byteLength); + if (pointer === 0) { + postFailure( + errorCodes.WASM_OUT_OF_MEMORY, + 'WebAssembly could not allocate memory for the GIF input', + diagnostics.join('\n'), + ); + return; + } + + let status; + try { + module.HEAPU8.set(input, pointer); + status = module._gifsicle_optimize( + pointer, + input.byteLength, + workerData.options.optimize, + workerData.options.careful ? 1 : 0, + workerData.options.colors, + workerData.options.lossy, + workerData.options.gammaType, + workerData.options.gamma, + ); + } finally { + module._free(pointer); + } + + if (status !== 0) { + const details = diagnostics.join('\n').trim(); + const bridgeMessage = module.UTF8ToString(module._gifsicle_error_message()); + const code = statuses[status] ?? errorCodes.PROCESSING_FAILED; + module._gifsicle_reset(); + postFailure( + code, + bridgeMessage || 'Gifsicle optimization failed', + details, + ); + return; + } + + const outputPointer = module._gifsicle_output_data(); + const outputSize = module._gifsicle_output_size(); + const heapLength = module.HEAPU8.byteLength; + if ( + outputPointer === 0 + || outputSize === 0 + || outputPointer > heapLength + || outputSize > heapLength - outputPointer + ) { + module._gifsicle_reset(); + postFailure( + errorCodes.PROCESSING_FAILED, + 'Gifsicle produced an invalid output range', + diagnostics.join('\n'), + ); + return; + } + const output = module.HEAPU8.slice(outputPointer, outputPointer + outputSize); + module._gifsicle_reset(); + parentPort.postMessage({ ok: true, output: output.buffer }, [output.buffer]); +} + +try { + await optimize(); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + postFailure( + outOfMemoryPattern.test(message) + ? errorCodes.WASM_OUT_OF_MEMORY + : errorCodes.WORKER_FAILED, + message, + '', + ); +} diff --git a/packages/optimizt-gifsicle/package.json b/packages/optimizt-gifsicle/package.json new file mode 100644 index 0000000..e38d4aa --- /dev/null +++ b/packages/optimizt-gifsicle/package.json @@ -0,0 +1,34 @@ +{ + "name": "@343dev/optimizt-gifsicle", + "version": "1.96.0-0", + "private": true, + "description": "Internal Gifsicle WebAssembly runtime for Optimizt", + "license": "SEE LICENSE IN GIFSICLE-LICENSE.md", + "repository": { + "type": "git", + "url": "git+https://github.com/343dev/optimizt.git", + "directory": "packages/optimizt-gifsicle" + }, + "type": "module", + "scripts": { + "setup-emsdk": "./scripts/setup-emsdk.sh", + "build:wasm": "./scripts/build-wasm.sh", + "build:native": "./scripts/build-native.sh", + "benchmark": "node ./scripts/benchmark.js", + "lint": "npx eslint ./", + "test": "node --test --test-concurrency=1", + "verify:upstream": "node ./scripts/verify-upstream.js", + "verify:patch": "./scripts/verify-patch.sh", + "verify:dist": "./scripts/verify-dist.sh", + "verify:native-sanitizers": "./scripts/verify-native-sanitizers.sh", + "preverify:parity": "./scripts/build-native-container.sh", + "verify:parity": "node ./scripts/verify-parity.js" + }, + "devDependencies": { + "@343dev/eslint-config": "5.0.0", + "eslint": "10.5.0" + }, + "engines": { + "node": ">=24.0.0" + } +} diff --git a/packages/optimizt-gifsicle/scripts/benchmark.js b/packages/optimizt-gifsicle/scripts/benchmark.js new file mode 100644 index 0000000..664bd98 --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/benchmark.js @@ -0,0 +1,105 @@ +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { cpus, tmpdir } from 'node:os'; +import path from 'node:path'; +import { performance } from 'node:perf_hooks'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import optimize from '../index.js'; +import { + nativeArguments, + nativeLauncher, + runNative, +} from './native-reference.js'; + +const fixtureUrl = new URL('../test/fixtures/animated.gif', import.meta.url); +const fixturePath = fileURLToPath(fixtureUrl); +const input = await readFile(fixtureUrl); +const nativeExecutable = process.env.GIFSICLE_NATIVE; +const launcher = nativeLauncher(); +const cases = [ + { name: 'no-options', options: {} }, + { + name: 'lossless', + options: { optimize: 3, careful: true, colors: 256, lossy: 0, gamma: 1 }, + }, + { + name: 'lossy', + options: { optimize: 3, careful: false, colors: 256, lossy: 100, gamma: 1 }, + }, +]; +const iterations = Number(process.env.GIFSICLE_BENCHMARK_ITERATIONS ?? 10); + +if (!Number.isSafeInteger(iterations) || iterations < 1) { + throw new Error('GIFSICLE_BENCHMARK_ITERATIONS must be a positive integer'); +} +if (!nativeExecutable) { + throw new Error('GIFSICLE_NATIVE must identify the canonical native reference'); +} + +async function measure(operation) { + const durations = []; + let outputSize; + for (let index = 0; index < iterations; index += 1) { + const start = performance.now(); + outputSize = await operation(index); + durations.push(performance.now() - start); + } + return { + iterations, + meanMilliseconds: durations.reduce((sum, value) => sum + value, 0) / iterations, + minimumMilliseconds: Math.min(...durations), + maximumMilliseconds: Math.max(...durations), + outputSize, + }; +} + +const directory = await mkdtemp(path.join(tmpdir(), 'gifsicle-benchmark-')); +const results = []; +try { + for (const [caseIndex, benchmarkCase] of cases.entries()) { + const wasm = await measure(async () => { + const output = await optimize(input, benchmarkCase.options); + return output.length; + }); + const native = await measure(async (iteration) => { + const outputPath = path.join(directory, `${caseIndex}-${iteration}.gif`); + await runNative( + nativeExecutable, + launcher, + nativeArguments(fixturePath, benchmarkCase.options, outputPath), + ); + const output = await readFile(outputPath); + return output.length; + }); + results.push({ + name: benchmarkCase.name, + options: benchmarkCase.options, + wasm, + native, + }); + } +} finally { + await rm(directory, { recursive: true, force: true }); +} + +console.log(JSON.stringify({ + schemaVersion: 1, + environment: { + platform: process.platform, + architecture: process.arch, + node: process.version, + cpus: cpus().map(cpu => cpu.model), + nativeProfile: 'Linux x64, GCC 14.2.0, -O3 -DNDEBUG, no threads, no SIMD', + }, + method: { + wasmDurationsIncludeFreshWorkerAndWebAssemblyInitialization: true, + nativeDurationsIncludeFreshProcessInitialization: true, + hardRatioGate: false, + }, + input: { + fixture: 'animated.gif', + size: input.length, + }, + results, +}, undefined, 2)); diff --git a/packages/optimizt-gifsicle/scripts/build-native-container.sh b/packages/optimizt-gifsicle/scripts/build-native-container.sh new file mode 100755 index 0000000..4e338ec --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/build-native-container.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly IMAGE='gcc:14.2.0@sha256:b99b86a28812b1e6453a231a947dc43d76fe192788a12f344a9b568bf9f5d24c' +readonly ENGINE="${CONTAINER_ENGINE:-docker}" + +"$ENGINE" run --rm \ + --platform linux/amd64 \ + --user "$(id -u):$(id -g)" \ + --env HOME=/tmp \ + --volume "$ROOT:/workspace" \ + --workdir /workspace \ + "$IMAGE" \ + ./scripts/build-native.sh diff --git a/packages/optimizt-gifsicle/scripts/build-native.sh b/packages/optimizt-gifsicle/scripts/build-native.sh new file mode 100755 index 0000000..d4c731a --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/build-native.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly UPSTREAM="$ROOT/upstream/gifsicle" +readonly OUTPUT="${NATIVE_OUTPUT:-$ROOT/verification/gifsicle-native}" +readonly CC="${CC:-gcc}" +readonly EXPECTED_GCC='14.2.0' + +actual_version="$($CC -dumpfullversion -dumpversion)" +if [[ "$actual_version" != "$EXPECTED_GCC" ]]; then + printf 'Expected GCC %s, got %s\n' "$EXPECTED_GCC" "$actual_version" >&2 + exit 1 +fi +if [[ "${NATIVE_SANITIZE:-0}" != '1' ]]; then + actual_system="$(uname -s)" + actual_target="$($CC -dumpmachine)" + if [[ "$actual_system" != 'Linux' || "$actual_target" != x86_64*-linux-gnu ]]; then + printf 'Expected Linux x64 GCC target, got %s %s\n' \ + "$actual_system" "$actual_target" >&2 + exit 1 + fi +fi + +build_dir="$(mktemp -d "${TMPDIR:-/tmp}/gifsicle-native.XXXXXX")" +cleanup() { + rm -rf "$build_dir" +} +trap cleanup EXIT +( + cd "$UPSTREAM" + sha256sum --check --strict --quiet "$ROOT/upstream/gifsicle.sha256" +) +cp -a "$UPSTREAM/." "$build_dir/" +( + cd "$build_dir" + git apply --check "$ROOT/upstream/gifsicle-wasm.patch" + git apply "$ROOT/upstream/gifsicle-wasm.patch" +) +cp "$ROOT/src/wasm/config.h" "$build_dir/config.h" +mkdir -p "$build_dir/objects" "$(dirname "$OUTPUT")" + +sources=( + clp.c fmalloc.c giffunc.c gifread.c gifunopt.c gifwrite.c kcolor.c + merge.c optimize.c quantize.c support.c xform.c gifsicle.c + gifsicle_bridge.c +) +objects=() +compile_flags=(-O3 -DNDEBUG -DHAVE_CONFIG_H=1 -std=gnu11) +link_flags=(-O3 -DNDEBUG -std=gnu11) +if [[ "${NATIVE_SANITIZE:-0}" == '1' ]]; then + compile_flags=( + -O1 -g -DNDEBUG -DHAVE_CONFIG_H=1 -std=gnu11 -fno-omit-frame-pointer + -fsanitize=address,undefined + ) + link_flags=( + -O1 -g -DNDEBUG -std=gnu11 -fno-omit-frame-pointer + -fsanitize=address,undefined + ) +fi +"$CC" "${compile_flags[@]}" \ + -I"$build_dir" -I"$build_dir/include" \ + -c "$ROOT/src/wasm/stable-qsort.c" \ + -o "$build_dir/objects/stable-qsort.o" +objects+=("$build_dir/objects/stable-qsort.o") +for source in "${sources[@]}"; do + object="$build_dir/objects/${source%.c}.o" + extra=() + if [[ "$source" == 'gifsicle.c' ]]; then + extra=(-Dmain=gifsicle_cli_main) + fi + "$CC" "${compile_flags[@]}" \ + -I"$build_dir" -I"$build_dir/include" "${extra[@]}" \ + -c "$build_dir/src/$source" -o "$object" + objects+=("$object") +done +"$CC" "${link_flags[@]}" \ + -I"$build_dir" -I"$build_dir/include" \ + "$ROOT/src/wasm/native-driver.c" "${objects[@]}" -lm -o "$OUTPUT" +printf 'Built native reference at %s with GCC %s\n' "$OUTPUT" "$actual_version" diff --git a/packages/optimizt-gifsicle/scripts/build-wasm.sh b/packages/optimizt-gifsicle/scripts/build-wasm.sh new file mode 100755 index 0000000..76fabed --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/build-wasm.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=scripts/emsdk-version.sh +source "$ROOT/scripts/emsdk-version.sh" +readonly EMSDK_VERSION +readonly UPSTREAM="$ROOT/upstream/gifsicle" +readonly PATCH="$ROOT/upstream/gifsicle-wasm.patch" +readonly OUTPUT_DIR="${WASM_OUTPUT_DIR:-$ROOT/dist}" +readonly SDK_DIR="${EMSDK_INSTALL_DIR:-$ROOT/.cache/emsdk}" +readonly EMCC="$SDK_DIR/upstream/emscripten/emcc" + +if [[ ! -x "$EMCC" ]]; then + printf 'Emscripten is not installed at %s. Run npm run setup-emsdk.\n' \ + "$SDK_DIR" >&2 + exit 1 +fi + +actual_version="$("$EMCC" --version | grep -m 1 emcc)" +if [[ "$actual_version" != *" $EMSDK_VERSION "* ]]; then + printf 'Expected Emscripten %s, got: %s\n' \ + "$EMSDK_VERSION" "$actual_version" >&2 + exit 1 +fi + +build_dir="$(mktemp -d "${TMPDIR:-/tmp}/gifsicle-wasm.XXXXXX")" +cleanup() { + rm -rf "$build_dir" +} +trap cleanup EXIT + +cp -a "$UPSTREAM/." "$build_dir/" +( + cd "$build_dir" + git apply --check "$PATCH" + git apply "$PATCH" +) +cp "$ROOT/src/wasm/config.h" "$build_dir/config.h" + +sources=( + clp.c + fmalloc.c + giffunc.c + gifread.c + gifunopt.c + gifwrite.c + kcolor.c + merge.c + optimize.c + quantize.c + support.c + xform.c + gifsicle.c + gifsicle_bridge.c +) +objects=() +mkdir -p "$build_dir/objects" "$OUTPUT_DIR" +"$EMCC" \ + -m64 \ + -O3 \ + -DNDEBUG \ + -DHAVE_CONFIG_H=1 \ + -ffile-prefix-map="$ROOT"=/gifsicle-package \ + -fdebug-prefix-map="$ROOT"=/gifsicle-package \ + -fmacro-prefix-map="$ROOT"=/gifsicle-package \ + -std=gnu11 \ + -I"$build_dir" \ + -I"$build_dir/include" \ + -c "$ROOT/src/wasm/stable-qsort.c" \ + -o "$build_dir/objects/stable-qsort.o" +objects+=("$build_dir/objects/stable-qsort.o") +for source in "${sources[@]}"; do + object="$build_dir/objects/${source%.c}.o" + extra=(-I"$build_dir" -I"$build_dir/include") + if [[ "$source" == 'gifsicle.c' ]]; then + extra+=(-Dmain=gifsicle_cli_main) + fi + "$EMCC" \ + -m64 \ + -O3 \ + -DNDEBUG \ + -DHAVE_CONFIG_H=1 \ + -ffile-prefix-map="$build_dir"=/gifsicle-source \ + -fdebug-prefix-map="$build_dir"=/gifsicle-source \ + -fmacro-prefix-map="$build_dir"=/gifsicle-source \ + -std=gnu11 \ + "${extra[@]}" \ + -c "$build_dir/src/$source" \ + -o "$object" + objects+=("$object") +done + +"$EMCC" \ + -m64 \ + -O3 \ + -DNDEBUG \ + "${objects[@]}" \ + -sMODULARIZE=1 \ + -sEXPORT_ES6=1 \ + -sENVIRONMENT=node \ + -sALLOW_MEMORY_GROWTH=1 \ + -sMAXIMUM_MEMORY=16GB \ + -sSIGNATURE_CONVERSIONS=gifsicle_optimize:_p_______,gifsicle_output_data:p,gifsicle_error_message:p \ + -sFILESYSTEM=0 \ + -sINCOMING_MODULE_JS_API=print,printErr \ + -sEXPORTED_FUNCTIONS=_malloc,_free,_gifsicle_optimize,_gifsicle_output_data,_gifsicle_output_size,_gifsicle_error_message,_gifsicle_reset \ + -sEXPORTED_RUNTIME_METHODS=HEAPU8,UTF8ToString \ + -o "$OUTPUT_DIR/gifsicle.mjs" + +if [[ ! -f "$OUTPUT_DIR/gifsicle.wasm" ]]; then + printf 'Emscripten did not produce %s/gifsicle.wasm\n' "$OUTPUT_DIR" >&2 + exit 1 +fi +printf 'Built %s and %s\n' \ + "$OUTPUT_DIR/gifsicle.mjs" "$OUTPUT_DIR/gifsicle.wasm" diff --git a/packages/optimizt-gifsicle/scripts/emsdk-version.sh b/packages/optimizt-gifsicle/scripts/emsdk-version.sh new file mode 100644 index 0000000..9d4c68f --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/emsdk-version.sh @@ -0,0 +1,3 @@ +# Pinned Emscripten toolchain shared by setup and build scripts. +EMSDK_VERSION='6.0.9' +EMSDK_COMMIT='5eb0bde7585670252e8ba05e9d361627bffd08b5' diff --git a/packages/optimizt-gifsicle/scripts/native-reference.js b/packages/optimizt-gifsicle/scripts/native-reference.js new file mode 100644 index 0000000..2757fb3 --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/native-reference.js @@ -0,0 +1,64 @@ +import { execFile } from 'node:child_process'; +import process from 'node:process'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile); +// Must match the default sRGB gamma in lib/options.js. +const DEFAULT_GAMMA = 2.2; + +export function nativeArguments(fixture, options, output) { + // Must mirror the gamma-type codes in lib/options.js and native-driver.c. + let gammaType = 0; + if (options.gamma === 'oklab') { + gammaType = 2; + } else if (typeof options.gamma === 'number') { + gammaType = 1; + } + return [ + fixture, + String(options.optimize || 0), + String(Number(options.careful || false)), + String(options.colors || 0), + String(options.lossy || 0), + String(gammaType), + String(gammaType === 1 ? options.gamma : DEFAULT_GAMMA), + output, + ]; +} + +export function nativeLauncher() { + let launcher = []; + if (process.env.GIFSICLE_NATIVE_LAUNCHER) { + try { + launcher = JSON.parse(process.env.GIFSICLE_NATIVE_LAUNCHER); + } catch { + throw new Error( + 'GIFSICLE_NATIVE_LAUNCHER must be a nonempty JSON array of strings', + ); + } + } + if ( + !Array.isArray(launcher) + || launcher.some(argument => typeof argument !== 'string') + || ( + process.env.GIFSICLE_NATIVE_LAUNCHER !== undefined + && launcher.length === 0 + ) + ) { + throw new Error('GIFSICLE_NATIVE_LAUNCHER must be a nonempty JSON array of strings'); + } + return launcher; +} + +export async function runNative(executable, launcher, arguments_) { + const { stdout, stderr } = await execFileAsync( + launcher[0] ?? executable, + launcher.length > 0 + ? [...launcher.slice(1), executable, ...arguments_] + : arguments_, + ); + if (stderr) { + process.stderr.write(stderr); + } + return stdout; +} diff --git a/packages/optimizt-gifsicle/scripts/setup-emsdk.sh b/packages/optimizt-gifsicle/scripts/setup-emsdk.sh new file mode 100755 index 0000000..63e4ebe --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/setup-emsdk.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=scripts/emsdk-version.sh +source "$ROOT/scripts/emsdk-version.sh" +readonly EMSDK_VERSION EMSDK_COMMIT +readonly SDK_DIR="${EMSDK_INSTALL_DIR:-$ROOT/.cache/emsdk}" + +if [[ -e "$SDK_DIR" && ! -d "$SDK_DIR/.git" ]]; then + printf '%s exists but is not an emsdk Git checkout\n' "$SDK_DIR" >&2 + exit 1 +fi + +if [[ ! -d "$SDK_DIR/.git" ]]; then + mkdir -p "$(dirname "$SDK_DIR")" + git clone --filter=blob:none --no-checkout \ + https://github.com/emscripten-core/emsdk.git "$SDK_DIR" + git -C "$SDK_DIR" fetch --depth 1 origin "$EMSDK_COMMIT" + git -C "$SDK_DIR" checkout --detach "$EMSDK_COMMIT" +fi + +actual_commit="$(git -C "$SDK_DIR" rev-parse HEAD 2>/dev/null || true)" +if [[ "$actual_commit" != "$EMSDK_COMMIT" ]]; then + printf 'Expected emsdk commit %s, found %s in %s\n' \ + "$EMSDK_COMMIT" "$actual_commit" "$SDK_DIR" >&2 + printf 'Remove that checkout or choose another EMSDK_INSTALL_DIR.\n' >&2 + exit 1 +fi + +if [[ ! -x "$SDK_DIR/emsdk" ]]; then + git -C "$SDK_DIR" reset --hard "$EMSDK_COMMIT" +fi + +"$SDK_DIR/emsdk" install "$EMSDK_VERSION" +"$SDK_DIR/emsdk" activate "$EMSDK_VERSION" + +actual_version="$("$SDK_DIR/upstream/emscripten/emcc" --version | grep -m 1 emcc)" +if [[ "$actual_version" != *" $EMSDK_VERSION "* ]]; then + printf 'Expected Emscripten %s, got: %s\n' \ + "$EMSDK_VERSION" "$actual_version" >&2 + exit 1 +fi + +printf 'Emscripten %s is ready at %s\n' "$EMSDK_VERSION" "$SDK_DIR" diff --git a/packages/optimizt-gifsicle/scripts/verify-dist.sh b/packages/optimizt-gifsicle/scripts/verify-dist.sh new file mode 100755 index 0000000..d3ec255 --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/verify-dist.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/gifsicle-dist.XXXXXX")" +trap 'rm -rf "$TEMP_DIR"' EXIT + +WASM_OUTPUT_DIR="$TEMP_DIR" "$ROOT/scripts/build-wasm.sh" + +for artifact in gifsicle.mjs gifsicle.wasm; do + if ! cmp -s "$ROOT/dist/$artifact" "$TEMP_DIR/$artifact"; then + printf 'Committed dist/%s differs from a canonical rebuild.\n' \ + "$artifact" >&2 + exit 1 + fi +done + +if grep -aF "$ROOT" "$ROOT/dist/gifsicle.mjs" "$ROOT/dist/gifsicle.wasm" >/dev/null; then + printf 'Generated distribution contains the repository absolute path.\n' >&2 + exit 1 +fi + +if find "$ROOT/dist" -type f \( -name '*.map' -o -name '*.debug' \) -print -quit | grep -q .; then + printf 'Generated distribution contains an unexpected debug artifact.\n' >&2 + exit 1 +fi + +( + cd /tmp + GIFSICLE_VERIFY_ROOT="$ROOT" node --input-type=module <<'EOF' +import { pathToFileURL } from 'node:url'; + +const modulePath = `${process.env.GIFSICLE_VERIFY_ROOT}/dist/gifsicle.mjs`; +const createModule = (await import(pathToFileURL(modulePath).href)).default; +const module = await createModule(); +const largeAllocation = module._malloc(5 * 1024 ** 3); +if (largeAllocation === 0) { + throw new Error('WebAssembly distribution cannot allocate beyond 4 GiB'); +} +const probe = largeAllocation + 4 * 1024 ** 3; +module.HEAPU8[probe] = 123; +if (module.HEAPU8[probe] !== 123) { + throw new Error('WebAssembly distribution cannot address memory beyond 4 GiB'); +} +module._free(largeAllocation); +if (typeof module._gifsicle_optimize !== 'function') { + throw new Error('Emscripten loader did not resolve gifsicle.wasm through import.meta.url'); +} +EOF +) + +printf 'Verified committed WebAssembly distribution.\n' diff --git a/packages/optimizt-gifsicle/scripts/verify-native-sanitizers.sh b/packages/optimizt-gifsicle/scripts/verify-native-sanitizers.sh new file mode 100755 index 0000000..ffff72e --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/verify-native-sanitizers.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/gifsicle-sanitize.XXXXXX")" +trap 'rm -rf "$TEMP_DIR"' EXIT + +NATIVE_SANITIZE=1 NATIVE_OUTPUT="$TEMP_DIR/gifsicle-native" \ + "$ROOT/scripts/build-native.sh" + +run_case() { + local fixture="$1" + local name="$2" + shift 2 + ASAN_OPTIONS='detect_leaks=1:halt_on_error=1' \ + UBSAN_OPTIONS='halt_on_error=1:print_stacktrace=1' \ + "$TEMP_DIR/gifsicle-native" \ + "$ROOT/test/fixtures/$fixture" "$@" "$TEMP_DIR/$name.gif" +} + +run_case animated.gif no-options 0 0 0 0 0 2.2 +run_case animated.gif lossless 3 1 256 0 1 1 +run_case animated.gif lossy 3 0 256 100 1 1 +run_case transparent.gif transparent 3 1 128 20 0 2.2 + +printf 'Verified native bridge with AddressSanitizer and UndefinedBehaviorSanitizer.\n' diff --git a/packages/optimizt-gifsicle/scripts/verify-parity.js b/packages/optimizt-gifsicle/scripts/verify-parity.js new file mode 100644 index 0000000..ee05ea0 --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/verify-parity.js @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; + +import optimize from '../index.js'; +import { + nativeArguments, + nativeLauncher, + runNative, +} from './native-reference.js'; + +const root = fileURLToPath(new URL('../', import.meta.url)); +const manifest = JSON.parse(await readFile( + new URL('../verification/parity-manifest.json', import.meta.url), + 'utf8', +)); +const nativeExecutable = process.env.GIFSICLE_NATIVE || path.join( + root, + 'verification/gifsicle-native', +); +const launcher = nativeLauncher(); + +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex'); +} + +assert.equal(manifest.schemaVersion, 1); +const directory = await mkdtemp(path.join(tmpdir(), 'gifsicle-parity-')); +try { + for (const [index, parityCase] of manifest.cases.entries()) { + const fixture = path.join(root, 'test/fixtures', parityCase.fixture); + const input = await readFile(fixture); + assert.equal(sha256(input), parityCase.inputSha256, `${parityCase.fixture} input hash`); + + const nativePath = path.join(directory, `${index}.native.gif`); + const arguments_ = nativeArguments(fixture, parityCase.options, nativePath); + await runNative(nativeExecutable, launcher, arguments_); + const native = await readFile(nativePath); + const wasm = await optimize(input, parityCase.options); + for (const [implementation, output, expected] of [ + ['native', native, parityCase.native], + ['WebAssembly', wasm, parityCase.wasm], + ]) { + assert.equal(output.length, expected.size, `${index} ${implementation} size`); + assert.equal(sha256(output), expected.sha256, `${index} ${implementation} hash`); + } + assert.deepEqual(wasm, native, `${index} native/WebAssembly bytes`); + } +} finally { + await rm(directory, { force: true, recursive: true }); +} + +console.log(`Verified ${manifest.cases.length} native/WebAssembly parity cases.`); diff --git a/packages/optimizt-gifsicle/scripts/verify-patch.sh b/packages/optimizt-gifsicle/scripts/verify-patch.sh new file mode 100755 index 0000000..6dd5623 --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/verify-patch.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/gifsicle-patch.XXXXXX")" +trap 'rm -rf "$TEMP_DIR"' EXIT + +cp -a "$ROOT/upstream/gifsicle/." "$TEMP_DIR/" +( + cd "$TEMP_DIR" + git apply --check "$ROOT/upstream/gifsicle-wasm.patch" + git apply "$ROOT/upstream/gifsicle-wasm.patch" +) + +printf 'Verified exact Gifsicle integration patch application.\n' diff --git a/packages/optimizt-gifsicle/scripts/verify-upstream.js b/packages/optimizt-gifsicle/scripts/verify-upstream.js new file mode 100644 index 0000000..a88968a --- /dev/null +++ b/packages/optimizt-gifsicle/scripts/verify-upstream.js @@ -0,0 +1,84 @@ +import { createHash } from 'node:crypto'; +import { readFile, readdir, stat } from 'node:fs/promises'; +import path from 'node:path'; + +const upstreamRoot = new URL('../upstream/gifsicle/', import.meta.url); +const hashManifestUrl = new URL('../upstream/gifsicle.sha256', import.meta.url); +const modeManifestUrl = new URL('../upstream/gifsicle.modes', import.meta.url); + +async function listFiles(directory, prefix = '') { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const relative = path.posix.join(prefix, entry.name); + if (entry.isDirectory()) { + const child = new URL(`${encodeURIComponent(entry.name)}/`, directory); + files.push(...await listFiles(child, relative)); + } else if (entry.isFile()) { + files.push(relative); + } else { + throw new Error(`Unexpected non-file in upstream snapshot: ${relative}`); + } + } + return files; +} + +async function readManifest(url, pattern, label) { + const manifest = await readFile(url, 'utf8'); + const values = new Map(); + for (const line of manifest.trimEnd().split(/\r?\n/)) { + const match = pattern.exec(line); + if (!match) { + throw new Error(`Malformed ${label} manifest line: ${line}`); + } + if (values.has(match.groups.file)) { + throw new Error(`Duplicate ${label} manifest file: ${match.groups.file}`); + } + values.set(match.groups.file, match.groups.value); + } + return values; +} + +const expectedHashes = await readManifest( + hashManifestUrl, + /^(?[0-9a-f]{64}) {2}(?:\.\/)?(?.+)$/, + 'hash', +); +const expectedModes = await readManifest( + modeManifestUrl, + /^(?644|755) (?.+)$/, + 'mode', +); +const listedFiles = await listFiles(upstreamRoot); +const files = listedFiles.toSorted((first, second) => first.localeCompare(second)); +const fileSet = new Set(files); +for (const manifest of [expectedHashes, expectedModes]) { + const onDiskOnly = files.filter(file => !manifest.has(file)); + const manifestOnly = manifest.keys().filter(file => !fileSet.has(file)).toArray(); + if (onDiskOnly.length > 0 || manifestOnly.length > 0) { + throw new Error( + 'Upstream snapshot file list differs from its manifest; ' + + `only on disk: [${onDiskOnly.join(', ')}]; ` + + `only in manifest: [${manifestOnly.join(', ')}]`, + ); + } +} + +for (const file of files) { + const encodedPath = file.split('/').map(segment => encodeURIComponent(segment)).join('/'); + const url = new URL(encodedPath, upstreamRoot); + const bytes = await readFile(url); + const actualHash = createHash('sha256').update(bytes).digest('hex'); + if (actualHash !== expectedHashes.get(file)) { + throw new Error(`Upstream hash mismatch: ${file}`); + } + if (process.platform !== 'win32') { + const statistics = await stat(url); + const actualMode = statistics.mode & 0o111 ? '755' : '644'; + if (actualMode !== expectedModes.get(file)) { + throw new Error(`Upstream mode mismatch: ${file}`); + } + } +} + +console.log(`Verified ${files.length} upstream Gifsicle files.`); diff --git a/packages/optimizt-gifsicle/src/wasm/config.h b/packages/optimizt-gifsicle/src/wasm/config.h new file mode 100644 index 0000000..c1f7f6c --- /dev/null +++ b/packages/optimizt-gifsicle/src/wasm/config.h @@ -0,0 +1,57 @@ +#ifndef GIFSICLE_CONFIG_H +#define GIFSICLE_CONFIG_H + +#include + +#define HAVE_CONFIG_H 1 +#define GIF_ALLOCATOR_DEFINED 1 +#define ENABLE_THREADS 0 +#define HAVE_CBRTF 1 +#define HAVE_POW 1 +#define HAVE_SIMD 0 +#define HAVE_INTTYPES_H 1 +#define HAVE_STDINT_H 1 +#define HAVE_STDLIB_H 1 +#define HAVE_STRING_H 1 +#define HAVE_SYS_TYPES_H 1 +#define HAVE_SYS_STAT_H 1 +#define HAVE_UNISTD_H 1 +#define HAVE_STRERROR 1 +#define HAVE_STRTOUL 1 +#define HAVE_MKSTEMP 1 +#define HAVE_SNPRINTF 1 +#define HAVE_INT64_T 1 +#define HAVE_UINT64_T 1 +#define HAVE_UINTPTR_T 1 +#define HAVE___SYNC_ADD_AND_FETCH 1 +#define SIZEOF_FLOAT 4 +#define SIZEOF_UNSIGNED_INT 4 +#if defined(__wasm32__) +#define SIZEOF_UNSIGNED_LONG 4 +#define SIZEOF_VOID_P 4 +#else +#define SIZEOF_UNSIGNED_LONG 8 +#define SIZEOF_VOID_P 8 +#endif +#define PATHNAME_SEPARATOR '/' +/* The native and WebAssembly builds must use the same random sequence. */ +int gifsicle_deterministic_rand(void); +#define RANDOM gifsicle_deterministic_rand + +/* config.h precedes , so this macro also rewrites libc's qsort + declaration. Keep this signature compatible with the standard function. */ +void gifsicle_stable_qsort(void *, size_t, size_t, + int (*)(const void *, const void *)); +#define qsort gifsicle_stable_qsort + +#define VERSION "1.96" +#define PACKAGE "gifsicle" +#define PACKAGE_NAME "gifsicle" +#define PACKAGE_STRING "gifsicle 1.96" +#define PACKAGE_TARNAME "gifsicle" +#define PACKAGE_VERSION "1.96" +#define PACKAGE_BUGREPORT "" +#define PACKAGE_URL "" +#define X_DISPLAY_MISSING 1 + +#endif diff --git a/packages/optimizt-gifsicle/src/wasm/native-driver.c b/packages/optimizt-gifsicle/src/wasm/native-driver.c new file mode 100644 index 0000000..801e5bd --- /dev/null +++ b/packages/optimizt-gifsicle/src/wasm/native-driver.c @@ -0,0 +1,74 @@ +/* Native parity driver for the filesystem-free Gifsicle bridge. + Copyright (C) 2026 Andrey Warkentin + SPDX-License-Identifier: MIT */ + +#include +#include +#include + +int gifsicle_optimize(const uint8_t *data, uint32_t length, + int optimize, int careful, int colors, int lossy, + int gamma_type, double gamma); +const uint8_t *gifsicle_output_data(void); +uint32_t gifsicle_output_size(void); +const char *gifsicle_error_message(void); +void gifsicle_reset(void); + +int +main(int argc, char **argv) +{ + FILE *file; + uint8_t *input; + long size; + int optimize; + int careful; + int colors; + int lossy; + int gamma_type; + double gamma; + int status; + + if (argc != 9) { + fprintf(stderr, "usage: %s input optimize careful colors lossy gamma-type gamma output\n", argv[0]); + return 2; + } + + file = fopen(argv[1], "rb"); + if (!file || fseek(file, 0, SEEK_END) || (size = ftell(file)) < 0 + || size > UINT32_MAX || fseek(file, 0, SEEK_SET)) { + fprintf(stderr, "could not inspect input\n"); + return 3; + } + input = malloc((size_t) size); + if (!input || fread(input, 1, (size_t) size, file) != (size_t) size) { + fprintf(stderr, "could not read input\n"); + return 3; + } + fclose(file); + + optimize = (int) strtol(argv[2], NULL, 10); + careful = (int) strtol(argv[3], NULL, 10); + colors = (int) strtol(argv[4], NULL, 10); + lossy = (int) strtol(argv[5], NULL, 10); + gamma_type = (int) strtol(argv[6], NULL, 10); + gamma = strtod(argv[7], NULL); + status = gifsicle_optimize(input, (uint32_t) size, optimize, careful, + colors, lossy, gamma_type, gamma); + free(input); + if (status) { + fprintf(stderr, "%s\n", gifsicle_error_message()); + gifsicle_reset(); + return status; + } + + file = fopen(argv[8], "wb"); + if (!file || fwrite(gifsicle_output_data(), 1, gifsicle_output_size(), file) + != gifsicle_output_size() + || fclose(file)) { + fprintf(stderr, "could not write output\n"); + gifsicle_reset(); + return 3; + } + gifsicle_reset(); + return 0; +} diff --git a/packages/optimizt-gifsicle/src/wasm/stable-qsort.c b/packages/optimizt-gifsicle/src/wasm/stable-qsort.c new file mode 100644 index 0000000..31eb6dc --- /dev/null +++ b/packages/optimizt-gifsicle/src/wasm/stable-qsort.c @@ -0,0 +1,76 @@ +/* Stable qsort compatibility for WebAssembly builds. + Copyright (C) 2026 Andrey Warkentin + SPDX-License-Identifier: MIT */ + +#include +#include +#include +#include + +int +gifsicle_deterministic_rand(void) +{ + static uint32_t state = 1; + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + return (int) (state & 0x7FFFFFFFU); +} + +void +gifsicle_stable_qsort(void *base, size_t count, size_t size, + int (*compare)(const void *, const void *)) +{ + unsigned char *values = base; + unsigned char *scratch; + unsigned char *source; + unsigned char *destination; + size_t width; + + if (count < 2 || size == 0) + return; + + scratch = Gif_Realloc(NULL, size, count, __FILE__, __LINE__); + source = values; + destination = scratch; + + for (width = 1; width < count;) { + size_t start; + for (start = 0; start < count; start += width * 2) { + size_t left = start; + size_t middle = start + width < count ? start + width : count; + size_t right = middle; + size_t end = middle + width < count ? middle + width : count; + size_t output = start; + + while (left < middle && right < end) { + size_t selected; + if (compare(source + left * size, source + right * size) <= 0) + selected = left++; + else + selected = right++; + memcpy(destination + output++ * size, source + selected * size, size); + } + while (left < middle) { + memcpy(destination + output++ * size, source + left++ * size, size); + } + while (right < end) { + memcpy(destination + output++ * size, source + right++ * size, size); + } + } + + { + unsigned char *temporary = source; + source = destination; + destination = temporary; + } + if (width > count / 2) + width = count; + else + width *= 2; + } + + if (source != values) + memcpy(values, source, count * size); + Gif_Free(scratch); +} diff --git a/packages/optimizt-gifsicle/test/api.test.js b/packages/optimizt-gifsicle/test/api.test.js new file mode 100644 index 0000000..5f2caaa --- /dev/null +++ b/packages/optimizt-gifsicle/test/api.test.js @@ -0,0 +1,189 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +import optimize, { + createOperation, + errorCodes, + optimize as namedOptimize, +} from '../index.js'; + +const fixtureUrl = new URL('fixtures/animated.gif', import.meta.url); + +function sha256(value) { + return createHash('sha256').update(value).digest('hex'); +} + +async function expectCode(promise, code) { + await assert.rejects(promise, (error) => { + assert.equal(error.code, code); + return true; + }); +} + +test('exports an asynchronous optimizer and frozen error codes', async () => { + assert.equal(namedOptimize, optimize); + assert.ok(Object.isFrozen(errorCodes)); + const input = await readFile(fixtureUrl); + const result = optimize(input); + assert.ok(result instanceof Promise); + assert.equal( + sha256(await result), + '0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8', + ); +}); + +test('snapshots selected Uint8Array bytes without modifying caller storage', async () => { + const fixture = await readFile(fixtureUrl); + const storage = Buffer.concat([Buffer.from('before'), fixture, Buffer.from('after')]); + const view = new Uint8Array(storage.buffer, storage.byteOffset + 6, fixture.length); + const promise = optimize(view, Object.create(null)); + view.fill(0); + const output = await promise; + + assert.equal( + sha256(output), + '0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8', + ); + assert.equal(storage.subarray(0, 6).toString(), 'before'); + assert.equal(storage.subarray(-5).toString(), 'after'); + assert.notEqual(output.buffer, view.buffer); +}); + +test('validates input and options with stable error codes', async () => { + const fixture = await readFile(fixtureUrl); + await expectCode(optimize('gif'), errorCodes.INVALID_INPUT); + await expectCode(optimize(new Uint8Array()), errorCodes.INVALID_INPUT); + await expectCode(optimize(fixture, 1), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { unknown: true }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { optimize: true }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { optimize: 4 }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { careful: 1 }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { colors: 1 }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { lossy: -1 }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { gamma: 'SRGB' }), errorCodes.INVALID_OPTIONS); + // eslint-disable-next-line unicorn/new-for-builtins + const boxedNumber = new Number(1); + await expectCode( + optimize(fixture, { optimize: boxedNumber }), + errorCodes.INVALID_OPTIONS, + ); + await expectCode(optimize(fixture, { colors: 2.5 }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { lossy: Infinity }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { gamma: 0 }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, { gamma: NaN }), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, []), errorCodes.INVALID_OPTIONS); + await expectCode(optimize(fixture, new Date()), errorCodes.INVALID_OPTIONS); + + await optimize(fixture, { + optimize: undefined, + careful: undefined, + colors: undefined, + lossy: undefined, + gamma: undefined, + }); +}); + +test('accepts exactly 128 MiB and rejects one byte more before Worker creation', { + timeout: 30_000, +}, async () => { + const fixture = await readFile(new URL('fixtures/single.gif', import.meta.url)); + const atLimit = Buffer.alloc(128 * 1024 * 1024); + fixture.copy(atLimit); + assert.ok(await optimize(atLimit) instanceof Buffer); + + const overLimit = Buffer.allocUnsafe(128 * 1024 * 1024 + 1); + const before = process.getActiveResourcesInfo(); + await expectCode(optimize(overLimit), errorCodes.INVALID_INPUT); + assert.deepEqual(process.getActiveResourcesInfo(), before); +}); + +test('returns Gifsicle output even when it is larger than the input', async () => { + const input = await readFile(fixtureUrl); + const output = await optimize(input, { careful: true }); + assert.ok(output.length > input.length); +}); + +test('supports every setting and false disables optional settings', async () => { + const fixture = await readFile(fixtureUrl); + for (const options of [ + { optimize: 0 }, + { optimize: 1 }, + { optimize: 2 }, + { optimize: 3 }, + { optimize: false }, + { careful: true }, + { careful: false }, + { colors: 2 }, + { colors: 256 }, + { colors: false }, + { lossy: 0 }, + { lossy: 20 }, + { lossy: 2_147_483_647 }, + { lossy: false }, + { gamma: 1 }, + { gamma: 2.2 }, + { gamma: 'srgb' }, + { gamma: 'oklab' }, + { gamma: false }, + ]) { + assert.ok(await optimize(fixture, options) instanceof Buffer); + } +}); + +test('classifies malformed GIF input and recovers supported truncation', async () => { + await expectCode(optimize(Buffer.from('not a gif')), errorCodes.INVALID_INPUT); + + const fixture = await readFile(fixtureUrl); + await expectCode(optimize(fixture.subarray(0, 13)), errorCodes.INVALID_INPUT); + await expectCode(optimize(fixture.subarray(0, -5)), errorCodes.INVALID_INPUT); + + const corruptCompressedData = Buffer.from( + await readFile(new URL('fixtures/single.gif', import.meta.url)), + ); + const imageSeparator = corruptCompressedData.indexOf(0x2C, 13); + assert.notEqual(imageSeparator, -1); + corruptCompressedData[imageSeparator + 10] = 13; + await expectCode(optimize(corruptCompressedData), errorCodes.INVALID_INPUT); + + const recovered = await optimize(fixture.subarray(0, -1)); + assert.ok(recovered instanceof Buffer); +}); + +test('settles only after its Worker resource exits', async () => { + const fixture = await readFile(fixtureUrl); + const operation = optimize(fixture); + assert.ok(process.getActiveResourcesInfo().includes('MessagePort')); + await operation; + assert.ok(!process.getActiveResourcesInfo().includes('MessagePort')); +}); + +test('exposes a cancellable operation for the Optimizt lifecycle', async () => { + const fixture = await readFile(fixtureUrl); + const operation = createOperation(fixture, { optimize: 3 }); + await operation.terminate(); + await expectCode(operation.promise, errorCodes.WORKER_FAILED); +}); + +test('supports the Optimizt byte-API migration presets', async () => { + const input = await readFile(fixtureUrl); + for (const options of [ + { optimize: 0, careful: true, colors: 256, lossy: 0, gamma: 1 }, + { optimize: 3, careful: false, colors: 256, lossy: 100, gamma: 1 }, + ]) { + const output = await optimize(input, options); + assert.ok(Buffer.isBuffer(output)); + } +}); + +test('supports concurrent isolated optimizations', async () => { + const fixture = await readFile(fixtureUrl); + const [levelOne, levelThree] = await Promise.all([ + optimize(fixture, { optimize: 1 }), + optimize(fixture, { optimize: 3 }), + ]); + assert.ok(levelOne instanceof Buffer); + assert.ok(levelThree instanceof Buffer); + assert.notEqual(sha256(levelOne), sha256(levelThree)); +}); diff --git a/packages/optimizt-gifsicle/test/fixtures/animated.gif b/packages/optimizt-gifsicle/test/fixtures/animated.gif new file mode 100644 index 0000000000000000000000000000000000000000..6e43e6bada4ea6d3fce32e604d113d688f26f719 GIT binary patch literal 8703 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 literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/test/fixtures/empty-transparent.gif b/packages/optimizt-gifsicle/test/fixtures/empty-transparent.gif new file mode 100644 index 0000000000000000000000000000000000000000..63a010c7141746c4e2431f609e77889f7a7aac84 GIT binary patch literal 219 zcmZ?wbhEHbWMW`qC}3bv{K>+|$iTp$1H?cPCLqDo63Af9zH#Yu-A&gscHP!3)BM_% zwxB9Xq4U_%ji>Ha)o#4~Vp$gZmfNyx*RFiCbk)6icV(->HeL43*>pW=egsfz=^~Cf zS4-wJ+aZyuLJB s*W}u147=(KA8}^nu8dg}rM-38)9SQaMIPFVUf#V7cEDpfT{H&(0O1E|<^TWy literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/test/fixtures/extensions.gif b/packages/optimizt-gifsicle/test/fixtures/extensions.gif new file mode 100644 index 0000000000000000000000000000000000000000..4f46c7645c481ae1b93517c98531273029daf7f4 GIT binary patch literal 856 zcmZ?wbhEHbG+=OH_{zY*kZs7pz@b=dX;^G&S#9aWAQ4n;8C30*ETNg*;#A$@)WTpm zrN?V6gVWY2UdI@`o^u2}ck+5374&>g)cc-e@3&t2&Jp!pAo;sQ_IJhX@0P{iom##J zP5E9u<@=nV?@NIw`TN?I|1E3(Z*BR1Y|8(0J^#-w{eNxi|9j{D|NjrPpF*JclZBCi zL6br8A5U6lMM-HNh*Wlf9??1;1I`v07E?kMdrk$WCk6ORUmIOu>C(UrC`QJ ziT)EyicZ?ZSvV&Zd*#|K*7Pksy*0)q!l(Ssk%VCJO;VM;d6!;)6_4&&e>P&r+&KsL zeNPdWDrsnxt!z!MaF(j-4DEJG>W^s-oXpF?I(s%ZH!uH`VrSXR`3v0z_<4D`Svj}` z8YXI%ZVC}tH-CFp?ewg*jI6wEiDff+_k@azL_}4uXK;>S|A6SitUzi2-B?&rBKQ`}6X z=MZD_E@dv`SJl^28Ygm#s#$HGp|a4eRp#NzRq95rN4X39vT{zeBzWt{TKt$iv(S6? z|JlOMAFik;a<@%zQ16p?)wpaDcl#timV;L^g=PmWp0mK}%B$l%9-EUCKQ_KRxLo>B zSFh5POfEr5gMx-N_%!K*vH9Nu}!bSNuHT;Lvs3@+O(DJ%tDB~CYa@0RCI=+eV2BU*q`NyBrxqr#si63q; xem!{OVcn#yKV+JJb1t0aCO>Dw5`iAW7f&X8v3+CY2o75LbXrW=&jC literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/test/fixtures/local-palette.gif b/packages/optimizt-gifsicle/test/fixtures/local-palette.gif new file mode 100644 index 0000000000000000000000000000000000000000..29d5d2ca1e7150c67b39edc628d8e3e45fdc26bd GIT binary patch literal 821 zcmZ?wbhEHbG+=OHC}m(!{K>+|z@W*X!vF*zu~vp`LkK3OK2E!>mUTYbgwodUn#^CjwBj~x4*Yl{L=X0Xo_Z)k__1brisP6*F z-zBoYD`tPUEdK7)@;zwE_v$I%=LCIU3Pj1@*S7p`S^Ix$%l~6j{-5jle{SjjYg_-{ zJNN(pe~>dM18n~fOevVLQKJ9ElA@C~aTd-=#a_8~i#2^qPj8KJiSQ}Eb0i^He3MjV zZ{DTXU&W())}M{oF?Y_vecw~WrAitaWh+~gE1adOIzzjilKNws11Ixxu+E;%&CSa{ zrPx_EbN)hi0e)UyZdML%frg2irJF(o*3I9ZRXaUvEh8&$TVmNv-aVn>A`wy5YdMZg zNUv`Y;APzyJS|XZa&)QB&O<9a7uWT9OTKyg$eaJxGq){)y?2D<>g={dyMyi1wO_*M0_l*WnNqH0!~XQ(W6Yn6F;a+SJ~>rw6k zzpR`SEeYN_vKBuk&n)zw{eQNw^M@o3;r#5Y6J2ShCfX4(uKqmm92lB==J#_uE0s|x8is=U!e@Up% z&F5rrXkcQuSngkPcjAXzj9(Alcvv@S>kpac-<%6)xyjF&utcE8@WqqKUTohOIf8>$ OKAjd*_A^3&!5RRF>`H+E literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/test/fixtures/megapixel.gif b/packages/optimizt-gifsicle/test/fixtures/megapixel.gif new file mode 100644 index 0000000000000000000000000000000000000000..871f46b57cb6f00ac0abbd5613f6de89fddce81c GIT binary patch literal 1790 zcmV-YS={|^`_I7nD%c!-#&xX9S( z_y`#(IZ0V*d5M{+xyjk-`3V{-I!an`U)E>J4;(@dyAW^yUW|_`wJW_ zJWO0{e2ko|yv*F}{0to}JxyJ0eT|*1z0KY2{S6*2K2Bb4evY25zRuq6{th26KTlt8 ze~+K9zt7+A{|_*rz<~q{8a#+Fp~8g>8#;UlF`~qY6f0W1h%uwajsF}wdi)47q{xvZ zOPV~1GNsCuEL*yK2{We5nKWzKyoocX&Ye7a`uqtrsL-KAiyA$OG^x_1Oq)7=3N@kh3pcLZxpeE=y^A-m-o1SL`uz(y zu;9Uj3mZO+II-fzj2kC~%Rzm7e-_U+ue zd;bnTy!i3t%bP!sKE3+&?AyD44?n*A`Sk1CzmGq^{{8&>`~UwBV1NP+NML~m9*AIq z3NFZCgAP6jVT2M+NMVH*UWj3a8g9s8haP?iVu&J+NMea5o`_m9*bQj3vj>! z4@_{u1|N)Y!U`|UaKjEi3~|H~PfT&e7GI2U#u{(TamOBi406aKk4$pOCZCLQ$||qS za?38i40Fsf&rEa8Hs6eM&N}bRbI(5i40O;!4^4E@MjwrI(n>GQbkj~h4RzE~Pfc~z zR$q;E)>?1Pb=O{h4R+XKk4<*jW}l6A+G?-OcH3^h4R_pf&rNsTcHfP6-g@uNci(>h z4S3*!4^DXDh98c2;)*ZMc;k*g4teB~PfmH|mj7RldFGmL&Uxpae-3)+qK{5`>878K zdg`jL&U)*vzYcrsvd>O??Y7^Jd+xgL&U^2^{|$`R1RGe){UK&wl&vzYl->^3P9y{r2CFfByRK z&wu~^{|~?b3UGh~ET91oh`OPtq%5T=Pl?J@s&bXAY^5t-3Cmc@a+b8Lr7drX z%UtSmm%QwyFMkQlU zl&W;4EN!VvUkcNh%5L*uV;Qu!JqFVGoPg#42{NjBTuA9}C&YN_Mi8t*m7)i`mR- gcC(!AtY<$9+R%!2w4^PqX-|vV)T(y1xGMkvI}I?KRsaA1 literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/test/fixtures/recoverable-trailing-garbage.gif b/packages/optimizt-gifsicle/test/fixtures/recoverable-trailing-garbage.gif new file mode 100644 index 0000000000000000000000000000000000000000..42e3d949f4f00f94035691e8dc864e11b10a79d4 GIT binary patch literal 66 zcmZ?wbhEHbWMp7uXkdT>#h)yUAf^t80Ld^gF}W}@Sf?ZwXJx156{lopCFZ4NXQkv7 JCuXN{0RTSl4HN(X literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/test/fixtures/single.gif b/packages/optimizt-gifsicle/test/fixtures/single.gif new file mode 100644 index 0000000000000000000000000000000000000000..228b102de7a0bcf0199fb096d668a867b4c0ed48 GIT binary patch literal 821 zcmZ?wbhEHbG+=OH_{zY*kZs7pz@b=dX;^G&S#9aWAQ4n;8C30*ETNg*;#A$@)WTpm zrN?V6gVWY2UdI@`o^u2}ck+5374&>g)cc-e@3&t2&Jp!pAo;sQ_IJhX@0P{iom##J zP5E9u<@=nV?@NIw`TN?I|1E3(Z*BR1Y|8(0J^#-w{eNxi|9j{D|NjrPpF*JclZBCi zL6boTWDdwr3~c`oOevVLQKJ9ElA@C~aTd-=#a_8~i#2^qPj8KJiSQ}Eb0i^He3MjV zZ{DTXU&W())}M{oF?Y_vecw~WrAitaWh+~gE1adOIzzjilKNws11Ixxu+E;%&CSa{ zrPx_EbN)hi0e)UyZdML%frg2irJF(o*3I9ZRXaUvEh8&$TVmNv-aVn>A`wy5YdMZg zNUv`Y;APzyJS|XZa&)QB&O<9a7uWT9OTKyg$eaJxGq){)y?2D<>g={dyMyi1wO_*M0_l*WnNqH0!~XQ(W6Yn6F;a+SJ~>rw6k zzpR`SEeYN_vKBuk&n)zw{eQNw^M@o3;r#5Y6J2ShCfX4(uKqmm92lB==J#_uE0s|x8is=U!e@Up% z&F5rrXkcQuSngkPcjAXzj9(Alcvv@S>kpac-<%6)xyjF&utcE8@WqqKUTohOIf8>$ OKAjd*_A^3&!5RR%{7QTP literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/test/fixtures/transparent.gif b/packages/optimizt-gifsicle/test/fixtures/transparent.gif new file mode 100644 index 0000000000000000000000000000000000000000..0029a9a7bf1e8bff6daedff6d1e6a098c8c9f8c3 GIT binary patch literal 242 zcmZ?wbhEHb)L;-{_{hNU9|$fp{AU0G#h)yUB0yRP!~w}OFiExauRQ&hfAO3xx4Jjq z+w+^h?UBc{XPwJlb%|nAu7Rd}KkFlj29C~IY_n$RtlXEZGd<7u^v~Vf(-=XTInnIG P(9HQr1E*$E9BK^!J*`{K literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/test/fixtures/zero-dimensions.gif b/packages/optimizt-gifsicle/test/fixtures/zero-dimensions.gif new file mode 100644 index 0000000000000000000000000000000000000000..3067bf11096a0012041b259d34185f600ca153fe GIT binary patch literal 35 icmZ?wbhEHbWMp7uXkY+=|NsB%fEgfyiOGeL!5RR3dj;44 literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/test/limits.test.js b/packages/optimizt-gifsicle/test/limits.test.js new file mode 100644 index 0000000..02694a1 --- /dev/null +++ b/packages/optimizt-gifsicle/test/limits.test.js @@ -0,0 +1,89 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import test from 'node:test'; + +import optimize, { errorCodes } from '../index.js'; + +const fixtureUrl = new URL('fixtures/animated.gif', import.meta.url); + +async function expectInvalid(input) { + await assert.rejects(optimize(input), (error) => { + assert.equal(error.code, errorCodes.INVALID_INPUT); + return true; + }); +} + +test('rejects excessive logical canvas area during parsing', async () => { + const input = Buffer.from(await readFile(fixtureUrl)); + input.writeUInt16LE(0xFF_FF, 6); + input.writeUInt16LE(0xFF_FF, 8); + await expectInvalid(input); +}); + +test('rejects excessive total frame area during parsing', async () => { + const input = Buffer.from(await readFile(fixtureUrl)); + const imageSeparator = input.indexOf(0x2C, 13); + assert.notEqual(imageSeparator, -1); + input.writeUInt16LE(0xFF_FF, imageSeparator + 5); + input.writeUInt16LE(0xFF_FF, imageSeparator + 7); + await expectInvalid(input); +}); + +test('accepts logical-canvas and cumulative-frame-area boundaries', async () => { + const base = await readFile(new URL('fixtures/megapixel.gif', import.meta.url)); + const imageSeparator = base.indexOf(0x2C, 20); + assert.notEqual(imageSeparator, -1); + const header = Buffer.from(base.subarray(0, imageSeparator)); + const frame = base.subarray(imageSeparator, -1); + + header.writeUInt16LE(16_384, 6); + header.writeUInt16LE(8192, 8); + const maximumCanvasGif = Buffer.concat([ + header, + frame, + Buffer.from([0x3B]), + ]); + assert.ok(await optimize(maximumCanvasGif) instanceof Buffer); + + const originalHeader = base.subarray(0, imageSeparator); + const frames = Array.from({ length: 128 }, () => frame); + const maximumFrameAreaGif = Buffer.concat([ + originalHeader, + ...frames, + Buffer.from([0x3B]), + ]); + assert.ok(await optimize(maximumFrameAreaGif) instanceof Buffer); +}); + +test('rejects the 100,001st frame while accepting 100,000 small frames', { + timeout: 120_000, +}, async () => { + const onePixelFrame = Buffer.from([ + 0x2C, 0, 0, 0, 0, 1, 0, 1, 0, 0, + 2, 2, 0x44, 1, 0, + ]); + const header = Buffer.from([ + ...Buffer.from('GIF89a'), + 1, 0, 1, 0, 0x80, 0, 0, + 0, 0, 0, 0xFF, 0xFF, 0xFF, + ]); + const frames = Array.from({ length: 100_000 }, () => onePixelFrame); + const atLimit = Buffer.concat([header, ...frames, Buffer.from([0x3B])]); + assert.ok(await optimize(atLimit) instanceof Buffer); + await expectInvalid(Buffer.concat([ + header, + ...frames, + onePixelFrame, + Buffer.from([0x3B]), + ])); +}); + +test('returns recovered output for trailing garbage', async () => { + const input = await readFile( + new URL('fixtures/recoverable-trailing-garbage.gif', import.meta.url), + ); + const output = await optimize(input); + assert.ok(output instanceof Buffer); + assert.ok(output.length < input.length); + assert.match(output.subarray(0, 6).toString(), /^GIF8[79]a$/); +}); diff --git a/packages/optimizt-gifsicle/test/native-reference.test.js b/packages/optimizt-gifsicle/test/native-reference.test.js new file mode 100644 index 0000000..b5a2645 --- /dev/null +++ b/packages/optimizt-gifsicle/test/native-reference.test.js @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import process from 'node:process'; +import { promisify } from 'node:util'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { nativeLauncher } from '../scripts/native-reference.js'; + +const execFileAsync = promisify(execFile); + +async function withLauncher(value, operation) { + const previous = process.env.GIFSICLE_NATIVE_LAUNCHER; + try { + if (value === undefined) { + delete process.env.GIFSICLE_NATIVE_LAUNCHER; + } else { + process.env.GIFSICLE_NATIVE_LAUNCHER = value; + } + return await operation(); + } finally { + if (previous === undefined) { + delete process.env.GIFSICLE_NATIVE_LAUNCHER; + } else { + process.env.GIFSICLE_NATIVE_LAUNCHER = previous; + } + } +} + +test('nativeLauncher reports malformed JSON as a configuration error', async () => { + for (const value of ['not-json', '[]', '[1]']) { + await withLauncher(value, () => { + assert.throws( + () => nativeLauncher(), + /GIFSICLE_NATIVE_LAUNCHER must be a nonempty JSON array of strings/, + ); + }); + } +}); + +test('runNative returns stdout and forwards stderr', async () => { + const child = [ + 'process.stdout.write("native output");', + 'process.stderr.write("native diagnostic");', + ].join(''); + const probe = [ + 'import { runNative } from "./scripts/native-reference.js";', + `const output = await runNative(process.execPath, [], ["--eval", ${JSON.stringify(child)}]);`, + 'process.stdout.write(`returned:${output}`);', + ].join('\n'); + const { stdout, stderr } = await execFileAsync( + process.execPath, + ['--input-type=module', '--eval', probe], + { cwd: fileURLToPath(new URL('../', import.meta.url)) }, + ); + assert.equal(stdout, 'returned:native output'); + assert.equal(stderr, 'native diagnostic'); +}); diff --git a/packages/optimizt-gifsicle/upstream/gifsicle-wasm.patch b/packages/optimizt-gifsicle/upstream/gifsicle-wasm.patch new file mode 100644 index 0000000..f93e29b --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle-wasm.patch @@ -0,0 +1,771 @@ +diff --git a/include/lcdfgif/gif.h b/include/lcdfgif/gif.h +index 28b1c83..7f05c66 100644 +--- a/include/lcdfgif/gif.h ++++ b/include/lcdfgif/gif.h +@@ -38,6 +38,9 @@ typedef uint16_t Gif_Code; + + /** GIF_STREAM **/ + ++#define GIF_READ_STATUS_OK 0 ++#define GIF_READ_STATUS_LIMIT 1 ++ + struct Gif_Stream { + Gif_Image **images; + int nimages; +@@ -54,6 +57,7 @@ struct Gif_Stream { + Gif_Extension* end_extension_list; + + unsigned errors; ++ unsigned read_status; + uint32_t user_flags; + + const char* landmark; +@@ -248,6 +252,12 @@ struct Gif_Record { + uint32_t length; + }; + ++typedef struct { ++ uint32_t max_frames; ++ uint64_t max_logical_area; ++ uint64_t max_total_frame_area; ++} Gif_ReadLimits; ++ + #define GIF_READ_COMPRESSED 1 + #define GIF_READ_UNCOMPRESSED 2 + #define GIF_READ_CONST_RECORD 4 +@@ -265,9 +275,17 @@ Gif_Stream* Gif_ReadRecord(const Gif_Record* record); + Gif_Stream* Gif_FullReadRecord(const Gif_Record* record, int flags, + const char* landmark, + Gif_ReadErrorHandler handler); ++Gif_Stream* Gif_FullReadRecordLimited(const Gif_Record* record, int flags, ++ const char* landmark, ++ const Gif_ReadLimits* limits, ++ Gif_ReadErrorHandler handler); + int Gif_WriteFile(Gif_Stream *gfs, FILE *f); + int Gif_FullWriteFile(Gif_Stream *gfs, + const Gif_CompressInfo *gcinfo, FILE *f); ++int Gif_FullWriteRecord(Gif_Stream *gfs, ++ const Gif_CompressInfo *gcinfo, ++ Gif_Record *record); ++void Gif_FreeRecord(Gif_Record *record); + + #define Gif_ReadFile(f) Gif_FullReadFile((f),GIF_READ_UNCOMPRESSED,0,0) + #define Gif_ReadRecord(r) Gif_FullReadRecord((r),GIF_READ_UNCOMPRESSED,0,0) +@@ -299,6 +317,8 @@ void Gif_Debug(char *x, ...); + #define GIF_DEBUG(x) + #endif + ++typedef void (*Gif_AllocationFailureHandler)(void); ++void Gif_SetAllocationFailureHandler(Gif_AllocationFailureHandler handler); + void* Gif_Realloc(void* p, size_t s, size_t n, + const char* file, int line); + void Gif_Free(void* p); +diff --git a/src/fmalloc.c b/src/fmalloc.c +index bddfca6..906de2a 100644 +--- a/src/fmalloc.c ++++ b/src/fmalloc.c +@@ -9,6 +9,18 @@ + extern "C" { + #endif + extern const char* program_name; ++static Gif_AllocationFailureHandler allocation_failure_handler; ++ ++void Gif_SetAllocationFailureHandler(Gif_AllocationFailureHandler handler) { ++ allocation_failure_handler = handler; ++} ++ ++static void allocation_failed(const char* message) { ++ if (allocation_failure_handler) ++ allocation_failure_handler(); ++ fprintf(stderr, "%s: %s\n", program_name, message); ++ exit(1); ++} + + void* Gif_Realloc(void* p, size_t s, size_t n, const char* file, int line) { + (void) file, (void) line; +@@ -16,15 +28,12 @@ void* Gif_Realloc(void* p, size_t s, size_t n, const char* file, int line) { + Gif_Free(p); + return (void*) 0; + } else if (s == 1 || n == 1 || s <= ((size_t) -1) / n) { +- p = realloc(p, s * n); +- if (!p) { +- fprintf(stderr, "%s: Out of memory, giving up\n", program_name); +- exit(1); +- } +- return p; ++ void* q = realloc(p, s * n); ++ if (!q) ++ allocation_failed("Out of memory, giving up"); ++ return q; + } else { +- fprintf(stderr, "%s: Out of memory, giving up (huge allocation)\n", program_name); +- exit(1); ++ allocation_failed("Out of memory, giving up (huge allocation)"); + return (void*) 0; + } + } +diff --git a/src/giffunc.c b/src/giffunc.c +index 3ddf2df..74bbae2 100644 +--- a/src/giffunc.c ++++ b/src/giffunc.c +@@ -33,6 +33,7 @@ Gif_NewStream(void) + gfs->end_comment = 0; + gfs->end_extension_list = 0; + gfs->errors = 0; ++ gfs->read_status = GIF_READ_STATUS_OK; + gfs->user_flags = 0; + gfs->refcount = 0; + gfs->landmark = 0; +diff --git a/src/gifread.c b/src/gifread.c +index 6be5e18..cb350e4 100644 +--- a/src/gifread.c ++++ b/src/gifread.c +@@ -41,6 +41,9 @@ typedef struct { + Gif_ReadErrorHandler handler; + int errors[2]; + ++ const Gif_ReadLimits *limits; ++ uint64_t total_frame_area; ++ + } Gif_Context; + + +@@ -413,15 +416,24 @@ read_color_table(int size, Gif_Reader *grr) + + + static int +-read_logical_screen_descriptor(Gif_Stream *gfs, Gif_Reader *grr) +- /* returns 0 on memory error */ ++read_logical_screen_descriptor(Gif_Context *gfc, Gif_Reader *grr) ++ /* returns 0 on memory or policy error */ + { ++ Gif_Stream *gfs = gfc->stream; + uint8_t packed; + + /* we don't care about logical screen width or height */ + gfs->screen_width = gifgetunsigned(grr); + gfs->screen_height = gifgetunsigned(grr); + ++ if (gfc->limits ++ && (uint64_t) gfs->screen_width * gfs->screen_height ++ > gfc->limits->max_logical_area) { ++ gfs->read_status = GIF_READ_STATUS_LIMIT; ++ gif_read_error(gfc, 1, "logical screen exceeds resource limit"); ++ return 0; ++ } ++ + packed = gifgetbyte(grr); + gfs->background = gifgetbyte(grr); + +@@ -593,6 +605,17 @@ read_image(Gif_Reader *grr, Gif_Context *gfc, Gif_Image *gfi, int read_flags) + Gif_MakeImageEmpty(gfi); + read_flags = 0; + } ++ if (gfc->limits) { ++ uint64_t area = (uint64_t) gfi->width * gfi->height; ++ if (area > gfc->limits->max_total_frame_area ++ || gfc->total_frame_area ++ > gfc->limits->max_total_frame_area - area) { ++ gfc->stream->read_status = GIF_READ_STATUS_LIMIT; ++ gif_read_error(gfc, 1, "total frame area exceeds resource limit"); ++ return 0; ++ } ++ gfc->total_frame_area += area; ++ } + GIF_DEBUG(("<%ux%u> ", gfi->width, gfi->height)); + + packed = gifgetbyte(grr); +@@ -786,7 +809,8 @@ read_comment_extension(Gif_Image *gfi, Gif_Reader *grr) + + static Gif_Stream * + read_gif(Gif_Reader *grr, int read_flags, +- const char* landmark, Gif_ReadErrorHandler handler) ++ const char* landmark, const Gif_ReadLimits *limits, ++ Gif_ReadErrorHandler handler) + { + Gif_Stream *gfs; + Gif_Image *gfi; +@@ -811,13 +835,15 @@ read_gif(Gif_Reader *grr, int read_flags, + gfc.handler = handler; + gfc.gfi = gfi; + gfc.errors[0] = gfc.errors[1] = 0; ++ gfc.limits = limits; ++ gfc.total_frame_area = 0; + + if (!gfs || !gfi || !gfc.prefix || !gfc.suffix || !gfc.length) + goto done; + gfs->landmark = landmark; + + GIF_DEBUG(("\nGIF ")); +- if (!read_logical_screen_descriptor(gfs, grr)) ++ if (!read_logical_screen_descriptor(&gfc, grr)) + goto done; + GIF_DEBUG(("logscrdesc ")); + +@@ -830,6 +856,12 @@ read_gif(Gif_Reader *grr, int read_flags, + case ',': /* image block */ + GIF_DEBUG(("imageread %d ", gfs->nimages)); + ++ if (limits && (uint32_t) gfs->nimages >= limits->max_frames) { ++ gfs->read_status = GIF_READ_STATUS_LIMIT; ++ gif_read_error(&gfc, 1, "frame count exceeds resource limit"); ++ goto done; ++ } ++ + gfi->identifier = last_name; + last_name = 0; + if (!Gif_AddImage(gfs, gfi)) +@@ -938,19 +970,27 @@ Gif_FullReadFile(FILE *f, int read_flags, + grr.byte_getter = file_byte_getter; + grr.block_getter = file_block_getter; + grr.eofer = file_eofer; +- return read_gif(&grr, read_flags, landmark, h); ++ return read_gif(&grr, read_flags, landmark, NULL, h); + } + + Gif_Stream * +-Gif_FullReadRecord(const Gif_Record *gifrec, int read_flags, +- const char* landmark, Gif_ReadErrorHandler h) ++Gif_FullReadRecordLimited(const Gif_Record *gifrec, int read_flags, ++ const char* landmark, const Gif_ReadLimits *limits, ++ Gif_ReadErrorHandler h) + { + Gif_Reader grr; + if (!gifrec) return 0; + make_data_reader(&grr, gifrec->data, gifrec->length); + if (read_flags & GIF_READ_CONST_RECORD) + read_flags |= GIF_READ_COMPRESSED; +- return read_gif(&grr, read_flags, landmark, h); ++ return read_gif(&grr, read_flags, landmark, limits, h); ++} ++ ++Gif_Stream * ++Gif_FullReadRecord(const Gif_Record *gifrec, int read_flags, ++ const char* landmark, Gif_ReadErrorHandler h) ++{ ++ return Gif_FullReadRecordLimited(gifrec, read_flags, landmark, NULL, h); + } + + +diff --git a/src/gifsicle.c b/src/gifsicle.c +index 6b9fe8e..b285585 100644 +--- a/src/gifsicle.c ++++ b/src/gifsicle.c +@@ -903,8 +903,8 @@ set_new_fixed_colormap(const char *name, int exact) + def_output_data.colormap_fixed_exact = exact; + } + +-static void +-do_colormap_change(Gif_Stream *gfs) ++void ++gifsicle_apply_colormap(Gif_Stream *gfs) + { + if (active_output_data.colormap_fixed) + colormap_stream(gfs, active_output_data.colormap_fixed, +@@ -1035,7 +1035,7 @@ merge_and_write_frames(const char *outfile, int f1, int f2) + active_output_data.scale_method, + active_output_data.scale_colors); + if (colormap_change) +- do_colormap_change(out); ++ gifsicle_apply_colormap(out); + if (output_transforms) + apply_color_transforms(output_transforms, out); + if (active_output_data.optimizing & GT_OPT_MASK) +diff --git a/src/gifsicle.h b/src/gifsicle.h +index 55b2f85..7731173 100644 +--- a/src/gifsicle.h ++++ b/src/gifsicle.h +@@ -186,6 +186,9 @@ extern int no_warnings; + extern int thread_count; + extern Gif_CompressInfo gif_write_info; + ++typedef void (*Gt_DiagnosticHandler)(const char *message, size_t length); ++void set_diagnostic_handler(Gt_DiagnosticHandler handler); ++ + void fatal_error(const char* format, ...) NORETURN; + void warning(int need_file, const char* format, ...); + void lwarning(const char* landmark, const char* format, ...); +@@ -281,6 +284,7 @@ enum { + }; + int set_dither_type(Gt_OutputData* od, const char* name); + void colormap_stream(Gif_Stream*, Gif_Colormap*, Gt_OutputData*, int allow_shrink); ++void gifsicle_apply_colormap(Gif_Stream *); + + struct kchist; + Gif_Colormap* colormap_blend_diversity(struct kchist* kch, Gt_OutputData* od); +diff --git a/src/gifsicle_bridge.c b/src/gifsicle_bridge.c +new file mode 100644 +index 0000000..cbae32d +--- /dev/null ++++ b/src/gifsicle_bridge.c +@@ -0,0 +1,241 @@ ++/* gifsicle_bridge.c - Filesystem-free WebAssembly interface. ++ Copyright (C) 2026 Andrey Warkentin ++ SPDX-License-Identifier: MIT */ ++ ++#include ++#include ++#include ++#include ++#include ++#include "gifsicle.h" ++#include "gifsicle_bridge.h" ++ ++#define ERROR_CAPACITY 4096 ++ ++static Gif_Record output_record; ++static char error_message[ERROR_CAPACITY]; ++static size_t error_length; ++static jmp_buf allocation_jump; ++static int allocation_jump_active; ++static int serious_read_error; ++ ++static void ++allocation_failed(void) ++{ ++ if (allocation_jump_active) ++ longjmp(allocation_jump, 1); ++} ++ ++void ++gifsicle_bridge_set_error(const char *message) ++{ ++ error_length = 0; ++ error_message[0] = 0; ++ if (message) ++ gifsicle_bridge_append_error(message, strlen(message)); ++} ++ ++void ++gifsicle_bridge_append_error(const char *message, size_t length) ++{ ++ size_t available = ERROR_CAPACITY - 1 - error_length; ++ if (length > available) ++ length = available; ++ if (length) { ++ memcpy(error_message + error_length, message, length); ++ error_length += length; ++ error_message[error_length] = 0; ++ } ++} ++ ++static void ++bridge_read_error(Gif_Stream *gfs, Gif_Image *gfi, ++ int is_error, const char *message) ++{ ++ (void) gfs, (void) gfi; ++ if (is_error > 0) ++ serious_read_error = 1; ++ if (message) ++ gifsicle_bridge_set_error(message); ++} ++ ++static void ++initialize_frame(Gt_Frame *frame, Gif_Stream *stream, Gif_Image *image) ++{ ++ memset(frame, 0, sizeof(*frame)); ++ frame->stream = stream; ++ frame->image = image; ++ frame->use = 1; ++ frame->interlacing = -1; ++ frame->left = -1; ++ frame->top = -1; ++ frame->delay = -1; ++ frame->disposal = -1; ++} ++ ++static void ++initialize_output(Gt_OutputData *output, const Gifsicle_BridgeOptions *options) ++{ ++ memset(output, 0, sizeof(*output)); ++ output->screen_width = -1; ++ output->screen_height = -1; ++ output->loopcount = -2; ++ output->colormap_size = options->colors; ++ output->colormap_algorithm = COLORMAP_DIVERSITY; ++ output->dither_type = dither_none; ++ output->dither_name = "none"; ++ output->colormap_gamma_type = options->gamma_type; ++ output->colormap_gamma = options->gamma; ++ output->optimizing = options->optimize; ++ output->scaling = GT_SCALING_NONE; ++ output->scale_method = SCALE_METHOD_MIX; ++} ++ ++static int ++process(const uint8_t *data, uint32_t length, ++ const Gifsicle_BridgeOptions *options) ++{ ++ Gif_Record input_record; ++ Gif_ReadLimits limits; ++ Gif_Stream *source; ++ Gif_Stream *result; ++ Gt_Frameset bridge_frames; ++ Gt_OutputData output_data; ++ int huge_stream = 0; ++ int compress_immediately; ++ int i; ++ ++ input_record.data = data; ++ input_record.length = length; ++ limits.max_frames = GIFSICLE_BRIDGE_MAX_FRAMES; ++ limits.max_logical_area = GIFSICLE_BRIDGE_MAX_AREA; ++ limits.max_total_frame_area = GIFSICLE_BRIDGE_MAX_AREA; ++ ++ no_warnings = 1; ++ error_count = 0; ++ serious_read_error = 0; ++ clp = 0; ++ set_diagnostic_handler(gifsicle_bridge_append_error); ++ ++ source = Gif_FullReadRecordLimited( ++ &input_record, ++ GIF_READ_UNCOMPRESSED | GIF_READ_CONST_RECORD, ++ "", ++ &limits, ++ bridge_read_error ++ ); ++ if (!source ++ || source->read_status == GIF_READ_STATUS_LIMIT ++ || serious_read_error ++ || source->nimages == 0) { ++ Gif_DeleteStream(source); ++ set_diagnostic_handler(0); ++ return GIFSICLE_BRIDGE_INVALID_INPUT; ++ } ++ ++ bridge_frames.count = source->nimages; ++ bridge_frames.cap = source->nimages; ++ bridge_frames.f = Gif_NewArray(Gt_Frame, source->nimages); ++ for (i = 0; i < source->nimages; ++i) { ++ initialize_frame(&bridge_frames.f[i], source, source->images[i]); ++ source->refcount++; ++ source->images[i]->refcount++; ++ } ++ source->refcount++; ++ ++ initialize_output(&output_data, options); ++ Gif_InitCompressInfo(&gif_write_info); ++ gif_write_info.loss = options->lossy; ++ gif_write_info.flags = options->careful ++ ? GIF_WRITE_CAREFUL_MIN_CODE_SIZE | GIF_WRITE_EAGER_CLEAR : 0; ++ ++ compress_immediately = options->optimize == 0 ++ && options->colors == 0 ++ && options->lossy == 0 ++ && !options->careful; ++ result = merge_frame_interval( ++ &bridge_frames, 0, -1, &output_data, compress_immediately, &huge_stream ++ ); ++ blank_frameset(&bridge_frames, 0, -1, 0); ++ Gif_DeleteArray(bridge_frames.f); ++ Gif_DeleteStream(source); ++ ++ if (!result) { ++ set_diagnostic_handler(0); ++ return GIFSICLE_BRIDGE_PROCESSING_FAILED; ++ } ++ ++ active_output_data = output_data; ++ kc_set_gamma(output_data.colormap_gamma_type, output_data.colormap_gamma); ++ if (output_data.colormap_size > 0) ++ gifsicle_apply_colormap(result); ++ if (output_data.optimizing & GT_OPT_MASK) ++ optimize_fragments(result, output_data.optimizing, huge_stream); ++ ++ if (!Gif_FullWriteRecord(result, &gif_write_info, &output_record)) { ++ Gif_DeleteStream(result); ++ set_diagnostic_handler(0); ++ return GIFSICLE_BRIDGE_PROCESSING_FAILED; ++ } ++ ++ Gif_DeleteStream(result); ++ set_diagnostic_handler(0); ++ return GIFSICLE_BRIDGE_OK; ++} ++ ++void ++gifsicle_reset(void) ++{ ++ Gif_FreeRecord(&output_record); ++ gifsicle_bridge_set_error(0); ++} ++ ++int ++gifsicle_optimize(const uint8_t *data, uint32_t length, ++ int optimize, int careful, int colors, int lossy, ++ int gamma_type, double gamma) ++{ ++ Gifsicle_BridgeOptions options; ++ int status; ++ ++ gifsicle_reset(); ++ options.optimize = optimize; ++ options.careful = careful; ++ options.colors = colors; ++ options.lossy = lossy; ++ options.gamma_type = gamma_type; ++ options.gamma = gamma; ++ ++ Gif_SetAllocationFailureHandler(allocation_failed); ++ allocation_jump_active = 1; ++ if (setjmp(allocation_jump)) { ++ allocation_jump_active = 0; ++ Gif_SetAllocationFailureHandler(0); ++ gifsicle_bridge_set_error("WebAssembly memory allocation failed"); ++ return GIFSICLE_BRIDGE_OUT_OF_MEMORY; ++ } ++ ++ status = process(data, length, &options); ++ allocation_jump_active = 0; ++ Gif_SetAllocationFailureHandler(0); ++ return status; ++} ++ ++const uint8_t * ++gifsicle_output_data(void) ++{ ++ return output_record.data; ++} ++ ++uint32_t ++gifsicle_output_size(void) ++{ ++ return output_record.length; ++} ++ ++ ++const char * ++gifsicle_error_message(void) ++{ ++ return error_message; ++} +diff --git a/src/gifsicle_bridge.h b/src/gifsicle_bridge.h +new file mode 100644 +index 0000000..5ece550 +--- /dev/null ++++ b/src/gifsicle_bridge.h +@@ -0,0 +1,34 @@ ++/* gifsicle_bridge.h - Filesystem-free WebAssembly interface. ++ Copyright (C) 2026 Andrey Warkentin ++ SPDX-License-Identifier: MIT */ ++ ++#ifndef GIFSICLE_BRIDGE_H ++#define GIFSICLE_BRIDGE_H ++ ++#include ++#include ++#include ++ ++#define GIFSICLE_BRIDGE_MAX_FRAMES 100000U ++#define GIFSICLE_BRIDGE_MAX_AREA UINT64_C(134217728) ++ ++enum { ++ GIFSICLE_BRIDGE_OK = 0, ++ GIFSICLE_BRIDGE_INVALID_INPUT = 1, ++ GIFSICLE_BRIDGE_PROCESSING_FAILED = 2, ++ GIFSICLE_BRIDGE_OUT_OF_MEMORY = 3 ++}; ++ ++typedef struct { ++ int optimize; ++ int careful; ++ int colors; ++ int lossy; ++ int gamma_type; ++ double gamma; ++} Gifsicle_BridgeOptions; ++ ++void gifsicle_bridge_set_error(const char *message); ++void gifsicle_bridge_append_error(const char *message, size_t length); ++ ++#endif +diff --git a/src/gifwrite.c b/src/gifwrite.c +index a087a71..a737070 100644 +--- a/src/gifwrite.c ++++ b/src/gifwrite.c +@@ -129,27 +129,53 @@ file_block_putter(const uint8_t *block, size_t size, Gif_Writer *grr) + } + + ++static int ++memory_reserve(Gif_Writer *grr, size_t len) ++{ ++ uint32_t cap; ++ uint8_t *v; ++ if (grr->errors) ++ return 0; ++ if (len > UINT32_MAX - grr->pos) { ++ grr->errors = 1; ++ return 0; ++ } ++ cap = grr->cap; ++ while (grr->pos + len > cap) { ++ if (cap == 0) ++ cap = 1024; ++ else if (cap > UINT32_MAX / 2) ++ cap = UINT32_MAX; ++ else ++ cap *= 2; ++ if (cap < grr->pos + len) { ++ grr->errors = 1; ++ return 0; ++ } ++ } ++ if (cap != grr->cap) { ++ v = Gif_Realloc(grr->v, sizeof(uint8_t), cap, __FILE__, __LINE__); ++ if (!v) { ++ grr->errors = 1; ++ return 0; ++ } ++ grr->v = v; ++ grr->cap = cap; ++ } ++ return 1; ++} ++ + static void + memory_byte_putter(uint8_t b, Gif_Writer *grr) + { +- if (grr->pos >= grr->cap) { +- grr->cap = (grr->cap ? grr->cap * 2 : 1024); +- Gif_ReArray(grr->v, uint8_t, grr->cap); +- } +- if (grr->v) { +- grr->v[grr->pos] = b; +- grr->pos++; +- } ++ if (memory_reserve(grr, 1)) ++ grr->v[grr->pos++] = b; + } + + static void + memory_block_putter(const uint8_t *data, size_t len, Gif_Writer *grr) + { +- while (grr->pos + len >= grr->cap) { +- grr->cap = (grr->cap ? grr->cap * 2 : 1024); +- Gif_ReArray(grr->v, uint8_t, grr->cap); +- } +- if (grr->v) { ++ if (memory_reserve(grr, len)) { + memcpy(grr->v + grr->pos, data, len); + grr->pos += len; + } +@@ -194,7 +220,7 @@ gif_writer_init(Gif_Writer* grr, FILE* f, const Gif_CompressInfo* gcinfo) + grr->byte_putter = memory_byte_putter; + grr->block_putter = memory_block_putter; + } +- return gfc_init(&grr->code_table, gcinfo); ++ return gfc_init(&grr->code_table, &grr->gcinfo); + } + + static void +@@ -951,14 +977,16 @@ write_image(Gif_Stream* gfs, Gif_Image* gfi, Gif_Writer* grr) + } + + } else if (!gfi->img) { +- Gif_UncompressImage(gfs, gfi); +- write_compressed_data(gfs, gfi, min_code_bits, grr); ++ int ok = Gif_UncompressImage(gfs, gfi) ++ && write_compressed_data(gfs, gfi, min_code_bits, grr); + Gif_ReleaseUncompressedImage(gfi); ++ return ok && !grr->errors; + + } else +- write_compressed_data(gfs, gfi, min_code_bits, grr); ++ return write_compressed_data(gfs, gfi, min_code_bits, grr) ++ && !grr->errors; + +- return 1; ++ return !grr->errors; + } + + +@@ -1141,11 +1169,44 @@ Gif_FullWriteFile(Gif_Stream *gfs, const Gif_CompressInfo *gcinfo, + { + Gif_Writer grr; + int ok = gif_writer_init(&grr, f, gcinfo) +- && write_gif(gfs, &grr); ++ && write_gif(gfs, &grr) ++ && !grr.errors; + gif_writer_cleanup(&grr); + return ok; + } + ++int ++Gif_FullWriteRecord(Gif_Stream *gfs, const Gif_CompressInfo *gcinfo, ++ Gif_Record *record) ++{ ++ Gif_Writer grr; ++ int ok; ++ if (!record) ++ return 0; ++ record->data = NULL; ++ record->length = 0; ++ ok = gif_writer_init(&grr, NULL, gcinfo) ++ && write_gif(gfs, &grr) ++ && !grr.errors; ++ if (ok) { ++ record->data = grr.v; ++ record->length = grr.pos; ++ grr.v = NULL; ++ } ++ gif_writer_cleanup(&grr); ++ return ok; ++} ++ ++void ++Gif_FreeRecord(Gif_Record *record) ++{ ++ if (record) { ++ Gif_DeleteArray(record->data); ++ record->data = NULL; ++ record->length = 0; ++ } ++} ++ + + Gif_Writer* + Gif_IncrementalWriteFileInit(Gif_Stream* gfs, const Gif_CompressInfo* gcinfo, +diff --git a/src/support.c b/src/support.c +index 60e6f86..5d78f26 100644 +--- a/src/support.c ++++ b/src/support.c +@@ -20,6 +20,13 @@ const char *program_name = "gifsicle"; + static int verbose_pos = 0; + int error_count = 0; + int no_warnings = 0; ++static Gt_DiagnosticHandler diagnostic_handler; ++ ++void ++set_diagnostic_handler(Gt_DiagnosticHandler handler) ++{ ++ diagnostic_handler = handler; ++} + + + static void +@@ -49,7 +56,10 @@ verror(const char* landmark, int need_file, + snprintf(pbuf, sizeof(pbuf), xfmt, program_name, landmark, *landmark ? ":" : ""); + p = strlen(pbuf); + +- Clp_vsnprintf(clp, buf, sizeof(buf), fmt, val); ++ if (clp) ++ Clp_vsnprintf(clp, buf, sizeof(buf), fmt, val); ++ else ++ vsnprintf(buf, sizeof(buf), fmt, val); + n = strlen(buf); + if ((size_t) n + 1 < sizeof(buf) && (n == 0 || buf[n - 1] != '\n')) { + buf[n++] = '\n'; +@@ -73,7 +83,10 @@ verror(const char* landmark, int need_file, + error_count++; + + verbose_endline(); +- fwrite(xbuf, 1, xi, stderr); ++ if (diagnostic_handler) ++ diagnostic_handler(xbuf, xi); ++ else ++ fwrite(xbuf, 1, xi, stderr); + } + + void fatal_error(const char* format, ...) { diff --git a/packages/optimizt-gifsicle/upstream/gifsicle.modes b/packages/optimizt-gifsicle/upstream/gifsicle.modes new file mode 100644 index 0000000..d99a049 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle.modes @@ -0,0 +1,59 @@ +644 .appveyor.yml +644 .github/workflows/tests.yml +644 .gitignore +644 .travis.yml +644 COPYING +644 INSTALL +644 Makefile.am +644 NEWS.md +644 README.md +644 configure.ac +644 gifdiff.1 +644 gifsicle.1 +644 gifview.1 +644 include/lcdf/clp.h +644 include/lcdf/inttypes.h +644 include/lcdfgif/gif.h +644 include/lcdfgif/gifx.h +644 logo.gif +644 logo1.gif +644 src/Makefile.am +644 src/Makefile.bcc +644 src/Makefile.mingw +644 src/Makefile.w32 +644 src/clp.c +644 src/fmalloc.c +644 src/gifdiff.c +644 src/giffunc.c +644 src/gifread.c +644 src/gifsicle.c +644 src/gifsicle.h +644 src/giftoc.c +644 src/gifunopt.c +644 src/gifview.c +644 src/gifwrite.c +644 src/gifx.c +644 src/kcolor.c +644 src/kcolor.h +644 src/merge.c +644 src/optimize.c +644 src/opttemplate.c +644 src/quantize.c +644 src/strerror.c +644 src/support.c +644 src/ungifwrt.c +644 src/win32cfg.h +644 src/xform.c +644 test/001-transexpand.testie +644 test/002-cropresize.testie +644 test/003-bgopt.testie +644 test/004-careful.testie +644 test/005-resize.testie +644 test/006-unoptdisposal.testie +644 test/007-alltransp.testie +644 test/008-resizemix.testie +644 test/009-zerowidth.testie +644 test/010-warnings.testie +644 test/011-resizemix.testie +755 bootstrap.sh +755 test/testie diff --git a/packages/optimizt-gifsicle/upstream/gifsicle.sha256 b/packages/optimizt-gifsicle/upstream/gifsicle.sha256 new file mode 100644 index 0000000..1c50508 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle.sha256 @@ -0,0 +1,59 @@ +8ea98473c8d89c825706667cee064c4477157e7651b92d9ad6cddf829d6a85c9 ./.appveyor.yml +55dec323f6021fe89d037d050dbf94792c65a7a5cade355ded9704800a0a75fc ./.github/workflows/tests.yml +7dfc8cd11442a634ec8b6516ab522482319a734a1c6266e21d0ac13ab8f339eb ./.gitignore +31d826aaa194e1489733a721dee723ba720b70667f59b9e3186f603b6a1f1c00 ./.travis.yml +8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643 ./COPYING +12284eedc69bbc36ffe0f58977bc9c04752db04a9dec47085fbeac90014c6382 ./INSTALL +d509fd1acd8377b9e45ac9909a909c65bbbbba234435a001a815ed92ab9e73f5 ./Makefile.am +d59fada434157cd795025c5f7c50d6389d09caf89f154377842d0bc59cc67854 ./NEWS.md +7bcf7dace4e11ddfc0bacf1c711b6ba6120828676d4e787d1667d871c6606597 ./README.md +a9b3926b67d2afb7b331ad73a54c87b5b2bc3e30fb4d89a5be6526ba2f1529a2 ./bootstrap.sh +8f5ebf47935ef2e117ce04fd6387e5514ec1f9ee7e12da6ff2c5be2e3a45f6c1 ./configure.ac +903bde5d8b1cd763ffda3d095428655d51be107d268a7cf064aa0fdc8ff1df1d ./gifdiff.1 +05c1b85815fdef67252cae223eda408e02a8ded437d049cc8ea1f3f23d945c04 ./gifsicle.1 +a0b14d22e816cc2b18c6b301e1b19790086a336d984da7189ede160ccb025ec6 ./gifview.1 +12fac2f5776694b27401635df55e5855dbd4819736841789ace2a0e432a534e7 ./include/lcdf/clp.h +f316d63310a7eb5ec5178ffa6bdcb42a2629d8160f7a63999e648a2184ce313a ./include/lcdf/inttypes.h +ab8f90364d22ae36040127168d75b414cce7108860fd65d3cc593d88a31fd47d ./include/lcdfgif/gif.h +f8512592ea44a7f89ff956f5a370b7faed314f01449952dafcce2098be78af32 ./include/lcdfgif/gifx.h +547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827 ./logo.gif +e1bcfc0f5ea18cc6592ee2dce22b85a26209502182d86db69607ff9279efe559 ./logo1.gif +2031a98b66c218da54e04a56df524d6568d031c9db2c00d476b9e499ab33a172 ./src/Makefile.am +50b5e06b5ace7fcc08bf9b3d8df05bf8f26c01f9998b89acdaf64a34fc06d9b9 ./src/Makefile.bcc +a618933aa9516a7e7611867fc673ed15c7fdd9b86bed8eb74e749f015524ad35 ./src/Makefile.mingw +273730cbfd63c8b5e2658592b06c9d8af6841338fb35133d8991a2ac26c4d803 ./src/Makefile.w32 +26158e44bd770bff71b60eccfbf6f2ed750306e17e4b7d5632ce5a7d30ac4869 ./src/clp.c +1a421510e3d479b30061cfdb8c830c233df5dc46af6c6ae1a156db141e112fd6 ./src/fmalloc.c +8ef75b5ce4d0520889d048b58f7e1b9601784d71068ed55d9bbb7cb50555686f ./src/gifdiff.c +888a54e28c51caf1d97df16cb46e4a074d576f5a120bbb331846f926cc108faf ./src/giffunc.c +50eb58f89a09d1d8d33e671b44a43b67bcd7c1eca8176b00b0ff5a0a93ebb43e ./src/gifread.c +dae8bf8daa793ee8d2cfe2f3da029a2fdfaafb1bae7d2359a831d90a1bc5ceb7 ./src/gifsicle.c +70abe5fe0548b59aeca27e8b62edbe404f74221774b4f9b509f10e54f5a2f05b ./src/gifsicle.h +cc76053dcb34c517bdbe321e63418a01f4a26d201a0a2969919fdafe1b6be06f ./src/giftoc.c +9ac0ac8959e7195699219ef846ca2d6cdc3e8fcb6350dd602bc50d3dddee6a0d ./src/gifunopt.c +3cf73380b968a52466f8f9930d80a214ff089dbf3b852e0d2f2508f9e299a094 ./src/gifview.c +a967019e0f993f1d38ce33835f7a13425a74cb9c8b0ad703d1b25d16f0ecb31b ./src/gifwrite.c +32336f7f218f2c0e3792e964ec622ccea5db3c4f22ccb8c8577cc62b97ff6fd7 ./src/gifx.c +f4468e5315a409ecfe0ff2679c63e61692ed03859cd40a677b358a2704cdfad9 ./src/kcolor.c +f9ca400af20f09c2c4c9c78efb33ac69083a3d288e4edc9d01011101f2519644 ./src/kcolor.h +e08bab97c4847605c0d7027a00e2c07875bf988e3a47b758d0e57a98bdada35d ./src/merge.c +16b01ebb7bb55fd0287be5d09f4942b0dbe1892fea739e6d16939990be81f29c ./src/optimize.c +738965cdec280c0b8bb18a69852e0f77c4cfeca5dcd27181c00bc8d627ae7843 ./src/opttemplate.c +1442862a69927b31fc51ef36f573849e299c85e5eb4c59b361cb47a1a1217733 ./src/quantize.c +02e3b479033cdf89865fca7dcf2bb083df25c3ca6acd357679af5108db6b48d7 ./src/strerror.c +283fdb3a5402d17688472580b228448950bc654a3ac9a45be13d4aec7c4937c2 ./src/support.c +dee10403f60bada9dd18e7a3ebc9af4f0d449ac29c965a185679e8e05faec984 ./src/ungifwrt.c +9a877ad872760fc5790026206767f049ab4d1a9ab809819cf73d23362303b31f ./src/win32cfg.h +7e79407a2cfbfd9326363927efd7d8879edd5b1318950fe91529915966c7eed7 ./src/xform.c +4615b0ce3f0bc9dc510c67df2e85c45134ab9ddaa5e2e75e56ff370bec4e3097 ./test/001-transexpand.testie +bb2347200b386e748f2f01c951bff0e9065ee4fe1f6e38a617cdec0b7df33210 ./test/002-cropresize.testie +ea1543422ab71b342228cc7403ec360bd9baa33b07fbfef25a3ce037939fa718 ./test/003-bgopt.testie +44b344081645cac5f461ed53fa601404cf3ce490a455201b61761895579a3a36 ./test/004-careful.testie +1abf0cf7c5d20e2681d839ab1cf03027db2140d684fc6b18b1f061356d6ca553 ./test/005-resize.testie +4be1c8d5b58c5aa8907ef46473648109dfe3e1e06e28ce4eba42ebeb4ebd3f3b ./test/006-unoptdisposal.testie +63986fd04d5e11bf22e338f0b3b7afc25449cfb1df9f1ae6e38b91b141dbccca ./test/007-alltransp.testie +f91dd769bf0190dec7f76001e0729fc987e67b4a88f99729c2d968c702926db0 ./test/008-resizemix.testie +871cda102681b4ead575d58c8ea18def01a261d1fd2f0dd3f76bd9ab4dc2c803 ./test/009-zerowidth.testie +56064654a761b22a5546daa1a6c81e23f87416c84860664963c77b876addb856 ./test/010-warnings.testie +22870d4e9f5500bdf1ecfbbba71ee66c5b5dddceabde458453d2024023a5ba9e ./test/011-resizemix.testie +0522e1d61b9bc76f19c3288d4d58625a21b7c1ed69fecaf18a642ef87af55473 ./test/testie diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/.appveyor.yml b/packages/optimizt-gifsicle/upstream/gifsicle/.appveyor.yml new file mode 100644 index 0000000..cc59ee7 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/.appveyor.yml @@ -0,0 +1,10 @@ +os: Visual Studio 2015 + +build_script: + - CALL "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\vcvars32.bat" + - cd "%APPVEYOR_BUILD_FOLDER%\src" + - nmake -f Makefile.w32 gifsicle.exe gifdiff.exe + - gifsicle.exe --version + - gifdiff.exe --version + +test: off diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/.github/workflows/tests.yml b/packages/optimizt-gifsicle/upstream/gifsicle/.github/workflows/tests.yml new file mode 100644 index 0000000..66173f9 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Tests + +on: + push: + branches: [ master, github ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure and build + run: | + autoreconf -i + ./configure + + - name: Make + run: make + + - name: Run tests + run: make check diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/.gitignore b/packages/optimizt-gifsicle/upstream/gifsicle/.gitignore new file mode 100644 index 0000000..66e712c --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/.gitignore @@ -0,0 +1,30 @@ +*.d +*.o +.deps +Makefile +Makefile.in +aclocal.m4 +autom4te* +config.cache +config.h +config.h.in +config.h.in~ +config.log +config.status +configure +depcomp +gifdiff +gifsicle +gifsicle-*.rpm +gifsicle-*.tar.gz +gifview +include +install-sh +missing +src/gifdiff +src/gifsicle +src/gifview +stamp-h* +ungifsicle-*.rpm +ungifsicle-*.tar.gz +compile diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/.travis.yml b/packages/optimizt-gifsicle/upstream/gifsicle/.travis.yml new file mode 100644 index 0000000..7c1a97d --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/.travis.yml @@ -0,0 +1,37 @@ +language: c +compiler: + - gcc + - clang +env: + global: + # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created + # via the "travis encrypt" command using the project repo's public key + - secure: "NEQUeZwTTJ+1Pdy2CpNte1nJLYRY2H8+dpvnGS2HP6U/Ok6ZQm4Zxic+ufhabuymbCV7Jvwwy6Gaj8+ObIDahhzAsiMRXSymnvFHELAnq7DiikJ2HqBvFhPzPI2/vDmEP37msGW1dloipLhdKMeuYYP++BpApnC6KVRVJDXg5JI=" +matrix: + include: + - compiler: gcc + - compiler: gcc + env: CCARG=-std=c89 + - compiler: gcc + env: CCARG=-m32 + addons: + apt: + packages: + - gcc-multilib + - libc6:i386 + - compiler: clang +script: ./configure || cat config.log && make check +before_script: + - export CC="$CC $CCARG" + - ./bootstrap.sh +sudo: false + +addons: + coverity_scan: + project: + name: "kohler/gifsicle" + description: "Build submitted via Travis CI" + notification_email: ekohler@gmail.com + build_command_prepend: "./configure; make clean" + build_command: "make -j 4" + branch_pattern: coverity_scan diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/COPYING b/packages/optimizt-gifsicle/upstream/gifsicle/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/INSTALL b/packages/optimizt-gifsicle/upstream/gifsicle/INSTALL new file mode 100644 index 0000000..95d84c8 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/INSTALL @@ -0,0 +1,176 @@ +Basic Installation +================== + + These are generic installation instructions. + + The `configure' shell script attempts to guess correct values for +various system-dependent variables used during compilation. It uses +those values to create a `Makefile' in each directory of the package. +It may also create one or more `.h' files containing system-dependent +definitions. Finally, it creates a shell script `config.status' that +you can run in the future to recreate the current configuration, a file +`config.cache' that saves the results of its tests to speed up +reconfiguring, and a file `config.log' containing compiler output +(useful mainly for debugging `configure'). + + If you need to do unusual things to compile the package, please try +to figure out how `configure' could check whether to do them, and mail +diffs or instructions to the address given in the `README' so they can +be considered for the next release. If at some point `config.cache' +contains results you don't want to keep, you may remove or edit it. + + The file `configure.in' is used to create `configure' by a program +called `autoconf'. You only need `configure.in' if you want to change +it or regenerate `configure' using a newer version of `autoconf'. + +The simplest way to compile this package is: + + 1. `cd' to the directory containing the package's source code and type + `./configure' to configure the package for your system. If you're + using `csh' on an old version of System V, you might need to type + `sh ./configure' instead to prevent `csh' from trying to execute + `configure' itself. + + Running `configure' takes awhile. While running, it prints some + messages telling which features it is checking for. + + 2. Type `make' to compile the package. + + 3. Optionally, type `make check' to run any self-tests that come with + the package. + + 4. Type `make install' to install the programs and any data files and + documentation. + + 5. You can remove the program binaries and object files from the + source code directory by typing `make clean'. To also remove the + files that `configure' created (so you can compile the package for + a different kind of computer), type `make distclean'. There is + also a `make maintainer-clean' target, but that is intended mainly + for the package's developers. If you use it, you may have to get + all sorts of other programs in order to regenerate files that came + with the distribution. + +Compilers and Options +===================== + + Some systems require unusual options for compilation or linking that +the `configure' script does not know about. You can give `configure' +initial values for variables by setting them in the environment. Using +a Bourne-compatible shell, you can do that on the command line like +this: + CC=c89 CFLAGS=-O2 LIBS=-lposix ./configure + +Or on systems that have the `env' program, you can do it like this: + env CPPFLAGS=-I/usr/local/include LDFLAGS=-s ./configure + +Compiling For Multiple Architectures +==================================== + + You can compile the package for more than one kind of computer at the +same time, by placing the object files for each architecture in their +own directory. To do this, you must use a version of `make' that +supports the `VPATH' variable, such as GNU `make'. `cd' to the +directory where you want the object files and executables to go and run +the `configure' script. `configure' automatically checks for the +source code in the directory that `configure' is in and in `..'. + + If you have to use a `make' that does not supports the `VPATH' +variable, you have to compile the package for one architecture at a time +in the source code directory. After you have installed the package for +one architecture, use `make distclean' before reconfiguring for another +architecture. + +Installation Names +================== + + By default, `make install' will install the package's files in +`/usr/local/bin', `/usr/local/man', etc. You can specify an +installation prefix other than `/usr/local' by giving `configure' the +option `--prefix=PATH'. + + You can specify separate installation prefixes for +architecture-specific files and architecture-independent files. If you +give `configure' the option `--exec-prefix=PATH', the package will use +PATH as the prefix for installing programs and libraries. +Documentation and other data files will still use the regular prefix. + + If the package supports it, you can cause programs to be installed +with an extra prefix or suffix on their names by giving `configure' the +option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. + +Optional Features +================= + + Some packages pay attention to `--enable-FEATURE' options to +`configure', where FEATURE indicates an optional part of the package. +They may also pay attention to `--with-PACKAGE' options, where PACKAGE +is something like `gnu-as' or `x' (for the X Window System). The +`README' should mention any `--enable-' and `--with-' options that the +package recognizes. + + For packages that use the X Window System, `configure' can usually +find the X include and library files automatically, but if it doesn't, +you can use the `configure' options `--x-includes=DIR' and +`--x-libraries=DIR' to specify their locations. + +Specifying the System Type +========================== + + There may be some features `configure' can not figure out +automatically, but needs to determine by the type of host the package +will run on. Usually `configure' can figure that out, but if it prints +a message saying it can not guess the host type, give it the +`--host=TYPE' option. TYPE can either be a short name for the system +type, such as `sun4', or a canonical name with three fields: + CPU-COMPANY-SYSTEM + +See the file `config.sub' for the possible values of each field. If +`config.sub' isn't included in this package, then this package doesn't +need to know the host type. + + If you are building compiler tools for cross-compiling, you can also +use the `--target=TYPE' option to select the type of system they will +produce code for and the `--build=TYPE' option to select the type of +system on which you are compiling the package. + +Sharing Defaults +================ + + If you want to set default values for `configure' scripts to share, +you can create a site shell script called `config.site' that gives +default values for variables like `CC', `cache_file', and `prefix'. +`configure' looks for `PREFIX/share/config.site' if it exists, then +`PREFIX/etc/config.site' if it exists. Or, you can set the +`CONFIG_SITE' environment variable to the location of the site script. +A warning: not all `configure' scripts look for a site script. + +Operation Controls +================== + + `configure' recognizes the following options to control how it +operates. + +`--cache-file=FILE' + Use and save the results of the tests in FILE instead of + `./config.cache'. Set FILE to `/dev/null' to disable caching, for + debugging `configure'. + +`--help' + Print a summary of the options to `configure', and exit. + +`--quiet' +`--silent' +`-q' + Do not print messages saying which checks are being made. + +`--srcdir=DIR' + Look for the package's source code in directory DIR. Usually + `configure' can determine that directory automatically. + +`--version' + Print the version of Autoconf used to generate the `configure' + script, and exit. + +`configure' also accepts some other, not widely useful, options. + diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/Makefile.am b/packages/optimizt-gifsicle/upstream/gifsicle/Makefile.am new file mode 100644 index 0000000..3ac5a60 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/Makefile.am @@ -0,0 +1,50 @@ +## Process this file with automake to produce Makefile.in +AUTOMAKE_OPTIONS = foreign + +SUBDIRS = src + +man_MANS = gifsicle.1 @OTHERMANS@ + +EXTRA_DIST = COPYING README.md NEWS.md \ + include/lcdf/clp.h include/lcdf/inttypes.h \ + include/lcdfgif/gif.h include/lcdfgif/gifx.h \ + gifsicle.1 gifview.1 gifdiff.1 logo.gif logo1.gif \ + test/testie \ + test/001-transexpand.testie \ + test/002-cropresize.testie \ + test/003-bgopt.testie \ + test/004-careful.testie \ + test/005-resize.testie \ + test/006-unoptdisposal.testie \ + test/007-alltransp.testie \ + test/008-resizemix.testie \ + test/009-zerowidth.testie \ + test/010-warnings.testie \ + test/011-resizemix.testie + +gifsicle: config.h + @cd src && $(MAKE) gifsicle +gifdiff: config.h + @cd src && $(MAKE) gifdiff +gifview: config.h + @cd src && $(MAKE) gifview + +srclinks: + cd $(top_srcdir); sh ./sourcecheckout.sh + +versionize: + perl -pi -e "s/^\\.ds V.*/.ds V $(VERSION)/;" $(top_srcdir)/gifsicle.1 $(top_srcdir)/gifview.1 $(top_srcdir)/gifdiff.1 + perl -pi -e "s/gifsicle [\d.]+/gifsicle $(VERSION)/; s/VERSION \"[\w.]+/VERSION \"$(VERSION)/;" $(top_srcdir)/src/win32cfg.h + +dist-ungif: dist + $(AMTAR) xzf gifsicle-$(VERSION).tar.gz + @rm gifsicle-$(VERSION)/src/gifwrite.c + rm -rf ungifsicle-$(VERSION) + mv gifsicle-$(VERSION) ungifsicle-$(VERSION) + GZIP=$(GZIP_ENV) $(AMTAR) chozf ungifsicle-$(VERSION).tar.gz ungifsicle-$(VERSION) + rm -rf ungifsicle-$(VERSION) + +check: all + $(top_srcdir)/test/testie -p $(top_builddir)/src $(top_srcdir)/test + +.PHONY: srclinks versionize rpm dist-ungif rpm-ungif check diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/NEWS.md b/packages/optimizt-gifsicle/upstream/gifsicle/NEWS.md new file mode 100644 index 0000000..324164f --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/NEWS.md @@ -0,0 +1,1207 @@ +Gifsicle NEWS +============= + +## Version 1.96 – 26.Feb.2025 + +* `--lossy` measures color errors uses the currently selected color space + (specified by `--gamma`, which defaults to sRGB). It also uses a different + algorithm for computing color differences. + + As a result, the `--lossy=N` option behaves differently in version 1.96 than + in previous versions. A given value of `N` may compress less than you + expect. The closest analogue to previous versions will be obtained with + `--lossy=N --gamma=1`. + +* Improve handling of GIFs with images positioned far outside the logical + screen. This can greatly reduce memory usage. + +* Add preliminary support for the Oklab color space (`--gamma=oklab`). + +* Add support for `--dither=atkinson` (daria@darkspot.net). + +* Add support for `--use-exact-colormap`. + +* Fix some bugs. + + +## Version 1.95 – 19.Feb.2024 + +* Fix some bugs. + + +## Version 1.94 – 23.Jun.2023 + +* Fix some bugs. + + +## Version 1.93 – 30.Jun.2021 + +* Fix security bug on certain resize operations with `--resize-method=box`. + +* Fix problems with colormapless GIFs. + + +## Version 1.92 – 18.Apr.2019 + +* Add `--lossy` option from Kornel Lipiński. + +* Remove an assertion failure possible with `--conserve-memory` + `--colors` + + `--careful`. + + +## Version 1.91 – 5.Jan.2018 + +* Several security bug fixes with malicious GIFs. + + +## Version 1.90 – 14.Aug.2017 + +* Kill a use-after-free error reported by @junxzm1990. + + +## Version 1.89 - 11.Jul.2017 + +* Add SIMD support for resizing. When enabled this improves resize + performance enormously for complex resize methods. + +* Add thread support for resizing. `-j[NTHREADS]` tells gifsicle to use + up to NTHREADS threads to resize an input image. There are several + caveats---multiple threads can be only used on unoptimized images. + Thanks to Github user @wilkesybear. + +* Quashed several crashes and undefined behaviors. Thanks to Github users + including @pornel, @strazzere, and @b0b0505. + +* Minor bug fixes. + + +## Version 1.88 - 1.Jul.2015 + +* Fix bug where long comments were read incorrectly. Reported by + kazarny. + +* Add --no-ignore-errors option. + + +## Version 1.87 - 9.Dec.2014 + +* Always optimize as if the background is transparent. This fixes some + rare bugs reported by Lars Dieckow. + +* Fix --crop issue with must-be-preserved frames that are out of the + crop window. + + +## Version 1.86 - 14.Oct.2014 + +* Further fix --rotate + --crop. + + +## Version 1.85 - 14.Oct.2014 + +* Greatly improve optimization time for images with many colors. + +* Add --no-extensions (with the s) and document those options more. + +* Fix bug in interaction of --resize and --rotate reported by Michał + Ziemba. + + +## Version 1.84 - 29.Jun.2014 + +* Correct optimizer bug that affected GIFs with 65535 or more total + colors. Reported by Jernej Simončič. + + +## Version 1.83 - 21.Apr.2014 + +* Correct bug in custom gamma values reported by Kornel Lesiński. + +* Update Windows build. + + +## Version 1.82 - 27.Mar.2014 + +* Correct bug in `mix` sampling method reported by Bryan Stillwell. + + +## Version 1.81 - 24.Mar.2014 + +* Correct bug in `mix` sampling method reported by Bryan Stillwell. + + +## Version 1.80 - 18.Mar.2014 + +* Bug fixes and improved error messages. + + +## Version 1.79 - 17.Mar.2014 + +* Major improvements in image scaling. Work sponsored by Tumblr via + Mike Hurwitz. + + * Add new resize sampling methods `mix`, `box`, `catrom`, `mitchell`, + `lanczos2`, and `lanczos3`, selectable by `--resize-method`. The + `catrom` filter often gives good results; the slightly faster `mix` + method (a bilinear interpolator) is now the default. These new + sampling methods consider all of the image's input colors when + shrinking the image, producing better, less noisy output for most + images. + + * Add `--resize-colors`, which allows Gifsicle to enlarge the palette + when resizing images. This is particularly important when shrinking + images with small colormaps---e.g., shrinking a black-and-white + image should probably introduce shades of gray. + +* Support extensions such as XMP4 in which extension packet boundaries + matter. Reported by `ata4`. + +* Many bug fixes, especially to cropping. Thanks to Tumblr and to + Bryan Stillwell, Tal Lev-Ami, "Marco," and others. + + +## Version 1.78 - 9.Dec.2013 + +* Correct an optimization bug introduced in 1.76. Reported by Tom + Roostan. + + +## Version 1.77 - 26.Nov.2013 + +* Major improvements to color selection (important when reducing + colormap size). Use gamma-corrected colors in selection and + dithering; this makes image quality much better. Also, when reducing + colors with dithering, prefer to select colors that dithering can't + approximate. + +* Add ordered dithering modes, which avoid animation artifacts. The + default ordered dithering mode (`--dither=ordered`) is a novel mode + that combines some of the visual advantages of error diffusion with + the artifact avoidance of ordered dithering. + +* Add halftone dithering (`--dither=halftone`). + +* gifview: Improved cache memory management for better animations. + Collect memory for old frames based on an explicit --memory-limit + (default 40MB). + +* gifview: Add `--fallback-delay` option, to specify a fallback delay + for frames with delay 0. Thanks to Sung Pae. + + +## Version 1.76 - 20.Nov.2013 + +* Fix `-O2` crashes introduced with `--resize` improvements. Reported + by Bryan Stillwell. + + +## Version 1.75 - 18.Nov.2013 + +* Improve `--careful` (fewer crashes). Reported by Bryan Stillwell. + +* Improve `-O2`: again, don't refuse to optimize images with local + color tables. Reported by Bryan Stillwell. + +* Greatly improve `--dither` speed. + + +## Version 1.74 - 16.Nov.2013 + +* Improve `--resize` behavior: avoid animation artifacts due to + different rounding decisions. Also speed it up. Reported by Bryan + Stillwell. + + +## Version 1.73 - 15.Nov.2013 + +* Fix bug where `-O2` would refuse to optimize some images with local + color tables, claiming that "more than 256 colors were required". + What was really required is previous disposal. Reported by Bryan + Stillwell. + + +## Version 1.72 - 9.Nov.2013 + +* Fix crash bugs on some combinations of `--crop` and `--resize` + (prevalent on images whose first frame didn't cover the whole + logical screen). Reported by Bryan Stillwell. + + +## Version 1.71 - 15.Jun.2013 + +* Avoid rounding errors in `--resize`. Reported by Paul Kane. + +* Report error when `-I` is combined with `-b`. Reported by Frank + Dana. + +* Frame selections also apply in batch mode. Reported by LOU Yu Hong + Leo. + + +## Version 1.70 - 31.Jan.2013 + +* Fix `--crop` bug introduced in version 1.68, visible in images + containing local color tables. Bug reported by Alberto Nannini. + + +## Version 1.69 - 31.Jan.2013 + +* Minor bug fix release. + + +## Version 1.68 - 24.Nov.2012 + +* gifsicle: Alberto Nannini reported some images that are optimized + beyond what Gifsicle can do. In fact, Gifsicle's GIF writer was + limited enough that even when running without optimization, it + seriously expanded the input images. Some improvements to Gifsicle's + writing procedure avoid this problem; now `gifsicle IMAGE` will + produce large results less often, and when it does generate larger + results, they're larger by hundreds of bytes, not hundreds of + thousands. However, due to restrictions in the current optimizer (and + on my time), I was unable to improve `gifsicle -O3`'s handling of + these images. The optimizer that produced the images must be doing + something pretty clever. (Is it SuperGIF?) + + +## Version 1.67 - 5.May.2012 + +* gifsicle: Frame specifications like "#2-0" are allowed; they insert + frames in reverse order. Feature request from Leon Arnott. + +* gifview: Add --min-delay option. + + +## Version 1.66 - 2.Apr.2012 + +* gifsicle: Add -Okeep-empty for Gerald Johanson. + + +## Version 1.65 - 2.Apr.2012 + +* gifsicle: Several users (Kornel Lesiński and others) reported "bugs" with + Gifsicle-optimized images and Mac programs like Safari. The bug is in + Safari, but add --careful to work around it. + +* Improve -O3 a bit (although for some images, the new -O3 is bigger than + the old). + + +## Version 1.64 - 22.Nov.2011 + +* gifsicle: Add --resize-fit options. Tom Glenne request. + + +## Version 1.63 - 17.Jul.2011 + +* gifsicle: Avoid crash on frame selections where frames are repeated. + Werner Lemberg report. + + +## Version 1.62 - 4.Apr.2011 + +* gifsicle: -O3 optimization level tries even harder, so that now gifsicle + -O3 should never produce larger results than gifsicle-1.60 -O2. Jernej + Simončič report. + + +## Version 1.61 - 25.Feb.2011 + +* gifsicle: Add new -O3 optimization level, which applies several + transparency heuristics and picks the one with the best result. David + Jamroga pointed out a file that gifsicle made larger; -O3 shrinks it. + +* gifsicle: Correct some optimizer bugs introduced in 1.59 that could lead + to visually different output. + +* gifdiff: Add -w/--ignore-redundancy option. + +* gifview: Correct crash bug. + +* gifsicle/gifdiff: Correct occasionally odd error messages. + + +## Version 1.60 - 12.Apr.2010 + +* GIF reading library: Correct error that could corrupt the reading of + certain large images. Reported by Jernej Simončič. + + +## Version 1.59 - 11.Mar.2010 + +* gifsicle -O2: Optimize away entirely-transparent frames when possible. + Requested by Gerald Johanson. + + +## Version 1.58 - 14.Jan.2010 + +* gifsicle: Fix optimizer bug reported by Dion Mendel. + + +## Version 1.57 - 11.Nov.2009 + +* gifsicle: Don't throw away totally-cropped frames with 0 delay, since + most browsers treat 0-delay frames as 100ms-delay frames. Reported by + Calle Kabo. + + +## Version 1.56 - 18.Oct.2009 + +* gifsicle: Fix --crop-transparency for animated images; previous versions + could lose frames. Problem reported by Daniel v. Dombrowski. + +* gifview: Make --disposal=background behavior look like Firefox. + + +## Version 1.55 - 3.Apr.2009 + +* gifsicle: Another optimize fix for --disposal previous. Reported by + Gerald Johanson. + + +## Version 1.54 - 3.Apr.2009 + +* gifsicle, gifview: Fix several serious bugs with --disposal previous that + could affect optimized, unoptimized, and displayed images. Reported by + Gerald Johanson. + + +## Version 1.53 - 19.Mar.2009 + +* gifsicle: Frames in an unoptimized image will use disposal "none" when + possible. This leads to fewer surprises later. + + +## Version 1.52 - 18.May.2008 + +* gifsicle: Fix bug introduced in 1.51: --crop-transparent works. Reported by + dgdd wanadoo fr. + + +## Version 1.51 - 12.May.2008 + +* gifsicle: `--crop` preserves the logical screen when it can. Reported by + Petio Tonev. + + +## Version 1.50 - 7.May.2008 + +* gifsicle, gifview: Refuse to read GIFs from the terminal. Requested by + Robert Riebisch. + + +## Version 1.49 - 2.May.2008 + +* Just some Makefile updates requested by Robert Riebisch. + + +## Version 1.48 - 16.Mar.2007 + +* gifsicle: Avoid crash in `--crop-transparency` when an image's first + frame is totally transparent. Reported by Gerald Johanson. + + +## Version 1.47 - 10.Mar.2007 + +* gifsicle: Improve `--nextfile` behavior. + + +## Version 1.46 - 9.Jan.2007 + +* gifsicle: Add `--nextfile` option, useful for scripts. Problem reported + by Jason Young. + + +## Version 1.45 - 30.Dec.2006 + +* Do not compile gifview by default when X is not available. + +* gifview: Add `--title` option, based on patch supplied by Andres Tello + Abrego. + + +## Version 1.44 - 3.Oct.2005 + +* gifview: Enforce a minimum delay of 0.002 sec. Requested by Dale Wiles. + + +## Version 1.43 - 8.Sep.2005 + +* '-I -b', and other similar combinations, causes a fatal error rather than + putting information in an unexpected file. Reported by Michiel de Bondt. + + +## Version 1.42 - 8.Jan.2005 + +* Improve manual page. + + +## Version 1.41 - 19.Aug.2004 + +* Fix problems with 64-bit machines. Thanks to Dan Stahlke for pointing + out the problem and providing a patch. + + +## Version 1.40 - 28.Aug.2003 + +* Fix longstanding bug where `--disposal=previous` was mistakenly + equivalent to `--disposal=asis`. + +* Include Makefile.bcc, from Stephen Schnipsel. + + +## Version 1.39 - 11.Jul.2003 + +* Fix Makefile.w32 (oops). + + +## Version 1.38 - 26.Jun.2003 + +* Include Makefile.w32 (oops). + + +## Version 1.37 - 11.Feb.2003 + +* Fix bug where combining `--rotate-X` and `-O` options would cause a + segmentation fault. Reported by Dan Lasley . + +* Rearrange source tree. + + +## Version 1.36 - 17.Nov.2002 + +* Fix subscript-out-of-range error in main.c reported by Andrea Suatoni. + + +## Version 1.35 - 14.Aug.2002 + +* Fixed bug where `--crop` could cause a segmentation fault, present since + 1.32 or 1.33. Reported by Tom Schumm . + + +## Version 1.34 - 13.Aug.2002 + +* Fixed bug where combining `--crop` and `-O` options could corrupt output. + Reported by Tom Schumm . + + +## Version 1.33 - 1.Aug.2002 + +* Be more careful about time while animating. In particular, prepare frames + before they are needed, so that they can be displayed exactly when + required. Problem reported by Walter Harms . + +* More warning fixes. + + +## Version 1.32 - 5.Jul.2002 + +* Add `--multifile` option handling concatenated GIF files. This is useful + for scripts. For example, `gifsicle --multifile -` will merge all GIF + files written to its standard input into a single animation. + +* More fixes for spurious background warnings. + + +## Version 1.31 - 17.Jun.2002 + +* Changed behavior of `--crop X,Y+WIDTHxHEIGHT` option when WIDTH or HEIGHT + is negative. Previously, zero or negative WIDTH and HEIGHT referred to + the image's entire width or height. Thus, the option `--crop 10,0+0x0` + would always lead to an error, because the crop left position (10) plus + the crop width (the image width) was 10 pixels beyond the image edge. The + new behavior measures zero or negative WIDTH and HEIGHT relative to the + image's bottom-right corner. + +* Changed background behavior. Hopefully the only user-visible effect will + be fewer spurious warnings. + +* Fixed a bug that could corrupt output when optimizing images with `-O2` + that had more than 256 colors. + + +## Version 1.30 - 27.Jul.2001 + +* Fixed bug in ungif code: Writing a large ungif could corrupt memory, + leading eventually to bad output. This bug has been present since + Gifsicle could write ungifs! Bad files and assistance provided by Jeff + Brown . + + +## Version 1.29 - 22.May.2001 + +* Fixed an optimization bug that could produce bad images. Again, reported + by Dale Wiles . It turns out that 1.26 was a + bad release! I've reconstructed and run some regression tests; hopefully + Dale and I have exhausted the bug stream. + + +## Version 1.28 - 13.May.2001 + +* `--colors` option still didn't work; it produced bad images. Again, + thanks to Dale Wiles for reporting the bug. + +* Added explicit `--conserve-memory` option. + + +## Version 1.27 - 10.May.2001 + +* Fixed `--colors` option, which caused segmentation faults. Thanks to Dale + Wiles for reporting the bug. + + +## Version 1.26 - 22.Apr.2001 + +* Added `--crop-transparency` option, which crops any transparent edges off + the image. Requested by Gre7g Luterman . + +* Try to conserve memory in gifsicle when working with huge images (> 20 + megabytes of uncompressed data). May make gifsicle slower if you actually + had enough memory to deal with the uncompressed data. + + +## Version 1.25 - 13.Feb.2001 + +* Fixed gifview bug: If every frame in an animation had a small or + zero delay, then gifview would previously enter a infinite loop and + become noninteractive. Reported by Franc,ois Petitjean + . + + +## Version 1.24 - 11.Feb.2001 + +* Delete `unoptimized_pixmaps` array when deleting a viewer in gifview. + Reported by Franc,ois Petitjean . + +* Added `--resize-width` and `--resize-height` options. + +* Why is it that Frenchmen are always telling me to delete the Clp_Parser + immediately before calling exit()? Don't they realize that exit() frees + application memory just as well as free()? Are things different in + France? More French, perhaps? + + +## Version 1.23 - 12.Dec.2000 + +* In Houston on a layover because of stupid Continental Airlines: allow + GIFs without terminators. Problem reported by Matt Olech + . + + +## Version 1.22 - 23.Nov.2000 + +* Handle time more carefully when displaying animations. Should result in + more precise timings. Problem reported by Iris Baye . + + +## Version 1.21 - 9.Sep.2000 + +* Fixed `--careful -O2`, which could create bad GIFs when there were bad + interactions with transparency. Bug reported by Manfred Schwarb + . + + +## Version 1.20 - 23.Jun.2000 + +* Added `--careful` option, because some bad GIF implementations (Java, + Internet Explorer) don't support Gifsicle's super-optimized, but legal, + GIFs. Problem reported by Andrea Beiser . + +* Gifdiff will work on very large images (better memory usage). + + +## Version 1.19 - 25.Apr.2000 + +* Fixed memory corruption bug: `gifsicle --use-colormap=FILE.gif` would + formerly cause a segmentation fault. Bug reported by Alan Watts + . + + +## Version 1.18 - 3.Apr.2000 + +* Gifview now uses X server memory more parsimoniously. This should make it + more robust, and nicer to the X server, on large animations. Requested by + Vladimir Eltsov . Appears to solve bug reported by + Vince McIntyre . + +* Gifview treats frame selection more sensibly. A frame selection just + tells it which frame to start on. + +* Added a message to the manual page warning people to quote the `#` + character in frame selections. The interactive bash shell, for example, + interprets it as a comment. + + +## Version 1.17 - 15.Mar.2000 + +* Added `--resize _xH` and `--resize Wx_`. Requested by Edwin Piekart + . + +* Changed behavior of `--no-logical-screen`. Now gifsicle will include all + visible GIFs when calculating the size of the logical screen. + +* Got rid of spurious redundant-option warnings. + + +## Version 1.16.1 - 10.Sep.1999 + +* Gifview can put a GIF (or an animated GIF) on the root window; use + `gifview -w root`. Requested by Roland Blais . + + +## Version 1.16 - 31.Aug.1999 + +* Optimization improvement: More colormap manipulation. Version 1.16 + produces smaller GIFs than 1.15 in the large majority of cases (but not + all). + +* Fixed memory corruption bug in quantization. Found and fixed by Steven + Marthouse . + + +## Version 1.15 - 20.Aug.1999 + +* Bug fix: no more assertion failures when combining images that require + local colormaps. + +* Fixed serious quantization bug introduced in 1.14: when reducing the + number of colors, gifsicle would ignore a random portion of the colors in + the old colormap. + +* Optimization bug fix: two adjacent identical frames will no longer crash + the optimizer. Bug introduced in 1.14. That's two for two: two "speed + improvements" in 1.14, two bugs introduced! + +* Bug fix: Exploding the standard input results in files named + `#stdin#.NNN`, as the documentation claims, not `.NNN`. + +* Optimization improvement: the optimizer reorders colormaps in a slightly + smarter way, which can result in smaller compression. + +* If a position option like `-p 0,5` is followed by a multi-frame GIF + (without frame specifications), then the position option places the + animation as a whole, not each of its individual frames. + +* An unrecognized nonnumeric frame specification is now treated as a file + name, so you can say `gifsicle #stdin#.000`, for example. + +* Gifview improvements: Now even images with local color tables can be + "unoptimized" or animated. + +* `--replace` now preserves the replaced frame's disposal as well as its + delay. + +* `gifsicle -I -` reports "" as the input file name. + + +## Version 1.14.2 - 16.Aug.1999 + +* Fixed memory bug in Gif_DeleteImage (I freed a block of memory, then + accessed it). I am a moron!! This bug affected Version 1.14.1 only. + + +## Version 1.14.1 - 9.Aug.1999 + +* Fixed configuration bug: you couldn't compile gifsicle from a different + directory (it told you to try --enable-ungif). + +* Steven Marthouse helped fix `Makefile.w32` to + enable wildcards in filenames under Windows. Requested by Mark Olesen + . + + +## Version 1.14 - 2.Aug.1999 + +* Switched to an adaptive tree strategy for LZW GIF compression, which is + faster than the old hashing strategy. This method is the brainchild of + Hans Dinsen-Hansen . + +* Bug fix/optimization improvement: Some images would include a transparent + index that was not a valid color (it was larger than the colormap). + Gifsicle formerly ignored these transparent indices. It doesn't any + longer; furthermore, it will generate those kinds of transparent index, + which can mean smaller colormaps and smaller GIFs. + +* Speed improvements in optimization and colormap quantization. + +* Added `--no-warnings` (`-w`) option to inhibit warning messages. + Suggested by Andrea Beiser . + +* `--info` output can now be sent to a file with `-o`, just as with any + other kind of output. + +* More specific error messages on invalid GIFs. + + +## Version 1.13 - 16.Jun.1999 + +* Added optional support for run-length-encoded GIFs to avoid patent + problems. Run-length encoding idea based on code by Hutchinson Avenue + Software Corporation found in Thomas Boutell's gd + library . + +* `gifsicle --explode` now generates filenames of the form `file.000 .. + file.100` rather than `file.0 .. file.100`, so you can use shell globbing + like `file.*` and automatically get the right order. + +* Added `--background` option to gifview, so you can set the color used for + transparent pixels. Suggested by Art Blair . + +* Gifdiff is built by default on Unix. + + +## Version 1.12 - 25.Mar.1999 + +* Added `--window` option to gifview, which lets it display a GIF in an + arbitrary X window. Thanks to Larry Smith for patches. + +* Added `--install-colormap` to gifview. Suggested by Yair Lenga + . + +* Gifsicle now exits with status 1 if there were any errors, status 0 + otherwise. Before, it exited with status 1 only if there was a fatal error, + or nothing was successfully output. The new behavior seems more useful. + Problem reported by David Kelly . + + +## Version 1.11.2 - 10.Mar.1999 + +* `gifsicle -U` will now unoptimize 1-image GIFs as well as animations. + + +## Version 1.11.1 - 24.Jan.1999 + +* Fixed core dump on corrupted GIF files (in particular, bad extensions). + Problem reported by tenthumbs . + + +## Version 1.11 - 22.Jan.1999 + +* Added `--scale XFACTORxYFACTOR` to complement `--resize`. + +* Made `--resize` work better on animations (no more off-by-one errors). + +* Fixed bug in `--use-colormap`: If transparency was added to the new + colormap, the transparent color index could later be used (incorrectly) + as a real image color. This was particularly common if the image was + dithered. + +* `--dither` now tries to mitigate "jumping dither" animation artifacts + (where adjacent frames in an animation have different dithering patterns, + so the animation shifts). It does an OK job. + +* Improvements to `--color-method=blend-diversity`. + + +## Version 1.10 - 6.Jan.1999 + +* Gifview is built by default. + +* No other changes from 1.10b1: Emil said it worked on Windows. + + +## Version 1.10b1 - 31.Dec.1998 + +* The two malloc packages that come with Gifsicle don't #define `malloc` or + `realloc`; they #define `xmalloc` and `xrealloc`. + +* Changes to Windows port from Emil Mikulic . + + +## Version 1.9.2 - 28.Dec.1998 + +* Gifsicle compiles out of the box on Windows! Port maintained by Emil + Mikulic . + +* Moved to config.h-based configuration to simplify GIF library and Windows + port. + +* Some CLP improvements. + + +## Version 1.9.1 - 16.Dec.1998 + +* If a reduced-colormap image needs a special transparent color, gifsicle + will now try to keep the same transparent color value as the input GIF. + +* Cropping an image now automatically recalculates the output logical + screen, rather than retaining the logical screen of the uncropped image. + +* Improvement to colormap modification behavior: If it looks like a color + reserved for transparency will be necessary, gifsicle will reserve a slot + for it. This will reduce the likelihood that you ask for a colormap of + size 64, say, and get one of size 128 (because gifsicle added a slot for + transparency). + + +## Version 1.9 - 14.Dec.1998 + +* `--no-background` is now the same as `--background=0` (set the background + to pixel 0) instead of `--same-background`. + + +## Version 1.9b3 - 10.Dec.1998 + +* Fixed a serious bug in merge.c introduced in 1.8: Merging several images + with global colormaps could corrupt the output colors. + +* Fixed a serious bug in optimize.c introduced in 1.9b1: Optimizing GIFs + which required local color tables could crash the program due to a buffer + overrun. (I was adding the background to the output colormap without + making sure there was room for it.) + +* Fixed an optimizer bug which could result in incorrect animations: + background disposal was not handled correctly. + +* The optimizer now generates `--disposal=none` instead of + `--disposal=asis` in certain situations, which can result in smaller + animations. Suggestion by Markus F.X.J. Oberhumer + . + +* GIF library changes: cleaner, somewhat faster GIF reading code inspired + by Patrick J. Naughton , whose code is distributed + with XV. + + +## Version 1.9b2 - 9.Dec.1998 + +* Fixed `--logical-screen` and `--no-logical-screen`, which had no effect + in 1.9b1. In a stunning turn of events, this problem was reported by + Markus F.X.J. Oberhumer . + + +## Version 1.9b1 - 5.Dec.1998 + +* Added `--resize WxH` option, prompted by code from Christian Kumpf + . + +* Made `--change-color PIXELINDEX color` work, and `--change-color color + PIXELINDEX' illegal. + +* Many behavior fixes relating to background and transparency. Gifsicle + would tend to create GIFs which looked as expected, but lost information + internally; for example, a color value which was only used for + transparency would be changed to black, no matter what the input color + was. In some cases, gifsicle simply reports a warning now where it + ignored information before. Problems reported by Markus F.X.J. Oberhumer + . + +* Better error messages are now given on redundant, ambiguous, or useless + command-line options. + +* Options that affect GIFs on output (such as `--output`, `--optimize`) now + behave more like other options: you should put them before the files they + affect. (You may still put them at the end of the argument list if they + should affect the last output file.) This should only affect people who + used batch or explode mode in complex ways. + +* Gifsicle will not write a GIF to stdout if it's connected to a terminal. + + +## Version 1.8 - 3.Dec.1998 + +* Fixed strange behavior when changing transparency: a new black entry was + added to the colormap in almost all situations. Problem reported by + Markus F.X.J. Oberhumer . + +* Fixed `--transform-colormap` bug: commands like "thing 2" (ending in an + integer) will work now. + +* Small fixes (in a gifsicle pipeline, error messages won't be interleaved). + + +## Version 1.7.1 - 2.Dec.1998 + +* Added configuration check for `strerror`. Problem reported by Mario + Gallotta . + +* Added colormap canonicalization: If you pipe a GIF through `gifsicle -O` + or `-O2`, the output colormaps will be arranged in a predictable order. + Feature suggested by Markus F.X.J. Oberhumer + . + + +## Version 1.7 - 28.Nov.1998 + +* Added `--use-colormap=gray` and `--use-colormap=bw`. Idea and some code + thanks to Christian Kumpf . + +* Added `--transform-colormap`, which allows you to plug programs into + gifsicle that arbitrarily change GIF files' colormaps. + +* All `--change-color` options now happen simultaneously, so you can safely + swap two colors with `--change-color C1 C2 --change-color C2 C1`. + +* Colormap modifications are slightly more powerful (they don't reserve a + color for transparency if they don't need one, and if a subset of the + modified colormap is used, only that subset is output). + +* Fixed `--use-colormap` bug: the last color in a used colormap would be + ignored (it was reserved for transparency). Symptom: if you put a + partially transparent image into the Web-safe palette, the result would + never contain white. + +* When changing to a grayscale colormap, gifsicle now uses luminance + difference to find the closest color. This gives better results for + `--use-colormap=gray` and the like. + +* Added Introduction and Concept Index sections to the gifsicle manpage. + +* The gifsicle package now uses automake. + + +## Version 1.6 - 23.Nov.1998 + +* A frame extracted from the middle of an animation will now have the + animation's screen size rather than 640x480. Problem reported by Mr. + Moose . + + +## Version 1.5 - 27.Sep.1998 + +* `--help` now prints on stdout, as according to the GNU standards. + +* Changes to support Win32 port thanks to Emil Mikulic + . + +* Makefiles: Added `make uninstall` target, enabled `./configure`'s + program name transformations, made VPATH builds possible. + + +## Version 1.4.1 - 16.Sep.1998 + +* Fixed `--unoptimize` bug: a frame's transparency could disappear under rare + circumstances. Bug reported by Rodney Brown + + +## Version 1.4 - 12.Sep.1998 + +* Added `--extension` and `--app-extension`. + + +## Version 1.3.4 - 7.Sep.1998 + +* More configuration changes. + +* Fixed bug in gifview `--geometry` option processing: `--geometry -0-0` + wasn't recognized. + + +## Version 1.3.2 - 5.Sep.1998 + +* Fixed configuration bugs reported by Alexander Mai + (OS/2 build didn't work). + + +## Version 1.3.1 - 4.Sep.1998 + +* Fixed configuration bug: int32_t could be improperly redefined. Reported by + Anne "Idiot" Dudfield and Dax Kelson + . + + +## Version 1.3 - 3.Sep.1998 + +* Added `--flip-*` and `--rotate-*` options. + +* Fixed rare bug in GIF writing code: the last pixel in a frame could + previously become corrupted. + + +## Version 1.3b1 - 2.Aug.1998 + +* Optimization has been completely overhauled. All optimization functions + are in a separate file, optimize.c. Some bugs have been caught. + Optimization is now quite powerful, and rarely expands any input file. + (An expansion of 1-30 bytes usually means that the input file cheated by + leaving off 'end-of-image' codes, which the standard says must be used.) + People interested in making optimization even better should contact me, + or just go ahead and hack the code. Thanks again to David Hedbor + for providing test cases. + + Specific changes: + + - The optimizer can handle any input file, even one with local colormaps or + more than 256 colors. If there are more than 256 colors, an optimal subset + is placed in the global colormap. + + - The global colormap is reordered so that some images can use a small + initial portion of it, which lets them compress smaller. + + - Bug fix: Images relying on background disposal are optimized correctly. + + - `-O2` never does worse than `-O1`. + + - Made the `-O2` heuristic better. Some other small changes and + improvements here and there. + + - The optimizer has been pretty extensively tested using gifdiff. + +* New program: gifdiff compares two GIFs for identical appearance. It is + not built unless you give the `--enable-gifdiff` option to `./configure`. + It's probably not useful unless you're testing a GIF optimizer. + +* Robustness fixes in GIF library. + + +## Version 1.2.1 - 8.Jun.1998 + +* `--info` now sends information to standard output. `--info --info` still + sends information to standard error. + +* `--transparent` didn't work. It does now. + +* Added `--background` option. Fixed background processing so that an input + background is reflected in the output GIF. + +* Re-fixed small bug with the interaction between per-frame options and + frame selections. + + +## Version 1.2 - 28.May.1998 + +* Fixed small bug with the interaction between per-frame options (`--name`, + `--comment`) and frame selections. + +* Fixed bad bug in merge: If a global colormap from the source needed to be + dumped into a local colormap in the destination, the pixel map was wrong, + resulting in bad colors in the destination. + +* Fixed memory leaks. Changed memory usage pattern in gifsicle to + release source image memory as soon as possible, resulting in a better + memory profile. Both programs now keep images in compressed form for + better performance. + +* Fixed bugs in gifread.c that corrupted memory when uncompressing some bad + images. + +* Added debugging malloc library (`./configure --enable-dmalloc` to use it). + +* Added `--no-delay` and `--no-disposal` options, which should have been there + all along. + +* `--crop` will not generate an image with 0 width or height, even if a + frame is cropped out of existence. + +* Other performance improvements. + + +## Version 1.2b6 - 25.May.1998 + +* License clarification in README. + +* GIF comments are now printed out more carefully in `--info`: binary + characters appear as backslash escapes. (Literal backslashes are printed + as `\\`.) + +* New `--extension-info/--xinfo` option. + +* Removed spurious warning about local colormaps when a `--colors` option + is in effect. + +* Replacing a named frame with another image now keeps the name by default. + +* Many changes to gifview. Added `--animate`, `--display`, `--geometry`, + `--name`, `--help` options, keystrokes, multiple GIFs on the command + line, and a manual page. + + +## Version 1.2b5 - 12.May.1998 + +* Longstanding bug fix in gifunopt.c: an interlaced replacement frame + caused strange behavior when later optimizing, as the underlying data + wasn't deinterlaced. + +* Further improvements in optimizer; specifically, `-O2` works better now + -- it uses actual image data to look for an unused pixel value when + there's no transparency specified. + + +## Version 1.2b4 - 12.May.1998 + +* Improved `--optimize`'s performance on images where successive frames had + different transparent color indexes. + + +## Version 1.2b3 - 11.May.1998 + +* Fixed bugs in `--unoptimize`: it would try to unoptimize images with + local color tables and some transparency manipulations could cause a + silent failure (example: 2 colors; frame 0 = [`*11`], frame 1 = [`*0*`], + where `*` is a transparent pixel. Unoptimized, frame 1 should contain [`*01`] + -- but this can't be expressed with only 2 colors.) + +* Greatly improved `--optimize`'s performance on some images by changing the + way frames were switched to `--disposal=background`. Problem noticed by + David Hedbor + +* Documentation improvements. + +* Unrecognized extensions are now passed through without change. The + `--no-extensions` option (currently undocumented) removes all + unrecognized extensions. + +* Bug fix in gifview: frame selections now prevent display of entire GIF. + + +## Version 1.2b2 - 9.May.1998 + +* Fixed bad dumb bug in gif.h: lack of parens around macro argument + +* Changed heuristic for diversity algorithm: never blend if there are 3 colors + or less + +* Some optimizations to Floyd-Steinberg code + + +## Version 1.2b1 - 8.May.1998 + +* Added colormap options: `--colors`, `--color-method`, `--dither`, and + `--use-colormap`. All this code is in quantize.c. + +* Renamed `--color-change` to `--change-color`. + + +## Version 1.1.2 + +* Added `--color-change` option. + +* Added negative frame numbers. `#-N` no longer means `#0-N`; it means the + `N`th frame from the end (where `#-1` is the last frame). You can use + negative frame numbers in ranges as well (e.g., `#-5--1`). + + +## Version 1.1.1 - 22.Nov.1997 + +* Fixed bug in CLP which segmentation-faulted on `-` arguments. + + +## Version 1.1 - 20.Nov.1997 + +* Added `--crop` option. + +* Changed usage behavior on bad command lines. + + +## Version 1.0 - 21.Jun.1997 + +* Added `--output/-o` option to specify output filename. + +* Slight behavior change: Now, if you replace a frame (say frame #2), its + delay value will be preserved in the replacement frame, unless you give + an explicit delay. (Before, the replacement frame's delay would be used, + which was probably 0.) + + +## Version 0.91 - 15.Jun.1997 + +* Bug fix: `--optimize` didn't handle the bottom row of an image's changed + area correctly. This could lead to incorrect animations. Specific change: + merge.c line 744: was y < fbt, now y <= fbt. + +* Added `-S` as synonym for `--logical-screen`. + + +## Version 0.9 - 12.Jun.1997 + +* First public release. diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/README.md b/packages/optimizt-gifsicle/upstream/gifsicle/README.md new file mode 100644 index 0000000..274c4cc --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/README.md @@ -0,0 +1,135 @@ +![gifsicle-logo](https://raw.githubusercontent.com/kohler/gifsicle/master/logo.gif) + +Gifsicle [![Build Status](https://github.com/kohler/gifsicle/actions/workflows/tests.yml/badge.svg)](https://github.com/kohler/gifsicle/actions/workflows/tests.yml) +======== + +Gifsicle manipulates GIF image files. Depending on command line +options, it can merge several GIFs into a GIF animation; explode an +animation into its component frames; change individual frames in an +animation; turn interlacing on and off; add transparency; add delays, +disposals, and looping to animations; add and remove comments; flip +and rotate; optimize animations for space; change images' colormaps; +and other things. + +Gifview, a companion program, displays GIF images and animations on an +X display. It can display multi-frame GIFs either as slideshows, +displaying one frame at a time, or as real-time animations. + +Gifdiff, another companion program, checks two GIF files for identical +visual appearance. This is probably most useful for testing +GIF-manipulating software. + +Each of these programs has a manpage, `PROGRAMNAME.1`. + +The Gifsicle package comes with NO WARRANTY, express or implied, +including, but not limited to, the implied warranties of +merchantability and fitness for a particular purpose. + +See `NEWS.md` in this directory for changes in recent versions. The Gifsicle +home page is: + +http://www.lcdf.org/gifsicle/ + + +Building Gifsicle on UNIX +------------------------- + +Type `./configure`, then `make`. + +If `./configure` does not exist (you downloaded from Github), run +`autoreconf -i` first. + +`./configure` accepts the usual options; see `INSTALL` for details. +To build without gifview (for example, if you don't have X11), use +`./configure --disable-gifview`. To build without gifdiff, +use `./configure --disable-gifdiff`. + +`make install` will build and install Gifsicle and its manual page +(under /usr/local by default). + + +Building Gifsicle on Windows +---------------------------- + +To build Gifsicle on Windows using Visual C, change into the `src` +directory and run + + nmake -f Makefile.w32 + +Gifview will not be built. The makefile is from Emil Mikulic + with updates by Steven Marthouse +. + +To build Gifsicle on Windows using Borland C++, change into the `src` +directory and run + + nmake -f Makefile.bcc + +Stephen Schnipsel provided `Makefile.bcc`. You +will need to edit one of these Makefiles to use a different compiler. +You can edit it with any text editor (like Notepad). See the file for +more information. + + +Contact +------- + +If you have trouble building or running Gifsicle, try GitHub issues: +https://github.com/kohler/gifsicle + +Eddie Kohler, ekohler@gmail.com +http://www.read.seas.harvard.edu/~kohler/ +Please note that I do not provide support for Windows. + + +Copyright/License +----------------- + +All source code is Copyright (C) 1997-2025 Eddie Kohler. + +IF YOU PLAN TO USE GIFSICLE ONLY TO CREATE OR MODIFY GIF IMAGES, DON'T +WORRY ABOUT THE REST OF THIS SECTION. Anyone can use Gifsicle however +they wish; the license applies only to those who plan to copy, +distribute, or alter its code. If you use Gifsicle for an +organizational or commercial Web site, I would appreciate a link to +the Gifsicle home page on any 'About This Server' page, but it's not +required. + +This code is distributed under the GNU General Public License, Version +2 (and only Version 2). The GNU General Public License is available +via the Web at or in the +'COPYING' file in this directory. + +The following alternative license may be used at your discretion. + +Permission is granted to copy, distribute, or alter Gifsicle, whole or +in part, as long as source code copyright notices are kept intact, +with the following restriction: Developers or distributors who plan to +use Gifsicle code, whole or in part, in a product whose source code +will not be made available to the end user -- more precisely, in a +context which would violate the GPL -- MUST contact the author and +obtain permission before doing so. + + +Authors +------- + +This program is dedicated to the memory of Anne Dudfield, who named +it. Anne was an incredible friend and ridiculous person. ANNIED!!!!!! + +Eddie Kohler \ +http://www.read.seas.harvard.edu/~kohler/ \ +He wrote it. + +David Hedbor \ +Many bug reports and constructive whining about the optimizer. + +Emil Mikulic \ +Win32 port help. + +Hans Dinsen-Hansen \ +http://www.danbbs.dk/~dino/ \ +Adaptive tree method for GIF writing. + +Kornel Lesiński \ +`--lossy` option. diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/bootstrap.sh b/packages/optimizt-gifsicle/upstream/gifsicle/bootstrap.sh new file mode 100755 index 0000000..68f4a17 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/bootstrap.sh @@ -0,0 +1,2 @@ +#!/bin/sh +autoreconf -i diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/configure.ac b/packages/optimizt-gifsicle/upstream/gifsicle/configure.ac new file mode 100644 index 0000000..0ababe4 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/configure.ac @@ -0,0 +1,317 @@ +dnl Process this file with autoconf to produce a configure script. +AC_INIT([gifsicle],[1.96]) +AC_PREREQ([2.69]) +AC_CONFIG_SRCDIR([src/gifsicle.h]) +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_FILES([Makefile src/Makefile]) +AM_INIT_AUTOMAKE +ac_user_cc=${CC+y} + +AC_PROG_MAKE_SET +AC_PROG_CC +AC_PROG_CPP +AC_C_CONST +AC_C_INLINE + +AC_CHECK_HEADERS([sys/select.h sys/stat.h inttypes.h unistd.h]) + +build_gifview=maybe +AC_ARG_ENABLE([gifview], + [AS_HELP_STRING([--disable-gifview], [do not build gifview X11 viewer])], + [if test "x$enableval" != xyes; then build_gifview=no; fi]) + +build_gifdiff=yes +AC_ARG_ENABLE([gifdiff], + [AS_HELP_STRING([--disable-gifdiff], [do not build gifdiff comparison utility])], + [if test "x$enableval" != xyes; then build_gifdiff=no; fi]) + +AC_ARG_ENABLE([warnings], + [AS_HELP_STRING([--disable-warnings], [compile without -W -Wall])], + [:], [enable_warnings=yes]) +if test x$enable_warnings = xyes; then + CC="$CC -W -Wall" +fi + +WERROR= +AC_ARG_ENABLE([werror], + [AS_HELP_STRING([--enable-werror], [compile with -Werror])], + [if test "x$enableval" = xyes; then WERROR="-Werror"; fi]) +AC_SUBST([WERROR]) + +use_dmalloc= +AC_ARG_ENABLE([dmalloc], + [AS_HELP_STRING([--enable-dmalloc], [build with debugging malloc library])], + [if test "x$enableval" = xyes; then use_dmalloc=yes; fi]) + +ungif= +AC_ARG_ENABLE([ungif], + [AS_HELP_STRING([--enable-ungif], [build without LZW compression])], + [if test "x$enableval" = xyes; then ungif=yes; fi]) + + +dnl +dnl Choose programs to build. Always build gifsicle +dnl + +OTHERPROGRAMS="" +OTHERMANS="" + +if test "x$build_gifview" = xyes -o "x$build_gifview" = xmaybe; then + AC_PATH_XTRA + if test "x$no_x" = xyes; then + if test "x$build_gifview" = xyes; then + AC_MSG_ERROR([ +****************************************************************************** +Cannot find X, but you explicitly requested that gifview be built. +You may need to install an X11 development package to get the X headers, +supply a '--with-x' option, or simply '--disable-gifview'.]) + else + AC_MSG_WARN([Not compiling gifview since X is not available.]) + build_gifview=no + fi + else + build_gifview=yes + fi +fi + +if test "x$build_gifview" = xyes ; then + OTHERPROGRAMS="$OTHERPROGRAMS gifview"'$(EXEEXT)' + OTHERMANS="$OTHERMANS gifview.1" + AC_CHECK_HEADERS([time.h sys/time.h]) + AC_CACHE_CHECK([for gettimeofday prototype], [ac_cv_gettimeofday], + [AC_COMPILE_IFELSE([AC_LANG_PROGRAM( + [[#include +#include ]], + [[gettimeofday((void*) 0, (void*) 0)]])], + [ac_cv_gettimeofday=2], + [AC_COMPILE_IFELSE([AC_LANG_PROGRAM( + [[#include + #include ]], + [[gettimeofday((void*) 0)]])], + [ac_cv_gettimeofday=1], [ac_cv_gettimeofday=0])])]) + AC_DEFINE_UNQUOTED([GETTIMEOFDAY_PROTO], [$ac_cv_gettimeofday], [Define to the number of arguments to gettimeofday.]) +else + AC_DEFINE_UNQUOTED(X_DISPLAY_MISSING, 1, [Define if X is not available.]) +fi + +if test "x$build_gifdiff" = xyes ; then + OTHERPROGRAMS="$OTHERPROGRAMS gifdiff"'$(EXEEXT)' + OTHERMANS="$OTHERMANS gifdiff.1" +fi + +AC_SUBST(OTHERPROGRAMS) +AC_SUBST(OTHERMANS) + + +dnl +dnl Set up ungif support +dnl + +if test "x$ungif" = xyes -o "x$GIFSICLE_UNGIF" = xyes ; then + GIFWRITE_O=ungifwrt.o + AC_DEFINE(GIF_UNGIF, 1, [Define if GIF LZW compression is off.]) +elif test -r $srcdir/src/gifwrite.c ; then + GIFWRITE_O=gifwrite.o +else + AC_MSG_ERROR([This version of gifsicle cannot create compressed GIFs. +*** Either reconfigure with the '--enable-ungif' option or download the +*** version with compression from http://www.lcdf.org/gifsicle/]) +fi +AC_SUBST(GIFWRITE_O) + +dnl +dnl random or rand, strerror, strtoul, mkstemp, snprintf +dnl + +AC_CHECK_FUNC([random], [random_func=random], [random_func=rand]) +AC_DEFINE_UNQUOTED(RANDOM, ${random_func}, [Define to a function that returns a random number.]) + +AC_REPLACE_FUNCS([strerror]) +AC_CHECK_FUNCS([strtoul mkstemp snprintf]) +AC_SEARCH_LIBS([pow], [m], [AC_DEFINE([HAVE_POW], [1], [Define to 1 if you have the `pow' function.])]) +AC_SEARCH_LIBS([cbrtf], [m], [AC_DEFINE([HAVE_CBRTF], [1], [Define to 1 if you have the `cbrtf' function.])]) + + +dnl +dnl integer types +dnl + +AC_CHECK_HEADERS(inttypes.h, have_inttypes_h=yes, have_inttypes_h=no) +AC_CHECK_HEADERS(sys/types.h, have_sys_types_h=yes, have_sys_types_h=no) + +if test $have_inttypes_h = no -a $have_sys_types_h = yes; then + AC_CACHE_CHECK([for uintXX_t typedefs], ac_cv_uint_t, + [AC_EGREP_HEADER(dnl +changequote(<<,>>)<<(^|[^a-zA-Z_0-9])uint32_t[^a-zA-Z_0-9]>>changequote([,]), + sys/types.h, ac_cv_uint_t=yes, ac_cv_uint_t=no)]) +fi +if test $have_inttypes_h = no -a $have_sys_types_h = yes -a "$ac_cv_uint_t" = no; then + AC_CACHE_CHECK([for u_intXX_t typedefs], ac_cv_u_int_t, + [AC_EGREP_HEADER(dnl +changequote(<<,>>)<<(^|[^a-zA-Z_0-9])u_int32_t[^a-zA-Z_0-9]>>changequote([,]), + sys/types.h, ac_cv_u_int_t=yes, ac_cv_u_int_t=no)]) +fi +if test $have_inttypes_h = yes -o "$ac_cv_uint_t" = yes; then + : +elif test "$ac_cv_u_int_t" = yes; then + AC_DEFINE([HAVE_U_INT_TYPES], [1], [Define if you have u_intXX_t types but not uintXX_t types.]) +fi + +AC_CHECK_TYPES([uintptr_t, int64_t, uint64_t], [], [], +[#if HAVE_INTTYPES_H +# include +#endif +#if HAVE_SYS_TYPES_H +# include +#endif +]) + +AC_CHECK_SIZEOF([float]) +AC_CHECK_SIZEOF([unsigned int]) +AC_CHECK_SIZEOF([unsigned long]) +AC_CHECK_SIZEOF([void *]) + + +dnl +dnl vector types +dnl + +AC_MSG_CHECKING([for usable vector_size vector types]) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM( + [AC_INCLUDES_DEFAULT + [typedef float float4 __attribute__((vector_size (4 * sizeof(float))));]], + [[float4 a; + a[0] = a[1] = a[2] = a[3] = (float) rand(); + a *= (float) 3; + printf("%f\n", a[0]);]])], + [AC_DEFINE([HAVE_VECTOR_SIZE_VECTOR_TYPES], 1, [Define to 1 if `vector_size' vector types are usable.]) + ac_cv_vector_size_vector_types=yes], [ac_cv_vector_size_vector_types=no]) +AC_MSG_RESULT([$ac_cv_vector_size_vector_types]) + +AC_MSG_CHECKING([for usable ext_vector_type vector types]) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM( + [AC_INCLUDES_DEFAULT + [typedef float float4 __attribute__((ext_vector_type (4)));]], + [[float4 a; + a[0] = a[1] = a[2] = a[3] = (float) rand(); + a *= (float) 3; + printf("%f\n", a[0]);]])], + [AC_DEFINE([HAVE_EXT_VECTOR_TYPE_VECTOR_TYPES], 1, [Define to 1 if `ext_vector_type' vector types are usable.]) + ac_cv_ext_vector_type_vector_types=yes], [ac_cv_ext_vector_type_vector_types=no]) +AC_MSG_RESULT([$ac_cv_ext_vector_type_vector_types]) + +AC_MSG_CHECKING([for __builtin_shufflevector]) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM( + [AC_INCLUDES_DEFAULT + [typedef float float4 __attribute__((ext_vector_type (4)));]], + [[float4 a; + a[0] = a[1] = a[2] = a[3] = (float) rand(); + a = __builtin_shufflevector(a, a, 3, 2, 1, 0); + printf("%f\n", a[0]);]])], + [AC_DEFINE([HAVE___BUILTIN_SHUFFLEVECTOR], 1, [Define to 1 if you have the `__builtin_shufflevector' function.]) + ac_cv___builtin_shufflevector=yes], [ac_cv___builtin_shufflevector=no]) +AC_MSG_RESULT([$ac_cv___builtin_shufflevector]) + +AC_ARG_ENABLE([simd], + [AS_HELP_STRING([--disable-simd], [do not use SIMD instructions])], + [], [enable_simd=maybe]) + +if test $ac_cv_vector_size_vector_types = no -a $ac_cv_ext_vector_type_vector_types = no; then + if test x$enable_simd = xmaybe; then + AC_MSG_WARN([ +Not using SIMD since your compiler's support for SIMD types is inadequate. +]) + elif test x$enable_simd != xno; then + AC_MSG_ERROR([ +****************************************************************************** +`--enable-simd' was supplied, but this compiler does not support SIMD types.]) + fi + enable_simd=no +fi +if test x"$enable_simd" != xno; then + AC_DEFINE([HAVE_SIMD], [1], [Define to 1 if SIMD types should be used.]) +fi + + +dnl +dnl threads +dnl + +AC_SEARCH_LIBS([pthread_create], [pthread], [have_threads=yes], [have_threads=no]) +AC_MSG_CHECKING([for __sync_add_and_fetch]) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM( + [AC_INCLUDES_DEFAULT + extern volatile int x;], + [[printf("%d", __sync_add_and_fetch(&x, 1));]])], + [AC_DEFINE([HAVE___SYNC_ADD_AND_FETCH], 1, [Define to 1 if you have the `__sync_add_and_fetch' function.]) + result=yes], [result=no]) +AC_MSG_RESULT([$result]) + +AC_ARG_ENABLE([threads], + [AS_HELP_STRING([--disable-threads], [do not include multithreading])], + [], [enable_threads=maybe]) + +if test "x$have_threads" = xno -a "x$enable_threads" = xyes; then + AC_MSG_ERROR([ +****************************************************************************** +Threading support is not available on this platform.]) +elif test "x$have_threads" = xyes -a "x$enable_threads" != xno; then + AC_DEFINE([ENABLE_THREADS], [1], [Define to 1 if multithreading support is available.]) +fi + + +dnl +dnl verbatim portions of the header +dnl + +AC_DEFINE_UNQUOTED(PATHNAME_SEPARATOR, ['/'], [Pathname separator character ('/' on Unix).]) +if false; then + AC_DEFINE(OUTPUT_GIF_TO_TERMINAL, 1, [Define to write GIFs to stdout even when stdout is a terminal.]) +fi + +AH_TOP([#ifndef GIFSICLE_CONFIG_H +#define GIFSICLE_CONFIG_H]) + +AH_BOTTOM([#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Use the clean-failing malloc library in fmalloc.c. */ +#define GIF_ALLOCATOR_DEFINED 1 +#define Gif_Free free + +/* Prototype strerror if we don't have it. */ +#ifndef HAVE_STRERROR +char *strerror(int errno); +#endif + +#ifdef __cplusplus +} +/* Get rid of a possible inline macro under C++. */ +# define inline inline +#endif + +/* Need _setmode under MS-DOS, to set stdin/stdout to binary mode */ +/* Need _fsetmode under OS/2 for the same reason */ +/* Windows has _isatty, not isatty */ +/* Modern Windows has _snprintf, but we prefer snprintf */ +#if defined(_MSDOS) || defined(_WIN32) || defined(__EMX__) || defined(__DJGPP__) +# include +# include +# define isatty _isatty +#endif + +#ifndef HAVE_SNPRINTF +#define snprintf(s, n, ...) sprintf((s), __VA_ARGS__) +#endif + +#endif /* GIFSICLE_CONFIG_H */]) + + +dnl +dnl Output +dnl + +AC_OUTPUT diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/gifdiff.1 b/packages/optimizt-gifsicle/upstream/gifsicle/gifdiff.1 new file mode 100644 index 0000000..81b8cdf --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/gifdiff.1 @@ -0,0 +1,97 @@ +.\" -*- mode: nroff -*- +.ds V 1.96 +.ds E " \-\- +.if t .ds E \(em +.de Op +.BR "\\$1" "\\$2" "\\$3" "\\$4" "\\$5" "\\$6" +.. +.de Sp +.if n .sp +.if t .sp 0.4 +.. +.de Es +.Sp +.RS 5 +.nf +.. +.de Ee +.fi +.RE +.PP +.. +.TH GIFDIFF 1 "11 July 2017" "Version \*V" +.SH NAME +gifdiff \- compares GIF images +.SH SYNOPSIS +.B gifdiff +\%[options] +GIF\-file\-1 +GIF\-file\-2 +' +.SH DESCRIPTION +.B gifdiff +compares two GIF files and determines if they appear identical. Differences +that don't affect appearance (like colormap ordering or how much an +animation is optimized) are not reported. +.PP +.B gifdiff +prints details of any differences it finds. If the GIFs are the same, it +prints nothing. It exits with status 0 if there were no differences, 1 if +there were some differences, and 2 if there was trouble. +' +.SH OPTIONS + +.PD 0 +.TP 5 +.Op \-\-brief ", " \-q +' +Report only whether the GIFs differ, not the details of the differences. +' +.Sp +.TP 5 +.Op \-\-ignore\-redundancy ", " \-w +' +Do not report differences in the numbers of redundant frames (frames which +do not change the displayed image). +' +.Sp +.TP 5 +.Op \-\-ignore\-background ", " \-B +' +Do not report differences in background colors. +' +.Sp +.TP 5 +.Op \-\-help +' +Print usage information and exit. +' +.Sp +.TP +.Op \-\-version +' +Print the version number and some quickie warranty information and exit. +' +.PD +' +.SH SEE ALSO + +.BR gifsicle (1) +' +.SH BUGS + +Please email suggestions, additions, patches and bugs to +ekohler@gmail.com. +' +.SH AUTHORS +.na +Eddie Kohler, ekohler@gmail.com +.br +http://www.read.seas.harvard.edu/~kohler/ +.PP +http://www.lcdf.org/gifsicle/ +.br +The +.B gifsicle +home page. +' diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/gifsicle.1 b/packages/optimizt-gifsicle/upstream/gifsicle/gifsicle.1 new file mode 100644 index 0000000..9809c82 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/gifsicle.1 @@ -0,0 +1,1404 @@ +.\" -*- mode: nroff -*- +.AM +.ds V 1.96 +.ds E " \-\- +.if t .ds E \(em +.de Op +.BR "\\$1" "\\$2" "\\$3" "\\$4" "\\$5" "\\$6" +.. +.de Oa +.IR "\fB\\$1\& \|\fI\\$2" "\\$3" "\\$4" "\\$5" "\\$6" +.. +.de Qo +.RB \(oq "\\$1" "\(cq\\$2" +.. +.de Qa +.BI "\fR\(oq\fB\\$1" " \\$2" " \\$3" " \\$4" "\fR\(cq\\$5" +.. +.de Sp +.if n .sp +.if t .sp 0.4 +.. +.de Ix +.TP 25 +\\$1 +.nh +\\$2 +.hy +.. +.de Es +.Sp +.RS 5 +.nf +.. +.de Ee +.fi +.RE +.PP +.. +.de Xs +.RS 5 +.nf +.. +.de Xe +.fi +.RE +.. +.TH GIFSICLE 1 "26 February 2025" "Version \*V" +.SH NAME +gifsicle \- manipulates GIF images and animations +.SH SYNOPSIS +.B gifsicle +\%[options, frames, and filenames].\|.\|. +' +.SH DESCRIPTION +.B gifsicle +creates, edits, manipulates, and +prints information about GIF images and animations. +.PP +.B Gifsicle +normally processes input GIF files according to its command line +options and writes the result to the standard output. The +.Op \-i +option, for example, tells +.B gifsicle +to interlace its inputs: +.Es +\fBgifsicle \-i < pic.gif > interlaced-pic.gif\fR +.Ee +.PP +By default, +.B gifsicle +combines two or more input files into a \(lqflipbook\(rq animation: +.Es +\fBgifsicle pic1.gif pic2.gif pic3.gif > animation.gif\fR +.Ee +Use options like +.Op \-\-delay ", " \-\-loopcount ", and " \-\-optimize +to tune your animations. +.PP +To modify GIF files in place, use the +.Op \-\-batch +option. With +.Op \-\-batch , +.B gifsicle +will modify the files you specify instead of writing a new file to the +standard output. To interlace all the GIFs in the current directory, you +could say: +.Es +\fBgifsicle \-\-batch \-i *.gif +.Ee +.PP +New users may want to skip to +the Examples section at the end. +' +.SH CONCEPT INDEX +' +Concepts are on the left, relevant +.B gifsicle +options are on the right. +' +.Sp +.PD 0 +.Ix "Animations, changing" "frame selections, frame changes, etc." +.Ix "\ \ \ disposal" "\fB\-\-disposal\fP" +.Ix "\ \ \ looping" "\fB\-\-loopcount\fP" +.Ix "\ \ \ portions of" "frame selections" +.Ix "\ \ \ smaller" "\fB\-\-optimize\fP, \fB\-\-colors\fP, \fB\-\-lossy\fP" +.Ix "\ \ \ speed" "\fB\-\-delay\fP" +.Ix "Bad output" "\fB\-\-careful\fP" +.Ix "Background color" "\fB\-\-background\fP" +.Ix "Colors, changing" "\fB\-\-change\-color\fP, \fB\-\-use\-colormap\fP, \fB\-\-dither\fP, \fB\-\-transform\-colormap\fP" +.Ix "\ \ \ reducing number" "\fB\-\-colors\fP, \fB\-\-dither\fP, \fB\-\-gamma\fP" +.Ix "Comments" "\fB\-\-comment\fP" +.Ix "Extensions" "\fB\-\-extension\fP, \fB\-\-app\-extension\fP, \fB\-\-extension\-info\fP" +.Ix "File size" "\fB\-\-optimize\fP, \fB\-\-unoptimize\fP, \fB\-\-colors\fP, \fB\-\-lossy\fP" +.TP 30 +Image transformations +.Ix "\ \ \ cropping" "\fB\-\-crop\fP, \fB\-\-crop\-transparency\fP" +.Ix "\ \ \ flipping" "\fB\-\-flip\-*\fP" +.Ix "\ \ \ resizing" "\fB\-\-resize\fP, \fB\-\-scale\fP" +.Ix "\ \ \ rotating" "\fB\-\-rotate\-*\fP" +.Ix "Grayscale" "\fB\-\-use\-colormap\fP" +.Ix "Interlacing" "\fB\-\-interlace\fP" +.Ix "Positioning frames" "\fB\-\-position\fP" +.Ix "Screen, logical" "\fB\-\-logical\-screen\fP" +.Ix "Selecting frames" "frame selections (like \fB'#0'\fP)" +.Ix "Transparency" "\fB\-\-transparent\fP" +.Ix "Warnings" "\fB\-\-no\-warnings\fP" +.PD +' +.SH COMMAND LINE + +.BR gifsicle 's +command line consists of GIF input files and options. Most options start +with a dash (\-) or plus (+); frame selections, a kind of option, start +with a number sign (#). Anything else is a GIF input file. +.PP +.B gifsicle +reads and processes GIF input files in order. If no GIF input file is +given, or you give the special filename \(oq\-\(cq, +it reads from the standard input. +.PP +.B gifsicle +exits with status 0 if there were no errors and status 1 otherwise. +' +.SH OPTIONS + +Every option has a long form, +.Qo \-\-long\-descriptive\-name . +You don't need to type the whole long descriptive name, just enough to +make it unambiguous. +.PP +Some options also have a short form, +.Qo \-X . +You can combine short options if they don't take arguments: +.Qo \-IIb +is the same as +.Qo "\-I \-I \-b" . +But be careful with options that do take arguments: +.Qo \-cblah +means +.Qo "\-c \fRblah" , +not +.Qo "\-c \-b \-l \-a \-h" . +.PP +Many options also have a converse, +.Qo \-\-no\-option , +which turns off the option. You can turn off a short option +.Qo \-X +by saying +.Qo \+X +instead. +' +.\" ----------------------------------------------------------------- +.SS Mode Options + +Mode options tell +.B gifsicle +what kind of output to generate. There can be at most one, and it must +precede any GIF inputs. +.TP 5 +.Op "\-\-merge" ", " "\-m" +' +Combine all GIF inputs into one file with multiple frames and write that +file to the standard output. This is the default mode. +' +.TP +.Op \-\-batch ", " \-b +' +Modify each GIF input in place by reading and writing to the same filename. +(GIFs read from the standard input are written to the standard output.) +' +.TP +.Op \-\-explode ", " \-e +' +Create an output GIF for each frame of each input file. The output GIFs are +named \(oqxxx.000\(cq, \(oqxxx.001\(cq, and so on, where \(oqxxx\(cq is the name of the input +file (or whatever you specified with +.Qo \-\-output ) +and the numeric extension is the frame number. +' +.TP +.Op \-\-explode\-by\-name ", " \-E +' +Same as +.Op \-\-explode "," +but write any named frames to files \(oqxxx.\fIname\fR\(cq instead of +\(oqxxx.\fIframe-number\fR\(cq. Frames are named using the +.Qo \-\-name +option. +' +.\" ----------------------------------------------------------------- +.SS General Options + +General options control the information +.B gifsicle +prints and where it writes its output. The info options and +.Op \-\-verbose +can be turned off with +.Qo \-\-no\-X . +' +.Sp +.PD 0 +.TP 5 +.Op \-\-info ", " \-I +' +Print a human-readable description of each input GIF to the standard +output, or whatever file you specify with +.Op \-o . +This option suppresses normal output, and cannot be combined with mode +options like +.Op \-\-batch . +If you give two +.Op \-\-info +or +.Op \-I +options, however, information is printed to standard error, and normal +output takes place as usual. +' +.Sp +.TP 5 +.Op \-\-color\-info ", " \-\-cinfo +' +Like +.Op \%\-\-info , +but also print information about input files' colormaps. +' +.Sp +.TP 5 +.Op \-\-extension\-info ", " \-\-xinfo +' +Like +.Op \%\-\-info , +but also print any unrecognized GIF extensions in a +.BR hexdump (1)-like +format. +' +.Sp +.TP 5 +.Op \-\-size\-info ", " \-\-sinfo +' +Like +.Op \%\-\-info , +but also print information about compressed image sizes. +' +.Sp +.TP 5 +.Op \-\-help ", " \-h +' +Print usage information and exit. +' +.Sp +.TP +.Oa \-o file +.TP +.Oa \-\-output file +' +Send output to +.IR file . +The special filename \(oq-\(cq means the standard output. +' +.Sp +.TP +.Op \-\-verbose ", " \-V +' +Print progress information (files read and written) to standard +error. +' +.Sp +.TP +.Op \-\-no\-warnings ", " \-w +' +Suppress all warning messages. +' +.Sp +.TP +.Op \-\-no\-ignore\-errors +' +Exit with status 1 when encountering a very erroneous GIF. Default is to +muddle on. +' +.Sp +.TP +.Op \-\-version +' +Print the version number and some short non-warranty information and exit. +' +.Sp +.PD 0 +.TP 5 +.Op \-\-careful +' +Write slightly larger GIFs that avoid bugs in some other GIF +implementations. Some Java and Internet Explorer versions cannot display +the correct, minimal GIFs that Gifsicle produces. Use the +.Op \-\-careful +option if you are having problems with a particular image. +' +.Sp +.TP +.Op \-\-conserve\-memory +' +Conserve memory usage at the expense of processing time. This may be useful +if you are processing large GIFs on a computer without very much memory. Or +say +.Op \-\-no\-conserve\-memory . +' +.Sp +.TP +.Op \-\-nextfile +' +Allow input files to contain multiple concatenated GIF images. If a +filename appears multiple times on the command line, \fBgifsicle\fR will +read a new image from the file each time. This option can help scripts +avoid the need for temporary files. For example, to create an animated GIF +with three frames with different delays, you might run "\fBgifsicle +\-\-nextfile \-d10 \- \-d20 \- \-d30 \- > out.gif\fR" and write the three +GIF images, in sequence, to \fBgifsicle\fR's standard input. +' +.Sp +.TP +.Op \-\-multifile +' +Like +.Op \-\-nextfile , +but read +.I as many GIF images as possible +from each file. This option is intended for scripts. For example, to merge +an unknown number of GIF images into a single animation, run "\fBgifsicle +\-\-multifile \- > out.gif\fR" and write the GIF images, in sequence, to +\fBgifsicle\fR's standard input. Any frame selections apply only to the +last file in the concatenation. +' +.PD +' +.\" ----------------------------------------------------------------- +.SS Frame Selections + +A frame selection tells +.B gifsicle +which frames to use from the current input file. They are useful only for +animations, as non-animated GIFs only have one frame. Here are the +acceptable forms for frame specifications. +.Sp +.PD 0 +.TP 13 +.BI # num +' +Select frame \fInum\fR. (The first frame is +.Qo #0 . +Negative numbers count backwards from the last frame, which is +.Qo #-1 .) +' +.TP 13 +.BI # num1 \- num2 +' +Select frames \fInum1\fR through \fInum2\fR. +' +.TP 13 +.BI # num1 \- +' +Select frames \fInum1\fR through the last frame. +' +.TP 13 +.BI # name +' +Select the frame named \fIname\fR. +.PD +.PP +The \(oq#\(cq character has special meaning for many shells, so you generally +need to quote it. +.PP +For example, +.Xs +\fBgifsicle happy.gif "#0"\fR +.Xe +uses the first frame from happy.gif; +.Xs +\fBgifsicle happy.gif "#0-2"\fR +.Xe +uses its first three frames; and +.Xs +\fBgifsicle happy.gif "#-1-0"\fR +.Xe +uses its frames in reverse order (starting from frame #-1\*Ethe +last frame\*Eand ending at frame #0\*Ethe first). +.PP +The action performed with the selected frames depends on the current +mode. In merge mode, only the selected frames are merged into the output +GIF. In batch mode, only the selected frames are modified; other frames +remain unchanged. In explode mode, only the selected frames are exploded +into output GIFs. +' +.\" ----------------------------------------------------------------- +.SS Frame Change Options + +Frame change options insert new frames into an animation or replace or +delete frames that already exist. Some things\*Efor example, changing one +frame in an animation\*Eare difficult to express with frame selections, but +easy with frame changes. +' +.TP 5 +.Oa \-\-delete frames " [" frames ".\|.\|.]" +' +Delete +.I frames +from the input GIF. +' +.TP +.Oa \-\-insert\-before "frame other-GIFs" +' +Insert +.I other-GIFs +before +.I frame +in the input GIF. +' +.TP +.Oa \-\-append "other-GIFs" +' +Append +.I other-GIFs +to the input GIF. +' +.TP +.Oa \-\-replace "frames other-GIFs" +' +Replace +.I frames +from the input GIF with +.IR other-GIFs . +' +.TP +\fB\-\-done\fR +' +Complete the current set of frame changes. +' +.PP +The +.I frames +arguments are frame selections (see above). These arguments always refer to +frames from the +.I original +input GIF. So, if \(oqa.gif\(cq has 3 frames and \(oqb.gif\(cq has one, this +command +.Xs +\fBgifsicle a.gif \-\-delete "#0" \-\-replace "#2" b.gif\fR +.Xe +will produce an output animation with 2 frames: \(oqa.gif\(cq frame 1, then +\(oqb.gif\(cq. +.PP +The +.I other-GIFs +arguments are any number of GIF input files and frame selections. +These images are combined in merge mode and added to the input GIF. +The +.I other-GIFs +last until the next frame change option, so this command replaces the +first frame of \(oqin.gif\(cq with the merge of \(oqa.gif\(cq and \(oqb.gif\(cq: +.Xs +\fBgifsicle \-b in.gif \-\-replace "#0" a.gif b.gif\fR +.Xe +.PP +This command, however, replaces the first frame of \(oqin.gif\(cq with +\(oqa.gif\(cq and then processes \(oqb.gif\(cq separately: +.Xs +\fBgifsicle \-b in.gif \-\-replace "#0" a.gif \-\-done b.gif\fR +.Xe +.PP +Warning: You shouldn't use both frame selections and frame changes on +the same input GIF. +' +.\" ----------------------------------------------------------------- +.SS Image Options + +Image options modify input images\*Eby changing their interlacing, +transparency, and cropping, for example. Image options have three forms: +.Qo \-\-X , +.Qo \-\-no\-X , +and +.Qo \-\-same\-X . +The +.Qo \-\-X +form selects a value for the feature, the +.Qo \-\-no\-X +form turns off the feature, and the +.Qo \-\-same\-X +form means that the feature's value is copied from each input. The default +is always +.Qo \-\-same\-X . +For example, +.Op \-\-background= """#0000FF""" +sets the background color to blue, +.Op \-\-no\-background +turns the background color off (by setting it to 0), and +.Op \-\-same\-background +uses input images' existing background colors. You can give each option +multiple times; for example, +.Xs +\fBgifsicle \-b \-O2 \-i a.gif \-\-same\-interlace b.gif c.gif\fR +.Xe +will make \(oqa.gif\(cq interlaced, but leave \(oqb.gif\(cq and \(oqc.gif\(cq interlaced only +if they were already. +' +.Sp +.PD 0 +.TP 5 +.Oa \-B color +.TP +.Oa \-\-background color +' +Set the output GIF's background to +.IR color . +The argument can have the same forms as in the +.Op \-\-transparent +option below. +' +.Sp +.TP +.Oa \-\-crop x1 , y1 - x2\fR,\fIy2 +.TP +.Oa \-\-crop x1 , y1 + width\fRx\fIheight +' +Crop the following input frames to a smaller rectangular area. The top-left +corner of this rectangle is +.RI ( x1 , y1 ); +you can give either the lower-right corner, +.RI ( x2 , y2 ), +or the width and height of the rectangle. In the +.IR x1 , y1 + width x height +form, +.I width +and +.I height +can be zero or negative. A zero dimension means the cropping area goes to +the edge of the image; a negative dimension brings the cropping area that +many pixels back from the image edge. For example, +.Op \-\-crop " 2,2+-2x-2" +will shave 2 pixels off each side of the input image. Cropping happens +before any rotation, flipping, resizing, or positioning. +' +.Sp +.TP +.Op \-\-crop\-transparency +' +Crop any transparent borders off the following input frames. This happens +after any cropping due to the +.Op \-\-crop +option. It works on the raw input images; for example, any transparency +options have not yet been applied. +' +.Sp +.TP +.Op \-\-flip\-horizontal +.TP +.Op \-\-flip\-vertical +' +Flip the following frames horizontally or vertically. +' +.Sp +.TP +.Op \-i +.TP +.Op \-\-interlace +' +Turn interlacing on. +' +.Sp +.TP +.Oa \-S width x height +.TP +.Oa \-\-logical\-screen width x height +' +Set the output logical screen to +.IR width x height . +.Op \-\-no\-logical\-screen +sets the output logical screen large enough to include all output frames, while +.Op \-\-same\-logical\-screen +sets the output logical screen large enough to include all input logical +screens. +.Op \-\-screen +is a synonym for +.Op \-\-logical\-screen . +' +.Sp +.TP +.Oa \-p x\fR,\fIy +.TP +.Oa \-\-position x\fR,\fIy +' +Set the following frames' positions to +.RI ( x , y ). +.Op \-\-no\-position +means +.Op \-\-position " 0,0." +Normally, +.Oa \-\-position x\fR,\fIy +places every succeeding frame exactly at \fIx\fR,\fIy\fR. However, if an +entire animation is input, \fIx\fR,\fIy\fR is treated as the position for +the animation. +' +.Sp +.TP +.Op \-\-rotate\-90 +.TP +.Op \-\-rotate\-180 +.TP +.Op \-\-rotate\-270 +' +Rotate the following frames by 90, 180, or 270 degrees. +.Op \-\-no\-rotate +turns off any rotation. +' +.Sp +.TP +.Oa \-t color +.TP +.Oa \-\-transparent color +' +Make +.I color +transparent in the following frames. +.I Color +can be a colormap index (0\-255), a hexadecimal color specification +(like "#FF00FF" for magenta), or slash- or comma-separated red, green +and blue values (each between 0 and 255). +.PD +' +.\" ----------------------------------------------------------------- +.SS Extension Options + +Extension options add non-visual information to the output GIF. This +includes names, comments, and generic extensions. +' +.Sp +.PD 0 +.TP 5 +.Oa \-\-app\-extension app\-name " " extension +' +Add an application extension named +.I app\-name +and with the value +.I extension +to the output GIF. +.Op \-\-no\-app\-extensions +removes application extensions from the input images. +' +.Sp +.TP +.Oa \-c text +.TP +.Oa \-\-comment text +' +Add a comment, +.IR text , +to the output GIF. The comment will be placed before the next frame in +the stream. +.Op \-\-no\-comments +removes comments from the input images. +' +.Sp +.TP +.Oa \-\-extension number " " extension +' +Add an extension numbered +.I number +and with the value +.I extension +to the output GIF. +.I Number +can be in decimal, octal, hex, or it can be a single character like \(oqn\(cq, +whose ASCII value is used. +.Op \-\-no\-extensions +(or +.Op +x ) +removes extensions from the input images. +' +.Sp +.TP +.Oa \-n text +.TP +.Oa \-\-name text +' +Set the next frame's name to +.IR text . +This name is stored as an extension in the output GIF (extension number +0xCE, followed by the characters of the frame name). +.Op \-\-no\-names +removes name extensions from the input images. +' +.PD +' +.\" ----------------------------------------------------------------- +.SS Animation Options + +Animation options apply to GIF animations, or to individual frames in GIF +animations. As with image options, most animation options have three forms, +.Qo \-\-X , +.Qo \-\-no\-X , +and +.Qo \-\-same\-X , +and you can give animation options multiple times; for example, +.Xs +\fBgifsicle \-b a.gif \-d50 "#0" "#1" \-d100 "#2" "#3"\fR +.Xe +sets the delays of frames 0 and 1 to 50, and frames 2 and 3 to 100. +' +.Sp +.PD 0 +.TP 5 +.Oa \-d time +.TP +.Oa \-\-delay time +' +Set the delay between frames to +.IR time +in hundredths of a second. +' +.Sp +.TP +.Oa \-D method +.TP +.Oa \-\-disposal method +' +Set the disposal method for the following frames to +.IR method . +A frame's disposal method determines how a viewer should remove the frame +when it's time to display the next. +.I Method +can be a number between 0 and 7 (although only 0 through 3 are +generally meaningful), or one of these names: +.BR none +(leave the frame visible for future frames to build upon), +.BR asis +(same as "none"), +.BR background " (or " bg ")" +(replace the frame with the background), or +.BR previous +(replace the frame with the area from the previous displayed frame). +.Op \-\-no\-disposal +means +.Op \-\-disposal = none . +' +.Sp +.TP +.Op \-l "[\fIcount\fR]" +.TP +.Op \-\-loopcount "[=\fIcount\fR]" +' +Set the Netscape loop extension to +.IR count . +.I Count +is an integer, or +.B forever +to loop endlessly. If you supply a +.Op \-\-loopcount +option without specifying +.IR count , +Gifsicle will use +.BR forever . +.Op \-\-no\-loopcount +(the default) turns off looping. +.Sp +Set the loop count to one less than the number of times you want the +animation to run. An animation with +.Op \-\-no\-loopcount +will show every frame once; +.Op \-\-loopcount =1 +will loop once, thus showing every frame twice; and so forth. +Note that +.Op \-\-loopcount =0 +is equivalent to +.Op \-\-loopcount =forever, +not +.Op \-\-no\-loopcount . +' +.Sp +.TP +.Op \-O "[\fIlevel\fR]" +.TP +.Op \-\-optimize "[=\fIlevel\fR]" +' +Attempt to shrink the file sizes of GIF animations. +.I Level +determines how much optimization is done; higher levels take longer, but +may have better results. There are currently three levels: +.Sp +.RS +.TP 5 +.Op \-O1 +Store only the changed portion of each image. This is the default. +.TP 5 +.Op \-O2 +Store only the changed portion of each image, and use transparency. +.TP 5 +.Op \-O3 +Try several optimization methods (usually slower, sometimes better results). +.Sp +.PP +Other optimization flags provide finer-grained control. +.Sp +.TP 5 +.Op \-Okeep-empty +Preserve empty transparent frames (they are dropped by default). +.Sp +.PP +You may also be interested in other options for shrinking GIFs, such as +.Op \-k , +.Op \-\-lossy , +and +.Op \-\-no\-extensions . +Note that +.Op \-O +does not guarantee to shrink file size, and in rare cases, even +.Op \-O3 +may actually enlarge file size. +.RE +' +.Sp +.TP 5 +.Oa \-U +.TP +.Oa \-\-unoptimize +' +Unoptimize GIF animations into an easy-to-edit form. +.Sp +GIF animations are often optimized (see +.Op \-\-optimize ) +to make them smaller and faster to load, which unfortunately makes them +difficult to edit. +.Op \-\-unoptimize +changes optimized input GIFs into unoptimized GIFs, where each frame is a +faithful representation of what a user would see at that point in the +animation. +.RE +' +.PD +' +.\" ----------------------------------------------------------------- +.SS Image Transformation Options +' +Image transformation options apply to entire GIFs as they are read or +written. They can be turned off with +.Qo \-\-no\-option . +' +.Sp +.PD 0 +.TP 5 +.Oa \-\-resize width x height +' +Resize the output GIF to the given +.IR width " and " height . +If +.I width +or +.I height +is an underscore \(oq_\(cq, that dimension is chosen so that the +aspect ratio remains unchanged. +Resizing happens after all input frames have been combined and before +optimization. Resizing uses logical screen dimensions; if +the input stream has an unusual logical screen (many GIF displayers ignore +logical screens), you may want to provide +.Op \-\-no\-logical\-screen +(or +.Op +S ) +to reset it so +.B gifsicle +uses image dimensions instead. See also +.Op \-\-resize\-method . +' +.Sp +.TP +.Oa \-\-resize\-width width +.TP +.Oa \-\-resize\-height height +' +Resize to a given width or height, preserving aspect ratio. Equivalent to +.Oa \-\-resize width x_ +or +.Oa \-\-resize "" _x height . +' +.Sp +.TP +.Oa \-\-resize\-fit width x height +.TP +.Oa \-\-resize\-touch width x height +' +Resize the output GIF to fit a rectangle with dimensions +.IR width x height . +The aspect ratio remains unchanged. The +.Op \-\-resize\-fit +option only shrinks the image\*Eno resize is performed if the GIF already +fits within the rectangle. The +.Op \-\-resize\-touch +option may shrink or grow the image; the output GIF will have width +.IR width , +height +.IR height , +or both. Either +.I width +or +.I height +may be an underscore \(oq_\(cq, which leaves that dimension unconstrained. +' +.Sp +.TP +.Oa \-\-resize\-fit\-width width +.TP +.Oa \-\-resize\-fit\-height height +.TP +.Oa \-\-resize\-touch\-width width +.TP +.Oa \-\-resize\-touch\-height height +' +Like +.Oa \-\-resize\-fit +and +.Op \-\-resize\-touch , +but constrains only one dimension. +' +.Sp +.TP +.Oa \-\-resize\-geometry geometry +' +Resize to a geometry specification as used in ImageMagick and GraphicsMagick. +.I Geometry +should match +.RI \(oq width x height [%][!][^][<][>]\(cq, +where +.IR width " and " height +are integers (or underscores, \(oq_\(cq, which leaves that dimension unconstrained). +The suffixes mean: +.RS +.TP 5 +.Op % +Treat the dimensions as percentages of the current size. +.TP 5 +.Op ! +Resize to given dimensions ignoring original aspect ratio. +.TP 5 +.Op < +Resize only if the image is smaller than the specified size. +.TP 5 +.Op > +Resize only if the image is larger than the specified size. +.TP 5 +.Op ^ +Dimensions are treated as minimum rather than maximum values. +.RE +' +.Sp +.TP +.Oa \-\-scale Xfactor [x Yfactor ] +' +Scale the output GIF's width and height by +.IR Xfactor " and " Yfactor . +If +.I Yfactor +is not given, it defaults to +.IR Xfactor . +Scaling happens after all input frames have been combined and before +optimization. +' +.Sp +.TP +.Oa \-\-resize\-method method +' +Set the method used to resize images. The \(oqsample\(cq method runs +very quickly, but when shrinking images, it produces noisy results. +The \(oqmix\(cq method is somewhat slower, but produces better-looking +results. The default method is +currently \(oqmix\(cq. +.RS +.Sp +.PP +Details: The resize methods differ most when shrinking images. The +\(oqsample\(cq method is a point sampler: each pixel position in the +output image maps to exactly one pixel position in the input. When +shrinking, full rows and columns from the input are dropped. The other +methods use all input pixels, which generally produces better-looking +images. The \(oqbox\(cq method, a box sampler, is faster than the more +complex filters and produces somewhat sharper results, but there will +be anomalies when shrinking images by a small amount in one dimension. +(Some output pixels will correspond to exactly 1 input row or column, +while others will correspond to exactly 2 input rows or columns.) The +\(oqmix\(cq method is a full bilinear interpolator. This is slower and +produces somewhat blurrier results, but avoids anomalies. +.Sp +.PP +Gifsicle also supports more complex resamplers, including +Catmull-Rom cubic resampling (\(oqcatrom\(cq), the Mitchell-Netravali +filter (\(oqmitchell\(cq), a 2-lobed Lanczos filter +(\(oqlanczos2\(cq), and a 3-lobed Lanczos filter (\(oqlanczos3\(cq). +These filters are slower still, but can give sharper, better results. +.RE +' +.Sp +.TP +.Oa \-\-resize\-colors n +' +Allow Gifsicle to add intermediate colors when resizing images. +Normally, Gifsicle's resize algorithms use input images' color +palettes without changes. When shrinking images with very few colors +(e.g., pure black-and-white images), adding intermediate colors can +improve the results. Example: +.Oa \-\-resize\-colors 64 +allows Gifsicle to add intermediate colors for images that have fewer +than 64 input colors. +.PD +' +.\" ----------------------------------------------------------------- +.SS Color Options +' +Color options apply to entire GIFs as they are read or +written. They can be turned off with +.Qo \-\-no\-option . +.Sp +.PD 0 +.TP 5 +.Oa \-k num +.TP +.Oa \-\-colors num +' +Reduce the number of distinct colors in each output GIF to +.I num +or less. +.I Num +must be between 2 and 256. This can be used to shrink output GIFs or +eliminate any local color tables. +.Sp +Normally, an adaptive group of colors is chosen from the existing +color table. You can affect this process with the +.Op \-\-color\-method +option or by giving your own colormap with +.Op \-\-use\-colormap . +Gifsicle may need to add an additional color (making +.IR num +1 +in all) if there is transparency in the image. +' +.Sp +.TP +.Oa \-\-color\-method method +' +Determine how a smaller colormap is chosen. +.RB \(oq diversity \(cq, +the default, is +.BR xv (1)'s +diversity algorithm, which uses a strict subset of the existing colors +and generally produces good results. +.RB \(oq blend\-diversity \(cq +is a modification of this: some color values are blended from groups of +existing colors. +.RB \(oq median\-cut \(cq +is the median cut algorithm described by Heckbert. +.Op \-\-method +is a synonym for +.Op \-\-color\-method . +' +.Sp +.TP 5 +.Oa \-f +.TP +.Op \-\-dither "[=\fImethod\fR]" +' +When +.Op \-\-dither +is on and the colormap is changed, combinations of colors are used to +approximate missing colors. This looks better, but makes bigger files +and can cause animation artifacts, so it is off by default. +.Sp +Specify a dithering method with the optional \fImethod\fR argument. +The default method, +.RB \(oq floyd-steinberg \(cq, +uses Floyd-Steinberg error +diffusion. This usually looks best, but can cause animation artifacts, +because dithering choices will vary from frame to frame. +The +.RB \(oq atkinson \(cq +method uses Atkinson error diffusion, which is a variant of Floyd-Steinberg. +It offers a more localized dithering pattern, which can be useful for +images with large areas of solid color. +Gifsicle also +supports ordered dithering methods that avoid animation artifacts. +The +.RB \(oq ro64 \(cq +method uses a +large, random-looking pattern and generally produces good results. The +.RB \(oq o3 \(cq, +.RB \(oq o4 \(cq, +and +.RB \(oq o8 \(cq +methods use smaller, more +regular patterns. The +.RB \(oq ordered \(cq +method chooses a good ordered +dithering algorithm. For special effects, try the halftone methods +.RB \(oq halftone \(cq, +.RB \(oq squarehalftone \(cq, +and +.RB \(oq diagonal \(cq. +' +.Sp +Some methods take optional parameters using commas. The halftone methods +take a cell size and a color limit: +.RB \(oq halftone,10,3 \(cq +creates 10-pixel wide halftone cells where each cell uses up to 3 +colors. +' +.Sp +.TP 5 +.Oa \-\-gamma gamma +' +Set the color space/gamma correction to +.IR gamma , +which can be a real number, +.RB \(oq srgb \(cq +(the default), or +.RB \(oq oklab \(cq. +Roughly speaking, higher +numbers exaggerate shadows and lower numbers exaggerate highlights. +The default color space is standard sRGB, +which usually works well. (Its effects are similar to +\fB\-\-gamma\fP=2.2.) Gifsicle uses gamma correction when choosing a +color palette (\fB\-\-colors\fP), when dithering +(\fB\-\-dither\fP), and when computing color errors during +lossy compression (\fB\-\-lossy\fP). +' +.Sp +.TP +.Op \-\-lossy "[=\fIlossiness\fR]" +' +Alter image colors to shrink output file size at the cost of artifacts +and noise. +.I Lossiness +determines how many artifacts are allowed; higher values can result +in smaller file sizes, but cause more artifacts. The default +.I lossiness +is 20. +' +.Sp +Gifsicle uses the color space set by \fB\-\-gamma\fP to evaluate artifacts. +If unsatisfied with the operation of \fB\-\-lossy\fP, +try another color space, such as \fB\-\-gamma\fP=1. +' +.Sp +.PD 0 +.TP +.Oa \-\-change\-color color1 " " color2 +' +Change +.I color1 +to +.I color2 +in the following input GIFs. (The +.I color +arguments have the same forms as in the +.Op \-t +option.) Change multiple colors by giving the option multiple +times. Color changes don't interfere with one another, so you can safely +swap two colors with +.Qa \-\-change\-color "color1 color2" \-\-change\-color "color2 color1" . +They all take effect as an input GIF is read. +.Op \-\-no\-change\-color +cancels all color changes. +' +.Sp +.TP +.Oa \-\-transform\-colormap command +' +.I Command +should be a shell command that reads from standard input and writes to +standard output. Each colormap in the output GIF is translated into text +colormap format (see +.Op \-\-use\-colormap +below) and piped to the command. The output that command generates +(which should also be in text colormap format) will replace the input +colormap. The replacement doesn't consider color matching, so pixels +that used color slot +.I n +in the input will still use color slot +.I n +in the output. +' +.Sp +.TP +.Oa \-\-use\-colormap colormap +' +Change the image to use +.IR colormap . +Each pixel in the image is changed to the closest match in +.I colormap +(or, if +.Op \-\-dither +is on, to a dithered combination of colors in +.IR colormap ")." +.I Colormap +can be +.BR web +for the 216-color \(lqWeb-safe palette\(rq; +.BR gray +for grayscale; +.BR bw +for black-and-white; or the name of a file. That file should either be a +text file (the format is described below) or a GIF file, whose global +colormap will be used. If +.Op \-\-colors\fR=\fIN +is also given, an +.IR N \-sized +subset of +.I colormap +will be used. +.Sp +Text colormap files use this format: +.Es +; each non-comment line represents one color, "red green blue" +; each component should be between 0 and 255 +0 0 0 ; like this +255 255 255 +; or use web hex notation +#ffffff ; like this +.Ee +' +.Sp +.TP +.Oa \-\-use\-exact\-colormap colormap +' +Same as +.Op \-\-use\-colormap\fR, +but the output GIF will use +.IR colormap +as is, rather than reordering it and dropping unused colors. +' +.PD +.PP +.br +' +.SH EXAMPLES +' +First, let's create an animation, \(oqanim.gif\(cq: +.Es +\fBgifsicle a.gif b.gif c.gif d.gif > anim.gif\fR +.Ee +This animation will move very quickly: since we didn't specify a delay, a +browser will cycle through the frames as fast as it can. Let's slow it down +and pause .5 seconds between frames, using the +.Op \-\-delay +option. +.Es +\fBgifsicle \-\-delay 50 a.gif b.gif c.gif d.gif > anim.gif\fR +.Ee +If we also want the GIF to loop three times, we can use +.Op \-\-loopcount : +.Es +\fBgifsicle \-d 50 \-\-loop=3 a.gif b.gif c.gif d.gif > anim.gif\fR +.Ee +(Rather than type +.Op \-\-delay +again, we used its short form, +.Op \-d . +Many options have short forms; you can see them by running +.RB \(oq "gifsicle \-\-help" \(cq. +We also abbreviated +.Op \-\-loopcount +to +.Op \-\-loop , +which is OK since no other option starts with \(oqloop\(cq.) +.PP +To explode \(oqanim.gif\(cq into its component frames: +.Es +\fBgifsicle \-\-explode anim.gif\fR +.br +\fBls anim.gif*\fR +.br +anim.gif anim.gif.000 anim.gif.001 anim.gif.002 anim.gif.003 +.Ee +To optimize \(oqanim.gif\(cq: +.Es +\fBgifsicle \-b \-O2 anim.gif\fR +.Ee +To change the second frame of \(oqanim.gif\(cq to \(oqx.gif\(cq: +.Es +\fBgifsicle \-b \-\-unoptimize \-O2 anim.gif \-\-replace "#1" x.gif\fR +.Ee +.Op \-\-unoptimize +is used since \(oqanim.gif\(cq was optimized in the last step. Editing +individual frames in optimized GIFs is dangerous without +.Op \-\-unoptimize ; +frames following the changed frame could be corrupted by the change. +Of course, this might be what you want. +.PP +Note that +.Op \-\-unoptimize +and +.Op \-\-optimize +can be on simultaneously. +.Op \-\-unoptimize +affects +.I input +GIF files, while +.Op \-\-optimize +affects +.I output +GIF files. +.PP +To print information about the first and fourth frames of \(oqanim.gif\(cq: +.Es +\fBgifsicle \-I "#0" "#3" < anim.gif\fR +.Ee +To make black the transparent color in all the GIFs in the current +directory, and also print information about each: +.Es +\fBgifsicle \-bII \-\-trans "#000000" *.gif\fR +.Ee +Giving +.Op \-I +twice forces normal output to occur. With only one +.Op \-I , +the GIFs would not be modified. +.PP +To change \(oqanim.gif\(cq to use a 64-color subset of the Web-safe palette: +.Es +\fBgifsicle \-b \-\-colors=64 \-\-use\-col=web anim.gif\fR +.Ee +To make a dithered black-and-white version of \(oqanim.gif\(cq: +.Es +\fBgifsicle \-\-dither \-\-use\-col=bw anim.gif > anim-bw.gif\fR +.Ee +.PP +To overlay one GIF atop another\*Eproducing a one-frame output GIF that +looks like the superposition of the two inputs\*Euse +.B gifsicle +twice: +.Es +\fBgifsicle bottom.gif top.gif | gifsicle \-U "#1" > result.gif\fR +.Ee +' +.SH BUGS +' +Some optimized output GIFs may appear incorrectly on some GIF +implementations (for example, Java's); see the +.Op \-\-careful +option. +.PP +Please email suggestions, additions, patches and bugs to +ekohler@gmail.com. +' +.SH "SEE ALSO" +' +For a tutorial on GIF images and animations, you might try some of the +resources listed on-line at webreference\%.com: +.br +http://www.webreference.com/authoring/graphics/animation\|.html +' +.SH AUTHORS +.na +Eddie Kohler +.br +http://www.read.seas.harvard.edu/~kohler/ +.br +He wrote it. +.PP +Anne Dudfield +.br +http://www.frii.com/~annied/ +.br +She named it. +.PP +Hans Dinsen-Hansen +.br +http://www.danbbs.dk/~dino/ +.br +Adaptive tree method for GIF writing. +.PP +Kornel Lesin\*[']ski +.br +.Op \-\-lossy +option. +.PP +http://www.lcdf.org/gifsicle/ +.br +The +.B gifsicle +home page. +' diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/gifview.1 b/packages/optimizt-gifsicle/upstream/gifsicle/gifview.1 new file mode 100644 index 0000000..3b5bf57 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/gifview.1 @@ -0,0 +1,312 @@ +.\" -*- mode: nroff -*- +.ds V 1.96 +.ds E " \-\- +.if t .ds E \(em +.de Op +.BR "\\$1" "\\$2" "\\$3" "\\$4" "\\$5" "\\$6" +.. +.de Oa +.IR "\fB\\$1\& \|\fI\\$2" "\\$3" "\\$4" "\\$5" "\\$6" +.. +.de Qo +.RB ` "\\$1" "'\\$2" +.. +.de Sp +.if n .sp +.if t .sp 0.4 +.. +.de Es +.Sp +.RS 5 +.nf +.. +.de Ee +.fi +.RE +.PP +.. +.TH GIFVIEW 1 "11 July 2017" "Version \*V" +.SH NAME +gifview \- displays GIF images and animations on the X window system +.SH SYNOPSIS +.B gifview +\%[\fB--display\fP \fIdisplay\fP] +\%[options] +\%[filenames and frames].\|.\|. +' +.SH DESCRIPTION +.B gifview +displays GIF image files on workstations and terminals running the X Window +System. +.B gifview +understands multi-image GIFs, which can be displayed either as slideshows +or as animations. +' +.SH INTERACTION +.B gifview +windows recognize several keystrokes and button commands. Many of them are +only useful for multi-image GIFs. +.TP 12 +.RB "Space or " n +' +Go to the next frame. +.TP +.BR b " or " p +Go to the previous frame. +.TP +.BR r " or " < +Go to the first frame. +.TP +.BR > +Go to the last frame. +.TP +ESC +Stop the animation. +.TP +.BR s " or " a +Toggle between animation and slideshow mode. +.TP +.BR u +Toggle between normal and unoptimized mode. +.TP +Backspace +' +Delete this window. +.TP +.B q +Quit +.BR gifview . +' +.PP +Left-clicking on a window goes to the next frame; right-clicking on a +window deletes that window. +' +.SH COMMAND LINE + +.BR gifview 's +command line consists of +.IR "GIF input files" +and +.IR options . +Most options start with a dash (\-) or plus (+); frame selections, a kind +of option, start with a number sign (#). Anything else is a GIF input file. +.PP +.B gifview +displays one window for each GIF input file you specify. If no GIF input +file is given, or you give the special filename `\-', it reads from the +standard input. +' +.SH OPTIONS + +.PD 0 +.TP 5 +.Op \-\-animate ", " \-a +' +Animate multi-image GIFs by default. Normally, multi-image GIFs first +appear in slideshow mode. You can always use the +.RB ` a ' +keystroke to toggle between modes. This option has a converse, +.Qo \-\-no\-animate +or +.Qo +a . +' +.Sp +.TP 5 +.Op \-\-unoptimize ", " \-U +' +Display multi-image GIFs as ``unoptimized'', which shows a faithful +representation of what a user will see at each frame of an animation. See +.BR gifsicle (1) +for a more detailed description of unoptimization. This option has a +converse, +.Qo \-\-no\-unoptimize +or +.Qo +U . +GIFs are always displayed unoptimized in animation mode. +' +.Sp +.TP 5 +.Oa \-d display +.TP +.Oa \-\-display display +' +Sets the X display to +.IR display . +This option must come before any GIF files. +' +.Sp +.TP 5 +.Oa \-\-name name +' +Sets the application name under which resources are found, rather than +the default of "gifview". Since +.B gifview +itself does not use the resource database, this is mostly useful for +communication with your window manager. +' +.Sp +.TP 5 +.Oa \-\-geometry geometry +' +Set the size and position of +.BR gifview 's +windows. This is a standard X option. At most one +.Op \-\-geometry +option can be given per window (that is, per input GIF file). +' +.Sp +.TP 5 +.Oa \-\-title title +' +Sets the +.B gifview +window's title. The default is "gifview", followed by information about the +currently displayed file and frame. +' +.Sp +.TP 5 +.Oa \-w window +.TP +.Oa \-\-window window +' +Display the next GIF input in an existing X window, instead of making a new +top-level window. This way, you can use +.B gifview +to display animated GIFs in a window you created with another program. The +.I window +argument should be an integer +.RB ( gifview +will use that window ID) +or `root' +.RB ( gifview +will use the root window). +' +.Sp +.TP 5 +.Oa \-\-new\-window window +' +Display the next GIF input in a new child of an existing X window. This +child window will disappear when +.B gifview +exits. The +.I window +argument should be an integer +.RB ( gifview +will use that window ID) +or `root' +.RB ( gifview +will use the root window). +' +.Sp +.TP 5 +.Op \-\-install\-colormap ", " \-i +' +Use a private colormap for each window (if you are using a PseudoColor +display). This avoids polluting the existing colormap, and may produce +better results if your colormap is full, but causes annoying colormap +flashing. +' +.Sp +.TP 5 +.Oa \-\-background color +.TP +.Oa \-\-bg color +' +Set the background color, which is used +for transparent pixels. +' +.Sp +.TP 5 +.Oa \-\-min\-delay delay +' +Set the minimum delay between frames to +.IR delay , +which is measured in hundredths of a second. Default is 0. +' +.Sp +.TP 5 +.Oa \-\-fallback\-delay delay +' +Set the frame delay of GIFs that do not specify a delay value +or have a delay of 0. +The final value is still subject to the value of \-\-min\-delay. +Like \-\-min\-delay, +.IR delay +is measured in hundredths of a second. Default is 0. +' +.Sp +.TP 5 +.Op \-\-no\-interactive ", " \+e +' +Don't pay attention to mouse buttons or keystrokes. +' +.Sp +.TP 5 +.Op \-\-memory\-limit lim +' +Cache at most +.I lim +megabytes of images in memory when animating. Default is 40. +' +.Sp +.TP 5 +.Op \-\-help +' +Print usage information and exit. +' +.Sp +.TP +.Op \-\-version +' +Print the version number and some quickie warranty information and exit. +' +.PD +' +.\" ----------------------------------------------------------------- +.SS Frame Selections + +A frame selection tells +.B gifview +which frame to initially display from the current input file. They are +useful only for animations, as non-animated GIFs only have one frame. Frame +selections can only be displayed in slideshow mode. +.Sp +.PD 0 +.TP 13 +.BI # num +' +Select frame \fInum\fR. (The first frame is +.Qo #0 . +Negative numbers count backwards from the last frame, which is +.Qo #-1 .) +' +.TP 13 +.BI # name +' +Select the frame named \fIname\fR. +.PD +.PP +If you give two or more frame selections, you will get one window per frame +selection. +' +.SH SEE ALSO + +.BR gifsicle (1) +' +.SH BUGS + +Please email suggestions, additions, patches and bugs to +ekohler@gmail.com. +' +.SH AUTHORS +.na +Eddie Kohler, ekohler@gmail.com +.br +http://www.read.seas.harvard.edu/~kohler/ +.PP +http://www.lcdf.org/gifsicle/ +.br +The +.B gifsicle +home page. +' diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdf/clp.h b/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdf/clp.h new file mode 100644 index 0000000..72656e3 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdf/clp.h @@ -0,0 +1,337 @@ +/* -*- related-file-name: "../../liblcdf/clp.c" -*- */ +#ifndef LCDF_CLP_H +#define LCDF_CLP_H +#ifdef __cplusplus +extern "C" { +#endif + +/* clp.h - Public interface to CLP. + * This file is part of CLP, the command line parser package. + * + * Copyright (c) 1997-2025 Eddie Kohler, ekohler@gmail.com + * + * CLP is free software. It is distributed under the GNU General Public + * License, Version 2, or, alternatively and at your discretion, under the + * more permissive (BSD-like) Click LICENSE file as described below. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, subject to the + * conditions listed in the Click LICENSE file, which is available in full at + * http://github.com/kohler/click/blob/master/LICENSE. The conditions + * include: you must preserve this copyright notice, and you cannot mention + * the copyright holders in advertising related to the Software without + * their permission. The Software is provided WITHOUT ANY WARRANTY, EXPRESS + * OR IMPLIED. This notice is a summary of the Click LICENSE file; the + * license in that file is binding. */ + +#include +#include + +typedef struct Clp_Option Clp_Option; +typedef struct Clp_Parser Clp_Parser; +typedef struct Clp_ParserState Clp_ParserState; + + +/** @brief Option description. + * + * CLP users declare arrays of Clp_Option structures to specify what options + * should be parsed. + * @sa Clp_NewParser, Clp_SetOptions */ +struct Clp_Option { + const char *long_name; /**< Name of long option, or NULL if the option + has no long name. */ + int short_name; /**< Character defining short option, or 0 if + the option has no short name. */ + int option_id; /**< User-specified ID defining option, + returned by Clp_Next. */ + int val_type; /**< ID of option's value type, or 0 if option + takes no value. */ + int flags; /**< Option parsing flags. */ +}; + +/** @name Value types + * These values describe the type of an option's argument and are used in + * the Clp_Option val_type field. For example, if an option took integers, its + * Clp_Option structure would have val_type set to Clp_ValInt. */ +/**@{*/ +#define Clp_NoVal 0 /**< @brief Option takes no value. */ +#define Clp_ValString 1 /**< @brief Option value is an + arbitrary string. */ +#define Clp_ValStringNotOption 2 /**< @brief Option value is a + non-option string. + + See Clp_DisallowOptions. */ +#define Clp_ValBool 3 /**< @brief Option value is a + boolean. + + Accepts "true", "false", "yes", "no", "1", and "0", or any + prefixes thereof. The match is case-insensitive. */ +#define Clp_ValInt 4 /**< @brief Option value is a + signed int. + + Accepts an optional "+" or "-" sign, followed by one or more + digits. The digits may be include a "0x" or "0X" prefix, for + a hexadecimal number, or a "0" prefix, for an octal number; + otherwise it is decimal. */ +#define Clp_ValUnsigned 5 /**< @brief Option value is an + unsigned int. + + Accepts an optional "+" sign, followed by one or more + digits. The digits may be include a "0x" or "0X" prefix, for + a hexadecimal number, or a "0" prefix, for an octal number; + otherwise it is decimal. */ +#define Clp_ValLong 6 /**< @brief Option value is a + signed long. */ +#define Clp_ValUnsignedLong 7 /**< @brief Option value is an + unsigned long. */ +#define Clp_ValDouble 8 /**< @brief Option value is a + double. + Accepts a real number as defined by strtod(). */ +#define Clp_ValFirstUser 10 /**< @brief Value types >= + Clp_ValFirstUser are available + for user types. */ +/**@}*/ + +/** @name Option flags + * These flags are used in the Clp_Option flags field. */ +/**@{*/ +#define Clp_Mandatory (1<<0) /**< @brief Option flag: value + is mandatory. + + It is an error if the option has no value. This is the + default if an option has arg_type != 0 and the Clp_Optional + flag is not provided. */ +#define Clp_Optional (1<<1) /**< @brief Option flag: value + is optional. */ +#define Clp_Negate (1<<2) /**< @brief Option flag: option + may be negated. + + --no-[long_name] will be accepted in argument lists. */ +#define Clp_OnlyNegated (1<<3) /**< @brief Option flag: option + must be negated. + + --no-[long_name] will be accepted in argument lists, but + --[long_name] will not. This is the default if long_name + begins with "no-". */ +#define Clp_PreferredMatch (1<<4) /**< @brief Option flag: prefer this + option when matching. + + Prefixes of --[long_name] should map to this option, even if + other options begin with --[long_name]. */ +/**@}*/ + +/** @name Option character types + * These flags are used in to define character types in Clp_SetOptionChar(). */ +/**@{*/ +/* Clp_NotOption 0 */ +#define Clp_Short (1<<0) /**< @brief Option character begins + a set of short options. */ +#define Clp_Long (1<<1) /**< @brief Option character begins + a long option. */ +#define Clp_ShortNegated (1<<2) /**< @brief Option character begins + a set of negated short options. */ +#define Clp_LongNegated (1<<3) /**< @brief Option character begins + a negated long option. */ +#define Clp_LongImplicit (1<<4) /**< @brief Option character can begin + a long option, and is part of that + long option. */ +/**@}*/ + +#define Clp_NotOption 0 /**< @brief Clp_Next value: argument + was not an option. */ +#define Clp_Done -1 /**< @brief Clp_Next value: there are + no more arguments. */ +#define Clp_BadOption -2 /**< @brief Clp_Next value: argument + was an erroneous option. */ +#define Clp_Error -3 /**< @brief Clp_Next value: internal + CLP error. */ + +#define Clp_ValSize 40 /**< @brief Minimum size of the + Clp_Parser val.cs field. */ +#define Clp_ValIntSize 10 /**< @brief Minimum size of the + Clp_Parser val.is field. */ + + +/** @brief A value parsing function. + * @param clp the parser + * @param vstr the value to be parsed + * @param complain if nonzero, report error messages via Clp_OptionError + * @param user_data user data passed to Clp_AddType() + * @return 1 if parsing succeeded, 0 otherwise + */ +typedef int (*Clp_ValParseFunc)(Clp_Parser *clp, const char *vstr, + int complain, void *user_data); + +/** @brief A function for reporting option errors. + * @param clp the parser + * @param message error message + */ +typedef void (*Clp_ErrorHandler)(Clp_Parser *clp, const char *message); + + +/** @brief Command line parser. + * + * A Clp_Parser object defines an instance of CLP, including allowed options, + * value types, and current arguments. + * @sa Clp_NewParser, Clp_SetOptions, Clp_SetArguments */ +struct Clp_Parser { + const Clp_Option *option; /**< The last option. */ + + int negated; /**< Whether the last option was negated. */ + + int have_val; /**< Whether the last option had a value. */ + const char *vstr; /**< The string value provided with the last + option. */ + + union { + int i; + unsigned u; + long l; + unsigned long ul; + double d; + const char *s; + void *pv; +#ifdef HAVE_INT64_TYPES + int64_t i64; + uint64_t u64; +#endif + char cs[Clp_ValSize]; + unsigned char ucs[Clp_ValSize]; + int is[Clp_ValIntSize]; + unsigned us[Clp_ValIntSize]; + } val; /**< The parsed value provided with the last + option. */ + + void *user_data; /**< Uninterpreted by CLP; users can set + arbitrarily. */ + + struct Clp_Internal *internal; +}; + +/** @cond never */ +#if __GNUC__ >= 4 +# define CLP_SENTINEL __attribute__((sentinel)) +#else +# define CLP_SENTINEL /* nothing */ +#endif +/** @endcond never */ + + +/** @brief Create a new Clp_Parser. */ +Clp_Parser *Clp_NewParser(int argc, const char * const *argv, + int nopt, const Clp_Option *opt); + +/** @brief Destroy a Clp_Parser object. */ +void Clp_DeleteParser(Clp_Parser *clp); + + +/** @brief Return @a clp's program name. */ +const char *Clp_ProgramName(Clp_Parser *clp); + +/** @brief Set @a clp's program name. */ +const char *Clp_SetProgramName(Clp_Parser *clp, const char *name); + + +/** @brief Set @a clp's error handler function. */ +Clp_ErrorHandler Clp_SetErrorHandler(Clp_Parser *clp, Clp_ErrorHandler errh); + +/** @brief Set @a clp's UTF-8 mode. */ +int Clp_SetUTF8(Clp_Parser *clp, int utf8); + +/** @brief Return @a clp's treatment of character @a c. */ +int Clp_OptionChar(Clp_Parser *clp, int c); + +/** @brief Set @a clp's treatment of character @a c. */ +int Clp_SetOptionChar(Clp_Parser *clp, int c, int type); + +/** @brief Set @a clp's option definitions. */ +int Clp_SetOptions(Clp_Parser *clp, int nopt, const Clp_Option *opt); + +/** @brief Set @a clp's arguments. */ +void Clp_SetArguments(Clp_Parser *clp, int argc, const char * const *argv); + +/** @brief Set whether @a clp is searching for options. */ +int Clp_SetOptionProcessing(Clp_Parser *clp, int on); + + +#define Clp_DisallowOptions (1<<0) /**< @brief Value type flag: value + can't be an option string. + + See Clp_AddType(). */ + +/** @brief Define a new value type for @a clp. */ +int Clp_AddType(Clp_Parser *clp, int val_type, int flags, + Clp_ValParseFunc parser, void *user_data); + + +#define Clp_AllowNumbers (1<<0) /**< @brief String list flag: allow + explicit numbers. + + See Clp_AddStringListType() and Clp_AddStringListTypeVec(). */ +#define Clp_StringListLong (1<<1) /**< @brief String list flag: values + have long type. */ + +/** @brief Define a new string list value type for @a clp. */ +int Clp_AddStringListTypeVec(Clp_Parser *clp, int val_type, int flags, + int nstrs, const char * const *strs, + const int *vals); + +/** @brief Define a new string list value type for @a clp. */ +int Clp_AddStringListType(Clp_Parser *clp, int val_type, int flags, ...) + CLP_SENTINEL; + + +/** @brief Parse and return the next argument from @a clp. */ +int Clp_Next(Clp_Parser *clp); + +/** @brief Return the next argument from @a clp without option parsing. */ +const char *Clp_Shift(Clp_Parser *clp, int allow_options); + + +/** @brief Create a new Clp_ParserState. */ +Clp_ParserState *Clp_NewParserState(void); + +/** @brief Destroy a Clp_ParserState object. */ +void Clp_DeleteParserState(Clp_ParserState *state); + +/** @brief Save @a clp's current state in @a state. */ +void Clp_SaveParser(const Clp_Parser *clp, Clp_ParserState *state); + +/** @brief Restore parser state from @a state into @a clp. */ +void Clp_RestoreParser(Clp_Parser *clp, const Clp_ParserState *state); + + +/** @brief Report a parser error. */ +int Clp_OptionError(Clp_Parser *clp, const char *format, ...); + +/** @brief Format a message. */ +int Clp_vsnprintf(Clp_Parser* clp, char* str, size_t size, + const char* format, va_list val); + +/** @brief Print a message. */ +int Clp_fprintf(Clp_Parser* clp, FILE* f, const char* format, ...); + +/** @brief Print a message. */ +int Clp_vfprintf(Clp_Parser* clp, FILE* f, const char* format, va_list val); + +/** @brief Extract the current option as a string. */ +int Clp_CurOptionNameBuf(Clp_Parser *clp, char *buf, int len); + +/** @brief Extract the current option as a string. */ +const char *Clp_CurOptionName(Clp_Parser *clp); + +/** @brief Test if the current option had long name @a name. */ +int Clp_IsLong(Clp_Parser *clp, const char *long_name); + +/** @brief Test if the current option had short name @a name. */ +int Clp_IsShort(Clp_Parser *clp, int short_name); + +/** @brief Return a UTF-8 or normal single quote. */ +const char* Clp_QuoteChar(const Clp_Parser* clp, int isclose); + +#undef CLP_SENTINEL +#ifdef __cplusplus +} +#endif +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdf/inttypes.h b/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdf/inttypes.h new file mode 100644 index 0000000..e2e1b2f --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdf/inttypes.h @@ -0,0 +1,31 @@ +#ifndef LCDF_INTTYPES_H +#define LCDF_INTTYPES_H +/* Define known-width integer types. */ + +#ifdef HAVE_INTTYPES_H +# include +#elif defined(HAVE_SYS_TYPES_H) +# include +# ifdef HAVE_U_INT_TYPES +typedef u_int8_t uint8_t; +typedef u_int16_t uint16_t; +typedef u_int32_t uint32_t; +# endif +#elif defined(_WIN32) +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +typedef __int16 int16_t; +typedef unsigned __int16 uint16_t; +typedef __int32 int32_t; +typedef unsigned __int32 uint32_t; +#endif + +#ifndef HAVE_UINTPTR_T +# if SIZEOF_VOID_P == SIZEOF_UNSIGNED_LONG +typedef unsigned long uintptr_t; +# elif SIZEOF_VOID_P == SIZEOF_UNSIGNED_INT +typedef unsigned int uintptr_t; +# endif +#endif + +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdfgif/gif.h b/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdfgif/gif.h new file mode 100644 index 0000000..28b1c83 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdfgif/gif.h @@ -0,0 +1,320 @@ +/* gif.h - Interface to the LCDF GIF library. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of the LCDF GIF library. + + The GIF library is free software. It is distributed under the GNU General + Public License, version 2; you can copy, distribute, or alter it at will, + as long as this notice is kept intact and this source code is made + available. There is no warranty, express or implied. */ + +#ifndef LCDF_GIF_H /* -*- mode: c -*- */ +#define LCDF_GIF_H +#include +#include +#include +#ifdef __cplusplus +extern "C" { +#endif + +/* NOTE: You should define the types uint8_t, uint16_t and uint32_t before + including this file, probably by #including . */ + +#define GIF_MAJOR_VERSION 2 +#define GIF_MINOR_VERSION 0 +#define GIF_VERSION "2.0" + +typedef struct Gif_Stream Gif_Stream; +typedef struct Gif_Image Gif_Image; +typedef struct Gif_Colormap Gif_Colormap; +typedef struct Gif_Comment Gif_Comment; +typedef struct Gif_Extension Gif_Extension; +typedef struct Gif_Record Gif_Record; + +typedef uint16_t Gif_Code; +#define GIF_MAX_CODE_BITS 12 +#define GIF_MAX_CODE 0x1000 +#define GIF_MAX_BLOCK 255 + + +/** GIF_STREAM **/ + +struct Gif_Stream { + Gif_Image **images; + int nimages; + int imagescap; + + Gif_Colormap *global; + uint16_t background; /* 256 means no background */ + + uint16_t screen_width; + uint16_t screen_height; + long loopcount; /* -1 means no loop count */ + + Gif_Comment* end_comment; + Gif_Extension* end_extension_list; + + unsigned errors; + uint32_t user_flags; + + const char* landmark; + int refcount; +}; + +Gif_Stream * Gif_NewStream(void); +void Gif_DeleteStream(Gif_Stream *); + +Gif_Stream * Gif_CopyStreamSkeleton(Gif_Stream *); +Gif_Stream * Gif_CopyStreamImages(Gif_Stream *); + +#define Gif_ScreenWidth(gfs) ((gfs)->screen_width) +#define Gif_ScreenHeight(gfs) ((gfs)->screen_height) +#define Gif_ImageCount(gfs) ((gfs)->nimages) + +#define GIF_UNOPTIMIZE_SIMPLEST_DISPOSAL 1 +#define GIF_MAX_SCREEN_WIDTH 65535 +#define GIF_MAX_SCREEN_HEIGHT 65535 + +void Gif_CalculateScreenSize(Gif_Stream *, int force); +int Gif_Unoptimize(Gif_Stream *); +int Gif_FullUnoptimize(Gif_Stream *, int flags); + + +/** GIF_IMAGE **/ + +struct Gif_Image { + uint8_t **img; /* img[y][x] == image byte (x,y) */ + uint8_t *image_data; + + uint16_t width; + uint16_t height; + uint16_t left; + uint16_t top; + uint16_t delay; + uint8_t disposal; + uint8_t interlace; + + short transparent; /* -1 means no transparent index */ + Gif_Colormap *local; + + char* identifier; + Gif_Comment* comment; + Gif_Extension* extension_list; + + void (*free_image_data)(void *); + + uint32_t compressed_len; + uint32_t compressed_errors; + uint8_t* compressed; + void (*free_compressed)(void *); + + uint32_t user_flags; + void* user_data; + void (*free_user_data)(void *); + int refcount; + +}; + +#define GIF_DISPOSAL_NONE 0 +#define GIF_DISPOSAL_ASIS 1 +#define GIF_DISPOSAL_BACKGROUND 2 +#define GIF_DISPOSAL_PREVIOUS 3 + +Gif_Image * Gif_NewImage(void); +void Gif_DeleteImage(Gif_Image *gfi); + +int Gif_AddImage(Gif_Stream *gfs, Gif_Image *gfi); +void Gif_RemoveImage(Gif_Stream *gfs, int i); +Gif_Image * Gif_CopyImage(Gif_Image *gfi); +void Gif_MakeImageEmpty(Gif_Image* gfi); + +Gif_Image * Gif_GetImage(Gif_Stream *gfs, int i); +Gif_Image * Gif_GetNamedImage(Gif_Stream *gfs, const char *name); +int Gif_ImageNumber(Gif_Stream *gfs, Gif_Image *gfi); + +#define Gif_ImageWidth(gfi) ((gfi)->width) +#define Gif_ImageHeight(gfi) ((gfi)->height) +#define Gif_ImageDelay(gfi) ((gfi)->delay) +#define Gif_ImageUserData(gfi) ((gfi)->userdata) +#define Gif_SetImageUserData(gfi, v) ((gfi)->userdata = v) +int Gif_ImageColorBound(const Gif_Image* gfi); + +typedef void (*Gif_ReadErrorHandler)(Gif_Stream* gfs, + Gif_Image* gfi, + int is_error, + const char* error_text); + +typedef struct { + int flags; + int loss; + void *padding[7]; +} Gif_CompressInfo; + +#define Gif_UncompressImage(gfs, gfi) Gif_FullUncompressImage((gfs),(gfi),0) +int Gif_FullUncompressImage(Gif_Stream* gfs, Gif_Image* gfi, + Gif_ReadErrorHandler handler); +int Gif_CompressImage(Gif_Stream *gfs, Gif_Image *gfi); +int Gif_FullCompressImage(Gif_Stream *gfs, Gif_Image *gfi, + const Gif_CompressInfo *gcinfo); +void Gif_ReleaseUncompressedImage(Gif_Image *gfi); +void Gif_ReleaseCompressedImage(Gif_Image *gfi); +int Gif_SetUncompressedImage(Gif_Image *gfi, uint8_t *data, + void (*free_data)(void *), int data_interlaced); +int Gif_CreateUncompressedImage(Gif_Image* gfi, int data_interlaced); + +int Gif_ClipImage(Gif_Image *gfi, int l, int t, int w, int h); + +void Gif_InitCompressInfo(Gif_CompressInfo *gcinfo); + + +/** GIF_COLORMAP **/ + +typedef struct { + uint8_t haspixel; /* semantics assigned by user */ + uint8_t gfc_red; /* red component (0-255) */ + uint8_t gfc_green; /* green component (0-255) */ + uint8_t gfc_blue; /* blue component (0-255) */ + uint32_t pixel; /* semantics assigned by user */ +} Gif_Color; + + +struct Gif_Colormap { + int ncol; + int capacity; + uint32_t user_flags; + int refcount; + Gif_Color *col; +}; + +Gif_Colormap * Gif_NewColormap(void); +Gif_Colormap * Gif_NewFullColormap(int count, int capacity); +void Gif_DeleteColormap(Gif_Colormap *); + +Gif_Colormap * Gif_CopyColormap(Gif_Colormap *); + +int Gif_ColorEq(Gif_Color *, Gif_Color *); +#define GIF_COLOREQ(c1, c2) \ +((c1)->gfc_red==(c2)->gfc_red && (c1)->gfc_green==(c2)->gfc_green && \ + (c1)->gfc_blue==(c2)->gfc_blue) +#define GIF_SETCOLOR(c, r, g, b) \ +((c)->gfc_red = (r), (c)->gfc_green = (g), (c)->gfc_blue = (b)) + +int Gif_FindColor(Gif_Colormap *, Gif_Color *); +int Gif_AddColor(Gif_Colormap *, Gif_Color *, int look_from); + + +/** GIF_COMMENT **/ + +struct Gif_Comment { + char **str; + int *len; + int count; + int cap; +}; + +Gif_Comment * Gif_NewComment(void); +void Gif_DeleteComment(Gif_Comment *); +int Gif_AddCommentTake(Gif_Comment *, char *, int); +int Gif_AddComment(Gif_Comment *, const char *, int); + + +/** GIF_EXTENSION **/ + +struct Gif_Extension { + int kind; /* negative kinds are reserved */ + char* appname; + int applength; + uint8_t* data; + uint32_t length; + int packetized; + + Gif_Stream *stream; + Gif_Image *image; + Gif_Extension *next; + void (*free_data)(void *); +}; + + +Gif_Extension* Gif_NewExtension(int kind, const char* appname, int applength); +void Gif_DeleteExtension(Gif_Extension* gfex); +Gif_Extension* Gif_CopyExtension(Gif_Extension* gfex); +int Gif_AddExtension(Gif_Stream* gfs, Gif_Image* gfi, + Gif_Extension* gfex); + + +/** READING AND WRITING **/ + +struct Gif_Record { + const unsigned char *data; + uint32_t length; +}; + +#define GIF_READ_COMPRESSED 1 +#define GIF_READ_UNCOMPRESSED 2 +#define GIF_READ_CONST_RECORD 4 +#define GIF_READ_TRAILING_GARBAGE_OK 8 +#define GIF_WRITE_CAREFUL_MIN_CODE_SIZE 1 +#define GIF_WRITE_EAGER_CLEAR 2 +#define GIF_WRITE_OPTIMIZE 4 +#define GIF_WRITE_SHRINK 8 + +void Gif_SetErrorHandler(Gif_ReadErrorHandler handler); +Gif_Stream* Gif_ReadFile(FILE* f); +Gif_Stream* Gif_FullReadFile(FILE* f, int flags, const char* landmark, + Gif_ReadErrorHandler handler); +Gif_Stream* Gif_ReadRecord(const Gif_Record* record); +Gif_Stream* Gif_FullReadRecord(const Gif_Record* record, int flags, + const char* landmark, + Gif_ReadErrorHandler handler); +int Gif_WriteFile(Gif_Stream *gfs, FILE *f); +int Gif_FullWriteFile(Gif_Stream *gfs, + const Gif_CompressInfo *gcinfo, FILE *f); + +#define Gif_ReadFile(f) Gif_FullReadFile((f),GIF_READ_UNCOMPRESSED,0,0) +#define Gif_ReadRecord(r) Gif_FullReadRecord((r),GIF_READ_UNCOMPRESSED,0,0) +#define Gif_CompressImage(s, i) Gif_FullCompressImage((s),(i),0) +#define Gif_WriteFile(s, f) Gif_FullWriteFile((s),0,(f)) + +typedef struct Gif_Writer Gif_Writer; +Gif_Writer* Gif_IncrementalWriteFileInit(Gif_Stream* gfs, const Gif_CompressInfo* gcinfo, FILE *f); +int Gif_IncrementalWriteImage(Gif_Writer* grr, Gif_Stream* gfs, Gif_Image* gfi); +int Gif_IncrementalWriteComplete(Gif_Writer* grr, Gif_Stream* gfs); + + +/** HOOKS AND MISCELLANEOUS **/ + +int Gif_InterlaceLine(int y, int height); +char * Gif_CopyString(const char *); + +#define GIF_T_STREAM (0) +#define GIF_T_IMAGE (1) +#define GIF_T_COLORMAP (2) +typedef void (*Gif_DeletionHookFunc)(int, void *, void *); +int Gif_AddDeletionHook(int, Gif_DeletionHookFunc, void *); +void Gif_RemoveDeletionHook(int, Gif_DeletionHookFunc, void *); + +#ifdef GIF_DEBUGGING +#define GIF_DEBUG(x) Gif_Debug x +void Gif_Debug(char *x, ...); +#else +#define GIF_DEBUG(x) +#endif + +void* Gif_Realloc(void* p, size_t s, size_t n, + const char* file, int line); +void Gif_Free(void* p); +#if !GIF_ALLOCATOR_DEFINED +# define Gif_Free free +#endif + +#ifndef Gif_New +# define Gif_New(t) ((t*) Gif_Realloc(0, sizeof(t), 1, __FILE__, __LINE__)) +# define Gif_NewArray(t, n) ((t*) Gif_Realloc(0, sizeof(t), (n), __FILE__, __LINE__)) +# define Gif_ReArray(p, t, n) ((p)=(t*) Gif_Realloc((void*) (p), sizeof(t), (n), __FILE__, __LINE__)) +# define Gif_Delete(p) Gif_Free((void*) (p)) +# define Gif_DeleteArray(p) Gif_Free((void*) (p)) +#endif + +#ifdef __cplusplus +} +#endif +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdfgif/gifx.h b/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdfgif/gifx.h new file mode 100644 index 0000000..698df94 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/include/lcdfgif/gifx.h @@ -0,0 +1,88 @@ +#ifndef LCDF_GIFX_H +#define LCDF_GIFX_H +#include +#ifdef __cplusplus +extern "C" { +#endif + +/* gifx.h - Functions to turn GIFs in memory into X Pixmaps. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of the LCDF GIF library. + + The LCDF GIF library is free software*. It is distributed under the GNU + General Public License, version 2; you can copy, distribute, or + alter it at will, as long as this notice is kept intact and this source + code is made available. There is no warranty, express or implied. */ + +#include + +#define GIFX_COLORMAP_EXTENSION -107 + + +typedef struct Gif_XContext Gif_XContext; +typedef struct Gif_XColormap Gif_XColormap; +typedef struct Gif_XFrame Gif_XFrame; + +struct Gif_XContext { + Display *display; + int screen_number; + Drawable drawable; + Visual *visual; + uint16_t depth; + uint16_t ncolormap; + Colormap colormap; + + uint16_t nclosest; + Gif_Color *closest; + + int free_deleted_colormap_pixels; + Gif_XColormap *xcolormap; + + GC image_gc; + GC mask_gc; + + unsigned long transparent_pixel; + unsigned long foreground_pixel; + int refcount; +}; + +struct Gif_XFrame { + Pixmap pixmap; + int postdisposal; + int user_data; +}; + +Gif_XContext * Gif_NewXContext(Display *display, Window window); +Gif_XContext * Gif_NewXContextFromVisual(Display *display, int screen_number, + Visual *visual, int depth, Colormap cmap); +void Gif_DeleteXContext(Gif_XContext *gfx); + +Pixmap Gif_XImage(Gif_XContext *gfx, Gif_Stream *gfs, Gif_Image *gfi); +Pixmap Gif_XImageColormap(Gif_XContext *gfx, Gif_Stream *gfs, + Gif_Colormap *gfcm, Gif_Image *gfi); +Pixmap Gif_XSubImage(Gif_XContext *gfx, Gif_Stream *gfs, + Gif_Image *gfi, int l, int t, int w, int h); +Pixmap Gif_XSubImageColormap(Gif_XContext *gfx, + Gif_Stream* gfs, Gif_Image *gfi, + Gif_Colormap* gfcm, int l, int t, int w, int h); + +Pixmap Gif_XMask(Gif_XContext *gfx, Gif_Stream *gfs, Gif_Image *gfi); +Pixmap Gif_XSubMask(Gif_XContext* gfx, Gif_Stream* gfs, Gif_Image* gfi, + int l, int t, int w, int h); + +Gif_XFrame * Gif_NewXFrames(Gif_Stream *gfs); +void Gif_DeleteXFrames(Gif_XContext *gfx, Gif_Stream *gfs, + Gif_XFrame *frames); +Pixmap Gif_XNextImage(Gif_XContext *gfx, Gif_Stream *gfs, int i, + Gif_XFrame *frames); + +int Gif_XAllocateColors(Gif_XContext *gfx, Gif_Colormap *gfcm); +void Gif_XDeallocateColors(Gif_XContext *gfx, Gif_Colormap *gfcm); +unsigned long * Gif_XClaimStreamColors(Gif_XContext *gfx, Gif_Stream *gfs, + int *np_store); + + +#ifdef __cplusplus +} +#endif +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/logo.gif b/packages/optimizt-gifsicle/upstream/gifsicle/logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..6e43e6bada4ea6d3fce32e604d113d688f26f719 GIT binary patch literal 8703 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 literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/logo1.gif b/packages/optimizt-gifsicle/upstream/gifsicle/logo1.gif new file mode 100644 index 0000000000000000000000000000000000000000..228b102de7a0bcf0199fb096d668a867b4c0ed48 GIT binary patch literal 821 zcmZ?wbhEHbG+=OH_{zY*kZs7pz@b=dX;^G&S#9aWAQ4n;8C30*ETNg*;#A$@)WTpm zrN?V6gVWY2UdI@`o^u2}ck+5374&>g)cc-e@3&t2&Jp!pAo;sQ_IJhX@0P{iom##J zP5E9u<@=nV?@NIw`TN?I|1E3(Z*BR1Y|8(0J^#-w{eNxi|9j{D|NjrPpF*JclZBCi zL6boTWDdwr3~c`oOevVLQKJ9ElA@C~aTd-=#a_8~i#2^qPj8KJiSQ}Eb0i^He3MjV zZ{DTXU&W())}M{oF?Y_vecw~WrAitaWh+~gE1adOIzzjilKNws11Ixxu+E;%&CSa{ zrPx_EbN)hi0e)UyZdML%frg2irJF(o*3I9ZRXaUvEh8&$TVmNv-aVn>A`wy5YdMZg zNUv`Y;APzyJS|XZa&)QB&O<9a7uWT9OTKyg$eaJxGq){)y?2D<>g={dyMyi1wO_*M0_l*WnNqH0!~XQ(W6Yn6F;a+SJ~>rw6k zzpR`SEeYN_vKBuk&n)zw{eQNw^M@o3;r#5Y6J2ShCfX4(uKqmm92lB==J#_uE0s|x8is=U!e@Up% z&F5rrXkcQuSngkPcjAXzj9(Alcvv@S>kpac-<%6)xyjF&utcE8@WqqKUTohOIf8>$ OKAjd*_A^3&!5RR%{7QTP literal 0 HcmV?d00001 diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.am b/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.am new file mode 100644 index 0000000..0fe8dfb --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.am @@ -0,0 +1,35 @@ +## Process this file with automake to produce Makefile.in +AUTOMAKE_OPTIONS = foreign check-news + +bin_PROGRAMS = gifsicle @OTHERPROGRAMS@ +EXTRA_PROGRAMS = gifview gifdiff + +LDADD = @LIBOBJS@ +gifsicle_LDADD = $(LDADD) @GIFWRITE_O@ +gifview_LDADD = $(LDADD) @X_LIBS@ @X_PRE_LIBS@ -lX11 @X_EXTRA_LIBS@ + +gifsicle_DEPENDENCIES = @GIFWRITE_O@ @LIBOBJS@ +gifview_DEPENDENCIES = @LIBOBJS@ +gifdiff_DEPENDENCIES = @LIBOBJS@ + +WERROR = @WERROR@ + +gifsicle_SOURCES = clp.c fmalloc.c \ + giffunc.c gifread.c gifunopt.c \ + gifsicle.h kcolor.h \ + kcolor.c merge.c optimize.c quantize.c support.c xform.c \ + gifsicle.c + +gifview_SOURCES = clp.c fmalloc.c \ + giffunc.c gifread.c gifx.c \ + gifview.c + +gifdiff_SOURCES = clp.c fmalloc.c \ + giffunc.c gifread.c \ + gifdiff.c + +AM_CPPFLAGS = $(X_CFLAGS) $(WERROR) -I$(top_srcdir)/include + +EXTRA_gifsicle_SOURCES = gifwrite.c ungifwrt.c + +EXTRA_DIST = opttemplate.c Makefile.bcc Makefile.w32 win32cfg.h diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.bcc b/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.bcc new file mode 100644 index 0000000..0b304d5 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.bcc @@ -0,0 +1,80 @@ +# Win32 Makefile originally by Emil Mikulic +# Updates by Eddie Kohler and +# Steven Marthouse + +# ---------------------------------------------------------- +# Makefile adapted for Borland C++ 5.5 Compiler +# Stephen Schnipsel +# +# set this to your bcc directory! +MYBCCDIR = D:\BCC55 +# +# ---------------------------------------------------------- + + +# *** MAKING UNGIFSICLE *** +# If `GIFWRITE_OBJ' is defined to `gifwrite.obj', Gifsicle will use +# Unisys-patented LZW compression. If it is defined to `ungifwrt.obj', it +# will use unpatented run-length compression, which creates larger GIFs but +# is completely free software. If you downloaded the ungifsicle package, +# which doesn't have `gifwrite.c', you MUST define `GIFWRITE_OBJ' to +# `ungifwrt.obj' by commenting the first line below and uncommenting the +# second. +GIFWRITE_OBJ = gifwrite.obj +#GIFWRITE_OBJ = ungifwrt.obj + +# *** SUPPORTING WILDCARD EXPANSION *** +# Define `SETARGV_OBJ' to the filename for the `setargv.obj' object file. +# The definition included here works for Microsoft compilers; you will +# probably need to change it if you're using a different compiler. You can +# define it to the empty string, in which case Gifsicle will compile fine, +# but you won't be able to use wildcards in file name arguments. +SETARGV_OBJ = $(MYBCCDIR)\LIB\WILDARGS.OBJ + + +CC = bcc32 +CFLAGS = -I.. -I..\INCLUDE -DHAVE_CONFIG_H=1 -D_CONSOLE -O2 -D_setmode=setmode + +GIFSICLE_OBJS = clp.obj fmalloc.obj giffunc.obj gifread.obj gifunopt.obj \ + $(GIFWRITE_OBJ) merge.obj optimize.obj quantize.obj support.obj \ + xform.obj gifsicle.obj $(SETARGV_OBJ) + +GIFDIFF_OBJS = clp.obj fmalloc.obj giffunc.obj gifread.obj gifdiff.obj \ + $(SETARGV_OBJ) + +.c.obj: + $(CC) $(CFLAGS) -c $< + +all: gifsicle.exe gifdiff.exe + +gifsicle.exe: $(GIFSICLE_OBJS) + $(CC) $(CFLAGS) -egifsicle $(GIFSICLE_OBJS) + +gifdiff.exe: $(GIFDIFF_OBJS) + $(CC) $(CFLAGS) -egifdiff $(GIFDIFF_OBJS) + +clp.obj: ..\config.h ..\include\lcdf\clp.h clp.c + +fmalloc.obj: ..\config.h fmalloc.c + +giffunc.obj: ..\config.h giffunc.c ..\include\lcdfgif\gif.h +gifread.obj: ..\config.h gifread.c ..\include\lcdfgif\gif.h +gifwrite.obj: ..\config.h gifwrite.c ..\include\lcdfgif\gif.h +ungifwrt.obj: ..\config.h ungifwrt.c ..\include\lcdfgif\gif.h +gifunopt.obj: ..\config.h gifunopt.c ..\include\lcdfgif\gif.h + +merge.obj: ..\config.h gifsicle.h merge.c +optimize.obj: ..\config.h gifsicle.h optimize.c +quantize.obj: ..\config.h gifsicle.h quantize.c +support.obj: ..\config.h gifsicle.h support.c +xform.obj: ..\config.h gifsicle.h xform.c +gifsicle.obj: ..\config.h gifsicle.h gifsicle.c + +..\config.h: win32cfg.h + copy win32cfg.h ..\config.h + +clean: + del *.obj + del *.exe + del *.tds + diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.mingw b/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.mingw new file mode 100644 index 0000000..ee23242 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.mingw @@ -0,0 +1,66 @@ +# Win32 Makefile originally by Emil Mikulic +# Updates by Eddie Kohler and +# Steven Marthouse + +# ---------------------------------------------------------- +# Makefile adapted for MinGW GCC Compiler +# José Manuel Muñoz +# ---------------------------------------------------------- + + +# *** MAKING UNGIFSICLE *** +# If `GIFWRITE_OBJ' is defined to `gifwrite.o', Gifsicle will use +# Unisys-patented LZW compression. If it is defined to `ungifwrt.o', it +# will use unpatented run-length compression, which creates larger GIFs but +# is completely free software. If you downloaded the ungifsicle package, +# which doesn't have `gifwrite.c', you MUST define `GIFWRITE_OBJ' to +# `ungifwrt.o' by commenting the first line below and uncommenting the +# second. +GIFWRITE_OBJ = gifwrite.o +#GIFWRITE_OBJ = ungifwrt.o + +# *** SUPPORTING WILDCARD EXPANSION *** +# MinGW seems to include support for this by itself. + +CC = gcc +MINGWFLAGS = -DHAVE_UINTPTR_T -DHAVE_INTTYPES_H +CFLAGS = -I.. -I..\include -DHAVE_CONFIG_H=1 -D_CONSOLE -O2 $(MINGWFLAGS) + +GIFSICLE_OBJS = clp.o fmalloc.o giffunc.o gifread.o gifunopt.o \ + $(GIFWRITE_OBJ) merge.o optimize.o quantize.o support.o \ + xform.o gifsicle.o + +GIFDIFF_OBJS = clp.o fmalloc.o giffunc.o gifread.o gifdiff.o + + +all: gifsicle.exe gifdiff.exe + +gifsicle.exe: $(GIFSICLE_OBJS) + $(CC) $(CFLAGS) -o $@ $(GIFSICLE_OBJS) + +gifdiff.exe: $(GIFDIFF_OBJS) + $(CC) $(CFLAGS) -o $@ $(GIFDIFF_OBJS) + +clp.o: ..\config.h ..\include\lcdf\clp.h clp.c + +fmalloc.o: ..\config.h fmalloc.c + +giffunc.o: ..\config.h giffunc.c ..\include\lcdfgif\gif.h +gifread.o: ..\config.h gifread.c ..\include\lcdfgif\gif.h +gifwrite.o: ..\config.h gifwrite.c ..\include\lcdfgif\gif.h +ungifwrt.o: ..\config.h ungifwrt.c ..\include\lcdfgif\gif.h +gifunopt.o: ..\config.h gifunopt.c ..\include\lcdfgif\gif.h + +merge.o: ..\config.h gifsicle.h merge.c +optimize.o: ..\config.h gifsicle.h optimize.c +quantize.o: ..\config.h gifsicle.h quantize.c +support.o: ..\config.h gifsicle.h support.c +xform.o: ..\config.h gifsicle.h xform.c +gifsicle.o: ..\config.h gifsicle.h gifsicle.c + +..\config.h: win32cfg.h + copy win32cfg.h ..\config.h + +clean: + del *.o + del *.exe diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.w32 b/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.w32 new file mode 100644 index 0000000..6cdf0ed --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/Makefile.w32 @@ -0,0 +1,62 @@ +# Win32 Makefile originally by Emil Mikulic +# Updates by Eddie Kohler and +# Steven Marthouse + +# This makefile should work under Win32 (95/98/NT/whatever) or Win64. +# It should work out-of-the-box with Visual C++ 5. +# +# C:\GIFSICLE> nmake -f Makefile.w32 + +# *** MAKING UNGIFSICLE *** +# If `GIFWRITE_OBJ' is defined to `gifwrite.obj', Gifsicle will use +# Unisys-patented LZW compression. If it is defined to `ungifwrt.obj', it +# will use unpatented run-length compression, which creates larger GIFs but +# is completely free software. If you downloaded the ungifsicle package, +# which doesn't have `gifwrite.c', you MUST define `GIFWRITE_OBJ' to +# `ungifwrt.obj' by commenting the first line below and uncommenting the +# second. +GIFWRITE_OBJ = gifwrite.obj +#GIFWRITE_OBJ = ungifwrt.obj + +CC = cl +CFLAGS = -I.. -I..\include -DHAVE_CONFIG_H -D_CONSOLE /W2 /O2 +LDLIBS = setargv.obj + +GIFSICLE_OBJS = clp.obj fmalloc.obj giffunc.obj gifread.obj gifunopt.obj \ + $(GIFWRITE_OBJ) merge.obj optimize.obj quantize.obj support.obj \ + xform.obj gifsicle.obj + +GIFDIFF_OBJS = clp.obj fmalloc.obj giffunc.obj gifread.obj gifdiff.obj + +.c.obj: + $(CC) $(CFLAGS) /c $< + +gifsicle.exe: $(GIFSICLE_OBJS) + $(CC) $(CFLAGS) /Fegifsicle.exe $(GIFSICLE_OBJS) /link $(LDLIBS) + +gifdiff.exe: $(GIFDIFF_OBJS) + $(CC) $(CFLAGS) /Fegifdiff.exe $(GIFDIFF_OBJS) /link $(LDLIBS) + +clp.obj: ..\config.h ..\include\lcdf\clp.h clp.c + +fmalloc.obj: ..\config.h fmalloc.c + +giffunc.obj: ..\config.h giffunc.c ..\include\lcdfgif\gif.h +gifread.obj: ..\config.h gifread.c ..\include\lcdfgif\gif.h +gifwrite.obj: ..\config.h gifwrite.c ..\include\lcdfgif\gif.h +ungifwrt.obj: ..\config.h ungifwrt.c ..\include\lcdfgif\gif.h +gifunopt.obj: ..\config.h gifunopt.c ..\include\lcdfgif\gif.h + +merge.obj: ..\config.h gifsicle.h merge.c +optimize.obj: ..\config.h gifsicle.h optimize.c +quantize.obj: ..\config.h gifsicle.h quantize.c +support.obj: ..\config.h gifsicle.h support.c +xform.obj: ..\config.h gifsicle.h xform.c +gifsicle.obj: ..\config.h gifsicle.h gifsicle.c + +..\config.h: win32cfg.h + copy win32cfg.h ..\config.h + +clean: + del *.obj + del *.exe diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/clp.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/clp.c new file mode 100644 index 0000000..59ef363 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/clp.c @@ -0,0 +1,2512 @@ +/* -*- related-file-name: "../include/lcdf/clp.h" -*- */ +/* clp.c - Complete source code for CLP. + * This file is part of CLP, the command line parser package. + * + * Copyright (c) 1997-2025 Eddie Kohler, ekohler@gmail.com + * + * CLP is free software. It is distributed under the GNU General Public + * License, Version 2, or, alternatively and at your discretion, under the + * more permissive (BSD-like) Click LICENSE file as described below. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, subject to the + * conditions listed in the Click LICENSE file, which is available in full at + * http://github.com/kohler/click/blob/master/LICENSE. The conditions + * include: you must preserve this copyright notice, and you cannot mention + * the copyright holders in advertising related to the Software without + * their permission. The Software is provided WITHOUT ANY WARRANTY, EXPRESS + * OR IMPLIED. This notice is a summary of the Click LICENSE file; the + * license in that file is binding. */ + +#if HAVE_CONFIG_H +# include +#endif +#include +#include +#include +#include +#include +#include +#include +#if HAVE_SYS_TYPES_H +# include +#endif +#if HAVE_INTTYPES_H || !defined(HAVE_CONFIG_H) +# include +#endif + +/* By default, assume we have inttypes.h, strtoul, and uintptr_t. */ +#if !defined(HAVE_STRTOUL) && !defined(HAVE_CONFIG_H) +# define HAVE_STRTOUL 1 +#endif +#if defined(HAVE_INTTYPES_H) || !defined(HAVE_CONFIG_H) +# include +#endif +#if !defined(HAVE_UINTPTR_T) && defined(HAVE_CONFIG_H) +typedef unsigned long uintptr_t; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + + +/** @file clp.h + * @brief Functions for parsing command line options. + * + * The CLP functions are used to parse command line arguments into options. + * It automatically handles value parsing, error messages, long options with + * minimum prefix matching, short options, and negated options. + * + * The CLP model works like this. + * + *

    + *
  1. The user declares an array of Clp_Option structures that define the + * options their program accepts.
  2. + *
  3. The user creates a Clp_Parser object using Clp_NewParser(), passing in + * the command line arguments to parse and the Clp_Option structures.
  4. + *
  5. A loop repeatedly calls Clp_Next() to parse the arguments.
  6. + *
+ * + * Unlike many command line parsing libraries, CLP steps through all arguments + * one at a time, rather than slurping up all options at once. This makes it + * meaningful to give an option more than once. + * + * Here's an example. + * + * @code + * #define ANIMAL_OPT 1 + * #define VEGETABLE_OPT 2 + * #define MINERALS_OPT 3 + * #define USAGE_OPT 4 + * + * static const Clp_Option options[] = { + * { "animal", 'a', ANIMAL_OPT, Clp_ValString, 0 }, + * { "vegetable", 'v', VEGETABLE_OPT, Clp_ValString, Clp_Negate | Clp_Optional }, + * { "minerals", 'm', MINERALS_OPT, Clp_ValInt, 0 }, + * { "usage", 0, USAGE_OPT, 0, 0 } + * }; + * + * int main(int argc, char *argv[]) { + * Clp_Parser *clp = Clp_NewParser(argc, argv, + * sizeof(options) / sizeof(options[0]), options); + * int opt; + * while ((opt = Clp_Next(clp)) != Clp_Done) + * switch (opt) { + * case ANIMAL_OPT: + * fprintf(stderr, "animal is %s\n", clp->val.s); + * break; + * case VEGETABLE_OPT: + * if (clp->negated) + * fprintf(stderr, "no vegetables!\n"); + * else if (clp->have_val) + * fprintf(stderr, "vegetable is %s\n", clp->val.s); + * else + * fprintf(stderr, "vegetables OK\n"); + * break; + * case MINERALS_OPT: + * fprintf(stderr, "%d minerals\n", clp->val.i); + * break; + * case USAGE_OPT: + * fprintf(stderr, "Usage: 20q [--animal=ANIMAL] [--vegetable[=VEGETABLE]] [--minerals=N]\n"); + * break; + * case Clp_NotOption: + * fprintf(stderr, "non-option %s\n", clp->vstr); + * break; + * } + * } + * } + * @endcode + * + * Here are a couple of executions. + * + *
+ * % ./20q --animal=cat
+ * animal is cat
+ * % ./20q --animal=cat -a dog -afish --animal bird --an=snake
+ * animal is cat
+ * animal is dog
+ * animal is fish
+ * animal is bird
+ * animal is snake
+ * % ./20q --no-vegetables
+ * no vegetables!
+ * % ./20q -v
+ * vegetables OK
+ * % ./20q -vkale
+ * vegetable is kale
+ * % ./20q -m10
+ * 10 minerals
+ * % ./20q -m foo
+ * '-m' expects an integer, not 'foo'
+ * 
+ */ + + +/* Option types for Clp_SetOptionChar */ +#define Clp_DoubledLong (Clp_LongImplicit * 2) + +#define Clp_InitialValType 8 +#define MAX_AMBIGUOUS_VALUES 4 + +typedef struct { + int val_type; + Clp_ValParseFunc func; + int flags; + void *user_data; +} Clp_ValType; + +typedef struct { + unsigned ilong : 1; + unsigned ishort : 1; + unsigned imandatory : 1; + unsigned ioptional : 1; + unsigned ipos : 1; + unsigned ineg : 1; + unsigned iprefmatch : 1; + unsigned lmmpos_short : 1; + unsigned lmmneg_short : 1; + unsigned unquoted : 1; + unsigned char ilongoff; + int lmmpos; + int lmmneg; +} Clp_InternOption; + + +#define Clp_OptionCharsSize 5 + +typedef struct { + int c; + int type; +} Clp_Oclass; +#define Clp_OclassSize 10 + +typedef struct Clp_Internal { + const Clp_Option *opt; + Clp_InternOption *iopt; + int nopt; + unsigned opt_generation; + + Clp_ValType *valtype; + int nvaltype; + + const char * const *argv; + int argc; + + Clp_Oclass oclass[Clp_OclassSize]; + int noclass; + int long1pos; + int long1neg; + int utf8; + + char option_chars[Clp_OptionCharsSize]; + const char *xtext; + + const char *program_name; + void (*error_handler)(Clp_Parser *, const char *); + + int option_processing; + int current_option; + + unsigned char is_short; + unsigned char whole_negated; /* true if negated by an option character */ + unsigned char could_be_short; + unsigned char current_short; + unsigned char negated_by_no; + + int ambiguous; + int ambiguous_values[MAX_AMBIGUOUS_VALUES]; +} Clp_Internal; + + +struct Clp_ParserState { + const char * const *argv; + int argc; + + char option_chars[Clp_OptionCharsSize]; + const char *xtext; + + int option_processing; + + unsigned opt_generation; + int current_option; + unsigned char is_short; + unsigned char whole_negated; + unsigned char current_short; + unsigned char negated_by_no; +}; + + +typedef struct Clp_StringList { + Clp_Option *items; + Clp_InternOption *iopt; + int nitems; + + unsigned char allow_int; + unsigned char val_long; + int nitems_invalid_report; +} Clp_StringList; + + +static const Clp_Option clp_option_sentinel[] = { + {"", 0, Clp_NotOption, 0, 0}, + {"", 0, Clp_Done, 0, 0}, + {"", 0, Clp_BadOption, 0, 0}, + {"", 0, Clp_Error, 0, 0} +}; + + +static int parse_string(Clp_Parser *, const char *, int, void *); +static int parse_int(Clp_Parser *, const char *, int, void *); +static int parse_bool(Clp_Parser *, const char *, int, void *); +static int parse_double(Clp_Parser *, const char *, int, void *); +static int parse_string_list(Clp_Parser *, const char *, int, void *); + +static int ambiguity_error(Clp_Parser *, int, int *, const Clp_Option *, + const Clp_InternOption *, const char *, const char *, + ...); + + +/******* + * utf8 + **/ + +#define U_REPLACEMENT 0xFFFD + +static char * +encode_utf8(char *s, int n, int c) +{ + if (c < 0 || c >= 0x110000 || (c >= 0xD800 && c <= 0xDFFF)) + c = U_REPLACEMENT; + if (c <= 0x7F && n >= 1) + *s++ = c; + else if (c <= 0x7FF && n >= 2) { + *s++ = 0xC0 | (c >> 6); + goto char1; + } else if (c <= 0xFFFF && n >= 3) { + *s++ = 0xE0 | (c >> 12); + goto char2; + } else if (n >= 4) { + *s++ = 0xF0 | (c >> 18); + *s++ = 0x80 | ((c >> 12) & 0x3F); + char2: + *s++ = 0x80 | ((c >> 6) & 0x3F); + char1: + *s++ = 0x80 | (c & 0x3F); + } + return s; +} + +static int +decode_utf8(const char *s, const char **cp) +{ + int c; + if ((unsigned char) *s <= 0x7F) /* 1 byte: 0x000000-0x00007F */ + c = *s++; + else if ((unsigned char) *s <= 0xC1) /* bad/overlong encoding */ + goto replacement; + else if ((unsigned char) *s <= 0xDF) { /* 2 bytes: 0x000080-0x0007FF */ + if ((s[1] & 0xC0) != 0x80) /* bad encoding */ + goto replacement; + c = (*s++ & 0x1F) << 6; + goto char1; + } else if ((unsigned char) *s <= 0xEF) { /* 3 bytes: 0x000800-0x00FFFF */ + if ((s[1] & 0xC0) != 0x80 /* bad encoding */ + || (s[2] & 0xC0) != 0x80 /* bad encoding */ + || ((unsigned char) *s == 0xE0 /* overlong encoding */ + && (s[1] & 0xE0) == 0x80) + || ((unsigned char) *s == 0xED /* encoded surrogate */ + && (s[1] & 0xE0) == 0xA0)) + goto replacement; + c = (*s++ & 0x0F) << 12; + goto char2; + } else if ((unsigned char) *s <= 0xF4) { /* 4 bytes: 0x010000-0x10FFFF */ + if ((s[1] & 0xC0) != 0x80 /* bad encoding */ + || (s[2] & 0xC0) != 0x80 /* bad encoding */ + || (s[3] & 0xC0) != 0x80 /* bad encoding */ + || ((unsigned char) *s == 0xF0 /* overlong encoding */ + && (s[1] & 0xF0) == 0x80) + || ((unsigned char) *s == 0xF4 /* encoded value > 0x10FFFF */ + && (unsigned char) s[1] >= 0x90)) + goto replacement; + c = (*s++ & 0x07) << 18; + c += (*s++ & 0x3F) << 12; + char2: + c += (*s++ & 0x3F) << 6; + char1: + c += (*s++ & 0x3F); + } else { + replacement: + c = U_REPLACEMENT; + for (s++; (*s & 0xC0) == 0x80; s++) + /* nothing */; + } + if (cp) + *cp = s; + return c; +} + +static int +utf8_charlen(const char *s) +{ + const char *sout; + (void) decode_utf8(s, &sout); + return sout - s; +} + +static int +clp_utf8_charlen(const Clp_Internal *cli, const char *s) +{ + return (cli->utf8 ? utf8_charlen(s) : 1); +} + + +/******* + * Clp_NewParser, etc. + **/ + +static int +min_different_chars(const char *s, const char *t) + /* Returns the minimum number of bytes required to distinguish + s from t. + If s is shorter than t, returns strlen(s). */ +{ + const char *sfirst = s; + while (*s && *t && *s == *t) + s++, t++; + if (!*s) + return s - sfirst; + else + return s - sfirst + 1; +} + +static int +long_as_short(const Clp_Internal *cli, const Clp_Option *o, + Clp_InternOption *io, int failure) +{ + if ((cli->long1pos || cli->long1neg) && io->ilong) { + const char *name = o->long_name + io->ilongoff; + if (cli->utf8) { + int c = decode_utf8(name, &name); + if (!*name && c && c != U_REPLACEMENT) + return c; + } else if (name[0] && !name[1]) + return (unsigned char) name[0]; + } + return failure; +} + +static void +compare_options(Clp_Parser *clp, const Clp_Option *o1, Clp_InternOption *io1, + const Clp_Option *o2, Clp_InternOption *io2) +{ + Clp_Internal *cli = clp->internal; + int short1, shortx1; + + /* ignore meaningless combinations */ + if ((!io1->ishort && !io1->ilong) || (!io2->ishort && !io2->ilong) + || !((io1->ipos && io2->ipos) || (io1->ineg && io2->ineg)) + || o1->option_id == o2->option_id) + return; + + /* look for duplication of short options */ + short1 = (io1->ishort ? o1->short_name : -1); + shortx1 = long_as_short(cli, o1, io1, -2); + if (short1 >= 0 || shortx1 >= 0) { + int short2 = (io2->ishort ? o2->short_name : -3); + int shortx2 = long_as_short(cli, o2, io2, -4); + if (short1 == short2) + Clp_OptionError(clp, "CLP internal error: more than 1 option has short name %<%c%>", short1); + else if ((short1 == shortx2 || shortx1 == short2 || shortx1 == shortx2) + && ((io1->ipos && io2->ipos && cli->long1pos) + || (io1->ineg && io2->ineg && cli->long1neg))) + Clp_OptionError(clp, "CLP internal error: 1-char long name conflicts with short name %<%c%>", (short1 == shortx2 ? shortx2 : shortx1)); + } + + /* analyze longest minimum match */ + if (io1->ilong) { + const char *name1 = o1->long_name + io1->ilongoff; + + /* long name's first character matches short name */ + if (io2->ishort && !io1->iprefmatch) { + int name1char = (cli->utf8 ? decode_utf8(name1, 0) : (unsigned char) *name1); + if (name1char == o2->short_name) { + if (io1->ipos && io2->ipos) + io1->lmmpos_short = 1; + if (io1->ineg && io2->ineg) + io1->lmmneg_short = 1; + } + } + + /* match long name to long name */ + if (io2->ilong) { + const char *name2 = o2->long_name + io2->ilongoff; + if (strcmp(name1, name2) == 0) + Clp_OptionError(clp, "CLP internal error: duplicate long name %<%s%>", name1); + if (io1->ipos && io2->ipos && !strncmp(name1, name2, io1->lmmpos) + && (!io1->iprefmatch || strncmp(name1, name2, strlen(name1)))) + io1->lmmpos = min_different_chars(name1, name2); + if (io1->ineg && io2->ineg && !strncmp(name1, name2, io1->lmmneg) + && (!io1->iprefmatch || strncmp(name1, name2, strlen(name1)))) + io1->lmmneg = min_different_chars(name1, name2); + } + } +} + +static void +calculate_lmm(Clp_Parser *clp, const Clp_Option *opt, Clp_InternOption *iopt, int nopt) +{ + int i, j; + for (i = 0; i < nopt; ++i) { + iopt[i].lmmpos = iopt[i].lmmneg = 1; + iopt[i].lmmpos_short = iopt[i].lmmneg_short = 0; + for (j = 0; j < nopt; ++j) + compare_options(clp, &opt[i], &iopt[i], &opt[j], &iopt[j]); + } +} + +/** @param argc number of arguments + * @param argv argument array + * @param nopt number of option definitions + * @param opt option definition array + * @return the parser + * + * The new Clp_Parser that will parse the arguments in @a argv according to + * the option definitions in @a opt. + * + * The Clp_Parser is created with the following characteristics: + * + *
    + *
  • The "-" character introduces short options (Clp_SetOptionChar(clp, + * '-', Clp_Short)).
  • + *
  • Clp_ProgramName is set from the first argument in @a argv, if any. The + * first argument returned by Clp_Next() will be the second argument in @a + * argv. Note that this behavior differs from Clp_SetArguments.
  • + *
  • UTF-8 support is on iff the LANG environment variable contains + * one of the substrings "UTF-8", "UTF8", or "utf8". Override this with + * Clp_SetUTF8().
  • + *
  • The Clp_ValString, Clp_ValStringNotOption, Clp_ValInt, Clp_ValUnsigned, + * Clp_ValLong, Clp_ValUnsignedLong, Clp_ValBool, and Clp_ValDouble types are + * installed.
  • + *
  • Errors are reported to standard error.
  • + *
+ * + * You may also create a Clp_Parser with no arguments or options + * (Clp_NewParser(0, 0, 0, 0)) and set the arguments and options + * later. + * + * Returns NULL if there isn't enough memory to construct the parser. + * + * @note The CLP library will not modify the contents of @a argv or @a opt. + * The calling program must not modify @a opt. It may modify @a argv in + * limited cases. + */ +Clp_Parser * +Clp_NewParser(int argc, const char * const *argv, int nopt, const Clp_Option *opt) +{ + Clp_Parser *clp = (Clp_Parser *)malloc(sizeof(Clp_Parser)); + Clp_Internal *cli = (Clp_Internal *)malloc(sizeof(Clp_Internal)); + Clp_InternOption *iopt = (Clp_InternOption *)malloc(sizeof(Clp_InternOption) * nopt); + if (cli) + cli->valtype = (Clp_ValType *)malloc(sizeof(Clp_ValType) * Clp_InitialValType); + if (!clp || !cli || !iopt || !cli->valtype) + goto failed; + + clp->option = &clp_option_sentinel[-Clp_Done]; + clp->negated = 0; + clp->have_val = 0; + clp->vstr = 0; + clp->user_data = 0; + clp->internal = cli; + + cli->opt = opt; + cli->nopt = nopt; + cli->iopt = iopt; + cli->opt_generation = 0; + cli->error_handler = 0; + + /* Assign program name (now so we can call Clp_OptionError) */ + if (argc > 0) { + const char *slash = strrchr(argv[0], '/'); + cli->program_name = slash ? slash + 1 : argv[0]; + } else + cli->program_name = 0; + + /* Assign arguments, skipping program name */ + Clp_SetArguments(clp, argc - 1, argv + 1); + + /* Initialize UTF-8 status and option classes */ + { + char *s = getenv("LANG"); + cli->utf8 = (s && (strstr(s, "UTF-8") != 0 || strstr(s, "UTF8") != 0 + || strstr(s, "utf8") != 0)); + } + cli->oclass[0].c = '-'; + cli->oclass[0].type = Clp_Short; + cli->noclass = 1; + cli->long1pos = cli->long1neg = 0; + + /* Add default type parsers */ + cli->nvaltype = 0; + Clp_AddType(clp, Clp_ValString, 0, parse_string, 0); + Clp_AddType(clp, Clp_ValStringNotOption, Clp_DisallowOptions, parse_string, 0); + Clp_AddType(clp, Clp_ValInt, 0, parse_int, (void*) (uintptr_t) 0); + Clp_AddType(clp, Clp_ValUnsigned, 0, parse_int, (void*) (uintptr_t) 1); + Clp_AddType(clp, Clp_ValLong, 0, parse_int, (void*) (uintptr_t) 2); + Clp_AddType(clp, Clp_ValUnsignedLong, 0, parse_int, (void*) (uintptr_t) 3); + Clp_AddType(clp, Clp_ValBool, 0, parse_bool, 0); + Clp_AddType(clp, Clp_ValDouble, 0, parse_double, 0); + + /* Set options */ + Clp_SetOptions(clp, nopt, opt); + + return clp; + + failed: + if (cli && cli->valtype) + free(cli->valtype); + if (cli) + free(cli); + if (clp) + free(clp); + if (iopt) + free(iopt); + return 0; +} + +/** @param clp the parser + * + * All memory associated with @a clp is freed. */ +void +Clp_DeleteParser(Clp_Parser *clp) +{ + int i; + Clp_Internal *cli; + if (!clp) + return; + + cli = clp->internal; + + /* get rid of any string list types */ + for (i = 0; i < cli->nvaltype; i++) + if (cli->valtype[i].func == parse_string_list) { + Clp_StringList *clsl = (Clp_StringList *)cli->valtype[i].user_data; + free(clsl->items); + free(clsl->iopt); + free(clsl); + } + + free(cli->valtype); + free(cli->iopt); + free(cli); + free(clp); +} + + +/** @param clp the parser + * @param errh error handler function + * @return previous error handler function + * + * The error handler function is called when CLP encounters an error while + * parsing the command line. It is called with the arguments "(*errh)(@a + * clp, s)", where s is a description of the error terminated by + * a newline. The s descriptions produced by CLP itself are prefixed + * by the program name, if any. */ +Clp_ErrorHandler +Clp_SetErrorHandler(Clp_Parser *clp, void (*errh)(Clp_Parser *, const char *)) +{ + Clp_Internal *cli = clp->internal; + Clp_ErrorHandler old = cli->error_handler; + cli->error_handler = errh; + return old; +} + +/** @param clp the parser + * @param utf8 does the parser support UTF-8? + * @return previous UTF-8 mode + * + * In UTF-8 mode, all input strings (arguments and long names for options) are + * assumed to be encoded via UTF-8, and all character names + * (Clp_SetOptionChar() and short names for options) may cover the whole + * Unicode range. Out of UTF-8 mode, all input strings are treated as binary, + * and all character names must be unsigned char values. + * + * Furthermore, error messages in UTF-8 mode may contain Unicode quote + * characters. */ +int +Clp_SetUTF8(Clp_Parser *clp, int utf8) +{ + Clp_Internal *cli = clp->internal; + int old_utf8 = cli->utf8; + cli->utf8 = utf8; + calculate_lmm(clp, cli->opt, cli->iopt, cli->nopt); + return old_utf8; +} + +/** @param clp the parser + * @param c character + * @return option character treatment + * + * Returns an integer specifying how CLP treats arguments that begin + * with character @a c. See Clp_SetOptionChar for possibilities. + */ +int +Clp_OptionChar(Clp_Parser *clp, int c) +{ + Clp_Internal *cli = clp->internal; + int i, oclass = 0; + if (cli->noclass > 0 && cli->oclass[0].c == 0) + oclass = cli->oclass[0].type; + for (i = 0; i < cli->noclass; ++i) + if (cli->oclass[i].c == c) + oclass = cli->oclass[i].type; + return oclass; +} + +/** @param clp the parser + * @param c character + * @param type option character treatment + * @return previous option character treatment, or -1 on error + * + * @a type specifies how CLP treats arguments that begin with character @a c. + * Possibilities are: + * + *
+ *
Clp_NotOption (or 0)
+ *
The argument cannot be an option.
+ *
Clp_Long
+ *
The argument is a long option.
+ *
Clp_Short
+ *
The argument is a set of short options.
+ *
Clp_Short|Clp_Long
+ *
The argument is either a long option or, if no matching long option is + * found, a set of short options.
+ *
Clp_LongNegated
+ *
The argument is a negated long option. For example, after + * Clp_SetOptionChar(@a clp, '^', Clp_LongNegated), the argument "^foo" is + * equivalent to "--no-foo".
+ *
Clp_ShortNegated
+ *
The argument is a set of negated short options.
+ *
Clp_ShortNegated|Clp_LongNegated
+ *
The argument is either a negated long option or, if no matching long + * option is found, a set of negated short options.
+ *
Clp_LongImplicit
+ *
The argument may be a long option, where the character @a c is actually + * part of the long option name. For example, after Clp_SetOptionChar(@a clp, + * 'f', Clp_LongImplicit), the argument "foo" may be equivalent to + * "--foo".
+ *
+ * + * In UTF-8 mode, @a c may be any Unicode character. Otherwise, @a c must be + * an unsigned char value. The special character 0 assigns @a type to @em + * every character. + * + * It is an error if @a c is out of range, @a type is illegal, or there are + * too many character definitions stored in @a clp already. The function + * returns -1 on error. + * + * A double hyphen "--" always introduces a long option. This behavior cannot + * currently be changed with Clp_SetOptionChar(). + */ +int +Clp_SetOptionChar(Clp_Parser *clp, int c, int type) +{ + int i, long1pos, long1neg; + int old = Clp_OptionChar(clp, c); + Clp_Internal *cli = clp->internal; + + if (type != Clp_NotOption && type != Clp_Short && type != Clp_Long + && type != Clp_ShortNegated && type != Clp_LongNegated + && type != Clp_LongImplicit && type != (Clp_Short | Clp_Long) + && type != (Clp_ShortNegated | Clp_LongNegated)) + return -1; + if (c < 0 || c >= (cli->utf8 ? 0x110000 : 256)) + return -1; + + if (c == 0) + cli->noclass = 0; + for (i = 0; i < cli->noclass; ++i) + if (cli->oclass[i].c == c) + break; + if (i == Clp_OclassSize) + return -1; + + cli->oclass[i].c = c; + cli->oclass[i].type = type; + if (cli->noclass == i) + cli->noclass = i + 1; + + long1pos = long1neg = 0; + for (i = 0; i < cli->noclass; ++i) { + if ((cli->oclass[i].type & Clp_Short) + && (cli->oclass[i].type & Clp_Long)) + long1pos = 1; + if ((cli->oclass[i].type & Clp_ShortNegated) + && (cli->oclass[i].type & Clp_LongNegated)) + long1neg = 1; + } + + if (long1pos != cli->long1pos || long1neg != cli->long1neg) { + /* Must recheck option set */ + cli->long1pos = long1pos; + cli->long1neg = long1neg; + calculate_lmm(clp, cli->opt, cli->iopt, cli->nopt); + } + + return old; +} + +/** @param clp the parser + * @param nopt number of option definitions + * @param opt option definition array + * @return 0 on success, -1 on failure + * + * Installs the option definitions in @a opt. Future option parsing will + * use @a opt to search for options. + * + * Also checks @a opt's option definitions for validity. "CLP internal + * errors" are reported via Clp_OptionError() if: + * + *
    + *
  • An option has a negative ID.
  • + *
  • Two different short options have the same name.
  • + *
  • Two different long options have the same name.
  • + *
  • A short and a long option are ambiguous, in that some option character + * might introduce either a short or a long option (e.g., Clp_SetOptionChar(@a + * clp, '-', Clp_Long|Clp_Short)), and a short name equals a long name.
  • + *
+ * + * If necessary memory cannot be allocated, this function returns -1 without + * modifying the parser. + * + * @note The CLP library will not modify the contents of @a argv or @a opt. + * The calling program must not modify @a opt either until another call to + * Clp_SetOptions() or the parser is destroyed. + */ +int +Clp_SetOptions(Clp_Parser *clp, int nopt, const Clp_Option *opt) +{ + Clp_Internal *cli = clp->internal; + Clp_InternOption *iopt; + int i; + static unsigned opt_generation = 0; + + if (nopt > cli->nopt) { + iopt = (Clp_InternOption *)malloc(sizeof(Clp_InternOption) * nopt); + if (!iopt) + return -1; + free(cli->iopt); + cli->iopt = iopt; + } + + cli->opt = opt; + cli->nopt = nopt; + cli->opt_generation = ++opt_generation; + iopt = cli->iopt; + cli->current_option = -1; + + /* Massage the options to make them usable */ + for (i = 0; i < nopt; ++i) { + memset(&iopt[i], 0, sizeof(iopt[i])); + + /* Ignore negative option_ids, which are internal to CLP */ + if (opt[i].option_id < 0) { + Clp_OptionError(clp, "CLP internal error: option %d has negative option_id", i); + iopt[i].ilong = iopt[i].ishort = iopt[i].ipos = iopt[i].ineg = iopt[i].unquoted = 0; + continue; + } + + /* Set flags based on input flags */ + iopt[i].ilong = (opt[i].long_name != 0 && opt[i].long_name[0] != 0); + iopt[i].ishort = (opt[i].short_name > 0 + && opt[i].short_name < (cli->utf8 ? 0x110000 : 256)); + iopt[i].ipos = 1; + iopt[i].ineg = (opt[i].flags & Clp_Negate) != 0; + iopt[i].imandatory = (opt[i].flags & Clp_Mandatory) != 0; + iopt[i].ioptional = (opt[i].flags & Clp_Optional) != 0; + iopt[i].iprefmatch = (opt[i].flags & Clp_PreferredMatch) != 0; + iopt[i].unquoted = 0; + iopt[i].ilongoff = 0; + + /* Enforce invariants */ + if (opt[i].val_type <= 0) + iopt[i].imandatory = iopt[i].ioptional = 0; + if (opt[i].val_type > 0 && !iopt[i].ioptional) + iopt[i].imandatory = 1; + + /* Options that start with 'no-' should be changed to OnlyNegated */ + if (iopt[i].ilong && strncmp(opt[i].long_name, "no-", 3) == 0) { + iopt[i].ipos = 0; + iopt[i].ineg = 1; + iopt[i].ilongoff = 3; + if (strncmp(opt[i].long_name + 3, "no-", 3) == 0) + Clp_OptionError(clp, "CLP internal error: option %d begins with \"no-no-\"", i); + } else if (opt[i].flags & Clp_OnlyNegated) { + iopt[i].ipos = 0; + iopt[i].ineg = 1; + } + } + + /* Check option set */ + calculate_lmm(clp, opt, iopt, nopt); + + return 0; +} + +/** @param clp the parser + * @param argc number of arguments + * @param argv argument array + * + * Installs the arguments in @a argv for parsing. Future option parsing will + * analyze @a argv. + * + * Unlike Clp_NewParser(), this function does not treat @a argv[0] specially. + * The first subsequent call to Clp_Next() will analyze @a argv[0]. + * + * This function also sets option processing to on, as by + * Clp_SetOptionProcessing(@a clp, 1). + * + * @note The CLP library will not modify the contents of @a argv. The calling + * program should not generally modify the element of @a argv that CLP is + * currently analyzing. + */ +void +Clp_SetArguments(Clp_Parser *clp, int argc, const char * const *argv) +{ + Clp_Internal *cli = clp->internal; + + cli->argc = argc + 1; + cli->argv = argv - 1; + + cli->is_short = 0; + cli->whole_negated = 0; + cli->option_processing = 1; + cli->current_option = -1; +} + + +/** @param clp the parser + * @param on whether to search for options + * @return previous option processing setting + * + * When option processing is off, every call to Clp_Next() returns + * Clp_NotOption. By default the option "--" turns off option + * processing and is otherwise ignored. + */ +int +Clp_SetOptionProcessing(Clp_Parser *clp, int on) +{ + Clp_Internal *cli = clp->internal; + int old = cli->option_processing; + cli->option_processing = on; + return old; +} + + +/******* + * functions for Clp_Option lists + **/ + +/* the ever-glorious argcmp */ + +static int +argcmp(const char *ref, const char *arg, int min_match, int fewer_dashes) + /* Returns 0 if ref and arg don't match. + Returns -1 if ref and arg match, but fewer than min_match characters. + Returns len if ref and arg match min_match or more characters; + len is the number of characters that matched in arg. + Allows arg to contain fewer dashes than ref iff fewer_dashes != 0. + + Examples: + argcmp("x", "y", 1, 0) --> 0 / just plain wrong + argcmp("a", "ax", 1, 0) --> 0 / ...even though min_match == 1 + and the 1st chars match + argcmp("box", "bo", 3, 0) --> -1 / ambiguous + argcmp("cat", "c=3", 1, 0) --> 1 / handles = arguments + */ +{ + const char *refstart = ref; + const char *argstart = arg; + assert(min_match > 0); + + compare: + while (*ref && *arg && *arg != '=' && *ref == *arg) + ref++, arg++; + + /* Allow arg to contain fewer dashes than ref */ + if (fewer_dashes && *ref == '-' && ref[1] && ref[1] == *arg) { + ref++; + goto compare; + } + + if (*arg && *arg != '=') + return 0; + else if (ref - refstart < min_match) + return -1; + else + return arg - argstart; +} + +static int +find_prefix_opt(Clp_Parser *clp, const char *arg, + int nopt, const Clp_Option *opt, + const Clp_InternOption *iopt, + int *ambiguous, int *ambiguous_values) + /* Looks for an unambiguous match of 'arg' against one of the long + options in 'opt'. Returns positive if it finds one; otherwise, returns + -1 and possibly changes 'ambiguous' and 'ambiguous_values' to keep + track of at most MAX_AMBIGUOUS_VALUES possibilities. */ +{ + int i, fewer_dashes = 0, first_ambiguous = *ambiguous; + int negated = clp && clp->negated; + int first_charlen = (clp ? clp_utf8_charlen(clp->internal, arg) : 1); + + retry: + for (i = 0; i < nopt; i++) { + int len, lmm; + if (!iopt[i].ilong || (negated ? !iopt[i].ineg : !iopt[i].ipos)) + continue; + + lmm = (negated ? iopt[i].lmmneg : iopt[i].lmmpos); + if (clp && clp->internal->could_be_short + && (negated ? iopt[i].lmmneg_short : iopt[i].lmmpos_short)) + lmm = (first_charlen >= lmm ? first_charlen + 1 : lmm); + len = argcmp(opt[i].long_name + iopt[i].ilongoff, arg, lmm, fewer_dashes); + if (len > 0) + return i; + else if (len < 0) { + if (*ambiguous < MAX_AMBIGUOUS_VALUES) + ambiguous_values[*ambiguous] = i; + ++(*ambiguous); + } + } + + /* If there were no partial matches, try again with fewer_dashes true */ + if (*ambiguous == first_ambiguous && !fewer_dashes) { + fewer_dashes = 1; + goto retry; + } + + return -1; +} + + +/***** + * Argument parsing + **/ + +static int +val_type_binsearch(Clp_Internal *cli, int val_type) +{ + unsigned l = 0, r = cli->nvaltype; + while (l < r) { + unsigned m = l + (r - l) / 2; + if (cli->valtype[m].val_type == val_type) + return m; + else if (cli->valtype[m].val_type < val_type) + l = m + 1; + else + r = m; + } + return l; +} + +/** @param clp the parser + * @param val_type value type ID + * @param flags value type flags + * @param parser parser function + * @param user_data user data for @a parser function + * @return 0 on success, -1 on failure + * + * Defines argument type @a val_type in parser @a clp. The parsing function + * @a parser will be passed argument values for type @a val_type. It should + * parse the argument into values (usually in @a clp->val, but sometimes + * elsewhere), report errors if necessary, and return whether the parse was + * successful. + * + * Any prior argument parser match @a val_type is removed. @a val_type must + * be greater than zero. + * + * @a flags specifies additional parsing flags. At the moment the only + * relevant flag is Clp_DisallowOptions, which means that separated values + * must not look like options. For example, assume argument + * --a/-a has mandatory value type Clp_ValStringNotOption + * (which has Clp_DisallowOptions). Then: + * + *
    + *
  • --a=--b will parse with value --b.
  • + *
  • -a--b will parse with value --b.
  • + *
  • --a --b will not parse, since the mandatory value looks like + * an option.
  • + *
  • -a --b will not parse, since the mandatory value looks like + * an option.
  • + *
+ */ +int +Clp_AddType(Clp_Parser *clp, int val_type, int flags, + Clp_ValParseFunc parser, void *user_data) +{ + Clp_Internal *cli = clp->internal; + int vtpos; + + if (val_type <= 0 || !parser) + return -1; + + vtpos = val_type_binsearch(cli, val_type); + + if (vtpos == cli->nvaltype || cli->valtype[vtpos].val_type != val_type) { + if (cli->nvaltype != 0 && (cli->nvaltype % Clp_InitialValType) == 0) { + Clp_ValType *new_valtype = + (Clp_ValType *) realloc(cli->valtype, sizeof(Clp_ValType) * (cli->nvaltype + Clp_InitialValType)); + if (!new_valtype) + return -1; + cli->valtype = new_valtype; + } + memmove(&cli->valtype[vtpos + 1], &cli->valtype[vtpos], + sizeof(Clp_ValType) * (cli->nvaltype - vtpos)); + cli->nvaltype++; + cli->valtype[vtpos].func = 0; + } + + if (cli->valtype[vtpos].func == parse_string_list) { + Clp_StringList *clsl = (Clp_StringList *) cli->valtype[vtpos].user_data; + free(clsl->items); + free(clsl->iopt); + free(clsl); + } + + cli->valtype[vtpos].val_type = val_type; + cli->valtype[vtpos].func = parser; + cli->valtype[vtpos].flags = flags; + cli->valtype[vtpos].user_data = user_data; + return 0; +} + + +/******* + * Default argument parsers + **/ + +static int +parse_string(Clp_Parser *clp, const char *arg, int complain, void *user_data) +{ + (void)complain, (void)user_data; + clp->val.s = arg; + return 1; +} + +static int +parse_int(Clp_Parser* clp, const char* arg, int complain, void* user_data) +{ + const char *val; + uintptr_t type = (uintptr_t) user_data; + if (*arg == 0 || isspace((unsigned char) *arg) + || ((type & 1) && *arg == '-')) + val = arg; + else if (type & 1) { /* unsigned */ +#if HAVE_STRTOUL + clp->val.ul = strtoul(arg, (char **) &val, 0); +#else + /* don't bother really trying to do it right */ + if (arg[0] == '-') + val = arg; + else + clp->val.l = strtol(arg, (char **) &val, 0); +#endif + } else + clp->val.l = strtol(arg, (char **) &val, 0); + if (type <= 1) + clp->val.u = (unsigned) clp->val.ul; + if (*arg != 0 && *val == 0) + return 1; + else { + if (complain) { + const char *message = (type & 1) + ? "%<%O%> expects a nonnegative integer, not %<%s%>" + : "%<%O%> expects an integer, not %<%s%>"; + Clp_OptionError(clp, message, arg); + } + return 0; + } +} + +static int +parse_double(Clp_Parser *clp, const char *arg, int complain, void *user_data) +{ + const char *val; + (void)user_data; + if (*arg == 0 || isspace((unsigned char) *arg)) + val = arg; + else + clp->val.d = strtod(arg, (char **) &val); + if (*arg != 0 && *val == 0) + return 1; + else { + if (complain) + Clp_OptionError(clp, "%<%O%> expects a real number, not %<%s%>", arg); + return 0; + } +} + +static int +parse_bool(Clp_Parser *clp, const char *arg, int complain, void *user_data) +{ + int i; + char lcarg[6]; + (void)user_data; + if (strlen(arg) > 5 || strchr(arg, '=') != 0) + goto error; + + for (i = 0; arg[i] != 0; i++) + lcarg[i] = tolower((unsigned char) arg[i]); + lcarg[i] = 0; + + if (argcmp("yes", lcarg, 1, 0) > 0 + || argcmp("true", lcarg, 1, 0) > 0 + || argcmp("1", lcarg, 1, 0) > 0) { + clp->val.i = 1; + return 1; + } else if (argcmp("no", lcarg, 1, 0) > 0 + || argcmp("false", lcarg, 1, 0) > 0 + || argcmp("1", lcarg, 1, 0) > 0) { + clp->val.i = 0; + return 1; + } + + error: + if (complain) + Clp_OptionError(clp, "%<%O%> expects a true-or-false value, not %<%s%>", arg); + return 0; +} + + +/***** + * Clp_AddStringListType + **/ + +static int +parse_string_list(Clp_Parser *clp, const char *arg, int complain, void *user_data) +{ + Clp_StringList *sl = (Clp_StringList *)user_data; + int idx, ambiguous = 0; + int ambiguous_values[MAX_AMBIGUOUS_VALUES + 1]; + + /* actually look for a string value */ + idx = find_prefix_opt + (0, arg, sl->nitems, sl->items, sl->iopt, + &ambiguous, ambiguous_values); + if (idx >= 0) { + clp->val.i = sl->items[idx].option_id; + if (sl->val_long) + clp->val.l = clp->val.i; + return 1; + } + + if (sl->allow_int) { + if (parse_int(clp, arg, 0, (void*) (uintptr_t) (sl->val_long ? 2 : 0))) + return 1; + } + + if (complain) { + const char *complaint = (ambiguous ? "ambiguous" : "invalid"); + if (!ambiguous) { + ambiguous = sl->nitems_invalid_report; + for (idx = 0; idx < ambiguous; idx++) + ambiguous_values[idx] = idx; + } + return ambiguity_error + (clp, ambiguous, ambiguous_values, sl->items, sl->iopt, + "", "option %<%V%> is %s", complaint); + } else + return 0; +} + + +static int +finish_string_list(Clp_Parser *clp, int val_type, int flags, + Clp_Option *items, int nitems, int itemscap) +{ + int i, niitems = nitems + (flags & Clp_AllowNumbers ? 1 : 0); + Clp_StringList *clsl = (Clp_StringList *)malloc(sizeof(Clp_StringList)); + Clp_InternOption *iopt = (Clp_InternOption *)malloc(sizeof(Clp_InternOption) * niitems); + if (!clsl || !iopt) + goto error; + + clsl->items = items; + clsl->iopt = iopt; + clsl->nitems = nitems; + clsl->allow_int = (flags & Clp_AllowNumbers) != 0; + clsl->val_long = (flags & Clp_StringListLong) != 0; + + memset(iopt, 0, sizeof(Clp_InternOption) * niitems); + for (i = 0; i < niitems; i++) { + iopt[i].ilong = iopt[i].ipos = 1; + iopt[i].ishort = iopt[i].ineg = iopt[i].ilongoff = iopt[i].iprefmatch = iopt[i].unquoted = 0; + } + calculate_lmm(clp, items, iopt, nitems); + + if (nitems < MAX_AMBIGUOUS_VALUES && nitems < itemscap && clsl->allow_int) { + items[nitems].long_name = "any integer"; + iopt[nitems].unquoted = 1; + clsl->nitems_invalid_report = nitems + 1; + } else if (nitems > MAX_AMBIGUOUS_VALUES + 1) + clsl->nitems_invalid_report = MAX_AMBIGUOUS_VALUES + 1; + else + clsl->nitems_invalid_report = nitems; + + if (Clp_AddType(clp, val_type, 0, parse_string_list, clsl) >= 0) + return 0; + + error: + if (clsl) + free(clsl); + if (iopt) + free(iopt); + return -1; +} + +/** @param clp the parser + * @param val_type value type ID + * @param flags string list flags + * @return 0 on success, -1 on failure + * + * Defines argument type @a val_type in parser @a clp. The parsing function + * sets @a clp->val.i to an integer. The value string is matched against + * strings provided in the ellipsis arguments. For example, the + * Clp_AddStringListType() call below has the same effect as the + * Clp_AddStringListTypeVec() call: + * + * For example: + * @code + * Clp_AddStringListType(clp, 100, Clp_AllowNumbers, "cat", 1, + * "cattle", 2, "dog", 3, (const char *) NULL); + * + * const char * const strs[] = { "cat", "cattle", "dog" }; + * const int vals[] = { 1, 2, 3 }; + * Clp_AddStringListTypeVec(clp, 100, Clp_AllowNumbers, 3, strs, vals); + * @endcode + * + * @note The CLP library will not modify any of the passed-in strings. The + * calling program must not modify or free them either until the parser is + * destroyed. + */ +int +Clp_AddStringListType(Clp_Parser *clp, int val_type, int flags, ...) +{ + int nitems = 0; + int itemscap = 5; + Clp_Option *items = (Clp_Option *)malloc(sizeof(Clp_Option) * itemscap); + + va_list val; + va_start(val, flags); + + if (!items) + goto error; + + /* slurp up the arguments */ + while (1) { + int value; + char *name = va_arg(val, char *); + if (!name) + break; + if (flags & Clp_StringListLong) { + long lvalue = va_arg(val, long); + value = (int) lvalue; + assert(value == lvalue); + } else + value = va_arg(val, int); + + if (nitems >= itemscap) { + Clp_Option *new_items; + itemscap *= 2; + new_items = (Clp_Option *)realloc(items, sizeof(Clp_Option) * itemscap); + if (!new_items) + goto error; + items = new_items; + } + + items[nitems].long_name = name; + items[nitems].option_id = value; + items[nitems].flags = 0; + nitems++; + } + + va_end(val); + if (finish_string_list(clp, val_type, flags, items, nitems, itemscap) >= 0) + return 0; + + error: + va_end(val); + if (items) + free(items); + return -1; +} + +/** @param clp the parser + * @param val_type value type ID + * @param flags string list flags + * @param nstrs number of strings in list + * @param strs array of strings + * @param vals array of values + * @return 0 on success, -1 on failure + * + * Defines argument type @a val_type in parser @a clp. The parsing function + * sets @a clp->val.i to an integer. The value string is matched against the + * @a strs. If there's a unique match, the corresponding entry from @a vals + * is returned. Unique prefix matches also work. Finally, if @a flags + * contains the Clp_AllowNumbers flag, then integers are also accepted. + * + * For example: + * @code + * const char * const strs[] = { "cat", "cattle", "dog" }; + * const int vals[] = { 1, 2, 3 }; + * Clp_AddStringListTypeVec(clp, 100, Clp_AllowNumbers, 3, strs, vals); + * @endcode + * + * Say that option --animal takes value type 100. Then: + * + *
    + *
  • --animal=cat will succeed and set @a clp->val.i = 1.
  • + *
  • --animal=cattle will succeed and set @a clp->val.i = 2.
  • + *
  • --animal=dog will succeed and set @a clp->val.i = 3.
  • + *
  • --animal=d will succeed and set @a clp->val.i = 3.
  • + *
  • --animal=c will fail, since c is ambiguous.
  • + *
  • --animal=4 will succeed and set @a clp->val.i = 4.
  • + *
+ * + * @note The CLP library will not modify the contents of @a strs or @a vals. + * The calling program can modify the @a strs array, but the actual strings + * (for instance, @a strs[0] and @a strs[1]) must not be modified or freed + * until the parser is destroyed. + */ +int +Clp_AddStringListTypeVec(Clp_Parser *clp, int val_type, int flags, + int nstrs, const char * const *strs, + const int *vals) + /* An alternate way to make a string list type. See Clp_AddStringListType + for the basics; this coalesces the strings and values into two arrays, + rather than spreading them out into a variable argument list. */ +{ + int i; + int itemscap = (nstrs < 5 ? 5 : nstrs); + Clp_Option *items = (Clp_Option *)malloc(sizeof(Clp_Option) * itemscap); + if (!items) + return -1; + + /* copy over items */ + for (i = 0; i < nstrs; i++) { + items[i].long_name = strs[i]; + items[i].option_id = vals[i]; + items[i].flags = 0; + } + + if (finish_string_list(clp, val_type, flags, items, nstrs, itemscap) >= 0) + return 0; + else { + free(items); + return -1; + } +} + + +/******* + * Returning information + **/ + +const char * +Clp_ProgramName(Clp_Parser *clp) +{ + return clp->internal->program_name; +} + +/** @param clp the parser + * @param name new program name + * @return previous program name + * + * The calling program should not modify or free @a name until @a clp itself + * is destroyed. */ +const char * +Clp_SetProgramName(Clp_Parser *clp, const char *name) +{ + const char *old = clp->internal->program_name; + clp->internal->program_name = name; + return old; +} + + +/****** + * Clp_ParserStates + **/ + +/** @return the parser state + * + * A Clp_ParserState object can store a parsing state of a Clp_Parser object. + * This state specifies exactly how far the Clp_Parser has gotten in parsing + * an argument list. The Clp_SaveParser() and Clp_RestoreParser() functions + * can be used to save this state and then restore it later, allowing a + * Clp_Parser to switch among argument lists. + * + * The initial state is empty, in that after Clp_RestoreParser(clp, state), + * Clp_Next(clp) would return Clp_Done. + * + * Parser states can be saved and restored among different parser objects. + * + * @sa Clp_DeleteParserState, Clp_SaveParser, Clp_RestoreParser + */ +Clp_ParserState * +Clp_NewParserState(void) +{ + Clp_ParserState *state = (Clp_ParserState *)malloc(sizeof(Clp_ParserState)); + if (state) { + state->argv = 0; + state->argc = 0; + state->option_chars[0] = 0; + state->xtext = 0; + state->option_processing = 0; + state->opt_generation = 0; + state->current_option = -1; + state->is_short = 0; + state->whole_negated = 0; + state->current_short = 0; + state->negated_by_no = 0; + } + return state; +} + +/** @param state parser state + * + * The memory associated with @a state is freed. + */ +void +Clp_DeleteParserState(Clp_ParserState *state) +{ + free(state); +} + +/** @param clp the parser + * @param state parser state + * @sa Clp_NewParserState, Clp_RestoreParser + */ +void +Clp_SaveParser(const Clp_Parser *clp, Clp_ParserState *state) +{ + Clp_Internal *cli = clp->internal; + state->argv = cli->argv; + state->argc = cli->argc; + memcpy(state->option_chars, cli->option_chars, Clp_OptionCharsSize); + state->xtext = cli->xtext; + + state->option_processing = cli->option_processing; + state->opt_generation = cli->opt_generation; + state->current_option = cli->current_option; + state->is_short = cli->is_short; + state->whole_negated = cli->whole_negated; + state->current_short = cli->current_short; + state->negated_by_no = cli->negated_by_no; +} + + +/** @param clp the parser + * @param state parser state + * + * The parser state in @a state is restored into @a clp. The next call to + * Clp_Next() will return the same result as it would have at the time @a + * state was saved (probably by Clp_SaveParser(@a clp, @a state)). + * + * A parser state contains information about arguments (argc and argv; see + * Clp_SetArguments()) and option processing (Clp_SetOptionProcessing()), but + * not about options (Clp_SetOptions()). Changes to options and value types + * are preserved across Clp_RestoreParser(). + * + * @sa Clp_NewParserState, Clp_SaveParser + */ +void +Clp_RestoreParser(Clp_Parser *clp, const Clp_ParserState *state) +{ + Clp_Internal *cli = clp->internal; + cli->argv = state->argv; + cli->argc = state->argc; + memcpy(cli->option_chars, state->option_chars, Clp_OptionCharsSize); + cli->xtext = state->xtext; + cli->option_processing = state->option_processing; + cli->is_short = state->is_short; + cli->whole_negated = state->whole_negated; + cli->current_short = state->current_short; + cli->negated_by_no = state->negated_by_no; + if (cli->opt_generation == state->opt_generation) + cli->current_option = state->current_option; + else + cli->current_option = -1; +} + + +/******* + * Clp_Next and its helpers + **/ + +static void +set_option_text(Clp_Internal *cli, const char *text, int n_option_chars) +{ + assert(n_option_chars < Clp_OptionCharsSize); + memcpy(cli->option_chars, text, n_option_chars); + cli->option_chars[n_option_chars] = 0; + cli->xtext = text + n_option_chars; +} + +static int +get_oclass(Clp_Parser *clp, const char *text, int *ocharskip) +{ + int c; + if (clp->internal->utf8) { + const char *s; + c = decode_utf8(text, &s); + *ocharskip = s - text; + } else { + c = (unsigned char) text[0]; + *ocharskip = 1; + } + return Clp_OptionChar(clp, c); +} + +static int +next_argument(Clp_Parser *clp, int want_argument) + /* Moves clp to the next argument. + Returns 1 if it finds another option. + Returns 0 if there aren't any more arguments. + Returns 0, sets clp->have_val = 1, and sets clp->vstr to the argument + if the next argument isn't an option. + If want_argument > 0, it'll look for an argument. + want_argument == 1: Accept arguments that start with Clp_NotOption + or Clp_LongImplicit. + want_argument == 2: Accept ALL arguments. + + Where is the option stored when this returns? + Well, cli->argv[0] holds the whole of the next command line argument. + cli->option_chars holds a string: what characters began the option? + It is generally "-" or "--". + cli->text holds the text of the option: + for short options, cli->text[0] is the relevant character; + for long options, cli->text holds the rest of the option. */ +{ + Clp_Internal *cli = clp->internal; + const char *text; + int oclass, ocharskip; + + /* clear relevant flags */ + clp->have_val = 0; + clp->vstr = 0; + cli->could_be_short = 0; + + /* if we're in a string of short options, move up one char in the string */ + if (cli->is_short) { + cli->xtext += clp_utf8_charlen(cli, cli->xtext); + if (cli->xtext[0] == 0) + cli->is_short = 0; + else if (want_argument > 0) { + /* handle -O[=]argument case */ + clp->have_val = 1; + if (cli->xtext[0] == '=') + clp->vstr = cli->xtext + 1; + else + clp->vstr = cli->xtext; + cli->is_short = 0; + return 0; + } + } + + /* if in short options, we're all set */ + if (cli->is_short) + return 1; + + /** if not in short options, move to the next argument **/ + cli->whole_negated = 0; + cli->xtext = 0; + + if (cli->argc <= 1) + return 0; + + cli->argc--; + cli->argv++; + text = cli->argv[0]; + + if (want_argument > 1) + goto not_option; + + if (text[0] == '-' && text[1] == '-') { + oclass = Clp_DoubledLong; + ocharskip = 2; + } else + oclass = get_oclass(clp, text, &ocharskip); + + /* If this character could introduce either a short or a long option, + try a long option first, but remember that short's a possibility for + later. */ + if ((oclass & (Clp_Short | Clp_ShortNegated)) + && (oclass & (Clp_Long | Clp_LongNegated))) { + oclass &= ~(Clp_Short | Clp_ShortNegated); + if (text[ocharskip]) + cli->could_be_short = 1; + } + + switch (oclass) { + + case Clp_Short: + cli->is_short = 1; + goto check_singleton; + + case Clp_ShortNegated: + cli->is_short = 1; + cli->whole_negated = 1; + goto check_singleton; + + case Clp_Long: + goto check_singleton; + + case Clp_LongNegated: + cli->whole_negated = 1; + goto check_singleton; + + check_singleton: + /* For options introduced with one character, option-char, + '[option-char]' alone is NOT an option. */ + if (!text[ocharskip]) + goto not_option; + set_option_text(cli, text, ocharskip); + break; + + case Clp_LongImplicit: + /* LongImplict: option_chars == "" (since all chars are part of the + option); restore head -> text of option */ + if (want_argument > 0) + goto not_option; + set_option_text(cli, text, 0); + break; + + case Clp_DoubledLong: + set_option_text(cli, text, ocharskip); + break; + + not_option: + case Clp_NotOption: + cli->is_short = 0; + clp->have_val = 1; + clp->vstr = text; + return 0; + + default: + assert(0 /* CLP misconfiguration: bad option type */); + + } + + return 1; +} + + +static void +switch_to_short_argument(Clp_Parser *clp) +{ + Clp_Internal *cli = clp->internal; + const char *text = cli->argv[0]; + int ocharskip, oclass = get_oclass(clp, text, &ocharskip); + assert(cli->could_be_short); + cli->is_short = 1; + cli->whole_negated = !!(oclass & Clp_ShortNegated); + set_option_text(cli, cli->argv[0], ocharskip); +} + + +static int +find_long(Clp_Parser *clp, const char *arg) + /* If arg corresponds to one of clp's options, finds that option & + returns it. If any argument is given after an = sign in arg, sets + clp->have_val = 1 and clp->vstr to that argument. Sets cli->ambiguous + to 1 iff there was no match because the argument was ambiguous. */ +{ + Clp_Internal *cli = clp->internal; + int optno, len, lmm; + const Clp_Option *opt = cli->opt; + const Clp_InternOption *iopt; + int first_negative_ambiguous; + + /* Look for a normal option. */ + optno = find_prefix_opt + (clp, arg, cli->nopt, opt, cli->iopt, + &cli->ambiguous, cli->ambiguous_values); + if (optno >= 0) + goto worked; + + /* If we can't find it, look for a negated option. */ + /* I know this is silly, but it makes me happy to accept + --no-no-option as a double negative synonym for --option. :) */ + first_negative_ambiguous = cli->ambiguous; + while (arg[0] == 'n' && arg[1] == 'o' && arg[2] == '-') { + arg += 3; + clp->negated = !clp->negated; + optno = find_prefix_opt + (clp, arg, cli->nopt, opt, cli->iopt, + &cli->ambiguous, cli->ambiguous_values); + if (optno >= 0) + goto worked; + } + + /* No valid option was found; return 0. Mark the ambiguous values found + through '--no' by making them negative. */ + { + int i, max = cli->ambiguous; + if (max > MAX_AMBIGUOUS_VALUES) max = MAX_AMBIGUOUS_VALUES; + for (i = first_negative_ambiguous; i < max; i++) + cli->ambiguous_values[i] = -cli->ambiguous_values[i] - 1; + } + return -1; + + worked: + iopt = &cli->iopt[optno]; + lmm = (clp->negated ? iopt->lmmneg : iopt->lmmpos); + if (cli->could_be_short + && (clp->negated ? iopt->lmmneg_short : iopt->lmmpos_short)) { + int first_charlen = clp_utf8_charlen(cli, arg); + lmm = (first_charlen >= lmm ? first_charlen + 1 : lmm); + } + len = argcmp(opt[optno].long_name + iopt->ilongoff, arg, lmm, 1); + assert(len > 0); + if (arg[len] == '=') { + clp->have_val = 1; + clp->vstr = arg + len + 1; + } + return optno; +} + + +static int +find_short(Clp_Parser *clp, const char *text) + /* If short_name corresponds to one of clp's options, returns it. */ +{ + Clp_Internal *cli = clp->internal; + const Clp_Option *opt = cli->opt; + const Clp_InternOption *iopt = cli->iopt; + int i, c; + if (clp->internal->utf8) + c = decode_utf8(text, 0); + else + c = (unsigned char) *text; + + for (i = 0; i < cli->nopt; i++) + if (iopt[i].ishort && opt[i].short_name == c + && (!clp->negated || iopt[i].ineg)) { + clp->negated = clp->negated || !iopt[i].ipos; + return i; + } + + return -1; +} + + +/** @param clp the parser + * @return option ID of next option + * + * Parse the next argument from the argument list, store information about + * that argument in the fields of @a clp, and return the option's ID. + * + * If an argument was successfully parsed, that option's ID is returned. + * Other possible return values are: + * + *
+ *
Clp_Done
+ *
There are no more arguments.
+ *
Clp_NotOption
+ *
The next argument was not an option. The argument's text is @a + * clp->vstr (and @a clp->val.s).
+ *
Clp_BadOption
+ *
The next argument was a bad option: either an option that wasn't + * understood, or an option lacking a required value, or an option whose value + * couldn't be parsed. The option has been skipped.
+ *
Clp_Error
+ *
There was an internal error. This should never occur unless a user + * messes with, for example, a Clp_Option array.
+ *
+ * + * The fields of @a clp are set as follows. + * + *
+ *
negated
+ *
1 if the option was negated, 0 if it wasn't.
+ *
have_val
+ *
1 if the option had a value, 0 if it didn't. Note that negated options + * are not allowed to have values.
+ *
vstr
+ *
The value string, if any. NULL if there was no value.
+ *
val
+ *
An option's value type will parse the value string into this + * union.
+ *
+ * + * The parsed argument is shifted off the argument list, so that sequential + * calls to Clp_Next() step through the argument list. + */ +int +Clp_Next(Clp_Parser *clp) +{ + Clp_Internal *cli = clp->internal; + int optno; + const Clp_Option *opt; + Clp_ParserState clpsave; + int vtpos, complain; + + /* Set up clp */ + cli->current_option = -1; + cli->ambiguous = 0; + + /* Get the next argument or option */ + if (!next_argument(clp, cli->option_processing ? 0 : 2)) { + clp->val.s = clp->vstr; + optno = clp->have_val ? Clp_NotOption : Clp_Done; + clp->option = &clp_option_sentinel[-optno]; + return optno; + } + + clp->negated = cli->whole_negated; + if (cli->is_short) + optno = find_short(clp, cli->xtext); + else + optno = find_long(clp, cli->xtext); + + /* If there's ambiguity between long & short options, and we couldn't + find a long option, look for a short option */ + if (optno < 0 && cli->could_be_short) { + switch_to_short_argument(clp); + optno = find_short(clp, cli->xtext); + } + + /* If we didn't find an option... */ + if (optno < 0 || (clp->negated && !cli->iopt[optno].ineg)) { + /* default processing for the "--" option: turn off option processing + and return the next argument */ + if (strcmp(cli->argv[0], "--") == 0) { + Clp_SetOptionProcessing(clp, 0); + return Clp_Next(clp); + } + + /* otherwise, report some error or other */ + if (cli->ambiguous) + ambiguity_error(clp, cli->ambiguous, cli->ambiguous_values, + cli->opt, cli->iopt, cli->option_chars, + "option %<%s%s%> is ambiguous", + cli->option_chars, cli->xtext); + else if (cli->is_short && !cli->could_be_short) + Clp_OptionError(clp, "unrecognized option %<%s%C%>", + cli->option_chars, cli->xtext); + else + Clp_OptionError(clp, "unrecognized option %<%s%s%>", + cli->option_chars, cli->xtext); + + clp->option = &clp_option_sentinel[-Clp_BadOption]; + return Clp_BadOption; + } + + /* Set the current option */ + cli->current_option = optno; + cli->current_short = cli->is_short; + cli->negated_by_no = clp->negated && !cli->whole_negated; + + /* The no-argument (or should-have-no-argument) case */ + if (clp->negated + || (!cli->iopt[optno].imandatory && !cli->iopt[optno].ioptional)) { + if (clp->have_val) { + Clp_OptionError(clp, "%<%O%> can%,t take an argument"); + clp->option = &clp_option_sentinel[-Clp_BadOption]; + return Clp_BadOption; + } else { + clp->option = &cli->opt[optno]; + return cli->opt[optno].option_id; + } + } + + /* Get an argument if we need one, or if it's optional */ + /* Sanity-check the argument type. */ + opt = &cli->opt[optno]; + if (opt->val_type <= 0) { + clp->option = &clp_option_sentinel[-Clp_Error]; + return Clp_Error; + } + vtpos = val_type_binsearch(cli, opt->val_type); + if (vtpos == cli->nvaltype + || cli->valtype[vtpos].val_type != opt->val_type) { + clp->option = &clp_option_sentinel[-Clp_Error]; + return Clp_Error; + } + + /* complain == 1 only if the argument was explicitly given, + or it is mandatory. */ + complain = (clp->have_val != 0) || cli->iopt[optno].imandatory; + Clp_SaveParser(clp, &clpsave); + + if (cli->iopt[optno].imandatory && !clp->have_val) { + /* Mandatory argument case */ + /* Allow arguments to options to start with a dash, but only if the + argument type allows it by not setting Clp_DisallowOptions */ + int disallow = (cli->valtype[vtpos].flags & Clp_DisallowOptions) != 0; + next_argument(clp, disallow ? 1 : 2); + if (!clp->have_val) { + int got_option = cli->xtext != 0; + Clp_RestoreParser(clp, &clpsave); + if (got_option) + Clp_OptionError(clp, "%<%O%> requires a non-option argument"); + else + Clp_OptionError(clp, "%<%O%> requires an argument"); + clp->option = &clp_option_sentinel[-Clp_BadOption]; + return Clp_BadOption; + } + + } else if (cli->is_short && !clp->have_val + && cli->xtext[clp_utf8_charlen(cli, cli->xtext)]) + /* The -[option]argument case: + Assume that the rest of the current string is the argument. */ + next_argument(clp, 1); + + /* Parse the argument */ + clp->option = opt; + if (clp->have_val) { + Clp_ValType *atr = &cli->valtype[vtpos]; + if (atr->func(clp, clp->vstr, complain, atr->user_data) <= 0) { + /* parser failed */ + clp->have_val = 0; + if (complain) { + clp->option = &clp_option_sentinel[-Clp_BadOption]; + return Clp_BadOption; + } else { + Clp_RestoreParser(clp, &clpsave); + clp->option = opt; + } + } + } + + return opt->option_id; +} + + +/** @param clp the parser + * @param allow_options whether options will be allowed + * + * Remove and return the next argument from @a clp's argument array. If there + * are no arguments left, or if the next argument is an option and @a + * allow_options != 0, then returns null. + */ +const char * +Clp_Shift(Clp_Parser *clp, int allow_options) + /* Returns the next argument from the argument list without parsing it. + If there are no more arguments, returns 0. */ +{ + Clp_ParserState clpsave; + Clp_SaveParser(clp, &clpsave); + next_argument(clp, allow_options ? 2 : 1); + if (!clp->have_val) + Clp_RestoreParser(clp, &clpsave); + return clp->vstr; +} + + +/******* + * Clp_OptionError + **/ + +typedef struct Clp_BuildString { + char* data; + char* pos; + char* end_data; + char buf[256]; +} Clp_BuildString; + +static void build_string_program_prefix(Clp_BuildString* bs, + const Clp_Parser* clp); + +static void build_string_init(Clp_BuildString* bs, Clp_Parser* clp) { + bs->data = bs->pos = bs->buf; + bs->end_data = &bs->buf[sizeof(bs->buf)]; + if (clp) + build_string_program_prefix(bs, clp); +} + +static void build_string_cleanup(Clp_BuildString* bs) { + if (bs->data != bs->buf) + free(bs->data); +} + +static int build_string_grow(Clp_BuildString* bs, size_t want) { + size_t ipos = bs->pos - bs->data, ncap; + if (!bs->pos) + return 0; + for (ncap = (bs->end_data - bs->data) << 1; ncap < want; ncap *= 2) + /* nada */; + if (bs->data == bs->buf) { + if ((bs->data = (char*) malloc(ncap))) + memcpy(bs->data, bs->buf, bs->pos - bs->buf); + } else + bs->data = (char*) realloc(bs->data, ncap); + if (!bs->data) { + bs->pos = bs->end_data = bs->data; + return 0; + } else { + bs->pos = bs->data + ipos; + bs->end_data = bs->data + ncap; + return 1; + } +} + +#define ENSURE_BUILD_STRING(bs, space) \ + ((((bs)->end_data - (bs)->pos) >= (space)) \ + || build_string_grow((bs), (bs)->pos - (bs)->data + (space))) + +static void +append_build_string(Clp_BuildString *bs, const char *s, int l) +{ + if (l < 0) + l = strlen(s); + if (ENSURE_BUILD_STRING(bs, l)) { + memcpy(bs->pos, s, l); + bs->pos += l; + } +} + +static void +build_string_program_prefix(Clp_BuildString* bs, const Clp_Parser* clp) +{ + const Clp_Internal* cli = clp->internal; + if (cli->program_name && cli->program_name[0]) { + append_build_string(bs, cli->program_name, -1); + append_build_string(bs, ": ", 2); + } +} + + +const char* +Clp_QuoteChar(const Clp_Parser* clp, int isclose) +{ + if (!clp->internal->utf8) { + return "'"; + } else if (isclose) { + return "\342\200\231"; + } else { + return "\342\200\230"; + } +} + +static void +Clp_vbsprintf(Clp_Parser *clp, Clp_BuildString *bs, + const char *fmt, va_list val) +{ + Clp_Internal *cli = clp->internal; + const char *percent; + int c; + + for (percent = strchr(fmt, '%'); percent; percent = strchr(fmt, '%')) { + append_build_string(bs, fmt, percent - fmt); + switch (*++percent) { + + case 's': { + const char *s = va_arg(val, const char *); + append_build_string(bs, s ? s : "(null)", -1); + break; + } + + case 'C': { + const char *s = va_arg(val, const char *); + if (cli->utf8) + c = decode_utf8(s, 0); + else + c = (unsigned char) *s; + goto char_c; + } + + case 'c': + c = va_arg(val, int); + goto char_c; + + char_c: + if (ENSURE_BUILD_STRING(bs, 5)) { + if (c >= 32 && c <= 126) + *bs->pos++ = c; + else if (c < 32) { + *bs->pos++ = '^'; + *bs->pos++ = c + 64; + } else if (cli->utf8 && c >= 127 && c < 0x110000) { + bs->pos = encode_utf8(bs->pos, 4, c); + } else if (c >= 127 && c <= 255) { + snprintf(bs->pos, 5, "\\%03o", c & 0xFF); + bs->pos += 4; + } else { + *bs->pos++ = '\\'; + *bs->pos++ = '?'; + } + } + break; + + case 'd': { + int d = va_arg(val, int); + if (ENSURE_BUILD_STRING(bs, 32)) { + bs->pos += snprintf(bs->pos, 32, "%d", d); + } + break; + } + + case 'O': + case 'V': { + int optno = cli->current_option; + const Clp_Option *opt = &cli->opt[optno]; + if (optno < 0) + append_build_string(bs, "(no current option!)", -1); + else if (cli->current_short) { + append_build_string(bs, cli->option_chars, -1); + if (ENSURE_BUILD_STRING(bs, 5)) { + if (cli->utf8) + bs->pos = encode_utf8(bs->pos, 5, opt->short_name); + else + *bs->pos++ = opt->short_name; + } + } else if (cli->negated_by_no) { + append_build_string(bs, cli->option_chars, -1); + append_build_string(bs, "no-", 3); + append_build_string(bs, opt->long_name + cli->iopt[optno].ilongoff, -1); + } else { + append_build_string(bs, cli->option_chars, -1); + append_build_string(bs, opt->long_name + cli->iopt[optno].ilongoff, -1); + } + if (optno >= 0 && clp->have_val && *percent == 'V') { + if (cli->current_short && !cli->iopt[optno].ioptional) + append_build_string(bs, " ", 1); + else if (!cli->current_short) + append_build_string(bs, "=", 1); + append_build_string(bs, clp->vstr, -1); + } + break; + } + + case '%': + if (ENSURE_BUILD_STRING(bs, 1)) + *bs->pos++ = '%'; + break; + + case '<': + append_build_string(bs, Clp_QuoteChar(clp, 0), -1); + break; + + case ',': + case '>': + append_build_string(bs, Clp_QuoteChar(clp, 1), -1); + break; + + case 0: + append_build_string(bs, "%", 1); + goto done; + + default: + if (ENSURE_BUILD_STRING(bs, 2)) { + *bs->pos++ = '%'; + *bs->pos++ = *percent; + } + break; + + } + fmt = ++percent; + } + + done: + append_build_string(bs, fmt, -1); +} + +static const char* build_string_text(Clp_BuildString* bs, int report_oom) { + if (bs->pos) { + *bs->pos = 0; + return bs->data; + } else if (report_oom) + return "out of memory\n"; + else + return NULL; +} + +static void +do_error(Clp_Parser *clp, Clp_BuildString *bs) +{ + const char *text = build_string_text(bs, 1); + if (clp->internal->error_handler != 0) + (*clp->internal->error_handler)(clp, text); + else + fputs(text, stderr); +} + +/** @param clp the parser + * @param format error format + * + * Format an error message from @a format and any additional arguments in + * the ellipsis. The resulting error string is then printed to standard + * error (or passed to the error handler specified by Clp_SetErrorHandler). + * Returns the number of characters printed. + * + * The following format characters are accepted: + * + *
+ *
%c
+ *
A character (type int). Control characters are printed in + * caret notation. If the parser is in UTF-8 mode, the character is formatted + * in UTF-8. Otherwise, special characters are printed with backslashes and + * octal notation.
+ *
%s
+ *
A string (type const char *).
+ *
%C
+ *
The argument is a string (type const char *). The first + * character in this string is printed. If the parser is in UTF-8 mode, this + * may involve multiple bytes.
+ *
%d
+ *
An integer (type int). Printed in decimal.
+ *
%O
+ *
The current option. No values are read from the argument list; the + * current option is defined in the Clp_Parser object itself.
+ *
%V
+ *
Like %O, but also includes the current value, + * if any.
+ *
%%
+ *
Prints a percent character.
+ *
%<
+ *
Prints an open quote string. In UTF-8 mode, prints a left single + * quote. Otherwise prints a single quote.
+ *
%>
+ *
Prints a closing quote string. In UTF-8 mode, prints a right single + * quote. Otherwise prints a single quote.
+ *
%,
+ *
Prints an apostrophe. In UTF-8 mode, prints a right single quote. + * Otherwise prints a single quote.
+ *
+ * + * Note that no flag characters, precision, or field width characters are + * currently supported. + * + * @sa Clp_SetErrorHandler + */ +int +Clp_OptionError(Clp_Parser *clp, const char *format, ...) +{ + Clp_BuildString bs; + va_list val; + va_start(val, format); + build_string_init(&bs, clp); + Clp_vbsprintf(clp, &bs, format, val); + append_build_string(&bs, "\n", 1); + va_end(val); + do_error(clp, &bs); + build_string_cleanup(&bs); + return bs.pos - bs.data; +} + +/** @param clp the parser + * @param f output file + * @param format error format + * + * Format an error message using @a format and additional arguments in the + * ellipsis, according to the Clp_OptionError formatting conventions. The + * resulting message is written to @a f. + * + * @sa Clp_OptionError */ +int +Clp_fprintf(Clp_Parser* clp, FILE* f, const char* format, ...) +{ + Clp_BuildString bs; + va_list val; + va_start(val, format); + build_string_init(&bs, NULL); + Clp_vbsprintf(clp, &bs, format, val); + va_end(val); + if (bs.pos != bs.data) + fwrite(bs.data, 1, bs.pos - bs.data, f); + build_string_cleanup(&bs); + return bs.pos - bs.data; +} + +/** @param clp the parser + * @param f output file + * @param format error format + * @param val arguments + * + * Format an error message using @a format and @a val, according to the + * Clp_OptionError formatting conventions. The resulting message is written + * to @a f. + * + * @sa Clp_OptionError */ +int +Clp_vfprintf(Clp_Parser* clp, FILE* f, const char* format, va_list val) +{ + Clp_BuildString bs; + build_string_init(&bs, NULL); + Clp_vbsprintf(clp, &bs, format, val); + if (bs.pos != bs.data) + fwrite(bs.data, 1, bs.pos - bs.data, f); + build_string_cleanup(&bs); + return bs.pos - bs.data; +} + +/** @param clp the parser + * @param str output string + * @param size size of output string + * @param format error format + * + * Format an error message from @a format and any additional arguments in + * the ellipsis, according to the Clp_OptionError formatting conventions. + * The resulting string is written to @a str. At most @a size characters are + * written to @a str, including a terminating null byte. The return value is + * the number of characters that would have been written (excluding the + * terminating null byte) if @a size were large enough to contain the entire + * string. + * + * @sa Clp_OptionError */ +int +Clp_vsnprintf(Clp_Parser* clp, char* str, size_t size, + const char* format, va_list val) +{ + Clp_BuildString bs; + build_string_init(&bs, NULL); + Clp_vbsprintf(clp, &bs, format, val); + if ((size_t) (bs.pos - bs.data) < size) { + memcpy(str, bs.data, bs.pos - bs.data); + str[bs.pos - bs.data] = 0; + } else { + memcpy(str, bs.data, size - 1); + str[size - 1] = 0; + } + build_string_cleanup(&bs); + return bs.pos - bs.data; +} + +static int +ambiguity_error(Clp_Parser *clp, int ambiguous, int *ambiguous_values, + const Clp_Option *opt, const Clp_InternOption *iopt, + const char *prefix, const char *fmt, ...) +{ + Clp_Internal *cli = clp->internal; + Clp_BuildString bs; + int i; + va_list val; + + va_start(val, fmt); + build_string_init(&bs, clp); + Clp_vbsprintf(clp, &bs, fmt, val); + append_build_string(&bs, "\n", 1); + + build_string_program_prefix(&bs, clp); + append_build_string(&bs, "(Possibilities are", -1); + + for (i = 0; i < ambiguous && i < MAX_AMBIGUOUS_VALUES; i++) { + int value = ambiguous_values[i]; + const char *no_dash = ""; + if (value < 0) + value = -(value + 1), no_dash = "no-"; + if (i == 0) + append_build_string(&bs, " ", 1); + else if (i == ambiguous - 1) + append_build_string(&bs, (i == 1 ? " and " : ", and "), -1); + else + append_build_string(&bs, ", ", 2); + if (!iopt[value].unquoted) + append_build_string(&bs, (cli->utf8 ? "\342\200\230" : "'"), -1); + append_build_string(&bs, prefix, -1); + append_build_string(&bs, no_dash, -1); + append_build_string(&bs, opt[value].long_name + iopt[value].ilongoff, -1); + if (!iopt[value].unquoted) + append_build_string(&bs, (cli->utf8 ? "\342\200\231" : "'"), -1); + } + + if (ambiguous > MAX_AMBIGUOUS_VALUES) + append_build_string(&bs, ", and others", -1); + append_build_string(&bs, ".)\n", -1); + va_end(val); + + do_error(clp, &bs); + build_string_cleanup(&bs); + return 0; +} + +static int +copy_string(char *buf, int buflen, int bufpos, const char *what) +{ + int l = strlen(what); + if (l > buflen - bufpos - 1) + l = buflen - bufpos - 1; + memcpy(buf + bufpos, what, l); + return l; +} + +/** @param clp the parser + * @param buf output buffer + * @param len length of output buffer + * @return number of characters written to the buffer, not including the terminating NUL + * + * A string that looks like the last option parsed by @a clp is extracted into + * @a buf. The correct option characters are put into the string first, + * followed by the option text. The output buffer is null-terminated unless + * @a len == 0. + * + * @sa Clp_CurOptionName + */ +int +Clp_CurOptionNameBuf(Clp_Parser *clp, char *buf, int len) +{ + Clp_Internal *cli = clp->internal; + int optno = cli->current_option; + int pos = 0; + if (optno < 0) + pos += copy_string(buf, len, pos, "(no current option!)"); + else if (cli->current_short) { + pos += copy_string(buf, len, pos, cli->option_chars); + if (cli->utf8) + pos = (encode_utf8(buf + pos, len - pos - 1, cli->opt[optno].short_name) - buf); + else if (pos < len - 1) + buf[pos++] = cli->opt[optno].short_name; + } else if (cli->negated_by_no) { + pos += copy_string(buf, len, pos, cli->option_chars); + pos += copy_string(buf, len, pos, "no-"); + pos += copy_string(buf, len, pos, cli->opt[optno].long_name + cli->iopt[optno].ilongoff); + } else { + pos += copy_string(buf, len, pos, cli->option_chars); + pos += copy_string(buf, len, pos, cli->opt[optno].long_name + cli->iopt[optno].ilongoff); + } + if (pos < len) + buf[pos] = 0; + return pos; +} + +/** @param clp the parser + * @return string describing the current option + * + * This function acts like Clp_CurOptionNameBuf(), but returns a pointer into + * a static buffer that will be rewritten on the next call to + * Clp_CurOptionName(). + * + * @note This function is not thread safe. + * + * @sa Clp_CurOptionName + */ +const char * +Clp_CurOptionName(Clp_Parser *clp) +{ + static char buf[256]; + Clp_CurOptionNameBuf(clp, buf, 256); + return buf; +} + +int +Clp_IsLong(Clp_Parser *clp, const char *long_name) +{ + Clp_Internal *cli = clp->internal; + int optno = cli->current_option; + return optno >= 0 && strcmp(cli->opt[optno].long_name, long_name) == 0; +} + +int +Clp_IsShort(Clp_Parser *clp, int short_name) +{ + Clp_Internal *cli = clp->internal; + int optno = cli->current_option; + return optno >= 0 && cli->opt[optno].short_name == short_name; +} + +#ifdef __cplusplus +} +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/fmalloc.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/fmalloc.c new file mode 100644 index 0000000..bddfca6 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/fmalloc.c @@ -0,0 +1,39 @@ +#if HAVE_CONFIG_H +# include +#endif +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif +extern const char* program_name; + +void* Gif_Realloc(void* p, size_t s, size_t n, const char* file, int line) { + (void) file, (void) line; + if (s == 0 || n == 0) { + Gif_Free(p); + return (void*) 0; + } else if (s == 1 || n == 1 || s <= ((size_t) -1) / n) { + p = realloc(p, s * n); + if (!p) { + fprintf(stderr, "%s: Out of memory, giving up\n", program_name); + exit(1); + } + return p; + } else { + fprintf(stderr, "%s: Out of memory, giving up (huge allocation)\n", program_name); + exit(1); + return (void*) 0; + } +} + +#undef Gif_Free +void Gif_Free(void* p) { + free(p); +} + +#ifdef __cplusplus +} +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/gifdiff.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifdiff.c new file mode 100644 index 0000000..ef7ac5e --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifdiff.c @@ -0,0 +1,632 @@ +/* gifdiff.c - Gifdiff compares GIF images for identical appearance. + Copyright (C) 1998-2025 Eddie Kohler, ekohler@gmail.com + This file is part of gifdiff, in the gifsicle package. + + Gifdiff is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#include +#include +#include +#include +#include +#include +#include +#if HAVE_UNISTD_H +# include +#endif + +#define QUIET_OPT 300 +#define HELP_OPT 301 +#define VERSION_OPT 302 +#define IGNORE_REDUNDANCY_OPT 303 +#define REDUNDANCY_OPT 304 +#define IGNORE_BACKGROUND_OPT 305 +#define BACKGROUND_OPT 306 + +const Clp_Option options[] = { + { "help", 'h', HELP_OPT, 0, 0 }, + { "brief", 'q', QUIET_OPT, 0, Clp_Negate }, + { "redundancy", 0, REDUNDANCY_OPT, 0, Clp_Negate }, + { "ignore-redundancy", 'w', IGNORE_REDUNDANCY_OPT, 0, Clp_Negate }, + { "bg", 0, BACKGROUND_OPT, 0, Clp_Negate }, + { "ignore-bg", 0, IGNORE_BACKGROUND_OPT, 0, Clp_Negate }, + { "background", 0, BACKGROUND_OPT, 0, Clp_Negate }, + { "ignore-background", 'B', IGNORE_BACKGROUND_OPT, 0, Clp_Negate }, + { "version", 'v', VERSION_OPT, 0, 0 } +}; + +const char *program_name; + +static const char *filename1; +static const char *filename2; + +static unsigned screen_width, screen_height; +#define TRANSP (0) + +static uint16_t *gdata[2]; +static uint16_t *glast[2]; +static uint16_t *scratch; +static uint16_t *line; + +static int brief; +static int ignore_redundancy; +static int ignore_background; + +static Clp_Parser* clp; + + +static void +combine_colormaps(Gif_Colormap *gfcm, Gif_Colormap *newcm) +{ + int i, gfcm_ncol = gfcm ? gfcm->ncol : 0; + for (i = 0; i < gfcm_ncol; i++) { + Gif_Color *c = &gfcm->col[i]; + c->pixel = Gif_AddColor(newcm, c, 1); + } +} + +static void +fill_area(uint16_t *data, int l, int t, int w, int h, uint16_t val) +{ + int x; + data += screen_width * t + l; + for (; h > 0; --h) { + for (x = w; x > 0; --x) + *data++ = val; + data += screen_width - w; + } +} + +static void +copy_area(uint16_t *dst, const uint16_t *src, int l, int t, int w, int h) +{ + dst += screen_width * t + l; + src += screen_width * t + l; + for (; h > 0; --h, dst += screen_width, src += screen_width) + memcpy(dst, src, sizeof(uint16_t) * w); +} + +static void +expand_bounds(int *lf, int *tp, int *rt, int *bt, const Gif_Image *gfi) +{ + int empty = (*lf >= *rt || *tp >= *bt); + if (empty || gfi->left < *lf) + *lf = gfi->left; + if (empty || gfi->top < *tp) + *tp = gfi->top; + if (empty || gfi->left + gfi->width > *rt) + *rt = gfi->left + gfi->width; + if (empty || gfi->top + gfi->height > *bt) + *bt = gfi->top + gfi->height; +} + + +static int +apply_image(int is_second, Gif_Stream *gfs, int imageno, uint16_t background) +{ + int i, x, y, any_change; + Gif_Image *gfi = gfs->images[imageno]; + Gif_Image *pgfi = imageno ? gfs->images[imageno - 1] : 0; + int width = gfi->width; + uint16_t map[256]; + uint16_t *data = gdata[is_second]; + uint16_t *last = glast[is_second]; + Gif_Colormap *gfcm = gfi->local ? gfi->local : gfs->global; + int gfcm_ncol = gfcm ? gfcm->ncol : 0; + + /* set up colormap */ + for (i = 0; i < gfcm_ncol; ++i) + map[i] = gfcm->col[i].pixel; + for (i = gfcm_ncol; i < 256; ++i) + map[i] = 1; + if (gfi->transparent >= 0 && gfi->transparent < 256) + map[gfi->transparent] = TRANSP; + + /* if this image's disposal is 'previous', save the post-disposal version in + 'scratch' */ + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) { + copy_area(scratch, data, gfi->left, gfi->top, gfi->width, gfi->height); + if (pgfi && pgfi->disposal == GIF_DISPOSAL_PREVIOUS) + copy_area(scratch, last, pgfi->left, pgfi->top, pgfi->width, pgfi->height); + else if (pgfi && pgfi->disposal == GIF_DISPOSAL_BACKGROUND) + fill_area(scratch, pgfi->left, pgfi->top, pgfi->width, pgfi->height, background); + } + + /* uncompress and clip */ + Gif_UncompressImage(gfs, gfi); + Gif_ClipImage(gfi, 0, 0, screen_width, screen_height); + + any_change = imageno == 0; + { + int lf = 0, tp = 0, rt = 0, bt = 0; + expand_bounds(&lf, &tp, &rt, &bt, gfi); + if (pgfi && pgfi->disposal == GIF_DISPOSAL_PREVIOUS) + expand_bounds(&lf, &tp, &rt, &bt, pgfi); + else if (pgfi && pgfi->disposal == GIF_DISPOSAL_BACKGROUND) { + expand_bounds(&lf, &tp, &rt, &bt, pgfi); + fill_area(last, pgfi->left, pgfi->top, pgfi->width, pgfi->height, background); + } else + pgfi = 0; + for (y = tp; y < bt; ++y) { + uint16_t *outd = data + screen_width * y + lf; + if (!any_change) + memcpy(line, outd, (rt - lf) * sizeof(uint16_t)); + if (pgfi && y >= pgfi->top && y < pgfi->top + pgfi->height) + memcpy(outd + pgfi->left - lf, + last + screen_width * y + pgfi->left, + pgfi->width * sizeof(uint16_t)); + if (y >= gfi->top && y < gfi->top + gfi->height) { + uint16_t *xoutd = outd + gfi->left - lf; + const uint8_t *ind = gfi->img[y - gfi->top]; + for (x = 0; x < width; ++x, ++ind, ++xoutd) + if (map[*ind] != TRANSP) + *xoutd = map[*ind]; + } + if (!any_change && memcmp(line, outd, (rt - lf) * sizeof(uint16_t)) != 0) + any_change = 1; + } + } + + Gif_ReleaseUncompressedImage(gfi); + Gif_ReleaseCompressedImage(gfi); + + /* switch 'glast' with 'scratch' if necessary */ + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) { + uint16_t *x = scratch; + scratch = glast[is_second]; + glast[is_second] = x; + } + + return any_change; +} + + +#define SAME 0 +#define DIFFERENT 1 +static int was_different; + +static void +different(const char *format, ...) +{ + va_list val; + va_start(val, format); + if (!brief) { + vfprintf(stdout, format, val); + fputc('\n', stdout); + } + va_end(val); + was_different = 1; +} + + +static void +name_loopcount(int loopcount, char *buf, size_t bufsz) +{ + if (loopcount < 0) + strcpy(buf, "none"); + else if (loopcount == 0) + strcpy(buf, "forever"); + else + snprintf(buf, bufsz, "%d", loopcount); +} + +static void +name_delay(int delay, char *buf, size_t bufsz) +{ + if (delay == 0) + strcpy(buf, "none"); + else + snprintf(buf, bufsz, "%d.%02ds", delay / 100, delay % 100); +} + +static void +name_color(int color, Gif_Colormap *gfcm, char *buf, size_t bufsz) +{ + if (color == TRANSP) + strcpy(buf, "transparent"); + else { + Gif_Color *c = &gfcm->col[color]; + snprintf(buf, bufsz, "#%02X%02X%02X", c->gfc_red, c->gfc_green, c->gfc_blue); + } +} + + +int +compare(Gif_Stream *s1, Gif_Stream *s2) +{ + Gif_Colormap *newcm; + int imageno1, imageno2, background1, background2; + char buf1[256], buf2[256], fbuf[256]; + + was_different = 0; + + /* Compare image counts and screen sizes. If either of these differs, quit + early. */ + Gif_CalculateScreenSize(s1, 0); + Gif_CalculateScreenSize(s2, 0); + + if (s1->screen_width != s2->screen_width + || s1->screen_height != s2->screen_height) { + different("screen sizes differ: <%dx%d >%dx%d", s1->screen_width, + s1->screen_height, s2->screen_width, s2->screen_height); + return DIFFERENT; + } + + if (s1->screen_width == 0 || s1->screen_height == 0 + || s2->screen_width == 0 || s2->screen_height == 0) { + /* paranoia -- don't think this can happen */ + different("zero screen sizes"); + return DIFFERENT; + } + + if (s1->nimages == 0 || s2->nimages == 0) { + if (s1->nimages != s2->nimages) { + different("frame counts differ: <#%d >#%d", s1->nimages, s2->nimages); + return DIFFERENT; + } else + return SAME; + } + + /* Create arrays for the image data */ + screen_width = s1->screen_width; + screen_height = s1->screen_height; + + gdata[0] = Gif_NewArray(uint16_t, screen_width * screen_height); + gdata[1] = Gif_NewArray(uint16_t, screen_width * screen_height); + glast[0] = Gif_NewArray(uint16_t, screen_width * screen_height); + glast[1] = Gif_NewArray(uint16_t, screen_width * screen_height); + scratch = Gif_NewArray(uint16_t, screen_width * screen_height); + line = Gif_NewArray(uint16_t, screen_width); + + /* Merge all distinct colors from the two images into one colormap, setting + the 'pixel' slots in the images' colormaps to the corresponding values + in the merged colormap. Don't forget transparency */ + newcm = Gif_NewFullColormap(1, 256); + combine_colormaps(s1->global, newcm); + combine_colormaps(s2->global, newcm); + for (imageno1 = 0; imageno1 < s1->nimages; ++imageno1) + combine_colormaps(s1->images[imageno1]->local, newcm); + for (imageno2 = 0; imageno2 < s2->nimages; ++imageno2) + combine_colormaps(s2->images[imageno2]->local, newcm); + + /* Choose the background values */ + background1 = background2 = TRANSP; + if ((s1->nimages == 0 || s1->images[0]->transparent < 0) + && s1->global && s1->background < s1->global->ncol) + background1 = s1->global->col[ s1->background ].pixel; + if ((s2->nimages == 0 || s2->images[0]->transparent < 0) + && s2->global && s2->background < s2->global->ncol) + background2 = s2->global->col[ s2->background ].pixel; + + /* Clear screens */ + fill_area(gdata[0], 0, 0, screen_width, screen_height, TRANSP); + fill_area(gdata[1], 0, 0, screen_width, screen_height, TRANSP); + + /* Loopcounts differ? */ + if (s1->loopcount != s2->loopcount) { + name_loopcount(s1->loopcount, buf1, sizeof(buf1)); + name_loopcount(s2->loopcount, buf2, sizeof(buf2)); + different("loop counts differ: <%s >%s", buf1, buf2); + } + + /* Loop over frames, comparing image data and delays */ + apply_image(0, s1, 0, background1); + apply_image(1, s2, 0, background2); + imageno1 = imageno2 = 0; + while (imageno1 != s1->nimages && imageno2 != s2->nimages) { + int fi1 = imageno1, fi2 = imageno2, + delay1 = s1->images[fi1]->delay, delay2 = s2->images[fi2]->delay; + + /* get message right */ + if (imageno1 == imageno2) + snprintf(fbuf, sizeof(fbuf), "#%d", imageno1); + else + snprintf(fbuf, sizeof(fbuf), "<#%d >#%d", imageno1, imageno2); + + /* compare pixels */ + if (memcmp(gdata[0], gdata[1], + screen_width * screen_height * sizeof(uint16_t)) != 0) { + unsigned d, c = screen_width * screen_height; + uint16_t *d1 = gdata[0], *d2 = gdata[1]; + for (d = 0; d < c; d++, d1++, d2++) + if (*d1 != *d2) { + name_color(*d1, newcm, buf1, sizeof(buf1)); + name_color(*d2, newcm, buf2, sizeof(buf2)); + different("frame %s pixels differ: %d,%d <%s >%s", + fbuf, d % screen_width, d / screen_width, buf1, buf2); + break; + } + } + + /* compare background */ + if (!ignore_background && background1 != background2 + && (imageno1 == 0 || s1->images[imageno1 - 1]->disposal == GIF_DISPOSAL_BACKGROUND) + && (imageno2 == 0 || s2->images[imageno2 - 1]->disposal == GIF_DISPOSAL_BACKGROUND)) { + unsigned d, c = screen_width * screen_height; + uint16_t *d1 = gdata[0], *d2 = gdata[1]; + for (d = 0; d < c; ++d, ++d1, ++d2) + if (*d1 == TRANSP || *d2 == TRANSP) { + name_color(background1, newcm, buf1, sizeof(buf1)); + name_color(background2, newcm, buf2, sizeof(buf2)); + different("frame %s background pixels differ: %d,%d <%s >%s", + fbuf, d % screen_width, d / screen_width, buf1, buf2); + background1 = background2 = TRANSP; + break; + } + } + + /* move to next images, skipping redundancy */ + for (++imageno1; + imageno1 < s1->nimages && !apply_image(0, s1, imageno1, background1); + ++imageno1) + delay1 += s1->images[imageno1]->delay; + for (++imageno2; + imageno2 < s2->nimages && !apply_image(1, s2, imageno2, background2); + ++imageno2) + delay2 += s2->images[imageno2]->delay; + + if (!ignore_redundancy) { + fi1 = (imageno1 - fi1) - (imageno2 - fi2); + for (; fi1 > 0; --fi1) + different("extra redundant frame: <#%d", imageno1 - fi1); + for (; fi1 < 0; ++fi1) + different("extra redundant frame: >#%d", imageno2 + fi1); + } + + if (delay1 != delay2) { + name_delay(delay1, buf1, sizeof(buf1)); + name_delay(delay2, buf2, sizeof(buf2)); + different("frame %s delays differ: <%s >%s", fbuf, buf1, buf2); + } + } + + if (imageno1 != s1->nimages || imageno2 != s2->nimages) + different("frame counts differ: <#%d >#%d", s1->nimages, s2->nimages); + + /* That's it! */ + Gif_DeleteColormap(newcm); + Gif_DeleteArray(gdata[0]); + Gif_DeleteArray(gdata[1]); + Gif_DeleteArray(glast[0]); + Gif_DeleteArray(glast[1]); + Gif_DeleteArray(scratch); + Gif_DeleteArray(line); + + return was_different ? DIFFERENT : SAME; +} + + +void short_usage(void) { + Clp_fprintf(clp, stderr, "Usage: %s [OPTION]... FILE1 FILE2\n\ +Try %<%s --help%> for more information.\n", + program_name, program_name); +} + +void usage(void) { + Clp_fprintf(clp, stdout, "\ +% compares two GIF files (either images or animations) for identical\n\ +visual appearance. An animation and an optimized version of the same animation\n\ +should compare as the same. Gifdiff exits with status 0 if the images are\n\ +the same, 1 if they%,re different, and 2 if there was some error.\n\ +\n\ +Usage: %s [OPTION]... FILE1 FILE2\n\n", program_name); + Clp_fprintf(clp, stdout, "\ +Options:\n\ + -q, --brief Don%,t report detailed differences.\n\ + -w, --ignore-redundancy Ignore differences in redundant frames.\n\ + -B, --ignore-background Ignore differences in background colors.\n\ + -h, --help Print this message and exit.\n\ + -v, --version Print version number and exit.\n\ +\n\ +Report bugs to .\n"); +} + + +void fatal_error(const char* format, ...) { + char buf[BUFSIZ]; + int n = snprintf(buf, BUFSIZ, "%s: ", program_name); + va_list val; + va_start(val, format); + Clp_vsnprintf(clp, buf + n, BUFSIZ - n, format, val); + va_end(val); + fputs(buf, stderr); + exit(2); /* exit(2) for trouble */ +} + +void error(const char* format, ...) { + char buf[BUFSIZ]; + int n = snprintf(buf, BUFSIZ, "%s: ", program_name); + va_list val; + va_start(val, format); + Clp_vsnprintf(clp, buf + n, BUFSIZ - n, format, val); + va_end(val); + fputs(buf, stderr); +} + +static int gifread_error_count; + +static void +gifread_error(Gif_Stream* gfs, Gif_Image* gfi, + int is_error, const char *message) +{ + static int last_is_error = 0; + static int last_which_image = 0; + static char last_message[256]; + static int different_error_count = 0; + static int same_error_count = 0; + int which_image = Gif_ImageNumber(gfs, gfi); + const char *filename = gfs->landmark; + if (which_image < 0) + which_image = gfs->nimages; + + if (gifread_error_count == 0) { + last_which_image = -1; + last_message[0] = 0; + different_error_count = 0; + } + + gifread_error_count++; + if (last_message[0] && different_error_count <= 10 + && (last_which_image != which_image || message == 0 + || strcmp(message, last_message) != 0)) { + const char *etype = last_is_error ? "error" : "warning"; + error("While reading %<%s%> frame #%d:\n", filename, last_which_image); + if (same_error_count == 1) + error(" %s: %s\n", etype, last_message); + else if (same_error_count > 0) + error(" %s: %s (%d times)\n", etype, last_message, same_error_count); + same_error_count = 0; + last_message[0] = 0; + } + + if (message) { + if (last_message[0] == 0) + different_error_count++; + same_error_count++; + strcpy(last_message, message); + last_which_image = which_image; + last_is_error = is_error; + } else + last_message[0] = 0; + + if (different_error_count == 11 && message) { + error("(more errors while reading %<%s%>)\n", filename); + different_error_count++; + } +} + +static Gif_Stream * +read_stream(const char **filename) +{ + FILE *f; + Gif_Stream *gfs; + if (*filename == 0) { +#if 0 + /* Since gifdiff always takes explicit filename arguments, + allow explicit reads from terminal. */ +#ifndef OUTPUT_GIF_TO_TERMINAL + if (isatty(fileno(stdin))) { + fatal_error(": is a terminal\n"); + return NULL; + } +#endif +#endif + f = stdin; +#if defined(_MSDOS) || defined(_WIN32) + _setmode(_fileno(stdin), _O_BINARY); +#elif defined(__DJGPP__) + setmode(fileno(stdin), O_BINARY); +#elif defined(__EMX__) + _fsetmode(stdin, "b"); +#endif + *filename = ""; + } else { + f = fopen(*filename, "rb"); + if (!f) + fatal_error("%s: %s\n", *filename, strerror(errno)); + } + gifread_error_count = 0; + gfs = Gif_FullReadFile(f, GIF_READ_COMPRESSED, *filename, gifread_error); + if (!gfs) + fatal_error("%s: file not in GIF format\n", *filename); + return gfs; +} + +int +main(int argc, char *argv[]) +{ + int how_many_inputs = 0; + int status; + const char **inputp; + Gif_Stream *gfs1, *gfs2; + + clp = Clp_NewParser(argc, (const char * const *)argv, + sizeof(options) / sizeof(options[0]), options); + + program_name = Clp_ProgramName(clp); + brief = 0; + + while (1) { + int opt = Clp_Next(clp); + switch (opt) { + + case HELP_OPT: + usage(); + exit(0); + break; + + case VERSION_OPT: + printf("gifdiff (LCDF Gifsicle) %s\n", VERSION); + printf("Copyright (C) 1998-2025 Eddie Kohler\n\ +This is free software; see the source for copying conditions.\n\ +There is NO warranty, not even for merchantability or fitness for a\n\ +particular purpose.\n"); + exit(0); + break; + + case QUIET_OPT: + brief = !clp->negated; + break; + + case IGNORE_REDUNDANCY_OPT: + ignore_redundancy = !clp->negated; + break; + + case REDUNDANCY_OPT: + ignore_redundancy = !!clp->negated; + break; + + case IGNORE_BACKGROUND_OPT: + ignore_background = !clp->negated; + break; + + case BACKGROUND_OPT: + ignore_background = !!clp->negated; + break; + + case Clp_NotOption: + if (how_many_inputs == 2) { + error("too many file arguments\n"); + goto bad_option; + } + inputp = (how_many_inputs == 0 ? &filename1 : &filename2); + how_many_inputs++; + if (strcmp(clp->vstr, "-") == 0) + *inputp = 0; + else + *inputp = clp->vstr; + break; + + bad_option: + case Clp_BadOption: + short_usage(); + exit(1); + break; + + case Clp_Done: + goto done; + + } + } + + done: + + if (how_many_inputs < 2) + fatal_error("need exactly 2 file arguments\n"); + if (filename1 == 0 && filename2 == 0) + fatal_error("can%,t read both files from stdin\n"); + + gfs1 = read_stream(&filename1); + gfs2 = read_stream(&filename2); + + status = (compare(gfs1, gfs2) == DIFFERENT); + if (status == 1 && brief) + printf("GIF files %s and %s differ\n", filename1, filename2); + + Gif_DeleteStream(gfs1); + Gif_DeleteStream(gfs2); + return status; +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/giffunc.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/giffunc.c new file mode 100644 index 0000000..3ddf2df --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/giffunc.c @@ -0,0 +1,844 @@ +/* giffunc.c - General functions for the GIF library. + Copyright (C) 1997-2021 Eddie Kohler, ekohler@gmail.com + This file is part of the LCDF GIF library. + + The LCDF GIF library is free software. It is distributed under the GNU + General Public License, version 2; you can copy, distribute, or alter it at + will, as long as this notice is kept intact and this source code is made + available. There is no warranty, express or implied. */ + +#if HAVE_CONFIG_H +# include +#endif +#include +#include +#include +#ifdef __cplusplus +extern "C" { +#endif + + +Gif_Stream * +Gif_NewStream(void) +{ + Gif_Stream *gfs = Gif_New(Gif_Stream); + if (!gfs) + return 0; + gfs->images = 0; + gfs->nimages = gfs->imagescap = 0; + gfs->global = 0; + gfs->background = 256; + gfs->screen_width = gfs->screen_height = 0; + gfs->loopcount = -1; + gfs->end_comment = 0; + gfs->end_extension_list = 0; + gfs->errors = 0; + gfs->user_flags = 0; + gfs->refcount = 0; + gfs->landmark = 0; + return gfs; +} + + +Gif_Image * +Gif_NewImage(void) +{ + Gif_Image *gfi = Gif_New(Gif_Image); + if (!gfi) + return 0; + gfi->width = gfi->height = 0; + gfi->img = 0; + gfi->image_data = 0; + gfi->left = gfi->top = 0; + gfi->delay = 0; + gfi->disposal = GIF_DISPOSAL_NONE; + gfi->interlace = 0; + gfi->local = 0; + gfi->transparent = -1; + gfi->user_flags = 0; + gfi->identifier = 0; + gfi->comment = 0; + gfi->extension_list = 0; + gfi->free_image_data = Gif_Free; + gfi->compressed_len = 0; + gfi->compressed_errors = 0; + gfi->compressed = 0; + gfi->free_compressed = 0; + gfi->user_data = 0; + gfi->free_user_data = 0; + gfi->refcount = 0; + return gfi; +} + + +Gif_Colormap * +Gif_NewColormap(void) +{ + return Gif_NewFullColormap(0, 256); +} + + +Gif_Colormap * +Gif_NewFullColormap(int count, int capacity) +{ + Gif_Colormap *gfcm = Gif_New(Gif_Colormap); + if (!gfcm || count < 0) { + Gif_Delete(gfcm); + return 0; + } + if (count > capacity) + capacity = count; + if (capacity < 256) + capacity = 256; + gfcm->ncol = count; + gfcm->capacity = capacity; + gfcm->col = Gif_NewArray(Gif_Color, capacity); + gfcm->refcount = 0; + gfcm->user_flags = 0; + if (!gfcm->col) { + Gif_Delete(gfcm); + return 0; + } + return gfcm; +} + + +Gif_Comment * +Gif_NewComment(void) +{ + Gif_Comment *gfcom = Gif_New(Gif_Comment); + if (!gfcom) + return 0; + gfcom->str = 0; + gfcom->len = 0; + gfcom->count = gfcom->cap = 0; + return gfcom; +} + + +Gif_Extension * +Gif_NewExtension(int kind, const char* appname, int applength) +{ + Gif_Extension *gfex = Gif_New(Gif_Extension); + if (!gfex) + return 0; + gfex->kind = kind; + if (appname) { + gfex->appname = (char*) Gif_NewArray(char, applength + 1); + if (!gfex->appname) { + Gif_Delete(gfex); + return 0; + } + memcpy(gfex->appname, appname, applength); + gfex->appname[applength] = 0; + gfex->applength = applength; + } else { + gfex->appname = 0; + gfex->applength = 0; + } + gfex->data = 0; + gfex->stream = 0; + gfex->image = 0; + gfex->next = 0; + gfex->free_data = 0; + gfex->packetized = 0; + return gfex; +} + +Gif_Extension* +Gif_CopyExtension(Gif_Extension* src) +{ + Gif_Extension* dst = Gif_NewExtension(src->kind, src->appname, src->applength); + if (!dst) + return NULL; + if (!src->data || !src->free_data) { + dst->data = src->data; + dst->length = src->length; + } else { + dst->data = Gif_NewArray(uint8_t, src->length); + if (!dst->data) { + Gif_DeleteExtension(dst); + return NULL; + } + memcpy(dst->data, src->data, src->length); + dst->length = src->length; + dst->free_data = Gif_Free; + } + dst->packetized = src->packetized; + return dst; +} + + +char * +Gif_CopyString(const char *s) +{ + int l; + char *copy; + if (!s) + return 0; + l = strlen(s); + copy = Gif_NewArray(char, l + 1); + if (!copy) + return 0; + memcpy(copy, s, l + 1); + return copy; +} + + +int +Gif_AddImage(Gif_Stream *gfs, Gif_Image *gfi) +{ + if (gfs->nimages >= gfs->imagescap) { + if (gfs->imagescap) + gfs->imagescap *= 2; + else + gfs->imagescap = 2; + Gif_ReArray(gfs->images, Gif_Image *, gfs->imagescap); + if (!gfs->images) + return 0; + } + gfs->images[gfs->nimages] = gfi; + gfs->nimages++; + gfi->refcount++; + return 1; +} + + +void +Gif_RemoveImage(Gif_Stream *gfs, int inum) +{ + int j; + if (inum < 0 || inum >= gfs->nimages) + return; + Gif_DeleteImage(gfs->images[inum]); + for (j = inum; j < gfs->nimages - 1; j++) + gfs->images[j] = gfs->images[j+1]; + gfs->nimages--; +} + + +int +Gif_ImageColorBound(const Gif_Image* gfi) +{ + if (gfi->compressed && gfi->compressed[0] > 0 && gfi->compressed[0] < 8) + return 1 << gfi->compressed[0]; + else + return 256; +} + + +int +Gif_AddCommentTake(Gif_Comment *gfcom, char *x, int xlen) +{ + if (gfcom->count >= gfcom->cap) { + if (gfcom->cap) + gfcom->cap *= 2; + else + gfcom->cap = 2; + Gif_ReArray(gfcom->str, char *, gfcom->cap); + Gif_ReArray(gfcom->len, int, gfcom->cap); + if (!gfcom->str || !gfcom->len) + return 0; + } + if (xlen < 0) + xlen = strlen(x); + gfcom->str[ gfcom->count ] = x; + gfcom->len[ gfcom->count ] = xlen; + gfcom->count++; + return 1; +} + + +int +Gif_AddComment(Gif_Comment *gfcom, const char *x, int xlen) +{ + char *new_x; + if (xlen < 0) + xlen = strlen(x); + new_x = Gif_NewArray(char, xlen); + if (!new_x) + return 0; + memcpy(new_x, x, xlen); + if (Gif_AddCommentTake(gfcom, new_x, xlen) == 0) { + Gif_DeleteArray(new_x); + return 0; + } else + return 1; +} + + +int +Gif_AddExtension(Gif_Stream* gfs, Gif_Image* gfi, Gif_Extension* gfex) +{ + Gif_Extension **pprev; + if (gfex->stream || gfex->image) + return 0; + pprev = gfi ? &gfi->extension_list : &gfs->end_extension_list; + while (*pprev) + pprev = &(*pprev)->next; + *pprev = gfex; + gfex->stream = gfs; + gfex->image = gfi; + gfex->next = 0; + return 1; +} + + +int +Gif_ImageNumber(Gif_Stream *gfs, Gif_Image *gfi) +{ + int i; + if (gfs && gfi) + for (i = 0; i != gfs->nimages; ++i) + if (gfs->images[i] == gfi) + return i; + return -1; +} + + +void +Gif_CalculateScreenSize(Gif_Stream *gfs, int force) +{ + int i; + int screen_width = force ? 0 : gfs->screen_width; + int screen_height = force ? 0 : gfs->screen_height; + + for (i = 0; i < gfs->nimages && (force || i == 0); ++i) { + Gif_Image *gfi = gfs->images[i]; + if (screen_width < gfi->left + gfi->width) + screen_width = gfi->left + gfi->width; + if (screen_height < gfi->top + gfi->height) + screen_height = gfi->top + gfi->height; + } + + if (gfs->screen_width < screen_width || force) + gfs->screen_width = screen_width; + if (gfs->screen_height < screen_height || force) + gfs->screen_height = screen_height; +} + + +Gif_Stream * +Gif_CopyStreamSkeleton(Gif_Stream *gfs) +{ + Gif_Stream *ngfs = Gif_NewStream(); + if (!ngfs) + return 0; + ngfs->global = Gif_CopyColormap(gfs->global); + ngfs->background = gfs->background; + ngfs->screen_width = gfs->screen_width; + ngfs->screen_height = gfs->screen_height; + ngfs->loopcount = gfs->loopcount; + if (gfs->global && !ngfs->global) { + Gif_DeleteStream(ngfs); + return 0; + } else + return ngfs; +} + + +Gif_Stream * +Gif_CopyStreamImages(Gif_Stream *gfs) +{ + Gif_Stream *ngfs = Gif_CopyStreamSkeleton(gfs); + int i; + if (!ngfs) + return 0; + for (i = 0; i < gfs->nimages; i++) { + Gif_Image *gfi = Gif_CopyImage(gfs->images[i]); + if (!gfi || !Gif_AddImage(ngfs, gfi)) { + Gif_DeleteStream(ngfs); + return 0; + } + } + return ngfs; +} + + +Gif_Colormap * +Gif_CopyColormap(Gif_Colormap *src) +{ + Gif_Colormap *dest; + if (!src) + return 0; + + dest = Gif_NewFullColormap(src->ncol, src->capacity); + if (!dest) + return 0; + + memcpy(dest->col, src->col, sizeof(src->col[0]) * src->ncol); + return dest; +} + + +Gif_Image * +Gif_CopyImage(Gif_Image *src) +{ + Gif_Image *dest; + uint8_t *data; + int i; + if (!src) + return 0; + + dest = Gif_NewImage(); + if (!dest) + return 0; + + dest->identifier = Gif_CopyString(src->identifier); + if (!dest->identifier && src->identifier) + goto failure; + if (src->comment) { + dest->comment = Gif_NewComment(); + if (!dest->comment) + goto failure; + for (i = 0; i < src->comment->count; i++) + if (!Gif_AddComment(dest->comment, src->comment->str[i], + src->comment->len[i])) + goto failure; + } + if (src->extension_list) { + Gif_Extension* gfex = src->extension_list; + while (gfex) { + Gif_Extension* dstex = Gif_CopyExtension(gfex); + if (!dstex) + goto failure; + Gif_AddExtension(NULL, dest, dstex); + gfex = gfex->next; + } + } + + dest->local = Gif_CopyColormap(src->local); + if (!dest->local && src->local) + goto failure; + dest->transparent = src->transparent; + + dest->delay = src->delay; + dest->disposal = src->disposal; + dest->left = src->left; + dest->top = src->top; + + dest->width = src->width; + dest->height = src->height; + + dest->interlace = src->interlace; + if (src->img) { + dest->img = Gif_NewArray(uint8_t *, dest->height + 1); + dest->image_data = Gif_NewArray(uint8_t, (size_t) dest->width * (size_t) dest->height); + dest->free_image_data = Gif_Free; + if (!dest->img || !dest->image_data) + goto failure; + for (i = 0, data = dest->image_data; i < dest->height; i++) { + memcpy(data, src->img[i], dest->width); + dest->img[i] = data; + data += dest->width; + } + dest->img[dest->height] = 0; + } + if (src->compressed) { + if (src->free_compressed == 0) + dest->compressed = src->compressed; + else { + dest->compressed = Gif_NewArray(uint8_t, src->compressed_len); + dest->free_compressed = Gif_Free; + memcpy(dest->compressed, src->compressed, src->compressed_len); + } + dest->compressed_len = src->compressed_len; + dest->compressed_errors = src->compressed_errors; + } + + return dest; + + failure: + Gif_DeleteImage(dest); + return 0; +} + + +void Gif_MakeImageEmpty(Gif_Image* gfi) { + Gif_ReleaseUncompressedImage(gfi); + Gif_ReleaseCompressedImage(gfi); + gfi->left = gfi->left < 0xFFFE ? gfi->left : 0xFFFE; + gfi->top = gfi->top < 0xFFFE ? gfi->top : 0xFFFE; + gfi->width = 1; + gfi->height = 1; + gfi->transparent = 0; + Gif_CreateUncompressedImage(gfi, 0); + gfi->img[0][0] = 0; +} + + +/** DELETION **/ + +typedef struct Gif_DeletionHook { + int kind; + Gif_DeletionHookFunc func; + void *callback_data; + struct Gif_DeletionHook *next; +} Gif_DeletionHook; + +static Gif_DeletionHook *all_hooks; + +void +Gif_DeleteStream(Gif_Stream *gfs) +{ + Gif_DeletionHook *hook; + int i; + if (!gfs || --gfs->refcount > 0) + return; + + for (i = 0; i < gfs->nimages; i++) + Gif_DeleteImage(gfs->images[i]); + Gif_DeleteArray(gfs->images); + + Gif_DeleteColormap(gfs->global); + + Gif_DeleteComment(gfs->end_comment); + while (gfs->end_extension_list) + Gif_DeleteExtension(gfs->end_extension_list); + + for (hook = all_hooks; hook; hook = hook->next) + if (hook->kind == GIF_T_STREAM) + (*hook->func)(GIF_T_STREAM, gfs, hook->callback_data); + Gif_Delete(gfs); +} + + +void +Gif_DeleteImage(Gif_Image *gfi) +{ + Gif_DeletionHook *hook; + if (!gfi || --gfi->refcount > 0) + return; + + for (hook = all_hooks; hook; hook = hook->next) + if (hook->kind == GIF_T_IMAGE) + (*hook->func)(GIF_T_IMAGE, gfi, hook->callback_data); + + Gif_DeleteArray(gfi->identifier); + Gif_DeleteComment(gfi->comment); + while (gfi->extension_list) + Gif_DeleteExtension(gfi->extension_list); + Gif_DeleteColormap(gfi->local); + if (gfi->image_data && gfi->free_image_data) + (*gfi->free_image_data)((void *)gfi->image_data); + Gif_DeleteArray(gfi->img); + if (gfi->compressed && gfi->free_compressed) + (*gfi->free_compressed)((void *)gfi->compressed); + if (gfi->user_data && gfi->free_user_data) + (*gfi->free_user_data)(gfi->user_data); + Gif_Delete(gfi); +} + + +void +Gif_DeleteColormap(Gif_Colormap *gfcm) +{ + Gif_DeletionHook *hook; + if (!gfcm || --gfcm->refcount > 0) + return; + + for (hook = all_hooks; hook; hook = hook->next) + if (hook->kind == GIF_T_COLORMAP) + (*hook->func)(GIF_T_COLORMAP, gfcm, hook->callback_data); + + Gif_DeleteArray(gfcm->col); + Gif_Delete(gfcm); +} + + +void +Gif_DeleteComment(Gif_Comment *gfcom) +{ + int i; + if (!gfcom) + return; + for (i = 0; i < gfcom->count; i++) + Gif_DeleteArray(gfcom->str[i]); + Gif_DeleteArray(gfcom->str); + Gif_DeleteArray(gfcom->len); + Gif_Delete(gfcom); +} + + +void +Gif_DeleteExtension(Gif_Extension *gfex) +{ + if (!gfex) + return; + if (gfex->data && gfex->free_data) + (*gfex->free_data)(gfex->data); + Gif_DeleteArray(gfex->appname); + if (gfex->stream || gfex->image) { + Gif_Extension** pprev; + if (gfex->image) + pprev = &gfex->image->extension_list; + else + pprev = &gfex->stream->end_extension_list; + while (*pprev && *pprev != gfex) + pprev = &(*pprev)->next; + if (*pprev) + *pprev = gfex->next; + } + Gif_Delete(gfex); +} + + +/** DELETION HOOKS **/ + +int +Gif_AddDeletionHook(int kind, void (*func)(int, void *, void *), void *cb) +{ + Gif_DeletionHook *hook = Gif_New(Gif_DeletionHook); + if (!hook) + return 0; + Gif_RemoveDeletionHook(kind, func, cb); + hook->kind = kind; + hook->func = func; + hook->callback_data = cb; + hook->next = all_hooks; + all_hooks = hook; + return 1; +} + +void +Gif_RemoveDeletionHook(int kind, void (*func)(int, void *, void *), void *cb) +{ + Gif_DeletionHook *hook = all_hooks, *prev = 0; + while (hook) { + if (hook->kind == kind && hook->func == func + && hook->callback_data == cb) { + if (prev) + prev->next = hook->next; + else + all_hooks = hook->next; + Gif_Delete(hook); + return; + } + prev = hook; + hook = hook->next; + } +} + + +int +Gif_ColorEq(Gif_Color *c1, Gif_Color *c2) +{ + return GIF_COLOREQ(c1, c2); +} + + +int +Gif_FindColor(Gif_Colormap *gfcm, Gif_Color *c) +{ + int i; + for (i = 0; i < gfcm->ncol; i++) + if (GIF_COLOREQ(&gfcm->col[i], c)) + return i; + return -1; +} + + +int +Gif_AddColor(Gif_Colormap *gfcm, Gif_Color *c, int look_from) +{ + int i; + if (look_from >= 0) + for (i = look_from; i < gfcm->ncol; i++) + if (GIF_COLOREQ(&gfcm->col[i], c)) + return i; + if (gfcm->ncol >= gfcm->capacity) { + gfcm->capacity *= 2; + Gif_ReArray(gfcm->col, Gif_Color, gfcm->capacity); + if (gfcm->col == 0) + return -1; + } + i = gfcm->ncol; + gfcm->ncol++; + gfcm->col[i] = *c; + return i; +} + + +Gif_Image * +Gif_GetImage(Gif_Stream *gfs, int imagenumber) +{ + if (imagenumber >= 0 && imagenumber < gfs->nimages) + return gfs->images[imagenumber]; + else + return 0; +} + + +Gif_Image * +Gif_GetNamedImage(Gif_Stream *gfs, const char *name) +{ + int i; + + if (!name) + return gfs->nimages ? gfs->images[0] : 0; + + for (i = 0; i < gfs->nimages; i++) + if (gfs->images[i]->identifier && + strcmp(gfs->images[i]->identifier, name) == 0) + return gfs->images[i]; + + return 0; +} + + +void +Gif_ReleaseCompressedImage(Gif_Image *gfi) +{ + if (gfi->compressed && gfi->free_compressed) + (*gfi->free_compressed)(gfi->compressed); + gfi->compressed = 0; + gfi->compressed_len = 0; + gfi->compressed_errors = 0; + gfi->free_compressed = 0; +} + +void +Gif_ReleaseUncompressedImage(Gif_Image *gfi) +{ + Gif_DeleteArray(gfi->img); + if (gfi->image_data && gfi->free_image_data) + (*gfi->free_image_data)(gfi->image_data); + gfi->img = 0; + gfi->image_data = 0; + gfi->free_image_data = 0; +} + + +int +Gif_ClipImage(Gif_Image *gfi, int left, int top, int width, int height) +{ + int new_width = gfi->width, new_height = gfi->height; + int y; + + if (!gfi->img) + return 0; + + if (gfi->left < left) { + int shift = left - gfi->left; + for (y = 0; y < gfi->height; y++) + gfi->img[y] += shift; + gfi->left += shift; + new_width -= shift; + } + + if (gfi->top < top) { + int shift = top - gfi->top; + for (y = gfi->height - 1; y >= shift; y++) + gfi->img[y - shift] = gfi->img[y]; + gfi->top += shift; + new_height -= shift; + } + + if (gfi->left + new_width >= width) + new_width = width - gfi->left; + + if (gfi->top + new_height >= height) + new_height = height - gfi->top; + + if (new_width < 0) + new_width = 0; + if (new_height < 0) + new_height = 0; + gfi->width = new_width; + gfi->height = new_height; + return 1; +} + + +int +Gif_InterlaceLine(int line, int height) +{ + height--; + if (line > height / 2) + return line * 2 - ( height | 1); + else if (line > height / 4) + return line * 4 - ((height & ~1) | 2); + else if (line > height / 8) + return line * 8 - ((height & ~3) | 4); + else + return line * 8; +} + + +int +Gif_SetUncompressedImage(Gif_Image *gfi, uint8_t *image_data, + void (*free_data)(void *), int data_interlaced) +{ + /* NB does not affect compressed image (and must not) */ + unsigned i; + unsigned width = gfi->width; + unsigned height = gfi->height; + uint8_t **img; + + Gif_ReleaseUncompressedImage(gfi); + if (!image_data) + return 0; + + img = Gif_NewArray(uint8_t *, height + 1); + if (!img) + return 0; + + if (data_interlaced) + for (i = 0; i < height; i++) + img[ Gif_InterlaceLine(i, height) ] = image_data + width * i; + else + for (i = 0; i < height; i++) + img[i] = image_data + width * i; + img[height] = 0; + + gfi->img = img; + gfi->image_data = image_data; + gfi->free_image_data = free_data; + return 1; +} + +int +Gif_CreateUncompressedImage(Gif_Image *gfi, int data_interlaced) +{ + size_t sz = (size_t) gfi->width * (size_t) gfi->height; + uint8_t *data = Gif_NewArray(uint8_t, sz ? sz : 1); + return Gif_SetUncompressedImage(gfi, data, Gif_Free, data_interlaced); +} + +void +Gif_InitCompressInfo(Gif_CompressInfo *gcinfo) +{ + gcinfo->flags = 0; + gcinfo->loss = 0; +} + + +void +Gif_Debug(char *x, ...) +{ + va_list val; + va_start(val, x); + vfprintf(stderr, x, val); + va_end(val); +} + + +#if !GIF_ALLOCATOR_DEFINED +void* Gif_Realloc(void* p, size_t s, size_t n, const char* file, int line) { + (void) file, (void) line; + if (s == 0 || n == 0) + Gif_Free(p); + else if (s == 1 || n == 1 || s <= ((size_t) -1) / n) + return realloc(p, s * n); + return (void*) 0; +} + +#undef Gif_Free +void Gif_Free(void* p) { + free(p); +} +#endif + +#ifdef __cplusplus +} +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/gifread.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifread.c new file mode 100644 index 0000000..6be5e18 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifread.c @@ -0,0 +1,979 @@ +/* gifread.c - Functions to read GIFs. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of the LCDF GIF library. + + The LCDF GIF library is free software. It is distributed under the GNU + General Public License, version 2; you can copy, distribute, or alter it at + will, as long as this notice is kept intact and this source code is made + available. There is no warranty, express or implied. */ + +#if HAVE_CONFIG_H +# include +#elif !defined(__cplusplus) +/* Assume we don't have inline by default */ +# define inline +#endif +#include +#include +#include +#include +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + + Gif_Stream *stream; + + Gif_Code *prefix; + uint8_t *suffix; + uint16_t *length; + + uint16_t width; + uint16_t height; + + uint8_t *image; + uint8_t *maximage; + + unsigned decodepos; + + Gif_Image* gfi; + Gif_ReadErrorHandler handler; + int errors[2]; + +} Gif_Context; + + +typedef struct Gif_Reader { + FILE *f; + const uint8_t *v; + uint32_t pos; + uint32_t length; + int is_record; + int is_eoi; + uint8_t (*byte_getter)(struct Gif_Reader *); + uint32_t (*block_getter)(uint8_t*, uint32_t, struct Gif_Reader*); + int (*eofer)(struct Gif_Reader *); +} Gif_Reader; + +static Gif_ReadErrorHandler default_error_handler = 0; + + +#define gifgetc(grr) ((char)(*grr->byte_getter)(grr)) +#define gifgetbyte(grr) ((*grr->byte_getter)(grr)) +#define gifgetblock(ptr, size, grr) ((*grr->block_getter)(ptr, size, grr)) +#define gifeof(grr) ((*grr->eofer)(grr)) + +static inline uint16_t +gifgetunsigned(Gif_Reader *grr) +{ + uint8_t one = gifgetbyte(grr); + uint8_t two = gifgetbyte(grr); + return one | (two << 8); +} + + +static uint8_t +file_byte_getter(Gif_Reader *grr) +{ + int i = getc(grr->f); + if (i != EOF) { + ++grr->pos; + return i; + } else + return 0; +} + +static uint32_t +file_block_getter(uint8_t *p, uint32_t s, Gif_Reader *grr) +{ + size_t nread = fread(p, 1, s, grr->f); + if (nread < s) + memset(p + nread, 0, s - nread); + grr->pos += nread; + return nread; +} + +static int +file_eofer(Gif_Reader *grr) +{ + int c = getc(grr->f); + if (c == EOF) + return 1; + else { + ungetc(c, grr->f); + return 0; + } +} + + +static uint8_t +record_byte_getter(Gif_Reader *grr) +{ + if (grr->pos < grr->length) + return grr->v[grr->pos++]; + else + return 0; +} + +static uint32_t +record_block_getter(uint8_t *p, uint32_t s, Gif_Reader *grr) +{ + uint32_t ncopy = (grr->pos + s <= grr->length ? s : grr->length - grr->pos); + memcpy(p, &grr->v[grr->pos], ncopy); + grr->pos += ncopy; + if (ncopy < s) + memset(p + ncopy, 0, s - ncopy); + return ncopy; +} + +static int +record_eofer(Gif_Reader *grr) +{ + return grr->pos == grr->length; +} + + +static void +make_data_reader(Gif_Reader *grr, const uint8_t *data, uint32_t length) +{ + grr->v = data; + grr->pos = 0; + grr->length = length; + grr->is_record = 1; + grr->byte_getter = record_byte_getter; + grr->block_getter = record_block_getter; + grr->eofer = record_eofer; +} + + +static void +gif_read_error(Gif_Context *gfc, int is_error, const char *text) +{ + Gif_ReadErrorHandler handler = gfc->handler ? gfc->handler : default_error_handler; + if (is_error >= 0) + gfc->errors[is_error > 0] += 1; + if (handler) + handler(gfc->stream, gfc->gfi, is_error, text); +} + + +static uint8_t +one_code(Gif_Context *gfc, Gif_Code code) +{ + uint8_t *suffixes = gfc->suffix; + Gif_Code *prefixes = gfc->prefix; + uint8_t *ptr; + int lastsuffix = 0; + int codelength = gfc->length[code]; + + gfc->decodepos += codelength; + ptr = gfc->image + gfc->decodepos; + while (codelength > 0) { + lastsuffix = suffixes[code]; + code = prefixes[code]; + --ptr; + if (ptr < gfc->maximage) + *ptr = lastsuffix; + --codelength; + } + + /* return the first pixel in the code, which, since we walked backwards + through the code, was the last suffix we processed. */ + return lastsuffix; +} + +static int +read_image_block(Gif_Reader *grr, uint8_t *buffer, int *bit_pos_store, + int *bit_len_store, int bits_needed) +{ + int bit_position = *bit_pos_store; + int bit_length = *bit_len_store; + uint8_t block_len; + + while (bit_position + bits_needed > bit_length) { + /* Read in the next data block. */ + if (bit_position >= 8) { + /* Need to shift down the upper, unused part of 'buffer' */ + int i = bit_position / 8; + buffer[0] = buffer[i]; + buffer[1] = buffer[i+1]; + bit_position -= i * 8; + bit_length -= i * 8; + } + block_len = gifgetbyte(grr); + GIF_DEBUG(("\nimage_block(%d) ", block_len)); + if (block_len == 0) return 0; + gifgetblock(buffer + bit_length / 8, block_len, grr); + bit_length += block_len * 8; + } + + *bit_pos_store = bit_position; + *bit_len_store = bit_length; + return 1; +} + + +static void +read_image_data(Gif_Context *gfc, Gif_Reader *grr) +{ + /* we need a bit more than GIF_MAX_BLOCK in case a single code is split + across blocks */ + uint8_t buffer[GIF_MAX_BLOCK + 5]; + int i; + uint32_t accum; + + int bit_position; + int bit_length; + + Gif_Code code; + Gif_Code old_code; + Gif_Code clear_code; /* eoi_code == clear_code + 1 */ + Gif_Code next_code; +#define CUR_BUMP_CODE (1 << bits_needed) +#define CUR_CODE_MASK ((1 << bits_needed) - 1) + + int min_code_size; + int bits_needed; + + gfc->decodepos = 0; + + min_code_size = gifgetbyte(grr); + GIF_DEBUG(("\n\nmin_code_size(%d) ", min_code_size)); + if (min_code_size >= GIF_MAX_CODE_BITS) { + gif_read_error(gfc, 1, "image corrupted, min_code_size too big"); + min_code_size = GIF_MAX_CODE_BITS - 1; + } else if (min_code_size < 2) { + gif_read_error(gfc, 1, "image corrupted, min_code_size too small"); + min_code_size = 2; + } + clear_code = 1 << min_code_size; + for (code = 0; code < clear_code; code++) { + gfc->prefix[code] = 49428; + gfc->suffix[code] = (uint8_t)code; + gfc->length[code] = 1; + } + + next_code = clear_code + 1; + bits_needed = min_code_size + 1; + + code = clear_code; + + bit_length = bit_position = 0; + /* Thus the 'Read in the next data block.' code below will be invoked on the + first time through: exactly right! */ + + while (1) { + + old_code = code; + + /* GET A CODE INTO THE 'code' VARIABLE. + * + * 9.Dec.1998 - Rather than maintain a byte pointer and a bit offset into + * the current byte (and the processing associated with that), we maintain + * one number: the offset, in bits, from the beginning of 'buffer'. This + * much cleaner choice was inspired by Patrick J. Naughton + * 's GIF-reading code, which does the same thing. + * His code distributed as part of XV in xvgif.c. */ + + if (bit_position + bits_needed > bit_length) + /* Read in the next data block. */ + if (!read_image_block(grr, buffer, &bit_position, &bit_length, + bits_needed)) + goto zero_length_block; + + i = bit_position / 8; + accum = buffer[i] + (buffer[i+1] << 8); + if (bits_needed >= 8) + accum |= (buffer[i+2]) << 16; + code = (Gif_Code)((accum >> (bit_position % 8)) & CUR_CODE_MASK); + bit_position += bits_needed; + + GIF_DEBUG(("%d ", code)); + + /* CHECK FOR SPECIAL OR BAD CODES: clear_code, eoi_code, or a code that is + * too large. */ + if (code == clear_code) { + GIF_DEBUG(("clear ")); + bits_needed = min_code_size + 1; + next_code = clear_code + 1; + continue; + + } else if (code == clear_code + 1) + break; + + else if (code > next_code && next_code && next_code != clear_code) { + /* code > next_code: a (hopefully recoverable) error. + + Bug fix, 5/27: Do this even if old_code == clear_code, and set code + to 0 to prevent errors later. (If we didn't zero code, we'd later set + old_code = code; then we had old_code >= next_code; so the prefixes + array got all screwed up!) + + Bug fix, 4/12/2010: It is not an error if next_code == clear_code. + This happens at the end of a large GIF: see the next comment ("If no + meaningful next code should be defined...."). */ + if (gfc->errors[1] < 20) + gif_read_error(gfc, 1, "image corrupted, code out of range"); + else if (gfc->errors[1] == 20) + gif_read_error(gfc, 1, "(not reporting more errors)"); + code = 0; + } + + /* PROCESS THE CURRENT CODE and define the next code. If no meaningful + * next code should be defined, then we have set next_code to either + * 'eoi_code' or 'clear_code' -- so we'll store useless prefix/suffix data + * in a useless place. */ + + /* *First,* set up the prefix and length for the next code + (in case code == next_code). */ + gfc->prefix[next_code] = old_code; + gfc->length[next_code] = gfc->length[old_code] + 1; + + /* Use one_code to process code. It's nice that it returns the first + pixel in code: that's what we need. */ + gfc->suffix[next_code] = one_code(gfc, code); + + /* Special processing if code == next_code: we didn't know code's final + suffix when we called one_code, but we do now. */ + /* 7.Mar.2014 -- Avoid error if image has zero width/height. */ + if (code == next_code && gfc->image + gfc->decodepos <= gfc->maximage) + gfc->image[gfc->decodepos - 1] = gfc->suffix[next_code]; + + /* Increment next_code except for the 'clear_code' special case (that's + when we're reading at the end of a GIF) */ + if (next_code != clear_code) { + next_code++; + if (next_code == CUR_BUMP_CODE) { + if (bits_needed < GIF_MAX_CODE_BITS) + bits_needed++; + else + next_code = clear_code; + } + } + + } + + /* read blocks until zero-length reached. */ + i = gifgetbyte(grr); + GIF_DEBUG(("\nafter_image(%d)\n", i)); + while (i > 0) { + gifgetblock(buffer, i, grr); + i = gifgetbyte(grr); + GIF_DEBUG(("\nafter_image(%d)\n", i)); + } + + /* zero-length block reached. */ + zero_length_block: { + long delta = (long) (gfc->maximage - gfc->image) - (long) gfc->decodepos; + char buf[BUFSIZ]; + if (delta > 0) { + snprintf(buf, sizeof(buf), "missing %ld %s of image data", delta, + delta == 1 ? "pixel" : "pixels"); + gif_read_error(gfc, 1, buf); + memset(&gfc->image[gfc->decodepos], 0, delta); + } else if (delta < -1) { + /* One pixel of superfluous data is OK; that could be the + code == next_code case. */ + snprintf(buf, sizeof(buf), "%ld superfluous pixels of image data", -delta); + gif_read_error(gfc, 0, buf); + } + } +} + + +static Gif_Colormap * +read_color_table(int size, Gif_Reader *grr) +{ + Gif_Colormap *gfcm = Gif_NewFullColormap(size, size); + unsigned char buf[256 * 3], *bptr, *eptr; + size_t n; + Gif_Color *c; + if (!gfcm) return 0; + assert(gfcm->capacity >= 256 && size >= 0 && size <= 256); + + GIF_DEBUG(("colormap(%d) ", size)); + n = gifgetblock(buf, size * 3, grr); + + /* always initialize 256 colors; colors not included in map are + initialized to black in case of bad input */ + memset(&buf[n], 0, 256 * 3 - n); + + for (bptr = buf, eptr = buf + (256 * 3), c = gfcm->col; + bptr != eptr; + bptr += 3, ++c) { + c->gfc_red = bptr[0]; + c->gfc_green = bptr[1]; + c->gfc_blue = bptr[2]; + c->haspixel = 0; + } + + return gfcm; +} + + +static int +read_logical_screen_descriptor(Gif_Stream *gfs, Gif_Reader *grr) + /* returns 0 on memory error */ +{ + uint8_t packed; + + /* we don't care about logical screen width or height */ + gfs->screen_width = gifgetunsigned(grr); + gfs->screen_height = gifgetunsigned(grr); + + packed = gifgetbyte(grr); + gfs->background = gifgetbyte(grr); + + /* don't care about pixel aspect ratio */ + gifgetbyte(grr); + + if (packed & 0x80) { /* have a global color table */ + int ncol = 1 << ((packed & 0x07) + 1); + gfs->global = read_color_table(ncol, grr); + if (!gfs->global) return 0; + gfs->global->refcount = 1; + } else + gfs->background = 256; + + return 1; +} + + +static int +read_compressed_image(Gif_Image *gfi, Gif_Reader *grr, int read_flags) +{ + if (grr->is_record) { + const uint32_t image_pos = grr->pos; + + /* scan over image */ + ++grr->pos; /* skip min code size */ + while (grr->pos < grr->length) { + int amt = grr->v[grr->pos]; + grr->pos += amt + 1; + if (amt == 0) + break; + } + if (grr->pos > grr->length) + grr->pos = grr->length; + + gfi->compressed_len = grr->pos - image_pos; + gfi->compressed_errors = 0; + if (read_flags & GIF_READ_CONST_RECORD) { + gfi->compressed = (uint8_t*) &grr->v[image_pos]; + gfi->free_compressed = 0; + } else { + gfi->compressed = Gif_NewArray(uint8_t, gfi->compressed_len); + gfi->free_compressed = Gif_Free; + if (!gfi->compressed) return 0; + memcpy(gfi->compressed, &grr->v[image_pos], gfi->compressed_len); + } + + } else { + /* non-record; have to read it block by block. */ + uint32_t comp_cap = 1024; + uint32_t comp_len; + uint8_t *comp = Gif_NewArray(uint8_t, comp_cap); + int i; + if (!comp) return 0; + + /* min code size */ + i = gifgetbyte(grr); + comp[0] = i; + comp_len = 1; + + i = gifgetbyte(grr); + while (i > 0) { + /* add 2 before check so we don't have to check after loop when appending + 0 block */ + if (comp_len + i + 2 > comp_cap) { + comp_cap *= 2; + Gif_ReArray(comp, uint8_t, comp_cap); + if (!comp) return 0; + } + comp[comp_len] = i; + gifgetblock(comp + comp_len + 1, i, grr); + comp_len += i + 1; + i = gifgetbyte(grr); + } + comp[comp_len++] = 0; + + gfi->compressed_len = comp_len; + gfi->compressed_errors = 0; + gfi->compressed = comp; + gfi->free_compressed = Gif_Free; + } + + return 1; +} + + +static int +uncompress_image(Gif_Context *gfc, Gif_Image *gfi, Gif_Reader *grr) +{ + int old_nerrors; + if (!Gif_CreateUncompressedImage(gfi, gfi->interlace)) + return 0; + gfc->width = gfi->width; + gfc->height = gfi->height; + gfc->image = gfi->image_data; + gfc->maximage = gfi->image_data + (unsigned) gfi->width * (unsigned) gfi->height; + old_nerrors = gfc->errors[1]; + read_image_data(gfc, grr); + gfi->compressed_errors = gfc->errors[1] - old_nerrors; + return 1; +} + + +int +Gif_FullUncompressImage(Gif_Stream* gfs, Gif_Image* gfi, + Gif_ReadErrorHandler h) +{ + Gif_Context gfc; + Gif_Reader grr; + int ok = 0; + + /* return right away if image is already uncompressed. this might screw over + people who expect re-uncompressing to restore the compressed version. */ + if (gfi->img) + return 2; + if (gfi->image_data) + /* we have uncompressed data, but not an 'img' array; + this shouldn't happen */ + return 0; + + gfc.stream = gfs; + gfc.gfi = gfi; + gfc.prefix = Gif_NewArray(Gif_Code, GIF_MAX_CODE); + gfc.suffix = Gif_NewArray(uint8_t, GIF_MAX_CODE); + gfc.length = Gif_NewArray(uint16_t, GIF_MAX_CODE); + gfc.handler = h; + gfc.errors[0] = gfc.errors[1] = 0; + + if (gfc.prefix && gfc.suffix && gfc.length && gfi->compressed) { + make_data_reader(&grr, gfi->compressed, gfi->compressed_len); + ok = uncompress_image(&gfc, gfi, &grr); + } + + Gif_DeleteArray(gfc.prefix); + Gif_DeleteArray(gfc.suffix); + Gif_DeleteArray(gfc.length); + if (gfc.errors[0] || gfc.errors[1]) + gif_read_error(&gfc, -1, 0); + return ok && !gfc.errors[1]; +} + + +static int +read_image(Gif_Reader *grr, Gif_Context *gfc, Gif_Image *gfi, int read_flags) + /* returns 0 on memory error */ +{ + uint8_t packed; + + gfi->left = gifgetunsigned(grr); + gfi->top = gifgetunsigned(grr); + gfi->width = gifgetunsigned(grr); + gfi->height = gifgetunsigned(grr); + /* Mainline GIF processors (Firefox, etc.) process missing width (height) + as screen_width (screen_height). */ + if (gfi->width == 0) + gfi->width = gfc->stream->screen_width; + if (gfi->height == 0) + gfi->height = gfc->stream->screen_height; + /* If still zero, error. */ + if (gfi->width == 0 || gfi->height == 0) { + gif_read_error(gfc, 1, "ignoring frame, zero width and/or height"); + Gif_MakeImageEmpty(gfi); + read_flags = 0; + } + /* If position out of range, error. */ + if ((unsigned) gfi->left + (unsigned) gfi->width > 0xFFFF + || (unsigned) gfi->top + (unsigned) gfi->height > 0xFFFF) { + gif_read_error(gfc, 1, "ignoring frame, violates 65535x65535 screen size limit"); + Gif_MakeImageEmpty(gfi); + read_flags = 0; + } + GIF_DEBUG(("<%ux%u> ", gfi->width, gfi->height)); + + packed = gifgetbyte(grr); + if (packed & 0x80) { /* have a local color table */ + int ncol = 1 << ((packed & 0x07) + 1); + gfi->local = read_color_table(ncol, grr); + if (!gfi->local) return 0; + gfi->local->refcount = 1; + } + + gfi->interlace = (packed & 0x40) != 0; + + /* Keep the compressed data if asked */ + if (read_flags & GIF_READ_COMPRESSED) { + if (!read_compressed_image(gfi, grr, read_flags)) + return 0; + if (read_flags & GIF_READ_UNCOMPRESSED) { + Gif_Reader new_grr; + make_data_reader(&new_grr, gfi->compressed, gfi->compressed_len); + if (!uncompress_image(gfc, gfi, &new_grr)) + return 0; + } + + } else if (read_flags & GIF_READ_UNCOMPRESSED) { + if (!uncompress_image(gfc, gfi, grr)) + return 0; + + } else { + /* skip over the image */ + uint8_t buffer[GIF_MAX_BLOCK]; + int i = gifgetbyte(grr); + while (i > 0) { + gifgetblock(buffer, i, grr); + i = gifgetbyte(grr); + } + } + + return 1; +} + + +static void +read_graphic_control_extension(Gif_Context *gfc, Gif_Image *gfi, + Gif_Reader *grr) +{ + uint8_t len; + uint8_t crap[GIF_MAX_BLOCK]; + + len = gifgetbyte(grr); + + if (len == 4) { + uint8_t packed = gifgetbyte(grr); + gfi->disposal = (packed >> 2) & 0x07; + gfi->delay = gifgetunsigned(grr); + gfi->transparent = gifgetbyte(grr); + if (!(packed & 0x01)) /* transparent color doesn't exist */ + gfi->transparent = -1; + len -= 4; + } + + if (len > 0) { + gif_read_error(gfc, 1, "bad graphic extension"); + gifgetblock(crap, len, grr); + } + + len = gifgetbyte(grr); + while (len > 0) { + gif_read_error(gfc, 1, "bad graphic extension"); + gifgetblock(crap, len, grr); + len = gifgetbyte(grr); + } +} + + +static char *last_name; + + +static char * +suck_data(char *data, int *store_len, Gif_Reader *grr) +{ + uint8_t len = gifgetbyte(grr); + int total_len = 0; + + while (len > 0) { + Gif_ReArray(data, char, total_len + len + 1); + if (!data) return 0; + gifgetblock((uint8_t *)data + total_len, len, grr); + + total_len += len; + data[total_len] = 0; + + len = gifgetbyte(grr); + } + + if (store_len) *store_len = total_len; + return data; +} + + +static int +read_unknown_extension(Gif_Context* gfc, Gif_Reader* grr, + int kind, char* appname, int applength) +{ + uint8_t block_len = gifgetbyte(grr); + uint8_t* data = 0; + int data_len = 0; + Gif_Extension *gfex = 0; + + while (block_len > 0) { + Gif_ReArray(data, uint8_t, data_len + block_len + 2); + if (!data) + goto done; + data[data_len] = block_len; + gifgetblock(data + data_len + 1, block_len, grr); + data_len += block_len + 1; + block_len = gifgetbyte(grr); + } + + if (data) + gfex = Gif_NewExtension(kind, appname, applength); + if (gfex) { + gfex->data = data; + gfex->free_data = Gif_Free; + gfex->length = data_len; + gfex->packetized = 1; + data[data_len] = 0; + Gif_AddExtension(gfc->stream, gfc->gfi, gfex); + } + + done: + if (!gfex) + Gif_DeleteArray(data); + while (block_len > 0) { + uint8_t buffer[GIF_MAX_BLOCK]; + gifgetblock(buffer, block_len, grr); + block_len = gifgetbyte(grr); + } + return gfex != 0; +} + + +static int +read_application_extension(Gif_Context *gfc, Gif_Reader *grr) +{ + Gif_Stream *gfs = gfc->stream; + uint8_t buffer[GIF_MAX_BLOCK + 1]; + uint8_t len = gifgetbyte(grr); + gifgetblock(buffer, len, grr); + + /* Read the Netscape loop extension. */ + if (len == 11 + && (memcmp(buffer, "NETSCAPE2.0", 11) == 0 + || memcmp(buffer, "ANIMEXTS1.0", 11) == 0)) { + + len = gifgetbyte(grr); + if (len == 3) { + gifgetbyte(grr); /* throw away the 1 */ + gfs->loopcount = gifgetunsigned(grr); + len = gifgetbyte(grr); + if (len) + gif_read_error(gfc, 1, "bad loop extension"); + } else + gif_read_error(gfc, 1, "bad loop extension"); + + while (len > 0) { + gifgetblock(buffer, len, grr); + len = gifgetbyte(grr); + } + return 1; + + } else + return read_unknown_extension(gfc, grr, 0xFF, (char*)buffer, len); +} + + +static int +read_comment_extension(Gif_Image *gfi, Gif_Reader *grr) +{ + int len; + Gif_Comment *gfcom = gfi->comment; + char *m = suck_data(0, &len, grr); + if (m) { + if (!gfcom) + gfcom = gfi->comment = Gif_NewComment(); + if (!gfcom || !Gif_AddCommentTake(gfcom, m, len)) + return 0; + } + return 1; +} + + +static Gif_Stream * +read_gif(Gif_Reader *grr, int read_flags, + const char* landmark, Gif_ReadErrorHandler handler) +{ + Gif_Stream *gfs; + Gif_Image *gfi; + Gif_Context gfc; + int unknown_block_type = 0; + + if (gifgetc(grr) != 'G' || + gifgetc(grr) != 'I' || + gifgetc(grr) != 'F') + return 0; + (void)gifgetc(grr); + (void)gifgetc(grr); + (void)gifgetc(grr); + + gfs = Gif_NewStream(); + gfi = Gif_NewImage(); + + gfc.stream = gfs; + gfc.prefix = Gif_NewArray(Gif_Code, GIF_MAX_CODE); + gfc.suffix = Gif_NewArray(uint8_t, GIF_MAX_CODE); + gfc.length = Gif_NewArray(uint16_t, GIF_MAX_CODE); + gfc.handler = handler; + gfc.gfi = gfi; + gfc.errors[0] = gfc.errors[1] = 0; + + if (!gfs || !gfi || !gfc.prefix || !gfc.suffix || !gfc.length) + goto done; + gfs->landmark = landmark; + + GIF_DEBUG(("\nGIF ")); + if (!read_logical_screen_descriptor(gfs, grr)) + goto done; + GIF_DEBUG(("logscrdesc ")); + + while (!gifeof(grr)) { + + uint8_t block = gifgetbyte(grr); + + switch (block) { + + case ',': /* image block */ + GIF_DEBUG(("imageread %d ", gfs->nimages)); + + gfi->identifier = last_name; + last_name = 0; + if (!Gif_AddImage(gfs, gfi)) + goto done; + else if (!read_image(grr, &gfc, gfi, read_flags)) { + Gif_RemoveImage(gfs, gfs->nimages - 1); + gfi = 0; + goto done; + } + + gfc.gfi = gfi = Gif_NewImage(); + if (!gfi) + goto done; + break; + + case ';': /* terminator */ + GIF_DEBUG(("term\n")); + goto done; + + case '!': /* extension */ + block = gifgetbyte(grr); + GIF_DEBUG(("ext(0x%02X) ", block)); + switch (block) { + + case 0xF9: + read_graphic_control_extension(&gfc, gfi, grr); + break; + + case 0xCE: + last_name = suck_data(last_name, 0, grr); + break; + + case 0xFE: + if (!read_comment_extension(gfi, grr)) goto done; + break; + + case 0xFF: + read_application_extension(&gfc, grr); + break; + + default: + read_unknown_extension(&gfc, grr, block, 0, 0); + break; + + } + break; + + default: + if (!unknown_block_type) { + char buf[256]; + snprintf(buf, sizeof(buf), "unknown block type %d at file offset %u", + block, grr->pos - 1); + gif_read_error(&gfc, 1, buf); + } + if (++unknown_block_type > 20) + goto done; + break; + + } + + } + + done: + + /* Move comments and extensions after last image into stream. */ + if (gfs && gfi) { + Gif_Extension* gfex; + gfs->end_comment = gfi->comment; + gfi->comment = 0; + gfs->end_extension_list = gfi->extension_list; + gfi->extension_list = 0; + for (gfex = gfs->end_extension_list; gfex; gfex = gfex->next) + gfex->image = NULL; + } + + Gif_DeleteImage(gfi); + Gif_DeleteArray(last_name); + Gif_DeleteArray(gfc.prefix); + Gif_DeleteArray(gfc.suffix); + Gif_DeleteArray(gfc.length); + gfc.gfi = 0; + last_name = 0; + + if (gfs) + gfs->errors = gfc.errors[1]; + if (gfs && gfc.errors[1] == 0 + && !(read_flags & GIF_READ_TRAILING_GARBAGE_OK) + && !grr->eofer(grr)) + gif_read_error(&gfc, 0, "trailing garbage after GIF ignored"); + /* finally, export last message */ + gif_read_error(&gfc, -1, 0); + + return gfs; +} + + +Gif_Stream * +Gif_FullReadFile(FILE *f, int read_flags, + const char* landmark, Gif_ReadErrorHandler h) +{ + Gif_Reader grr; + if (!f) return 0; + grr.f = f; + grr.pos = 0; + grr.is_record = 0; + grr.byte_getter = file_byte_getter; + grr.block_getter = file_block_getter; + grr.eofer = file_eofer; + return read_gif(&grr, read_flags, landmark, h); +} + +Gif_Stream * +Gif_FullReadRecord(const Gif_Record *gifrec, int read_flags, + const char* landmark, Gif_ReadErrorHandler h) +{ + Gif_Reader grr; + if (!gifrec) return 0; + make_data_reader(&grr, gifrec->data, gifrec->length); + if (read_flags & GIF_READ_CONST_RECORD) + read_flags |= GIF_READ_COMPRESSED; + return read_gif(&grr, read_flags, landmark, h); +} + + +#undef Gif_ReadFile +#undef Gif_ReadRecord + +Gif_Stream * +Gif_ReadFile(FILE *f) +{ + return Gif_FullReadFile(f, GIF_READ_UNCOMPRESSED, 0, 0); +} + +Gif_Stream * +Gif_ReadRecord(const Gif_Record *gifrec) +{ + return Gif_FullReadRecord(gifrec, GIF_READ_UNCOMPRESSED, 0, 0); +} + +void +Gif_SetErrorHandler(Gif_ReadErrorHandler handler) { + default_error_handler = handler; +} + +#ifdef __cplusplus +} +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/gifsicle.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifsicle.c new file mode 100644 index 0000000..6b9fe8e --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifsicle.c @@ -0,0 +1,2236 @@ +/* -*- c-basic-offset: 2 -*- */ +/* gifsicle.c - gifsicle's main loop. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#include +#include "gifsicle.h" +#include "kcolor.h" +#include +#include +#include +#include +#include +#include +#if HAVE_UNISTD_H +# include +#endif + +#ifndef static_assert +#define static_assert(x, msg) switch ((int) (x)) case 0: case !!((int) (x)): +#endif + +Gt_Frame def_frame; + +Gt_Frameset *frames = 0; +int first_input_frame = 0; +Gt_Frameset *nested_frames = 0; + +Gif_Stream *input = 0; +const char *input_name = 0; +static int unoptimizing = 0; + +const int GIFSICLE_DEFAULT_THREAD_COUNT = 8; +int thread_count = 0; + +static int gif_read_flags = 0; +static int nextfile = 0; +Gif_CompressInfo gif_write_info; + +static int frames_done = 0; +static int files_given = 0; + +int warn_local_colormaps = 1; + +static Gt_ColorTransform *input_transforms; +static Gt_ColorTransform *output_transforms; + +int mode = BLANK_MODE; +int nested_mode = 0; + +static int infoing = 0; +int verbosing = 0; +static int no_ignore_errors = 0; + + +#define CHANGED(next, flag) (((next) & (1<<(flag))) != 0) +#define UNCHECKED_MARK_CH(where, what) \ + next_##where |= 1< suppresses normal output, can%,t use with an\n output mode like %<--merge%> or %<--batch%>.\n (Try %<-II%>, which doesn%,t suppress normal output.)"); + if (newmode != BLANK_MODE && newmode != mode) + fatal_error("too late to change modes"); +} + + +void +set_frame_change(int kind) +{ + int i; + Gt_Frameset *fset; + + set_mode(BLANK_MODE); + if (mode < DELETING && frames_done) { + fatal_error("frame selection and frame changes don%,t mix"); + return; + } + assert(!nested_mode); + nested_mode = mode; + if (frame_spec_1 > frame_spec_2) { + i = frame_spec_1; + frame_spec_1 = frame_spec_2; + frame_spec_2 = i; + } + + switch (kind) { + + case DELETE_OPT: + mode = DELETING; + break; + + case REPLACE_OPT: + for (i = frame_spec_1; i < frame_spec_2; i++) + FRAME(frames, i).use = 0; + /* We want to use the last frame's delay, but nothing else about it. */ + FRAME(frames, i).use = -1; + /* FALLTHRU */ + + case INSERT_OPT: + /* Define a nested frameset (or use an existing one). */ + fset = FRAME(frames, frame_spec_2).nest; + if (!fset) + fset = new_frameset(8); + FRAME(frames, frame_spec_2).nest = fset; + + /* Later: Merge frames at the end of the nested frameset. */ + mode = INSERTING; + nested_frames = frames; + frames = fset; + break; + + case APPEND_OPT: + /* Just merge frames at the end of this frameset. */ + mode = INSERTING; + break; + + } +} + +static void +frame_change_done(void) +{ + if (nested_mode) + mode = nested_mode; + if (nested_frames) + frames = nested_frames; + nested_mode = 0; + nested_frames = 0; +} + + +static void +show_frame(int imagenumber, int usename) +{ + Gif_Image *gfi; + Gt_Frame *frame; + + if (!input || !(gfi = Gif_GetImage(input, imagenumber))) + return; + + switch (mode) { + + case MERGING: + case INSERTING: + case EXPLODING: + case INFOING: + case BATCHING: + if (!frames_done) + clear_frameset(frames, first_input_frame); + frame = add_frame(frames, input, gfi); + if (usename) + frame->explode_by_name = 1; + break; + + case DELETING: + frame = &FRAME(frames, first_input_frame + imagenumber); + frame->use = 0; + break; + + } + + next_frame = 0; + frames_done = 1; +} + + +/***** + * input a stream + **/ + +static void +gifread_error(Gif_Stream* gfs, Gif_Image* gfi, + int is_error, const char* message) +{ + static int last_is_error = 0; + static char last_landmark[256]; + static char last_message[256]; + static int different_error_count = 0; + static int same_error_count = 0; + char landmark[256]; + int which_image = Gif_ImageNumber(gfs, gfi); + if (gfs && which_image < 0) + which_image = gfs->nimages; + + /* ignore warnings if "no_warning" */ + if (no_warnings && is_error == 0) + return; + + if (message) { + const char *filename = gfs && gfs->landmark ? gfs->landmark : ""; + if (gfi && (which_image != 0 || gfs->nimages > 1)) + snprintf(landmark, sizeof(landmark), "%s:#%d", + filename, which_image < 0 ? gfs->nimages : which_image); + else + snprintf(landmark, sizeof(landmark), "%s", filename); + } + + if (last_message[0] + && different_error_count <= 10 + && (!message + || strcmp(message, last_message) != 0 + || strcmp(landmark, last_landmark) != 0)) { + const char* etype = last_is_error ? "read error: " : ""; + void (*f)(const char*, const char*, ...) = last_is_error ? lerror : lwarning; + if (gfi && gfi->user_flags) + /* error already reported */; + else if (same_error_count == 1) + f(last_landmark, "%s%s", etype, last_message); + else if (same_error_count > 0) + f(last_landmark, "%s%s (%d times)", etype, last_message, same_error_count); + same_error_count = 0; + last_message[0] = 0; + } + + if (message) { + if (last_message[0] == 0) + different_error_count++; + same_error_count++; + strncpy(last_message, message, sizeof(last_message)); + last_message[sizeof(last_message) - 1] = 0; + strncpy(last_landmark, landmark, sizeof(last_landmark)); + last_landmark[sizeof(last_landmark) - 1] = 0; + last_is_error = is_error; + if (different_error_count == 11) { + if (!(gfi && gfi->user_flags)) + error(0, "(plus more errors; is this GIF corrupt?)"); + different_error_count++; + } + } else + last_message[0] = 0; + + { + unsigned long missing; + if (message && sscanf(message, "missing %lu pixel", &missing) == 1 + && missing > 10000 && no_ignore_errors) { + gifread_error(gfs, 0, -1, 0); + lerror(landmark, "fatal error: too many missing pixels, giving up"); + exit(1); + } + } + + if (gfi && is_error < 0) + gfi->user_flags |= 1; +} + +struct StoredFile { + FILE *f; + struct StoredFile *next; + char name[1]; +}; + +static struct StoredFile *stored_files = 0; + +static FILE * +open_giffile(const char *name) +{ + struct StoredFile *sf; + FILE *f; + + if (name == 0 || strcmp(name, "-") == 0) { +#ifndef OUTPUT_GIF_TO_TERMINAL + if (isatty(fileno(stdin))) { + lerror("", "Is a terminal"); + return NULL; + } +#endif +#if defined(_MSDOS) || defined(_WIN32) + _setmode(_fileno(stdin), _O_BINARY); +#elif defined(__DJGPP__) + setmode(fileno(stdin), O_BINARY); +#elif defined(__EMX__) + _fsetmode(stdin, "b"); +#endif + return stdin; + } + + if (nextfile) + for (sf = stored_files; sf; sf = sf->next) + if (strcmp(name, sf->name) == 0) + return sf->f; + + f = fopen(name, "rb"); + + if (f && nextfile) { + sf = (struct StoredFile *) malloc(sizeof(struct StoredFile) + strlen(name)); + sf->f = f; + sf->next = stored_files; + stored_files = sf; + strcpy(sf->name, name); + } else if (!f) + lerror(name, "%s", strerror(errno)); + + return f; +} + +static void +close_giffile(FILE *f, int final) +{ + struct StoredFile **sf_pprev, *sf; + + if (!final && nextfile) { + int c = getc(f); + if (c == EOF) + final = 1; + else + ungetc(c, f); + } + + for (sf_pprev = &stored_files; (sf = *sf_pprev); sf_pprev = &sf->next) + if (sf->f == f) { + if (final) { + fclose(f); + *sf_pprev = sf->next; + free((void *) sf); + } + return; + } + + if (f != stdin) + fclose(f); +} + +void +input_stream(const char *name) +{ + char* component_namebuf; + FILE *f; + Gif_Stream *gfs; + int i; + int saved_next_frame = next_frame; + int componentno = 0; + const char *main_name = 0; + Gt_Frame old_def_frame; + + input = 0; + input_name = name; + frames_done = 0; + next_frame = 0; + next_input = 0; + if (next_output) + combine_output_options(); + files_given++; + + set_mode(BLANK_MODE); + + f = open_giffile(name); + if (!f) + return; + if (f == stdin) { + name = ""; + input_name = 0; + } + main_name = name; + + retry_file: + /* change filename for component files */ + componentno++; + if (componentno > 1) { + size_t namelen = strlen(main_name) + 10; + component_namebuf = (char*) malloc(namelen); + snprintf(component_namebuf, namelen, "%s~%d", main_name, componentno); + name = component_namebuf; + } else + component_namebuf = 0; + + /* check for empty file */ + i = getc(f); + if (i == EOF) { + if (!(gif_read_flags & GIF_READ_TRAILING_GARBAGE_OK)) + lerror(name, "empty file"); + else if (nextfile) + lerror(name, "no more images in file"); + goto error; + } + ungetc(i, f); + + if (verbosing) + verbose_open('<', name); + + /* read file */ + { + int old_error_count = error_count; + gfs = Gif_FullReadFile(f, gif_read_flags | GIF_READ_COMPRESSED, + name, gifread_error); + if ((!gfs || (Gif_ImageCount(gfs) == 0 && gfs->errors > 0)) + && componentno != 1) + lerror(name, "trailing garbage ignored"); + if (!no_ignore_errors) + error_count = old_error_count; + } + + if (!gfs || (Gif_ImageCount(gfs) == 0 && gfs->errors > 0)) { + if (componentno == 1) + lerror(name, "file not in GIF format"); + Gif_DeleteStream(gfs); + if (verbosing) + verbose_close('>'); + goto error; + } + + /* special processing for components after the first */ + if (componentno > 1) { + if (mode == BATCHING || mode == INSERTING) + fatal_error("%s: %<--multifile%> is useful only in merge mode", name); + input_done(); + } + + input = gfs; + + /* Processing when we've got a new input frame */ + set_mode(BLANK_MODE); + + if (active_output_data.output_name == 0) { + /* Don't override explicit output names. + This code works 'cause output_name is reset to 0 after each output. */ + if (mode == BATCHING) + active_output_data.output_name = input_name; + else if (mode == EXPLODING) { + /* Explode into current directory. */ + const char *explode_name = (input_name ? input_name : "#stdin#"); + const char *slash = strrchr(explode_name, PATHNAME_SEPARATOR); + if (slash) + active_output_data.output_name = slash + 1; + else + active_output_data.output_name = explode_name; + } + } + + /* This code rather sucks. Here's the problem: Since we consider options + strictly sequentially, one at a time, we can't tell the difference + between these: + + --name=X g.gif h.gif // name on g.gif #0 + --name=X g.gif #2 h.gif // name on g.gif #2 + g.gif --name=X #2 h.gif // name on g.gif #2 + g.gif --name=X h.gif // name on h.gif #0 !!! + + Here's the solution. Mark when we CHANGE an option. After processing + an input GIF, mark all the options as 'unchanged' -- but leave the + VALUES as is. Then when we read the next frame, CLEAR the unchanged + options. So it's like so: (* means changed, . means not.) + + [-.] --name=X [X*] g.gif [X.] #2 [-.] h.gif == name on g.gif #2 + [-.] g.gif [-.] --name=X [X*] #2 [-.] h.gif == name on g.gif #2 + [-.] --name=X [X*] g.gif [X.|-.] h.gif == name on g.gif #0 + [-.] g.gif [-.] --name=X [X*] h.gif == name on h.gif #0 */ + + /* Clear old options from the last input stream */ + if (!CHANGED(saved_next_frame, CH_NAME)) + def_frame.name = 0; + if (!CHANGED(saved_next_frame, CH_COMMENT)) + def_frame.comment = 0; + if (!CHANGED(saved_next_frame, CH_EXTENSION)) + def_frame.extensions = 0; + def_frame.input_filename = input_name; + + old_def_frame = def_frame; + first_input_frame = frames->count; + def_frame.position_is_offset = 1; + for (i = 0; i < gfs->nimages; i++) + add_frame(frames, gfs, gfs->images[i]); + def_frame = old_def_frame; + + if (unoptimizing) + if (!Gif_FullUnoptimize(gfs, GIF_UNOPTIMIZE_SIMPLEST_DISPOSAL)) { + static int context = 0; + if (!context) { + lwarning(name, "GIF too complex to unoptimize\n" + " (The reason was local color tables or complex transparency.\n" + " Try running the GIF through % first.)"); + context = 1; + } else + lwarning(name, "GIF too complex to unoptimize"); + } + + apply_color_transforms(input_transforms, gfs); + gfs->refcount++; + + /* Read more files. */ + free(component_namebuf); + if ((gif_read_flags & GIF_READ_TRAILING_GARBAGE_OK) && !nextfile) + goto retry_file; + close_giffile(f, 0); + return; + + error: + free(component_namebuf); + close_giffile(f, 1); +} + +void +input_done(void) +{ + if (!input) + return; + + if (verbosing) + verbose_close('>'); + + Gif_DeleteStream(input); + input = 0; + + if (mode == DELETING) + frame_change_done(); + if (mode == BATCHING || mode == EXPLODING) + output_frames(); +} + + +/***** + * colormap stuff + **/ + +static void +set_new_fixed_colormap(const char *name, int exact) +{ + int i; + if (name && strcmp(name, "web") == 0) { + Gif_Colormap *cm = Gif_NewFullColormap(216, 256); + Gif_Color *col = cm->col; + for (i = 0; i < 216; i++) { + col[i].gfc_red = (i / 36) * 0x33; + col[i].gfc_green = ((i / 6) % 6) * 0x33; + col[i].gfc_blue = (i % 6) * 0x33; + } + def_output_data.colormap_fixed = cm; + + } else if (name && (strcmp(name, "gray") == 0 + || strcmp(name, "grey") == 0)) { + Gif_Colormap *cm = Gif_NewFullColormap(256, 256); + Gif_Color *col = cm->col; + for (i = 0; i < 256; i++) + col[i].gfc_red = col[i].gfc_green = col[i].gfc_blue = i; + def_output_data.colormap_fixed = cm; + + } else if (name && strcmp(name, "bw") == 0) { + Gif_Colormap *cm = Gif_NewFullColormap(2, 256); + cm->col[0].gfc_red = cm->col[0].gfc_green = cm->col[0].gfc_blue = 0; + cm->col[1].gfc_red = cm->col[1].gfc_green = cm->col[1].gfc_blue = 255; + def_output_data.colormap_fixed = cm; + + } else + def_output_data.colormap_fixed = read_colormap_file(name, 0); + + def_output_data.colormap_fixed_exact = exact; +} + +static void +do_colormap_change(Gif_Stream *gfs) +{ + if (active_output_data.colormap_fixed) + colormap_stream(gfs, active_output_data.colormap_fixed, + &active_output_data, !active_output_data.colormap_fixed_exact); + + if (active_output_data.colormap_size > 0) { + kchist kch; + Gif_Colormap* (*adapt_func)(kchist*, Gt_OutputData*); + Gif_Colormap* new_cm; + + /* set up the histogram */ + { + uint32_t ntransp; + int i, any_locals = 0; + for (i = 0; i < gfs->nimages; i++) + if (gfs->images[i]->local) + any_locals = 1; + kchist_make(&kch, gfs, &ntransp); + if (kch.n <= active_output_data.colormap_size + && !any_locals + && !active_output_data.colormap_fixed) { + warning(1, "trivial adaptive palette (only %d colors in source)", kch.n); + kchist_cleanup(&kch); + return; + } + active_output_data.colormap_needs_transparency = ntransp > 0; + } + + switch (active_output_data.colormap_algorithm) { + case COLORMAP_DIVERSITY: + adapt_func = &colormap_flat_diversity; + break; + case COLORMAP_BLEND_DIVERSITY: + adapt_func = &colormap_blend_diversity; + break; + case COLORMAP_MEDIAN_CUT: + adapt_func = &colormap_median_cut; + break; + default: + fatal_error("can't happen"); + } + + new_cm = (*adapt_func)(&kch, &active_output_data); + colormap_stream(gfs, new_cm, &active_output_data, 1); + + Gif_DeleteColormap(new_cm); + kchist_cleanup(&kch); + } +} + + +/***** + * output GIF images + **/ + +static void +write_stream(const char *output_name, Gif_Stream *gfs) +{ + FILE *f; + + if (output_name) + f = fopen(output_name, "wb"); + else { +#ifndef OUTPUT_GIF_TO_TERMINAL + if (isatty(fileno(stdout))) { + lerror("", "Is a terminal: try `-o OUTPUTFILE`"); + return; + } +#endif +#if defined(_MSDOS) || defined(_WIN32) + _setmode(_fileno(stdout), _O_BINARY); +#elif defined(__DJGPP__) + setmode(fileno(stdout), O_BINARY); +#elif defined(__EMX__) + _fsetmode(stdout, "b"); +#endif + f = stdout; + output_name = ""; + } + + if (f) { + Gif_FullWriteFile(gfs, &gif_write_info, f); + fclose(f); + any_output_successful = 1; + } else + lerror(output_name, "%s", strerror(errno)); +} + +static void +merge_and_write_frames(const char *outfile, int f1, int f2) +{ + Gif_Stream *out; + int compress_immediately; + int colormap_change; + int huge_stream; + assert(!nested_mode); + if (verbosing) + verbose_open('[', outfile ? outfile : "#stdout#"); + active_output_data.active_output_name = outfile; + + colormap_change = active_output_data.colormap_size > 0 + || active_output_data.colormap_fixed; + warn_local_colormaps = !colormap_change; + + if (!(active_output_data.scaling + || (active_output_data.optimizing & GT_OPT_MASK) + || colormap_change)) + compress_immediately = 1; + else + compress_immediately = active_output_data.conserve_memory; + + out = merge_frame_interval(frames, f1, f2, &active_output_data, + compress_immediately, &huge_stream); + + if (out) { + double w, h; + if (active_output_data.scaling == GT_SCALING_SCALE) { + w = active_output_data.scale_x * out->screen_width; + h = active_output_data.scale_y * out->screen_height; + } else { + w = active_output_data.resize_width; + h = active_output_data.resize_height; + } + kc_set_gamma(active_output_data.colormap_gamma_type, + active_output_data.colormap_gamma); + if (active_output_data.scaling != GT_SCALING_NONE) + resize_stream(out, w, h, active_output_data.resize_flags, + active_output_data.scale_method, + active_output_data.scale_colors); + if (colormap_change) + do_colormap_change(out); + if (output_transforms) + apply_color_transforms(output_transforms, out); + if (active_output_data.optimizing & GT_OPT_MASK) + optimize_fragments(out, active_output_data.optimizing, huge_stream); + write_stream(outfile, out); + Gif_DeleteStream(out); + } + + if (verbosing) + verbose_close(']'); + active_output_data.active_output_name = 0; +} + +static void +output_information(const char *outfile) +{ + FILE *f; + int i, j; + Gt_Frame *fr; + Gif_Stream *gfs; + + if (infoing == 2) + f = stderr; + else if (outfile == 0) + f = stdout; + else { + f = fopen(outfile, "w"); + if (!f) { + lerror(outfile, "%s", strerror(errno)); + return; + } + } + + for (i = 0; i < frames->count; i++) + FRAME(frames, i).stream->user_flags = 97; + + for (i = 0; i < frames->count; i++) + if (FRAME(frames, i).stream->user_flags == 97) { + fr = &FRAME(frames, i); + gfs = fr->stream; + gfs->user_flags = 0; + stream_info(f, gfs, fr->input_filename, fr->info_flags); + for (j = i; j < frames->count; j++) + if (FRAME(frames, j).stream == gfs) { + fr = &FRAME(frames, j); + image_info(f, gfs, fr->image, fr->info_flags); + } + } + + if (f != stderr && f != stdout) + fclose(f); +} + +void +output_frames(void) +{ + /* Use the current output name, not the stored output name. + This supports 'gifsicle a.gif -o xxx'. + It's not like any other option, but seems right: it fits the natural + order -- input, then output. */ + int i; + const char *outfile = active_output_data.output_name; + active_output_data.output_name = 0; + + /* Output information only now. */ + if (infoing) + output_information(outfile); + + if (infoing != 1 && frames->count > 0) + switch (mode) { + + case MERGING: + case BATCHING: + case INFOING: + merge_and_write_frames(outfile, 0, -1); + break; + + case EXPLODING: { + /* Use the current output name for consistency, even though that means + we can't explode different frames to different names. Not a big deal + anyway; they can always repeat the gif on the cmd line. */ + int max_nimages = 0; + for (i = 0; i < frames->count; i++) { + Gt_Frame *fr = &FRAME(frames, i); + if (fr->stream->nimages > max_nimages) + max_nimages = fr->stream->nimages; + } + + if (!outfile) /* Watch out! */ + outfile = "-"; + + for (i = 0; i < frames->count; i++) { + Gt_Frame *fr = &FRAME(frames, i); + int imagenumber = Gif_ImageNumber(fr->stream, fr->image); + char *explodename; + + const char *imagename = 0; + if (fr->explode_by_name) + imagename = fr->name ? fr->name : fr->image->identifier; + + explodename = explode_filename(outfile, imagenumber, imagename, + max_nimages); + merge_and_write_frames(explodename, i, i); + } + break; + } + + case INSERTING: + /* do nothing */ + break; + + } + + active_next_output = 0; + clear_frameset(frames, 0); + + /* cropping: clear the 'crop->ready' information, which depended on the last + input image. */ + if (def_frame.crop) + def_frame.crop->ready = 0; +} + + +/***** + * parsing arguments + **/ + +int +frame_argument(Clp_Parser *clp, const char *arg) +{ + /* Returns 0 iff you should try a file named 'arg'. */ + int val = parse_frame_spec(clp, arg, -1, 0); + if (val == -97) + return 0; + else if (val > 0) { + int i, delta = (frame_spec_1 <= frame_spec_2 ? 1 : -1); + for (i = frame_spec_1; i != frame_spec_2 + delta; i += delta) + show_frame(i, frame_spec_name != 0); + if (next_output) + combine_output_options(); + return 1; + } else + return 1; +} + +static int +handle_extension(Clp_Parser *clp, int is_app) +{ + Gif_Extension *gfex; + const char *extension_type = clp->vstr; + const char *extension_body = Clp_Shift(clp, 1); + if (!extension_body) { + Clp_OptionError(clp, "%O requires two arguments"); + return 0; + } + + UNCHECKED_MARK_CH(frame, CH_EXTENSION); + if (is_app) + gfex = Gif_NewExtension(255, extension_type, 11); + else if (!isdigit(extension_type[0]) && extension_type[1] == 0) + gfex = Gif_NewExtension(extension_type[0], 0, 0); + else { + long l = strtol(extension_type, (char **)&extension_type, 0); + if (*extension_type != 0 || l < 0 || l >= 256) + fatal_error("bad extension type: must be a number between 0 and 255"); + gfex = Gif_NewExtension(l, 0, 0); + } + + gfex->data = (uint8_t *)extension_body; + gfex->length = strlen(extension_body); + gfex->next = def_frame.extensions; + def_frame.extensions = gfex; + + return 1; +} + + +/***** + * option processing + **/ + +static void +initialize_def_frame(void) +{ + /* frame defaults */ + def_frame.stream = 0; + def_frame.image = 0; + def_frame.use = 1; + + def_frame.name = 0; + def_frame.no_name = 0; + def_frame.comment = 0; + def_frame.no_comments = 0; + + def_frame.interlacing = -1; + def_frame.transparent.haspixel = 0; + def_frame.left = -1; + def_frame.top = -1; + def_frame.position_is_offset = 0; + + def_frame.crop = 0; + + def_frame.delay = -1; + def_frame.disposal = -1; + + def_frame.nest = 0; + def_frame.explode_by_name = 0; + + def_frame.no_extensions = 0; + def_frame.no_app_extensions = 0; + def_frame.extensions = 0; + + def_frame.flip_horizontal = 0; + def_frame.flip_vertical = 0; + def_frame.total_crop = 0; + + /* output defaults */ + def_output_data.output_name = 0; + + def_output_data.screen_mode = 0; + def_output_data.screen_width = -1; + def_output_data.screen_height = -1; + def_output_data.background.haspixel = 0; + def_output_data.loopcount = -2; + + def_output_data.colormap_size = 0; + def_output_data.colormap_fixed = 0; + def_output_data.colormap_fixed_exact = 0; + def_output_data.colormap_algorithm = COLORMAP_DIVERSITY; + def_output_data.dither_type = dither_none; + def_output_data.dither_name = "none"; + def_output_data.colormap_gamma_type = KC_GAMMA_SRGB; + def_output_data.colormap_gamma = 2.2; + + def_output_data.optimizing = 0; + def_output_data.scaling = GT_SCALING_NONE; + def_output_data.scale_method = SCALE_METHOD_MIX; + def_output_data.scale_colors = 0; + + def_output_data.conserve_memory = 0; + + active_output_data = def_output_data; +} + +static void +combine_output_options(void) +{ + int recent = next_output; + next_output = active_next_output; +#define COMBINE_ONE_OUTPUT_OPTION(value, field) \ + if (CHANGED(recent, value)) { \ + MARK_CH(output, value); \ + active_output_data.field = def_output_data.field; \ + } + + COMBINE_ONE_OUTPUT_OPTION(CH_OUTPUT, output_name); + + if (CHANGED(recent, CH_LOGICAL_SCREEN)) { + MARK_CH(output, CH_LOGICAL_SCREEN); + active_output_data.screen_mode = def_output_data.screen_mode; + active_output_data.screen_width = def_output_data.screen_width; + active_output_data.screen_height = def_output_data.screen_height; + } + COMBINE_ONE_OUTPUT_OPTION(CH_BACKGROUND, background); + COMBINE_ONE_OUTPUT_OPTION(CH_LOOPCOUNT, loopcount); + + COMBINE_ONE_OUTPUT_OPTION(CH_OPTIMIZE, optimizing); + COMBINE_ONE_OUTPUT_OPTION(CH_COLORMAP, colormap_size); + COMBINE_ONE_OUTPUT_OPTION(CH_COLORMAP_METHOD, colormap_algorithm); + if (CHANGED(recent, CH_USE_COLORMAP)) { + MARK_CH(output, CH_USE_COLORMAP); + if (def_output_data.colormap_fixed) + def_output_data.colormap_fixed->refcount++; + Gif_DeleteColormap(active_output_data.colormap_fixed); + active_output_data.colormap_fixed = def_output_data.colormap_fixed; + active_output_data.colormap_fixed_exact = def_output_data.colormap_fixed_exact; + } + if (CHANGED(recent, CH_DITHER)) { + MARK_CH(output, CH_DITHER); + active_output_data.dither_type = def_output_data.dither_type; + active_output_data.dither_data = def_output_data.dither_data; + } + if (CHANGED(recent, CH_GAMMA)) { + MARK_CH(output, CH_GAMMA); + active_output_data.colormap_gamma_type = def_output_data.colormap_gamma_type; + active_output_data.colormap_gamma = def_output_data.colormap_gamma; + } + + if (CHANGED(recent, CH_RESIZE)) { + MARK_CH(output, CH_RESIZE); + active_output_data.scaling = def_output_data.scaling; + active_output_data.resize_width = def_output_data.resize_width; + active_output_data.resize_height = def_output_data.resize_height; + active_output_data.resize_flags = def_output_data.resize_flags; + active_output_data.scale_x = def_output_data.scale_x; + active_output_data.scale_y = def_output_data.scale_y; + } + + if (CHANGED(recent, CH_RESIZE_METHOD)) { + MARK_CH(output, CH_RESIZE_METHOD); + active_output_data.scale_method = def_output_data.scale_method; + } + + if (CHANGED(recent, CH_SCALE_COLORS)) { + MARK_CH(output, CH_SCALE_COLORS); + active_output_data.scale_colors = def_output_data.scale_colors; + } + + COMBINE_ONE_OUTPUT_OPTION(CH_MEMORY, conserve_memory); + + def_output_data.colormap_fixed = 0; + def_output_data.colormap_fixed_exact = 0; + def_output_data.output_name = 0; + + active_next_output |= next_output; + next_output = 0; +} + +static void +redundant_option_warning(const char* opttype) +{ + static int context = 0; + if (!context) { + warning(0, "redundant %s option\n" + " (The %s option was overridden by another %s option\n" + " before it had any effect.)", opttype, opttype, opttype); + context = 1; + } else + warning(0, "redundant %s option", opttype); +} + +static void +print_useless_options(const char *type_name, int value, const char *names[]) +{ + int explanation_printed = 0; + int i; + if (!value) return; + for (i = 0; i < 32; i++) + if (CHANGED(value, i)) { + if (!explanation_printed) { + warning(0, "useless %s-related %s option\n" + " (It didn%,t affect any %s.)", names[i], type_name, type_name); + explanation_printed = 1; + } else + warning(0, "useless %s-related %s option", names[i], type_name); + } +} + +static Gt_Crop * +copy_crop(Gt_Crop *oc) +{ + Gt_Crop *nc = Gif_New(Gt_Crop); + /* Memory leak on crops, but this just is NOT a problem. */ + if (oc) + *nc = *oc; + else + memset(nc, 0, sizeof(Gt_Crop)); + nc->ready = 0; + return nc; +} + +static void +parse_resize_geometry_opt(Gt_OutputData* odata, const char* str, Clp_Parser* clp) +{ + double x, y; + int flags = GT_RESIZE_FIT, scale = 0; + + if (*str == '_' || *str == 'x') { + x = 0; + str += (*str == '_'); + } else if (isdigit((unsigned char) *str)) + x = strtol(str, (char**) &str, 10); + else + goto error; + + if (*str == 'x') { + ++str; + if (*str == '_' || !isdigit((unsigned char) *str)) { + y = 0; + str += (*str == '_'); + } else + y = strtol(str, (char**) &str, 10); + } else + y = x; + + for (; *str != 0; ++str) + if (*str == '%') + scale = 1; + else if (*str == '!') + flags = 0; + else if (*str == '^') + flags |= GT_RESIZE_FIT | GT_RESIZE_MIN_DIMEN; + else if (*str == '<') + flags |= GT_RESIZE_FIT | GT_RESIZE_FIT_UP; + else if (*str == '>') + flags |= GT_RESIZE_FIT | GT_RESIZE_FIT_DOWN; + else + goto error; + + if (scale) { + odata->scaling = GT_SCALING_SCALE; + odata->scale_x = x / 100.0; + odata->scale_y = y / 100.0; + } else { + odata->scaling = GT_SCALING_RESIZE; + odata->resize_width = x; + odata->resize_height = y; + } + odata->resize_flags = flags; + return; + +error: + Clp_OptionError(clp, "argument to %O must be a valid geometry specification"); +} + + + +/***** + * main + **/ + +int +main(int argc, char *argv[]) +{ + /* Check SIZEOF constants (useful for Windows). If these assertions fail, + you've used the wrong Makefile. You should've used Makefile.w32 for + 32-bit Windows and Makefile.w64 for 64-bit Windows. */ + static_assert(sizeof(unsigned int) == SIZEOF_UNSIGNED_INT, "unsigned int has the wrong size."); + static_assert(sizeof(unsigned long) == SIZEOF_UNSIGNED_LONG, "unsigned long has the wrong size."); + static_assert(sizeof(void*) == SIZEOF_VOID_P, "void* has the wrong size."); + + clp = Clp_NewParser(argc, (const char * const *)argv, sizeof(options) / sizeof(options[0]), options); + + Clp_AddStringListType + (clp, LOOP_TYPE, Clp_AllowNumbers, + "infinite", 0, "forever", 0, + (const char*) 0); + Clp_AddStringListType + (clp, DISPOSAL_TYPE, Clp_AllowNumbers, + "none", GIF_DISPOSAL_NONE, + "asis", GIF_DISPOSAL_ASIS, + "background", GIF_DISPOSAL_BACKGROUND, + "bg", GIF_DISPOSAL_BACKGROUND, + "previous", GIF_DISPOSAL_PREVIOUS, + (const char*) 0); + Clp_AddStringListType + (clp, COLORMAP_ALG_TYPE, 0, + "diversity", COLORMAP_DIVERSITY, + "blend-diversity", COLORMAP_BLEND_DIVERSITY, + "median-cut", COLORMAP_MEDIAN_CUT, + (const char*) 0); + Clp_AddStringListType + (clp, OPTIMIZE_TYPE, Clp_AllowNumbers, + "keep-empty", GT_OPT_KEEPEMPTY + 1, + "no-keep-empty", GT_OPT_KEEPEMPTY, + "drop-empty", GT_OPT_KEEPEMPTY, + "no-drop-empty", GT_OPT_KEEPEMPTY + 1, + (const char*) 0); + Clp_AddStringListType + (clp, RESIZE_METHOD_TYPE, 0, + "point", SCALE_METHOD_POINT, + "sample", SCALE_METHOD_POINT, + "mix", SCALE_METHOD_MIX, + "box", SCALE_METHOD_BOX, + "catrom", SCALE_METHOD_CATROM, + "lanczos", SCALE_METHOD_LANCZOS3, + "lanczos2", SCALE_METHOD_LANCZOS2, + "lanczos3", SCALE_METHOD_LANCZOS3, + "mitchell", SCALE_METHOD_MITCHELL, + "fast", SCALE_METHOD_POINT, + "good", SCALE_METHOD_MIX, + (const char*) 0); + Clp_AddType(clp, DIMENSIONS_TYPE, 0, parse_dimensions, 0); + Clp_AddType(clp, POSITION_TYPE, 0, parse_position, 0); + Clp_AddType(clp, SCALE_FACTOR_TYPE, 0, parse_scale_factor, 0); + Clp_AddType(clp, FRAME_SPEC_TYPE, 0, parse_frame_spec, 0); + Clp_AddType(clp, COLOR_TYPE, Clp_DisallowOptions, parse_color, 0); + Clp_AddType(clp, RECTANGLE_TYPE, 0, parse_rectangle, 0); + Clp_AddType(clp, TWO_COLORS_TYPE, Clp_DisallowOptions, parse_two_colors, 0); + Clp_SetOptionChar(clp, '+', Clp_ShortNegated); + Clp_SetErrorHandler(clp, clp_error_handler); + + program_name = Clp_ProgramName(clp); + + frames = new_frameset(16); + initialize_def_frame(); + Gif_InitCompressInfo(&gif_write_info); + Gif_SetErrorHandler(gifread_error); + +#if ENABLE_THREADS + pthread_mutex_init(&kd3_sort_lock, 0); +#endif + + /* Yep, I'm an idiot. + GIF dimensions are unsigned 16-bit integers. I assume that these + numbers will fit in an 'int'. This assertion tests that assumption. + Really I should go through & change everything over, but it doesn't + seem worth my time. */ + { + uint16_t m = 0xFFFFU; + int i = m; + assert(i > 0 && "configuration/lameness failure! bug the author!"); + } + + while (1) { + int opt = Clp_Next(clp); + switch (opt) { + + /* MODE OPTIONS */ + + case 'b': + set_mode(BATCHING); + break; + + case 'm': + set_mode(MERGING); + break; + + case 'e': + set_mode(EXPLODING); + def_frame.explode_by_name = 0; + break; + + case 'E': + set_mode(EXPLODING); + def_frame.explode_by_name = 1; + break; + + /* INFORMATION OPTIONS */ + + case INFO_OPT: + if (clp->negated) + infoing = 0; + else + /* switch between infoing == 1 (suppress regular output) and 2 (don't + suppress) */ + infoing = (infoing == 1 ? 2 : 1); + break; + + case COLOR_INFO_OPT: + if (clp->negated) + def_frame.info_flags &= ~INFO_COLORMAPS; + else { + def_frame.info_flags |= INFO_COLORMAPS; + if (!infoing) + infoing = 1; + } + break; + + case EXTENSION_INFO_OPT: + if (clp->negated) + def_frame.info_flags &= ~INFO_EXTENSIONS; + else { + def_frame.info_flags |= INFO_EXTENSIONS; + if (!infoing) + infoing = 1; + } + break; + + case SIZE_INFO_OPT: + if (clp->negated) + def_frame.info_flags &= ~INFO_SIZES; + else { + def_frame.info_flags |= INFO_SIZES; + if (!infoing) + infoing = 1; + } + break; + + case VERBOSE_OPT: + verbosing = clp->negated ? 0 : 1; + break; + + /* FRAME CHANGE OPTIONS */ + + case DELETE_OPT: + case REPLACE_OPT: + case INSERT_OPT: + case APPEND_OPT: + frame_change_done(); + set_frame_change(opt); + break; + + case ALTER_DONE_OPT: + frame_change_done(); + break; + + /* IMAGE OPTIONS */ + + case NAME_OPT: + if (clp->negated) goto no_names; + MARK_CH(frame, CH_NAME); + def_frame.name = clp->vstr; + break; + + no_names: + case NO_NAME_OPT: + MARK_CH(frame, CH_NAME); + def_frame.no_name = 1; + def_frame.name = 0; + break; + + case SAME_NAME_OPT: + def_frame.no_name = 0; + def_frame.name = 0; + break; + + case COMMENT_OPT: + if (clp->negated) goto no_comments; + MARK_CH(frame, CH_COMMENT); + if (!def_frame.comment) def_frame.comment = Gif_NewComment(); + Gif_AddComment(def_frame.comment, clp->vstr, -1); + break; + + no_comments: + case NO_COMMENTS_OPT: + Gif_DeleteComment(def_frame.comment); + def_frame.comment = 0; + def_frame.no_comments = 1; + break; + + case SAME_COMMENTS_OPT: + def_frame.no_comments = 0; + break; + + case 'i': + MARK_CH(frame, CH_INTERLACE); + def_frame.interlacing = clp->negated ? 0 : 1; + break; + + case SAME_INTERLACE_OPT: + def_frame.interlacing = -1; + break; + + case POSITION_OPT: + MARK_CH(frame, CH_POSITION); + def_frame.left = clp->negated ? 0 : position_x; + def_frame.top = clp->negated ? 0 : position_y; + break; + + case SAME_POSITION_OPT: + def_frame.left = -1; + def_frame.top = -1; + break; + + case 't': + MARK_CH(frame, CH_TRANSPARENT); + if (clp->negated) + def_frame.transparent.haspixel = 255; + else { + def_frame.transparent = parsed_color; + def_frame.transparent.haspixel = parsed_color.haspixel ? 2 : 1; + } + break; + + case SAME_TRANSPARENT_OPT: + def_frame.transparent.haspixel = 0; + break; + + case BACKGROUND_OPT: + MARK_CH(output, CH_BACKGROUND); + if (clp->negated) { + def_output_data.background.haspixel = 2; + def_output_data.background.pixel = 0; + } else { + def_output_data.background = parsed_color; + def_output_data.background.haspixel = parsed_color.haspixel ? 2 : 1; + } + break; + + case SAME_BACKGROUND_OPT: + MARK_CH(output, CH_BACKGROUND); + def_output_data.background.haspixel = 0; + break; + + case LOGICAL_SCREEN_OPT: + MARK_CH(output, CH_LOGICAL_SCREEN); + if (clp->negated) { + def_output_data.screen_mode = -1; + def_output_data.screen_width = def_output_data.screen_height = 0; + } else { + def_output_data.screen_mode = 1; + def_output_data.screen_width = dimensions_x; + def_output_data.screen_height = dimensions_y; + } + break; + + case SAME_LOGICAL_SCREEN_OPT: + MARK_CH(output, CH_LOGICAL_SCREEN); + def_output_data.screen_mode = 0; + def_output_data.screen_width = def_output_data.screen_height = -1; + break; + + case CROP_OPT: + if (clp->negated) + goto no_crop; + MARK_CH(frame, CH_CROP); + { + Gt_Crop *crop = copy_crop(def_frame.crop); + /* Memory leak on crops, but this just is NOT a problem. */ + crop->spec_x = position_x; + crop->spec_y = position_y; + crop->spec_w = dimensions_x; + crop->spec_h = dimensions_y; + def_frame.crop = crop; + } + break; + + no_crop: + case SAME_CROP_OPT: + def_frame.crop = 0; + break; + + case CROP_TRANSPARENCY_OPT: + if (clp->negated) + goto no_crop_transparency; + def_frame.crop = copy_crop(def_frame.crop); + def_frame.crop->transparent_edges = 1; + break; + + no_crop_transparency: + if (def_frame.crop && def_frame.crop->transparent_edges) { + def_frame.crop = copy_crop(def_frame.crop); + def_frame.crop->transparent_edges = 0; + } + break; + + /* extensions options */ + + case NO_EXTENSIONS_OPT: + def_frame.no_extensions = 1; + break; + + case NO_APP_EXTENSIONS_OPT: + def_frame.no_app_extensions = 1; + break; + + case SAME_EXTENSIONS_OPT: + def_frame.no_extensions = 0; + break; + + case SAME_APP_EXTENSIONS_OPT: + def_frame.no_app_extensions = 0; + break; + + case EXTENSION_OPT: + if (!handle_extension(clp, 0)) + goto bad_option; + break; + + case APP_EXTENSION_OPT: + if (!handle_extension(clp, 1)) + goto bad_option; + break; + + /* IMAGE DATA OPTIONS */ + + case FLIP_HORIZ_OPT: + MARK_CH(frame, CH_FLIP); + def_frame.flip_horizontal = !clp->negated; + break; + + case FLIP_VERT_OPT: + MARK_CH(frame, CH_FLIP); + def_frame.flip_vertical = !clp->negated; + break; + + case NO_FLIP_OPT: + def_frame.flip_horizontal = def_frame.flip_vertical = 0; + break; + + case NO_ROTATE_OPT: + def_frame.rotation = 0; + break; + + case ROTATE_90_OPT: + MARK_CH(frame, CH_ROTATE); + def_frame.rotation = 1; + break; + + case ROTATE_180_OPT: + MARK_CH(frame, CH_ROTATE); + def_frame.rotation = 2; + break; + + case ROTATE_270_OPT: + MARK_CH(frame, CH_ROTATE); + def_frame.rotation = 3; + break; + + /* ANIMATION OPTIONS */ + + case 'd': + MARK_CH(frame, CH_DELAY); + def_frame.delay = clp->negated ? 0 : clp->val.i; + break; + + case SAME_DELAY_OPT: + def_frame.delay = -1; + break; + + case DISPOSAL_OPT: + MARK_CH(frame, CH_DISPOSAL); + if (clp->negated) + def_frame.disposal = GIF_DISPOSAL_NONE; + else if (clp->val.i < 0 || clp->val.i > 7) + error(0, "disposal must be between 0 and 7"); + else + def_frame.disposal = clp->val.i; + break; + + case SAME_DISPOSAL_OPT: + def_frame.disposal = -1; + break; + + case 'l': + MARK_CH(output, CH_LOOPCOUNT); + if (clp->negated) + def_output_data.loopcount = -1; + else + def_output_data.loopcount = (clp->have_val ? clp->val.i : 0); + break; + + case SAME_LOOPCOUNT_OPT: + MARK_CH(output, CH_LOOPCOUNT); + def_output_data.loopcount = -2; + break; + + case OPTIMIZE_OPT: { + int o; + UNCHECKED_MARK_CH(output, CH_OPTIMIZE); + if (clp->negated || (clp->have_val && clp->val.i < 0)) + o = 0; + else + o = (clp->have_val ? clp->val.i : 1); + if (o > GT_OPT_MASK && (o & 1)) + def_output_data.optimizing |= o - 1; + else if (o > GT_OPT_MASK) + def_output_data.optimizing &= ~o; + else + def_output_data.optimizing = (def_output_data.optimizing & ~GT_OPT_MASK) | o; + break; + } + + case UNOPTIMIZE_OPT: + UNCHECKED_MARK_CH(input, CH_UNOPTIMIZE); + unoptimizing = clp->negated ? 0 : 1; + break; + + case THREADS_OPT: + if (clp->negated) + thread_count = 0; + else if (clp->have_val) + thread_count = clp->val.i; + else + thread_count = GIFSICLE_DEFAULT_THREAD_COUNT; + break; + + /* WHOLE-GIF OPTIONS */ + + case CAREFUL_OPT: { + if (clp->negated) + gif_write_info.flags = 0; + else + gif_write_info.flags = GIF_WRITE_CAREFUL_MIN_CODE_SIZE | GIF_WRITE_EAGER_CLEAR; + break; + } + + case CHANGE_COLOR_OPT: { + next_input |= 1 << CH_CHANGE_COLOR; + if (clp->negated) + input_transforms = delete_color_transforms + (input_transforms, &color_change_transformer); + else if (parsed_color2.haspixel) + error(0, "COLOR2 must be in RGB format in %<--change-color COLOR1 COLOR2%>"); + else + input_transforms = append_color_change + (input_transforms, parsed_color, parsed_color2); + break; + } + + case COLOR_TRANSFORM_OPT: + next_output |= 1 << CH_COLOR_TRANSFORM; + if (clp->negated) + output_transforms = delete_color_transforms + (output_transforms, &pipe_color_transformer); + else + output_transforms = append_color_transform + (output_transforms, &pipe_color_transformer, (void *)clp->vstr); + break; + + case COLORMAP_OPT: + MARK_CH(output, CH_COLORMAP); + if (clp->negated) + def_output_data.colormap_size = 0; + else { + def_output_data.colormap_size = clp->val.i; + if (def_output_data.colormap_size < 2 + || def_output_data.colormap_size > 256) { + Clp_OptionError(clp, "argument to %O must be between 2 and 256"); + def_output_data.colormap_size = 0; + } + } + break; + + case GRAY_OPT: + MARK_CH(output, CH_USE_COLORMAP); + Gif_DeleteColormap(def_output_data.colormap_fixed); + set_new_fixed_colormap("gray", 0); + break; + + case USE_COLORMAP_OPT: + MARK_CH(output, CH_USE_COLORMAP); + Gif_DeleteColormap(def_output_data.colormap_fixed); + if (clp->negated) + def_output_data.colormap_fixed = 0; + else + set_new_fixed_colormap(clp->vstr, 0); + break; + + case USE_EXACT_COLORMAP_OPT: + MARK_CH(output, CH_USE_COLORMAP); + Gif_DeleteColormap(def_output_data.colormap_fixed); + if (clp->negated) + def_output_data.colormap_fixed = 0; + else + set_new_fixed_colormap(clp->vstr, 1); + break; + + case COLORMAP_ALGORITHM_OPT: + MARK_CH(output, CH_COLORMAP_METHOD); + def_output_data.colormap_algorithm = clp->val.i; + break; + + case DITHER_OPT: { + const char* name; + if (clp->negated) + name = "none"; + else if (!clp->have_val) + name = "default"; + else + name = clp->val.s; + if (strcmp(name, "posterize") == 0) + name = "none"; + if (strcmp(name, def_output_data.dither_name) != 0 + && (strcmp(name, "none") == 0 + || strcmp(def_output_data.dither_name, "default") != 0)) + MARK_CH(output, CH_DITHER); + UNCHECKED_MARK_CH(output, CH_DITHER); + if (set_dither_type(&def_output_data, name) < 0) + Clp_OptionError(clp, "%<%s%> is not a valid dither", name); + def_output_data.dither_name = name; + break; + } + + case GAMMA_OPT: { + if (clp->negated) { + MARK_CH(output, CH_GAMMA); + def_output_data.colormap_gamma_type = KC_GAMMA_NUMERIC; + def_output_data.colormap_gamma = 1.0; + break; + } + if (strlen(clp->val.s) == 4 + && tolower((unsigned char) clp->val.s[0]) == 's' + && tolower((unsigned char) clp->val.s[1]) == 'r' + && tolower((unsigned char) clp->val.s[2]) == 'g' + && tolower((unsigned char) clp->val.s[3]) == 'b') { + MARK_CH(output, CH_GAMMA); + def_output_data.colormap_gamma_type = KC_GAMMA_SRGB; + break; + } +#if HAVE_CBRTF + if (strlen(clp->val.s) == 5 + && tolower((unsigned char) clp->val.s[0]) == 'o' + && tolower((unsigned char) clp->val.s[1]) == 'k' + && tolower((unsigned char) clp->val.s[2]) == 'l' + && tolower((unsigned char) clp->val.s[3]) == 'a' + && tolower((unsigned char) clp->val.s[4]) == 'b') { + MARK_CH(output, CH_GAMMA); + def_output_data.colormap_gamma_type = KC_GAMMA_OKLAB; + break; + } +#endif +#if HAVE_POW + { + char* ends; + double gamma = strtod(clp->val.s, &ends); + if (*clp->val.s && !*ends && !isspace((unsigned char) *clp->val.s)) { + MARK_CH(output, CH_GAMMA); + def_output_data.colormap_gamma_type = KC_GAMMA_NUMERIC; + def_output_data.colormap_gamma = gamma; + break; + } + } +#endif + Clp_OptionError(clp, "%O not supported"); + break; + } + + case RESIZE_OPT: + case RESIZE_FIT_OPT: + case RESIZE_TOUCH_OPT: + MARK_CH(output, CH_RESIZE); + if (clp->negated) + def_output_data.scaling = GT_SCALING_NONE; + else if (dimensions_x <= 0 && dimensions_y <= 0) { + error(0, "one of W and H must be positive in %<%s WxH%>", Clp_CurOptionName(clp)); + def_output_data.scaling = GT_SCALING_NONE; + } else { + def_output_data.scaling = GT_SCALING_RESIZE; + def_output_data.resize_width = dimensions_x; + def_output_data.resize_height = dimensions_y; + def_output_data.resize_flags = 0; + if (opt != RESIZE_OPT) + def_output_data.resize_flags |= GT_RESIZE_FIT; + if (opt == RESIZE_FIT_OPT) + def_output_data.resize_flags |= GT_RESIZE_FIT_DOWN; + } + break; + + case RESIZE_WIDTH_OPT: + case RESIZE_HEIGHT_OPT: + case RESIZE_FIT_WIDTH_OPT: + case RESIZE_FIT_HEIGHT_OPT: + case RESIZE_TOUCH_WIDTH_OPT: + case RESIZE_TOUCH_HEIGHT_OPT: + MARK_CH(output, CH_RESIZE); + if (clp->negated) + def_output_data.scaling = GT_SCALING_NONE; + else if (clp->val.u == 0) { + error(0, "%s argument must be positive", Clp_CurOptionName(clp)); + def_output_data.scaling = GT_SCALING_NONE; + } else { + unsigned dimen[2] = {0, 0}; + dimen[(opt == RESIZE_HEIGHT_OPT || opt == RESIZE_FIT_HEIGHT_OPT)] = clp->val.u; + def_output_data.scaling = GT_SCALING_RESIZE; + def_output_data.resize_width = dimen[0]; + def_output_data.resize_height = dimen[1]; + def_output_data.resize_flags = 0; + if (opt != RESIZE_WIDTH_OPT && opt != RESIZE_HEIGHT_OPT) + def_output_data.resize_flags |= GT_RESIZE_FIT; + if (opt == RESIZE_FIT_WIDTH_OPT || opt == RESIZE_FIT_HEIGHT_OPT) + def_output_data.resize_flags |= GT_RESIZE_FIT_DOWN; + } + break; + + case SCALE_OPT: + MARK_CH(output, CH_RESIZE); + if (clp->negated) + def_output_data.scaling = GT_SCALING_NONE; + else if (parsed_scale_factor_x <= 0 || parsed_scale_factor_y <= 0) { + error(0, "%s X and Y factors must be positive", Clp_CurOptionName(clp)); + def_output_data.scaling = GT_SCALING_NONE; + } else { + def_output_data.scaling = GT_SCALING_SCALE; + def_output_data.scale_x = parsed_scale_factor_x; + def_output_data.scale_y = parsed_scale_factor_y; + def_output_data.resize_flags = 0; + } + break; + + case RESIZE_GEOMETRY_OPT: + MARK_CH(output, CH_RESIZE); + if (clp->negated) + def_output_data.scaling = GT_SCALING_NONE; + else + parse_resize_geometry_opt(&def_output_data, clp->val.s, clp); + break; + + case RESIZE_METHOD_OPT: + MARK_CH(output, CH_RESIZE_METHOD); + def_output_data.scale_method = clp->val.i; + break; + + case RESIZE_COLORS_OPT: + MARK_CH(output, CH_SCALE_COLORS); + def_output_data.scale_colors = clp->negated ? 0 : clp->val.i; + if (def_output_data.scale_colors > 256) { + error(0, "%s can be at most 256", Clp_CurOptionName(clp)); + def_output_data.scale_colors = 256; + } + break; + + case LOSSY_OPT: + if (clp->have_val) + gif_write_info.loss = clp->val.i; + else + gif_write_info.loss = 20; + break; + + /* RANDOM OPTIONS */ + + case NO_WARNINGS_OPT: + no_warnings = !clp->negated; + break; + + case WARNINGS_OPT: + no_warnings = clp->negated; + break; + + case IGNORE_ERRORS_OPT: + no_ignore_errors = clp->negated; + break; + + case CONSERVE_MEMORY_OPT: + MARK_CH(output, CH_MEMORY); + def_output_data.conserve_memory = clp->negated ? -1 : 1; + break; + + case MULTIFILE_OPT: + if (clp->negated) + gif_read_flags &= ~GIF_READ_TRAILING_GARBAGE_OK; + else { + gif_read_flags |= GIF_READ_TRAILING_GARBAGE_OK; + nextfile = 0; + } + break; + + case NEXTFILE_OPT: + if (clp->negated) + gif_read_flags &= ~GIF_READ_TRAILING_GARBAGE_OK; + else { + gif_read_flags |= GIF_READ_TRAILING_GARBAGE_OK; + nextfile = 1; + } + break; + + case VERSION_OPT: +#ifdef GIF_UNGIF + printf("LCDF Gifsicle %s (ungif)\n", VERSION); +#else + printf("LCDF Gifsicle %s\n", VERSION); +#endif + printf("Copyright (C) 1997-2025 Eddie Kohler\n\ +This is free software; see the source for copying conditions.\n\ +There is NO warranty, not even for merchantability or fitness for a\n\ +particular purpose.\n"); + exit(EXIT_OK); + break; + + case HELP_OPT: + usage(clp); + exit(EXIT_OK); + break; + + case OUTPUT_OPT: + MARK_CH(output, CH_OUTPUT); + if (strcmp(clp->vstr, "-") == 0) + def_output_data.output_name = 0; + else + def_output_data.output_name = clp->vstr; + break; + + /* NONOPTIONS */ + + case Clp_NotOption: + if (clp->vstr[0] != '#' || !frame_argument(clp, clp->vstr)) { + input_done(); + input_stream(clp->vstr); + } + break; + + case Clp_Done: + goto done; + + bad_option: + case Clp_BadOption: + short_usage(clp); + exit(EXIT_USER_ERR); + break; + + default: + break; + + } + } + + done: + + if (next_output) + combine_output_options(); + if (!files_given) + input_stream(0); + + frame_change_done(); + input_done(); + if ((mode == MERGING && !error_count) || mode == INFOING) + output_frames(); + + verbose_endline(); + print_useless_options("frame", next_frame, frame_option_types); + print_useless_options("input", next_input, input_option_types); + if (any_output_successful) + print_useless_options("output", active_next_output, output_option_types); + blank_frameset(frames, 0, 0, 1); + Clp_DeleteParser(clp); + return (error_count ? EXIT_ERR : EXIT_OK); +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/gifsicle.h b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifsicle.h new file mode 100644 index 0000000..55b2f85 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifsicle.h @@ -0,0 +1,350 @@ +/* gifsicle.h - Function declarations for gifsicle. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#ifndef GIFSICLE_H +#define GIFSICLE_H +#include +#include +#ifdef __GNUC__ +#define NORETURN __attribute__ ((noreturn)) +#define USED_ATTR __attribute__ ((used)) +#else +#define NORETURN +#define USED_ATTR +#endif + +typedef struct Gt_Frameset Gt_Frameset; +typedef struct Gt_Crop Gt_Crop; +typedef struct Gt_ColorTransform Gt_ColorTransform; + +#if ENABLE_THREADS +#include +extern pthread_mutex_t kd3_sort_lock; +#endif + +typedef struct Gt_Frame { + + Gif_Stream *stream; + Gif_Image *image; + int use; + + const char *name; + int no_name; + Gif_Comment *comment; + int no_comments; + + Gif_Color transparent; /* also background */ + int interlacing; + int left; + int top; + + Gt_Crop *crop; + int left_offset; + int top_offset; + + int delay; + int disposal; + + Gt_Frameset *nest; + int explode_by_name; + + int no_extensions; + int no_app_extensions; + Gif_Extension *extensions; + + unsigned flip_horizontal: 1; + unsigned flip_vertical: 1; + unsigned info_flags: 3; + unsigned position_is_offset: 1; + unsigned total_crop: 1; + unsigned rotation; + + const char *input_filename; + +} Gt_Frame; + + +struct Gt_Frameset { + int count; + int cap; + Gt_Frame *f; +}; + + +struct Gt_Crop { + int ready; + int transparent_edges; + int spec_x; + int spec_y; + int spec_w; + int spec_h; + int x; + int y; + int w; + int h; + int left_offset; + int top_offset; +}; + + +typedef void (*colormap_transform_func)(Gif_Colormap *, void *); + +struct Gt_ColorTransform { + Gt_ColorTransform *prev; + Gt_ColorTransform *next; + colormap_transform_func func; + void *data; +}; + + +typedef struct { + + const char *output_name; + const char *active_output_name; + + int screen_mode; + int screen_width; + int screen_height; + + Gif_Color background; + int loopcount; + + int colormap_size; + Gif_Colormap *colormap_fixed; + int colormap_fixed_exact; + int colormap_algorithm; + int colormap_needs_transparency; + int dither_type; + const uint8_t* dither_data; + const char* dither_name; + int colormap_gamma_type; + double colormap_gamma; + + int optimizing; + + int scaling; + int resize_width; + int resize_height; + int resize_flags; + double scale_x; + double scale_y; + int scale_method; + int scale_colors; + + int conserve_memory; + +} Gt_OutputData; + +extern Gt_OutputData active_output_data; +extern Clp_Parser* clp; + +#define GT_SCALING_NONE 0 +#define GT_SCALING_RESIZE 1 +#define GT_SCALING_SCALE 2 + +#define GT_RESIZE_FIT 1 +#define GT_RESIZE_FIT_DOWN 2 +#define GT_RESIZE_FIT_UP 4 +#define GT_RESIZE_MIN_DIMEN 8 + +#define SCALE_METHOD_POINT 0 +#define SCALE_METHOD_BOX 1 +#define SCALE_METHOD_MIX 2 +#define SCALE_METHOD_CATROM 3 +#define SCALE_METHOD_LANCZOS2 4 +#define SCALE_METHOD_LANCZOS3 5 +#define SCALE_METHOD_MITCHELL 6 + +#define GT_OPT_MASK 0xFFFF +#define GT_OPT_KEEPEMPTY 0x10000 + + +/***** + * helper + **/ + +static inline int +constrain(int low, int x, int high) +{ + return x < low ? low : (x < high ? x : high); +} + + +/***** + * error & verbose + **/ +extern const char *program_name; +extern int verbosing; +extern int error_count; +extern int no_warnings; +extern int thread_count; +extern Gif_CompressInfo gif_write_info; + +void fatal_error(const char* format, ...) NORETURN; +void warning(int need_file, const char* format, ...); +void lwarning(const char* landmark, const char* format, ...); +void error(int need_file, const char* format, ...); +void lerror(const char* landmark, const char* format, ...); +void clp_error_handler(Clp_Parser *clp, const char *clp_message); +void usage(Clp_Parser* clp); +void short_usage(Clp_Parser* clp); + +void verbose_open(char, const char *); +void verbose_close(char); +void verbose_endline(void); + +const char* debug_color_str(const Gif_Color* gfc); + +#define EXIT_OK 0 +#define EXIT_ERR 1 +#define EXIT_USER_ERR 1 + +/***** + * info &c + **/ +#define INFO_COLORMAPS 1 +#define INFO_EXTENSIONS 2 +#define INFO_SIZES 4 +void stream_info(FILE *f, Gif_Stream *gfs, const char *filename, int flags); +void image_info(FILE *f, Gif_Stream *gfs, Gif_Image *gfi, int flags); + +char *explode_filename(const char *filename, int number, + const char *name, int max_nimg); + +/***** + * merging images + **/ +void unmark_colors(Gif_Colormap *); +void unmark_colors_2(Gif_Colormap *); +void mark_used_colors(Gif_Stream *gfs, Gif_Image *gfi, Gt_Crop *crop, + int compress_immediately); +int find_color_index(Gif_Color *c, int nc, Gif_Color *); +int merge_colormap_if_possible(Gif_Colormap *, Gif_Colormap *); + +extern int warn_local_colormaps; +void merge_stream(Gif_Stream *dest, Gif_Stream *src, int no_comments); +void merge_comments(Gif_Comment *destc, Gif_Comment *srcc); +Gif_Image* merge_image(Gif_Stream* dest, Gif_Stream* src, Gif_Image* srci, + Gt_Frame* srcfr, int same_compressed_ok); + +void optimize_fragments(Gif_Stream *, int optimizeness, int huge_stream); + +/***** + * image/colormap transformations + **/ +Gif_Colormap *read_colormap_file(const char *, FILE *); +void apply_color_transforms(Gt_ColorTransform *, Gif_Stream *); + +typedef void (*color_transform_func)(Gif_Colormap *, void *); +Gt_ColorTransform *append_color_transform + (Gt_ColorTransform *list, color_transform_func, void *); +Gt_ColorTransform *delete_color_transforms + (Gt_ColorTransform *list, color_transform_func); + +void color_change_transformer(Gif_Colormap *, void *); +Gt_ColorTransform *append_color_change + (Gt_ColorTransform *list, Gif_Color, Gif_Color); + +void pipe_color_transformer(Gif_Colormap *, void *); + +void combine_crop(Gt_Crop *dstcrop, const Gt_Crop *srccrop, const Gif_Image *gfi); +int crop_image(Gif_Image* gfi, Gt_Frame* fr, int preserve_total_crop); + +void flip_image(Gif_Image* gfi, Gt_Frame* fr, int is_vert); +void rotate_image(Gif_Image* gfi, Gt_Frame* fr, int rotation); +void resize_dimensions(int* w, int* h, double new_width, double new_height, + int flags); +void resize_stream(Gif_Stream* gfs, double new_width, double new_height, + int flags, int method, int scale_colors); + +/***** + * quantization + **/ +#define KC_GAMMA_SRGB 0 +#define KC_GAMMA_NUMERIC 1 +#define KC_GAMMA_OKLAB 2 +void kc_set_gamma(int type, double gamma); + +#define COLORMAP_DIVERSITY 0 +#define COLORMAP_BLEND_DIVERSITY 1 +#define COLORMAP_MEDIAN_CUT 2 + +enum { + dither_none = 0, dither_default, dither_floyd_steinberg, + dither_ordered, dither_ordered_new, dither_atkinson +}; +int set_dither_type(Gt_OutputData* od, const char* name); +void colormap_stream(Gif_Stream*, Gif_Colormap*, Gt_OutputData*, int allow_shrink); + +struct kchist; +Gif_Colormap* colormap_blend_diversity(struct kchist* kch, Gt_OutputData* od); +Gif_Colormap* colormap_flat_diversity(struct kchist* kch, Gt_OutputData* od); +Gif_Colormap* colormap_median_cut(struct kchist* kch, Gt_OutputData* od); + + +/***** + * parsing stuff + **/ +extern int frame_spec_1; +extern int frame_spec_2; +extern char * frame_spec_name; +extern int dimensions_x; +extern int dimensions_y; +extern int position_x; +extern int position_y; +extern Gif_Color parsed_color; +extern Gif_Color parsed_color2; +extern double parsed_scale_factor_x; +extern double parsed_scale_factor_y; + +int parse_frame_spec(Clp_Parser*, const char*, int, void*); +int parse_dimensions(Clp_Parser*, const char*, int, void*); +int parse_position(Clp_Parser*, const char*, int, void*); +int parse_scale_factor(Clp_Parser*, const char*, int, void*); +int parse_color(Clp_Parser*, const char*, int, void*); +int parse_rectangle(Clp_Parser*, const char*, int, void*); +int parse_two_colors(Clp_Parser*, const char*, int, void*); + +extern Gif_Stream *input; +extern const char *input_name; + +void input_stream(const char*); +void input_done(void); +void output_frames(void); + +/***** + * stuff with frames + **/ +extern Gt_Frame def_frame; +#define FRAME(fs, i) ((fs)->f[i]) + +Gt_Frameset * new_frameset(int initial_cap); +Gt_Frame* add_frame(Gt_Frameset*, Gif_Stream*, Gif_Image*); +void clear_def_frame_once_options(void); + +Gif_Stream * merge_frame_interval(Gt_Frameset*, int f1, int f2, + Gt_OutputData*, int compress, int *huge); +void clear_frameset(Gt_Frameset*, int from); +void blank_frameset(Gt_Frameset*, int from, int to, int delete_ob); + +/***** + * mode + **/ +#define BLANK_MODE 0 +#define MERGING 1 +#define BATCHING 2 +#define EXPLODING 3 +#define INFOING 4 +#define DELETING 5 +#define INSERTING 6 + +extern int mode; +extern int nested_mode; + +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/giftoc.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/giftoc.c new file mode 100644 index 0000000..2f5faec --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/giftoc.c @@ -0,0 +1,222 @@ +#if HAVE_CONFIG_H +# include +#endif +#ifndef PATHNAME_SEPARATOR +# define PATHNAME_SEPARATOR '/' +#endif +#include +#include +#include +#include +#include + +int is_static = 1; +int is_const = 1; + +static void * +fmalloc(int size) +{ + void *p = malloc(size); + if (!p) { + fputs("giftoc: Out of memory!\n", stderr); + exit(1); + } + return p; +} + +void +print_reckless(FILE *f, char *gifrecname) +{ + unsigned long size = 0; + int c; + int lasthex = 0; + + printf("\n%sGifRecord %s = { (unsigned char *)\"", + is_static ? "static " : "", gifrecname); + size = 0; + c = getc(f); + while (c != EOF) { + + if (size % 60 == 0) printf("\"\n \""); + + switch (c) { + + case '\\': + fputs("\\\\", stdout); + lasthex = 0; + break; + + case '\"': + fputs("\\\"", stdout); + lasthex = 0; + break; + + case '\b': + fputs("\\b", stdout); + lasthex = 0; + break; + + case '\r': + fputs("\\r", stdout); + lasthex = 0; + break; + + case '\n': + fputs("\\n", stdout); + lasthex = 0; + break; + + case '\f': + fputs("\\f", stdout); + lasthex = 0; + break; + + case '\t': + fputs("\\t", stdout); + lasthex = 0; + break; + + case '0': case '1': case '2': case '3': case '4': case '5': case '6': + case '7': + if (lasthex) + printf("\\%o", c); + else + putchar(c); + break; + + default: + if (isprint(c)) { + putchar(c); + lasthex = 0; + } else { + printf("\\%o", c); + lasthex = 1; + } + break; + + } + + size++; + c = getc(f); + } + + printf("\",\n %lu\n};\n", size); +} + +void +print_unreckless(FILE *f, char *gifrecname) +{ + unsigned long size = 0; + int c; + + printf("\nstatic %sunsigned char %s_data[] = {", + (is_const ? "const " : ""), gifrecname); + size = 0; + c = getc(f); + while (c != EOF) { + if (size % 20 == 0) printf("\n"); + printf("%d,", c); + size++; + c = getc(f); + } + printf("};\n%s%sGif_Record %s = { %s_data, %lu };\n", + (is_static ? "static " : ""), + (is_const ? "const " : ""), + gifrecname, gifrecname, size); +} + +int +main(int argc, char *argv[]) +{ + int reckless = 0; + int make_name = 0; + const char *directory = ""; + + argc--, argv++; + + while (argv[0] && argv[0][0] == '-') { + if (!strcmp(argv[0], "-reckless")) + reckless = 1, argc--, argv++; + else if (!strcmp(argv[0], "-static")) + is_static = 1, argc--, argv++; + else if (!strcmp(argv[0], "-extern")) + is_static = 0, argc--, argv++; + else if (!strcmp(argv[0], "-makename")) + make_name = 1, argc--, argv++; + else if (!strcmp(argv[0], "-nonconst")) + is_const = 0, argc--, argv++; + else if (!strcmp(argv[0], "-const")) + is_const = 1, argc--, argv++; + else if (!strcmp(argv[0], "-dir") && argc > 1) { + directory = argv[1], argc -= 2, argv += 2; + /* make sure directory is slash-terminated */ + if (directory[ strlen(directory) - 1 ] != PATHNAME_SEPARATOR + && directory[0]) { + size_t len = strlen(directory) + 2; + char *ndirectory = (char *)fmalloc(len); + snprintf(ndirectory, len, "%s%c", directory, PATHNAME_SEPARATOR); + directory = ndirectory; + } + } else + break; + } + + if ((!make_name && argc % 2 != 0) + || argc < 1 + || (argv[0] && argv[0][0] == '-')) { + fprintf(stderr, "\ +usage: giftoc [OPTIONS] FILE NAME [FILE NAME...]\n\ +or: giftoc -makename [OPTIONS] FILE [FILE...]\n\ + OPTIONS are -reckless, -extern, -nonconst, -dir DIR\n"); + exit(1); + } + + if (!is_static) + printf("#include \"config.h\"\n#include \n\n"); + + for (; argc > 0; argc--, argv++) { + FILE *f; + char *rec_name = 0; + char *file_name = (char *)fmalloc(strlen(argv[0]) + strlen(directory) + 1); + + strcpy(file_name, directory); + strcat(file_name, argv[0]); + f = fopen(file_name, "rb"); + + if (!f) { + fprintf(stderr, "%s: %s\n", file_name, strerror(errno)); + goto done; + } + + if (make_name) { + char *sin, *sout; + sin = strrchr(file_name, PATHNAME_SEPARATOR) + 1; + if (!sin) sin = file_name; + sout = rec_name = (char *)fmalloc(strlen(sin) + 2); + if (isdigit(*sin)) *sout++ = 'N'; + for (; *sin; sin++, sout++) + if (isalnum(*sin)) + *sout = *sin; + else + *sout = '_'; + *sout = 0; + + } else { + argv++, argc--; + rec_name = argv[0]; + } + + if (reckless) print_reckless(f, rec_name); + else print_unreckless(f, rec_name); + + done: + if (make_name) + free(rec_name); + free(file_name); + if (f) + fclose(f); + } + + exit(0); + +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/gifunopt.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifunopt.c new file mode 100644 index 0000000..e8b68fb --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifunopt.c @@ -0,0 +1,238 @@ +/* gifunopt.c - Unoptimization function for the GIF library. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of the LCDF GIF library. + + The LCDF GIF library is free software. It is distributed under the GNU + General Public License, version 2; you can copy, distribute, or alter it at + will, as long as this notice is kept intact and this source code is made + available. There is no warranty, express or implied. */ + +#if HAVE_CONFIG_H +# include +#endif +#include +#include +#include +#ifdef __cplusplus +extern "C" { +#endif + +#define TRANSPARENT 256 + +static void +put_image_in_screen(Gif_Stream *gfs, Gif_Image *gfi, uint16_t *screen) +{ + int transparent = gfi->transparent; + unsigned x, y; + unsigned w = gfi->width; + unsigned h = gfi->height; + if (gfi->left + w > gfs->screen_width) + w = gfs->screen_width - gfi->left; + if (gfi->top + h > gfs->screen_height) + h = gfs->screen_height - gfi->top; + + for (y = 0; y < h; y++) { + uint16_t *move = screen + (unsigned) gfs->screen_width * (y + gfi->top) + gfi->left; + uint8_t *line = gfi->img[y]; + for (x = 0; x < w; x++, move++, line++) + if (*line != transparent) + *move = *line; + } +} + + +static void +put_background_in_screen(Gif_Stream *gfs, Gif_Image *gfi, uint16_t *screen) +{ + uint16_t solid; + unsigned x, y; + unsigned w = gfi->width; + unsigned h = gfi->height; + if (gfi->left + w > gfs->screen_width) w = gfs->screen_width - gfi->left; + if (gfi->top + h > gfs->screen_height) h = gfs->screen_height - gfi->top; + + if (gfi->transparent < 0 && gfs->images[0]->transparent < 0 + && gfs->global && gfs->background < gfs->global->ncol) + solid = gfs->background; + else + solid = TRANSPARENT; + + for (y = 0; y < h; y++) { + uint16_t *move = screen + (unsigned) gfs->screen_width * (y + gfi->top) + gfi->left; + for (x = 0; x < w; x++, move++) + *move = solid; + } +} + + +static int +create_image_data(Gif_Stream *gfs, Gif_Image *gfi, uint16_t *screen, + uint8_t *new_data, int *used_transparent) +{ + int have[257]; + int transparent = -1; + unsigned pos, size = (unsigned) gfs->screen_width * (unsigned) gfs->screen_height; + uint16_t *move; + int i; + + /* mark colors used opaquely in the image */ + assert(TRANSPARENT == 256); + for (i = 0; i < 257; i++) + have[i] = 0; + for (pos = 0, move = screen; pos != size; ++pos, move++) + have[*move] = 1; + + /* the new transparent color is a color unused in either */ + if (have[TRANSPARENT]) { + for (i = 0; i < 256 && transparent < 0; i++) + if (!have[i]) + transparent = i; + if (transparent < 0) + goto error; + if (transparent >= gfs->global->ncol) { + Gif_ReArray(gfs->global->col, Gif_Color, 256); + if (!gfs->global->col) goto error; + gfs->global->ncol = transparent + 1; + } + } + + /* map the wide image onto the new data */ + *used_transparent = 0; + for (pos = 0, move = screen; pos != size; ++pos, move++, new_data++) + if (*move == TRANSPARENT) { + *new_data = transparent; + *used_transparent = 1; + } else + *new_data = *move; + + gfi->transparent = transparent; + return 1; + + error: + return 0; +} + + +static int +unoptimize_image(Gif_Stream *gfs, Gif_Image *gfi, uint16_t *screen) +{ + unsigned size = (unsigned) gfs->screen_width * (unsigned) gfs->screen_height; + int used_transparent; + uint8_t *new_data = Gif_NewArray(uint8_t, size); + uint16_t *new_screen = screen; + if (!new_data) return 0; + + /* Oops! May need to uncompress it */ + Gif_UncompressImage(gfs, gfi); + Gif_ReleaseCompressedImage(gfi); + + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) { + new_screen = Gif_NewArray(uint16_t, size); + if (!new_screen) return 0; + memcpy(new_screen, screen, size * sizeof(uint16_t)); + } + + put_image_in_screen(gfs, gfi, new_screen); + if (!create_image_data(gfs, gfi, new_screen, new_data, &used_transparent)) { + Gif_DeleteArray(new_data); + return 0; + } + + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) + Gif_DeleteArray(new_screen); + else if (gfi->disposal == GIF_DISPOSAL_BACKGROUND) + put_background_in_screen(gfs, gfi, screen); + + gfi->left = 0; + gfi->top = 0; + gfi->width = gfs->screen_width; + gfi->height = gfs->screen_height; + gfi->disposal = used_transparent; + Gif_SetUncompressedImage(gfi, new_data, Gif_Free, 0); + + return 1; +} + + +static int +no_more_transparency(Gif_Image *gfi1, Gif_Image *gfi2) +{ + int t1 = gfi1->transparent, t2 = gfi2->transparent, y; + if (t1 < 0) + return 1; + for (y = 0; y < gfi1->height; ++y) { + uint8_t *d1 = gfi1->img[y], *d2 = gfi2->img[y], *ed1 = d1 + gfi1->width; + while (d1 < ed1) { + if (*d1 == t1 && *d2 != t2) + return 0; + ++d1, ++d2; + } + } + return 1; +} + + +int +Gif_FullUnoptimize(Gif_Stream *gfs, int flags) +{ + int ok = 1; + int i; + unsigned pos, size; + uint16_t *screen; + uint16_t background; + Gif_Image *gfi; + + if (gfs->nimages < 1) return 1; + for (i = 0; i < gfs->nimages; i++) + if (gfs->images[i]->local) + return 0; + if (!gfs->global) + return 0; + + Gif_CalculateScreenSize(gfs, 0); + size = (unsigned) gfs->screen_width * (unsigned) gfs->screen_height; + + screen = Gif_NewArray(uint16_t, size); + gfi = gfs->images[0]; + if (gfi->transparent < 0 + && gfs->global && gfs->background < gfs->global->ncol) + background = gfs->background; + else + background = TRANSPARENT; + for (pos = 0; pos != size; ++pos) + screen[pos] = background; + + for (i = 0; i < gfs->nimages; i++) + if (!unoptimize_image(gfs, gfs->images[i], screen)) + ok = 0; + + if (ok) { + if (flags & GIF_UNOPTIMIZE_SIMPLEST_DISPOSAL) { + /* set disposal based on use of transparency. + If (every transparent pixel in frame i is also transparent in frame + i - 1), then frame i - 1 gets disposal ASIS; otherwise, disposal + BACKGROUND. */ + for (i = 0; i < gfs->nimages; ++i) + if (i == gfs->nimages - 1 + || no_more_transparency(gfs->images[i+1], gfs->images[i])) + gfs->images[i]->disposal = GIF_DISPOSAL_NONE; + else + gfs->images[i]->disposal = GIF_DISPOSAL_BACKGROUND; + } else + for (i = 0; i < gfs->nimages; ++i) + gfs->images[i]->disposal = GIF_DISPOSAL_BACKGROUND; + } + + Gif_DeleteArray(screen); + return ok; +} + +int +Gif_Unoptimize(Gif_Stream *gfs) +{ + return Gif_FullUnoptimize(gfs, 0); +} + +#ifdef __cplusplus +} +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/gifview.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifview.c new file mode 100644 index 0000000..ae13ec2 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifview.c @@ -0,0 +1,1419 @@ +/* gifview.c - gifview's main loop. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of gifview, in the gifsicle package. + + Gifview is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#include +#ifdef X_DISPLAY_MISSING +#error "You can't compile gifview without X." +#endif + +#include +#include +#include +#include +#include +#include +#include +#if HAVE_SYS_SELECT_H +# include +#endif +#include +#include +#include +#include +#include +#include +#include +#if HAVE_UNISTD_H +# include +#endif + +#ifdef __cplusplus +#define EXTERN extern "C" +#else +#define EXTERN extern +#endif + +/***** + * TIME STUFF (from xwrits) + **/ + +#define MICRO_PER_SEC 1000000 + +#define xwADDTIME(result, a, b) do { \ + (result).tv_sec = (a).tv_sec + (b).tv_sec; \ + if (((result).tv_usec = (a).tv_usec + (b).tv_usec) >= MICRO_PER_SEC) { \ + (result).tv_sec++; \ + (result).tv_usec -= MICRO_PER_SEC; \ + } } while (0) + +#define xwSUBTIME(result, a, b) do { \ + (result).tv_sec = (a).tv_sec - (b).tv_sec; \ + if (((result).tv_usec = (a).tv_usec - (b).tv_usec) < 0) { \ + (result).tv_sec--; \ + (result).tv_usec += MICRO_PER_SEC; \ + } } while (0) + +#define xwSETMINTIME(a, b) do { \ + if ((b).tv_sec < (a).tv_sec || \ + ((b).tv_sec == (a).tv_sec && (b).tv_usec < (a).tv_usec)) \ + (a) = (b); \ + } while (0) + +#define xwTIMEGEQ(a, b) ((a).tv_sec > (b).tv_sec || \ + ((a).tv_sec == (b).tv_sec && (a).tv_usec >= (b).tv_usec)) + +#define xwTIMEGT(a, b) ((a).tv_sec > (b).tv_sec || \ + ((a).tv_sec == (b).tv_sec && (a).tv_usec > (b).tv_usec)) + +#define xwTIMELEQ0(a) ((a).tv_sec < 0 || ((a).tv_sec == 0 && (a).tv_usec <= 0)) + +#ifdef X_GETTIMEOFDAY +# define xwGETTIMEOFDAY(a) X_GETTIMEOFDAY(a) +#elif GETTIMEOFDAY_PROTO == 0 +EXTERN int gettimeofday(struct timeval *, struct timezone *); +# define xwGETTIMEOFDAY(a) gettimeofday((a), 0) +#elif GETTIMEOFDAY_PROTO == 1 +# define xwGETTIMEOFDAY(a) gettimeofday((a)) +#else +# define xwGETTIMEOFDAY(a) gettimeofday((a), 0) +#endif + +#define xwGETTIME(a) do { xwGETTIMEOFDAY(&(a)); xwSUBTIME((a), (a), genesis_time); } while (0) +struct timeval genesis_time; + + +/***** + * THE VIEWER STRUCTURE + **/ + +static unsigned pixel_memory_limit_kb = 40000; +static unsigned pixel_memory_kb; + +typedef struct Gt_Viewer { + + Display *display; + int screen_number; + Visual *visual; + int depth; + Colormap colormap; + Gif_XContext *gfx; + Cursor arrow_cursor; + Cursor wait_cursor; + + Window parent; + int top_level; + + Window window; + int use_window; + int width; + int height; + int resizable; + int being_deleted; + + Gif_Stream *gfs; + const char *name; + const char *title; + + Gif_Image **im; + int nim; + + Pixmap pixmap; + int im_pos; + int was_unoptimized; + + Gif_XFrame *unoptimized_frames; + int n_unoptimized_frames; + + struct Gt_Viewer *next; + + int can_animate; + int animating; + int unoptimizing; + int scheduled; + int preparing; + struct Gt_Viewer *anim_next; + struct timeval timer; + int anim_loop; + +} Gt_Viewer; + +const char *program_name = "gifview"; +static Clp_Parser* clp; + +static const char *cur_display_name = 0; +static Display *cur_display = 0; +static const char *cur_geometry_spec = 0; +static Cursor cur_arrow_cursor = 0; +static Cursor cur_wait_cursor = 0; +static const char *cur_resource_name; +static const char *cur_window_title = 0; +static Window cur_use_window = None; +static int cur_use_window_new = 0; +static const char *cur_background_color = "black"; + +static Gt_Viewer *viewers; +static Gt_Viewer *animations; +static int animating = 0; +static int unoptimizing = 0; +static int install_colormap = 0; +static int interactive = 1; +static int min_delay = 0; +static int fallback_delay = 0; + +static struct timeval preparation_time; + + +#define DISPLAY_OPT 300 +#define UNOPTIMIZE_OPT 301 +#define VERSION_OPT 302 +#define ANIMATE_OPT 303 +#define GEOMETRY_OPT 304 +#define NAME_OPT 305 +#define HELP_OPT 306 +#define WINDOW_OPT 307 +#define INSTALL_COLORMAP_OPT 308 +#define INTERACTIVE_OPT 309 +#define BACKGROUND_OPT 310 +#define NEW_WINDOW_OPT 311 +#define TITLE_OPT 312 +#define MIN_DELAY_OPT 313 +#define FALLBACK_DELAY_OPT 314 +#define MEMORY_LIMIT_OPT 315 + +#define WINDOW_TYPE (Clp_ValFirstUser) + +const Clp_Option options[] = { + { "animate", 'a', ANIMATE_OPT, 0, Clp_Negate }, + { "background", 'b', BACKGROUND_OPT, Clp_ValString, 0 }, + { "bg", 't', BACKGROUND_OPT, Clp_ValString, 0 }, + { "display", 'd', DISPLAY_OPT, Clp_ValStringNotOption, 0 }, + { "geometry", 'g', GEOMETRY_OPT, Clp_ValString, 0 }, + { "install-colormap", 'i', INSTALL_COLORMAP_OPT, 0, Clp_Negate }, + { "interactive", 'e', INTERACTIVE_OPT, 0, Clp_Negate }, + { "help", 0, HELP_OPT, 0, 0 }, + { "memory-limit", 0, MEMORY_LIMIT_OPT, Clp_ValUnsigned, Clp_Negate }, + { "min-delay", 0, MIN_DELAY_OPT, Clp_ValInt, Clp_Negate }, + { "fallback-delay", 0, FALLBACK_DELAY_OPT, Clp_ValInt, Clp_Negate }, + { "name", 0, NAME_OPT, Clp_ValString, 0 }, + { "title", 'T', TITLE_OPT, Clp_ValString, 0 }, + { "unoptimize", 'U', UNOPTIMIZE_OPT, 0, Clp_Negate }, + { "version", 0, VERSION_OPT, 0, 0 }, + { "window", 'w', WINDOW_OPT, WINDOW_TYPE, 0 }, + { "new-window", 0, NEW_WINDOW_OPT, WINDOW_TYPE, 0 } +}; + + +/***** + * Diagnostics + **/ + +void fatal_error(const char* format, ...) { + char buf[BUFSIZ]; + int n = snprintf(buf, BUFSIZ, "%s: ", program_name); + va_list val; + va_start(val, format); + Clp_vsnprintf(clp, buf + n, BUFSIZ - n, format, val); + va_end(val); + fputs(buf, stderr); + exit(1); +} + +void error(const char* format, ...) { + char buf[BUFSIZ]; + int n = snprintf(buf, BUFSIZ, "%s: ", program_name); + va_list val; + va_start(val, format); + Clp_vsnprintf(clp, buf + n, BUFSIZ - n, format, val); + va_end(val); + fputs(buf, stderr); +} + +void warning(const char* format, ...) { + char buf[BUFSIZ]; + int n = snprintf(buf, BUFSIZ, "%s: warning: ", program_name); + va_list val; + va_start(val, format); + Clp_vsnprintf(clp, buf + n, BUFSIZ - n, format, val); + va_end(val); + fputs(buf, stderr); +} + +void short_usage(void) { + Clp_fprintf(clp, stderr, "\ +Usage: %s [--display DISPLAY] [OPTION]... [FILE | FRAME]...\n\ +Try %<%s --help%> for more information.\n", + program_name, program_name); +} + +void usage(void) { + Clp_fprintf(clp, stdout, "\ +% is a lightweight GIF viewer for X. It can display animated GIFs as\n\ +slideshows, one frame at a time, or as animations.\n\ +\n\ +Usage: %s [--display DISPLAY] [OPTION]... [FILE | FRAME]...\n\n", program_name); + Clp_fprintf(clp, stdout, "\ +Options are:\n\ + -a, --animate Animate multiframe GIFs.\n\ + -U, --unoptimize Unoptimize displayed GIFs.\n\ + -d, --display DISPLAY Set display to DISPLAY.\n\ + --name NAME Set application resource name to NAME.\n\ + -g, --geometry GEOMETRY Set window geometry.\n\ + -T, --title TITLE Set window title.\n"); + Clp_fprintf(clp, stdout, "\ + -w, --window WINDOW Show GIF in existing WINDOW.\n\ + --new-window WINDOW Show GIF in new child of existing WINDOW.\n\ + -i, --install-colormap Use a private colormap.\n\ + --bg, --background COLOR Use COLOR for transparent pixels.\n\ + --min-delay DELAY Set minimum frame delay to DELAY/100 sec.\n\ + --fallback-delay DELAY Set fallback frame delay to DELAY/100 sec.\n\ + +e, --no-interactive Ignore buttons and keystrokes.\n"); + Clp_fprintf(clp, stdout, "\ + --memory-limit LIM Cache at most LIM megabytes of animation.\n\ + --help Print this message and exit.\n\ + --version Print version number and exit.\n\ +\n\ +Frame selections: #num, #num1-num2, #num1-, #name\n\n"); + Clp_fprintf(clp, stdout, "\ +Keystrokes:\n\ + [N]/[Space] Go to next frame. [P]/[B] Go to previous frame.\n\ + [R]/[<] Go to first frame. [>] Go to last frame.\n\ + [ESC] Stop animation. [S]/[A] Toggle animation.\n\ + [U] Toggle unoptimization. [Backspace]/[W] Delete window.\n\ + [Q] Quit.\n\ +\n\ +Left mouse button goes to next frame, right mouse button deletes window.\n\ +\n\ +Report bugs to .\n"); +} + + +/***** + * Window creation + **/ + +#if defined(__cplusplus) || defined(c_plusplus) +#define VISUAL_CLASS c_class +#else +#define VISUAL_CLASS class +#endif + +static void +choose_visual(Gt_Viewer *viewer) +{ + Display *display = viewer->display; + int screen_number = viewer->screen_number; + VisualID default_visualid = DefaultVisual(display, screen_number)->visualid; + + XVisualInfo visi_template; + int nv, i; + XVisualInfo *v, *best_v = 0; + Gt_Viewer *trav; + + /* Look for an existing Gt_Viewer with the same display and screen number */ + if (!install_colormap) + for (trav = viewers; trav; trav = trav->next) + if (trav != viewer && trav->display == display + && trav->screen_number == screen_number) { + viewer->visual = trav->visual; + viewer->depth = trav->depth; + viewer->colormap = trav->colormap; + viewer->gfx = trav->gfx; + viewer->gfx->refcount++; + return; + } + + /* Find the default visual's XVisualInfo & put it in best_v */ + visi_template.screen = screen_number; + v = XGetVisualInfo(display, VisualScreenMask, &visi_template, &nv); + for (i = 0; i < nv && !best_v; i++) + if (v[i].visualid == default_visualid) + best_v = &v[i]; + + if (!best_v) { + + /* This should never happen. If we can't find the default visual's + XVisualInfo, we just use the default visual */ + viewer->visual = DefaultVisual(display, screen_number); + viewer->depth = DefaultDepth(display, screen_number); + viewer->colormap = DefaultColormap(display, screen_number); + + } else { + + /* Which visual to choose? This isn't exactly a simple decision, since + we want to avoid colormap flashing while choosing a nice visual. So + here's the algorithm: Prefer the default visual, or take a TrueColor + visual with strictly greater depth. */ + for (i = 0; i < nv; i++) + if (v[i].depth > best_v->depth && v[i].VISUAL_CLASS == TrueColor) + best_v = &v[i]; + + viewer->visual = best_v->visual; + viewer->depth = best_v->depth; + if (best_v->visualid != default_visualid + || (best_v->VISUAL_CLASS == PseudoColor && install_colormap)) + viewer->colormap = + XCreateColormap(display, RootWindow(display, screen_number), + viewer->visual, AllocNone); + else + viewer->colormap = DefaultColormap(display, screen_number); + + } + + viewer->gfx = Gif_NewXContextFromVisual + (display, screen_number, viewer->visual, viewer->depth, viewer->colormap); + viewer->gfx->refcount++; + + if (v) XFree(v); +} + + +Gt_Viewer * +new_viewer(Display *display, Gif_Stream *gfs, const char *name) +{ + Gt_Viewer *viewer; + int i; + + /* Make the Gt_Viewer structure */ + viewer = Gif_New(Gt_Viewer); + viewer->display = display; + + if (cur_use_window) { + XWindowAttributes attr; + + if (cur_use_window == (Window)(-1)) { /* means use root window */ + viewer->screen_number = DefaultScreen(display); + cur_use_window = RootWindow(display, viewer->screen_number); + } + + XGetWindowAttributes(display, cur_use_window, &attr); + + viewer->screen_number = -1; + for (i = 0; i < ScreenCount(display); i++) + if (ScreenOfDisplay(display, i) == attr.screen) + viewer->screen_number = i; + assert(viewer->screen_number >= 0); + + viewer->visual = attr.visual; + viewer->depth = attr.depth; + viewer->colormap = attr.colormap; + + viewer->gfx = Gif_NewXContextFromVisual + (display, viewer->screen_number, viewer->visual, viewer->depth, + viewer->colormap); + viewer->gfx->refcount++; + + /* Before -- use root window, if that's what we were given; otherwise, + create a child of the window we were given */ + /* 13.Nov.2001 - don't make a child of the window we were given! */ + if (cur_use_window_new) { + viewer->window = None; + viewer->use_window = 0; + } else { + viewer->window = cur_use_window; + viewer->use_window = 1; + } + viewer->parent = cur_use_window; + viewer->top_level = 0; + viewer->resizable = 0; + + } else { + viewer->screen_number = DefaultScreen(display); + choose_visual(viewer); + viewer->window = None; + viewer->parent = RootWindow(display, viewer->screen_number); + viewer->use_window = 0; + viewer->top_level = 1; + viewer->resizable = 1; + } + + /* assign background color */ + if (cur_background_color) { + XColor color; + if (!XParseColor(viewer->display, viewer->colormap, cur_background_color, + &color)) { + error("invalid background color %<%s%>\n", cur_background_color); + cur_background_color = 0; + } else if (!XAllocColor(viewer->display, viewer->colormap, &color)) + warning("can%,t allocate background color\n"); + else { + unsigned long pixel = color.pixel; + Gif_XContext *gfx = viewer->gfx; + if (pixel != gfx->transparent_pixel && gfx->refcount > 1) { + /* copy X context */ + viewer->gfx = Gif_NewXContextFromVisual + (gfx->display, gfx->screen_number, gfx->visual, gfx->depth, + gfx->colormap); + viewer->gfx->refcount++; + gfx->refcount--; + } + viewer->gfx->transparent_pixel = pixel; + } + } + + if (!cur_arrow_cursor) { + cur_arrow_cursor = XCreateFontCursor(display, XC_left_ptr); + cur_wait_cursor = XCreateFontCursor(display, XC_watch); + } + viewer->arrow_cursor = cur_arrow_cursor; + viewer->wait_cursor = cur_wait_cursor; + + viewer->being_deleted = 0; + viewer->gfs = gfs; + gfs->refcount++; + viewer->name = name; + viewer->title = cur_window_title; + viewer->nim = Gif_ImageCount(gfs); + viewer->im = Gif_NewArray(Gif_Image *, viewer->nim); + for (i = 0; i < viewer->nim; i++) + viewer->im[i] = gfs->images[i]; + viewer->pixmap = None; + viewer->im_pos = -1; + viewer->was_unoptimized = 0; + viewer->unoptimized_frames = Gif_NewXFrames(gfs); + viewer->n_unoptimized_frames = 0; + viewer->next = viewers; + viewers = viewer; + viewer->animating = 0; + viewer->unoptimizing = unoptimizing; + viewer->scheduled = 0; + viewer->preparing = 0; + viewer->anim_next = 0; + viewer->anim_loop = 0; + viewer->timer.tv_sec = viewer->timer.tv_usec = 0; + + return viewer; +} + + +void +delete_viewer(Gt_Viewer *viewer) +{ + Gt_Viewer *prev = 0, *trav; + if (viewer->pixmap && !viewer->was_unoptimized) + XFreePixmap(viewer->display, viewer->pixmap); + + for (trav = viewers; trav != viewer; prev = trav, trav = trav->next) + ; + if (prev) prev->next = viewer->next; + else viewers = viewer->next; + + Gif_DeleteXFrames(viewer->gfx, viewer->gfs, viewer->unoptimized_frames); + Gif_DeleteStream(viewer->gfs); + Gif_DeleteArray(viewer->im); + Gif_DeleteXContext(viewer->gfx); + Gif_Delete(viewer); +} + + +static Gt_Viewer * +next_viewer(Gif_Stream *gfs, const char *name) +{ + Gt_Viewer *viewer = new_viewer(cur_display, gfs, name); + cur_use_window = None; + return viewer; +} + +static Gt_Viewer * +get_input_stream(const char *name) +{ + FILE *f; + Gif_Stream *gfs = 0; + + if (name == 0 || strcmp(name, "-") == 0) { +#ifndef OUTPUT_GIF_TO_TERMINAL + if (isatty(fileno(stdin))) { + error(": is a terminal\n"); + return NULL; + } +#endif + f = stdin; +#if defined(_MSDOS) || defined(_WIN32) + _setmode(_fileno(stdin), _O_BINARY); +#elif defined(__DJGPP__) + setmode(fileno(stdin), O_BINARY); +#elif defined(__EMX__) + _fsetmode(stdin, "b"); +#endif + name = ""; + } else + f = fopen(name, "rb"); + if (!f) { + error("%s: %s\n", name, strerror(errno)); + return 0; + } + + gfs = Gif_FullReadFile(f, GIF_READ_COMPRESSED, 0, 0); + fclose(f); + + if (!gfs || Gif_ImageCount(gfs) == 0) { + error("%s: file not in GIF format\n", name); + Gif_DeleteStream(gfs); + return 0; + } + + if (!cur_display) { + cur_display = XOpenDisplay(cur_display_name); + if (!cur_display) { + error("can%,t open display\n"); + return 0; + } + } + + return next_viewer(gfs, name); +} + + +/***** + * Schedule stuff + **/ + +void +switch_animating(Gt_Viewer *viewer, int animating) +{ + int i; + Gif_Stream *gfs = viewer->gfs; + if (animating == viewer->animating || !viewer->can_animate) + return; + for (i = 0; i < gfs->nimages; i++) + viewer->im[i] = gfs->images[i]; + viewer->animating = animating; + if (!animating) + viewer->timer.tv_sec = viewer->timer.tv_usec = 0; +} + + +void +unschedule(Gt_Viewer *viewer) +{ + Gt_Viewer *prev, *trav; + if (!viewer->scheduled) return; + for (prev = 0, trav = animations; trav; prev = trav, trav = trav->anim_next) + if (trav == viewer) + break; + if (trav) { + if (prev) prev->anim_next = viewer->anim_next; + else animations = viewer->anim_next; + } + viewer->scheduled = 0; + viewer->timer.tv_sec = viewer->timer.tv_usec = 0; +} + +void +schedule(Gt_Viewer *viewer) +{ + Gt_Viewer *prev, *trav; + + if (viewer->scheduled) + unschedule(viewer); + + prev = 0; + for (trav = animations; trav; prev = trav, trav = trav->anim_next) + if (xwTIMEGEQ(trav->timer, viewer->timer)) + break; + if (prev) { + viewer->anim_next = trav; + prev->anim_next = viewer; + } else { + viewer->anim_next = animations; + animations = viewer; + } + + viewer->scheduled = 1; +} + +void +schedule_next_frame(Gt_Viewer *viewer) +{ + struct timeval interval; + int delay = viewer->im[viewer->im_pos]->delay; + int next_pos = viewer->im_pos + 1; + if (delay < 1) + delay = fallback_delay; + if (delay < min_delay) + delay = min_delay; + if (next_pos == viewer->nim) + next_pos = 0; + + if (viewer->timer.tv_sec == 0 && viewer->timer.tv_usec == 0) + xwGETTIME(viewer->timer); + + interval.tv_sec = delay / 100; + interval.tv_usec = (delay % 100) * (MICRO_PER_SEC / 100); + if (delay == 0) + interval.tv_usec = 2000; + xwADDTIME(viewer->timer, viewer->timer, interval); + + /* 1.Aug.2002 - leave some time to prepare the frame if necessary */ + if (viewer->unoptimized_frames[next_pos].pixmap) { + xwSUBTIME(viewer->timer, viewer->timer, preparation_time); + viewer->preparing = 1; + } + + schedule(viewer); +} + + +/***** + * X stuff + **/ + +int +parse_geometry(const char *const_g, XSizeHints *sh, int screen_width, + int screen_height) +{ + char *g = (char *)const_g; + sh->flags = 0; + + if (isdigit(*g)) { + sh->flags |= USSize; + sh->width = strtol(g, &g, 10); + if (g[0] == 'x' && isdigit(g[1])) + sh->height = strtol(g + 1, &g, 10); + else + goto error; + } else if (!*g) + goto error; + + if (*g == '+' || *g == '-') { + int x_minus, y_minus; + sh->flags |= USPosition | PWinGravity; + x_minus = *g == '-'; + sh->x = strtol(g + 1, &g, 10); + if (x_minus) sh->x = screen_width - sh->x - sh->width; + + y_minus = *g == '-'; + if (*g == '-' || *g == '+') + sh->y = strtol(g + 1, &g, 10); + else + goto error; + if (y_minus) sh->y = screen_height - sh->y - sh->height; + + if (x_minus) + sh->win_gravity = y_minus ? SouthEastGravity : NorthEastGravity; + else + sh->win_gravity = y_minus ? SouthWestGravity : NorthWestGravity; + + } else if (*g) + goto error; + + return 1; + + error: + warning("bad geometry specification\n"); + sh->flags = 0; + return 0; +} + + +static Atom wm_delete_window_atom; +static Atom wm_protocols_atom; + +void +create_viewer_window(Gt_Viewer *viewer, int w, int h) +{ + Display *display = viewer->display; + char *stringlist[2]; + XTextProperty window_name, icon_name; + XClassHint classh; + XSizeHints *sizeh = XAllocSizeHints(); /* sets all fields to 0 */ + + /* Set the window's geometry */ + sizeh->width = w ? w : 1; + sizeh->height = h ? h : 1; + if (cur_geometry_spec) { + int scr_width = DisplayWidth(viewer->display, viewer->screen_number); + int scr_height = DisplayHeight(viewer->display, viewer->screen_number); + parse_geometry(cur_geometry_spec, sizeh, scr_width, scr_height); + } + + /* Open the display and create the window */ + if (!viewer->window) { + XSetWindowAttributes x_set_attr; + unsigned long x_set_attr_mask; + x_set_attr.colormap = viewer->colormap; + x_set_attr.backing_store = NotUseful; + x_set_attr.save_under = False; + x_set_attr.border_pixel = 0; + x_set_attr.background_pixel = 0; + x_set_attr_mask = CWColormap | CWBorderPixel | CWBackPixel + | CWBackingStore | CWSaveUnder; + + viewer->window = XCreateWindow + (display, viewer->parent, + sizeh->x, sizeh->y, sizeh->width, sizeh->height, 0, + viewer->depth, InputOutput, viewer->visual, + x_set_attr_mask, &x_set_attr); + XDefineCursor(display, viewer->window, viewer->arrow_cursor); + } + + /* If user gave us geometry, don't change the size later */ + if (sizeh->flags & USSize) + viewer->resizable = 0; + viewer->width = w; + viewer->height = h; + + /* Set the window's title and class (for window manager resources) */ + if (viewer->top_level) { + stringlist[0] = "gifview"; + stringlist[1] = 0; + XStringListToTextProperty(stringlist, 1, &window_name); + XStringListToTextProperty(stringlist, 1, &icon_name); + classh.res_name = (char *)cur_resource_name; + classh.res_class = "Gifview"; + XSetWMProperties(display, viewer->window, &window_name, &icon_name, + NULL, 0, sizeh, NULL, &classh); + XFree(window_name.value); + XFree(icon_name.value); + + if (!wm_delete_window_atom) { + wm_delete_window_atom = XInternAtom(display, "WM_DELETE_WINDOW", False); + wm_protocols_atom = XInternAtom(display, "WM_PROTOCOLS", False); + } + XSetWMProtocols(display, viewer->window, &wm_delete_window_atom, 1); + } + + if (interactive) + XSelectInput(display, viewer->window, ButtonPressMask | KeyPressMask + | StructureNotifyMask); + else + XSelectInput(display, viewer->window, StructureNotifyMask); + + XFree(sizeh); +} + + +void +pre_delete_viewer(Gt_Viewer *viewer) +{ + if (viewer->being_deleted) return; + viewer->being_deleted = 1; + + if (viewer->scheduled) unschedule(viewer); + + if (viewer->window && !viewer->use_window) + XDestroyWindow(viewer->display, viewer->window); + else + delete_viewer(viewer); +} + + +Gt_Viewer * +find_viewer(Display *display, Window window) +{ + Gt_Viewer *v; + for (v = viewers; v; v = v->next) + if (v->display == display && v->window == window) + return v; + return 0; +} + + +void +set_viewer_name(Gt_Viewer *viewer, int slow_number) +{ + Gif_Image *gfi; + char *strs[2]; + char *identifier; + XTextProperty name_prop; + int im_pos = (slow_number >= 0 ? slow_number : viewer->im_pos); + int len; + + if (!viewer->top_level || im_pos >= viewer->nim || viewer->being_deleted) + return; + + gfi = viewer->im[im_pos]; + len = strlen(viewer->title) + strlen(viewer->name) + 14; + identifier = (slow_number >= 0 ? (char *)0 : gfi->identifier); + if (identifier) + len += 2 + strlen(identifier); + + strs[0] = Gif_NewArray(char, len); + if (strcmp(viewer->title, "gifview") != 0) + snprintf(strs[0], len, "%s", viewer->title); + else if (slow_number >= 0) + snprintf(strs[0], len, "gifview: %s [#%d]", viewer->name, im_pos); + else if (viewer->nim == 1 && identifier) + snprintf(strs[0], len, "gifview: %s #%s", viewer->name, identifier); + else if (viewer->animating || viewer->nim == 1) + snprintf(strs[0], len, "gifview: %s", viewer->name); + else if (!identifier) + snprintf(strs[0], len, "gifview: %s #%d", viewer->name, im_pos); + else + snprintf(strs[0], len, "gifview: %s #%d #%s", viewer->name, im_pos, identifier); + strs[1] = 0; + + XStringListToTextProperty(strs, 1, &name_prop); + XSetWMName(viewer->display, viewer->window, &name_prop); + XSetWMIconName(viewer->display, viewer->window, &name_prop); + + XFree(name_prop.value); + Gif_DeleteArray(strs[0]); +} + +static unsigned screen_memory_kb(const Gt_Viewer* viewer) { + return 1 + ((unsigned) (viewer->gfs->screen_width + * viewer->gfs->screen_height) / 334); +} + +static Pixmap +unoptimized_frame(Gt_Viewer *viewer, int frame, int slow) +{ + /* create a new unoptimized frame if necessary */ + if (!viewer->unoptimized_frames[frame].pixmap) { + (void) Gif_XNextImage(viewer->gfx, viewer->gfs, frame, + viewer->unoptimized_frames); + pixel_memory_kb += screen_memory_kb(viewer); + viewer->unoptimized_frames[viewer->n_unoptimized_frames].user_data = + frame; + ++viewer->n_unoptimized_frames; + if (slow) { + set_viewer_name(viewer, frame); + XFlush(viewer->display); + } + } + + /* kill some old frames if over the memory limit */ + while (pixel_memory_limit_kb != (unsigned) -1 + && pixel_memory_limit_kb < pixel_memory_kb + && viewer->n_unoptimized_frames > 1) { + int killidx, killframe, i = 0; + do { + killidx = random() % viewer->n_unoptimized_frames; + killframe = viewer->unoptimized_frames[killidx].user_data; + ++i; + } while (killframe == frame + || (i < 10 && killframe > frame && killframe < frame + 5) + || (i < 10 && (killframe % 50) == 0)); + XFreePixmap(viewer->display, viewer->unoptimized_frames[killframe].pixmap); + viewer->unoptimized_frames[killframe].pixmap = None; + --viewer->n_unoptimized_frames; + viewer->unoptimized_frames[killidx].user_data = + viewer->unoptimized_frames[viewer->n_unoptimized_frames].user_data; + pixel_memory_kb -= screen_memory_kb(viewer); + } + + return viewer->unoptimized_frames[frame].pixmap; +} + +void +prepare_frame(Gt_Viewer *viewer, int frame) +{ + Display *display = viewer->display; + Window window = viewer->window; + int changed_cursor = 0; + + if (viewer->being_deleted || !viewer->animating) + return; + + if (frame < 0 || frame > viewer->nim - 1) + frame = 0; + + /* Change cursor if we need to wait. */ + if ((viewer->animating || viewer->unoptimizing) + && !viewer->unoptimized_frames[frame].pixmap) { + if (frame > viewer->im_pos + 10 || frame < viewer->im_pos) { + changed_cursor = 1; + XDefineCursor(display, window, viewer->wait_cursor); + XFlush(display); + } + } + + /* Prepare the frame */ + (void) unoptimized_frame(viewer, frame, changed_cursor && !viewer->animating); + + /* Restore cursor */ + if (changed_cursor) + XDefineCursor(display, window, viewer->arrow_cursor); + + /* schedule actual view of window */ + xwADDTIME(viewer->timer, viewer->timer, preparation_time); + viewer->preparing = 0; + schedule(viewer); +} + +void +view_frame(Gt_Viewer *viewer, int frame) +{ + Display *display = viewer->display; + Window window = viewer->window; + Pixmap old_pixmap = viewer->pixmap; + int need_set_name = 0; + + if (viewer->being_deleted) + return; + + if (frame < 0) + frame = 0; + if (frame > viewer->nim - 1 && viewer->animating) { + int loopcount = viewer->gfs->loopcount; + if (loopcount == 0 || loopcount > viewer->anim_loop) { + viewer->anim_loop++; + frame = 0; + } else { + switch_animating(viewer, 0); + need_set_name = 1; + } + } + if (frame > viewer->nim - 1) + frame = viewer->nim - 1; + + if (frame != viewer->im_pos) { + Gif_Image *gfi = viewer->im[frame]; + int width, height, changed_cursor = 0; + + /* Change cursor if we need to wait. */ + if ((viewer->animating || viewer->unoptimizing) + && !viewer->unoptimized_frames[frame].pixmap) { + if (frame > viewer->im_pos + 10 || frame < viewer->im_pos) { + changed_cursor = 1; + XDefineCursor(display, window, viewer->wait_cursor); + XFlush(display); + } + } + + /* 5/26/98 Do some noodling around to try and use memory most effectively. + If animating, keep the uncompressed frame; otherwise, throw it away. */ + if (viewer->animating || viewer->unoptimizing) + viewer->pixmap = unoptimized_frame(viewer, frame, + changed_cursor && !viewer->animating); + else + viewer->pixmap = Gif_XImage(viewer->gfx, viewer->gfs, gfi); + + /* put the image on the window */ + if (viewer->animating || viewer->unoptimizing) + width = viewer->gfs->screen_width, height = viewer->gfs->screen_height; + else + width = Gif_ImageWidth(gfi), height = Gif_ImageHeight(gfi); + if (!window) { + create_viewer_window(viewer, width, height); + window = viewer->window; + } + XSetWindowBackgroundPixmap(display, window, viewer->pixmap); + if (old_pixmap || viewer->use_window) /* clear existing window */ + XClearWindow(display, window); + /* Only change size after changing pixmap. */ + if ((viewer->width != width || viewer->height != height) + && viewer->resizable) { + XWindowChanges winch; + winch.width = viewer->width = width; + winch.height = viewer->height = height; + XReconfigureWMWindow + (display, window, viewer->screen_number, + CWWidth | CWHeight, &winch); + } + + /* Get rid of old pixmaps */ + if (!viewer->was_unoptimized && old_pixmap) + XFreePixmap(display, old_pixmap); + viewer->was_unoptimized = viewer->animating || viewer->unoptimizing; + + /* Restore cursor */ + if (changed_cursor) + XDefineCursor(display, window, viewer->arrow_cursor); + + /* Do we need a new name? */ + if ((!viewer->animating && Gif_ImageCount(viewer->gfs) > 1) + || old_pixmap == None) + need_set_name = 1; + } + + viewer->im_pos = frame; + viewer->preparing = 0; + + if (need_set_name) + set_viewer_name(viewer, -1); + + if (!old_pixmap && !viewer->use_window) + /* first image; map the window */ + XMapRaised(display, window); + else if (viewer->animating) + /* only schedule next frame if image is already mapped */ + schedule_next_frame(viewer); +} + + +/***** + * Command line arguments: marking frames, being done with streams + **/ + +int +frame_argument(Gt_Viewer *viewer, const char *arg) +{ + const char *c = arg; + int n1 = 0; + + /* Get a number range (#x, #x-y, #x-, or #-y). First, read x. */ + if (isdigit(c[0])) + n1 = strtol(arg, (char **)&c, 10); + + /* It really was a number range only if c is now at the end of the + argument. */ + if (c[0] != 0) { + Gif_Image *gfi = Gif_GetNamedImage(viewer->gfs, c); + if (!gfi) + error("no frame named %<%s%>\n", c); + else + n1 = Gif_ImageNumber(viewer->gfs, gfi); + } else { + if (n1 < 0 || n1 >= viewer->nim) { + error("no frame number %d\n", n1); + n1 = 0; + } + } + + return n1; +} + + +void +input_stream_done(Gt_Viewer *viewer, int first_frame) +{ + viewer->can_animate = Gif_ImageCount(viewer->gfs) > 1; + switch_animating(viewer, animating && viewer->can_animate); + if (first_frame < 0) + first_frame = 0; + view_frame(viewer, first_frame); +} + + +void +key_press(Gt_Viewer *viewer, XKeyEvent *e) +{ + char buf[32]; + KeySym key; + int nbuf = XLookupString(e, buf, 32, &key, 0); + if (nbuf > 1) buf[0] = 0; /* ignore multikey sequences */ + + if (key == XK_space || key == XK_F || key == XK_f || key == XK_N || key == XK_n) + /* space, N or F: one frame ahead */ + view_frame(viewer, viewer->im_pos + 1); + + else if (key == XK_B || key == XK_b || key == XK_P || key == XK_p) + /* B or P: one frame back */ + view_frame(viewer, viewer->im_pos - 1); + + else if (key == XK_W || key == XK_w || key == XK_BackSpace) + /* backspace: delete viewer */ + pre_delete_viewer(viewer); + + else if (key == XK_Q || key == XK_q) + /* Q: quit application */ + exit(0); + + else if (key == XK_S || key == XK_s || key == XK_a || key == XK_A) { + /* S or A: toggle animation */ + switch_animating(viewer, !viewer->animating); + + if (viewer->animating) { + int pos = viewer->im_pos; + if (viewer->im_pos >= viewer->nim - 1) { + pos = 0; + viewer->anim_loop = 0; + } + view_frame(viewer, pos); + + } else + unschedule(viewer); + + set_viewer_name(viewer, -1); + + } else if (key == XK_U || key == XK_u) { + /* U: toggle unoptimizing */ + int pos = viewer->im_pos; + viewer->unoptimizing = !viewer->unoptimizing; + if (!viewer->animating) { + viewer->im_pos = -1; + view_frame(viewer, pos); + set_viewer_name(viewer, -1); + } + + } else if (key == XK_R || key == XK_r + || (nbuf == 1 && buf[0] == '<')) { + /* R or <: reset to first frame */ + unschedule(viewer); + viewer->anim_loop = 0; + view_frame(viewer, 0); + + } else if (nbuf == 1 && buf[0] == '>') { + /* >: reset to last frame */ + unschedule(viewer); + viewer->anim_loop = 0; + view_frame(viewer, viewer->nim - 1); + + } else if (key == XK_Escape && viewer->animating) { + /* Escape: stop animation */ + switch_animating(viewer, 0); + unschedule(viewer); + set_viewer_name(viewer, -1); + + } else if (key == XK_Z || key == XK_z) { + /* Z: trigger resizability */ + viewer->resizable = !viewer->resizable; + } +} + + +void +loop(void) +{ + struct timeval now, stop_loop, stop_delta; + fd_set xfds; + XEvent e; + int pending; + Gt_Viewer *v; + Display *display = viewers->display; + int x_socket = ConnectionNumber(display); + stop_delta.tv_sec = 0; + stop_delta.tv_usec = 200000; + + xwGETTIME(now); + FD_ZERO(&xfds); + + while (viewers) { + + /* Check for any animations */ + /* 13.Feb.2001 - Use the 'pending' counter to avoid a tight loop + if all the frames in an animation have delay 0. Reported by + Franc,ois Petitjean. */ + /* 1.Aug.2002 - Switch to running the loop for max 0.2s. */ + xwADDTIME(stop_loop, now, stop_delta); + while (animations && xwTIMEGEQ(now, animations->timer) + && xwTIMEGEQ(stop_loop, now)) { + v = animations; + animations = v->anim_next; + v->scheduled = 0; + if (v->preparing) + prepare_frame(v, v->im_pos + 1); + else { + if (xwTIMEGEQ(now, v->timer)) + v->timer = now; + view_frame(v, v->im_pos + 1); + } + xwGETTIME(now); + } + + pending = XPending(display); + if (!pending) { + /* select() until event arrives */ + struct timeval timeout, *timeout_ptr; + int retval; + + if (animations) { + xwSUBTIME(timeout, animations->timer, now); + timeout_ptr = &timeout; + } else + timeout_ptr = 0; + + FD_SET(x_socket, &xfds); + retval = select(x_socket + 1, &xfds, 0, 0, timeout_ptr); + pending = (retval <= 0 ? 0 : FD_ISSET(x_socket, &xfds)); + } + + if (pending) + while (XPending(display)) { + XNextEvent(display, &e); + v = find_viewer(e.xany.display, e.xany.window); + if (v) { + if (interactive) { + if (e.type == ButtonPress && e.xbutton.button == 1) + /* Left mouse button: go to next frame */ + view_frame(v, v->im_pos + 1); + + else if (e.type == ButtonPress && e.xbutton.button == 4) + /* mousewheel forward */ + view_frame(v, v->im_pos + 1); + + else if (e.type == ButtonPress && e.xbutton.button == 5) + /* mousewheel backward */ + view_frame(v, v->im_pos - 1); + + else if (e.type == ButtonPress && e.xbutton.button == 3) + /* Right mouse button: delete window */ + pre_delete_viewer(v); + + else if (e.type == KeyPress) + /* Key press: call function */ + key_press(v, &e.xkey); + } + + if (e.type == ClientMessage + && e.xclient.message_type == wm_protocols_atom + && (Atom)(e.xclient.data.l[0]) == wm_delete_window_atom) + /* WM_DELETE_WINDOW message: delete window */ + pre_delete_viewer(v); + + else if (e.type == MapNotify && v->animating + && v->scheduled == 0) + /* Window was just mapped; now, start animating it */ + schedule_next_frame(v); + + else if (e.type == DestroyNotify) + /* Once the window has been destroyed, delete related state */ + delete_viewer(v); + } + } + + xwGETTIME(now); + } +} + + +int +main(int argc, char *argv[]) +{ + Gt_Viewer *viewer = 0; + int viewer_given = 0; + int any_errors = 0; + int first_frame = -1; + + clp = Clp_NewParser(argc, (const char * const *)argv, + sizeof(options) / sizeof(options[0]), options); + Clp_SetOptionChar(clp, '+', Clp_ShortNegated); + Clp_AddStringListType + (clp, WINDOW_TYPE, Clp_AllowNumbers | Clp_StringListLong, + "root", (long) -1, + (const char*) 0); + program_name = cur_resource_name = cur_window_title = Clp_ProgramName(clp); + + xwGETTIMEOFDAY(&genesis_time); + preparation_time.tv_sec = 0; + preparation_time.tv_usec = 200000; + + while (1) { + int opt = Clp_Next(clp); + switch (opt) { + + case DISPLAY_OPT: + if (cur_display) + fatal_error("%<--display%> must come before all other options\n"); + cur_display_name = clp->vstr; + cur_display = 0; + cur_arrow_cursor = cur_wait_cursor = None; + break; + + case TITLE_OPT: + cur_window_title = clp->vstr; + break; + + case GEOMETRY_OPT: + cur_geometry_spec = clp->vstr; + break; + + case NAME_OPT: + cur_resource_name = clp->vstr; + break; + + case UNOPTIMIZE_OPT: + unoptimizing = clp->negated ? 0 : 1; + break; + + case BACKGROUND_OPT: + cur_background_color = clp->vstr; + break; + + case ANIMATE_OPT: + animating = clp->negated ? 0 : 1; + break; + + case INSTALL_COLORMAP_OPT: + install_colormap = clp->negated ? 0 : 1; + break; + + case WINDOW_OPT: + cur_use_window = clp->val.ul; + cur_use_window_new = 0; + break; + + case NEW_WINDOW_OPT: + cur_use_window = clp->val.ul; + cur_use_window_new = 1; + break; + + case INTERACTIVE_OPT: + interactive = clp->negated ? 0 : 1; + break; + + case MIN_DELAY_OPT: + min_delay = clp->negated ? 0 : clp->val.i; + break; + + case FALLBACK_DELAY_OPT: + fallback_delay = clp->negated ? 0 : clp->val.i; + break; + + case MEMORY_LIMIT_OPT: + if (clp->negated || clp->val.u >= ((unsigned) -1 / 1000)) + pixel_memory_limit_kb = (unsigned) -1; + else + pixel_memory_limit_kb = clp->val.u * 1000; + break; + + case VERSION_OPT: + printf("gifview (LCDF Gifsicle) %s\n", VERSION); + printf("Copyright (C) 1997-2025 Eddie Kohler\n\ +This is free software; see the source for copying conditions.\n\ +There is NO warranty, not even for merchantability or fitness for a\n\ +particular purpose.\n"); + exit(0); + break; + + case HELP_OPT: + usage(); + exit(0); + break; + + case Clp_NotOption: + if (clp->vstr[0] == '#') { + if (!viewer_given) { + viewer = get_input_stream(0); + viewer_given = 1; + } + if (viewer && first_frame >= 0) { + /* copy viewer if 2 frame specs given */ + input_stream_done(viewer, first_frame); + viewer = next_viewer(viewer->gfs, viewer->name); + } + if (viewer) + first_frame = frame_argument(viewer, clp->vstr + 1); + } else { + if (viewer) input_stream_done(viewer, first_frame); + first_frame = -1; + viewer = get_input_stream(clp->vstr); + viewer_given = 1; + } + break; + + case Clp_Done: + goto done; + + case Clp_BadOption: + any_errors = 1; + break; + + default: + break; + + } + } + + done: + + if (!viewer_given) { + if (any_errors) { + short_usage(); + exit(1); + } + viewer = get_input_stream(0); + } + if (viewer) input_stream_done(viewer, first_frame); + + if (viewers) loop(); + + return 0; +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/gifwrite.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifwrite.c new file mode 100644 index 0000000..a087a71 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifwrite.c @@ -0,0 +1,1214 @@ +/* -*- mode: c; c-basic-offset: 2 -*- */ +/* gifwrite.c - Functions to write GIFs. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of the LCDF GIF library. + + The LCDF GIF library is free software. It is distributed under the GNU + General Public License, version 2; you can copy, distribute, or alter it at + will, as long as this notice is kept intact and this source code is made + available. There is no warranty, express or implied. */ + +#if HAVE_CONFIG_H +# include +#elif !defined(__cplusplus) && !defined(inline) +/* Assume we don't have inline by default */ +# define inline +#endif +#include +#include +#include +#include +#include +#include "kcolor.h" +#ifdef __cplusplus +extern "C" { +#endif + +#define WRITE_BUFFER_SIZE 255 +#define NODES_SIZE GIF_MAX_CODE +#define LINKS_SIZE (GIF_MAX_CODE + 256) + +/* 1.Aug.1999 - Removed code hashing in favor of an adaptive tree strategy + based on Whirlgif-3.04, written by Hans Dinsen-Hansen . Mr. + Dinsen-Hansen brought the adaptive tree strategy to my attention and argued + at length that it was better than hashing. It runs a lot faster, though it + creates identical compression. + + Each code is represented by a Node. The Nodes form a tree with variable + fan-out -- up to 'clear_code' children per Node. There are two kinds of + Node, TABLE and LINKS. In a TABLE node, the children are stored in a table + indexed by suffix -- thus, it's very efficient to determine a given child. + In a LINKS node, the existent children are stored in a linked list. This is + slightly slower to access. When a LINKS node gets more than + 'MAX_LINKS_TYPE-1' children, it is converted to a TABLE node. (This is why + it's an adaptive tree.) + + Problems with this implementation: MAX_LINKS_TYPE is fixed, so GIFs with + very small numbers of colors (2, 4, 8) won't get the speed benefits of + TABLE nodes. */ + +#define TABLE_TYPE 0 +#define LINKS_TYPE 1 +#define MAX_LINKS_TYPE 5 +typedef struct Gif_Node { + Gif_Code code; + uint8_t type; + uint8_t suffix; + uint32_t length; + struct Gif_Node* sibling; + union { + struct Gif_Node* s; + struct Gif_Node** m; + } child; +} Gif_Node; + +typedef struct Gif_LossyNode { + Gif_Node node; + unsigned long diffaccum; + wkcolor error; +} Gif_LossyNode; + +typedef struct Gif_CodeTable { + Gif_Node** links; + int clear_code; + unsigned max_loss; + uint32_t maxpos; + + union { + Gif_Node* nl; + Gif_LossyNode* ls; + } nodes; + int links_pos; + + kcolor* global_kcs; + kcolor* local_kcs; + kcolor* kcs; + double loss_scale; + unsigned* kcdiff; +} Gif_CodeTable; + + +struct Gif_Writer { + FILE *f; + uint8_t *v; + uint32_t pos; + uint32_t cap; + Gif_CompressInfo gcinfo; + int global_size; + int local_size; + int errors; + int cleared; + Gif_CodeTable code_table; + void (*byte_putter)(uint8_t, struct Gif_Writer *); + void (*block_putter)(const uint8_t *, size_t, struct Gif_Writer *); +}; + + +#define gifputbyte(b, grr) ((*grr->byte_putter)(b, grr)) +#define gifputblock(b, l, grr) ((*grr->block_putter)(b, l, grr)) + +static inline void +gifputunsigned(uint16_t uns, Gif_Writer *grr) +{ + gifputbyte(uns & 0xFF, grr); + gifputbyte(uns >> 8, grr); +} + + +static void +file_byte_putter(uint8_t b, Gif_Writer *grr) +{ + fputc(b, grr->f); +} + +static void +file_block_putter(const uint8_t *block, size_t size, Gif_Writer *grr) +{ + if (fwrite(block, 1, size, grr->f) != size) + grr->errors = 1; +} + + +static void +memory_byte_putter(uint8_t b, Gif_Writer *grr) +{ + if (grr->pos >= grr->cap) { + grr->cap = (grr->cap ? grr->cap * 2 : 1024); + Gif_ReArray(grr->v, uint8_t, grr->cap); + } + if (grr->v) { + grr->v[grr->pos] = b; + grr->pos++; + } +} + +static void +memory_block_putter(const uint8_t *data, size_t len, Gif_Writer *grr) +{ + while (grr->pos + len >= grr->cap) { + grr->cap = (grr->cap ? grr->cap * 2 : 1024); + Gif_ReArray(grr->v, uint8_t, grr->cap); + } + if (grr->v) { + memcpy(grr->v + grr->pos, data, len); + grr->pos += len; + } +} + + +static inline int +gfc_init(Gif_CodeTable* gfc, const Gif_CompressInfo* gcinfo) +{ + gfc->max_loss = 0; + if (gcinfo->loss > 0) + gfc->max_loss = gcinfo->loss * kc_distance(kc_make8g(0, 0, 0), kc_make8g(0, 0, 8)); + if (gfc->max_loss > 0) { + gfc->loss_scale = 4.0 / gfc->max_loss; + gfc->nodes.ls = Gif_NewArray(Gif_LossyNode, NODES_SIZE); + } else + gfc->nodes.nl = Gif_NewArray(Gif_Node, NODES_SIZE); + gfc->links = Gif_NewArray(Gif_Node*, LINKS_SIZE); + gfc->global_kcs = NULL; + gfc->local_kcs = NULL; + gfc->kcs = NULL; + gfc->kcdiff = NULL; + return (gfc->max_loss > 0 ? !!gfc->nodes.ls : !!gfc->nodes.nl) && gfc->links; +} + +static int +gif_writer_init(Gif_Writer* grr, FILE* f, const Gif_CompressInfo* gcinfo) +{ + grr->f = f; + grr->v = NULL; + grr->pos = grr->cap = 0; + if (gcinfo) + grr->gcinfo = *gcinfo; + else + Gif_InitCompressInfo(&grr->gcinfo); + grr->errors = 0; + grr->cleared = 0; + if (f) { + grr->byte_putter = file_byte_putter; + grr->block_putter = file_block_putter; + } else { + grr->byte_putter = memory_byte_putter; + grr->block_putter = memory_block_putter; + } + return gfc_init(&grr->code_table, gcinfo); +} + +static void +gif_writer_cleanup(Gif_Writer* grr) +{ + Gif_DeleteArray(grr->v); + if (grr->code_table.max_loss > 0) + Gif_DeleteArray(grr->code_table.nodes.ls); + else + Gif_DeleteArray(grr->code_table.nodes.nl); + Gif_DeleteArray(grr->code_table.links); + Gif_DeleteArray(grr->code_table.global_kcs); + Gif_DeleteArray(grr->code_table.local_kcs); + Gif_DeleteArray(grr->code_table.kcdiff); +} + + +static inline void +gfc_clear(Gif_CodeTable *gfc) +{ + int c; + /* The first clear_code nodes are reserved for single-pixel codes */ + gfc->links_pos = gfc->clear_code; + for (c = 0; c < gfc->clear_code; c++) { + Gif_Node* n = gfc->max_loss > 0 ? &gfc->nodes.ls[c].node : &gfc->nodes.nl[c]; + n->code = c; + n->type = LINKS_TYPE; + n->suffix = c; + n->length = 1; + n->child.s = NULL; + gfc->links[c] = n; + } +} + +static inline Gif_Node * +gfc_lookup(Gif_CodeTable *gfc, Gif_Node *node, uint8_t suffix) +{ + assert(suffix < gfc->clear_code); + if (!node) + return gfc->links[suffix]; + if (node->type == TABLE_TYPE) + return node->child.m[suffix]; + for (node = node->child.s; node; node = node->sibling) + if (node->suffix == suffix) + return node; + return NULL; +} + +static void +gfc_change_node_to_table(Gif_CodeTable *gfc, Gif_Node *work_node, + Gif_Node *next_node) +{ + /* change links node to table node */ + Gif_Code c; + Gif_Node **table = &gfc->links[gfc->links_pos]; + Gif_Node *n; + gfc->links_pos += gfc->clear_code; + + for (c = 0; c < gfc->clear_code; c++) + table[c] = 0; + table[next_node->suffix] = next_node; + for (n = work_node->child.s; n; n = n->sibling) + table[n->suffix] = n; + + work_node->type = TABLE_TYPE; + work_node->child.m = table; +} + +static inline void +gfc_define(Gif_CodeTable* gfc, Gif_Node* work_node, uint8_t suffix, + Gif_Code next_code) +{ + /* Add a new code to our dictionary. First reserve a node for the + added code. It's LINKS_TYPE at first. */ + Gif_Node* next_node = gfc->max_loss > 0 ? &gfc->nodes.ls[next_code].node : &gfc->nodes.nl[next_code]; + next_node->code = next_code; + next_node->type = LINKS_TYPE; + next_node->suffix = suffix; + next_node->length = work_node->length + 1; + next_node->child.s = NULL; + + /* link next_node into work_node's set of children */ + if (work_node->type == TABLE_TYPE) + work_node->child.m[suffix] = next_node; + else if (work_node->type < MAX_LINKS_TYPE + || gfc->links_pos + gfc->clear_code > LINKS_SIZE) { + next_node->sibling = work_node->child.s; + work_node->child.s = next_node; + if (work_node->type < MAX_LINKS_TYPE) + work_node->type++; + } else + gfc_change_node_to_table(gfc, work_node, next_node); +} + +static inline const uint8_t * +gif_imageline(Gif_Image *gfi, unsigned pos) +{ + unsigned y, x; + if (gfi->width == 0) + return NULL; + y = pos / gfi->width; + x = pos - y * gfi->width; + if (y == (unsigned) gfi->height) + return NULL; + else if (!gfi->interlace) + return gfi->img[y] + x; + else + return gfi->img[Gif_InterlaceLine(y, gfi->height)] + x; +} + +static inline unsigned +gif_line_endpos(Gif_Image *gfi, unsigned pos) +{ + unsigned y = pos / gfi->width; + return (y + 1) * gfi->width; +} + +static inline uint8_t +gfc_pixel(Gif_CodeTable* gfc, Gif_Image* gfi, unsigned pos) +{ + unsigned y = pos / gfi->width; + unsigned x = pos - y * gfi->width; + if (gfi->interlace) + y = Gif_InterlaceLine(y, gfi->height); + uint8_t p = gfi->img[y][x]; + if (p >= gfc->clear_code) + /* should not happen unless GIF_WRITE_CAREFUL_MIN_CODE_BITS */ + p = 0; + return p; +} + +static int +gfc_lossy_init(Gif_CodeTable* gfc, Gif_Stream* gfs, Gif_Image* gfi) +{ + int i, j; + Gif_Colormap* gfcm = gfi->local ? gfi->local : gfs->global; + kcolor** kcs = gfi->local ? &gfc->local_kcs : &gfc->global_kcs; + kcolor* old_kcs = gfc->kcs; + if (!*kcs) { + *kcs = Gif_NewArray(kcolor, 256); + if (!*kcs) { + gfc->max_loss = 0; + return 0; + } + for (i = 0; i != gfcm->ncol; ++i) { + (*kcs)[i] = kc_makegfcg(&gfcm->col[i]); + } + for (; i < 256; ++i) { + (*kcs)[i] = kc_make8g(0, 0, 0); + } + } + gfc->kcs = *kcs; + if (gfc->kcdiff && kcs == &gfc->global_kcs && old_kcs == gfc->global_kcs) { + return 1; + } + if (!gfc->kcdiff + && !(gfc->kcdiff = Gif_NewArray(unsigned, 256 * 256))) { + gfc->max_loss = 0; + return 0; + } + for (i = 0; i != gfcm->ncol; ++i) { + for (j = 0; j != i; ++j) { + gfc->kcdiff[(i << 8) | j] = gfc->kcdiff[(j << 8) | i] = + kc_distance(gfc->kcs[i], gfc->kcs[j]); + } + gfc->kcdiff[(i << 8) | i] = 0; + } + return 1; +} + +/* difference between expected color a+dither and color b (used to calculate dithering required) */ +static inline wkcolor diffused_difference(const Gif_CodeTable* gfc, + int expected, int actual, wkcolor error) +{ + int i; + kcolor a = gfc->kcs[expected], b = gfc->kcs[actual]; + wkcolor wkc; + for (i = 0; i != 3; ++i) { + wkc.a[i] = (int32_t) a.a[i] - (int32_t) b.a[i] + 3 * error.a[i] / 4; + } + return wkc; +} + +static inline int +gfc_lossy_candidate(Gif_CodeTable* gfc, Gif_Image* gfi, + Gif_LossyNode* n, Gif_LossyNode* ch, + uint8_t expected) +{ + uint8_t actual = ch->node.suffix; + unsigned diff; + /* exact match */ + if (expected == actual) { + ch->diffaccum = n->diffaccum; + ch->error = diffused_difference(gfc, expected, expected, n->error); + return 1; + } + /* transparency never matches */ + if (expected == gfi->transparent || actual == gfi->transparent) + return 0; + /* check local loss */ + diff = gfc->kcdiff[(expected << 8) | actual]; + if (diff > gfc->max_loss) + return 0; + /* check accumulated loss */ + if (!wkc_iszero(n->error) + && kc_distance(kc_adjust(gfc->kcs[expected], n->error), gfc->kcs[actual]) > gfc->max_loss) + return 0; + /* success */ + ch->diffaccum = n->diffaccum + diff; + ch->error = diffused_difference(gfc, expected, actual, n->error); + return 1; +} + +static Gif_Node* +gfc_lookup_lossy(Gif_CodeTable* gfc, Gif_Image* gfi, unsigned pos) +{ + Gif_LossyNode* best; + double best_score; + Gif_LossyNode* stack[NODES_SIZE]; + unsigned nstack; + + if (pos == gfc->maxpos) + return NULL; + + /* initialize with first node */ + best = (Gif_LossyNode*) gfc_lookup(gfc, NULL, gfc_pixel(gfc, gfi, pos)); + best->diffaccum = 0; + best->error = wkc_zero(); + best_score = 1.0; + stack[0] = best; + nstack = 1; + + /* explore until stack is empty */ + while (nstack > 0) { + Gif_LossyNode* n = stack[nstack - 1]; + unsigned tpos = pos + n->node.length; + int i; + uint8_t expected; + --nstack; + + /* check if this node is better than current best */ + if (n != best) { + double score = n->node.length - n->diffaccum * gfc->loss_scale; + if (score > best_score) { + best = n; + best_score = score; + } + } + + /* reset error at start of line */ + if (tpos == gfc->maxpos) + continue; + if (tpos % gfi->width == 0) + n->error = wkc_zero(); + + expected = gfc_pixel(gfc, gfi, tpos); + + /* consider all children */ + if (n->node.type == TABLE_TYPE) { + for (i = 0; i < gfc->clear_code; ++i) { + Gif_LossyNode* ch = (Gif_LossyNode*) n->node.child.m[i]; + if (ch && gfc_lossy_candidate(gfc, gfi, n, ch, expected)) + stack[nstack++] = ch; + } + } else { + Gif_LossyNode* ch; + for (ch = (Gif_LossyNode*) n->node.child.s; ch; ch = (Gif_LossyNode*) ch->node.sibling) { + if (gfc_lossy_candidate(gfc, gfi, n, ch, expected)) + stack[nstack++] = ch; + } + } + } + + return &best->node; +} + +static int +write_compressed_data(Gif_Stream* gfs, Gif_Image* gfi, + int min_code_bits, Gif_Writer* grr) +{ + Gif_CodeTable* gfc = &grr->code_table; + uint8_t stack_buffer[512 - 24]; + uint8_t *buf = stack_buffer; + unsigned bufpos = 0; + unsigned bufcap = sizeof(stack_buffer) * 8; + + unsigned pos; + unsigned clear_bufpos, clear_pos; + unsigned line_endpos; + const unsigned image_endpos = (size_t) gfi->height * (size_t) gfi->width; + const uint8_t *imageline; + + unsigned run = 0; +#define RUN_EWMA_SHIFT 4 +#define RUN_EWMA_SCALE 19 +#define RUN_INV_THRESH ((unsigned) (1 << RUN_EWMA_SCALE) / 3000) + unsigned run_ewma = 0; + Gif_Node *work_node; + Gif_Node *next_node; + Gif_Code next_code = 0; + Gif_Code output_code; + uint8_t suffix; + + int cur_code_bits; + + /* Here we go! */ + gifputbyte(min_code_bits, grr); +#define CUR_BUMP_CODE (1 << cur_code_bits) + gfc->clear_code = (Gif_Code) (1 << min_code_bits); + gfc->maxpos = gfi->width * gfi->height; + grr->cleared = 0; + + cur_code_bits = min_code_bits + 1; + /* next_code set by first runthrough of output clear_code */ + GIF_DEBUG(("clear(%d) eoi(%d) bits(%d) ", gfc->clear_code, gfc->clear_code + 1, cur_code_bits)); + + work_node = NULL; + output_code = gfc->clear_code; + /* Because output_code is clear_code, we'll initialize next_code, et al. + below. */ + + pos = clear_pos = clear_bufpos = 0; + line_endpos = gfi->width; + imageline = gif_imageline(gfi, pos); + + if (gfc->max_loss > 0) { + gfc_lossy_init(gfc, gfs, gfi); + } + + while (1) { + + /***** + * Output 'output_code' to the memory buffer. */ + if (bufpos + 32 >= bufcap) { + unsigned ncap = bufcap * 2 + (24 << 3); + uint8_t *nbuf = Gif_NewArray(uint8_t, ncap >> 3); + if (!nbuf) + goto error; + memcpy(nbuf, buf, bufcap >> 3); + if (buf != stack_buffer) + Gif_DeleteArray(buf); + buf = nbuf; + bufcap = ncap; + } + + { + unsigned endpos = bufpos + cur_code_bits; + do { + if (bufpos & 7) + buf[bufpos >> 3] |= output_code << (bufpos & 7); + else if (bufpos & 0x7FF) + buf[bufpos >> 3] = output_code >> (bufpos - endpos + cur_code_bits); + else { + buf[bufpos >> 3] = 255; + endpos += 8; + } + + bufpos += 8 - (bufpos & 7); + } while (bufpos < endpos); + bufpos = endpos; + } + + + /***** + * Handle special codes. */ + + if (output_code == gfc->clear_code) { + /* Clear data and prepare gfc */ + cur_code_bits = min_code_bits + 1; + next_code = gfc->clear_code + 2; + run_ewma = 1 << RUN_EWMA_SCALE; + run = 0; + gfc_clear(gfc); + clear_pos = clear_bufpos = 0; + + GIF_DEBUG(("clear ")); + + } else if (output_code == gfc->clear_code + 1) + break; + + else { + if (next_code > CUR_BUMP_CODE && cur_code_bits < GIF_MAX_CODE_BITS) + /* bump up compression size */ + ++cur_code_bits; + + /* Adjust current run length average. */ + run = (run << RUN_EWMA_SCALE) + (1 << (RUN_EWMA_SHIFT - 1)); + if (run < run_ewma) + run_ewma -= (run_ewma - run) >> RUN_EWMA_SHIFT; + else + run_ewma += (run - run_ewma) >> RUN_EWMA_SHIFT; + + /* Reset run length. */ + run = !!work_node; + } + + + /***** + * Find the next code to output. */ + if (gfc->max_loss) { + work_node = gfc_lookup_lossy(gfc, gfi, pos); + run = work_node ? work_node->length : 0; + pos += run; + + if (pos < image_endpos) { + /* Output the current code. */ + if (next_code < GIF_MAX_CODE) { + gfc_define(gfc, work_node, gfc_pixel(gfc, gfi, pos), next_code); + next_code++; + } else + next_code = GIF_MAX_CODE + 1; /* to match "> CUR_BUMP_CODE" above */ + + /* Check whether to clear table. */ + if (next_code > 4094) { + int do_clear = grr->gcinfo.flags & GIF_WRITE_EAGER_CLEAR; + + if (!do_clear) { + unsigned pixels_left = image_endpos - pos - 1; + if (pixels_left) { + /* Always clear if run_ewma gets small relative to + min_code_bits. Otherwise, clear if #images/run is smaller + than an empirical threshold, meaning it will take more than + 3000 or so average runs to complete the image. */ + if (run_ewma < ((36U << RUN_EWMA_SCALE) / min_code_bits) + || pixels_left > UINT_MAX / RUN_INV_THRESH + || run_ewma < pixels_left * RUN_INV_THRESH) + do_clear = 1; + } + } + + if ((do_clear || run < 7) && !clear_pos) { + clear_pos = pos - run; + clear_bufpos = bufpos; + } else if (!do_clear && run > 50) + clear_pos = clear_bufpos = 0; + + if (do_clear) { + GIF_DEBUG(("rewind %u pixels/%d bits", pos + 1 - clear_pos, bufpos + cur_code_bits - clear_bufpos)); + output_code = gfc->clear_code; + pos = clear_pos; + + bufpos = clear_bufpos; + buf[bufpos >> 3] &= (1 << (bufpos & 7)) - 1; + grr->cleared = 1; + continue; + } + } + + /* Adjust current run length average. */ + run = (run << RUN_EWMA_SCALE) + (1 << (RUN_EWMA_SHIFT - 1)); + if (run < run_ewma) + run_ewma -= (run_ewma - run) >> RUN_EWMA_SHIFT; + else + run_ewma += (run - run_ewma) >> RUN_EWMA_SHIFT; + } + + output_code = work_node ? work_node->code : gfc->clear_code + 1; + } else { + /* If height is 0 -- no more pixels to write -- we output work_node next + time around. */ + while (imageline) { + suffix = *imageline; + if (suffix >= gfc->clear_code) + /* should not happen unless GIF_WRITE_CAREFUL_MIN_CODE_BITS */ + suffix = 0; + next_node = gfc_lookup(gfc, work_node, suffix); + + imageline++; + pos++; + if (pos == line_endpos) { + imageline = gif_imageline(gfi, pos); + line_endpos += gfi->width; + } + + if (next_node) { + work_node = next_node; + ++run; + continue; + } + + /* Output the current code. */ + if (next_code < GIF_MAX_CODE) { + gfc_define(gfc, work_node, suffix, next_code); + next_code++; + } else + next_code = GIF_MAX_CODE + 1; /* to match "> CUR_BUMP_CODE" above */ + + /* Check whether to clear table. */ + if (next_code > 4094) { + int do_clear = grr->gcinfo.flags & GIF_WRITE_EAGER_CLEAR; + + if (!do_clear) { + unsigned pixels_left = image_endpos - pos; + if (pixels_left) { + /* Always clear if run_ewma gets small relative to + min_code_bits. Otherwise, clear if #images/run is smaller + than an empirical threshold, meaning it will take more than + 3000 or so average runs to complete the image. */ + if (run_ewma < ((36U << RUN_EWMA_SCALE) / min_code_bits) + || pixels_left > UINT_MAX / RUN_INV_THRESH + || run_ewma < pixels_left * RUN_INV_THRESH) + do_clear = 1; + } + } + + if ((do_clear || run < 7) && !clear_pos) { + clear_pos = pos - (run + 1); + clear_bufpos = bufpos; + } else if (!do_clear && run > 50) + clear_pos = clear_bufpos = 0; + + if (do_clear) { + GIF_DEBUG(("rewind %u pixels/%d bits ", pos - clear_pos, bufpos + cur_code_bits - clear_bufpos)); + output_code = gfc->clear_code; + pos = clear_pos; + imageline = gif_imageline(gfi, pos); + line_endpos = gif_line_endpos(gfi, pos); + bufpos = clear_bufpos; + buf[bufpos >> 3] &= (1 << (bufpos & 7)) - 1; + work_node = NULL; + grr->cleared = 1; + goto found_output_code; + } + } + + output_code = work_node->code; + work_node = gfc->links[suffix]; + goto found_output_code; + } + + /* Ran out of data if we get here. */ + output_code = (work_node ? work_node->code : gfc->clear_code + 1); + work_node = NULL; + + found_output_code: ; + } + } + + /* Output memory buffer to stream. */ + bufpos = (bufpos + 7) >> 3; + buf[(bufpos - 1) & 0xFFFFFF00] = (bufpos - 1) & 0xFF; + buf[bufpos] = 0; + gifputblock(buf, bufpos + 1, grr); + + if (buf != stack_buffer) + Gif_DeleteArray(buf); + return 1; + + error: + if (buf != stack_buffer) + Gif_DeleteArray(buf); + return 0; +} + + +static int +calculate_min_code_bits(Gif_Image *gfi, const Gif_Writer* grr) +{ + int colors_used = -1, min_code_bits, i; + + if (grr->gcinfo.flags & GIF_WRITE_CAREFUL_MIN_CODE_SIZE) { + /* calculate m_c_b based on colormap */ + if (grr->local_size > 0) + colors_used = grr->local_size; + else if (grr->global_size > 0) + colors_used = grr->global_size; + + } else if (gfi->img) { + /* calculate m_c_b from uncompressed data */ + int x, y, width = gfi->width, height = gfi->height; + colors_used = 0; + for (y = 0; y < height && colors_used < 128; y++) { + uint8_t *data = gfi->img[y]; + for (x = width; x > 0; x--, data++) + if (*data > colors_used) + colors_used = *data; + } + colors_used++; + + } else if (gfi->compressed) { + /* take m_c_b from compressed image */ + colors_used = 1 << gfi->compressed[0]; + + } else { + /* should never happen */ + colors_used = 256; + } + + min_code_bits = 2; /* min_code_bits of 1 isn't allowed */ + i = 4; + while (i < colors_used) { + min_code_bits++; + i *= 2; + } + + return min_code_bits; +} + + +static int get_color_table_size(const Gif_Stream* gfs, Gif_Image* gfi, + Gif_Writer* grr); + +static void +save_compression_result(Gif_Image* gfi, Gif_Writer* grr, int ok) +{ + if (!(grr->gcinfo.flags & GIF_WRITE_SHRINK) + || (ok && (!gfi->compressed || gfi->compressed_len > grr->pos))) { + if (gfi->compressed) + (*gfi->free_compressed)((void *) gfi->compressed); + if (ok) { + gfi->compressed_len = grr->pos; + gfi->compressed_errors = 0; + gfi->compressed = grr->v; + gfi->free_compressed = Gif_Free; + grr->v = 0; + grr->cap = 0; + } else + gfi->compressed = 0; + } + grr->pos = 0; +} + +int +Gif_FullCompressImage(Gif_Stream *gfs, Gif_Image *gfi, + const Gif_CompressInfo *gcinfo) +{ + int ok = 0; + uint8_t min_code_bits; + Gif_Writer grr; + + if (!gif_writer_init(&grr, NULL, gcinfo)) { + if (!(grr.gcinfo.flags & GIF_WRITE_SHRINK)) + Gif_ReleaseCompressedImage(gfi); + goto done; + } + + grr.global_size = get_color_table_size(gfs, 0, &grr); + grr.local_size = get_color_table_size(gfs, gfi, &grr); + + min_code_bits = calculate_min_code_bits(gfi, &grr); + ok = write_compressed_data(gfs, gfi, min_code_bits, &grr); + save_compression_result(gfi, &grr, ok); + + if ((grr.gcinfo.flags & (GIF_WRITE_OPTIMIZE | GIF_WRITE_EAGER_CLEAR)) + == GIF_WRITE_OPTIMIZE + && grr.cleared && ok) { + grr.gcinfo.flags |= GIF_WRITE_EAGER_CLEAR | GIF_WRITE_SHRINK; + if (write_compressed_data(gfs, gfi, min_code_bits, &grr)) + save_compression_result(gfi, &grr, 1); + } + + done: + gif_writer_cleanup(&grr); + return ok; +} + + +static int +get_color_table_size(const Gif_Stream* gfs, Gif_Image* gfi, Gif_Writer* grr) +{ + Gif_Colormap *gfcm = gfi ? gfi->local : gfs->global; + int ncol, totalcol, i; + + if (!gfcm || gfcm->ncol <= 0) + return 0; + + /* Make sure ncol is reasonable */ + ncol = gfcm->ncol; + + /* Possibly bump up 'ncol' based on 'transparent' values, if + careful_min_code_bits */ + if (grr->gcinfo.flags & GIF_WRITE_CAREFUL_MIN_CODE_SIZE) { + if (gfi && gfi->transparent >= ncol) + ncol = gfi->transparent + 1; + else if (!gfi) + for (i = 0; i < gfs->nimages; i++) + if (gfs->images[i]->transparent >= ncol) + ncol = gfs->images[i]->transparent + 1; + } + + /* Make sure the colormap is a power of two entries! */ + /* GIF format doesn't allow a colormap with only 1 entry. */ + if (ncol > 256) + ncol = 256; + for (totalcol = 2; totalcol < ncol; totalcol *= 2) + /* nada */; + + return totalcol; +} + +static void +write_color_table(Gif_Colormap* gfcm, int totalcol, Gif_Writer* grr) +{ + Gif_Color *c = gfcm->col; + int i, ncol = gfcm->ncol; + + for (i = 0; i < ncol && i < totalcol; i++, c++) { + gifputbyte(c->gfc_red, grr); + gifputbyte(c->gfc_green, grr); + gifputbyte(c->gfc_blue, grr); + } + + /* Pad out colormap with black. */ + for (; i < totalcol; i++) { + gifputbyte(0, grr); + gifputbyte(0, grr); + gifputbyte(0, grr); + } +} + + +static int +write_image(Gif_Stream* gfs, Gif_Image* gfi, Gif_Writer* grr) +{ + uint8_t min_code_bits, packed = 0; + grr->local_size = get_color_table_size(gfs, gfi, grr); + + gifputbyte(',', grr); + gifputunsigned(gfi->left, grr); + gifputunsigned(gfi->top, grr); + gifputunsigned(gfi->width, grr); + gifputunsigned(gfi->height, grr); + + if (grr->local_size > 0) { + int size = 2; + packed |= 0x80; + while (size < grr->local_size) + size *= 2, packed++; + } + + if (gfi->interlace) packed |= 0x40; + gifputbyte(packed, grr); + + if (grr->local_size > 0) + write_color_table(gfi->local, grr->local_size, grr); + + /* calculate min_code_bits here (because calculation may involve + recompression, if GIF_WRITE_CAREFUL_MIN_CODE_SIZE is true) */ + min_code_bits = calculate_min_code_bits(gfi, grr); + + /* use existing compressed data if it exists. This will tend to whip + people's asses who uncompress an image, keep the compressed data around, + but modify the uncompressed data anyway. That sucks. */ + if (gfi->compressed + && (!(grr->gcinfo.flags & GIF_WRITE_CAREFUL_MIN_CODE_SIZE) + || gfi->compressed[0] == min_code_bits)) { + uint8_t *compressed = gfi->compressed; + uint32_t compressed_len = gfi->compressed_len; + while (compressed_len > 0) { + uint16_t amt = (compressed_len > 0x7000 ? 0x7000 : compressed_len); + gifputblock(compressed, amt, grr); + compressed += amt; + compressed_len -= amt; + } + + } else if (!gfi->img) { + Gif_UncompressImage(gfs, gfi); + write_compressed_data(gfs, gfi, min_code_bits, grr); + Gif_ReleaseUncompressedImage(gfi); + + } else + write_compressed_data(gfs, gfi, min_code_bits, grr); + + return 1; +} + + +static void +write_logical_screen_descriptor(Gif_Stream *gfs, Gif_Writer *grr) +{ + uint8_t packed = 0x70; /* high resolution colors */ + grr->global_size = get_color_table_size(gfs, 0, grr); + + Gif_CalculateScreenSize(gfs, 0); + gifputunsigned(gfs->screen_width, grr); + gifputunsigned(gfs->screen_height, grr); + + if (grr->global_size > 0) { + uint16_t size = 2; + packed |= 0x80; + while (size < grr->global_size) + size *= 2, packed++; + } + + gifputbyte(packed, grr); + if (gfs->background < grr->global_size) + gifputbyte(gfs->background, grr); + else + gifputbyte(255, grr); + gifputbyte(0, grr); /* no aspect ratio information */ + + if (grr->global_size > 0) + write_color_table(gfs->global, grr->global_size, grr); +} + + +/* extension byte table: + 0x01 plain text extension + 0xCE name* + 0xF9 graphic control extension + 0xFE comment extension + 0xFF application extension + */ + +static void +write_graphic_control_extension(Gif_Image *gfi, Gif_Writer *grr) +{ + uint8_t packed = 0; + gifputbyte('!', grr); + gifputbyte(0xF9, grr); + gifputbyte(4, grr); + if (gfi->transparent >= 0) packed |= 0x01; + packed |= (gfi->disposal & 0x07) << 2; + gifputbyte(packed, grr); + gifputunsigned(gfi->delay, grr); + gifputbyte((uint8_t)gfi->transparent, grr); + gifputbyte(0, grr); +} + + +static void +blast_data(const uint8_t *data, int len, Gif_Writer *grr) +{ + while (len > 0) { + int s = len > 255 ? 255 : len; + gifputbyte(s, grr); + gifputblock(data, s, grr); + data += s; + len -= s; + } + gifputbyte(0, grr); +} + + +static void +write_name_extension(char *id, Gif_Writer *grr) +{ + gifputbyte('!', grr); + gifputbyte(0xCE, grr); + blast_data((uint8_t *)id, strlen(id), grr); +} + + +static void +write_comment_extensions(Gif_Comment *gfcom, Gif_Writer *grr) +{ + int i; + for (i = 0; i < gfcom->count; i++) { + gifputbyte('!', grr); + gifputbyte(0xFE, grr); + blast_data((const uint8_t *)gfcom->str[i], gfcom->len[i], grr); + } +} + + +static void +write_netscape_loop_extension(uint16_t value, Gif_Writer *grr) +{ + gifputblock((const uint8_t *)"!\xFF\x0BNETSCAPE2.0\x03\x01", 16, grr); + gifputunsigned(value, grr); + gifputbyte(0, grr); +} + + +static void +write_generic_extension(Gif_Extension *gfex, Gif_Writer *grr) +{ + uint32_t pos = 0; + if (gfex->kind < 0) return; /* ignore our private extensions */ + + gifputbyte('!', grr); + gifputbyte(gfex->kind, grr); + if (gfex->kind == 255) { /* an application extension */ + if (gfex->applength) { + gifputbyte(gfex->applength, grr); + gifputblock((const uint8_t*) gfex->appname, gfex->applength, grr); + } + } + if (gfex->packetized) + gifputblock(gfex->data, gfex->length, grr); + else { + while (pos + 255 < gfex->length) { + gifputbyte(255, grr); + gifputblock(gfex->data + pos, 255, grr); + pos += 255; + } + if (pos < gfex->length) { + uint32_t len = gfex->length - pos; + gifputbyte(len, grr); + gifputblock(gfex->data + pos, len, grr); + } + } + gifputbyte(0, grr); +} + +static int +write_gif(Gif_Stream *gfs, Gif_Writer *grr) +{ + Gif_Extension* gfex; + int ok = 0; + int i; + + { + uint8_t isgif89a = 0; + if (gfs->end_comment || gfs->end_extension_list || gfs->loopcount > -1) + isgif89a = 1; + for (i = 0; i < gfs->nimages && !isgif89a; i++) { + Gif_Image* gfi = gfs->images[i]; + if (gfi->identifier || gfi->transparent != -1 || gfi->disposal + || gfi->delay || gfi->comment || gfi->extension_list) + isgif89a = 1; + } + if (isgif89a) + gifputblock((const uint8_t *)"GIF89a", 6, grr); + else + gifputblock((const uint8_t *)"GIF87a", 6, grr); + } + + write_logical_screen_descriptor(gfs, grr); + + if (gfs->loopcount > -1) + write_netscape_loop_extension(gfs->loopcount, grr); + + for (i = 0; i < gfs->nimages; i++) + if (!Gif_IncrementalWriteImage(grr, gfs, gfs->images[i])) + goto done; + + for (gfex = gfs->end_extension_list; gfex; gfex = gfex->next) + write_generic_extension(gfex, grr); + if (gfs->end_comment) + write_comment_extensions(gfs->end_comment, grr); + + gifputbyte(';', grr); + ok = 1; + + done: + return ok; +} + + +int +Gif_FullWriteFile(Gif_Stream *gfs, const Gif_CompressInfo *gcinfo, + FILE *f) +{ + Gif_Writer grr; + int ok = gif_writer_init(&grr, f, gcinfo) + && write_gif(gfs, &grr); + gif_writer_cleanup(&grr); + return ok; +} + + +Gif_Writer* +Gif_IncrementalWriteFileInit(Gif_Stream* gfs, const Gif_CompressInfo* gcinfo, + FILE *f) +{ + Gif_Writer* grr = Gif_New(Gif_Writer); + if (!grr || !gif_writer_init(grr, f, gcinfo)) { + Gif_Delete(grr); + return NULL; + } + gifputblock((const uint8_t *)"GIF89a", 6, grr); + write_logical_screen_descriptor(gfs, grr); + if (gfs->loopcount > -1) + write_netscape_loop_extension(gfs->loopcount, grr); + return grr; +} + +int +Gif_IncrementalWriteImage(Gif_Writer* grr, Gif_Stream* gfs, Gif_Image* gfi) +{ + Gif_Extension *gfex; + for (gfex = gfi->extension_list; gfex; gfex = gfex->next) + write_generic_extension(gfex, grr); + if (gfi->comment) + write_comment_extensions(gfi->comment, grr); + if (gfi->identifier) + write_name_extension(gfi->identifier, grr); + if (gfi->transparent != -1 || gfi->disposal || gfi->delay) + write_graphic_control_extension(gfi, grr); + return write_image(gfs, gfi, grr); +} + +int +Gif_IncrementalWriteComplete(Gif_Writer* grr, Gif_Stream* gfs) +{ + Gif_Extension* gfex; + for (gfex = gfs->end_extension_list; gfex; gfex = gfex->next) + write_generic_extension(gfex, grr); + if (gfs->end_comment) + write_comment_extensions(gfs->end_comment, grr); + gifputbyte(';', grr); + gif_writer_cleanup(grr); + Gif_Delete(grr); + return 1; +} + + +#undef Gif_CompressImage +#undef Gif_WriteFile + +int +Gif_CompressImage(Gif_Stream *gfs, Gif_Image *gfi) +{ + return Gif_FullCompressImage(gfs, gfi, 0); +} + +int +Gif_WriteFile(Gif_Stream *gfs, FILE *f) +{ + return Gif_FullWriteFile(gfs, 0, f); +} + + +#ifdef __cplusplus +} +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/gifx.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifx.c new file mode 100644 index 0000000..3b12fcb --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/gifx.c @@ -0,0 +1,852 @@ +/* gifx.c - Functions to turn GIFs in memory into X Pixmaps. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of the LCDF GIF library. + + The LCDF GIF library is free software. It is distributed under the GNU + General Public License, version 2; you can copy, distribute, or alter it at + will, as long as this notice is kept intact and this source code is made + available. There is no warranty, express or implied. */ + +#if HAVE_CONFIG_H +# include +#endif +#include +#include +#include +#include +#ifdef __cplusplus +extern "C" { +#endif + +#define SAFELS(a,b) ((b) < 0 ? (a) >> -(b) : (a) << (b)) + +struct Gif_XColormap { + + Gif_XContext *x_context; + Gif_Colormap *colormap; + + int allocated; + int claimed; + uint16_t npixels; + unsigned long *pixels; + + Gif_XColormap *next; + +}; + +static unsigned long crap_pixels[256]; + + +static void +load_closest(Gif_XContext *gfx) +{ + XColor *color; + uint16_t ncolor; + uint16_t ncolormap; + int i; + + if (gfx->closest) return; + + ncolormap = ncolor = gfx->ncolormap; + if (ncolor > 256) ncolor = 256; + color = Gif_NewArray(XColor, ncolor); + + if (ncolormap > 256) + for (i = 0; i < ncolor; i++) + color[i].pixel = (rand() >> 4) % ncolormap; + else + for (i = 0; i < ncolor; i++) + color[i].pixel = i; + XQueryColors(gfx->display, gfx->colormap, color, ncolor); + + gfx->closest = Gif_NewArray(Gif_Color, ncolor); + for (i = 0; i < ncolor; i++) { + Gif_Color *c = &gfx->closest[i]; + c->haspixel = 1; + c->gfc_red = color[i].red >> 8; + c->gfc_green = color[i].green >> 8; + c->gfc_blue = color[i].blue >> 8; + c->pixel = color[i].pixel; + } + gfx->nclosest = ncolor; + + Gif_DeleteArray(color); +} + + +static unsigned long +allocate_closest(Gif_XContext *gfx, Gif_Color *c) +{ + Gif_Color *closer; + Gif_Color *got = 0; + uint32_t distance = 0x4000000; + int i; + + load_closest(gfx); + + for (i = 0, closer = gfx->closest; i < gfx->nclosest; i++, closer++) { + int redd = c->gfc_red - closer->gfc_red; + int greend = c->gfc_green - closer->gfc_green; + int blued = c->gfc_blue - closer->gfc_blue; + uint32_t d = redd * redd + greend * greend + blued * blued; + if (d < distance) { + distance = d; + got = closer; + } + } + + if (!got) return 0; + if (!got->haspixel) { + XColor xcol; + xcol.red = got->gfc_red | (got->gfc_red << 8); + xcol.green = got->gfc_green | (got->gfc_green << 8); + xcol.blue = got->gfc_blue | (got->gfc_blue << 8); + if (XAllocColor(gfx->display, gfx->colormap, &xcol) == 0) { + /* Probably was a read/write color cell. Get rid of it!! */ + *got = gfx->closest[gfx->nclosest - 1]; + gfx->nclosest--; + return allocate_closest(gfx, c); + } + got->pixel = xcol.pixel; + got->haspixel = 1; + } + + return got->pixel; +} + + +static void +allocate_colors(Gif_XColormap *gfxc) +{ + Gif_XContext *gfx = gfxc->x_context; + uint16_t size = gfxc->colormap->ncol; + Gif_Color *c = gfxc->colormap->col; + unsigned long *pixels = gfxc->pixels; + XColor xcol; + int i; + if (!gfxc->allocated) { + if (size > gfxc->npixels) size = gfxc->npixels; + for (i = 0; i < size; i++, c++) { + xcol.red = c->gfc_red | (c->gfc_red << 8); + xcol.green = c->gfc_green | (c->gfc_green << 8); + xcol.blue = c->gfc_blue | (c->gfc_blue << 8); + if (XAllocColor(gfx->display, gfx->colormap, &xcol)) + pixels[i] = xcol.pixel; + else + pixels[i] = allocate_closest(gfx, c); + } + gfxc->allocated = 1; + gfxc->claimed = 0; + } +} + +static void +deallocate_colors(Gif_XColormap *gfxc) +{ + Gif_XContext *gfx = gfxc->x_context; + if (gfxc->allocated && !gfxc->claimed) { + XFreeColors(gfx->display, gfx->colormap, gfxc->pixels, gfxc->npixels, 0); + gfxc->allocated = 0; + } +} + + +static Gif_XColormap * +create_x_colormap_extension(Gif_XContext *gfx, Gif_Colormap *gfcm) +{ + Gif_XColormap *gfxc; + unsigned long *pixels; + if (!gfcm) return 0; + gfxc = Gif_New(Gif_XColormap); + pixels = gfxc ? Gif_NewArray(unsigned long, 256) : 0; + if (pixels) { + gfxc->x_context = gfx; + gfxc->colormap = gfcm; + gfxc->allocated = 0; + gfxc->npixels = gfcm->ncol; + gfxc->pixels = pixels; + gfxc->next = gfx->xcolormap; + gfx->xcolormap = gfxc; + return gfxc; + } else { + Gif_Delete(gfxc); + Gif_DeleteArray(pixels); + return 0; + } +} + +static Gif_XColormap * +find_x_colormap_extension(Gif_XContext *gfx, Gif_Colormap *gfcm, int create) +{ + Gif_XColormap *gfxc = gfx->xcolormap; + if (!gfcm) return 0; + while (gfxc) { + if (gfxc->colormap == gfcm) + return gfxc; + gfxc = gfxc->next; + } + if (create) + return create_x_colormap_extension(gfx, gfcm); + else + return 0; +} + +int +Gif_XAllocateColors(Gif_XContext *gfx, Gif_Colormap *gfcm) +{ + Gif_XColormap *gfxc = find_x_colormap_extension(gfx, gfcm, 1); + if (gfxc) { + allocate_colors(gfxc); + return 1; + } else + return 0; +} + +void +Gif_XDeallocateColors(Gif_XContext *gfx, Gif_Colormap *gfcm) +{ + Gif_XColormap *gfxc = find_x_colormap_extension(gfx, gfcm, 0); + if (gfxc) + deallocate_colors(gfxc); +} + + +unsigned long * +Gif_XClaimStreamColors(Gif_XContext *gfx, Gif_Stream *gfs, int *np_store) +{ + int i; + int npixels = 0; + unsigned long *pixels; + Gif_Colormap *global = gfs->global; + *np_store = 0; + + for (i = 0; i < gfs->nimages; i++) { + Gif_Image *gfi = gfs->images[i]; + Gif_Colormap *gfcm = (gfi->local ? gfi->local : global); + Gif_XColormap *gfxc = find_x_colormap_extension(gfx, gfcm, 0); + if (gfxc && gfxc->allocated && gfxc->claimed == 0) { + gfxc->claimed = 2; + npixels += gfxc->npixels; + if (gfcm == global) global = 0; + } + } + + if (!npixels) return 0; + + pixels = Gif_NewArray(unsigned long, npixels); + if (!pixels) return 0; + *np_store = npixels; + + npixels = 0; + global = gfs->global; + for (i = 0; i < gfs->nimages; i++) { + Gif_Image *gfi = gfs->images[i]; + Gif_Colormap *gfcm = (gfi->local ? gfi->local : global); + Gif_XColormap *gfxc = find_x_colormap_extension(gfx, gfcm, 0); + if (gfxc && gfxc->allocated && gfxc->claimed == 2) { + memcpy(pixels + npixels, gfxc->pixels, gfxc->npixels); + npixels += gfxc->npixels; + gfxc->claimed = 1; + if (gfcm == global) global = 0; + } + } + + return pixels; +} + + +/* Getting pixmaps */ + +#define BYTESIZE 8 + +static int +put_sub_image_colormap(Gif_XContext *gfx, + Gif_Stream* gfs, Gif_Image *gfi, Gif_Colormap *gfcm, + int left, int top, int width, int height, + Pixmap pixmap, int pixmap_x, int pixmap_y) +{ + XImage *ximage; + uint8_t *xdata; + + int i, j, k; + size_t bytes_per_line; + + unsigned long saved_transparent = 0; + int release_uncompressed = 0; + uint16_t nct; + unsigned long *pixels; + + /* Find the correct image and colormap */ + if (!gfi) return 0; + if (!gfx->image_gc) + gfx->image_gc = XCreateGC(gfx->display, pixmap, 0, 0); + if (!gfx->image_gc) + return 0; + + /* Make sure the image is uncompressed */ + if (!gfi->img && !gfi->image_data && gfi->compressed) { + Gif_UncompressImage(gfs, gfi); + release_uncompressed = 1; + } + + /* Check subimage dimensions */ + if (width <= 0 || height <= 0 || left < 0 || top < 0 + || left+width <= 0 || top+height <= 0 + || left+width > gfi->width || top+height > gfi->height) + return 0; + + /* Allocate colors from the colormap; make sure the transparent color + * has the given pixel value */ + if (gfcm) { + Gif_XColormap *gfxc = find_x_colormap_extension(gfx, gfcm, 1); + if (!gfxc) return 0; + allocate_colors(gfxc); + pixels = gfxc->pixels; + nct = gfxc->npixels; + } else { + for (i = 0; i < 256; i++) crap_pixels[i] = gfx->foreground_pixel; + pixels = crap_pixels; + nct = 256; + } + if (gfi->transparent >= 0 && gfi->transparent < 256) { + saved_transparent = pixels[ gfi->transparent ]; + pixels[ gfi->transparent ] = gfx->transparent_pixel; + } + + /* Set up the X image */ + if (gfx->depth <= 8) i = 8; + else if (gfx->depth <= 16) i = 16; + else i = 32; + ximage = + XCreateImage(gfx->display, gfx->visual, gfx->depth, + gfx->depth == 1 ? XYBitmap : ZPixmap, 0, NULL, + width, height, i, 0); + + ximage->bitmap_bit_order = ximage->byte_order = LSBFirst; + bytes_per_line = ximage->bytes_per_line; + xdata = Gif_NewArray(uint8_t, bytes_per_line * height); + ximage->data = (char *)xdata; + + /* The main loop */ + if (ximage->bits_per_pixel % 8 == 0) { + /* Optimize for cases where a pixel is exactly one or more bytes */ + int bytes_per_pixel = ximage->bits_per_pixel / 8; + + for (j = 0; j < height; j++) { + uint8_t *line = gfi->img[top + j] + left; + uint8_t *writer = xdata + bytes_per_line * j; + for (i = 0; i < width; i++) { + unsigned long pixel; + if (line[i] < nct) + pixel = pixels[line[i]]; + else + pixel = pixels[0]; + for (k = 0; k < bytes_per_pixel; k++) { + *writer++ = pixel; + pixel >>= 8; + } + } + } + + } else { + /* Other bits-per-pixel */ + int bits_per_pixel = ximage->bits_per_pixel; + uint32_t bits_per_pixel_mask = (1UL << bits_per_pixel) - 1; + + for (j = 0; j < height; j++) { + int imshift = 0; + uint32_t impixel = 0; + uint8_t *line = gfi->img[top + j] + left; + uint8_t *writer = xdata + bytes_per_line * j; + + for (i = 0; i < width; i++) { + unsigned long pixel; + if (line[i] < nct) + pixel = pixels[line[i]]; + else + pixel = pixels[0]; + + impixel |= SAFELS(pixel & bits_per_pixel_mask, imshift); + while (imshift + bits_per_pixel >= BYTESIZE) { + *writer++ = impixel; + imshift -= BYTESIZE; + impixel = SAFELS(pixel, imshift); + } + imshift += bits_per_pixel; + } + + if (imshift) + *writer++ = impixel; + } + } + + /* Restore saved transparent pixel value */ + if (gfi->transparent >= 0 && gfi->transparent < 256) + pixels[ gfi->transparent ] = saved_transparent; + + /* Put it onto the pixmap */ + XPutImage(gfx->display, pixmap, gfx->image_gc, ximage, 0, 0, + pixmap_x, pixmap_y, width, height); + + Gif_DeleteArray(xdata); + ximage->data = 0; /* avoid freeing it again in XDestroyImage */ + XDestroyImage(ximage); + + if (release_uncompressed) + Gif_ReleaseUncompressedImage(gfi); + + return 1; +} + + +Pixmap +Gif_XSubImageColormap(Gif_XContext *gfx, + Gif_Stream* gfs, Gif_Image *gfi, Gif_Colormap *gfcm, + int left, int top, int width, int height) +{ + Pixmap pixmap = + XCreatePixmap(gfx->display, gfx->drawable, + width ? width : 1, height ? height : 1, gfx->depth); + if (pixmap) { + if (put_sub_image_colormap(gfx, gfs, gfi, gfcm, left, top, width, height, + pixmap, 0, 0)) + return pixmap; + else + XFreePixmap(gfx->display, pixmap); + } + return None; +} + +Pixmap +Gif_XImage(Gif_XContext *gfx, Gif_Stream *gfs, Gif_Image *gfi) +{ + Gif_Colormap *gfcm; + if (!gfi && gfs->nimages) gfi = gfs->images[0]; + if (!gfi) return None; + gfcm = gfi->local; + if (!gfcm) gfcm = gfs->global; + return Gif_XSubImageColormap(gfx, gfs, gfi, gfcm, + 0, 0, gfi->width, gfi->height); +} + +Pixmap +Gif_XImageColormap(Gif_XContext *gfx, Gif_Stream *gfs, Gif_Colormap *gfcm, + Gif_Image *gfi) +{ + if (!gfi && gfs->nimages) gfi = gfs->images[0]; + if (!gfi) return None; + return Gif_XSubImageColormap(gfx, gfs, gfi, gfcm, + 0, 0, gfi->width, gfi->height); +} + +Pixmap +Gif_XSubImage(Gif_XContext *gfx, Gif_Stream *gfs, Gif_Image *gfi, + int left, int top, int width, int height) +{ + Gif_Colormap *gfcm; + if (!gfi && gfs->nimages) gfi = gfs->images[0]; + if (!gfi) return None; + gfcm = gfi->local; + if (!gfcm) gfcm = gfs->global; + return Gif_XSubImageColormap(gfx, gfs, gfi, gfcm, + left, top, width, height); +} + + +Pixmap +Gif_XSubMask(Gif_XContext* gfx, Gif_Stream* gfs, Gif_Image* gfi, + int left, int top, int width, int height) +{ + Pixmap pixmap = None; + XImage *ximage; + uint8_t *xdata; + + int i, j; + int transparent; + size_t bytes_per_line; + int release_uncompressed = 0; + + /* Find the correct image */ + if (!gfi) return None; + + /* Check subimage dimensions */ + if (width <= 0 || height <= 0 || left < 0 || top < 0 + || left+width <= 0 || top+height <= 0 + || left+width > gfi->width || top+height > gfi->height) + return None; + + /* Make sure the image is uncompressed */ + if (!gfi->img && !gfi->image_data && gfi->compressed) { + Gif_UncompressImage(gfs, gfi); + release_uncompressed = 1; + } + + /* Create the X image */ + ximage = + XCreateImage(gfx->display, gfx->visual, 1, + XYBitmap, 0, NULL, + width, height, + 8, 0); + + ximage->bitmap_bit_order = ximage->byte_order = LSBFirst; + bytes_per_line = ximage->bytes_per_line; + xdata = Gif_NewArray(uint8_t, bytes_per_line * height); + ximage->data = (char *)xdata; + + transparent = gfi->transparent; + + /* The main loop */ + for (j = 0; j < height; j++) { + int imshift = 0; + uint32_t impixel = 0; + uint8_t *line = gfi->img[top + j] + left; + uint8_t *writer = xdata + bytes_per_line * j; + + for (i = 0; i < width; i++) { + if (line[i] == transparent) + impixel |= 1 << imshift; + + if (++imshift >= BYTESIZE) { + *writer++ = impixel; + imshift = 0; + impixel = 0; + } + } + + if (imshift) + *writer++ = impixel; + } + + /* Create the pixmap */ + pixmap = + XCreatePixmap(gfx->display, gfx->drawable, + width ? width : 1, height ? height : 1, 1); + if (!gfx->mask_gc) + gfx->mask_gc = XCreateGC(gfx->display, pixmap, 0, 0); + + if (pixmap && gfx->mask_gc) + XPutImage(gfx->display, pixmap, gfx->mask_gc, ximage, 0, 0, 0, 0, + width, height); + + Gif_DeleteArray(xdata); + ximage->data = 0; /* avoid freeing it again in XDestroyImage */ + XDestroyImage(ximage); + + if (release_uncompressed) + Gif_ReleaseUncompressedImage(gfi); + + return pixmap; +} + + +Pixmap +Gif_XMask(Gif_XContext *gfx, Gif_Stream *gfs, Gif_Image *gfi) +{ + if (!gfi && gfs->nimages) gfi = gfs->images[0]; + if (!gfi) return None; + return Gif_XSubMask(gfx, gfs, gfi, 0, 0, gfi->width, gfi->height); +} + +static Pixmap +screen_pixmap(Gif_XContext *gfx, Gif_Stream *gfs) +{ + return XCreatePixmap(gfx->display, gfx->drawable, + gfs->screen_width, gfs->screen_height, gfx->depth); +} + +static int +apply_background(Gif_XContext *gfx, Gif_Stream *gfs, int i, Pixmap pixmap) +{ + Gif_Image *gfi = gfs->images[i >= 0 ? i : 0]; + Gif_Colormap *gfcm = (gfi->local ? gfi->local : gfs->global); + unsigned long bg_pixel; + + /* find bg_pixel */ + if (gfs->global && gfs->background < gfs->global->ncol + && gfs->images[0]->transparent < 0) { + Gif_XColormap *gfxc = find_x_colormap_extension(gfx, gfcm, 1); + if (!gfxc) + return -1; + allocate_colors(gfxc); + bg_pixel = gfxc->pixels[gfs->background]; + } else + bg_pixel = gfx->transparent_pixel; + + /* install it as the foreground color on gfx->image_gc */ + if (!gfx->image_gc) + gfx->image_gc = XCreateGC(gfx->display, pixmap, 0, 0); + if (!gfx->image_gc) + return -1; + XSetForeground(gfx->display, gfx->image_gc, bg_pixel); + gfx->transparent_pixel = bg_pixel; + + /* clear the image portion */ + if (i < 0) + XFillRectangle(gfx->display, pixmap, gfx->image_gc, + 0, 0, gfs->screen_width, gfs->screen_height); + else /*if (gfi->transparent < 0)*/ + XFillRectangle(gfx->display, pixmap, gfx->image_gc, + gfi->left, gfi->top, gfi->width, gfi->height); + /*else { + Pixmap mask = Gif_XMask(gfx, gfs, gfi); + if (mask == None) + return -1; + XSetClipMask(gfx->display, gfx->image_gc, mask); + XSetClipOrigin(gfx->display, gfx->image_gc, gfi->left, gfi->top); + XFillRectangle(gfx->display, pixmap, gfx->image_gc, + gfi->left, gfi->top, gfi->width, gfi->height); + XSetClipMask(gfx->display, gfx->image_gc, None); + XFreePixmap(gfx->display, mask); + }*/ + + return 0; +} + +static int +apply_image(Gif_XContext *gfx, Gif_Stream *gfs, Gif_Image *gfi, Pixmap pixmap) +{ + Pixmap image = Gif_XImage(gfx, gfs, gfi), mask; + if (image == None) + return -1; + + if (gfi->transparent >= 0) { + mask = Gif_XMask(gfx, gfs, gfi); + if (mask == None) { + XFreePixmap(gfx->display, image); + return -1; + } + + XSetClipMask(gfx->display, gfx->image_gc, mask); + XSetClipOrigin(gfx->display, gfx->image_gc, gfi->left, gfi->top); + XCopyArea(gfx->display, image, pixmap, gfx->image_gc, + 0, 0, gfi->width, gfi->height, gfi->left, gfi->top); + XSetClipMask(gfx->display, gfx->image_gc, None); + XFreePixmap(gfx->display, mask); + } else { + XCopyArea(gfx->display, image, pixmap, gfx->image_gc, + 0, 0, gfi->width, gfi->height, gfi->left, gfi->top); + } + + XFreePixmap(gfx->display, image); + return 0; +} + +static int +fullscreen(Gif_Stream *gfs, Gif_Image *gfi, int require_opaque) +{ + return (gfi->left == 0 && gfi->top == 0 && gfi->width == gfs->screen_width + && gfi->height == gfs->screen_height + && (!require_opaque || gfi->transparent < 0)); +} + +Gif_XFrame * +Gif_NewXFrames(Gif_Stream *gfs) +{ + int i, last_postdisposal = -1; + Gif_XFrame *fs = Gif_NewArray(Gif_XFrame, gfs->nimages); + if (!fs) + return 0; + for (i = 0; i < gfs->nimages; ++i) { + Gif_Image *gfi = gfs->images[i]; + fs[i].pixmap = None; + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) + fs[i].postdisposal = last_postdisposal; + else + fs[i].postdisposal = i; + last_postdisposal = fs[i].postdisposal; + } + return fs; +} + +void +Gif_DeleteXFrames(Gif_XContext *gfx, Gif_Stream *gfs, Gif_XFrame *fs) +{ + int i; + for (i = 0; i < gfs->nimages; ++i) + if (fs[i].pixmap) + XFreePixmap(gfx->display, fs[i].pixmap); + Gif_DeleteArray(fs); +} + +Pixmap +Gif_XNextImage(Gif_XContext *gfx, Gif_Stream *gfs, int i, Gif_XFrame *frames) +{ + Pixmap result = None; + unsigned long old_transparent = gfx->transparent_pixel; + Gif_Image *gfi; + int previ, scani; + + /* return already rendered pixmap if any */ + if (frames[i].pixmap != None) + return frames[i].pixmap; + + /* render fullscreen image */ + gfi = gfs->images[i]; + if (fullscreen(gfs, gfi, 1)) { + frames[i].pixmap = Gif_XImage(gfx, gfs, gfi); + return frames[i].pixmap; + } + + /* image is not full screen, need to find background */ + previ = i - 1; + if (previ >= 0) + previ = frames[previ].postdisposal; + + /* scan backwards for a renderable image */ + scani = previ; + while (scani >= 0 + && frames[scani].pixmap == None + && !fullscreen(gfs, gfs->images[scani], 1)) + --scani; + + /* create the pixmap */ + result = screen_pixmap(gfx, gfs); + if (result == None) + return None; + + /* scan forward to produce background */ + gfi = (scani >= 0 ? gfs->images[scani] : 0); + if (gfi && (gfi->disposal != GIF_DISPOSAL_BACKGROUND + || !fullscreen(gfs, gfi, 1))) { + /* perhaps we need to create an image (if so, must be fullscreen) */ + if (frames[scani].pixmap == None) { + frames[scani].pixmap = Gif_XImage(gfx, gfs, gfi); + if (frames[scani].pixmap == None) + goto error_exit; + } + XCopyArea(gfx->display, frames[scani].pixmap, result, gfx->image_gc, + 0, 0, gfs->screen_width, gfs->screen_height, 0, 0); + } + if (!gfi || gfi->disposal == GIF_DISPOSAL_BACKGROUND) { + if (apply_background(gfx, gfs, scani, result) < 0) + goto error_exit; + } + + while (scani < previ) { + ++scani; + gfi = gfs->images[scani]; + if (gfi->disposal == GIF_DISPOSAL_BACKGROUND) { + if (apply_background(gfx, gfs, scani, result) < 0) + goto error_exit; + } else if (gfi->disposal != GIF_DISPOSAL_PREVIOUS) { + if (apply_image(gfx, gfs, gfs->images[scani], result) < 0) + goto error_exit; + } + } + + /* apply image */ + if (gfs->screen_width != 0 && gfs->screen_height != 0) { + if (apply_image(gfx, gfs, gfs->images[i], result) < 0) + goto error_exit; + } + + frames[i].pixmap = result; + return frames[i].pixmap; + + error_exit: + XFreePixmap(gfx->display, result); + gfx->transparent_pixel = old_transparent; + return None; +} + + +/** CREATING AND DESTROYING XCONTEXTS **/ + +static void +delete_xcolormap(Gif_XColormap *gfxc) +{ + Gif_XContext *gfx = gfxc->x_context; + Gif_XColormap *prev = 0, *trav = gfx->xcolormap; + while (trav != gfxc && trav) { + prev = trav; + trav = trav->next; + } + if (gfx->free_deleted_colormap_pixels) + deallocate_colors(gfxc); + if (prev) + prev->next = gfxc->next; + else + gfx->xcolormap = gfxc->next; + Gif_DeleteArray(gfxc->pixels); + Gif_Delete(gfxc); +} + +static void +delete_colormap_hook(int dummy, void *colormap_x, void *callback_x) +{ + Gif_Colormap *gfcm = (Gif_Colormap *)colormap_x; + Gif_XContext *gfx = (Gif_XContext *)callback_x; + Gif_XColormap *gfxc; + (void) dummy; + for (gfxc = gfx->xcolormap; gfxc; gfxc = gfxc->next) + if (gfxc->colormap == gfcm) { + delete_xcolormap(gfxc); + return; + } +} + + +Gif_XContext * +Gif_NewXContextFromVisual(Display *display, int screen_number, + Visual *visual, int depth, Colormap colormap) +{ + Gif_XContext *gfx; + + gfx = Gif_New(Gif_XContext); + gfx->display = display; + gfx->screen_number = screen_number; + gfx->drawable = RootWindow(display, screen_number); + + gfx->visual = visual; + gfx->colormap = colormap; + gfx->ncolormap = visual->map_entries; + gfx->depth = depth; + + gfx->closest = 0; + gfx->nclosest = 0; + + gfx->free_deleted_colormap_pixels = 0; + gfx->xcolormap = 0; + + gfx->image_gc = None; + gfx->mask_gc = None; + + gfx->transparent_pixel = 0UL; + gfx->foreground_pixel = 1UL; + gfx->refcount = 0; + + Gif_AddDeletionHook(GIF_T_COLORMAP, delete_colormap_hook, gfx); + return gfx; +} + + +Gif_XContext * +Gif_NewXContext(Display *display, Window window) +{ + XWindowAttributes attr; + XGetWindowAttributes(display, window, &attr); + return Gif_NewXContextFromVisual(display, XScreenNumberOfScreen(attr.screen), + attr.visual, attr.depth, attr.colormap); +} + + +void +Gif_DeleteXContext(Gif_XContext *gfx) +{ + if (!gfx) return; + if (--gfx->refcount > 0) return; + while (gfx->xcolormap) + delete_xcolormap(gfx->xcolormap); + if (gfx->image_gc) + XFreeGC(gfx->display, gfx->image_gc); + if (gfx->mask_gc) + XFreeGC(gfx->display, gfx->mask_gc); + Gif_DeleteArray(gfx->closest); + Gif_RemoveDeletionHook(GIF_T_COLORMAP, delete_colormap_hook, gfx); + Gif_Delete(gfx); +} + + +#ifdef __cplusplus +} +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/kcolor.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/kcolor.c new file mode 100644 index 0000000..c373f35 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/kcolor.c @@ -0,0 +1,708 @@ +/* kcolor.c - Kcolor subsystem for gifsicle. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#include +#include "gifsicle.h" +#include "kcolor.h" +#include +#include +#include +#include +#include +#include + +/* Invariant: (0<=x<256) ==> (srgb_revgamma[srgb_gamma[x] >> 7] <= x). */ + +static const uint16_t srgb_gamma_table_256[256] = { + 0, 10, 20, 30, 40, 50, 60, 70, + 80, 90, 99, 110, 120, 132, 144, 157, + 170, 184, 198, 213, 229, 246, 263, 281, + 299, 319, 338, 359, 380, 403, 425, 449, + 473, 498, 524, 551, 578, 606, 635, 665, + 695, 727, 759, 792, 825, 860, 895, 931, + 968, 1006, 1045, 1085, 1125, 1167, 1209, 1252, + 1296, 1341, 1386, 1433, 1481, 1529, 1578, 1629, + 1680, 1732, 1785, 1839, 1894, 1950, 2007, 2065, + 2123, 2183, 2244, 2305, 2368, 2432, 2496, 2562, + 2629, 2696, 2765, 2834, 2905, 2977, 3049, 3123, + 3198, 3273, 3350, 3428, 3507, 3587, 3668, 3750, + 3833, 3917, 4002, 4088, 4176, 4264, 4354, 4444, + 4536, 4629, 4723, 4818, 4914, 5011, 5109, 5209, + 5309, 5411, 5514, 5618, 5723, 5829, 5936, 6045, + 6154, 6265, 6377, 6490, 6604, 6720, 6836, 6954, + 7073, 7193, 7315, 7437, 7561, 7686, 7812, 7939, + 8067, 8197, 8328, 8460, 8593, 8728, 8863, 9000, + 9139, 9278, 9419, 9560, 9704, 9848, 9994, 10140, + 10288, 10438, 10588, 10740, 10893, 11048, 11204, 11360, + 11519, 11678, 11839, 12001, 12164, 12329, 12495, 12662, + 12831, 13000, 13172, 13344, 13518, 13693, 13869, 14047, + 14226, 14406, 14588, 14771, 14955, 15141, 15328, 15516, + 15706, 15897, 16089, 16283, 16478, 16675, 16872, 17071, + 17272, 17474, 17677, 17882, 18088, 18295, 18504, 18714, + 18926, 19138, 19353, 19569, 19786, 20004, 20224, 20445, + 20668, 20892, 21118, 21345, 21573, 21803, 22034, 22267, + 22501, 22736, 22973, 23211, 23451, 23692, 23935, 24179, + 24425, 24672, 24920, 25170, 25421, 25674, 25928, 26184, + 26441, 26700, 26960, 27222, 27485, 27749, 28016, 28283, + 28552, 28823, 29095, 29368, 29643, 29920, 30197, 30477, + 30758, 31040, 31324, 31610, 31897, 32185, 32475, 32767 +}; + +static const uint16_t srgb_revgamma_table_256[256] = { + 0, 1628, 2776, 3619, 4309, 4904, 5434, 5914, + 6355, 6765, 7150, 7513, 7856, 8184, 8497, 8798, + 9086, 9365, 9634, 9895, 10147, 10393, 10631, 10864, + 11091, 11312, 11528, 11739, 11946, 12148, 12347, 12541, + 12732, 12920, 13104, 13285, 13463, 13639, 13811, 13981, + 14149, 14314, 14476, 14637, 14795, 14951, 15105, 15257, + 15408, 15556, 15703, 15848, 15991, 16133, 16273, 16412, + 16549, 16685, 16819, 16953, 17084, 17215, 17344, 17472, + 17599, 17725, 17849, 17973, 18095, 18217, 18337, 18457, + 18575, 18692, 18809, 18925, 19039, 19153, 19266, 19378, + 19489, 19600, 19710, 19819, 19927, 20034, 20141, 20247, + 20352, 20457, 20560, 20664, 20766, 20868, 20969, 21070, + 21170, 21269, 21368, 21466, 21564, 21661, 21758, 21854, + 21949, 22044, 22138, 22232, 22326, 22418, 22511, 22603, + 22694, 22785, 22875, 22965, 23055, 23144, 23232, 23321, + 23408, 23496, 23583, 23669, 23755, 23841, 23926, 24011, + 24095, 24180, 24263, 24347, 24430, 24512, 24595, 24676, + 24758, 24839, 24920, 25001, 25081, 25161, 25240, 25319, + 25398, 25477, 25555, 25633, 25710, 25788, 25865, 25941, + 26018, 26094, 26170, 26245, 26321, 26396, 26470, 26545, + 26619, 26693, 26766, 26840, 26913, 26986, 27058, 27130, + 27202, 27274, 27346, 27417, 27488, 27559, 27630, 27700, + 27770, 27840, 27910, 27979, 28048, 28117, 28186, 28255, + 28323, 28391, 28459, 28527, 28594, 28661, 28728, 28795, + 28862, 28928, 28995, 29061, 29127, 29192, 29258, 29323, + 29388, 29453, 29518, 29582, 29646, 29711, 29775, 29838, + 29902, 29965, 30029, 30092, 30155, 30217, 30280, 30342, + 30404, 30466, 30528, 30590, 30652, 30713, 30774, 30835, + 30896, 30957, 31017, 31078, 31138, 31198, 31258, 31318, + 31378, 31437, 31497, 31556, 31615, 31674, 31733, 31791, + 31850, 31908, 31966, 32024, 32082, 32140, 32198, 32255, + 32313, 32370, 32427, 32484, 32541, 32598, 32654, 32711 +}; + +static const float linear_srgb_table_256[256] = { + 0.00000, 0.04984009, 0.08494473, 0.11070206, 0.13180381, 0.1500052, 0.1661857, 0.18085852, + 0.19435316, 0.20689574, 0.21864912, 0.22973509, 0.2402475, 0.25026038, 0.25983337, 0.26901522, + 0.27784654, 0.28636143, 0.29458886, 0.3025538, 0.31027776, 0.31777957, 0.32507575, 0.33218095, + 0.33910814, 0.34586892, 0.35247374, 0.35893196, 0.3652521, 0.3714419, 0.37750843, 0.38345808, + 0.38929683, 0.39503005, 0.40066284, 0.40619975, 0.41164514, 0.417003, 0.42227703, 0.42747074, + 0.4325873, 0.4376298, 0.44260103, 0.4475037, 0.45234028, 0.45711315, 0.46182457, 0.4664766, + 0.47107124, 0.4756104, 0.4800958, 0.4845292, 0.48891217, 0.49324623, 0.49753287, 0.5017734, + 0.5059693, 0.5101216, 0.5142317, 0.5183006, 0.5223295, 0.5263194, 0.53027135, 0.53418624, + 0.53806514, 0.54190874, 0.5457181, 0.54949385, 0.5532369, 0.556948, 0.5606278, 0.5642771, + 0.56789654, 0.5714868, 0.57504845, 0.5785821, 0.5820884, 0.58556795, 0.58902115, 0.59244865, + 0.59585094, 0.5992285, 0.60258186, 0.60591143, 0.60921764, 0.612501, 0.61576194, 0.6190008, + 0.622218, 0.62541395, 0.62858903, 0.6317436, 0.63487804, 0.6379926, 0.6410878, 0.6441637, + 0.64722085, 0.6502595, 0.6532799, 0.65628237, 0.65926725, 0.6622347, 0.6651851, 0.66811866, + 0.67103565, 0.6739363, 0.67682093, 0.6796897, 0.6825429, 0.6853807, 0.6882034, 0.69101113, + 0.69380414, 0.6965826, 0.69934684, 0.70209694, 0.7048331, 0.7075556, 0.7102645, 0.71296, + 0.7156424, 0.7183118, 0.7209683, 0.7236121, 0.7262435, 0.7288625, 0.73146933, 0.73406404, + 0.73664695, 0.73921806, 0.7417776, 0.74432564, 0.7468624, 0.749388, 0.75190246, 0.7544061, + 0.7568989, 0.759381, 0.76185256, 0.7643137, 0.7667645, 0.7692052, 0.7716358, 0.7740564, + 0.77646714, 0.77886814, 0.78125954, 0.78364134, 0.7860138, 0.7883768, 0.79073066, 0.7930754, + 0.795411, 0.7977377, 0.80005556, 0.8023647, 0.8046651, 0.80695695, 0.8092403, 0.8115152, + 0.8137818, 0.81604016, 0.8182903, 0.8205324, 0.8227665, 0.8249926, 0.8272109, 0.8294214, + 0.8316242, 0.8338194, 0.836007, 0.8381871, 0.84035975, 0.84252506, 0.8446831, 0.84683394, + 0.84897757, 0.85111415, 0.8532437, 0.85536623, 0.8574819, 0.8595907, 0.8616927, 0.86378807, + 0.8658767, 0.8679587, 0.87003416, 0.87210315, 0.87416565, 0.8762218, 0.8782716, 0.8803151, + 0.8823524, 0.8843835, 0.8864085, 0.8884274, 0.8904402, 0.8924471, 0.89444804, 0.8964431, + 0.8984324, 0.9004158, 0.90239346, 0.9043654, 0.9063318, 0.9082925, 0.91024756, 0.9121972, + 0.9141413, 0.91608, 0.9180133, 0.9199412, 0.92186373, 0.92378104, 0.9256931, 0.92759997, + 0.92950165, 0.9313982, 0.93328965, 0.9351761, 0.9370575, 0.9389339, 0.9408054, 0.9426719, + 0.9445336, 0.94639045, 0.9482424, 0.9500897, 0.9519322, 0.95377004, 0.9556032, 0.9574316, + 0.9592555, 0.9610748, 0.96288955, 0.9646998, 0.9665055, 0.9683068, 0.9701037, 0.9718961, + 0.9736842, 0.9754679, 0.97724736, 0.9790225, 0.9807934, 0.9825601, 0.98432255, 0.9860808, + 0.987835, 0.989585, 0.9913309, 0.99307275, 0.9948106, 0.99654436, 0.99827415, 1.00000 +}; + +const uint16_t* gamma_tables[2] = { + srgb_gamma_table_256, + srgb_revgamma_table_256 +}; + +#if ENABLE_THREADS +pthread_mutex_t kd3_sort_lock; +#endif + + +const char* kc_debug_str(kcolor x) { + static int whichbuf = 0; + static char buf[8][64]; + whichbuf = (whichbuf + 1) % 8; + if (x.a[0] >= 0 && x.a[1] >= 0 && x.a[2] >= 0) { + x = kc_revgamma_transform(x); + snprintf(buf[whichbuf], sizeof(buf[whichbuf]), "#%02X%02X%02X", + x.a[0] >> 7, x.a[1] >> 7, x.a[2] >> 7); + } else + snprintf(buf[whichbuf], sizeof(buf[whichbuf]), "<%d,%d,%d>", + x.a[0], x.a[1], x.a[2]); + return buf[whichbuf]; +} + +void kc_set_gamma(int type, double gamma) { + static int cur_type = KC_GAMMA_SRGB; + static double cur_gamma = 2.2; + int i, j; + uint16_t* g[2]; +#if !HAVE_POW + if (type == KC_GAMMA_NUMERIC && gamma != 1.0) { + type = KC_GAMMA_SRGB; + } +#endif + if (type == cur_type && (type != KC_GAMMA_NUMERIC || gamma == cur_gamma)) { + return; + } + if (type != KC_GAMMA_NUMERIC) { + if (gamma_tables[0] != srgb_gamma_table_256) { + Gif_DeleteArray(gamma_tables[0]); + } + gamma_tables[0] = srgb_gamma_table_256; + gamma_tables[1] = srgb_revgamma_table_256; + } else { + if (gamma_tables[0] == srgb_gamma_table_256) { + gamma_tables[0] = Gif_NewArray(uint16_t, 512); + gamma_tables[1] = gamma_tables[0] + 256; + } + g[0] = (uint16_t*) gamma_tables[0]; + g[1] = (uint16_t*) gamma_tables[1]; + for (j = 0; j != 256; ++j) { +#if HAVE_POW + g[0][j] = (int) (pow(j / 255.0, gamma) * 32767 + 0.5); + g[1][j] = (int) (pow(j / 256.0, 1/gamma) * 32767 + 0.5); +#else + g[0][j] = (int) (j / 255.0 * 32767 + 0.5); + g[1][j] = (int) (j / 256.0 * 32767 + 0.5); +#endif + /* The ++gamma_tables[][] ensures that round-trip gamma correction + always preserve the input colors. Without it, one might have, + for example, input values 0, 1, and 2 all mapping to + gamma-corrected value 0. Then a round-trip through gamma + correction loses information. + */ + for (i = j ? 0 : 2; i != 2; ++i) { + while (g[i][j] <= g[i][j-1] && g[i][j] < 32767) + ++g[i][j]; + } + } + } + cur_type = type; + cur_gamma = gamma; +} + +kcolor kc_revgamma_transform(kcolor x) { + int d; + for (d = 0; d != 3; ++d) { + int c = gamma_tables[1][x.a[d] >> 7]; + while (c < 0x7F80 && x.a[d] >= gamma_tables[0][(c + 0x80) >> 7]) + c += 0x80; + x.a[d] = c; + } + return x; +} + +#if 0 +static void kc_test_gamma() { + int x, y, z; + for (x = 0; x != 256; ++x) + for (y = 0; y != 256; ++y) + for (z = 0; z != 256; ++z) { + kcolor k = kc_make8g(x, y, z); + kc_revgamma_transform(&k); + if ((k.a[0] >> 7) != x || (k.a[1] >> 7) != y + || (k.a[2] >> 7) != z) { + kcolor kg = kc_make8g(x, y, z); + fprintf(stderr, "#%02X%02X%02X ->g #%04X%04X%04X ->revg #%02X%02X%02X!\n", + x, y, z, kg.a[0], kg.a[1], kg.a[2], + k.a[0] >> 7, k.a[1] >> 7, k.a[2] >> 7); + assert(0); + } + } +} +#endif + +kcolor kc_oklab_transform(int a0, int a1, int a2) { +#if HAVE_CBRTF + float cr = linear_srgb_table_256[a0]; + float cg = linear_srgb_table_256[a1]; + float cb = linear_srgb_table_256[a2]; + + float l = 0.4122214708f * cr + 0.5363325363f * cg + 0.0514459929f * cb; + float m = 0.2119034982f * cr + 0.6806995451f * cg + 0.1073969566f * cb; + float s = 0.0883024619f * cr + 0.2817188376f * cg + 0.6299787005f * cb; + + float l_ = cbrtf(l); + float m_ = cbrtf(m); + float s_ = cbrtf(s); + + float okl = 0.2104542553f*l_ + 0.7936177850f*m_ - 0.0040720468f*s_; + float oka = 1.9779984951f*l_ - 2.4285922050f*m_ + 0.4505937099f*s_; + float okb = 0.0259040371f*l_ + 0.7827717662f*m_ - 0.8086757660f*s_; + + // On sRGB colors, `okl` ranges from 0 to 1, + // `oka` ranges from -0.23388740 to +0.27621666, + // `okb` ranges from -0.31152815 to +0.19856972. + + kcolor kc; + kc.a[0] = okl * 32767; + kc.a[1] = (oka + 0.5) * 32767; + kc.a[2] = (okb + 0.5) * 32767; + return kc; +#else + /* fall back to sRGB gamma */ + return kc_make8g(a0, a1, a2); +#endif +} + + +static int kchist_sizes[] = { + 4093, 16381, 65521, 262139, 1048571, 4194301, 16777213, + 67108859, 268435459, 1073741839 +}; + +static void kchist_grow(kchist* kch); + +void kchist_init(kchist* kch) { + int i; + kch->h = Gif_NewArray(kchistitem, kchist_sizes[0]); + kch->n = 0; + kch->capacity = kchist_sizes[0]; + for (i = 0; i != kch->capacity; ++i) + kch->h[i].count = 0; +} + +void kchist_cleanup(kchist* kch) { + Gif_DeleteArray(kch->h); + kch->h = NULL; +} + +kchistitem* kchist_add(kchist* kch, kcolor k, kchist_count_t count) { + unsigned hash1, hash2 = 0; + kacolor ka; + kchistitem *khi; + ka.k = k; + ka.a[3] = 0; + + if (!kch->capacity + || kch->n > ((kch->capacity * 3) >> 4)) + kchist_grow(kch); + hash1 = (((ka.a[0] & 0x7FE0) << 15) + | ((ka.a[1] & 0x7FE0) << 5) + | ((ka.a[2] & 0x7FE0) >> 5)) % kch->capacity; + + while (1) { + khi = &kch->h[hash1]; + if (!khi->count + || memcmp(&khi->ka, &ka, sizeof(ka)) == 0) + break; + if (!hash2) { + hash2 = (((ka.a[0] & 0x03FF) << 20) + | ((ka.a[1] & 0x03FF) << 10) + | (ka.a[2] & 0x03FF)) % kch->capacity; + hash2 = hash2 ? hash2 : 1; + } + hash1 += hash2; + if (hash1 >= (unsigned) kch->capacity) + hash1 -= kch->capacity; + } + + if (!khi->count) { + khi->ka = ka; + ++kch->n; + } + khi->count += count; + if (khi->count < count) + khi->count = (kchist_count_t) -1; + return khi; +} + +static void kchist_grow(kchist* kch) { + kchistitem* oldh = kch->h; + int i, oldcapacity = kch->capacity ? kch->capacity : kch->n; + for (i = 0; kchist_sizes[i] <= oldcapacity; ++i) + /* do nothing */; + kch->capacity = kchist_sizes[i]; + kch->h = Gif_NewArray(kchistitem, kch->capacity); + kch->n = 0; + for (i = 0; i != kch->capacity; ++i) + kch->h[i].count = 0; + for (i = 0; i != oldcapacity; ++i) + if (oldh[i].count) + kchist_add(kch, oldh[i].ka.k, oldh[i].count); + Gif_DeleteArray(oldh); +} + +void kchist_compress(kchist* kch) { + int i, j; + for (i = 0, j = kch->n; i != kch->n; ) + if (kch->h[i].count) + ++i; + else if (kch->h[j].count) { + kch->h[i] = kch->h[j]; + ++i, ++j; + } else + ++j; + kch->capacity = 0; +} + +void kchist_make(kchist* kch, Gif_Stream* gfs, uint32_t* ntransp_store) { + uint32_t gcount[256], lcount[256]; + uint32_t nbackground = 0, ntransparent = 0; + int x, y, i, imagei; + kchist_init(kch); + + for (i = 0; i != 256; ++i) + gcount[i] = 0; + + /* Count pixels */ + for (imagei = 0; imagei < gfs->nimages; ++imagei) { + Gif_Image* gfi = gfs->images[imagei]; + Gif_Colormap* gfcm = gfi->local ? gfi->local : gfs->global; + uint32_t* count = gfi->local ? lcount : gcount; + uint32_t old_transparent_count = 0; + int only_compressed = (gfi->img == 0); + if (!gfcm) + continue; + if (count == lcount) + for (i = 0; i != 256; ++i) + count[i] = 0; + if (gfi->transparent >= 0) + old_transparent_count = count[gfi->transparent]; + + /* unoptimize the image if necessary */ + if (only_compressed) + Gif_UncompressImage(gfs, gfi); + + /* sweep over the image data, counting pixels */ + for (y = 0; y < gfi->height; ++y) { + const uint8_t* data = gfi->img[y]; + for (x = 0; x < gfi->width; ++x, ++data) + ++count[*data]; + } + + /* add counted colors to global histogram (local only) */ + if (gfi->local) + for (i = 0; i != gfcm->ncol; ++i) + if (count[i] && i != gfi->transparent) + kchist_add(kch, kc_makegfcg(&gfcm->col[i]), count[i]); + if (gfi->transparent >= 0 + && count[gfi->transparent] != old_transparent_count) { + ntransparent += count[gfi->transparent] - old_transparent_count; + count[gfi->transparent] = old_transparent_count; + } + + /* if this image has background disposal, count its size towards the + background's pixel count */ + if (gfi->disposal == GIF_DISPOSAL_BACKGROUND) + nbackground += (unsigned) gfi->width * (unsigned) gfi->height; + + /* throw out compressed image if necessary */ + if (only_compressed) + Gif_ReleaseUncompressedImage(gfi); + } + + if (gfs->images[0]->transparent < 0 + && gfs->global && gfs->background < gfs->global->ncol) + gcount[gfs->background] += nbackground; + else + ntransparent += nbackground; + + if (gfs->global) + for (i = 0; i != gfs->global->ncol; ++i) + if (gcount[i]) + kchist_add(kch, kc_makegfcg(&gfs->global->col[i]), gcount[i]); + + /* now, make the linear histogram from the hashed histogram */ + kchist_compress(kch); + *ntransp_store = ntransparent; +} + + +/***** + * kd_tree allocation and deallocation + **/ + +struct kd3_treepos { + int pivot; + int offset; +}; + +void kd3_init(kd3_tree* kd3, kcolor (*transform)(int, int, int)) { + kd3->tree = NULL; + kd3->ks = Gif_NewArray(kcolor, 256); + kd3->nitems = 0; + kd3->items_cap = 256; + kd3->transform = transform ? transform : kc_make8g; + kd3->xradius = NULL; + kd3->disabled = -1; +} + +void kd3_cleanup(kd3_tree* kd3) { + Gif_DeleteArray(kd3->tree); + Gif_DeleteArray(kd3->ks); + Gif_DeleteArray(kd3->xradius); +} + +void kd3_add_transformed(kd3_tree* kd3, kcolor k) { + if (kd3->nitems == kd3->items_cap) { + kd3->items_cap *= 2; + Gif_ReArray(kd3->ks, kcolor, kd3->items_cap); + } + kd3->ks[kd3->nitems] = k; + ++kd3->nitems; + if (kd3->tree) { + Gif_DeleteArray(kd3->tree); + Gif_DeleteArray(kd3->xradius); + kd3->tree = NULL; + kd3->xradius = NULL; + } +} + +static kd3_tree* kd3_sorter; + +static int kd3_item_compare_0(const void* a, const void* b) { + const int* aa = (const int*) a, *bb = (const int*) b; + return kd3_sorter->ks[*aa].a[0] - kd3_sorter->ks[*bb].a[0]; +} + +static int kd3_item_compare_1(const void* a, const void* b) { + const int* aa = (const int*) a, *bb = (const int*) b; + return kd3_sorter->ks[*aa].a[1] - kd3_sorter->ks[*bb].a[1]; +} + +static int kd3_item_compare_2(const void* a, const void* b) { + const int* aa = (const int*) a, *bb = (const int*) b; + return kd3_sorter->ks[*aa].a[2] - kd3_sorter->ks[*bb].a[2]; +} + +static int (*kd3_item_compares[])(const void*, const void*) = { + &kd3_item_compare_0, &kd3_item_compare_1, &kd3_item_compare_2 +}; + +static int kd3_item_compare_all(const void* a, const void* b) { + const int* aa = (const int*) a, *bb = (const int*) b; + return memcmp(&kd3_sorter->ks[*aa], &kd3_sorter->ks[*bb], sizeof(kcolor)); +} + +static int kd3_build_range(int* perm, int nperm, int n, int depth) { + kd3_tree* kd3 = kd3_sorter; + int m, nl, nr, aindex = depth % 3; + if (depth > kd3->maxdepth) + kd3->maxdepth = depth; + while (n >= kd3->ntree) { + kd3->ntree *= 2; + Gif_ReArray(kd3->tree, kd3_treepos, kd3->ntree); + } + if (nperm <= 1) { + kd3->tree[n].pivot = (nperm == 0 ? -1 : perm[0]); + kd3->tree[n].offset = -1; + return 2; + } + + qsort(perm, nperm, sizeof(int), kd3_item_compares[aindex]); + + /* pick pivot: a color component to split */ + m = nperm >> 1; + while (m > 0 + && kd3->ks[perm[m]].a[aindex] == kd3->ks[perm[m-1]].a[aindex]) + --m; + if (m == 0) { /* don't split entirely to the right (infinite loop) */ + m = nperm >> 1; + while (m < nperm - 1 /* also, don't split entirely to the left */ + && kd3->ks[perm[m]].a[aindex] == kd3->ks[perm[m-1]].a[aindex]) + ++m; + } + if (m == 0) + kd3->tree[n].pivot = kd3->ks[perm[m]].a[aindex]; + else + kd3->tree[n].pivot = kd3->ks[perm[m-1]].a[aindex] + + ((kd3->ks[perm[m]].a[aindex] - kd3->ks[perm[m-1]].a[aindex]) >> 1); + + /* recurse */ + nl = kd3_build_range(perm, m, n+1, depth+1); + kd3->tree[n].offset = 1+nl; + nr = kd3_build_range(&perm[m], nperm - m, n+1+nl, depth+1); + return 1+nl+nr; +} + +#if 0 +static void kd3_print_depth(kd3_tree* kd3, int depth, kd3_treepos* p, + int* a, int* b) { + int i; + char x[6][10]; + for (i = 0; i != 3; ++i) { + if (a[i] == INT_MIN) + snprintf(x[2*i], sizeof(x[2*i]), "*"); + else + snprintf(x[2*i], sizeof(x[2*i]), "%d", a[i]); + if (b[i] == INT_MAX) + snprintf(x[2*i+1], sizeof(x[2*i+1]), "*"); + else + snprintf(x[2*i+1], sizeof(x[2*i+1]), "%d", b[i]); + } + printf("%*s<%s:%s,%s:%s,%s:%s>", depth*3, "", + x[0], x[1], x[2], x[3], x[4], x[5]); + if (p->offset < 0) { + if (p->pivot >= 0) { + assert(kd3->ks[p->pivot].a[0] >= a[0]); + assert(kd3->ks[p->pivot].a[1] >= a[1]); + assert(kd3->ks[p->pivot].a[2] >= a[2]); + assert(kd3->ks[p->pivot].a[0] < b[0]); + assert(kd3->ks[p->pivot].a[1] < b[1]); + assert(kd3->ks[p->pivot].a[2] < b[2]); + printf(" ** @%d: <%d,%d,%d>\n", p->pivot, kd3->ks[p->pivot].a[0], kd3->ks[p->pivot].a[1], kd3->ks[p->pivot].a[2]); + } + } else { + int aindex = depth % 3, x[3]; + assert(p->pivot >= a[aindex]); + assert(p->pivot < b[aindex]); + printf((aindex == 0 ? " | <%d,_,_>\n" : + aindex == 1 ? " | <_,%d,_>\n" : " | <_,_,%d>\n"), p->pivot); + memcpy(x, b, sizeof(int) * 3); + x[aindex] = p->pivot; + kd3_print_depth(kd3, depth + 1, p + 1, a, x); + memcpy(x, a, sizeof(int) * 3); + x[aindex] = p->pivot; + kd3_print_depth(kd3, depth + 1, p + p->offset, x, b); + } +} + +static void kd3_print(kd3_tree* kd3) { + int a[3], b[3]; + a[0] = a[1] = a[2] = INT_MIN; + b[0] = b[1] = b[2] = INT_MAX; + kd3_print_depth(kd3, 0, kd3->tree, a, b); +} +#endif + +void kd3_build_xradius(kd3_tree* kd3) { + int i, j; + /* create xradius */ + if (kd3->xradius) + return; + kd3->xradius = Gif_NewArray(unsigned, kd3->nitems); + for (i = 0; i != kd3->nitems; ++i) + kd3->xradius[i] = (unsigned) -1; + for (i = 0; i != kd3->nitems; ++i) + for (j = i + 1; j != kd3->nitems; ++j) { + unsigned dist = kc_distance(kd3->ks[i], kd3->ks[j]); + unsigned radius = dist / 4; + if (radius < kd3->xradius[i]) + kd3->xradius[i] = radius; + if (radius < kd3->xradius[j]) + kd3->xradius[j] = radius; + } +} + +void kd3_build(kd3_tree* kd3) { + int i, delta, *perm; + assert(!kd3->tree); + + /* create tree */ + kd3->tree = Gif_NewArray(kd3_treepos, 256); + kd3->ntree = 256; + kd3->maxdepth = 0; + + /* create copy of items; remove duplicates */ + perm = Gif_NewArray(int, kd3->nitems); + for (i = 0; i != kd3->nitems; ++i) + perm[i] = i; +#if ENABLE_THREADS + /* + * Because kd3_sorter is a static global used in some + * sorting comparators, put a mutex around this + * code block to avoid an utter catastrophe. + */ + pthread_mutex_lock(&kd3_sort_lock); +#endif + + kd3_sorter = kd3; + qsort(perm, kd3->nitems, sizeof(int), kd3_item_compare_all); + for (i = 0, delta = 1; i + delta < kd3->nitems; ++i) + if (memcmp(&kd3->ks[perm[i]], &kd3->ks[perm[i+delta]], + sizeof(kcolor)) == 0) + ++delta, --i; + else if (delta > 1) + perm[i+1] = perm[i+delta]; + + kd3_build_range(perm, kd3->nitems - (delta - 1), 0, 0); + assert(kd3->maxdepth < 32); + +#if ENABLE_THREADS + pthread_mutex_unlock(&kd3_sort_lock); +#endif + Gif_DeleteArray(perm); +} + +void kd3_init_build(kd3_tree* kd3, kcolor (*transform)(int, int, int), + const Gif_Colormap* gfcm) { + int i; + kd3_init(kd3, transform); + for (i = 0; i < gfcm->ncol; ++i) + kd3_add8g(kd3, gfcm->col[i].gfc_red, gfcm->col[i].gfc_green, + gfcm->col[i].gfc_blue); + kd3_build(kd3); +} + +int kd3_closest_transformed(kd3_tree* kd3, kcolor k, unsigned* dist_store) { + const kd3_treepos* stack[32]; + uint8_t state[32]; + int stackpos = 0; + int result = -1; + unsigned mindist = (unsigned) -1; + + if (!kd3->tree) + kd3_build(kd3); + + stack[0] = kd3->tree; + state[0] = 0; + + while (stackpos >= 0) { + const kd3_treepos* p; + assert(stackpos < 32); + p = stack[stackpos]; + + if (p->offset < 0) { + if (p->pivot >= 0 && kd3->disabled != p->pivot) { + unsigned dist = kc_distance(kd3->ks[p->pivot], k); + if (dist < mindist) { + mindist = dist; + result = p->pivot; + } + } + if (--stackpos >= 0) + ++state[stackpos]; + } else if (state[stackpos] == 0) { + if (k.a[stackpos % 3] < p->pivot) + stack[stackpos + 1] = p + 1; + else + stack[stackpos + 1] = p + p->offset; + ++stackpos; + state[stackpos] = 0; + } else { + int delta = k.a[stackpos % 3] - p->pivot; + if (state[stackpos] == 1 + && (unsigned) delta * (unsigned) delta < mindist) { + if (delta < 0) + stack[stackpos + 1] = p + p->offset; + else + stack[stackpos + 1] = p + 1; + ++stackpos; + state[stackpos] = 0; + } else if (--stackpos >= 0) + ++state[stackpos]; + } + } + + if (dist_store) + *dist_store = mindist; + return result; +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/kcolor.h b/packages/optimizt-gifsicle/upstream/gifsicle/src/kcolor.h new file mode 100644 index 0000000..b1c16df --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/kcolor.h @@ -0,0 +1,354 @@ +/* kcolor.h - Color-oriented function declarations for gifsicle. + Copyright (C) 2013-2021 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#ifndef GIFSICLE_KCOLOR_H +#define GIFSICLE_KCOLOR_H +#include +#include + +/* kcolor: a 3D vector, each component has 15 bits of precision */ +/* 15 bits means KC_MAX * KC_MAX always fits within a signed 32-bit + integer, and a 3-D squared distance always fits within an unsigned 32-bit + integer. */ +#define KC_MAX 0x7FFF +#define KC_WHOLE 0x8000 +#define KC_HALF 0x4000 +#define KC_QUARTER 0x2000 +#define KC_BITS 15 +typedef struct kcolor { + int16_t a[3]; +} kcolor; + +#undef min +#undef max +#define min(a, b) ((a) < (b) ? (a) : (b)) +#define max(a, b) ((a) > (b) ? (a) : (b)) + +#define KC_CLAMPV(v) (max(0, min((v), KC_MAX))) + +typedef union kacolor { + kcolor k; + int16_t a[4]; +#if HAVE_INT64_T + int64_t q; /* to get better alignment */ +#endif +} kacolor; + + +/* gamma_tables[0]: array of 256 gamma-conversion values + gamma_tables[1]: array of 256 reverse gamma-conversion values */ +extern const uint16_t* gamma_tables[2]; + + +/* return the gamma transformation of `a0/a1/a2` [RGB] */ +static inline kcolor kc_make8g(int a0, int a1, int a2) { + kcolor kc; + kc.a[0] = gamma_tables[0][a0]; + kc.a[1] = gamma_tables[0][a1]; + kc.a[2] = gamma_tables[0][a2]; + return kc; +} + +/* return the gamma transformation of `*gfc` */ +static inline kcolor kc_makegfcg(const Gif_Color* gfc) { + return kc_make8g(gfc->gfc_red, gfc->gfc_green, gfc->gfc_blue); +} + +/* return the uncorrected representation of `a0/a1/a2` [RGB] */ +static inline kcolor kc_make8ng(int a0, int a1, int a2) { + kcolor kc; + kc.a[0] = (a0 << 7) + (a0 >> 1); + kc.a[1] = (a1 << 7) + (a1 >> 1); + kc.a[2] = (a2 << 7) + (a2 >> 1); + return kc; +} + +/* return the kcolor representation of `*gfc` (no gamma transformation) */ +static inline kcolor kc_makegfcng(const Gif_Color* gfc) { + return kc_make8ng(gfc->gfc_red, gfc->gfc_green, gfc->gfc_blue); +} + +/* return transparency */ +static inline kacolor kac_transparent() { + kacolor x; + x.a[0] = x.a[1] = x.a[2] = x.a[3] = 0; + return x; +} + +/* return a hex color string definition for `x` */ +const char* kc_debug_str(kcolor x); + +/* set `*x` to the reverse gamma transformation of `*x` */ +kcolor kc_revgamma_transform(kcolor x); + +/* return the reverse gramma transformation of `*x` as a Gif_Color */ +static inline Gif_Color kc_togfcg(kcolor x) { + Gif_Color gfc; + x = kc_revgamma_transform(x); + gfc.gfc_red = (uint8_t) (x.a[0] >> 7); + gfc.gfc_green = (uint8_t) (x.a[1] >> 7); + gfc.gfc_blue = (uint8_t) (x.a[2] >> 7); + gfc.haspixel = 0; + return gfc; +} + + +/* return the squared Euclidean distance between `*x` and `*y` */ +static inline uint32_t kc_distance(kcolor x, kcolor y) { + /* It’s OK to use unsigned multiplication for this: the low 32 bits + are the same either way. Unsigned avoids undefined behavior. */ + uint32_t d0 = (uint32_t) x.a[0] - (uint32_t) y.a[0]; + uint32_t d1 = (uint32_t) x.a[1] - (uint32_t) y.a[1]; + uint32_t d2 = (uint32_t) x.a[2] - (uint32_t) y.a[2]; + return d0 * d0 + d1 * d1 + d2 * d2; +} + +/* return the luminance value for `*x`; result is between 0 and KC_MAX */ +static inline int kc_luminance(kcolor kc) { + return (55 * kc.a[0] + 183 * kc.a[1] + 19 * kc.a[2]) >> 8; +} + +/* set `*x` to the grayscale version of `*x`, transformed by luminance */ +static inline kcolor kc_luminance_transform(int a0, int a1, int a2) { + /* For grayscale colormaps, use distance in luminance space instead of + distance in RGB space. The weights for the R,G,B components in + luminance space are 0.2126,0.7152,0.0722. (That's ITU primaries, which + are compatible with sRGB; NTSC recommended our previous values, + 0.299,0.587,0.114.) Using the proportional factors 55,183,19 we get a + scaled gray value between 0 and 255 * 257; dividing by 256 gives us + what we want. Thanks to Christian Kumpf, , for + providing a patch. */ + kcolor kc = kc_make8g(a0, a1, a2); + kc.a[0] = kc.a[1] = kc.a[2] = kc_luminance(kc); + return kc; +} + +kcolor kc_oklab_transform(int a0, int a1, int a2); + + +/* wkcolor: like kcolor, but components are 32 bits instead of 16 */ + +typedef struct wkcolor { + int32_t a[3]; +} wkcolor; + +static inline wkcolor wkc_zero(void) { + wkcolor wkc; + wkc.a[0] = wkc.a[1] = wkc.a[2] = 0; + return wkc; +} + +static inline int wkc_iszero(wkcolor wkc) { + return wkc.a[0] == 0 && wkc.a[1] == 0 && wkc.a[2] == 0; +} + +static inline kcolor kc_adjust(kcolor k, wkcolor delta) { + k.a[0] = KC_CLAMPV((int32_t) k.a[0] + delta.a[0]); + k.a[1] = KC_CLAMPV((int32_t) k.a[1] + delta.a[1]); + k.a[2] = KC_CLAMPV((int32_t) k.a[2] + delta.a[2]); + return k; +} + + +/* kd3_tree: kd-tree for 3 dimensions, indexing kcolors */ + +typedef struct kd3_tree kd3_tree; +typedef struct kd3_treepos kd3_treepos; + +struct kd3_tree { + kd3_treepos* tree; + int ntree; + int disabled; + kcolor* ks; + int nitems; + int items_cap; + int maxdepth; + kcolor (*transform)(int, int, int); + unsigned* xradius; +}; + +/* initialize `kd3` with the given color `transform` (may be NULL) */ +void kd3_init(kd3_tree* kd3, kcolor (*transform)(int, int, int)); + +/* free `kd3` */ +void kd3_cleanup(kd3_tree* kd3); + +/* return the transformed color for 8-bit color `a0/a1/a2` (RGB) */ +static inline kcolor kd3_make8g(kd3_tree* kd3, int a0, int a1, int a2) { + return kd3->transform(a0, a1, a2); +} + +/* return the transformed color for `*gfc` */ +static inline kcolor kd3_makegfcg(kd3_tree* kd3, const Gif_Color* gfc) { + return kd3_make8g(kd3, gfc->gfc_red, gfc->gfc_green, gfc->gfc_blue); +} + +/* add the transformed color `k` to `*kd3` (do not apply `kd3->transform`). */ +void kd3_add_transformed(kd3_tree* kd3, kcolor k); + +/* given 8-bit color `a0/a1/a2` (RGB), transform it by `kd3->transform` + (e.g., apply gamma), and add it to `*kd3` */ +static inline void kd3_add8g(kd3_tree* kd3, int a0, int a1, int a2) { + kd3_add_transformed(kd3, kd3_make8g(kd3, a0, a1, a2)); +} + +/* set `kd3->xradius`. given color `i`, `kd3->xradius[i]` is the square of the + color's uniquely owned neighborhood. + If `kc_distance(&kd3->ks[i], &k) < kd3->xradius[i]`, then + `kd3_closest_transformed(kd3, &k) == i`. */ +void kd3_build_xradius(kd3_tree* kd3); + +/* build the actual kd-tree for `kd3`. must be called before kd3_closest. */ +void kd3_build(kd3_tree* kd3); + +/* kd3_init + kd3_add8g for all colors in `gfcm` + kd3_build */ +void kd3_init_build(kd3_tree* kd3, kcolor (*transform)(int, int, int), + const Gif_Colormap* gfcm); + +/* return the index of the color in `*kd3` closest to `k`. + if `dist!=NULL`, store the distance from `k` to that index in `*dist`. */ +int kd3_closest_transformed(kd3_tree* kd3, kcolor k, unsigned* dist); + +/* given 8-bit color `a0/a1/a2` (RGB), transform it by `kd3->transform` + (e.g., apply gamma), and return the index of the color in `*kd3` + closest to the result. */ +static inline int kd3_closest8g(kd3_tree* kd3, int a0, int a1, int a2) { + return kd3_closest_transformed(kd3, kd3_make8g(kd3, a0, a1, a2), NULL); +} + +/* disable color index `i` in `*kd3`: it will never be returned by + `kd3_closest*` */ +static inline void kd3_disable(kd3_tree* kd3, int i) { + assert((unsigned) i < (unsigned) kd3->nitems); + assert(kd3->disabled < 0 || kd3->disabled == i); + kd3->disabled = i; +} + +/* enable all color indexes in `*kd3` */ +static inline void kd3_enable_all(kd3_tree* kd3) { + kd3->disabled = -1; +} + + +typedef uint32_t kchist_count_t; +typedef struct kchistitem { + kacolor ka; + kchist_count_t count; +} kchistitem; + +typedef struct kchist { + kchistitem* h; + int n; + int capacity; +} kchist; + +void kchist_init(kchist* kch); +void kchist_cleanup(kchist* kch); +void kchist_make(kchist* kch, Gif_Stream* gfs, uint32_t* ntransp); +kchistitem* kchist_add(kchist* kch, kcolor color, kchist_count_t count); +void kchist_compress(kchist* kch); + +static inline int kchistitem_compare_red(const void* va, const void* vb) { + const kchistitem* a = (const kchistitem*) va; + const kchistitem* b = (const kchistitem*) vb; + return a->ka.a[0] - b->ka.a[0]; +} + +static inline int kchistitem_compare_green(const void* va, const void* vb) { + const kchistitem* a = (const kchistitem*) va; + const kchistitem* b = (const kchistitem*) vb; + return a->ka.a[1] - b->ka.a[1]; +} + +static inline int kchistitem_compare_blue(const void* va, const void* vb) { + const kchistitem* a = (const kchistitem*) va; + const kchistitem* b = (const kchistitem*) vb; + return a->ka.a[2] - b->ka.a[2]; +} + +static inline int kchistitem_compare_popularity(const void* va, const void* vb) { + const kchistitem* a = (const kchistitem*) va; + const kchistitem* b = (const kchistitem*) vb; + return a->count > b->count ? -1 : a->count != b->count; +} + + +typedef struct { + kchist* kch; + int* closest; + uint32_t* min_dist; + uint32_t* min_dither_dist; + int* chosen; + int nchosen; +} kcdiversity; + +void kcdiversity_init(kcdiversity* div, kchist* kch, int dodither); +void kcdiversity_cleanup(kcdiversity* div); +int kcdiversity_find_popular(kcdiversity* div); +int kcdiversity_find_diverse(kcdiversity* div, double ditherweight); +int kcdiversity_choose(kcdiversity* div, int chosen, int dodither); + + +#if HAVE_SIMD && HAVE_VECTOR_SIZE_VECTOR_TYPES +typedef float float4 __attribute__((vector_size (sizeof(float) * 4))); +typedef int int4 __attribute__((vector_size (sizeof(int) * 4))); +#elif HAVE_SIMD && HAVE_EXT_VECTOR_TYPE_VECTOR_TYPES +typedef float float4 __attribute__((ext_vector_type (4))); +#else +typedef float float4[4]; +#endif + +typedef union scale_color { + float4 a; +} scale_color; + +static inline void sc_clear(scale_color* x) { + x->a[0] = x->a[1] = x->a[2] = x->a[3] = 0; +} + +static inline scale_color sc_makekc(kcolor k) { + scale_color sc; + sc.a[0] = k.a[0]; + sc.a[1] = k.a[1]; + sc.a[2] = k.a[2]; + sc.a[3] = KC_MAX; + return sc; +} + +static inline scale_color sc_make(float a0, float a1, float a2, float a3) { + scale_color sc; + sc.a[0] = a0; + sc.a[1] = a1; + sc.a[2] = a2; + sc.a[3] = a3; + return sc; +} + +#if HAVE_SIMD +# define SCVEC_ADDV(sc, sc2) (sc).a += (sc2).a +# define SCVEC_MULV(sc, sc2) (sc).a *= (sc2).a +# define SCVEC_MULF(sc, f) (sc).a *= (f) +# define SCVEC_DIVF(sc, f) (sc).a /= (f) +# define SCVEC_ADDVxF(sc, sc2, f) (sc).a += (sc2).a * (f) +# if HAVE___BUILTIN_SHUFFLEVECTOR +# define SCVEC_ROT3(out, sc) do { (out).a = __builtin_shufflevector((sc).a, (sc).a, 1, 2, 0, 3); } while (0) +# else +# define SCVEC_ROT3(out, sc) do { int4 shufmask__ = {1, 2, 0, 3}; (out).a = __builtin_shuffle((sc).a, shufmask__); } while (0) +# endif +#else +# define SCVEC_FOREACH(t) do { int k__; for (k__ = 0; k__ != 4; ++k__) { t; } } while (0) +# define SCVEC_ADDV(sc, sc2) SCVEC_FOREACH((sc).a[k__] += (sc2).a[k__]) +# define SCVEC_MULV(sc, sc2) SCVEC_FOREACH((sc).a[k__] *= (sc2).a[k__]) +# define SCVEC_MULF(sc, f) SCVEC_FOREACH((sc).a[k__] *= (f)) +# define SCVEC_DIVF(sc, f) SCVEC_FOREACH((sc).a[k__] /= (f)) +# define SCVEC_ADDVxF(sc, sc2, f) SCVEC_FOREACH((sc).a[k__] += (sc2).a[k__] * (f)) +# define SCVEC_ROT3(out, sc) do { float __a0 = (sc).a[0]; (out).a[0] = (sc).a[1]; (out).a[1] = (sc).a[2]; (out).a[2] = __a0; (out).a[3] = (sc).a[3]; } while (0) +#endif + +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/merge.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/merge.c new file mode 100644 index 0000000..5b6b110 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/merge.c @@ -0,0 +1,403 @@ +/* merge.c - Functions which actually combine and manipulate GIF image data. + Copyright (C) 1997-2021 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#include +#include "gifsicle.h" +#include +#include +#include +#include +#include +#include + +/* First merging stage: Mark the used colors in all colormaps. */ + +void +unmark_colors(Gif_Colormap *gfcm) +{ + int i; + if (gfcm) + for (i = 0; i < gfcm->ncol; i++) + gfcm->col[i].haspixel = 0; +} + +void +unmark_colors_2(Gif_Colormap *gfcm) +{ + int i; + for (i = 0; i < gfcm->ncol; i++) { + gfcm->col[i].pixel = 256; + gfcm->col[i].haspixel = 0; + } +} + + +void +mark_used_colors(Gif_Stream *gfs, Gif_Image *gfi, Gt_Crop *crop, + int compress_immediately) +{ + Gif_Colormap *gfcm = gfi->local ? gfi->local : gfs->global; + Gif_Color *col; + int i, j, l, t, r, b, nleft, ncol, transp = gfi->transparent; + + /* There might not be a colormap. */ + if (!gfcm) + return; + col = gfcm->col; + ncol = gfcm->ncol; + + /* Mark color used for transparency. */ + if (transp >= 0 && transp < ncol) + col[transp].haspixel |= 2; + + /* Only mark colors until we've seen all of them. The left variable keeps + track of how many are left. */ + for (i = nleft = 0; i < ncol; ++i) + if (!(col[i].haspixel & 1) && i != transp) + ++nleft; + if (nleft == 0) + return; + + if (gfi->img || Gif_UncompressImage(gfs, gfi) == 2) + compress_immediately = 0; + + /* Loop over every pixel (until we've seen all colors) */ + if (crop) { + Gt_Crop c; + combine_crop(&c, crop, gfi); + l = c.x; + t = c.y; + r = l + c.w; + b = t + c.h; + } else { + l = t = 0; + r = gfi->width; + b = gfi->height; + } + + for (j = t; j != b; ++j) { + uint8_t *data = gfi->img[j] + l; + for (i = l; i != r; ++i, ++data) + if (*data < ncol && !(col[*data].haspixel & 1) && *data != transp) { + col[*data].haspixel |= 1; + --nleft; + if (nleft == 0) + goto done; + } + } + + done: + if (compress_immediately > 0) + Gif_ReleaseUncompressedImage(gfi); +} + + +int +find_color_index(Gif_Color *c, int nc, Gif_Color *color) +{ + int index; + for (index = 0; index < nc; index++) + if (GIF_COLOREQ(&c[index], color)) + return index; + return -1; +} + + +int +merge_colormap_if_possible(Gif_Colormap *dest, Gif_Colormap *src) +{ + Gif_Color *srccol; + Gif_Color *destcol = dest->col; + int destncol = dest->ncol; + int dest_user_flags = dest->user_flags; + int i, x; + int trivial_map = 1; + + if (!src) + return 1; + + srccol = src->col; + for (i = 0; i < src->ncol; i++) { + if (srccol[i].haspixel & 1) { + /* Store an image color cell's mapping to the global colormap in + its 'pixel' slot. This is useful caching: oftentimes many + input frames will share a colormap */ + int mapto = (srccol[i].pixel < 256 ? (int)srccol[i].pixel : -1); + + if (mapto == -1) + mapto = find_color_index(destcol, destncol, &srccol[i]); + + if (mapto == -1 && destncol < 256) { + /* add the color */ + mapto = destncol; + destcol[mapto] = srccol[i]; + destncol++; + } + + if (mapto == -1) + /* check for a pure-transparent color */ + for (x = 0; x < destncol; x++) + if (destcol[x].haspixel == 2) { + mapto = x; + destcol[mapto] = srccol[i]; + break; + } + + if (mapto == -1) + /* give up and require a local colormap */ + goto local_colormap_required; + + assert(mapto >= 0 && mapto < destncol); + assert(GIF_COLOREQ(&destcol[mapto], &srccol[i])); + + srccol[i].pixel = mapto; + destcol[mapto].haspixel = 1; + if (mapto != i) + trivial_map = 0; + + } else if (srccol[i].haspixel & 2) { + /* a dedicated transparent color; if trivial_map & at end of + colormap insert it with haspixel == 2. (strictly not + necessary; we do it to try to keep the map trivial.) */ + if (trivial_map && i == destncol) { + destcol[destncol] = srccol[i]; + destncol++; + } + } + } + + /* success! save new number of colors */ + dest->ncol = destncol; + dest->user_flags = dest_user_flags; + return 1; + + /* failure: a local colormap is required */ + local_colormap_required: + if (warn_local_colormaps == 1) { + static int context = 0; + if (!context) { + warning(1, "too many colors, using local colormaps\n" + " (You may want to try %<--colors 256%>.)"); + context = 1; + } else + warning(1, "too many colors, using local colormaps"); + warn_local_colormaps = 2; + } + + /* 9.Dec.1998 - This must have been a longstanding bug! We MUST clear + the cached mappings of any pixels in the source colormap we + assigned this time through, since we are throwing those colors + away. We assigned it this time through if the cached mapping is >= + dest->ncol. */ + for (x = 0; x < i; x++) + if ((srccol[x].haspixel & 1) && srccol[x].pixel >= (uint32_t)dest->ncol) + srccol[x].pixel = 256; + + return 0; +} + + +void +merge_stream(Gif_Stream *dest, Gif_Stream *src, int no_comments) +{ + int i; + assert(dest->global); + + /* unmark colors in global and local colormaps -- 12/9 */ + if (src->global) + unmark_colors_2(src->global); + for (i = 0; i < src->nimages; i++) + if (src->images[i]->local) + unmark_colors_2(src->images[i]->local); + + if (dest->loopcount < 0) + dest->loopcount = src->loopcount; + + if (src->end_comment && !no_comments) { + if (!dest->end_comment) + dest->end_comment = Gif_NewComment(); + merge_comments(dest->end_comment, src->end_comment); + } +} + + +void +merge_comments(Gif_Comment *destc, Gif_Comment *srcc) +{ + int i; + for (i = 0; i < srcc->count; i++) + Gif_AddComment(destc, srcc->str[i], srcc->len[i]); +} + + +static void merge_image_input_colors(uint8_t* inused, const Gif_Image* srci) { + int i, x, y, nleft = Gif_ImageColorBound(srci); + for (i = 0; i != 256; ++i) + inused[i] = 0; + for (y = 0; y != srci->height && nleft > 0; ++y) { + const uint8_t* data = srci->img[y]; + for (x = 0; x != srci->width; ++x, ++data) { + nleft -= 1 - inused[*data]; + inused[*data] = 1; + } + } + if (srci->transparent >= 0) + inused[srci->transparent] = 0; +} + + +Gif_Image * +merge_image(Gif_Stream *dest, Gif_Stream *src, Gif_Image *srci, + Gt_Frame* srcfr, int same_compressed_ok) +{ + Gif_Colormap *imagecm; + int imagecm_ncol; + int i; + Gif_Colormap *localcm = 0; + Gif_Colormap *destcm = dest->global; + + uint8_t map[256]; /* map[input pixel value] == output pixval */ + int trivial_map; /* does the map take input pixval --> the same + pixel value for all colors in the image? */ + uint8_t inused[256]; /* inused[input pival] == 1 iff used */ + uint8_t used[256]; /* used[output pixval K] == 1 iff K was used + in the image */ + + + Gif_Image *desti; + + /* mark colors that were actually used in this image */ + imagecm = srci->local ? srci->local : src->global; + imagecm_ncol = imagecm ? imagecm->ncol : 0; + merge_image_input_colors(inused, srci); + for (i = imagecm_ncol; i != 256; ++i) + if (inused[i]) { + lwarning(srcfr->input_filename, "some colors undefined by colormap"); + break; + } + + /* map[old_pixel_value] == new_pixel_value */ + for (i = 0; i < 256; i++) + map[i] = used[i] = 0; + + /* Merge the colormap */ + if (merge_colormap_if_possible(dest->global, imagecm)) { + /* Create 'map' and 'used' for global colormap. */ + for (i = 0; i != imagecm_ncol; ++i) + if (inused[i]) + map[i] = imagecm->col[i].pixel; + + } else { + /* Need a local colormap. */ + destcm = localcm = Gif_NewFullColormap(0, 256); + for (i = 0; i != imagecm_ncol; ++i) + if (inused[i]) { + map[i] = localcm->ncol; + localcm->col[localcm->ncol] = imagecm->col[i]; + ++localcm->ncol; + } + } + + trivial_map = 1; + for (i = 0; i != 256; ++i) + if (inused[i]) { + used[map[i]] = 1; + trivial_map = trivial_map && map[i] == i; + } + + /* Decide on a transparent index */ + if (srci->transparent >= 0) { + int found_transparent = -1; + + /* try to keep the map trivial -- prefer same transparent index */ + if (trivial_map && !used[srci->transparent]) + found_transparent = srci->transparent; + else + for (i = destcm->ncol - 1; i >= 0; i--) + if (!used[i]) + found_transparent = i; + + /* 1.Aug.1999 - Allow for the case that the transparent index is bigger + than the number of colors we've created thus far. */ + if (found_transparent < 0 || found_transparent >= destcm->ncol) { + Gif_Color *c; + found_transparent = destcm->ncol; + /* 1.Aug.1999 - Don't update destcm->ncol -- we want the output colormap + to be as small as possible. */ + c = &destcm->col[found_transparent]; + if (imagecm && srci->transparent < imagecm->ncol) + *c = imagecm->col[srci->transparent]; + c->haspixel = 2; + assert(c->haspixel == 2 && found_transparent < 256); + } + + map[srci->transparent] = found_transparent; + if (srci->transparent != found_transparent) trivial_map = 0; + } + + assert(destcm->ncol <= 256); + /* Make the new image. */ + desti = Gif_NewImage(); + + desti->identifier = Gif_CopyString(srci->identifier); + if (srci->transparent > -1) + desti->transparent = map[srci->transparent]; + desti->delay = srci->delay; + desti->disposal = srci->disposal; + desti->left = srci->left; + desti->top = srci->top; + desti->interlace = srci->interlace; + + desti->width = srci->width; + desti->height = srci->height; + desti->local = localcm; + + if (trivial_map && same_compressed_ok && srci->compressed + && !srci->compressed_errors) { + desti->compressed_len = srci->compressed_len; + desti->compressed = Gif_NewArray(uint8_t, srci->compressed_len); + desti->free_compressed = Gif_Free; + memcpy(desti->compressed, srci->compressed, srci->compressed_len); + } else { + int i, j; + Gif_CreateUncompressedImage(desti, desti->interlace); + + if (trivial_map) + for (j = 0; j < desti->height; j++) + memcpy(desti->img[j], srci->img[j], desti->width); + + else + for (j = 0; j < desti->height; j++) { + uint8_t *srcdata = srci->img[j]; + uint8_t *destdata = desti->img[j]; + for (i = 0; i < desti->width; i++, srcdata++, destdata++) + *destdata = map[*srcdata]; + } + } + + /* comments and extensions */ + if (srci->comment) { + desti->comment = Gif_NewComment(); + merge_comments(desti->comment, srci->comment); + } + if (srci->extension_list && !srcfr->no_extensions) { + Gif_Extension* gfex; + for (gfex = srci->extension_list; gfex; gfex = gfex->next) + if (gfex->kind != 255 || !srcfr->no_app_extensions) + Gif_AddExtension(dest, desti, Gif_CopyExtension(gfex)); + } + while (srcfr->extensions) { + Gif_Extension* next = srcfr->extensions->next; + Gif_AddExtension(dest, desti, srcfr->extensions); + srcfr->extensions = next; + } + + Gif_AddImage(dest, desti); + return desti; +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/optimize.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/optimize.c new file mode 100644 index 0000000..e153cdb --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/optimize.c @@ -0,0 +1,467 @@ +/* optimize.c - Functions to optimize animated GIFs. + Copyright (C) 1997-2021 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#include +#include "gifsicle.h" +#include "kcolor.h" +#include +#include + +typedef int32_t penalty_type; + +typedef struct { + int left; + int top; + int width; + int height; +} Gif_OptBounds; + +typedef struct { + uint16_t left; + uint16_t top; + uint16_t width; + uint16_t height; + uint32_t size; + uint8_t disposal; + int transparent; + uint8_t *needed_colors; + unsigned required_color_count; + int32_t active_penalty; + int32_t global_penalty; + int32_t colormap_penalty; + Gif_Image *new_gfi; +} Gif_OptData; + +/* Screen width and height */ +static int screen_width; +static int screen_height; + +/* Colormap containing all colors in the image. May have >256 colors */ +static Gif_Colormap *all_colormap; +/* Histogram so we can find colors quickly */ +static kchist all_colormap_hist; + +/* The old global colormap, or a fake one we created if necessary */ +static Gif_Colormap *in_global_map; + +/* The new global colormap */ +static Gif_Colormap *out_global_map; + +#define TRANSP (0) +#define NOT_IN_OUT_GLOBAL (256) +static unsigned background; +static int image_index; + +static penalty_type *permuting_sort_values; + +#define REQUIRED 2 +#define REPLACE_TRANSP 1 + + +/***** + * SIMPLE HELPERS + * new and delete optimize data; and colormap_combine; and sorting permutations + **/ + +Gif_OptData * +new_opt_data(void) +{ + Gif_OptData *od = Gif_New(Gif_OptData); + od->needed_colors = 0; + od->global_penalty = 1; + return od; +} + +void +delete_opt_data(Gif_OptData *od) +{ + if (!od) return; + Gif_DeleteArray(od->needed_colors); + Gif_Delete(od); +} + + +/* all_colormap_add: Ensure that each color in 'src' is represented in + 'all_colormap'. For each color 'i' in 'src', src->col[i].pixel == some j + so that GIF_COLOREQ(&src->col[i], &all_colormap->col[j]). + all_colormap->col[0] is reserved for transparency; no source color will + be mapped to it. */ + +static void all_colormap_add(const Gif_Colormap* src) { + int i; + + /* expand dst->col if necessary. This might change dst->col */ + if (all_colormap->ncol + src->ncol >= all_colormap->capacity) { + all_colormap->capacity *= 2; + Gif_ReArray(all_colormap->col, Gif_Color, all_colormap->capacity); + } + + for (i = 0; i < src->ncol; ++i) { + kchistitem* khi = kchist_add(&all_colormap_hist, + kc_makegfcng(&src->col[i]), 0); + if (!khi->count) { + all_colormap->col[all_colormap->ncol] = src->col[i]; + all_colormap->col[all_colormap->ncol].pixel = 0; + khi->count = all_colormap->ncol; + ++all_colormap->ncol; + } + src->col[i].pixel = khi->count; + } +} + + +/***** + * MANIPULATING IMAGE AREAS + **/ + +static Gif_OptBounds +safe_bounds(Gif_Image *area) +{ + /* Returns bounds constrained to lie within the screen. */ + Gif_OptBounds b; + b.left = constrain(0, area->left, screen_width); + b.top = constrain(0, area->top, screen_height); + b.width = constrain(0, area->left + area->width, screen_width) - b.left; + b.height = constrain(0, area->top + area->height, screen_height) - b.top; + return b; +} + + +/***** + * FIND THE SMALLEST BOUNDING RECTANGLE ENCLOSING ALL CHANGES + **/ + +/* fix_difference_bounds: make sure the image isn't 0x0. */ + +static void +fix_difference_bounds(Gif_OptData *bounds) +{ + if (bounds->width == 0 || bounds->height == 0) { + bounds->top = 0; + bounds->left = 0; + bounds->width = 1; + bounds->height = 1; + } + /* assert that image lies completely within screen */ + assert(bounds->top < screen_height && bounds->left < screen_width + && bounds->top + bounds->height <= screen_height + && bounds->left + bounds->width <= screen_width); +} + + +/***** + * CALCULATE OUTPUT GLOBAL COLORMAP + **/ + +static void +increment_penalties(Gif_OptData *opt, penalty_type *penalty, int32_t delta) +{ + int i; + int all_ncol = all_colormap->ncol; + uint8_t *need = opt->needed_colors; + for (i = 1; i < all_ncol; i++) + if (need[i] == REQUIRED) + penalty[i] += delta; +} + + +/***** + * CREATE COLOR MAPPING FOR A PARTICULAR IMAGE + **/ + +/* sort_colormap_permutation_rgb: for canonicalizing local colormaps by + arranging them in RGB order */ + +static int +colormap_rgb_permutation_sorter(const void *v1, const void *v2) +{ + const Gif_Color *col1 = (const Gif_Color *)v1; + const Gif_Color *col2 = (const Gif_Color *)v2; + int value1 = (col1->gfc_red << 16) | (col1->gfc_green << 8) | col1->gfc_blue; + int value2 = (col2->gfc_red << 16) | (col2->gfc_green << 8) | col2->gfc_blue; + return value1 - value2; +} + + +/* prepare_colormap_map: Create and return an array of bytes mapping from + global pixel values to pixel values for this image. It may add colormap + cells to 'into'; if there isn't enough room in 'into', it will return 0. It + sets the 'transparent' field of 'gfi->optdata', but otherwise doesn't + change or read it at all. */ + +static uint8_t * +prepare_colormap_map(Gif_Image *gfi, Gif_Colormap *into, uint8_t *need) +{ + int i; + int is_global = (into == out_global_map); + + int all_ncol = all_colormap->ncol; + Gif_Color *all_col = all_colormap->col; + + int ncol = into->ncol; + Gif_Color *col = into->col; + + uint8_t *map = Gif_NewArray(uint8_t, all_ncol); + uint8_t into_used[256]; + + /* keep track of which pixel indices in 'into' have been used; initially, + all unused */ + for (i = 0; i < 256; i++) + into_used[i] = 0; + + /* go over all non-transparent global pixels which MUST appear + (need[P]==REQUIRED) and place them in 'into' */ + for (i = 1; i < all_ncol; i++) { + int val; + if (need[i] != REQUIRED) + continue; + + /* fail if a needed pixel isn't in the global map */ + if (is_global) { + val = all_col[i].pixel; + if (val >= ncol) + goto error; + } else { + /* always place colors in a local colormap */ + if (ncol == 256) + goto error; + val = ncol; + col[val] = all_col[i]; + col[val].pixel = i; + ncol++; + } + + map[i] = val; + into_used[val] = 1; + } + + if (!is_global) { + qsort(col, ncol, sizeof(Gif_Color), colormap_rgb_permutation_sorter); + for (i = 0; i < ncol; ++i) + map[col[i].pixel] = i; + } + + /* now check for transparency */ + gfi->transparent = -1; + if (need[TRANSP]) { + int transparent = -1; + + /* first, look for an unused index in 'into'. Pick the lowest one: the + lower transparent index we get, the more likely we can shave a bit off + min_code_bits later, thus saving space */ + for (i = 0; i < ncol; i++) + if (!into_used[i]) { + transparent = i; + break; + } + + /* otherwise, [1.Aug.1999] use a fake slot for the purely transparent + color. Don't actually enter the transparent color into the colormap -- + we might be able to output a smaller colormap! If there's no room for + it, give up */ + if (transparent < 0) { + if (ncol < 256) { + transparent = ncol; + /* 1.Aug.1999 - don't increase ncol */ + col[ncol] = all_col[TRANSP]; + } else + goto error; + } + + /* change mapping */ + map[TRANSP] = transparent; + for (i = 1; i < all_ncol; i++) + if (need[i] == REPLACE_TRANSP) + map[i] = transparent; + + gfi->transparent = transparent; + } + + /* If we get here, it worked! Commit state changes (the number of color + cells in 'into') and return the map. */ + into->ncol = ncol; + return map; + + error: + /* If we get here, it failed! Return 0 and don't change global state. */ + Gif_DeleteArray(map); + return 0; +} + + +/* prepare_colormap: make a colormap up from the image data by fitting any + used colors into a colormap. Returns a map from global color index to index + in this image's colormap. May set a local colormap on 'gfi'. */ + +static uint8_t * +prepare_colormap(Gif_Image *gfi, uint8_t *need) +{ + uint8_t *map; + + /* try to map pixel values into the global colormap */ + Gif_DeleteColormap(gfi->local); + gfi->local = 0; + map = prepare_colormap_map(gfi, out_global_map, need); + + if (!map) { + /* that didn't work; add a local colormap. */ + gfi->local = Gif_NewFullColormap(0, 256); + map = prepare_colormap_map(gfi, gfi->local, need); + } + + return map; +} + + +/***** + * INITIALIZATION AND FINALIZATION + **/ + +static int +initialize_optimizer(Gif_Stream *gfs) +{ + int i; + + if (gfs->nimages < 1) + return 0; + + /* combine colormaps */ + all_colormap = Gif_NewFullColormap(1, 384); + all_colormap->col[0].gfc_red = 255; + all_colormap->col[0].gfc_green = 255; + all_colormap->col[0].gfc_blue = 255; + + in_global_map = gfs->global; + if (!in_global_map) { + Gif_Color *col; + in_global_map = Gif_NewFullColormap(256, 256); + col = in_global_map->col; + for (i = 0; i < 256; i++, col++) + col->gfc_red = col->gfc_green = col->gfc_blue = i; + } + + { + int any_globals = 0; + int first_transparent = -1; + + kchist_init(&all_colormap_hist); + for (i = 0; i < gfs->nimages; i++) { + Gif_Image *gfi = gfs->images[i]; + if (gfi->local) + all_colormap_add(gfi->local); + else + any_globals = 1; + if (gfi->transparent >= 0 && first_transparent < 0) + first_transparent = i; + } + if (any_globals) + all_colormap_add(in_global_map); + kchist_cleanup(&all_colormap_hist); + + /* try and maintain transparency's pixel value */ + if (first_transparent >= 0) { + Gif_Image *gfi = gfs->images[first_transparent]; + Gif_Colormap *gfcm = gfi->local ? gfi->local : gfs->global; + all_colormap->col[TRANSP] = gfcm->col[gfi->transparent]; + } + } + + /* find screen_width and screen_height, and clip all images to screen */ + Gif_CalculateScreenSize(gfs, 0); + screen_width = gfs->screen_width; + screen_height = gfs->screen_height; + for (i = 0; i < gfs->nimages; i++) + Gif_ClipImage(gfs->images[i], 0, 0, screen_width, screen_height); + + /* choose background */ + if (gfs->images[0]->transparent < 0 + && gfs->global && gfs->background < in_global_map->ncol) + background = in_global_map->col[gfs->background].pixel; + else + background = TRANSP; + + return 1; +} + +static void +finalize_optimizer(Gif_Stream *gfs, int optimize_flags) +{ + int i; + + if (background == TRANSP) + gfs->background = (uint8_t)gfs->images[0]->transparent; + + /* 11.Mar.2010 - remove entirely transparent frames. */ + for (i = 1; i < gfs->nimages && !(optimize_flags & GT_OPT_KEEPEMPTY); ++i) { + Gif_Image *gfi = gfs->images[i]; + if (gfi->width == 1 && gfi->height == 1 && gfi->transparent >= 0 + && !gfi->identifier && !gfi->comment + && (gfi->disposal == GIF_DISPOSAL_ASIS + || gfi->disposal == GIF_DISPOSAL_NONE + || gfi->disposal == GIF_DISPOSAL_PREVIOUS) + && gfi->delay && gfs->images[i-1]->delay) { + Gif_UncompressImage(gfs, gfi); + if (gfi->img[0][0] == gfi->transparent + && (gfs->images[i-1]->disposal == GIF_DISPOSAL_ASIS + || gfs->images[i-1]->disposal == GIF_DISPOSAL_NONE)) { + gfs->images[i-1]->delay += gfi->delay; + Gif_DeleteImage(gfi); + memmove(&gfs->images[i], &gfs->images[i+1], sizeof(Gif_Image *) * (gfs->nimages - i - 1)); + --gfs->nimages; + --i; + } + } + } + + /* 10.Dec.1998 - prefer GIF_DISPOSAL_NONE to GIF_DISPOSAL_ASIS. This is + semantically "wrong" -- it's better to set the disposal explicitly than + rely on default behavior -- but will result in smaller GIF files, since + the graphic control extension can be left off in many cases. */ + for (i = 0; i < gfs->nimages; i++) + if (gfs->images[i]->disposal == GIF_DISPOSAL_ASIS + && gfs->images[i]->delay == 0 + && gfs->images[i]->transparent < 0) + gfs->images[i]->disposal = GIF_DISPOSAL_NONE; + + Gif_DeleteColormap(in_global_map); + Gif_DeleteColormap(all_colormap); +} + + +/* two versions of the optimization template */ +#define palindex_type uint16_t +#define X(t) t ## 16 +#include "opttemplate.c" +#undef palindex_type +#undef X + +#define palindex_type uint32_t +#define X(t) t ## 32 +#include "opttemplate.c" + +/* the interface function! */ + +void +optimize_fragments(Gif_Stream *gfs, int optimize_flags, int huge_stream) +{ + if (!initialize_optimizer(gfs)) + return; + if ((unsigned) all_colormap->ncol >= 0xFFFF) { + create_subimages32(gfs, optimize_flags, !huge_stream); + create_out_global_map32(gfs); + create_new_image_data32(gfs, optimize_flags); + finalize_optimizer_data32(); + } else { + create_subimages16(gfs, optimize_flags, !huge_stream); + create_out_global_map16(gfs); + create_new_image_data16(gfs, optimize_flags); + finalize_optimizer_data16(); + } + finalize_optimizer(gfs, optimize_flags); +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/opttemplate.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/opttemplate.c new file mode 100644 index 0000000..736f7e1 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/opttemplate.c @@ -0,0 +1,960 @@ +/* opttemplate.c - Functions to optimize animated GIFs. + Copyright (C) 1997-2021 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +static palindex_type *X(last_data); +static palindex_type *X(this_data); +static palindex_type *X(next_data); + + +/* sort_permutation: sorts a given permutation 'perm' according to the + corresponding values in 'values'. Thus, in the output, the sequence + '[ values[perm[i]] | i <- 0..size-1 ]' will be monotonic, either up or + (if is_down != 0) down. */ + +/* 9.Dec.1998 - Dumb idiot, it's time you stopped using C. The optimizer was + broken because I switched to uint32_t's for the sorting values without + considering the consequences; and the consequences were bad. */ + +static int +X(permuting_sorter_up)(const void *v1, const void *v2) { + const palindex_type *n1 = (const palindex_type *)v1; + const palindex_type *n2 = (const palindex_type *)v2; + if (permuting_sort_values[*n1] < permuting_sort_values[*n2]) + return -1; + else if (permuting_sort_values[*n1] == permuting_sort_values[*n2]) + return 0; + else + return 1; +} + +static int +X(permuting_sorter_down)(const void *v1, const void *v2) { + const palindex_type *n1 = (const palindex_type *)v1; + const palindex_type *n2 = (const palindex_type *)v2; + if (permuting_sort_values[*n1] > permuting_sort_values[*n2]) + return -1; + else if (permuting_sort_values[*n1] == permuting_sort_values[*n2]) + return 0; + else + return 1; +} + +static palindex_type* +X(sort_permutation)(palindex_type *perm, int size, + penalty_type *values, int is_down) { + permuting_sort_values = values; + if (is_down) + qsort(perm, size, sizeof(palindex_type), X(permuting_sorter_down)); + else + qsort(perm, size, sizeof(palindex_type), X(permuting_sorter_up)); + permuting_sort_values = 0; + return perm; +} + + +/***** + * MANIPULATING IMAGE AREAS + **/ + +static void +X(copy_data_area)(palindex_type *dst, palindex_type *src, Gif_Image *area) +{ + Gif_OptBounds ob; + int y; + if (!area) + return; + ob = safe_bounds(area); + dst += ob.top * (unsigned) screen_width + ob.left; + src += ob.top * (unsigned) screen_width + ob.left; + for (y = 0; y < ob.height; y++) { + memcpy(dst, src, sizeof(palindex_type) * ob.width); + dst += screen_width; + src += screen_width; + } +} + +static void +X(copy_data_area_subimage)(palindex_type *dst, palindex_type *src, + Gif_OptData *area) +{ + Gif_Image img; + img.left = area->left; + img.top = area->top; + img.width = area->width; + img.height = area->height; + X(copy_data_area)(dst, src, &img); +} + +static void +X(erase_data_area)(palindex_type *dst, Gif_Image *area) +{ + int x, y; + Gif_OptBounds ob = safe_bounds(area); + dst += ob.top * (unsigned) screen_width + ob.left; + for (y = 0; y < ob.height; y++) { + for (x = 0; x < ob.width; x++) + dst[x] = TRANSP; + dst += screen_width; + } +} + +static void +X(erase_data_area_subimage)(palindex_type *dst, Gif_OptData *area) +{ + Gif_Image img; + img.left = area->left; + img.top = area->top; + img.width = area->width; + img.height = area->height; + X(erase_data_area)(dst, &img); +} + +static void +X(erase_screen)(palindex_type *dst) +{ + uint32_t i; + uint32_t screen_size = (unsigned) screen_width * (unsigned) screen_height; + for (i = 0; i < screen_size; i++) + *dst++ = TRANSP; +} + +/***** + * APPLY A GIF FRAME OR DISPOSAL TO AN IMAGE DESTINATION + **/ + +static void +X(apply_frame)(palindex_type *dst, Gif_Stream* gfs, Gif_Image* gfi, + int replace, int save_uncompressed) +{ + int i, y, was_compressed = 0; + palindex_type map[256]; + Gif_Colormap *colormap = gfi->local ? gfi->local : in_global_map; + Gif_OptBounds ob = safe_bounds(gfi); + + if (!gfi->img) { + was_compressed = 1; + Gif_UncompressImage(gfs, gfi); + } + + /* make sure transparency maps to TRANSP */ + for (i = 0; i < colormap->ncol; i++) + map[i] = colormap->col[i].pixel; + /* out-of-bounds colors map to 0, for the sake of argument */ + y = colormap->ncol ? colormap->col[0].pixel : 0; + for (i = colormap->ncol; i < 256; i++) + map[i] = y; + if (gfi->transparent >= 0 && gfi->transparent < 256) + map[gfi->transparent] = TRANSP; + else + replace = 1; + + /* map the image */ + dst += ob.left + ob.top * (unsigned) screen_width; + for (y = 0; y < ob.height; y++) { + uint8_t *gfi_pointer = gfi->img[y]; + int x; + + if (replace) + for (x = 0; x < ob.width; x++) + dst[x] = map[gfi_pointer[x]]; + else + for (x = 0; x < ob.width; x++) { + palindex_type new_pixel = map[gfi_pointer[x]]; + if (new_pixel != TRANSP) + dst[x] = new_pixel; + } + + dst += screen_width; + } + + if (was_compressed && !save_uncompressed) + Gif_ReleaseUncompressedImage(gfi); +} + +static void +X(apply_frame_disposal)(palindex_type *into_data, palindex_type *from_data, + palindex_type *previous_data, Gif_Image *gfi) +{ + unsigned screen_size = (unsigned) screen_width * (unsigned) screen_height; + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) + memcpy(into_data, previous_data, sizeof(palindex_type) * screen_size); + else { + memcpy(into_data, from_data, sizeof(palindex_type) * screen_size); + if (gfi->disposal == GIF_DISPOSAL_BACKGROUND) + X(erase_data_area)(into_data, gfi); + } +} + + +/***** + * FIND THE SMALLEST BOUNDING RECTANGLE ENCLOSING ALL CHANGES + **/ + +/* find_difference_bounds: Find the smallest rectangular area containing all + the changes and store it in 'bounds'. */ + +static void +X(find_difference_bounds)(Gif_OptData *bounds, Gif_Image *gfi, Gif_Image *last) +{ + int lf, rt, lf_min, rt_max, tp, bt, x, y; + + /* 1.Aug.99 - use current bounds if possible, since this function is a speed + bottleneck */ + if (!last + || last->disposal == GIF_DISPOSAL_NONE + || last->disposal == GIF_DISPOSAL_ASIS) { + Gif_OptBounds ob = safe_bounds(gfi); + lf_min = ob.left; + rt_max = ob.left + ob.width - 1; + tp = ob.top; + bt = ob.top + ob.height - 1; + } else { + lf_min = 0; + rt_max = screen_width - 1; + tp = 0; + bt = screen_height - 1; + } + + for (; tp < screen_height; tp++) + if (memcmp(X(last_data) + (unsigned) screen_width * tp, + X(this_data) + (unsigned) screen_width * tp, + screen_width * sizeof(palindex_type)) != 0) + break; + for (; bt >= tp; bt--) + if (memcmp(X(last_data) + (unsigned) screen_width * bt, + X(this_data) + (unsigned) screen_width * bt, + screen_width * sizeof(palindex_type)) != 0) + break; + + lf = screen_width; + rt = 0; + for (y = tp; y <= bt; y++) { + palindex_type *ld = X(last_data) + (unsigned) screen_width * y; + palindex_type *td = X(this_data) + (unsigned) screen_width * y; + for (x = lf_min; x < lf; x++) + if (ld[x] != td[x]) + break; + lf = x; + + for (x = rt_max; x > rt; x--) + if (ld[x] != td[x]) + break; + rt = x; + } + + /* 19.Aug.1999 - handle case when there's no difference between frames */ + if (tp > bt) { + tp = bt = constrain(0, gfi->top, screen_height - 1); + lf = rt = constrain(0, gfi->left, screen_width - 1); + } + + bounds->left = lf; + bounds->top = tp; + bounds->width = rt + 1 - lf; + bounds->height = bt + 1 - tp; +} + + +/* expand_difference_bounds: If the current image has background disposal and + the background is transparent, we must expand the difference bounds to + include any blanked (newly transparent) pixels that are still transparent + in the next image. This function does that by comparing this_data and + next_data. The new bounds are passed and stored in 'bounds'; the image's + old bounds, which are also the maximum bounds, are passed in + 'this_bounds'. */ + +static int +X(expand_difference_bounds)(Gif_OptData *bounds, Gif_Image *this_bounds) +{ + int x, y, expanded = 0; + Gif_OptBounds ob = safe_bounds(this_bounds); + + if (bounds->width <= 0 || bounds->height <= 0) { + bounds->left = bounds->top = 0; + bounds->width = screen_width; + bounds->height = screen_height; + } + + /* 20.Nov.2013 - The image `bounds` might be larger than `this_bounds` + because of a previous frame's background disposal. Don't accidentally + shrink `this_bounds`. */ + if (ob.left > bounds->left) { + ob.width = (ob.left + ob.width) - bounds->left; + ob.left = bounds->left; + } + if (ob.top > bounds->top) { + ob.height = (ob.top + ob.height) - bounds->top; + ob.top = bounds->top; + } + if (ob.left + ob.width < bounds->left + bounds->width) + ob.width = bounds->left + bounds->width - ob.left; + if (ob.top + ob.height < bounds->top + bounds->height) + ob.height = bounds->top + bounds->height - ob.top; + + for (; ob.top < bounds->top; ++ob.top, --ob.height) { + palindex_type *now = X(this_data) + (unsigned) screen_width * ob.top; + palindex_type *next = X(next_data) + (unsigned) screen_width * ob.top; + for (x = ob.left; x < ob.left + ob.width; ++x) + if (now[x] != TRANSP && next[x] == TRANSP) { + expanded = 1; + goto found_top; + } + } + + found_top: + for (; ob.top + ob.height > bounds->top + bounds->height; --ob.height) { + palindex_type *now = X(this_data) + (unsigned) screen_width * (ob.top + ob.height - 1); + palindex_type *next = X(next_data) + (unsigned) screen_width * (ob.top + ob.height - 1); + for (x = ob.left; x < ob.left + ob.width; ++x) + if (now[x] != TRANSP && next[x] == TRANSP) { + expanded = 1; + goto found_bottom; + } + } + + found_bottom: + for (; ob.left < bounds->left; ++ob.left, --ob.width) { + palindex_type *now = X(this_data) + ob.left; + palindex_type *next = X(next_data) + ob.left; + for (y = ob.top; y < ob.top + ob.height; ++y) + if (now[y * (unsigned) screen_width] != TRANSP + && next[y * (unsigned) screen_width] == TRANSP) { + expanded = 1; + goto found_left; + } + } + + found_left: + for (; ob.left + ob.width > bounds->left + bounds->width; --ob.width) { + palindex_type *now = X(this_data) + ob.left + ob.width - 1; + palindex_type *next = X(next_data) + ob.left + ob.width - 1; + for (y = ob.top; y < ob.top + ob.height; ++y) + if (now[y * (unsigned) screen_width] != TRANSP + && next[y * (unsigned) screen_width] == TRANSP) { + expanded = 1; + goto found_right; + } + } + + found_right: + if (!expanded) + for (y = ob.top; y < ob.top + ob.height; ++y) { + palindex_type *now = X(this_data) + y * (unsigned) screen_width; + palindex_type *next = X(next_data) + y * (unsigned) screen_width; + for (x = ob.left; x < ob.left + ob.width; ++x) + if (now[x] != TRANSP && next[x] == TRANSP) { + expanded = 1; + break; + } + } + + bounds->left = ob.left; + bounds->top = ob.top; + bounds->width = ob.width; + bounds->height = ob.height; + return expanded; +} + + +/***** + * DETERMINE WHICH COLORS ARE USED + **/ + +/* get_used_colors: mark which colors are needed by a given image. Returns a + need array so that need[j] == REQUIRED if the output colormap must + include all_color j; REPLACE_TRANSP if it should be replaced by + transparency; and 0 if it's not in the image at all. + + If use_transparency > 0, then a pixel which was the same in the last frame + may be replaced with transparency. If use_transparency == 2, transparency + MUST be set. (This happens on the first image if the background should be + transparent.) */ + +static void +X(get_used_colors)(Gif_OptData *bounds, int use_transparency) +{ + int top = bounds->top, width = bounds->width, height = bounds->height; + int i, x, y; + int all_ncol = all_colormap->ncol; + uint8_t *need = Gif_NewArray(uint8_t, all_ncol); + + for (i = 0; i < all_ncol; i++) + need[i] = 0; + + /* set elements that are in the image. need == 2 means the color + must be in the map; need == 1 means the color may be replaced by + transparency. */ + for (y = top; y < top + height; y++) { + palindex_type *data = X(this_data) + (unsigned) screen_width * y + bounds->left; + palindex_type *last = X(last_data) + (unsigned) screen_width * y + bounds->left; + for (x = 0; x < width; x++) { + if (data[x] != last[x]) + need[data[x]] = REQUIRED; + else if (need[data[x]] == 0) + need[data[x]] = REPLACE_TRANSP; + } + } + if (need[TRANSP]) + need[TRANSP] = REQUIRED; + + /* check for too many colors; also force transparency if needed */ + { + int count[3]; + /* Count distinct pixels in each category */ + count[0] = count[1] = count[2] = 0; + for (i = 0; i < all_ncol; i++) + count[need[i]]++; + /* If use_transparency is large and there's room, add transparency */ + if (use_transparency > 1 && !need[TRANSP] && count[REQUIRED] < 256) { + need[TRANSP] = REQUIRED; + count[REQUIRED]++; + } + /* If too many "potentially transparent" pixels, force transparency */ + if (count[REPLACE_TRANSP] + count[REQUIRED] > 256) + use_transparency = 1; + /* Make sure transparency is marked necessary if we use it */ + if (count[REPLACE_TRANSP] > 0 && use_transparency && !need[TRANSP]) { + need[TRANSP] = REQUIRED; + count[REQUIRED]++; + } + /* If not using transparency, change "potentially transparent" pixels to + "actually used" pixels */ + if (!use_transparency) { + for (i = 0; i < all_ncol; i++) + if (need[i] == REPLACE_TRANSP) + need[i] = REQUIRED; + count[REQUIRED] += count[REPLACE_TRANSP]; + } + /* If we can afford to have transparency, and we want to use it, then + include it */ + if (count[REQUIRED] < 256 && use_transparency && !need[TRANSP]) { + need[TRANSP] = REQUIRED; + count[REQUIRED]++; + } + bounds->required_color_count = count[REQUIRED]; + } + + bounds->needed_colors = need; +} + + +/***** + * FIND SUBIMAGES AND COLORS USED + **/ + +static void +X(create_subimages)(Gif_Stream *gfs, int optimize_flags, int save_uncompressed) +{ + unsigned screen_size; + Gif_Image *last_gfi; + int next_data_valid; + palindex_type *previous_data = 0; + int local_color_tables = 0; + + screen_size = (unsigned) screen_width * (unsigned) screen_height; + + X(last_data) = Gif_NewArray(palindex_type, screen_size); + X(this_data) = Gif_NewArray(palindex_type, screen_size); + X(next_data) = Gif_NewArray(palindex_type, screen_size); + next_data_valid = 0; + + /* do first image. Remember to uncompress it if necessary */ + X(erase_screen)(X(last_data)); + X(erase_screen)(X(this_data)); + last_gfi = 0; + + /* PRECONDITION: + previous_data -- garbage + last_data -- optimized image after disposal of previous optimized frame + this_data -- input image after disposal of previous input frame + next_data -- input image after application of current input frame, + if next_image_valid */ + for (image_index = 0; image_index < gfs->nimages; image_index++) { + Gif_Image *gfi = gfs->images[image_index]; + Gif_OptData *subimage = new_opt_data(); + if (gfi->local) + local_color_tables = 1; + + /* save previous data if necessary */ + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS + || (local_color_tables && image_index > 0 + && last_gfi->disposal > GIF_DISPOSAL_ASIS)) { + if (!previous_data) + previous_data = Gif_NewArray(palindex_type, screen_size); + memcpy(previous_data, X(this_data), + sizeof(palindex_type) * screen_size); + } + + /* set this_data equal to the current image */ + if (next_data_valid) { + palindex_type *temp = X(this_data); + X(this_data) = X(next_data); + X(next_data) = temp; + next_data_valid = 0; + } else + X(apply_frame)(X(this_data), gfs, gfi, 0, save_uncompressed); + + retry_frame: + /* find minimum area of difference between this image and last image */ + subimage->disposal = GIF_DISPOSAL_ASIS; + if (image_index > 0) { + X(find_difference_bounds)(subimage, gfi, last_gfi); + } else { + Gif_OptBounds ob = safe_bounds(gfi); + subimage->left = ob.left; + subimage->top = ob.top; + subimage->width = ob.width; + subimage->height = ob.height; + } + + /* might need to expand difference border on background disposal */ + if ((gfi->disposal == GIF_DISPOSAL_BACKGROUND + || gfi->disposal == GIF_DISPOSAL_PREVIOUS) + && image_index < gfs->nimages - 1) { + /* set up next_data */ + Gif_Image *next_gfi = gfs->images[image_index + 1]; + X(apply_frame_disposal)(X(next_data), X(this_data), previous_data, gfi); + X(apply_frame)(X(next_data), gfs, next_gfi, 0, save_uncompressed); + next_data_valid = 1; + /* expand border as necessary */ + if (X(expand_difference_bounds)(subimage, gfi)) + subimage->disposal = GIF_DISPOSAL_BACKGROUND; + } + + fix_difference_bounds(subimage); + + /* set map of used colors */ + { + int use_transparency = (optimize_flags & GT_OPT_MASK) > 1 + && image_index > 0; + if (image_index == 0 && background == TRANSP) + use_transparency = 2; + X(get_used_colors)(subimage, use_transparency); + /* Gifsicle's optimization strategy normally creates frames with ASIS + or BACKGROUND disposal (not PREVIOUS disposal). However, there are + cases when PREVIOUS disposal is strictly required, or a frame would + require more than 256 colors. Detect this case and try to recover. */ + if (subimage->required_color_count > 256) { + if (image_index > 0 && local_color_tables) { + Gif_OptData *subimage = (Gif_OptData*) last_gfi->user_data; + if ((last_gfi->disposal == GIF_DISPOSAL_PREVIOUS + || last_gfi->disposal == GIF_DISPOSAL_BACKGROUND) + && subimage->disposal != last_gfi->disposal) { + subimage->disposal = last_gfi->disposal; + memcpy(X(last_data), previous_data, sizeof(palindex_type) * screen_size); + goto retry_frame; + } + } + fatal_error("%d colors required in a frame (256 is max)", + subimage->required_color_count); + } + } + + gfi->user_data = subimage; + last_gfi = gfi; + + /* Apply optimized disposal to last_data and unoptimized disposal to + this_data. Before 9.Dec.1998 I applied unoptimized disposal uniformly + to both. This led to subtle bugs. After all, to determine bounds, we + want to compare the current image (only obtainable through unoptimized + disposal) with what WILL be left after the previous OPTIMIZED image's + disposal. This fix is repeated in create_new_image_data */ + if (subimage->disposal == GIF_DISPOSAL_BACKGROUND) + X(erase_data_area_subimage)(X(last_data), subimage); + else + X(copy_data_area_subimage)(X(last_data), X(this_data), subimage); + + if (last_gfi->disposal == GIF_DISPOSAL_BACKGROUND) + X(erase_data_area)(X(this_data), last_gfi); + else if (last_gfi->disposal == GIF_DISPOSAL_PREVIOUS) { + palindex_type *temp = previous_data; + previous_data = X(this_data); + X(this_data) = temp; + } + } + + Gif_DeleteArray(X(next_data)); + if (previous_data) + Gif_DeleteArray(previous_data); +} + + +/***** + * CALCULATE OUTPUT GLOBAL COLORMAP + **/ + +/* create_out_global_map: The interface function to this pass. It creates + out_global_map and sets pixel values on all_colormap appropriately. + Specifically: + + all_colormap->col[P].pixel >= 256 ==> P is not in the global colormap. + + Otherwise, all_colormap->col[P].pixel == the J so that + GIF_COLOREQ(&all_colormap->col[P], &out_global_map->col[J]). + + On return, the 'colormap_penalty' component of an image's Gif_OptData + structure is <0 iff that image will need a local colormap. + + 20.Aug.1999 - updated to new version that arranges the entire colormap, not + just the stuff above 256 colors. */ + +static void +X(create_out_global_map)(Gif_Stream *gfs) +{ + int all_ncol = all_colormap->ncol; + penalty_type *penalty = Gif_NewArray(penalty_type, all_ncol); + palindex_type *permute = Gif_NewArray(palindex_type, all_ncol); + palindex_type *ordering = Gif_NewArray(palindex_type, all_ncol); + int cur_ncol, i, imagei; + int nglobal_all = (all_ncol <= 257 ? all_ncol - 1 : 256); + int permutation_changed; + assert(all_ncol <= 0x7FFFFFFF); + + /* initial permutation is null */ + for (i = 0; i < all_ncol - 1; i++) + permute[i] = i + 1; + + /* choose appropriate penalties for each image */ + for (imagei = 0; imagei < gfs->nimages; imagei++) { + Gif_OptData *opt = (Gif_OptData *)gfs->images[imagei]->user_data; + palindex_type pi; + opt->global_penalty = opt->colormap_penalty = 1; + for (pi = 2; pi < opt->required_color_count; pi *= 2) + opt->colormap_penalty *= 3; + opt->active_penalty = + (all_ncol > 257 ? opt->colormap_penalty : opt->global_penalty); + } + + /* set initial penalties for each color */ + for (i = 1; i < all_ncol; i++) + penalty[i] = 0; + for (imagei = 0; imagei < gfs->nimages; imagei++) { + Gif_OptData *opt = (Gif_OptData *)gfs->images[imagei]->user_data; + increment_penalties(opt, penalty, opt->active_penalty); + } + permutation_changed = 1; + + /* Loop, removing one color at a time. */ + for (cur_ncol = all_ncol - 1; cur_ncol; cur_ncol--) { + palindex_type removed; + + /* sort permutation based on penalty */ + if (permutation_changed) + X(sort_permutation)(permute, cur_ncol, penalty, 1); + permutation_changed = 0; + + /* update reverse permutation */ + removed = permute[cur_ncol - 1]; + ordering[removed] = cur_ncol - 1; + + /* decrement penalties for colors that are out of the running */ + for (imagei = 0; imagei < gfs->nimages; imagei++) { + Gif_OptData *opt = (Gif_OptData *)gfs->images[imagei]->user_data; + uint8_t *need = opt->needed_colors; + if (opt->global_penalty > 0 && need[removed] == REQUIRED) { + increment_penalties(opt, penalty, -opt->active_penalty); + opt->global_penalty = 0; + opt->colormap_penalty = (cur_ncol > 256 ? -1 : 0); + permutation_changed = 1; + } + } + + /* change colormap penalties if we're no longer working w/globalmap */ + if (cur_ncol == 257) { + for (i = 0; i < all_ncol; i++) + penalty[i] = 0; + for (imagei = 0; imagei < gfs->nimages; imagei++) { + Gif_OptData *opt = (Gif_OptData *)gfs->images[imagei]->user_data; + opt->active_penalty = opt->global_penalty; + increment_penalties(opt, penalty, opt->global_penalty); + } + permutation_changed = 1; + } + } + + /* make sure background is in the global colormap */ + if (background != TRANSP && ordering[background] >= 256) { + palindex_type other = permute[255]; + ordering[other] = ordering[background]; + ordering[background] = 255; + } + + /* assign out_global_map based on permutation */ + out_global_map = Gif_NewFullColormap(nglobal_all, 256); + + for (i = 1; i < all_ncol; i++) + if (ordering[i] < 256) { + out_global_map->col[ordering[i]] = all_colormap->col[i]; + all_colormap->col[i].pixel = ordering[i]; + } else + all_colormap->col[i].pixel = NOT_IN_OUT_GLOBAL; + + /* set the stream's background color */ + if (background != TRANSP) + gfs->background = ordering[background]; + + /* cleanup */ + Gif_DeleteArray(penalty); + Gif_DeleteArray(permute); + Gif_DeleteArray(ordering); +} + + +/***** + * CREATE OUTPUT FRAME DATA + **/ + +/* simple_frame_data: just copy the data from the image into the frame data. + No funkiness, no transparency, nothing */ + +static void +X(simple_frame_data)(Gif_Image *gfi, uint8_t *map) +{ + Gif_OptBounds ob = safe_bounds(gfi); + int x, y; + unsigned scan_width = gfi->width; + + for (y = 0; y < ob.height; y++) { + palindex_type *from = X(this_data) + (unsigned) screen_width * (y + ob.top) + ob.left; + uint8_t *into = gfi->image_data + y * scan_width; + for (x = 0; x < ob.width; x++) + *into++ = map[*from++]; + } +} + + +/* transp_frame_data: copy the frame data into the actual image, using + transparency occasionally according to a heuristic described below */ + +static void +X(transp_frame_data)(Gif_Stream *gfs, Gif_Image *gfi, uint8_t *map, + int optimize_flags, Gif_CompressInfo *gcinfo) +{ + Gif_OptBounds ob = safe_bounds(gfi); + int x, y, transparent = gfi->transparent; + palindex_type *last = 0; + palindex_type *cur = 0; + uint8_t *data, *begin_same; + uint8_t *t2_data = 0, *last_for_t2; + int nsame; + + /* First, try w/o transparency. Compare this to the result using + transparency and pick the better of the two. */ + X(simple_frame_data)(gfi, map); + Gif_FullCompressImage(gfs, gfi, gcinfo); + gcinfo->flags |= GIF_WRITE_SHRINK; + + /* Actually copy data to frame. + + Use transparency if possible to shrink the size of the written GIF. + + The written GIF will be small if patterns (sequences of pixel values) + recur in the image. + We could conceivably use transparency to produce THE OPTIMAL image, + with the most recurring patterns of the best kinds; but this would + be very hard (wouldn't it?). Instead, we settle for a heuristic: + we try and create RUNS. (Since we *try* to create them, they will + presumably recur!) A RUN is a series of adjacent pixels all with the + same value. + + By & large, we just use the regular image's values. However, we might + create a transparent run *not in* the regular image, if TWO OR MORE + adjacent runs OF DIFFERENT COLORS *could* be made transparent. + + (An area can be made transparent if the corresponding area in the previous + frame had the same colors as the area does now.) + + Why? If only one run (say of color C) could be transparent, we get no + large immediate advantage from making it transparent (it'll be a run of + the same length regardless). Also, we might LOSE: what if the run was + adjacent to some more of color C, which couldn't be made transparent? If + we use color C (instead of the transparent color), then we get a longer + run. + + This simple heuristic does a little better than Gifwizard's (6/97) + on some images, but does *worse than nothing at all* on others. + + However, it DOES do better than the complicated, greedy algorithm that + preceded it; and now we pick either the transparency-optimized version or + the normal version, whichever compresses smaller, for the best of both + worlds. (9/98) + + On several images, making SINGLE color runs transparent wins over the + previous heuristic, so try both at optimize level 3 or above (the cost is + ~30%). (2/11) */ + + data = begin_same = last_for_t2 = gfi->image_data; + nsame = 0; + + for (y = 0; y < ob.height; ++y) { + last = X(last_data) + (unsigned) screen_width * (y + ob.top) + ob.left; + cur = X(this_data) + (unsigned) screen_width * (y + ob.top) + ob.left; + for (x = 0; x < ob.width; ++x) { + if (*cur != *last && map[*cur] != transparent) { + if (nsame == 1 && data[-1] != transparent + && (optimize_flags & GT_OPT_MASK) > 2) { + if (!t2_data) + t2_data = Gif_NewArray(uint8_t, (size_t) ob.width * (size_t) ob.height); + memcpy(t2_data + (last_for_t2 - gfi->image_data), + last_for_t2, begin_same - last_for_t2); + memset(t2_data + (begin_same - gfi->image_data), + transparent, data - begin_same); + last_for_t2 = data; + } + nsame = 0; + } else if (nsame == 0) { + begin_same = data; + ++nsame; + } else if (nsame == 1 && map[*cur] != data[-1]) { + memset(begin_same, transparent, data - begin_same); + ++nsame; + } + if (nsame > 1) + *data = transparent; + else + *data = map[*cur]; + ++data, ++cur, ++last; + } + } + + if (t2_data) + memcpy(t2_data + (last_for_t2 - gfi->image_data), + last_for_t2, data - last_for_t2); + + + /* Now, try compressed transparent version(s) and pick the better of the + two (or three). */ + Gif_FullCompressImage(gfs, gfi, gcinfo); + if (t2_data) { + Gif_SetUncompressedImage(gfi, t2_data, Gif_Free, 0); + Gif_FullCompressImage(gfs, gfi, gcinfo); + } + Gif_ReleaseUncompressedImage(gfi); + + gcinfo->flags &= ~GIF_WRITE_SHRINK; +} + + +/***** + * CREATE NEW IMAGE DATA + **/ + +/* last == what last image ended up looking like + this == what new image should look like + + last = apply O1 + dispose O1 + ... + apply On-1 + dispose On-1 + this = apply U1 + dispose U1 + ... + apply Un-1 + dispose Un-1 + apply Un + + invariant: apply O1 + dispose O1 + ... + apply Ok + === apply U1 + dispose U1 + ... + apply Uk */ + +static void +X(create_new_image_data)(Gif_Stream *gfs, int optimize_flags) +{ + Gif_Image cur_unopt_gfi; /* placeholder; maintains pre-optimization + image size so we can apply background + disposal */ + unsigned screen_size = (unsigned) screen_width * (unsigned) screen_height; + palindex_type *previous_data = 0; + Gif_CompressInfo gcinfo = gif_write_info; + if ((optimize_flags & GT_OPT_MASK) >= 3) + gcinfo.flags |= GIF_WRITE_OPTIMIZE; + + gfs->global = out_global_map; + + /* do first image. Remember to uncompress it if necessary */ + X(erase_screen)(X(last_data)); + X(erase_screen)(X(this_data)); + + for (image_index = 0; image_index < gfs->nimages; image_index++) { + Gif_Image *cur_gfi = gfs->images[image_index]; + Gif_OptData *opt = (Gif_OptData *)cur_gfi->user_data; + int was_compressed = (cur_gfi->img == 0); + + /* save previous data if necessary */ + if (cur_gfi->disposal == GIF_DISPOSAL_PREVIOUS) { + if (!previous_data) + previous_data = Gif_NewArray(palindex_type, screen_size); + X(copy_data_area)(previous_data, X(this_data), cur_gfi); + } + + /* set up this_data to be equal to the current image */ + X(apply_frame)(X(this_data), gfs, cur_gfi, 0, 0); + + /* save actual bounds and disposal from unoptimized version so we can + apply the disposal correctly next time through */ + cur_unopt_gfi = *cur_gfi; + + /* set bounds and disposal from optdata */ + Gif_ReleaseUncompressedImage(cur_gfi); + cur_gfi->left = opt->left; + cur_gfi->top = opt->top; + cur_gfi->width = opt->width; + cur_gfi->height = opt->height; + cur_gfi->disposal = opt->disposal; + if (image_index > 0) + cur_gfi->interlace = 0; + + /* find the new image's colormap and then make new data */ + { + uint8_t *map = prepare_colormap(cur_gfi, opt->needed_colors); + uint8_t *data = Gif_NewArray(uint8_t, (size_t) cur_gfi->width * (size_t) cur_gfi->height); + Gif_SetUncompressedImage(cur_gfi, data, Gif_Free, 0); + + /* don't use transparency on first frame */ + if ((optimize_flags & GT_OPT_MASK) > 1 && image_index > 0 + && cur_gfi->transparent >= 0) + X(transp_frame_data)(gfs, cur_gfi, map, optimize_flags, &gcinfo); + else + X(simple_frame_data)(cur_gfi, map); + + if (cur_gfi->img) { + if (was_compressed || (optimize_flags & GT_OPT_MASK) > 1) { + Gif_FullCompressImage(gfs, cur_gfi, &gcinfo); + Gif_ReleaseUncompressedImage(cur_gfi); + } else /* bug fix 22.May.2001 */ + Gif_ReleaseCompressedImage(cur_gfi); + } + + Gif_DeleteArray(map); + } + + delete_opt_data(opt); + cur_gfi->user_data = 0; + + /* Set up last_data and this_data. last_data must contain this_data + new + disposal. this_data must contain this_data + old disposal. */ + if (cur_gfi->disposal == GIF_DISPOSAL_NONE + || cur_gfi->disposal == GIF_DISPOSAL_ASIS) + X(copy_data_area)(X(last_data), X(this_data), cur_gfi); + else if (cur_gfi->disposal == GIF_DISPOSAL_BACKGROUND) + X(erase_data_area)(X(last_data), cur_gfi); + else if (cur_gfi->disposal != GIF_DISPOSAL_PREVIOUS) + assert(0 && "optimized frame has strange disposal"); + + if (cur_unopt_gfi.disposal == GIF_DISPOSAL_BACKGROUND) + X(erase_data_area)(X(this_data), &cur_unopt_gfi); + else if (cur_unopt_gfi.disposal == GIF_DISPOSAL_PREVIOUS) + X(copy_data_area)(X(this_data), previous_data, &cur_unopt_gfi); + } + + if (previous_data) + Gif_DeleteArray(previous_data); +} + + +/***** + * INITIALIZATION AND FINALIZATION + **/ + +static void +X(finalize_optimizer_data)(void) { + Gif_DeleteArray(X(last_data)); + Gif_DeleteArray(X(this_data)); +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/quantize.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/quantize.c new file mode 100644 index 0000000..b3bff1a --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/quantize.c @@ -0,0 +1,1477 @@ +/* quantize.c - Histograms and quantization for gifsicle. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#include +#include "gifsicle.h" +#include "kcolor.h" +#include +#include +#include +#include +#include +#include + + +/* COLORMAP FUNCTIONS return a palette (a vector of Gif_Colors). The + pixel fields are undefined; the haspixel fields are all 0. */ + +typedef struct { + int first; + int size; + uint32_t pixel; +} adaptive_slot; + +Gif_Colormap* colormap_median_cut(kchist* kch, Gt_OutputData* od) +{ + int adapt_size = od->colormap_size; + adaptive_slot *slots = Gif_NewArray(adaptive_slot, adapt_size); + Gif_Colormap *gfcm = Gif_NewFullColormap(adapt_size, 256); + Gif_Color *adapt = gfcm->col; + int nadapt; + int i, j, k; + + /* This code was written with reference to ppmquant by Jef Poskanzer, + part of the pbmplus package. */ + + if (adapt_size < 2 || adapt_size > 256) + fatal_error("adaptive palette size must be between 2 and 256"); + if (adapt_size >= kch->n && !od->colormap_fixed) + warning(1, "trivial adaptive palette (only %d %s in source)", + kch->n, kch->n == 1 ? "color" : "colors"); + if (adapt_size >= kch->n) + adapt_size = kch->n; + + /* 0. remove any transparent color from consideration; reduce adaptive + palette size to accommodate transparency if it looks like that'll be + necessary */ + if (adapt_size > 2 && adapt_size < kch->n && kch->n <= 265 + && od->colormap_needs_transparency) + adapt_size--; + + /* 1. set up the first slot, containing all pixels. */ + slots[0].first = 0; + slots[0].size = kch->n; + slots[0].pixel = 0; + for (i = 0; i < kch->n; i++) + slots[0].pixel += kch->h[i].count; + + /* 2. split slots until we have enough. */ + for (nadapt = 1; nadapt < adapt_size; nadapt++) { + adaptive_slot *split = 0; + kcolor minc, maxc; + kchistitem *slice; + + /* 2.1. pick the slot to split. */ + { + uint32_t split_pixel = 0; + for (i = 0; i < nadapt; i++) + if (slots[i].size >= 2 && slots[i].pixel > split_pixel) { + split = &slots[i]; + split_pixel = slots[i].pixel; + } + if (!split) + break; + } + slice = &kch->h[split->first]; + + /* 2.2. find its extent. */ + { + kchistitem *trav = slice; + minc = maxc = trav->ka.k; + for (i = 1, trav++; i < split->size; i++, trav++) + for (k = 0; k != 3; ++k) { + minc.a[k] = min(minc.a[k], trav->ka.a[k]); + maxc.a[k] = max(maxc.a[k], trav->ka.a[k]); + } + } + + /* 2.3. decide how to split it. use the luminance method. also sort + the colors. */ + { + double red_diff = 0.299 * (maxc.a[0] - minc.a[0]); + double green_diff = 0.587 * (maxc.a[1] - minc.a[1]); + double blue_diff = 0.114 * (maxc.a[2] - minc.a[2]); + if (red_diff >= green_diff && red_diff >= blue_diff) + qsort(slice, split->size, sizeof(kchistitem), kchistitem_compare_red); + else if (green_diff >= blue_diff) + qsort(slice, split->size, sizeof(kchistitem), kchistitem_compare_green); + else + qsort(slice, split->size, sizeof(kchistitem), kchistitem_compare_blue); + } + + /* 2.4. decide where to split the slot and split it there. */ + { + uint32_t half_pixels = split->pixel / 2; + uint32_t pixel_accum = slice[0].count; + uint32_t diff1, diff2; + for (i = 1; i < split->size - 1 && pixel_accum < half_pixels; i++) + pixel_accum += slice[i].count; + + /* We know the area before the split has more pixels than the + area after, possibly by a large margin (bad news). If it + would shrink the margin, change the split. */ + diff1 = 2*pixel_accum - split->pixel; + diff2 = split->pixel - 2*(pixel_accum - slice[i-1].count); + if (diff2 < diff1 && i > 1) { + i--; + pixel_accum -= slice[i].count; + } + + slots[nadapt].first = split->first + i; + slots[nadapt].size = split->size - i; + slots[nadapt].pixel = split->pixel - pixel_accum; + split->size = i; + split->pixel = pixel_accum; + } + } + + /* 3. make the new palette by choosing one color from each slot. */ + for (i = 0; i < nadapt; i++) { + double px[3]; + kchistitem* slice = &kch->h[ slots[i].first ]; + kcolor kc; + px[0] = px[1] = px[2] = 0; + for (j = 0; j != slots[i].size; ++j) + for (k = 0; k != 3; ++k) + px[k] += slice[j].ka.a[k] * (double) slice[j].count; + kc.a[0] = (int) (px[0] / slots[i].pixel); + kc.a[1] = (int) (px[1] / slots[i].pixel); + kc.a[2] = (int) (px[2] / slots[i].pixel); + adapt[i] = kc_togfcg(kc); + } + + Gif_DeleteArray(slots); + gfcm->ncol = nadapt; + return gfcm; +} + + +void kcdiversity_init(kcdiversity* div, kchist* kch, int dodither) { + int i; + div->kch = kch; + qsort(kch->h, kch->n, sizeof(kchistitem), kchistitem_compare_popularity); + div->closest = Gif_NewArray(int, kch->n); + div->min_dist = Gif_NewArray(uint32_t, kch->n); + for (i = 0; i != kch->n; ++i) + div->min_dist[i] = (uint32_t) -1; + if (dodither) { + div->min_dither_dist = Gif_NewArray(uint32_t, kch->n); + for (i = 0; i != kch->n; ++i) + div->min_dither_dist[i] = (uint32_t) -1; + } else + div->min_dither_dist = NULL; + div->chosen = Gif_NewArray(int, kch->n); + div->nchosen = 0; +} + +void kcdiversity_cleanup(kcdiversity* div) { + Gif_DeleteArray(div->closest); + Gif_DeleteArray(div->min_dist); + Gif_DeleteArray(div->min_dither_dist); + Gif_DeleteArray(div->chosen); +} + +int kcdiversity_find_popular(kcdiversity* div) { + int i, n = div->kch->n; + for (i = 0; i != n && div->min_dist[i] == 0; ++i) + /* spin */; + return i; +} + +int kcdiversity_find_diverse(kcdiversity* div, double ditherweight) { + int i, n = div->kch->n, chosen = kcdiversity_find_popular(div); + if (chosen == n) + /* skip */; + else if (!ditherweight || !div->min_dither_dist) { + for (i = chosen + 1; i != n; ++i) + if (div->min_dist[i] > div->min_dist[chosen]) + chosen = i; + } else { + double max_dist = div->min_dist[chosen] + ditherweight * div->min_dither_dist[chosen]; + for (i = chosen + 1; i != n; ++i) + if (div->min_dist[i] != 0) { + double dist = div->min_dist[i] + ditherweight * div->min_dither_dist[i]; + if (dist > max_dist) { + chosen = i; + max_dist = dist; + } + } + } + return chosen; +} + +int kcdiversity_choose(kcdiversity* div, int chosen, int dodither) { + int i, j, k, n = div->kch->n; + kchistitem* hist = div->kch->h; + + div->min_dist[chosen] = 0; + if (div->min_dither_dist) + div->min_dither_dist[chosen] = 0; + div->closest[chosen] = chosen; + + /* adjust the min_dist array */ + for (i = 0; i != n; ++i) + if (div->min_dist[i]) { + uint32_t dist = kc_distance(hist[i].ka.k, hist[chosen].ka.k); + if (dist < div->min_dist[i]) { + div->min_dist[i] = dist; + div->closest[i] = chosen; + } + } + + /* also account for dither distances */ + if (dodither && div->min_dither_dist) + for (i = 0; i != div->nchosen; ++i) { + kcolor x = hist[chosen].ka.k, *y = &hist[div->chosen[i]].ka.k; + /* penalize combinations with large luminance difference */ + double dL = abs(kc_luminance(x) - kc_luminance(*y)); + dL = (dL > 8192 ? dL * 4 / 32767. : 1); + /* create combination */ + for (k = 0; k != 3; ++k) + x.a[k] = (x.a[k] + y->a[k]) >> 1; + /* track closeness of combination to other colors */ + for (j = 0; j != n; ++j) + if (div->min_dist[j]) { + double dist = kc_distance(hist[j].ka.k, x) * dL; + if (dist < div->min_dither_dist[j]) + div->min_dither_dist[j] = (uint32_t) dist; + } + } + + div->chosen[div->nchosen] = chosen; + ++div->nchosen; + return chosen; +} + +static void colormap_diversity_do_blend(kcdiversity* div) { + int i, j, k, n = div->kch->n; + kchistitem* hist = div->kch->h; + int* chosenmap = Gif_NewArray(int, n); + scale_color* di = Gif_NewArray(scale_color, div->nchosen); + for (i = 0; i != div->nchosen; ++i) + for (k = 0; k != 4; ++k) + di[i].a[k] = 0; + for (i = 0; i != div->nchosen; ++i) + chosenmap[div->chosen[i]] = i; + for (i = 0; i != n; ++i) { + double count = hist[i].count; + if (div->closest[i] == i) + count *= 3; + j = chosenmap[div->closest[i]]; + for (k = 0; k != 3; ++k) + di[j].a[k] += hist[i].ka.a[k] * count; + di[j].a[3] += count; + } + for (i = 0; i != div->nchosen; ++i) { + int match = div->chosen[i]; + if (di[i].a[3] >= 5 * hist[match].count) + for (k = 0; k != 3; ++k) + hist[match].ka.a[k] = (int) (di[i].a[k] / di[i].a[3]); + } + Gif_DeleteArray(chosenmap); + Gif_DeleteArray(di); +} + + +static Gif_Colormap * +colormap_diversity(kchist* kch, Gt_OutputData* od, int blend) +{ + int adapt_size = od->colormap_size; + kcdiversity div; + Gif_Colormap* gfcm = Gif_NewFullColormap(adapt_size, 256); + int nadapt = 0; + int chosen; + + /* This code uses XV's modified diversity algorithm, and was written + with reference to XV's implementation of that algorithm by John Bradley + and Tom Lane . */ + + if (adapt_size < 2 || adapt_size > 256) + fatal_error("adaptive palette size must be between 2 and 256"); + if (adapt_size > kch->n && !od->colormap_fixed) + warning(1, "trivial adaptive palette (only %d colors in source)", kch->n); + if (adapt_size > kch->n) + adapt_size = kch->n; + + /* 0. remove any transparent color from consideration; reduce adaptive + palette size to accommodate transparency if it looks like that'll be + necessary */ + /* It will be necessary to accommodate transparency if (1) there is + transparency in the image; (2) the adaptive palette isn't trivial; and + (3) there are a small number of colors in the image (arbitrary constant: + <= 265), so it's likely that most images will use most of the slots, so + it's likely there won't be unused slots. */ + if (adapt_size > 2 && adapt_size < kch->n && kch->n <= 265 + && od->colormap_needs_transparency) + adapt_size--; + + /* blending has bad effects when there are very few colors */ + if (adapt_size < 4) + blend = 0; + + /* 1. initialize min_dist and sort the colors in order of popularity. */ + kcdiversity_init(&div, kch, od->dither_type != dither_none); + + /* 2. choose colors one at a time */ + for (nadapt = 0; nadapt < adapt_size; nadapt++) { + /* 2.1. choose the color to be added */ + if (nadapt == 0 || (nadapt >= 10 && nadapt % 2 == 0)) + /* 2.1a. want most popular unchosen color */ + chosen = kcdiversity_find_popular(&div); + else if (od->dither_type == dither_none) + /* 2.1b. choose based on diversity from unchosen colors */ + chosen = kcdiversity_find_diverse(&div, 0); + else { + /* 2.1c. choose based on diversity from unchosen colors, but allow + dithered combinations to stand in for colors, particularly early + on in the color finding process */ + /* Weight assigned to dithered combinations drops as we proceed. */ +#if HAVE_POW + double ditherweight = 0.05 + pow(0.25, 1 + (nadapt - 1) / 3.); +#else + double ditherweight = nadapt < 4 ? 0.25 : 0.125; +#endif + chosen = kcdiversity_find_diverse(&div, ditherweight); + } + + kcdiversity_choose(&div, chosen, + od->dither_type != dither_none + && nadapt > 0 && nadapt < 64); + } + + /* 3. make the new palette by choosing one color from each slot. */ + if (blend) + colormap_diversity_do_blend(&div); + + for (nadapt = 0; nadapt != div.nchosen; ++nadapt) + gfcm->col[nadapt] = kc_togfcg(kch->h[div.chosen[nadapt]].ka.k); + gfcm->ncol = nadapt; + + kcdiversity_cleanup(&div); + return gfcm; +} + + +Gif_Colormap* colormap_blend_diversity(kchist* kch, Gt_OutputData* od) { + return colormap_diversity(kch, od, 1); +} + +Gif_Colormap* colormap_flat_diversity(kchist* kch, Gt_OutputData* od) { + return colormap_diversity(kch, od, 0); +} + + +void +colormap_image_posterize(Gif_Image *gfi, uint8_t *new_data, + Gif_Colormap *old_cm, kd3_tree* kd3, + uint32_t *histogram) +{ + int ncol = old_cm->ncol; + Gif_Color *col = old_cm->col; + int map[256]; + int i, j; + int transparent = gfi->transparent; + + /* find closest colors in new colormap */ + assert(old_cm->capacity >= 256); + for (i = 0; i < ncol; ++i) { + map[i] = col[i].pixel = kd3_closest8g(kd3, col[i].gfc_red, col[i].gfc_green, col[i].gfc_blue); + col[i].haspixel = 1; + } + for (i = ncol; i < 256; ++i) { + map[i] = col[i].pixel = 0; + col[i].haspixel = 1; + } + + /* map image */ + for (j = 0; j < gfi->height; j++) { + uint8_t *data = gfi->img[j]; + for (i = 0; i < gfi->width; i++, data++, new_data++) + if (*data != transparent) { + *new_data = map[*data]; + histogram[*new_data]++; + } + } +} + + +#define DITHER_SCALE 1024 +#define DITHER_SHIFT 10 +#define DITHER_SCALE_M1 (DITHER_SCALE-1) +#define DITHER_ITEM2ERR (1<<(DITHER_SHIFT-7)) +#define N_RANDOM_VALUES 512 + +void +colormap_image_floyd_steinberg(Gif_Image *gfi, uint8_t *all_new_data, + Gif_Colormap *old_cm, kd3_tree* kd3, + uint32_t *histogram) +{ + static int32_t *random_values = 0; + + int width = gfi->width; + int dither_direction = 0; + int transparent = gfi->transparent; + int i, j, k; + kcolor *old_kc; + wkcolor *err, *err1; + + /* Initialize distances; beware uninitialized colors */ + assert(old_cm->capacity >= 256); + old_kc = Gif_NewArray(kcolor, old_cm->capacity); + for (i = 0; i < old_cm->ncol; ++i) { + Gif_Color* c = &old_cm->col[i]; + old_kc[i] = kd3_makegfcg(kd3, c); + c->pixel = kd3_closest_transformed(kd3, old_kc[i], NULL); + c->haspixel = 1; + } + for (i = old_cm->ncol; i < 256; ++i) { + Gif_Color* c = &old_cm->col[i]; + old_kc[i] = kd3_makegfcg(kd3, c); + c->pixel = 0; + c->haspixel = 1; + } + + /* This code was written with reference to ppmquant by Jef Poskanzer, part + of the pbmplus package. */ + + /* Initialize Floyd-Steinberg error vectors to small random values, so we + don't get artifacts on the top row */ + err = Gif_NewArray(wkcolor, width + 2); + err1 = Gif_NewArray(wkcolor, width + 2); + /* Use the same random values on each call in an attempt to minimize + "jumping dithering" effects on animations */ + if (!random_values) { + random_values = Gif_NewArray(int32_t, N_RANDOM_VALUES); + for (i = 0; i < N_RANDOM_VALUES; i++) + random_values[i] = RANDOM() % (DITHER_SCALE_M1 * 2) - DITHER_SCALE_M1; + } + for (i = 0; i < gfi->width + 2; i++) { + j = (i + gfi->left) * 3; + for (k = 0; k < 3; ++k) + err[i].a[k] = random_values[ (j + k) % N_RANDOM_VALUES ]; + } + /* err1 initialized below */ + + kd3_build_xradius(kd3); + + /* Do the image! */ + for (j = 0; j < gfi->height; j++) { + int d0, d1, d2, d3; /* used for error diffusion */ + uint8_t *data, *new_data; + int x; + + if (dither_direction) { + x = width - 1; + d0 = 0, d1 = 2, d2 = 1, d3 = 0; + } else { + x = 0; + d0 = 2, d1 = 0, d2 = 1, d3 = 2; + } + data = &gfi->img[j][x]; + new_data = all_new_data + j * (unsigned) width + x; + + for (i = 0; i < width + 2; i++) + err1[i].a[0] = err1[i].a[1] = err1[i].a[2] = 0; + + /* Do a single row */ + while (x >= 0 && x < width) { + int e; + kcolor use; + + /* the transparent color never gets adjusted */ + if (*data == transparent) + goto next; + + /* find desired new color */ + use = old_kc[*data]; + + /* use Floyd-Steinberg errors to adjust */ + for (k = 0; k < 3; ++k) { + int v = use.a[k] + + (err[x+1].a[k] & ~(DITHER_ITEM2ERR-1)) / DITHER_ITEM2ERR; + use.a[k] = KC_CLAMPV(v); + } + + e = old_cm->col[*data].pixel; + if (kc_distance(kd3->ks[e], use) < kd3->xradius[e]) + *new_data = e; + else + *new_data = kd3_closest_transformed(kd3, use, NULL); + histogram[*new_data]++; + + /* calculate and propagate the error between desired and selected color. + Assume that, with a large scale (1024), we don't need to worry about + image artifacts caused by error accumulation (the fact that the + error terms might not sum to the error). */ + for (k = 0; k < 3; ++k) { + e = (use.a[k] - kd3->ks[*new_data].a[k]) * DITHER_ITEM2ERR; + if (e) { + err [x+d0].a[k] += ((e * 7) & ~15) / 16; + err1[x+d1].a[k] += ((e * 3) & ~15) / 16; + err1[x+d2].a[k] += ((e * 5) & ~15) / 16; + err1[x+d3].a[k] += ( e & ~15) / 16; + } + } + + next: + if (dither_direction) + x--, data--, new_data--; + else + x++, data++, new_data++; + } + /* Did a single row */ + + /* change dithering directions */ + { + wkcolor *temp = err1; + err1 = err; + err = temp; + dither_direction = !dither_direction; + } + } + + /* delete temporary storage */ + Gif_DeleteArray(err); + Gif_DeleteArray(err1); + Gif_DeleteArray(old_kc); +} + +/* This function is a variant of the colormap_image_floyd_steinberg function + since the Atkinson dither is a variant of the Floyd-Steinberg dither. */ +void +colormap_image_atkinson(Gif_Image *gfi, uint8_t *all_new_data, + Gif_Colormap *old_cm, kd3_tree* kd3, + uint32_t *histogram) +{ + static int32_t *random_values = 0; + int transparent = gfi->transparent; + int i, j, k; + kcolor *old_kc; + wkcolor *err[3]; + + /* Initialize distances; beware uninitialized colors */ + assert(old_cm->capacity >= 256); + old_kc = Gif_NewArray(kcolor, old_cm->capacity); + for (i = 0; i < old_cm->ncol; ++i) { + Gif_Color* c = &old_cm->col[i]; + old_kc[i] = kd3_makegfcg(kd3, c); + c->pixel = kd3_closest_transformed(kd3, old_kc[i], NULL); + c->haspixel = 1; + } + for (i = old_cm->ncol; i < 256; ++i) { + Gif_Color* c = &old_cm->col[i]; + old_kc[i] = kd3_makegfcg(kd3, c); + c->pixel = 0; + c->haspixel = 1; + } + + /* Initialize Atkinson error vectors */ + for (i = 0; i < 3; i++) + err[i] = Gif_NewArray(wkcolor, gfi->width + 2); + + /* Use the same random values on each call in an attempt to minimize + "jumping dithering" effects on animations */ + if (!random_values) { + random_values = Gif_NewArray(int32_t, N_RANDOM_VALUES); + for (i = 0; i < N_RANDOM_VALUES; i++) + random_values[i] = RANDOM() % (DITHER_SCALE_M1 * 2) - DITHER_SCALE_M1; + } + + for (i = 0; i < gfi->width + 2; i++) { + j = (i + gfi->left) * 3; + for (k = 0; k < 3; ++k) + err[0][i].a[k] = random_values[ (j + k) % N_RANDOM_VALUES ]; + } + + kd3_build_xradius(kd3); + + /* Do the image! */ + for (j = 0; j < gfi->height; j++) { + uint8_t *data = &gfi->img[j][0]; + uint8_t *new_data = all_new_data + j * (unsigned) gfi->width; + + /* Initialize error rows for this pass */ + for (i = 0; i < gfi->width + 2; i++) + for (k = 0; k < 3; k++) + err[1][i].a[k] = err[2][i].a[k] = 0; + + for (int x = 0; x < gfi->width; x++, data++, new_data++) { + int e; + kcolor use; + + /* Transparent color never gets adjusted, skip */ + if (*data == transparent) { + continue; + } + + /* Calculate desired new color including current error */ + use = old_kc[*data]; + + for (k = 0; k < 3; ++k) + use.a[k] = KC_CLAMPV(use.a[k] + err[0][x].a[k] / DITHER_SCALE); + + /* Find the closest color in the colormap */ + e = old_cm->col[*data].pixel; + if (kc_distance(kd3->ks[e], use) < kd3->xradius[e]) + *new_data = e; + else + *new_data = kd3_closest_transformed(kd3, use, NULL); + histogram[*new_data]++; + + /* Calculate and propagate the error using the Atkinson dithering + algorithm */ + for (k = 0; k < 3; ++k) { + int e = (use.a[k] - kd3->ks[*new_data].a[k]) * DITHER_SCALE; + if (x + 1 < gfi->width) err[0][x + 1].a[k] += e / 8; + if (x + 2 < gfi->width) err[0][x + 2].a[k] += e / 8; + if (j + 1 < gfi->height) { + err[1][x].a[k] += e / 8; + err[1][x + 1].a[k] += e / 8; + if (x + 1 < gfi->width) err[1][x + 1].a[k] += e / 8; + } + if (j + 2 < gfi->height) + err[2][x].a[k] += e / 8; + } + } + + /* Rotate error buffers */ + wkcolor *temp = err[0]; + err[0] = err[1]; + err[1] = err[2]; + err[2] = temp; + } + + /* Delete temporary storage */ + for (i = 0; i < 3; i++) { + Gif_DeleteArray(err[i]); + } + Gif_DeleteArray(old_kc); +} + +typedef struct odselect_planitem { + uint8_t plan; + uint16_t frac; +} odselect_planitem; + +static int* ordered_dither_lum; + +static void plan_from_cplan(uint8_t* plan, int nplan, + const odselect_planitem* cp, int ncp, int whole) { + int i, cfrac_subt = 0, planpos = 0, end_planpos; + for (i = 0; i != ncp; ++i) { + cfrac_subt += cp[i].frac; + end_planpos = cfrac_subt * nplan / whole; + while (planpos != end_planpos) + plan[planpos++] = cp[i].plan; + } + assert(planpos == nplan); +} + +static int ordered_dither_plan_compare(const void* xa, const void* xb) { + const uint8_t* a = (const uint8_t*) xa; + const uint8_t* b = (const uint8_t*) xb; + if (ordered_dither_lum[*a] != ordered_dither_lum[*b]) + return ordered_dither_lum[*a] - ordered_dither_lum[*b]; + else + return *a - *b; +} + +static int kc_line_closest(kcolor p0, kcolor p1, kcolor ref, + double* t, unsigned* dist) { + wkcolor p01, p0ref; + kcolor online; + unsigned den; + int d; + for (d = 0; d != 3; ++d) { + p01.a[d] = p1.a[d] - p0.a[d]; + p0ref.a[d] = ref.a[d] - p0.a[d]; + } + den = (unsigned) + (p01.a[0]*p01.a[0] + p01.a[1]*p01.a[1] + p01.a[2]*p01.a[2]); + if (den == 0) + return 0; + /* NB: We've run out of bits of precision. We can calculate the + denominator in unsigned arithmetic, but the numerator might + be negative, or it might be so large that it is unsigned. + Calculate the numerator as a double. */ + *t = ((double) p01.a[0]*p0ref.a[0] + p01.a[1]*p0ref.a[1] + + p01.a[2]*p0ref.a[2]) / den; + if (*t < 0 || *t > 1) + return 0; + for (d = 0; d != 3; ++d) { + int v = (int) (p01.a[d] * *t) + p0.a[d]; + online.a[d] = KC_CLAMPV(v); + } + *dist = kc_distance(online, ref); + return 1; +} + +static int kc_plane_closest(kcolor p0, kcolor p1, kcolor p2, kcolor ref, + double* t, unsigned* dist) { + wkcolor p0ref, p01, p02; + double n[3], pvec[3], det, qvec[3], u, v; + int d; + + /* Calculate the non-unit normal of the plane determined by the input + colors (p0-p2) */ + for (d = 0; d != 3; ++d) { + p0ref.a[d] = ref.a[d] - p0.a[d]; + p01.a[d] = p1.a[d] - p0.a[d]; + p02.a[d] = p2.a[d] - p0.a[d]; + } + n[0] = p01.a[1]*p02.a[2] - p01.a[2]*p02.a[1]; + n[1] = p01.a[2]*p02.a[0] - p01.a[0]*p02.a[2]; + n[2] = p01.a[0]*p02.a[1] - p01.a[1]*p02.a[0]; + + /* Moeller-Trumbore ray tracing algorithm: trace a ray from `ref` along + normal `n`; convert to barycentric coordinates to see if the ray + intersects with the triangle. */ + pvec[0] = n[1]*p02.a[2] - n[2]*p02.a[1]; + pvec[1] = n[2]*p02.a[0] - n[0]*p02.a[2]; + pvec[2] = n[0]*p02.a[1] - n[1]*p02.a[0]; + + det = pvec[0]*p01.a[0] + pvec[1]*p01.a[1] + pvec[2]*p01.a[2]; + if (fabs(det) <= 0.0001220703125) /* optimizer will take care of that */ + return 0; + det = 1 / det; + + u = (p0ref.a[0]*pvec[0] + p0ref.a[1]*pvec[1] + p0ref.a[2]*pvec[2]) * det; + if (u < 0 || u > 1) + return 0; + + qvec[0] = p0ref.a[1]*p01.a[2] - p0ref.a[2]*p01.a[1]; + qvec[1] = p0ref.a[2]*p01.a[0] - p0ref.a[0]*p01.a[2]; + qvec[2] = p0ref.a[0]*p01.a[1] - p0ref.a[1]*p01.a[0]; + + v = (n[0]*qvec[0] + n[1]*qvec[1] + n[2]*qvec[2]) * det; + if (v < 0 || v > 1 || u + v > 1) + return 0; + + /* Now we know at there is a point in the triangle that is closer to + `ref` than any point along its edges. Return the barycentric + coordinates for that point and the distance to that point. */ + t[0] = u; + t[1] = v; + v = (p02.a[0]*qvec[0] + p02.a[1]*qvec[1] + p02.a[2]*qvec[2]) * det; + *dist = (unsigned) (v * v * (n[0]*n[0] + n[1]*n[1] + n[2]*n[2]) + 0.5); + return 1; +} + +static void limit_ordered_dither_plan(uint8_t* plan, int nplan, int nc, + kcolor want, kd3_tree* kd3) { + unsigned mindist, dist; + int ncp = 0, nbestcp = 0, i, j, k; + double t[2]; + odselect_planitem cp[256], bestcp[16]; + nc = nc <= 16 ? nc : 16; + + /* sort colors */ + cp[0].plan = plan[0]; + cp[0].frac = 1; + for (ncp = i = 1; i != nplan; ++i) + if (plan[i - 1] == plan[i]) + ++cp[ncp - 1].frac; + else { + cp[ncp].plan = plan[i]; + cp[ncp].frac = 1; + ++ncp; + } + + /* calculate plan */ + mindist = (unsigned) -1; + for (i = 0; i != ncp; ++i) { + /* check for closest single color */ + dist = kc_distance(kd3->ks[cp[i].plan], want); + if (dist < mindist) { + bestcp[0].plan = cp[i].plan; + bestcp[0].frac = KC_WHOLE; + nbestcp = 1; + mindist = dist; + } + + for (j = i + 1; nc >= 2 && j < ncp; ++j) { + /* check for closest blend of two colors */ + if (kc_line_closest(kd3->ks[cp[i].plan], + kd3->ks[cp[j].plan], + want, &t[0], &dist) + && dist < mindist) { + bestcp[0].plan = cp[i].plan; + bestcp[1].plan = cp[j].plan; + bestcp[1].frac = (int) (KC_WHOLE * t[0]); + bestcp[0].frac = KC_WHOLE - bestcp[1].frac; + nbestcp = 2; + mindist = dist; + } + + for (k = j + 1; nc >= 3 && k < ncp; ++k) + /* check for closest blend of three colors */ + if (kc_plane_closest(kd3->ks[cp[i].plan], + kd3->ks[cp[j].plan], + kd3->ks[cp[k].plan], + want, &t[0], &dist) + && dist < mindist) { + bestcp[0].plan = cp[i].plan; + bestcp[1].plan = cp[j].plan; + bestcp[1].frac = (int) (KC_WHOLE * t[0]); + bestcp[2].plan = cp[k].plan; + bestcp[2].frac = (int) (KC_WHOLE * t[1]); + bestcp[0].frac = KC_WHOLE - bestcp[1].frac - bestcp[2].frac; + nbestcp = 3; + mindist = dist; + } + } + } + + plan_from_cplan(plan, nplan, bestcp, nbestcp, KC_WHOLE); +} + +static void set_ordered_dither_plan(uint8_t* plan, int nplan, int nc, + Gif_Color* gfc, kd3_tree* kd3) { + kcolor want, cur; + wkcolor err = wkc_zero(); + int i, d; + + want = kd3_makegfcg(kd3, gfc); + + for (i = 0; i != nplan; ++i) { + for (d = 0; d != 3; ++d) { + int v = want.a[d] + err.a[d]; + cur.a[d] = KC_CLAMPV(v); + } + plan[i] = kd3_closest_transformed(kd3, cur, NULL); + for (d = 0; d != 3; ++d) + err.a[d] += want.a[d] - kd3->ks[plan[i]].a[d]; + } + + qsort(plan, nplan, 1, ordered_dither_plan_compare); + + if (nc < nplan && plan[0] != plan[nplan-1]) { + int ncp = 1; + for (i = 1; i != nplan; ++i) + ncp += plan[i-1] != plan[i]; + if (ncp > nc) + limit_ordered_dither_plan(plan, nplan, nc, want, kd3); + } + + gfc->haspixel = 1; +} + +static void pow2_ordered_dither(Gif_Image* gfi, uint8_t* all_new_data, + Gif_Colormap* old_cm, kd3_tree* kd3, + uint32_t* histogram, const uint8_t* matrix, + uint8_t* plan) { + int mws, nplans, i, x, y; + for (mws = 0; (1 << mws) != matrix[0]; ++mws) + /* nada */; + for (nplans = 0; (1 << nplans) != matrix[2]; ++nplans) + /* nada */; + + for (y = 0; y != gfi->height; ++y) { + uint8_t *data, *new_data, *thisplan; + data = gfi->img[y]; + new_data = all_new_data + y * (unsigned) gfi->width; + + for (x = 0; x != gfi->width; ++x) + /* the transparent color never gets adjusted */ + if (data[x] != gfi->transparent) { + thisplan = &plan[data[x] << nplans]; + if (!old_cm->col[data[x]].haspixel) + set_ordered_dither_plan(thisplan, 1 << nplans, matrix[3], + &old_cm->col[data[x]], kd3); + i = matrix[4 + ((x + gfi->left) & (matrix[0] - 1)) + + (((y + gfi->top) & (matrix[1] - 1)) << mws)]; + new_data[x] = thisplan[i]; + histogram[new_data[x]]++; + } + } +} + +static void colormap_image_ordered(Gif_Image* gfi, uint8_t* all_new_data, + Gif_Colormap* old_cm, kd3_tree* kd3, + uint32_t* histogram, + const uint8_t* matrix) { + int mw = matrix[0], mh = matrix[1], nplan = matrix[2]; + uint8_t* plan = Gif_NewArray(uint8_t, nplan * old_cm->ncol); + int i, x, y; + + /* Written with reference to Joel Ylilouma's versions. */ + + /* Initialize colors */ + assert(old_cm->capacity >= 256); + for (i = 0; i != 256; ++i) + old_cm->col[i].haspixel = 0; + + /* Initialize luminances, create luminance sorter */ + ordered_dither_lum = Gif_NewArray(int, kd3->nitems); + for (i = 0; i != kd3->nitems; ++i) + ordered_dither_lum[i] = kc_luminance(kd3->ks[i]); + + /* Do the image! */ + if ((mw & (mw - 1)) == 0 && (mh & (mh - 1)) == 0 + && (nplan & (nplan - 1)) == 0) + pow2_ordered_dither(gfi, all_new_data, old_cm, kd3, histogram, + matrix, plan); + else + for (y = 0; y != gfi->height; ++y) { + uint8_t *data, *new_data, *thisplan; + data = gfi->img[y]; + new_data = all_new_data + y * (unsigned) gfi->width; + + for (x = 0; x != gfi->width; ++x) + /* the transparent color never gets adjusted */ + if (data[x] != gfi->transparent) { + thisplan = &plan[nplan * data[x]]; + if (!old_cm->col[data[x]].haspixel) + set_ordered_dither_plan(thisplan, nplan, matrix[3], + &old_cm->col[data[x]], kd3); + i = matrix[4 + (x + gfi->left) % mw + + ((y + gfi->top) % mh) * mw]; + new_data[x] = thisplan[i]; + histogram[new_data[x]]++; + } + } + + /* delete temporary storage */ + Gif_DeleteArray(ordered_dither_lum); + Gif_DeleteArray(plan); +} + + +static void dither(Gif_Image* gfi, uint8_t* new_data, Gif_Colormap* old_cm, + kd3_tree* kd3, uint32_t* histogram, Gt_OutputData* od) { + if (od->dither_type == dither_default + || od->dither_type == dither_floyd_steinberg) + colormap_image_floyd_steinberg(gfi, new_data, old_cm, kd3, histogram); + else if (od->dither_type == dither_ordered + || od->dither_type == dither_ordered_new) + colormap_image_ordered(gfi, new_data, old_cm, kd3, histogram, + od->dither_data); + else if (od->dither_type == dither_atkinson) + colormap_image_atkinson(gfi, new_data, old_cm, kd3, histogram); + else + colormap_image_posterize(gfi, new_data, old_cm, kd3, histogram); +} + +/* return value 1 means run the dither again */ +static int +try_assign_transparency(Gif_Image *gfi, Gif_Colormap *old_cm, uint8_t *new_data, + Gif_Colormap *new_cm, int *new_ncol, + kd3_tree* kd3, uint32_t *histogram) +{ + uint32_t min_used; + int i, j; + int transparent = gfi->transparent; + int new_transparent = -1; + Gif_Color transp_value; + + if (transparent < 0) + return 0; + + if (old_cm) + transp_value = old_cm->col[transparent]; + else + GIF_SETCOLOR(&transp_value, 0, 0, 0); + + /* look for an unused pixel in the existing colormap; prefer the same color + we had */ + for (i = 0; i < *new_ncol; i++) + if (histogram[i] == 0 && GIF_COLOREQ(&transp_value, &new_cm->col[i])) { + new_transparent = i; + goto found; + } + for (i = 0; i < *new_ncol; i++) + if (histogram[i] == 0) { + new_transparent = i; + goto found; + } + + /* try to expand the colormap */ + if (*new_ncol < 256) { + assert(*new_ncol < new_cm->capacity); + new_transparent = *new_ncol; + new_cm->col[new_transparent] = transp_value; + (*new_ncol)++; + goto found; + } + + /* not found: mark the least-frequently-used color as the new transparent + color and return 1 (meaning 'dither again') */ + assert(*new_ncol == 256); + min_used = 0xFFFFFFFFU; + for (i = 0; i < 256; i++) + if (histogram[i] < min_used) { + new_transparent = i; + min_used = histogram[i]; + } + kd3_disable(kd3, new_transparent); /* mark it as unusable */ + return 1; + + found: + for (j = 0; j < gfi->height; j++) { + uint8_t *data = gfi->img[j]; + for (i = 0; i < gfi->width; i++, data++, new_data++) + if (*data == transparent) + *new_data = new_transparent; + } + + gfi->transparent = new_transparent; + return 0; +} + +static int color_compare_popularity(const void* va, const void* vb) { + const Gif_Color* a = (const Gif_Color*) va; + const Gif_Color* b = (const Gif_Color*) vb; + return a->pixel > b->pixel ? -1 : a->pixel != b->pixel; +} + +void +colormap_stream(Gif_Stream* gfs, Gif_Colormap* new_cm, Gt_OutputData* od, + int allow_shrink) +{ + kd3_tree kd3; + Gif_Color *new_col = new_cm->col; + int new_ncol = new_cm->ncol, new_gray; + int imagei, j; + assert(new_cm->capacity >= 256); + + /* new_col[j].pixel == number of pixels with color j in the new image. */ + for (j = 0; j < 256; j++) + new_col[j].pixel = 0; + + /* initialize kd3 tree */ + new_gray = 1; + for (j = 0; new_gray && j < new_cm->ncol; ++j) { + if (new_col[j].gfc_red != new_col[j].gfc_green + || new_col[j].gfc_red != new_col[j].gfc_blue) + new_gray = 0; + } + if (new_gray) { + kd3_init_build(&kd3, kc_luminance_transform, new_cm); + } else if (od->colormap_gamma_type == KC_GAMMA_OKLAB) { + kd3_init_build(&kd3, kc_oklab_transform, new_cm); + } else { + kd3_init_build(&kd3, NULL, new_cm); + } + + for (imagei = 0; imagei < gfs->nimages; imagei++) { + Gif_Image *gfi = gfs->images[imagei]; + Gif_Colormap *gfcm = gfi->local ? gfi->local : gfs->global; + int only_compressed = (gfi->img == 0); + + if (gfcm) { + /* If there was an old colormap, change the image data */ + uint8_t *new_data = Gif_NewArray(uint8_t, (unsigned) gfi->width * (unsigned) gfi->height); + uint32_t histogram[256]; + unmark_colors(new_cm); + unmark_colors(gfcm); + + if (only_compressed) + Gif_UncompressImage(gfs, gfi); + + kd3_enable_all(&kd3); + do { + for (j = 0; j < 256; j++) + histogram[j] = 0; + dither(gfi, new_data, gfcm, &kd3, histogram, od); + } while (try_assign_transparency(gfi, gfcm, new_data, new_cm, &new_ncol, + &kd3, histogram)); + + Gif_ReleaseUncompressedImage(gfi); + /* version 1.28 bug fix: release any compressed version or it'll cause + bad images */ + Gif_ReleaseCompressedImage(gfi); + Gif_SetUncompressedImage(gfi, new_data, Gif_Free, 0); + + /* update count of used colors */ + for (j = 0; j < 256; j++) + new_col[j].pixel += histogram[j]; + if (gfi->transparent >= 0) + /* we don't have data on the number of used colors for transparency + so fudge it. */ + new_col[gfi->transparent].pixel += (unsigned) gfi->width * (unsigned) gfi->height / 8; + + } else { + /* Can't compress new_cm afterwards if we didn't actively change colors + over */ + allow_shrink = 0; + } + + if (gfi->local) { + Gif_DeleteColormap(gfi->local); + gfi->local = 0; + } + + /* 1.92: recompress *after* deleting the local colormap */ + if (gfcm && only_compressed) { + Gif_FullCompressImage(gfs, gfi, &gif_write_info); + Gif_ReleaseUncompressedImage(gfi); + } + } + + /* Set new_cm->ncol from new_ncol. We didn't update new_cm->ncol before so + the closest-color algorithms wouldn't see any new transparent colors. + That way added transparent colors were only used for transparency. */ + new_cm->ncol = new_ncol; + + /* change the background. I hate the background by now */ + if ((gfs->nimages == 0 || gfs->images[0]->transparent < 0) + && gfs->global && gfs->background < gfs->global->ncol) { + Gif_Color *c = &gfs->global->col[gfs->background]; + gfs->background = kd3_closest8g(&kd3, c->gfc_red, c->gfc_green, c->gfc_blue); + new_col[gfs->background].pixel++; + } else if (gfs->nimages > 0 && gfs->images[0]->transparent >= 0) + gfs->background = gfs->images[0]->transparent; + else + gfs->background = 0; + + Gif_DeleteColormap(gfs->global); + kd3_cleanup(&kd3); + + /* We may have used only a subset of the colors in new_cm. We try to store + only that subset, just as if we'd piped the output of 'gifsicle + --use-colormap=X' through 'gifsicle' another time. */ + gfs->global = Gif_CopyColormap(new_cm); + for (j = 0; j < new_cm->ncol; ++j) + gfs->global->col[j].haspixel = 0; + if (allow_shrink) { + /* only bother to recompress if we'll get anything out of it */ + allow_shrink = 0; + for (j = 0; j < new_cm->ncol - 1; j++) + if (new_col[j].pixel == 0 || new_col[j].pixel < new_col[j+1].pixel) { + allow_shrink = 1; + break; + } + } + + if (allow_shrink) { + int map[256]; + + /* Gif_CopyColormap copies the 'pixel' values as well */ + new_col = gfs->global->col; + for (j = 0; j < new_cm->ncol; j++) + new_col[j].haspixel = j; + + /* sort based on popularity */ + qsort(new_col, new_cm->ncol, sizeof(Gif_Color), color_compare_popularity); + + /* set up the map and reduce the number of colors */ + for (j = 0; j < new_cm->ncol; j++) + map[ new_col[j].haspixel ] = j; + for (j = 0; j < new_cm->ncol; j++) + if (!new_col[j].pixel) { + gfs->global->ncol = j; + break; + } + + /* map the image data, transparencies, and background */ + if (gfs->background < gfs->global->ncol) + gfs->background = map[gfs->background]; + for (imagei = 0; imagei < gfs->nimages; imagei++) { + Gif_Image *gfi = gfs->images[imagei]; + int only_compressed = (gfi->img == 0); + uint32_t size; + uint8_t *data; + if (only_compressed) + Gif_UncompressImage(gfs, gfi); + + data = gfi->image_data; + for (size = (unsigned) gfi->width * (unsigned) gfi->height; size > 0; size--, data++) + *data = map[*data]; + if (gfi->transparent >= 0) + gfi->transparent = map[gfi->transparent]; + + if (only_compressed) { + Gif_FullCompressImage(gfs, gfi, &gif_write_info); + Gif_ReleaseUncompressedImage(gfi); + } + } + } +} + + +/* Halftone algorithms */ + +static const uint8_t dither_matrix_o3x3[4 + 3*3] = { + 3, 3, 9, 9, + 2, 6, 3, + 5, 0, 8, + 1, 7, 4 +}; + +static const uint8_t dither_matrix_o4x4[4 + 4*4] = { + 4, 4, 16, 16, + 0, 8, 3, 10, + 12, 4, 14, 6, + 2, 11, 1, 9, + 15, 7, 13, 5 +}; + +static const uint8_t dither_matrix_o8x8[4 + 8*8] = { + 8, 8, 64, 64, + 0, 48, 12, 60, 3, 51, 15, 63, + 32, 16, 44, 28, 35, 19, 47, 31, + 8, 56, 4, 52, 11, 59, 7, 55, + 40, 24, 36, 20, 43, 27, 39, 23, + 2, 50, 14, 62, 1, 49, 13, 61, + 34, 18, 46, 30, 33, 17, 45, 29, + 10, 58, 6, 54, 9, 57, 5, 53, + 42, 26, 38, 22, 41, 25, 37, 21 +}; + +static const uint8_t dither_matrix_ro64x64[4 + 64*64] = { + 64, 64, 16, 16, + 6, 15, 2, 15, 2, 14, 1, 13, 2, 14, 5, 13, 0, 14, 0, 9, 6, 10, 7, 13, 6, 13, 3, 10, 5, 15, 4, 11, 0, 11, 6, 10, 7, 12, 7, 13, 0, 9, 6, 15, 6, 10, 0, 15, 1, 15, 0, 8, 0, 15, 6, 15, 7, 15, 7, 9, 1, 15, 3, 8, 1, 8, 0, 14, + 9, 3, 10, 5, 10, 6, 10, 5, 9, 6, 9, 2, 9, 4, 13, 4, 13, 3, 8, 3, 10, 1, 13, 6, 11, 1, 12, 3, 14, 5, 15, 3, 8, 3, 8, 3, 12, 4, 11, 3, 13, 3, 8, 4, 9, 6, 12, 4, 11, 6, 11, 3, 10, 0, 12, 1, 11, 7, 12, 4, 12, 4, 11, 5, + 1, 14, 0, 10, 2, 9, 2, 11, 1, 8, 1, 8, 3, 9, 4, 15, 7, 13, 7, 14, 7, 14, 0, 10, 0, 14, 7, 9, 0, 11, 1, 15, 0, 11, 0, 11, 3, 15, 7, 14, 6, 10, 5, 8, 0, 11, 0, 8, 7, 11, 0, 15, 0, 12, 1, 13, 6, 9, 0, 15, 4, 9, 1, 8, + 10, 5, 15, 6, 13, 6, 14, 7, 14, 4, 12, 5, 15, 6, 10, 2, 11, 3, 10, 3, 11, 3, 13, 6, 11, 5, 14, 3, 14, 6, 8, 5, 14, 5, 14, 7, 10, 7, 11, 3, 13, 3, 13, 2, 14, 4, 15, 5, 15, 3, 8, 4, 11, 4, 9, 5, 12, 3, 8, 5, 15, 2, 13, 5, + 3, 10, 2, 9, 5, 15, 4, 9, 0, 11, 7, 11, 0, 14, 5, 11, 5, 14, 7, 15, 6, 9, 0, 9, 0, 8, 4, 14, 6, 12, 0, 11, 4, 15, 5, 8, 6, 10, 6, 11, 6, 10, 3, 12, 5, 9, 6, 9, 5, 12, 6, 12, 7, 14, 0, 14, 2, 15, 5, 8, 2, 10, 3, 9, + 14, 7, 14, 7, 8, 1, 12, 2, 14, 4, 15, 3, 11, 5, 12, 2, 8, 0, 10, 3, 12, 1, 13, 5, 14, 5, 11, 0, 11, 3, 13, 7, 8, 1, 12, 3, 15, 3, 14, 2, 15, 3, 11, 6, 15, 1, 12, 1, 9, 3, 9, 3, 11, 3, 11, 7, 11, 5, 12, 0, 14, 6, 13, 6, + 5, 10, 6, 12, 3, 15, 3, 8, 7, 10, 0, 15, 7, 10, 5, 8, 1, 14, 5, 10, 7, 14, 2, 14, 0, 14, 1, 8, 3, 8, 5, 10, 5, 15, 0, 8, 6, 14, 0, 8, 2, 15, 4, 11, 6, 10, 0, 8, 5, 9, 4, 14, 1, 15, 3, 10, 1, 15, 0, 15, 5, 8, 7, 13, + 15, 3, 8, 0, 9, 6, 12, 6, 15, 0, 11, 4, 14, 3, 15, 3, 10, 5, 12, 3, 10, 3, 9, 6, 9, 5, 13, 5, 15, 6, 14, 0, 9, 0, 12, 4, 11, 3, 15, 4, 11, 6, 15, 2, 15, 3, 14, 4, 13, 2, 11, 1, 8, 5, 12, 7, 9, 4, 8, 5, 12, 2, 11, 0, + 0, 13, 5, 11, 5, 14, 5, 15, 7, 9, 5, 8, 2, 10, 3, 15, 6, 9, 3, 12, 5, 11, 6, 14, 3, 13, 6, 13, 0, 9, 1, 11, 3, 8, 3, 9, 4, 9, 6, 14, 7, 8, 6, 12, 7, 13, 6, 9, 5, 13, 3, 15, 5, 14, 3, 15, 4, 12, 4, 12, 2, 13, 0, 15, + 10, 7, 14, 1, 8, 2, 10, 2, 12, 0, 13, 1, 13, 5, 11, 6, 12, 3, 9, 6, 15, 1, 9, 1, 10, 6, 10, 3, 15, 6, 15, 5, 15, 7, 12, 6, 12, 0, 9, 3, 12, 3, 11, 3, 8, 3, 13, 2, 9, 3, 9, 6, 10, 2, 11, 6, 9, 0, 9, 1, 9, 6, 10, 4, + 3, 10, 4, 9, 4, 9, 6, 13, 3, 15, 0, 8, 4, 10, 0, 14, 5, 15, 7, 15, 2, 12, 7, 10, 5, 15, 7, 8, 0, 9, 0, 8, 6, 14, 4, 15, 2, 15, 1, 15, 0, 9, 5, 14, 0, 12, 7, 10, 6, 8, 6, 11, 0, 13, 7, 14, 1, 13, 2, 10, 5, 9, 0, 14, + 14, 6, 13, 1, 12, 3, 8, 3, 9, 6, 12, 4, 15, 2, 8, 5, 9, 1, 10, 1, 8, 5, 15, 0, 8, 2, 12, 3, 12, 5, 12, 5, 11, 2, 10, 2, 8, 5, 10, 5, 12, 4, 9, 3, 8, 4, 13, 1, 15, 0, 12, 3, 8, 4, 10, 3, 9, 5, 13, 5, 13, 3, 11, 4, + 4, 12, 3, 10, 6, 9, 0, 14, 0, 8, 1, 9, 1, 9, 6, 15, 0, 10, 2, 9, 6, 10, 7, 10, 2, 14, 4, 8, 0, 13, 4, 8, 1, 11, 6, 14, 7, 14, 0, 8, 4, 14, 7, 14, 4, 9, 2, 12, 0, 12, 3, 8, 5, 9, 6, 14, 0, 9, 2, 14, 5, 10, 1, 15, + 10, 2, 15, 6, 14, 3, 11, 6, 15, 4, 15, 4, 13, 5, 11, 3, 12, 4, 14, 5, 12, 0, 13, 3, 8, 6, 15, 2, 10, 6, 13, 2, 12, 6, 8, 3, 10, 3, 14, 5, 8, 0, 10, 3, 13, 2, 9, 6, 10, 6, 15, 4, 13, 2, 8, 3, 14, 6, 11, 7, 13, 0, 10, 5, + 2, 13, 1, 9, 5, 10, 3, 15, 6, 10, 0, 15, 4, 15, 4, 15, 6, 9, 0, 14, 1, 9, 6, 12, 0, 11, 5, 9, 0, 14, 5, 14, 6, 9, 3, 11, 7, 11, 1, 15, 1, 9, 3, 13, 5, 12, 0, 9, 2, 9, 6, 13, 2, 9, 6, 13, 7, 11, 7, 8, 0, 13, 3, 9, + 9, 6, 13, 6, 13, 0, 8, 5, 14, 2, 11, 7, 11, 2, 11, 2, 12, 3, 11, 4, 13, 4, 10, 3, 15, 5, 13, 0, 11, 6, 10, 0, 13, 3, 15, 7, 14, 3, 8, 4, 12, 6, 9, 6, 10, 3, 14, 5, 13, 5, 11, 3, 13, 6, 8, 2, 14, 3, 15, 3, 11, 4, 12, 6, + 4, 10, 7, 11, 1, 10, 2, 14, 7, 14, 0, 14, 4, 15, 0, 8, 0, 15, 5, 15, 7, 11, 7, 14, 6, 12, 0, 8, 1, 15, 1, 14, 2, 8, 0, 11, 4, 8, 5, 11, 1, 15, 0, 12, 7, 14, 0, 11, 4, 8, 2, 8, 0, 14, 2, 14, 4, 9, 4, 10, 7, 15, 5, 14, + 14, 0, 13, 2, 14, 5, 11, 5, 10, 2, 10, 6, 11, 3, 12, 5, 11, 7, 11, 1, 12, 3, 11, 3, 9, 3, 12, 4, 11, 6, 9, 5, 14, 5, 12, 7, 14, 2, 12, 1, 8, 4, 11, 7, 10, 3, 14, 4, 14, 0, 14, 5, 11, 7, 11, 5, 15, 1, 14, 0, 10, 3, 10, 0, + 5, 12, 7, 10, 7, 11, 0, 14, 5, 14, 1, 14, 3, 8, 0, 15, 0, 9, 5, 9, 1, 8, 6, 15, 6, 15, 7, 13, 7, 9, 4, 11, 0, 10, 6, 10, 7, 12, 7, 15, 0, 9, 7, 14, 2, 11, 0, 12, 7, 11, 7, 13, 0, 9, 6, 14, 2, 11, 1, 15, 0, 8, 0, 10, + 10, 3, 15, 3, 15, 3, 8, 4, 11, 3, 11, 6, 15, 6, 11, 5, 12, 5, 13, 0, 15, 4, 8, 1, 11, 3, 10, 3, 15, 3, 14, 1, 14, 6, 13, 3, 11, 3, 11, 3, 13, 6, 8, 3, 12, 5, 8, 4, 13, 3, 8, 2, 13, 4, 8, 3, 14, 7, 8, 5, 14, 4, 15, 5, + 3, 9, 0, 10, 0, 12, 3, 11, 7, 10, 6, 8, 7, 12, 0, 11, 1, 8, 5, 12, 5, 12, 7, 11, 2, 14, 0, 10, 0, 12, 0, 15, 2, 9, 5, 9, 0, 15, 4, 14, 6, 9, 7, 11, 6, 10, 0, 9, 1, 9, 0, 14, 4, 12, 5, 9, 6, 10, 4, 11, 6, 12, 5, 12, + 12, 6, 15, 4, 8, 4, 15, 7, 15, 3, 14, 0, 9, 3, 13, 5, 14, 5, 11, 1, 9, 0, 15, 3, 8, 7, 12, 7, 8, 5, 9, 4, 12, 6, 12, 1, 10, 6, 9, 0, 15, 3, 14, 0, 15, 3, 13, 5, 15, 5, 9, 6, 10, 0, 14, 0, 13, 1, 15, 1, 8, 3, 8, 1, + 2, 8, 7, 15, 0, 11, 1, 12, 1, 12, 0, 15, 0, 11, 2, 13, 1, 10, 5, 8, 7, 13, 6, 10, 0, 11, 4, 12, 7, 10, 1, 9, 1, 9, 0, 12, 2, 11, 7, 15, 1, 11, 0, 9, 2, 8, 4, 12, 2, 12, 6, 14, 4, 14, 3, 13, 3, 15, 1, 14, 4, 15, 6, 12, + 13, 6, 11, 3, 12, 4, 8, 4, 9, 4, 9, 5, 12, 6, 10, 5, 14, 7, 14, 3, 10, 2, 13, 3, 14, 5, 8, 2, 13, 1, 15, 6, 15, 4, 8, 4, 15, 5, 10, 1, 14, 5, 12, 4, 13, 5, 11, 1, 9, 5, 9, 3, 9, 0, 10, 7, 10, 7, 11, 6, 10, 3, 9, 0, + 3, 15, 5, 8, 1, 9, 5, 8, 4, 13, 6, 8, 4, 15, 6, 10, 6, 14, 7, 11, 0, 9, 1, 12, 0, 8, 6, 12, 6, 10, 3, 12, 7, 13, 7, 11, 3, 8, 0, 13, 7, 12, 0, 15, 4, 12, 5, 8, 4, 15, 5, 10, 0, 15, 3, 15, 1, 8, 1, 15, 4, 14, 6, 15, + 10, 6, 14, 0, 13, 4, 13, 1, 9, 1, 13, 3, 11, 2, 12, 1, 8, 3, 12, 3, 14, 6, 8, 4, 15, 4, 8, 3, 15, 1, 9, 6, 11, 2, 14, 3, 13, 7, 10, 4, 10, 3, 8, 7, 11, 2, 12, 1, 11, 1, 14, 3, 9, 6, 9, 7, 13, 5, 11, 7, 10, 2, 9, 3, + 1, 9, 0, 14, 7, 12, 7, 14, 5, 9, 6, 10, 6, 14, 6, 15, 1, 14, 7, 10, 3, 12, 0, 14, 5, 9, 6, 8, 1, 12, 0, 13, 2, 8, 4, 9, 6, 15, 0, 14, 4, 10, 7, 11, 5, 14, 2, 8, 4, 15, 3, 8, 7, 13, 1, 9, 0, 15, 0, 14, 5, 14, 0, 14, + 15, 4, 10, 6, 11, 1, 10, 2, 14, 3, 15, 1, 10, 3, 10, 3, 8, 4, 12, 3, 10, 6, 9, 6, 14, 2, 12, 3, 11, 4, 9, 6, 12, 5, 15, 0, 10, 3, 8, 4, 14, 1, 14, 3, 11, 2, 12, 7, 11, 2, 12, 6, 10, 1, 13, 4, 11, 4, 10, 5, 11, 2, 11, 7, + 0, 8, 1, 11, 4, 8, 4, 10, 6, 11, 3, 10, 6, 10, 6, 8, 1, 11, 1, 15, 7, 15, 6, 9, 1, 9, 7, 8, 0, 14, 5, 12, 3, 13, 4, 12, 0, 15, 5, 12, 1, 12, 7, 9, 6, 8, 3, 15, 0, 13, 6, 15, 7, 15, 7, 13, 2, 15, 1, 14, 2, 15, 0, 8, + 12, 5, 13, 5, 15, 1, 12, 1, 15, 1, 14, 6, 14, 2, 14, 2, 14, 6, 11, 6, 11, 3, 12, 2, 13, 6, 15, 3, 9, 5, 8, 2, 8, 5, 9, 1, 9, 4, 9, 2, 8, 4, 13, 1, 13, 0, 11, 6, 11, 6, 9, 2, 11, 3, 10, 1, 10, 6, 9, 6, 11, 7, 15, 5, + 5, 12, 0, 9, 5, 8, 5, 9, 0, 10, 2, 13, 4, 8, 5, 14, 2, 9, 2, 15, 4, 8, 2, 8, 0, 12, 6, 12, 0, 10, 5, 15, 4, 9, 3, 13, 0, 9, 4, 15, 1, 14, 1, 9, 0, 9, 2, 14, 6, 12, 0, 15, 7, 9, 2, 9, 0, 15, 6, 10, 3, 15, 3, 13, + 9, 1, 15, 5, 12, 3, 14, 0, 15, 7, 8, 5, 14, 1, 11, 1, 13, 6, 10, 7, 13, 0, 14, 5, 10, 7, 11, 3, 15, 6, 11, 1, 12, 2, 8, 4, 13, 5, 10, 2, 10, 6, 13, 5, 13, 4, 10, 5, 9, 3, 8, 4, 12, 1, 12, 5, 11, 6, 14, 3, 11, 7, 11, 7, + 2, 11, 4, 12, 7, 8, 3, 15, 3, 11, 4, 11, 7, 10, 1, 10, 2, 10, 0, 11, 4, 12, 0, 10, 1, 8, 7, 11, 1, 10, 1, 15, 7, 10, 2, 11, 1, 9, 6, 14, 2, 11, 1, 14, 4, 12, 2, 13, 1, 9, 5, 8, 7, 11, 7, 10, 7, 15, 4, 13, 3, 10, 1, 11, + 13, 5, 8, 0, 15, 3, 11, 6, 12, 7, 15, 3, 15, 1, 15, 4, 14, 5, 13, 6, 8, 1, 15, 6, 13, 5, 12, 2, 15, 4, 9, 4, 15, 3, 14, 6, 14, 5, 9, 3, 12, 6, 11, 5, 8, 2, 9, 5, 12, 5, 15, 1, 13, 3, 14, 2, 8, 0, 8, 0, 12, 7, 15, 5, + 0, 14, 1, 8, 2, 8, 2, 15, 2, 9, 6, 12, 0, 12, 7, 12, 7, 13, 0, 15, 6, 14, 5, 11, 1, 12, 5, 8, 5, 8, 1, 14, 0, 10, 4, 12, 6, 9, 6, 8, 0, 9, 0, 15, 4, 15, 5, 13, 1, 15, 5, 12, 7, 10, 6, 14, 4, 9, 6, 15, 0, 15, 7, 13, + 10, 4, 13, 5, 13, 5, 11, 5, 12, 5, 9, 3, 9, 4, 8, 3, 9, 3, 10, 6, 11, 1, 15, 2, 8, 5, 15, 1, 13, 2, 11, 6, 14, 6, 8, 2, 15, 3, 14, 3, 13, 6, 9, 6, 9, 0, 10, 2, 10, 7, 11, 1, 15, 3, 9, 3, 13, 3, 10, 3, 9, 4, 10, 1, + 4, 11, 7, 10, 2, 14, 4, 15, 3, 15, 2, 15, 7, 10, 2, 14, 1, 8, 0, 15, 2, 8, 1, 12, 2, 13, 1, 8, 7, 12, 6, 11, 1, 10, 4, 11, 2, 15, 0, 13, 0, 12, 7, 11, 5, 12, 7, 8, 6, 15, 4, 12, 7, 10, 6, 10, 0, 10, 0, 11, 4, 12, 7, 10, + 13, 2, 14, 2, 10, 7, 11, 1, 11, 7, 11, 7, 12, 3, 11, 7, 13, 4, 11, 6, 12, 5, 11, 6, 10, 7, 13, 4, 11, 3, 15, 3, 14, 4, 15, 3, 8, 5, 10, 7, 10, 6, 15, 3, 9, 3, 15, 1, 11, 3, 9, 0, 15, 1, 15, 3, 15, 4, 13, 5, 8, 3, 15, 3, + 3, 9, 1, 12, 4, 14, 0, 11, 7, 15, 1, 11, 3, 14, 0, 11, 5, 10, 2, 10, 2, 8, 6, 14, 7, 11, 0, 10, 7, 14, 7, 15, 6, 9, 6, 9, 1, 12, 7, 9, 4, 8, 7, 13, 0, 13, 6, 12, 6, 8, 1, 9, 1, 15, 4, 12, 0, 12, 4, 10, 0, 13, 0, 12, + 13, 5, 10, 6, 9, 1, 13, 6, 11, 0, 14, 7, 11, 7, 14, 5, 15, 2, 13, 5, 15, 4, 9, 2, 13, 3, 14, 7, 10, 3, 10, 3, 12, 3, 12, 2, 8, 5, 15, 3, 13, 3, 11, 3, 10, 4, 9, 3, 15, 1, 14, 4, 11, 6, 10, 3, 8, 4, 13, 0, 10, 4, 8, 5, + 5, 8, 3, 13, 4, 14, 1, 13, 7, 9, 2, 14, 4, 14, 4, 12, 7, 10, 6, 10, 5, 12, 2, 11, 6, 12, 7, 13, 4, 11, 2, 11, 3, 8, 5, 15, 2, 10, 1, 9, 2, 12, 0, 12, 0, 10, 1, 12, 0, 9, 7, 10, 0, 15, 4, 9, 0, 8, 1, 14, 4, 14, 4, 13, + 12, 2, 8, 6, 10, 0, 8, 5, 13, 3, 8, 5, 10, 3, 11, 2, 15, 0, 13, 3, 8, 2, 15, 6, 9, 1, 8, 3, 14, 1, 15, 5, 14, 7, 8, 0, 12, 6, 15, 5, 8, 5, 9, 4, 15, 5, 8, 5, 15, 4, 12, 3, 9, 6, 12, 3, 13, 5, 11, 7, 10, 1, 8, 3, + 4, 15, 2, 11, 2, 10, 6, 11, 3, 14, 7, 15, 0, 10, 0, 14, 3, 12, 4, 8, 7, 9, 3, 11, 0, 13, 2, 12, 2, 13, 5, 8, 5, 8, 2, 11, 4, 12, 2, 10, 7, 8, 6, 10, 7, 12, 7, 11, 0, 15, 7, 8, 7, 15, 0, 9, 2, 12, 6, 13, 0, 9, 4, 9, + 8, 3, 12, 5, 15, 5, 13, 1, 8, 7, 8, 3, 12, 5, 9, 5, 9, 6, 14, 1, 12, 3, 15, 7, 10, 7, 8, 5, 8, 5, 15, 0, 13, 1, 14, 7, 9, 1, 14, 7, 13, 2, 12, 3, 8, 1, 13, 3, 8, 4, 12, 0, 11, 3, 12, 5, 11, 5, 9, 3, 12, 6, 14, 2, + 5, 13, 2, 8, 6, 12, 7, 10, 5, 14, 4, 11, 2, 11, 2, 13, 2, 13, 5, 10, 5, 9, 0, 10, 7, 13, 4, 15, 2, 9, 1, 9, 5, 9, 6, 10, 4, 12, 5, 12, 4, 10, 3, 14, 3, 15, 1, 8, 4, 12, 5, 8, 5, 14, 7, 11, 7, 15, 5, 11, 2, 13, 1, 9, + 10, 0, 13, 6, 10, 2, 15, 3, 8, 1, 12, 2, 14, 6, 8, 5, 10, 7, 13, 1, 13, 1, 12, 7, 9, 1, 9, 2, 12, 5, 12, 4, 12, 3, 12, 2, 9, 0, 8, 1, 15, 1, 10, 7, 10, 6, 12, 4, 11, 1, 14, 2, 11, 0, 14, 1, 9, 2, 12, 2, 8, 5, 13, 5, + 6, 10, 7, 11, 0, 9, 3, 9, 5, 11, 0, 8, 0, 13, 2, 13, 4, 13, 5, 9, 4, 8, 2, 8, 0, 10, 0, 14, 6, 9, 6, 9, 4, 14, 7, 9, 0, 11, 7, 10, 7, 11, 7, 14, 4, 14, 5, 15, 4, 8, 4, 11, 5, 14, 0, 8, 1, 14, 7, 14, 2, 11, 7, 13, + 12, 3, 13, 3, 14, 5, 15, 6, 12, 0, 15, 4, 11, 5, 9, 5, 9, 2, 14, 1, 12, 1, 13, 5, 15, 4, 11, 7, 12, 1, 13, 2, 8, 1, 12, 0, 15, 6, 14, 2, 14, 3, 10, 1, 11, 3, 11, 0, 12, 1, 15, 2, 11, 1, 12, 4, 9, 4, 10, 2, 13, 6, 10, 2, + 2, 8, 4, 8, 4, 9, 0, 9, 2, 15, 5, 9, 6, 11, 7, 14, 0, 9, 2, 12, 2, 8, 0, 10, 1, 9, 7, 8, 2, 9, 1, 11, 2, 11, 2, 8, 1, 9, 1, 11, 7, 13, 6, 11, 1, 11, 0, 9, 2, 13, 4, 14, 1, 15, 2, 8, 7, 15, 0, 13, 6, 9, 4, 13, + 13, 5, 13, 1, 14, 0, 12, 6, 9, 6, 12, 0, 13, 0, 11, 3, 13, 6, 11, 7, 12, 5, 14, 6, 14, 5, 15, 3, 13, 6, 14, 7, 15, 6, 15, 5, 13, 4, 14, 5, 8, 2, 14, 3, 14, 6, 14, 5, 10, 6, 10, 2, 9, 5, 15, 5, 11, 1, 8, 5, 13, 3, 11, 2, + 2, 8, 6, 8, 0, 9, 1, 15, 0, 11, 4, 15, 7, 8, 4, 8, 1, 13, 7, 11, 1, 13, 5, 15, 1, 15, 2, 9, 0, 13, 5, 12, 3, 12, 2, 8, 1, 10, 1, 13, 5, 15, 3, 9, 3, 9, 2, 12, 5, 14, 6, 13, 1, 9, 6, 9, 1, 8, 4, 15, 7, 10, 7, 15, + 14, 5, 14, 3, 12, 4, 10, 7, 12, 7, 8, 2, 12, 1, 13, 2, 11, 4, 13, 3, 8, 5, 8, 1, 10, 7, 12, 6, 9, 5, 8, 0, 8, 7, 14, 5, 13, 6, 8, 4, 8, 2, 12, 6, 13, 5, 8, 7, 8, 3, 10, 3, 13, 4, 12, 1, 13, 4, 11, 1, 14, 3, 8, 1, + 2, 13, 6, 10, 0, 9, 1, 15, 0, 12, 4, 11, 2, 9, 0, 9, 0, 13, 1, 8, 0, 11, 5, 9, 6, 13, 2, 15, 2, 12, 7, 12, 6, 15, 2, 14, 1, 13, 1, 14, 4, 11, 2, 14, 1, 9, 0, 15, 1, 12, 5, 14, 6, 13, 2, 15, 3, 9, 1, 15, 7, 8, 1, 14, + 11, 7, 13, 3, 15, 6, 8, 4, 8, 4, 14, 2, 14, 6, 13, 6, 11, 4, 14, 4, 15, 4, 14, 3, 10, 2, 10, 5, 9, 6, 9, 3, 9, 1, 11, 6, 9, 4, 8, 4, 15, 0, 8, 4, 13, 4, 9, 4, 9, 5, 9, 3, 10, 2, 9, 5, 12, 6, 8, 4, 12, 0, 11, 4, + 4, 10, 4, 13, 1, 12, 7, 8, 0, 13, 4, 12, 2, 13, 1, 14, 7, 15, 3, 8, 7, 12, 3, 10, 4, 9, 4, 11, 7, 8, 2, 11, 2, 10, 0, 12, 1, 14, 6, 14, 7, 13, 7, 11, 3, 10, 0, 10, 4, 9, 3, 13, 0, 13, 7, 8, 7, 9, 7, 11, 2, 8, 1, 14, + 15, 0, 11, 1, 9, 6, 15, 3, 10, 7, 9, 1, 9, 7, 10, 7, 10, 1, 12, 5, 8, 1, 15, 7, 13, 3, 12, 2, 13, 2, 14, 7, 14, 5, 10, 4, 8, 4, 11, 1, 10, 3, 14, 0, 14, 6, 14, 5, 13, 2, 8, 7, 10, 7, 13, 2, 14, 2, 13, 2, 14, 5, 11, 6, + 7, 11, 5, 13, 1, 11, 7, 8, 5, 14, 4, 13, 1, 14, 5, 10, 2, 15, 4, 13, 7, 11, 7, 10, 1, 8, 5, 9, 4, 12, 7, 10, 1, 13, 7, 13, 7, 11, 0, 14, 5, 14, 2, 14, 4, 12, 4, 13, 7, 13, 7, 8, 2, 14, 2, 13, 5, 9, 5, 9, 2, 8, 1, 10, + 12, 0, 8, 2, 13, 4, 12, 1, 11, 1, 10, 1, 10, 4, 12, 2, 11, 5, 8, 1, 15, 3, 14, 1, 13, 4, 14, 2, 9, 2, 12, 2, 8, 4, 8, 2, 12, 3, 10, 6, 11, 0, 11, 7, 10, 1, 9, 2, 10, 0, 13, 1, 11, 7, 10, 6, 13, 1, 12, 1, 12, 6, 14, 7, + 2, 14, 4, 15, 7, 14, 5, 8, 4, 13, 2, 10, 4, 13, 0, 13, 1, 11, 1, 12, 7, 12, 6, 13, 4, 13, 4, 13, 2, 12, 2, 13, 1, 13, 7, 10, 2, 11, 2, 10, 7, 15, 7, 11, 5, 13, 2, 8, 2, 14, 4, 9, 2, 14, 0, 8, 1, 8, 7, 10, 5, 15, 6, 14, + 11, 7, 10, 2, 11, 1, 13, 0, 11, 1, 15, 7, 8, 1, 9, 4, 13, 4, 8, 4, 8, 3, 10, 0, 9, 0, 9, 2, 9, 7, 9, 6, 10, 7, 13, 1, 13, 7, 14, 6, 11, 3, 12, 2, 8, 2, 12, 5, 11, 5, 12, 2, 10, 5, 14, 4, 12, 4, 13, 2, 9, 1, 10, 0, + 2, 12, 0, 13, 7, 13, 2, 12, 4, 8, 1, 12, 4, 13, 6, 11, 7, 13, 7, 13, 1, 9, 7, 10, 2, 10, 1, 9, 2, 10, 1, 11, 6, 9, 4, 13, 2, 10, 0, 10, 2, 14, 0, 13, 7, 10, 7, 10, 0, 12, 0, 9, 0, 13, 2, 13, 1, 9, 0, 15, 2, 14, 2, 13, + 11, 7, 8, 4, 10, 2, 10, 7, 12, 1, 11, 7, 8, 2, 13, 1, 10, 0, 10, 2, 13, 6, 14, 0, 14, 7, 13, 6, 14, 5, 13, 4, 15, 1, 10, 2, 13, 7, 13, 7, 9, 4, 9, 6, 13, 3, 13, 3, 8, 4, 13, 4, 10, 6, 10, 5, 12, 4, 10, 7, 11, 6, 9, 6, + 4, 14, 6, 11, 7, 13, 6, 10, 4, 8, 4, 11, 6, 8, 5, 13, 7, 14, 7, 14, 1, 9, 0, 12, 1, 9, 1, 12, 4, 14, 7, 10, 4, 13, 7, 13, 7, 11, 4, 10, 1, 11, 7, 13, 4, 12, 1, 10, 4, 12, 2, 8, 2, 12, 2, 10, 2, 12, 1, 12, 4, 12, 2, 12, + 8, 2, 13, 2, 8, 1, 13, 1, 12, 1, 12, 2, 12, 2, 11, 1, 9, 2, 11, 3, 12, 4, 8, 4, 12, 4, 10, 6, 10, 1, 13, 2, 11, 2, 10, 1, 12, 0, 14, 2, 14, 4, 9, 2, 8, 1, 13, 4, 8, 0, 12, 4, 11, 6, 12, 6, 11, 7, 10, 7, 11, 2, 10, 7 +}; + +/*static const uint8_t dither_matrix_halftone8[4 + 8*8] = { + 8, 8, 64, 3, + 60, 53, 42, 26, 27, 43, 54, 61, + 52, 41, 25, 13, 14, 28, 44, 55, + 40, 24, 12, 5, 6, 15, 29, 45, + 39, 23, 4, 0, 1, 7, 16, 30, + 38, 22, 11, 3, 2, 8, 17, 31, + 51, 37, 21, 10, 9, 18, 32, 46, + 59, 50, 36, 20, 19, 33, 47, 56, + 63, 58, 49, 35, 34, 48, 57, 62 +}; */ + +static const uint8_t dither_matrix_diagonal45_8[4 + 8*8] = { + 8, 8, 64, 2, + 16, 32, 48, 56, 40, 24, 8, 0, + 36, 52, 60, 44, 28, 12, 4, 20, + 49, 57, 41, 25, 9, 1, 17, 33, + 61, 45, 29, 13, 5, 21, 37, 53, + 42, 26, 10, 2, 18, 34, 50, 58, + 30, 14, 6, 22, 38, 54, 62, 46, + 11, 3, 19, 35, 51, 59, 43, 27, + 7, 23, 39, 55, 63, 47, 31, 15 +}; + + +typedef struct halftone_pixelinfo { + int x; + int y; + double distance; + double angle; +} halftone_pixelinfo; + +static inline halftone_pixelinfo* halftone_pixel_make(int w, int h) { + int x, y, k; + halftone_pixelinfo* hp = Gif_NewArray(halftone_pixelinfo, w * h); + for (y = k = 0; y != h; ++y) + for (x = 0; x != w; ++x, ++k) { + hp[k].x = x; + hp[k].y = y; + hp[k].distance = -1; + } + return hp; +} + +static inline void halftone_pixel_combine(halftone_pixelinfo* hp, + double cx, double cy) { + double d = (hp->x - cx) * (hp->x - cx) + (hp->y - cy) * (hp->y - cy); + if (hp->distance < 0 || d < hp->distance) { + hp->distance = d; + hp->angle = atan2(hp->y - cy, hp->x - cx); + } +} + +static inline int halftone_pixel_compare(const void* va, const void* vb) { + const halftone_pixelinfo* a = (const halftone_pixelinfo*) va; + const halftone_pixelinfo* b = (const halftone_pixelinfo*) vb; + if (fabs(a->distance - b->distance) > 0.01) + return a->distance < b->distance ? -1 : 1; + else + return a->angle < b->angle ? -1 : 1; +} + +static uint8_t* halftone_pixel_matrix(halftone_pixelinfo* hp, + int w, int h, int nc) { + int i; + uint8_t* m = Gif_NewArray(uint8_t, 4 + w * h); + m[0] = w; + m[1] = h; + m[3] = nc; + if (w * h > 255) { + double s = 255. / (w * h); + m[2] = 255; + for (i = 0; i != w * h; ++i) + m[4 + hp[i].x + hp[i].y*w] = (int) (i * s); + } else { + m[2] = w * h; + for (i = 0; i != w * h; ++i) + m[4 + hp[i].x + hp[i].y*w] = i; + } + Gif_DeleteArray(hp); + return m; +} + +static uint8_t* make_halftone_matrix_square(int w, int h, int nc) { + halftone_pixelinfo* hp = halftone_pixel_make(w, h); + int i; + for (i = 0; i != w * h; ++i) + halftone_pixel_combine(&hp[i], (w-1)/2.0, (h-1)/2.0); + qsort(hp, w * h, sizeof(*hp), halftone_pixel_compare); + return halftone_pixel_matrix(hp, w, h, nc); +} + +static uint8_t* make_halftone_matrix_triangular(int w, int h, int nc) { + halftone_pixelinfo* hp = halftone_pixel_make(w, h); + int i; + for (i = 0; i != w * h; ++i) { + halftone_pixel_combine(&hp[i], (w-1)/2.0, (h-1)/2.0); + halftone_pixel_combine(&hp[i], -0.5, -0.5); + halftone_pixel_combine(&hp[i], w-0.5, -0.5); + halftone_pixel_combine(&hp[i], -0.5, h-0.5); + halftone_pixel_combine(&hp[i], w-0.5, h-0.5); + } + qsort(hp, w * h, sizeof(*hp), halftone_pixel_compare); + return halftone_pixel_matrix(hp, w, h, nc); +} + +int set_dither_type(Gt_OutputData* od, const char* name) { + int parm[4], nparm = 0; + const char* comma = strchr(name, ','); + char buf[256]; + + /* separate arguments from dither name */ + if (comma && (size_t) (comma - name) < sizeof(buf)) { + memcpy(buf, name, comma - name); + buf[comma - name] = 0; + name = buf; + } + for (nparm = 0; + comma && *comma && isdigit((unsigned char) comma[1]); + ++nparm) + parm[nparm] = strtol(&comma[1], (char**) &comma, 10); + + /* parse dither name */ + if (od->dither_type == dither_ordered_new) + Gif_DeleteArray(od->dither_data); + od->dither_type = dither_none; + + if (strcmp(name, "none") == 0 || strcmp(name, "posterize") == 0) + /* ok */; + else if (strcmp(name, "default") == 0) + od->dither_type = dither_default; + else if (strcmp(name, "floyd-steinberg") == 0 + || strcmp(name, "fs") == 0) + od->dither_type = dither_floyd_steinberg; + else if (strcmp(name, "atkinson") == 0 + || strcmp(name, "at") == 0) + od->dither_type = dither_atkinson; + else if (strcmp(name, "o3") == 0 || strcmp(name, "o3x3") == 0 + || (strcmp(name, "o") == 0 && nparm >= 1 && parm[0] == 3)) { + od->dither_type = dither_ordered; + od->dither_data = dither_matrix_o3x3; + } else if (strcmp(name, "o4") == 0 || strcmp(name, "o4x4") == 0 + || (strcmp(name, "o") == 0 && nparm >= 1 && parm[0] == 4)) { + od->dither_type = dither_ordered; + od->dither_data = dither_matrix_o4x4; + } else if (strcmp(name, "o8") == 0 || strcmp(name, "o8x8") == 0 + || (strcmp(name, "o") == 0 && nparm >= 1 && parm[0] == 8)) { + od->dither_type = dither_ordered; + od->dither_data = dither_matrix_o8x8; + } else if (strcmp(name, "ro64") == 0 || strcmp(name, "ro64x64") == 0 + || strcmp(name, "o") == 0 || strcmp(name, "ordered") == 0) { + od->dither_type = dither_ordered; + od->dither_data = dither_matrix_ro64x64; + } else if (strcmp(name, "diag45") == 0 || strcmp(name, "diagonal") == 0) { + od->dither_type = dither_ordered; + od->dither_data = dither_matrix_diagonal45_8; + } else if (strcmp(name, "halftone") == 0 || strcmp(name, "half") == 0 + || strcmp(name, "trihalftone") == 0 + || strcmp(name, "trihalf") == 0) { + int size = nparm >= 1 && parm[0] > 0 ? parm[0] : 6; + int ncol = nparm >= 2 && parm[1] > 1 ? parm[1] : 2; + od->dither_type = dither_ordered_new; + od->dither_data = make_halftone_matrix_triangular(size, (int) (size * sqrt(3) + 0.5), ncol); + } else if (strcmp(name, "sqhalftone") == 0 || strcmp(name, "sqhalf") == 0 + || strcmp(name, "squarehalftone") == 0) { + int size = nparm >= 1 && parm[0] > 0 ? parm[0] : 6; + int ncol = nparm >= 2 && parm[1] > 1 ? parm[1] : 2; + od->dither_type = dither_ordered_new; + od->dither_data = make_halftone_matrix_square(size, size, ncol); + } else + return -1; + + if (od->dither_type == dither_ordered + && nparm >= 2 && parm[1] > 1 && parm[1] != od->dither_data[3]) { + int size = od->dither_data[0] * od->dither_data[1]; + uint8_t* dd = Gif_NewArray(uint8_t, 4 + size); + memcpy(dd, od->dither_data, 4 + size); + dd[3] = parm[1]; + od->dither_data = dd; + od->dither_type = dither_ordered_new; + } + return 0; +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/strerror.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/strerror.c new file mode 100644 index 0000000..0f153cd --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/strerror.c @@ -0,0 +1,25 @@ +/* Some operating systems don't have strerror. + This file provides a definition which David Mazieres + assures me works. */ + +#if HAVE_CONFIG_H +# include +#endif +#ifdef __cplusplus +extern "C" { +#endif + +char * +strerror(int errno) +{ + extern int sys_nerr; + extern char *sys_errlist[]; + if (errno < 0 || errno >= sys_nerr) + return (char *)"bad error number"; + else + return sys_errlist[errno]; +} + +#ifdef __cplusplus +} +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/support.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/support.c new file mode 100644 index 0000000..60e6f86 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/support.c @@ -0,0 +1,1783 @@ +/* support.c - Support functions for gifsicle. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#include +#include "gifsicle.h" +#include +#include +#include +#include +#include +#include + +const char *program_name = "gifsicle"; +static int verbose_pos = 0; +int error_count = 0; +int no_warnings = 0; + + +static void +verror(const char* landmark, int need_file, + int seriousness, const char *fmt, va_list val) +{ + char pbuf[256], buf[BUFSIZ], xbuf[BUFSIZ]; + const char* xfmt; + int n, i, p; + size_t xi; + + if (!fmt || !*fmt) + return; + + if (!landmark && need_file && active_output_data.active_output_name + && mode != BLANK_MODE && mode != MERGING && nested_mode != MERGING) + landmark = active_output_data.active_output_name; + else if (!landmark) + landmark = ""; + + if (seriousness > 2) + xfmt = "%s:%s%s fatal error: "; + else if (seriousness == 1) + xfmt = "%s:%s%s warning: "; + else + xfmt = "%s:%s%s "; + snprintf(pbuf, sizeof(pbuf), xfmt, program_name, landmark, *landmark ? ":" : ""); + p = strlen(pbuf); + + Clp_vsnprintf(clp, buf, sizeof(buf), fmt, val); + n = strlen(buf); + if ((size_t) n + 1 < sizeof(buf) && (n == 0 || buf[n - 1] != '\n')) { + buf[n++] = '\n'; + buf[n] = 0; + } + + xi = 0; + for (i = 0; i != n; ) { + /* char* pos = (char*) memchr(&buf[i], '\n', n - i); + int l = (pos ? &pos[1] - &buf[i] : n - i); */ + int l = n - i; + int xd = snprintf(&xbuf[xi], sizeof(xbuf) - xi, "%.*s%.*s", + (int) p, pbuf, (int) l, &buf[i]); + i += l; + xi = (xi + xd > sizeof(xbuf) ? sizeof(xbuf) : xi + xd); + } + + if (seriousness == 1 && no_warnings) + return; + else if (seriousness > 1) + error_count++; + + verbose_endline(); + fwrite(xbuf, 1, xi, stderr); +} + +void fatal_error(const char* format, ...) { + va_list val; + va_start(val, format); + verror((const char*) 0, 0, 3, format, val); + va_end(val); + exit(EXIT_USER_ERR); +} + +void lerror(const char* landmark, const char* format, ...) { + va_list val; + va_start(val, format); + verror(landmark, 2, 2, format, val); + va_end(val); +} + +void error(int need_file, const char* format, ...) { + va_list val; + va_start(val, format); + verror((const char*) 0, need_file, 2, format, val); + va_end(val); +} + +void lwarning(const char* landmark, const char* format, ...) { + va_list val; + va_start(val, format); + verror(landmark, 2, 1, format, val); + va_end(val); +} + +void warning(int need_file, const char* format, ...) { + va_list val; + va_start(val, format); + verror((const char*) 0, need_file, 1, format, val); + va_end(val); +} + +void +clp_error_handler(Clp_Parser *clp, const char *message) +{ + (void) clp; + verbose_endline(); + fputs(message, stderr); +} + + +void +short_usage(Clp_Parser* clp) +{ + fprintf(stderr, "Usage: %s [OPTION | FILE | FRAME]...\n\ +Try %s%s --help%s for more information.\n", + program_name, Clp_QuoteChar(clp, 0), program_name, Clp_QuoteChar(clp, 1)); +} + + +void +usage(Clp_Parser* clp) +{ + const char* q0 = Clp_QuoteChar(clp, 0); + const char* q1 = Clp_QuoteChar(clp, 1); + printf("\ +Gifsicle manipulates GIF images. Its most common uses include combining\n\ +single images into animations, adding transparency, optimizing animations for\n\ +space, and printing information about GIFs.\n\ +\n\ +Usage: %s [OPTION | FILE | FRAME]...\n\n", program_name); + printf("\ +Mode options (at most one, before any filenames):\n\ + -m, --merge Merge mode: combine inputs, write stdout\n\ + -b, --batch Batch mode: modify inputs, write back to\n\ + same filenames\n\ + -e, --explode Explode mode: write N files for each input,\n\ + one per frame, to %sinput.frame-number%s\n\ + -E, --explode-by-name Explode mode, but write %sinput.name%s\n\n", + q0, q1, q0, q1); + printf("\ +General options (also --no-OPTION for info and verbose):\n\ + -I, --info Print info about input GIFs\n\ + -I -I Print info to standard error; do not suppress\n\ + normal output\n\ + --color-info, --cinfo --info plus colormap details\n\ + --extension-info, --xinfo --info plus extension details\n\ + --size-info, --sinfo --info plus compression information\n\ + -V, --verbose Print progress (files read and written)\n"); + printf("\ + -h, --help Print this message and exit\n\ + --version Print version number and exit\n\ + -o, --output FILE Write output to FILE\n\ + -w, --no-warnings Don%st report warnings\n\ + --no-ignore-errors Quit on very erroneous input GIFs\n\ + --conserve-memory Conserve memory at the expense of speed\n\ + --nextfile, --multifile Support concatenated GIF files\n\n", + q1); + printf("\ +Frame selections: #num, #num1-num2, #num1-, #name\n\ +\n\ +Frame change options:\n\ + --delete FRAMES Delete FRAMES from input\n\ + --insert-before FRAME GIFS Insert GIFS before FRAMES in input\n\ + --append GIFS Append GIFS to input\n\ + --replace FRAMES GIFS Replace FRAMES with GIFS in input\n\ + --done Done with frame changes\n\n"); + printf("\ +Image options (also --no-OPTION and --same-OPTION):\n\ + -B, --background COL Make COL the background color\n\ + --crop X,Y+WxH, --crop X,Y-X2,Y2\n\ + Crop the image\n\ + --crop-transparency Crop transparent borders off the image\n\ + --flip-horizontal, --flip-vertical\n\ + Flip the image\n"); + printf("\ + -i, --interlace Turn on interlacing\n\ + -S, --logical-screen WxH Set logical screen to WxH\n\ + -p, --position X,Y Set frame position to (X,Y)\n\ + --rotate-90, --rotate-180, --rotate-270, --no-rotate\n\ + Rotate the image\n\ + -t, --transparent COL Make COL transparent\n\n"); + printf("\ +Extension options:\n\ + --app-extension N D Add an app extension named N with data D\n\ + -c, --comment TEXT Add a comment before the next frame\n\ + --extension N D Add an extension number N with data D\n\ + -n, --name TEXT Set next frame%ss name\n\ + --no-comments, --no-names, --no-extensions\n\ + Remove comments (names, extensions) from input\n", + q1); + printf("\ +Animation options (also --no-OPTION and --same-OPTION):\n\ + -d, --delay TIME Set frame delay to TIME (in 1/100sec)\n\ + -D, --disposal METHOD Set frame disposal to METHOD\n\ + -l, --loopcount[=N] Set loop extension to N (default forever)\n\ + -O, --optimize[=LEVEL] Optimize output GIFs\n\ + -U, --unoptimize Unoptimize input GIFs\n"); +#if ENABLE_THREADS + printf("\ + -j, --threads[=THREADS] Use multiple threads to improve speed\n"); +#endif + printf("\n\ +Whole-GIF options (also --no-OPTION):\n\ + --careful Write larger GIFs that avoid bugs in other\n\ + programs\n\ + --change-color COL1 COL2 Change COL1 to COL2 throughout\n\ + -k, --colors N Reduce the number of colors to N\n\ + --color-method METHOD Set method for choosing reduced colors\n\ + -f, --dither Dither image after changing colormap\n"); +#if HAVE_POW + printf("\ + --gamma G Set gamma for color reduction [2.2]\n"); +#endif + printf("\ + --lossy[=LOSSINESS] Alter image colors to shrink output file size\n\ + at the cost of artifacts and noise\n\ + --resize WxH Resize the output GIF to WxH\n\ + --scale XFACTOR[xYFACTOR] Scale the output GIF by XFACTORxYFACTOR\n\ + --resize-width W Resize to width W and proportional height\n\ + --resize-height H Resize to height H and proportional width\n\ + --resize-fit WxH Shrink to fit WxH, preserving aspect ratio\n\ + --resize-touch WxH Resize to touch WxH, preserving aspect ratio\n\ + --resize-geometry GEOM Resize to a geometry specification\n"); + printf("\ + --resize-method METHOD Set resizing method\n\ + --resize-colors N Resize can add new colors up to N\n\ + --transform-colormap CMD Transform each output colormap by shell CMD\n\ + --use-colormap CMAP Output GIF uses colors from CMAP, which can\n\ + be %sweb%s, %sgray%s, %sbw%s, or a GIF file\n\ + --use-exact-colormap CMAP Same, but use CMAP as is (not a subset)\n\ + --gray Same as --use-colormap gray\n\n", + q1, q0, q1, q0, q1, q0); + printf("\ +Report bugs to .\n\ +Too much information? Try %s%s --help | less%s.\n", q0, program_name, q1); +#ifdef GIF_UNGIF + printf("\ +This version of Gifsicle writes uncompressed GIFs, which can be far larger\n\ +than compressed GIFs. See http://www.lcdf.org/gifsicle for more information.\n"); +#endif +} + + +void +verbose_open(char open, const char *name) +{ + int l = strlen(name); + if (verbose_pos && verbose_pos + 3 + l > 79) { + fputc('\n', stderr); + verbose_pos = 0; + } + if (verbose_pos) { + fputc(' ', stderr); + verbose_pos++; + } + fputc(open, stderr); + fputs(name, stderr); + verbose_pos += 1 + l; +} + + +void +verbose_close(char close) +{ + fputc(close, stderr); + verbose_pos++; +} + + +void +verbose_endline(void) +{ + if (verbose_pos) { + fputc('\n', stderr); + fflush(stderr); + verbose_pos = 0; + } +} + + +/***** + * Info functions + **/ + +const char* debug_color_str(const Gif_Color* gfc) { + static int whichbuf = 0; + static char buf[4][8]; + whichbuf = (whichbuf + 1) % 4; + snprintf(buf[whichbuf], sizeof(buf[whichbuf]), "#%02X%02X%02X", + gfc->gfc_red, gfc->gfc_green, gfc->gfc_blue); + return buf[whichbuf]; +} + +static void +safe_puts(const char *s, uint32_t len, FILE *f) +{ + const char *last_safe = s; + for (; len > 0; len--, s++) + if (*s < ' ' || *s >= 0x7F || *s == '\\') { + if (last_safe != s) { + size_t n = s - last_safe; + if (fwrite(last_safe, 1, n, f) != n) + return; + } + last_safe = s + 1; + switch (*s) { + case '\a': fputs("\\a", f); break; + case '\b': fputs("\\b", f); break; + case '\f': fputs("\\f", f); break; + case '\n': fputs("\\n", f); break; + case '\r': fputs("\\r", f); break; + case '\t': fputs("\\t", f); break; + case '\v': fputs("\\v", f); break; + case '\\': fputs("\\\\", f); break; + case 0: if (len > 1) fputs("\\000", f); break; + default: fprintf(f, "\\%03o", (unsigned char) *s); break; + } + } + if (last_safe != s) { + size_t n = s - last_safe; + if (fwrite(last_safe, 1, n, f) != n) + return; + } +} + + +static void +comment_info(FILE *where, Gif_Comment *gfcom, const char *prefix) +{ + int i; + for (i = 0; i < gfcom->count; i++) { + fputs(prefix, where); + safe_puts(gfcom->str[i], gfcom->len[i], where); + fputc('\n', where); + } +} + + +#define COLORMAP_COLS 4 + +static void +colormap_info(FILE *where, Gif_Colormap *gfcm, const char *prefix) +{ + int i, j; + int nrows = ((gfcm->ncol - 1) / COLORMAP_COLS) + 1; + + for (j = 0; j < nrows; j++) { + int which = j; + fputs(prefix, where); + for (i = 0; i < COLORMAP_COLS && which < gfcm->ncol; i++, which += nrows) { + if (i) fputs(" ", where); + fprintf(where, " %3d: #%02X%02X%02X", which, gfcm->col[which].gfc_red, + gfcm->col[which].gfc_green, gfcm->col[which].gfc_blue); + } + fputc('\n', where); + } +} + + +static void +extension_info(FILE *where, Gif_Stream *gfs, Gif_Extension *gfex, + int count, int image_position) +{ + uint8_t *data = gfex->data; + uint32_t pos = 0; + uint32_t len = gfex->length; + + fprintf(where, " extension %d: ", count); + if (gfex->kind == 255) { + fprintf(where, "app '"); + safe_puts(gfex->appname, gfex->applength, where); + fprintf(where, "'"); + } else { + if (gfex->kind >= 32 && gfex->kind < 127) + fprintf(where, "'%c' (0x%02X)", gfex->kind, gfex->kind); + else + fprintf(where, "0x%02X", gfex->kind); + } + if (image_position >= gfs->nimages) + fprintf(where, " at end"); + else + fprintf(where, " before #%d", image_position); + if (gfex->packetized) + fprintf(where, " packetized"); + fprintf(where, "\n"); + + /* Now, hexl the data. */ + while (len > 0) { + uint32_t row = 16; + uint32_t i; + if (row > len) row = len; + fprintf(where, " %08x: ", pos); + + for (i = 0; i < row; i += 2) { + if (i + 1 >= row) + fprintf(where, "%02x ", data[i]); + else + fprintf(where, "%02x%02x ", data[i], data[i+1]); + } + for (; i < 16; i += 2) + fputs(" ", where); + + putc(' ', where); + for (i = 0; i < row; i++, data++) + putc((*data >= ' ' && *data < 127 ? *data : '.'), where); + putc('\n', where); + + pos += row; + len -= row; + } +} + + +void +stream_info(FILE *where, Gif_Stream *gfs, const char *filename, int flags) +{ + Gif_Extension *gfex; + int n, i; + + if (!gfs) + return; + + verbose_endline(); + fprintf(where, "* %s %d image%s\n", (filename ? filename : ""), + gfs->nimages, gfs->nimages == 1 ? "" : "s"); + fprintf(where, " logical screen %dx%d\n", + gfs->screen_width, gfs->screen_height); + + if (gfs->global) { + fprintf(where, " global color table [%d]\n", gfs->global->ncol); + if (flags & INFO_COLORMAPS) + colormap_info(where, gfs->global, " |"); + fprintf(where, " background %d\n", gfs->background); + } + + if (gfs->end_comment) + comment_info(where, gfs->end_comment, " end comment "); + + if (gfs->loopcount == 0) + fprintf(where, " loop forever\n"); + else if (gfs->loopcount > 0) + fprintf(where, " loop count %u\n", (unsigned)gfs->loopcount); + + n = 0; + for (i = 0; i < gfs->nimages; ++i) + for (gfex = gfs->images[i]->extension_list; gfex; gfex = gfex->next, ++n) + if (flags & INFO_EXTENSIONS) + extension_info(where, gfs, gfex, n, i); + for (gfex = gfs->end_extension_list; gfex; gfex = gfex->next, ++n) + if (flags & INFO_EXTENSIONS) + extension_info(where, gfs, gfex, n, gfs->nimages); + if (n && !(flags & INFO_EXTENSIONS)) + fprintf(where, " extensions %d\n", n); +} + + +static const char *disposal_names[] = { + "none", "asis", "background", "previous", "4", "5", "6", "7" +}; + +void +image_info(FILE *where, Gif_Stream *gfs, Gif_Image *gfi, int flags) +{ + int num; + if (!gfs || !gfi) + return; + num = Gif_ImageNumber(gfs, gfi); + + verbose_endline(); + fprintf(where, " + image #%d ", num); + if (gfi->identifier) + fprintf(where, "#%s ", gfi->identifier); + + fprintf(where, "%dx%d", gfi->width, gfi->height); + if (gfi->left || gfi->top) + fprintf(where, " at %d,%d", gfi->left, gfi->top); + + if (gfi->interlace) + fprintf(where, " interlaced"); + + if (gfi->transparent >= 0) + fprintf(where, " transparent %d", gfi->transparent); + + fprintf(where, "\n"); + + if ((flags & INFO_SIZES) && gfi->compressed) + fprintf(where, " compressed size %u\n", gfi->compressed_len); + + if (gfi->comment) + comment_info(where, gfi->comment, " comment "); + + if (gfi->local) { + fprintf(where, " local color table [%d]\n", gfi->local->ncol); + if (flags & INFO_COLORMAPS) + colormap_info(where, gfi->local, " |"); + } + + if (gfi->disposal || gfi->delay) { + fprintf(where, " "); + if (gfi->disposal) + fprintf(where, " disposal %s", disposal_names[gfi->disposal]); + if (gfi->delay) + fprintf(where, " delay %d.%02ds", + gfi->delay / 100, gfi->delay % 100); + fprintf(where, "\n"); + } +} + + +char * +explode_filename(const char *filename, int number, const char *name, int max_nimages) +{ + static char *s; + int l = strlen(filename); + l += name ? strlen(name) : 10; + + Gif_Delete(s); + s = Gif_NewArray(char, l + 3); + if (name) + snprintf(s, l + 3, "%s.%s", filename, name); + else if (max_nimages <= 1000) + snprintf(s, l + 3, "%s.%03d", filename, number); + else { + int digits; + unsigned j; + unsigned max = (max_nimages < 0 ? 0 : max_nimages); + for (digits = 4, j = 10000; max > j; digits++) + j *= 10; + snprintf(s, l + 3, "%s.%0*d", filename, digits, number); + } + + return s; +} + + +/***** + * parsing functions + **/ + +int frame_spec_1; +int frame_spec_2; +char *frame_spec_name; +int dimensions_x; +int dimensions_y; +int position_x; +int position_y; +Gif_Color parsed_color; +Gif_Color parsed_color2; +double parsed_scale_factor_x; +double parsed_scale_factor_y; + +int +parse_frame_spec(Clp_Parser *clp, const char *arg, int complain, void *thunk) +{ + char *c; + (void)thunk; + + frame_spec_1 = 0; + frame_spec_2 = -1; + frame_spec_name = 0; + + if (!input && !input_name) + input_stream(0); + if (!input) + return 0; + + if (arg[0] != '#') { + if (complain) + return Clp_OptionError(clp, "frame specifications must start with #"); + else + return 0; + } + arg++; + c = (char *)arg; + + /* Get a number range (#x, #x-y, or #x-). First, read x. */ + if (isdigit(c[0])) + frame_spec_1 = frame_spec_2 = strtol(c, &c, 10); + else if (c[0] == '-' && isdigit(c[1])) + frame_spec_1 = frame_spec_2 = Gif_ImageCount(input) + strtol(c, &c, 10); + + /* Then, if the next character is a dash, read y. Be careful to prevent + #- from being interpreted as a frame range. */ + if (c[0] == '-' && (frame_spec_2 >= 0 || c[1] != 0)) { + c++; + if (isdigit(c[0])) + frame_spec_2 = strtol(c, &c, 10); + else if (c[0] == '-' && isdigit(c[1])) + frame_spec_2 = Gif_ImageCount(input) + strtol(c, &c, 10); + else + frame_spec_2 = Gif_ImageCount(input) - 1; + } + + /* It really was a number range (and not a frame name) + only if c is now at the end of the argument. */ + if (c[0] != 0) { + Gif_Image *gfi = Gif_GetNamedImage(input, arg); + if (gfi) { + frame_spec_name = (char *)arg; + frame_spec_1 = frame_spec_2 = Gif_ImageNumber(input, gfi); + return 1; + } else if (complain < 0) /* -1 is special value meaning 'don't complain + about frame NAMES, but do complain about + frame numbers.' */ + return -97; /* Return -97 on bad frame name. */ + else if (complain) + return Clp_OptionError(clp, "no frame named %<#%s%>", arg); + else + return 0; + + } else { + if (frame_spec_1 >= 0 && frame_spec_1 < Gif_ImageCount(input) + && frame_spec_2 >= 0 && frame_spec_2 < Gif_ImageCount(input)) + return 1; + else if (!complain) + return 0; + else + return Clp_OptionError(clp, "frame %<#%s%> out of range, image has %d frames", arg, Gif_ImageCount(input)); + } +} + +int +parse_dimensions(Clp_Parser *clp, const char *arg, int complain, void *thunk) +{ + char *val; + (void)thunk; + + if (*arg == '_' && arg[1] == 'x') { + dimensions_x = 0; + val = (char *)(arg + 1); + } else + dimensions_x = strtol(arg, &val, 10); + if (*val == 'x') { + if (val[1] == '_' && val[2] == 0) { + dimensions_y = 0; + val = val + 2; + } else + dimensions_y = strtol(val + 1, &val, 10); + if (*val == 0) + return 1; + } + + if (complain) + return Clp_OptionError(clp, "invalid dimensions %<%s%> (want WxH)", arg); + else + return 0; +} + +int +parse_position(Clp_Parser *clp, const char *arg, int complain, void *thunk) +{ + char *val; + (void)thunk; + + position_x = strtol(arg, &val, 10); + if (*val == ',') { + position_y = strtol(val + 1, &val, 10); + if (*val == 0) + return 1; + } + + if (complain) + return Clp_OptionError(clp, "invalid position %<%s%> (want 'X,Y')", arg); + else + return 0; +} + +static double strtod_fraction(const char* arg, char** endptr) { + char* val, *val2; + double d = strtod(arg, &val); + if (arg != val && *val == '/') { + double denom = strtod(val + 1, &val2); + if (val2 != val + 1 && denom) { + d /= denom; + val = val2; + } + } + if (endptr) + *endptr = val; + return d; +} + +int +parse_scale_factor(Clp_Parser *clp, const char *arg, int complain, void *thunk) +{ + char *val; + (void)thunk; + + parsed_scale_factor_x = strtod_fraction(arg, &val); + if (*val == 'x') { + parsed_scale_factor_y = strtod_fraction(val + 1, &val); + if (*val == 0) + return 1; + } else if (*val == 0) { + parsed_scale_factor_y = parsed_scale_factor_x; + return 1; + } + + if (complain) + return Clp_OptionError(clp, "invalid scale factor %<%s%> (want XxY)", arg); + else + return 0; +} + +int +parse_rectangle(Clp_Parser *clp, const char *arg, int complain, void *thunk) +{ + const char *input_arg = arg; + char *val; + int x = position_x = strtol(arg, &val, 10); + (void)thunk; + + if (*val == ',') { + int y = position_y = strtol(val + 1, &val, 10); + if (*val == '-' && parse_position(clp, val + 1, 0, 0)) { + if (x >= 0 && y >= 0 + && (position_x <= 0 || x < position_x) + && (position_y <= 0 || y < position_y)) { + /* 18.May.2008: Found it unintuitive that X,Y-0,0 acted like X,Y+-Xx-Y. + Therefore changed it so that X,Y-0,0 acts like X,Y+0,0, and similar + for negative dimensions. Probably safe to change this behavior + since only X,Y+0,0 was documented. */ + dimensions_x = (position_x <= 0 ? -position_x : position_x - x); + dimensions_y = (position_y <= 0 ? -position_y : position_y - y); + position_x = x; + position_y = y; + return 1; + } + } else if (*val == '+' && parse_dimensions(clp, val + 1, 0, 0)) + return 1; + } else if (*val == 'x') { + dimensions_x = position_x; + dimensions_y = strtol(val + 1, &val, 10); + if (*val == 0) { + position_x = position_y = 0; + return 1; + } + } + + if (complain) + return Clp_OptionError(clp, "invalid rectangle %<%s%> (want X1,Y1-X2,Y2 or X1,Y1+WxH", input_arg); + else + return 0; +} + +static int +xvalue(char c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return c - '0'; + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + return c - 'A' + 10; + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + return c - 'a' + 10; + default: + return -1; + } +} + +static int +parse_hex_color_channel(const char *s, int ndigits) +{ + int val1 = xvalue(s[0]); + if (val1 < 0) return -1; + if (ndigits == 1) + return val1 * 16 + val1; + else { + int val2 = xvalue(s[1]); + if (val2 < 0) return -1; + return val1 * 16 + val2; + } +} + +int +parse_color(Clp_Parser *clp, const char *arg, int complain, void *thunk) +{ + const char *input_arg = arg; + char *str; + int red, green, blue; + (void)thunk; + + if (*arg == '#') { + int len = strlen(++arg); + if (len == 0 || len % 3 != 0 + || (int)strspn(arg, "0123456789ABCDEFabcdef") != len) { + if (complain) + Clp_OptionError(clp, "invalid color %<%s%> (want #RGB or #RRGGBB)", + input_arg); + return 0; + } + + len /= 3; + red = parse_hex_color_channel(&arg[ 0 * len ], len); + green = parse_hex_color_channel(&arg[ 1 * len ], len); + blue = parse_hex_color_channel(&arg[ 2 * len ], len); + goto gotrgb; + + } else if (!isdigit(*arg)) + goto error; + + red = strtol(arg, &str, 10); + if (*str == 0) { + if (red < 0 || red > 255) + goto error; + parsed_color.haspixel = 1; + parsed_color.pixel = red; + return 1; + + } else if (*str != ',' && *str != '/') + goto error; + + if (*++str == 0) goto error; + green = strtol(str, &str, 10); + if (*str != ',' && *str != '/') goto error; + + if (*++str == 0) goto error; + blue = strtol(str, &str, 10); + if (*str != 0) goto error; + + gotrgb: + if (red < 0 || green < 0 || blue < 0 + || red > 255 || green > 255 || blue > 255) + goto error; + parsed_color.gfc_red = red; + parsed_color.gfc_green = green; + parsed_color.gfc_blue = blue; + parsed_color.haspixel = 0; + return 1; + + error: + if (complain) + return Clp_OptionError(clp, "invalid color %<%s%>", input_arg); + else + return 0; +} + +int +parse_two_colors(Clp_Parser *clp, const char *arg, int complain, void *thunk) +{ + Gif_Color old_color; + if (parse_color(clp, arg, complain, thunk) <= 0) + return 0; + old_color = parsed_color; + + arg = Clp_Shift(clp, 0); + if (!arg && complain) + return Clp_OptionError(clp, "%<%O%> takes two color arguments"); + else if (!arg) + return 0; + + if (parse_color(clp, arg, complain, thunk) <= 0) + return 0; + + parsed_color2 = parsed_color; + parsed_color = old_color; + return 1; +} + + +/***** + * reading a file as a colormap + **/ + +static Gif_Colormap * +read_text_colormap(FILE *f, const char *name) +{ + char buf[BUFSIZ]; + Gif_Colormap *cm = Gif_NewFullColormap(0, 256); + Gif_Color *col = cm->col; + int ncol = 0; + unsigned red, green, blue; + float fred, fgreen, fblue; + + while (fgets(buf, BUFSIZ, f)) { + + if (sscanf(buf, "%g %g %g", &fred, &fgreen, &fblue) == 3) { + if (fred < 0) fred = 0; + if (fgreen < 0) fgreen = 0; + if (fblue < 0) fblue = 0; + red = (unsigned)(fred + .5); + green = (unsigned)(fgreen + .5); + blue = (unsigned)(fblue + .5); + goto found; + + } else if (buf[0] == '#' + && strspn(buf + 1, "0123456789abcdefABCDEF") == 3 + && (!buf[4] || isspace((unsigned char) buf[4]))) { + sscanf(buf + 1, "%1x%1x%1x", &red, &green, &blue); + red += red << 4; + green += green << 4; + blue += blue << 4; + goto found; + + } else if (buf[0] == '#' + && strspn(buf + 1, "0123456789abcdefABCDEF") == 6 + && (!buf[7] || isspace((unsigned char) buf[7]))) { + sscanf(buf + 1, "%2x%2x%2x", &red, &green, &blue); + found: + if (red > 255) red = 255; + if (green > 255) green = 255; + if (blue > 255) blue = 255; + if (ncol >= 256) { + lerror(name, "maximum 256 colors allowed in colormap"); + break; + } else { + col[ncol].gfc_red = red; + col[ncol].gfc_green = green; + col[ncol].gfc_blue = blue; + ncol++; + } + } + + /* handle too-long lines gracefully */ + if (strchr(buf, '\n') == 0) { + int c; + for (c = getc(f); c != '\n' && c != EOF; c = getc(f)) + ; + } + } + + if (ncol == 0) { + lerror(name, "file not in colormap format"); + Gif_DeleteColormap(cm); + return 0; + } else { + cm->ncol = ncol; + return cm; + } +} + +static void +no_gifread_error(Gif_Stream* gfs, Gif_Image* gfi, + int is_error, const char *message) { + (void) gfs, (void) gfi, (void) is_error, (void) message; +} + +Gif_Colormap * +read_colormap_file(const char *name, FILE *f) +{ + Gif_Colormap *cm = 0; + int c; + int my_file = 0; + + if (name && strcmp(name, "-") == 0) + name = 0; + if (!f) { + my_file = 1; + if (!name) + f = stdin; + else + f = fopen(name, "rb"); + if (!f) { + lerror(name, "%s", name, strerror(errno)); + return 0; + } + } + + name = name ? name : ""; + if (verbosing) verbose_open('<', name); + + c = getc(f); + ungetc(c, f); + if (c == 'G') { + Gif_Stream *gfs = Gif_FullReadFile(f, GIF_READ_COMPRESSED, 0, no_gifread_error); + if (!gfs) + lerror(name, "file not in GIF format"); + else if (!gfs->global + && (gfs->nimages == 0 || !gfs->images[0]->local)) + lerror(name, "can%,t use as palette (no global color table)"); + else { + if (gfs->errors) + lwarning(name, "there were errors reading this GIF"); + cm = Gif_CopyColormap(gfs->global ? gfs->global : gfs->images[0]->local); + } + + Gif_DeleteStream(gfs); + } else + cm = read_text_colormap(f, name); + + if (my_file) fclose(f); + if (verbosing) verbose_close('>'); + return cm; +} + + +/***** + * Frame stuff + **/ + + +Gt_Frameset * +new_frameset(int initial_cap) +{ + Gt_Frameset *fs = Gif_New(Gt_Frameset); + if (initial_cap < 0) initial_cap = 0; + fs->cap = initial_cap; + fs->count = 0; + fs->f = Gif_NewArray(Gt_Frame, initial_cap); + return fs; +} + + +void +clear_def_frame_once_options(void) +{ + /* Get rid of next-frame-only options. + + This causes problems with frame selection. In the command 'gifsicle + -nblah f.gif', the name should be applied to frame 0 of f.gif. This will + happen automatically when f.gif is read, since all of its frames will be + added when it is input. After frame 0, the name in def_frame will be + cleared. + + Now, 'gifsicle -nblah f.gif #1' should apply the name to frame 1 of + f.gif. But once f.gif is input, its frames are added, and the name + component of def_frame is cleared!! So when #1 comes around it's gone! + + We handle this in gifsicle.c using the _change fields. */ + + def_frame.name = 0; + def_frame.comment = 0; + def_frame.extensions = 0; +} + + +Gt_Frame * +add_frame(Gt_Frameset *fset, Gif_Stream *gfs, Gif_Image *gfi) +{ + int number = fset->count++; + while (number >= fset->cap) { + fset->cap *= 2; + Gif_ReArray(fset->f, Gt_Frame, fset->cap); + } + + /* Mark the stream and the image both */ + gfs->refcount++; + gfi->refcount++; + fset->f[number] = def_frame; + fset->f[number].stream = gfs; + fset->f[number].image = gfi; + + clear_def_frame_once_options(); + + return &fset->f[number]; +} + + +static Gt_Frame **merger = 0; +static int nmerger = 0; +static int mergercap = 0; + +static void +merger_add(Gt_Frame *fp) +{ + while (nmerger >= mergercap) + if (mergercap) { + mergercap *= 2; + Gif_ReArray(merger, Gt_Frame *, mergercap); + } else { + mergercap = 16; + merger = Gif_NewArray(Gt_Frame *, mergercap); + } + merger[ nmerger++ ] = fp; +} + + +static void +merger_flatten(Gt_Frameset *fset, int f1, int f2) +{ + int i; + assert(f1 >= 0 && f2 < fset->count); + for (i = f1; i <= f2; i++) { + Gt_Frameset *nest = FRAME(fset, i).nest; + + if (nest && nest->count > 0) { + if (FRAME(fset, i).use < 0 && nest->count == 1) { + /* use < 0 means use the frame's delay, disposal and name (if not + explicitly overridden), but not the frame itself. */ + if (FRAME(nest, 0).delay < 0) + FRAME(nest, 0).delay = FRAME(fset, i).image->delay; + if (FRAME(nest, 0).disposal < 0) + FRAME(nest, 0).disposal = FRAME(fset, i).image->disposal; + if (FRAME(nest, 0).name == 0 && FRAME(nest, 0).no_name == 0) + FRAME(nest, 0).name = + Gif_CopyString(FRAME(fset, i).image->identifier); + } + merger_flatten(nest, 0, nest->count - 1); + } + + if (FRAME(fset, i).use > 0) + merger_add(&FRAME(fset, i)); + } +} + + +static int +find_color_or_error(Gif_Color *color, Gif_Stream *gfs, Gif_Image *gfi, + const char *color_context) +{ + Gif_Colormap *gfcm = gfs->global; + int index; + if (gfi && gfi->local) + gfcm = gfi->local; + + if (color->haspixel == 2) { /* have pixel value, not color */ + if (!gfcm || color->pixel < (uint32_t)gfcm->ncol) + return color->pixel; + else { + if (color_context) + lwarning(gfs->landmark, "%s color out of range", color_context); + return -1; + } + } + + index = gfcm ? Gif_FindColor(gfcm, color) : -1; + if (index < 0 && color_context) + lwarning(gfs->landmark, "%s color not in colormap", color_context); + return index; +} + +static void +set_background(Gif_Stream *gfs, Gt_OutputData *output_data) +{ + Gif_Color background; + int i, j, conflict, want_transparent; + Gif_Image *gfi; + + /* Check for user-specified background. */ + /* If they specified the number, silently cooperate. */ + if (output_data->background.haspixel == 2) { + gfs->background = output_data->background.pixel; + return; + } + + /* Otherwise, if they specified a color, search for it. */ + if (output_data->background.haspixel) { + if (gfs->images[0]->transparent >= 0) { + static int context = 0; + if (!context) { + warning(1, "irrelevant background color\n" + " (The background will appear transparent because" + " the first image contains transparency.)"); + context = 1; + } else + warning(1, "irrelevant background color"); + } + background = output_data->background; + goto search; + } + + /* If we get here, user doesn't care about background. */ + /* Search for required background colors. */ + conflict = want_transparent = background.haspixel = 0; + for (i = j = 0; i < nmerger; i++) { + if (merger[i]->total_crop) /* frame does not correspond to an image */ + continue; + gfi = gfs->images[j]; + if (gfi->disposal == GIF_DISPOSAL_BACKGROUND + || (j == 0 && (gfi->left != 0 || gfi->top != 0 + || gfi->width != gfs->screen_width + || gfi->height != gfs->screen_height))) { + /* transparent.haspixel set below, at merge_frame_done */ + int original_bg_transparent = (merger[i]->transparent.haspixel == 2); + if ((original_bg_transparent && background.haspixel) + || (!original_bg_transparent && want_transparent)) + conflict = 2; + else if (original_bg_transparent) + want_transparent = 1; + else if (merger[i]->transparent.haspixel) { + if (background.haspixel + && !GIF_COLOREQ(&background, &merger[i]->transparent)) + conflict = 1; + else { + background = merger[i]->transparent; + background.haspixel = 1; + } + } + } + j++; + } + + /* Report conflicts. */ + if (conflict || (want_transparent && gfs->images[0]->transparent < 0)) { + static int context = 0; + if (!context) { + warning(1, "input images have conflicting background colors\n" + " (This means some animation frames may appear incorrect.)"); + context = 1; + } else + warning(1, "input images have conflicting background colors"); + } + + /* If no important background color, bag. */ + if (!background.haspixel) { + gfs->background = 0; + return; + } + + search: + i = find_color_or_error(&background, gfs, 0, "background"); + gfs->background = (i >= 0 ? i : 0); +} + + + +static void +fix_total_crop(Gif_Stream *dest, Gif_Image *srci, int merger_index) +{ + /* Salvage any relevant information from a frame that's been completely + cropped away. This ends up being comments and delay. */ + Gt_Frame *fr = merger[merger_index]; + Gt_Frame *next_fr = 0; + Gif_Image *prev_image; + assert(dest->nimages > 0); + prev_image = dest->images[dest->nimages - 1]; + if (merger_index < nmerger - 1) + next_fr = merger[merger_index + 1]; + + /* Don't save identifiers since the frame that was to be identified, is + gone. Save comments though. */ + if (!fr->no_comments && srci->comment && next_fr) { + if (!next_fr->comment) + next_fr->comment = Gif_NewComment(); + merge_comments(next_fr->comment, srci->comment); + } + if (fr->comment && next_fr) { + if (!next_fr->comment) + next_fr->comment = Gif_NewComment(); + merge_comments(next_fr->comment, fr->comment); + Gif_DeleteComment(fr->comment); + fr->comment = 0; + } + + /* Save delay by adding it to the previous frame's delay. */ + if (fr->delay < 0) + fr->delay = srci->delay; + prev_image->delay += fr->delay; + + /* Mark this image as totally cropped. */ + fr->total_crop = 1; +} + + +static void +handle_flip_and_rotate(Gif_Image* desti, Gt_Frame* fr) +{ + desti->left += fr->left_offset; + desti->top += fr->top_offset; + + if (fr->flip_horizontal) + flip_image(desti, fr, 0); + if (fr->flip_vertical) + flip_image(desti, fr, 1); + + if (fr->rotation == 1) + rotate_image(desti, fr, 1); + else if (fr->rotation == 2) { + flip_image(desti, fr, 0); + flip_image(desti, fr, 1); + } else if (fr->rotation == 3) + rotate_image(desti, fr, 3); + + desti->left -= fr->left_offset; + desti->top -= fr->top_offset; +} + +static void +analyze_crop(int nmerger, Gt_Crop* crop, int compress_immediately) +{ + int i, l = 0x7FFFFFFF, r = 0, t = 0x7FFFFFFF, b = 0; + Gif_Stream* cropped_gfs = 0; + + /* find cropped stream */ + for (i = 0; i < nmerger; i++) + if (merger[i]->crop == crop) + cropped_gfs = merger[i]->stream; + + /* find border of frames */ + for (i = 0; i < nmerger; i++) + if (merger[i]->crop == crop) { + Gt_Frame *fr = merger[i]; + int ll, tt, rr, bb; + if (!fr->position_is_offset) { + ll = fr->image->left; + tt = fr->image->top; + rr = fr->image->left + fr->image->width; + bb = fr->image->top + fr->image->height; + } else { + ll = tt = 0; + rr = fr->stream->screen_width; + bb = fr->stream->screen_height; + } + if (ll < l) + l = ll; + if (tt < t) + t = tt; + if (rr > r) + r = rr; + if (bb > b) + b = bb; + } + + if (t > b) + /* total crop */ + l = r = t = b = 0; + + crop->x = crop->spec_x + l; + crop->y = crop->spec_y + t; + crop->w = crop->spec_w + (crop->spec_w <= 0 ? r - crop->x : 0); + crop->h = crop->spec_h + (crop->spec_h <= 0 ? b - crop->y : 0); + crop->left_offset = crop->x; + crop->top_offset = crop->y; + if (crop->x < 0 || crop->y < 0 || crop->w <= 0 || crop->h <= 0 + || crop->x + crop->w > r || crop->y + crop->h > b) { + lerror(cropped_gfs ? cropped_gfs->landmark : (const char*) 0, + "cropping dimensions don%,t fit image"); + crop->ready = 2; + } else + crop->ready = 1; + + /* Remove transparent edges. */ + if (crop->transparent_edges && crop->ready == 1) { + int have_l = crop->x, have_t = crop->y, + have_r = crop->x + crop->w, have_b = crop->y + crop->h; + l = t = 0x7FFFFFFF, r = b = 0; + + for (i = 0; + i < nmerger && (l > have_l || t > have_t || r < have_r || b < have_b); + ++i) { + if (merger[i]->crop == crop) { + Gt_Frame *fr = merger[i]; + Gif_Image *srci = fr->image; + int ll = constrain(have_l, srci->left, have_r), + tt = constrain(have_t, srci->top, have_b), + rr = constrain(have_l, srci->left + srci->width, have_r), + bb = constrain(have_t, srci->top + srci->height, have_b); + + if (srci->transparent >= 0) { + int x, y; + uint8_t **img; + Gif_UncompressImage(fr->stream, srci); + img = srci->img; + + /* Move top edge down over transparency */ + while (tt < bb && tt < t) { + uint8_t *data = img[tt - srci->top]; + for (x = 0; x < srci->width; ++x) + if (data[x] != srci->transparent) + goto found_top; + ++tt; + } + + found_top: + /* Move bottom edge up over transparency */ + while (bb > tt + 1 && bb > b) { + uint8_t *data = img[bb - 1 - srci->top]; + for (x = 0; x < srci->width; ++x) + if (data[x] != srci->transparent) + goto found_bottom; + --bb; + } + + found_bottom: + if (tt < bb) { + /* Move left edge right over transparency */ + while (ll < rr && ll < l) { + for (y = tt - srci->top; y < bb - srci->top; ++y) + if (img[y][ll - srci->left] != srci->transparent) + goto found_left; + ++ll; + } + + found_left: + /* Move right edge left over transparency */ + while (rr > ll + 1 && rr > r) { + for (y = tt - srci->top; y < bb - srci->top; ++y) + if (img[y][rr - 1 - srci->left] != srci->transparent) + goto found_right; + --rr; + } + } + + found_right: + if (compress_immediately > 0 && srci->compressed) + Gif_ReleaseUncompressedImage(srci); + } + + if (tt < bb) { + if (ll < l) + l = ll; + if (tt < t) + t = tt; + if (rr > r) + r = rr; + if (bb > b) + b = bb; + } + } + } + + if (t > b) { + crop->w = crop->h = 0; + } else { + crop->x = l; + crop->y = t; + crop->w = r - l; + crop->h = b - t; + crop->left_offset = crop->x; + crop->top_offset = crop->y; + } + } +} + + +static inline int +apply_frame_transparent(Gif_Image *gfi, Gt_Frame *fr) +{ + int old_transparent = gfi->transparent; + if (fr->transparent.haspixel == 255) + gfi->transparent = -1; + else if (fr->transparent.haspixel) { + gfi->transparent = + find_color_or_error(&fr->transparent, fr->stream, gfi, + "transparent"); + if (gfi->transparent < 0) + fr->transparent.haspixel = 0; + } + return old_transparent; +} + +static void mark_used_background_color(Gt_Frame* fr) { + Gif_Stream* gfs = fr->stream; + Gif_Image* gfi = fr->image; + if ((fr->transparent.haspixel != 0 + ? fr->transparent.haspixel != 255 + : gfi->transparent < 0) + && ((fr->disposal >= 0 + ? fr->disposal + : gfi->disposal) == GIF_DISPOSAL_BACKGROUND + || gfi->left != 0 + || gfi->top != 0 + || gfi->width != gfs->screen_width + || gfi->height != gfs->screen_height) + && gfs->global + && gfs->background < gfs->global->ncol) + gfs->global->col[gfs->background].haspixel |= 1; +} + +Gif_Stream * +merge_frame_interval(Gt_Frameset *fset, int f1, int f2, + Gt_OutputData *output_data, int compress_immediately, + int *huge_stream) +{ + Gif_Stream *dest = Gif_NewStream(); + Gif_Colormap *global = Gif_NewFullColormap(256, 256); + int i, same_compressed_ok, all_same_compressed_ok; + + global->ncol = 0; + dest->global = global; + if (output_data->active_output_name) + dest->landmark = output_data->active_output_name; + /* 11/23/98 A new stream's screen size is 0x0; we'll use the max of the + merged-together streams' screen sizes by default (in merge_stream()) */ + + if (f2 < 0) + f2 = fset->count - 1; + nmerger = 0; + merger_flatten(fset, f1, f2); + if (nmerger == 0) { + error(1, "empty output GIF not written"); + return 0; + } + + /* decide whether stream is huge */ + { + size_t s; + for (i = s = 0; i < nmerger; i++) + s += (((unsigned) merger[i]->image->width + * (unsigned) merger[i]->image->height) / 1024) + 1; + *huge_stream = (s > 200 * 1024); /* 200 MB */ + if (*huge_stream && !compress_immediately) { + warning(1, "huge GIF, conserving memory (processing may take a while)"); + compress_immediately = 1; + } + } + + /* merge stream-specific info and clear colormaps */ + for (i = 0; i < nmerger; i++) + merger[i]->stream->user_flags = 1; + for (i = 0; i < nmerger; i++) { + if (merger[i]->stream->user_flags) { + Gif_Stream *src = merger[i]->stream; + Gif_CalculateScreenSize(src, 0); + /* merge_stream() unmarks the global colormap */ + merge_stream(dest, src, merger[i]->no_comments); + src->user_flags = 0; + } + if (merger[i]->image->local) + unmark_colors_2(merger[i]->image->local); + } + + /* is it ok to save the same compressed image? This is true only if we + will recompress later from scratch. */ + /* 13.Aug.2002 - Can't save the same compressed image if we crop, so turn + off all_same_compressed_ok below ('analyze crops'). Reported by Tom + Schumm . */ + if (output_data->colormap_size > 0 + || output_data->colormap_fixed + || (output_data->optimizing & GT_OPT_MASK) + || output_data->scaling > 0) + all_same_compressed_ok = 1; + else + all_same_compressed_ok = 0; + + /* analyze crops */ + for (i = 0; i < nmerger; i++) { + if (merger[i]->crop) + merger[i]->crop->ready = all_same_compressed_ok = 0; + } + for (i = 0; i < nmerger; i++) { + if (merger[i]->crop && !merger[i]->crop->ready) + analyze_crop(nmerger, merger[i]->crop, compress_immediately); + } + + /* mark used colors */ + for (i = 0; i < nmerger; ++i) { + int old_transp = apply_frame_transparent(merger[i]->image, merger[i]); + mark_used_colors(merger[i]->stream, merger[i]->image, merger[i]->crop, + compress_immediately); + merger[i]->image->transparent = old_transp; + mark_used_background_color(merger[i]); + } + + /* copy stream-wide information from output_data */ + if (output_data->loopcount > -2) + dest->loopcount = output_data->loopcount; + dest->screen_width = dest->screen_height = 0; + + /** ACTUALLY MERGE FRAMES INTO THE NEW STREAM **/ + for (i = 0; i < nmerger; i++) { + Gt_Frame *fr = merger[i]; + Gif_Image *srci; + Gif_Image *desti; + int old_transp; + + /* Make a copy of the image and crop it if we're cropping */ + fr->left_offset = fr->top_offset = 0; + if (fr->crop) { + int preserve_total_crop; + srci = Gif_CopyImage(fr->image); + Gif_UncompressImage(fr->stream, srci); + + /* Zero-delay frames are a special case. You might think it was OK to + get rid of totally-cropped delay-0 frames, but many browsers treat + zero-delay frames as 100ms. So don't completely crop a zero-delay + frame: leave it around. Problem reported by Calle Kabo. */ + preserve_total_crop = (dest->nimages == 0 || fr->delay == 0 + || (fr->delay < 0 && srci->delay == 0)); + + if (!crop_image(srci, fr, preserve_total_crop)) { + /* We cropped the image out of existence! Be careful not to make 0x0 + frames. */ + fix_total_crop(dest, srci, i); + goto merge_frame_done; + } + } else { + srci = fr->image; + Gif_UncompressImage(fr->stream, srci); + } + + old_transp = apply_frame_transparent(srci, fr); + + /* Is it ok to use the old image's compressed version? */ + /* 11.Feb.2003 -- It is NOT ok to use the old image's compressed version + if you are going to flip or rotate the image. Crash reported by Dan + Lasley . */ + same_compressed_ok = all_same_compressed_ok; + if ((fr->interlacing >= 0 && fr->interlacing != srci->interlace) + || fr->flip_horizontal || fr->flip_vertical || fr->rotation) + same_compressed_ok = 0; + + desti = merge_image(dest, fr->stream, srci, fr, same_compressed_ok); + + srci->transparent = old_transp; /* restore real transparent value */ + + /* Flipping and rotating */ + if (fr->flip_horizontal || fr->flip_vertical || fr->rotation) { + handle_flip_and_rotate(desti, fr); + } + + /* Names and comments */ + if (fr->name || fr->no_name) { + Gif_DeleteArray(desti->identifier); + desti->identifier = Gif_CopyString(fr->name); + } + if (fr->no_comments && desti->comment) { + Gif_DeleteComment(desti->comment); + desti->comment = 0; + } + if (fr->comment) { + if (!desti->comment) + desti->comment = Gif_NewComment(); + merge_comments(desti->comment, fr->comment); + /* delete the comment early to help with memory; set field to 0 so we + don't re-free it later */ + Gif_DeleteComment(fr->comment); + fr->comment = 0; + } + + if (fr->interlacing >= 0) { + desti->interlace = fr->interlacing; + } + if (fr->left >= 0) { + int left = fr->left + (fr->position_is_offset ? desti->left : 0); + if (left + desti->width > 65535) { + error(1, "left position %d out of range", left); + return 0; + } + desti->left = left; + } + if (fr->top >= 0) { + int top = fr->top + (fr->position_is_offset ? desti->top : 0); + if (top + desti->height > 65535) { + error(1, "top position %d out of range", top); + return 0; + } + desti->top = top; + } + + if (fr->delay >= 0) { + desti->delay = fr->delay; + } + if (fr->disposal >= 0) { + desti->disposal = fr->disposal; + } + + /* logical screen */ + if (output_data->screen_mode <= 0) { + int w, h; + if (output_data->screen_mode < 0 + || fr->crop + || ((fr->left >= 0 || fr->top >= 0) && !fr->position_is_offset)) { + w = desti->left + desti->width; + h = desti->top + desti->height; + } else { + if (fr->rotation == 1 || fr->rotation == 3) { + w = fr->stream->screen_height; + h = fr->stream->screen_width; + } else { + w = fr->stream->screen_width; + h = fr->stream->screen_height; + } + if (fr->left >= 0) { + w += fr->left; + } + if (fr->top >= 0) { + h += fr->top; + } + } + if (w > dest->screen_width) { + dest->screen_width = w; + } + if (h > dest->screen_height) { + dest->screen_height = h; + } + } + + /* compress immediately if possible to save on memory */ + if (desti->img) { + if (compress_immediately > 0) { + Gif_FullCompressImage(dest, desti, &gif_write_info); + Gif_ReleaseUncompressedImage(desti); + } else if (desti->compressed) + Gif_ReleaseCompressedImage(desti); + } else if (compress_immediately <= 0) { + Gif_UncompressImage(dest, desti); + Gif_ReleaseCompressedImage(desti); + } + + merge_frame_done: + /* 6/17/02 store information about image's background */ + if (fr->stream->images[0]->transparent >= 0) { + fr->transparent.haspixel = 2; + } else if (fr->stream->global + && fr->stream->background < fr->stream->global->ncol) { + fr->transparent = fr->stream->global->col[fr->stream->background]; + fr->transparent.haspixel = 1; + } else { + fr->transparent.haspixel = 0; + } + + /* Destroy the copied, cropped image if necessary */ + if (fr->crop) { + Gif_DeleteImage(srci); + } + + /* if we can, delete the image's data right now to save memory */ + srci = fr->image; + assert(srci->refcount > 1); + if (--srci->refcount == 1) { + /* only 1 reference ==> the reference is from the input stream itself */ + Gif_ReleaseUncompressedImage(srci); + Gif_ReleaseCompressedImage(srci); + } + fr->image = 0; + + /* 5/26/98 Destroy the stream now to help with memory. Assumes that + all frames are added with add_frame() which properly increments the + stream's refcount. Set field to 0 so we don't redelete */ + Gif_DeleteStream(fr->stream); + fr->stream = 0; + } + /** END MERGE LOOP **/ + + if (output_data->screen_mode == 1) { + dest->screen_width = output_data->screen_width; + dest->screen_height = output_data->screen_height; + } + + /* Find the background color in the colormap, or add it if we can */ + set_background(dest, output_data); + + /* The global colormap might be empty even given images that use it, if + those images are entirely transparent. Since absent global colormaps + are surprising, assign a black-and-white colormap */ + if (dest->global->ncol == 0) { + for (i = 0; i != dest->nimages; ++i) + if (!dest->images[i]->local) { + GIF_SETCOLOR(&dest->global->col[0], 0, 0, 0); + GIF_SETCOLOR(&dest->global->col[1], 255, 255, 255); + dest->global->ncol = 2; + break; + } + } + + return dest; +} + + +void +blank_frameset(Gt_Frameset *fset, int f1, int f2, int delete_object) +{ + int i; + if (delete_object) f1 = 0, f2 = -1; + if (f2 < 0) f2 = fset->count - 1; + for (i = f1; i <= f2; i++) { + /* We may have deleted stream and image earlier to save on memory; see + above in merge_frame_interval(); but if we didn't, do it now. */ + if (FRAME(fset, i).image && FRAME(fset, i).image->refcount > 1) + FRAME(fset, i).image->refcount--; + Gif_DeleteStream(FRAME(fset, i).stream); + Gif_DeleteComment(FRAME(fset, i).comment); + if (FRAME(fset, i).nest) + blank_frameset(FRAME(fset, i).nest, 0, 0, 1); + } + if (delete_object) { + Gif_DeleteArray(fset->f); + Gif_Delete(fset); + } +} + + +void +clear_frameset(Gt_Frameset *fset, int f1) +{ + blank_frameset(fset, f1, -1, 0); + fset->count = f1; +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/ungifwrt.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/ungifwrt.c new file mode 100644 index 0000000..713a738 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/ungifwrt.c @@ -0,0 +1,847 @@ +/* -*- mode: c; c-basic-offset: 2 -*- */ +/* ungifwrt.c - Functions to write unGIFs -- GIFs with run-length compression, + not LZW compression. Idea due to Hutchinson Avenue Software Corporation + . + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of the LCDF GIF library. + + The LCDF GIF library is free software. It is distributed under the GNU + General Public License, version 2; you can copy, distribute, or alter it at + will, as long as this notice is kept intact and this source code is made + available. There is no warranty, express or implied. */ + +#if HAVE_CONFIG_H +# include +#elif !defined(__cplusplus) && !defined(inline) +/* Assume we don't have inline by default */ +# define inline +#endif +#include +#include +#include +#include +#ifdef __cplusplus +extern "C" { +#endif + +#define WRITE_BUFFER_SIZE 255 + +struct Gif_Writer { + FILE *f; + uint8_t *v; + uint32_t pos; + uint32_t cap; + Gif_CompressInfo gcinfo; + int global_size; + int local_size; + int errors; + int cleared; + Gif_Code* rle_next; + void (*byte_putter)(uint8_t, struct Gif_Writer *); + void (*block_putter)(const uint8_t *, size_t, struct Gif_Writer *); +}; + + +#define gifputbyte(b, grr) ((*grr->byte_putter)(b, grr)) +#define gifputblock(b, l, grr) ((*grr->block_putter)(b, l, grr)) + +static inline void +gifputunsigned(uint16_t uns, Gif_Writer *grr) +{ + gifputbyte(uns & 0xFF, grr); + gifputbyte(uns >> 8, grr); +} + + +static void +file_byte_putter(uint8_t b, Gif_Writer *grr) +{ + fputc(b, grr->f); +} + +static void +file_block_putter(const uint8_t *block, size_t size, Gif_Writer *grr) +{ + if (fwrite(block, 1, size, grr->f) != size) + grr->errors = 1; +} + + +static void +memory_byte_putter(uint8_t b, Gif_Writer *grr) +{ + if (grr->pos >= grr->cap) { + grr->cap = (grr->cap ? grr->cap * 2 : 1024); + Gif_ReArray(grr->v, uint8_t, grr->cap); + } + if (grr->v) { + grr->v[grr->pos] = b; + grr->pos++; + } +} + +static void +memory_block_putter(const uint8_t *data, size_t len, Gif_Writer *grr) +{ + while (grr->pos + len >= grr->cap) { + grr->cap = (grr->cap ? grr->cap * 2 : 1024); + Gif_ReArray(grr->v, uint8_t, grr->cap); + } + if (grr->v) { + memcpy(grr->v + grr->pos, data, len); + grr->pos += len; + } +} + + +static int +gif_writer_init(Gif_Writer* grr, FILE* f, const Gif_CompressInfo* gcinfo) +{ + grr->f = f; + grr->v = NULL; + grr->pos = grr->cap = 0; + if (gcinfo) + grr->gcinfo = *gcinfo; + else + Gif_InitCompressInfo(&grr->gcinfo); + grr->errors = 0; + grr->cleared = 0; + /* 27.Jul.2001: Must allocate GIF_MAX_CODE + 1 because we assign to + rle_next[GIF_MAX_CODE]! Thanks, Jeff Brown , for + supplying the buggy files. */ + grr->rle_next = Gif_NewArray(Gif_Code, GIF_MAX_CODE + 1); + if (f) { + grr->byte_putter = file_byte_putter; + grr->block_putter = file_block_putter; + } else { + grr->byte_putter = memory_byte_putter; + grr->block_putter = memory_block_putter; + } + return grr->rle_next != 0; +} + +static void +gif_writer_cleanup(Gif_Writer* grr) +{ + Gif_DeleteArray(grr->v); + Gif_DeleteArray(grr->rle_next); +} + + +static inline const uint8_t * +gif_imageline(Gif_Image *gfi, unsigned pos) +{ + unsigned y, x; + if (gfi->width == 0) + return NULL; + y = pos / gfi->width; + x = pos - y * gfi->width; + if (y == (unsigned) gfi->height) + return NULL; + else if (!gfi->interlace) + return gfi->img[y] + x; + else + return gfi->img[Gif_InterlaceLine(y, gfi->height)] + x; +} + +static inline unsigned +gif_line_endpos(Gif_Image *gfi, unsigned pos) +{ + unsigned y = pos / gfi->width; + return (y + 1) * gfi->width; +} + +/* Write GIFs compressed with run-length encoding, an idea from code by + Hutchinson Avenue Software Corporation found in + Thomas Boutell's gd library . */ + +static int +write_compressed_data(Gif_Image *gfi, + int min_code_bits, Gif_Writer *grr) +{ + uint8_t stack_buffer[512 - 24]; + uint8_t *buf = stack_buffer; + unsigned bufpos = 0; + unsigned bufcap = sizeof(stack_buffer) * 8; + + unsigned pos; + unsigned clear_bufpos, clear_pos; + unsigned line_endpos; + const uint8_t *imageline; + + unsigned run; +#ifndef GIF_NO_COMPRESSION +#define RUN_EWMA_SHIFT 4 +#define RUN_EWMA_SCALE 19 +#define RUN_INV_THRESH ((unsigned) (1 << RUN_EWMA_SCALE) / 3000) + unsigned run_ewma; + Gif_Code* rle_next = grr->rle_next; +#endif + + Gif_Code next_code = 0; + Gif_Code output_code; + uint8_t suffix; + + int cur_code_bits; + + /* Here we go! */ + gifputbyte(min_code_bits, grr); +#define CLEAR_CODE ((Gif_Code) (1 << min_code_bits)) +#define EOI_CODE ((Gif_Code) (CLEAR_CODE + 1)) +#define CUR_BUMP_CODE (1 << cur_code_bits) + grr->cleared = 0; + + cur_code_bits = min_code_bits + 1; + /* next_code set by first runthrough of output clear_code */ + GIF_DEBUG(("clear(%d) eoi(%d) bits(%d) ", CLEAR_CODE, EOI_CODE, cur_code_bits)); + + output_code = CLEAR_CODE; + /* Because output_code is clear_code, we'll initialize next_code, et al. + below. */ + + pos = clear_pos = clear_bufpos = 0; + line_endpos = gfi->width; + imageline = gif_imageline(gfi, pos); + + while (1) { + + /***** + * Output 'output_code' to the memory buffer. */ + if (bufpos + 32 >= bufcap) { + unsigned ncap = bufcap * 2 + (24 << 3); + uint8_t *nbuf = Gif_NewArray(uint8_t, ncap >> 3); + if (!nbuf) + goto error; + memcpy(nbuf, buf, bufcap >> 3); + if (buf != stack_buffer) + Gif_DeleteArray(buf); + buf = nbuf; + bufcap = ncap; + } + + { + unsigned endpos = bufpos + cur_code_bits; + do { + if (bufpos & 7) + buf[bufpos >> 3] |= output_code << (bufpos & 7); + else if (bufpos & 0x7FF) + buf[bufpos >> 3] = output_code >> (bufpos - endpos + cur_code_bits); + else { + buf[bufpos >> 3] = 255; + endpos += 8; + } + + bufpos += 8 - (bufpos & 7); + } while (bufpos < endpos); + bufpos = endpos; + } + + + /***** + * Handle special codes. */ + + if (output_code == CLEAR_CODE) { + /* Clear data and prepare gfc */ + cur_code_bits = min_code_bits + 1; + next_code = EOI_CODE + 1; +#ifndef GIF_NO_COMPRESSION + { + Gif_Code c; + for (c = 0; c < CLEAR_CODE; ++c) + rle_next[c] = CLEAR_CODE; + } + run_ewma = 1 << RUN_EWMA_SCALE; +#endif + run = 0; + clear_pos = clear_bufpos = 0; + + GIF_DEBUG(("clear ")); + + } else if (output_code == EOI_CODE) + break; + + else { + if (next_code > CUR_BUMP_CODE && cur_code_bits < GIF_MAX_CODE_BITS) + /* bump up compression size */ + ++cur_code_bits; + +#ifndef GIF_NO_COMPRESSION + /* Adjust current run length average. */ + run = (run << RUN_EWMA_SCALE) + (1 << (RUN_EWMA_SHIFT - 1)); + if (run < run_ewma) + run_ewma -= (run_ewma - run) >> RUN_EWMA_SHIFT; + else + run_ewma += (run - run_ewma) >> RUN_EWMA_SHIFT; +#else + if (cur_code_bits != min_code_bits) { + /* never bump up compression size -- keep cur_code_bits small by + generating clear_codes */ + output_code = CLEAR_CODE; + continue; + } +#endif + + /* Reset run length. */ + run = 0; + } + + + /***** + * Find the next code to output. */ + + /* If height is 0 -- no more pixels to write -- we output work_node next + time around. */ + if (imageline) { + output_code = suffix = *imageline; + + while (1) { + imageline++; + pos++; + if (pos == line_endpos) { + imageline = gif_imageline(gfi, pos); + line_endpos += gfi->width; + } + run++; +#ifndef GIF_NO_COMPRESSION + if (!imageline || *imageline != suffix || rle_next[output_code] == CLEAR_CODE) + break; + output_code = rle_next[output_code]; +#else + break; +#endif + } + + /* Output the current code. */ + if (next_code < GIF_MAX_CODE) { +#ifndef GIF_NO_COMPRESSION + if (imageline && *imageline == suffix) { + rle_next[output_code] = next_code; + rle_next[next_code] = CLEAR_CODE; + } +#endif + next_code++; + } else + next_code = GIF_MAX_CODE + 1; /* to match "> CUR_BUMP_CODE" above */ + + /* Check whether to clear table. */ + if (next_code > 4094) { + int do_clear = grr->gcinfo.flags & GIF_WRITE_EAGER_CLEAR; + +#ifndef GIF_NO_COMPRESSION + if (!do_clear) { + unsigned pixels_left = (unsigned) gfi->width * (unsigned) gfi->height - pos; + if (pixels_left) { + /* Always clear if run_ewma gets small relative to + min_code_bits. Otherwise, clear if #images/run is smaller + than an empirical threshold, meaning it will take more than + 3000 or so average runs to complete the image. */ + if (run_ewma < ((36U << RUN_EWMA_SCALE) / min_code_bits) + || pixels_left > UINT_MAX / RUN_INV_THRESH + || run_ewma < pixels_left * RUN_INV_THRESH) + do_clear = 1; + } + } +#else + do_clear = 1; +#endif + + if ((do_clear || run < 7) && !clear_pos) { + clear_pos = pos - run; + clear_bufpos = bufpos; + } else if (!do_clear && run > 50) + clear_pos = clear_bufpos = 0; + + if (do_clear) { + GIF_DEBUG(("rewind %u pixels/%d bits ", pos - clear_pos, bufpos + cur_code_bits - clear_bufpos)); + output_code = CLEAR_CODE; + pos = clear_pos; + imageline = gif_imageline(gfi, pos); + line_endpos = gif_line_endpos(gfi, pos); + bufpos = clear_bufpos; + buf[bufpos >> 3] &= (1 << (bufpos & 7)) - 1; + grr->cleared = 1; + } + } + } else + output_code = EOI_CODE; + } + + /* Output memory buffer to stream. */ + bufpos = (bufpos + 7) >> 3; + buf[(bufpos - 1) & 0xFFFFFF00] = (bufpos - 1) & 0xFF; + buf[bufpos] = 0; + gifputblock(buf, bufpos + 1, grr); + + if (buf != stack_buffer) + Gif_DeleteArray(buf); + return 1; + + error: + if (buf != stack_buffer) + Gif_DeleteArray(buf); + return 0; +} + + +static int +calculate_min_code_bits(Gif_Image *gfi, const Gif_Writer *grr) +{ + int colors_used = -1, min_code_bits, i; + + if (grr->gcinfo.flags & GIF_WRITE_CAREFUL_MIN_CODE_SIZE) { + /* calculate m_c_b based on colormap */ + if (grr->local_size > 0) + colors_used = grr->local_size; + else if (grr->global_size > 0) + colors_used = grr->global_size; + + } else if (gfi->img) { + /* calculate m_c_b from uncompressed data */ + int x, y, width = gfi->width, height = gfi->height; + colors_used = 0; + for (y = 0; y < height && colors_used < 128; y++) { + uint8_t *data = gfi->img[y]; + for (x = width; x > 0; x--, data++) + if (*data > colors_used) + colors_used = *data; + } + colors_used++; + + } else if (gfi->compressed) { + /* take m_c_b from compressed image */ + colors_used = 1 << gfi->compressed[0]; + + } else { + /* should never happen */ + colors_used = 256; + } + + min_code_bits = 2; /* min_code_bits of 1 isn't allowed */ + i = 4; + while (i < colors_used) { + min_code_bits++; + i *= 2; + } + + return min_code_bits; +} + + +static int get_color_table_size(const Gif_Stream *gfs, Gif_Image *gfi, + Gif_Writer *grr); + +static void +save_compression_result(Gif_Image *gfi, Gif_Writer *grr, int ok) +{ + if (!(grr->gcinfo.flags & GIF_WRITE_SHRINK) + || (ok && (!gfi->compressed || gfi->compressed_len > grr->pos))) { + if (gfi->compressed) + (*gfi->free_compressed)((void *) gfi->compressed); + if (ok) { + gfi->compressed_len = grr->pos; + gfi->compressed_errors = 0; + gfi->compressed = grr->v; + gfi->free_compressed = Gif_Free; + grr->v = 0; + grr->cap = 0; + } else + gfi->compressed = 0; + } + grr->pos = 0; +} + +int +Gif_FullCompressImage(Gif_Stream *gfs, Gif_Image *gfi, + const Gif_CompressInfo *gcinfo) +{ + int ok = 0; + uint8_t min_code_bits; + Gif_Writer grr; + + if (!gif_writer_init(&grr, NULL, gcinfo)) { + if (!(grr.gcinfo.flags & GIF_WRITE_SHRINK)) + Gif_ReleaseCompressedImage(gfi); + goto done; + } + + grr.global_size = get_color_table_size(gfs, 0, &grr); + grr.local_size = get_color_table_size(gfs, gfi, &grr); + + min_code_bits = calculate_min_code_bits(gfi, &grr); + ok = write_compressed_data(gfi, min_code_bits, &grr); + save_compression_result(gfi, &grr, ok); + + if ((grr.gcinfo.flags & (GIF_WRITE_OPTIMIZE | GIF_WRITE_EAGER_CLEAR)) + == GIF_WRITE_OPTIMIZE + && grr.cleared && ok) { + grr.gcinfo.flags |= GIF_WRITE_EAGER_CLEAR | GIF_WRITE_SHRINK; + if (write_compressed_data(gfi, min_code_bits, &grr)) + save_compression_result(gfi, &grr, 1); + } + + done: + gif_writer_cleanup(&grr); + return ok; +} + + +static int +get_color_table_size(const Gif_Stream *gfs, Gif_Image *gfi, Gif_Writer *grr) +{ + Gif_Colormap *gfcm = (gfi ? gfi->local : gfs->global); + int ncol, totalcol, i; + + if (!gfcm || gfcm->ncol <= 0) + return 0; + + /* Make sure ncol is reasonable */ + ncol = gfcm->ncol; + + /* Possibly bump up 'ncol' based on 'transparent' values, if + careful_min_code_bits */ + if (grr->gcinfo.flags & GIF_WRITE_CAREFUL_MIN_CODE_SIZE) { + if (gfi && gfi->transparent >= ncol) + ncol = gfi->transparent + 1; + else if (!gfi) + for (i = 0; i < gfs->nimages; i++) + if (gfs->images[i]->transparent >= ncol) + ncol = gfs->images[i]->transparent + 1; + } + + /* Make sure the colormap is a power of two entries! */ + /* GIF format doesn't allow a colormap with only 1 entry. */ + if (ncol > 256) + ncol = 256; + for (totalcol = 2; totalcol < ncol; totalcol *= 2) + /* nada */; + + return totalcol; +} + +static void +write_color_table(Gif_Colormap *gfcm, int totalcol, Gif_Writer *grr) +{ + Gif_Color *c = gfcm->col; + int i, ncol = gfcm->ncol; + + for (i = 0; i < ncol && i < totalcol; i++, c++) { + gifputbyte(c->gfc_red, grr); + gifputbyte(c->gfc_green, grr); + gifputbyte(c->gfc_blue, grr); + } + + /* Pad out colormap with black. */ + for (; i < totalcol; i++) { + gifputbyte(0, grr); + gifputbyte(0, grr); + gifputbyte(0, grr); + } +} + + +static int +write_image(Gif_Stream *gfs, Gif_Image *gfi, Gif_Writer *grr) +{ + uint8_t min_code_bits, packed = 0; + grr->local_size = get_color_table_size(gfs, gfi, grr); + + gifputbyte(',', grr); + gifputunsigned(gfi->left, grr); + gifputunsigned(gfi->top, grr); + gifputunsigned(gfi->width, grr); + gifputunsigned(gfi->height, grr); + + if (grr->local_size > 0) { + int size = 2; + packed |= 0x80; + while (size < grr->local_size) + size *= 2, packed++; + } + + if (gfi->interlace) packed |= 0x40; + gifputbyte(packed, grr); + + if (grr->local_size > 0) + write_color_table(gfi->local, grr->local_size, grr); + + /* calculate min_code_bits here (because calculation may involve + recompression, if GIF_WRITE_CAREFUL_MIN_CODE_SIZE is true) */ + min_code_bits = calculate_min_code_bits(gfi, grr); + + /* use existing compressed data if it exists. This will tend to whip + people's asses who uncompress an image, keep the compressed data around, + but modify the uncompressed data anyway. That sucks. */ + if (gfi->compressed + && (!(grr->gcinfo.flags & GIF_WRITE_CAREFUL_MIN_CODE_SIZE) + || gfi->compressed[0] == min_code_bits)) { + uint8_t *compressed = gfi->compressed; + uint32_t compressed_len = gfi->compressed_len; + while (compressed_len > 0) { + uint16_t amt = (compressed_len > 0x7000 ? 0x7000 : compressed_len); + gifputblock(compressed, amt, grr); + compressed += amt; + compressed_len -= amt; + } + + } else if (!gfi->img) { + Gif_UncompressImage(gfs, gfi); + write_compressed_data(gfi, min_code_bits, grr); + Gif_ReleaseUncompressedImage(gfi); + + } else + write_compressed_data(gfi, min_code_bits, grr); + + return 1; +} + + +static void +write_logical_screen_descriptor(Gif_Stream *gfs, Gif_Writer *grr) +{ + uint8_t packed = 0x70; /* high resolution colors */ + grr->global_size = get_color_table_size(gfs, 0, grr); + + Gif_CalculateScreenSize(gfs, 0); + gifputunsigned(gfs->screen_width, grr); + gifputunsigned(gfs->screen_height, grr); + + if (grr->global_size > 0) { + uint16_t size = 2; + packed |= 0x80; + while (size < grr->global_size) + size *= 2, packed++; + } + + gifputbyte(packed, grr); + if (gfs->background < grr->global_size) + gifputbyte(gfs->background, grr); + else + gifputbyte(255, grr); + gifputbyte(0, grr); /* no aspect ratio information */ + + if (grr->global_size > 0) + write_color_table(gfs->global, grr->global_size, grr); +} + + +/* extension byte table: + 0x01 plain text extension + 0xCE name* + 0xF9 graphic control extension + 0xFE comment extension + 0xFF application extension + */ + +static void +write_graphic_control_extension(Gif_Image *gfi, Gif_Writer *grr) +{ + uint8_t packed = 0; + gifputbyte('!', grr); + gifputbyte(0xF9, grr); + gifputbyte(4, grr); + if (gfi->transparent >= 0) packed |= 0x01; + packed |= (gfi->disposal & 0x07) << 2; + gifputbyte(packed, grr); + gifputunsigned(gfi->delay, grr); + gifputbyte((uint8_t)gfi->transparent, grr); + gifputbyte(0, grr); +} + + +static void +blast_data(const uint8_t *data, int len, Gif_Writer *grr) +{ + while (len > 0) { + int s = len > 255 ? 255 : len; + gifputbyte(s, grr); + gifputblock(data, s, grr); + data += s; + len -= s; + } + gifputbyte(0, grr); +} + + +static void +write_name_extension(char *id, Gif_Writer *grr) +{ + gifputbyte('!', grr); + gifputbyte(0xCE, grr); + blast_data((uint8_t *)id, strlen(id), grr); +} + + +static void +write_comment_extensions(Gif_Comment *gfcom, Gif_Writer *grr) +{ + int i; + for (i = 0; i < gfcom->count; i++) { + gifputbyte('!', grr); + gifputbyte(0xFE, grr); + blast_data((const uint8_t *)gfcom->str[i], gfcom->len[i], grr); + } +} + + +static void +write_netscape_loop_extension(uint16_t value, Gif_Writer *grr) +{ + gifputblock((const uint8_t *)"!\xFF\x0BNETSCAPE2.0\x03\x01", 16, grr); + gifputunsigned(value, grr); + gifputbyte(0, grr); +} + + +static void +write_generic_extension(Gif_Extension *gfex, Gif_Writer *grr) +{ + uint32_t pos = 0; + if (gfex->kind < 0) return; /* ignore our private extensions */ + + gifputbyte('!', grr); + gifputbyte(gfex->kind, grr); + if (gfex->kind == 255) { /* an application extension */ + if (gfex->applength) { + gifputbyte(gfex->applength, grr); + gifputblock((const uint8_t*) gfex->appname, gfex->applength, grr); + } + } + if (gfex->packetized) + gifputblock(gfex->data, gfex->length, grr); + else { + while (pos + 255 < gfex->length) { + gifputbyte(255, grr); + gifputblock(gfex->data + pos, 255, grr); + pos += 255; + } + if (pos < gfex->length) { + uint32_t len = gfex->length - pos; + gifputbyte(len, grr); + gifputblock(gfex->data + pos, len, grr); + } + } + gifputbyte(0, grr); +} + +static int +write_gif(Gif_Stream *gfs, Gif_Writer *grr) +{ + Gif_Extension* gfex; + int ok = 0; + int i; + + { + uint8_t isgif89a = 0; + if (gfs->end_comment || gfs->end_extension_list || gfs->loopcount > -1) + isgif89a = 1; + for (i = 0; i < gfs->nimages && !isgif89a; i++) { + Gif_Image* gfi = gfs->images[i]; + if (gfi->identifier || gfi->transparent != -1 || gfi->disposal + || gfi->delay || gfi->comment || gfi->extension_list) + isgif89a = 1; + } + if (isgif89a) + gifputblock((const uint8_t *)"GIF89a", 6, grr); + else + gifputblock((const uint8_t *)"GIF87a", 6, grr); + } + + write_logical_screen_descriptor(gfs, grr); + + if (gfs->loopcount > -1) + write_netscape_loop_extension(gfs->loopcount, grr); + + for (i = 0; i < gfs->nimages; i++) + if (!Gif_IncrementalWriteImage(grr, gfs, gfs->images[i])) + goto done; + + for (gfex = gfs->end_extension_list; gfex; gfex = gfex->next) + write_generic_extension(gfex, grr); + if (gfs->end_comment) + write_comment_extensions(gfs->end_comment, grr); + + gifputbyte(';', grr); + ok = 1; + + done: + return ok; +} + + +int +Gif_FullWriteFile(Gif_Stream *gfs, const Gif_CompressInfo *gcinfo, + FILE *f) +{ + Gif_Writer grr; + int ok = gif_writer_init(&grr, f, gcinfo) + && write_gif(gfs, &grr); + gif_writer_cleanup(&grr); + return ok; +} + + +Gif_Writer* +Gif_IncrementalWriteFileInit(Gif_Stream* gfs, const Gif_CompressInfo* gcinfo, + FILE *f) +{ + Gif_Writer* grr = Gif_New(Gif_Writer); + if (!grr || !gif_writer_init(grr, f, gcinfo)) { + Gif_Delete(grr); + return NULL; + } + gifputblock((const uint8_t *)"GIF89a", 6, grr); + write_logical_screen_descriptor(gfs, grr); + if (gfs->loopcount > -1) + write_netscape_loop_extension(gfs->loopcount, grr); + return grr; +} + +int +Gif_IncrementalWriteImage(Gif_Writer* grr, Gif_Stream* gfs, Gif_Image* gfi) +{ + Gif_Extension *gfex; + for (gfex = gfi->extension_list; gfex; gfex = gfex->next) + write_generic_extension(gfex, grr); + if (gfi->comment) + write_comment_extensions(gfi->comment, grr); + if (gfi->identifier) + write_name_extension(gfi->identifier, grr); + if (gfi->transparent != -1 || gfi->disposal || gfi->delay) + write_graphic_control_extension(gfi, grr); + return write_image(gfs, gfi, grr); +} + +int +Gif_IncrementalWriteComplete(Gif_Writer* grr, Gif_Stream* gfs) +{ + Gif_Extension* gfex; + for (gfex = gfs->end_extension_list; gfex; gfex = gfex->next) + write_generic_extension(gfex, grr); + if (gfs->end_comment) + write_comment_extensions(gfs->end_comment, grr); + gifputbyte(';', grr); + gif_writer_cleanup(grr); + Gif_Delete(grr); + return 1; +} + + +#undef Gif_CompressImage +#undef Gif_WriteFile + +int +Gif_CompressImage(Gif_Stream *gfs, Gif_Image *gfi) +{ + return Gif_FullCompressImage(gfs, gfi, 0); +} + +int +Gif_WriteFile(Gif_Stream *gfs, FILE *f) +{ + return Gif_FullWriteFile(gfs, 0, f); +} + + +#ifdef __cplusplus +} +#endif diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/win32cfg.h b/packages/optimizt-gifsicle/upstream/gifsicle/src/win32cfg.h new file mode 100644 index 0000000..853cf65 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/win32cfg.h @@ -0,0 +1,203 @@ +/* Hand-edited file based on config.h.in */ +/* config.h.in. Generated from configure.ac by autoheader. */ + +#ifndef GIFSICLE_CONFIG_H +#define GIFSICLE_CONFIG_H + +/* Define to 1 if multithreading support is available. */ +/* #undef ENABLE_THREADS */ + +/* Define to the number of arguments to gettimeofday (gifview only). */ +/* #undef GETTIMEOFDAY_PROTO */ + +/* Define if GIF LZW compression is off. */ +/* #undef GIF_UNGIF */ + +/* Define to 1 if you have the `cbrtf' function. */ +#define HAVE_CBRTF 1 + +/* Define to 1 if `ext_vector_type' vector types are usable. */ +/* #undef HAVE_EXT_VECTOR_TYPE_VECTOR_TYPES */ + +/* Define to 1 if the system has the type `int64_t'. */ +/* #undef HAVE_INT64_T */ + +/* Define to 1 if you have the header file. */ +#if defined(_MSC_VER) && _MSC_VER >= 1900 +# define HAVE_INTTYPES_H 1 +#endif + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_MEMORY_H */ + +/* Define to 1 if you have the `mkstemp' function. */ +/* #undef HAVE_MKSTEMP */ + +/* Define to 1 if you have the `pow' function. */ +#define HAVE_POW 1 + +/* Define to 1 if SIMD types should be used. */ +/* #undef HAVE_SIMD */ + +/* Define to 1 if you have the header file. */ +#if defined(_MSC_VER) && _MSC_VER >= 1900 +# define HAVE_STDINT_H 1 +#endif + +/* Define to 1 if you have the header file. */ +#define HAVE_STDLIB_H 1 + +/* Define to 1 if you have the `strerror' function. */ +#define HAVE_STRERROR 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_STRINGS_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_STRING_H 1 + +/* Define to 1 if you have the `strtoul' function. */ +#define HAVE_STRTOUL 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_SELECT_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_STAT_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_TIME_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_SYS_TYPES_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_TIME_H */ + +/* Define to 1 if the system has the type `uint64_t'. */ +/* #undef HAVE_UINT64_T */ + +/* Define to 1 if the system has the type `uintptr_t'. */ +#if defined(_MSC_VER) && _MSC_VER >= 1900 +# define HAVE_UINTPTR_T 1 +#endif + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_UNISTD_H */ + +/* Define if you have u_intXX_t types but not uintXX_t types. */ +/* #undef HAVE_U_INT_TYPES */ + +/* Define to 1 if `vector_size' vector types are usable. */ +/* #undef HAVE_VECTOR_SIZE_VECTOR_TYPES */ + +/* Define to 1 if you have the `__builtin_shufflevector' function. */ +/* #undef HAVE___BUILTIN_SHUFFLEVECTOR */ + +/* Define to 1 if you have the `__sync_add_and_fetch' function. */ +/* #undef HAVE___SYNC_ADD_AND_FETCH */ + +/* Define to write GIFs to stdout even when stdout is a terminal. */ +/* #undef OUTPUT_GIF_TO_TERMINAL */ + +/* Name of package */ +#define PACKAGE "gifsicle" + +/* Define to the address where bug reports for this package should be sent. */ +#define PACKAGE_BUGREPORT "" + +/* Define to the full name of this package. */ +#define PACKAGE_NAME "gifsicle" + +/* Define to the full name and version of this package. */ +#define PACKAGE_STRING "gifsicle 1.96" + +/* Define to the one symbol short name of this package. */ +#define PACKAGE_TARNAME "gifsicle" + +/* Define to the home page for this package. */ +#define PACKAGE_URL "" + +/* Define to the version of this package. */ +#define PACKAGE_VERSION "1.96" + +/* Pathname separator character ('/' on Unix). */ +#define PATHNAME_SEPARATOR '\\' + +/* Define to a function that returns a random number. */ +#define RANDOM rand + +/* The size of `float', as computed by sizeof. */ +#define SIZEOF_FLOAT 4 + +/* The size of `unsigned int', as computed by sizeof. */ +#define SIZEOF_UNSIGNED_INT 4 + +/* The size of `unsigned long', as computed by sizeof. */ +#define SIZEOF_UNSIGNED_LONG 4 + +/* The size of `void *', as computed by sizeof. */ +#ifdef _WIN64 +#define SIZEOF_VOID_P 8 +#else +#define SIZEOF_VOID_P 4 +#endif + +/* Define to 1 if you have the ANSI C header files. */ +#define STDC_HEADERS 1 + +/* Version number of package */ +#define VERSION "1.96 (Windows)" + +/* Define if X is not available. */ +#define X_DISPLAY_MISSING 1 + +/* Define to empty if `const' does not conform to ANSI C. */ +/* #undef const */ + +/* Define to `__inline__' or `__inline' if that's what the C compiler + calls it, or to nothing if 'inline' is not supported under any name. */ +#ifndef __cplusplus +# ifndef inline +# define inline __inline +# endif +#endif + +/* Windows doesn't have popen, but it does have _popen. */ +#define popen _popen +#define pclose _pclose + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Use the clean-failing malloc library in fmalloc.c. */ +#define GIF_ALLOCATOR_DEFINED 1 +#define Gif_Free free + +/* Prototype strerror if we don't have it. */ +#ifndef HAVE_STRERROR +char *strerror(int errno); +#endif + +#ifdef __cplusplus +} +/* Get rid of a possible inline macro under C++. */ +# define inline inline +#endif + +/* Need _setmode under MS-DOS, to set stdin/stdout to binary mode */ +/* Need _fsetmode under OS/2 for the same reason */ +/* Windows has _isatty and _snprintf, not the normal versions */ +#if defined(_MSDOS) || defined(_WIN32) || defined(__EMX__) || defined(__DJGPP__) +# include +# include +# define isatty _isatty +# if defined(_MSC_VER) && _MSC_VER < 1900 +# define snprintf _snprintf +# endif +#endif + +#endif /* GIFSICLE_CONFIG_H */ diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/src/xform.c b/packages/optimizt-gifsicle/upstream/gifsicle/src/xform.c new file mode 100644 index 0000000..fb59646 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/src/xform.c @@ -0,0 +1,1405 @@ +/* xform.c - Image transformation functions for gifsicle. + Copyright (C) 1997-2025 Eddie Kohler, ekohler@gmail.com + This file is part of gifsicle. + + Gifsicle is free software. It is distributed under the GNU Public License, + version 2; you can copy, distribute, or alter it at will, as long + as this notice is kept intact and this source code is made available. There + is no warranty, express or implied. */ + +#include +#include "gifsicle.h" +#include +#include +#include +#include +#include +#include +#include +#include "kcolor.h" +#if HAVE_UNISTD_H +# include +#endif +#if HAVE_SYS_TYPES_H && HAVE_SYS_STAT_H +# include +# include +#endif +#ifndef M_PI +/* -std=c89 does not define M_PI */ +# define M_PI 3.14159265358979323846 +#endif + + +/****** + * color transforms + **/ + +Gt_ColorTransform * +append_color_transform(Gt_ColorTransform *list, + color_transform_func func, void *data) +{ + Gt_ColorTransform *trav; + Gt_ColorTransform *xform = Gif_New(Gt_ColorTransform); + xform->next = 0; + xform->func = func; + xform->data = data; + + for (trav = list; trav && trav->next; trav = trav->next) + ; + if (trav) { + trav->next = xform; + return list; + } else + return xform; +} + +Gt_ColorTransform * +delete_color_transforms(Gt_ColorTransform *list, color_transform_func func) +{ + Gt_ColorTransform *prev = 0, *trav = list; + while (trav) { + Gt_ColorTransform *next = trav->next; + if (trav->func == func) { + if (prev) prev->next = next; + else list = next; + Gif_Delete(trav); + } else + prev = trav; + trav = next; + } + return list; +} + +void +apply_color_transforms(Gt_ColorTransform *list, Gif_Stream *gfs) +{ + int i; + Gt_ColorTransform *xform; + for (xform = list; xform; xform = xform->next) { + if (gfs->global) + xform->func(gfs->global, xform->data); + for (i = 0; i < gfs->nimages; i++) + if (gfs->images[i]->local) + xform->func(gfs->images[i]->local, xform->data); + } +} + + +typedef struct Gt_ColorChange { + struct Gt_ColorChange *next; + Gif_Color old_color; + Gif_Color new_color; +} Gt_ColorChange; + +void +color_change_transformer(Gif_Colormap *gfcm, void *thunk) +{ + int i, have; + Gt_ColorChange *first_change = (Gt_ColorChange *)thunk; + Gt_ColorChange *change; + + /* change colors named by color */ + for (i = 0; i < gfcm->ncol; i++) + for (change = first_change; change; change = change->next) { + if (!change->old_color.haspixel) + have = GIF_COLOREQ(&gfcm->col[i], &change->old_color); + else + have = (change->old_color.pixel == (uint32_t)i); + + if (have) { + gfcm->col[i] = change->new_color; + break; /* ignore remaining color changes */ + } + } +} + +Gt_ColorTransform * +append_color_change(Gt_ColorTransform *list, + Gif_Color old_color, Gif_Color new_color) +{ + Gt_ColorTransform *xform; + Gt_ColorChange *change = Gif_New(Gt_ColorChange); + change->old_color = old_color; + change->new_color = new_color; + change->next = 0; + + for (xform = list; xform && xform->next; xform = xform->next) + ; + if (!xform || xform->func != &color_change_transformer) + return append_color_transform(list, &color_change_transformer, change); + else { + Gt_ColorChange *prev = (Gt_ColorChange *)(xform->data); + while (prev->next) prev = prev->next; + prev->next = change; + return list; + } +} + + +void +pipe_color_transformer(Gif_Colormap *gfcm, void *thunk) +{ + int i, status; + FILE *f; + Gif_Color *col = gfcm->col; + Gif_Colormap *new_cm = 0; + char *command = (char *)thunk; +#ifdef HAVE_MKSTEMP +# ifdef P_tmpdir + char tmp_file[] = P_tmpdir "/gifsicle.XXXXXX"; +# else + char tmp_file[] = "/tmp/gifsicle.XXXXXX"; +# endif +#else + char *tmp_file = tmpnam(0); +#endif + char *new_command; + size_t new_command_sz; + +#ifdef HAVE_MKSTEMP + { + mode_t old_mode = umask(077); + if (mkstemp(tmp_file) < 0) + fatal_error("can%,t create temporary file!"); + umask(old_mode); + } +#else + if (!tmp_file) + fatal_error("can%,t create temporary file!"); +#endif + + new_command_sz = strlen(command) + strlen(tmp_file) + 4; + new_command = Gif_NewArray(char, new_command_sz); + snprintf(new_command, new_command_sz, "%s >%s", command, tmp_file); + f = popen(new_command, "w"); + if (!f) + fatal_error("can%,t run color transformation command: %s", strerror(errno)); + Gif_DeleteArray(new_command); + + for (i = 0; i < gfcm->ncol; i++) + fprintf(f, "%d %d %d\n", col[i].gfc_red, col[i].gfc_green, col[i].gfc_blue); + + errno = 0; + status = pclose(f); + if (status < 0) { + error(1, "color transformation error: %s", strerror(errno)); + goto done; + } else if (status > 0) { + error(1, "color transformation command failed"); + goto done; + } + + f = fopen(tmp_file, "r"); + if (!f || feof(f)) { + error(1, "color transformation command generated no output", command); + if (f) fclose(f); + goto done; + } + new_cm = read_colormap_file("", f); + fclose(f); + + if (new_cm) { + int nc = new_cm->ncol; + if (nc < gfcm->ncol) { + nc = gfcm->ncol; + warning(1, "too few colors in color transformation results"); + } else if (nc > gfcm->ncol) + warning(1, "too many colors in color transformation results"); + for (i = 0; i < nc; i++) + col[i] = new_cm->col[i]; + } + + done: + remove(tmp_file); + Gif_DeleteColormap(new_cm); +} + + +void +combine_crop(Gt_Crop* dstcrop, const Gt_Crop* srccrop, const Gif_Image* gfi) +{ + int cl = srccrop->x - gfi->left, cr = cl + srccrop->w, + ct = srccrop->y - gfi->top, cb = ct + srccrop->h, + dl = cl > 0 ? cl : 0, dr = cr < gfi->width ? cr : gfi->width, + dt = ct > 0 ? ct : 0, db = cb < gfi->height ? cb : gfi->height; + + if (dl < dr) { + dstcrop->x = dl; + dstcrop->w = dr - dl; + } else { + dstcrop->x = (cl <= 0 ? 0 : srccrop->w - 1) + (srccrop->left_offset - gfi->left); + dstcrop->w = 0; + } + if (dt < db) { + dstcrop->y = dt; + dstcrop->h = db - dt; + } else { + dstcrop->y = (ct <= 0 ? 0 : srccrop->h - 1) + (srccrop->top_offset - gfi->top); + dstcrop->h = 0; + } +} + + +/***** + * crop image; returns true if the image exists + **/ + +int +crop_image(Gif_Image* gfi, Gt_Frame* fr, int preserve_total_crop) +{ + Gt_Crop c; + int j; + + combine_crop(&c, fr->crop, gfi); + + fr->left_offset = fr->crop->left_offset; + fr->top_offset = fr->crop->top_offset; + + if (c.w > 0 && c.h > 0 && gfi->img) { + uint8_t** old_img = gfi->img; + gfi->img = Gif_NewArray(uint8_t *, c.h + 1); + for (j = 0; j < c.h; j++) + gfi->img[j] = old_img[c.y + j] + c.x; + gfi->img[c.h] = 0; + Gif_DeleteArray(old_img); + gfi->left += c.x - fr->left_offset; + gfi->top += c.y - fr->top_offset; + gfi->width = c.w; + gfi->height = c.h; + } else if (preserve_total_crop) { + Gif_MakeImageEmpty(gfi); + } else { + Gif_DeleteArray(gfi->img); + gfi->img = 0; + gfi->width = gfi->height = 0; + } + + return gfi->img != 0; +} + + +/***** + * flip and rotate + **/ + +void +flip_image(Gif_Image* gfi, Gt_Frame *fr, int is_vert) +{ + int x, y; + int width = gfi->width; + int height = gfi->height; + uint8_t **img = gfi->img; + + /* horizontal flips */ + if (!is_vert) { + uint8_t *buffer = Gif_NewArray(uint8_t, width); + uint8_t *trav; + for (y = 0; y < height; y++) { + memcpy(buffer, img[y], width); + trav = img[y] + width - 1; + for (x = 0; x < width; x++) + *trav-- = buffer[x]; + } + gfi->left = fr->stream->screen_width - (gfi->left + width); + if (fr->crop) + fr->left_offset = fr->stream->screen_width - (fr->left_offset + fr->crop->w); + Gif_DeleteArray(buffer); + } + + /* vertical flips */ + if (is_vert) { + uint8_t **buffer = Gif_NewArray(uint8_t *, height); + memcpy(buffer, img, height * sizeof(uint8_t *)); + for (y = 0; y < height; y++) + img[y] = buffer[height - y - 1]; + gfi->top = fr->stream->screen_height - (gfi->top + height); + if (fr->crop) + fr->top_offset = fr->stream->screen_height - (fr->top_offset + fr->crop->h); + Gif_DeleteArray(buffer); + } +} + +void +rotate_image(Gif_Image* gfi, Gt_Frame* fr, int rotation) +{ + int x, y; + int width = gfi->width; + int height = gfi->height; + int ntop, nleft, nbottom, nright; + uint8_t **img = gfi->img; + uint8_t *new_data = Gif_NewArray(uint8_t, (unsigned) width * (unsigned) height); + uint8_t *trav = new_data; + + /* this function can only rotate by 90 or 270 degrees */ + assert(rotation == 1 || rotation == 3); + + if (rotation == 1) { + for (x = 0; x < width; x++) + for (y = height - 1; y >= 0; y--) + *trav++ = img[y][x]; + } else { + for (x = width - 1; x >= 0; x--) + for (y = 0; y < height; y++) + *trav++ = img[y][x]; + } + + if (rotation == 1) { + ntop = gfi->left; + nleft = fr->stream->screen_height - (gfi->top + height); + if (fr->crop) { + x = fr->left_offset; + fr->left_offset = fr->stream->screen_height - (fr->top_offset + fr->crop->h); + fr->top_offset = x; + } + } else { + ntop = fr->stream->screen_width - (gfi->left + width); + nleft = gfi->top; + if (fr->crop) { + y = fr->top_offset; + fr->top_offset = fr->stream->screen_width - (fr->left_offset + fr->crop->w); + fr->left_offset = y; + } + } + nbottom = ntop + width; + nright = nleft + height; + + if (nbottom <= 0 || nright <= 0) { + ntop = nleft = nbottom = nright = 0; + } else if (ntop < 0 || nleft < 0) { + int nwidth = nright - (nleft < 0 ? 0 : nleft); + uint8_t *xdata; + xdata = Gif_NewArray(uint8_t, (unsigned) (nright - nleft) * (unsigned) (nbottom - ntop)); + for (y = ntop < 0 ? -ntop : 0; y < width; ++y) { + memcpy(xdata + y * nwidth, new_data + y * height + (nleft < 0 ? -nleft : 0), nwidth); + } + Gif_DeleteArray(new_data); + new_data = xdata; + ntop = ntop < 0 ? 0 : ntop; + nleft = nleft < 0 ? 0 : nleft; + } + + Gif_ReleaseUncompressedImage(gfi); + gfi->width = nright - nleft; + gfi->height = nbottom - ntop; + gfi->left = nleft; + gfi->top = ntop; + Gif_SetUncompressedImage(gfi, new_data, Gif_Free, 0); +} + + +/***** + * scale + **/ + +/* kcscreen: A frame buffer containing `kacolor`s (one per pixel). + Kcolors with components < 0 represent transparency. */ +typedef struct { + kacolor* data; /* data buffer: (x,y) in data[y*width + x] */ + kacolor* scratch; /* scratch buffer, used for previous disposal */ + unsigned width; /* screen width */ + unsigned height; /* screen height */ + kacolor bg; /* background color */ +} kcscreen; + +/* initialize `kcs` to an empty state */ +static void kcscreen_clear(kcscreen* kcs) { + kcs->data = kcs->scratch = NULL; +} + +/* initialize `kcs` for stream `gfs`. Screen size is `sw`x`sh`; if either + component is <= 0, the value of `gfs->screen_*` is taken. */ +static void kcscreen_init(kcscreen* kcs, Gif_Stream* gfs, int sw, int sh) { + unsigned i, sz; + assert(!kcs->data && !kcs->scratch); + kcs->width = sw <= 0 ? gfs->screen_width : sw; + kcs->height = sh <= 0 ? gfs->screen_height : sh; + sz = kcs->width * kcs->height; + kcs->data = Gif_NewArray(kacolor, sz); + if ((gfs->nimages == 0 || gfs->images[0]->transparent < 0) + && gfs->global && gfs->background < gfs->global->ncol) { + kcs->bg.k = kc_makegfcg(&gfs->global->col[gfs->background]); + kcs->bg.a[3] = KC_MAX; + } else + kcs->bg.a[0] = kcs->bg.a[1] = kcs->bg.a[2] = kcs->bg.a[3] = 0; + for (i = 0; i != sz; ++i) + kcs->data[i] = kcs->bg; +} + +/* free memory associated with `kcs` */ +static void kcscreen_cleanup(kcscreen* kcs) { + Gif_DeleteArray(kcs->data); + Gif_DeleteArray(kcs->scratch); +} + +/* apply image `gfi` to screen `kcs`, using the color array `ks` */ +static void kcscreen_apply(kcscreen* kcs, const Gif_Image* gfi, + const kcolor* ks) { + int x, y; + assert((unsigned) gfi->left + gfi->width <= kcs->width); + assert((unsigned) gfi->top + gfi->height <= kcs->height); + + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) { + if (!kcs->scratch) + kcs->scratch = Gif_NewArray(kacolor, kcs->width * kcs->height); + for (y = gfi->top; y != gfi->top + gfi->height; ++y) + memcpy(&kcs->scratch[y * kcs->width + gfi->left], + &kcs->data[y * kcs->width + gfi->left], + sizeof(kacolor) * gfi->width); + } + + for (y = gfi->top; y != gfi->top + gfi->height; ++y) { + const uint8_t* linein = gfi->img[y - gfi->top]; + kacolor* lineout = &kcs->data[y * kcs->width + gfi->left]; + for (x = 0; x != gfi->width; ++x) + if (linein[x] != gfi->transparent) { + lineout[x].k = ks[linein[x]]; + lineout[x].a[3] = KC_MAX; + } + } +} + +/* dispose of image `gfi`, which was previously applied by kcscreen_apply */ +static void kcscreen_dispose(kcscreen* kcs, const Gif_Image* gfi) { + int x, y; + assert((unsigned) gfi->left + gfi->width <= kcs->width); + assert((unsigned) gfi->top + gfi->height <= kcs->height); + + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) { + for (y = gfi->top; y != gfi->top + gfi->height; ++y) + memcpy(&kcs->data[y * kcs->width + gfi->left], + &kcs->scratch[y * kcs->width + gfi->left], + sizeof(kacolor) * gfi->width); + } else if (gfi->disposal == GIF_DISPOSAL_BACKGROUND) { + for (y = gfi->top; y != gfi->top + gfi->height; ++y) + for (x = gfi->left; x != gfi->left + gfi->width; ++x) + kcs->data[y * kcs->width + x] = kcs->bg; + } +} + + +/* ksscreen: A frame buffer containing `scale_color`s (one per pixel). + Kcolors with components < 0 represent transparency. */ +typedef struct { + scale_color* data; /* data buffer: (x,y) in data[y*width + x] */ + scale_color* scratch; /* scratch buffer, used for previous disposal */ + unsigned width; /* screen width */ + unsigned height; /* screen height */ + scale_color bg; /* background color */ +} ksscreen; + +/* initialize `kss` to an empty state */ +static void ksscreen_clear(ksscreen* kss) { + kss->data = kss->scratch = NULL; +} + +/* initialize `kss` for stream `gfs`. Screen size is `sw`x`sh`; if either + component is <= 0, the value of `gfs->screen_*` is taken. */ +static void ksscreen_init(ksscreen* kss, Gif_Stream* gfs, int sw, int sh) { + unsigned i, sz; + assert(!kss->data && !kss->scratch); + kss->width = sw <= 0 ? gfs->screen_width : sw; + kss->height = sh <= 0 ? gfs->screen_height : sh; + sz = kss->width * kss->height; + kss->data = Gif_NewArray(scale_color, sz); + if ((gfs->nimages == 0 || gfs->images[0]->transparent < 0) + && gfs->global && gfs->background < gfs->global->ncol) { + kss->bg = sc_makekc(kc_makegfcg(&gfs->global->col[gfs->background])); + } else + sc_clear(&kss->bg); + for (i = 0; i != sz; ++i) + kss->data[i] = kss->bg; +} + +/* free memory associated with `kss` */ +static void ksscreen_cleanup(ksscreen* kss) { + Gif_DeleteArray(kss->data); + Gif_DeleteArray(kss->scratch); +} + +/* apply image `gfi` to screen `kss`, using the color array `ks` */ +static void ksscreen_apply(ksscreen* kss, const Gif_Image* gfi, + const kcolor* ks) { + int x, y; + assert((unsigned) gfi->left + gfi->width <= kss->width); + assert((unsigned) gfi->top + gfi->height <= kss->height); + + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) { + if (!kss->scratch) + kss->scratch = Gif_NewArray(scale_color, kss->width * kss->height); + for (y = gfi->top; y != gfi->top + gfi->height; ++y) + memcpy(&kss->scratch[y * kss->width + gfi->left], + &kss->data[y * kss->width + gfi->left], + sizeof(scale_color) * gfi->width); + } + + for (y = gfi->top; y != gfi->top + gfi->height; ++y) { + const uint8_t* linein = gfi->img[y - gfi->top]; + scale_color* lineout = &kss->data[y * kss->width + gfi->left]; + for (x = 0; x != gfi->width; ++x) + if (linein[x] != gfi->transparent) + lineout[x] = sc_makekc(ks[linein[x]]); + } +} + +/* dispose of image `gfi`, which was previously applied by ksscreen_apply */ +static void ksscreen_dispose(ksscreen* kss, const Gif_Image* gfi) { + int x, y; + assert((unsigned) gfi->left + gfi->width <= kss->width); + assert((unsigned) gfi->top + gfi->height <= kss->height); + + if (gfi->disposal == GIF_DISPOSAL_PREVIOUS) { + for (y = gfi->top; y != gfi->top + gfi->height; ++y) + memcpy(&kss->data[y * kss->width + gfi->left], + &kss->scratch[y * kss->width + gfi->left], + sizeof(scale_color) * gfi->width); + } else if (gfi->disposal == GIF_DISPOSAL_BACKGROUND) { + for (y = gfi->top; y != gfi->top + gfi->height; ++y) + for (x = gfi->left; x != gfi->left + gfi->width; ++x) + kss->data[y * kss->width + x] = kss->bg; + } +} + + +typedef struct { + float w; + int ipos; + int opos; +} scale_weight; + +typedef struct { + scale_weight* ws; + int n; +} scale_weightset; + +typedef struct { + Gif_Stream* gfs; + Gif_Image* gfi; + int imageno; + kd3_tree* kd3; + ksscreen iscr; + kcscreen oscr; + kcscreen xscr; + float oxf; /* (input width) / (output width) */ + float oyf; /* (input height) / (output height) */ + float ixf; /* (output width) / (input width) */ + float iyf; /* (output height) / (input height) */ + scale_weightset xweights; + scale_weightset yweights; + kd3_tree global_kd3; + kd3_tree local_kd3; + unsigned max_desired_dist; + int scale_colors; +} scale_context; + +#if ENABLE_THREADS +static pthread_mutex_t global_colormap_lock = PTHREAD_MUTEX_INITIALIZER; +#define GLOBAL_COLORMAP_LOCK() pthread_mutex_lock(&global_colormap_lock) +#define GLOBAL_COLORMAP_UNLOCK() pthread_mutex_unlock(&global_colormap_lock) +#else +#define GLOBAL_COLORMAP_LOCK() /* nada */ +#define GLOBAL_COLORMAP_UNLOCK() /* nada */ +#endif + +static void sctx_init(scale_context* sctx, Gif_Stream* gfs, int nw, int nh) { + sctx->gfs = gfs; + sctx->gfi = NULL; + sctx->global_kd3.ks = NULL; + sctx->local_kd3.ks = NULL; + ksscreen_clear(&sctx->iscr); + kcscreen_clear(&sctx->oscr); + kcscreen_clear(&sctx->xscr); + sctx->iscr.width = gfs->screen_width; + sctx->iscr.height = gfs->screen_height; + sctx->oscr.width = nw; + sctx->oscr.height = nh; + sctx->xscr.width = nw; + sctx->xscr.height = nh; + sctx->ixf = (double) nw / gfs->screen_width; + sctx->iyf = (double) nh / gfs->screen_height; + sctx->oxf = gfs->screen_width / (double) nw; + sctx->oyf = gfs->screen_height / (double) nh; + sctx->xweights.ws = sctx->yweights.ws = NULL; + sctx->xweights.n = sctx->yweights.n = 0; + sctx->max_desired_dist = 16000; + sctx->scale_colors = 0; + sctx->kd3 = NULL; + sctx->imageno = 0; +} + +static void sctx_cleanup(scale_context* sctx) { + if (sctx->global_kd3.ks) + kd3_cleanup(&sctx->global_kd3); + ksscreen_cleanup(&sctx->iscr); + kcscreen_cleanup(&sctx->oscr); + kcscreen_cleanup(&sctx->xscr); + Gif_DeleteArray(sctx->xweights.ws); + Gif_DeleteArray(sctx->yweights.ws); +} + +static void scale_image_data_point(scale_context* sctx, Gif_Image* gfo) { + Gif_Image* gfi = sctx->gfi; + uint8_t* data = gfo->image_data; + int xo, yo; + uint16_t* xoff = Gif_NewArray(uint16_t, gfo->width); + for (xo = 0; xo != gfo->width; ++xo) + xoff[xo] = (int) ((gfo->left + xo + 0.5) * sctx->oxf) - gfi->left; + + for (yo = 0; yo != gfo->height; ++yo) { + int yi = (int) ((gfo->top + yo + 0.5) * sctx->oyf) - gfi->top; + const uint8_t* in_line = gfi->img[yi]; + for (xo = 0; xo != gfo->width; ++xo, ++data) + *data = in_line[xoff[xo]]; + } + + Gif_DeleteArray(xoff); +} + +static void scale_image_update_global_kd3(scale_context* sctx) { + Gif_Colormap* gfcm = sctx->gfs->global; + assert(sctx->kd3 == &sctx->global_kd3); + while (sctx->kd3->nitems < gfcm->ncol) { + Gif_Color* gfc = &gfcm->col[sctx->kd3->nitems]; + kd3_add8g(sctx->kd3, gfc->gfc_red, gfc->gfc_green, gfc->gfc_blue); + } +} + +static void scale_image_prepare(scale_context* sctx) { + if (sctx->gfi->local) { + sctx->kd3 = &sctx->local_kd3; + kd3_init_build(sctx->kd3, NULL, sctx->gfi->local); + } else { + sctx->kd3 = &sctx->global_kd3; + if (!sctx->kd3->ks) + kd3_init(sctx->kd3, NULL); + GLOBAL_COLORMAP_LOCK(); + scale_image_update_global_kd3(sctx); + GLOBAL_COLORMAP_UNLOCK(); + if (!sctx->kd3->tree) + kd3_build(sctx->kd3); + kd3_enable_all(sctx->kd3); + } + if (sctx->gfi->transparent >= 0 + && sctx->gfi->transparent < sctx->kd3->nitems) + kd3_disable(sctx->kd3, sctx->gfi->transparent); + + /* apply image to sctx->iscr */ + if (!sctx->iscr.data) { + /* first image, set up screens */ + ksscreen_init(&sctx->iscr, sctx->gfs, 0, 0); + kcscreen_init(&sctx->oscr, sctx->gfs, + sctx->oscr.width, sctx->oscr.height); + kcscreen_init(&sctx->xscr, sctx->gfs, + sctx->xscr.width, sctx->xscr.height); + } + ksscreen_apply(&sctx->iscr, sctx->gfi, sctx->kd3->ks); +} + +static void scale_image_output_row(scale_context* sctx, scale_color* sc, + Gif_Image* gfo, int yo) { + int k, xo; + kacolor* oscr = &sctx->xscr.data[sctx->xscr.width * (yo + gfo->top) + + gfo->left]; + + for (xo = 0; xo != gfo->width; ++xo) + if (sc[xo].a[3] <= (int) (KC_MAX / 4)) + oscr[xo] = kac_transparent(); + else { + /* don't effectively mix partially transparent pixels with black */ + if (sc[xo].a[3] <= (int) (KC_MAX * 31 / 32)) + SCVEC_MULF(sc[xo], KC_MAX / sc[xo].a[3]); + /* find closest color */ + for (k = 0; k != 3; ++k) { + int v = (int) (sc[xo].a[k] + 0.5); + oscr[xo].a[k] = KC_CLAMPV(v); + } + oscr[xo].a[3] = KC_MAX; + } +} + +static int scale_image_add_colors(scale_context* sctx, Gif_Image* gfo) { + kchist kch; + kcdiversity div; + Gif_Color gfc; + Gif_Colormap* gfcm = sctx->gfi->local ? sctx->gfi->local : sctx->gfs->global; + int yo, xo, i, nadded; + + kchist_init(&kch); + for (yo = 0; yo != gfo->height; ++yo) { + kacolor* xscr = &sctx->xscr.data[sctx->xscr.width * (yo + gfo->top) + + gfo->left]; + for (xo = 0; xo != gfo->width; ++xo) + if (xscr[xo].a[3]) + kchist_add(&kch, xscr[xo].k, 1); + } + for (i = 0; i != gfcm->ncol; ++i) + kchist_add(&kch, kc_makegfcg(&gfcm->col[i]), (kchist_count_t) -1); + kchist_compress(&kch); + + kcdiversity_init(&div, &kch, 0); + for (i = 0; i != kch.n && i != gfcm->ncol + && kch.h[i].count == (kchist_count_t) -1; ++i) + kcdiversity_choose(&div, i, 0); + + nadded = 0; + while (gfcm->ncol < sctx->scale_colors) { + int chosen = kcdiversity_find_diverse(&div, 0); + if (chosen >= kch.n || div.min_dist[chosen] <= sctx->max_desired_dist) + break; + kcdiversity_choose(&div, chosen, 0); + gfc = kc_togfcg(kch.h[chosen].ka.k); + Gif_AddColor(gfcm, &gfc, gfcm->ncol); + kd3_add8g(sctx->kd3, gfc.gfc_red, gfc.gfc_green, gfc.gfc_blue); + ++nadded; + } + + kcdiversity_cleanup(&div); + kchist_cleanup(&kch); + return nadded != 0; +} + +static void scale_image_complete(scale_context* sctx, Gif_Image* gfo) { + int yo, xo, transparent = sctx->gfi->transparent; + unsigned dist, dist2, max_dist; + + retry: + max_dist = 0; + /* look up palette for output screen */ + for (yo = 0; yo != gfo->height; ++yo) { + uint8_t* data = gfo->img[yo]; + kacolor* xscr = &sctx->xscr.data[sctx->xscr.width * (yo + gfo->top) + + gfo->left]; + kacolor* oscr = &sctx->oscr.data[sctx->oscr.width * (yo + gfo->top) + + gfo->left]; + for (xo = 0; xo != gfo->width; ++xo) + if (xscr[xo].a[3]) { + data[xo] = kd3_closest_transformed(sctx->kd3, xscr[xo].k, &dist); + /* maybe previous color is actually closer to desired */ + if (transparent >= 0 && oscr[xo].a[3]) { + dist2 = kc_distance(oscr[xo].k, xscr[xo].k); + if (dist2 <= dist) { + data[xo] = transparent; + dist = dist2; + } + } + max_dist = dist > max_dist ? dist : max_dist; + } else + data[xo] = transparent; + } + + /* if the global colormap has changed, we must retry + (this can only happen if ENABLE_THREADS) */ + if (!sctx->gfi->local) { + Gif_Colormap* gfcm = sctx->gfs->global; + GLOBAL_COLORMAP_LOCK(); + if (gfcm->ncol > sctx->kd3->nitems) { + scale_image_update_global_kd3(sctx); + GLOBAL_COLORMAP_UNLOCK(); + goto retry; + } + } + + /* maybe add some colors */ + if (max_dist > sctx->max_desired_dist) { + Gif_Colormap* gfcm = sctx->gfi->local ? sctx->gfi->local : sctx->gfs->global; + if (gfcm->ncol < sctx->scale_colors + && scale_image_add_colors(sctx, gfo)) { + if (!sctx->gfi->local) + GLOBAL_COLORMAP_UNLOCK(); + goto retry; + } + } + + if (!sctx->gfi->local) + GLOBAL_COLORMAP_UNLOCK(); + + /* apply disposal to sctx->iscr and sctx->oscr */ + if (sctx->imageno != sctx->gfs->nimages - 1) { + ksscreen_dispose(&sctx->iscr, sctx->gfi); + + if (sctx->gfi->disposal == GIF_DISPOSAL_BACKGROUND) + kcscreen_dispose(&sctx->oscr, gfo); + else if (sctx->gfi->disposal != GIF_DISPOSAL_PREVIOUS) + kcscreen_apply(&sctx->oscr, gfo, sctx->kd3->ks); + } + + if (sctx->gfi->local) + kd3_cleanup(sctx->kd3); +} + +typedef struct pixel_range { + int lo; + int hi; +} pixel_range; + +typedef struct pixel_range2 { + float bounds[2]; + int lo; + int hi; +} pixel_range2; + +static inline pixel_range make_pixel_range(int xi, int maxi, + int maxo, float f) { + pixel_range pr; + pr.lo = (int) (xi * f); + if (xi + 1 == maxi) + pr.hi = maxo; + else + pr.hi = (int) ((xi + 1) * f); + if (pr.hi == pr.lo) + ++pr.hi; + return pr; +} + +static inline pixel_range2 make_pixel_range2(int xi, int maxi, + int maxo, float f) { + pixel_range2 pr; + pr.bounds[0] = xi * f; + pr.bounds[1] = (xi + 1) * f; + pr.lo = (int) pr.bounds[0]; + if (xi + 1 == maxi) + pr.hi = maxo; + else + pr.hi = (int) ceil(pr.bounds[1]); + if (pr.hi == pr.lo) + ++pr.hi; + return pr; +} + +static void scale_image_data_box(scale_context* sctx, Gif_Image* gfo) { + uint16_t* xoff = Gif_NewArray(uint16_t, sctx->iscr.width); + scale_color* sc = Gif_NewArray(scale_color, gfo->width); + int* nsc = Gif_NewArray(int, gfo->width); + int xi0, xi1, xo, yo, i, j; + scale_image_prepare(sctx); + + /* develop scale mapping of input -> output pixels */ + for (xo = 0; xo != gfo->width; ++xo) { + pixel_range xpr = make_pixel_range(xo + gfo->left, sctx->oscr.width, + sctx->iscr.width, sctx->oxf); + for (i = xpr.lo; i != xpr.hi; ++i) + xoff[i] = xo; + } + xi0 = make_pixel_range(gfo->left, sctx->oscr.width, + sctx->iscr.width, sctx->oxf).lo; + xi1 = make_pixel_range(gfo->left + gfo->width - 1, sctx->oscr.width, + sctx->iscr.width, sctx->oxf).hi; + + /* walk over output image */ + for (yo = 0; yo != gfo->height; ++yo) { + pixel_range ypr = make_pixel_range(yo + gfo->top, sctx->oscr.height, + sctx->iscr.height, sctx->oyf); + for (xo = 0; xo != gfo->width; ++xo) + sc_clear(&sc[xo]); + for (xo = 0; xo != gfo->width; ++xo) + nsc[xo] = 0; + + /* collect input mixes */ + for (j = ypr.lo; j != ypr.hi; ++j) { + const scale_color* indata = &sctx->iscr.data[sctx->iscr.width*j]; + for (i = xi0; i != xi1; ++i) { + ++nsc[xoff[i]]; + SCVEC_ADDV(sc[xoff[i]], indata[i]); + } + } + + /* scale channels */ + for (xo = 0; xo != gfo->width; ++xo) + SCVEC_DIVF(sc[xo], (float) nsc[xo]); + + /* generate output */ + scale_image_output_row(sctx, sc, gfo, yo); + } + + scale_image_complete(sctx, gfo); + + Gif_DeleteArray(xoff); + Gif_DeleteArray(sc); + Gif_DeleteArray(nsc); +} + +static inline float mix_factor(int val, const float bounds[2]) { + if (val < bounds[0] && bounds[1] < val + 1) + return bounds[1] - bounds[0]; + else if (val < bounds[0]) + return 1 - (bounds[0] - val); + else if (bounds[1] < val + 1) + return bounds[1] - val; + else + return 1.0; +} + +typedef struct scale_mixinfo { + uint16_t xi; + uint16_t xo; + float f; +} scale_mixinfo; + +static void scale_image_data_mix(scale_context* sctx, Gif_Image* gfo) { + int xo, yo, i, j, nmix = 0, mixcap = sctx->iscr.width * 2; + scale_color* sc = Gif_NewArray(scale_color, gfo->width); + scale_mixinfo* mix = Gif_NewArray(scale_mixinfo, mixcap); + + scale_image_prepare(sctx); + + /* construct mixinfo */ + for (xo = 0; xo != gfo->width; ++xo) { + pixel_range2 xpr = make_pixel_range2(xo + gfo->left, sctx->oscr.width, + sctx->iscr.width, sctx->oxf); + while (nmix + xpr.hi - xpr.lo > mixcap) { + mixcap *= 2; + Gif_ReArray(mix, scale_mixinfo, mixcap); + } + for (i = xpr.lo; i < xpr.hi; ++i, ++nmix) { + mix[nmix].xi = i; + mix[nmix].xo = xo; + mix[nmix].f = mix_factor(i, xpr.bounds); + } + } + + for (yo = 0; yo != gfo->height; ++yo) { + pixel_range2 ypr = make_pixel_range2(yo + gfo->top, sctx->oscr.height, + sctx->iscr.height, sctx->oyf); + + for (xo = 0; xo != gfo->width; ++xo) + sc_clear(&sc[xo]); + + /* collect input mixes */ + for (j = ypr.lo; j < ypr.hi; ++j) { + float jf = mix_factor(j, ypr.bounds) * sctx->ixf * sctx->iyf; + const scale_color* indata = &sctx->iscr.data[sctx->iscr.width*j]; + + for (i = 0; i != nmix; ++i) + SCVEC_ADDVxF(sc[mix[i].xo], indata[mix[i].xi], + (float) (mix[i].f * jf)); + } + + /* generate output */ + scale_image_output_row(sctx, sc, gfo, yo); + } + + scale_image_complete(sctx, gfo); + + Gif_DeleteArray(sc); + Gif_DeleteArray(mix); +} + + +static void scale_weightset_add(scale_weightset* wset, + int ipos, int opos, double w) { + if (wset->n && wset->ws[wset->n - 1].ipos == ipos + && wset->ws[wset->n - 1].opos == opos) + wset->ws[wset->n - 1].w += w; + else { + if (!wset->ws) + wset->ws = Gif_NewArray(scale_weight, 256); + else if (wset->n > 128 && (wset->n & (wset->n - 1)) == 0) + Gif_ReArray(wset->ws, scale_weight, wset->n * 2); + wset->ws[wset->n].w = w; + wset->ws[wset->n].ipos = ipos; + wset->ws[wset->n].opos = opos; + ++wset->n; + } +} + +static inline double scale_weight_cubic(double x, double b, double c) { + x = fabs(x); + if (x < 1) + return ((12 - 9*b - 6*c) * x*x*x + + (-18 + 12*b + 6*c) * x*x + + (6 - 2*b)) / 6; + else if (x < 2) + return ((-b - 6*c) * x*x*x + + (6*b + 30*c) * x*x + + (-12*b - 48*c) * x + + (8*b + 24*c)) / 6; + else + return 0; +} + +static inline double scale_weight_catrom(double x) { + return scale_weight_cubic(x, 0, 0.5); +} + +static inline double scale_weight_mitchell(double x) { + return scale_weight_cubic(x, 1.0/3, 1.0/3); +} + +static inline double sinc(double x) { + if (x <= 0.000000005) + return 1; + else + return sin(M_PI * x) / (M_PI * x); +} + +static inline double scale_weight_lanczos(double x, int lobes) { + x = fabs(x); + if (x < lobes) + return sinc(x) * sinc(x / lobes); + else + return 0; +} + +static inline double scale_weight_lanczos2(double x) { + return scale_weight_lanczos(x, 2); +} + +static inline double scale_weight_lanczos3(double x) { + return scale_weight_lanczos(x, 3); +} + +static void scale_weightset_create(scale_weightset* wset, int nin, int nout, + double (*weightf)(double), double radius) { + double of = (double) nin / nout; + double reduction = of > 1.0 ? of : 1.0; + int opos; + + for (opos = 0; opos != nout; ++opos) { + double icenter = (opos + 0.5) * of - 0.5; + int ipos0 = (int) ceil(icenter - radius * reduction - 0.0001); + int ipos1 = (int) floor(icenter + radius * reduction + 0.0001) + 1; + int wset0 = wset->n; + double sum = 0; + + for (; ipos0 != ipos1; ++ipos0) { + double w = (*weightf)((ipos0 - icenter) / reduction); + if (w != 0.0) { + int ripos = ipos0 < 0 ? 0 : (ipos0 < nin ? ipos0 : nin - 1); + scale_weightset_add(wset, ripos, opos, w); + sum += w; + } + } + + for (; wset0 != wset->n; ++wset0) + wset->ws[wset0].w /= sum; + } + + scale_weightset_add(wset, nin, nout, 0); +} + +static void scale_image_data_weighted(scale_context* sctx, Gif_Image* gfo, + double (*weightf)(double), + double radius) { + int yi, yi0, yi1, xo, yo; + scale_color* sc = Gif_NewArray(scale_color, gfo->width); + scale_color* kcx = Gif_NewArray(scale_color, gfo->width * sctx->iscr.height); + const scale_weight* w, *ww; + + if (!sctx->xweights.ws) { + scale_weightset_create(&sctx->xweights, sctx->iscr.width, + sctx->oscr.width, weightf, radius); + scale_weightset_create(&sctx->yweights, sctx->iscr.height, + sctx->oscr.height, weightf, radius); + } + + scale_image_prepare(sctx); + + /* scale every input row by the X scaling factor */ + { + double ycmp = radius * (sctx->oyf > 1 ? sctx->oyf : 1); + yi0 = (int) floor(gfo->top * sctx->oyf - ycmp - 0.0001); + yi0 = yi0 < 0 ? 0 : yi0; + yi1 = (int) ceil((gfo->top + gfo->height) * sctx->oyf + ycmp + 0.0001) + 1; + yi1 = yi1 < (int) sctx->iscr.height ? yi1 : (int) sctx->iscr.height; + } + + for (ww = sctx->xweights.ws; ww->opos < gfo->left; ++ww) + /* skip */; + for (yi = yi0; yi != yi1; ++yi) { + const scale_color* iscr = &sctx->iscr.data[sctx->iscr.width * yi]; + scale_color* oscr = &kcx[(unsigned) gfo->width * yi]; + for (xo = 0; xo != gfo->width; ++xo) + sc_clear(&oscr[xo]); + for (w = ww; w->opos < gfo->left + gfo->width; ++w) + SCVEC_ADDVxF(oscr[w->opos - gfo->left], iscr[w->ipos], w->w); + } + + /* scale the resulting rows by the y factor */ + for (w = sctx->yweights.ws; w->opos < gfo->top; ++w) + /* skip */; + for (yo = 0; yo != gfo->height; ++yo) { + for (xo = 0; xo != gfo->width; ++xo) + sc_clear(&sc[xo]); + for (; w->opos < gfo->top + yo + 1; ++w) { + const scale_color* iscr = &kcx[(unsigned) gfo->width * w->ipos]; + assert(w->ipos >= yi0 && w->ipos < yi1); + for (xo = 0; xo != gfo->width; ++xo) + SCVEC_ADDVxF(sc[xo], iscr[xo], w->w); + } + /* generate output */ + scale_image_output_row(sctx, sc, gfo, yo); + } + + scale_image_complete(sctx, gfo); + + Gif_DeleteArray(sc); + Gif_DeleteArray(kcx); +} + +static void scale_image_data_catrom(scale_context* sctx, Gif_Image* gfo) { + scale_image_data_weighted(sctx, gfo, scale_weight_catrom, 2); +} + +static void scale_image_data_mitchell(scale_context* sctx, Gif_Image* gfo) { + scale_image_data_weighted(sctx, gfo, scale_weight_mitchell, 2); +} + +static void scale_image_data_lanczos2(scale_context* sctx, Gif_Image* gfo) { + scale_image_data_weighted(sctx, gfo, scale_weight_lanczos2, 2); +} + +static void scale_image_data_lanczos3(scale_context* sctx, Gif_Image* gfo) { + scale_image_data_weighted(sctx, gfo, scale_weight_lanczos3, 3); +} + + +static void scale_image(scale_context* sctx, int method) { + Gif_Image* gfi = sctx->gfi; + Gif_Image gfo; + int was_compressed = (gfi->img == 0); + + /* Fri 9 Jan 1999: Fix problem with resizing animated GIFs: we scaled + from left edge of the *subimage* to right edge of the subimage, + causing consistency problems when several subimages overlap. + Solution: always use scale factors relating to the *whole image* (the + screen size). */ + + /* calculate new width and height based on the four edges (left, right, + top, bottom). This is better than simply multiplying the width and + height by the scale factors because it avoids roundoff + inconsistencies between frames on animated GIFs. Don't allow 0-width + or 0-height images; GIF doesn't support them well. */ + gfo = *gfi; + gfo.img = NULL; + gfo.image_data = NULL; + gfo.compressed = NULL; + + gfo.left = (int) (gfi->left * sctx->ixf); + gfo.top = (int) (gfi->top * sctx->iyf); + gfo.width = (int) ceil((gfi->left + gfi->width) * sctx->ixf) - gfo.left; + gfo.height = (int) ceil((gfi->top + gfi->height) * sctx->iyf) - gfo.top; + /* account for floating point errors at bottom right edge */ + if ((unsigned) gfi->left + gfi->width == sctx->iscr.width) + gfo.width = sctx->oscr.width - gfo.left; + if ((unsigned) gfi->top + gfi->height == sctx->iscr.height) + gfo.height = sctx->oscr.height - gfo.top; + + /* Point scaling method is special: rather than rendering the input + image into a frame buffer, it works on per-frame data. Thus we must + verify that the output bounds completely fit into the input image's + per-frame data. Using pixel midpoints complicates this task. */ + if (method == SCALE_METHOD_POINT) { + if (gfo.width && (int) ((gfo.left + 0.5) * sctx->oxf) < gfi->left) + ++gfo.left, --gfo.width; + if (gfo.width && (int) ((gfo.left + gfo.width - 0.5) * sctx->oxf) + >= gfi->left + gfi->width) + --gfo.width; + if (gfo.height && (int) ((gfo.top + 0.5) * sctx->oyf) < gfi->top) + ++gfo.top, --gfo.height; + if (gfo.height && (int) ((gfo.top + gfo.height - 0.5) * sctx->oyf) + >= gfi->top + gfi->height) + --gfo.height; + } + + if (gfo.width == 0 || gfo.height == 0) { + gfo.width = gfo.height = 1; + Gif_CreateUncompressedImage(&gfo, 0); + gfo.image_data[0] = gfo.transparent = 0; + gfo.disposal = GIF_DISPOSAL_ASIS; + goto done; + } + + if (was_compressed) + Gif_UncompressImage(sctx->gfs, gfi); + Gif_CreateUncompressedImage(&gfo, 0); + + if (method == SCALE_METHOD_MIX) + scale_image_data_mix(sctx, &gfo); + else if (method == SCALE_METHOD_BOX) + scale_image_data_box(sctx, &gfo); + else if (method == SCALE_METHOD_CATROM) + scale_image_data_catrom(sctx, &gfo); + else if (method == SCALE_METHOD_LANCZOS2) + scale_image_data_lanczos2(sctx, &gfo); + else if (method == SCALE_METHOD_LANCZOS3) + scale_image_data_lanczos3(sctx, &gfo); + else if (method == SCALE_METHOD_MITCHELL) + scale_image_data_mitchell(sctx, &gfo); + else + scale_image_data_point(sctx, &gfo); + + done: + Gif_ReleaseUncompressedImage(gfi); + Gif_ReleaseCompressedImage(gfi); + *gfi = gfo; + if (was_compressed) { + Gif_FullCompressImage(sctx->gfs, gfi, &gif_write_info); + Gif_ReleaseUncompressedImage(gfi); + } +} + + +#if ENABLE_THREADS +typedef struct { + pthread_t threadid; + Gif_Stream* gfs; + int imageno; + int* next_imageno; + int nw; + int nh; + int scale_colors; + int method; +} scale_thread_context; + +#if !HAVE___SYNC_ADD_AND_FETCH +static pthread_mutex_t next_imageno_lock = PTHREAD_MUTEX_INITIALIZER; +#endif + +void* scale_image_threaded(void* args) { + scale_thread_context* ctx = (scale_thread_context*) args; + scale_context sctx; + sctx_init(&sctx, ctx->gfs, ctx->nw, ctx->nh); + sctx.scale_colors = ctx->scale_colors; + do { + sctx.imageno = ctx->imageno; + sctx.gfi = ctx->gfs->images[ctx->imageno]; + scale_image(&sctx, ctx->method); +#if HAVE___SYNC_ADD_AND_FETCH + ctx->imageno = __sync_add_and_fetch(ctx->next_imageno, 1); +#else + pthread_mutex_lock(&next_imageno_lock); + ctx->imageno = ++*ctx->next_imageno; + pthread_mutex_unlock(&next_imageno_lock); +#endif + } while (ctx->imageno < ctx->gfs->nimages); + sctx_cleanup(&sctx); + return 0; +} +#endif + +void +resize_dimensions(int* w, int* h, double new_width, double new_height, + int flags) +{ + if (new_width < 0.5 && new_height < 0.5) + /* do nothing */ + return; + else if (new_width < 0.5) + new_width = *w * new_height / *h; + else if (new_height < 0.5) + new_height = *h * new_width / *w; + + if (flags & GT_RESIZE_FIT) { + double factor, xfactor, yfactor; + if (((flags & GT_RESIZE_FIT_DOWN) + && *w < new_width + 0.5 + && *h < new_height + 0.5) + || ((flags & GT_RESIZE_FIT_UP) + && (*w >= new_width + 0.5 + || *h >= new_height + 0.5))) + return; + xfactor = new_width / *w; + yfactor = new_height / *h; + if ((xfactor < yfactor) == !(flags & GT_RESIZE_MIN_DIMEN)) + factor = xfactor; + else + factor = yfactor; + new_width = *w * factor; + new_height = *h * factor; + } + + if (new_width >= GIF_MAX_SCREEN_WIDTH + 0.5 + || new_height >= GIF_MAX_SCREEN_HEIGHT + 0.5) + fatal_error("new image is too large (max size 65535x65535)"); + + *w = (int) (new_width + 0.5); + *h = (int) (new_height + 0.5); + + /* refuse to create 0-pixel dimensions */ + if (*w == 0) + *w = 1; + if (*h == 0) + *h = 1; +} + +void +resize_stream(Gif_Stream* gfs, + double new_width, double new_height, + int flags, int method, int scale_colors) +{ + int nw, nh, nthreads = thread_count, i; + (void) i; + + Gif_CalculateScreenSize(gfs, 0); + assert(gfs->nimages > 0); + nw = gfs->screen_width; + nh = gfs->screen_height; + resize_dimensions(&nw, &nh, new_width, new_height, flags); + if (nw == gfs->screen_width && nh == gfs->screen_height) + return; + + /* no point to MIX or BOX method if we're expanding the image in + both dimens */ + if (method == SCALE_METHOD_BOX + && gfs->screen_width <= nw && gfs->screen_height <= nh) + method = SCALE_METHOD_POINT; + if (method == SCALE_METHOD_MIX + && gfs->screen_width <= nw && gfs->screen_height <= nh + && (nw % gfs->screen_width) == 0 && (nh % gfs->screen_height) == 0) + method = SCALE_METHOD_POINT; + /* ensure we understand the method */ + if (method != SCALE_METHOD_MIX && method != SCALE_METHOD_BOX + && method != SCALE_METHOD_CATROM && method != SCALE_METHOD_LANCZOS2 + && method != SCALE_METHOD_LANCZOS3 && method != SCALE_METHOD_MITCHELL) + method = SCALE_METHOD_POINT; + + if (nthreads > gfs->nimages) + nthreads = gfs->nimages; +#if ENABLE_THREADS + /* Threaded resize only works if the input image is unoptimized. */ + for (i = 0; nthreads > 1 && i < gfs->nimages; ++i) + if (gfs->images[i]->left != 0 + || gfs->images[i]->top != 0 + || gfs->images[i]->width != gfs->screen_width + || gfs->images[i]->height != gfs->screen_height + || (i != gfs->nimages - 1 + && gfs->images[i]->disposal != GIF_DISPOSAL_BACKGROUND + && gfs->images[i+1]->transparent >= 0)) { + warning(1, "image too complex for multithreaded resize, using 1 thread\n (Try running the GIF through %.)"); + nthreads = 1; + } + if (nthreads > 1) { + int next_imageno = nthreads - 1, i; + scale_thread_context* ctx = Gif_NewArray(scale_thread_context, nthreads); + for (i = 0; i < nthreads; ++i) { + ctx[i].gfs = gfs; + ctx[i].imageno = i; + ctx[i].next_imageno = &next_imageno; + ctx[i].nw = nw; + ctx[i].nh = nh; + ctx[i].scale_colors = scale_colors; + ctx[i].method = method; + pthread_create(&ctx[i].threadid, NULL, scale_image_threaded, &ctx[i]); + } + for (i = 0; i < nthreads; ++i) + pthread_join(ctx[i].threadid, NULL); + Gif_DeleteArray(ctx); + } +#else + nthreads = 1; +#endif + + if (nthreads <= 1) { + scale_context sctx; + sctx_init(&sctx, gfs, nw, nh); + sctx.scale_colors = scale_colors; + + for (sctx.imageno = 0; sctx.imageno < gfs->nimages; ++sctx.imageno) { + sctx.gfi = gfs->images[sctx.imageno]; + scale_image(&sctx, method); + } + sctx_cleanup(&sctx); + } + + gfs->screen_width = nw; + gfs->screen_height = nh; +} diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/001-transexpand.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/001-transexpand.testie new file mode 100644 index 0000000..2949977 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/001-transexpand.testie @@ -0,0 +1,47 @@ +%script +for i in 0 1 2 3; do + gifsicle -O$i x.gif > y.gif + gifdiff x.gif y.gif +done + +%file -e x.gif +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 +uidghuXc1idOy6MskEDXLygiDIiLgVIJSgAAOw== diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/002-cropresize.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/002-cropresize.testie new file mode 100644 index 0000000..5fcdf33 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/002-cropresize.testie @@ -0,0 +1,505 @@ +%script +gifsicle -O2 --crop 25,0+36x36 --resize 100x100 x.gif > y.gif + +%file -e x.gif +R0lGODlhVgAkAPcFAEAUEAAAAIEAAOIAAP/mIP///1VACLoAAPbKAPraEL4AAP/aFP/iHPK2APrS +CPK+AP8AAO6yAO4AAIUAAPLCAP/eGOYA6voA+u6qAJEAAP/iIG1MBBgQAOadAN6VAI0AAJ0AAIEM +DPIAAFlACOqhAPrWDKVpBKVxBEA0EOqqAHFVBF1EBM4AAOoAAMIAAKUAANoAALIAAM6FAMoQEO6u +AOaZAP//APbGAPrOBPbKBPLGAGVIBOalAMqFAOKVAKVtAJVhBLp1AJlpBN6uAK6FAK59AMKJAMKN +AM6JANIE1s6NAO62AKp1BHFQBGVMBEwgDN62ALKNAPrSDHlZBMaFAIllBLJ1APK6ANKNAM4A0r6J +AHlQBOKdAL59AGFEBOKZALp5AL6RAMKRAJFhBIVZBJVlBLJ9ANaVAOK2AMJ9AIVdBNadAOa6BKoA +AIltBL6ZBMqlAFkwEF00ENahAFAkEH1hBMqlBOK6BG1IMDgQEO66AJlxBG1QBK6BBNalAJ19BAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+A0c0NAAh+QQFZACAACH+ +JERlc2lnbiBieSBBaXdhbiAod3d3LmtvbG9ib2sud3JnLnJ1KQAh/wtORVRTQ0FQRTIuMAMBAAAA +LAAAAABWACQAAAj/AAEJHEiwoMGDCBMqXMiwocOHECNKnEixokWKAhQIEGAx48aEFiwI3Djg40KP +HDGWHFAypcKQBQWsbGnwwgWRGWeaPOhxgMaJMmeydEmQgwEOgCzYHBg06NCCNi3kFFqSp4KZPyE6 +tcGyK1GCFpIovTCyK9euVWNeZXnWq1q2XbMyDDB1aFwBAQhGzZKEYF2hGg34BUC4K2EAPuX+dSo3 +YQC6a+0q0Ig3r8CogIxqXpx445ERmgEUGI1YNOmNLjlP3umYbsa1qCs/Bisy81EDGxUUJrwRAYIG +TI6aHl1gOGnggp3kho36gZYdCh9Ljy1b+uyEKnBUMM1dwwIHFFJs/wA0nHDx8uHHu5GyHYAAwhoY +lMgRQU106/jzXz9oIEoJ0YeZB8ACCSAQwQkGlBcgcQAYiOAbCXBHGgMJONCAGSMgpJ9+COCHkAFQ +RHjeYcUtsMBvSoBYHgHlndhAimwAuOCADjxwxgoabmjddxRY96EOIpLGoIm/eWAAkIcRoORhRDZg +ZA4ykkjjA1zgeJCOjylJoIHSfUhBCQwkmeSWERhJgZJopkkAmUbqUB6DFV7xhZUGSZeAdGIS5mKX +/A3hgJpp8oiBDAYMAWig4A1qABr/EcYiYRTShwWdBdnJAIsMMljkfgUZQAQChxLAgIsk/OApqIeO ++lupBoSBw6EV1P+YghUZXhnAl5dGKaCBHnBakAoPIJAAA8QSWwEDCDyAgQ/jAStsBShEi8KxyS47 +XhUUOLAAsaJWkMANEXRARmt+VqCBrgBSMKivBBlQRLAAJGAiAA4kS0MHJgjmbrAOyEtgvQ/cmy8g +TohBAQ4lyFvhDUuQAAZ0OX467LmOHrbqD+wSNIIRDVBgMQUNYNBBELUCsnEDvqUMssgkDzTFGg/c +QFiyEZAgwxb3OTtssQwcu4CyzGbcrhA8RNBAAxFgQIIHQAg2dNFHJ7100wU1cQQNSyCdQgddNLGQ +dO8i0K+JFdqLL58KGbDBCUh0UEMPJmzgdKdrt/123HMTtEMZVNT/UEMaY3jxtXUcU+DbYSGP7CFD +BmSQgQGQL9T445Er5HjlEBlAtNFIK8103g5NIIIIEzQkOukMnV76RGqz7Tbccks0wwQHSED7QrPX +fntCudt+QEWQXw66QweAAEgIIQACwu8IFX988ss3bzzyyjM/keoTHXCACwTF0MIHB2nP/UDeg1+Q ++N1/P1Hvu0N0AAsQWC+B+Qa9H/9A84cPv/z0uz899NZzSAYgcD+BvCCABBlgAQFxwIMo0HoNjAj6 +yKc+iEgAAjBgikIumMGRJISDGpSI/fg3EQkgcIMnRIgJL/LAgURQgu57SAolAkIPXuSGODzICnPI +wx4KZIY+DKIQBYfIw4AAACH5BAQUAAAALA0ADwAJAAUAAAgdAAMEIECwoEGDCxYAWAiAAMOGBwks +cBCRwQIEAQEAIfkEBRQAgAAsDQAHADUADQAACIoAAQkcSLCgwYMIDSpQMGCggAENE0qcSFHAQoiA +BjxcSLGjR4EGogiwyFDjSAEIPqpMqAJHBQYnRxLQsMDBypsFo5RggKIngZ89USTASVRgAhQ/CeRR ++nMBiqJFGcwcmBSq1YkAsgLKCuBqUQAFwoIV65VoAYFdAZ0texMs16wF0rJVyXWr1rkqAwIAIfkE +BTIAgAAsNQAHAA0ABwAACC8AAQECQFDgwIICARRYCGDgwgINFT6EOBGiRIsYGV4kyPFhRI8ENi5I +iPBgQ0ABAQAh+QQJCgCAACwAAAAAVgAkAAAIlgABCRxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgz +atzIsaPHjyBDihxJsqTJkyhTqvQoQACglitLMiBAs2ZMkQsYaKhJk8HNkAAEEhAY9OfHBDN5EvBp +1KNODYBsNp1KtarVlACKXu1IU+tWjQQAJPD6FWNWsmUjil07Nu3FrGPbun0LCO3cu3jz6t3Lt6/f +v4ADC+4bEAAh+QQJEgCAACwGAAMATwAgAAAI/wABCRw4UIACAQIIKlzI0CBChgItWBCIcMBDiAUP +JsRIUIDFARY3cpSo0KPHkAsvXJho8CNKiA4HHORIEeRJmww5GOAAyILKgjZB4iSo0kJLl0NLKnA5 +E+NNG0KTKrSQxOeFmiChRhWZUahWoVwBHf3aVGGAo0FBHgxAdGWWJB2XghV60EBHAHiF4gUgsyxa +pGUFBjgrN6gCjYMHFgWgs/Ffj4cRHhnRGECBy3wtY0Yo8nHfiwMHIzxsk/PgxAMtACDAeKeB0Xnx +IkSAoAGTnZovF8iN2bZdJ6PlchbwQMuO0KfPDk+enOBqAANV4KigubqGBQ4opNgAKDfe3d61c/93 +I6XCcAEaGJTIEUENcubwmTtnLdBAlBKW934HsCABgggnGOCdfroB8F+AbxCg4IIKMpCAAw2YMQIg +8cWHgHwCPVcfFAnkRyB/C9SmhAFQeMdagSE2MCIbDDLIAHYPnLEChRUmhx0FzekHHSAG6NBhgSim +6EGPHeK14F4LCGlADvqxhqQDD3AxY42DKdjff6c9p+COBlBQAgN7ObnalREMSUGLLZI5pA5oNvjg +FV9MOVgCWRqJZG1ZOkkfj0M40KaV2WEggwF9/knAjYIagEYJbTrIHhZyBpAAAxp4V2BtHiS22pb1 +EYHAny/WRsIPBngKaoqjGhAGDm1WAGUKVkz/OJiXDJyo427/ZcpWd5wKpMIDCEzKwLAMVMAAAg9g +4AN3vwZbAQrQomAssspyVwUFDhA7bAUJ3BBBB2QIFkCfFVQK3l67USAoat3tWF8RwAKQQJIAOIAs +DR2YYJcB8CLgwLz92fsAvvoC4oQYFOBQwrwP3rAECWAcR6Onk1ZqJ16i/sAuQyMY0QAFeyFAQQMY +dBDEhAJ13ABtLI9c8skDTbHGAzdg/EAEJMiwBXLNCkussQsku+zGDBkgBA8RNNBABBiQ4AEQdhFk +NNJKM+001Ao1cQQNSyydQgddNEHQaf3+m+SD9+Z7Gk08bnACEh3U0IMJG0StkAFuwy033XYT/7RD +GVTUUEMaY3gxdnIeU4CAfiSbjCFNBmSQgQGUQy455X0zdHnmC02dNF5NP8052xOIIMIEbANS+umk +m44623i/Hffcdaeu0AwTHCBB7jThrjvvGPm++wG2U7657QodAAIgIYQACAjEQ6Q8885DL/3yzT8f +feusIz/QAQe4QFAMLXzAEPjiD0S++cmHP375bAsPvPcHsADB9hKwv1D99w+U//n2w5/+MDK97FnP +exmAQP8E8oLtKSSBCwREAzWnwO1NkCPoe98AUycBCMCgIBzp4AcpghERgpAm/BOg9/znQJpIoIUl +hCFHIGhBGbLNhtJDHg5L6METrvCHQFzhCw2DSMQi2m6HRkxi6gICACH5BAkPAIAALAYAAwBPACAA +AAj/AAEJHDhQgAIBAggqXMjQIEKGAi1YEIhwwEOIBQ8mxEhQgMUBFjdylKjQo8eQCy9cmGjwI0qI +Dgcc5EgR5EmbDDkY4ADIgsqCNkHiJKjSQkuXQ0sqcDkT400bQpMqtJDE54WaIKFGFZlRqFahXAEd +/dpUYYCjQUEeDEB0ZZYkHZeCFXrQQEcAeIXiBSCzLFqkZQUGOCs3qAKNgwcWBaSz8V+PhxEeGdEY +QIHLfC1jRijycd+LAwcjPGyT8+DEA0ky3mlgdF68CBEgaMBkp+bLBW5jpm3XyWi5nAU80LIj9Omz +wY8f56gCRwXN0DUscEAhxQZAt/Hmzl79uhspzwEI/8CrgUGJHBHUGFfOXjlGA1FKWN6rHcCCBAgi +nDCQnT5uAPnt90YC0GHGQAIONGDGCIC01x4C7jFkABQEbrdXbgssMJsSE2ZHQHYaNsAhG/P5Z58D +D5yxQoMOHjcdBctJqEOFmP2X4WweGDDjXgT0uNeNDeSYQ4kXnvgAFyu2OFiP9+V3GkQGUFACAzzy +2GQEOVLQ45ZcEnBljjpk9x+CV3yR5GAJnFYlXiE+KeEQDnTJ5YsYyGAAnHJuSaedaMiH14d4HYge +FmcGkAADGoiJGY6oLWQAEQjkSQADIZLww6OR5knpbJYaEAYOeVaAYgpWMDiYlAx8aGF9+XnQ6EIq +PP+AwKEM1MpABQwg8AAGPlwX66wVoCAsCrjqyut1VVDggK21VpDADRF0QIZgAcBZQaKrzkdBna8+ +8cRABhQhKwAJZAiAA7rS0IEJdoUrqwPl3ofuA+qyC4gTYlCAQwnlInjDEiSAURyLkB6a6J97cfoD +anI8oQEB3wo0ghENUJAwBQ1g0EEQDEpMsWwgY6wxxwNNscYDN+ClawQkyLCFcb/SaiuuC+zaa2Jy +SOEwxAQZIAQPETTQQAQYkOABEHb1/HPQQxd9dNIDNXEEDUsMnUIHXTRB0GniIgBvhgimu+6TOTus +s0IGbHACEh3U0IMJG0Dds9psuw233APtUAYVNdT/kMYYXmx9HMUUyLZXxhu790Qc3kKZQQYGRE6T +AY9HjvdClV+O9tJCE2000grtTEAcEE0ggggT0CSQ6airznrqqqe9dttvx82Qt4szNMMEB0jAO027 +9/47RsH7foDqAkWeOUR4+MjQASAAEkIIgIBwPETQS0+99dhHP33117t+OuwQ0UHH8we4QFAMLXyA +vvoDse++Qgekv377qhc/PPIDHcACBOGTwPwW4j8ADkSAz/tfAAeIkex9j3v8E0gGIGBAgbwgfAqZ +YAUBcUGGaDB8HeRI/eAnEPlFUCASgAAMCsKRFK6QIhhxIQtpUsAFnvCAGKSJBHIYQx5y5IMDCeEN +HX3YwAgSsYUqnOENl8jEG+6wiVCMIv+OKMUq8i8gACH5BAkSAIAALAYAAwBPACAAAAj/AAEJHDhQ +gAIBAggqXMjQIEKGAi1YEIhwwEOIBQ8mxEhQgMUBFjdylKjQo8eQCy9cmGjwI0qIDgcc5EgR5Emb +DDkY4ADIgsqCNkHiJKjSQkuXQ0sqcDkT400bQpMqtJDE54WaIKFGFZlRqFahXAEd/dpUYYCjQUEe +DEB0ZZYkHZeCFXrQQEcAeIXiBSCzLFqkZQUGOCs3qAKNgwcWBaCz8V+PhxEeGdEYQIHLfC1jRijy +cd+LAwcjPGyT8+DEAy0AIMB4p4HRefEiRICgAZOdmi8XyI3Ztl0no+VyFvBAy47Qp88OT56c4GoA +A1XgqLCaNQHWGhY4oJBiA6DceHeD/+fu3Y0U6gAE4NXAoESOCGqQM5/P3DlrgQailKiOgD+ABQkg +EMEJBoC3F3gCEvhGApo1yEACDjRgxgiA0EcfAvUJ9Bx+UDB4HQbVgbjAArUpYQAU4LGm238lGsCG +ZQfCqN0DZ6xQoYXJaUdBczHip4OHCIBIQJD/kdiABwb8uNd1rOE1Ym1I5gDjgbvNyIWNOA52HYAC +nvbcddABYgAFJTCw5JJcRoAkBUy2yWSaSOoA3ooQXvEFloMl4CVeTbLYgJdN3ifmEG66qSMGMhgw +hAOFvrkdogagsR+fez34HhZ4BpAAAypitiKUiVUnqAFEINAoAQwYScIPpJraaKq1rf9qQBg4NFqB +Aw+kYAWFg5HJ6ZR77SagB6hVFyYgKjyAwKYMNMtABQwg8AAGPniX7LIVoKAtCtBKS613VVDggLPN +VpDADRF0QIZgASxagYox7kYBoqh9d6yYRSgLQAIjAuCAtDR0YIJdBuSLgAP8AvjvAwEPDIgTYlCA +Qwn8QnjDEiSAcdyNpW7aZ5+x/lAvQyMY0QAFeyFAQQMYdBAEhQKV3ABtNK/c8ssDTbHGAzfgJW0E +JMiwBXLXMusstAtMW+3IDBkgBA8RNNBABBiQ4AEQdhHkNNRSU2011go1cQQNS0ydQgddNEHQaQYj +PCKEAAt8Gk1ibnACEh3U0IMJG2T/rZABduOtN99+E7RDGVTUUEMaY3ixdnImU9DfXiy7nCFNBmSQ +gQGcY64554Uz9HnoC20dNV5VX0063ROIIMIEdAPS+uusuw473YDfnffefceu0AwTHCBB8DQBLzzx +GBk//AG+cz667wodAAIgIYQACAjMQyQ99dZjr/301V+ffe20Qz/QAQe4QFAMLXzAEPrqD8S++9Gn +v377dCuPvPkHsADB+BKg30L697+BBPB9/gOgADGyvfB5z3wZgEABBfKC8SkkghMERAVFJ8HxbZAj +8LvfAmMnAQjAoCAcKeEJKYIRFaKQJgRUoPkMaEGaSKCGLcQhRzDoQR3SzYfagx4QGFtowhfO8IhI +nOENk8jEJvpuiE6MYuwCAgAh+QQJDwCAACwGAAMATwAgAAAI/wABCRw4UIACAQIIKlzI0CBChgIt +WBCIcMBDiAUPJsRIUIDFARY3cpSo0KPHkAsvXJho8CNKiA4HHORIEeRJmww5GOAAyILKgjZB4iSo +0kJLl0NLKnA5E+NNG0KTKrSQxOeFmiChRhWZUahWoVwBHf3aVGGAo0FBHgxAdGWWJB2XghV60EBH +AHiF4gUgsyxapGUFBjgrN6gCjYMHFgWks/Ffj4cRHhnRGECBy3wtY0Yo8nHfiwMHIzxsk/PgxANJ +Mt5pYHRevAgRIGjAZKfmywVuY6Zt18louZwFPNCyI/Tps8GPH+eoAkcFAAQIQJeuYYEDCik2ALqN +Nzd37NrdSP95DkAAXg0MSuSIoMa48vfKMRqIUmI6AvsAFiRAEOGEAe57cceff28koNmBDCTgQANm +jAAIfPAhEB9DBkBhYHQYTJfhAgvMpkSF3EmHW34eGsCGZQGiaN0DZ6zwIITHWUfBchTqYKAGCGRI +QI75ddiABwbYuFd00uHF4WxA5oBigLmtyIWLMA4WnX78nQaRARSUwMCQQ1IZAZAUECkmkV4CqQN3 +Iyp4xRdQDpbAaVwaOZuVFA7hwJhiyoiBDAbYiSeZ1+1pABr14VUkAAmuh0WbASTAgIiYjYgkagsZ +QAQCfxLAgI8k/GAppn9uOlunBoSBw58VOPBAClY4OFiWjy7/uVdu/HlA6UIqPICAowz0ykAFDCDw +AAY+aJfrrhWgoCwKwApLrHZVUOCAr71WkMANEXRAhmAB2FmBBrJ2BwAFe976xBMDGVCErgAkwCEA +DghLQwcm2KWurg64q1+8D8xbLyBOiEEBDiW4q+ANS5AARnEvXuoouIbuNeoPqMnxhAYEoCvQCEY0 +QIHEFDSAQQdBOLhxx7KlHPLIJQ80xRoP3ICXsBGQIMMWxh3Lq6/ALjBssYnJIcXFGRNkgBA8RNBA +AxFgQIIHQNhlNNJKM+001FIP1MQRNCzBdAoddNEEQaeti0C+HCooL71WCn3x0AoZsMEJSHRQQw8m +bJC10XPX/3133nsPtEMZVNRQQxpjeEH2cR1TINteIpMc3xNxnHtlBhkYoDlNBmCueeALeQ563FQv +3fTTUStENAFxQDSBCCJMQJNAr8c+e+2yzy433XbjrTdD51LO0AwTHCBB8TQRbzzyGCl//AGzC6S5 +6BDhER0ADB0AAiAhhAAICNBDpD333oMv/vbdfx/+7bDnDhEddGR/gAsExdDCB/LTP5D9+Ct0wPz1 +u9/snMe86A3kACyAwPok0L+FIFCBA2Fg9hK4wAZiZHzpM58BBZIBCEBQIC9Yn0I6+EFAhJAhJFzf +CTnyP/0JhH8bFIgEIACDgnBkhjWkCEZwaEOaPLCCMYygCCtpIoEh7tCIHEnhQFYYRCRecINOvCEN +exjEKloxiEW8oha3aMAocvGLBgwIACH5BAkSAIAALAYAAwBPACAAAAj/AAEJHDhQgAIBAggqXMjQ +IEKGAi1YEIhwwEOIBQ8mxEhQgMUBFjdylKjQo8eQCy9cmGjwI0qIDgcc5EgR5EmbDDkY4ADIgsqC +NkHiJKjSQkuXQ0sqcDkT400bQpMqtJDE54WaIKFGFZlRqFahXAEd/dpUYYCjQUEeDEB0ZZYkHZeC +FXrQQEcAeIXiBSCzLFqkZQUGOCs3qAKNgwcWBaCz8V+PhxEeGdEYQIHLfC1jRijycd+LAwcjPGyT +8+DEAy0AIMB4p4HRefEiRICgAZOdmi8XyI3Ztl0no+VyFvBAy47Qp88OT56c4GoAA1XgqKC5uoYF +Diik2AAoN97d3rVz/3cjhToAAXg1MCiRI4Ia5MzjM3fOWqCBKCUs7/0OYEECBBGcYIB3++kGAIAC +vpFAdZgxkIADDZgxAiDyyYfAfAI9Zx8UC4K3124LLFCbEgZA4R1rBorYAIls6Fdgfw48cMYKFFaY +HHYUNLcfdIAYoEOHmKWoogc+LogXAUjuFWJtRObg4ocwPsAFjTYOhqR/AJ72XJL2UVACA3uxdiSW +ERBJAZJopkkAmUTq4J2BD17xBZWDJaDlkUrWpqWY9fU4hANqpokjBjIY8GegaA5aKBr54YmXg+1h +QWcACTCAYpCYMZnYalz2SAQCiBLAgIok/GDAp6GOWlupBoSBA6IVxP+YghUTDualpU/yB6AHqHHK +IyAqPIBApQwUy0AFDCDwAAY+cBfssBWgIC0KyCrLLHdVUOCAscVWkMANEXRAhmAB/FmBBrnqRwGh +qHX3a49FCAtAAiEC4ICyNHRggl0GxIuAA/T6d+8D+e4LiBNiUIBDCfQ+eMMSJIBxXI2fVoquowc2 +UGq7DI1gRAMU7IUABQ1g0EEQEwrkcQO0tUyyySgPNMUaD9yAl7IRkCDDFsg9S6yxyC6wbLMcM2SA +EDxE0EADEWBAggdA2EXQ0Ukv3fTTUSvUxBE0LMF0Ch100QRBp/kLcIgP4qvvaTT1uMEJSHRQQw8m +bCC1Qga8Hffcdd//TdAOZVBRQw1pjOEF2cl9TAEC+5V8MoY0GZBBBgZUHvnklfvNEOaaL0S10ng5 +DXXnbU8ggggTtA2I6aiXfnrqbecNt9x02626QjNMcIAEutOU++69Y/Q77wfcXjnntyt0AAiAhBAC +ICAUD9HyzT8f/fTMOw+99K63nvxABxzgAkExtPABQ+GPP1D55ysvPvnmtz188N8fwAIE3EvQ/kL2 +4z+Q/ui7X/72hxHqae9638sABPwnkBdwTyEKZCAgHLi5BXKPghxJH/wIqDoJQAAGBeGIB0FIEYyM +MIQ06d8Av/e/B9JEAi40YQw5EsELzrBtN5xe8nJowg+ikIVADCILDmEoxCIa8XY8PKISVRcQACH5 +BAkPAIAALAYAAwBPACAAAAj/AAEJHDhQgAIBAggqXMjQIEKGAi1YEIhwwEOIBQ8mxEhQgMUBFjdy +lKjQo8eQCy9cmGjwI0qIDgcc5EgR5EmbDDkY4ADIgsqCNkHiJKjSQkuXQ0sqcDkT400bQpMqtJDE +54WaIKFGFZlRqFahXAEd/dpUYYCjQUEeDEB0ZZYkHZeCFXrQQEcAeIXiBSCzLFqkZQUGOCs3qAKN +gwcWBaSz8V+PhxEeGdEYQIHLfC1jRijycd+LAwcjPGyT8+DEA0ky3mlgdF68CBEgaMBkp+bLBW5j +pm3XyWi5nAU80LIj9OmzwY8f56gCRwXN0DUscEAhxQZAt/Hmzl79uhspzwEI/8CrgUGJHBHUGFfO +XjlGA1FKWN6rHcCCBAginDCQnT5uAPnt90YC0GHGQAIONGDGCIC01x4C7jFkABQEbrdXbgssMJsS +E2ZHQHYaNsAhG/P5Z58DD5yxQoMOHjcdBctJqEOFmP2X4WweGDDjXgT0uNeNDeSYQ4kXnvgAFyu2 +OFiP9+V3GkQGUFACAzzy2GQEOVLQ45ZcEnBljjpk9x+CV3yR5GAJHNclkE9KOIQDXXL5IgYyGPBm +nFvOWSca8uH1IV4HoofFmQHgmWeIHqC2kAFEIGAoAyGS8AOjjuIJ6WySGhAGDnhWgGIKVjA4mJQM +/OknffklyhZEKjyAQAIMxP8aawUMIPAABj5c1+qr4eFFq624XlcFBQ7IOmsCN0TQARmCBXCnoXoq +CsgTTwxkQBGuApBAhgA4YCsNHZhg17WuOqAABBAo4O0D4IoLiBNiUIBDCdsieMMSJIBRHIuNPhrp +D6jJ8YQGBFQr0AhGNEDBXghQ0AAGHQTB4MEJU4DAYQ0/HPHEgEyxxgM34GVrBCTIsIVxu8JqLK0L +3JprYnJIMbAGBgtkgBA8RNBAAxFgQIIHQNhF0M0579zzz0Er1MQRNCzBcwoddNEEQadhi4AD2963 +brtPxjywzAoZsMEJSHRQQw8mbCB02GOXfXbaaxO0QxlU1FBDGmN4QfVxFcv/tpfGQbj3RBzUQplB +BgYkTpMBhyceN0ONP74ozjrz7DPQj8+sQRwQTSCCCBPQJJDnoItOeuiii0222WirzRC1gzM0wwQH +SEA7TbPXfjtGudt+gOg2M4645ALh4SNDB4AASAghAALC7xAlv3zzz0evPPPOQ2/656hDRAcdyB/g +AkExtPBB+OMPVP75Ch0gPvnmi9777sAPdAALEGgvAfsL3Z//QPtDHv70xz+MSA971aufQDKALu29 +QHsKYeD/BPJAyDVwIBXkiPvSJ5D1KVAgEoAADArCkRCOkCIYMSEJaeI/An4QgBCkiQRimEIackSC +DrSh6HQYPQXyMIUiXOELEodIxBfOsIhITGL9fqjEJgIvIAAh+QQJCgCAACwGAAMATwAgAAAI/wAB +CRw4UIACAQIIKlzI0CBChgItWIBIUaDDhBULDhAwYCPGihIVcuToceGFCxMRHkT4seHBAQczAiJJ +smPLgRwMcABk4aTGjR1tKjxpwaDHjjFdAoV5U2RQG0GFMrSQpOcFi0+jblxodGnQpl2hIm0KKEBX +m0EPBiBINEsSgme9HjQAV8HTmnPrRsV7M4BZu19hrvQ7kCignIjjclSw8siIxIDRMkbouGBkoJP7 +mjUImKUAv4QHhjys04BKAAAGoEaIAEEDJqUFoE69cXbr13SdqOzM8oGWHQNB+/X8WTjoiipwVABQ +oABz5xoWOKCQYgMgFc+dy27ufHp1QG6kLP8HIBuABgYlckRQE9y4e/cUDUQpwXw2aucLEiCIcMJA +9ub/Obdff28k8NyBDCTgQANmjFDWe+4hYBxEBkBhoHP24beAa0r4x9199wHIoQFs1JchANI9cMYK +D0IImnQUCEehDhcC+OECGzbggYcAZiiijgbkYOJs+DnwABcsuugXAQTkt99xDBlAQQkMzEaAlU5G +sCMF2QFw5Yf6aWmADl2C6cAVXyTpVwLCMekmjq5BuZABQzjg5p1MwoiBDHQ6YCWTs+nJJxr0oXYl +agmqh4WaAeCJJ5w6hjYnEQg4yiQDOZLwgwGUWkoApq5pakAYOFhagZEpWOGgX1MycKih9u3/54Gk +C6nwAAIJMKCrrhUwgMADGPhgna247sqrr8AKC0gVFDhgLAMVJHBDBB2QIZBfdXra5HR70qqQAUXc +CkACOALgwK80dGACXeDe6uds5z6Q7rqAOCEGBTiUQK6CNyxBAhjAPdippaA2oKlfqEE0ghENcIka +AhQ0gEEHQTgo0MINtwEBBG1EPHHFA02xxgM3PPxABCTIsEV7xCbQ66W9LpDsBkteSaEQPETQQAMR +YECCB0DQRZABOGvMsc9ACz1QE0fQsATPKXTQRRMEgRYuAg6Qm1+884J2pYEUbnACEh3U0IMJGyg9 +tNhttGE22moPtEMZVNRQQxpjeFG1cAxT/9DabBJTbNxsFRmQQQYGJJ5R4ozHvdDhiheOs848Ix30 +uJiDXdEEIogwgUwCHQ46556DboDYZL+dtkCoGag5RDNMcIAEsstE+ucVxT577aYbjrjjAAASPEUH +gABICCEAAsIBuctOO/MQFX988suDDsjt1it0wAEuEBRDCx9EbzzyykOvPffegy+T7s9nP9ABLEBg +vgThM7R99wN9Xz9B8Ms/EP0ykR75que+DGzMfC8wn/biN7/9EcSA/hNIAjNyv/Q5EHQSgAAMCkIR +CCJQgQTJ4AYtEkAG/u+C1pMACCkiQg6ycIUV8eBAJug+gcDwhda7YUVaSMIa+hAQOvwhQw1UKMQi +GjGHR0ziEQMCACH5BAUKAIAALAYAAwBPACAAAAj/AAEJHDhQgAIBAggqXMjQIEKGAi1YgEhRoMOE +FQsOEDBgI8aKEhVy5Ohx4YULExEeRPix4cEBBzMCIkmyY8uBHAxwAGThpMaNHW0qPGnBoMeOMV0C +hXlTZFAbQYUytJCk5wWLT6NuXGh0adCmXaEibQooQFebQQ8GIEg0SxKCZ70eNABXwdOac+tGxXsz +gFm7X2Gu9DuQKKCciONyVLDyyIjEgNEyRui4YGSgk/uaNQiYpQC/hAeGPKzTgEoAAAagRogAQQMm +pQWgTr1xduvXdJ2o7MzygZYdA0H79fxZOOiKKnBUAFCgAHPnGhY4oJBiAyAVz53Lbu58enVAbqQs +/wcgG4AGBiVyRFAT3Lh79xQNRCmRvf6CBAginDCQvXl/5/nt90YCzxXIQAIONGDGCGW95x4CxkFk +ABQEOoeahQAssIBrSvDH3YUX+sehAWwwNxuI0j1wxgoNOgiadBQIJ6EOFc6GoYaueeChfyd+mKMB +OZjYY4YOPMAFiy76RQAB9+V3HEMGUFACA7MRUGWTEehIQX1WfohflgboUJ+XDlzxBZJ+JQBalbNt +2MCTCxkwhANL1mknjBjIIKcDVS7Z5nR5GoAGfahZidqB6mGBZgAJMNClfz424EFocRKBgJ12MuAm +CT8YYCmmdWrqGqcGhIEDqARUUGQKVjDol5SOCv9pIwD5TboWRCo8gECjDPTKQAUMIPAABj5Yl+uu +vvYKrLDEWlcFBQ4k+2sCN0TQARkC+TVnBRrICiIFeVIaZxG6ApCAhgA4ICwNHZhAlwHkIsDnbOo+ +wK67gDghBgU4lHAugjcsQQIYwDVoaaPdFmpbA5yKu9AIRjSwJWoIUNAABh0EwaBAEEvcBgQQtGEx +xhoPNMUaD9xA8QMRkCDDFu0dy6uvwC4wbLEOxykEDxE00EAEGJDgARB0EWTAzh+HHPTQRQ/UxBE0 +LPFzCh100QRBoMXrwLn31XsvnPFtcAISHdTQgwkbNG202G20YTbaag+0QxlU1FBDGmN4gbVwEVP+ +0NpsF2ccoUwGZJCBAYhnhPjicS9keOIVHc2zz0ALTbRMC00ggggTYA6I4ZhrzjnmBohN9ttpe07Q +DBMcIEHrMoneeUWsuw476YUf3jjmB4AASAghAALCAbS3/jrxEPX+e/DDey676gQdcIALBMXQwgfJ ++w688MgrJD31A1mPfUa1Hw+9QAewAEH3Eoy/0PfVX/+++uy7T5Hy2zcPfQYgd/9C996j30DaxxD+ +rW8g/8sI/MInP+hJAAIwKAhFDOg/ABLkgRG0iEzSd0CBEPB8gJCABSmCQQmScIQVoSACUbhBB7Lw +fSAsoQZBCMIX0pAhIryhDnfoORvy8IeYCwgAIfkEBRIAgAAsDwAMAB4ADAAACC0AAQkcSLCgQYIF +BiY8yLDhQkAPG0qcSLGixYsYM2rcyLGjR44APh4EEFKkwIAAIfkEBQoAgAAsDwAWAAMAAgAACAgA +EQAa2CBCQAAh+QQJEgCAACwGAAMATwAgAAAIcgABCRxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgz +atzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjyhQJAMDMjARy2rxZMYHOBDtLBu0IwCfQmiiRcizK +FChPijWBOn0KFdBQqlizat3KtavXr2DDim0YEAAh+QQJCgCAACwGAAEATwAiAAAI/wABCRxIsKDB +gwgTGhQgQAFDhgojSpxIMaEFCw0HMBwwwKGAihMbQpx4caHHhyMBXbiQUSPHlxoRivwY0qVGmgY5 +GOAAyMJKggJuKoDpUCDLoRxtBOXo8aDIjjgTLl3KMWpBC0l8Xhg4FeZLnC2rdizqdOhSsjJf2vBq +daWFLEkKZrSB1CtZhgqQopSJdC1TqwIDhLUJNQBBt4B0KjaAd/DXxRxQCqDgpw8fA3LrEkYbWLDm +oHkZBjA8sGTinTtHwHmYt7XDIyMW/8lRIkGCEg4Q6NmDmevnsSk749X7cPRoiQailKjAQDJDBA2Y +YFZdgoF16xUSIHgAW6CT4VUfPv/QsmOg8dGSz5+PqAIHAAAa3hOYr2GBAwopNiQvoaF/fwIaaAed +dIC4IUUF7wkgHwMl5BCBGuapJ6GECiUHQAEFXJjhfAQsoF0EJ7TH4YgA2offBiu8kYCGLDKQgAMN +mDECIBNOiIB6CRmgYYYYbtjhAtDth8KIGnC4AAoIgGgAGxe+52SG9j1wxgo01nieieshpCOG73EJ +wHwLANmAAVAkwAAKaA6ZJgpiKmFADk062WSUXFBp5Whgfmhcjiz2mWeSBuiQwJAkgolkAx4EuuOi +L17xhZ2jJWCckwQ4KeaeWg5BKaVg3oeBARQUOqKHSSaKRgmbvueig1hAGoCZlfb/uCh0HhynJREI +iEoAA2KSYMAQDujaoacyGBAGDqJW4MADKVgx42gUVBernE2WaitCKjyAgJnXMcDcdhj4YACuuvIK +HQk/GFAFBQ50620CN0TQARmdAVtBfBnKmSEFGMhw7UEGFPEAAA4ksAAAL25HQwcmGJDttu5++0C4 +GwDihBgU4FCbhw7csAQJYJRXJa5mxiefk+f+8O9BIxjRAMoIUNAABh0EMWPA2jogZ24PLNywQFOs +8cAN720XAQkybBHhw9xex9wCE/uwwcoAC8FDBA00EAEGJHgARG+AtNwABW1AAEEbMtNsM0FNHEHD +Elqn0EEXTRBknMAIFBxmwj0z/4xpRAZscAISHdTQgwn6FWSA1WWfzbXXYA+0QxlU1FBDGmN4Yfd5 +LlOAAAJOzlwzjhMZYEAGGZiupeBttGE44pEXhLrqFVqNtdaPfw0SQROIIMIEgJsuvEK9/1664IS/ +nvjuAs0wwQESPB8R6hI5D730pZ+eeuwgHQACICGEAAgIByRUPPAJeQ+++ORTdD7zAx1wgAsExdDC +BwdZH335B8lP/0D2w19E9Ic95h2ABRDgHyAkIMCCqC9841MgQQ6YwIEwUCIPZJ8EK5IBsynwBRsE +hP/qdz+DdLCCAgFhREYIwBIyTwIQgAFX+odABV7wIDCUoUAAYxAK2rCBLwyh7C88OBAVIkQCQkzI +CT+YxIk0kSA5nGH6XhhDKcIPJEi8IkWyqEX4PbGL8QOjGMEYEAAh+QQFCgCAACwGAAEATwAiAAAI +/wABCRxIsKDBgwgTHgQAQKHDhxAjQrRgARAAAgQaStwISIACAQIiUjToEaRJkAMvXLAAAORHkw5L +hoQoYEBNmzMNcjDAAZAFlQRruhxAdMBHgSoteMRJ9CXCkkZzJrx5k6jUghaS/LwwkGrRr1KX2vxq +laSCsVEVVrVB9mrSLEkKLrVxlmzajnUH2KjqNGhdtk2vCgwg1mrRjwEIJgW0s7EBl4XBNo5cU8FH +A3Lzor07MABhzZVfeiY4kjFPniPgmLTM+uORETtd1j2pZw/mrqCN9u1M2ONsk55HOzQQpUQFBidN +ImjABHPq5AwqJEDw4LVAJ7KtmnygZQfv4MmDi/9PrFAFDoYa0GPUsMABhRQbiJfQQJ8+AQ3TlzcH +5EZKBYYCoMdACTlEoMZ34yUonkLEAVBAAQ5CiBEBC0wXwQnm1achfe29t8EKbyQQ4YgMJOBAA2aM +AIiCCiIwXkIGRAjhgxJSuMBy8qEw4X0TLoACAhcawIaDDBUJYXsPnLHCiiyK1+GCCMX4IENTXmQj +jlAkwAAKXOrYJQo3NqCEATkQWSSRSHKxZJOeYVQhkMHBOOKcblpogA4J6Lhjjz824MGdMgZq4hVf +rOlZAsEVmRFDYcYZ5RCKKuqmexgYQMGee74ZwZ9olBApQyUWiIWhAWiZEY2BLueBcAcZQAQCmGL/ +xECYJBgwhAOxTkoBBjIYEAYOsVbgwAMpWKGiZxSUwMCpZxIJ5KrkIaTCAwhoycC10TFAHQY+uApr +rLMuR8IPBlRBgQPYXivdDRF0QMZgAdxaQXoQngnhrjKw2moRDwDgQAILAGAidTR0YIIB01abbrbb ++rABIE6IQQEOJQBs4g1LkACGd0y+qmV6DC0KgLg/6HvQCEY0UCQCCFDQAAYdBKGiAfwi4MCZDhBs +8G1TrPHADQxRFwEJMmzBW8LWYnvcAg9wu4HJrQrBQwQNNBABBiR4AMRtgKDcAAVtQABBGy7DLDNB +TRxBwxJWp9BBF00QFFzN/y5QYc4PFGyCo8Nt/3ACEh3U0IMJ8RVkgNRhj4211lwPtEMZVNRQQxpj +eCG3eClTwHKRL8f8IkQGGJBBBqFH6XcbbQhOeOMFjV46g1JTbfXiW3M00AQiiDDBcKH3rhDuuoPu +N+CqF247IDNMcIAEyjs0+kPJL9886KKTzvpGB4AASAghAALCAQkBv3tC2W/f/fcRiX+8QAcc4AJB +MbTwwUHRMw/+Qe2/P1D88ztU//S2OwALIHA/QEigfwUpH/e8V0CCCJCAAzngQxR4vgZKJANiK+AL +LAiI/MFPfgbBIAQFskGHeHB/ILSdBCAAg67gb4AFlOBBVthCgQjGIA+MIQJVyMHWZXAgJUSIBCd6 +mBARapCIEEEiQWjoQvKpkIVNXJ9EhihFiFCxigHE4gS1yEUpBgQAIfkECQoAgAAsAAAAAFUAIwAA +CFkAAQkcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cw +Y8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypdyjRmQAAh+QQFEgCAACwGAAMATwAgAAAI/wAB +CRw4UIACAQIIKlzI0CBChgItWIBIUaDDhBULDhAwYCPGihIVcuToceGFCxMzLnQ44KBKkiQ7fiTI +wQAHQBZOatzYUabCkxaSIDyIcKZIBTxbGhXZ00ZPnwyD5rxgsenTjQ2RIuxIdCXSAU65LgUUwODT +mAcDEASaJQlBsz3RCjBwVOZVrAW/2hWrMEBZvTwVEPU7ECigmojh2hWM8MiIxHptyBUwIu/Zll0J ++h36tagAv4QHhjxs08BQAAAGoEaIAEEDJqUVBz7oGjYgJ5xlFn2gZcdA0Js9AwdeUQWOCgAKFEi+ +XMMCBxRSbACkIrdHhACgSwfkRkoFzwI0MP8okSOCmt/D06enaCBKCebLlS9fkABBhBMGmIOXv9w+ +/jcEBChggAwk4EADZlSmnnoIDAeRAVAkwBxqyqG2wAKuKZGffBRSWGGGBrAx4IAMPPfAGSuQtSBw +z1FAHEMG6CBhhRMCcKFrHmxII2rwAYCjATnwCAABQprIRYor+hUgffaB9iAFJTDAI5GoEcBkBDlS +0KMGPdaHZYwjCligA1d8gaRfCYA2JY8YNuAkjEM4EKaALWIggwFxThkgm9DZaQAaJcxZYHlYnBlA +AgwQySGHOIa2kAFEIDAnASW6RsIPkEoqaJuXGhAGDnNW4MADKVihYABQJpqckKva54GjC6n/8AAC +iDJgKwMVMIDAAxj4MJ2stN5qa6679jpdFRQ4ICyuCdwQQQdkCORXnBVwuZyQy1FgJ6wKGVDErAAk +cGF2u9LQgQl0eTurAyi0i4ID5Z5LlxNiUIBDCeIaeMMSJIDhm4qRIspllTxa+gO3Co1gRANaooYA +BQ1g0EEQlQmkcAOtZQyxxBQPNMUaD9zg8AMRkCDDFugBW+utuS7Aq68IdysEDxE00EAEGJDgARB0 +EWTAzDXfnPPOPQ/UxBE0LHFzCh100YRmfn2LgAPi0gfvA+aa8GZFBmxwAhId1NCDCRsU7bPXYItN +ttkD7VAGFTXUkMYYXkAN2sIUICBkxBM796iSARlkYMDgGQ1uONsLBU4410A3gNrQPKu00AQiiDCB +5IAELjnllkve9ddhj1025gTNMMEBEpyuEueXV2Q66qp7DrjgiEt+AAiAhBACICAc4PrpqfsO0e25 +79475qyTTtABB7hAUAwtfDA87rrzLrxCzDs/EPTSZ/R68MoLdAALEFwvQfcLZf989OmTbz76FBFf +/fHKZwBB+QO9cD327g90PkP2w59A9JcR9W2PfcqTAARgUBCKBPB6BGSIAhloEZWMT4CA+F/4Mri/ +ikywgRSRQAcz8sD8jdB2CTwhQ1TowQWCcIPhYyEMJSjDGdrwhsvDoQ5xGBAAIfkECQoAgAAsBgAD +AE8AIAAACJQAAQkcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4rMCAAAoJIjDaJE +qRFAgQIuYaYkaPKkS5IwS5aUOVNggYE/MwatGbTnx5gwX/I06lGnzZpMo0qdSrXqRwJQrW7EmiCr +1osEurLsieBj17NeZzaI0LEk2q8bTaaFS7eu3bt48+rdy7ev37+AmQYEACH5BAUSAIAALAYAAwBP +ACAAAAj/AAEJHDhQgAIBAggqXMjQIEKGAi1YgEhRoMOEFQsOEDBgI8aKEhVy5Ohx4YULExEeRPix +4cEBBzMCIkmyY8uBHAxwAGThpMaNHW0qPGnBoMeOK10ChXlTZFAbQYUytJCk5wWLT6NuXGh0adSb +XaEibQooQFebQQ8GIEg0SxKCZ70eNABXwdOaSQvaDYr3ZgCze9EqWPl3IFFAORPH5TgY4ZERigMD +bfzgsd6vMPMO/KtyL0sBfwsPDIlYpwGVAAAMSI0QAYIGTHQC6GyTZQEAsOk6oe0R4QMtOzaHNvt5 ++PCKKnBUAFDgdnMAGhY4oJBiAyDmBT4LwH6bunU3UpbP/06tgUGJHBHUCDfO3jhFA1FKME9N//aC +BAginDDAnX7q5rflt98bCWBnIAMJONCAGSOU1R57CLjHkAFQFHibf/Yt8JoSFPZHQH8aNsAhG/Nh +CIB0D5yxgoMPhiYdBcdNqIOFzwF4YogeGDAjfQT0SN8COBqQQ4n1nejAA1ys2OJfPd6XX2gQGUBB +CQzwyKOTEeRIQY9cdkkAljnq0J+NCV7xhZJ/JRCalamFCOWEQzjgZZcvYiCDAXHOyWWdd6IhX2of +poYgeligGUACDHxoo42veSDaQgYQgYCeBDAQIgk/RDqpnpa+hqkBYeCgZwVHpmBFg39NmSiR/wGQ +n6NrQf+kwgMIIMrArQxUwAACD2Dgg3Wz1loBCsSioCuvvlpXBQUO4HprBQncEEEHZAj0V5wVaMDq +fBTY+SikRdAKQAJAAuAArzR0YAJdBoSLgAPk3nfuA+muC4gTYlCAQwnkJnjDEiSAEZyDkiKqLaD0 +efrDtwuNYEQDFCRMQQMYdBBEgwI53IBrHE9c8cUDTbHGAzekxmsEJMiwhXDB2oqrrgv0+ivDkArB +QwQNNBABBiR4AARdBBlgM8468+wz0AM1cQQNS+icQgddNEFQaO7CC2SC6Kr7ZkUGbHACEh3U0IMJ +GyAdtNdgi0222QPtUAYVNdSQxhheTD3cwxQg4B/FFkv2mJEBGWRgwOB/Bz442wsZjrhCQt/cQGpG +/yzTQhOIIMIEk1d+uUyaYy5T11+HPXbZkys0wwQHSIB6RqenvjpFrat+QOmDK166QgeAAEgIIQAC +wuwQ5b57778HrzvvvgPPueWe3y7QAQe4QFAMLXzAEPTSD0S99bhHP331MsX+uvMHsACB8hJwv1D5 +5w+U/vXmo68+RcIjX7zzGUDQvkAvKK9Q/vsDRP8YAkDlDbAi2Pve/CYnAQjAoCAVaeADLUIRCUIw +I+yTn/Pc57+MSKCDFQRhRQo4kANuUIT0ux0KI+jAC27whTDc4AdjSMMalm6FNsxh6QICACH5BAUS +AIAALA8AFgADAAIAAAgIABEAGtggQkAAIfkEBBIAAAAsDwAWAAMAAgAACAcAAQhsIDAgACH5BAkK +AIAALAYAAwBPACAAAAiEAAEJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIENqJEAA +EEmRKAmSRGCSAMuUKEliaDkTpkiXM3Ha3Mmzp8+fQIMKHUq0qNGjSAu+TBoRQEmCDSIwfejU6VSK +ALKSTHBVIoAEX792hRgWrIKxZAEBQMu2rdu3cOPKFRoQACH5BAkKAIAALAEAAwBUACAAAAj/AAEJ +HEiQoAAFAgQUXMjQIEKFDS1YaEhR4MGEFSkKGLCRI8SKEgtu7Iix4IULExM+LNnw4gCEGReSHECT +I0MOBjgAsnBy4MyaHwWetHDQI82HDIt2hBnTYk0bNWkGJWghCc8LTmlCjToVUFGOUaWKVKC1JtOK +Ab5KNSsgAMGhWZI45MrWwFytHV+y9EoWKFu0afuuVfAwgFuhKAHhXKwWLOGER0YwFuwY4YPIPilv +fNyVoGGVfRMmNGyY6kTFOQ2oBABgAOuECBA0YJITAGipogsAmG3XyW2PCR9o2dGQ9GfRbY2TrqgC +RwUABAhAl65hgQMKKTYAAlCgAHIB3LsD/8Cu3Y2U57ZZa2BQIkcENcWVy5dP0UCUEtMR5AewIAGC +CCcYEJ5urLHWnW7/BfhGAuE1yEACDjRgxggLzTcfAso1ZAAUDEaHwXQfLrCAbEpsOCB0J47YQIls +cFeggfw58MAZK1RooXHWUWCchjp0iMCHBPzIn4oeGNBjgdFJx5qIshWZg4sv6mbdA1zUWNCNhkXX +33/LMWQABSUwgCSSW0ZQJAVJpplkmUXqcOKBAEB4xRdWemZYAqSNuaRsXS5kwBAOqJlmjhjI8Geg +gmp5XaEGoIEfa0oC8KB7WNQ5EGkJMCAdnHA2WZqXRCCQKAEMqEjCDwaEOmqpsp1qQBg4JP9agYwp +WEHhlQGAqSmUBSJo5qcMqfAAApkyYCwDFTCAwAMY+KCdsMRWgMK0KCS7bLPaVUGBAwsYS2oFCdwQ +QQdkEMQaIIYBWoEGvMJIQaHA+lnEsHGKCIADy9LQgQl2GTAvAg4kICKE+e7bmxgU4FCCwBDesAQJ +YBC3HaQAGBZqpuxSzFqrP8S70AhGNEBBgQhQ0AAGHQRxKyAgNxDbyyajrPJAU6zxwA0bPxABCTJs +IVCcDKIYALTFHpvsAsw667GfQvAQQQMNRIABCR4AYRdBBjT9dNRTV331QE0cQcMSUafQQRdNmDud +dIb9G/DA+D6grwl91rfBCUh0UEMPJmz/8DXWd+e9d99/D7RDGVTUUEMaY3hhbgIJJFmxYSFTEFuB +J6ecYUwGZJCBAaBn1PnnoVfkeekFsQY5AFhrDbXUVFvdVEETiCDCBE3VfntMuuPOUJznFmRA4Hrz +7ffsBM0wwQESLJ+R8sw7TxH0zR/QEOv1jY468oAcAAIgIYQACAjWN+Q9+OKTb/734Y9fPveA9A6/ +QAcc4AJBMbTwAUP13z9Q/vsrSP/wpz/4UU963DsACyDwPgkEcCEKZOBAHMi/BTbwgbM7X/vUx70M +QECCAnnB+wriQRACQoQMKeH7UIi8Af6vgNyTAARg4JOKyJCGFqHIDWuYQAtOEIPIk8AIKzMixNkV +cX4CUeFAWAi/IWbEiRSBYgxnyEMkWvGKMZQiFrfIRQF28YtIDAgAIfkECQoAgAAsBgADAE8AIAAA +CP8AAQkcOFCAAgECCCpcyNAgQoYCLViASFGgw4QVCw4QMGAjxooSFXLk6HHhhQsTER5E+LHhwQEH +MwIiSbJjy4EcDHAAZOGkxo0dbSo8acGgx44rXQKFeVNkUBtBhTK0kKTnBYtPo25caHRp1JtdoSJt +CihAV5tBDwYgSDRLEoJnvR40AFfB05pJC9oNivdmALN70SpY+XcgUUA5E8flOBjhkRGKAwNt/OCx +3q8w8w78q3IvSwF/Cw8MiVinAZUAAAxIjRABggZMdALobJNlAQCw6Tqh7RHhAy07Noc2+3n48Ioq +cFQAUOB2cwAaFjigkGIDIOYFPgvAfpu6dTdSls//Tq2BQYkcEdQIN87eOEUDUUowT03/9oIECCKc +MMCdfurmt+W33xsJYGcgAwk40IAZI5TV3oPHMWQAFAXe5p99C7ymxIT9EdBfhg1syMZ8FwIg3QNn +rOAghKFNF+FCBuhQ4XMAmgiiBzEWmBoBPNK3wI0G5EBifSY68AAXKrL4FwH35RcaRAZQUAID9Hm4 +Y5MR4EgBj1x2ySR+WcbYX40JXvFFku1VSd9rT0o4hANedikdBRjIYMCbcXI5Z50GoCHfjvQhiB4W +aP7FAAEajPlcAx6IBiMRCORJAAMgkvCDAZBKSulrlhoQBg55VmBkClY0GNqUPVpIZJaOLqTCAwgk +/8DArLNWwAACD2Dgg3WvxloBCsCiYCuuulpXBQUO0FprAjdE0AEZArVYgQYeqvofAHW2qpABRcAK +QAI/AuAArjR0YAJd3MLqALj3jftAuecC4oQYFOBQArgJ3rAECWAEt2Ksh/pnJW6WaqvQCEY0QMGa +FDSAQQdBNCgQwg24ZnHDD0c80BRrPHBDarhGQIIMW6wHa4E9MmDrArnuavC2QvAQQQMNRIABCR4A +QRdBBsQ8c80357zzQE0cQcMSNafQQRdNEDTcAwD0mJq78LZZkQEbnIBEBzX0YMIGQ/Oc9dZdfx32 +QDuUQUUNNaQxhhdODxe1l7hl7J5MBmSQgQF8Z+yU9959V6R34BQB8IDU2OKss0wLTSCCCBMw7jjk +Mk0eeUaG02024wrNMMEBEnyekeegi04R6aEfgPnhAxPOuUAHgABICCEAAoLqEMU+e+235y477bbj +XjjrAFD++kAHHOACQTG08AFDyS8/UPPPKxQ9885jnlrpwr9+AAsQCC9B9Qt9H/5A40MPvvjkU6Q7 +8L0fnwEE5wv0QvcEzV8/IPczpL/w/avI9aaXveNJAAIwKEhFDphAi1CEgQrMiPnYdzz04W+BF4SI +BDJIkf8NJIAV5GDuvFdBQEDQgSVMoQpft8EVuvCFEoShDFcYEAAh+QQJCgCAACwGAAMATwAgAAAI +/wABCRxIsKDBgwcFKBAgACEgCxYcSlTIUKJAhhgZLrT4MCJBAQNAhmxY8MKFiBozTlw4YKNDhQNi +tmQY8iAHAxwemhwoUmRMkgNNWoBJcybQjwpCxnSZUKbTnggtJLGwE5BPG05rFoSpNOtRq0kHYF36 +9WLYrAqwMgUkNEsSpE9lLjQAN6YNnwuPcv0p92uAAD6dLsxLUCigm4j3KlWQ98iIxGcXL3zgmGdk +kIwrEvwLmLHnvAI4E4QoELEBjQAADEjNEAGCBkxwAtAYNmMBALDpOqH9E+MDLTsGcv6bEePwABZV +4KgAoMBt5wA0LHBAIcUGQM0LFM9+u/p1N1KYz/9OrYFBiRwR1Ag/zp69QwNRSjRPTf/2ggQIIpww +wJ1+aue35bffGwlkZyADCTjQgBkjANLeg8O9B0WBt/ln3wKvKWEAFP0R0B+GDWjIxnwWAjDdA2es +4CCEx1EgGkIG6EAhdACaCKIHMRaYGgE80rfAjQbkQGJ9JjrwABcqssjZffn99R4FJTBAn4c7MhkB +jhTwqOWWBFiJow791ZjgFV8k+eCU9DXgJIxDOMDlltNRgIEMBrT5ppZxzmkAGvLtSB+C6GFhJmcE +aOBhjTV6gByMRCBwJwEMgEjCDwY0+mikr01qQBg43FmBkSlY0eBwDPCowZD/AaCoRCo8gEACDMT/ +GmsFDCDwAAY+XNfqqxWg4CsKtNqK63VVUODAArFCWkECN0TQARkCDVeBoYf6N58Mi8JYhKsAJPAj +AA7YSkMHJtBlwLYIOODtfeE+MG65gDghBgU4lOBtgjcsQQIYwa34F6zU/knfpNkiNIIRDVBAHwIU +NIBBB0E0KNDBDbhmccMPRzzQFGs8cENqtkZAggxbrPcXr6WmRusCt+ZaMIxC8BBBAw1EgAEJHgBB +F0EGxDxzzTfnvPNATRxBwxI1p9BBF01sNly6BfYoLrlrWmTABicg0UENPZiwwdA8Y601116DPdAO +ZVBRQw1pjOGF0y0C0CN9EEfIESAGZJCBAXxb760332Yb9HfgBfUsN5cA6Hx3QROIIMIEdzf+OEeS +Q84Rfw/MDcDXixM0wwQHSAC6RZ+HPrpDpYt+wN0AZL4lAJ0PdAAIgIQQAiAgrI7Q7LXfnvvutNuO +u+4Sta557AIdcIALBMXQwgcHKc/8QM5DX5D0zT9PuvEeIp88CxAQL4H1Bh0AvvjkE2R++AONbxHv +9P2OfAYQsC/QC8QXRL/9gOB/0P7E859DsEc97SFPAhCAAU8kgkAFXsQhDVygRNaHPu8JRAL5swgG +F7fBxQFwIAL0Xgbf17kRajCBErSgCleoQROy8IUwBIQLY0hDjgQEACH5BAlkAIAALAYAAwBPACAA +AAj/AAEJHEiwoMGDBwUoECAAISALFhxKVMhQokCGGBkutPgwIkEBA0CGbFjwwoWIGjNOXDhgo0OF +A2K2ZBjyIAcDHB6aHChSZEySA01agElzJtCPCkLGdJlQptOeCC0ksbATkE8bTmsWhKk061GrSQdg +Xfr1YtisCrAyBSQ0SxKkT2UuNAA3pg2fC49y/Sn3a4AAPp0uzEtQKKCbiPcqVZD3yIjEZxcvfOCY +Z2SQjCsS/AuYsee8AjgThCgQsQGNAAAMSM0QAYIGTHAC0Bg2YwEAsOk6of0T4wMtOwZy/psR4/AA +FlXgqACgwG3nADQscEAhxQZAzQsUz367+nU3UpjP/06tgUGJHBHUCD/Onr1DA1FKNE9N//aCBAgi +nDDAnX5q57flt98bCWRnIAMJONCAGSMA0t6Dw70HRYG3+WffAq8pYQAU/RHQH4YNaMjGfBYCMN0D +Z6zgIITHUSAaQgboQCF0AJoIogcxFpgaATzSt8CNBuRAYn0mOvAAFyqyyNl9+f31HgUlMECfhzsy +GQGOFPCo5ZYEWImjDv3VmOAVXyT54JT0NeAkjEM4wOWW01GAgQwGtPmmlnHOaQAa8u1IH4LoYWEm +ZwRo4GGNNXqAHIxEIHAnAQyASMIPBjT6aKSvTWpAGDjcWYGRKVjR4HAM8KjBkP8BoKhEKjyAQAIM +xP8aawUMIPAABj5c1+qrFaDgKwq02orrdVVQ4MACsUJaQQI3RNABGQINV4Ghh/o3nwyLwliEqwAk +8CMADthKQwcm0GXAtgg44O194T4wbrmAOCEGBTiU4G2CNyxBAhjBrfgXrNT+Sd+k2SI0ghENUEAf +AhQ0gEEHQTQo0MENuGZxww9HPNAUazxwQ2q2RkCCDFus9xevpSob6a25FgyjEDxE0EADEWBAggdA +0EWQATDLTLPNOOs8UBNH0LAEzSl00EUTmw2X7roJikvumhYZsMEJSHRQQw8mbCD0zldnvXXXXw+0 +QxlU1FBDGmN40XSLFtOXcYQcAWJABhkYoHfVeOv7XbZBff9dEM8xz1zzzTnXXdAEIogwQd2MO85R +5I9zZDXWWnPttbV1zzDBARJ8bpHnoIvuEOmhH6D43XnvDQCPD1CgsH8HHQACICGEAAgIqiNkO+66 +8+777bnv3rtFlA/0+qMeFnTAAS4QFEMLH9QOvfTUG/R89ANNX71EqJuOnbW0O88CBMdL8L3256e/ +PkEHtD+Q+hb9Xrzwig+UAQToD/TC8QXZX/8E8r+DCPB4BXTI9rD3PsVJAAIw4IlEHhjBiziEghKU +SPwGCAj65Y8gEgCgRULoQBFK5ID+M6HiVKjA/LHwghDM4AdnSMMadvCFNsyhDp23wx72MCAAIfkE +CQoAgAAsBgACAE8AIQAACP8AAQkcSLCgwYMIBSgQgLChw4EKGT6cSLGggAEXJRa0YKGiRYwYJwoY +STLiQ44GL4IMSfDChY6ARi4k2VClTYcKB+gcMJMlQQ4GOACy4JKgygE2dmoE5NJCkpwjdc5MuTOp +zqUfdyq9etAp0QsQFWg9ijWm2JVjjZ69qvRgTq1Sky4c2DRLkpQl8wowoLbq0akQ9ZY0GCDAUa0L +ATN9CQioYwMj4JSoAAAAg8oJEDw4MiKogbdsFSwEwHlgnTsJKl8G4ODGnCkFCxsWTXumANkDUTb2 +bCDKZAAFCgAXjgBBAyYGkss8WzI4AOSAnNhJ7dy5Zi07BsouLHh7gIcqcFD/Fl65snAHFFJsAKQC +eF7zztUDciMFeHnzAHJEUKPdu3//DfVWwmXklSdcZhGc0NtwBcInXIIGvEHdffY50IAZIwDy34bb +BQhFAgRW59wCxilhwIcMAkBAiiUawMYCKQZ33gNnrKAhh95RgBtCBugA4oqVAQnAAiQ24EGPIJZH +wJLlFXlkDguspmKTDjzAhY04yrYAgoUFSMGSYIZJwJYIRHDkl2KGSaaZPaa5JAMJOHDFF1huqGR5 +DXTJ4xAOuLnkAuhhIIMBfPo5ZqCDolGCm3Dqh0WdsoEZo3MefMcjEQj4yUCRJPxgAKaacuppGDi4 +WUGVKViR4XYMSNqgfZU6/6TCAwgkQNmblmmGgQ/rzVprBSgEi0IFDOjKKyBVUOBAlKtVkMANEXRA +hkDbVaCBBiu+KpwMlh5UHq0OpFaZA5rR0IEJfBlQBLgJEBlnuefy5YQYFOBQQrtx3rAECWBkd2Nh +PzKpWnmddluQikw2UNzCFDSAQQdBZFieEQovjEDDD0cMSHlrPHDDwg9EQIIMW/RX2K+tEsAAsQs8 +sOsGBg805ZKp8RBBAw1EgAEJHgDBF8IrCmEzzjrz7PPGkh5BwxI5p9BBF00Q5B0C4RJZGbwm6Hlw +kASktsEJGWRQQw8mbPDzijQD8DUSYZNtNtIIp1YGFWGnMYYXUvtHQXEIA/+QcYcGAZDa4ABMIIII +EyQnM9epAWKA4YjzJZCSXQMACOQTPGSA0BH0XdnRDgkuOgAzTHCABKYfTHhqpZ+eusyrF+76AZpv +ALSkFFlu+QEgABJCCICAQDvso/PuO/DCEy9u778HP3zoDwhsuUeAHHCACwTF0MIHBOle/fXZby/z +xtZjP5D23EMv5vQVHcACBM9LkP5B7sM/kPz0vx///AjNAED0QqIeIDIAAfsJ5AXPMwgBDQgIBB5k +gc9zYEOM9z8mCVAgEoAADCDykAxuUCBluZ8GOThB8G2sMuK7oAQSOJEVVsSFFKnf/i4oEBZOxIYT +9AgEByJBGvrwhw3xIAkSgUjEImIQh0ZMIg2RqMQmIiQgACH5BAkKAIAALAYAAQBPACIAAAj/AAEJ +HEiwoMGDCBMiFKBAgACFECNKjMjwIcWGAxpCrDhRYEWLHQsKGDASJEELFjwqIDkgo0mRLF8mLNnS +IUSUBkeyJFnwwoWUDFvaaOnyoM6jFGsS5VmQgwEOgCz4JKhzwNCaBH1aCEq0qkaqRK8yXRh26diT +SaReGMhVKVaRK7tmbGiy7c6zAwPY9SogwECtWZLkdEi4sAADBA04VBC3MADEbA0bNhhAb1yljB1W +FqgVkNPPBkbAKVEBAAAGphMgeHBkxFMDAAoUMCxbNoARA+vcSWAaNQAHN+ZMKVh5cWPClTcLxOn5 +tYEopGMXkA4AAYIGTAxolz49tunaALID/3Jih7dt26u17MibXG/h9u0VqsBRurtp7w4opNgASAX3 ++9/Zth8gbkjhHYDT5RCBGuzB5yB8CT1XAmr2fQeAahGcIOF/BPyHQIYGvGEegPg1YAZuD6YI4UEG +QJEAhefZtsB1SrT44n0E5HjfjA3UyMYC/4HnwANnrACIig5SEB+LOrzYoWlPArAAjx4YoEOOWGZJ +wJTXVZnDAr4BEOUCQ3JhJJLJLYBhcgSZJpABFGipJZAARFBlnHJiqeaHVV6ZJwMJOHDFF2c+iON9 +17EJyKGAGDCEA3lG2aEMjkIaKZkUYEApGiX8mYCCWBSanI7ggdeAB5tdyJuYjxGBQJ6qQv/Z6qt/ +0knCDwaEgUOeFQyZghUoJscAqSROZ6dyUOrY3wMIJFBajmFiCcCyzVaAwrUolCYrf1VQ4ACYvlWQ +wA0RdECGQO1VoEGHFXqnaaoJJCBto0Uw68CqqcmrrAH1InDvlLzFK+t4YlCAQwkJqAncEiSAsd6R +yb247qGmNXArsrEONIIRDVjnMQX5TivQxh17jADIYioLyBRrPHCDxw9EQIIMWzQYQLMM5JxzBQws +8AAGPmyA7KoFGSAEDxE00EAEGFzoZmJHJ720qvMK1MQRNCyxdAoddNEEQfClBrADq9HQgQmKLoqQ +ARuckEEGNfRgwmMsto3E2z2EXNAOZVD/8XYaY3gBdnspV4fAfRh0EMSKEE0ggggTaAeRAY5D7rTI +BlU+gUKFV8w0CR4AAVlHM0xwgASmR1T66aZjbtDqqB+AUMpZAhC3CRuMPtEBIAASQgiAgCB7Qrz7 +DrzwCBX/e/DDFwTAAzqa9rbkIQFywAEuEBRDCx8kj7323Bt0ffYDbd+989DXDnn1Ah3AAgTNS3D+ +Qe7DP5D89L8f//xtpn8f683rSAYgYD+BvCCABRlgAQFxwIMosHkNPMjzlLU85FVPAhCAAVsggkEN +eiQhHdxgQu5DPoGYj30SQCAHVYiQFHakfvtjn/Wqx8LkheSBA4mgDHfIQxBmUIQ9DKIQDgXiwiEa +cYg1PKISDxIQACH5BAkPAIAALAEAAgBUACEAAAj/AAEJHEiwoMGDCAsKUAAAQEKCCxs+FLhQgMSJ +BysKwPhQwAAABBwatGBhYMSQGxN6tBiSo8IBHmGmTEjSoEeQAGAWvHChJKCTORXMfPkRpUuTA2Am +TTpUIAcDHABZ4AmxaE6mBHlaSAJ0qVCbSXFidRlzgI2lYwtunXrBZMOQStO6FWux4VeTDHGWbXow +wEK0ZYUGGKg1SxKDIFEKWMzYAEEDiRt+LEAZwFCWihnzLRjArwLAAxQIFdBZoFZAT1NDJhCyIYOG +CRA8ODIC6uqQlSnrBjBiYGQArwE4uDFnysPOi0UzZdy5tMCaqG3/zl0AAAIEDZgYuA2geveGu7UD +/4pM3foDLTsQNkeuef36hNMbyq/ugEKKDeS9z698n7x88ADkEIEa6rlnoIEJQQbcd/9VF1sEJyjY +XWW4VYYAhBLqB6ADDZjRG2cHGoiAewgZAEUCr+222wLYKWEiivKx1hoALDbgIhsLTFjdbg48cMYK +BoXo3gL1vXeQATqgOOOMC9ToAZIyRiljk9g9mcMCweHUEJEPcAEkiEKytsCDzZVIgZRSjnnhk2ei +OeWDT+rgJgEMJODAFV98SVBzCTQXo3w1lnnkEA7MSQCRFGAggwGEGoqoogagUYKbdQqIhZ4D8clA +hTta2IAHzhlkABEIzMlAjST8MGqplKKqahg4uP9ZQY8pWPHhngFQUMKmDM5nXQSgDoaQCg8gkEAF +ib3GgGwY+LABIMQaWwEK1KJQwbIPNPtsFRQ4gGVwFSRwQwQdkEGQRJ0RWoEGvQKYqAyhilpEsQ4k +IJ8DstHQgQmOGTAvAvU2aWe++zrmhBgU4FBCAmMOtwQJYKQ33lsAdEYqiuxS3BB2qcZr0AhGNHDd +yBQ0gEEHQdwKssgjI1DyySkPNMUaLcsWAQkybCEQAAnYi1MA0aLIwNAMXLtAts56LKoQPETQQAMR +YECCB0A49hjTTkMtNdVWD9TEETQsAXUKHXTRxLmJhdTZvwE3TLAJgj5kwAYnZJBBDT2YsEHXj9H/ +jYTdee990A5lUGF3GmN4cW7PMlbcWcgUXCefySiTiNEEIogwwXYTGYC55nwf9PkEB8Fm79VNPx31 +1FUfJdAMExwgQewYwS477QnZPvsBiPksqt8d4K136BgdAAIgIYQACAi8J2Q88sozj9DzyS/f/Lly +G2A3564DcsABLhAUQwsfTA+++OQb9H34A41fvuujdy/QASxAcL0E7x9Ev/0D4a9//ffLn0t0hzvX +ZQAC/BPIC65nkAMmEBALPIgDrxfBo1Avegx0iQQgAAOTTGSDHaRIQkDoQQuer33p654EMvhBFiJk +hfKbHwD7J0ALds+F04uhQCY4kArq8IdA/CAHFEsYxCIakSAwPKISl4jDJTqxIAEBACH5BAUKAIAA +LAYAAQBPACEAAAj/AAEJHEiQIAAABRMqXMiwocOHBAUoEAAIAAGEEBNKPAhxI8aGEgVwfChgQEmL +HzMOPHlRoQULKxWgpMiwpMiWDkuaRPnwpcYBKE0WvHABJqCNFwdMXKhzZs4BQIPSLMjBAAdAFohG +BJoU6lRARC0kQQoAqtKvApt2RRtRAdeuQl0myXph5cGkOuNGvFtW5MGlMfnmZQsogICbFw8rDhBg +YNgsSRJaTKxYsQGCBiYfBFqgM4CviD9XZss4gObSqBkLDAuoquvMBC4eZHAwAYIHR0ZYhX3Rc+ff +AEYM1AyANgAHN+ZMKcj4dGrUA3223k3cdwEACBA0YGKAN4Dr3w8C/+deMfb3655va9kxEPXB588Z +Vj9I/7oDCik2aPZM//z1/Pv1F14OEajRHnypPQDdQpkVF15/19kWwQkN+ocScAhMWCF49R3XgBnC +PUefaQdl18CCCRkARQK0AQfcAtopoSKL9MUmGwAwNiAjGwtYCJwDD5yxQmHuuejZfSmUxqAOLN54 +4wI5emCADjZWaSOU2kmZwwLGoXTQAkByMSRqLQoYYYYnqKaQARRYaeUCEkrZpptXxjklnQQwkIAD +V3wxJmM0cigejjGqmeIQDuBJAJgUYCCDAYgqyqijBqCBp54EYvEnkww4SR+WDXhgaEEGEIHApTmS +8EOpp9LJQKqrhv+BA50VAJmCFSEGMCeecGYoamMLqfAAAglUMBltDNyGgQ8bACIssRWgIC0KFST7 +wLLNVkGBA1waV0ECN0TQARkCMRYpr/c5OiqpRQzrQAL0OXAbDR2YcJkB7SLwLpR7zlvvZU6IQQEO +JSQAJ3JLkAAGe0SayqIGTr6qnarrFjSCEQ1kpzEFDWDQQRDCCXRxxhojwLHHIA80xRol3xYBCTJs +ceCzLDJgMwPVLnAtsxWTKgQPETTQQAQYkOABEJdh9nPQQxd9dNIDNXEEDUsMnUIHXTRBUGn57nuw +vyYo6ZABG5yQQQY19GDCBlBjVjYSZ6vNtkI7lEHF2WmM4cXWqGHkTEHJJ3+cGkQTiCDCBN2NXfjh +bSu0+ARjLy000UYjrZJAM0xwgASaP5T55p0z9DnnB4z9dtxrN/7QASAAEkIIgIBQOkOsuw677AvV +/nrsszdkwOOqr36ACwTF0MIHuQ9f/PEJHaD8QMYj39DooV9+AAsQ9C6B9Apdn/1A23ePvfbc5976 +7rhfngEE3wv0Qu8Jrd8+IO8rJH/v9dP+vEDRXw6IBBCAwUocAkABpoUhBRxgQ7xHPv8JRALwewgE +MzLBjNxvIPnzXwRXp5INEjCACnSgCEdIQQ+S8IQoJIgJU8hCiAQEACH5BAkeAIAALAAAAABVACIA +AAhYAAEJHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuX +MGPKnEmzps2bOHPq3Mmzp8+fQIMKHUq0qNGjSJMqXYoyIAAh+QQJCgCAACwGAAEATwAhAAAI/wAB +CRxIkCAAAAUTKlzIsKHDhwQFKBAACAABhBATSqQIcSPHhhszAhIwgKRFjCIHklyp0IIFlRMHTARZ +smTHASVPPnSpESdJmwQvXHg5UkFNmR8L/lzq8OfBi0ALcjDAAZAFoRFxDrChNalQC0kk4uSKc6ZS +rWSjajRqEarPhWCvXoCp9ahagQIOAvCp1yxeo127KgwgIC+Bi4UTBwgw8GuWJBoTS05sgKABAAUy +78WsOenkzwkXB2gLQLTpxQK/AprK2sAIOCUqHGRwMAGCB0dGUL2cuTdn3yMG1rmTYPZBBzfmTCm4 +mPTp0wN5rt5tIEpsztgRIGjAxID33wcLhP/X3B2QEzvFNWu+rWXHQNMHnz9vqAKHbPF6MQNwQCHF +BkAqgJefb/4B4oYU+g0IQA4RqPGefKc9YBpD1ZVAG356iWdbBCdUCN5FviHAoQFvpJeffg40YEZw +z+k12kHaNTChQgZAkcCF6mm2wHZK1HijXodddNCODfTIxgLg+ebAA2esAMhpSWrGXwqiLWSADjcK +edKQRHpwZZBgBrlAlwbksABtT+m1wJJcOGnahSdqKOIJqA10kEAGUBBmmEgCEIGXeu4p5oZe6iAo +AQwk4MAVX7i52I8YhgcAkUrUCSRCBgzhwKGXypDppoeuSQEGnqJxaKIMYuEolgxoqeWY23n/gBoA +CRR3kgFEIJDAnrTaehGuCJzaJwk/GBAGDoJWsGQKVrAYQKChEmppWxcB+ICush2GJrUIqXBtAhWg +IC4KsqX5XxUUOHAmmhUkcEMEHZAh0GKaHkqAqKTOWmuQmBZxrQO21rbrYf3+m8CYxdWapnliUIBD +CQcresMSJIDh3pMB5HqjBq4yQCSx0yo80AhGNKDdyRQIjBLJJp+MQMrcCjTFGi7fFgEJMmzxoLe6 +MuCzzxV4/AAGPmxgqa0FGSAEDxE00EAEGNB6p2VLN/10r/wO1MQRNCzxdAoddNEEQaL5iwDACDtw +Gw0dmFClQCglvcEJGWRQQw8mAFBZQgbM/41E3T2oXNAOZVBRdxpjeEG2aSVT4DIFDWDQQRDQPTSB +CCJM4J1DBlyeudRxE+T5BJxX7TTUJHgAxN4izTDBARK8/pDrsL8eOkG0x34A534DbsIGrGd0AAiA +hBACICDsztDwxR+f/ELMG4+88g11jrnmKQl0wAEuEBRDCx9Az7334Ce0ffcDfR9+Q7nLnv0BLEBA +vQTrKwS//APRb3/889cPPfHSe15KMgAB/AnkBdRLCAENCAgEKmSB1HPg8saXvvKlRAIQgIFKHIJB +DeKFIR3cYEPu17/sCUQCCXwICjOywoxAcCASfJ8MhZe9EH7QhDjMoUhaqMMe+nB5PwyiDgIDAgAh ++QQJCgCAACwBAAIAVAAgAAAI/wABCRxIkKAABQIKKlyo8GBChgUdPoQIyCFFigIGZJx4keFGjQst +WBjocABCjBpBdow4IKVKiiIbtsz4UuCFCyMrKnB5ciHNnytJthxgYyjHgRwMcABk4abBoUVnErxp +IcnBllFNHq0I1WjQq0a9hkzS9ALJnWGlRgTAdiZbAD0Fgp0plmIAAXjz6hUQIMBAqlmSNNy71wBB +AwAKKAYwIPFijoT3XuxLubLlvjZxAkrK2cAIOCUqsGXANgGCB0dGKEWsuLVj1yMG1rmToAIDBhUW +OLgxZ4rdy8AxC4y5ebWBKKEdK0eAoAETA9Bfsy0wffFzQE7sJLitoTsDB6e17P9gGLy8X4gqcIim +/jYxAAcUUmwApEJ6e9fyAbmRYlsDgf8aMFBCDhGoQZ55fT1QGUTHlUAae29RZ1oEJzQoHQHSIUCh +AW/856GHDCTgQANmxFbQZW8F8BZzDSy4kAFQbCedaws0pwSMMrLl4Vs1NnAjGwu0h+FbDjxwxgoK +WTbjYvClQBlDBuiw3ZAAULlAjx5E+eGWBFzZXJY5cPkfA7o9wAWSJ1L2YHvuTXiCcAoZQIGYHi4w +YZZz0tnlnVqKGaIDV3yBJiBsAUKZjBBOB0CPSsBZkAFDOKCnbhRgIAOkenYJn6UGoFGCnwkQiAWS +b2HYl5QMUGkllo4eRgQCdJL/2RwJPxjwaqw90mpAGDiIWUGRKVgxAgAJJKAjAAHkSaedGnrQKkEq +PIBAbVUSQBoDp2Hgw3zRTlsBCuCiYFu22wJSBQUOLHAbWxUkcEMEHZAhkI7/IRvppJvK8Oy8bEnr +gLFsgfcADR2YYJgBRfibwJUinkawwdiJQQEOJSws4g1LkADGeMQmsGMAr27nX6myNkDrvtVi2ABz +LFPQAAYdBBHbW0aszDICLsMsM6FsrXHzaRGQIMMWA5VmbF/dbnfbbbYt8IC2GzxbZbXG8hBBAw1E +gAEJHgBhWMoACGE11lpz7TWhOx5BwxJZp9BBF00QRGyhlCWMwL8MC/zwkwWV/0qAsRuckEEGNfRg +wgZfY/gf4CcgMbjhiKNNNQBlUDF4GmN4oRAAAllWMwU35xyzZZsbazoAE4ggwgTQFU2vsYAYkPrq +hvE7OSCzTxCUQAaIfXXWW3ddO0RzAzzDBAdIgHzfpxt7fPLLF9086tAfsHvsgTueAeTDU8Q55weA +AEgIIQACgvXSzw1A+OOXf376AItPvvno7y676qxfL9ABB7hAUAwt+IDcCAUI/vlvIAAU4LwIZcD/ +BfB6z1Ne/XZ3ABZAoH4SUOBCKnjBgWRwgxbEoAZXwr75ve96GYBABwXyggkWJIUrBEQLFwLD+s0w +KA1E4AOvJwEIwIAkF+nhDzzlAhEhApGCIfTgCHcnARcG0YkMaaL+BlLDgdxQf1CkSBY3OEWCGJGI +XQyjGPUnxTGa8Ywd2SIa10iRgAAAIfkECQoAgAAsBQACAFAAIAAACP8AAQkcSFCAAgEEEypcKNAg +QoYJHT6ECMghRYUCBmSceJHhRo0LLVgY6HDAQYofOaIcoJGlypAjI7p0mfDChZgGW5p82XAmyIsZ +Wdpg+TMhBwMcAFmwWZDoUJoDbVpIknPA050YnRLlSVIBUZ1FE05deqHr16BhGwJY63ItgJNmt25l +GECA3bt4BQQIEPVmliQY8+Y1QNAAgAKIAQw4nJij4LwL90qeTHmvQKmAjmo2MAJOiQprGaxNgODB +kRFIDSNezZj1iIF17iSowIBBhQUObsyZorCyb8kDRQrcbCDKZ8bIESBowMSA89ZrC0RP3ByQEzsJ +amvYzsBBaS07CP7/Hs8XogocoKW7PQzAAYUUGwCpgL6eNXxAbqTQ1kCgvwYGJeQQgRrikbfXA5NB +VFwJoqnnlnSkRXDCgtARAB0CEhrwRn8ccshAAg40YMZrgFTmVgBuKddAggsZAEV20LG2wHJKuAjj +Why6NWMDNbKxwHoWuuXAA2esIBBlMSbmXgrAtahDdkECEOUCO3pggA4dZkkAlctZmYOW/TGA2wNc +GFmiZA2ux16EJ1jWIgVgcrhAhFbCGeeWdF4Z54cOXPGFmZLB6GB0AOyohJsKGTCEA3fiRgEGMijK +aJyOQmoAGnsmICAWgAbwJANRTlklogkZQAQCe+5Iwg+mogqmmMut/2pAGDiAWcGQKVhB4l52Ukon +qQmp8AACs0lJgGgMlIaBD/EJS2wFKESLAm3KMgtIFRQ4sEBta1WQwA0RdEDGQHst2qh7kAJbWBHD +OpCAkKXR0IEJhBnALgLuUglivPMS5oQYFOBQQgJz5rYECWCEd2QAp0IZKqwNrKouQSMY0YByGFPQ +AAYdBEEiIBVfjDECGnPs8UBTrPHADRg/EAEJMmxRoLPZ1VYbbQs8sOwGExcmBA8RNNBABBiQ4AEQ +hPkMtNBEG410Qk0cQcMSQ6fQQRdNJCTZvfkWzK8JTVJkwAYnZJBBDT2YsEHSpZKNhNlpr73QDmVQ +YXYaY3ih9WQWU/8wcskdU9YRIBOIIMIEzl1kQOGHs80Q4xMoJKWxCRjwc9BDF32044PPMMEBEnze +keegiw4R6aEfkJBb/b079glvZxA35x0dAAIgIYQACAiqQ2Q77rrzztDvue/e+0AAvKs8AIAsbjji +gyd0wAEuEBRDCx8MT7312Cs0ffUDXZ898sszDwjqpkcv0AEsQHC8BOMvxL77A8Evf/vvx488IOYD +Qnzwx4teBiBAP4G8IIAJGWABAXHAhSjweA2kyPe4p7/BSQACMCDJRS6YwYZAhIMavMj88qc+gkgA +gRtEIUNOWMIHDiSCJfRfDFU4vBgCAoQetKEOd1hCFvLwh0AUYRAFh8jDgAAAIfkEBQoAgAAsBgAC +AE8AIAAACP8AAQkcOFCAAgEEEypcyLChw4cQCw4QMDFiQ4MCEDK0YEFgxokaHWIMKXLARJMkG3JM +SJEiSIUXLnQ0ePIlQ4wDDj50OcCGyYoLORjgAMhCTIk1USaMaYFmUqAsFdTUeVPqT54pCVpIYvSC +x58+f0ItaLWn2LGAnIbNmRVQgIxw42YMEIAg0yxJCDpF+fOgAb0AAv8MDCAn1bRlkx4WSLex48d0 +BzIFJLTy3pMKDgo4MqIygAKgC38ODZesWIqZMyaEzLqx1o6UhxrIqEBw4IwIEDRgMnQ06AK+Q+/+ +64S21bgPtOwY2Lp53YYqcFQYTV3DAgcUUmwA5Dsw8O7Zt7v/kTIdgIDAGhiUyBFBDXPndB84bmgg +SonPhL0DWJAAQYQTBnSX328A+AfgGwlQFxoDCTjQgBkjuPUYYQEQllsD8y1kABQJfkcYcAssoJsS +G3ZHQHciNkAiG/gNuJ8DD5yxgoSNdUcgdim4pqEOHYZGYIi6eWAAj4QRYCRhQDYgZA4tfvjiA1zM +6BgDTerX33+RaUhBCVQGdqKX/PknJAVGlmkmAWFGIKQONobW4BVfSElXAlR6qF+KSmSpkAFDOHCm +mddRgIEMfP4JKHaDGoDGfV4SxiB7WMjJIwNfAlBpkh7omZABRCBgKAEMpEjCD5x6amiouo1qQBg4 +GFoBjClY/xEhXWR+iuaVmT63kAoPIEAnA8AyUAEDCDyAgQ/b8eprBSg0i8KwxR67XRUUOBAssBUk +cEMEHZDBWAB92hrooJpuWkSvACQQIgAOFEtDByb8ZcC5CDigLn/tPvBuvIA4IQYFOJSgboM3LEEC +GMtJ2CmdR3qJagOjlpvQCEY0QIGFFDSAQQdBRCgQxQ3kJnLGG3c80BRrPHBDYMVGQIIMW7yn7K/B +DruAschKvKkQPETQQAMRYECCB0D8RZABPPsMtNBEGz1QE0fQsATQKXTQRRMENUavvSE26C68Ojpk +wAYnINFBDT2YsIHTR5NtNtpqsz3QDmVQUUMNaYzhRdaOVfXc68gac/xYRAZkkIEBiD9U+OGJO2R4 +4/Ql/XPQQxdtEUETiCDCBBFlvjlEnnMO0dhlG5722pcPNMMEB0jA+kOrt/56Q7G7foBFBoQut0UH +gABICCEAAsLtDPX+e/DDF+878MITDzvrtqcu0AEHuEBQDC18sBD11g+EvfYJcX999g8Zz3zyqR/A +AgTOSwC+QuqzP5D726/f/vvFVz8+/hBlAIH8AnmB8xLiPwACQoALKaDzEOiQ+N1PeoCQAARgUBCH +SJCCHmnIBSv4EAUOhIHSk8AAHyJCi5QwdRvMIAQBMcLyXa6FETnhCmdIwxfW8IY4BERAAAA7 diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/003-bgopt.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/003-bgopt.testie new file mode 100644 index 0000000..ae56f55 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/003-bgopt.testie @@ -0,0 +1,3140 @@ +%script +for i in 0 1 2 3; do + gifsicle -O$i x.gif > y.gif + gifdiff x.gif y.gif +done + +%file -e x.gif +R0lGODlhLAHIAPcAAAAAAAsBAQQJAQsKAQEBCwsDCwoKCgMKDBMBABsCABUIBgwSAxcWCwIBEwsB +EwYGGBIGFRMTExsbGxUbGQ4QGiMAACIKACoHAjMMACoSATsdADYYACEiGzYiABgZIxQPLSEcKCMj +IysrKzMzMzs7OxwhHkYZAEspAFMrAFk4Ak44CHA5AFxBCmVDBHZVDHtdKUNDQ0tLS1RUVFpaWmNj +Y2trbGprZXJycnx8fHRxeGtoc4lUCZNtGqhxF45aJYhoI5RwLK19O+l+MJZ5RrWMMJyHHsqcBtyb +AtmZCcqYGNmZF+WVAeyUAuSdAuydA+WcDO2cC+eXDPKdA/OdC/eYBuScEuyeE+WcHeqcG+mWFfSc +EvecGO+PF9WiAtyjAt2kDN2qB9ykG9imF9OsFeSkAuyjAuOqAeuqAuSkDeyjC+qrC/GiBPulBPur +A/GiCvOsC/inCOq1BPe0AuWkFOyiE+OjG+ujG+WsHOirFvSiE/OrFPSjGvmnGeqzGvW0FMqcJtyd +IdWbKMubOtibN8SNMOiaJvWbJvKZMdimJ9enOM2rLuamJ/WoKOi2J/m1JOSrO+ilN/ekNfy3Neaz +NvvHLLuGSbGTSryYaNaZRdeeWMqSTd6sTNqlSNqzR8+jVNipW9u0V8WkSOOoSOq2SP21ReSmUui3 +W/SvUdqyZei6Y/O0bP+dUvrFRfvIVP7WVvPNR+fHav3IZ//YZ/3Mef/ZdufLef/pcf/qX4ODg4yM +jJOTk5ycnJ+WkY6Nk6SkpK2trb+3rrS0tLu7u724sKiYgdm1mNS/sbjDuurIhPvEhv7Yh/rMk/3a +lP/niv/mmf/3l//3itDJtvjOrP7oqf/1p//quP/3t8S/xrzFyb7J3cTExM3NzcjDxcrM1c3U3NPT +09vb29fX1tDMzP/uxv/8w//7y//3yP/s2f/90//23f/93P3z0+rv3OXl5ezs7Orq4P/05P/85P79 +7Pf66//t9+v8/PT09Pz19Pb78/799P31/vX8/v7+/vX1/fTy6uHY3SH+A0c0NAAh+QQIKAAAACH/ +C05FVFNDQVBFMi4wAwEAAAAsAAAAACwBqAAACP8AAQgcSLCgwYMIEypcyLChw4cQI0qcSLGixYsY +M2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+rcybOnz59AgwodSrSo0aNIkypd +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 +TDMjRzUpWDgnQy02SDg2Vzo2VyksZDskZD0saj0mYzwyczkrXB0VVkQcaEUbdE4aWkooSEQ6WEY4 +V00xbEEsaEQnckIsdkcnaEY2d0k1cFUweGUwfWAaKx1KLh9QHSxILCBMJypFNilHNjVHLCVTMiNT +NyhaKDdYMjZROy1jOjRmMTltHxVDSDdORTRrSjptRTZnTDxySDp2Uj53RjRnbTpJOENXNUhuS0pG +Uk9KVUxFVlNKXl5eWVdVT0pXZlpFaVlHdlhGZl1Xak9JXmBcZWNadGdUTEJ3VER0W0t7Vkd5UVBt +ZVR0aGdmdnRvXGNtPkE6mTMd0ToVjFIcsXMah0oqhUw0iVM4mFc4kFUqqFw3s141rlYrkG4vq2I4 +s2I0vGM0uGU6q3MqlmUcz1EvyWkt1G4t2HUpxGU1y2Y0y2ozxmk41Gs02XM4ynQs6Hc363sx7VMu +ildFjXJKsWtJiHZvpVxbz3RH9WxS/Hpo+XVs5lxQkjxBfoJ2sooysJg4yJUy6oIo06k7m4VGnIZJ +mYZKmIlUsJJKh4V5lop4p5R5tJR3uqNU0q5Oz5d1/Yd585Bw96V62qJQ889Q991hOlGIP2SVV0uE +T1COZ1iGaVqMUmiSc2OLcm2SV1ukV2ysa3KzcHvCQz6ChHiPm3mcdYiwcoKadonJmpSFkY6OpZiI +rZiOp6WWraSRkpOura2tp6emsq+xoZ2r+Y6E0aaI+62K2ame5c6riJXLlajOq7TQoLPjucnltsXY +z8/Sy9bq2ef06/b8+vv29fz8/Pz89vj27+7v89zVwrjECP8A3a0b2G6gwYMI1xUrJkycMGED3ZGL +uGzdMHcY3flbR+5ixmXkwgWDOBEiwonrxA1UiVKcNm2/voH7Ji6cNlbAxOnUCQ5dvZ9AgwodWi8e +uF/aCtGiVogaNVZOf/2i9quQNnBWX2rVRg1cV2rbyolNJ3arVmpm06p9iVatV7PbwJVLR5fuXLro +0MWLh+7fv7p1twnWNrdvvH9686JLtxjwtnSPH9utC+4xOHAvmyLFejkz11+XO7O6XCj0ZZmXxV1e +J+lTa1CbJGHaFPvSp9u3OXHKpMiIE0WaNA0jJoxSLWLDbC0itqw5sVrBbSmbrmsKIyNGIk2qxH2S +pEeRJE3/Ci/euyRJkR45eeDgQQoePZpA6fEgCJRZUJ7wYJVO3Llz4/x3jjnmCEjggQT+Z84vH4hw +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 +AC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElF +QzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2 +MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPt +zAAEEwsAA1yeAAAAAVhZWiD/AAAAAABMCVYAUAAAAFcf521lYXMAAAAAAAAAAQAAAAAAAAAAAAAA +AAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAt +ADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcA +vADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFg +AWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwC/xQCHQImAi8COAJB +AksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2AsECywLVAuAC6wL1AwADCwMWAyEDLQM4A0MDTwNaA2YD +cgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQTBCAELQQ7BEgEVQRjBHEEfgSMBJoEqAS2BMQE0wTh +BPAE/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXFBdUF5QX2BgYGFgYnBjcGSAZZBmoGewaMBp0G +rwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfSB+UH+AgLCB8IMghGCFoIbgiCCJYIqgi+ +CNII5wj7CRAJJQk6CU8JZP8JeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsi +CzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N ++A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RET +ETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsU +rRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiRf/rhfSF/cYGxhAGGUY +ihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzM +HPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUh +oSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3 +JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDks +biyiLNctDC1BLXYtqy3h/y4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioy +YzKbMtQzDTNGM38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjI +OQU5Qjl/Obw5+To2OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/ +4kAjQGRApkDnQSlBakGsQe5CMEJyQrVC90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1 +R3tHwEgFSEtIkUjXSR1JY0mpSfBKN0p9SsRLDEtTS5pL4kwqTHJMuk0CTf9KTZNN3E4lTm5Ot08A +T0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RX +klfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBX +YKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp +8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4 +dBR0cHTMdSh1hXXhdj7/dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4B +fmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6J +M4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSK +lPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg +2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamq/xyqj6sCq3Wr6axcrNCt +RK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4s660JbSctRO1irYBtnm28Ldot+C4WbjRuUq5wro7 +urW7LrunvCG8m70VvY++Cr6Evv+/er/1wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dBx7/I +Pci8yTrJuco4yrfLNsu2zDXMtc01zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV +1tjXXNfg2GTY6Nls2fHadtr724DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5Pzl +hOYN5pbnH+ep6DLovFTpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0 +wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t//8ALAAAAAAsAcgAhxwdHBka +Fx0WCywcGB4gHhciHSIiHjcmGxwdIxwZMyMcKR4hIRwkKxkpNCIjIigpKSYrOTg5OTAxMkccGVQe +G0oiHFUiHU41HkslI1gnJFE4M2krJ3QsJnMsKXksKWk0KnQzK203NWEiHjtBPE1FOm1MOHNjPS00 +RDM6Szc5RzU1TjEsWkc4S18ybThFTTJZYUdGR1JQU21UTnppR3JoVldaaFVUcGhUcVpmcGdnaWdn +dnh4eXVublJoXIlzVYt2bZBiYoFMPnCJeFOLdpaHXI6EWJmHZZeJeI2Id6iSdbKldsO4fnMvkHwY +ungYrngorGIomFdMmVtVmFdOjWZZmHhXl3FUi1RykHh4h3VpmHl1mG1qjVVKplZPqWxUq2lmqHl2 +p3d1t3Burlp0rHwOyn0Ux30Q0Xogw3Z1xnBUyDpjhYMbupkauIQbqqIbu4gmt5c5uo8tsaQvuIQv +mIdQkoV6mopyj5hHt6VKvIp3qal1poMbxIMXyYQUz4slx5Qry5o4yJs105s61Jk31ZUs0qQ006cf +0ptGxaVJxatVx7NYzrtn1bNrzJBv0sd32sNs2Mp548535MRd18B/vVaPknCPkU+umGupmFeJqFKM +rnCKq064pXGwqjzFpT/EnkrEqWbKtmzJtUnDnXWN5nWL0kO2n4eIiJeYmJGNkKeYh66Wj5OlmLim +ibeol6ynlpmXp5GMq6aZt6mTr5KpsKapqKqotre6ubOvr8i7iMaqmMq3mNa7ntm7ndS2m8eokMm4 +p9W7o9m8ota7qtm7q9W2pta8s8qyrsudmL3CvK3DsIvEtcbFutTLtdbKuNLHsM7Bk7KTypaxxLeq +yLq2yLG0zZ+X4suH29OW3cyQ2sO2y8y83Ma41cyx2NSM49SH59ST49Wd5Nia58yN5Nah5dmh5tij +6c694sm46NS65cuq5uG25LzFxazH2LbJ26rFzrPN47vM47XS5bbU6bnU6rnT5q7O453DycXHxsjE +2szS09PO0czD4tLD5NTE6MbU49vl4gj/ALFduwYNWh49BcWJu4YNGqM7TppI3OPnD5w7bTK2gaNI +zpw1b+IAAgQHUSJJkbp9+zbwGrhx5MqlS3eu5rl55+DZPFeNlCZOm0ptAgWK0yV79erJczfLkqVO +UDuVKkU0FNGrWKMhrffuXbx58+LFUypPHj168uKhHSuvHjtNYLRsgSXNHD98/PLq5WdOm99tfvfm +tctPG7dYWbRo0kSNndizZsWWTfuYXjx57WC9WlWJc+dQoEFXBSWqdCinQyolW3fvHrLXy+4tW7Yu +WaVKdGnW27lzJrlw4cqR+wbu2rdu1h4lwgMHzp8/a9a0iYPnzpw3bZzIuRMHzpsyEs/4/1EESeG3 +dAX1NCoITSA2bNbey4eWbr44R4nmPIlIhiKgN3jgMQcT03GkCBwZxfHGH4DIAQceiSziiDfdDPQS +OeOMcw1NvOFUEzV5aLLJJ0JxUtQsSHnFziyUbLJJVFJZgtWMRXESTTTscKXWPGmRdZZlZ4kllluz +wKVFLNygg5dg5uxjTl9+EaaNXoQVpg00WmhhiibR1CMkZJIJKRmP8kQzyzrr0ILKKqtcokxoVl1l +ySWXWDKEm8kkg4xre/ZJCzLr3HaJFvSdk1RN8hhak2/k/Fbce8ktd0dzccTRxhtPvGGSHJVmJAce +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 +////////////////////////A+gAAAAA/////////////////////////////wPoAAAAAP////// +//////////////////////8D6AAAAAD/////////////////////////////A+gAADhCSU0EAAAA +AAAAAgAAOEJJTQQCAAAAAAAGAAAAAAAAOEJJTQQwAAAAAAAD/wEBAQA4QklNBC0AAAAAAAYAAQAA +AAg4QklNBAgAAAAAABAAAAABAAACQAAAAkAAAAAAOEJJTQQeAAAAAAAEAAAAADhCSU0EGgAAAAAD +TwAAAAYAAAAAAAAAAAAABLAAAAeAAAAADQBUAHIAaQBvADEAOQAyADAAeAAxADIAMAAwAAAAAQAA +AAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAeAAAAEsAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA +AAAAAAAAAAAAAAAAABAAAAABAAAAAAAAbnVsbAAAAAIAAAAGYm91bmRzT2JqYwAAAAEAAAAAAABS +Y3QxAAAABP8AAAAAVG9wIGxvbmcAAAAAAAAAAExlZnRsb25nAAAAAAAAAABCdG9tbG9uZwAABLAA +AAAAUmdodGxvbmcAAAeAAAAABnNsaWNlc1ZsTHMAAAABT2JqYwAAAAEAAAAAAAVzbGljZQAAABIA +AAAHc2xpY2VJRGxvbmcAAAAAAAAAB2dyb3VwSURsb25nAAAAAAAAAAZvcmlnaW5lbnVtAAAADEVT +bGljZU9yaWdpbgAAAA1hdXRvR2VuZXJhdGVkAAAAAFR5cGVlbnVtAAAACkVTbGljZVR5cGUAAAAA +SW1nIAAAAAZib3VuZHNPYmpjAAAAAQAAAAAAAFJjdDEAAAD/BAAAAABUb3AgbG9uZwAAAAAAAAAA +TGVmdGxvbmcAAAAAAAAAAEJ0b21sb25nAAAEsAAAAABSZ2h0bG9uZwAAB4AAAAADdXJsVEVYVAAA +AAEAAAAAAABudWxsVEVYVAAAAAEAAAAAAABNc2dlVEVYVAAAAAEAAAAAAAZhbHRUYWdURVhUAAAA +AQAAAAAADmNlbGxUZXh0SXNIVE1MYm9vbAEAAAAIY2VsbFRleHRURVhUAAAAAQAAAAAACWhvcnpB +bGlnbmVudW0AAAAPRVNsaWNlSG9yekFsaWduAAAAB2RlZmF1bHQAAAAJdmVydEFsaWduZW51bQAA +AA9FU2xp/2NlVmVydEFsaWduAAAAB2RlZmF1bHQAAAALYmdDb2xvclR5cGVlbnVtAAAAEUVTbGlj +ZUJHQ29sb3JUeXBlAAAAAE5vbmUAAAAJdG9wT3V0c2V0bG9uZwAAAAAAAAAKbGVmdE91dHNldGxv +bmcAAAAAAAAADGJvdHRvbU91dHNldGxvbmcAAAAAAAAAC3JpZ2h0T3V0c2V0bG9uZwAAAAAAOEJJ +TQQoAAAAAAAMAAAAAT/wAAAAAAAAOEJJTQQUAAAAAAAEAAAACDhCSU0EDAAAAAAbTgAAAAEAAACg +AAAAZAAAAeAAALuAAAAbMgAYAAH/2P/gABBKRklGAAECAP8ASABIAAD/7QAMQWRvYmVfQ00AAf/u +AA5BZG9iZQBkgAAAAAH/2wCEAAwICAgJCAwJCQwRCwoLERUPDAwPFRgTExUTExgRDAwMDAwMEQwM +DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBDQsLDQ4NEA4OEBQODg4UFA4ODg4UEQwMDAwMEREM +DAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAGQAoAMBIgACEQEDEQH/3QAE +AAr/xAE/AAABBQEBAQEBAQAAAAAAAAADAAECBAUGBwgJCgsBAAEFAQEBAQEBAAAAAAAAAAEAAgME +BQYHCAn/CgsQAAEEAQMCBAIFBwYIBQMMMwEAAhEDBCESMQVBUWETInGBMgYUkaGxQiMkFVLBYjM0 +coLRQwclklPw4fFjczUWorKDJkSTVGRFwqN0NhfSVeJl8rOEw9N14/NGJ5SkhbSVxNTk9KW1xdXl +9VZmdoaWprbG1ub2N0dXZ3eHl6e3x9fn9xEAAgIBAgQEAwQFBgcHBgU1AQACEQMhMRIEQVFhcSIT +BTKBkRShsUIjwVLR8DMkYuFygpJDUxVjczTxJQYWorKDByY1wtJEk1SjF2RFVTZ0ZeLys4TD03Xj +80aUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm9ic3R1dn/3eHl6e3x//aAAwDAQACEQMRAD8A8qUm +MLvIeKs9L6XndXza8DAr9XJtnYyQ0GBP07C1jf7S1b/qd9a8Z3pv6PmEt0/R1OtH+fR6jElOMGMb +wPmVKAeQtrG+pX1uyXba+kZTT/wrPSH+dkGpq1af8WfWKWMv67mYXQ8dxhzsm5rn/wDW2sd6D3fy +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 +AAAAAAAAAAAAAABkZXNjAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4gSUVD +NjE5NjYtMi4xAAAAAAAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYx +OTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdmlldwAAAAAAE6T+ABRfLgAQzxQAA+3M +AAQTCwADXJ4AAAABWFlaIP8AAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAA +AAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0A +MgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8 +AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWAB +ZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAL/FAIdAiYCLwI4AkEC +SwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNy +A34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE +8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQav +BsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I +0gjnCPsJEAklCToJTwlk/wl5CY8JpAm6Cc8J5Qn7ChEKJwo9ClQKagqBCpgKrgrFCtwK8wsLCyIL +OQtRC2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzADNkM8w0NDSYNQA1aDXQNjg2pDcMN3g34 +DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+WD7MPzw/sEAkQJhBDEGEQfhCbELkQ1xD1ERMR +MRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLDEuMTAxMjE0MTYxODE6QTxRPlFAYUJxRJFGoUixSt +FM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJFmwWjxayFtYW+hcdF0EXZReJF/+uF9IX9xgbGEAYZRiK +GK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc +9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGh +Ic4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm +6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxu +LKIs1y0MLUEtdi2rLeH/LhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJj +Mpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5 +BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/i +QCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVH +e0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJN/0pNk03cTiVObk63TwBP +SU+TT91QJ1BxULtRBlFQUZtR5lIxUnxSx1MTU19TqlP2VEJUj1TbVShVdVXCVg9WXFapVvdXRFeS +V+BYL1h9WMtZGllpWbhaB1pWWqZa9VtFW5Vb5Vw1XIZc1l0nXXhdyV4aXmxevV8PX2Ffs2AFYFdg +qmD8YU9homH1YklinGLwY0Njl2PrZEBklGTpZT1lkmXnZj1mkmboZz1nk2fpaD9olmjsaUNpmmnx +akhqn2r3a09rp2v/bFdsr20IbWBtuW4SbmtuxG8eb3hv0XArcIZw4HE6cZVx8HJLcqZzAXNdc7h0 +FHRwdMx1KHWFdeF2Pv92m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+ +Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokz +iZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU +9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDY +oUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqar/HKqPqwKrdavprFys0K1E +rbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6 +tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9 +yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW +2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE +5g3mlucf56noMui8VOlG6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC +9VD13vZt9vv3ivgZ+Kj5OPnH+lf65/t3/Af8mP0p/br+S/7c/23//wAsAAAAACwBqgCHBQMCCQYE +DAoFCwULDQoMBgUJEwsFEg0LGAwHFBENGxQMGxYLDQsSCggSEg0TFA4ZDhESDBQZFBITGhUTHRoU +FhIbGRQdHBsbFhkYDhENIxcNIhwUIh0aKRwWLhcPJSEbKyMcKCMYMiQdNicdPzEfGRUhGxgkExQn +Ix0lJBwzGigsJSIjKiQkLCgpMyYhNCojOywkNi0rPDIsNjIrLSQ8KyczMS4yMiw8NTMzOzg4KTQ3 +HCAdQi4nRDMrSjUtRTYqSzozRjk1Uzs0VDw5VDkqTjAeSEM6V0Q7UUw3bU02cWgzOzdGMS5DGDVI +Qj1HRTxUNUlPN1lkN2d0RUNFTEpFSUZKTUpNR0VHUU5MVkpEVFJNWVVJTURdTEhTUU5RUkpcVVNU +WVVVXFpUWVZaXVpcVlVZSlRUZlhKYV1dZltVcFhJaGVadWhYdG1UTkRiT0NoUUVkVEtlW1ZnV1Zt +Yl5hYlxtZFt2UWpwZWJkaWVla2ZpbWpraWhncW1udWpld3Npa2d3cm5xcWt8dnNzeXZ1e3h3enZ5 +fXt7dXR6a3N1WmNcOUA1iV44jmxSqHdVh3hogX5+hnt1lHppmG9YsqM9los5sIpZs6lIg4F+ioZ4 +l4l3kIhxsotwmZJayJZ0zq5o3tBeT3qFdGuGe3WJdW6IPXKEgX+AgXqNhHuWTYyaVIuYVYyUa42T +WJWkbJqlcqy3dLXFdbjGdrfKeLfJdrjJe7nHcLbGfMLPhYKCiYWFiomJk4yJmZWIi4WamJaXj5CV +ppeHpZuYs5mJqqeXuKmXtbGcubScrqOMk4ujm5Wplo+ppJy0oZqqhq+2qKWmuramqqS5t7Kxn6Cd +2KeGxKmX0a6U5q6J5rKN6bKO6bOI5bOR6rSR67qY87uX7bGOx7enzbuv6b2i872jxp2Eysa31si4 +0cu06siv88Obg7vHlrvHtrPEmMLM2tjK29jS0cvH5dLE69TG7NTJ5trJ6dzT8NrN5OLG5ePK6eTK +6ujW8+fX8u7X7erp3uDcwb7BaoJ/CP8AAQQAMKCggIIFCShkQMCBAwMGFCyYOFEDhQkTNCjQsMGC +Bw0fNXTwMFKECx4ohQghQkRIEiJHhsiUGRMJzCNHjPz40UPIkJhJZI7w8AOmEZBDiCRNqiHEhgUc +N0gNQUHqAqkbqmKlcIGCV68Ur05dkIHEIp49eoyUgeRHiLRwffRYICJEiBEfdmSAIOHBAQGABRgQ +DNhABgEZMjRg8OBBhQoRHjt+XKJxZcuYG2vWzJDBYYgZDCQ4jDiB6AQJQFxAwcUNl9duYrtu/fr1 +kycpbr9xPZu3bN67Yctu/Zs3lycqLuRdoDUrWYTQCzJgIL3hgQoOEni1uF0kyQ7gPYj/MAmDh48h +KmVmmUkzJs4kQYPW/EFihIiQCzrcpDlCww+cR2ggAnvoceRUVWJhhdUOUnFwEQcQQpjVVwvsYJdd +T5GwE1wdaEBCFm71sJMPPwRBggYgjGBXCx9gMEEFDyQggGeIZYDaAaaZBsEDEkRgwY8/UvbYYyeU +YGSRJZzwgJFGPmYBjA549leNMuKI2mkGUKBaDbS5sdtvX4KZwhvCERdmbFycWRyYxXFBQwlOMcfg +BjtMVFAAAyiUUJ4EUMcAAgdM4MABFGhg6AYdgGDSST6Y59NMOMkU3xBBUfoTTkgIgdIII4CnQVgI +fLoAAh74ANRP/Zl6BBICUurTEUJ4/xDCC04puEGtO3xw6wYc2HqBgxRQREGuds0QggIa/uDDsvfJ +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 +EA8RDhETDBUaExMTFBcZFRobGxscGhcVJRsYLBkTHCcNHScUJiMcNyQdFx4iCRkjHSMjGCUqGSsz +GzA3IyQkKiYjKysrJykoNSsmNzItMzMzPTo0Ozs7OzY0KTQzRjUrRTw2UDcrSzIlOk0wS0Q8RUM8 +V0U5RVg6Zko5dVA5S2Y9JjtFPj5AGzZCMUpTLFlpNF5uOVpmNF5wN2R0NmBuQ0NDTEpES0tLR0hG +Uk1EV0tFVVNLW1RLWFVJU1NTXFxcWllVTVJQZ1ZKd1lGY11TalxValNIVm9Ia2RaaGVZd2dbdWpU +R2p2TG12Y2Rja2tramlmd2xjeXVndHR0fHx8eHh2bm9wTF1jQT5DhVo9jmA/hFtCh15GimNIiGtX +jHRdhnFZmnRblGtRpnhYh3hplXtoh31zjXdrp31jZ4BbioR0iIZ2k4x7l4Z4lpJ9kYJst4lprYty +q49Yx5h2zJdx16N9zaF846t9TXqJfX6BPW+AgH+BV4iYUISUXJapZZmpaJ6xaqa4c6u7aYmTbrfL +crbMcrnPeLfIc7rRer7SdbjRb7PIe8LUfMTad8LWc8DPfsbgg4ODi4uMh4eIm5aEk5OTm5ubl5eX +lI6HpZmIrpmMqKOMq6eTtK6Vta+YuqeXtrCXtrCYu7WcvrmeubOXtqWMo6OjrKysp6eovrqhtrKn +s7Ozu7y7t7e4sa6vn5+gxKubwbqexrSb1qmI46yF7rOG7bSK5rOL8rWG87aJ9bqM+ryN9riH67ya ++L2Swr2jyLalz7mr1aqOv8G+7cGf/sOU/caZ/sKQyMKo2se00caz78Kh5sSp9sin58y57dK96tm9 +5dW58NS//dW0+dCu8uC9vb7Bgsbajczhw8TDzMzMx8nI09PT3Nzc19jX0NDO7tTB7NvD8dTB9NrE ++97K+NvG6tzT/uPM9uXJ/uXR/OrW//TY6ODQ3d7g5eXl6+vr6Ojn8/Pz/v7+9/j48fDu3+DhCP8A +hQAYSLCgwYMIEw4UwLChw4YIEBgQQICAgAEKM2rcyLGjx48gQ4oUAKmkyZORagDAWDDAQAY7Tsqc +SbMmJEkmH0l6BOnSDpEABAL1OIChRYkWjxqoSAABw4gGnLIcSrWq1atYMxq6pDPnTUmTfgJwSRCD +FZto06KVFOkBUIFksxIcQEABw6gR897N6zQqRQR1BcSVS7iwYbkud1xCG85KXAxX1EpOi1OSI0iP +NF0ZKrRwUQVMQ1eU+BDiRMAWGU49zLq1a44oavKE5MjSlQQbrkTSNLn3WkmSGDngPFbuAMAC8J6e +aDHq6IkQKyYnAJ3h6+vYX7tcwMgmJcuWGEX/8k3+pGXgkig5uuSjuMjOWCk25Uu/vn371AFn38+f +sCHZjwQ4W3kE3gTJd49MwkgCVAWRVQACNDBfU0pV5FdSTslnn1GAOUXAYP2FKGJH/xVoomSOWHYJ +DlXBR1VpMDqUYYx7SYeXdIKNqOOOCGFw4o+U6cSIAi1eJeN99y2VHF+ljUYhdKvxKOV+LmHQHZBY +1rSiVS6GNABoNTY1nQBIhfZXhtSJFmNESU3pJn9WZinnSSkqIgCXVNEFVWoP2QddfU8hueGbhLLm +0gCKLDaniTiV1Oh6KuEJFISAkvlkRAooed9oeVFHUY2okRZloaRalcBWi8oZiSOYNIKVgyLR/2Vp +aBIxF9FDnE5UXUOikUYmc9PVNWqpxHp0aCOpzvlIJZdEKmlIfNIoLV63JkdtX0xW25QCdikAYrHg +ciRIOMnKuV4iWcH10ZdNOacrrX2JOSGtdp3ZabwesklkAJ6G669GAQQQWbmMAmdSbSikC5Ke823Y +rmmg+XqftHdJCIAJJgTA7bD/diyIogQXeFlJjmgml7odQVUam7vu1WFqzqGpsoUwNuDSJv3c84B8 +Hfc8kEsDh0wecCkaHEkmwp380V9PNVyfaJzSl5+gYkZIpAex9INPP0kA0MBFPvcck9AnXkZJT+3J +BStHX1rqa0O2Lvfpw7jOKhF9DFkcQBdb4/+Tzz7jFFXXt2GTigPZQO4ESdJKc8QvUxQHSjVeTTKn +wNcrITEOP/n43Xk/Dn7NceFSYlQD4lg6Eo5YjW/Up1H24ouUw07TGqHNAAiAhC387IOPPvno4zc/ +SHhtXalkxeUSWSwRTpDzrHEwHuq+WWagepYYYhD0H3WJEMPW9okajkfaaJeY0HErIVkadEFOP74D +33k+/ODT9deqjThqwMUp//NgGAkARlbjv+ts4ErUG5ojHqGeSRiiBhogHPcy4j2DFEVQMWtZ5PLW +gAawZAAk6IIs8MEPf+hDfsP7BxxC4DUJVWR0hvrgSpJ3qJWsZADLG8gABzAVHh5keRO8Cgf/Evgj +SjzCEZMIRyQWcQgr7AAFDCiSRgbQwaPYKFtPUw5qLlfFqTzgB5soRz760Y/O4eOMw8MHKFhIgAYs +pC4IOEwBmadDC9pQgDe8ow8DOEMb8pCP/AtY8lpyFQcgkIgEOluAHOGITFzikZdgRCKugAMMBPEg +a0uIB9yykts1oF6fpEjESAOYDprSLlMJwAPEAAZcnIOM+zAjGseotU6AYCVEGsAmvACAqLiRMAW0 +4Vj4p8M/8lCQgryhMY05TGQiU5jNG6Z7fjYUBSQCkXNipDaBo4lLREIRgqgBDA1SwYE0oBDpyAUS +IliQohTFahFziAUF4AETZOETt3glP/px/8J+Co+W+7iFFiCwEjcGYAnk4Ec6FMCmqwyyf3lcpkQn +StGKGtOZGMUhEJMJzY8kABHYXNROvmPEI2YiHIkgQfemaRAlFIKEWhtHKLrwAxtA4J2D4ZddGmCC +HwhhEJyIBTn+Qcbe+dOff+uHP8bhhYzl7k4DQEIz4IePfagUNOPsyEZr6EeLTrQA7ryIOwnwR7F6 +9ZgYBWJxmte8YBbkECFNFXpIZolIsM4j5SSBF7qAjvmRkYz+OEc6BmuLWuAiFrUARzrMkQ50/PWv +R/Wn3/ZBRnzcogskGOCdAOAB91F1jOfwwACWklVCUjOHyTwrD8V6EYq8c4O4kpVqL7rHiP9SMyNn +iWuWGsmTRUIiHILIwHtY+jMseMELaEzuGffB3OZSFn6xVK50z5iPpHKOHJxYggeWt9kIIAEWW/Nd +3zoRAY0JQAFZ/VYABenV1sKWRqAx0/nqJS2KplWat70tRsKg2zmtqqSXWISzQlJBunhACLbwx2P9 +4Y/pVvfBD/YHLR+7T85ZFhRZIAGRpgkBJHzCHGQ0IxlrccsIJWWzP9zebbfK3tdukIumjLGMZxzj +A3CxLp+EWmgqmtaA4XAuAMAByPqblgRdcyaZGNkCLzGJ2wgzJJl8HlZzh4IkhEIWsjDHParq3OZK ++Izn+Mc57pGOWsACFrmQBSeSoAQSQAD/xQvxwA+0MELAUpdzsXBQAMrbSfRuRK3KpChZo+WQNtKY +xhFItKIXzegIHNqUB/ikhOIbrQGAlbZ47KPpiCyZSDwCuIY8iREjQeqbWKIRkSptQrznkvNKaDUR +cAAEfkDrWtPaBCQAAQQcEIEHQOCXGREACJDQiVtsLcRopCw+PqFSr7mEBKHYrgcTolbVMoUugqPI +oR3taAfwmte9jnWiH0Ducpvb3BAgNwTCLe5vO/qUOX4hXchqX5ecjtM24Ul6JoGIETDhYyPDTCYq +UYnwsOgqXfpBhj80Fio2INGi4xjFnhcBEAjBC6AQYz96p1xapmMQHigoRkqQC/qBgIpw/65jHS3q +Xm65/OXqM2WjFQ2Bmtv85hqAgAYkYHMP3PznQK95r9edaG87oIMvN1PA6I2RDeA7LZ4GCwr+sAcr +aELJnnaEFaywAYUVRAPHzUIWsGACEIi2IJEblQAqjgQsfCIW6NjaPr+c3L/xQx+1UAKRIjQWE+RC +wdW96mo+uEOJuvdGCICxKY8ea1nTXN01x4AGPED5ylv+8pYPAeZ97vOg25zR3nZ00lODbQQs4ukz +YSAkKrGqcOAADdDwwxUw0aiSPCITguiA0uKShL0iAQzH7cQ9wJELTigBCRo+N7l7/QCLK2EJXQAF +KMZxD308FsLYt3s+xtEFELjELgBYQP8QbkHZzunDHyqddjGhuUwYoUbSHeT2zG+OeRDY//74z3/+ +Q8D//vN/85bneULXbZ/kcvSWCEOGeo5ACZSwCKcXDj0gA8gwDL+QBNaDCYmgCJhgCYrQAarGEfAB +ArvkBb/XBSaYDkVFDs2AD1lQC/gwZmF2DmN2D7S0TyGWfQ9GXQrGT+CAWVC1YRrwBe+zD/8UPP5g +Al4TQIU3UWTVNIoXfw8Xeg8QAZEHAZunf/ZHAlq4hVzYhVj4hQAoATkXdIrmbR00ACWCejJBCZNw +CMxyCCmADMQwDMZABZVAG5pgBVewGJFwcK9SEAQAAkrgBT/QBV/QBWBwCw3WD7CABPz/8AldcA44 +KEueg33UBTzP5Q/phAUh8IOtFgR1ZkLyEzxbg4Qb5kzG5F4cRGOhB270d3kfwH/4hwIgwIUYc4u4 +mIu6aAIn4IVYyH8fEIuV53nhBgCCoIYzQQmWkAiHEA6MMAK+UAwU+AxMkAiWoDo4EAZXtyVelzsM +1wAPYFxw8AW54Dv7AAoRsFQeFlknVF3C8zvmN2GVVQ65AAYatn4rYQKdgIJllH3DYwtnVRrcYmiL +J25DFwFjWHkhYHYLWYu1uIW4KAYSOZEUWZFisIsYiTFa+JAg0JCaFwIfMIweMIYExV/IiBIJggOK +EA4o4AfI8AvDMAzPYAbNmCAkMHvr/2EFuwchRTEXsyYEWVBV/BAKAXAL/WACTmALXcZc/vBcj7UP +92AOsfAJmxAE2/UzORIAIaAEgzAOsPQ77Jgzm+BBmMOEp1ER6nMARwduNFdzmQeS+LeFvUgCtlaX +dnmXeFlrFHmLXeiL9/d/lRcAY3OSOcEeOBAONbAGzwCTwyAMxSAHPhAOkrAIL3AFSeYTjaNefxQA +cuYFQSACSYAOSXACWWALg3WagzUOscAJncAJWkBrHgA2DZcjABABJuAFxrZxRFiJD3ZClAUKGpA7 +9RJo2DYhXMRtDSBrr6iQcfmQuIYxFikEQTCd1Fmd1YkESGCd2lmdeGkDYmADNuCX+f8nmOJAmAcz +CYmgAIyAA2sADTEZk44JCDgwCZdwCE0ABiXRhwvQjQahAB5gM8Q0EESgAhBCBxYAANxyXn/xY3Ox +TASxSlkAC+awg6SIg78DP7VgAwWFllD1R1CBADLGaDaXcwopi1mohbd4kWLwA9sZBNj5ojAaozI6 +o9nZokFAaxOpAnyphSiQayQgAPdGmMDBgJYgBEvQA00ADYz5nsMQDD/wCD4hB2DAEwKGAfxZEBDg +BVmABD+ghZoHADBABRgBB3dAnMsEIg3gASSABZyQT/4wd5NYXfAIP7fwA+bEUM2BEToGY+DmAL7m +lpWnfyiaoivqUyxanUuQBIq6qIz/2qiO+qhJQKMtKgRC8AMqepElQAImEAEoMAnmSRtHdAmGQAQo +AANMyqTCQAwwoIEt8AdY8AiWcAg10HV/+C0NYIJKsAldEAsNBgYjkAdrKgRbYAHASHm/5gGaZwNJ +gAVgAAtCBTz7FKfuaEZJtWxO1Ubbcl4G4BLvF6LMN6KBmn9a2IsYowK1RqnUiQSJ+qhY0K7u+q7w +Gq9YAKmROqNLsATUWZc9BQIcICD++q+mZgkhZRmTsAhHIAMicKpMCgzP0ASIkAgwEA1YEAmYsAM4 +QKsIlxAI0AV1EARZAAacQD+y8AF7QAXmIAtwQAs/8Al+ow/mALLCo2D8YIOxFKeR/yWz+VALWEBQ +AICtd5MaHzJvMQduUwiolfcB4qqpJuCd5zqd94qd7Cqv7aoFWCB2Vmu1Uiuvjiqj9zqdlRoEJIAA +ivCvAnIZkMQIgiAInpoTlWFEIfNpOKAHaxAMCvuewvAMbNBEbEAMSpAJkZADOoCxz3IQIDCvSTCO +KIgLEsALprAE+oAH9CAFSCALi6izscCOSFVZaISJf3UPsKAF3ldQLiQdKnMnoCFzvFaF9Xeig9pT +YnCj05mdL6oEiiq1YHC7uJu7uru7uyt2WbuokooEQrAAhnCN2sRA9BkejbADHBBFilETCyQJhqAI +IpMJsqEJhNAEdauwyKAHYRAGf//wC0uACYpQAz0gXFhRTgEDASigBVlQDvxQDiJAD1sQAuXQCdKg +DUCQBYqYD+eABV1QDtMFj/7QBZvwXL2TD+RQCzQVAQRBXxNXkInWc5l3f3JpAhPpU9S5BFC7qPKq +Be6bBSY4wiRsgl9wwodYwipsgr07dvHKqMenBBAwLpXASJiACUqUCFbQAXG0diBQAKhCE6xyCISw +KI6wCJNQCSqwvQpLDL6gAVAQDfKZhzjQA4IrRQhxXjg0AA+wBIj1AlwAB9OgBbFgB/VgByQwCOgw +s52ABCE7sxtng2T0BVtwBKXZCZ4JAijmcAsAwTHiLXvmig+AAYAajBbcuj3FojP/2qhVe7ViBwYs +fFySPMmUXMmW7AUpXMK467vwWrseYAWWUAlJFA6LYAg7sAFOYQF0SWslEAI1gT2HkAPUmyUJggiL +gJh88AxMfKrEYAF8AA1rgAORgAI5YMVX2hIPsRAAwATyIA908AAloA3ysAUesASDoKURoHB4PMld +kAQhAAf1IA+kQADP42ox4hxLwiZ10bPr5muE/AE1Z6LOeYu1FgTSGaO1u6yOvMKF0M/+/M8AXQib +MNAEPdCDcNAIjdBwcNAoPMJg4MhZ4L4hsAPh0E2KsMMMkKavq6727FMi8Lw1EcvHKBk6cQnWc8SZ +MCApUgmEIA47oL27rLB9AAzQ/wADVyCrPeADV9wgCvE4F9FG3AIAycALaZAHIwAApSAP31ACBQGO +CqEA29AO21APqKBjVt0hSzEf0OE1yzkAHmB/Pdq69Ry7SMDIWADCIXy7XXDJ/kzQndCanBDXcj3X +cv3Wdv3Wm9AJdRDQDN3QuPsFL4AD4oC2HaAAEUACZT2v+Nq1LioBezgTlxHLObAI6FHZRAMcqzJw +hjAemZCB1osZq3IJgmAIhpACvOwLvrDLcwgMN1kDOqADxpy+GQFHPBQRbmQHe9AEr3AKAAAESj0N +EGAAjjYAL3Bo9KQNeGABXFAPXPBU7yU1OGS0CykYD9m6hSqdLlrWjNquEf2xkP98yUBF1+I93uRd +3oXACf3cCf180JP8BV6gBSgQTrKmAkngu40aoxAwy45CCZEQqzmQA3AFvUSTnzbAAiEgyocwPbRR +CUhTA4xQAtsrB7vsmH3QAShQxTqg0wrjPPPGQxbhRlLgCk3AC/TABELNDfFQBwiKXh+gAsZzGggw +ABKwDkXABCkgBfTg4vVCM1ndK9CR1fwSksi6kA7cAEoLnZZ6qByMnbSrqI6s1u7t3l5wzZsw3p9w +5Vie5Vq+5Vw+3q2p3p1A0Ae9CV/gAAIAASZgXF7w0GjdrjCcBCpwA2vLgDZsCDnAA7JME5bNSCuy +Bt7QBD1QCR9jUg6oCToQBjj/IAe6zMt60AaLrrDCgAxrwAEb0AOA2wMccMztlHgYcV7kXAK60ATU +0A7aAAFcQA/bIA9OEH5k1QU3VWhnLg0wUNQpkArTICi4HhEAAAHAWIsExZkYU5dO+6IevM8mLMmD +MNB2HddcjuWh8OzQHu3SPu3P/gmhYO1Xzuysedd4bQLtm9eDkMkPLXZU6+bLCgJWwBUHktl2zgM6 +QAiWLRPnkQl5KAPPIAyBEASCkAi0x0iIEAmVwAEh0ASPjqrIIAN9wMTIgAQ3gAE+oAPF3HWXtBEo +80NNMUAWMQARwAtVYAdKzQojoA7aMPJH7cAvAAdeUy/o5QGjAAN54AavwAv1/+AGXuMcTIE3SzIa +6CVsDelmAjQAGJzk6crktTu1IlzC15zseQ3XzM7loQAKaSYLsXBmVE/1sYBYVy99Wr/1W3/tWz7X +ev0JYb4J/SzJJHy7EX3WO6sI10gymNDueI4IlxBwB4MemMBEIBAMxiAM0cAEh+MokpSHAEDwTPwM +fWDadVsMf1ADVsoBPQDbmS7bGXErC8F3ePAKbiAN8lAPeWAHqN4O1BABVAQAeGCnX/NCHyAKUKAG +adAO67AOrCAr+OEpDhEVRAIBIIACJXACDsAvAaABQfC614kETb6szIq7JnzQ/Vzl4+31Vx4KsCD1 +shAKndAFWCAGlUoCH/nV4P8pBlkwCJ8gC7VQC7GQC+Zv/lyv9dGe7eJN0IVwzcd1iLobBJ0aILSB +CYhQzHhexDVBe4uACADRoweHPdAA+YLWZkc4R48wIUKUCAqgYRUtXqxITEQaaBiHIZPjYEAAADUE +cgAQIMAAAC1dvoQZs2UQmS8HKGCZUkADAHDkbRuFqlSqLcm+bZPnKYCAASBYNUBAIOoAD1yYpIHB +a926aREEfP0aFYEBsF8JkBVAgCkKEyQ8DBjwNYCKIHWXIMGbJAkWvlqygAHc5YsXOIUMG+aUOPEn +xqEcg4KFS1YnMEqW/BDxQokTOnbw2PFsRxQdJ0deWDBxIkiWTbFu4YIVO7b/rFygbN++7fgTp92K +O3XatOlwIS9duoApFEKQpkeQHD3MwYNHjxyIIF2/7kiSpUqKDlHPgSMFsV8yoECTY0VTw0mCGP3o +5VH+MDMX+mD8hUwGgARMAeDoYQMARsqpJgNfEuLAlgQgqaUBeHKBnm1GkECCCCJ4QR5tkBoFgAV6 +4gKAAxBAgKoqnMijCia2waartBQwgES1voqRRBnTGgACECBg6cUAQsArSCX04gsLLQITzAsvBhmk +kN8UY+wTx0K5LZdYYuEkCRJMOIIOT1xpZRdpWiGzTDJnaUUaaZKZhRZPuBjiAw1+6OSWW2TBM888 +Y8ulT9uo1I0xxTgBbpNB/+DwopBBgsCAEUkecWQSRHLQgQcdchBkEkcgkcSRTCCp5LscKJ2Ogz6i +aWOMFXqRAZFLOF1EECvWeGY+i4QxZpg2RgjBl1yHMYYYYiQIAIGlVtoBJQJTIqlBBWFKUEG4Xvoq +gmnkcSUKPubAwgkN19kmHjsAUKABaUAIgAACqNqCiTfyUIEOeVIBoCwb70XAXrJUWlABdZkC4i4k +9NqLrywONu6LwbwQbtDFprStTytj8eIHFabwpJVkdjmlY48/BtnjVVAhUxpX7DgCgw+0qGWcWq6E +OZY9YemTZlAA1c1hxabwAIAdLnHE00R0IFoH6hDB5BFJIMEkVh4orXS6Gv/OEyYQNmTQA4hKKHnk +EkSscMJWjIRBhg0RNCCCmIqEKWYPANSKSwAAMMhgQLhIGonfZxF8Vu+WAuBplHmsUYOPUrLA4ydt +NqRnipY84YVctSJ4ZYQ08kjBjXroAKABf9UFPfTQy9oJRgEQUAAAE5JAYsiC/UJSSSV/e3IxKR8D +RWJcYgHDBCDsWCV44YcnnnhVjldlFeRNUSXNVbgIAQIscAEHF+utj1lmWWTz889AeSvEhAEBMMQS +TxcpWgcfcuiBU0kkWeQQqNMvFRDyivED6yUgeeSRSa4IgR7Ehh89nCEFCXjDM34hjGdAgVxjYYpK +JDhBCjrrWdHaG7UAUAT/eXyDD3yQhyx0oaHFLa4dTgDAB+SBwgN8ZRt3UMEeUkCHehjhgaTDYQ6/ +ogAB+AsAHiBYkWBnnC4oyVCbqB1vbkel3OUiNrGohRdOUARPzKJ4V8QiFlGhio6pYhazqIMIIuAF +cNiiFmdEY/a0x6fa4CYXSTAWThrAiOss4mk+INr6XDWJWM2vaNLhAQ6IgIwFEuNqa8DBJB4RiUlk +QAYdsZUwhgEMXEHDFxMQgRNC8Idi+KIXEughib4SF2ZVcIJ/u2AGU2ITa8lDGnzYxidKOMt1yEMK +APCEPSKQALXsoh5TcAIQpCEPENyELFLB171IF6N/4cQBQTTSX4i4pEIE/6cTUFqibWQjm1rAAggl +8EQysjhO4SEveeU0JyrUOYtkwEFHuSBHLex0CzReD2Yzq40sOEGCASkgdTuwBCQiQQg/6sAKmaiE +INK30Er14Aa0+kU09DADGZzhB/ybRCIkMMBbRQMZaSgACQoiAlrpQQ6d85wOB1AAU0pwbzRR5fg8 +AAIAJO4brzCFKWZZwm1oQx5H8AA93CAiA0gjHqhIgRHasQwdmgUtySQRWhrAoCD05WCAkZ2SqnlN +292uibKRWSxs8YUPZEGc5ERrWoV3ilckgxVKEEAXyAGOedb1jPZc4y26ILdRJiAAiJBEJARRUEwh +gqGHDVAfijGMP8jgav9sGEIlJHEJHbCBo8AgBjL0cAEFJKEBBdEDCGSwBwc64AD+lIpURgkX1rK2 +ggrCoIL4RRI4zAIAJfhJPWrBi53uVB5UeIU8RiAiZcTDE5mrx7gMoJaoGIAsYslXU9Mitx9kIZqC +AUMRmWRNbOLOibORxZXkOYQRjEKt51XrFlWRjFFEQAzgKIct5GuLutLTei+rBS6UMKCpng4AKJgE +JA47YAL3AAN+GEYxZjADMqyADTBIhCQwEYY/cJQYvkhBAH7wMguQ5xlmOAMwGjBiEnsOKjwc5Yxa +m7e/FSgmCbKgTVyyEgCMwBNIKcUAVvENe9Cit72Vx7VYEZdkxAMPIpj/BT2KUC/pNnmHQjiOccCA +l0EsCYlcjZKUIgZeme3OFqBAgRLQO2a0oiJ4p2gFK0qggVikAxzjGMd85TtPXNgCFiioF05OhwAA +CCIcCiVwoBe6SWMEQwYglsEaqnAITVSCCRxl2x4iwAlz8CMUaICGMMgGDT0E4AEl9meo/YkWpqx4 +JSxhSYxdEluZpHp8TqjHNrZBj1Z44se3Xly4OMeLetzhCEGG6r9ipEMDfI5BIEBCXdwSAAd0YRN1 +cFhjqESz2VzveuX4ggXqQGZuZzF5Zj5eOxvwCXPA2dzNmO8tyvEJnsxIAQnwqwYmYQhB17tomyxG +H2RAhjOsYA1r8IEm/wSBBkh30gj58Ic+znGMX+GqFwKIAAQi0AAHTHzEp/VnWtS1Yo6vMsasnnEp +Tw2ACODa5EAWwSnqQYVR1MMTbxMdMs2CTKhGt0AsOUAXOIHErjLxu7AI75VwcddxeCEEpuh20seJ +PGnYYQCcQMc4ylEOc4PjzV4IQMb3LIAPXUER9g50dIxmqmj0AQZkoCinwyCOHayBoxX58C32gQ99 +jA0ZaPB0xUsMahTv2QAcR/Vs+faSBlUQLgUAgBFOvngOaYUO25jHLaEqALIMe5lqUW2MSBwXnQ8q +mz+H2X3tVAtydAECSFd66ou3vFoLIBTnmPrU4UyOUAAAAhBYyukE0P8fBBwC7AUu2kD0EA1AwKDf +mAbEEiZRAwS/HRmAQMI++pENjKgNGmYQgMQr7oC9k5gA/S0Lx13qLJqomoKsZYoAnICNxS9eHqvI +g4bsYEOo1BxfpPuXjFIHgS984ppe3bLZELqhkyd6IgcvgADVU8DhQZ5VoAUvwgMIqAV0IAdyKAcL +jCcFeAANcAAeggsFCIAO+D1764EaaIJnAAYYKIMFOwZicAJEUIEB+pVh+IVnYINCuAWKkA9kEIEG +uL0LAcLuQy1RkouW0hsYcwnAW7GdcAEnaD+TwzEtqAdqEIV6QCHTiYq0kJGYO4uoCB2WsAHe4A3v ++q6gu68zuoX5Iof/T/CABXTD4kGFU5AGLtAAcjAHckiHdLBAdOgCANCADdAADFAAYwkAHBjBeqMO +EEAIM3AsKAiGZ2gCE3y7WymGJlgDP/gDY5CkivgFYugDCbi9H4y4CHAA7gO1BliusiCAllolILAb +JUS/F8k+F3iCJzS5dVABJigCO+ggKmiJEQsdqDqmHoIKkvCALAiFMfQ5LhMvNJIzWyCHWHAAUXhD +NzQnVXCFU0iGIzCBezAHc9DDCjQHEkCAENAAD/AADlQADjDEQwy0gVgDaOiFCyApYoiGNQCAPZjE +i0AGZKgwjyCGGUAADQBFCHiA23sAU+y+HFJCfik/GkO1UiMdz/EK/wt4BVvsrXVYnKNwgy0oIXkw +BRFoCdQZtWITtVAjpR/BgixjIsjgsvwqwPmyujcbh6qoRgW8xgZcrxP4An1Ih2+8w3PIhQDQgKI0 +Sg3wnA2oAR/oAYbCAQaogd+jDhT4CDlYA0IyhkvaR+eTgwkIxa/8wYqbOFL0nJMsgBlJv9ZSCZqA +RbPovgcIAF7EyFurpQ5anGyQB3kQhROIgGnhOL7yNAEIAi9gyQAEOvGyHjuZr2Ywt3FAhx84gptU +PXWizMocGS4agWb4B6AEx3sQA9tLx6LEgHSEAAHIgBrYgR7wgR2ogQwgiUN0qDOABmS4CGIQoK2M +JGTIH7DkzQh4gP8HIEWLI7GmUssASBDAA4tQa4CxdAAIaIAPmMsfo4Zl0MhZ6il5ABdUGAIREIEP +sIAPEAEQIAAkCAEs2IdO6IJcUMZtCjooQkP5YszGhDN08AIXkEw3rEwzu8xWqAMT+Ad0AFB0MIdz +gAX+aIBz/EMNgICifAAESIANyIBUKxF37AEN0Efc3Ef8KQNQ/AB0REfeRMhPE84RQ63pKrUVY8uV +wKESE8uI+0FpiM5Zkgc8CK4f2wa8tIbLeYM0SIM3eBcVAAEl8Id+sAUw0CdYkBkBjIUzVExbkM9x +sEBzAIcEvM8qPTNpcIFC8Ad0OAcANQd8QAJyUYCjPEoP4BGdGET/AHBHHdiBB5CAFMDQtwsEMmCB +BgABD/VQEA3FUuw+8As/1loCJlNFAigXivNNsPSACFCGGN3IZWiCPfiGjCyhO+DRSk0DNcAcMcgH +hCOHEPAALUgCJSgEWLgFoUPDNLSFZohPKK3ACrwHJJACK61S9TKFEyCHfziHXEWHf7gFuEiAgSRT +owyBB0i1BQiAQ7QUUnGkOB2gXyiDCLhTPDXTPO3N7VtI0ikA1qIJ/COx4HTR2/PQBqAGRkWKFEkD +atBIbrDOdngFS+VRTFWBIdiHfNAHen2ALtiHdKAJEAADeZIn+JTPC6zAPESHW1ABWa3S5JFDKSiE +fTgHXM3VfNgv/x4C1mA1SgdokKgEO0shmh7YgQCAAWYVG82CVhD4AE+V1mkFy1HkU75TLYkkgEAF +i+771ttTUHQMAQGAA0b9hmmAgUt9hUi1TnngBXflUXj5AYSr13zYhxCIBX7wB374gjE9gViIM3BQ +VTiLvVb9xnT4By2AA4SVVS5CgnPAh3+AWHwABz2DC4sVVkEEgA3oAUt5GocCkMPCgafhAQdYgZAV +WVtBBhWAABIIgcJNWTztzQtRyBFbLmYqtUBVi0JdThDF08L9AJ2NTqTYAhnIA0zdKaRQBjVYA3e9 +gxSIvnzAh03Vh31AggDogk3lBzAIARAAAVwYB5qculbNQ6AU0P8kENs3TB5XKIVScAUukgYj6IR+ ++Ad8uAdczQeaQDGidNuiNNMAwACxMxoG2ACG6gEcyACpuYAzuIC/nY/8WAMBIAHanV0PQNkO/VAQ +VdzgJDF/ycKvCNO9u5CDhAALgAA8PVnaBQESEIAXWIdXgNH261kYcIN3lQah9SllcFc1eJcV0IJ+ +QN1NRd1+4IQQcAAxOId7CAIEANIPsAWqE1g75Mxv/Adc4ILfrUZspAPmwcZR+AG6w4ez/Yd8iIVi +sbnpPcp3A5BAshCG4gEMaAAe2AEHmAE2uIA0IN9hkKRfoMFhuIYmgIATSF8AjtaUPccFVdCw7FPS +ud9yyV/97V//dPyAk51dACZcIcBIpKiCJ+hRoD2KSG1XS82DFLEAJ5hXDFbdfRgHEBADD/gAAIYA +IdAAMWhVFP7GAAXQfOgCOnBha8TGUuCCVgi3IQgF5b3hs82HHyAXsEgAHzZKuOQAK+AAGLiAGwCP +1OyAC/iAHOiAFTgDRhxfjiqGX/CF/OjHXvZlYigGZDgDDGiLLM5iELBcw01HCTDTahXCf0GCAQBC +B/hNcMVTLaZdEtgSEMiQJ0SKV2iCPHBXn6KGPaiCo+1cGMCkTfZjP15dJBBkbTYBE9ARMVAATjgH +Rh7QXM3VzRyCSX7hZOCCOmgFNMODF0g4fVDo1L2FYuE9nBhT/1LWAAXIAAwwgxnAACsQCELAAQUo +AxbYAAloAjMogzOYABjwhcWyiFzJFSn2AzMgAynWTTbwAz3wAznwgzXwhT4ITzGY52LWZmz21A6V +Ew8oyK8ETvktMXVp3Zr1YnTE5gDWZhI4gXmGgGWgh7z8serchnWgBqNNAzQghXZdgzx4AyoQXAiw +gU34B35oZwxW6H6IBQj46Xm+0x8IASE4h2/k574+B3+QhSI4K4BOPXVyBVYwhS0oXi96gVroB4XW +hxveh0BdALDIF+n14Q2obAngLA7ogA6oAQYI3/3YNzIoaRkggDFovvlggxXog2d4Bj3gAuMojEII +gg84BhkAgf/LEAMxsAGgPmZk3uI8fd9QTOoR9ack4E2jDgE1ZuP0led5/gEQsIAt2AI8ODl52AOw +7lFxZgIRqBAkILe3Jm963Yfp/oGfdovU0ABb2Gu/Xt7UtQE6IJNT8ATgIWyl08YpsANMVoUp4AS3 +plfU9Yda4OESCQtms9gNwADq1YAFYICJU8ULCYARgAEZGAMzYOIVGAM2+EePWOJn+AM0+IJ94Id+ +QAdcKAQqAAT9qIsgEALfVu+pDu7hJm6wBM5Pm98kyD5rZt+opvG69m0h+IH0jQBWICHr/AZ5UAMF +XuDuPmsZuIADQAEskIVzOPG5S93yVl3UNQEQqGsT8AASeIH/cdMHLv1gtD1bwDYAU+BPKqCDU8Dv +/EYvdVIeV/CEIpgFVXgFJ8ACC4ZrfPAHfqps0hlliT5H9g0BDkjHBscAI1aLBpAAFgDvMhgDMmgD +X0AGtdG0YmiDP4AGGNCATniCLXiCNZADTlosFwiCJagLIk/veZ7qqKZd9y1upPbNEeXxZr5mLQ7y +eRaDHxD2HxCCJVCCE1iF3tqGdtiGIQiBEbAAKjhrIgDv21OCWhhSQOdyuB7wfBCDCJD1thBzEhCD +B/iCfNjV+O7kfgACC5AGOgjbZLBkOu+242kFKgITT1gC8u4HL/CQr+iP/viKBUD0B2AAf2KA5nx0 +9k3HDXiA/4wrAAkQgRWYAT0IBkL6hYyX4kDoAznQgz8Ahn6EYmAAhhUIklf/ARmXdRrXYsN13/4F +1xBVXM/xLKPG2TWW6l83gWEndiGoCyQ4gWnYEFlbnHiYhhdYgk3gBCQQAAtgXyBIApYZUshW3S3f +8reO60AF8zCnXTGAgCTwh4ft5BuuVxQ4glLAA0xmBYxB2Jw8nqRbLy5wgmQwhWS4dqjtY6Y1Bwdo +qqzzAEAMxET3gE8rkbix7ARQgOak3hAARAx4gMomgAJggTPwA2R4Bk4fBo+SD3n8gFDFC1cPAmH3 +6RmHbi0+WcR15gtJAgeo3OfWZiw2ARsIdmH3+Z9fgiQAAf8akgd3kId4WAdRUAEsMBRGEYMk6AI4 ++IJOKIdtJ+96dX5+6EM7Dfd5joAQGOQl8Id/gGzIJlIM4IJkyMZWEAUn0G88yG9XMIUX2KJdqIMk +cGt/2Ad/8Id86IckENQcUoAGb/RAnKqbiC6GBIgEChRoKGjQIIYFAQqsYPMH2bNhEicKK4bsz5oR +YrQkSaIECZIgQX6QNGHSBImUJECwBBHipYeYHiDMhGAzAgQHDhpgcRCiZUuVJ03YEPNDiBCRQUCC +TILEg5RUpk59cWECDKdBWSJo8eKlS5Zc+vKRLWv2LFp8/bwAENBgqEkQnEiYCCHE3z98+saS7bfp +gKdkqlT/tTqCZxXixIoXK0alylWpOowXDx48+TJmxKgcSytCZ5YrUyr2kd7nL5+/frcCKEgg4DVs +16wxyHwA+zZu2AgEDEgwAIECCAcxbPCwQQMC1ivQ9PGFjJgwYxdXEABAogsWLB0/hhRZEq5KoCA8 +hJBJ06ZNBxEiOMAC4WdQlXRNFjWKdMnSph2zL/2xBEkWm3SCBAoQmFBHF2DUQQ5aDTqYj1pYANDA +AB7YwJJJIYDBT4ZCNIiXFj+U4spjnjiR2WWbqXJKMkDUMQuKiaFyiikxZubYYK3QoUUyq5gSQC38 ++OMPaamRE0BuCCi5pAI65fbka7vhhoBvCDTwgAYeZGkc/3kJDUDABCs0sYYeflwQQAIKgQBGdtsx +5d0PYohhknzhsSQTnjXdBIF7QMl3klEkJSXSEv8psV92WYDRhVdedRKECYMIYcImYGxyz4OZkoUP +Pvx8MeFrdHkAQlxI8IMESx6i1U8zTgBRWTJTSGbjYq6c0sooFnjCyoqHoViKJ628MpivtCr2mClK +lAhAEvyUVuQLK0A5LbXV4paAAzFhcFCWGjiwWwMsSNsAAQIB4EEWWbTZEUhBICXnnCadIJ94H5iH +Hr59gvAnfTbY8INSIjHVEaJaKNoFo15wIsQPm8wVhBdfaOogX/jk08+nSFZokgfzobQPFiGQEMRZ +++TDT/8oUwyRzCmqiGZsYzPOAoQLo/BqCh0osjKKHa3k6AnMxz52RCmxnntxkfxkAcUx1jr9dG5N +cntQTN8KAEAAAwgwEAAOGLyuU0jgN1KcP5ykEgokjMBSeXniC4EWHfNrgpxHIUWofklkh4XBiyLs +xSCcIGFCJ145kMQXDE58lsWb8tMFAAoMAIB1JpzgwVAf3PPFBySIMSToJq/FxRA+61gEZqqsUspl +quwCRwpSmOLYKTljdqsddPisSjJuwBE0Yiu24gId0gDhQQPNOLtPP/fAAMwzKbQFNfVJ3uZaAlNr +D4ECAQQAwAANBMAVm9qx291IRoFnp0sh2BvTeTbFza//DSrUPeh/ee/d999eCOiFGDbhvyBooAu2 +WJyDLMaPTUROcg1ogAJQgrn5hAAfXhDZ50BHpH7g4wtaIBodqOGZzJjCDq1zxQio8IRTrIIVnphC +Zk4xCzuUgESEwcMHNqPDHfIQFTGTxhSmcAojKKEBsVje497wjGfsoXpOtFb4tDc1DwhAS1iLXBLK +RzCnKIVh3zlbSoDitppkQWR08RzdxHA3geVNb3zTAhgW9QXAbcJhLBmE/yCwhFggsEEW60cWpjeA +CNClQqOiE0r6RAIbPIs0/BgHF6bwAmlswQsvKEUPcdQKPOBhd4MhkSmUQQUVvIEIg0mGElwgjdu1 +wg4N/+jkKUKjgiFcxlZAm4wqZgEHJTghkh4AhbP68Y8V/EIiwbjAE6mXACW5RgBUgo0CqCTFghxn +W7noQh1MIADvJQE75gubUo5SEhuYoAQosdNL2jYTDEAgC6RKI7zEEDCQKOFQbuSbouTohUIUYhOf +0ALllCA4CHTCYmPZC0ITqtCEpiYJ02sA5jgGgAeg5GzlIRUJ8HGapOXCDlMYgTRa8QIgzKIyJv0k +F4ZVGVc4BhW0UEEc4iAFXrUCBU6AES5lSIcRFGEWsSyFFFRAhx4tBhWusB1jVMEKUljAA6YAAgm+ +4Cx+cAIN0PjFL5CBhsgJoJnJ/GpvBkCQaTYgZbHAR/86cNEFCAQgi1oAW7vcRZIfkJOc52RfniQA +hnem8ShKGRjB9pYuv+Gxn53gBCcgQDktcMIDSyCHPxDal35QtrKWvew5gjChCIRgKGIIAQD4WtEf +vEQMGjgHP5a3D354oRRJqIArYsWFkp70k6bYgklVxKIsiOENaeBCK1wxig9IIXW8cwMQVGCzU5zi +CVsAAi51NIqY7ZArrfjAC7TAj37wYwh/IMYwKhIMCXy1vLdREm8coD0BAO4TzfhEOdBRC6d4II7q +Mt+b3OVFf4ExjC1530zA4DkVzHVQ+dFfog7WhUHAQUCHDYVDAaCBTWABBHzUKGX9cQ9wyOITicJC +ujj/ssUkgMAB+4KLCUDAmrMh8idi8IAKQmEL5q2WC66YggVMsQsgTDe6dQDusYxaChHE4Q1osHEy +uJCCKdT2pLzgAhGIANxbOeENKpjCKhPzydzNooeqQEUrWlCKUhigCHfhBzhkgIxiDgN6TZieeeP8 +mgYIh5oNWMJhc/GFItyiFkHIwke+AJb7aoc7+EEKSfwVL/aNB09dIAFJ1DgopiAhsII9mFcMe1hO +5IIElENCY7OQWn+gIxZdCMJKYiKelrhPSx9A8XyoKGHwmEQD7/RABABwi+3mow6x+gEeXBEDl9VI +RiuahRbqsAvFsEKGRHDBG95ABTjEFghEONHtdrEF/zSgAQnJMEUrjIAGOHyAFLWFzBQ8qTpWzCgZ +L6BDHQDw0X8scA3FmMgwkCEDOMs5mbvZTQCy5QEHkEAWXigHF26RhXHwDQtKEGCCwJCFtxJMbGOb +a7yG4l+2xeTRc50ngg2Wz4TVcdOfCEUoFNuAQZDYHOTgBImzJFoU05zmJIjAlzZWTnOm+AED8NgP +InBEfqADDkCUAh5KgeVReKK2MjxCsEy6ilZ44gMxfQMp7OAKFFLBCDGUhhNEMQ0xpEIVvJACGu5A +hB8IpjI4ckKTVyQN4lVBAzgeBz+UAIh7h9cYwfhAAvjdbyh59TXLLPyctwWLLJQDFh6pBZvEVsdB +fP/hC/Z9K37jql85qYBO9GqfB75gAqRMuo3Z0QL/GDWIknPiEyfPRSe+h4ROeAAJP3gABGZec5uD +BwJSuhoKKmoSCHxJAHCBADpSe4vibUEKrahDFaTBydzmaAqlOCmLhgADIy9jGT1ThQrgsIViJ/VW +R3hDPYxQh1bw4gt3eEMcRCAKnAZvFk4IFmVYpAQwbAEEU6gALvADFfiCMUhERfSBAoQAAgAA4inJ +QPxbAniPBCLJ4F0PAHCCF8ACOYwDJ5TDIGjBEpgAYtVRxAiaxFGcR+TX5mXc57WP6LkLGyGBPV1a +FiDMFwzCIBhW651cKMRCIMkFEgzAA2COGHQerLH/mMfQ3HgQQJQYwNVgVK2JFdacEUqAAPPwwydw +gTTQgRNQwxZoIR10Um2ZApM5XQnE1CvMAymIQjKUwghYgxsUS/5JA9TVAxzcVJK9wjKkwRMQAf2t +Au9sARwQlZbJkBS8ABeUgBaEwCf0AxqA10RAAxsMwHEoxARKIOVcUdRs0zZh4hN5VfeMDCf8wziU +wzl4GBCggOshVifgUcRc3rqAxNiQnhgURX/9lxfYQFJQmqWBWLrYoFcICGK5XiiAQi7EQhIEQSd0 +QQRE1O49I33ERQhEAG8IAAEgABNCkMdQUXV0gRbwVQiYCnd1gSfsAh4MAR0WDxfgn2JARhGoG4tI +/wG03UE2uAMeFE8dwAA9bIEd/KFisIgLlEI8KIMKsEIyqIEozMMoxMEFBMtmAKI0cIET7EKTJcMW +EEAdhMAWhEAX5AMUFIMwAEN4IQMMJEAIZEkDnEuTRAAGOAAEiEHurYR2VBiAhMAPJMESkEDHaAAn +JlMAOJYNMBY5kEMtcAQEgAIorCIntGIhREyCEJqb4McSkF6i3SJLABB+ANY9pUsNdsEcsd4OFiMs +wEIsiMEHPgAazUcSQmMYzUS1QEAEnEQIfIl1UFVE2UA7bRc/OMFPHYE0vEAeWGRRTd0LtcKxnMIH +UMEbTEM72GMIbUEc1IMgEuJiAGQqxAM3DMEoJP8DHMDBPGQDKagAELTdJ3WCFixbZUqDFADBFIQA +F9BAFtwDGjyDMEiEMRCDCGRLQSjWyISAwYjBmvzAAAwOCKjLD6hEopCYwYDAU2AN9SDAIBHIEqCA +B4SCOWgHCOQCLCDlKnZCJ9QB5SHMCbaJoXURSbyAXZ0TClzlD/AioiSK3zSKMIYlKIylLNTCcULA +WiISGK3nB5QHBDwAuTCh04RAA5xAis0Zs3iKB8yJGEQAMPEDPijB1qnCC5gCCNjBLijLSaHCLNQB +GLAMILLIFJTAG+jCO2BDPNyBG1DDFJACPcCBHVDmKrDUiuyCC6BoPEybNNSBG7iDOmBDHEDAKLD/ +FCCygtKp24i2gha0wAuQABdUQRKUgxwUQzFFRy8owHEUBAYsYBKcQAioyzkJAZ/UBd8EgRjsTXaY +wKj8WRYsgWIV3jNdT1fxBqolgQkoQQm8KQgIQS3EQi7kAnd+Aivi4CCIJ6Fl3qEhWnrOSxhJClPY +kxspGMkJIzHWJyzIgizEwjgEgQCQ035ajgcIKG5oDfUogAC8B0WBADQBwBGFjApgTgMECZpNwS7c +SgzYwQgEWxHs0JehQkROgYgOhimEQBW8wjtwQz3mARdQwxCQgkL+Tk7hqDK8gzuQghNIgydQwTxg +AztYAwyQlEmdwhHUFiDuwkeNQAnQgR2IQS3s/8EziKQwRMMaBMBJGsQCiFUSpAShZUEQtIcQeIAQ +lM/+gEFTBIENpIQJdI+1JAeaLgEWEMW+AoAJ/EAd/CksCCoohAKhLiUOfkXlZQEX/ZUMHlqkZdw5 +FUJ+WNpg6RMOmhzKGWOmygIu3EISaMAzogCF9FsAgIAG6KepKkBo+UMnDEBdGG0EmENqhcIWfFsr +RNILNN0QSB1EboGwDgaLOEEK3IE1tEM2ZMM8wEGzqkAapoIU0GhiyNAQLMM7vMMyAAHVSUE8fO07 +6IIE7MopOAYrRMH1SR3vfBQJxIAneAIJvGs0/AJ0QEMTmCRCRBNrYAEJhMA9nVp7JAFNiAFTLP8B +vKzEvqyEbUjgtETgpAQBRwQBFvxAAopBEmAuLNRCoAoqx44gP3VCFpCAzLGETtIEqSzFEgDBv9iV +OZHAJgRBL4JYHHnlPtWBd4ZlKGinptbCLcAp7yWonDmhNZpAMzbAbVQIaA0ACECQCUCA0vLDIKzf +rdQBCYjZK5RrZYwoEDkBac6CCjwBL7zD14JtHGhhCSxDPOiCFPhj2rZCEbBtNrDDEZjCKDCB/XKD +OsTDE/DIr86CEngCbVVG1oLACDjBK5iCCTSDHhSDvBYDC6jXQbRG9wSA5r7aUogBCbxHEmhBwOLJ +B0CAvWjAA5wwkgCcc+6GayhJANCNCQhW5K7/KQp8hBh0wi1kbC6gHKES6qNpAOaChBAYhUgAgRBc +SMcATFXSSScsgaX1jRzBQQ4uLw9iqqYmsRg0gK25JKxV4G7E5ZMQwABozGtEUATsGj8kQSmYjh0o +gAeMGYdaxiqQaLrlSB2oACmwAzcsMjbMA7O6wgt46zQcga/WFhNYg/26Az+WghEosiK3wzSogA8N +cjIsQc8cJh3uC26ZQgncQh4crjAIAzKMgG5yaWtsjfdgFEucgC46wPjMBAShKtR4j6kqSWih2ltp +ARJgAEsggcEgAc7iKRbUgiwgpTWDAZuqLqqBwAkcJwig2nqWwBIIWl3oIl2dxIC85y+qHj8V/8Jh +ndwnmHEs2MImgAABkAA45AMuKMEQDgU1Dp4cG4ip4oYBCDNsmMADcAI/5IMS0M4mSUADmEIpoE5q +vkD0MZdIQQE2eO0iZ0M80AEdsLIiqwMRkNATFLBH3wEXrEIRLPIiq8M8SBkvrEgyDAEc/OGNDgEE +tEAduAIrkIAtuEEgEEMxEEMwzIQGbEtB4MYJC4BLfssleo8TQadzCi0ILAEIJEEIoGl5kAASvJoW +0AlztmcsbCosdIEKLEUynoAQvLC/VqzpekQStCcn1MLCvNo5Ew4SnN5gIS/guPM7n5wxamrNWq4C +kMA59EM+rJYsvAXHVKA1DoD7CB5sEChsRP/uEvCDORhBSd0KHZjAB4wZRaft1EXA2d7KKExAKiSr +S3ODO9wBHYzCC3jtO6iAJ/TQkZaCE7SDstbtF7CCEXgrOyjytZ6Az5gCKlUBaWKtSAlA1LpCK4DA +OGjBDLSBHwRCH0TA1ExJM0l1nFEJen1PaCkBCZAY9nrAD4DBi6GaTSKBV9skwMjCF9zkWY/HyDSF +FkAKqhSCLDQDOpADKGABEHwBLsDCEmTxC8AcX3flHOWgpaKcdo7lLYQCCEDAASABanAKhKRYXEA2 +bDjA1UDJABQfb7waBPhDM0xBZ29SC+xqKcQAZbgQAGghcisBEWBDay/ya8OBJxBBPGCDOxj/wWfk +FokIlxO4QzZwAztMwxagwo0rspJzQ22LYazY6kkhRggQAF/yzgiAwy1MwBnMABnIwAJwCwaUF3QO +hJrrhAMswEBIYG9E0zYRnwl06UhULAlwRGepAAioEd+kROd0QS58AhK0tXZASpzoLglwgi2YQzqU +wziQAzo0gxcIgRbEwnxpQAjAnLqs8xzpYOsVo6Ae4y14QTMSgBYo9l7sAz7opEmUgIfrBmVPywP8 +QAPcQixsAW25Ah48qR081WHOAh0AQNOuAiqIwGrnuGuPwhbcwRP8+DxYbW3p7Qsh+XCrAxeYghHk +uEfTQRGwTDJUAfxe+SkIQARwwSmpwBEJ/wELnMEZrECZD0cy+UaT2MQUaUAASAAUpEABUE7WCK0J +KDMn4MM9FAKkZXUQiMxxxnVOZoEXYEEQCJQswG4osCIYjIQS8A0YfMItpMOjj4Ojh4IMygIsYAGn +K5hXrB4ZQ/gx1sInCMEDsMQm+IPFaBQ+nECtm1Osw4b1Qg2bYsF30hbVjUAE2MEpvMBhJoMTQABw +zQIVEIGyL/I7mC0cSEG3xgMpFIE0XPkq8AIebIG11+MTjMIT4LhLZ8M3TAMKmIJBcoESLDcmeQIA +lIAdsIIpzF0oSGgKyAAbwPu97ubTIJ5LTpMNK0ATBEMfmEEZyAALIBPlQAA5SOg5nEM+1P8CEjgo +pFAUXSRsEojsINxnNSdlx3ZCyXkBR7DJEnjlxXp8OZQDJ2gBELwzFhwvo8BBHTgYJ8RsoMbCJ6DA +5KRkFjCPYp8GCdS650C2ZcPGrD8JdIJACYCAiPDKrXiCBIgAHrAC0jfGzADBFuzCLrwAKWCmstet +FHDBFnSr24qyr5KIjtBB2GtyHUzBRqsDlIPttPHC8317ZbiQKnABAEAdQLA6Ja1IJ378zl04w2JB +CA0PNTgQMJFiRYsTEyiAuJGjhgJ7oLUZc2ZMEzNm2LCRcaBBs334zqHDZw6JkBEmfiwBocEDBCyy +ao2zFUuWLFi5QIX6xIlTp02DBnnxkgX/S5IkYMBgyfKJHLgk5LR86vQJC9YuXqBu6sSUU6hQuXLJ +AhUBQAgSIR5MXJILXz4TBH6QMAHiYmHDhy8CQFyRwAAPNj6MYKVK1alWeIxskZKsRClXrlatQuXK +BZct0ko54baa9Wp27JZJmQInHjd26oCYcjV6d+VkXETFy8Yu27w7W55ga80N2zw3QpLtogMk2SnK +rDypOgJgSilWr5I58cKvH79PF1Is0LDhoQcHAzIuxqix40YPHgas8TVmRpkVE2Zow49hnoEmmDVU +0KKcfc5pEB0kgggBCy2yQMKDH0wAp5laOIyFqKPcCoWpTgaBw4suUARDizCU6OKTQbLg/wSUL7Co +BZ1PsgDjrEEKKaSTtT5xCxRYYLlFCQBMSFJJEBoAwAMQAEuSBMQCkM/KKytqYLAldqHMMjuIIAII +aUrwpBVVQhOtBFJOg4O25VrDRgoj7nAnG27ceQKOZFDps89VkqGCDuG4ycYdUpyQApvb2FmNnjdE +AMEU6aijTJVW3GjlhQGmMEWg8LQgrzwuAniAI4muDKC+jjxQAApoAkFDBRFS4MOXYogZBphfhomG +mDZU+GSff875J50ghAChKi0CIyGLdG6xpUOikFLqE7GciuqLLrLoFosYltiqky4GaQadZkLJ4gup +evyRk2tDASUXWIAywQMl8bXBXgEGiP+AhCkNa1KJfJJQDMvEDp4oBBCm6LKyVsAcYYRkXKBjFjRX +ccWUD0Rxgpoj7qgNTtekIIIU4Qy9w4lkLEXzlPBCvpObd5YhQgrXsHkHnidSIIUILqjhAohZrFOF +FS5ccQ8O0VRJpgosyDuoHxMC8GAjVOXLaFX7HhBhGGGIeQaZYJ4Zxuyzzf5FGGiAgSIJfP4xB59c +9BUjiySyIEHfWsCx5ZZbavGQyFysZWqTQrTlNgsttMBCCBOweDcJrtKVapBNNmEKXlCGlCUWWB4Q +zATRRyedySoDRjKfffbpwuCEDyPg9cIa0ACOVk7pkxc8XCBChFOmqONMylwp5YMnqFP/QZl2thl5 +ZiPESMVOdmgGopU0Q3t5CE/cWW04bMR4olFs3JnmCRVgmIaUF6Sh44XqKDNlCk8ICAEPVlB5mYsk +oj6InAE40oAEWKl2W9tIAfqANgUuMG3DgAYbxPAPfKAjH1kITBKwUBUSgCAJ5vAb4HAhOFgk5VpN +eYoXtgUGb2XQcSBAQifuAoqztGst74rXvDx3Cy9oCV89xBeTZjeRBgxgAOToR1/MAzuKBFEATJyI +ASYiu9udQjTYgcH56sAFLkjDFMTDgwp2RooPLGMe7xiZoZ7wAV3YaTXv2B72VvEyF0iPNYZSQRG6 +Nw9SROADcVhGNrBxAlrU4QCUyZgn/7gwhQeMwBOTQcUs6MC/g0wSBQ7YyAIGYJgBIIBfBrQPA17F +QFGiDRjQWAMR8nGOe6BDDGIgwbIeRwJcCMVvtQihUUZorbVgTipdUCFVWIiFIKDABELIRRdoqLkb +0stDzUBCBHwYzSUNAAADIIAQAfAJfvQlH/jgRxacqET5yE54luKFJ1xABSgoAQ9FgN+l6nCCeWiB +CxEQARWW4Y7usUYd2IhHHBygjHc0qjimuVjLXPGCaZjRe/NQARHqoQ5SCUADAHCDPo0AB08A4H6W +qcMURjCAF2AMFRBTwiQniYQBOEQDGJAPAD25kQbsIRqiFAYyyjZKaKwADPugYChOQP8CJLBQDEF4 +li2QCrjAGYVzbhnRJnrpy8Vl0CpWUQIScuEFtQBJSJ3zXC1sEYJ7/UtJgiFdD0uggSYOoElZOKI+ +8tHNffwgnIWBogKqqcQD6IYyqNCFJ1QQBzekQBQxmAxlWkEHFdCDCidwAxwC0AAp6OId3cOGOtTh +jlQAYBnvkNk7lKEEoqEJTaYYwTRaUxwnPIEWIjCABQ6whIJxgR6kMAIeBrAFos2CCxAQAQSO4ApV +oOJSdkACSg8SCwBsJJOG4WRMIeIeGOR0gcIoBiDaAA1RAoMYvvgAOFKJDySMjipJOMEmkHCLcfQN +hB7CZeFMKJUUTrUqSVBCEZQAi0H/uKstbpkXLDwUi1vAgofSNPDo6AIA/sE1rt78RF0RA+GLXLMx +D2jZKVzhCRG84Q1BSAIQRnHYxJ6AHp4IABdGEAQTDIACREhFO9yhjmy8YxoR6Oyi2viEUljqFNgR +w2uAjA162MEBBqDAkSkQgTpgQQHUYEcQpvCCIszCFMn4gQQ8MYIpnKlPGV4CcvmRDwhggCdYs0iV +CADdjQhgD9RV4E2bsAIZ+AIZCgTGMJBxjTUEwR/nyMcn/oWEH4DABFqQCjpswV5b1gKX1VqKU6B6 +Iqmy0KpIgAUcahgkec3LQxwaxycGsKT79OQ+ICA0vs4KAgAIAR39iCtczSHOJsKu/0kjGB7xSkGA +LbzBDSMAAR0cltgU0GMZ1FSCFypwZANEgAmkgDE31HECgbKmOVvQAvxeRgcizON7lSWFBQaQbCRT +IAmDAAEp6LEFFDhhpJYhAhruIIJM9bUVowACmPmRhASI1TAJSICaN4KBFIzya8iYgTVIcYE+IINX +w/hFMZDRBz0gIwXgvcc/ggACMZggCFcNRRJuQY711vKWRyHho084X2DWNwmyyBy84kWkTtdCvV4Q +gAoGU5FMElEACmgABEBwViUpQACfcDU++hEECdt11gmTnQukQUXKzMITYmBCHN5ghAhMgRfWcYVi +3cEOEYSgE0U4QLIrUAEDaMAIpP9Qh3ME+hprrDEVKpgFcfX3gXcop3xPsAAIxo1kWtwiCG6oLQDu +UART8KIUQMD6CETBZVUUTwjlQa4sqoYBog8wAZxEQABUBfANtEoNbn4zMprAjG7o4gJygIYwhPEM +QMggBSIAhhwK0Y9z7IMTQU+CBkwQi8n9IB3l6BvJP+RosWyiDoP4QgpVSOmWv1xIOIwFLv52i3Qk +QQEbJwxiMvnzEPTQBh5QsDdhsXRZF0Z2QzgoYvHQhSe4AesDAMFBpaOCeLAjBUvYhBIMQNySDQWC +AAKEIBVIobPYYR50YY3YQQXs4ExeRgpOYI0ORQQAYAlKwMjG7QDGgR9iYQviARv/FIAUYMATkkHb +3iAOUmAUJG8VTOEF9gHMxGzMLMLzqgTgIOIBeoHg8AwKmKEauoEZRMAMkAEZ2EAF3sAbYAAQgIEI +JGgmVAAEfiAJlsAcOKEL0MscNKSWAudzQEQpDKdHTgQr6MsqYsGG/EvmOASsbMEckkAA9AVgYmci +iIiPfMhJxsEE1K8wUMcO80I+DIA7WMZSIMYLRAEKOMwIAiDEUiEFVcAdsOEDtOAesoADxy0LlsAD +SEAFROEdUiEORIGy5gEOoO4UTkEZgqAORgEbtGACkKAClCAEAq8CDsAl8OET3cHFiMAOpOEJiOAO +0kAFrAMVQqMyfsAfkKs8sEAA/yDAIgZIAAKAPgCOAWDAB29KBpjhGqrhGq4BBqBABqDAGrzhGlbA +D6CBCG5hH+5hH7IABTwgCbzAXMqtC+rgXJKqQ4xC+ZqCDKFvhZIgDatP5q4vWpAKDgVASRbjmgQA +isLvXvCFLtbvdZqkRCRMdr6gOlZhuA4RHRSRCuJgBJ6gOvJvHibxA6MAE5UNLVRAVj5ABSTgCeLg +TtphGj5gx05hFlRgGeJAAVrSDSBAFlDgyJJtBCjgFvvhH1KBHdwBn7SgDpLhBKiA11ygT4ZrI1lh +CPJhGfkhFwLAzCgC9HRQA0JgAeTAB4eBGGCAGbyhGtzSG9RgFLqhGpzBG1hAD/8eaBP64R7yoRxM +oAQ8YBPKoRzS4QU6AQtigRyaIR/dy+TE0IQKAYV8KfoyqBbaYtM8JxY4JFqagR4VQF9MIGGIjq1O +LUnMz64kjIgYEq9kwR8aYCEBgA5YZiMvBQ9CxQjigAqoII0EohXqwAQkMQTMgR+QQCUpYAC44B6Q +IA6YEw7c4ANSwYxUazMq4wSmQR08gcPuIAnyYQBpIAZiAAUqwBYYZFHe4RW2YQvoYBRGYAWpAAjw +J02MJgjwjR/I4TAGIPQArlUAAS2PQRvdMkC9wRuOwS2vQQT6ABr0wK3gph/gIARQIAvOoRnKgRyC +gBOyoBzGYTFriShkAS5I6Kn/JNMMGadGPoFz4sJzcIHmbKEzx8EcvkAOk0SJRrMBxs8EbCAEAOAP +ew4A7iJ2gugAziHf6goBZIcOhoe4LoN/sOAJ0OAJ3uADkNQV7OAE3uEdVAAc+AEA024EUKAFDCAG ++mEZlkEOSMEfROEJukcdqIcXSoAVWGEUVIBN3eEVpmEa6gAfKqAFaKBPWyAKjuAc/EEfzKhR4mEL +6iALiOANnHRoioZ4gMCbaPAZDUM/YwoDPGCRRgn2Co4IgjBAQdVAWaAPiuEXjKAv8KEvpgAFXqAW +0qEZyOEWsOATvEAxF1OpcKEocklEIDMqJnOqauET4IKZVjRawGEcNNQcYCHU//4yNGXNmjhRDFQt +AO4qACJAH2qhrqoEiproBw4iWxcyxEjLMjwBam4hBeLNDY7ABbrEE1KAepYhHfgBCwagT/s0BvoU +H/ShUayhUWKM2nZxCqTBDoxAOMzzstAhBqJgYReWBubAH/LBHhqlUOLBDbhABdwACtBgC4Tg/YiL +FYpgSPGNBABgARDgZDlJGqdxPxMADUxvgYChGGQAEEI1QKGhG1cAEIphGJ6gHfUhVfNhCbAACcZB +5MyhXMfiVReTQ0Loc/ixE+qg+VIk+m7hLYiVRRMNWcvBHMYhdEbHBNRKnBiyATygBJqkuQLgFvoB +HSj1IvJzbAGAE0Tl+woDAf/YCnsQyxNOah8C6wkmIA5EwEzYUznYwR76oQ5QYA6iwE+jYAnugR6c +hzXcQRQ8YBm4QAoIhTXowRwUlmEXlxP6wR5Syx3uAAhcwBTfIEy6rq+MJmTrU9VGs7kmwlKhSwAS +iOCEwYFUIBVqFlTNUWdRDxz2Aa7+QR/OIXKQgBzKwVzAAIVsYXmRalqYyi3EghN4SdKwQgtuoXM8 +BPuMdRyKlhzM4R6E4AHKStZOliEJgAACQDGyqR/0QR9O0yJkpyLwqhyihhOYyAAcMgLgyGg8QZLA +gAj2wAVUQAVKIBlMCxvaIRuywR9woQXmgAYaNgpi4B/gAcg0eHxewx1E4Av/uCAORIaf8EEIokBx +JxgI0mEeWoM43METFOAJgJEKvkALdIGKRKOkjEBkwUzpFEABNqkilgvgHuADsJFAZAAGetdAuxEG +1uAIm0Bt4Uof/sEfbCEJ4GAQuLYcbOHYwEBpvVBwqoVXs0UypcoWhqR7WfRYw1d88SEUGsAGRGf9 +CgMA4nab9qEcZPeJBKB+KaJfJBUhwM8CTAFvseOk+AEclPANVKCJ7KC1pqEd1KEbuIEegMBhF1Zx +j2BfN/g1WvhQPIAIRmGEV0Md6GETLKAFVJkGTEESW7g57gAA0CAFnoAKyCEJBCI0lLQIyKE+XUcB +BoiTBkgBVha6QkABpmuU/34BGY5hBaBgiX2XGSagF6DhDGLB1bqJivdBFjDoE9ChE7JAvjrBg27V +lpIvKUQE0hIHK2whF9TYIME3fM3BHNDhHEjgA/SlBOhw/a7JbgGAPL2pYAqD57BJCJBrCZyoMUAA +f/xk8vRWaoIADeIADkTAA6BzJxlqZpZBlfm0BUYAD1j4jF4jG/ppFwOgs+BkxlKBDzzhFNZhn1iD +oEgXAohABYxAFs5BtHB4I2fBCHoZ3x4MmCcCAYKZdg3IAwLgDLSLgW6qF/4WmkO1GwbrGtjAIOL3 +qu+hHziBRuaFHAZhVmtEQ8MYDB3tR3hpEFCkC5oBwHDBe20hnskhHdJhnv/NIR82AUmUxAPomCI4 +6X3zwRYMQ3aaiwCaRJvKozxkwYnw6gWIZiM3st6O6yBAgQjk4A2gAAmYAEJQmjXe4RtKAQ+q4A50 +AU46WYNBSwIWCk4wyx3kQR5IGU7aYRlUIALi4An4YRyMoLFFQ3u0FN9uIZzyE+DYrKYYqBj0YALe +AKpr1htS4A/2oAv4gcHiFx/aEZmQwAs4AQxuIQlgoVbBQdFKDimCREQlTa1loa3/JmvleZ7rWZVA +AALkeEZljSGhSHa0oB/6AQkA4DX5+pqYiIcPIh/0+iIWuxAtxRVGYQiiZh9OYAuEkQSmAQQcYLO9 +B09a+7VHZqSHY8ZGAbX/IvfDW3gEnlRub8EIzkQ+k6EIbKE+z4GIgLgi/BngDKAPiFuBjOEPGlEe +lBtUj8EbRqEM+gC68wFi8+Fn9dUclGAQtCAUYuEccGQTckEx8/GWQLSEzNpXu2AczhtrwZcc4pq9 +G0RQccFHk0Sf9xqbxIAfQEEx9rhHZbdJ6NPyogYL9vuJOOkAAMAFDNxSSoE+ReUWPgAK4MADlEEX +AICyZCYbvkFmQNw1XLjRId02HBi0XOANxGBIS7wQ++RliuAW6rMfVK3nBCAaZxfgIqA/mVoY1kAG +quBTd9xA3w0MwiyuaB1u/poqvLkZ0qGGs4ALyzm80RnlIhOFxkEz4bnL/+eanhvkHu5BH8yhAQhA +SkbnzPHKBNLBMKDodQgbAGTBt5mo/U5clykPudIBCWAAAragHu6AFCL9w9+BFNi93UH87eAgTDbh +IIxkNrOHIGqhPvnBh1M2GonZqFkFAyzAF0TJGByIGOQgBZ751bsx4ZLA1faB1os8VfcBFAYhCGDh +VdHBBnKBa2mpvYqCc65F2FGoHLAPqZoBWb18rtGhnv9h5vOBHDyAACAA50xzr9kX6IB7dgbgAbYS +3/jQIvBqy0iry0rBW8HsHLJABeTdedQBTxIOBlZAFKKetJkjHs4HCGaQH2rBCDSSNmcBCPq9Pr0A +AF68IhJALC/VAnxQbf+eIRjMQATQYMeP4Ri6AQZOoB8q3h8qvsGoOx9cBAjMZSnOQUNHTlqadh8f +sx+lQuVZ3uW/XNmLxdbPgRNXCl/Wj1vXqjHqmK2wSZKAWu0pwgDwagt2oWiMUWNewN9PIOudZ56e +QBQANw5kfzleQwQs4APLoxaOQLfzRxqAIBbwDbHVXpiHWgA+j6I8qWvQ8myewRdkYAR6Fxq8oRuy +/xreQRfSHPADX/D74hZi5AtqoYO8XPGn/JxLyISyuBzcEHwrVK4t/+Lgxh/IAQKm3azke/3ys+kA +QoDAgQAGDBQAoBa/hQwZ4osggAABAQgUAKCTTJVGja5MgUDXcGE/fln/4HA7iTKlypUn1XF7p+sJ +mjhP4rC8eTObO10hzPEbya8WkIyoVq06JW2IwpAM0wU4eBBBAAQLNFi9ilUDBhHDunr9CvaXMWhQ +qHSrhvbYNVIy0TyBwWIFCHz99uWzmy8vvr37BoUCEyQdOnPkCo8DZ8tWrVq4ZMmCBSpUqE+cOnUa +NIjcLXDgxo0rnM4cOnTnzt3bi2+fLQEmTGhg3bo11Nm0ZxekTWAAAKgKzjFtSAKAAgQGECAAwGWX +KqNFiyro8pvfrSc4q7N8N43Zq2XWu7PDNo/Kl58MbxWZtRHVqWRAlv7e56G2gAADEGS9j+EC2P1h +n62B0c0xaF3DDAtm/7DxhhwK/nFBJ/zchVde+qB2DxIm3IJPaOmkU45nid3CWCyP5RIZZZVtsolm +nn1GzobmlFYaanThMoAYJnjwAAStkWACCPL9eBAAu9F2m0ANACBEdCKBMMBEFBFw0S7NbcSKBw3k +81s/SnTHZZdenuTOCbaEdMsRrSy3inpJxaIkP2IMCZVUBlV1n1U4csVfnsM8owcM3qTVDRF9eFMM +MtBAgwwyaRCxT6ON5gXphPmMg+Fo5mxIjmeI2QIiLiLCkstkJqJIji2eldNiaKSZJqM+/YBS440k +eOBBbGL4COSPcNomgAECWRRKm/ygsCuUdPCCiiuoLKeKKyAAcAtTI/+BYdKX1l7LTTbv8AICU+Zl +hCZSQ7CpZBe1SfWUAHXauQCeeoIlDDF/pADoHjJA88svYO0BRbD+2BXhhPj8k48+ow12aTorjsNp +iI+BEtlkJ5pjKqrkmIMxjOegVjA/mwjwwo0f/AACCCaQwGN8uda260C5AeCkkb61aYsHTQ4UgB2t +nIJKUaqgMosIDWgR3TgqYIs0l+xkM88WWTDVDBDKoamKNO1FN1IuwiUQ53G/rutBACO8u58xwxCj +AjPXVLPMCr4UAxYww+jRywt0+fNvhHrhc84/51iKaabNfBgiLKCKykll5nhWGMajlfYPavrkRZIA +P9zogQ0moMBjbBj/rAx6rwMY9CsASAi7EBbC+SrAAJ7s3DNzs4xA68xMJSFK0rqzxA473LSjgk8h +NTPEmRqpqVSb59TGdQBTCeCABxh4gNUDurFBdtzPQLHHNd1AIcczYJk9TC/FsPH0v5Cuv9c/fv+N +sGGHNVzLpyRKnPjiLDpOmvt76TM5LHVBATYCgQZMcIISnIxHCoRI6ASiG5Yd5EiyQN0nWmaAUbji +Z0U5ijRK4AELcCI65CDB7k6YEp2MYgm/KUfxjqeKU8wCCORSEgkCoADaSGUgdVJAAFYggTdkzyu/ +8E8aukEKGSDjK8IYRhOHEYhAFCMF5KjLPvwBob0NDEbxy5SHGuap/xGVKH8sutjj7nEafAAQUvwA +gwJsoEDYxAYCDzBBHKEyOgCkazYD2CMeB6KAAUAAS23aBwRmo4BSKMsVq/iZDF+gARKUQElaqBYK +reWSFLrDA7D4TTqAYDxHSuMFuFDSSLLQsoGgCwHQk95VrBeAJjShAHoY4jCMUYw+oKEbKfBDnuL1 +C2AgQw7j+ZcxIaXGgb0PYemQH2JAVD8xSoYy6ACN40pzj8itkY1gIMDlTNC62CCwAQ1AgQ1IUJs8 +CikiEiEdVF4mEANYRAmoQwcfG9CzfOZzFkGIwA8iMKbf4OMHl9Td0sJjguic44UcRAophbUJ4ejQ +eTkUAAQ2YCcHBP9AAmYowwwmUEtb/uIJTjCD+LJHDCOcw4p6S+bA7gE/jBXGi4lZTCzsN01OVPNi +Getb5PCxPsqZ65sNCEAExIlOAdQqBD/SjZCa5M6DGIA+ToJSJ4WFSjwewBSM7CAr1lOEANhAA0SL +jheIQI+CXus77khFBGwXkn+cp1k9U0UyXlDD6NgilefCgFUg4EMWnGEMZphBAdIwRGEgQwYrECk0 +zgCdu2AxYO2DB0wRRpgOzQ+aIpLF/T7xCYyJtjTw8J8a84I3f/DjCwr4AcpyCIBaoexkrXNgrpzK +1/mQDgFQKoewlAABEtxMAIGMgCmMwhz1JCMJAJCkBwj5GxXcYR7/au0SO9ShDna0wwJ5DUk+hsCK +n4n3g6Us5CFp07w9OmADHnBAHy8w2DKYoQkTgIIt79sVYgBDBSu9ovokJDD3wVS0hdFsM24Bopsa +7rPn4Omq3KdN1OINS14QgOZIkEMDAMBksWFqrnzFunXS5ikSyY0HsMYPfAghczYbiAEGIAF96jMZ +UwAABGzggGhFBx8Y0AV1q2udpbVjHidwkJL0AYTw1hUpJXBPlviBhNxSxHkDccB5ndcEM4yBsGe4 +wAX6gN8hAuOx0HlUUAP8j3tYijDkMHDDbirNUJwjNC+C3D8mFMDJqpYTA/hmRVvXOQWq7IEIKZZu +nGSRJEhrIbUo/xkJGmAQ0uUGAsg1ikbWswUAPOAHEIDOk8dxAu4A+Sa9ywY73qECerYpH0GA4bJm +193feAGHAuEaArhGXCojcqMz6Kh8zwDEM/gizHoixjCAEYwUmIOldykYmu8Bj7/J1DCDaxgt7BeZ +OZ/Rf9tMLd74AYsA7MhJvhoAjzo36AcKCdGHLl2sH5QFD8wqqgMZgAYqjdz1UCG2QkjAeNp0Cwmk +YtS8w8Y77DGCfxdyCVxdVs+SMYJ3NwUJAWiZrXHtx4MkwHkGOkMZfs0CApiB2PwxtjCgsYZB9EN9 +V4xU+5aJDky5uVOdJREo/iGa/kXuznmGkGprIW7avlOBsbEtbv9mQ4AEFMRXgXTnAN7KFFvciNP0 +Lk7rLIDvRmIaAD/wQASENRJwCPzHXMoGNrJhraXFAxsX2ATqflKEV4j31SIo72+wELod0kYBG5/A +DMjw8SZcgAUXkAAahk1y/rjgH1ZMLYBfDr8NNbMc5ag2zT0LsX9sW5sBhJCj+AGOoJugAQfJTQN2 +JHT55LHiuyGArltHuiP9gCmfkDfVa5ObD2T9KLOYwgPi49u3n2MEd+CSS97hjuTPY/nWcQnTdBEB +I79dCYrkoF0/4OSGdAIAGghBUQk9m40HQARmMMMEYHCGM0igABOQwR4S7xXFNkHleTsz5KV9qTZr +Coz2ywXO7bz/F0GVD3mzD/yQDgLAQEZHXDaGVEAiERAkJOsGQTdjEQ4CFOkgBDpiAhBAbxTBTgCg +e1m3HlwAAD3xduTxDyZABe7AJcpAClTwBDD4BHBACthQHfTgCR6gYyeIBa9zPOvhAdnHECTwAFbh +ACvjQ7NxawKQAHwHABFQADIwWMB2AU2QAhMgAmsAf8NQDIAAAp7XKFgkIe0DU+eAMc2kf81wYDYV +CwumedH2U0AFKQDjKP2gDxGAAq3xALRhbifTGh62Mi9DOj4kaQAQfPyQCx4AAj9QAnsoH/JUghuk +EflGYyFwgiHRD0ngBPHQHcugC58IiqKwBU+QCtjlO9llcO3w/wQkABKXyA9a4AkbtCwyhH2/4Q8g +4Fca4IjgN1EB4HdsQAZnMAErwAZlIAODhz0k9wuL5SCOYn8vVRqDETieMTiXByp8g0buI4CO0ij9 +4I0eEAKtwYEuQ1yCJE6u8UAWMQCQuI6ThiXogARHJRu5khsjYDxpshxI4QR14IoMMRKD8AHLwIIp +oQ2+cxLIl3zJh5DssAzWYJDqoBPqQARP04+vGIsNJQ0eYHcN0Q+4aBW7yIuq9Cs7dAEyYAYwUABN +QAZmcAZNwAIwIAckhwx+EAKeN4BiCECQh1nO9CG4cG2Gg41xuD7cuA91MRIbqDmqd26d80DwpFsR +0VxBoQGZg/9AIKlDFDEAICCJybUeTuAFFUkenEArpJBWLAGR2IBd2IV22HBqp5hd8ZAKEECRFakF +eNAKyxJD0lCLvzF6V8FKpJNxD4RrtyaBALACHlcGwWiMM+BLYQYNMBAKjSeGznZ/0lhgX2SNfGNa +cjiAYFiUBZgEDRAy5/VO9tYaEaAjSQU6UNIk9DEQKIAFp3dhoQMl9rgsyKUmTuB2YNkPWNAJXeAB +T4AN8dA7xWmcBrkSEMkN8aAOTwACSiB9/ZgEdXCXjhQ0G9kQFxQCVhGSP8I1CpCEAlAAH0eeZdCS +MyAHf4BfyKAHSFAXeGNmBfNyl3WGl8kwCFY/IiIj/5MXRFn/lN5YlANkI+mGR+N2eigQiKIDM6OT +GyDwaA0QAiSAK4EIJScQSvkmDU4wQmCJD4PACYNQCEHgAdP1DsdposepLe5wByoAAoWQBZ5WkUng +BsZDd0K4JA4wld0ZOj4kAh7XUVtWfmMwA2XABoGQWM+gAmPCcjiZk2hUGgSmfwzDf7GgDz+1TQN4 +Rd3ojVsKCwSkAoAYEQLBW7HVGkwCfoc2OgLBI67Fi0dioaeAJkaBFErwCWDJD+cQCrDQBYXACVkQ +AGKQCmRHat/BDvOwDERABCqgBYWABWBQkSOBBVwgDXCqHrMAAja6EOEWAhugo0DCSky4cUNanh9H +Biu5ZY35/y4n1wTpE59juBdlyEzyc5+34ClUaqUB5J9aWpS2IFY9gnsDoAB2tIG8uBsVJxGdM6Hp +GAAlgB5o0jNIYQTBApbjUAvj0AWY0QlBMAISIJyCqhLqAB7Y4AYisAVEIAaFUAiDoGhg2QVbkAw8 +U6kjgKkLIQYKEAKf06lQkQC3toS5NgGIKV9jQAZ/AAy+8AeomqrR0AcnwI1ZNIZpRp8Jk4acQnP5 +wHmTg0XwqaVb2g/n4ADn1lR5OI+EVqwGcVQ88iwrwzq9omEjMAtwKqdqcgRX5YojcY2YgRmFoAKr +KAFxgA3u8A4rkXypkAJi8AZQQALpmq5J0o//WAWT6jMQN6uv/GALC0A9P9KvOjoVASADgEeeY0Bs +ZlMMIwAO/OCMyCQjlwU4baZZFFs/AjiHHCu33vgBJHBO8hFI4WgCNpCsoFOyFoVAeihluAEANuAE +GxGndjUudsoJstAPH4qzXiACbkAFEPABcaAL15VJ77AMSCACVCAHq4iuo0sCdvoJTjCpljYLTSYs +SGCv+YqEAQBf5KllbRBmxnByM+AgBBhUZKhmscq2UppgAQEAOw== diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/004-careful.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/004-careful.testie new file mode 100644 index 0000000..05dd1eb --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/004-careful.testie @@ -0,0 +1,538 @@ +%script +for i in 0 1 2 3; do + gifsicle -O$i x.gif > y.gif + gifdiff x.gif y.gif + gifsicle -O$i --careful x.gif > y.gif + gifdiff x.gif y.gif +done + +%file -e x.gif +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 +VquCPNrD/RAIACH5BAQAAP8ALAAAAABkAFAAAAf/gHgyBQWDhDJ/hAV/g4gyg4yFiYyRjYSMjzJp +koaUlouHj56TiY6Sh4mXiKCXipEhNZWFs5aNj7NporWFmpk1BTW3u7aNmJvDspnJnLSVlSWqp6mG +vGnTnJOrtLy7i8WEm4cF4c6Ons3andTilg3mwbXCMr/EwtjxmfbD8pm0+uj+8g3bJiNOq1OqIpFj +VWwVJVahPl364wmSsm/MvEkzZW5dKlTuAjIjRlBfOVy5/uTqx1GGnGoERxI82RGgIxLrbkW7lQyT +KUWK+uXSuHLjrV+oPl0zp7HWyaQFGrx0lE/kQHoxH9FzGXCewGDBItq8l9UqwRLCSjlkOonhxoPh +/woFyzZNIMtpeB1C3EnxYSRQkUjI8ckNV1mSHAs/ovMP0cqqAyNnTStZhh2ESpUCZmVt8zZv89qK +Bu3U21+gCU1D/Hsa1YKituBBnpyYpaQ0c5E9SvN4duWsFffJWNC3JkZqa30Wl6YRKV52oREHZd0J +lTjT6g6GqO17ZjHY47KmrF1Nl7FMxw4H7Dns8ual3oOSJadO5VBXxDC9362z7az98jUVjR2RaaJS +Y4qhR5k8dHBTyTy80JOML/xM6Bg/WbnnH2nLSDTIMaI05cl4/41jETUf1YWYX9ehhghFrHhQIDz+ +gFdPZTIwlh9vVFkzEGxe2QhcZCGNdRxNih3UUv9ndR2jXHaZleeRbdjJ54GP4jkJ5G4RVjhMWLFt +Q2ND3S3Go1CcmJSJB0CJVoqLdP2mWQGDwciLgB09qA1gtgiZnIBx4BiLcffog6AmgyqmZ5qycXSO +LXJA1iEjbIqYzqHZGChdOoV0dpGLreiXl5LZwFRdan8QmGZiY3oH4Y3bPNbgI4MNM9SKupGVj42V +aPjTOU5+cw52hNlqTSwUvagIfaKk4oxqqMKaVKoF4toVeX1+1cummgKDzqP36Hleq5aFuo4qOamY +omqZUhVpN8Hd+VGJlk6XznmdvCYTdq92hWVD3+ZTgxy/+PnqrfFVO9KD72YQl515mScOkyIm+2z/ +JC/huaou5pJqnF95UgUGHY8uw6Msfsq0j4TZ0rrtKIQ6am1tC4QYDbsUy8uuKzz7x2Oo7Ji4GlMZ +IdnQsH8ssJWg622q3mJG8sdxZDQVq+YjRcpMypPMJeRo0I7w1lqaPiJdX5VQDc3Ma+XYNQuNLHf4 +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 +05K4Cp7CsAqcYFchEAgAOw== diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/005-resize.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/005-resize.testie new file mode 100644 index 0000000..f902782 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/005-resize.testie @@ -0,0 +1,62 @@ +%script +gifsicle --resize 30x30 x.gif > y.gif + +%file -e x.gif +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 diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/006-unoptdisposal.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/006-unoptdisposal.testie new file mode 100644 index 0000000..3e3bd4b --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/006-unoptdisposal.testie @@ -0,0 +1,12 @@ +%script +for i in 0 1 2 3; do + gifsicle -O$i x.gif > y.gif + gifdiff x.gif y.gif +done + +%file -e x.gif +R0lGODlhKAAUAPEAAP/////TAP8AAAAAACH5BAEUAAAALAAAAAAoABQAAAIahI+py+0Po5y02ouz +3rz7D4biSJbmiabqihUAIfkEARQAAAAsAAAAACgAFAAAAiiEj6nL7Q+jnLTai7PevPu/BeIYgAiJ +mgaamiypvmMsl25Ny/m7t2YBACH5BAkUAAAALAAAAAAoABQAAAIohI+py+0Po5y02ouz3rz7vwni +KIAIiZoGmposqb5jLJduTcv5u7dmAQAh+QQBFAAAACwAAAAAKAAUAAACGoSPqcvtD6OctNqLs968 ++w+G4kiW5omm6ooVADs= diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/007-alltransp.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/007-alltransp.testie new file mode 100644 index 0000000..d34d97e --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/007-alltransp.testie @@ -0,0 +1,25 @@ +%script +gifsicle -I < x.gif +gifsicle x.gif | gifsicle -I +gifsicle --scale=2 x.gif | gifsicle -I + +%file -e x.gif +R0lGODlhAgACAHAAACH5BAEBAAAALAAAAAACAAIAAAIChFEAOw== + +%expect stdout +* 1 image + logical screen 2x2 + + image #0 2x2 transparent 0 + delay 0.01s +* 1 image + logical screen 2x2 + global color table [2] + background 0 + + image #0 2x2 transparent 0 + delay 0.01s +* 1 image + logical screen 4x4 + global color table [2] + background 0 + + image #0 4x4 transparent 0 + delay 0.01s diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/008-resizemix.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/008-resizemix.testie new file mode 100644 index 0000000..0bbb669 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/008-resizemix.testie @@ -0,0 +1,29 @@ +%script +gifsicle --resize=250x_ strip.gif > stripx.gif +gifsicle greenmini.gif stripx.gif > stripy.gif +gifsicle -UO2 --crop=0,0+10x7 stripy.gif '#1' > overlay.gif +gifsicle --crop=0,0+10x7 greenmini.gif > minibit.gif +gifdiff overlay.gif minibit.gif + +%file -e greenmini.gif +R0lGODdh+gAHAPAAAD3kxzzixSwAAAAA+gAHAAACP4SPqcvtD6OctL4AsN1V8w+G4kiW5omm6sq2 +7gvH8kzX9o1vXs73fhPYBYfDn/FITCqXjCIumGBmPrtc9dgoAAA7 + +%file -e strip.gif +R0lGODlhbgEKAPYAAAgEBQYIDQoJDgwLEwwMGhINFRkLFhIOGgsSFg0RHRUTHBkUGBoVHRsZHhQO +IhQUIxoWIhYYJhwaIxwcKyIcJCkZJSIdKxwhKycjJyQiLCsjLDclKyMjMyslMiwqMysrPDMrNCsz +Njk0Oy5IOjg3RkhESlZLVkxTVFhVW2VbZmhka3JqbGpodHd1e3l4hIeEiomIlZaUmaWbp6Kroail +q7elqam2rqmotrartrm0u8e2usrJzNra3eXc5Ofm6PX27fDv8gAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+ +KEVkaXRlZCB3aXRoIEx1bmFQaWM6IGh0dHA6Ly9sdW5hcGljLmNvbS8AIfkEAQAAQQAsAAAAAG4B +CgAAB/+AQYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKz +tLW2t7iOP0G7vYK7vMHCi8DAhMbExkCFQMi5z9DRtkA8Ojk42DU2NNjc2dg4Njg1NTfaODc32NfX +4eDi69jw8uDoNznp1zQ0NjL7MzRy5NjhQ5rBgwhN9ZihYgQGDRgoSMyQoUFEDBcxdHiIIcNDiBo6 +dAgJooOHDRo8hNTAkqWFDBU8XrhAwYKEmxIUUGBwoMEABSQwBBjQQEACDA1QvNjhLKHTp1Ad8Ygh +ggIGEFg9eMBacqRJEFpbilQpcsNIDRk0xIQZcScFCHDDIShgoKCuggMHENxFgCDBTwIBQrg4kcGC +hgEIGgAA0EDFjqiQI0sWBARGhwkePojswIEDSw4XPFBoUEIDAxAROAiQcKFBAwkTIEhgUADChAkS +HuiGEEFBbwUJ6iZoYBe4gr4KHhxwcPdABBgvVLxAgeFC9RARWxSczL17NBwkOEzgIJv1hZsRYCuQ +YCLGCxAGJMDlIGFBegcRbDKQP+Cm7AgA/hcBb74VZ+AAEQzg3HorsJDCdNhdgIAIITQgAg3eRRUI +ADs= diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/009-zerowidth.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/009-zerowidth.testie new file mode 100644 index 0000000..d84218a --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/009-zerowidth.testie @@ -0,0 +1,68 @@ +%script +gifsicle zerowidth-ok.gif | gifsicle -I + +%file -e zerowidth-ok.gif +R0lGODlhgAKQAZECAAAAAKqqqgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFDwADACwAAAAA +AAAAAQAC/oSPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gvH8kzX9o3n+s73/g8MCofEovGITCqX +zKbzCY1Kp9Sq9YrNarfcrvcLDovH5LL5jE6r1+y2+w2Py+f0uv2Oz+v3/L7/DxgoOEhYaHiImFgT +wNjIqAgZKenl6Dh5iZmpucnZ6fkJGio6SlpqeoqaqrrK2ur6ChsrO0tba3uLm6u7y9vr+wscLDxM +XGx8jJysvMzc7PwMHS09TV1tfY2drb3N3e39DR4uPk5ebn6Onq6+zt7u/g4fLz9PX29/j5+vv8/f +7/8PMKDAgQQLGjyIMKHChQwbOnwIMaLEiRQrWryIMaPG/o0cO3r8CDKkyJEkS5o8iTKlypUsW7p8 +CTOmzJk0a9q8iTOnzp08e/r8CTSo0KFEixo9ijSp0qVMmzp9CjWq1KlUq1q9ijWr1q1cu3r9Cjas +2LFky5o9izat2rVs27p9Czeu3Ll069q9izev3r18+/r9Cziw4MGECxs+jDix4sWMGzt+DDmy5MmU +K1u+jDmz5s2cO3v+DDq06NGkS5s+jTq16tWsW7t+DTu27Nm0a9u+jTu37t28e/v+DTy48OHEixs/ +jjy58uXMmzt/Dj269OnUq1u/jj279u3cu3v/Dj68+PHky5s/jz69+vXs27t/Dz++/Pn069u/jz+/ +/v38/vv7/w9ggAIOSGCBBh6IYIIKLshggw4+CGGEEk5IYYUWXohhhhpuyGGHHn4IYogijkhiiSae +iGKKKq7IYosuvghjjDLOSGONNt6IY4467shjjz7+CGSQQg5JZJFGHolkkkouyWSTTj4JZZRSTkll +lVZeiWWWWm7JZZdefglmmGKOSWaZZp6JZppqrslmm26+CWeccs5JZ5123olnnnruyWeffv4JaKCC +DkpooYYeimiiii7KaKOOPgpppJJOSmmlll6Kaaaabsppp55+Cmqooo5Kaqmmnopqqqquymqrrr4K +a6yyzkprrbbeimuuuu7Ka6++/gpssMIOS2yxxh6LFmyyyi7LbLPOPgtttNJOS2211mpZAAAAIfkE +BQ8AAwAsAAAOAAgAAgAAAgSEjwkFADs= + +%file -e zerowidth-bad.gif +R0lGODdhQgFCAfcAAAAAAFUAAIAAAKoAANUAAP8AAAArAFUrAIArAKorANUrAP8rAABVAFVVAIBV +AKpVANVVAP9VAACAAFWAAICAAKqAANWAAP+AAACqAFWqAICqAKqqANWqAP+qAADVAFXVAIDVAKrV +ANXVAP/VAAD/AFX/AID/AKr/ANX/AP//AAAAVVUAVYAAVaoAVdUAVf8AVQArVVUrVYArVaorVdUr +Vf8rVQBVVVVVVYBVVapVVdVVVf9VVQCAVVWAVYCAVaqAVdWAVf+AVQCqVVWqVYCqVaqqVdWqVf+q +VQDVVVXVVYDVVarVVdXVVf/VVQD/VVX/VYD/Var/VdX/Vf//VQAAgFUAgIAAgKoAgNUAgP8AgAAr +gFUrgIArgKorgNUrgP8rgABVgFVVgIBVgKpVgNVVgP9VgACAgFWAgICAgKqAgNWAgP+AgACqgFWq +gICqgKqqgNWqgP+qgADVgFXVgIDVgKrVgNXVgP/VgAD/gFX/gID/gKr/gNX/gP//gAAAqlUAqoAA +qqoAqtUAqv8AqgArqlUrqoArqqorqtUrqv8rqgBVqlVVqoBVqqpVqtVVqv9VqgCAqlWAqoCAqqqA +qtWAqv+AqgCqqlWqqoCqqqqqqtWqqv+qqgDVqlXVqoDVqqrVqtXVqv/VqgD/qlX/qoD/qqr/qtX/ +qv//qgAA1VUA1YAA1aoA1dUA1f8A1QAr1VUr1YAr1aor1dUr1f8r1QBV1VVV1YBV1apV1dVV1f9V +1QCA1VWA1YCA1aqA1dWA1f+A1QCq1VWq1YCq1aqq1dWq1f+q1QDV1VXV1YDV1arV1dXV1f/V1QD/ +1VX/1YD/1ar/1dX/1f//1QAA/1UA/4AA/6oA/9UA//8A/wAr/1Ur/4Ar/6or/9Ur//8r/wBV/1VV +/4BV/6pV/9VV//9V/wCA/1WA/4CA/6qA/9WA//+A/wCq/1Wq/4Cq/6qq/9Wq//+q/wDV/1XV/4DV +/6rV/9XV///V/wD//1X//4D//6r//9X//z8/P2tra5WVlcPDw////ywAAAAAAABCAQcI/wABCBxI +sKDBgwgTKlzIsKHDhxAjSpxIsaLFixgt/vuXsaPHjyBDihxJsuTFjQA4mlzJsqXLlzBjJkSpUqbN +mzhz6pS5sebOn0CDCh16sKdPokiTKl0K0ijTp1CjSkXodKrVq1iBVs3KtavXklu/ih1LNmLYsmjT +qj2rtq1brmzfyp3LNC7du3h/2s3Lt+/LvX4DCw4JeLDhwxQLI17MWKHixpAjP45MGfHkypgDX87M +Ge/mzqDffg5NGu3o0qi/nk7NGuvq1rCjvo5NW+ns2riF3s7NW+fu3sB59gxOXPbw4shtH0/OXPfy +5tB9P49OXTjK6thj/s7O3eH27uBnTv8PT77j9/Loz6Mnr349+PbuucOPj30+fer270PPr585//7I +/QcgcQIOCFyBBvKGYIK4LcggbQ4+CFuEErJGYYWoXYghaRpuCFqHHnIGYoiYjUgiZSaeCFmKKjLG +YouWjQfjezLOKF+NNtaHY4747cjjfj7+6F+QQgZIZJEEHonkgUouqWCTTjYIZZQQTknlhFZeaWGW +WmbIZZccfgnmh2KOKWKZZpaIZpoorsnmim6+6WKccsZ4XZ25vYinV3ruCRedfmoGaKB99UmoVYYe +KlWiikLFaKN1DQrpXI9OmlSllhKFaabO3clpZ5t+qpekoppGaqlkhYoqTqqualOrrmr/d2qsXcFK +q0u23spSrrqaxGuvJP0KrEjCDtvUrMYa52mygi7LbKHIPrtUsdKeFG21SFGLbWLXbtvpUd5S2m24 +ozpLrlvanuvduOqyym67r74Lr6zmzpuqvPbiim++u+7Lr6/+/htswAITS3DBx9aLsGsHL2xeww5j +lK7DEy9cMcIXF5yxwBv/2zG/H+cbsr0jz1syvCe3m7K6K5/bMrkvhxuztzNvWzO2N1ebs7Q786xw +xFNBDLRGPw/taNFGRwpu0osizfSlTj+tadRSd1o1olRfXa7WTS/NdbZZf+2u12JbXfa0YZ9Nr9rK +kc22dG6/PbbcU8dNt3V3m5331nvz/9033H/vJHTeg99dON2Hy53424uz3bjaj58dedmTi13515dz +nbnWm1/dedWfSx3606MzXXrSpxud+tCrA916xK9TnHbghM1O+0exW2z77Q/bzfvAvv9ucPDCJ1z8 +2sf/tXvyE+WO8fLMmwV99A85r/H01DdkPcfYZ7/Q9h537714xI/fvPjmGwQ+yOinT9D6IrfvvkDw +kyz//PWbfL/7+aO8f/r9U9n/zBdAlg1wfAV02QG9l0CYLTB7DZTZA6kXQZpNMHoVtNkFmZdBnG0w +eR3U2QePF0KflW9+6zohChlSwme1kFkvTFYMjTXDYdUQWDfsVQ51tcNb9ZBWP4xVEKBdNcRVFRFV +RyxVEkW1xE81kVNPzFQULTXFSVURUldsVBYVtcVDdZFQXwxUGP00xj2VEU9nrFMa5bTGN7WRTW9M +UxzNNMcx1RFMd+xSHrW0xyv1kUp/jFIgnTTIJRUSSYcsUiKFtMgfNZJHj8xRJG00yRlVEkaXbFEm +VaTCFbKQJkYJpShHScpSmvKUqEylKlfJyla68pWwjKUsZ0lLWgakADs= + +%stdout +* 2 images + logical screen 640x400 + global color table [2] + background 0 + loop forever + + image #0 640x256 transparent 2 + disposal asis delay 0.15s + + image #1 8x2 at 0,14 transparent 2 + disposal asis delay 0.15s diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/010-warnings.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/010-warnings.testie new file mode 100644 index 0000000..3124ef1 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/010-warnings.testie @@ -0,0 +1,20 @@ +%script +export LANG=C +gifsicle blank.gif -o /dev/null 2>&1 && echo blank worked +gifsicle -w blank.gif -o /dev/null 2>&1 && echo blank worked +gifsicle blank-garbage.gif -o /dev/null 2>&1 && echo blank-garbage worked +gifsicle -w blank-garbage.gif -o /dev/null 2>&1 && echo blank-garbage worked + +%file -e blank.gif +R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw== + +%file -e blank-garbage.gif +R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAO2Rhc2prZm5zZGtqYW5m +a2pkbnNha2YK + +%expect stdout +blank worked +blank worked +gifsicle:blank-garbage.gif: warning: trailing garbage after GIF ignored +blank-garbage worked +blank-garbage worked diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/011-resizemix.testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/011-resizemix.testie new file mode 100644 index 0000000..b5688b5 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/011-resizemix.testie @@ -0,0 +1,8 @@ +%script +gifsicle -O2 --careful --resize-method=mix --resize=200x20 in.gif | gifdiff - want.gif + +%file -e in.gif +R0lGODdhlgAPAPMOADhbmB89fxRBgSRCfilGgi1KhzFNiipQjDBUkDBWjDRXlC5dmDZckz1imSA9eAAAACwAAAAAlgAPAAAE/5DJSStLMmmtukcKIo7icSBmaqxsaxTwC8PEXBB4Tgz8kPe8gHBILBqPSIdyiRQul5aoZcPxWEmklPbg6hZeLNtMR8YBfbtBc802PpXrt6MhrU8S1nwHWzqZUFtdgitiYmWHZ2pti25vcW8NkZJ0dhgXenp8fFtcgl+fhWM0NYc/Z4yoQ3KPTwsLknUAABIKsphXmpudLLthYKFjpKVppolAqXJPjHKudhKytLcfIbl+1i69XsA3wcPFxkGoA8lwi8yuC9ATsuzt0nt7mlqAKoNgMsDe+sTgPUZnyZC9cdWOgq2Dtt6ByAVohAl7hH6FqjFKWBkfaHTwSHNGgL8iAKWVjCu3bCDBdigBvPvAcEufhwYefvEEats+RP2MJVkyLlzJVulSolxJjSEKeoEgisGXz1tGMjk7IhnpCJUcoUKlLTSKhVO2iExjbKt481tUAQJSqS2CNeFKrvI4eSI0dmJZqFGPrd3bVqUHV5jgdvU6U2ldQ3fN5tzLt62VBVoFv0wKke5hsjecgvOYKC1jtX07oIsseTJMF4XDXC6TuVTezp/VRgAAOw== + +%file -e want.gif +R0lGODdhyAAUAPMOAD1imR89fxRBgSRCfilGgi1KhzFNiipQjDBUkDBWjDRXlC5dmDZckyA9eDhbmAAAACwAAAAAyAAUAAAE/5DJSatNSeKNlf8eEiJkWR4ogq6o4b5wXMyFQd/EXRB87w9A4C8YDBiPyKRyyWweG9Ao1PmURi3YLIWzAXlHJhNr3IqZX7UZWofzuYdEIaFIrdup1qg9D9X6LQgcJF9eYWFkY2eKNWZsNG+QPHEDPXR3l5gBfA17fAB/oFsihF+GJykrKoiKrGuOOpGxk0CZtUybnXkAu7y8oRoTCaOkhabGiAetNi5pr7CPOTuxlbO21ki4dZsLC72ffw4OEyLhw8RgxqYrZ8lnjM3O0DPT1HCzk9eam1a129y84sAFZOAh3Lli6dSVidGuETMb8eRJoyeH2j181vZJseVvwS8J4f8mHCyE7piqAyfZKWMWcV4bem8uVrM2QOOUfny46Qw3ECTPnz9HfhiW8CQqMishsmwJsymcOTKJMJmk8VpHoBV4KtAaVKiCUUVPnmq4shlEZ9Hm9ZgYidKctkKiSl1CdZ/VnNywUuBqcGtfoWFJpBqb9OGyiGnZsoXk1m3MuHJp0Y0jBUiUAXfz6FwAtHNXr1/DIhq7sAwjVmrOInZKMfI9PJVrWsKpOa/n26BDB1ZhdFVhNktXN3UM1zURAVRq2s3I57bzv8TA7j6EjGwrR8HjsW5tPIgA5PnCJ3mOe6SI6Qp9u3vXUvh2xt29gxdPnzx0r+iPIUPNvj3a97LENxtaffnY58AX3JyTH3XVnZaUf/8B6IaAkhEYnoEImregYNVZ9yCEbEhoEYXzWXgNhiAkeNCGpCFVGAwgwuKDWjBReJyJF9r3wWYassihi+stEmMkNE5j4404XhMBADs= diff --git a/packages/optimizt-gifsicle/upstream/gifsicle/test/testie b/packages/optimizt-gifsicle/upstream/gifsicle/test/testie new file mode 100755 index 0000000..a5f7f33 --- /dev/null +++ b/packages/optimizt-gifsicle/upstream/gifsicle/test/testie @@ -0,0 +1,1937 @@ +#! /usr/bin/perl -w +use Symbol 'qualify_to_ref'; +use IO::Handle; +use Errno; +use POSIX ":sys_wait_h"; +use MIME::Base64; +use Time::HiRes qw(gettimeofday); +no locale; +use bytes; +require 5.006; + +($preserve_temporaries, $expand_mode, $verbose) = (0, 0, 0); +$running_pid = 0; +%require_error_commands = (); +$quiet_ebadf = 0; + +## utilities + +sub index2 ($$;$) { + my($result) = (defined($_[2]) ? index($_[0], $_[1], $_[2]) : index($_[0], $_[1])); + $result = length $_[0] if $result < 0; + $result; +} + +sub shquote ($) { + my($t) = @_; + $t =~ s/\'/\'\"\'\"\'/g; + "'$t'"; +} + +sub min (@) { + my($m) = pop @_; + foreach my $mm (@_) { + $m = $mm if $mm < $m; + } + $m; +} + + +## testie ipc + +sub tipc_write ($$;$$) { + my($fh, $command, $arg, $noflush) = @_; + die "!" if $command !~ /\A[A-Z]\z/; + $arg = "" if !defined($arg); + # print STDERR "$$ write $command $arg\n"; + print $fh $command, length($arg), " ", $arg, "\n"; + $fh->flush if !$noflush; +} + +sub tipc_error () { + if ($! == 0 || $!{EINTR} || $!{EAGAIN} || $!{EWOULDBLOCK}) { + return; + } elsif ($!{EBADF} && $::quiet_ebadf) { + exit(0); + } else { + die "testie: ipc error: $!"; + } +} + +sub tipc_read ($$) { + my($fh, $bufref) = @_; + my($n, $x); + while (1) { + # does the buffer contain a valid command? + if ($$bufref =~ /\A(\s*([A-Z])(\d+) )/ + && length($$bufref) >= length($1) + $3) { + my($v) = substr($$bufref, length($1), $3); + $$bufref = substr($$bufref, length($1) + $3); + return ($2, $v); + } + + # if not try to read more data + $x = ""; + $n = sysread($fh, $x, 4096); + tipc_error if !defined($n); + return () if !$n; + $$bufref .= $x; + } +} + + +## testie error handler object + +package TestieErrorHandler; + +sub new (;$) { + my($print_context) = @_; + bless ["", $print_context], TestieErrorHandler; +} + +sub message ($@) { + my($teh) = shift @_; + print STDERR $teh->[0], @_; + $teh->[0] = ""; +} + +sub showmessage ($@) { + my($teh) = shift @_; + print @_; +} + +sub context ($@) { + my($teh) = shift @_; + if ($teh->[1]) { # print_context + my($t) = join("", @_); + print STDERR $teh->[0], $t; + $teh->[0] = "\r" . (" " x length($t)) . "\r"; + } +} + +sub clear ($) { +} + +sub complete ($$) { +} + + +## testie error handler object + +package TestieChildErrorHandler; + +sub new ($) { + my($fh) = @_; + bless ["", "", $fh], TestieChildErrorHandler; +} + +sub message ($@) { + my($eh) = shift @_; + $eh->clear if $eh->[0] ne "E" && $eh->[1] ne ""; + $eh->[0] = "E"; + $eh->[1] .= join("", @_); +} + +sub showmessage ($@) { + my($eh) = shift @_; + $eh->clear if $eh->[0] ne "S" && $eh->[1] ne ""; + $eh->[0] = "S"; + $eh->[1] .= join("", @_); +} + +sub context ($@) { + my($eh) = shift @_; + $eh->clear if $eh->[1] ne ""; + my($fh, $t) = ($eh->[2], join("", @_)); + ::tipc_write($fh, "C", $t); +} + +sub clear ($) { + my($eh) = shift @_; + ::tipc_write($eh->[2], $eh->[0], $eh->[1], 1) if $eh->[1] ne ""; + $eh->[0] = ""; + $eh->[1] = ""; +} + +sub complete ($$) { + my($eh, $tctr) = @_; + $eh->clear; + my(@t, $k, $v); + while (($k, $v) = each %$tctr) { + my($t) = "\"" . quotemeta($k) . "\" => "; + if (ref($v)) { + $t .= "[" . join(", ", map { "\"".quotemeta($_)."\"" } @$v) . "]"; + } else { + $t .= $v; + } + push @t, $t; + } + ::tipc_write($eh->[2], "T", "{" . join(", ", @t) . "}"); +} + + +## testie error counter object + +package TestieCounter; + +my @counters = ("errors", "require_errors", "test_attempts", "test_skips", + "test_failures", "bad_files"); + +sub new () { + my($tctr) = bless { "require_error_commands" => [] }, TestieCounter; + foreach my $x (@counters) { + $tctr->{$x} = 0; + } + $tctr; +} + +sub add ($$) { + my($tctr, $tctr1) = @_; + foreach my $x (@counters) { + $tctr->{$x} += $tctr1->{$x}; + } + push @{$tctr->{"require_error_commands"}}, @{$tctr1->{"require_error_commands"}}; + $tctr; +} + + +## main testie test object + +package Testie; + +## read testie file + +my %_special_filerefs = ('stdin' => 1, 'stdout' => 2, 'stderr' => 2); +%_variables = (); +$timeout = 45; + +sub _get ($;$) { + my($tt, $acrossfiles) = @_; + my($lines) = $tt->{"_data"}; + my $t; + while (defined($t = shift @$lines)) { + if (!ref $t) { + ++$tt->{"_line"}; + last; + } elsif ($acrossfiles) { + $tt->{"_file"} = $t->[0]; + $tt->{"_line"} = $t->[1]; + } else { + unshift @$lines, $t; + $t = undef; + last; + } + } + $t; +} + +sub _unget ($$) { + my($tt, $t) = @_; + if (defined($t) && $t ne "") { + unshift @{$tt->{"_data"}}, $t; + --$tt->{"_line"}; + } +} + +# return a command at a given line number +sub command_at ($$;$) { + my($tt, $lineno, $script_type) = @_; + return undef if !defined($lineno); + $lineno =~ s/^\s*|\s*$//g; + + $script_type = 'script' if !defined($script_type); + my($lineno_arr) = $tt->{$script_type . '_lineno'}; + for ($i = 0; $i < @$lineno_arr; $i++) { + return $tt->{$script_type}->[$i] if $lineno_arr->[$i] eq $lineno; + } + undef; +} + +# report an error +sub eh ($) { + my($tt) = @_; + $tt->{"_eh"}; +} + +sub file_err ($$;$) { + my($tt, $text, $lineno) = @_; + $text .= "\n" if $text !~ /\n$/s; + $lineno = $tt->{"_line"} if !defined($lineno); + $tt->eh->message($tt->{"_file"}, ":", $lineno, ': ', $text); + $tt->{'err'}++; +} + +sub _shell_split (\@$\@$$) { + my($arr, $fn, $lineno_arr, $text, $lineno) = @_; + my($qf, $qb, $func, $out) = (0, 0, 0, ''); + my($sq, $dq, $bq, $nl, $hh, $lb, $rb) = (-2, -2, -2, -2, -2, -2, -2); + my($first, $pos) = (0, 0); + $lineno -= ($text =~ tr/\n//); + + while ($pos < length $text) { + $sq = ::index2($text, "\'", $pos) if $sq < $pos; + $dq = ::index2($text, "\"", $pos) if $dq < $pos; + $bq = ::index2($text, "\`", $pos) if $bq < $pos; + $nl = ::index2($text, "\n", $pos) if $nl < $pos; + $hh = ::index2($text, "#", $pos) if $hh < $pos; + $lb = ::index2($text, "{", $pos) if $lb < $pos; + $rb = ::index2($text, "}", $pos) if $rb < $pos; + + if ($qf == 1) { + $qf = 0 if $sq < length $text; + $out .= substr($text, $pos, $sq + 1 - $pos); + $pos = $sq + 1; + next; + } elsif ($qf == 2) { + $qf = 0 if $dq < length $text; + $out .= substr($text, $pos, $dq - $pos) . '"'; + $pos = $dq + 1; + next; + } + + # find minimum + my($min) = ::min($sq, $dq, $bq, $nl, $hh, $lb, $rb); + $out .= substr($text, $pos, $min - $pos) . substr($text, $min, 1); + + if ($sq == $min) { + $qf = 1; + $pos = $sq + 1; + } elsif ($dq == $min) { + $qf = 2; + $pos = $dq + 1; + } elsif ($bq == $min) { + $qb = !$qb; + $pos = $bq + 1; + } elsif ($lb == $min) { + $func++; + $pos = $lb + 1; + } elsif ($rb == $min) { + $func--; + $pos = $rb + 1; + } elsif ($hh == $min) { + $out .= substr($text, $min + 1, $nl - $min); + $lineno++; + $pos = $nl + 1; + } elsif (!$qb && !$func && ($nl == $pos || substr($text, $nl - 1, 1) ne "\\")) { + push @$arr, $out; + push @$lineno_arr, "$fn:$lineno"; + $out = ''; + $lineno += (substr($text, $first, $nl - $first + 1) =~ tr/\n//); + $first = $pos = $nl + 1; + } else { + $pos = $nl + 1; + } + } + + if ($first < length $text) { + push @$arr, $out; + push @$lineno_arr, "$fn:$lineno"; + } + + if ($qf == 1) { + "unmatched single quote"; + } elsif ($qf == 2) { + "unmatched double quote"; + } elsif ($qb) { + "unmatched backquote"; + } else { + ""; + } +} + +sub _read_text ($) { + my($tt) = @_; + my($r, $t) = (''); + while (defined($t = $tt->_get())) { + last if $t =~ /^\%/; + $t =~ s/^\\\%/\%/; + $r .= $t; + } + $tt->_unget($t); + $r; +} + +sub _read_text_into ($$) { + my($tt, $section) = @_; + $tt->{$section} = '' if !defined($tt->{$section}); + $tt->{$section} .= $tt->_read_text(); +} + +sub _read_script_section ($$$) { + my($tt, $args, $script_type) = @_; + + my($lineno_type, $quiet_type) = ($script_type . '_lineno', $script_type . '_quietline'); + $tt->{$lineno_type} = [] if !exists $tt->{$lineno_type}; + $tt->{$quiet_type} = {} if !exists $tt->{$quiet_type}; + + my($quiet); + if ($script_type eq 'require' & $args eq '-q') { + $quiet = 1; + } elsif ($args ne '') { + $tt->file_err("arguments to '\%$script_type' ignored"); + } + #$tt->file_err("multiple '\%$script_type' sections defined") if $tt->{$script_type}; + my($r) = $tt->_read_text(); + my $count = @{$tt->{$lineno_type}}; + my($what) = _shell_split(@{$tt->{$script_type}}, $tt->{"_file"}, @{$tt->{$lineno_type}}, $r, $tt->{"_line"} + 1); + $tt->file_err("$what in '\%$script_type'") if $what ne ''; + while ($quiet && $count < @{$tt->{$lineno_type}}) { + my($line) = $tt->{$lineno_type}->[$count++]; + $tt->{$quiet_type}->{$line} = 1; + } +} + +sub braces_to_regex ($$) { + my($x, $mode) = @_; + my($re, $message) = ("", undef); + while ($x =~ /\A(.*?)\{\{(.*?)\}\}(.*)\z/) { + my($before, $middle, $after) = ($1, $2, $3); + if ($middle =~ /\A\?/) { + $before =~ s/\s+\z//; + $middle =~ s/\A\?\s*//; + $middle =~ s/\s+\z//; + $after =~ s/\A\s+//; + $message = (defined($message) ? $message . " " . $middle : $middle); + $x = $before . $after; + } else { + $before = quotemeta($before) if $mode == 1; + $middle =~ s,(\A|[^\\]|\\\\)/,$1\\/,g; # not 100% right sadly + $re .= $before . "(?:" . $middle . ")"; + $x = $after; + } + } + $x = quotemeta($x) if $mode == 1; + wantarray ? ($re . $x, $message) : $re . $x; +} + +sub _read_file_section ($$$$;$) { + my($tt, $args, $secname, $prefix, $backup_file) = @_; + $args =~ s/\s+$//; + + # split arguments to get fileref + my(@args) = split(/\s+/, $args); + + # assert that we understand $secname + die if $secname ne 'file' && $secname ne 'expect' && $secname ne 'expectv' && $secname ne 'expectx' && $secname ne 'ignore' && $secname ne 'ignorex' && $secname ne 'ignorev'; + + # check for alternates and length + my($alternate, $delfirst, $whitespace, $base64, $regex_opts, $length) + = (0, 0, 0, 0, '', undef); + while (@args) { + if ($args[0] =~ /\A-a/) { + $alternate = 1; + } elsif ($args[0] =~ /\A-d/) { + $delfirst = 1; + } elsif ($args[0] =~ /\A-i/) { + $regex_opts .= "(?i)"; + } elsif ($args[0] =~ /\A-e/) { + $base64 = 1; + } elsif ($args[0] =~ /\A-w/) { + $whitespace = 1; + } elsif ($args[0] =~ /\A\+(\d+)\z/) { + $length = $1; + } else { + last; + } + $args[0] = "-$1" if $args[0] =~ /\A-.(.*)\z/; + shift @args if $args[0] !~ /\A-./; + } + + # make sure there are filerefs + if (!@args && $backup_file) { + push @args, $backup_file; + } elsif (!@args) { + push @args, "stdin" if $secname eq 'file'; + push @args, "stdout" if $secname eq 'expect' || $secname eq 'expectv' || $secname eq 'expectx'; + push @args, "all" if $secname eq 'ignore' || $secname eq 'ignorev' || $secname eq 'ignorex'; + } + + # complain about '%file -aiw' + if (($secname eq 'file' || $secname eq 'ignore' || $secname eq 'ignorev' || $secname eq 'ignorex') && $alternate) { + $tt->file_err("'\%$secname -a' is illegal"); + } + if (($secname eq 'file' || $secname eq 'expectv') && $regex_opts) { + $tt->file_err("'\%$secname -i' is illegal"); + } + if (($secname eq 'file' || $secname eq 'expectv') && $whitespace) { + $tt->file_err("'\%$secname -w' is illegal"); + } + $secname .= "v" if $secname eq "expect" && $base64; + if (($secname eq "filex" || $secname eq "expectx" || $secname eq "ignore" || $secname eq "ignorev" || $secname eq "ignorex") && $base64) { + $tt->file_err("'\%$secname -e' is illegal"); + } + + # read contents + my($seclineno) = $tt->{"_line"}; + my($firstline) = $tt->{"_file"} . ":" . ($seclineno + 1); + my($file_data) = ""; + if (defined($length)) { + my($t); + while (length($file_data) < $length && defined($t = $tt->_get())) { + $file_data .= $t; + if (length($file_data) > $length) { + # save extra data from the first line + $tt->_unget(substr($t, $length - length($file_data))); + $file_data = substr($file_data, 0, $length); + } + } + $tt->file_err("file too short", $seclineno) + if length($file_data) != $length; + } else { + $file_data = $tt->_read_text(); + } + + # modify contents based on flags + $alternate = 1 if $secname eq 'ignore' || $secname eq 'ignorev' || $secname eq 'ignorex'; # 'ignore' always behaves like -a + if ($delfirst) { + $file_data =~ s{^.}{}mg; + } + if (($secname eq 'ignore' || $secname eq 'ignorev' || $secname eq 'ignorex') + && $whitespace) { + $file_data =~ tr/ \f\r\t\013//d; + } + if ($secname eq 'ignore') { + $file_data =~ s{^(.+)}{braces_to_regex($1, 1)}meg; + } elsif ($secname eq 'ignorev') { + $file_data =~ s{^(.+)}{quotemeta($1)}meg; + } elsif ($secname eq 'ignorex') { + $file_data =~ s[\s*\{\{\?.*?\}\}\s*][]mg; + } + if ($regex_opts && $secname eq 'expect') { + $file_data =~ s{\{\{}{\{\{$regex_opts}g; + } elsif ($regex_opts) { + $file_data =~ s{^(?=.)}{$regex_opts}mg; + } + if ($base64) { + $file_data = MIME::Base64::decode_base64($file_data); + } + + # stick contents where appropriate + my($fn); + foreach $fn (@args) { + if (($fn eq 'stdin' && $secname ne 'file') + || (($fn eq 'stdout' || $fn eq 'stderr') && $secname eq 'file') + || ($fn eq 'all' && ($secname ne 'ignore' && $secname ne 'ignorev' && $secname ne 'ignorex'))) { + $tt->file_err("'$fn' not meaningful for '\%$secname'", $seclineno); + } + + my($hashkey) = $prefix . ":" . $fn; + if (!($fn =~ m,\A[-A-Za-z_0-9.]+\z, + || ($fn =~ m,\A[-A-Za-z_0-9./]+\z, + && $fn !~ m,(\A\.\./|/\.\./|/\.\.\z|\A/|//|/\z),))) { + $tt->file_err("bad filename '\%$secname $fn'", $seclineno); + next; + } elsif (!exists($tt->{$hashkey})) { + push @{$tt->{$secname}}, $fn; + $tt->{$hashkey} = []; + } elsif (!$alternate) { + $tt->file_err("'\%$secname $fn' already defined", $seclineno); + } + + push @{$tt->{$hashkey}}, $file_data; + my($num) = @{$tt->{$hashkey}} - 1; + $tt->{"F:$fn"} = 1; + $tt->{"firstline:$hashkey:$num"} = $firstline; + $tt->{"whitespace:$hashkey:$num"} = 1 if $whitespace; + } +} + +sub _skip_section ($) { + my($tt) = @_; + my($t); + while (defined($t = $tt->_get())) { + last if $t =~ /^%/; + } + $tt->_unget($t); +} + +sub parse ($) { + my($tt) = @_; + my($t, $read_command); + + # delete garbage + my(@deletes, $k, $v); + while (($k, $v) = each %$tt) { + push @deletes, $k if $k ne "_data" && $k ne "err" && $k ne "_eh"; + } + foreach $k (@deletes) { + delete $tt->{$k}; + } + + while (defined($t = $tt->_get(1))) { + if ($t =~ /^%\s*(\w+)\s*(.*?)\s*$/) { + my($command) = lc($1); + my($args) = $2; + if ($command eq 'script' || $command eq 'test') { + $tt->_read_script_section($args, 'script'); + } elsif ($command eq 'require') { + $tt->_read_script_section($args, 'require'); + } elsif ($command eq 'info') { + $tt->file_err("arguments to '\%info' ignored") if $args ne ''; + $tt->_read_text_into('info'); + } elsif ($command eq 'desc') { + $tt->file_err("arguments to '\%desc' ignored") if $args ne ''; + $tt->_read_text_into('info'); + } elsif ($command eq 'cut') { + $tt->_read_text_into('cut'); + } elsif ($command eq 'stdin' || $command eq 'input') { + $tt->_read_file_section($args, "file", "f", "stdin"); + } elsif ($command eq 'file') { + $tt->_read_file_section($args, 'file', 'f'); + } elsif ($command eq 'stdout' || $command eq 'output') { + $tt->_read_file_section($args, "expect", "e", "stdout"); + } elsif ($command eq 'stderr') { + $tt->_read_file_section($args, "expect", "e", "stderr"); + } elsif ($command eq 'expect') { + $tt->_read_file_section($args, 'expect', 'e'); + } elsif ($command eq 'expectx') { + $tt->_read_file_section($args, 'expectx', 'x'); + } elsif ($command eq 'expectv' || $command eq 'expect_verbatim' + || $command eq 'verbatim') { + $tt->_read_file_section($args, 'expectv', 'v'); + } elsif ($command eq 'ignore') { + $tt->_read_file_section($args, 'ignore', 'i'); + } elsif ($command eq 'ignorev') { + $tt->_read_file_section($args, 'ignorev', 'i'); + } elsif ($command eq 'ignorex') { + $tt->_read_file_section($args, 'ignorex', 'i'); + } elsif ($command eq 'include') { + if ($args !~ /^\//) { + my($oldfn) = $tt->{"_file"}; + $oldfn =~ s/(\A|\/)[^\/]+\z/$1/; + $args = $oldfn . $args; + } + if (open(INCLUDE, "<", $args)) { + my(@ilines, $it); + push @ilines, [$args, 0]; + push @ilines, $it while defined($it = ); + push @ilines, [$tt->{"_file"}, $tt->{"_line"}]; + unshift @{$tt->{"_data"}}, @ilines; + } else { + $tt->file_err("\%include $args: $!"); + } + } elsif ($command eq 'eot') { + unshift @{$tt->{"_data"}}, [$tt->{"_file"}, $tt->{"_line"}]; + $tt->{"continue"} = 1; + last; + } elsif ($command eq 'eof') { + 1 while defined($t = $tt->_get()); + } else { + $tt->file_err("unrecognized command '$command'"); + $tt->_skip_section(); + } + $read_command = 1; + } else { + if ($t =~ /^%/) { + $tt->file_err("bad '\%' command"); + } elsif ($t !~ /^[\#!]/ && $t =~ /\S/) { + $tt->file_err("warning: garbage ignored") if $read_command; + $read_command = 0; + } + } + } + + $tt; +} + +sub read (*$;$) { + my($fh, $teh, $fn) = @_; + $fh = ::qualify_to_ref($fh, caller); + my($t, $tt); + + $tt = bless { "err" => 0, "_data" => [[$fn, 0]], "_eh" => $teh }, Testie; + push @{$tt->{"_data"}}, $t while defined($t = <$fh>); + + $tt->parse(); + $tt; +} + +sub have_file ($$) { + my($tt, $fileref) = @_; + exists($tt->{"F:$fileref"}); +} + +sub empty ($) { + my($tt) = @_; + !exists($tt->{'script'}); +} + +sub save_files ($&) { + my($tt, $fileref_subr) = @_; + my($fn, $dirn, $actual); + + # create implied subdirectories + foreach $fn (keys %$tt) { + next if $fn !~ m,\AF:(.*)/([^/]*)\z,; + $dirn = $1; + while (!-d $fileref_subr->($dirn)) { + $fn = $dirn; + $fn = $1 while ($fn =~ m,\A(.*)/([^/]*)\z, + && !-d $fileref_subr->($1)); + $actual = $fileref_subr->($fn); + mkdir $actual || die "$actual: $!\n"; + } + } + + # write '%file' contents + foreach $fn (@{$tt->{'file'}}) { + $actual = $fileref_subr->($fn); + next if !defined($actual); + open OUT, ">", $actual || die "$actual: $!\n"; + print OUT $tt->{"f:$fn"}->[0]; + close OUT; + } +} + +sub script_text ($&$) { + my($tt, $fileref_subr, $script_type) = @_; + my($subbody, $var, $val) = ''; + + my($t) = ''; + if (!$::expand_mode) { + $t .= <<'EOD;'; +testie_failed () { + exitval=$? + test $exitval = 0 || (echo; echo testie_failure:$exitval) >&2 + exit $exitval +} +testie_subtest () { + echo testie_subtest "$@" + echo testie_subtest "$@" >&2 +} +trap testie_failed EXIT +EOD; + } + + my($scriptarr, $linenoarr) = ($tt->{$script_type}, $tt->{$script_type . "_lineno"}); + my($last_unfinished) = 0; + foreach my $i (0..$#{$tt->{$script_type}}) { + my($ln, $text) = ($linenoarr->[$i], $scriptarr->[$i]); + if (!$::expand_mode && !$last_unfinished) { + $t .= "echo >&2; echo testie_lineno:$ln >&2\n"; + } + my(@c, @d); + _shell_split(@c, "", @d, $text, 0); + die if @c != 1; + chomp $c[0]; + next if $c[0] =~ /^\s*$/s; + $last_unfinished = ($c[0] =~ /(?:\&\&|\|\||\|)\s*\z/); + $c[0] =~ s,^(\s*)\./,$1../, if !$::expand_mode; + $t .= $c[0] . "\n"; + } + + $t; +} + +sub output_error ($$$$) { + my($tt, $fileref_subr, $script_type, $tctr) = @_; + my($fp) = $tt->{'errprefix'}; + + if (!open(ERR, "<", $fileref_subr->('stderr'))) { + $tt->eh->message($fp, $!, "\n"); + ++$tctr->{"errors"}; + return $tctr; + } + + my($errortext, $subtest, $t, $lineno, $failure) = ('', ''); + while ($t = ) { + if ($t =~ /^testie_lineno:(.*)$/) { + $lineno = $1; + $errortext = ''; + } elsif ($t =~ /^testie_failure:(.*)$/) { + $failure = $1; + } elsif ($t =~ /^testie_subtest (.*)$/) { + $subtest = " subtest $1"; + } else { + $errortext .= $t; + } + } + close ERR; + $lineno = $fp if !defined($lineno); + $lineno =~ s/: *\z//; + + my($failure_text); + if (!defined($failure)) { + $failure_text = "undefined error"; + } elsif ($failure eq "timeout") { + $failure_text = "timed out after $Testie::timeout sec"; + } elsif ($failure == 1) { + $failure_text = "failure"; + } else { + $failure_text = "error $failure"; + } + if (defined($script_type) && $script_type eq 'require') { + $failure_text = "requirement $failure_text"; + ++$tctr->{"require_errors"}; + } else { + ++$tctr->{"errors"}; + } + + $errortext =~ s/\s*\z//; + + my($cmd) = $tt->command_at($lineno, $script_type); + + # exit early if quiet + return $tctr if $tt->{$script_type . '_quietline'}->{$lineno} && $::verbose <= 0; + + $lineno .= $subtest; + if ($errortext =~ /^testie_error:/) { + while ($errortext =~ /^testie_error:([^\n]*)/g) { + $tt->eh->message($lineno, ": ", $1, "\n"); + } + $errortext =~ s/^testie_error:([^\n]*)//g; + $errortext =~ s/\s*//; + $tt->eh->message($lineno, ": (There were other errors as well.)\n") + if $errortext ne ''; + } elsif (!defined($cmd)) { + $tt->eh->message($lineno, ": $failure_text at undefined point in script\n"); + } else { + $cmd =~ s/^\s*|\s*$//g; + $cmd =~ s/([\000-\037])/'^' . chr(ord($1) + ord('@'))/eg; + $cmd =~ s/([\177-\377])/"\\" . sprintf("%03o", ord($1))/eg; + if (length($cmd) > 40) { + $cmd = substr($cmd, 0, 40) . "..."; + } + # if nonverbose requirement, remember command, don't print error + if (defined($script_type) && $script_type eq 'require' && $::verbose <= 0) { + push @{$tctr->{"require_error_commands"}}, $cmd; + } else { + $tt->eh->message($lineno, ": $failure_text at '$cmd'\n"); + while ($errortext =~ /([^\n]*)/g) { + $tt->eh->message($lineno, ": $1\n") if $1 ne ''; + } + } + } + + $tctr; +} + +sub _output_expectation_error ($$$$$) { + my($fp, $efn, $etrack, $teh, $tctr) = @_; + + # fix subtest description + if (defined($etrack->{"subtest"})) { + $fp =~ s/: \z/ /; + $fp .= "subtest " . $etrack->{"subtest"} . ": "; + } + if (defined($etrack->{"expectedline"})) { + $fp = $etrack->{"expectedline"} . ": "; + } + + # output message + if ($efn eq 'stdout') { + $teh->message($fp, "standard output has unexpected value starting at line " . $etrack->{"textline"} . "\n"); + } elsif ($efn eq 'stderr') { + $teh->message($fp, "standard error has unexpected value starting at line " . $etrack->{"textline"} . "\n"); + } else { + $teh->message($fp, "file $efn has unexpected value starting at line " . $etrack->{"textline"} . "\n"); + } + + # output expected and text data if possible + $etrack->{"expected"} = "" if $etrack->{"expected"} eq "\376"; + $etrack->{"expected"} =~ s/\r?\n?\z//; + $etrack->{"text"} = "" if $etrack->{"text"} eq "\376"; + $etrack->{"text"} =~ s/\r?\n?\z//; + if ($etrack->{"expected"} =~ /\A[\t\040-\176]*\z/ + && $etrack->{"text"} =~ /\A[\t\040-\176]*\z/) { + $etrack->{"expected"} =~ s/\s*\{\{\?.*?\}\}\s*//g if $etrack->{"mode"} != 0; + $teh->message($fp, $efn, ":", $etrack->{"textline"}, ": expected '", $etrack->{"expected"}, "'\n", + $fp, $efn, ":", $etrack->{"textline"}, ": but got '", $etrack->{"text"}, "'\n"); + } + if (defined($etrack->{"message"})) { + $teh->message($fp, $efn, ":", $etrack->{"textline"}, ": ", $etrack->{"message"}, "\n"); + } + + # maintain error count + ++$tctr->{"errors"}; + return $tctr; +} + +sub _expect_trim_whitespace ($) { + my($out) = ""; + foreach my $x (split(/(\{\{.*?\}\})/, $_[0])) { + $x =~ tr/ \f\r\t\013//d if $x !~ /\A\{\{/; + $out .= $x; + } + return $out; +} + +sub _check_one_typed_expect ($$$$$) { + my($tt, $raw_text, $fn, $ignores, $etrack) = @_; + my($mode) = ($fn =~ /^v/ ? 0 : ($fn =~ /^e/ ? 1 : 2)); + my($expnum) = 0; + + foreach my $exp (@{$tt->{$fn}}) { + my($text) = $raw_text; + my($whitespace) = $tt->{"whitespace:$fn:$expnum"}; + + # escape in common case + return 0 if $text eq $exp; + + # check that files really disagree (in later modes) + if ($mode > 0) { + # ignore differences in amounts of vertical whitespace + $text =~ s/[ \f\r\t\013]+\n/\n/g; + $text =~ s/\n\n+\z/\n/; + $text =~ s/\A\n//; + $exp =~ s/[ \f\r\t\013]+\n/\n/g; + $exp =~ s/\n\n+\z/\n/; + + return 0 if $text eq $exp; + + # ignore explicitly ignored text + $text = $ignores->($text) if $ignores; + } + + # line-by-line comparison + my(@tl) = (split(/\n/, $text), "\376"); + my(@el) = (split(/\n/, $exp), "\376"); + my($tp, $ep, $subtest, $message) = (0, 0, undef, undef); + while ($tp < @tl && $ep < @el) { + + # skip blank lines and ignored lines + ++$ep while $el[$ep] eq '' && $mode > 0; + ++$tp while ($tl[$tp] eq '' && $mode > 0) || $tl[$tp] eq "\377"; + + # process testie_subtest + if (length($tl[$tp]) > 15 && substr($tl[$tp], 0, 15) eq "testie_subtest ") { + $subtest = substr($tl[$tp], 15); + $tp++; + next; + } + + # compare lines + my($tline, $eline) = ($tl[$tp], $el[$ep]); + if ($whitespace) { + $tline =~ tr/ \f\r\t\013//d; + $eline = _expect_trim_whitespace($eline); + } + if ($mode != 0 && $eline =~ /\{\{/) { + my($re); + ($re, $message) = braces_to_regex($eline, $mode); + last if $tline !~ m/\A$re\z/; + } elsif ($mode == 2) { + last if $tline !~ m/\A$eline\z/; + } elsif ($tline ne $eline) { + last; + } + + $tp++, $ep++; + } + return 0 if $tp >= @tl || $ep >= @el; + + if (!defined($etrack->{"textline"}) || $tp + 1 > $etrack->{"textline"}) { + $etrack->{"text"} = $tl[$tp]; + $etrack->{"expected"} = $el[$ep]; + $etrack->{"textline"} = $tp + 1; + if (defined($tt->{"firstline:$fn:$expnum"}) + && $tt->{"firstline:$fn:$expnum"} =~ /^(.*):(\d+)$/) { + $etrack->{"expectedline"} = $1 . ":" . ($2 + $ep); + } else { + $etrack->{"expectedline"} = undef; + } + $etrack->{"mode"} = $mode; + $etrack->{"subtest"} = $subtest; + $etrack->{"message"} = $message; + } + + ++$expnum; + } + + return -1; +} + +sub _create_ignores ($$) { + my($tt, $efn) = @_; + my($ignores, $wignores, $body) = ("", ""); + + foreach my $fn ($efn, "all") { + next if !exists($tt->{"i:$fn"}); + for (my $expnum = 0; $expnum < @{$tt->{"i:$fn"}}; ++$expnum) { + if ($tt->{"whitespace:i:$fn:$expnum"}) { + $wignores .= $tt->{"i:$fn"}->[$expnum] . "\n"; + } else { + $ignores .= $tt->{"i:$fn"}->[$expnum] . "\n"; + } + } + } + # ignore testie messages + $ignores .= "testie_lineno:.*\ntestie_error:.*\n" if $efn eq "stderr"; + + if ($ignores eq "" && $wignores eq "") { + return undef; + } elsif ($wignores eq "") { + $ignores =~ s{^([ \t]*\S[^\n]*)}{\$t =~ s/^$1\[ \\t\]*\$/\\377/mg;}mg; + $body = "sub (\$) { my(\$t) = \@_; $ignores \$t; }\n"; + } else { + $ignores =~ s{^([ \t]*\S[^\n]*)}{s/\\A$1\[ \\t\]*\\z/\\377/;}mg; + $wignores =~ s{^(\S[^\n]*)}{\$_ = "\\377" if \$x =~ m/\\A$1\\z/;}mg; + $body = "sub (\$) { my(\$t) = \@_; my(\$x); join(\"\\n\", map { " + . "\$x = \$_; \$x =~ tr/ \\f\\r\\t\\013//d;\n$ignores$wignores " + . "\"\$_\\n\" } split /\\n/, \"\$t\\n\"); }\n"; + } + return eval($body); +} + +sub _check_one_expect ($$$$) { + my($tt, $fileref_subr, $efn, $tctr) = @_; + my($fp) = $tt->{'errprefix'}; + my($etrack) = {}; + + # read file text + if (!open(IN, "<", $fileref_subr->($efn))) { + $tt->eh->message($fp, $efn, ": ", $!, "\n"); + ++$tctr->{"errors"}; + return 0; + } + my($raw_text) = ; + $raw_text = '' if !defined($raw_text); + close IN; + + # prepare $ignores + my($ignores) = _create_ignores($tt, $efn); + + # now compare alternates + foreach my $fn ("v:$efn", "e:$efn", "x:$efn") { + return 0 if _check_one_typed_expect($tt, $raw_text, $fn, $ignores, $etrack) >= 0; + } + + # if we get here, none of the attempts matched + _output_expectation_error($fp, $efn, $etrack, $tt->eh, $tctr); +} + + +sub check_expects ($$$) { + my($tt, $fileref_subr, $tctr) = @_; + my($fp) = $tt->{'errprefix'}; + local($/) = undef; + my($expectx) = 0; + my($tp, @tl, $ep, @el); + + # check expected files + my(%done); + foreach my $efn (@{$tt->{'expect'}}, @{$tt->{'expectx'}}, @{$tt->{'expectv'}}) { + next if $done{$efn}; + _check_one_expect($tt, $fileref_subr, $efn, $tctr); + $done{$efn} = 1; + } + + 0; +} + + +package main; + +my($dir, @show, $show_stdout, $show_stderr, %child_pids); +my($SHELL) = "/bin/sh"; + +sub script_fn_to_fn ($) { + my($fn) = @_; + $fn; +} + +sub out_script_fn_to_fn ($) { + my($fn) = @_; + "$dir/$fn"; +} + +sub _shell ($$$$$) { + my($dir, $scriptfn, $stdin, $stdout, $stderr) = @_; + $scriptfn = "./$scriptfn" if $scriptfn !~ m|^/|; + + # Create a new process group so we can (likely) kill any children + # processes the script carelessly left behind. Thanks, Chuck Blake! + my($child_pid) = fork(); + if (!defined($child_pid)) { + die "cannot fork: $!\n"; + } elsif ($child_pid == 0) { + eval { setpgrp() }; + chdir($dir); + open(STDIN, "<", $stdin) || die "$stdin: $!\n"; + open(STDOUT, ">", $stdout) || die "$stdout: $!\n"; + open(STDERR, ">", $stderr) || die "$stderr: $!\n"; + my($var, $val); + $ENV{$var} = $val while (($var, $val) = each %Testie::_variables); + $ENV{"rundir"} = ".."; + exec $SHELL, "-e", $scriptfn; + } else { + $running_pid = $child_pid; + my($result) = undef; + if ($Testie::timeout > 0) { + my($before) = Time::HiRes::time(); + my($delta) = 10; + do { + Time::HiRes::usleep($delta); + $result = $? if waitpid($child_pid, WNOHANG) > 0; + $delta = ($delta < 150000 ? $delta * 2 : 300000); + } while (!defined($result) && Time::HiRes::time() < $before + $Testie::timeout); + if (!defined($result)) { + if (open(X, ">>", out_script_fn_to_fn($stderr))) { + print X "testie_failure:timeout\n"; + close X; + } + $result = 124; + } + $result = 124 if !defined($result); + } else { + waitpid($child_pid, 0); + } + $result = $? if !defined($result); + # sleep for 1 millisecond to give remaining background jobs a chance + # to die + select(undef, undef, undef, 0.001); + kill('HUP', -$child_pid); # kill any processes left behind + $running_pid = 0; + $result; + } +} + +sub execute_test ($$) { + my($tt, $fn) = @_; + my($tctr, $teh) = (TestieCounter::new, $tt->eh); + ++$tctr->{"test_attempts"}; + my($f); + + # count attempt + $tt->{"errprefix"} = $fn . ": "; + + # print description in superverbose mode + if ($::verbose > 1) { + return $tctr if $tt->empty; + if ($tt->{'info'}) { + my($desc) = $tt->{'info'}; + $desc =~ s/^(.*?)\t/$1 . (' ' x (8 - (length($1) % 8)))/egm + while $desc =~ /\t/; + $desc =~ s/\r\n/\n/g; + $desc =~ tr/\r/\n/; + $desc =~ s/\A\n+//s; + $desc =~ s/\n\n.*\z//s; + $desc =~ s/^/ /mg; + $desc .= "\n" if $desc !~ /\n\z/; + $teh->message($fn, " Information:\n", $desc); + } + $teh->message($fn, " Results:\n"); + $tt->{'errprefix'} = " "; + } + + # maybe note that we're running the test + if ($::verbose == 1) { + $teh->message($tt->{'errprefix'}, "Running...\n"); + } elsif ($::verbose == 0) { + my($cr_out) = "[" . $tt->{"errprefix"}; + $cr_out =~ s/:\s+\z//; + $cr_out = "[..." . substr($cr_out, -73) if length($cr_out) > 76; + $teh->context($cr_out, "] "); + } + + # check requirements + if (exists $tt->{'require'}) { + open(SCR, ">", "$dir/\%require") || die "$dir/\%require: $!\n"; + print SCR $tt->script_text(\&script_fn_to_fn, 'require'); + close SCR; + + if (!$expand_mode) { + my($exitval) = _shell($dir, '%require', '/dev/null', '/dev/null', script_fn_to_fn('stderr')); + + # if it exited with a bad value, quit + if ($exitval) { + return $tt->output_error(\&out_script_fn_to_fn, 'require', $tctr); + } + } + } + + # save the files it names + $tt->save_files(\&out_script_fn_to_fn); + + # save the script + open(SCR, ">", "$dir/\%script") || die "$dir/\%script: $!\n"; + print SCR $tt->script_text(\&script_fn_to_fn, 'script'); + close SCR; + + # exit if expand mode + return $tctr if $expand_mode; + + # run the script + my($actual_stdin) = ($tt->have_file('stdin') ? script_fn_to_fn('stdin') : "/dev/null"); + my($actual_stdout) = ($show_stdout || $tt->have_file('stdout') ? script_fn_to_fn('stdout') : "/dev/null"); + my($actual_stderr) = script_fn_to_fn('stderr'); + my($exitval) = _shell($dir, '%script', $actual_stdin, $actual_stdout, $actual_stderr); + + # expand "--show-alls" + my(@xshow); + foreach $f (@show) { + if ($f->[0] eq "*") { + my(%expanded, @shownit, $k, $v); + %expanded = ("stdout" => 1, "stderr" => 1); + push @xshow, ["stdout", $f->[1]], ["stderr", $f->[1]]; + while (($k, $v) = each %$tt) { + next if $k !~ /\A[exv]:(.*)\z/ || exists $expanded{$1}; + $expanded{$1} = 1; + push @shownit, [$1, $f->[1]]; + } + push @xshow, sort { $a->[0] cmp $b->[0] } @shownit; + } else { + push @xshow, $f; + } + } + + # echo files + foreach $f (@xshow) { + $efn = $f->[0]; + if (-r out_script_fn_to_fn($efn)) { + $teh->showmessage("$fn: ", $efn, "\n", "=" x 79, "\n"); + local($/) = undef; + open(X, "<", out_script_fn_to_fn($efn)); + my($text) = ; + close(X); + if ($f->[1] && defined($text)) { + my($ignores) = Testie::_create_ignores($tt, $efn); + if ($ignores) { + $text = $ignores->($text); + $text =~ s/^\377\n//mg; + } + } + $teh->showmessage($text) if defined $text; + $teh->showmessage("=" x 79, "\n"); + } elsif ($efn ne "*") { + $teh->showmessage("$fn: $efn does not exist\n"); + } + } + + if ($exitval) { + # if it exited with a bad value, quit + $tt->output_error(\&out_script_fn_to_fn, 'script', $tctr); + } elsif ($tt->check_expects(\&out_script_fn_to_fn, $tctr)) { + # expectsnothing to do + } else { + # success, print message if verbose + if ($::verbose > 0 && !$tt->empty && $tctr->{"errors"} == 0) { + $teh->message($tt->{'errprefix'}, "Success!\n"); + } + } + + $teh->message("\n") if $::verbose > 1; + return $tctr; +} + +sub run_test_read_file ($$) { + my($fn, $teh) = @_; + + # read the testie + my($tt, $display_fn, $close_in); + if (!defined($fn) || $fn eq '-') { + if (!open(IN, "<&=STDIN")) { + $teh->message(": $!\n"); + return (); + } + $display_fn = ""; + } elsif (-d $fn) { + $teh->message($fn, ": is a directory\n"); + return (); + } else { + if (!open(IN, "<", $fn)) { + $teh->message($fn, ": $!\n"); + return (); + } + $display_fn = $fn; + $close_in = 1; + } + + $tt = Testie::read(IN, $teh, $display_fn); + return ($tt, $display_fn, $close_in); +} + +sub run_test_body ($$) { + my($fn, $teh) = @_; + my($tctr) = TestieCounter::new; + + my($tt, $display_fn, $close_in) = run_test_read_file($fn, $teh); + if (!defined($tt)) { + ++$tctr->{"bad_files"}; + return $tctr; + } + + my($suffix) = ''; + + while (1) { + my($tctr1) = execute_test($tt, $display_fn . $suffix); + if ($tctr1->{"require_errors"}) { + ++$tctr->{"test_skips"}; + } elsif ($tctr1->{"errors"}) { + ++$tctr->{"test_failures"}; + } + $tctr->add($tctr1); + last if !exists $tt->{'continue'}; + if (!($suffix =~ s/^<(\d+)>$/"<" . ($1+1) . ">"/e)) { + $suffix = "<2>"; + } + $tt->parse(); + } + + close IN if $close_in; + return $tctr; +} + +sub run_test ($$$) { + my($fn, $teh, $testnumber) = @_; + + if (!$::expand_mode) { + $dir = "testie$$" . ($testnumber ? "-$testnumber" : ""); + if (-d $dir) { + $teh->message("warning: $dir directory exists; removing it\n"); + system("/bin/rm -rf $dir"); + -d $dir && die "cannot remove $dir directory: $!\n"; + } + mkdir $dir || die "cannot create $dir directory: $!\n"; + } + + my($tctr) = run_test_body($fn, $teh); + $teh->complete($tctr); + + system("/bin/rm -rf $dir") if !$preserve_temporaries; + undef $dir; + return $tctr; +} + +sub cleanup () { + kill("HUP", -$running_pid) if $running_pid; # kill any processes left behind + + my(@children) = keys %child_pids; + foreach my $kid (@children) { + kill("HUP", $kid) if $child_pids{$kid}; + } + + system("/bin/rm -rf $dir 2>/dev/null") + if defined($dir) && !$preserve_temporaries; +} + +$SIG{'INT'} = $SIG{'QUIT'} = $SIG{'TERM'} = sub { + cleanup; + exit(1); +}; +$SIG{'__DIE__'} = \&cleanup; + + +# child processing + +sub testie_child () { + my($p2cr, $p2cw, $c2pr, $c2pw); + pipe($p2cr, $p2cw); + pipe($c2pr, $c2pw); + $p2cw->autoflush(1); + defined($c2pr->blocking(0)) || die "cannot set nonblocking: $!"; + defined($p2cr->blocking(0)) || die "cannot set nonblocking: $!"; + binmode $p2cr; + binmode $p2cw; + binmode $c2pr; + binmode $c2pw; + + my($child_pid) = fork(); + if (!defined($child_pid)) { + die "cannot fork: $!\n"; + } elsif ($child_pid) { + $p2cr->close; + $c2pw->close; + $child_pids{$child_pid} = 1; + return [$c2pr, $p2cw, "", $child_pid]; + } + + $SIG{"CHLD"} = "DEFAULT"; # reset SIG{CHLD} handler from parent + eval { setpgrp() }; + $::quiet_ebadf = 1; + $p2cw->close; + $c2pr->close; + my($p2crbuf, $testnumber) = ("", 0); + my($teh) = TestieChildErrorHandler::new($c2pw); + my($command, $arg, $rin, $rout, $win, $wout); + $rin = $win = ""; + vec($rin, $p2cr->fileno, 1) = 1; + vec($win, $c2pw->fileno, 1) = 1; + + while (1) { + if ((($command, $arg) = tipc_read($p2cr, \$p2crbuf))) { + if ($command eq "T") { + run_test($arg, $teh, $testnumber); + ++$testnumber; + } elsif ($command eq "X") { + exit(0); + } else { + print STDERR "ipc error: bad command $command\n"; + exit(1); + } + } + + tipc_error if select($rout = $rin, undef, $wout = $win, undef) < 0; + + # EPIPE/SIGPIPE to catch dead parent + tipc_error if !defined(syswrite($c2pw, " ")); + } +} + +sub testie_child_reaper { + my $kid; + while (($kid = waitpid(-1, WNOHANG)) > 0) { + delete $child_pids{$kid}; + } +} + +sub testie_parent_loop (\@$$$) { + my($tests, $tctr, $teh, $jobs) = @_; + my($testpos, $testdone, $rin, $rout) = (0, 0, "", ""); + my(@children, @child_out, $command, $arg); + $SIG{"CHLD"} = \&testie_child_reaper; + + for (my $i = 0; $i < $jobs; ++$i) { + last if $testpos == @$tests; + push @children, testie_child; + my($c2pr, $p2cw) = ($children[$i]->[0], $children[$i]->[1]); + tipc_write($p2cw, "T", $tests->[$testpos]); + ++$testpos; + vec($rin, $c2pr->fileno, 1) = 1; + push @child_out, []; + } + + while ($testdone < @$tests) { + tipc_error if select($rout = $rin, undef, undef, undef) < 0; + + for (my $i = 0; $i < @children; ++$i) { + my($c2pr, $p2cw, $c2prbufref) = ($children[$i]->[0], $children[$i]->[1], \($children[$i]->[2])); + while ((($command, $arg) = tipc_read($c2pr, $c2prbufref))) { + if ($command eq "C") { + $teh->context($arg); + } elsif ($command eq "S" || $command eq "E") { + push @{$child_out[$i]}, [$command, $arg]; + } elsif ($command eq "T") { + my($tctr1) = eval($arg); + bless $tctr1, TestieCounter; + $tctr->add($tctr1); + foreach my $x (@{$child_out[$i]}) { + if ($x->[0] eq "S") { + $teh->showmessage($x->[1]); + } else { + $teh->message($x->[1]); + } + } + $child_out[$i] = []; + ++$testdone; + if ($testpos < @$tests) { + tipc_write($p2cw, "T", $tests->[$testpos]); + ++$testpos; + } + } else { + die "ipc error: bad command $command"; + } + } + } + } +} + + +# help/usage + +sub help () { + print <<'EOD;'; +'Testie' is a simple test harness. + +Usage: testie [OPTIONS] [FILE]... + +Options: + VARIABLE=VALUE Variable settings for test script. + -V, --verbose Print information for successful tests. + -VV, --superverbose Print initial %info for all tests. + -s, --show TESTIEFILE Show contents of TESTIEFILE on completion. + -S, --show-raw TESTIEFILE Like --show, but include ignored lines. + --show-all Show contents of all TESTIEFILEs on completion. + --show-all-raw Like --show-all, but include ignored lines. + --preserve-temporaries Preserve temporary files. + -e, --expand Expand test files into current directory. + -p, --path DIR Prepend DIR to PATH. + -t, --timeout T Set timeout to T [45 sec]. + -v, --version Print version information and exit. + --help Print this message and exit. + +Report bugs and suggestions to . +EOD; + exit(0); +} + +sub usage () { + print STDERR <<'EOD;'; +Usage: testie [-V] [FILE]... +Try 'testie --help' for more information. +EOD; + exit(1); +} + +sub print_version () { + print <<'EOD;'; +Testie 1.3 +Copyright (c) 2002-2016 Eddie Kohler +Copyright (c) 2002-2003 International Computer Science Institute +Copyright (c) 2004-2007 Regents of the University of California +Copyright (c) 2008-2010 Meraki, Inc. +This is free software; see the source for copying conditions. +There is NO warranty, not even for merchantability or fitness for a +particular purpose. +EOD; + exit(0); +} + +sub argcmp ($$$;\$) { + my($arg, $opt, $min_match, $store) = @_; + $$store = undef if defined($store); + return 0 if substr($arg, 0, 2 + $min_match) ne substr($opt, 0, 2 + $min_match); + my($eq) = index($arg, '='); + my($last) = ($eq >= 0 ? $eq : length($arg)); + return 0 if $last > length($opt) || substr($arg, 0, $last) ne substr($opt, 0, $last); + return 0 if !defined($store) && $eq >= 0; + $$store = substr($arg, $eq + 1) if defined($store) && $eq >= 0; + 1; +} + + +# directory searching + +sub search_dir ($$) { + my($dir, $aref) = @_; + $dir =~ s/\/+$//; + if (!opendir(DIR, $dir)) { + print STDERR "$dir: $!\n"; + return; + } + my(@f) = grep { !/^\.\.?$/ } readdir(DIR); + closedir(DIR); + foreach my $f (sort { $a cmp $b } @f) { + if (-d "$dir/$f") { + &search_dir("$dir/$f", $aref); + } elsif ($f =~ /^[^#\.].*\.testie$/) { + push @$aref, "$dir/$f"; + } + } +} + + +# argument processing + +$dir = undef; + +my(@tests, $arg, $jobs, @pathprepend); +$Testie::_variables{"LC_ALL"} = "C"; + +while (@ARGV) { + $_ = shift @ARGV; + if (/^([A-Za-z_]\w*)=(.*)$/s) { + $Testie::_variables{$1} = $2; + } elsif (/^-$/) { + push @tests, $_; + } elsif (!/^-/) { + if (-d $_) { + search_dir($_, \@tests); + } else { + push @tests, $_; + } + } elsif (/^-v$/ || argcmp($_, '--version', 4)) { + print_version; + } elsif (/^-q$/ || argcmp($_, '--quiet', 1)) { + $::verbose = -1; + } elsif (/^-V$/ || argcmp($_, '--verbose', 4)) { + $::verbose = 1; + } elsif (/^-VV$/ || argcmp($_, '--superverbose', 2)) { + $::verbose = 2; + } elsif (/^-e$/ || argcmp($_, '--expand', 1)) { + $expand_mode = 1; + $preserve_temporaries = 1; + $dir = "."; + } elsif (argcmp($_, '--help', 1)) { + help; + } elsif (argcmp($_, '--preserve-temporaries', 2) + || argcmp($_, '--preserve-temps', 2)) { + $preserve_temporaries = 1; + } elsif (/^-p$/ || argcmp($_, '--path', 2)) { + usage if @ARGV == 0; + push @pathprepend, shift @ARGV; + } elsif (/^-p(.+)$/) { + push @pathprepend, $1; + } elsif (argcmp($_, '--path', 2, $arg)) { + push @pathprepend, $arg; + } elsif (/^-s$/ || argcmp($_, '--show', 2)) { + usage if @ARGV == 0; + push @show, [(shift @ARGV), 1]; + } elsif (/^-s(.+)$/) { + push @show, [$1, 1]; + } elsif (argcmp($_, '--show', 2, $arg)) { + push @show, [$arg, 1]; + } elsif (/^-S$/ || argcmp($_, '--show-raw', 6)) { + usage if @ARGV == 0; + push @show, [(shift @ARGV), 0]; + } elsif (/^-S(.+)$/) { + push @show, [$1, 0]; + } elsif (argcmp($_, '--show-raw', 6, $arg)) { + push @show, [$arg, 0]; + } elsif (argcmp($_, '--show-all', 6)) { + push @show, ["*", 1]; + } elsif (argcmp($_, '--show-all-raw', 9)) { + push @show, ["*", 0]; + } elsif (/^-t$/ || argcmp($_, '--timeout', 1)) { + usage if @ARGV == 0; + $Testie::timeout = shift @ARGV; + } elsif (/^-t(.+)$/) { + $Testie::timeout = $1; + } elsif (argcmp($_, '--timeout', 1, $arg)) { + $Testie::timeout = $arg; + } elsif (/^-j$/ || argcmp($_, "--jobs", 1)) { + usage if @ARGV == 0 || $ARGV[0] !~ /\A\d+\z/; + $jobs = shift @ARGV; + } elsif (/^-j(\d+)$/) { + $jobs = $1; + } elsif (argcmp($_, "--jobs", 1, $arg) && $arg =~ /\A\d+\z/) { + $jobs = $arg; + } else { + usage; + } +} + +# prepend to path +if (@pathprepend) { + my($i, $cwd); + chomp($cwd = `pwd`); + for ($i = 0; $i != @pathprepend; ++$i) { + if ($pathprepend[$i] !~ m{\A/}) { + $pathprepend[$i] =~ s{\A\./}{}; + $pathprepend[$i] = $cwd . "/" . $pathprepend[$i]; + } + } + $ENV{"PATH"} = join(":", @pathprepend) . ":" . $ENV{"PATH"}; +} + +# check @show for stdout/stderr +foreach my $s (@show) { + $show_stdout = 1 if $s->[0] eq 'stdout' || $s->[0] eq "*"; + $show_stderr = 1 if $s->[0] eq 'stderr' || $s->[0] eq "*"; +} + +push @tests, '-' if !@tests; +my($tctr) = TestieCounter::new; +my($teh) = TestieErrorHandler::new(@tests > 1 && -t STDERR); + +if ($jobs && $jobs > 1) { + testie_parent_loop(@tests, $tctr, $teh, $jobs); +} else { + my($testnumber) = 0; + foreach my $test (@tests) { + my($tctr1) = run_test($test, $teh, $testnumber); + $tctr->add($tctr1); + ++$testnumber; + } +} + +# Print messages about failed requirements +@require_error_commands = sort { $a cmp $b } @{$tctr->{"require_error_commands"}}; +if (@require_error_commands) { + # make list unique + for (my $i = 1; $i < @require_error_commands; ) { + if ($require_error_commands[$i] eq $require_error_commands[$i - 1]) { + splice(@require_error_commands, $i, 1); + } else { + ++$i; + } + } + $teh->message("testie: requirement failures blocked ", $tctr->{"require_errors"}, ($tctr->{"require_errors"} > 1 ? " tests" : " test"), ", use '-V' for details\n"); + $teh->message("testie: (", (@require_error_commands > 1 ? "commands" : "command"), " '", join("', '", @require_error_commands), "')\n"); +} + +my($attempts, $failures, $skips, $successes) = + ($tctr->{"test_attempts"}, $tctr->{"test_failures"}, $tctr->{"test_skips"}, + $tctr->{"test_attempts"} - $tctr->{"test_failures"} - $tctr->{"test_skips"}); +$teh->message("testie: ", + $successes, ($successes == 1 ? " success, " : " successes, "), + $failures, ($failures == 1 ? " failure, " : " failures, "), + $skips, " skipped\n"); + +if ($tctr->{"bad_files"} > 0) { + exit(2); +} elsif ($attempts == 0 + || ($tctr->{"errors"} == 0 && $skips < $attempts)) { + exit(0); +} else { + exit(1); +} + + +=pod + +=head1 NAME + +testie - simple test harness + +=head1 SYNOPSIS + +testie [OPTIONS] [FILE]... + +=head1 DESCRIPTION + +Testie is a simple test harness. A testie test comprises a shell +script and, optionally, input and expected output files for that +script. Testie runs the script; the test succeeds if all of the script +commands succeed, and the actual output files match expectations. + +Testie accepts test filenames and directories as arguments. +Directories are recursively searched for F<*.testie> files. It +reports problems for failed tests, plus a summary. + +Testie exits with status 0 if all tests succeed, 1 if any test fails, +and 2 if a test fails due to an internal error. Tests whose B<%require> +prerequisites fail do not affect the exit status, except that if all +tests' prerequisites fail, the return status is 1 instead of 0. + +=head1 OPTIONS + +=over 8 + +=item B<-j>I, B<--jobs>=I + +Run up to I tests simultaneously. Like Make's B<-j> option. + +=item I=I + +Provide an environment variable setting for I within the script. + +=item B<-s>, B<--show> I + +Echo the contents of I on completion. I should be one of the +filenames specified by B<%file> or B<%expect>, or B or B. +Leaves out any ignored lines. + +=item B<-S>, B<--show-raw> I + +Like B<--show>, but includes any ignored lines. + +=item B<--show-all> + +Calls B<--show> for all filenames specified by any B<%expect>, plus B +and B. Leaves out any ignored lines. + +=item B<--show-all-raw> + +Like B<--show-all>, but includes any ignored lines. + +=item B<-e>, B<--expand> + +Don't run the given test; instead, expand its files into the current +directory. The script is stored in a file called F<%script>. + +=item B<--preserve-temporaries> + +Preserve temporary test directories. Testie runs each test in its own +subdirectory of the current directory. Test directories are named +F, and are typically removed on test completion. +Examining the contents of a test directory can be useful when +debugging a test. + +=item B<-p>, B<--path> I + +Prepend I to the C environment variable before running the +test script. + +=item B<-V>, B<--verbose> + +Print information to standard error about successful tests as well as +unsuccessful tests. + +=item B<-VV>, B<--superverbose> + +Like B<--verbose>, but use a slightly different format, and +additionally print every test's B<%info> section before the test results. + +=item B<-q>, B<--quiet> + +Don't print information to the terminal while running multiple tests. + +=item B<-v>, B<--version> + +Print version number information and exit. + +=item B<--help> + +Print help information and exit. + +=back + +=head1 FILE FORMAT + +Testie test files consist of several sections, each introduced by a line +starting with B<%>. There must be, at least, a B<%script> section. +The B<%file> and B<%expect> sections define input and output files by +name. + +=over 8 + +=item B<%script> + +The B shell script that controls the test. Testie will run each +command in sequence. Every command in the script must succeed, with +exit status 0, or the test will fail. Use B<%file> sections to define +script input files and B<%expect> sections to check script output files +for expected values. + +The B<%script> section can contain subtests. To start a new subtest, +execute a command like S>. Testie will +report the problematic C when standard output or error +doesn't match an expected value. + +The script's environment is populated with any Is set on the +testie command line with B=I> syntax. Also, the +B<$rundir> environment variable is set to the directory in which +testie was originally run. + +=item B<%require [-q]> + +An B shell script defining prerequisites that must be satisfied +before the test can run. Every command in the script must succeed, with +exit status 0, for the test to run. Standard output and error are not +checked, however. The B<-q> flag tells testie not to print an error message +if a requirement fails. + +Testie runs the requirement script before creating any other test files. +For example, contents of B<%file> sections are not available. + +=item B<%info> + +A short description of the test. In B<--superverbose> mode, the first +paragraph of its contents is printed before the test results. + +=item B<%cut> + +This section is ignored. It is intended to comment out obsolete parts of +the test. + +=item B<%file [-de] [+I] I...> + +Create an input file for the script. I can be B, +which sets the script's standard input. If B<+>I is provided, +the file data consists of the I bytes following this line; +otherwise, it consists of the data up to the next section. The B<-d> +flag tells testie to delete the first character of each line in the +section. The B<-e> flag indicates that the section was MIME +Base64-encoded (see L); it is decoded before use. To +include a file with lines that start with B<%> (which would normally +start a new section), use B<-d> and preface each line of the file with +a space, or use B<-e>. + +=item B<%expect [-adeiw] [+I] I...> + +Define an expected output file. Differences between the script's +output I and the contents of the B<%expect> section will +cause the test to fail. + +I can be B, for standard output. If B<+>I is +provided, the file data consists of the I bytes following this +line; otherwise, it consists of the data up to the next section. + +After running the script, testie compares the I generated by +the script with the provided data. The files are compared +line-by-line. Testie ignores blank lines, differences in trailing +whitespace, and lines in the script output that match B<%ignore> +patterns (see below). The B<-w> flag causes testie to ignore +differences in amount of whitespace within each line. + +B<%expect> lines can contain Perl regular expressions, enclosed by two +sets of braces. The B<%expect> line + + foo{{(bar)?}} + +matches either C or C. The B<-i> flag makes all such +regular expressions case-insensitive. (Text outside of regular +expressions must match case.) + +Document an B<%expect> line with C<{{?comment}}> blocks. For example: + + foo {{? the sort was in the right order}} + +Testie ignores whitespace before and after the C<{{?comment}}> block, and if +the actual output differs from this expected line, it prints the comment in +addition to the line differences. + +The B<-a> flag marks this expected output as an alternate. Testie will +compare the script's output file with each provided alternate; the +test succeeds if any of the alternates match. The B<-d> flag behaves +as in B<%file>. + +=item B<%expectv [-ade] [+I] I...> + +Define a literal expected output file. This behaves like B<%expect>, +except that the script's output file must match the provided data +I: B<%expectv> never ignores whitespace differences, does not +treat C<{{}}> blocks as regular expressions, and does not parse +B<%ignore> patterns. + +=item B<%expectx [-adiw] [+I] I...> + +Define a regular-expression expected output file. This behaves like +B<%expect>, except that every line is treated as a regular expression. +C<{{?comment}}> blocks are ignored, but other brace pairs are treated +according to the normal regular expression rules. + +=item B<%stdin [-de] [+I]> + +Same as B<%file stdin>. + +=item B<%stdout [-adeiw] [+I]> + +Same as B<%expect stdout>. + +=item B<%stderr [-adeiw] [+I]> + +Same as B<%expect stderr>. + +=item B<%ignorex [-di] [+I] [I]> + +Each line in the B<%ignorex> section is a Perl regular expression. Lines in +the supplied I that match any of those regular expressions will not +be considered when comparing files with B<%expect> data. The regular +expression must match the whole line. I may be B, in which case +the regular expressions will apply to all B<%expect> files. C<{{?comment}}> +blocks are ignored. + +=item B<%ignore>, B<%ignorev [-adeiw] [+I] [I]> + +Like B<%ignorex>, but B<%ignore> parses regular expressions only inside +double braces (C<{{ }}>), and B<%ignorev> lines must match exactly. + +=item B<%include I> + +Interpolate the contents of another testie file. + +=item B<%eot> + +Marks the end of the current test. The rest of the file will be parsed for +additional tests. + +=item B<%eof> + +The rest of the file is ignored. + +=back + +=head1 EXAMPLE + +This simple testie script checks that 'grep -c' works for a simple output +file. + + %script + grep -c B. + %stdin + Bfoo + B + %stdout + 1 + +=head1 ENVIRONMENT + +By default, testie sets the C environment variable to "C"; without +this setting commands like B have unpredictable effects. To set +C to another value, set it in the B<%script> section. + +=head1 AUTHOR + +Eddie Kohler, diff --git a/packages/optimizt-gifsicle/verification/benchmark.json b/packages/optimizt-gifsicle/verification/benchmark.json new file mode 100644 index 0000000..4e83f21 --- /dev/null +++ b/packages/optimizt-gifsicle/verification/benchmark.json @@ -0,0 +1,98 @@ +{ + "schemaVersion": 1, + "environment": { + "platform": "linux", + "architecture": "arm64", + "node": "v24.18.0", + "cpus": [ + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown" + ], + "nativeProfile": "Linux x64, GCC 14.2.0, -O3 -DNDEBUG, no threads, no SIMD" + }, + "method": { + "wasmDurationsIncludeFreshWorkerAndWebAssemblyInitialization": true, + "nativeDurationsIncludeFreshProcessInitialization": true, + "hardRatioGate": false + }, + "input": { + "fixture": "animated.gif", + "size": 8703 + }, + "results": [ + { + "name": "no-options", + "options": {}, + "wasm": { + "iterations": 5, + "meanMilliseconds": 24.955196399999995, + "minimumMilliseconds": 22.49634899999998, + "maximumMilliseconds": 31.140494999999994, + "outputSize": 8703 + }, + "native": { + "iterations": 5, + "meanMilliseconds": 20.017408999999994, + "minimumMilliseconds": 17.72041999999999, + "maximumMilliseconds": 26.95790199999999, + "outputSize": 8703 + } + }, + { + "name": "lossless", + "options": { + "optimize": 3, + "careful": true, + "colors": 256, + "lossy": 0, + "gamma": 1 + }, + "wasm": { + "iterations": 5, + "meanMilliseconds": 27.558294799999988, + "minimumMilliseconds": 26.383482999999956, + "maximumMilliseconds": 29.06028200000003, + "outputSize": 8711 + }, + "native": { + "iterations": 5, + "meanMilliseconds": 21.889247, + "minimumMilliseconds": 21.172679000000016, + "maximumMilliseconds": 22.778391, + "outputSize": 8711 + } + }, + { + "name": "lossy", + "options": { + "optimize": 3, + "careful": false, + "colors": 256, + "lossy": 100, + "gamma": 1 + }, + "wasm": { + "iterations": 5, + "meanMilliseconds": 31.115703800000006, + "minimumMilliseconds": 30.21607700000004, + "maximumMilliseconds": 32.178122999999914, + "outputSize": 7449 + }, + "native": { + "iterations": 5, + "meanMilliseconds": 24.754871000000026, + "minimumMilliseconds": 23.890310999999997, + "maximumMilliseconds": 25.596857, + "outputSize": 7449 + } + } + ] +} diff --git a/packages/optimizt-gifsicle/verification/native-reference.md b/packages/optimizt-gifsicle/verification/native-reference.md new file mode 100644 index 0000000..95323b2 --- /dev/null +++ b/packages/optimizt-gifsicle/verification/native-reference.md @@ -0,0 +1,27 @@ +# Native reference environment + +Canonical native parity uses Linux x64 and GCC 14.2.0 with `-O3 -DNDEBUG`, no +threads, and no SIMD. The build compiles the same pristine snapshot and exact +integration patch as the WebAssembly distribution. + +The canonical container is the official `gcc:14.2.0` image pinned by immutable +OCI index digest: + +```text +gcc:14.2.0@sha256:b99b86a28812b1e6453a231a947dc43d76fe192788a12f344a9b568bf9f5d24c +``` + +The Linux amd64 manifest referenced by that index is: + +```text +sha256:82549aa8f90ada3236a8be70c74543132a76662ef33f0c3271ed802b81584a82 +``` + +Run `npm run verify:parity`; its pre-script builds the native reference in that +container before comparing it with WebAssembly. To build directly inside the +container, invoke `npm run build:native`. For release builds, the script verifies +Linux, an x86-64 GNU compiler target, and `gcc -dumpfullversion -dumpversion` +before compiling. Sanitizer verification may +run the same bridge natively on another architecture, but it does not generate +the canonical parity reference. The resulting executable is a verification +artifact and is not included in the npm package. diff --git a/packages/optimizt-gifsicle/verification/parity-manifest.json b/packages/optimizt-gifsicle/verification/parity-manifest.json new file mode 100644 index 0000000..fea1a3a --- /dev/null +++ b/packages/optimizt-gifsicle/verification/parity-manifest.json @@ -0,0 +1,373 @@ +{ + "schemaVersion": 1, + "gifsicle": "1.96", + "nativeProfile": "Linux x64, GCC 14.2.0, -O3 -DNDEBUG, no threads, no SIMD", + "cases": [ + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": {}, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "optimize": 0 + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "optimize": 1 + }, + "native": { + "size": 9652, + "sha256": "6ed8b4f782a470894377f4baea421549296d57690b987be04c3756bd206c3d0f" + }, + "wasm": { + "size": 9652, + "sha256": "6ed8b4f782a470894377f4baea421549296d57690b987be04c3756bd206c3d0f" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "optimize": 2 + }, + "native": { + "size": 8671, + "sha256": "59b3c0615b30e60a164def8201a98a870a806ea4bb296ba58af1df9005750767" + }, + "wasm": { + "size": 8671, + "sha256": "59b3c0615b30e60a164def8201a98a870a806ea4bb296ba58af1df9005750767" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "optimize": 3 + }, + "native": { + "size": 8637, + "sha256": "4dbe61e5d2aca8a1f408e4d2798c141acb87b456f878f5dcdc99c44b31bff8ce" + }, + "wasm": { + "size": 8637, + "sha256": "4dbe61e5d2aca8a1f408e4d2798c141acb87b456f878f5dcdc99c44b31bff8ce" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "careful": false + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "careful": true + }, + "native": { + "size": 8745, + "sha256": "3780b1dbfe527474030120a4d13d9cef3d5c8010da3a1304d136b77c73a8c26a" + }, + "wasm": { + "size": 8745, + "sha256": "3780b1dbfe527474030120a4d13d9cef3d5c8010da3a1304d136b77c73a8c26a" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "colors": 2 + }, + "native": { + "size": 4543, + "sha256": "8b6d90a15bd7bc22f3db56b32e1563ccd34d64f305ad58fa5f3b1bfbb83b6c30" + }, + "wasm": { + "size": 4543, + "sha256": "8b6d90a15bd7bc22f3db56b32e1563ccd34d64f305ad58fa5f3b1bfbb83b6c30" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "colors": 128 + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "colors": 256 + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "lossy": 0 + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "lossy": 20 + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "lossy": 100 + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "lossy": 2147483647 + }, + "native": { + "size": 4758, + "sha256": "ac0c1d00e293468925a173f1e18c28e193cdad1e481dcee7f9f3542c92a9c648" + }, + "wasm": { + "size": 4758, + "sha256": "ac0c1d00e293468925a173f1e18c28e193cdad1e481dcee7f9f3542c92a9c648" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "gamma": 1 + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "gamma": 2.2 + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "gamma": "srgb" + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "gamma": "oklab" + }, + "native": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + }, + "wasm": { + "size": 8703, + "sha256": "0e79171206b236d2f3e3384616c85ae4e8775708020175dcef61e3dcf1d9edd8" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "optimize": 3, + "careful": false, + "colors": 256, + "lossy": 100, + "gamma": 1 + }, + "native": { + "size": 7449, + "sha256": "cdc7004eec18743dba65eca534cc124000fd083ca9556cdc69683698ad4b0451" + }, + "wasm": { + "size": 7449, + "sha256": "cdc7004eec18743dba65eca534cc124000fd083ca9556cdc69683698ad4b0451" + } + }, + { + "fixture": "animated.gif", + "inputSha256": "547d09ab752a4289aa275344efee0fa4f0ce3bcddaa3793d152d0af1998d2827", + "options": { + "optimize": 0, + "careful": true, + "colors": 256, + "lossy": 0, + "gamma": 1 + }, + "native": { + "size": 8745, + "sha256": "3780b1dbfe527474030120a4d13d9cef3d5c8010da3a1304d136b77c73a8c26a" + }, + "wasm": { + "size": 8745, + "sha256": "3780b1dbfe527474030120a4d13d9cef3d5c8010da3a1304d136b77c73a8c26a" + } + }, + { + "fixture": "single.gif", + "inputSha256": "e1bcfc0f5ea18cc6592ee2dce22b85a26209502182d86db69607ff9279efe559", + "options": { + "optimize": 3 + }, + "native": { + "size": 821, + "sha256": "cc31482771ecc5ecbc30892c7c065a54f983345c3f6d61b6dd39aee44f0d9982" + }, + "wasm": { + "size": 821, + "sha256": "cc31482771ecc5ecbc30892c7c065a54f983345c3f6d61b6dd39aee44f0d9982" + } + }, + { + "fixture": "transparent.gif", + "inputSha256": "fed87c356b391d20acdf3559e8e43748c65a17311b1523dfe115d0a95af91382", + "options": { + "optimize": 3 + }, + "native": { + "size": 162, + "sha256": "6cd47e242da7c2c439e8bf6f0e8dfc1881ca1d39c5d5fa84b189657280526359" + }, + "wasm": { + "size": 162, + "sha256": "6cd47e242da7c2c439e8bf6f0e8dfc1881ca1d39c5d5fa84b189657280526359" + } + }, + { + "fixture": "local-palette.gif", + "inputSha256": "2336b2d3d46b59f5b7ece15e1c38bd5921854826be956e73ab22f820b5374faf", + "options": { + "optimize": 3 + }, + "native": { + "size": 821, + "sha256": "cc31482771ecc5ecbc30892c7c065a54f983345c3f6d61b6dd39aee44f0d9982" + }, + "wasm": { + "size": 821, + "sha256": "cc31482771ecc5ecbc30892c7c065a54f983345c3f6d61b6dd39aee44f0d9982" + } + }, + { + "fixture": "extensions.gif", + "inputSha256": "5199e0a884b4feca9e593263fadde07f42355459f34d63b92a8e5b5438cc3f22", + "options": { + "optimize": 3 + }, + "native": { + "size": 856, + "sha256": "93a079f084617087e4941e29b40b7c66917a3b3cd86ff430da750f29090ea1ac" + }, + "wasm": { + "size": 856, + "sha256": "93a079f084617087e4941e29b40b7c66917a3b3cd86ff430da750f29090ea1ac" + } + } + ] +} diff --git a/packages/optimizt/CHANGELOG.md b/packages/optimizt/CHANGELOG.md index d718751..4c1a00c 100644 --- a/packages/optimizt/CHANGELOG.md +++ b/packages/optimizt/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Guetzli now uses wasm64 and requires Node.js 24.0.0 or newer. +- **BREAKING:** Guetzli and Gifsicle now use wasm64 and require Node.js 24.0.0 or newer. - **BREAKING:** Processing now exits with status `1` when argument validation, filesystem preflight, configuration loading, or any image operation fails. A valid run with no eligible images still exits `0`. - **BREAKING:** Status, progress, warnings, errors, and summaries now use `stderr`. `stdout` is reserved for help and version output and remains empty during image processing. - **BREAKING:** Missing or inaccessible explicit operands and explicitly supplied files with unsupported extensions now fail instead of being silently ignored. Unsupported files found while traversing directories remain ignored. diff --git a/packages/optimizt/MIGRATION.md b/packages/optimizt/MIGRATION.md index a81db2d..89084af 100644 --- a/packages/optimizt/MIGRATION.md +++ b/packages/optimizt/MIGRATION.md @@ -2,7 +2,7 @@ ## 13.0.0 → Unreleased -Optimizt now requires Node.js 24.0.0 or newer because its bundled Guetzli runtime uses WebAssembly memory64. +Optimizt now requires Node.js 24.0.0 or newer because its bundled Guetzli and Gifsicle runtimes use WebAssembly memory64. It also treats the exit status and files on disk as the command result. Review scripts and custom configuration for the changes below. diff --git a/packages/optimizt/convert.js b/packages/optimizt/convert.js index 26f6497..640f4dc 100644 --- a/packages/optimizt/convert.js +++ b/packages/optimizt/convert.js @@ -1,43 +1,176 @@ -import fs from 'node:fs/promises'; +import fs from 'node:fs'; +import os from 'node:os'; + +import pLimit from 'p-limit'; import { atomicWrite } from './lib/atomic-write.js'; import { calculateRatio } from './lib/calculate-ratio.js'; +import { createProgressBarContainer } from './lib/create-progress-bar-container.js'; import { SUPPORTED_FILE_TYPES } from './lib/constants.js'; import { describeCodecFailure } from './lib/describe-codec-failure.js'; -import { createOperationRuntime } from './lib/operation-runtime.js'; -import { parseImageMetadata } from './lib/parse-image-metadata.js'; +import { formatBytes } from './lib/format-bytes.js'; +import { getPlural } from './lib/get-plural.js'; +import { getRelativePath } from './lib/get-relative-path.js'; +import { isInterrupted } from './lib/lifecycle.js'; import sharp from './lib/sharp.js'; +import { + LOG_TYPES, + log, + logProgress, + logProgressVerbose, +} from './lib/log.js'; +import { OUTCOME_STATUS } from './lib/outcome-status.js'; +import { parseImageMetadata } from './lib/parse-image-metadata.js'; +import { programOptions } from './lib/program-options.js'; +import { showTotal } from './lib/show-total.js'; + +export async function convert({ operations, config, configPath }) { + const { isLossless } = programOptions; + const filePathsCount = new Set(operations.map(operation => operation.input)).size; + + if (!filePathsCount) { + return; + } + + log(`Converting ${filePathsCount} ${getPlural(filePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})`); + + const progressBarTotal = operations.length; + const progressBarContainer = createProgressBarContainer(progressBarTotal); + const progressBar = progressBarContainer.create(progressBarTotal, 0); + + const totalSize = { before: 0, after: 0 }; + + const getConfig = format => config?.[format]?.[isLossless ? 'lossless' : 'lossy']; + + const avifConfig = getConfig('avif'); + const webpConfig = getConfig('webp'); -export async function convert({ operations, config, configPath, lifecycle, options, reporter }) { - const runtime = createOperationRuntime({ - detect: (_operation, input) => parseImageMetadata(input), - lifecycle, - read: operation => fs.readFile(operation.input), - writer: { write: atomicWrite }, - }); - return runtime.execute({ - onCompleted: reporter.operationCompleted, - operations, - policy: { - complete({ encoded, input }) { - return { after: encoded.length, before: input.length, ratio: calculateRatio(input.length, encoded.length), write: true }; - }, - }, - selectCodec(metadata, operation) { - if (!metadata.format) throw new Error('Unable to read the image format. Check that the file is a valid, supported image.'); - if (!SUPPORTED_FILE_TYPES.CONVERT.includes(metadata.format)) throw new Error(`Unsupported image format: "${metadata.format}"`); - return { encode: input => encode(input, metadata, operation.format) }; - }, - }); - - async function encode(input, metadata, format) { - if (format === 'avif' && metadata.pages > 1) throw new Error('Unable to create an animated AVIF. Use WebP or provide a non-animated image.'); - const codecConfig = config?.[format]?.[options.isLossless ? 'lossless' : 'lossy'] || {}; - try { - const image = sharp(input, { animated: format === 'webp' && metadata.pages > 1 }).rotate(); - return await image[format](codecConfig).toBuffer(); - } catch (error) { - throw describeCodecFailure({ configPath, error, format, isLossless: options.isLossless, mode: 'convert' }); + const cpuCount = os.cpus().length; + const tasksSimultaneousLimit = pLimit(cpuCount); + + const outcomes = await Promise.all(operations.map((operation, planIndex) => tasksSimultaneousLimit(() => { + if (isInterrupted()) return { planIndex, status: OUTCOME_STATUS.UNSTARTED }; + const isAvif = operation.format === 'avif'; + return processFile({ + config: (isAvif ? avifConfig : webpConfig) || {}, + configPath, + filePath: { input: operation.input, output: operation.output }, + format: isAvif ? 'AVIF' : 'WebP', + isLossless, + processFunction: isAvif ? processAvif : processWebp, + progressBar, + progressBarContainer, + planIndex, + skipReason: operation.skipReason, + totalSize, + }); + }))); + + progressBarContainer.update(); // Prevent logs lost. See: https://github.com/npkgz/cli-progress/issues/145#issuecomment-1859594159 + progressBarContainer.stop(); + + showTotal(totalSize.before, totalSize.after, outcomes, { conversion: true }); + return { failed: outcomes.filter(outcome => outcome.status === OUTCOME_STATUS.FAILED).length, outcomes }; +} + +async function processFile({ + filePath, + config, + configPath, + isLossless, + progressBarContainer, + progressBar, + planIndex, + totalSize, + skipReason, + format, + processFunction, +}) { + const outputFilePath = filePath.output; + + try { + if (skipReason) { + logProgressVerbose(getRelativePath(outputFilePath), { + description: `Output file already exists: ${outputFilePath}. Use --force to replace it.`, + progressBarContainer, + }); + + return { planIndex, status: OUTCOME_STATUS.SKIPPED }; } + + const fileBuffer = await fs.promises.readFile(filePath.input); + const processedFileBuffer = await processFunction({ fileBuffer, config, configPath, isLossless }); + + await atomicWrite(outputFilePath, processedFileBuffer); + + const fileSize = fileBuffer.length; + const processedFileSize = processedFileBuffer.length; + + totalSize.before += fileSize; + totalSize.after += Math.min(fileSize, processedFileSize); + + const ratio = calculateRatio(fileSize, processedFileSize); + const before = formatBytes(fileSize); + const after = formatBytes(processedFileSize); + + logProgress(getRelativePath(outputFilePath), { + type: LOG_TYPES.SUCCESS, + description: `${before} → ${format} ${after}. Ratio: ${ratio}%`, + progressBarContainer, + }); + return { after: processedFileSize, before: fileSize, planIndex, status: OUTCOME_STATUS.PROCESSED }; + } catch (error) { + // Work abandoned during shutdown is left undone by the interruption, not failed. + if (isInterrupted()) return { planIndex, status: OUTCOME_STATUS.UNSTARTED }; + return { error, output: outputFilePath, planIndex, status: OUTCOME_STATUS.FAILED }; + } finally { + progressBar.increment(); + } +} + +async function processAvif({ fileBuffer, config, configPath, isLossless }) { + const imageMetadata = await parseImageMetadata(fileBuffer); + checkImageFormat(imageMetadata.format); + + const isAnimated = imageMetadata.pages > 1; + + if (isAnimated) { + throw new Error('Unable to create an animated AVIF. Use WebP or provide a non-animated image.'); // See: https://github.com/strukturag/libheif/issues/377 + } + + // Only the codec call is enriched, so detection and support errors keep speaking for themselves. + try { + return await sharp(fileBuffer) + .rotate() // Rotate image using information from EXIF Orientation tag + .avif(config) + .toBuffer(); + } catch (error) { + throw describeCodecFailure({ configPath, error, format: 'avif', isLossless, mode: 'convert' }); + } +} + +async function processWebp({ fileBuffer, config, configPath, isLossless }) { + const imageMetadata = await parseImageMetadata(fileBuffer); + checkImageFormat(imageMetadata.format); + + const isAnimated = imageMetadata.pages > 1; + + try { + return await sharp(fileBuffer, { animated: isAnimated }) + .rotate() // Rotate image using information from EXIF Orientation tag + .webp(config) + .toBuffer(); + } catch (error) { + throw describeCodecFailure({ configPath, error, format: 'webp', isLossless, mode: 'convert' }); + } +} + +function checkImageFormat(imageFormat) { + if (!imageFormat) { + throw new Error('Unable to read the image format. Check that the file is a valid, supported image.'); + } + + if (!SUPPORTED_FILE_TYPES.CONVERT.includes(imageFormat)) { + throw new Error(`Unsupported image format: "${imageFormat}"`); } } diff --git a/packages/optimizt/lib/gifsicle.js b/packages/optimizt/lib/gifsicle.js new file mode 100644 index 0000000..79a4199 --- /dev/null +++ b/packages/optimizt/lib/gifsicle.js @@ -0,0 +1 @@ +export { createOperation as createGifOperation } from '../vendor/gifsicle/index.js'; diff --git a/packages/optimizt/lib/guetzli.js b/packages/optimizt/lib/guetzli.js index e5595b5..9fee860 100644 --- a/packages/optimizt/lib/guetzli.js +++ b/packages/optimizt/lib/guetzli.js @@ -1,61 +1,8 @@ -import { spawn } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { optionsToArguments } from './options-to-arguments.js'; - -const DIAGNOSTIC_LIMIT = 64 * 1024; - export const nodeExecutable = process.execPath; export const guetzliRunner = path.resolve( path.dirname(fileURLToPath(import.meta.url)), '../vendor/guetzli/cli.js', ); - -export function encodeWithGuetzli(input, options, lifecycle, { - command = nodeExecutable, - runner = guetzliRunner, -} = {}) { - return new Promise((resolve, reject) => { - const child = spawn(command, [runner, ...optionsToArguments({ options })]); - lifecycle.registerChild(child); - const stdout = []; - let diagnostic = Buffer.alloc(0); - let isTruncated = false; - let isSettled = false; - - child.stdout.on('data', (chunk) => { - stdout.push(chunk); - }); - child.stderr.on('data', (chunk) => { - diagnostic = Buffer.concat([diagnostic, chunk]); - if (diagnostic.length > DIAGNOSTIC_LIMIT) { - diagnostic = diagnostic.subarray(diagnostic.length - DIAGNOSTIC_LIMIT); - isTruncated = true; - } - }); - child.once('error', error => rejectOnce(new Error(`Guetzli failed to start: ${error.message}`, { cause: error }))); - child.stdin.on('error', (error) => { - if (error.code !== 'EPIPE') rejectOnce(new Error(`Could not send input to Guetzli: ${error.message}`, { cause: error })); - }); - child.once('close', (code, signal) => { - if (isSettled) return; - if (code === 0) { - isSettled = true; - resolve(Buffer.concat(stdout)); - return; - } - const reason = diagnostic.toString('utf8').trim(); - const exit = signal ? `terminated by signal ${signal}` : `exited with code ${code}`; - const prefix = isTruncated ? '[stderr truncated] ' : ''; - rejectOnce(new Error(`${prefix}${reason || 'Guetzli failed'} (${exit})`)); - }); - child.stdin.end(input); - - function rejectOnce(error) { - if (isSettled) return; - isSettled = true; - reject(error); - } - }); -} diff --git a/packages/optimizt/lib/lifecycle.js b/packages/optimizt/lib/lifecycle.js index f0630ca..59a029c 100644 --- a/packages/optimizt/lib/lifecycle.js +++ b/packages/optimizt/lib/lifecycle.js @@ -1,5 +1,6 @@ const state = { activeChildren: new Set(), + activeCancellations: new Set(), forceExitTimer: undefined, interruptCount: 0, interruptedSignal: undefined, @@ -14,17 +15,28 @@ export function registerChild(child) { if (isInterrupted()) child.kill('SIGTERM'); } +export function registerCancellable(cancel, completion) { + state.activeCancellations.add(cancel); + void completion.then( + () => state.activeCancellations.delete(cancel), + () => state.activeCancellations.delete(cancel), + ); + + if (isInterrupted()) void cancel(); +} + export function installSignalHandlers() { for (const signal of ['SIGINT', 'SIGTERM']) { process.on(signal, () => { state.interruptCount += 1; state.interruptedSignal ||= signal; // A second interrupt is the emergency escape hatch and must not wait for cleanup. - if (state.interruptCount > 1) process.exit(signalExitCode(signal)); // eslint-disable-line n/no-process-exit + if (state.interruptCount > 1) return forceExit(signal); for (const child of state.activeChildren) child.kill('SIGTERM'); + for (const cancel of state.activeCancellations) void cancel(); // Sharp offers no cancellation for a running pipeline, so shutdown is bounded // instead: whatever is still native-bound loses the process after five seconds. - state.forceExitTimer = setTimeout(() => process.exit(signalExitCode(signal)), 5000); // eslint-disable-line n/no-process-exit + state.forceExitTimer = setTimeout(() => forceExit(signal), 5000); state.forceExitTimer.unref(); }); } @@ -39,6 +51,14 @@ export function finishLifecycle() { return state.interruptedSignal ? signalExitCode(state.interruptedSignal) : undefined; } +function forceExit(signal) { + // Forced shutdown must bypass synchronous exit hooks too: Sharp WASM's hook + // waits for libvips workers and can deadlock while a pipeline is still active. + // Normal completion keeps these hooks; only the emergency paths abandon cleanup. + process.removeAllListeners('exit'); + process.exit(signalExitCode(signal)); // eslint-disable-line n/no-process-exit +} + function signalExitCode(signal) { if (process.platform === 'win32') return 1; return signal === 'SIGINT' ? 130 : 143; diff --git a/packages/optimizt/optimize.js b/packages/optimizt/optimize.js index 8193d2b..01ca3b8 100644 --- a/packages/optimizt/optimize.js +++ b/packages/optimizt/optimize.js @@ -1,64 +1,248 @@ -import fs from 'node:fs/promises'; +import { spawn } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import pLimit from 'p-limit'; import { optimize as svgoOptimize } from 'svgo'; import { atomicWrite } from './lib/atomic-write.js'; +import { guetzliRunner, nodeExecutable } from './lib/guetzli.js'; import { calculateRatio } from './lib/calculate-ratio.js'; +import { createProgressBarContainer } from './lib/create-progress-bar-container.js'; import { describeCodecFailure } from './lib/describe-codec-failure.js'; +import { formatBytes } from './lib/format-bytes.js'; import { createGifOperation } from './lib/gifsicle.js'; -import { encodeWithGuetzli } from './lib/guetzli.js'; -import { createOperationRuntime } from './lib/operation-runtime.js'; -import { parseImageMetadata } from './lib/parse-image-metadata.js'; +import { getPlural } from './lib/get-plural.js'; +import { getRelativePath } from './lib/get-relative-path.js'; +import { isInterrupted, registerCancellable, registerChild } from './lib/lifecycle.js'; import sharp from './lib/sharp.js'; +import { + LOG_TYPES, + log, + logProgress, + logProgressVerbose, +} from './lib/log.js'; +import { optionsToArguments } from './lib/options-to-arguments.js'; +import { OUTCOME_STATUS } from './lib/outcome-status.js'; +import { parseImageMetadata } from './lib/parse-image-metadata.js'; +import { programOptions } from './lib/program-options.js'; +import { showTotal } from './lib/show-total.js'; -export async function optimize({ operations, config, configPath, lifecycle, options, reporter }) { - const runtime = createOperationRuntime({ - detect: (_operation, input) => parseImageMetadata(input), - lifecycle, - read: operation => fs.readFile(operation.input), - writer: { write: atomicWrite }, - }); - return runtime.execute({ - onCompleted: reporter.operationCompleted, - operations, - policy: { - complete({ encoded, input, metadata }) { - const ratio = calculateRatio(input.length, encoded.length); - const isChanged = !input.equals(encoded); - const isWrite = ratio > 0 || (metadata.format === 'svg' && isChanged); - return { before: input.length, after: isWrite ? encoded.length : input.length, ratio, write: isWrite }; // eslint-disable-line unicorn/prefer-minimal-ternary - }, - }, - selectCodec(metadata) { - const codec = CODECS.get(metadata.format); - if (!metadata.format) throw new Error('Unable to read the image format. Check that the file is a valid, supported image.'); - if (!codec) throw new Error(`Unsupported image format: "${metadata.format}"`); - return { - resource: metadata.format === 'jpeg' && options.isLossless ? 'guetzli' : undefined, - async encode(input) { - try { - return await codec({ config, input, isLossless: options.isLossless, lifecycle }); - } catch (error) { - throw describeCodecFailure({ configPath, error, format: metadata.format, isLossless: options.isLossless, mode: 'optimize' }); - } - }, - }; - }, - }); +export async function optimize({ operations, config, configPath }) { + const { isLossless } = programOptions; + const filePaths = operations.map(operation => ({ input: operation.input, output: operation.output })); + + const filePathsCount = filePaths.length; + + if (filePathsCount <= 0) { + return; + } + + log(`Optimizing ${filePathsCount} ${getPlural(filePathsCount, 'image', 'images')} (${isLossless ? 'lossless' : 'lossy'})`); + + const progressBarContainer = createProgressBarContainer(filePathsCount); + const progressBar = progressBarContainer.create(filePathsCount, 0); + + const totalSize = { before: 0, after: 0 }; + + const cpuCount = os.cpus().length; + const tasksSimultaneousLimit = pLimit(cpuCount); + const guetzliTasksSimultaneousLimit = pLimit(1); // Guetzli uses a large amount of memory and a significant amount of CPU time. To reduce system load, we only allow one instance of guetzli to run at the same time. + + const outcomes = await Promise.all( + filePaths.map((filePath, planIndex) => { + const extension = path.extname(filePath.input).toLowerCase(); + const isJpeg = extension === '.jpg' || extension === '.jpeg'; + + const limit = isJpeg && isLossless + ? guetzliTasksSimultaneousLimit + : tasksSimultaneousLimit; + + return limit(() => isInterrupted() + ? { planIndex, status: OUTCOME_STATUS.UNSTARTED } + : processFile({ + filePath, + config, + configPath, + progressBarContainer, + progressBar, + totalSize, + isLossless, + planIndex, + })); + }), + ); + + progressBarContainer.update(); // Prevent logs lost. See: https://github.com/npkgz/cli-progress/issues/145#issuecomment-1859594159 + progressBarContainer.stop(); + + showTotal(totalSize.before, totalSize.after, outcomes); + return { failed: outcomes.filter(outcome => outcome.status === OUTCOME_STATUS.FAILED).length, outcomes }; +} + +async function processFile({ + filePath, + config, + configPath, + progressBarContainer, + progressBar, + totalSize, + isLossless, + planIndex, +}) { + try { + const fileBuffer = await fs.promises.readFile(filePath.input); + const processedFileBuffer = await processFileByFormat({ fileBuffer, config, configPath, isLossless }); + + const fileSize = fileBuffer.length; + const processedFileSize = processedFileBuffer.length; + + totalSize.before += fileSize; + totalSize.after += Math.min(fileSize, processedFileSize); + + const ratio = calculateRatio(fileSize, processedFileSize); + + const isOptimized = ratio > 0; + const isChanged = !fileBuffer.equals(processedFileBuffer); + const isSvg = path.extname(filePath.input).toLowerCase() === '.svg'; + + if (!isOptimized && (!isChanged || !isSvg)) { + logProgressVerbose(getRelativePath(filePath.output), { + description: `${(isChanged ? 'File size increased' : 'Nothing changed')}. Skipped`, + progressBarContainer, + }); + + return { planIndex, status: OUTCOME_STATUS.SKIPPED }; + } + + await atomicWrite(filePath.output, processedFileBuffer); + + const before = formatBytes(fileSize); + const after = formatBytes(processedFileSize); + + logProgress(getRelativePath(filePath.output), { + type: isOptimized ? LOG_TYPES.SUCCESS : LOG_TYPES.WARNING, + description: `${before} → ${after}. Ratio: ${ratio}%`, + progressBarContainer, + }); + return { after: processedFileSize, before: fileSize, planIndex, status: OUTCOME_STATUS.PROCESSED }; + } catch (error) { + // Work abandoned during shutdown is left undone by the interruption, not failed. + if (isInterrupted()) return { planIndex, status: OUTCOME_STATUS.UNSTARTED }; + return { error, output: filePath.output, planIndex, status: OUTCOME_STATUS.FAILED }; + } finally { + progressBar.increment(); + } } -const CODECS = new Map([ - ['gif', ({ config, input, isLossless, lifecycle }) => { - const operation = createGifOperation(input, (isLossless ? config?.gif?.lossless : config?.gif?.lossy) || {}); - lifecycle.registerCancellable(operation.terminate, operation.promise); - return operation.promise; - }], - ['jpeg', async ({ config, input, isLossless, lifecycle }) => { - const image = sharp(input).rotate(); - if (!isLossless) return image.jpeg(config?.jpeg?.lossy || {}).toBuffer(); - const prepared = await image.toColorspace('srgb').jpeg({ quality: 100, optimizeCoding: false }).toBuffer(); - return encodeWithGuetzli(prepared, config?.jpeg?.lossless || {}, lifecycle); - }], - ['png', ({ config, input, isLossless }) => sharp(input).png(isLossless ? config?.png?.lossless : config?.png?.lossy || {}).toBuffer()], - ['svg', ({ config, input }) => Buffer.from(svgoOptimize(input, config.svg).data)], +async function processFileByFormat({ fileBuffer, config, configPath, isLossless }) { + const imageMetadata = await parseImageMetadata(fileBuffer); + const format = imageMetadata.format; + + if (!format) { + throw new Error('Unable to read the image format. Check that the file is a valid, supported image.'); + } + + const processByFormat = PROCESS_BY_FORMAT.get(format); + if (!processByFormat) { + throw new Error(`Unsupported image format: "${format}"`); + } + + // Only the codec call is enriched, so filesystem and detection errors keep speaking + // for themselves. + try { + return await processByFormat({ fileBuffer, config, isLossless }); + } catch (error) { + throw describeCodecFailure({ configPath, error, format, isLossless, mode: 'optimize' }); + } +} + +const PROCESS_BY_FORMAT = new Map([ + ['gif', processGif], + ['jpeg', processJpeg], + ['png', processPng], + ['svg', processSvg], ]); + +async function processJpeg({ fileBuffer, config, isLossless }) { + const sharpImage = sharp(fileBuffer) + .rotate(); // Rotate image using information from EXIF Orientation tag + + if (!isLossless) { + return sharpImage + .jpeg(config?.jpeg?.lossy || {}) + .toBuffer(); + } + + const inputBuffer = await sharpImage + .toColorspace('srgb') // Replace colorspace (guetzli works only with sRGB) + .jpeg({ quality: 100, optimizeCoding: false }) // Applying maximum quality to minimize losses during image processing with sharp + .toBuffer(); + + const commandOptions = [ + guetzliRunner, + ...optionsToArguments({ + options: config?.jpeg?.lossless || {}, + }), + ]; + + return pipe({ + command: nodeExecutable, + commandOptions, + inputBuffer, + }); +} + +function processPng({ fileBuffer, config, isLossless }) { + return sharp(fileBuffer) + .png(isLossless ? config?.png?.lossless : config?.png?.lossy || {}) + .toBuffer(); +} + +function processGif({ fileBuffer, config, isLossless }) { + const operation = createGifOperation( + fileBuffer, + (isLossless ? config?.gif?.lossless : config?.gif?.lossy) || {}, + ); + registerCancellable(operation.terminate, operation.promise); + return operation.promise; +} + +function processSvg({ fileBuffer, config }) { + return Buffer.from( + svgoOptimize( + fileBuffer, + config.svg, + ).data, + ); +} + +function pipe({ command, commandOptions, inputBuffer }) { + return new Promise((resolve, reject) => { + const process = spawn(command, commandOptions); + registerChild(process); + + process.stdin.write(inputBuffer); + process.stdin.end(); + + const stdoutChunks = []; + process.stdout.on('data', (chunk) => { + stdoutChunks.push(chunk); + }); + + process.on('error', (error) => { + reject(new Error(`Unable to optimize the image: ${error.message}`)); + }); + + process.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Unable to optimize the image. The encoder exited with code ${code}.`)); + return; + } + + const processedFileBuffer = Buffer.concat(stdoutChunks); + resolve(processedFileBuffer); + }); + }); +} diff --git a/packages/optimizt/package.json b/packages/optimizt/package.json index 6bbf6a6..6860864 100644 --- a/packages/optimizt/package.json +++ b/packages/optimizt/package.json @@ -37,7 +37,7 @@ "vendor/" ], "scripts": { - "build": "node ../optimizt-sharp/scripts/build.mjs && node scripts/vendor-sharp.mjs && node scripts/vendor-guetzli.mjs", + "build": "node ../optimizt-sharp/scripts/build.mjs && node scripts/vendor-sharp.mjs && node scripts/vendor-guetzli.mjs && node scripts/vendor-gifsicle.mjs", "lint": "eslint ./", "prepack": "npm run build", "test": "npm run build && vitest run", @@ -53,8 +53,7 @@ "p-limit": "7.3.0", "semver": "^7.8.4", "svgo": "4.0.1", - "write-file-atomic": "7.0.1", - "@343dev/gifsicle": "1.2.0" + "write-file-atomic": "7.0.1" }, "devDependencies": { "@343dev/eslint-config": "5.0.0", diff --git a/packages/optimizt/scripts/vendor-gifsicle.mjs b/packages/optimizt/scripts/vendor-gifsicle.mjs new file mode 100644 index 0000000..56afeec --- /dev/null +++ b/packages/optimizt/scripts/vendor-gifsicle.mjs @@ -0,0 +1,80 @@ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const repoDirectory = path.resolve(packageDirectory, '..', '..'); +const workspaceDirectory = path.resolve(packageDirectory, '..', 'optimizt-gifsicle'); +const gifsicleDistributionDirectory = path.join(workspaceDirectory, 'dist'); +const destinationDirectory = path.join(packageDirectory, 'vendor', 'gifsicle'); +for (const artifact of ['gifsicle.mjs', 'gifsicle.wasm']) { + try { + await fs.access(path.join(gifsicleDistributionDirectory, artifact)); + } catch { + throw new Error(`Missing Gifsicle WebAssembly artifact: ${path.join(gifsicleDistributionDirectory, artifact)}. Build packages/optimizt-gifsicle before vendoring.`); + } +} + +const sourceRevision = git(['rev-parse', 'HEAD']); +if (!/^[0-9a-f]{40}$/.test(sourceRevision)) { + throw new Error(`Expected a full Git commit SHA, received: ${sourceRevision}`); +} + +const sourcePath = 'packages/optimizt-gifsicle'; +const isDevelopmentBuild = process.env.OPTIMIZT_SKIP_GIFSICLE_SOURCE_CHECK === '1'; +const sourceStatus = git([ + 'status', '--porcelain=v1', '--untracked-files=all', '--', sourcePath, +]); +if (sourceStatus && !isDevelopmentBuild) { + throw new Error([ + 'Cannot vendor the publishable Gifsicle runtime from modified source/build files.', + 'Commit or discard all changes under packages/optimizt-gifsicle so SOURCE.md identifies the exact source used for the WebAssembly build.', + '', + sourceStatus, + ].join('\n')); +} +await fs.rm(destinationDirectory, { force: true, recursive: true }); +await fs.mkdir(destinationDirectory, { recursive: true }); +await Promise.all([ + fs.cp(gifsicleDistributionDirectory, path.join(destinationDirectory, 'dist'), { recursive: true }), + fs.cp(path.join(workspaceDirectory, 'lib'), path.join(destinationDirectory, 'lib'), { recursive: true }), + fs.copyFile(path.join(workspaceDirectory, 'index.js'), path.join(destinationDirectory, 'index.js')), + fs.copyFile(path.join(workspaceDirectory, 'LICENSE'), path.join(destinationDirectory, 'LICENSE')), + fs.copyFile(path.join(workspaceDirectory, 'GIFSICLE-LICENSE.md'), path.join(destinationDirectory, 'GIFSICLE-LICENSE.md')), + fs.copyFile(path.join(workspaceDirectory, 'MUSL-COPYRIGHT'), path.join(destinationDirectory, 'MUSL-COPYRIGHT')), + fs.copyFile(path.join(workspaceDirectory, 'THIRD_PARTY_NOTICES.md'), path.join(destinationDirectory, 'THIRD_PARTY_NOTICES.md')), +]); +await fs.writeFile(path.join(destinationDirectory, 'SOURCE.md'), sourceDocument(sourceRevision)); + +function git(arguments_) { + try { + return execFileSync('git', ['-C', repoDirectory, ...arguments_], { + encoding: 'utf8', + }).trim(); + } catch (error) { + throw new Error(`Unable to determine Gifsicle source provenance with Git: ${error.message}`, { cause: error }); + } +} + +function sourceDocument(revision) { + if (isDevelopmentBuild && sourceStatus) { + return `# Development build\n\nThis vendored runtime was produced from uncommitted local source and must not be published.\n`; + } + const repoUrl = `https://github.com/343dev/optimizt/tree/${revision}/${sourcePath}`; + const archiveUrl = `https://github.com/343dev/optimizt/archive/${revision}.tar.gz`; + return `# Corresponding source for Gifsicle WebAssembly + +The bundled Gifsicle WebAssembly runtime was built from the exact Optimizt +repository revision \`${revision}\`. + +- Complete corresponding source: <${repoUrl}> +- Source archive for the same immutable revision: <${archiveUrl}> +- Upstream Gifsicle project: + +The Optimizt source directory at that revision contains the pristine upstream +Gifsicle source, the Optimizt modifications and patch, the WebAssembly bridge, +the build scripts, the pinned Emscripten version and build configuration, and +the other files needed to reproduce the corresponding WebAssembly build. +`; +} diff --git a/packages/optimizt/tests/lifecycle.test.js b/packages/optimizt/tests/lifecycle.test.js index fa19c95..c911c79 100644 --- a/packages/optimizt/tests/lifecycle.test.js +++ b/packages/optimizt/tests/lifecycle.test.js @@ -10,6 +10,7 @@ const installedListeners = []; afterEach(() => { for (const [signal, listener] of installedListeners) process.removeListener(signal, listener); installedListeners.length = 0; + vi.useRealTimers(); vi.restoreAllMocks(); }); @@ -33,7 +34,45 @@ function createChildStub() { return { kill: vi.fn(), once: vi.fn() }; } +function pendingPromise() { + return new Promise(() => {}); +} + describe('shutdown coordinator', () => { + test.each(['SIGINT', 'SIGTERM'])('a second %s bypasses exit hooks before forcing exit', async (signal) => { + const lifecycle = await loadLifecycle(); + const removeListeners = vi.spyOn(process, 'removeAllListeners').mockReturnValue(process); + const exit = vi.spyOn(process, 'exit').mockImplementation(() => {}); + + process.emit(signal); + expect(removeListeners).not.toHaveBeenCalled(); + expect(exit).not.toHaveBeenCalled(); + process.emit(signal); + + expect(removeListeners).toHaveBeenCalledExactlyOnceWith('exit'); + expect(exit).toHaveBeenCalledExactlyOnceWith(interruptStatus[signal]); + expect(removeListeners.mock.invocationCallOrder[0]).toBeLessThan(exit.mock.invocationCallOrder[0]); + lifecycle.finishLifecycle(); + }); + + test.each(['SIGINT', 'SIGTERM'])('the %s shutdown deadline bypasses exit hooks too', async (signal) => { + vi.useFakeTimers(); + const lifecycle = await loadLifecycle(); + const removeListeners = vi.spyOn(process, 'removeAllListeners').mockReturnValue(process); + const exit = vi.spyOn(process, 'exit').mockImplementation(() => {}); + + process.emit(signal); + vi.advanceTimersByTime(4999); + expect(exit).not.toHaveBeenCalled(); + expect(removeListeners).not.toHaveBeenCalled(); + vi.advanceTimersByTime(1); + + expect(removeListeners).toHaveBeenCalledExactlyOnceWith('exit'); + expect(exit).toHaveBeenCalledExactlyOnceWith(interruptStatus[signal]); + expect(removeListeners.mock.invocationCallOrder[0]).toBeLessThan(exit.mock.invocationCallOrder[0]); + lifecycle.finishLifecycle(); + }); + test('nothing is interrupted and no status is forced without a signal', async () => { const lifecycle = await loadLifecycle(); const child = createChildStub(); @@ -66,4 +105,38 @@ describe('shutdown coordinator', () => { expect(lateChild.kill).toHaveBeenCalledWith('SIGTERM'); expect(lifecycle.finishLifecycle()).toBe(interruptStatus.SIGINT); }); + + test('an interrupt cancels active in-process operations', async () => { + const lifecycle = await loadLifecycle(); + const cancel = vi.fn(); + lifecycle.registerCancellable(cancel, pendingPromise()); + + process.emit('SIGINT'); + + expect(cancel).toHaveBeenCalledOnce(); + expect(lifecycle.finishLifecycle()).toBe(interruptStatus.SIGINT); + }); + + test('an operation registered after the interrupt is cancelled immediately', async () => { + const lifecycle = await loadLifecycle(); + const cancel = vi.fn(); + + process.emit('SIGTERM'); + lifecycle.registerCancellable(cancel, pendingPromise()); + + expect(cancel).toHaveBeenCalledOnce(); + expect(lifecycle.finishLifecycle()).toBe(interruptStatus.SIGTERM); + }); + + test('a completed operation is no longer cancelled', async () => { + const lifecycle = await loadLifecycle(); + const cancel = vi.fn(); + lifecycle.registerCancellable(cancel, Promise.resolve()); + await Promise.resolve(); + + process.emit('SIGINT'); + + expect(cancel).not.toHaveBeenCalled(); + expect(lifecycle.finishLifecycle()).toBe(interruptStatus.SIGINT); + }); }); From e2e815dd4ad80a98d56d7357d6e9d09966fe7a32 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Fri, 25 Sep 2026 11:48:04 +0700 Subject: [PATCH 5/6] docs(optimizt): sync CLI option descriptions --- packages/optimizt/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/optimizt/README.md b/packages/optimizt/README.md index 4c483fd..14aedb5 100644 --- a/packages/optimizt/README.md +++ b/packages/optimizt/README.md @@ -40,13 +40,15 @@ This command can replace the original file. To keep it, use `--output` with a se | --- | --- | | `--avif` | Create AVIF versions of images. | | `--webp` | Create WebP versions of images. | -| `-f, --force` | Replace existing AVIF and WebP versions. | +| `-f, --force` | Replace existing conversion targets. Requires `--avif` or `--webp`. | | `-l, --lossless` | Use the lossless profile. JPEG compression is still lossy. | -| `-v, --verbose` | Show detailed output, including skipped files. | -| `-c, --config ` | Use a custom configuration file instead of the bundled defaults. | -| `-o, --output ` | Write results to an existing directory. | +| `-v, --verbose` | Show skipped and deduplicated work. | +| `-c, --config ` | Use the specified CJS configuration file instead of the defaults. | +| `-o, --output ` | Write results under an existing output directory. | | `-p, --prefix ` | Add a prefix to output file names. | | `-s, --suffix ` | Add a suffix to output file names. | +| `--no-color` | Disable colored output. | +| `--debug` | Include stack traces and Optimizt and Node.js version details in errors. | | `-V, --version` | Show the version. | | `-h, --help` | Show help. | From 5ea6e32df7b40860d1a8f62770fa061f5912f4d8 Mon Sep 17 00:00:00 2001 From: 343dev <343dev@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:25:13 +0700 Subject: [PATCH 6/6] docs(optimizt): document WASM runtime changes --- packages/optimizt/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/optimizt/CHANGELOG.md b/packages/optimizt/CHANGELOG.md index 4c1a00c..fc6c430 100644 --- a/packages/optimizt/CHANGELOG.md +++ b/packages/optimizt/CHANGELOG.md @@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Guetzli and Gifsicle now use wasm64 and require Node.js 24.0.0 or newer. +- **BREAKING:** Sharp, Guetzli, and Gifsicle now run from WebAssembly bundled with Optimizt instead of platform-specific native binaries. Guetzli and Gifsicle use wasm64, so Optimizt requires Node.js 24.0.0 or newer. +- **BREAKING:** GIF optimization now rejects encoded inputs larger than 128 MiB, images with more than 100,000 frames, and images whose canvas or total frame area exceeds 134,217,728 pixels. - **BREAKING:** Processing now exits with status `1` when argument validation, filesystem preflight, configuration loading, or any image operation fails. A valid run with no eligible images still exits `0`. - **BREAKING:** Status, progress, warnings, errors, and summaries now use `stderr`. `stdout` is reserved for help and version output and remains empty during image processing. - **BREAKING:** Missing or inaccessible explicit operands and explicitly supplied files with unsupported extensions now fail instead of being silently ignored. Unsupported files found while traversing directories remain ignored.