[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: esimov\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help me improve the library\nlabels: ''\n\n---\n\n### Describe the bug\n<!--A clear and concise description of what the bug is.\n\n- Describe if it has been cloned via `git clone` or downloaded with `go get`?\n- You don't know how to install and build Caire from the source code?\nIf you have followed the README file, but still have issues post your action here.\n- Describe your bug if it haven't been already reported.\n-->\n\n### API related bug\n<!--A clear and concise description of the API usage.-->\n\n### Expected behavior\n<!--A clear and concise description of what you expected to happen.-->\n\n### Screenshots\n<!--Add screenshots to help explain your problem.-->\n- [Screenshots, logs or errors]\n\n### Bug with the Desktop version (please complete the following information):\n - Sytem information like OS: [e.g. macOS, Ubuntu]\n - You are using the binary file from the uploaded releases or you are doing a manual build?\n\n### Additional context\n<!--Add any other context about the problem here.-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\nlabels: ''\n\n---\n\n### Is your feature request related to a problem? Please describe.\n<!--A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]-->\n\n### Describe the solution you'd like\n<!--A clear and concise description of what you want to happen.-->\n\n### Describe alternatives you've considered\n<!--A clear and concise description of any alternative solutions or features you've considered.-->\n\n### Additional context\n<!--Add any other context or screenshots about the feature request here.-->\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    branches: [master]\n\njobs:\n  build:\n    name: Build\n    strategy:\n      fail-fast: false\n      matrix:\n        go-version: [~1.21, ~1.22]\n        os: [ubuntu-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    env:\n      GO111MODULE: \"on\"\n    steps:\n      - name: Install Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Cache-Go\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/go/pkg/mod              # Module download cache\n            ~/.cache/go-build         # Build cache (Linux)\n            ~/Library/Caches/go-build # Build cache (Mac)\n          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}\n      - name: Install Linux Dependencies\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n          sudo apt-get update -y\n          sudo apt-get install -y gcc pkg-config libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev\n      - name: Checkout code\n        uses: actions/checkout@v2\n\n      - name: Download Go modules\n        run: go mod download\n\n      - name: Run Tests\n        id: makefile\n        run: |\n          make test"
  },
  {
    "path": ".gitignore",
    "content": "*.jpg\n*.png\n*.jpeg\ncoverage.out\ntest-report.json\n/packages\n!/testdata/*.png\n!/testdata/*.jpg\n!/examples/**/*.png\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Endre Simo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "all: \n\t@./build.sh\nclean:\n\t@rm -f caire\ninstall: all\n\t@cp caire /usr/local/bin\nuninstall: \n\t@rm -f /usr/local/bin/caire\npackage:\n\t@NOCOPY=1 ./build.sh package\ntest:\n\tgo test -v -json ./... -run=. > ./test-report.json -coverprofile=coverage.out"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\"><img alt=\"Caire Logo\" src=\"https://user-images.githubusercontent.com/883386/51555990-a1762600-1e81-11e9-9a6a-0cd815870358.png\" height=\"180\"></h1>\n\n[![build](https://github.com/esimov/caire/actions/workflows/build.yml/badge.svg)](https://github.com/esimov/caire/actions/workflows/build.yml)\n[![Go Reference](https://pkg.go.dev/badge/github.com/esimov/caire.svg)](https://pkg.go.dev/github.com/esimov/caire)\n[![license](https://img.shields.io/github/license/esimov/caire)](./LICENSE)\n[![release](https://img.shields.io/badge/release-v1.5.0-blue.svg)](https://github.com/esimov/caire/releases/tag/v1.5.0)\n[![homebrew](https://img.shields.io/badge/homebrew-v1.5.0-orange.svg)](https://formulae.brew.sh/formula/caire)\n[![caire](https://snapcraft.io/caire/badge.svg)](https://snapcraft.io/caire)\n\n**Caire** is a content aware image resize library based on *[Seam Carving for Content-Aware Image Resizing](https://inst.eecs.berkeley.edu/~cs194-26/fa16/hw/proj4-seamcarving/imret.pdf)* paper.\n\n## How does it work\n* An energy map (edge detection) is generated from the provided image.\n* The algorithm tries to find the least important parts of the image taking into account the lowest energy values.\n* Using a dynamic programming approach the algorithm will generate individual seams across the image from top to down, or from left to right (depending on the horizontal or vertical resizing) and will allocate for each seam a custom value, the least important pixels having the lowest energy cost and the most important ones having the highest cost.\n* We traverse the image from the second row to the last row and compute the cumulative minimum energy for all possible connected seams for each entry.\n* The minimum energy level is calculated by summing up the current pixel value with the lowest value of the neighboring pixels obtained from the previous row.\n* We traverse the image from top to bottom and compute the minimum energy level. For each pixel in a row we compute the energy of the current pixel plus the energy of one of the three possible pixels above it.\n* Find the lowest cost seam from the energy matrix starting from the last row and remove it.\n* Repeat the process.\n\n#### The process illustrated:\n\n| Original image | Energy map | Seams applied\n|:--:|:--:|:--:|\n| ![original](https://user-images.githubusercontent.com/883386/35481925-de130752-0435-11e8-9246-3950679b4fd6.jpg) | ![sobel](https://user-images.githubusercontent.com/883386/35481899-5d5096ca-0435-11e8-9f9b-a84fefc06470.jpg) | ![debug](https://user-images.githubusercontent.com/883386/35481949-5c74dcb0-0436-11e8-97db-a6169cb150ca.jpg) | ![out](https://user-images.githubusercontent.com/883386/35564985-88c579d4-05c4-11e8-9068-5141714e6f43.jpg) |\n\n## Features\nKey features which differentiates this library from the other existing open source solutions:\n\n- [x] **GUI progress indicator**\n- [x] Customizable command line support\n- [x] Support for both shrinking or enlarging the image\n- [x] Resize image both vertically and horizontally\n- [x] Face detection to avoid face deformation\n- [x] Support for multiple output image type (jpg, jpeg, png, bmp)\n- [x] Support for `stdin` and `stdout` pipe commands\n- [x] Can process whole directories recursively and concurrently\n- [x] Use of sobel threshold for fine tuning\n- [x] Use of blur filter for increased edge detection\n- [x] Support for squaring the image with a single command\n- [x] Support for proportional scaling\n- [x] Support for protective mask\n- [x] Support for removal mask\n- [x] [GUI debug mode support](#masks-support)\n\n## Install\nFirst, install Go, set your `GOPATH`, and make sure `$GOPATH/bin` is on your `PATH`.\n\n```bash\n$ go install github.com/esimov/caire/cmd/caire@latest\n```\n\n## MacOS (Brew) install\nThe library can also be installed via Homebrew.\n\n```bash\n$ brew install caire\n```\n\n## Usage\n\n```bash\n$ caire -in input.jpg -out output.jpg\n```\n\n### Supported commands:\n```bash\n$ caire --help\n```\nThe following flags are supported:\n\n| Flag | Default | Description |\n| --- | --- | --- |\n| `in` | - | Input file |\n| `out` | - | Output file |\n| `width` | n/a | New width |\n| `height` | n/a | New height |\n| `preview` | true | Show GUI window |\n| `perc` | false | Reduce image by percentage |\n| `square` | false | Reduce image to square dimensions |\n| `blur` | 4 | Blur radius |\n| `sobel` | 2 | Sobel filter threshold |\n| `debug` | false | Use debugger |\n| `face` | false | Use face detection |\n| `angle` | float | Plane rotated faces angle |\n| `mask` | string | Mask file path |\n| `rmask` | string | Remove mask file path |\n| `color` | string | Seam color (default `#ff0000`) |\n| `shape` | string | Shape type used for debugging: `circle`,`line` (default `circle`) |\n\n## Face detection\n\nThe library is capable of detecting human faces prior resizing the images by using the lightweight Pigo (https://github.com/esimov/pigo) face detection library.\n\nThe image below illustrates the application capabilities for human face detection prior resizing. It's clearly visible that with face detection activated the algorithm will avoid cropping pixels inside the detected faces, retaining the face zone unaltered.\n\n| Original image | With face detection | Without face detection\n|:--:|:--:|:--:|\n| ![Original](https://user-images.githubusercontent.com/883386/37569642-0c5f49e8-2aee-11e8-8ac1-d096c0387ca0.jpg) | ![With Face Detection](https://user-images.githubusercontent.com/883386/41292871-6ca43280-6e5c-11e8-9d72-5b9a138228b6.jpg) | ![Without Face Detection](https://user-images.githubusercontent.com/883386/41292872-6cc90e8e-6e5c-11e8-8b41-5b4eb5042381.jpg) |\n\n[Sample image source](http://www.lens-rumors.com/wp-content/uploads/2014/12/EF-M-55-200mm-f4.5-6.3-IS-STM-sample.jpg)\n\n### GUI progress indicator\n\n<p align=\"center\"><img alt=\"GUI preview\" title=\"GUI preview\" src=\"https://github.com/esimov/caire/raw/master/gui_preview.gif\"></p>\n\nA GUI preview mode is also incorporated into the library for in time process visualization. The [Gio](http://gioui.org/) GUI library has been used because of its robustness and modern architecture. Prior running it please make sure that you have installed all the required dependencies noted in the installation section (https://gioui.org/#installation) .\n\nThe preview window is activated by default but you can deactivate it any time by setting the `-preview` flag to false. When the images are processed concurrently from a directory the preview mode is deactivated.\n\n### Face detection to avoid face deformation\nIn order to detect faces prior rescaling, use the `-face` flag. There is no need to provide a face classification file, since it's already embedded into the generated binary file. The sample code below will resize the provided image with 20%, but checks for human faces in order tot avoid face deformations.\n\nFor face detection related settings please check the Pigo [documentation](https://github.com/esimov/pigo/blob/master/README.md).\n\n```bash\n$ caire -in input.jpg -out output.jpg -face=1 -perc=1 -width=20\n```\n\n### Support for `stdin` and `stdout` pipe commands\nYou can also use `stdin` and `stdout` with `-`:\n\n```bash\n$ cat input/source.jpg | caire -in - -out - >out.jpg\n```\n\n`in` and `out` default to `-` so you can also use:\n\n```bash\n$ cat input/source.jpg | caire >out.jpg\n$ caire -out out.jpg < input/source.jpg\n```\n\nYou can provide also an image URL for the `-in` flag or even use **curl** or **wget** as a pipe command in which case there is no need to use the `-in` flag.\n\n```bash\n$ caire -in <image_url> -out <output-folder>\n$ curl -s <image_url> | caire > out.jpg\n```\n\n### Process multiple images from a directory concurrently\nThe library can also process multiple images from a directory **concurrently**. You have to provide only the source and the destination folder and the new width or height in this case.\n\n```bash\n$ caire -in <input_folder> -out <output-folder>\n```\n\n### Support for multiple output image type\nThere is no need to define the output file type, just use the correct extension and the library will encode the image to that specific type.\n\n### Other options\nIn case you wish to scale down the image by a specific percentage, it can be used the **`-perc`** boolean flag. In this case the values provided for the `width` and `height` are expressed in percentage and not pixel values. For example to reduce the image dimension by 20% both horizontally and vertically you can use the following command:\n\n```bash\n$ caire -in input/source.jpg -out ./out.jpg -perc=1 -width=20 -height=20 -debug=false\n```\n\nAlso the library supports the **`-square`** option. When this option is used the image will be resized to a square, based on the shortest edge.\n\nWhen an image is resized on both the X and Y axis, the algorithm will first try to rescale it prior resizing, but also will preserve the image aspect ratio. The seam carving algorithm is applied only to the remaining points. Ex. : given an image of dimensions 2048x1536 if we want to resize to the 1024x500, the tool first rescale the image to 1024x768 and then will remove only the remaining 268px.\n\n### Masks support:\n\n- `-mask`: The path to the protective mask. The mask should be in binary format and have the same size as the input image. White areas represent regions where no seams should be carved.\n- `-rmask`: The path to the removal mask. The mask should be in binary format and have the same size as the input image. White areas represent regions to be removed.\n\nMask | Mask removal\n:-: | :-:\n<video src='https://user-images.githubusercontent.com/883386/197509861-86733da8-0846-419a-95eb-4fb5a97607d5.mp4' width=180/> | <video src='https://user-images.githubusercontent.com/883386/197397857-7b785d7c-2f80-4aed-a5d2-75c429389060.mp4' width=180/>\n\n### Caire integrations\n- [x] Caire can be used as a serverless function via OpenFaaS: https://github.com/esimov/caire-openfaas\n- [x] Caire can also be used as a `snap` function (https://snapcraft.io/caire): `$ snap run caire --h`\n\n<a href=\"https://snapcraft.io/caire\"><img src=\"https://raw.githubusercontent.com/snapcore/snap-store-badges/master/EN/%5BEN%5D-snap-store-white-uneditable.png\" alt=\"snapcraft caire\"></a>\n\n## Results\n\n#### Shrunk images\n| Original | Shrunk |\n| --- | --- |\n| ![broadway_tower_edit](https://user-images.githubusercontent.com/883386/35498083-83d6015e-04d5-11e8-936a-883e17b76f9d.jpg) | ![broadway_tower_edit](https://user-images.githubusercontent.com/883386/35498110-a4a03328-04d5-11e8-9bf1-f526ef033d6a.jpg) |\n| ![waterfall](https://user-images.githubusercontent.com/883386/35498250-2f31e202-04d6-11e8-8840-a78f40fc1a0c.png) | ![waterfall](https://user-images.githubusercontent.com/883386/35498209-0411b16a-04d6-11e8-9ce2-ec4bce34828a.jpg) |\n| ![dubai](https://user-images.githubusercontent.com/883386/35498466-1375b88a-04d7-11e8-8f8e-9d202da6a6b3.jpg) | ![dubai](https://user-images.githubusercontent.com/883386/35498499-3c32fc38-04d7-11e8-9f0d-07f63a8bd420.jpg) |\n| ![boat](https://user-images.githubusercontent.com/883386/35498465-1317a678-04d7-11e8-9185-ec92ea57f7c6.jpg) | ![boat](https://user-images.githubusercontent.com/883386/35498498-3c0f182c-04d7-11e8-9af8-695bc071e0f1.jpg) |\n\n#### Enlarged images\n| Original | Extended |\n| --- | --- |\n| ![gasadalur](https://user-images.githubusercontent.com/883386/35498662-e11853c4-04d7-11e8-98d7-fcdb27207362.jpg) | ![gasadalur](https://user-images.githubusercontent.com/883386/35498559-87eb6426-04d7-11e8-825c-2dd2abdfc112.jpg) |\n| ![dubai](https://user-images.githubusercontent.com/883386/35498466-1375b88a-04d7-11e8-8f8e-9d202da6a6b3.jpg) | ![dubai](https://user-images.githubusercontent.com/883386/35498827-8cee502c-04d8-11e8-8449-05805f196d60.jpg) |\n### Useful resources\n* https://en.wikipedia.org/wiki/Seam_carving\n* https://inst.eecs.berkeley.edu/~cs194-26/fa16/hw/proj4-seamcarving/imret.pdf\n* http://pages.cs.wisc.edu/~moayad/cs766/download_files/alnammi_cs_766_final_report.pdf\n* https://stacks.stanford.edu/file/druid:my512gb2187/Zargham_Nassirpour_Content_aware_image_resizing.pdf\n\n## Author\n\n* Endre Simo ([@simo_endre](https://twitter.com/simo_endre))\n\n## License\n\nCopyright © 2018 Endre Simo\n\nThis project is under the MIT License. See the LICENSE file for the full license text.\n"
  },
  {
    "path": "build.sh",
    "content": "#!/bin/bash\nset -e\n\nVERSION=\"1.5.0\"\nPROTECTED_MODE=\"no\"\n\nexport GO15VENDOREXPERIMENT=1\n\ncd $(dirname \"${BASH_SOURCE[0]}\")\nOD=\"$(pwd)\"\nWD=$OD\n\npackage() {\n\techo Packaging $1 Binary\n\tbdir=caire-${VERSION}-$2-$3\n\trm -rf packages/$bdir && mkdir -p packages/$bdir\n\tGOOS=$2 GOARCH=$3 ./build.sh\n\tif [ \"$2\" == \"windows\" ]; then\n\t\tmv caire packages/$bdir/caire.exe\n\telse\n\t\tmv caire packages/$bdir\n\tfi\n\tcp README.md packages/$bdir\n\tcd packages\n\tif [ \"$2\" == \"linux\" ]; then\n\t\ttar -zcf $bdir.tar.gz $bdir\n\telse\n\t\tzip -r -q $bdir.zip $bdir\n\tfi\n\trm -rf $bdir\n\tcd ..\n}\n\nif [ \"$1\" == \"package\" ]; then\n\trm -rf packages/\n\tpackage \"Windows\" \"windows\" \"amd64\"\n\tpackage \"Mac\" \"darwin\" \"amd64\"\n\tpackage \"Linux\" \"linux\" \"amd64\"\n\tpackage \"FreeBSD\" \"freebsd\" \"amd64\"\n\texit\nfi\n\n# temp directory for storing isolated environment.\nTMP=\"$(mktemp -d -t sdb.XXXX)\"\nrmtemp() {\n\trm -rf \"$TMP\"\n}\ntrap rmtemp EXIT\n\nif [ \"$NOCOPY\" != \"1\" ]; then\n\t# copy all files to an isolated directory.\n\tWD=\"$TMP/src/github.com/esimov/caire\"\n\texport GOPATH=\"$TMP\"\n\tfor file in `find . -type f`; do\n\t\t# TODO: use .gitignore to ignore, or possibly just use git to determine the file list.\n\t\tif [[ \"$file\" != \".\" && \"$file\" != ./.git* && \"$file\" != ./caire ]]; then\n\t\t\tmkdir -p \"$WD/$(dirname \"${file}\")\"\n\t\t\tcp -P \"$file\" \"$WD/$(dirname \"${file}\")\"\n\t\tfi\n\tdone\n\tcd $WD\nfi\n\n# build and store objects into original directory.\ngo build -ldflags \"-X main.Version=$VERSION\" -o \"$OD/caire\" cmd/caire/main.go"
  },
  {
    "path": "carver.go",
    "content": "package caire\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"math\"\n\n\t\"github.com/esimov/caire/utils\"\n\tpigo \"github.com/esimov/pigo/core\"\n)\n\n// SeamCarver defines the Carve interface method, which have to be\n// implemented by the Processor struct.\ntype SeamCarver interface {\n\tResize(*image.NRGBA) (image.Image, error)\n}\n\n// maxFaceDetAttempts defines the maximum number of attempts of face detections\nconst maxFaceDetAttempts = 20\n\nvar (\n\tdetAttempts    int\n\tisFaceDetected bool\n)\n\nvar (\n\tsobel       *image.NRGBA\n\tenergySeams = make([][]Seam, 0)\n)\n\n// Carver is the main entry struct having as parameters the newly generated image width, height and seam points.\ntype Carver struct {\n\tPoints []float64\n\tSeams  []Seam\n\tWidth  int\n\tHeight int\n}\n\n// Seam struct contains the seam pixel coordinates.\ntype Seam struct {\n\tX int\n\tY int\n}\n\n// NewCarver returns an initialized Carver structure.\nfunc NewCarver(width, height int) *Carver {\n\treturn &Carver{\n\t\tPoints: make([]float64, width*height),\n\t\tSeams:  []Seam{},\n\t\tWidth:  width,\n\t\tHeight: height,\n\t}\n}\n\n// Get energy pixel value.\nfunc (c *Carver) get(x, y int) float64 {\n\tpx := x + y*c.Width\n\treturn c.Points[px]\n}\n\n// Set energy pixel value.\nfunc (c *Carver) set(x, y int, px float64) {\n\tidx := x + y*c.Width\n\tc.Points[idx] = px\n}\n\n// ComputeSeams compute the minimum energy level based on the following logic:\n//\n//   - traverse the image from the second row to the last row\n//     and compute the cumulative minimum energy M for all possible\n//     connected seams for each entry (i, j).\n//\n//   - the minimum energy level is calculated by summing up the current pixel value\n//     with the minimum pixel value of the neighboring pixels from the previous row.\nfunc (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, error) {\n\twidth, height := img.Bounds().Dx(), img.Bounds().Dy()\n\tsobel = c.SobelDetector(img, float64(p.SobelThreshold))\n\n\tdets := []pigo.Detection{}\n\n\tif p.FaceDetector != nil && p.FaceDetect && detAttempts < maxFaceDetAttempts {\n\t\tvar ratio float64\n\n\t\tif width < height {\n\t\t\tratio = float64(width) / float64(height)\n\t\t} else {\n\t\t\tratio = float64(height) / float64(width)\n\t\t}\n\t\tminSize := float64(utils.Min(width, height)) * ratio / 3\n\n\t\t// Transform the image to pixel array.\n\t\tpixels := rgbToGrayscale(img)\n\n\t\tcParams := pigo.CascadeParams{\n\t\t\tMinSize:     int(minSize),\n\t\t\tMaxSize:     utils.Min(width, height),\n\t\t\tShiftFactor: 0.1,\n\t\t\tScaleFactor: 1.1,\n\n\t\t\tImageParams: pigo.ImageParams{\n\t\t\t\tPixels: pixels,\n\t\t\t\tRows:   height,\n\t\t\t\tCols:   width,\n\t\t\t\tDim:    width,\n\t\t\t},\n\t\t}\n\t\tif p.vRes {\n\t\t\tp.FaceAngle = 0.2\n\t\t}\n\t\t// Run the classifier over the obtained leaf nodes and return the detection results.\n\t\t// The result contains quadruplets representing the row, column, scale and detection score.\n\t\tdets = p.FaceDetector.RunCascade(cParams, p.FaceAngle)\n\n\t\t// Calculate the intersection over union (IoU) of two clusters.\n\t\tdets = p.FaceDetector.ClusterDetections(dets, 0.1)\n\n\t\tif len(dets) == 0 {\n\t\t\t// Retry detecting faces for a certain amount of time.\n\t\t\tif detAttempts < maxFaceDetAttempts {\n\t\t\t\tdetAttempts++\n\t\t\t}\n\t\t} else {\n\t\t\tdetAttempts = 0\n\t\t\tisFaceDetected = true\n\t\t}\n\t}\n\n\t// Traverse the pixel data of the binary file used for protecting the regions\n\t// which we do not want to be altered by the seam carver,\n\t// obtain the white patches and apply it to the sobel image.\n\tif len(p.MaskPath) > 0 && p.Mask != nil {\n\t\tp.DebugMask = image.NewNRGBA(img.Bounds())\n\n\t\tfor i := 0; i < width*height; i++ {\n\t\t\tx := i % width\n\t\t\ty := (i - x) / width\n\n\t\t\tr, g, b, _ := p.Mask.At(x, y).RGBA()\n\t\t\tif r>>8 == 0xff && g>>8 == 0xff && b>>8 == 0xff {\n\t\t\t\tif isFaceDetected {\n\t\t\t\t\t// Reduce the brightness of the mask with a small factor if human faces are detected.\n\t\t\t\t\t// This way we can avoid the seam carver to remove\n\t\t\t\t\t// the pixels inside the detected human faces.\n\t\t\t\t\tsobel.Set(x, y, color.RGBA{R: 225, G: 225, B: 225, A: 255})\n\t\t\t\t} else {\n\t\t\t\t\tsobel.Set(x, y, color.White)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Traverse the pixel data of the binary file used to remove the image regions\n\t// we do not want to be retained in the final image, obtain the white patches,\n\t// but this time inverse the colors to black and merge it back to the sobel image.\n\tif len(p.RMaskPath) > 0 && p.RMask != nil {\n\t\tp.DebugMask = image.NewNRGBA(img.Bounds())\n\n\t\tfor i := 0; i < width*height; i++ {\n\t\t\tx := i % width\n\t\t\ty := (i - x) / width\n\n\t\t\tr, g, b, _ := p.RMask.At(x, y).RGBA()\n\t\t\t// Replace the white pixels with black.\n\t\t\tif r>>8 == 0xff && g>>8 == 0xff && b>>8 == 0xff {\n\t\t\t\tif isFaceDetected {\n\t\t\t\t\t// Reduce the brightness of the mask with a small factor if human faces are detected.\n\t\t\t\t\t// This way we can avoid the seam carver to remove\n\t\t\t\t\t// the pixels inside the detected human faces.\n\t\t\t\t\tsobel.Set(x, y, color.RGBA{R: 25, G: 25, B: 25, A: 255})\n\t\t\t\t} else {\n\t\t\t\t\tsobel.Set(x, y, color.Black)\n\t\t\t\t}\n\t\t\t\tp.DebugMask.Set(x, y, color.Black)\n\t\t\t} else {\n\t\t\t\tp.DebugMask.Set(x, y, color.Transparent)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Iterate over the detected faces and fill out the rectangles with white.\n\t// We need to trick the sobel detector to consider them as important image parts.\n\tfor _, face := range dets {\n\t\tif (p.NewHeight != 0 && p.NewHeight < face.Scale) ||\n\t\t\t(p.NewWidth != 0 && p.NewWidth < face.Scale) {\n\t\t\treturn nil, fmt.Errorf(\"%s %s\",\n\t\t\t\t\"cannot resize the image to the specified dimension without face deformation.\\n\",\n\t\t\t\t\"\\tRemove the face detection option in case you still wish to resize the image.\")\n\t\t}\n\t\tif face.Q > 5.0 {\n\t\t\tscale := int(float64(face.Scale) / 1.7)\n\t\t\trect := image.Rect(\n\t\t\t\tface.Col-scale,\n\t\t\t\tface.Row-scale,\n\t\t\t\tface.Col+scale,\n\t\t\t\tface.Row+scale,\n\t\t\t)\n\t\t\tp.DebugMask = image.NewNRGBA(img.Bounds())\n\t\t\tdraw.Draw(sobel, rect, &image.Uniform{color.White}, image.Point{}, draw.Src)\n\t\t\tdraw.Draw(p.DebugMask, rect, &image.Uniform{color.White}, image.Point{}, draw.Src)\n\t\t}\n\t}\n\n\t// Increase the energy value for each of the selected seam from the seams table\n\t// in order to avoid picking the same seam over and over again.\n\t// We expand the energy level of the selected seams to have a better redistribution.\n\tif len(energySeams) > 0 {\n\t\tfor i := 0; i < len(energySeams); i++ {\n\t\t\tfor _, seam := range energySeams[i] {\n\t\t\t\tsobel.Set(seam.X, seam.Y, &image.Uniform{color.White})\n\t\t\t}\n\t\t}\n\t}\n\n\tvar srcImg *image.NRGBA\n\tif p.BlurRadius > 0 {\n\t\tsrcImg = image.NewNRGBA(img.Bounds())\n\t\terr := Stackblur(srcImg, sobel, uint32(p.BlurRadius))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error bluring the image: %w\", err)\n\t\t}\n\t} else {\n\t\tsrcImg = sobel\n\t}\n\n\tfor x := 0; x < c.Width; x++ {\n\t\tfor y := 0; y < c.Height; y++ {\n\t\t\tr, _, _, a := srcImg.At(x, y).RGBA()\n\t\t\tc.set(x, y, float64(r)/float64(a))\n\t\t}\n\t}\n\n\tvar left, middle, right float64\n\n\t// Traverse the image from top to bottom and compute the minimum energy level.\n\t// For each pixel in a row we compute the energy of the current pixel\n\t// plus the energy of one of the three possible pixels above it.\n\tfor y := 1; y < c.Height; y++ {\n\t\tfor x := 1; x < c.Width-1; x++ {\n\t\t\tleft = c.get(x-1, y-1)\n\t\t\tmiddle = c.get(x, y-1)\n\t\t\tright = c.get(x+1, y-1)\n\t\t\tmin := math.Min(math.Min(left, middle), right)\n\t\t\t// Set the minimum energy level.\n\t\t\tc.set(x, y, c.get(x, y)+min)\n\t\t}\n\t\t// Special cases: pixels are far left or far right\n\t\tleft := c.get(0, y) + math.Min(c.get(0, y-1), c.get(1, y-1))\n\t\tc.set(0, y, left)\n\t\tright := c.get(0, y) + math.Min(c.get(c.Width-1, y-1), c.get(c.Width-2, y-1))\n\t\tc.set(c.Width-1, y, right)\n\t}\n\treturn srcImg, nil\n}\n\n// FindLowestEnergySeams find the lowest vertical energy seam.\nfunc (c *Carver) FindLowestEnergySeams(p *Processor) []Seam {\n\t// Find the lowest cost seam from the energy matrix starting from the last row.\n\tvar (\n\t\tmin = math.MaxFloat64\n\t\tpx  int\n\t)\n\tseams := make([]Seam, 0)\n\n\t// Find the pixel on the last row with the minimum cumulative energy and use this as the starting pixel\n\tfor x := 0; x < c.Width; x++ {\n\t\tseam := c.get(x, c.Height-1)\n\t\tif seam < min {\n\t\t\tmin = seam\n\t\t\tpx = x\n\t\t}\n\t}\n\n\tseams = append(seams, Seam{X: px, Y: c.Height - 1})\n\tvar left, middle, right float64\n\n\t// Walk up in the matrix table, check the immediate three top pixels seam level\n\t// and add that one which has the lowest cumulative energy.\n\tfor y := c.Height - 2; y >= 0; y-- {\n\t\tmiddle = c.get(px, y)\n\t\t// Leftmost seam, no child to the left\n\t\tif px == 0 {\n\t\t\tright = c.get(px+1, y)\n\t\t\tif right < middle {\n\t\t\t\tpx++\n\t\t\t}\n\t\t\t// Rightmost seam, no child to the right\n\t\t} else if px == c.Width-1 {\n\t\t\tleft = c.get(px-1, y)\n\t\t\tif left < middle {\n\t\t\t\tpx--\n\t\t\t}\n\t\t} else {\n\t\t\tleft = c.get(px-1, y)\n\t\t\tright = c.get(px+1, y)\n\t\t\tmin := math.Min(math.Min(left, middle), right)\n\n\t\t\tif min == left {\n\t\t\t\tpx--\n\t\t\t} else if min == right {\n\t\t\t\tpx++\n\t\t\t}\n\t\t}\n\t\tseams = append(seams, Seam{X: px, Y: y})\n\t}\n\n\t// compare against c.Width and NOT c.Height, because the image is rotated.\n\tif p.NewWidth > c.Width || (p.NewHeight > 0 && p.NewHeight > c.Width) {\n\t\t// Include the currently processed energy seam into the seams table,\n\t\t// but only when an image enlargement operation is commenced.\n\t\t// We need to take this approach in order to avoid picking the same seam each time.\n\t\tenergySeams = append(energySeams, seams)\n\t}\n\treturn seams\n}\n\n// RemoveSeam remove the least important columns based on the stored energy (seams) level.\nfunc (c *Carver) RemoveSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGBA {\n\tbounds := img.Bounds()\n\t// Reduce the image width with one pixel on each iteration.\n\tdst := image.NewNRGBA(image.Rect(0, 0, bounds.Dx()-1, bounds.Dy()))\n\n\tfor _, seam := range seams {\n\t\ty := seam.Y\n\t\tfor x := 0; x < bounds.Max.X; x++ {\n\t\t\tif seam.X == x {\n\t\t\t\tif debug {\n\t\t\t\t\tc.Seams = append(c.Seams, Seam{X: x, Y: y})\n\t\t\t\t}\n\t\t\t} else if seam.X < x {\n\t\t\t\tdst.Set(x-1, y, img.At(x, y))\n\t\t\t} else {\n\t\t\t\tdst.Set(x, y, img.At(x, y))\n\t\t\t}\n\t\t}\n\t}\n\treturn dst\n}\n\n// AddSeam add a new seam.\nfunc (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGBA {\n\tvar (\n\t\tlr, lg, lb uint32\n\t\trr, rg, rb uint32\n\t)\n\n\tbounds := img.Bounds()\n\tdst := image.NewNRGBA(image.Rect(0, 0, bounds.Dx()+1, bounds.Dy()))\n\n\tfor _, seam := range seams {\n\t\ty := seam.Y\n\t\tfor x := 0; x < bounds.Max.X; x++ {\n\t\t\tif seam.X == x {\n\t\t\t\tif debug {\n\t\t\t\t\tc.Seams = append(c.Seams, Seam{X: x, Y: y})\n\t\t\t\t}\n\t\t\t\tif x > 0 && x != bounds.Max.X {\n\t\t\t\t\tlr, lg, lb, _ = img.At(x-1, y).RGBA()\n\t\t\t\t} else {\n\t\t\t\t\tlr, lg, lb, _ = img.At(x, y).RGBA()\n\t\t\t\t}\n\n\t\t\t\tif x < bounds.Max.X-1 {\n\t\t\t\t\trr, rg, rb, _ = img.At(x+1, y).RGBA()\n\t\t\t\t} else if x == bounds.Max.X {\n\t\t\t\t\trr, rg, rb, _ = img.At(x, y).RGBA()\n\t\t\t\t}\n\n\t\t\t\t// calculate the average color of the neighboring pixels\n\t\t\t\tavr, avg, avb := (lr+rr)>>1, (lg+rg)>>1, (lb+rb)>>1\n\t\t\t\tdst.Set(x, y, color.RGBA{uint8(avr >> 8), uint8(avg >> 8), uint8(avb >> 8), 0xff})\n\t\t\t\tdst.Set(x+1, y, img.At(x, y))\n\t\t\t} else if seam.X < x {\n\t\t\t\tdst.Set(x, y, img.At(x-1, y))\n\t\t\t\tdst.Set(x+1, y, img.At(x, y))\n\t\t\t} else {\n\t\t\t\tdst.Set(x, y, img.At(x, y))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn dst\n}\n"
  },
  {
    "path": "carver_benchmark_test.go",
    "content": "package caire\n\nimport (\n\t\"image\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc Benchmark_Carver(b *testing.B) {\n\tsampleImg := filepath.Join(\"./testdata\", \"sample.jpg\")\n\tf, err := os.Open(sampleImg)\n\tif err != nil {\n\t\tb.Fatalf(\"could not load sample image: %v\", err)\n\t}\n\tdefer f.Close()\n\n\tsrc, _, err := image.Decode(f)\n\tif err != nil {\n\t\tb.Fatalf(\"error decoding image: %v\", err)\n\t}\n\tb.ResetTimer()\n\n\timg := imgToNRGBA(src)\n\n\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\tc := NewCarver(width, height)\n\n\tfor i := 0; i < b.N; i++ {\n\t\t_, err := c.ComputeSeams(p, img)\n\t\tif err != nil {\n\t\t\tb.FailNow()\n\t\t}\n\n\t\tseams := c.FindLowestEnergySeams(p)\n\t\timg = c.RemoveSeam(img, seams, p.Debug)\n\t}\n\n}\n"
  },
  {
    "path": "carver_test.go",
    "content": "package caire\n\nimport (\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/esimov/caire/utils\"\n\tpigo \"github.com/esimov/pigo/core\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst (\n\timgWidth  = 10\n\timgHeight = 10\n)\n\nvar p *Processor\n\nfunc init() {\n\tp = &Processor{\n\t\tNewWidth:       imgWidth,\n\t\tNewHeight:      imgHeight,\n\t\tBlurRadius:     1,\n\t\tSobelThreshold: 4,\n\t\tPercentage:     false,\n\t\tSquare:         false,\n\t\tDebug:          false,\n\t}\n}\n\nfunc TestCarver_EnergySeamShouldNotBeDetected(t *testing.T) {\n\tassert := assert.New(t)\n\n\tvar seams [][]Seam\n\tvar totalEnergySeams int\n\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\n\tvar c = NewCarver(dx, dy)\n\tfor range imgWidth {\n\t\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\t\tc = NewCarver(width, height)\n\n\t\t_, err := c.ComputeSeams(p, img)\n\t\tassert.NoError(err)\n\n\t\tles := c.FindLowestEnergySeams(p)\n\t\tseams = append(seams, les)\n\t}\n\n\tfor i := range seams {\n\t\tfor s := range seams[i] {\n\t\t\ttotalEnergySeams += seams[i][s].X\n\t\t}\n\t}\n\tassert.Equal(0, totalEnergySeams)\n}\n\nfunc TestCarver_DetectHorizontalEnergySeam(t *testing.T) {\n\tvar seams [][]Seam\n\tvar totalEnergySeams int\n\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\tdraw.Draw(img, img.Bounds(), &image.Uniform{image.White}, image.Point{}, draw.Src)\n\n\t// Replace the pixel colors in a single row from 0xff to 0xdd. 5 is an arbitrary value.\n\t// The seam detector should recognize that line as being of low energy density\n\t// and should perform the seam computation process.\n\t// This way we'll make sure, that the seam detector correctly detects one and only one line.\n\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\tfor x := 0; x < dx; x++ {\n\t\timg.Pix[(5*dx+x)*4+0] = 0xdd\n\t\timg.Pix[(5*dx+x)*4+1] = 0xdd\n\t\timg.Pix[(5*dx+x)*4+2] = 0xdd\n\t\timg.Pix[(5*dx+x)*4+3] = 0xdd\n\t}\n\n\tvar c = NewCarver(dx, dy)\n\tfor x := 0; x < imgWidth; x++ {\n\t\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\n\t\tc = NewCarver(width, height)\n\t\t_, err := c.ComputeSeams(p, img)\n\t\tassert.NoError(t, err)\n\n\t\tles := c.FindLowestEnergySeams(p)\n\t\tseams = append(seams, les)\n\t}\n\n\tfor i := range seams {\n\t\tfor s := range seams[i] {\n\t\t\ttotalEnergySeams += seams[i][s].X\n\t\t}\n\t}\n\tassert.Greater(t, totalEnergySeams, 0)\n}\n\nfunc TestCarver_DetectVerticalEnergySeam(t *testing.T) {\n\tvar seams [][]Seam\n\tvar totalEnergySeams int\n\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\tdraw.Draw(img, img.Bounds(), &image.Uniform{image.White}, image.Point{}, draw.Src)\n\n\t// Replace the pixel colors in a single column from 0xff to 0xdd. 5 is an arbitrary value.\n\t// The seam detector should recognize that line as being of low energy density\n\t// and should perform the seam computation process.\n\t// This way we'll make sure, that the seam detector correctly detects one and only one line.\n\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\tfor y := 0; y < dy; y++ {\n\t\timg.Pix[5*4+(dx*y)*4+0] = 0xdd\n\t\timg.Pix[5*4+(dx*y)*4+1] = 0xdd\n\t\timg.Pix[5*4+(dx*y)*4+2] = 0xdd\n\t\timg.Pix[5*4+(dx*y)*4+3] = 0xff\n\t}\n\n\tvar c = NewCarver(dx, dy)\n\timg = rotateImage90(img)\n\tfor x := 0; x < imgHeight; x++ {\n\t\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\n\t\tc = NewCarver(width, height)\n\t\t_, err := c.ComputeSeams(p, img)\n\t\tassert.NoError(t, err)\n\n\t\tles := c.FindLowestEnergySeams(p)\n\t\tseams = append(seams, les)\n\t}\n\n\tfor i := range seams {\n\t\tfor s := range seams[i] {\n\t\t\ttotalEnergySeams += seams[i][s].X\n\t\t}\n\t}\n\tassert.Greater(t, totalEnergySeams, 0)\n}\n\nfunc TestCarver_RemoveSeam(t *testing.T) {\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\tbounds := img.Bounds()\n\n\t// We choose to fill up the background with an uniform white color\n\t// and afterwards we replace the colors in a single row with lower intensity ones.\n\tdraw.Draw(img, bounds, &image.Uniform{image.White}, image.Point{}, draw.Src)\n\torigImg := img\n\n\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\t// Replace the pixels in row 5 with lower intensity colors.\n\tfor x := 0; x < dx; x++ {\n\t\timg.Set(x, 5, color.RGBA{R: 0xdd, G: 0xdd, B: 0xdd, A: 0xff})\n\t}\n\n\tc := NewCarver(dx, dy)\n\t_, err := c.ComputeSeams(p, img)\n\tassert.NoError(t, err)\n\n\tseams := c.FindLowestEnergySeams(p)\n\timg = c.RemoveSeam(img, seams, false)\n\n\tisEq := true\n\t// The test should pass if the detector correctly finds the row which pixel values are of lower intensity.\n\tfor x := 0; x < dx; x++ {\n\t\tfor y := 0; y < dy; y++ {\n\t\t\t// In case the seam detector correctly recognize the modified line as of low importance\n\t\t\t// it should remove it, which means the new image width should be 1px less then the original image.\n\t\t\tr0, g0, b0, _ := origImg.At(x, y).RGBA()\n\t\t\tr1, g1, b1, _ := img.At(x, y).RGBA()\n\n\t\t\tif r0>>8 != r1>>8 && g0>>8 != g1>>8 && b0>>8 != b1>>8 {\n\t\t\t\tisEq = false\n\t\t\t}\n\t\t}\n\t}\n\tassert.False(t, isEq)\n}\n\nfunc TestCarver_AddSeam(t *testing.T) {\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\tbounds := img.Bounds()\n\n\t// We choose to fill up the background with an uniform white color\n\t// Afterwards we'll replace the colors in a single row with lower intensity ones.\n\tdraw.Draw(img, bounds, &image.Uniform{image.White}, image.Point{}, draw.Src)\n\torigImg := img\n\n\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\t// Replace the pixels in row 5 with lower intensity colors.\n\tfor x := 0; x < dx; x++ {\n\t\timg.Set(x, 5, color.RGBA{R: 0xdd, G: 0xdd, B: 0xdd, A: 0xff})\n\t}\n\n\tc := NewCarver(dx, dy)\n\t_, err := c.ComputeSeams(p, img)\n\tassert.NoError(t, err)\n\n\tseams := c.FindLowestEnergySeams(p)\n\timg = c.AddSeam(img, seams, false)\n\n\tdx, dy = img.Bounds().Dx(), img.Bounds().Dy()\n\n\tisEq := true\n\t// The test should pass if the detector correctly finds the row which has lower intensity colors.\n\tfor x := 0; x < dx; x++ {\n\t\tfor y := 0; y < dy; y++ {\n\t\t\tr0, g0, b0, _ := origImg.At(x, y).RGBA()\n\t\t\tr1, g1, b1, _ := img.At(x, y).RGBA()\n\n\t\t\tif r0>>8 != r1>>8 && g0>>8 != g1>>8 && b0>>8 != b1>>8 {\n\t\t\t\tisEq = false\n\t\t\t}\n\t\t}\n\t}\n\tassert.False(t, isEq)\n}\n\nfunc TestCarver_ComputeSeams(t *testing.T) {\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\n\t// We choose to fill up the background with an uniform white color\n\t// Afterwards we'll replace the colors in a single row with lower intensity ones.\n\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\t// Replace the pixels in row 5 with lower intensity colors.\n\tfor x := 0; x < dx; x++ {\n\t\timg.Pix[(5*dx+x)*4+0] = 0xdd\n\t\timg.Pix[(5*dx+x)*4+1] = 0xdd\n\t\timg.Pix[(5*dx+x)*4+2] = 0xdd\n\t\timg.Pix[(5*dx+x)*4+3] = 0xdd\n\t}\n\n\tc := NewCarver(dx, dy)\n\t_, err := c.ComputeSeams(p, img)\n\tassert.NoError(t, err)\n\n\totherThenZero := findNonZeroValue(c.Points)\n\n\tassert.True(t, otherThenZero)\n}\n\nfunc TestCarver_ShouldDetectFace(t *testing.T) {\n\tp.FaceDetect = true\n\n\tsampleImg := filepath.Join(\"./testdata\", \"sample.jpg\")\n\tf, err := os.Open(sampleImg)\n\tif err != nil {\n\t\tt.Fatalf(\"could not load sample image: %v\", err)\n\t}\n\tdefer f.Close()\n\n\tp.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)\n\tif err != nil {\n\t\tt.Fatalf(\"error unpacking the cascade file: %v\", err)\n\t}\n\n\tsrc, _, err := image.Decode(f)\n\tif err != nil {\n\t\tt.Fatalf(\"error decoding image: %v\", err)\n\t}\n\timg := imgToNRGBA(src)\n\tdx, dy := img.Bounds().Max.X, img.Bounds().Max.Y\n\n\t// Transform the image to a pixel array.\n\tpixels := rgbToGrayscale(img)\n\n\tcParams := pigo.CascadeParams{\n\t\tMinSize:     100,\n\t\tMaxSize:     utils.Max(dx, dy),\n\t\tShiftFactor: 0.1,\n\t\tScaleFactor: 1.1,\n\n\t\tImageParams: pigo.ImageParams{\n\t\t\tPixels: pixels,\n\t\t\tRows:   dy,\n\t\t\tCols:   dx,\n\t\t\tDim:    dx,\n\t\t},\n\t}\n\n\t// Run the classifier over the obtained leaf nodes and return the detection results.\n\t// The result contains quadruplets representing the row, column, scale and detection score.\n\tfaces := p.FaceDetector.RunCascade(cParams, p.FaceAngle)\n\n\t// Calculate the intersection over union (IoU) of two clusters.\n\tfaces = p.FaceDetector.ClusterDetections(faces, 0.2)\n\n\tassert.Equal(t, 1, len(faces))\n}\n\nfunc TestCarver_ShouldNotRemoveFaceZone(t *testing.T) {\n\tp.FaceDetect = true\n\tp.BlurRadius = 10\n\n\tsampleImg := filepath.Join(\"./testdata\", \"sample.jpg\")\n\tf, err := os.Open(sampleImg)\n\tif err != nil {\n\t\tt.Fatalf(\"could not load sample image: %v\", err)\n\t}\n\tdefer f.Close()\n\n\tp.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)\n\tif err != nil {\n\t\tt.Fatalf(\"error unpacking the cascade file: %v\", err)\n\t}\n\n\tsrc, _, err := image.Decode(f)\n\tif err != nil {\n\t\tt.Fatalf(\"error decoding image: %v\", err)\n\t}\n\timg := imgToNRGBA(src)\n\tdx, dy := img.Bounds().Max.X, img.Bounds().Max.Y\n\n\tc := NewCarver(dx, dy)\n\t// Transform the image to a pixel array.\n\tpixels := rgbToGrayscale(img)\n\n\tsobel := c.SobelDetector(img, float64(p.SobelThreshold))\n\n\terr = Stackblur(img, sobel, uint32(p.BlurRadius))\n\tassert.NoError(t, err)\n\n\tcParams := pigo.CascadeParams{\n\t\tMinSize:     100,\n\t\tMaxSize:     utils.Max(dx, dy),\n\t\tShiftFactor: 0.1,\n\t\tScaleFactor: 1.1,\n\n\t\tImageParams: pigo.ImageParams{\n\t\t\tPixels: pixels,\n\t\t\tRows:   dy,\n\t\t\tCols:   dx,\n\t\t\tDim:    dx,\n\t\t},\n\t}\n\n\t// Run the classifier over the obtained leaf nodes and return the detection results.\n\t// The result contains quadruplets representing the row, column, scale and detection score.\n\tfaces := p.FaceDetector.RunCascade(cParams, p.FaceAngle)\n\n\t// Calculate the intersection over union (IoU) of two clusters.\n\tfaces = p.FaceDetector.ClusterDetections(faces, 0.2)\n\n\t// Range over all the detected faces and draw a white rectangle mask over each of them.\n\t// We need to trick the sobel detector to consider them as important image parts.\n\tvar rect image.Rectangle\n\tfor _, face := range faces {\n\t\tif face.Q > 5.0 {\n\t\t\trect = image.Rect(\n\t\t\t\tface.Col-face.Scale/2,\n\t\t\t\tface.Row-face.Scale/2,\n\t\t\t\tface.Col+face.Scale/2,\n\t\t\t\tface.Row+face.Scale/2,\n\t\t\t)\n\t\t\tdraw.Draw(sobel, rect, &image.Uniform{image.White}, image.Point{}, draw.Src)\n\t\t}\n\t}\n\t_, err = c.ComputeSeams(p, img)\n\tassert.Error(t, err)\n\n\tseams := c.FindLowestEnergySeams(p)\n\n\tfor _, seam := range seams {\n\t\tif seam.X >= rect.Min.X && seam.X <= rect.Max.X {\n\t\t\tt.Errorf(\"Carver shouldn't remove seams from face zone\")\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc TestCarver_ShouldNotResizeWithFaceDistorsion(t *testing.T) {\n\tp.FaceDetect = true\n\tp.BlurRadius = 10\n\tp.NewHeight = 200\n\n\tsampleImg := filepath.Join(\"./testdata\", \"sample.jpg\")\n\tf, err := os.Open(sampleImg)\n\tif err != nil {\n\t\tt.Fatalf(\"could not load sample image: %v\", err)\n\t}\n\tdefer f.Close()\n\n\tp.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)\n\tif err != nil {\n\t\tt.Fatalf(\"error unpacking the cascade file: %v\", err)\n\t}\n\n\tsrc, _, err := image.Decode(f)\n\tif err != nil {\n\t\tt.Fatalf(\"error decoding image: %v\", err)\n\t}\n\timg := imgToNRGBA(src)\n\tdx, dy := img.Bounds().Max.X, img.Bounds().Max.Y\n\n\t// Transform the image to a pixel array.\n\tpixels := rgbToGrayscale(img)\n\tcParams := pigo.CascadeParams{\n\t\tMinSize:     100,\n\t\tMaxSize:     utils.Max(dx, dy),\n\t\tShiftFactor: 0.1,\n\t\tScaleFactor: 1.1,\n\n\t\tImageParams: pigo.ImageParams{\n\t\t\tPixels: pixels,\n\t\t\tRows:   dy,\n\t\t\tCols:   dx,\n\t\t\tDim:    dx,\n\t\t},\n\t}\n\n\t// Run the classifier over the obtained leaf nodes and return the detection results.\n\t// The result contains quadruplets representing the row, column, scale and detection score.\n\tfaces := p.FaceDetector.RunCascade(cParams, p.FaceAngle)\n\n\t// Calculate the intersection over union (IoU) of two clusters.\n\tfaces = p.FaceDetector.ClusterDetections(faces, 0.2)\n\n\tfor _, face := range faces {\n\t\tif p.NewHeight < face.Scale {\n\t\t\tt.Errorf(\"Should not resize image without face deformation.\")\n\t\t}\n\t}\n}\n\n// findNonZeroValue utility function to check if the slice contains values other then zeros.\nfunc findNonZeroValue(points []float64) bool {\n\tvar found = false\n\tfor i := 0; i < len(points); i++ {\n\t\tif points[i] != 0 {\n\t\t\tfound = true\n\t\t}\n\t}\n\treturn found\n}\n"
  },
  {
    "path": "cmd/caire/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"gioui.org/app\"\n\t\"github.com/esimov/caire\"\n\t\"github.com/esimov/caire/utils\"\n)\n\nconst HelpBanner = `\n┌─┐┌─┐┬┬─┐┌─┐\n│  ├─┤│├┬┘├┤\n└─┘┴ ┴┴┴└─└─┘\n\nContent aware image resize library.\n    Version: %s\n\n`\n\n// pipeName indicates that stdin/stdout is being used as file names.\nconst pipeName = \"-\"\n\n// Version indicates the current build version.\nvar Version string\n\nvar (\n\t// Flags\n\tsource         = flag.String(\"in\", pipeName, \"Source\")\n\tdestination    = flag.String(\"out\", pipeName, \"Destination\")\n\tblurRadius     = flag.Int(\"blur\", 4, \"Blur radius\")\n\tsobelThreshold = flag.Int(\"sobel\", 2, \"Sobel filter threshold\")\n\tnewWidth       = flag.Int(\"width\", 0, \"New width\")\n\tnewHeight      = flag.Int(\"height\", 0, \"New height\")\n\tpercentage     = flag.Bool(\"perc\", false, \"Reduce image by percentage\")\n\tsquare         = flag.Bool(\"square\", false, \"Reduce image to square dimensions\")\n\tdebug          = flag.Bool(\"debug\", false, \"Show the seams\")\n\tshapeType      = flag.String(\"shape\", \"circle\", \"Shape type used for debugging: circle|line\")\n\tseamColor      = flag.String(\"color\", \"#ff0000\", \"Seam color\")\n\tpreview        = flag.Bool(\"preview\", true, \"Show GUI window\")\n\tmaskPath       = flag.String(\"mask\", \"\", \"Mask file path for retaining area\")\n\trMaskPath      = flag.String(\"rmask\", \"\", \"Mask file path for removing area\")\n\tfaceDetect     = flag.Bool(\"face\", false, \"Use face detection\")\n\tfaceAngle      = flag.Float64(\"angle\", 0.0, \"Face rotation angle\")\n\tworkers        = flag.Int(\"conc\", runtime.NumCPU(), \"Number of files to process concurrently\")\n)\n\nfunc main() {\n\tlog.SetFlags(0)\n\n\tflag.Usage = func() {\n\t\tfmt.Fprintf(os.Stderr, fmt.Sprintf(HelpBanner, Version))\n\t\tflag.PrintDefaults()\n\t}\n\tflag.Parse()\n\n\tproc := &caire.Processor{\n\t\tBlurRadius:     *blurRadius,\n\t\tSobelThreshold: *sobelThreshold,\n\t\tNewWidth:       *newWidth,\n\t\tNewHeight:      *newHeight,\n\t\tPercentage:     *percentage,\n\t\tSquare:         *square,\n\t\tDebug:          *debug,\n\t\tPreview:        *preview,\n\t\tFaceDetect:     *faceDetect,\n\t\tFaceAngle:      *faceAngle,\n\t\tMaskPath:       *maskPath,\n\t\tRMaskPath:      *rMaskPath,\n\t\tSeamColor:      *seamColor,\n\t\tShapeType:      caire.ShapeType(*shapeType),\n\t}\n\n\tif !(*newWidth > 0 || *newHeight > 0 || *percentage || *square) {\n\t\tflag.Usage()\n\t\tlog.Fatalf(\"%s%s\",\n\t\t\tutils.DecorateText(\"\\nPlease provide a width, height or percentage for image rescaling!\", utils.ErrorMessage),\n\t\t\tutils.DefaultColor,\n\t\t)\n\t} else {\n\t\top := &caire.Image{\n\t\t\tSrc:      *source,\n\t\t\tDst:      *destination,\n\t\t\tWorkers:  *workers,\n\t\t\tPipeName: pipeName,\n\t\t}\n\n\t\tif *preview {\n\t\t\t// When the preview mode is activated we have to execute the resizing process\n\t\t\t// in a separate goroutine in order to not block the Gio thread,\n\t\t\t// which have to run on the main OS thread of the operating systems like MacOS.\n\t\t\tgo proc.Execute(op)\n\t\t\tapp.Main()\n\t\t} else {\n\t\t\tproc.Execute(op)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "doc.go",
    "content": "/*\nPackage caire is a content aware image resize library, which can rescale the source image seamlessly\nboth vertically and horizontally by eliminating the less important parts of the image.\n\nThe package provides a command line interface, supporting various flags for different types of rescaling operations.\nTo check the supported commands type:\n\n\t$ caire --help\n\nIn case you wish to integrate the API in a self constructed environment here is a simple example:\n\n\tpackage main\n\n\timport (\n\t\t\"fmt\"\n\t\t\"github.com/esimov/caire\"\n\t)\n\n\tfunc main() {\n\t\tp := &caire.Processor{\n\t\t\t// Initialize struct variables\n\t\t}\n\n\t\tif err := p.Process(in, out); err != nil {\n\t\t\tfmt.Printf(\"Error rescaling image: %s\", err.Error())\n\t\t}\n\t}\n */\npackage caire\n"
  },
  {
    "path": "draw.go",
    "content": "package caire\n\nimport (\n\t\"image/color\"\n\t\"math\"\n\n\t\"gioui.org/f32\"\n\t\"gioui.org/op/clip\"\n\t\"gioui.org/op/paint\"\n\t\"github.com/esimov/caire/utils\"\n)\n\ntype ShapeType string\n\nconst (\n\tCircle ShapeType = \"circle\"\n\tLine   ShapeType = \"line\"\n)\n\n// DrawSeam visualizes the seam carver in action when the preview mode is activated.\n// It receives as parameters the shape type, the seam (x,y) coordinates and it's thickness.\nfunc (g *Gui) DrawSeam(shape ShapeType, x, y, thickness float32) {\n\tr := getRatio(g.cfg.window.width, g.cfg.window.height)\n\n\tswitch shape {\n\tcase Circle:\n\t\tg.drawCircle(x*r, y*r, thickness)\n\tcase Line:\n\t\tg.drawLine(x*r, y*r, thickness)\n\t}\n}\n\n// drawCircle draws a circle at the seam (x,y) coordinate with the provided size.\nfunc (g *Gui) drawCircle(x, y, radius float32) {\n\tvar (\n\t\tsq   float64\n\t\tp1   f32.Point\n\t\tp2   f32.Point\n\t\torig = g.point(x-radius, y)\n\t)\n\n\tsq = math.Sqrt(float64(radius*radius) - float64(radius*radius))\n\tp1 = g.point(x+float32(sq), y).Sub(orig)\n\tp2 = g.point(x-float32(sq), y).Sub(orig)\n\n\tcol := utils.HexToRGBA(g.proc.SeamColor)\n\tg.setFillColor(col)\n\n\tvar path clip.Path\n\tpath.Begin(g.ctx.Ops)\n\tpath.Move(orig)\n\tpath.Arc(p1, p2, 2*math.Pi)\n\tpath.Close()\n\n\tdefer clip.Outline{Path: path.End()}.Op().Push(g.ctx.Ops).Pop()\n\tpaint.ColorOp{Color: g.setColor(g.getFillColor())}.Add(g.ctx.Ops)\n\tpaint.PaintOp{}.Add(g.ctx.Ops)\n}\n\n// drawLine draws a line at the seam (x,y) coordinate with the provided line thickness.\nfunc (g *Gui) drawLine(x, y, thickness float32) {\n\tvar (\n\t\tp1   = g.point(x, y)\n\t\tp2   = g.point(x, y+1)\n\t\tpath clip.Path\n\t)\n\n\tpath.Begin(g.ctx.Ops)\n\tpath.Move(p1)\n\tpath.Line(p2.Sub(path.Pos()))\n\tpath.Close()\n\n\tcol := utils.HexToRGBA(g.proc.SeamColor)\n\tg.setFillColor(col)\n\n\tdefer clip.Stroke{Path: path.End(), Width: float32(thickness)}.Op().Push(g.ctx.Ops).Pop()\n\tpaint.ColorOp{Color: g.setColor(g.getFillColor())}.Add(g.ctx.Ops)\n\tpaint.PaintOp{}.Add(g.ctx.Ops)\n}\n\n// point converts the seam (x,y) coordinate to Gio f32.Point.\nfunc (g *Gui) point(x, y float32) f32.Point {\n\treturn f32.Point{\n\t\tX: x,\n\t\tY: y,\n\t}\n}\n\n// setColor sets the seam color.\nfunc (g *Gui) setColor(c color.Color) color.NRGBA {\n\trc, gc, bc, ac := c.RGBA()\n\treturn color.NRGBA{\n\t\tR: uint8(rc >> 8),\n\t\tG: uint8(gc >> 8),\n\t\tB: uint8(bc >> 8),\n\t\tA: uint8(ac >> 8),\n\t}\n}\n\n// setFillColor sets the paint fill color.\nfunc (g *Gui) setFillColor(c color.Color) {\n\tg.cfg.color.fill = c\n}\n\n// getFillColor retrieve the paint fill color.\nfunc (g *Gui) getFillColor() color.Color {\n\treturn g.cfg.color.fill\n}\n\n// getRatio returns the image aspect ratio.\nfunc getRatio(w, h float32) float32 {\n\tvar r float32 = 1\n\tif w > maxScreenX && h > maxScreenY {\n\t\twr := maxScreenX / float32(w) // width ratio\n\t\thr := maxScreenY / float32(h) // height ratio\n\n\t\tr = utils.Max(wr, hr)\n\t}\n\treturn r\n}\n"
  },
  {
    "path": "exec.go",
    "content": "package caire\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"image\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"slices\"\n\n\t\"github.com/esimov/caire/utils\"\n\t\"golang.org/x/term\"\n)\n\nvar (\n\t// imgFile holds the file being accessed, be it normal file or pipe name.\n\timgFile *os.File\n\n\t// Common file related variable\n\tfs os.FileInfo\n)\n\ntype Image struct {\n\tSrc, Dst, PipeName string\n\tWorkers            int\n}\n\n// result holds the relevant information about the resizing process and the generated image.\ntype result struct {\n\tpath string\n\terr  error\n}\n\nfunc Resize(s SeamCarver, img *image.NRGBA) (image.Image, error) {\n\treturn s.Resize(img)\n}\n\n// Execute executes the image resizing process.\n// In case the preview mode is activated it will be invoked in a separate goroutine\n// in order to avoid blocking the main OS thread. Otherwise it will be called normally.\nfunc (p *Processor) Execute(img *Image) {\n\tvar err error\n\tdefaultMsg := fmt.Sprintf(\"%s %s\",\n\t\tutils.DecorateText(\"⚡ CAIRE\", utils.StatusMessage),\n\t\tutils.DecorateText(\"⇢ resizing image (be patient, it may take a while)...\", utils.DefaultMessage),\n\t)\n\tp.Spinner = utils.NewSpinner(defaultMsg, time.Millisecond*80)\n\n\t// Supported files\n\tvalidExtensions := []string{\".jpg\", \".png\", \".jpeg\", \".bmp\", \".gif\"}\n\n\t// Check if source path is a local image or URL.\n\tif utils.IsValidUrl(img.Src) {\n\t\tsrc, err := utils.DownloadImage(img.Src)\n\t\tif src != nil {\n\t\t\tdefer os.Remove(src.Name())\n\t\t}\n\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\n\t\t\t\tutils.DecorateText(\"Failed to load the source image: %v\", utils.ErrorMessage),\n\t\t\t\tutils.DecorateText(err.Error(), utils.DefaultMessage),\n\t\t\t)\n\t\t}\n\t\tfs, err = src.Stat()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\n\t\t\t\tutils.DecorateText(\"Failed to load the source image: %v\", utils.ErrorMessage),\n\t\t\t\tutils.DecorateText(err.Error(), utils.DefaultMessage),\n\t\t\t)\n\t\t}\n\t\timg, err := os.Open(src.Name())\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\n\t\t\t\tutils.DecorateText(\"Unable to open the temporary image file: %v\", utils.ErrorMessage),\n\t\t\t\tutils.DecorateText(err.Error(), utils.DefaultMessage),\n\t\t\t)\n\t\t}\n\n\t\timgFile = img\n\t} else {\n\t\t// Check if the source is a pipe name or a regular file.\n\t\tif img.Src == img.PipeName {\n\t\t\tfs, err = os.Stdin.Stat()\n\t\t} else {\n\t\t\tfs, err = os.Stat(img.Src)\n\t\t}\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\n\t\t\t\tutils.DecorateText(\"Failed to load the source image: %v\", utils.ErrorMessage),\n\t\t\t\tutils.DecorateText(err.Error(), utils.DefaultMessage),\n\t\t\t)\n\t\t}\n\t}\n\n\tnow := time.Now()\n\n\tswitch mode := fs.Mode(); {\n\tcase mode.IsDir():\n\t\tvar wg sync.WaitGroup\n\t\t// Read destination file or directory.\n\t\t_, err := os.Stat(img.Dst)\n\t\tif err != nil {\n\t\t\terr = os.Mkdir(img.Dst, 0755)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\n\t\t\t\t\tutils.DecorateText(\"Unable to get dir stats: %v\\n\", utils.ErrorMessage),\n\t\t\t\t\tutils.DecorateText(err.Error(), utils.DefaultMessage),\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\tp.Preview = false\n\n\t\t// Limit the concurrently running workers to maxWorkers.\n\t\tif img.Workers <= 0 || img.Workers > runtime.NumCPU() {\n\t\t\timg.Workers = runtime.NumCPU()\n\t\t}\n\n\t\t// Process recursively the image files from the specified directory concurrently.\n\t\tch := make(chan result)\n\t\tdone := make(chan any)\n\t\tdefer close(done)\n\n\t\tpaths, errc := walkDir(done, img.Src, validExtensions)\n\n\t\twg.Add(img.Workers)\n\t\tfor range img.Workers {\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\timg.consumer(p, img.Dst, ch, done, paths)\n\t\t\t}()\n\t\t}\n\n\t\t// Close the channel after the values are consumed.\n\t\tgo func() {\n\t\t\tdefer close(ch)\n\t\t\twg.Wait()\n\t\t}()\n\n\t\t// Consume the channel values.\n\t\tfor res := range ch {\n\t\t\tif res.err != nil {\n\t\t\t\terr = res.err\n\t\t\t}\n\t\t\timg.printOpStatus(res.path, err)\n\t\t}\n\n\t\tif err = <-errc; err != nil {\n\t\t\tfmt.Fprintf(os.Stderr, utils.DecorateText(err.Error(), utils.ErrorMessage))\n\t\t}\n\n\tcase mode.IsRegular() || mode&os.ModeNamedPipe != 0: // check for regular files or pipe names\n\t\text := filepath.Ext(img.Dst)\n\t\tif !slices.Contains(validExtensions, ext) && img.Dst != img.PipeName {\n\t\t\tlog.Fatalf(utils.DecorateText(fmt.Sprintf(\"%v file type not supported\", ext), utils.ErrorMessage))\n\t\t}\n\n\t\terr = img.process(p, img.Src, img.Dst)\n\t\timg.printOpStatus(img.Dst, err)\n\t}\n\tif err == nil {\n\t\tfmt.Fprintf(os.Stderr, \"\\nExecution time: %s\\n\", utils.DecorateText(\n\t\t\tutils.FormatTime(time.Since(now)), utils.SuccessMessage),\n\t\t)\n\t}\n}\n\n// consumer reads the path names from the paths channel and calls the resizing processor against the source image.\nfunc (img *Image) consumer(\n\tp *Processor,\n\tdest string,\n\tres chan<- result,\n\tdone <-chan any,\n\tpaths <-chan string,\n) {\n\tfor src := range paths {\n\t\tdst := filepath.Join(dest, filepath.Base(src))\n\t\terr := img.process(p, src, dst)\n\n\t\tselect {\n\t\tcase <-done:\n\t\t\treturn\n\t\tcase res <- result{\n\t\t\tpath: src,\n\t\t\terr:  err,\n\t\t}:\n\t\t}\n\t}\n}\n\n// processor calls the resizer method over the source image and returns the error in case exists.\nfunc (img *Image) process(p *Processor, in, out string) error {\n\tvar (\n\t\tsuccessMsg string\n\t\terrorMsg   string\n\t)\n\t// Start the progress indicator.\n\tp.Spinner.Start()\n\n\tsuccessMsg = fmt.Sprintf(\"%s %s %s\",\n\t\tutils.DecorateText(\"⚡ CAIRE\", utils.StatusMessage),\n\t\tutils.DecorateText(\"⇢\", utils.DefaultMessage),\n\t\tutils.DecorateText(\"the image has been resized successfully ✔\", utils.SuccessMessage),\n\t)\n\n\terrorMsg = fmt.Sprintf(\"%s %s %s\",\n\t\tutils.DecorateText(\"⚡ CAIRE\", utils.StatusMessage),\n\t\tutils.DecorateText(\"resizing image failed...\", utils.DefaultMessage),\n\t\tutils.DecorateText(\"✘\", utils.ErrorMessage),\n\t)\n\n\tsrc, dst, err := img.pathToFile(in, out)\n\tif err != nil {\n\t\tp.Spinner.StopMsg = errorMsg\n\t\treturn err\n\t}\n\n\t// Capture CTRL-C signal and restores back the cursor visibility.\n\tsignalChan := make(chan os.Signal, 1)\n\tsignal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)\n\tgo func() {\n\t\t<-signalChan\n\t\tfunc() {\n\t\t\tp.Spinner.RestoreCursor()\n\t\t\tos.Remove(dst.(*os.File).Name())\n\t\t\tos.Exit(1)\n\t\t}()\n\t}()\n\n\tdefer func() {\n\t\tif img, ok := src.(*os.File); ok {\n\t\t\tif err := img.Close(); err != nil {\n\t\t\t\tlog.Printf(\"could not close the opened file: %v\", err)\n\t\t\t}\n\t\t}\n\t}()\n\n\tdefer func() {\n\t\tif img, ok := dst.(*os.File); ok {\n\t\t\tif err := img.Close(); err != nil {\n\t\t\t\tlog.Printf(\"could not close the opened file: %v\", err)\n\t\t\t}\n\t\t}\n\t}()\n\n\tif len(p.MaskPath) > 0 {\n\t\tmask, err := decodeImg(p.MaskPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cannot decode image: %w\", err)\n\t\t}\n\t\tp.Mask = dither(imgToNRGBA(mask))\n\t\tp.DebugMask = p.Mask\n\t}\n\n\tif len(p.RMaskPath) > 0 {\n\t\trmask, err := decodeImg(p.RMaskPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cannot decode image: %w\", err)\n\t\t}\n\t\tp.RMask = dither(imgToNRGBA(rmask))\n\t\tp.DebugMask = p.RMask\n\t}\n\n\terr = p.Process(src, dst)\n\tif err != nil {\n\t\t// remove the generated image file in case of an error\n\t\tos.Remove(dst.(*os.File).Name())\n\n\t\tp.Spinner.StopMsg = errorMsg\n\t\t// Stop the progress indicator.\n\t\tp.Spinner.Stop()\n\n\t\treturn err\n\t} else {\n\t\tp.Spinner.StopMsg = successMsg\n\t\t// Stop the progress indicator.\n\t\tp.Spinner.Stop()\n\t}\n\n\treturn nil\n}\n\n// pathToFile converts the source and destination paths to readable and writable files.\nfunc (img *Image) pathToFile(in, out string) (io.Reader, io.Writer, error) {\n\tvar (\n\t\tsrc io.Reader\n\t\tdst io.Writer\n\t\terr error\n\t)\n\t// Check if the source path is a local image or URL.\n\tif utils.IsValidUrl(in) {\n\t\tsrc = imgFile\n\t} else {\n\t\t// Check if the source is a pipe name or a regular file.\n\t\tif in == img.PipeName {\n\t\t\tif term.IsTerminal(int(os.Stdin.Fd())) {\n\t\t\t\treturn nil, nil, errors.New(\"`-` should be used with a pipe for stdin\")\n\t\t\t}\n\t\t\tsrc = os.Stdin\n\t\t} else {\n\t\t\tsrc, err = os.Open(in)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"unable to open the source file: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check if the destination is a pipe name or a regular file.\n\tif out == img.PipeName {\n\t\tif term.IsTerminal(int(os.Stdout.Fd())) {\n\t\t\treturn nil, nil, errors.New(\"`-` should be used with a pipe for stdout\")\n\t\t}\n\t\tdst = os.Stdout\n\t} else {\n\t\tdst, err = os.OpenFile(out, os.O_CREATE|os.O_WRONLY, 0755)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"unable to create the destination file: %v\", err)\n\t\t}\n\t}\n\n\treturn src, dst, nil\n}\n\n// printOpStatus displays the relevant information about the image resizing process.\nfunc (img *Image) printOpStatus(fname string, err error) {\n\tif err != nil {\n\t\tlog.Fatalf(\n\t\t\tutils.DecorateText(\"\\nError resizing the image: %s\", utils.ErrorMessage),\n\t\t\tutils.DecorateText(fmt.Sprintf(\"\\n\\tReason: %v\\n\", err.Error()), utils.DefaultMessage),\n\t\t)\n\t} else {\n\t\tif fname != img.PipeName {\n\t\t\tfmt.Fprintf(os.Stderr, \"\\nThe image has been saved as: %s %s\\n\\n\",\n\t\t\t\tutils.DecorateText(filepath.Base(fname), utils.SuccessMessage),\n\t\t\t\tutils.DefaultColor,\n\t\t\t)\n\t\t}\n\t}\n}\n\n// walkDir starts a new goroutine to walk the specified directory tree\n// in recursive manner and sends the path of each regular file to a new channel.\n// It finishes in case the done channel is getting closed.\nfunc walkDir(\n\tdone <-chan any,\n\tsrc string,\n\tsrcExts []string,\n) (<-chan string, <-chan error) {\n\tpathChan := make(chan string)\n\terrChan := make(chan error, 1)\n\n\tgo func() {\n\t\t// Close the paths channel after Walk returns.\n\t\tdefer close(pathChan)\n\n\t\terrChan <- filepath.Walk(src, func(path string, f os.FileInfo, err error) error {\n\t\t\tisFileSupported := false\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif !f.Mode().IsRegular() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// Get the file base name.\n\t\t\tfx := filepath.Ext(f.Name())\n\t\t\tif slices.Contains(srcExts, fx) {\n\t\t\t\tisFileSupported = true\n\t\t\t}\n\n\t\t\tif isFileSupported {\n\t\t\t\tselect {\n\t\t\t\tcase <-done:\n\t\t\t\t\treturn errors.New(\"directory walk cancelled\")\n\t\t\t\tcase pathChan <- path:\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}()\n\treturn pathChan, errChan\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/esimov/caire\n\ngo 1.22\n\nrequire (\n\tgioui.org v0.8.0\n\tgithub.com/disintegration/imaging v1.6.2\n\tgithub.com/esimov/pigo v1.4.5\n\tgithub.com/stretchr/testify v1.10.0\n\tgolang.org/x/exp v0.0.0-20240707233637-46b078467d37\n\tgolang.org/x/image v0.23.0\n\tgolang.org/x/term v0.0.0-20220722155259-a9ba230a4035\n)\n\nrequire (\n\tgioui.org/shader v1.0.8 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/go-text/typesetting v0.2.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgolang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect\n\tgolang.org/x/sys v0.29.0 // indirect\n\tgolang.org/x/text v0.21.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=\neliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=\ngioui.org v0.8.0 h1:QV5p5JvsmSmGiIXVYOKn6d9YDliTfjtLlVf5J+BZ9Pg=\ngioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=\ngioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=\ngioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=\ngioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=\ngithub.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=\ngithub.com/esimov/pigo v1.4.5 h1:ySG0QqMh02VNALvHnx04L1ScRu66N6XA5vLLga8GiLg=\ngithub.com/esimov/pigo v1.4.5/go.mod h1:SGkOUpm4wlEmQQJKlaymAkThY8/8iP+XE0gFo7g8G6w=\ngithub.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=\ngithub.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=\ngithub.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=\ngithub.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=\ngithub.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngolang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=\ngolang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=\ngolang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=\ngolang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=\ngolang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=\ngolang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=\ngolang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=\ngolang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=\ngolang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "gui.go",
    "content": "package caire\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"math\"\n\t\"math/rand\"\n\t\"time\"\n\n\t\"gioui.org/app\"\n\t\"gioui.org/f32\"\n\t\"gioui.org/font/gofont\"\n\t\"gioui.org/io/key\"\n\t\"gioui.org/io/system\"\n\t\"gioui.org/layout\"\n\t\"gioui.org/op\"\n\t\"gioui.org/op/clip\"\n\t\"gioui.org/op/paint\"\n\t\"gioui.org/text\"\n\t\"gioui.org/unit\"\n\t\"gioui.org/widget\"\n\t\"gioui.org/widget/material\"\n\t\"github.com/esimov/caire/imop\"\n\t\"github.com/esimov/caire/utils\"\n)\n\ntype hudControlType int\n\nconst (\n\thudShowSeams hudControlType = iota\n\thudShowDebugMask\n)\n\nconst (\n\t// The starting colors for the linear gradient, used when the image is resized both horizontally and vertically.\n\t// In this case the preview mode is deactivated and a dynamic gradient overlay is shown.\n\tredStart   = 137\n\tgreenStart = 47\n\tblueStart  = 54\n\n\t// The ending colors for the linear gradient. The starting colors and ending colors are lerped.\n\tredEnd   = 255\n\tgreenEnd = 112\n\tblueEnd  = 105\n)\n\nvar (\n\tmaxScreenX float32 = 1280\n\tmaxScreenY float32 = 720\n\n\tdefaultBkgColor  = color.Transparent\n\tdefaultFillColor = color.Black\n)\n\ntype interval struct {\n\tmin, max float64\n}\n\n// Gui is the basic struct containing all of the information needed for the UI operation.\n// It receives the resized image transferred through a channel which is called in a separate goroutine.\ntype Gui struct {\n\tcfg struct {\n\t\tx      interval\n\t\ty      interval\n\t\tchrot  bool\n\t\tangle  float32\n\t\twindow struct {\n\t\t\twidth  float32\n\t\t\theight float32\n\t\t\ttitle  string\n\t\t}\n\t\tcolor struct {\n\t\t\trandR uint8\n\t\t\trandG uint8\n\t\t\trandB uint8\n\n\t\t\tbackground color.Color\n\t\t\tfill       color.Color\n\t\t}\n\t\ttimeStamp time.Time\n\t}\n\tprocess struct {\n\t\tisDone bool\n\t\timg    image.Image\n\t\tseams  []Seam\n\n\t\tworker <-chan worker\n\t\terr    chan<- error\n\t}\n\tproc    *Processor\n\tcompOp  *imop.Composite\n\tblendOp *imop.Blend\n\ttheme   *material.Theme\n\tctx     layout.Context\n\thuds    map[hudControlType]*hudCtrl\n\tview    struct {\n\t\thuds layout.List\n\t}\n}\n\ntype hudCtrl struct {\n\tenabled widget.Bool\n\thudType hudControlType\n\ttitle   string\n}\n\n// NewGUI initializes the Gio interface.\nfunc NewGUI(width, height int) *Gui {\n\tdefaultColor := color.NRGBA{R: 0x2d, G: 0x23, B: 0x2e, A: 0xff}\n\n\tgui := &Gui{\n\t\tctx: layout.Context{\n\t\t\tOps: new(op.Ops),\n\t\t\tConstraints: layout.Constraints{\n\t\t\t\tMax: image.Pt(width, height),\n\t\t\t},\n\t\t},\n\t\tcompOp:  imop.InitOp(),\n\t\tblendOp: imop.NewBlend(),\n\t\ttheme:   material.NewTheme(),\n\t\thuds:    make(map[hudControlType]*hudCtrl),\n\t}\n\n\tgui.theme.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))\n\tgui.theme.TextSize = unit.Sp(16)\n\tgui.theme.Palette.ContrastBg = defaultColor\n\tgui.theme.FingerSize = 10\n\n\tgui.initWindow(width, height)\n\n\treturn gui\n}\n\n// AddHudControl adds a new hud control for debugging.\nfunc (g *Gui) AddHudControl(hudControlType hudControlType, title string, enabled bool) {\n\tcontrol := &hudCtrl{\n\t\thudType: hudControlType,\n\t\ttitle:   title,\n\t\tenabled: widget.Bool{},\n\t}\n\tcontrol.enabled.Value = enabled\n\tg.huds[hudControlType] = control\n}\n\n// initWindow creates and initializes the GUI window.\nfunc (g *Gui) initWindow(width, height int) {\n\trand.NewSource(time.Now().UnixNano())\n\n\tg.cfg.angle = 45\n\tg.cfg.color.randR = uint8(random(1, 2))\n\tg.cfg.color.randG = uint8(random(1, 2))\n\tg.cfg.color.randB = uint8(random(1, 2))\n\n\tg.cfg.window.width, g.cfg.window.height = float32(width), float32(height)\n\tg.cfg.x = interval{min: 0, max: float64(width)}\n\tg.cfg.y = interval{min: 0, max: float64(height)}\n\n\tg.cfg.color.background = defaultBkgColor\n\tg.cfg.color.fill = defaultFillColor\n\n\tif !resizeXY {\n\t\tg.cfg.window.width, g.cfg.window.height = g.getWindowSize()\n\t}\n\tg.cfg.window.title = \"Preview process...\"\n}\n\n// getWindowSize returns the resized image dimension.\nfunc (g *Gui) getWindowSize() (float32, float32) {\n\tw, h := g.cfg.window.width, g.cfg.window.height\n\t// Maintain the image aspect ratio in case the image width and height is greater than the predefined window.\n\tr := getRatio(w, h)\n\tif w > maxScreenX && h > maxScreenY {\n\t\tw = w * r\n\t\th = h * r\n\t}\n\treturn w, h\n}\n\n// Run is the core method of the Gio GUI application.\n// This updates the window with the resized image received from a channel\n// and terminates when the image resizing operation completes.\nfunc (g *Gui) Run() error {\n\tvar (\n\t\trc uint8 = redStart\n\t\tgc uint8 = greenStart\n\t\tbc uint8 = blueStart\n\n\t\tdescRed, descGreen, descBlue bool\n\t)\n\n\twidth := unit.Dp(g.cfg.window.width)\n\theight := unit.Dp(g.cfg.window.height)\n\n\tw := new(app.Window)\n\tw.Option(\n\t\tapp.Title(g.cfg.window.title),\n\t\tapp.Size(width, height),\n\t\tapp.MinSize(width, height),\n\t\tapp.MaxSize(width, height),\n\t)\n\n\t// Center the window.\n\tw.Perform(system.ActionCenter)\n\n\tg.cfg.timeStamp = time.Now()\n\n\tif g.proc.Debug {\n\t\tg.AddHudControl(hudShowSeams, \"Show seams\", true)\n\t\tif len(g.proc.MaskPath) > 0 || len(g.proc.RMaskPath) > 0 || g.proc.FaceDetect {\n\t\t\tg.AddHudControl(hudShowDebugMask, \"Debug mode\", false)\n\t\t}\n\t}\n\n\tabortFn := func() {\n\t\tvar dx, dy int\n\n\t\tif g.process.img != nil {\n\t\t\tbounds := g.process.img.Bounds()\n\t\t\tdx, dy = bounds.Max.X, bounds.Max.Y\n\t\t}\n\n\t\tif !g.process.isDone {\n\t\t\tif (g.proc.NewWidth > 0 && g.proc.NewWidth != dx) ||\n\t\t\t\t(g.proc.NewHeight > 0 && g.proc.NewHeight != dy) {\n\n\t\t\t\terrorMsg := fmt.Sprintf(\"%s %s %s\",\n\t\t\t\t\tutils.DecorateText(\"⚡ CAIRE\", utils.StatusMessage),\n\t\t\t\t\tutils.DecorateText(\"⇢ process aborted by the user...\", utils.DefaultMessage),\n\t\t\t\t\tutils.DecorateText(\"✘\\n\", utils.ErrorMessage),\n\t\t\t\t)\n\t\t\t\tg.proc.Spinner.StopMsg = errorMsg\n\t\t\t\tg.proc.Spinner.Stop()\n\t\t\t}\n\t\t}\n\t\tg.proc.Spinner.RestoreCursor()\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase res := <-g.process.worker:\n\t\t\tif res.done {\n\t\t\t\tw.Option(app.Title(\"Done!\"))\n\t\t\t\tg.process.isDone = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif resizeXY {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tg.process.img = res.img\n\t\t\tg.process.seams = res.seams\n\n\t\t\tif mask, ok := g.huds[hudShowDebugMask]; ok {\n\t\t\t\tif mask.enabled.Value && res.mask != nil {\n\t\t\t\t\tbounds := res.img.Bounds()\n\t\t\t\t\tsrcBitmap := imop.NewBitmap(bounds)\n\t\t\t\t\tdstBitmap := imop.NewBitmap(bounds)\n\n\t\t\t\t\tuniformCol := image.NewNRGBA(bounds)\n\n\t\t\t\t\tcol := color.RGBA{R: 0x2f, G: 0xf3, B: 0xe0, A: 0xff}\n\t\t\t\t\tdraw.Draw(uniformCol, uniformCol.Bounds(), &image.Uniform{col}, image.Point{}, draw.Src)\n\n\t\t\t\t\t_ = g.compOp.Set(imop.DstIn)\n\t\t\t\t\tg.compOp.Draw(srcBitmap, res.mask, uniformCol, nil)\n\n\t\t\t\t\t_ = g.blendOp.Set(imop.Screen)\n\t\t\t\t\t_ = g.compOp.Set(imop.SrcAtop)\n\t\t\t\t\tg.compOp.Draw(dstBitmap, res.img, srcBitmap.Img, g.blendOp)\n\n\t\t\t\t\tg.process.img = dstBitmap.Img\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif g.proc.vRes {\n\t\t\t\tg.process.img = rotateImage270(g.process.img.(*image.NRGBA))\n\t\t\t}\n\t\t\tw.Invalidate()\n\t\tdefault:\n\t\t\tswitch e := w.Event().(type) {\n\t\t\tcase app.FrameEvent:\n\t\t\t\tg.ctx = app.NewContext(g.ctx.Ops, e)\n\n\t\t\t\tfor {\n\t\t\t\t\tevent, ok := g.ctx.Event(key.Filter{\n\t\t\t\t\t\tName: key.NameEscape,\n\t\t\t\t\t})\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tswitch event := event.(type) {\n\t\t\t\t\tcase key.Event:\n\t\t\t\t\t\tswitch event.Name {\n\t\t\t\t\t\tcase key.NameEscape:\n\t\t\t\t\t\t\tw.Perform(system.ActionClose)\n\t\t\t\t\t\t\tabortFn()\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t{ // red\n\t\t\t\t\tif descRed {\n\t\t\t\t\t\trc--\n\t\t\t\t\t} else {\n\t\t\t\t\t\trc++\n\t\t\t\t\t}\n\t\t\t\t\tif rc >= redEnd {\n\t\t\t\t\t\tdescRed = !descRed\n\t\t\t\t\t}\n\t\t\t\t\tif rc == redStart {\n\t\t\t\t\t\tdescRed = !descRed\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t{ // green\n\t\t\t\t\tif descGreen {\n\t\t\t\t\t\tgc--\n\t\t\t\t\t} else {\n\t\t\t\t\t\tgc++\n\t\t\t\t\t}\n\t\t\t\t\tif gc >= greenEnd {\n\t\t\t\t\t\tdescGreen = !descGreen\n\t\t\t\t\t}\n\t\t\t\t\tif gc == greenStart {\n\t\t\t\t\t\tdescGreen = !descGreen\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t{ // blue\n\t\t\t\t\tif descBlue {\n\t\t\t\t\t\tbc--\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbc++\n\t\t\t\t\t}\n\t\t\t\t\tif bc >= blueEnd {\n\t\t\t\t\t\tdescBlue = !descBlue\n\t\t\t\t\t}\n\t\t\t\t\tif bc == blueStart {\n\t\t\t\t\t\tdescBlue = !descBlue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tg.draw(color.NRGBA{R: rc, G: gc, B: bc})\n\t\t\t\te.Frame(g.ctx.Ops)\n\t\t\tcase app.DestroyEvent:\n\t\t\t\tabortFn()\n\t\t\t\treturn e.Err\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype (\n\tC = layout.Context\n\tD = layout.Dimensions\n)\n\n// draw draws the resized image in the GUI window (obtained from a channel)\n// and in case the debug mode is activated it prints out the seams.\nfunc (g *Gui) draw(bgColor color.NRGBA) {\n\tg.ctx.Execute(op.InvalidateCmd{})\n\n\tc := g.setColor(g.cfg.color.background)\n\tpaint.Fill(g.ctx.Ops, c)\n\n\tif g.process.img != nil {\n\t\tsrc := paint.NewImageOp(g.process.img)\n\t\tsrc.Add(g.ctx.Ops)\n\n\t\tlayout.Stack{}.Layout(g.ctx,\n\t\t\tlayout.Stacked(func(gtx C) D {\n\t\t\t\tpaint.FillShape(gtx.Ops, c,\n\t\t\t\t\tclip.Rect{Max: g.ctx.Constraints.Max}.Op(),\n\t\t\t\t)\n\t\t\t\treturn layout.UniformInset(unit.Dp(0)).Layout(gtx,\n\t\t\t\t\tfunc(gtx C) D {\n\t\t\t\t\t\twidget.Image{\n\t\t\t\t\t\t\tSrc:   src,\n\t\t\t\t\t\t\tScale: 1 / float32(unit.Dp(1)),\n\t\t\t\t\t\t\tFit:   widget.Contain,\n\t\t\t\t\t\t}.Layout(gtx)\n\n\t\t\t\t\t\tif seam, ok := g.huds[hudShowSeams]; ok {\n\t\t\t\t\t\t\tif seam.enabled.Value {\n\t\t\t\t\t\t\t\ttr := f32.Affine2D{}\n\t\t\t\t\t\t\t\tscreen := layout.FPt(g.ctx.Constraints.Max)\n\t\t\t\t\t\t\t\twidth, height := float32(g.process.img.Bounds().Dx()), float32(g.process.img.Bounds().Dy())\n\t\t\t\t\t\t\t\tsw, sh := float32(screen.X), float32(screen.Y)\n\n\t\t\t\t\t\t\t\tif sw > width {\n\t\t\t\t\t\t\t\t\tratio := sw / width\n\t\t\t\t\t\t\t\t\ttr = tr.Scale(f32.Pt(sw/2, sh/2), f32.Pt(1, ratio))\n\t\t\t\t\t\t\t\t} else if sh > height {\n\t\t\t\t\t\t\t\t\tratio := sh / height\n\t\t\t\t\t\t\t\t\ttr = tr.Scale(f32.Pt(sw/2, sh/2), f32.Pt(ratio, 1))\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif g.proc.vRes {\n\t\t\t\t\t\t\t\t\tangle := float32(270 * math.Pi / 180)\n\t\t\t\t\t\t\t\t\thalf := float32(math.Round(float64(sh*0.5-height*0.5) * 0.5))\n\n\t\t\t\t\t\t\t\t\tox := math.Abs(float64(sw - (sw - (sw/2 - sh/2))))\n\t\t\t\t\t\t\t\t\toy := math.Abs(float64(sh - (sh - (sw/2 - height/2 + half))))\n\t\t\t\t\t\t\t\t\ttr = tr.Rotate(f32.Pt(sw/2, sh/2), -angle)\n\n\t\t\t\t\t\t\t\t\tif screen.X > screen.Y {\n\t\t\t\t\t\t\t\t\t\ttr = tr.Offset(f32.Pt(float32(ox), float32(oy)))\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\ttr = tr.Offset(f32.Pt(float32(-ox), float32(-oy)))\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\top.Affine(tr).Add(gtx.Ops)\n\n\t\t\t\t\t\t\t\tfor _, s := range g.process.seams {\n\t\t\t\t\t\t\t\t\tdpx := gtx.Dp(unit.Dp(s.X))\n\t\t\t\t\t\t\t\t\tdpy := gtx.Dp(unit.Dp(s.Y))\n\t\t\t\t\t\t\t\t\tg.DrawSeam(g.proc.ShapeType, float32(dpx), float32(dpy), 1.0)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn layout.Dimensions{Size: gtx.Constraints.Max}\n\t\t\t\t\t})\n\t\t\t}),\n\t\t)\n\t}\n\tif g.proc.Debug {\n\t\tlayout.Stack{}.Layout(g.ctx,\n\t\t\tlayout.Stacked(func(gtx C) D {\n\t\t\t\thudHeight := 30\n\t\t\t\tr := image.Rectangle{\n\t\t\t\t\tMax: image.Point{\n\t\t\t\t\t\tX: gtx.Constraints.Max.X,\n\t\t\t\t\t\tY: hudHeight,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tdefer op.Offset(image.Pt(0, gtx.Constraints.Max.Y-hudHeight)).Push(gtx.Ops).Pop()\n\t\t\t\treturn layout.Stack{}.Layout(gtx,\n\t\t\t\t\tlayout.Expanded(func(gtx C) D {\n\t\t\t\t\t\tpaint.FillShape(gtx.Ops, color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xcc}, clip.Rect(r).Op())\n\t\t\t\t\t\treturn layout.Dimensions{Size: r.Max}\n\t\t\t\t\t}),\n\t\t\t\t\tlayout.Stacked(func(gtx C) D {\n\t\t\t\t\t\tborder := image.Rectangle{\n\t\t\t\t\t\t\tMax: image.Point{\n\t\t\t\t\t\t\t\tX: gtx.Constraints.Max.X,\n\t\t\t\t\t\t\t\tY: gtx.Dp(unit.Dp(0.5)),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpaint.FillShape(gtx.Ops, color.NRGBA{R: 0xd0, G: 0xcd, B: 0xd7, A: 0xaa}, clip.Rect(border).Op())\n\t\t\t\t\t\treturn layout.Dimensions{Size: r.Max}\n\t\t\t\t\t}),\n\t\t\t\t\tlayout.Stacked(func(gtx C) D {\n\t\t\t\t\t\treturn g.view.huds.Layout(gtx, len(g.huds),\n\t\t\t\t\t\t\tfunc(gtx layout.Context, index int) D {\n\t\t\t\t\t\t\t\tif hud, ok := g.huds[hudControlType(index)]; ok {\n\t\t\t\t\t\t\t\t\tcheckbox := material.CheckBox(g.theme, &hud.enabled, fmt.Sprintf(\"%v\", hud.title))\n\t\t\t\t\t\t\t\t\tcheckbox.Size = 20\n\t\t\t\t\t\t\t\t\treturn checkbox.Layout(gtx)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\treturn D{}\n\t\t\t\t\t\t\t})\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t}),\n\t\t)\n\t}\n\n\t// Disable the preview mode and warn the user in case the image is resized both horizontally and vertically.\n\tif resizeXY {\n\t\tvar msg string\n\n\t\tif !g.process.isDone {\n\t\t\tmsg = \"Preview is not available while the image is resized both horizontally and vertically!\"\n\t\t} else {\n\t\t\tmsg = \"Done, you may close this window!\"\n\t\t\tbgColor = color.NRGBA{R: 45, G: 45, B: 42, A: 0xff}\n\t\t}\n\t\tg.displayMessage(g.ctx, bgColor, msg)\n\t}\n}\n\n// displayMessage show a static message when the image is resized both horizontally and vertically.\nfunc (g *Gui) displayMessage(ctx layout.Context, bgCol color.NRGBA, msg string) {\n\tg.theme.Palette.Fg = color.NRGBA{R: 251, G: 254, B: 249, A: 0xff}\n\tpaint.ColorOp{Color: bgCol}.Add(ctx.Ops)\n\n\trect := image.Rectangle{\n\t\tMax: ctx.Constraints.Max,\n\t}\n\n\tdefer clip.Rect(rect).Push(ctx.Ops).Pop()\n\tpaint.PaintOp{}.Add(ctx.Ops)\n\n\tlayout.Stack{}.Layout(ctx,\n\t\tlayout.Stacked(func(gtx C) D {\n\t\t\treturn layout.UniformInset(unit.Dp(4)).Layout(ctx, func(gtx C) D {\n\t\t\t\tif !g.process.isDone {\n\t\t\t\t\tgtx.Constraints.Min.Y = 0\n\t\t\t\t\ttr := f32.Affine2D{}\n\t\t\t\t\tdr := image.Rectangle{Max: gtx.Constraints.Min}\n\n\t\t\t\t\ttr = tr.Rotate(f32.Pt(float32(ctx.Constraints.Max.X/2), float32(ctx.Constraints.Max.Y/2)), 0.005*-g.cfg.angle)\n\t\t\t\t\top.Affine(tr).Add(gtx.Ops)\n\n\t\t\t\t\tsince := time.Since(g.cfg.timeStamp)\n\n\t\t\t\t\tif since.Seconds() > 5 {\n\t\t\t\t\t\tg.cfg.timeStamp = time.Now()\n\t\t\t\t\t\tg.cfg.color.randR = uint8(random(1, 2))\n\t\t\t\t\t\tg.cfg.color.randG = uint8(random(1, 2))\n\t\t\t\t\t\tg.cfg.color.randB = uint8(random(1, 2))\n\t\t\t\t\t}\n\n\t\t\t\t\tpaint.LinearGradientOp{\n\t\t\t\t\t\tStop1:  layout.FPt(dr.Min.Div(2)),\n\t\t\t\t\t\tStop2:  layout.FPt(dr.Max.Mul(2)),\n\t\t\t\t\t\tColor1: color.NRGBA{R: 41, G: bgCol.G * g.cfg.color.randG, B: bgCol.B * g.cfg.color.randB, A: 0xFF},\n\t\t\t\t\t\tColor2: color.NRGBA{R: bgCol.R * g.cfg.color.randR, G: 29, B: 54, A: 0xFF},\n\t\t\t\t\t}.Add(gtx.Ops)\n\t\t\t\t\tpaint.PaintOp{}.Add(gtx.Ops)\n\n\t\t\t\t\tif g.cfg.chrot {\n\t\t\t\t\t\tg.cfg.angle--\n\t\t\t\t\t} else {\n\t\t\t\t\t\tg.cfg.angle++\n\t\t\t\t\t}\n\t\t\t\t\tif g.cfg.angle == -90 || g.cfg.angle == 90 {\n\t\t\t\t\t\tg.cfg.chrot = !g.cfg.chrot\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn layout.Dimensions{\n\t\t\t\t\tSize: gtx.Constraints.Max,\n\t\t\t\t}\n\t\t\t})\n\t\t}),\n\t\tlayout.Stacked(func(gtx C) D {\n\t\t\treturn layout.UniformInset(unit.Dp(4)).Layout(ctx, func(gtx C) D {\n\t\t\t\treturn layout.Center.Layout(ctx, func(gtx C) D {\n\t\t\t\t\tm := material.Label(g.theme, unit.Sp(40), msg)\n\t\t\t\t\tm.Alignment = text.Middle\n\n\t\t\t\t\treturn m.Layout(gtx)\n\t\t\t\t})\n\t\t\t})\n\t\t}),\n\t\tlayout.Stacked(func(gtx C) D {\n\t\t\tinfo := \"(You will be notified once the process is finished.)\"\n\t\t\tif g.process.isDone {\n\t\t\t\treturn layout.Dimensions{}\n\t\t\t}\n\n\t\t\treturn layout.Inset{Top: 70}.Layout(ctx, func(gtx C) D {\n\t\t\t\treturn layout.Center.Layout(ctx, func(gtx C) D {\n\t\t\t\t\treturn material.Label(g.theme, unit.Sp(13), info).Layout(gtx)\n\t\t\t\t})\n\t\t\t})\n\t\t}),\n\t)\n}\n\n// random generates a random number between two numbers.\nfunc random(min, max float32) float32 {\n\treturn rand.Float32()*(max-min) + min\n}\n"
  },
  {
    "path": "image.go",
    "content": "package caire\r\n\r\nimport (\r\n\t\"errors\"\r\n\t\"fmt\"\r\n\t\"image\"\r\n\t\"image/color\"\r\n\t\"image/jpeg\"\r\n\t\"image/png\"\r\n\t\"io\"\r\n\t\"os\"\r\n\t\"path/filepath\"\r\n\t\"strings\"\r\n\r\n\t\"github.com/esimov/caire/utils\"\r\n\t\"golang.org/x/image/bmp\"\r\n)\r\n\r\n// decodeImg decodes an image file to type image.Image\r\nfunc decodeImg(src string) (image.Image, error) {\r\n\tfile, err := os.Open(src)\r\n\tif err != nil {\r\n\t\treturn nil, fmt.Errorf(\"could not open the mask file: %v\", err)\r\n\t}\r\n\r\n\tctype, err := utils.DetectContentType(file.Name())\r\n\tif err != nil {\r\n\t\treturn nil, err\r\n\t}\r\n\r\n\tif !strings.Contains(ctype.(string), \"image\") {\r\n\t\treturn nil, fmt.Errorf(\"the mask should be an image file\")\r\n\t}\r\n\r\n\timg, _, err := image.Decode(file)\r\n\tif err != nil {\r\n\t\treturn nil, fmt.Errorf(\"could not decode the mask file: %v\", err)\r\n\t}\r\n\r\n\treturn img, nil\r\n}\r\n\r\n// encodeImg encodes an image to a destination of type io.Writer.\r\nfunc encodeImg(p *Processor, w io.Writer, img *image.NRGBA) error {\r\n\tswitch w := w.(type) {\r\n\tcase *os.File:\r\n\t\text := filepath.Ext(w.Name())\r\n\t\tswitch ext {\r\n\t\tcase \"\", \".jpg\", \".jpeg\":\r\n\t\t\tres, err := Resize(p, img)\r\n\t\t\tif err != nil {\r\n\t\t\t\treturn err\r\n\t\t\t}\r\n\t\t\treturn jpeg.Encode(w, res, &jpeg.Options{Quality: 100})\r\n\t\tcase \".png\":\r\n\t\t\tres, err := Resize(p, img)\r\n\t\t\tif err != nil {\r\n\t\t\t\treturn err\r\n\t\t\t}\r\n\t\t\treturn png.Encode(w, res)\r\n\t\tcase \".bmp\":\r\n\t\t\tres, err := Resize(p, img)\r\n\t\t\tif err != nil {\r\n\t\t\t\treturn err\r\n\t\t\t}\r\n\t\t\treturn bmp.Encode(w, res)\r\n\t\tdefault:\r\n\t\t\treturn errors.New(\"unsupported image format\")\r\n\t\t}\r\n\tdefault:\r\n\t\tres, err := Resize(p, img)\r\n\t\tif err != nil {\r\n\t\t\treturn err\r\n\t\t}\r\n\t\treturn jpeg.Encode(w, res, &jpeg.Options{Quality: 100})\r\n\t}\r\n}\r\n\r\n// rotateImage90 rotate the image by 90 degree counter clockwise.\r\nfunc rotateImage90(src *image.NRGBA) *image.NRGBA {\r\n\tb := src.Bounds()\r\n\tdst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X))\r\n\tfor dstY := 0; dstY < b.Max.X; dstY++ {\r\n\t\tfor dstX := 0; dstX < b.Max.Y; dstX++ {\r\n\t\t\tsrcX := b.Max.X - dstY - 1\r\n\t\t\tsrcY := dstX\r\n\r\n\t\t\tsrcOff := srcY*src.Stride + srcX*4\r\n\t\t\tdstOff := dstY*dst.Stride + dstX*4\r\n\t\t\tcopy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])\r\n\t\t}\r\n\t}\r\n\treturn dst\r\n}\r\n\r\n// rotateImage270 rotate the image by 270 degree counter clockwise.\r\nfunc rotateImage270(src *image.NRGBA) *image.NRGBA {\r\n\tb := src.Bounds()\r\n\tdst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X))\r\n\tfor dstY := 0; dstY < b.Max.X; dstY++ {\r\n\t\tfor dstX := 0; dstX < b.Max.Y; dstX++ {\r\n\t\t\tsrcX := dstY\r\n\t\t\tsrcY := b.Max.Y - dstX - 1\r\n\r\n\t\t\tsrcOff := srcY*src.Stride + srcX*4\r\n\t\t\tdstOff := dstY*dst.Stride + dstX*4\r\n\t\t\tcopy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])\r\n\t\t}\r\n\t}\r\n\r\n\treturn dst\r\n}\r\n\r\n// imgToNRGBA converts any image type to *image.NRGBA with min-point at (0, 0).\r\nfunc imgToNRGBA(img image.Image) *image.NRGBA {\r\n\tsrcBounds := img.Bounds()\r\n\tif srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 {\r\n\t\tif src0, ok := img.(*image.NRGBA); ok {\r\n\t\t\treturn src0\r\n\t\t}\r\n\t}\r\n\tsrcMinX := srcBounds.Min.X\r\n\tsrcMinY := srcBounds.Min.Y\r\n\r\n\tdstBounds := srcBounds.Sub(srcBounds.Min)\r\n\tdstW := dstBounds.Dx()\r\n\tdstH := dstBounds.Dy()\r\n\tdst := image.NewNRGBA(dstBounds)\r\n\r\n\tswitch src := img.(type) {\r\n\tcase *image.NRGBA:\r\n\t\trowSize := srcBounds.Dx() * 4\r\n\t\tfor dstY := 0; dstY < dstH; dstY++ {\r\n\t\t\tdi := dst.PixOffset(0, dstY)\r\n\t\t\tsi := src.PixOffset(srcMinX, srcMinY+dstY)\r\n\t\t\tfor dstX := 0; dstX < dstW; dstX++ {\r\n\t\t\t\tcopy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize])\r\n\t\t\t}\r\n\t\t}\r\n\tcase *image.YCbCr:\r\n\t\tfor dstY := 0; dstY < dstH; dstY++ {\r\n\t\t\tdi := dst.PixOffset(0, dstY)\r\n\t\t\tfor dstX := 0; dstX < dstW; dstX++ {\r\n\t\t\t\tsrcX := srcMinX + dstX\r\n\t\t\t\tsrcY := srcMinY + dstY\r\n\t\t\t\tsiy := src.YOffset(srcX, srcY)\r\n\t\t\t\tsic := src.COffset(srcX, srcY)\r\n\t\t\t\tr, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic])\r\n\t\t\t\tdst.Pix[di+0] = r\r\n\t\t\t\tdst.Pix[di+1] = g\r\n\t\t\t\tdst.Pix[di+2] = b\r\n\t\t\t\tdst.Pix[di+3] = 0xff\r\n\t\t\t\tdi += 4\r\n\t\t\t}\r\n\t\t}\r\n\tdefault:\r\n\t\tfor dstY := 0; dstY < dstH; dstY++ {\r\n\t\t\tdi := dst.PixOffset(0, dstY)\r\n\t\t\tfor dstX := 0; dstX < dstW; dstX++ {\r\n\t\t\t\tc := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)\r\n\t\t\t\tdst.Pix[di+0] = c.R\r\n\t\t\t\tdst.Pix[di+1] = c.G\r\n\t\t\t\tdst.Pix[di+2] = c.B\r\n\t\t\t\tdst.Pix[di+3] = c.A\r\n\t\t\t\tdi += 4\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\treturn dst\r\n}\r\n\r\n// imgToPix converts an image to a pixel array.\r\nfunc imgToPix(src *image.NRGBA) []uint8 {\r\n\tbounds := src.Bounds()\r\n\tpixels := make([]uint8, 0, bounds.Max.X*bounds.Max.Y*4)\r\n\r\n\tfor x := bounds.Min.X; x < bounds.Max.X; x++ {\r\n\t\tfor y := bounds.Min.Y; y < bounds.Max.Y; y++ {\r\n\t\t\tr, g, b, _ := src.At(y, x).RGBA()\r\n\t\t\tpixels = append(pixels, uint8(r>>8), uint8(g>>8), uint8(b>>8), 255)\r\n\t\t}\r\n\t}\r\n\r\n\treturn pixels\r\n}\r\n\r\n// pixToImage converts an array buffer to an image.\r\nfunc pixToImage(pixels []uint8, width, height int) image.Image {\r\n\tdst := image.NewNRGBA(image.Rect(0, 0, width, height))\r\n\tbounds := dst.Bounds()\r\n\tdx, dy := bounds.Max.X, bounds.Max.Y\r\n\tcol := color.NRGBA{\r\n\t\tR: uint8(0),\r\n\t\tG: uint8(0),\r\n\t\tB: uint8(0),\r\n\t\tA: uint8(255),\r\n\t}\r\n\r\n\tfor x := bounds.Min.X; x < dx; x++ {\r\n\t\tfor y := bounds.Min.Y; y < dy*4; y += 4 {\r\n\t\t\tcol.R = uint8(pixels[y+x*dy*4])\r\n\t\t\tcol.G = uint8(pixels[y+x*dy*4+1])\r\n\t\t\tcol.B = uint8(pixels[y+x*dy*4+2])\r\n\t\t\tcol.A = uint8(pixels[y+x*dy*4+3])\r\n\r\n\t\t\tdst.SetNRGBA(x, int(y/4), col)\r\n\t\t}\r\n\t}\r\n\r\n\treturn dst\r\n}\r\n\r\n// rgbToGrayscale converts an image to grayscale mode and\r\n// returns the pixel values as an one dimensional array.\r\nfunc rgbToGrayscale(src *image.NRGBA) []uint8 {\r\n\twidth, height := src.Bounds().Dx(), src.Bounds().Dy()\r\n\tgray := make([]uint8, width*height)\r\n\r\n\tfor y := 0; y < height; y++ {\r\n\t\tfor x := 0; x < width; x++ {\r\n\t\t\tr, g, b, _ := src.At(x, y).RGBA()\r\n\t\t\tgray[y*width+x] = uint8(\r\n\t\t\t\t(0.299*float64(r) +\r\n\t\t\t\t\t0.587*float64(g) +\r\n\t\t\t\t\t0.114*float64(b)) / 256,\r\n\t\t\t)\r\n\t\t}\r\n\t}\r\n\r\n\treturn gray\r\n}\r\n\r\n// dither converts an image to black and white image, where the white is fully transparent.\r\nfunc dither(src *image.NRGBA) *image.NRGBA {\r\n\tvar (\r\n\t\tbounds   = src.Bounds()\r\n\t\tdithered = image.NewNRGBA(bounds)\r\n\t\tdx       = bounds.Dx()\r\n\t\tdy       = bounds.Dy()\r\n\t)\r\n\r\n\tfor x := 0; x < dx; x++ {\r\n\t\tfor y := 0; y < dy; y++ {\r\n\t\t\tr, g, b, _ := src.At(x, y).RGBA()\r\n\t\t\tthreshold := func() color.Color {\r\n\t\t\t\tif r > 127 && g > 127 && b > 127 {\r\n\t\t\t\t\treturn color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}\r\n\t\t\t\t}\r\n\t\t\t\treturn color.NRGBA{A: 0x00}\r\n\t\t\t}\r\n\t\t\tdithered.Set(x, y, threshold())\r\n\t\t}\r\n\t}\r\n\r\n\treturn dithered\r\n}\r\n"
  },
  {
    "path": "image_test.go",
    "content": "package caire\n\nimport (\n\t\"image\"\n\t\"image/color\"\n\t\"image/color/palette\"\n\t\"image/draw\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/esimov/caire/utils\"\n)\n\nfunc TestImage_ShouldGetSampleImage(t *testing.T) {\n\tpath := filepath.Join(\"./testdata\", \"sample.jpg\")\n\t_, err := os.ReadFile(path)\n\tif err != nil {\n\t\tt.Errorf(\"Should get the sample image\")\n\t}\n}\n\nfunc TestImage_ImgToNRGBA(t *testing.T) {\n\trect := image.Rect(-1, -1, 15, 15)\n\tcolors := palette.Plan9\n\ttestCases := []struct {\n\t\tname string\n\t\timg  image.Image\n\t}{\n\t\t{\n\t\t\tname: \"NRGBA\",\n\t\t\timg:  makeNRGBAImage(rect, colors),\n\t\t},\n\t\t{\n\t\t\tname: \"YCbCr-444\",\n\t\t\timg:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio444),\n\t\t},\n\t\t{\n\t\t\tname: \"YCbCr-422\",\n\t\t\timg:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio422),\n\t\t},\n\t\t{\n\t\t\tname: \"YCbCr-420\",\n\t\t\timg:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio420),\n\t\t},\n\t\t{\n\t\t\tname: \"YCbCr-440\",\n\t\t\timg:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio440),\n\t\t},\n\t\t{\n\t\t\tname: \"YCbCr-410\",\n\t\t\timg:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio410),\n\t\t},\n\t\t{\n\t\t\tname: \"YCbCr-411\",\n\t\t\timg:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio411),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := tc.img.Bounds()\n\t\t\tfor y := r.Min.Y; y < r.Max.Y; y++ {\n\t\t\t\tbuf := make([]byte, r.Dx()*4)\n\t\t\t\tscan(tc.img, 0, y-r.Min.Y, r.Dx(), y+1-r.Min.Y, buf)\n\t\t\t\twantBuf := readRow(tc.img, y)\n\t\t\t\tif !compareBytes(buf, wantBuf, 1) {\n\t\t\t\t\tt.Errorf(\"scan horizontal line (y=%d): got %v want %v\", y, buf, wantBuf)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor x := r.Min.X; x < r.Max.X; x++ {\n\t\t\t\tbuf := make([]byte, r.Dy()*4)\n\t\t\t\tscan(tc.img, x-r.Min.X, 0, x+1-r.Min.X, r.Dy(), buf)\n\t\t\t\twantBuf := readColumn(tc.img, x)\n\t\t\t\tif !compareBytes(buf, wantBuf, 1) {\n\t\t\t\t\tt.Errorf(\"scan vertical line (x=%d): got %v want %v\", x, buf, wantBuf)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc scan(img image.Image, x1, y1, x2, y2 int, dst []uint8) {\n\tswitch img := img.(type) {\n\tcase *image.NRGBA:\n\t\tsize := (x2 - x1) * 4\n\t\tj := 0\n\t\ti := y1*img.Stride + x1*4\n\t\tfor y := y1; y < y2; y++ {\n\t\t\tcopy(dst[j:j+size], img.Pix[i:i+size])\n\t\t\tj += size\n\t\t\ti += img.Stride\n\t\t}\n\tcase *image.YCbCr:\n\t\tj := 0\n\t\tx1 += img.Rect.Min.X\n\t\tx2 += img.Rect.Min.X\n\t\ty1 += img.Rect.Min.Y\n\t\ty2 += img.Rect.Min.Y\n\t\tfor y := y1; y < y2; y++ {\n\t\t\tiy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X)\n\t\t\tfor x := x1; x < x2; x++ {\n\t\t\t\tvar ic int\n\t\t\t\tswitch img.SubsampleRatio {\n\t\t\t\tcase image.YCbCrSubsampleRatio444:\n\t\t\t\t\tic = (y-img.Rect.Min.Y)*img.CStride + (x - img.Rect.Min.X)\n\t\t\t\tcase image.YCbCrSubsampleRatio422:\n\t\t\t\t\tic = (y-img.Rect.Min.Y)*img.CStride + (x/2 - img.Rect.Min.X/2)\n\t\t\t\tcase image.YCbCrSubsampleRatio420:\n\t\t\t\t\tic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x/2 - img.Rect.Min.X/2)\n\t\t\t\tcase image.YCbCrSubsampleRatio440:\n\t\t\t\t\tic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x - img.Rect.Min.X)\n\t\t\t\tdefault:\n\t\t\t\t\tic = img.COffset(x, y)\n\t\t\t\t}\n\n\t\t\t\tyy := int(img.Y[iy])\n\t\t\t\tcb := int(img.Cb[ic]) - 128\n\t\t\t\tcr := int(img.Cr[ic]) - 128\n\n\t\t\t\tr := (yy<<16 + 91881*cr + 1<<15) >> 16\n\t\t\t\tif r > 0xff {\n\t\t\t\t\tr = 0xff\n\t\t\t\t} else if r < 0 {\n\t\t\t\t\tr = 0\n\t\t\t\t}\n\n\t\t\t\tg := (yy<<16 - 22554*cb - 46802*cr + 1<<15) >> 16\n\t\t\t\tif g > 0xff {\n\t\t\t\t\tg = 0xff\n\t\t\t\t} else if g < 0 {\n\t\t\t\t\tg = 0\n\t\t\t\t}\n\n\t\t\t\tb := (yy<<16 + 116130*cb + 1<<15) >> 16\n\t\t\t\tif b > 0xff {\n\t\t\t\t\tb = 0xff\n\t\t\t\t} else if b < 0 {\n\t\t\t\t\tb = 0\n\t\t\t\t}\n\n\t\t\t\tdst[j+0] = uint8(r)\n\t\t\t\tdst[j+1] = uint8(g)\n\t\t\t\tdst[j+2] = uint8(b)\n\t\t\t\tdst[j+3] = 0xff\n\n\t\t\t\tiy++\n\t\t\t\tj += 4\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc makeYCbCrImage(rect image.Rectangle, colors []color.Color, sr image.YCbCrSubsampleRatio) *image.YCbCr {\n\timg := image.NewYCbCr(rect, sr)\n\tj := 0\n\tfor y := rect.Min.Y; y < rect.Max.Y; y++ {\n\t\tfor x := rect.Min.X; x < rect.Max.X; x++ {\n\t\t\tiy := img.YOffset(x, y)\n\t\t\tic := img.COffset(x, y)\n\t\t\tc := color.NRGBAModel.Convert(colors[j]).(color.NRGBA)\n\t\t\timg.Y[iy], img.Cb[ic], img.Cr[ic] = color.RGBToYCbCr(c.R, c.G, c.B)\n\t\t\tj++\n\t\t}\n\t}\n\treturn img\n}\n\nfunc makeNRGBAImage(rect image.Rectangle, colors []color.Color) *image.NRGBA {\n\timg := image.NewNRGBA(rect)\n\tfillDrawImage(img, colors)\n\treturn img\n}\n\nfunc fillDrawImage(img draw.Image, colors []color.Color) {\n\tcolorsNRGBA := make([]color.NRGBA, len(colors))\n\tfor i, c := range colors {\n\t\tnrgba := color.NRGBAModel.Convert(c).(color.NRGBA)\n\t\tnrgba.A = uint8(i % 256)\n\t\tcolorsNRGBA[i] = nrgba\n\t}\n\trect := img.Bounds()\n\ti := 0\n\tfor y := rect.Min.Y; y < rect.Max.Y; y++ {\n\t\tfor x := rect.Min.X; x < rect.Max.X; x++ {\n\t\t\timg.Set(x, y, colorsNRGBA[i])\n\t\t\ti++\n\t\t}\n\t}\n}\n\nfunc readRow(img image.Image, y int) []uint8 {\n\trow := make([]byte, img.Bounds().Dx()*4)\n\ti := 0\n\tfor x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {\n\t\tc := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)\n\t\trow[i+0] = c.R\n\t\trow[i+1] = c.G\n\t\trow[i+2] = c.B\n\t\trow[i+3] = c.A\n\t\ti += 4\n\t}\n\treturn row\n}\n\nfunc readColumn(img image.Image, x int) []uint8 {\n\tcolumn := make([]byte, img.Bounds().Dy()*4)\n\ti := 0\n\tfor y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {\n\t\tc := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)\n\t\tcolumn[i+0] = c.R\n\t\tcolumn[i+1] = c.G\n\t\tcolumn[i+2] = c.B\n\t\tcolumn[i+3] = c.A\n\t\ti += 4\n\t}\n\treturn column\n}\n\nfunc compareBytes(a, b []uint8, delta int) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := 0; i < len(a); i++ {\n\t\tif utils.Abs(int(a[i])-int(b[i])) > delta {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "imop/blend.go",
    "content": "// Package imop implements the Porter-Duff composition operations\r\n// used for mixing a graphic element with its backdrop.\r\n// Porter and Duff presented in their paper 12 different composition operation,\r\n// but the image/draw core package implements only the source-over-destination and source.\r\n// This package is aimed to overcome the missing composite operations.\r\n\r\n// It is mainly used to debug the seam carving operation correctness\r\n// with face detection and image mask enabled.\r\n// When the GUI mode and the debugging option is activated it will show\r\n// the image mask and the detected faces rectangles in a distinct color.\r\npackage imop\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n\t\"sort\"\r\n\r\n\t\"github.com/esimov/caire/utils\"\r\n)\r\n\r\ntype BlendType int\r\n\r\nconst (\r\n\tNormal BlendType = iota\r\n\tDarken\r\n\tLighten\r\n\tMultiply\r\n\tScreen\r\n\tOverlay\r\n\tSoftLight\r\n\tHardLight\r\n\tColorDodge\r\n\tColorBurn\r\n\tDifference\r\n\tExclusion\r\n\r\n\t// Non-separable blend modes\r\n\tHue\r\n\tSaturation\r\n\tColorMode\r\n\tLuminosity\r\n)\r\n\r\n// Blend struct contains the currently active blend mode and all the supported blend modes.\r\ntype Blend struct {\r\n\tCurrentOp BlendType\r\n\tModes     []BlendType\r\n}\r\n\r\n// Color represents the RGB channel of a specific color.\r\ntype Color struct {\r\n\tR, G, B float64\r\n}\r\n\r\n// NewBlend intantiates a new Blend.\r\nfunc NewBlend() *Blend {\r\n\treturn &Blend{\r\n\t\tModes: []BlendType{\r\n\t\t\tNormal, Darken, Lighten, Multiply,\r\n\t\t\tScreen, Overlay, SoftLight, HardLight,\r\n\t\t\tColorDodge, ColorBurn, Difference, Exclusion,\r\n\t\t\tHue, Saturation, ColorMode, Luminosity,\r\n\t\t},\r\n\t}\r\n}\r\n\r\n// Set activate one of the supported blend modes.\r\nfunc (bl *Blend) Set(blendType BlendType) error {\r\n\tif utils.Contains(bl.Modes, blendType) {\r\n\t\tbl.CurrentOp = blendType\r\n\t\treturn nil\r\n\t}\r\n\r\n\treturn fmt.Errorf(\"unsupported blend mode\")\r\n}\r\n\r\n// Get returns the active blend mode.\r\nfunc (bl *Blend) Get() BlendType {\r\n\treturn bl.CurrentOp\r\n}\r\n\r\n// Lum gets the luminosity of a color.\r\nfunc (bl *Blend) Lum(rgb Color) float64 {\r\n\treturn 0.3*rgb.R + 0.59*rgb.G + 0.11*rgb.B\r\n}\r\n\r\n// SetLum set the luminosity on a color.\r\nfunc (bl *Blend) SetLum(rgb Color, l float64) Color {\r\n\tdelta := l - bl.Lum(rgb)\r\n\treturn bl.clip(Color{\r\n\t\trgb.R + delta,\r\n\t\trgb.G + delta,\r\n\t\trgb.B + delta,\r\n\t})\r\n}\r\n\r\n// clip clips the channels of a color between certain min and max values.\r\nfunc (bl *Blend) clip(rgb Color) Color {\r\n\tr, g, b := rgb.R, rgb.G, rgb.B\r\n\r\n\tl := bl.Lum(rgb)\r\n\tmin := utils.Min(r, g, b)\r\n\tmax := utils.Max(r, g, b)\r\n\r\n\tif min < 0 {\r\n\t\tr = l + (((r - l) * l) / (l - min))\r\n\t\tg = l + (((g - l) * l) / (l - min))\r\n\t\tb = l + (((b - l) * l) / (l - min))\r\n\t}\r\n\tif max > 1 {\r\n\t\tr = l + (((r - l) * (1 - l)) / (max - l))\r\n\t\tg = l + (((g - l) * (1 - l)) / (max - l))\r\n\t\tb = l + (((b - l) * (1 - l)) / (max - l))\r\n\t}\r\n\r\n\treturn Color{R: r, G: g, B: b}\r\n}\r\n\r\n// Sat gets the saturation of a color.\r\nfunc (bl *Blend) Sat(rgb Color) float64 {\r\n\treturn utils.Max(rgb.R, rgb.G, rgb.B) - utils.Min(rgb.R, rgb.G, rgb.B)\r\n}\r\n\r\n// channel is a key/value struct pair used for sorting the color channels\r\n// based on the color components having the minimum, middle, and maximum\r\n// values upon entry to the function.\r\n// The key component holds the channel name and val is the value it has.\r\ntype channel struct {\r\n\tkey string\r\n\tval float64\r\n}\r\n\r\nfunc (bl *Blend) SetSat(rgb Color, s float64) Color {\r\n\tcolor := map[string]float64{\r\n\t\t\"R\": rgb.R,\r\n\t\t\"G\": rgb.G,\r\n\t\t\"B\": rgb.B,\r\n\t}\r\n\tchannels := make([]channel, 0, 3)\r\n\tfor k, v := range color {\r\n\t\tchannels = append(channels, channel{k, v})\r\n\t}\r\n\t// Sort the color channels based on their values.\r\n\tsort.Slice(channels, func(i, j int) bool { return channels[i].val < channels[j].val })\r\n\tminChan, midChan, maxChan := channels[0].key, channels[1].key, channels[2].key\r\n\r\n\tif color[maxChan] > color[minChan] {\r\n\t\tcolor[midChan] = (((color[midChan] - color[minChan]) * s) / (color[maxChan] - color[minChan]))\r\n\t\tcolor[maxChan] = s\r\n\t} else {\r\n\t\tcolor[midChan], color[maxChan] = 0, 0\r\n\t}\r\n\tcolor[minChan] = 0\r\n\r\n\treturn Color{\r\n\t\tR: color[\"R\"],\r\n\t\tG: color[\"G\"],\r\n\t\tB: color[\"B\"],\r\n\t}\r\n}\r\n\r\n// Applies the alpha blending formula for a blend operation.\r\n// See: https://www.w3.org/TR/compositing-1/#blending\r\nfunc (bl *Blend) AlphaCompose(\r\n\tbackdropAlpha,\r\n\tsourceAlpha,\r\n\tcompositeAlpha,\r\n\tbackdropColor,\r\n\tsourceColor,\r\n\tcompositeColor float64,\r\n) float64 {\r\n\treturn ((1 - sourceAlpha/compositeAlpha) * backdropColor) +\r\n\t\t(sourceAlpha / compositeAlpha *\r\n\t\t\tmath.Round((1-backdropAlpha)*sourceColor+backdropAlpha*compositeColor))\r\n}\r\n"
  },
  {
    "path": "imop/blend_test.go",
    "content": "package imop\r\n\r\nimport (\r\n\t\"image\"\r\n\t\"image/color\"\r\n\t\"image/draw\"\r\n\t\"testing\"\r\n\r\n\t\"github.com/stretchr/testify/assert\"\r\n)\r\n\r\nfunc TestBlend_Basic(t *testing.T) {\r\n\tassert := assert.New(t)\r\n\r\n\top := NewBlend()\r\n\tassert.Empty(op.Get())\r\n\top.Set(Darken)\r\n\tassert.Equal(Darken, op.Get())\r\n\top.Set(Lighten)\r\n\tassert.Equal(Lighten, op.Get())\r\n\r\n\trgb := Color{R: 0xff, G: 0xff, B: 0xff}\r\n\tlum := op.Lum(rgb)\r\n\tassert.Equal(255.0, lum)\r\n\r\n\trgb = Color{R: 0, G: 0, B: 0}\r\n\tlum = op.Lum(rgb)\r\n\tassert.Equal(0.0, lum)\r\n\r\n\trgb = Color{R: 127, G: 127, B: 127}\r\n\tlum = op.Lum(rgb)\r\n\tassert.Equal(127.0, lum)\r\n\r\n\tforeground := Color{R: 0xff, G: 0xff, B: 0xff}\r\n\tbackground := Color{R: 0, G: 0, B: 0}\r\n\r\n\tassert.Equal(0.0, op.Sat(foreground))\r\n\tsat := op.SetSat(background, op.Sat(foreground))\r\n\tassert.Equal(Color{R: 0, G: 0, B: 0}, sat)\r\n}\r\n\r\nfunc TestBlend_Modes(t *testing.T) {\r\n\t// Note: all the expected values are taken by using as reference the results\r\n\t// obtained in Photoshop by overlapping two layers and applying the blend mode.\r\n\tassert := assert.New(t)\r\n\r\n\top := InitOp()\r\n\tblend := NewBlend()\r\n\r\n\tpinkFront := color.RGBA{R: 214, G: 20, B: 65, A: 255}\r\n\torangeBack := color.RGBA{R: 250, G: 121, B: 17, A: 255}\r\n\r\n\trect := image.Rect(0, 0, 1, 1)\r\n\tbmp := NewBitmap(rect)\r\n\tsource := image.NewNRGBA(rect)\r\n\tbackdrop := image.NewNRGBA(rect)\r\n\r\n\top.Set(SrcOver)\r\n\r\n\t// Darken\r\n\tblend.Set(Darken)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected := []uint8{214, 20, 17, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// Multiply\r\n\tblend.Set(Multiply)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{209, 9, 4, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// Screen\r\n\tblend.Set(Screen)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{254, 131, 77, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// Overlay\r\n\tblend.Set(Overlay)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{253, 18, 8, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// SoftLight\r\n\tblend.Set(SoftLight)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{232, 19, 23, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// HardLight\r\n\tblend.Set(HardLight)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{251, 67, 9, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// ColorDodge\r\n\tblend.Set(ColorDodge)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{255, 131, 22, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// ColorBurn\r\n\tblend.Set(ColorBurn)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{249, 0, 0, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// Difference\r\n\tblend.Set(Difference)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{35, 101, 48, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// Exclusion\r\n\tblend.Set(Exclusion)\r\n\tdraw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{44, 122, 73, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n}\r\n\r\nfunc TestBlend_NonSeparableModes(t *testing.T) {\r\n\tassert := assert.New(t)\r\n\r\n\top := InitOp()\r\n\tblend := NewBlend()\r\n\r\n\tfrontColor := color.RGBA{R: 250, G: 121, B: 17, A: 255}\r\n\tbackColor := color.RGBA{R: 214, G: 20, B: 65, A: 255}\r\n\r\n\trect := image.Rect(0, 0, 1, 1)\r\n\tbmp := NewBitmap(rect)\r\n\tsource := image.NewNRGBA(rect)\r\n\tbackdrop := image.NewNRGBA(rect)\r\n\r\n\top.Set(SrcOver)\r\n\r\n\t// Hue\r\n\tblend.Set(Hue)\r\n\tdraw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected := []uint8{255, 97, 133, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// Saturation\r\n\tblend.Set(Saturation)\r\n\tdraw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{233, 126, 39, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// Color\r\n\tblend.Set(ColorMode)\r\n\tdraw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{255, 97, 133, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n\r\n\t// Luminosity\r\n\tblend.Set(Luminosity)\r\n\tdraw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)\r\n\tdraw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)\r\n\top.Draw(bmp, source, backdrop, blend)\r\n\r\n\texpected = []uint8{148, 66, 0, 255}\r\n\tassert.EqualValues(expected, bmp.Img.Pix)\r\n}\r\n"
  },
  {
    "path": "imop/comp.go",
    "content": "// Package imop implements the Porter-Duff composition operations\n// used for mixing a graphic element with its backdrop.\n// Porter and Duff presented in their paper 12 different composition operation, but the\n// core image/draw core package implements only the source-over-destination and source.\n// This package implements all of the 12 composite operation together with some blending modes.\npackage imop\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"math\"\n\n\t\"github.com/esimov/caire/utils\"\n)\n\ntype CompType int\n\nconst (\n\tClear CompType = iota\n\tCopy\n\tDst\n\tSrcOver\n\tDstOver\n\tSrcIn\n\tDstIn\n\tSrcOut\n\tDstOut\n\tSrcAtop\n\tDstAtop\n\tXor\n)\n\n// Bitmap holds an image type as a placeholder for the Porter-Duff composition\n// operations which can be used as a source or destination image.\ntype Bitmap struct {\n\tImg *image.NRGBA\n}\n\n// Composite struct contains the currently active composition operation and all the supported operations.\ntype Composite struct {\n\tCurrentOp CompType\n\tOps       []CompType\n}\n\n// NewBitmap initializes a new Bitmap.\nfunc NewBitmap(rect image.Rectangle) *Bitmap {\n\treturn &Bitmap{\n\t\tImg: image.NewNRGBA(rect),\n\t}\n}\n\n// InitOp initializes a new composition operation.\nfunc InitOp() *Composite {\n\treturn &Composite{\n\t\tCurrentOp: SrcOver,\n\t\tOps: []CompType{\n\t\t\tClear,\n\t\t\tCopy,\n\t\t\tDst,\n\t\t\tSrcOver,\n\t\t\tDstOver,\n\t\t\tSrcIn,\n\t\t\tDstIn,\n\t\t\tSrcOut,\n\t\t\tDstOut,\n\t\t\tSrcAtop,\n\t\t\tDstAtop,\n\t\t\tXor,\n\t\t},\n\t}\n}\n\n// Set changes the current composition operation.\nfunc (op *Composite) Set(compType CompType) error {\n\tif utils.Contains(op.Ops, compType) {\n\t\top.CurrentOp = compType\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"unsupported composition operation\")\n}\n\n// Set changes the current composition operation.\nfunc (op *Composite) Get() CompType {\n\treturn op.CurrentOp\n}\n\n// Draw applies the currently active Ported-Duff composition operation formula,\n// taking as parameter the source and the destination image and draws the result into the bitmap.\n// If a blend mode is activated it will plug in the alpha blending formula also into the equation.\nfunc (op *Composite) Draw(bitmap *Bitmap, src, dst *image.NRGBA, blend *Blend) {\n\tdx, dy := src.Bounds().Dx(), src.Bounds().Dy()\n\n\tvar (\n\t\tr, g, b, a     uint32\n\t\trn, gn, bn, an float64\n\t)\n\n\tfor x := 0; x < dx; x++ {\n\t\tfor y := 0; y < dy; y++ {\n\t\t\tr1, g1, b1, a1 := src.At(x, y).RGBA()\n\t\t\tr2, g2, b2, a2 := dst.At(x, y).RGBA()\n\n\t\t\trs, gs, bs, as := r1>>8, g1>>8, b1>>8, a1>>8\n\t\t\trb, gb, bb, ab := r2>>8, g2>>8, b2>>8, a2>>8\n\n\t\t\t// normalize the values.\n\t\t\trsn := float64(rs) / 255\n\t\t\tgsn := float64(gs) / 255\n\t\t\tbsn := float64(bs) / 255\n\t\t\tasn := float64(as) / 255\n\n\t\t\trbn := float64(rb) / 255\n\t\t\tgbn := float64(gb) / 255\n\t\t\tbbn := float64(bb) / 255\n\t\t\tabn := float64(ab) / 255\n\n\t\t\t// applying the alpha composition formula\n\t\t\tswitch op.CurrentOp {\n\t\t\tcase Clear:\n\t\t\t\trn, gn, bn, an = 0, 0, 0, 0\n\t\t\tcase Copy:\n\t\t\t\trn = asn * rsn\n\t\t\t\tgn = asn * gsn\n\t\t\t\tbn = asn * bsn\n\t\t\t\tan = asn * asn\n\t\t\tcase Dst:\n\t\t\t\trn = abn * rbn\n\t\t\t\tgn = abn * gbn\n\t\t\t\tbn = abn * bbn\n\t\t\t\tan = abn * abn\n\t\t\tcase SrcOver:\n\t\t\t\trn = asn*rsn + abn*rbn*(1-asn)\n\t\t\t\tgn = asn*gsn + abn*gbn*(1-asn)\n\t\t\t\tbn = asn*bsn + abn*bbn*(1-asn)\n\t\t\t\tan = asn + abn*(1-asn)\n\t\t\tcase DstOver:\n\t\t\t\trn = asn*rsn*(1-abn) + abn*rbn\n\t\t\t\tgn = asn*gsn*(1-abn) + abn*gbn\n\t\t\t\tbn = asn*bsn*(1-abn) + abn*bbn\n\t\t\t\tan = asn*(1-abn) + abn\n\t\t\tcase SrcIn:\n\t\t\t\trn = asn * rsn * abn\n\t\t\t\tgn = asn * gsn * abn\n\t\t\t\tbn = asn * bsn * abn\n\t\t\t\tan = asn * abn\n\t\t\tcase DstIn:\n\t\t\t\trn = abn * rbn * asn\n\t\t\t\tgn = abn * gbn * asn\n\t\t\t\tbn = abn * bbn * asn\n\t\t\t\tan = abn * asn\n\t\t\tcase SrcOut:\n\t\t\t\trn = asn * rsn * (1 - abn)\n\t\t\t\tgn = asn * gsn * (1 - abn)\n\t\t\t\tbn = asn * bsn * (1 - abn)\n\t\t\t\tan = asn * (1 - abn)\n\t\t\tcase DstOut:\n\t\t\t\trn = abn * rbn * (1 - asn)\n\t\t\t\tgn = abn * gbn * (1 - asn)\n\t\t\t\tbn = abn * bbn * (1 - asn)\n\t\t\t\tan = abn * (1 - asn)\n\t\t\tcase SrcAtop:\n\t\t\t\trn = asn*rsn*abn + (1-asn)*abn*rbn\n\t\t\t\tgn = asn*gsn*abn + (1-asn)*abn*gbn\n\t\t\t\tbn = asn*bsn*abn + (1-asn)*abn*bbn\n\t\t\t\tan = asn*abn + abn*(1-asn)\n\t\t\tcase DstAtop:\n\t\t\t\trn = asn*rsn*(1-abn) + abn*rbn*asn\n\t\t\t\tgn = asn*gsn*(1-abn) + abn*gbn*asn\n\t\t\t\tbn = asn*bsn*(1-abn) + abn*bbn*asn\n\t\t\t\tan = asn*(1-abn) + abn*asn\n\t\t\tcase Xor:\n\t\t\t\trn = asn*rsn*(1-abn) + abn*rbn*(1-asn)\n\t\t\t\tgn = asn*gsn*(1-abn) + abn*gbn*(1-asn)\n\t\t\t\tbn = asn*bsn*(1-abn) + abn*bbn*(1-asn)\n\t\t\t\tan = asn*(1-abn) + abn*(1-asn)\n\t\t\t}\n\n\t\t\tr = uint32(rn * 255)\n\t\t\tg = uint32(gn * 255)\n\t\t\tb = uint32(bn * 255)\n\t\t\ta = uint32(an * 255)\n\n\t\t\tbitmap.Img.Set(x, y, color.NRGBA{\n\t\t\t\tR: uint8(r),\n\t\t\t\tG: uint8(g),\n\t\t\t\tB: uint8(b),\n\t\t\t\tA: uint8(a),\n\t\t\t})\n\n\t\t\t// applying the blending mode\n\t\t\tif blend != nil {\n\t\t\t\trn, gn, bn, an = 0, 0, 0, 0 // reset the colors\n\t\t\t\tr1, g1, b1, a1 = src.At(x, y).RGBA()\n\t\t\t\tr2, g2, b2, a2 = dst.At(x, y).RGBA()\n\n\t\t\t\trs, gs, bs, as = r1>>8, g1>>8, b1>>8, a1>>8\n\t\t\t\trb, gb, bb, ab = r2>>8, g2>>8, b2>>8, a2>>8\n\n\t\t\t\trsn = float64(rs) / 255\n\t\t\t\tgsn = float64(gs) / 255\n\t\t\t\tbsn = float64(bs) / 255\n\t\t\t\tasn = float64(as) / 255\n\n\t\t\t\trbn = float64(rb) / 255\n\t\t\t\tgbn = float64(gb) / 255\n\t\t\t\tbbn = float64(bb) / 255\n\t\t\t\tabn = float64(ab) / 255\n\n\t\t\t\tforeground := Color{R: rsn, G: gsn, B: bsn}\n\t\t\t\tbackground := Color{R: rbn, G: gbn, B: bbn}\n\n\t\t\t\tswitch blend.CurrentOp {\n\t\t\t\tcase Normal:\n\t\t\t\t\trn, gn, bn, an = rsn, gsn, bsn, asn\n\t\t\t\tcase Darken:\n\t\t\t\t\trn = utils.Min(rsn, rbn)\n\t\t\t\t\tgn = utils.Min(gsn, gbn)\n\t\t\t\t\tbn = utils.Min(bsn, bbn)\n\t\t\t\t\tan = utils.Min(asn, abn)\n\t\t\t\tcase Lighten:\n\t\t\t\t\trn = utils.Max(rsn, rbn)\n\t\t\t\t\tgn = utils.Max(gsn, gbn)\n\t\t\t\t\tbn = utils.Max(bsn, bbn)\n\t\t\t\t\tan = utils.Max(asn, abn)\n\t\t\t\tcase Screen:\n\t\t\t\t\trn = 1 - (1-rsn)*(1-rbn)\n\t\t\t\t\tgn = 1 - (1-gsn)*(1-gbn)\n\t\t\t\t\tbn = 1 - (1-bsn)*(1-bbn)\n\t\t\t\t\tan = 1 - (1-asn)*(1-abn)\n\t\t\t\tcase Multiply:\n\t\t\t\t\trn = rsn * rbn\n\t\t\t\t\tgn = gsn * gbn\n\t\t\t\t\tbn = bsn * bbn\n\t\t\t\t\tan = asn * abn\n\t\t\t\tcase Overlay:\n\t\t\t\t\tif rsn <= 0.5 {\n\t\t\t\t\t\trn = 2 * rsn * rbn\n\t\t\t\t\t} else {\n\t\t\t\t\t\trn = 1 - 2*(1-rsn)*(1-rbn)\n\t\t\t\t\t}\n\n\t\t\t\t\tif gsn <= 0.5 {\n\t\t\t\t\t\tgn = 2 * gsn * gbn\n\t\t\t\t\t} else {\n\t\t\t\t\t\tgn = 1 - 2*(1-gsn)*(1-gbn)\n\t\t\t\t\t}\n\n\t\t\t\t\tif bsn <= 0.5 {\n\t\t\t\t\t\tbn = 2 * bsn * bbn\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbn = 1 - 2*(1-bsn)*(1-bbn)\n\t\t\t\t\t}\n\n\t\t\t\t\tif asn <= 0.5 {\n\t\t\t\t\t\tan = 2 * asn * abn\n\t\t\t\t\t} else {\n\t\t\t\t\t\tan = 1 - 2*(1-asn)*(1-abn)\n\t\t\t\t\t}\n\t\t\t\tcase SoftLight:\n\t\t\t\t\tif rbn < 0.5 {\n\t\t\t\t\t\trn = rsn - (1-2*rbn)*rsn*(1-rsn)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar w3r float64\n\t\t\t\t\t\tif rsn < 0.25 {\n\t\t\t\t\t\t\tw3r = ((16*rsn-12)*rsn + 4) * rsn\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tw3r = math.Sqrt(rsn)\n\t\t\t\t\t\t}\n\t\t\t\t\t\trn = rsn + (2*rbn-1)*(w3r-rsn)\n\t\t\t\t\t}\n\n\t\t\t\t\tif gbn < 0.5 {\n\t\t\t\t\t\tgn = gsn - (1-2*gbn)*gsn*(1-gsn)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar w3g float64\n\t\t\t\t\t\tif gsn < 0.25 {\n\t\t\t\t\t\t\tw3g = ((16*gsn-12)*gsn + 4) * gsn\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tw3g = math.Sqrt(gsn)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgn = gsn + (2*gbn-1)*(w3g-gsn)\n\t\t\t\t\t}\n\n\t\t\t\t\tif bbn < 0.5 {\n\t\t\t\t\t\tbn = bsn - (1-2*bbn)*bsn*(1-bsn)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar w3b float64\n\t\t\t\t\t\tif bsn < 0.25 {\n\t\t\t\t\t\t\tw3b = ((16*bsn-12)*bsn + 4) * bsn\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tw3b = math.Sqrt(bsn)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbn = bsn + (2*bbn-1)*(w3b-bsn)\n\t\t\t\t\t}\n\n\t\t\t\t\tif abn < 0.5 {\n\t\t\t\t\t\tan = asn - (1-2*abn)*asn*(1-asn)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar w3a float64\n\t\t\t\t\t\tif asn < 0.25 {\n\t\t\t\t\t\t\tw3a = ((16*asn-12)*asn + 4) * asn\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tw3a = math.Sqrt(asn)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tan = asn + (2*abn-1)*(w3a-asn)\n\t\t\t\t\t}\n\t\t\t\tcase HardLight:\n\t\t\t\t\tif rbn < 0.5 {\n\t\t\t\t\t\trn = rbn - (1-2*rsn)*rbn*(1-rbn)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar w3r float64\n\t\t\t\t\t\tif rbn < 0.25 {\n\t\t\t\t\t\t\tw3r = ((16*rbn-12)*rbn + 4) * rbn\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tw3r = math.Sqrt(rbn)\n\t\t\t\t\t\t}\n\t\t\t\t\t\trn = rbn + (2*rsn-1)*(w3r-rbn)\n\t\t\t\t\t}\n\n\t\t\t\t\tif gbn < 0.5 {\n\t\t\t\t\t\tgn = gbn - (1-2*gsn)*gbn*(1-gbn)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar w3g float64\n\t\t\t\t\t\tif gbn < 0.25 {\n\t\t\t\t\t\t\tw3g = ((16*gbn-12)*gbn + 4) * gbn\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tw3g = math.Sqrt(gbn)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tgn = gbn + (2*gsn-1)*(w3g-gbn)\n\t\t\t\t\t}\n\n\t\t\t\t\tif bbn < 0.5 {\n\t\t\t\t\t\tbn = bbn - (1-2*bsn)*bbn*(1-bbn)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar w3b float64\n\t\t\t\t\t\tif bbn < 0.25 {\n\t\t\t\t\t\t\tw3b = ((16*bbn-12)*bbn + 4) * bbn\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tw3b = math.Sqrt(bbn)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbn = bbn + (2*bsn-1)*(w3b-bbn)\n\t\t\t\t\t}\n\n\t\t\t\t\tif abn < 0.5 {\n\t\t\t\t\t\tan = abn - (1-2*asn)*abn*(1-abn)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar w3a float64\n\t\t\t\t\t\tif abn < 0.25 {\n\t\t\t\t\t\t\tw3a = ((16*abn-12)*abn + 4) * abn\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tw3a = math.Sqrt(abn)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tan = abn + (2*asn-1)*(w3a-abn)\n\t\t\t\t\t}\n\t\t\t\tcase ColorDodge:\n\t\t\t\t\tif rsn < 1 {\n\t\t\t\t\t\trn = utils.Min(1, rbn/(1-rsn))\n\t\t\t\t\t} else if rsn == 1 {\n\t\t\t\t\t\trn = 1\n\t\t\t\t\t}\n\n\t\t\t\t\tif gsn < 1 {\n\t\t\t\t\t\tgn = utils.Min(1, gbn/(1-gsn))\n\t\t\t\t\t} else if gsn == 1 {\n\t\t\t\t\t\tgn = 1\n\t\t\t\t\t}\n\n\t\t\t\t\tif bsn < 1 {\n\t\t\t\t\t\tbn = utils.Min(1, bbn/(1-bsn))\n\t\t\t\t\t} else if bsn == 1 {\n\t\t\t\t\t\tbn = 1\n\t\t\t\t\t}\n\n\t\t\t\t\tif asn < 1 {\n\t\t\t\t\t\tan = utils.Min(1, abn/(1-asn))\n\t\t\t\t\t} else if asn == 1 {\n\t\t\t\t\t\tan = 1\n\t\t\t\t\t}\n\t\t\t\tcase ColorBurn:\n\t\t\t\t\tif rsn > 0 {\n\t\t\t\t\t\trn = 1 - utils.Min(1, (1-rbn)/rsn)\n\t\t\t\t\t} else if rsn == 0 {\n\t\t\t\t\t\trn = 0\n\t\t\t\t\t}\n\n\t\t\t\t\tif gsn > 0 {\n\t\t\t\t\t\tgn = 1 - utils.Min(1, (1-gbn)/gsn)\n\t\t\t\t\t} else if gsn == 0 {\n\t\t\t\t\t\tgn = 0\n\t\t\t\t\t}\n\n\t\t\t\t\tif bsn > 0 {\n\t\t\t\t\t\tbn = 1 - utils.Min(1, (1-bbn)/bsn)\n\t\t\t\t\t} else if bsn == 0 {\n\t\t\t\t\t\tbn = 0\n\t\t\t\t\t}\n\n\t\t\t\t\tif asn > 0 {\n\t\t\t\t\t\tan = 1 - utils.Min(1, (1-abn)/asn)\n\t\t\t\t\t} else if asn == 0 {\n\t\t\t\t\t\tan = 0\n\t\t\t\t\t}\n\t\t\t\tcase Difference:\n\t\t\t\t\trn = utils.Abs(rbn - rsn)\n\t\t\t\t\tgn = utils.Abs(gbn - gsn)\n\t\t\t\t\tbn = utils.Abs(bbn - bsn)\n\t\t\t\t\tan = 1\n\t\t\t\tcase Exclusion:\n\t\t\t\t\trn = rsn + rbn - 2*rsn*rbn\n\t\t\t\t\tgn = gsn + gbn - 2*gsn*gbn\n\t\t\t\t\tbn = bsn + bbn - 2*bsn*bbn\n\t\t\t\t\tan = 1\n\n\t\t\t\t// Non-separable blend modes\n\t\t\t\t// https://www.w3.org/TR/compositing-1/#blendingnonseparable\n\t\t\t\tcase Hue:\n\t\t\t\t\tsat := blend.SetSat(background, blend.Sat(foreground))\n\t\t\t\t\trgb := blend.SetLum(sat, blend.Lum(foreground))\n\n\t\t\t\t\ta := asn + abn - asn*abn\n\t\t\t\t\trn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)\n\t\t\t\t\tgn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)\n\t\t\t\t\tbn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)\n\t\t\t\t\trn, gn, bn = rn/255, gn/255, bn/255\n\t\t\t\t\tan = a\n\t\t\t\tcase Saturation:\n\t\t\t\t\tsat := blend.SetSat(foreground, blend.Sat(background))\n\t\t\t\t\trgb := blend.SetLum(sat, blend.Lum(foreground))\n\n\t\t\t\t\ta := asn + abn - asn*abn\n\t\t\t\t\trn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)\n\t\t\t\t\tgn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)\n\t\t\t\t\tbn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)\n\t\t\t\t\trn, gn, bn = rn/255, gn/255, bn/255\n\t\t\t\t\tan = a\n\t\t\t\tcase ColorMode:\n\t\t\t\t\trgb := blend.SetLum(background, blend.Lum(foreground))\n\n\t\t\t\t\ta := asn + abn - asn*abn\n\t\t\t\t\trn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)\n\t\t\t\t\tgn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)\n\t\t\t\t\tbn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)\n\t\t\t\t\trn, gn, bn = rn/255, gn/255, bn/255\n\t\t\t\t\tan = a\n\t\t\t\tcase Luminosity:\n\t\t\t\t\trgb := blend.SetLum(foreground, blend.Lum(background))\n\n\t\t\t\t\ta := asn + abn - asn*abn\n\t\t\t\t\trn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)\n\t\t\t\t\tgn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)\n\t\t\t\t\tbn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)\n\t\t\t\t\trn, gn, bn = rn/255, gn/255, bn/255\n\t\t\t\t\tan = a\n\t\t\t\t}\n\n\t\t\t\tr = uint32(rn * 255)\n\t\t\t\tg = uint32(gn * 255)\n\t\t\t\tb = uint32(bn * 255)\n\t\t\t\ta = uint32(an * 255)\n\n\t\t\t\tbitmap.Img.Set(x, y, color.NRGBA{\n\t\t\t\t\tR: uint8(r),\n\t\t\t\t\tG: uint8(g),\n\t\t\t\t\tB: uint8(b),\n\t\t\t\t\tA: uint8(a),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "imop/comp_test.go",
    "content": "package imop\n\nimport (\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestComp_Basic(t *testing.T) {\n\tassert := assert.New(t)\n\n\top := InitOp()\n\n\top.Set(Clear)\n\tassert.Equal(Clear, op.Get())\n\tassert.NotEqual(\"unsupported_composite_operation\", op.Get())\n\n\top.Set(Dst)\n\tassert.Equal(Dst, op.Get())\n}\n\nfunc TestComp_Ops(t *testing.T) {\n\tassert := assert.New(t)\n\top := InitOp()\n\n\ttransparent := color.NRGBA{R: 0, G: 0, B: 0, A: 0}\n\tcyan := color.NRGBA{R: 33, G: 150, B: 243, A: 255}\n\tmagenta := color.NRGBA{R: 233, G: 30, B: 99, A: 255}\n\n\trect := image.Rect(0, 0, 10, 10)\n\tbmp := NewBitmap(rect)\n\tsource := image.NewNRGBA(rect)\n\tbackdrop := image.NewNRGBA(rect)\n\n\t// No composition operation applied. The SrcOver is the default one.\n\tdraw.Draw(source, image.Rect(0, 4, 6, 10), &image.Uniform{cyan}, image.Point{}, draw.Src)\n\tdraw.Draw(backdrop, image.Rect(4, 0, 10, 6), &image.Uniform{magenta}, image.Point{}, draw.Src)\n\top.Draw(bmp, source, backdrop, nil)\n\n\t// Pick three representative points/pixels from the generated image output.\n\t// Depending on the applied composition operation the colors of the\n\t// selected pixels should be the source color, the destination color or transparent.\n\ttopRight := bmp.Img.At(9, 0)\n\tbottomLeft := bmp.Img.At(0, 9)\n\tcenter := bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, magenta)\n\tassert.EqualValues(bottomLeft, cyan)\n\tassert.EqualValues(center, cyan)\n\n\t// Clear\n\top.Set(Clear)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, transparent)\n\tassert.EqualValues(bottomLeft, transparent)\n\tassert.EqualValues(center, transparent)\n\n\t// Copy\n\top.Set(Copy)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, transparent)\n\tassert.EqualValues(bottomLeft, cyan)\n\tassert.EqualValues(center, cyan)\n\n\t// Dst\n\top.Set(Dst)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, magenta)\n\tassert.EqualValues(bottomLeft, transparent)\n\tassert.EqualValues(center, magenta)\n\n\t// SrcOver\n\top.Set(SrcOver)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, magenta)\n\tassert.EqualValues(bottomLeft, cyan)\n\tassert.EqualValues(center, cyan)\n\n\t// DstOver\n\top.Set(DstOver)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, magenta)\n\tassert.EqualValues(bottomLeft, cyan)\n\tassert.EqualValues(center, magenta)\n\n\t// SrcIn\n\top.Set(SrcIn)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, transparent)\n\tassert.EqualValues(bottomLeft, transparent)\n\tassert.EqualValues(center, cyan)\n\n\t// DstIn\n\top.Set(DstIn)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, transparent)\n\tassert.EqualValues(bottomLeft, transparent)\n\tassert.EqualValues(center, magenta)\n\n\t// SrcOut\n\top.Set(SrcOut)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, transparent)\n\tassert.EqualValues(bottomLeft, cyan)\n\tassert.EqualValues(center, transparent)\n\n\t// DstOut\n\top.Set(DstOut)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, magenta)\n\tassert.EqualValues(bottomLeft, transparent)\n\tassert.EqualValues(center, transparent)\n\n\t// SrcAtop\n\top.Set(SrcAtop)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, magenta)\n\tassert.EqualValues(bottomLeft, transparent)\n\tassert.EqualValues(center, cyan)\n\n\t// DstAtop\n\top.Set(DstAtop)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, transparent)\n\tassert.EqualValues(bottomLeft, cyan)\n\tassert.EqualValues(center, magenta)\n\n\t// Xor\n\top.Set(Xor)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, magenta)\n\tassert.EqualValues(bottomLeft, cyan)\n\tassert.EqualValues(center, transparent)\n\t// DstAtop\n\top.Set(DstAtop)\n\top.Draw(bmp, source, backdrop, nil)\n\n\ttopRight = bmp.Img.At(9, 0)\n\tbottomLeft = bmp.Img.At(0, 9)\n\tcenter = bmp.Img.At(5, 5)\n\n\tassert.EqualValues(topRight, transparent)\n\tassert.EqualValues(bottomLeft, cyan)\n\tassert.EqualValues(center, magenta)\n}\n"
  },
  {
    "path": "preview.go",
    "content": "package caire\n\nimport (\n\t\"os\"\n)\n\n// showPreview spawns a new Gio GUI window and updates its content with the resized image received from a channel.\nfunc (p *Processor) showPreview(\n\timgWorker <-chan worker,\n\terrChan chan<- error,\n\tguiWindow struct {\n\t\twidth  int\n\t\theight int\n\t},\n) {\n\tvar gui = NewGUI(guiWindow.width, guiWindow.height)\n\tgui.proc = p\n\tgui.process.worker = imgWorker\n\n\t// Run the Gio GUI app in a separate goroutine\n\tgo func() {\n\t\tif err := gui.Run(); err != nil {\n\t\t\terrChan <- err\n\t\t}\n\t\t// It's important to call os.Exit(0) in order to terminate\n\t\t// the execution of the GUI app when pressing ESC key.\n\t\tos.Exit(0)\n\t}()\n}\n"
  },
  {
    "path": "processor.go",
    "content": "package caire\n\nimport (\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/draw\"\n\t\"io\"\n\t\"math\"\n\n\t\"github.com/disintegration/imaging\"\n\t\"github.com/esimov/caire/utils\"\n\tpigo \"github.com/esimov/pigo/core\"\n)\n\n//go:embed data/facefinder\nvar cascadeFile []byte\n\nvar (\n\tresizeXY = false // the image is resized both vertically and horizontally\n\n\timgWorker = make(chan worker) // channel used to transfer the image to the GUI\n\terrs      = make(chan error)\n)\n\nvar _ SeamCarver = (*Processor)(nil)\n\n// worker struct contains all the information needed for transferring the resized image to the Gio GUI.\ntype worker struct {\n\timg   *image.NRGBA\n\tmask  *image.NRGBA\n\tseams []Seam\n\tdone  bool\n}\n\n// shrinkFn is a generic function used to shrink an image.\ntype shrinkFn func(*image.NRGBA) (*image.NRGBA, error)\n\n// enlargeFn is a generic function used to enlarge an image.\ntype enlargeFn func(*image.NRGBA) (*image.NRGBA, error)\n\n// Processor options\ntype Processor struct {\n\tFaceAngle      float64\n\tSeamColor      string\n\tMaskPath       string\n\tRMaskPath      string\n\tShapeType      ShapeType\n\tSobelThreshold int\n\tBlurRadius     int\n\tNewWidth       int\n\tNewHeight      int\n\tFaceDetector   *pigo.Pigo\n\tSpinner        *utils.Spinner\n\tMask           *image.NRGBA\n\tRMask          *image.NRGBA\n\tDebugMask      *image.NRGBA\n\tPercentage     bool\n\tSquare         bool\n\tDebug          bool\n\tPreview        bool\n\tFaceDetect     bool\n\tvRes           bool\n}\n\nvar (\n\tshrinkHorizFn  shrinkFn\n\tshrinkVertFn   shrinkFn\n\tenlargeHorizFn enlargeFn\n\tenlargeVertFn  enlargeFn\n)\n\n// Carve is the main entry point for the image resize operation.\n// The new image can be resized either horizontally or vertically (or both).\n// Depending on the provided options the image can be either reduced or enlarged.\nfunc (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {\n\tvar (\n\t\tnewImg    image.Image\n\t\tnewWidth  int\n\t\tnewHeight int\n\t\tpw, ph    int\n\t\terr       error\n\t)\n\tc := NewCarver(img.Bounds().Dx(), img.Bounds().Dy())\n\n\tif p.NewWidth > c.Width {\n\t\tnewWidth = p.NewWidth - (p.NewWidth - (p.NewWidth - c.Width))\n\t} else {\n\t\tnewWidth = c.Width - (c.Width - (c.Width - p.NewWidth))\n\t}\n\n\tif p.NewHeight > c.Height {\n\t\tnewHeight = p.NewHeight - (p.NewHeight - (p.NewHeight - c.Height))\n\t} else {\n\t\tnewHeight = c.Height - (c.Height - (c.Height - p.NewHeight))\n\t}\n\n\tif p.NewWidth == 0 {\n\t\tnewWidth = p.NewWidth\n\t}\n\tif p.NewHeight == 0 {\n\t\tnewHeight = p.NewHeight\n\t}\n\n\t// shrinkHorizFn calls itself recursively to shrink the image horizontally.\n\t// If the image is resized on both X and Y axis it calls the shrink and enlarge\n\t// function intermittently up until the desired dimension is reached.\n\t// I opted for this solution instead of resizing the image sequentially,\n\t// to avoid resulting visual artefacts, since this way\n\t// the horizontal and vertical seams are merged together seamlessly.\n\tshrinkHorizFn = func(img *image.NRGBA) (*image.NRGBA, error) {\n\t\tp.vRes = false\n\t\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\t\tif dx > p.NewWidth {\n\t\t\timg, err = p.shrink(img)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif p.NewHeight > 0 && p.NewHeight != dy {\n\t\t\t\tif p.NewHeight <= dy {\n\t\t\t\t\timg, err = shrinkVertFn(img)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\timg, err = enlargeVertFn(img)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\timg, err = shrinkHorizFn(img)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn img, nil\n\t}\n\n\t// enlargeHorizFn calls itself recursively to enlarge the image horizontally.\n\tenlargeHorizFn = func(img *image.NRGBA) (*image.NRGBA, error) {\n\t\tp.vRes = false\n\t\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\t\tif dx < p.NewWidth {\n\t\t\timg, err = p.enlarge(img)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif p.NewHeight > 0 && p.NewHeight != dy {\n\t\t\t\tif p.NewHeight <= dy {\n\t\t\t\t\timg, err = shrinkVertFn(img)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\timg, err = enlargeVertFn(img)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\timg, err = enlargeHorizFn(img)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn img, nil\n\t}\n\n\t// shrinkVertFn calls itself recursively to shrink the image vertically.\n\tshrinkVertFn = func(img *image.NRGBA) (*image.NRGBA, error) {\n\t\tp.vRes = true\n\t\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\n\t\t// If the image is resized both horizontally and vertically we need\n\t\t// to rotate the image each time when invoking the shrink function.\n\t\t// Otherwise we rotate the image only once, right before calling this function.\n\t\tif resizeXY {\n\t\t\tdx, dy = img.Bounds().Dy(), img.Bounds().Dx()\n\t\t\timg = rotateImage90(img)\n\t\t}\n\t\tif dx > p.NewHeight {\n\t\t\timg, err = p.shrink(img)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif resizeXY {\n\t\t\t\timg = rotateImage270(img)\n\t\t\t}\n\t\t\tif p.NewWidth > 0 && p.NewWidth != dy {\n\t\t\t\tif p.NewWidth <= dy {\n\t\t\t\t\timg, err = shrinkHorizFn(img)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\timg, err = enlargeHorizFn(img)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\timg, err = shrinkVertFn(img)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif resizeXY {\n\t\t\t\timg = rotateImage270(img)\n\t\t\t}\n\t\t}\n\t\treturn img, nil\n\t}\n\n\t// enlargeVertFn calls itself recursively to enlarge the image vertically.\n\tenlargeVertFn = func(img *image.NRGBA) (*image.NRGBA, error) {\n\t\tp.vRes = true\n\t\tdx, dy := img.Bounds().Dx(), img.Bounds().Dy()\n\n\t\tif resizeXY {\n\t\t\tdx, dy = img.Bounds().Dy(), img.Bounds().Dx()\n\t\t\timg = rotateImage90(img)\n\t\t}\n\t\tif dx < p.NewHeight {\n\t\t\timg, err = p.enlarge(img)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif resizeXY {\n\t\t\t\timg = rotateImage270(img)\n\t\t\t}\n\t\t\tif p.NewWidth > 0 && p.NewWidth != dy {\n\t\t\t\tif p.NewWidth <= dy {\n\t\t\t\t\timg, err = shrinkHorizFn(img)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\timg, err = enlargeHorizFn(img)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\timg, err = enlargeVertFn(img)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif resizeXY {\n\t\t\t\timg = rotateImage270(img)\n\t\t\t}\n\t\t}\n\t\treturn img, nil\n\t}\n\n\tif p.Percentage || p.Square {\n\t\tpw = c.Width - c.Height\n\t\tph = c.Height - c.Width\n\n\t\t// In case pw and ph is zero, it means that the target image is square.\n\t\t// In this case we can simply resize the image without running the carving operation.\n\t\tif p.Percentage && pw == 0 && ph == 0 {\n\t\t\tpw = c.Width - int(float64(c.Width)-(float64(p.NewWidth)/100*float64(c.Width)))\n\t\t\tph = c.Height - int(float64(c.Height)-(float64(p.NewHeight)/100*float64(c.Height)))\n\n\t\t\tp.NewWidth = utils.Abs(c.Width - pw)\n\t\t\tp.NewHeight = utils.Abs(c.Height - ph)\n\n\t\t\tresImgSize := utils.Min(p.NewWidth, p.NewHeight)\n\t\t\treturn imaging.Resize(img, resImgSize, 0, imaging.Lanczos), nil\n\t\t}\n\n\t\t// When the square option is used the image will be resized to a square based on the shortest edge.\n\t\tif p.Square {\n\t\t\t// Calling the image rescale method only when both a new width and height is provided.\n\t\t\tif p.NewWidth != 0 && p.NewHeight != 0 {\n\t\t\t\tp.NewWidth = utils.Min(p.NewWidth, p.NewHeight)\n\t\t\t\tp.NewHeight = p.NewWidth\n\n\t\t\t\tnewImg = p.calculateFitness(img, c)\n\t\t\t\tdst := image.NewNRGBA(newImg.Bounds())\n\t\t\t\tdraw.Draw(dst, newImg.Bounds(), newImg, image.Point{}, draw.Src)\n\t\t\t\timg = dst\n\n\t\t\t\tnw, nh := img.Bounds().Dx(), img.Bounds().Dy()\n\n\t\t\t\tp.NewWidth = utils.Min(nw, nh)\n\t\t\t\tp.NewHeight = p.NewWidth\n\t\t\t} else {\n\t\t\t\treturn nil, errors.New(\"please provide a new WIDTH and HEIGHT when using the square option\")\n\t\t\t}\n\t\t}\n\n\t\t// Use the Percentage flag only for shrinking the image.\n\t\tif p.Percentage {\n\t\t\t// Calculate the new image size based on the provided percentage.\n\t\t\tpw = c.Width - int(float64(c.Width)-(float64(p.NewWidth)/100*float64(c.Width)))\n\t\t\tph = c.Height - int(float64(c.Height)-(float64(p.NewHeight)/100*float64(c.Height)))\n\n\t\t\tif p.NewWidth != 0 {\n\t\t\t\tp.NewWidth = utils.Abs(c.Width - pw)\n\t\t\t}\n\t\t\tif p.NewHeight != 0 {\n\t\t\t\tp.NewHeight = utils.Abs(c.Height - ph)\n\t\t\t}\n\t\t\tif pw >= c.Width || ph >= c.Height {\n\t\t\t\treturn nil, errors.New(\"cannot use the percentage flag for image enlargement\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Rescale the image when it is resized both horizontally and vertically.\n\t// First the image is scaled down or up by preserving the image aspect ratio,\n\t// then the seam carving algorithm is applied only to the remaining pixels.\n\n\t// Scale the width and height by the smaller factor (i.e Min(wScaleFactor, hScaleFactor))\n\t// Example: input: 5000x2500, scale: 2160x1080, final target: 1920x1080\n\tif (c.Width > p.NewWidth && c.Height > p.NewHeight) &&\n\t\t(p.NewWidth != 0 && p.NewHeight != 0) {\n\n\t\tnewImg = p.calculateFitness(img, c)\n\n\t\tdx0, dy0 := img.Bounds().Max.X, img.Bounds().Max.Y\n\t\tdx1, dy1 := newImg.Bounds().Max.X, newImg.Bounds().Max.Y\n\n\t\t// Rescale the image when the new image width or height are preserved, otherwise\n\t\t// it might happen, that the generated image size does not match with the requested image size.\n\t\tif !((p.NewWidth == 0 && dx0 == dx1) || (p.NewHeight == 0 && dy0 == dy1)) {\n\t\t\tdst := image.NewNRGBA(newImg.Bounds())\n\t\t\tdraw.Draw(dst, newImg.Bounds(), newImg, image.Point{}, draw.Src)\n\t\t\timg = dst\n\t\t}\n\t}\n\n\t// Run the carver function if the desired image width is not identical with the rescaled image width.\n\tif newWidth > 0 && p.NewWidth != c.Width {\n\t\tif p.NewWidth > c.Width {\n\t\t\timg, err = enlargeHorizFn(img)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\timg, err = shrinkHorizFn(img)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\t// Run the carver function if the desired image height is not identical with the rescaled image height.\n\tif newHeight > 0 && p.NewHeight != c.Height {\n\t\tif !resizeXY {\n\t\t\timg = rotateImage90(img)\n\n\t\t\tif p.Mask != nil {\n\t\t\t\tp.Mask = rotateImage90(p.Mask)\n\t\t\t}\n\t\t\tif p.RMask != nil {\n\t\t\t\tp.RMask = rotateImage90(p.RMask)\n\t\t\t}\n\t\t}\n\t\tif p.NewHeight > c.Height {\n\t\t\timg, err = enlargeVertFn(img)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\timg, err = shrinkVertFn(img)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tif !resizeXY {\n\t\t\timg = rotateImage270(img)\n\n\t\t\tif p.Mask != nil {\n\t\t\t\tp.Mask = rotateImage270(p.Mask)\n\t\t\t}\n\t\t\tif p.RMask != nil {\n\t\t\t\tp.RMask = rotateImage270(p.RMask)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Signal that the process is done and no more data is sent through the channel.\n\tgo func() {\n\t\timgWorker <- worker{\n\t\t\timg:   nil,\n\t\t\tmask:  nil,\n\t\t\tseams: nil,\n\t\t\tdone:  true,\n\t\t}\n\t}()\n\n\treturn img, nil\n}\n\n// calculateFitness iteratively try to find the best image aspect ratio for the rescale.\nfunc (p *Processor) calculateFitness(img *image.NRGBA, c *Carver) *image.NRGBA {\n\tvar (\n\t\tw      = float64(c.Width)\n\t\th      = float64(c.Height)\n\t\tnw     = float64(p.NewWidth)\n\t\tnh     = float64(p.NewHeight)\n\t\tnewImg *image.NRGBA\n\t)\n\twsf := w / nw\n\thsf := h / nh\n\tsw := math.Round(w / math.Min(wsf, hsf))\n\tsh := math.Round(h / math.Min(wsf, hsf))\n\n\tif sw <= sh {\n\t\tnewImg = imaging.Resize(img, 0, int(sw), imaging.Lanczos)\n\t\tif p.Mask != nil {\n\t\t\tp.Mask = imaging.Resize(p.Mask, 0, int(sw), imaging.Lanczos)\n\t\t}\n\t\tif p.RMask != nil {\n\t\t\tp.RMask = imaging.Resize(p.RMask, 0, int(sw), imaging.Lanczos)\n\t\t}\n\t} else {\n\t\tnewImg = imaging.Resize(img, 0, int(sh), imaging.Lanczos)\n\t\tif p.Mask != nil {\n\t\t\tp.Mask = imaging.Resize(p.Mask, 0, int(sh), imaging.Lanczos)\n\t\t}\n\t\tif p.RMask != nil {\n\t\t\tp.RMask = imaging.Resize(p.RMask, 0, int(sh), imaging.Lanczos)\n\t\t}\n\t}\n\tdx, dy := newImg.Bounds().Max.X, newImg.Bounds().Max.Y\n\tc.Width = dx\n\tc.Height = dy\n\n\tif int(sw) < p.NewWidth || int(sh) < p.NewHeight {\n\t\tnewImg = p.calculateFitness(newImg, c)\n\t}\n\treturn newImg\n}\n\n// Process encodes the resized image into an io.Writer interface.\n// We are using the io package, since we can provide different input and output types,\n// as long as they implement the io.Reader and io.Writer interface.\nfunc (p *Processor) Process(r io.Reader, w io.Writer) error {\n\tvar err error\n\n\tif p.FaceDetect {\n\t\t// Instantiate a new Pigo object in case the face detection option is used.\n\t\tp.FaceDetector = pigo.NewPigo()\n\n\t\t// Unpack the binary file. This will return the number of cascade trees,\n\t\t// the tree depth, the threshold and the prediction from tree's leaf nodes.\n\t\tp.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error unpacking the cascade file: %v\", err)\n\t\t}\n\t}\n\n\tif p.NewWidth != 0 && p.NewHeight != 0 {\n\t\tresizeXY = true\n\t}\n\n\tsrc, _, err := image.Decode(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\timg := imgToNRGBA(src)\n\n\tif p.Preview {\n\t\tguiWidth := img.Bounds().Max.X\n\t\tguiHeight := img.Bounds().Max.Y\n\n\t\tif p.NewWidth > guiWidth {\n\t\t\tguiWidth = p.NewWidth\n\t\t}\n\t\tif p.NewHeight > guiHeight {\n\t\t\tguiHeight = p.NewHeight\n\t\t}\n\t\tif resizeXY {\n\t\t\tguiWidth = int(maxScreenX)\n\t\t\tguiHeight = int(maxScreenY)\n\t\t}\n\n\t\tguiWindow := struct {\n\t\t\twidth  int\n\t\t\theight int\n\t\t}{\n\t\t\twidth:  guiWidth,\n\t\t\theight: guiHeight,\n\t\t}\n\t\t// Lunch Gio GUI thread.\n\t\tgo p.showPreview(imgWorker, errs, guiWindow)\n\t}\n\n\treturn encodeImg(p, w, img)\n}\n\n// shrink reduces the image dimension either horizontally or vertically.\nfunc (p *Processor) shrink(img *image.NRGBA) (*image.NRGBA, error) {\n\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\tc := NewCarver(width, height)\n\n\tif _, err := c.ComputeSeams(p, img); err != nil {\n\t\treturn nil, err\n\t}\n\tseams := c.FindLowestEnergySeams(p)\n\timg = c.RemoveSeam(img, seams, p.Debug)\n\n\tif p.Mask != nil {\n\t\tp.Mask = c.RemoveSeam(p.Mask, seams, false)\n\t\tdraw.Draw(p.DebugMask, img.Bounds(), p.Mask, image.Point{}, draw.Over)\n\t}\n\n\tif p.RMask != nil {\n\t\tp.RMask = c.RemoveSeam(p.RMask, seams, false)\n\t\tdraw.Draw(p.DebugMask, img.Bounds(), p.RMask, image.Point{}, draw.Over)\n\t}\n\n\tgo func() {\n\t\tselect {\n\t\tcase imgWorker <- worker{\n\t\t\timg:   img,\n\t\t\tmask:  p.DebugMask,\n\t\t\tseams: c.Seams,\n\t\t\tdone:  false,\n\t\t}:\n\t\tcase <-errs:\n\t\t\treturn\n\t\t}\n\t}()\n\treturn img, nil\n}\n\n// enlarge increases the image dimension either horizontally or vertically.\nfunc (p *Processor) enlarge(img *image.NRGBA) (*image.NRGBA, error) {\n\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\tc := NewCarver(width, height)\n\n\tif _, err := c.ComputeSeams(p, img); err != nil {\n\t\treturn nil, err\n\t}\n\tseams := c.FindLowestEnergySeams(p)\n\timg = c.AddSeam(img, seams, p.Debug)\n\n\tif p.Mask != nil {\n\t\tp.Mask = c.AddSeam(p.Mask, seams, false)\n\t\tp.DebugMask = p.Mask\n\t}\n\n\tif p.RMask != nil {\n\t\tp.RMask = c.AddSeam(p.RMask, seams, false)\n\t\tp.DebugMask = p.RMask\n\t}\n\n\tgo func() {\n\t\tselect {\n\t\tcase imgWorker <- worker{\n\t\t\timg:   img,\n\t\t\tmask:  p.DebugMask,\n\t\t\tseams: c.Seams,\n\t\t\tdone:  false,\n\t\t}:\n\t\tcase <-errs:\n\t\t\treturn\n\t\t}\n\t}()\n\treturn img, nil\n}\n"
  },
  {
    "path": "processor_test.go",
    "content": "package caire\n\nimport (\n\t\"image\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestResize_ShrinkImageWidth(t *testing.T) {\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\tvar c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())\n\tnewWidth := imgWidth / 2\n\n\tp.NewWidth = newWidth\n\tp.NewHeight = imgHeight\n\n\tfor x := 0; x < newWidth; x++ {\n\t\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\t\tc = NewCarver(width, height)\n\n\t\t_, err := c.ComputeSeams(p, img)\n\t\tassert.NoError(t, err)\n\n\t\tseams := c.FindLowestEnergySeams(p)\n\t\timg = c.RemoveSeam(img, seams, p.Debug)\n\t}\n\timgWidth := img.Bounds().Max.X\n\n\tassert.Equal(t, imgWidth, newWidth)\n}\n\nfunc TestResize_ShrinkImageHeight(t *testing.T) {\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\tvar c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())\n\tnewHeight := imgHeight / 2\n\n\tp.NewWidth = imgWidth\n\tp.NewHeight = newHeight\n\n\timg = rotateImage90(img)\n\tfor x := 0; x < newHeight; x++ {\n\t\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\t\tc = NewCarver(width, height)\n\n\t\t_, err := c.ComputeSeams(p, img)\n\t\tassert.NoError(t, err)\n\n\t\tseams := c.FindLowestEnergySeams(p)\n\t\timg = c.RemoveSeam(img, seams, p.Debug)\n\t}\n\timg = rotateImage270(img)\n\timgHeight := img.Bounds().Max.Y\n\n\tassert.Equal(t, imgHeight, newHeight)\n}\n\nfunc TestResize_EnlargeImageWidth(t *testing.T) {\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\tvar c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())\n\tnewWidth := imgWidth * 2\n\n\tp.NewWidth = newWidth\n\tp.NewHeight = imgHeight\n\n\tfor x := 0; x < newWidth; x++ {\n\t\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\t\tc = NewCarver(width, height)\n\n\t\t_, err := c.ComputeSeams(p, img)\n\t\tassert.NoError(t, err)\n\n\t\tseams := c.FindLowestEnergySeams(p)\n\t\timg = c.AddSeam(img, seams, p.Debug)\n\t}\n\timgWidth := img.Bounds().Max.X - img.Bounds().Dx()\n\n\tassert.NotEqual(t, imgWidth, newWidth)\n}\n\nfunc TestResize_EnlargeImageHeight(t *testing.T) {\n\timg := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))\n\tvar c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())\n\tnewHeight := imgHeight * 2\n\n\tp.NewWidth = imgWidth\n\tp.NewHeight = newHeight\n\n\timg = rotateImage90(img)\n\tfor x := 0; x < newHeight; x++ {\n\t\twidth, height := img.Bounds().Max.X, img.Bounds().Max.Y\n\t\tc = NewCarver(width, height)\n\n\t\t_, err := c.ComputeSeams(p, img)\n\t\tassert.NoError(t, err)\n\n\t\tseams := c.FindLowestEnergySeams(p)\n\t\timg = c.AddSeam(img, seams, p.Debug)\n\t}\n\timg = rotateImage270(img)\n\timgHeight := img.Bounds().Max.Y - img.Bounds().Dy()\n\n\tassert.NotEqual(t, imgHeight, newHeight)\n}\n"
  },
  {
    "path": "snapcraft.yaml",
    "content": "name: caire\nversion: '1.4.4'\nsummary: Content aware image resize library\ndescription: |\n  Content aware image resize library\ngrade: stable\nconfinement: strict\nbase: core18\nparts:\n  caire:\n    plugin: go\n    source: https://github.com/esimov/caire.git\n    go-importpath: github.com/esimov/caire\n    stage-packages:\n      - libegl1\n      - libglvnd0\n      - libwayland-client0\n      - libwayland-cursor0\n      - libwayland-egl1\n      - libx11-6\n      - libx11-xcb1\n      - libxau6\n      - libxcb-xkb1\n      - libxcb1\n      - libxcursor1\n      - libxdmcp6\n      - libxfixes3\n      - libxkbcommon-x11-0\n      - libxkbcommon0\n      - libxrender1\n\n    build-packages:\n      - gcc\n      - pkg-config\n      - libwayland-dev\n      - libx11-dev\n      - libx11-xcb-dev\n      - libxkbcommon-x11-dev\n      - libgles2-mesa-dev\n      - libegl1-mesa-dev\n      - libffi-dev\n      - libxcursor-dev\n      - libvulkan-dev\n\napps:\n  caire:\n    command: bin/caire\n    plugs:\n      - home\n      - x11"
  },
  {
    "path": "sobel.go",
    "content": "package caire\n\nimport (\n\t\"image\"\n\t\"math\"\n)\n\ntype kernel [][]int32\n\nvar (\n\tkernelX = kernel{\n\t\t{-1, 0, 1},\n\t\t{-2, 0, 2},\n\t\t{-1, 0, 1},\n\t}\n\n\tkernelY = kernel{\n\t\t{-1, -2, -1},\n\t\t{0, 0, 0},\n\t\t{1, 2, 1},\n\t}\n)\n\n// SobelDetector uses the sobel filter operator for detecting image edges.\n// See https://en.wikipedia.org/wiki/Sobel_operator\nfunc (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA {\n\tvar sumX, sumY int32\n\tdx, dy := img.Bounds().Max.X, img.Bounds().Max.Y\n\tdst := image.NewNRGBA(img.Bounds())\n\n\t// Get 3x3 window of pixels because image data given is just a 1D array of pixels\n\tmaxPixelOffset := dx*2 + len(kernelX) - 1\n\n\tdata := c.getImageData(img)\n\tlength := len(data)*4 - maxPixelOffset\n\tmagnitudes := make([]uint8, length)\n\n\tfor i := 0; i < length; i++ {\n\t\t// Sum each pixel with the kernel value\n\t\tsumX, sumY = 0, 0\n\t\tfor x := 0; x < len(kernelX); x++ {\n\t\t\tfor y := 0; y < len(kernelY); y++ {\n\t\t\t\tif idx := i + (dx * y) + x; idx < len(data) {\n\t\t\t\t\tr := data[i+(dx*y)+x]\n\t\t\t\t\tsumX += int32(r) * kernelX[y][x]\n\t\t\t\t\tsumY += int32(r) * kernelY[y][x]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tmagnitude := math.Sqrt(float64(sumX*sumX) + float64(sumY*sumY))\n\t\t// Check for pixel color boundaries\n\t\tif magnitude < 0 {\n\t\t\tmagnitude = 0\n\t\t} else if magnitude > 255 {\n\t\t\tmagnitude = 255\n\t\t}\n\n\t\t// Set magnitude to 0 if doesn't exceed threshold, else set to magnitude\n\t\tif magnitude > threshold {\n\t\t\tmagnitudes[i] = uint8(magnitude)\n\t\t} else {\n\t\t\tmagnitudes[i] = 0\n\t\t}\n\t}\n\n\tdataLength := dx * dy * 4\n\tedges := make([]int32, dataLength)\n\n\t// Apply the kernel values\n\tfor i := 0; i < dataLength; i++ {\n\t\tedges[i] = 0\n\t\tif i%4 != 0 {\n\t\t\tm := magnitudes[i/4]\n\t\t\tif m != 0 {\n\t\t\t\tedges[i-1] = int32(m)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Generate the new image with the sobel filter applied\n\tfor idx := 0; idx < len(edges); idx += 4 {\n\t\tdst.Pix[idx] = uint8(edges[idx])\n\t\tdst.Pix[idx+1] = uint8(edges[idx+1])\n\t\tdst.Pix[idx+2] = uint8(edges[idx+2])\n\t\tdst.Pix[idx+3] = 0xff\n\t}\n\treturn dst\n}\n\n// getImageData gets the red component of an image and returns an array of pixel brightness values.\nfunc (c *Carver) getImageData(img *image.NRGBA) []uint8 {\n\tdx, dy := img.Bounds().Max.X, img.Bounds().Max.Y\n\tpixels := make([]uint8, dx*dy)\n\n\tfor i := range pixels {\n\t\tpixels[i] = img.Pix[i*4]\n\t}\n\treturn pixels\n}\n"
  },
  {
    "path": "stackblur.go",
    "content": "// Go implementation of the StackBlur algorithm\n// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php\n\npackage caire\n\nimport (\n\t\"errors\"\n\t\"image\"\n\t\"image/color\"\n)\n\n// blurStack is a linked list containing the color value and a pointer to the next struct.\ntype blurStack struct {\n\tr, g, b, a uint32\n\tnext       *blurStack\n}\n\nvar mulTable = []uint32{\n\t512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512,\n\t454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512,\n\t482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456,\n\t437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512,\n\t497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328,\n\t320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456,\n\t446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335,\n\t329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512,\n\t505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405,\n\t399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328,\n\t324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271,\n\t268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456,\n\t451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388,\n\t385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335,\n\t332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292,\n\t289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259,\n}\n\nvar shgTable = []uint32{\n\t9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,\n\t17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,\n\t19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,\n\t20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,\n\t21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,\n\t21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,\n\t22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,\n\t22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,\n\t23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,\n\t23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,\n\t23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,\n\t23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,\n\t24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,\n\t24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,\n\t24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,\n\t24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,\n}\n\n// Stackblur takes the source image and returns it's blurred version by applying the blur radius defined as parameter. The destination image must be a image.NRGBA.\nfunc Stackblur(dst, src image.Image, radius uint32) error {\n\t// Limit the maximum blur radius to 255 to avoid overflowing the multable.\n\tif int(radius) >= len(mulTable) {\n\t\tradius = uint32(len(mulTable) - 1)\n\t}\n\n\tif radius < 1 {\n\t\treturn errors.New(\"blur radius must be greater than 0\")\n\t}\n\n\timg, ok := dst.(*image.NRGBA)\n\tif !ok {\n\t\treturn errors.New(\"the destination image must be image.NRGBA\")\n\t}\n\n\tprocess(img, src, radius)\n\treturn nil\n}\n\nfunc process(dst *image.NRGBA, src image.Image, radius uint32) {\n\tsrcBounds := src.Bounds()\n\tsrcMinX := srcBounds.Min.X\n\tsrcMinY := srcBounds.Min.Y\n\n\tdstBounds := srcBounds.Sub(srcBounds.Min)\n\tdstW := dstBounds.Dx()\n\tdstH := dstBounds.Dy()\n\n\tswitch src0 := src.(type) {\n\tcase *image.NRGBA:\n\t\trowSize := srcBounds.Dx() * 4\n\t\tfor dstY := 0; dstY < dstH; dstY++ {\n\t\t\tdi := src0.PixOffset(0, dstY)\n\t\t\tsi := src0.PixOffset(srcMinX, srcMinY+dstY)\n\t\t\tfor dstX := 0; dstX < dstW; dstX++ {\n\t\t\t\tcopy(dst.Pix[di:di+rowSize], src0.Pix[si:si+rowSize])\n\t\t\t}\n\t\t}\n\tcase *image.YCbCr:\n\t\tfor dstY := 0; dstY < dstH; dstY++ {\n\t\t\tdi := dst.PixOffset(0, dstY)\n\t\t\tfor dstX := 0; dstX < dstW; dstX++ {\n\t\t\t\tsrcX := srcMinX + dstX\n\t\t\t\tsrcY := srcMinY + dstY\n\t\t\t\tsiy := src0.YOffset(srcX, srcY)\n\t\t\t\tsic := src0.COffset(srcX, srcY)\n\t\t\t\tr, g, b := color.YCbCrToRGB(src0.Y[siy], src0.Cb[sic], src0.Cr[sic])\n\t\t\t\tdst.Pix[di+0] = r\n\t\t\t\tdst.Pix[di+1] = g\n\t\t\t\tdst.Pix[di+2] = b\n\t\t\t\tdst.Pix[di+3] = 0xff\n\t\t\t\tdi += 4\n\t\t\t}\n\t\t}\n\tdefault:\n\t\tfor dstY := 0; dstY < dstH; dstY++ {\n\t\t\tdi := dst.PixOffset(0, dstY)\n\t\t\tfor dstX := 0; dstX < dstW; dstX++ {\n\t\t\t\tc := color.NRGBAModel.Convert(src.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)\n\t\t\t\tdst.Pix[di+0] = c.R\n\t\t\t\tdst.Pix[di+1] = c.G\n\t\t\t\tdst.Pix[di+2] = c.B\n\t\t\t\tdst.Pix[di+3] = c.A\n\t\t\t\tdi += 4\n\t\t\t}\n\t\t}\n\t}\n\n\tblurImage(dst, radius)\n}\n\nfunc blurImage(src *image.NRGBA, radius uint32) {\n\tvar (\n\t\tstackEnd *blurStack\n\t\tstackIn  *blurStack\n\t\tstackOut *blurStack\n\t)\n\n\tvar width, height = uint32(src.Bounds().Dx()), uint32(src.Bounds().Dy())\n\tvar (\n\t\tdiv, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32\n\t\tx, y, i, p, yp, yi, yw,\n\t\trSum, gSum, bSum, aSum,\n\t\trOutSum, gOutSum, bOutSum, aOutSum,\n\t\trInSum, gInSum, bInSum, aInSum,\n\t\tpr, pg, pb, pa uint32\n\t)\n\n\tdiv = radius + radius + 1\n\twidthMinus1 = width - 1\n\theightMinus1 = height - 1\n\tradiusPlus1 = radius + 1\n\tsumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2\n\n\tstackStart := new(blurStack)\n\tstack := stackStart\n\n\tfor i = 1; i < div; i++ {\n\t\tstack.next = new(blurStack)\n\t\tstack = stack.next\n\t\tif i == radiusPlus1 {\n\t\t\tstackEnd = stack\n\t\t}\n\t}\n\tstack.next = stackStart\n\n\tmulSum := mulTable[radius]\n\tshgSum := shgTable[radius]\n\n\tfor y = 0; y < height; y++ {\n\t\trInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0\n\n\t\tpr = uint32(src.Pix[yi])\n\t\tpg = uint32(src.Pix[yi+1])\n\t\tpb = uint32(src.Pix[yi+2])\n\t\tpa = uint32(src.Pix[yi+3])\n\n\t\trOutSum = radiusPlus1 * pr\n\t\tgOutSum = radiusPlus1 * pg\n\t\tbOutSum = radiusPlus1 * pb\n\t\taOutSum = radiusPlus1 * pa\n\n\t\trSum += sumFactor * pr\n\t\tgSum += sumFactor * pg\n\t\tbSum += sumFactor * pb\n\t\taSum += sumFactor * pa\n\n\t\tstack = stackStart\n\n\t\tfor i = 0; i < radiusPlus1; i++ {\n\t\t\tstack.r = pr\n\t\t\tstack.g = pg\n\t\t\tstack.b = pb\n\t\t\tstack.a = pa\n\t\t\tstack = stack.next\n\t\t}\n\n\t\tfor i = 1; i < radiusPlus1; i++ {\n\t\t\tvar diff uint32\n\t\t\tif widthMinus1 < i {\n\t\t\t\tdiff = widthMinus1\n\t\t\t} else {\n\t\t\t\tdiff = i\n\t\t\t}\n\t\t\tp = yi + (diff << 2)\n\t\t\tpr = uint32(src.Pix[p])\n\t\t\tpg = uint32(src.Pix[p+1])\n\t\t\tpb = uint32(src.Pix[p+2])\n\t\t\tpa = uint32(src.Pix[p+3])\n\n\t\t\tstack.r = pr\n\t\t\tstack.g = pg\n\t\t\tstack.b = pb\n\t\t\tstack.a = pa\n\n\t\t\trSum += stack.r * (radiusPlus1 - i)\n\t\t\tgSum += stack.g * (radiusPlus1 - i)\n\t\t\tbSum += stack.b * (radiusPlus1 - i)\n\t\t\taSum += stack.a * (radiusPlus1 - i)\n\n\t\t\trInSum += pr\n\t\t\tgInSum += pg\n\t\t\tbInSum += pb\n\t\t\taInSum += pa\n\n\t\t\tstack = stack.next\n\t\t}\n\t\tstackIn = stackStart\n\t\tstackOut = stackEnd\n\n\t\tfor x = 0; x < width; x++ {\n\t\t\tpa = (aSum * mulSum) >> shgSum\n\t\t\tsrc.Pix[yi+3] = uint8(pa)\n\n\t\t\tif pa != 0 {\n\t\t\t\tsrc.Pix[yi] = uint8((rSum * mulSum) >> shgSum)\n\t\t\t\tsrc.Pix[yi+1] = uint8((gSum * mulSum) >> shgSum)\n\t\t\t\tsrc.Pix[yi+2] = uint8((bSum * mulSum) >> shgSum)\n\t\t\t} else {\n\t\t\t\tsrc.Pix[yi] = 0\n\t\t\t\tsrc.Pix[yi+1] = 0\n\t\t\t\tsrc.Pix[yi+2] = 0\n\t\t\t}\n\n\t\t\trSum -= rOutSum\n\t\t\tgSum -= gOutSum\n\t\t\tbSum -= bOutSum\n\t\t\taSum -= aOutSum\n\n\t\t\trOutSum -= stackIn.r\n\t\t\tgOutSum -= stackIn.g\n\t\t\tbOutSum -= stackIn.b\n\t\t\taOutSum -= stackIn.a\n\n\t\t\tp = x + radius + 1\n\n\t\t\tif p > widthMinus1 {\n\t\t\t\tp = widthMinus1\n\t\t\t}\n\t\t\tp = (yw + p) << 2\n\n\t\t\tstackIn.r = uint32(src.Pix[p])\n\t\t\tstackIn.g = uint32(src.Pix[p+1])\n\t\t\tstackIn.b = uint32(src.Pix[p+2])\n\t\t\tstackIn.a = uint32(src.Pix[p+3])\n\n\t\t\trInSum += stackIn.r\n\t\t\tgInSum += stackIn.g\n\t\t\tbInSum += stackIn.b\n\t\t\taInSum += stackIn.a\n\n\t\t\trSum += rInSum\n\t\t\tgSum += gInSum\n\t\t\tbSum += bInSum\n\t\t\taSum += aInSum\n\n\t\t\tstackIn = stackIn.next\n\n\t\t\tpr = stackOut.r\n\t\t\tpg = stackOut.g\n\t\t\tpb = stackOut.b\n\t\t\tpa = stackOut.a\n\n\t\t\trOutSum += pr\n\t\t\tgOutSum += pg\n\t\t\tbOutSum += pb\n\t\t\taOutSum += pa\n\n\t\t\trInSum -= pr\n\t\t\tgInSum -= pg\n\t\t\tbInSum -= pb\n\t\t\taInSum -= pa\n\n\t\t\tstackOut = stackOut.next\n\n\t\t\tyi += 4\n\t\t}\n\t\tyw += width\n\t}\n\n\tfor x = 0; x < width; x++ {\n\t\trInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0\n\n\t\tyi = x << 2\n\t\tpr = uint32(src.Pix[yi])\n\t\tpg = uint32(src.Pix[yi+1])\n\t\tpb = uint32(src.Pix[yi+2])\n\t\tpa = uint32(src.Pix[yi+3])\n\n\t\trOutSum = radiusPlus1 * pr\n\t\tgOutSum = radiusPlus1 * pg\n\t\tbOutSum = radiusPlus1 * pb\n\t\taOutSum = radiusPlus1 * pa\n\n\t\trSum += sumFactor * pr\n\t\tgSum += sumFactor * pg\n\t\tbSum += sumFactor * pb\n\t\taSum += sumFactor * pa\n\n\t\tstack = stackStart\n\n\t\tfor i = 0; i < radiusPlus1; i++ {\n\t\t\tstack.r = pr\n\t\t\tstack.g = pg\n\t\t\tstack.b = pb\n\t\t\tstack.a = pa\n\t\t\tstack = stack.next\n\t\t}\n\n\t\typ = width\n\n\t\tfor i = 1; i <= radius; i++ {\n\t\t\tyi = (yp + x) << 2\n\t\t\tpr = uint32(src.Pix[yi])\n\t\t\tpg = uint32(src.Pix[yi+1])\n\t\t\tpb = uint32(src.Pix[yi+2])\n\t\t\tpa = uint32(src.Pix[yi+3])\n\n\t\t\tstack.r = pr\n\t\t\tstack.g = pg\n\t\t\tstack.b = pb\n\t\t\tstack.a = pa\n\n\t\t\trSum += stack.r * (radiusPlus1 - i)\n\t\t\tgSum += stack.g * (radiusPlus1 - i)\n\t\t\tbSum += stack.b * (radiusPlus1 - i)\n\t\t\taSum += stack.a * (radiusPlus1 - i)\n\n\t\t\trInSum += pr\n\t\t\tgInSum += pg\n\t\t\tbInSum += pb\n\t\t\taInSum += pa\n\n\t\t\tstack = stack.next\n\n\t\t\tif i < heightMinus1 {\n\t\t\t\typ += width\n\t\t\t}\n\t\t}\n\n\t\tyi = x\n\t\tstackIn = stackStart\n\t\tstackOut = stackEnd\n\n\t\tfor y = 0; y < height; y++ {\n\t\t\tp = yi << 2\n\t\t\tpa = (aSum * mulSum) >> shgSum\n\t\t\tsrc.Pix[p+3] = uint8(pa)\n\n\t\t\tif pa > 0 {\n\t\t\t\tsrc.Pix[p] = uint8((rSum * mulSum) >> shgSum)\n\t\t\t\tsrc.Pix[p+1] = uint8((gSum * mulSum) >> shgSum)\n\t\t\t\tsrc.Pix[p+2] = uint8((bSum * mulSum) >> shgSum)\n\t\t\t} else {\n\t\t\t\tsrc.Pix[p] = 0\n\t\t\t\tsrc.Pix[p+1] = 0\n\t\t\t\tsrc.Pix[p+2] = 0\n\t\t\t}\n\n\t\t\trSum -= rOutSum\n\t\t\tgSum -= gOutSum\n\t\t\tbSum -= bOutSum\n\t\t\taSum -= aOutSum\n\n\t\t\trOutSum -= stackIn.r\n\t\t\tgOutSum -= stackIn.g\n\t\t\tbOutSum -= stackIn.b\n\t\t\taOutSum -= stackIn.a\n\n\t\t\tp = y + radiusPlus1\n\n\t\t\tif p > heightMinus1 {\n\t\t\t\tp = heightMinus1\n\t\t\t}\n\t\t\tp = (x + (p * width)) << 2\n\n\t\t\tstackIn.r = uint32(src.Pix[p])\n\t\t\tstackIn.g = uint32(src.Pix[p+1])\n\t\t\tstackIn.b = uint32(src.Pix[p+2])\n\t\t\tstackIn.a = uint32(src.Pix[p+3])\n\n\t\t\trInSum += stackIn.r\n\t\t\tgInSum += stackIn.g\n\t\t\tbInSum += stackIn.b\n\t\t\taInSum += stackIn.a\n\n\t\t\trSum += rInSum\n\t\t\tgSum += gInSum\n\t\t\tbSum += bInSum\n\t\t\taSum += aInSum\n\n\t\t\tstackIn = stackIn.next\n\n\t\t\tpr = stackOut.r\n\t\t\tpg = stackOut.g\n\t\t\tpb = stackOut.b\n\t\t\tpa = stackOut.a\n\n\t\t\trOutSum += pr\n\t\t\tgOutSum += pg\n\t\t\tbOutSum += pb\n\t\t\taOutSum += pa\n\n\t\t\trInSum -= pr\n\t\t\tgInSum -= pg\n\t\t\tbInSum -= pb\n\t\t\taInSum -= pa\n\n\t\t\tstackOut = stackOut.next\n\n\t\t\tyi += width\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "utils/download.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n)\n\n// DownloadImage downloads the image from the internet and saves it into a temporary file.\nfunc DownloadImage(url string) (*os.File, error) {\n\t// Retrieve the url and decode the response body.\n\tres, err := http.Get(url)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to download image file from URI: %s, status %v\", url, res.Status)\n\t}\n\tdefer res.Body.Close()\n\n\tdata, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to read response body: %w\", err)\n\t}\n\n\ttmpfile, err := os.CreateTemp(\"/tmp\", \"image\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create temporary file: %w\", err)\n\t}\n\n\t// Copy the image binary data into the temporary file.\n\t_, err = io.Copy(tmpfile, bytes.NewBuffer(data))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to copy the source URI into the destination file\")\n\t}\n\n\tctype, err := DetectContentType(tmpfile.Name())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !strings.Contains(ctype.(string), \"image\") {\n\t\treturn nil, fmt.Errorf(\"the downloaded file is not a valid image type\")\n\t}\n\n\treturn tmpfile, nil\n}\n\n// IsValidUrl tests a string to determine if it is a well-structured url or not.\nfunc IsValidUrl(uri string) bool {\n\t_, err := url.ParseRequestURI(uri)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tu, err := url.Parse(uri)\n\tif err != nil || u.Scheme == \"\" || u.Host == \"\" {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// DetectContentType detects the file type by reading MIME type information of the file content.\nfunc DetectContentType(fname string) (any, error) {\n\tfile, err := os.Open(fname)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := file.Close(); err != nil {\n\t\t\tlog.Printf(\"could not close the opened file: %v\", err)\n\t\t}\n\t}()\n\n\t// Only the first 512 bytes are used to sniff the content type.\n\tbuffer := make([]byte, 512)\n\t_, err = file.Read(buffer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Reset the read pointer if necessary.\n\tif _, err := file.Seek(0, 0); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Always returns a valid content-type and \"application/octet-stream\" if no others seemed to match.\n\tcontentType := http.DetectContentType(buffer)\n\n\treturn string(contentType), nil\n}\n"
  },
  {
    "path": "utils/download_test.go",
    "content": "package utils\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestUtils_ShouldDownloadImage(t *testing.T) {\n\tf, err := DownloadImage(\"https://raw.githubusercontent.com/esimov/caire/master/testdata/sample.jpg\")\n\tif err != nil {\n\t\tt.Fatalf(\"could't download test file: %v\", err)\n\t}\n\n\tif !strings.Contains(f.Name(), \"tmp\") {\n\t\tt.Errorf(\"The downloaded image should have been saved in a temporary folder\")\n\t}\n}\n\nfunc TestUtils_ShouldBeValidUrl(t *testing.T) {\n\tok := IsValidUrl(\"https://github.com/esimov/caire/\")\n\tif !ok {\n\t\tt.Errorf(\"A valid URL should have been provided\")\n\t}\n}\n\nfunc TestUtils_ShouldDetectValidFileType(t *testing.T) {\n\tsampleImg := filepath.Join(\"../testdata\", \"sample.jpg\")\n\n\tftype, err := DetectContentType(sampleImg)\n\tif err != nil {\n\t\tt.Fatalf(\"could not detect content type: %v\", err)\n\t}\n\n\tif !strings.Contains(ftype.(string), \"image\") {\n\t\tt.Errorf(\"Content type expected to be of type image, got: %v\", ftype)\n\t}\n}\n"
  },
  {
    "path": "utils/format.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"image/color\"\n\t\"math\"\n\t\"strings\"\n\t\"time\"\n)\n\n// MessageType is a custom type used as a placeholder for various message types.\ntype MessageType int\n\n// The message types used across the CLI application.\nconst (\n\tDefaultMessage MessageType = iota\n\tSuccessMessage\n\tErrorMessage\n\tStatusMessage\n)\n\n// Colors used across the CLI application.\nconst (\n\tDefaultColor = \"\\x1b[0m\"\n\tStatusColor  = \"\\x1b[36m\"\n\tSuccessColor = \"\\x1b[32m\"\n\tErrorColor   = \"\\x1b[31m\"\n)\n\n// DecorateText shows the message types in different colors.\nfunc DecorateText(s string, msgType MessageType) string {\n\tswitch msgType {\n\tcase DefaultMessage:\n\t\ts = DefaultColor + s\n\tcase StatusMessage:\n\t\ts = StatusColor + s\n\tcase SuccessMessage:\n\t\ts = SuccessColor + s\n\tcase ErrorMessage:\n\t\ts = ErrorColor + s\n\tdefault:\n\t\treturn s\n\t}\n\treturn s + DefaultColor\n}\n\n// FormatTime formats time.Duration output to a human readable value.\nfunc FormatTime(d time.Duration) string {\n\tif d.Seconds() < 60.0 {\n\t\treturn fmt.Sprintf(\"%.2fs\", d.Seconds())\n\t}\n\tif d.Minutes() < 60.0 {\n\t\tremainingSeconds := math.Mod(d.Seconds(), 60)\n\t\treturn fmt.Sprintf(\"%dm %.2fs\", int64(d.Minutes()), remainingSeconds)\n\t}\n\tif d.Hours() < 24.0 {\n\t\tremainingMinutes := math.Mod(d.Minutes(), 60)\n\t\tremainingSeconds := math.Mod(d.Seconds(), 60)\n\t\treturn fmt.Sprintf(\"%dh %dm %.2fs\",\n\t\t\tint64(d.Hours()), int64(remainingMinutes), remainingSeconds)\n\t}\n\tremainingHours := math.Mod(d.Hours(), 24)\n\tremainingMinutes := math.Mod(d.Minutes(), 60)\n\tremainingSeconds := math.Mod(d.Seconds(), 60)\n\treturn fmt.Sprintf(\"%dd %dh %dm %.2fs\",\n\t\tint64(d.Hours()/24), int64(remainingHours),\n\t\tint64(remainingMinutes), remainingSeconds)\n}\n\n// HexToRGBA converts a color expressed as hexadecimal string to RGBA color.\nfunc HexToRGBA(x string) color.NRGBA {\n\tvar r, g, b, a uint8\n\n\tx = strings.TrimPrefix(x, \"#\")\n\ta = 255\n\tif len(x) == 2 {\n\t\tformat := \"%03x\"\n\t\tfmt.Sscanf(x, format, &r, &g, &b)\n\t}\n\tif len(x) == 3 {\n\t\tformat := \"%1x%1x%1x\"\n\t\tfmt.Sscanf(x, format, &r, &g, &b)\n\t\tr |= r << 4\n\t\tg |= g << 4\n\t\tb |= b << 4\n\t}\n\tif len(x) == 6 {\n\t\tformat := \"%02x%02x%02x\"\n\t\tfmt.Sscanf(x, format, &r, &g, &b)\n\t}\n\tif len(x) == 8 {\n\t\tformat := \"%02x%02x%02x%02x\"\n\t\tfmt.Sscanf(x, format, &r, &g, &b, &a)\n\t}\n\treturn color.NRGBA{R: r, G: g, B: b, A: a}\n}\n\n// RGB returns color based on RGB in range 0..1\nfunc RGB(r, g, b float32) color.NRGBA {\n\treturn color.NRGBA{R: sat8(r), G: sat8(g), B: sat8(b), A: 0xFF}\n}\n\n// RGBA returns color based on RGBA in range 0..1\nfunc RGBA(r, g, b, a float32) color.NRGBA {\n\treturn color.NRGBA{R: sat8(r), G: sat8(g), B: sat8(b), A: sat8(a)}\n}\n\n// HSLA returns color based on HSLA in range 0..1\nfunc HSLA(h, s, l, a float32) color.NRGBA { return RGBA(hsla(h, s, l, a)) }\n\n// HSL returns color based on HSL in range 0..1\nfunc HSL(h, s, l float32) color.NRGBA { return HSLA(h, s, l, 1) }\n\nfunc hue(v1, v2, h float32) float32 {\n\tif h < 0 {\n\t\th += 1\n\t}\n\tif h > 1 {\n\t\th -= 1\n\t}\n\tif 6*h < 1 {\n\t\treturn v1 + (v2-v1)*6*h\n\t} else if 2*h < 1 {\n\t\treturn v2\n\t} else if 3*h < 2 {\n\t\treturn v1 + (v2-v1)*(2.0/3.0-h)*6\n\t}\n\n\treturn v1\n}\n\nfunc hsla(h, s, l, a float32) (r, g, b, ra float32) {\n\tif s == 0 {\n\t\treturn l, l, l, a\n\t}\n\n\th = mod32(h, 1)\n\n\tvar v2 float32\n\tif l < 0.5 {\n\t\tv2 = l * (1 + s)\n\t} else {\n\t\tv2 = (l + s) - s*l\n\t}\n\n\tv1 := 2*l - v2\n\tr = hue(v1, v2, h+1.0/3.0)\n\tg = hue(v1, v2, h)\n\tb = hue(v1, v2, h-1.0/3.0)\n\tra = a\n\n\treturn\n}\n\nfunc sat8(v float32) uint8 {\n\tv *= 255.0\n\tif v >= 255 {\n\t\treturn 255\n\t} else if v <= 0 {\n\t\treturn 0\n\t}\n\treturn uint8(v)\n}\n\nfunc mod32(x, y float32) float32 { return float32(math.Mod(float64(x), float64(y))) }\n"
  },
  {
    "path": "utils/spinner.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\t\"unicode/utf8\"\n)\n\n// Spinner initializes the progress indicator.\ntype Spinner struct {\n\tmu         *sync.RWMutex\n\tdelay      time.Duration\n\twriter     io.Writer\n\tmessage    string\n\tlastOutput string\n\tStopMsg    string\n\thideCursor bool\n\tstopChan   chan struct{}\n}\n\n// NewSpinner instantiates a new progress indicator.\nfunc NewSpinner(msg string, d time.Duration) *Spinner {\n\treturn &Spinner{\n\t\tmu:         &sync.RWMutex{},\n\t\tdelay:      d,\n\t\twriter:     os.Stderr,\n\t\tmessage:    msg,\n\t\thideCursor: true,\n\t\tstopChan:   make(chan struct{}, 1),\n\t}\n}\n\n// Start starts the progress indicator.\nfunc (s *Spinner) Start() {\n\tif s.hideCursor && runtime.GOOS != \"windows\" {\n\t\t// hides the cursor\n\t\tfmt.Fprintf(s.writer, \"\\033[?25l\")\n\t}\n\n\tcharSet := []string{\"⠈⠁\", \"⠈⠑\", \"⠈⠱\", \"⠈⡱\", \"⢀⡱\", \"⢄⡱\", \"⢄⡱\", \"⢆⡱\", \"⢎⡱\", \"⢎⡰\", \"⢎⡠\", \"⢎⡀\", \"⢎⠁\", \"⠎⠁\", \"⠊⠁\"}\n\tgo func() {\n\t\tfor {\n\t\t\tfor _, r := range charSet {\n\t\t\t\tselect {\n\t\t\t\tcase <-s.stopChan:\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t\ts.mu.Lock()\n\n\t\t\t\t\toutput := fmt.Sprintf(\"\\r%s%s %s%s\", s.message, StatusColor, r, DefaultColor)\n\t\t\t\t\tfmt.Fprintf(s.writer, output)\n\t\t\t\t\ts.lastOutput = output\n\n\t\t\t\t\ts.mu.Unlock()\n\t\t\t\t\ttime.Sleep(s.delay)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// Stop stops the progress indicator.\nfunc (s *Spinner) Stop() {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\ts.clear()\n\ts.RestoreCursor()\n\tif len(s.StopMsg) > 0 {\n\t\tfmt.Fprintf(s.writer, s.StopMsg)\n\t}\n\ts.stopChan <- struct{}{}\n}\n\n// RestoreCursor restores back the cursor visibility.\nfunc (s *Spinner) RestoreCursor() {\n\tif s.hideCursor && runtime.GOOS != \"windows\" {\n\t\t// makes the cursor visible\n\t\tfmt.Fprint(s.writer, \"\\033[?25h\")\n\t}\n}\n\n// clear deletes the last line. Caller must hold the the locker.\nfunc (s *Spinner) clear() {\n\tn := utf8.RuneCountInString(s.lastOutput)\n\tif runtime.GOOS == \"windows\" {\n\t\tclearString := \"\\r\" + strings.Repeat(\" \", n) + \"\\r\"\n\t\tfmt.Fprint(s.writer, clearString)\n\t\ts.lastOutput = \"\"\n\t\treturn\n\t}\n\tfor _, c := range []string{\"\\b\", \"\\127\", \"\\b\", \"\\033[K\"} { // \"\\033[K\" for macOS Terminal\n\t\tfmt.Fprint(s.writer, strings.Repeat(c, n))\n\t}\n\tfmt.Fprintf(s.writer, \"\\r\\033[K\") // clear line\n\ts.lastOutput = \"\"\n}\n"
  },
  {
    "path": "utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"slices\"\n\n\t\"golang.org/x/exp/constraints\"\n)\n\n// Min returns the slowest value of the provided parameters.\nfunc Min[T constraints.Ordered](values ...T) T {\n\tvar acc T = values[0]\n\n\tfor _, v := range values {\n\t\tif v < acc {\n\t\t\tacc = v\n\t\t}\n\t}\n\treturn acc\n}\n\n// Max returns the biggest value of the provided parameters.\nfunc Max[T constraints.Ordered](values ...T) T {\n\tvar acc T = values[0]\n\n\tfor _, v := range values {\n\t\tif v > acc {\n\t\t\tacc = v\n\t\t}\n\t}\n\treturn acc\n}\n\n// Abs returns the absolut value of x.\nfunc Abs[T constraints.Signed | constraints.Float](x T) T {\n\tif x < 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\n// Contains returns true if a value is available in the collection.\nfunc Contains[T comparable](slice []T, value T) bool {\n\treturn slices.Contains(slice, value)\n}\n"
  }
]