Full Code of esimov/caire for AI

master 072a5888af55 cached
37 files
153.7 KB
52.7k tokens
197 symbols
1 requests
Download .txt
Repository: esimov/caire
Branch: master
Commit: 072a5888af55
Files: 37
Total size: 153.7 KB

Directory structure:
gitextract_jflphtww/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── build.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── build.sh
├── carver.go
├── carver_benchmark_test.go
├── carver_test.go
├── cmd/
│   └── caire/
│       └── main.go
├── data/
│   └── facefinder
├── doc.go
├── draw.go
├── exec.go
├── go.mod
├── go.sum
├── gui.go
├── image.go
├── image_test.go
├── imop/
│   ├── blend.go
│   ├── blend_test.go
│   ├── comp.go
│   └── comp_test.go
├── preview.go
├── processor.go
├── processor_test.go
├── snapcraft.yaml
├── sobel.go
├── stackblur.go
└── utils/
    ├── download.go
    ├── download_test.go
    ├── format.go
    ├── spinner.go
    └── utils.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
github: esimov


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help me improve the library
labels: ''

---

### Describe the bug
<!--A clear and concise description of what the bug is.

- Describe if it has been cloned via `git clone` or downloaded with `go get`?
- You don't know how to install and build Caire from the source code?
If you have followed the README file, but still have issues post your action here.
- Describe your bug if it haven't been already reported.
-->

### API related bug
<!--A clear and concise description of the API usage.-->

### Expected behavior
<!--A clear and concise description of what you expected to happen.-->

### Screenshots
<!--Add screenshots to help explain your problem.-->
- [Screenshots, logs or errors]

### Bug with the Desktop version (please complete the following information):
 - Sytem information like OS: [e.g. macOS, Ubuntu]
 - You are using the binary file from the uploaded releases or you are doing a manual build?

### Additional context
<!--Add any other context about the problem here.-->


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
labels: ''

---

### Is your feature request related to a problem? Please describe.
<!--A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]-->

### Describe the solution you'd like
<!--A clear and concise description of what you want to happen.-->

### Describe alternatives you've considered
<!--A clear and concise description of any alternative solutions or features you've considered.-->

### Additional context
<!--Add any other context or screenshots about the feature request here.-->


================================================
FILE: .github/workflows/build.yml
================================================
name: build

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

jobs:
  build:
    name: Build
    strategy:
      fail-fast: false
      matrix:
        go-version: [~1.21, ~1.22]
        os: [ubuntu-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    env:
      GO111MODULE: "on"
    steps:
      - name: Install Go
        uses: actions/setup-go@v4
        with:
          go-version: ${{ matrix.go-version }}

      - name: Cache-Go
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod              # Module download cache
            ~/.cache/go-build         # Build cache (Linux)
            ~/Library/Caches/go-build # Build cache (Mac)
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
      - name: Install Linux Dependencies
        if: matrix.os == 'ubuntu-latest'
        run: |
          sudo apt-get update -y
          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
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Download Go modules
        run: go mod download

      - name: Run Tests
        id: makefile
        run: |
          make test

================================================
FILE: .gitignore
================================================
*.jpg
*.png
*.jpeg
coverage.out
test-report.json
/packages
!/testdata/*.png
!/testdata/*.jpg
!/examples/**/*.png


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Endre Simo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
all: 
	@./build.sh
clean:
	@rm -f caire
install: all
	@cp caire /usr/local/bin
uninstall: 
	@rm -f /usr/local/bin/caire
package:
	@NOCOPY=1 ./build.sh package
test:
	go test -v -json ./... -run=. > ./test-report.json -coverprofile=coverage.out

================================================
FILE: README.md
================================================
<h1 align="center"><img alt="Caire Logo" src="https://user-images.githubusercontent.com/883386/51555990-a1762600-1e81-11e9-9a6a-0cd815870358.png" height="180"></h1>

[![build](https://github.com/esimov/caire/actions/workflows/build.yml/badge.svg)](https://github.com/esimov/caire/actions/workflows/build.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/esimov/caire.svg)](https://pkg.go.dev/github.com/esimov/caire)
[![license](https://img.shields.io/github/license/esimov/caire)](./LICENSE)
[![release](https://img.shields.io/badge/release-v1.5.0-blue.svg)](https://github.com/esimov/caire/releases/tag/v1.5.0)
[![homebrew](https://img.shields.io/badge/homebrew-v1.5.0-orange.svg)](https://formulae.brew.sh/formula/caire)
[![caire](https://snapcraft.io/caire/badge.svg)](https://snapcraft.io/caire)

**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.

## How does it work
* An energy map (edge detection) is generated from the provided image.
* The algorithm tries to find the least important parts of the image taking into account the lowest energy values.
* 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.
* 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.
* 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.
* 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.
* Find the lowest cost seam from the energy matrix starting from the last row and remove it.
* Repeat the process.

#### The process illustrated:

| Original image | Energy map | Seams applied
|:--:|:--:|:--:|
| ![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) |

## Features
Key features which differentiates this library from the other existing open source solutions:

- [x] **GUI progress indicator**
- [x] Customizable command line support
- [x] Support for both shrinking or enlarging the image
- [x] Resize image both vertically and horizontally
- [x] Face detection to avoid face deformation
- [x] Support for multiple output image type (jpg, jpeg, png, bmp)
- [x] Support for `stdin` and `stdout` pipe commands
- [x] Can process whole directories recursively and concurrently
- [x] Use of sobel threshold for fine tuning
- [x] Use of blur filter for increased edge detection
- [x] Support for squaring the image with a single command
- [x] Support for proportional scaling
- [x] Support for protective mask
- [x] Support for removal mask
- [x] [GUI debug mode support](#masks-support)

## Install
First, install Go, set your `GOPATH`, and make sure `$GOPATH/bin` is on your `PATH`.

```bash
$ go install github.com/esimov/caire/cmd/caire@latest
```

## MacOS (Brew) install
The library can also be installed via Homebrew.

```bash
$ brew install caire
```

## Usage

```bash
$ caire -in input.jpg -out output.jpg
```

### Supported commands:
```bash
$ caire --help
```
The following flags are supported:

| Flag | Default | Description |
| --- | --- | --- |
| `in` | - | Input file |
| `out` | - | Output file |
| `width` | n/a | New width |
| `height` | n/a | New height |
| `preview` | true | Show GUI window |
| `perc` | false | Reduce image by percentage |
| `square` | false | Reduce image to square dimensions |
| `blur` | 4 | Blur radius |
| `sobel` | 2 | Sobel filter threshold |
| `debug` | false | Use debugger |
| `face` | false | Use face detection |
| `angle` | float | Plane rotated faces angle |
| `mask` | string | Mask file path |
| `rmask` | string | Remove mask file path |
| `color` | string | Seam color (default `#ff0000`) |
| `shape` | string | Shape type used for debugging: `circle`,`line` (default `circle`) |

## Face detection

The library is capable of detecting human faces prior resizing the images by using the lightweight Pigo (https://github.com/esimov/pigo) face detection library.

The 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.

| Original image | With face detection | Without face detection
|:--:|:--:|:--:|
| ![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) |

[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)

### GUI progress indicator

<p align="center"><img alt="GUI preview" title="GUI preview" src="https://github.com/esimov/caire/raw/master/gui_preview.gif"></p>

A 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) .

The 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.

### Face detection to avoid face deformation
In 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.

For face detection related settings please check the Pigo [documentation](https://github.com/esimov/pigo/blob/master/README.md).

```bash
$ caire -in input.jpg -out output.jpg -face=1 -perc=1 -width=20
```

### Support for `stdin` and `stdout` pipe commands
You can also use `stdin` and `stdout` with `-`:

```bash
$ cat input/source.jpg | caire -in - -out - >out.jpg
```

`in` and `out` default to `-` so you can also use:

```bash
$ cat input/source.jpg | caire >out.jpg
$ caire -out out.jpg < input/source.jpg
```

You 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.

```bash
$ caire -in <image_url> -out <output-folder>
$ curl -s <image_url> | caire > out.jpg
```

### Process multiple images from a directory concurrently
The 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.

```bash
$ caire -in <input_folder> -out <output-folder>
```

### Support for multiple output image type
There 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.

### Other options
In 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:

```bash
$ caire -in input/source.jpg -out ./out.jpg -perc=1 -width=20 -height=20 -debug=false
```

Also the library supports the **`-square`** option. When this option is used the image will be resized to a square, based on the shortest edge.

When 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.

### Masks support:

- `-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.
- `-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.

Mask | Mask removal
:-: | :-:
<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/>

### Caire integrations
- [x] Caire can be used as a serverless function via OpenFaaS: https://github.com/esimov/caire-openfaas
- [x] Caire can also be used as a `snap` function (https://snapcraft.io/caire): `$ snap run caire --h`

<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>

## Results

#### Shrunk images
| Original | Shrunk |
| --- | --- |
| ![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) |
| ![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) |
| ![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) |
| ![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) |

#### Enlarged images
| Original | Extended |
| --- | --- |
| ![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) |
| ![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) |
### Useful resources
* https://en.wikipedia.org/wiki/Seam_carving
* https://inst.eecs.berkeley.edu/~cs194-26/fa16/hw/proj4-seamcarving/imret.pdf
* http://pages.cs.wisc.edu/~moayad/cs766/download_files/alnammi_cs_766_final_report.pdf
* https://stacks.stanford.edu/file/druid:my512gb2187/Zargham_Nassirpour_Content_aware_image_resizing.pdf

## Author

* Endre Simo ([@simo_endre](https://twitter.com/simo_endre))

## License

Copyright © 2018 Endre Simo

This project is under the MIT License. See the LICENSE file for the full license text.


================================================
FILE: build.sh
================================================
#!/bin/bash
set -e

VERSION="1.5.0"
PROTECTED_MODE="no"

export GO15VENDOREXPERIMENT=1

cd $(dirname "${BASH_SOURCE[0]}")
OD="$(pwd)"
WD=$OD

package() {
	echo Packaging $1 Binary
	bdir=caire-${VERSION}-$2-$3
	rm -rf packages/$bdir && mkdir -p packages/$bdir
	GOOS=$2 GOARCH=$3 ./build.sh
	if [ "$2" == "windows" ]; then
		mv caire packages/$bdir/caire.exe
	else
		mv caire packages/$bdir
	fi
	cp README.md packages/$bdir
	cd packages
	if [ "$2" == "linux" ]; then
		tar -zcf $bdir.tar.gz $bdir
	else
		zip -r -q $bdir.zip $bdir
	fi
	rm -rf $bdir
	cd ..
}

if [ "$1" == "package" ]; then
	rm -rf packages/
	package "Windows" "windows" "amd64"
	package "Mac" "darwin" "amd64"
	package "Linux" "linux" "amd64"
	package "FreeBSD" "freebsd" "amd64"
	exit
fi

# temp directory for storing isolated environment.
TMP="$(mktemp -d -t sdb.XXXX)"
rmtemp() {
	rm -rf "$TMP"
}
trap rmtemp EXIT

if [ "$NOCOPY" != "1" ]; then
	# copy all files to an isolated directory.
	WD="$TMP/src/github.com/esimov/caire"
	export GOPATH="$TMP"
	for file in `find . -type f`; do
		# TODO: use .gitignore to ignore, or possibly just use git to determine the file list.
		if [[ "$file" != "." && "$file" != ./.git* && "$file" != ./caire ]]; then
			mkdir -p "$WD/$(dirname "${file}")"
			cp -P "$file" "$WD/$(dirname "${file}")"
		fi
	done
	cd $WD
fi

# build and store objects into original directory.
go build -ldflags "-X main.Version=$VERSION" -o "$OD/caire" cmd/caire/main.go

================================================
FILE: carver.go
================================================
package caire

import (
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"math"

	"github.com/esimov/caire/utils"
	pigo "github.com/esimov/pigo/core"
)

// SeamCarver defines the Carve interface method, which have to be
// implemented by the Processor struct.
type SeamCarver interface {
	Resize(*image.NRGBA) (image.Image, error)
}

// maxFaceDetAttempts defines the maximum number of attempts of face detections
const maxFaceDetAttempts = 20

var (
	detAttempts    int
	isFaceDetected bool
)

var (
	sobel       *image.NRGBA
	energySeams = make([][]Seam, 0)
)

// Carver is the main entry struct having as parameters the newly generated image width, height and seam points.
type Carver struct {
	Points []float64
	Seams  []Seam
	Width  int
	Height int
}

// Seam struct contains the seam pixel coordinates.
type Seam struct {
	X int
	Y int
}

// NewCarver returns an initialized Carver structure.
func NewCarver(width, height int) *Carver {
	return &Carver{
		Points: make([]float64, width*height),
		Seams:  []Seam{},
		Width:  width,
		Height: height,
	}
}

// Get energy pixel value.
func (c *Carver) get(x, y int) float64 {
	px := x + y*c.Width
	return c.Points[px]
}

// Set energy pixel value.
func (c *Carver) set(x, y int, px float64) {
	idx := x + y*c.Width
	c.Points[idx] = px
}

// ComputeSeams compute the minimum energy level based on the following logic:
//
//   - traverse the image from the second row to the last row
//     and compute the cumulative minimum energy M for all possible
//     connected seams for each entry (i, j).
//
//   - the minimum energy level is calculated by summing up the current pixel value
//     with the minimum pixel value of the neighboring pixels from the previous row.
func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image.NRGBA, error) {
	width, height := img.Bounds().Dx(), img.Bounds().Dy()
	sobel = c.SobelDetector(img, float64(p.SobelThreshold))

	dets := []pigo.Detection{}

	if p.FaceDetector != nil && p.FaceDetect && detAttempts < maxFaceDetAttempts {
		var ratio float64

		if width < height {
			ratio = float64(width) / float64(height)
		} else {
			ratio = float64(height) / float64(width)
		}
		minSize := float64(utils.Min(width, height)) * ratio / 3

		// Transform the image to pixel array.
		pixels := rgbToGrayscale(img)

		cParams := pigo.CascadeParams{
			MinSize:     int(minSize),
			MaxSize:     utils.Min(width, height),
			ShiftFactor: 0.1,
			ScaleFactor: 1.1,

			ImageParams: pigo.ImageParams{
				Pixels: pixels,
				Rows:   height,
				Cols:   width,
				Dim:    width,
			},
		}
		if p.vRes {
			p.FaceAngle = 0.2
		}
		// Run the classifier over the obtained leaf nodes and return the detection results.
		// The result contains quadruplets representing the row, column, scale and detection score.
		dets = p.FaceDetector.RunCascade(cParams, p.FaceAngle)

		// Calculate the intersection over union (IoU) of two clusters.
		dets = p.FaceDetector.ClusterDetections(dets, 0.1)

		if len(dets) == 0 {
			// Retry detecting faces for a certain amount of time.
			if detAttempts < maxFaceDetAttempts {
				detAttempts++
			}
		} else {
			detAttempts = 0
			isFaceDetected = true
		}
	}

	// Traverse the pixel data of the binary file used for protecting the regions
	// which we do not want to be altered by the seam carver,
	// obtain the white patches and apply it to the sobel image.
	if len(p.MaskPath) > 0 && p.Mask != nil {
		p.DebugMask = image.NewNRGBA(img.Bounds())

		for i := 0; i < width*height; i++ {
			x := i % width
			y := (i - x) / width

			r, g, b, _ := p.Mask.At(x, y).RGBA()
			if r>>8 == 0xff && g>>8 == 0xff && b>>8 == 0xff {
				if isFaceDetected {
					// Reduce the brightness of the mask with a small factor if human faces are detected.
					// This way we can avoid the seam carver to remove
					// the pixels inside the detected human faces.
					sobel.Set(x, y, color.RGBA{R: 225, G: 225, B: 225, A: 255})
				} else {
					sobel.Set(x, y, color.White)
				}
			}
		}
	}

	// Traverse the pixel data of the binary file used to remove the image regions
	// we do not want to be retained in the final image, obtain the white patches,
	// but this time inverse the colors to black and merge it back to the sobel image.
	if len(p.RMaskPath) > 0 && p.RMask != nil {
		p.DebugMask = image.NewNRGBA(img.Bounds())

		for i := 0; i < width*height; i++ {
			x := i % width
			y := (i - x) / width

			r, g, b, _ := p.RMask.At(x, y).RGBA()
			// Replace the white pixels with black.
			if r>>8 == 0xff && g>>8 == 0xff && b>>8 == 0xff {
				if isFaceDetected {
					// Reduce the brightness of the mask with a small factor if human faces are detected.
					// This way we can avoid the seam carver to remove
					// the pixels inside the detected human faces.
					sobel.Set(x, y, color.RGBA{R: 25, G: 25, B: 25, A: 255})
				} else {
					sobel.Set(x, y, color.Black)
				}
				p.DebugMask.Set(x, y, color.Black)
			} else {
				p.DebugMask.Set(x, y, color.Transparent)
			}
		}
	}

	// Iterate over the detected faces and fill out the rectangles with white.
	// We need to trick the sobel detector to consider them as important image parts.
	for _, face := range dets {
		if (p.NewHeight != 0 && p.NewHeight < face.Scale) ||
			(p.NewWidth != 0 && p.NewWidth < face.Scale) {
			return nil, fmt.Errorf("%s %s",
				"cannot resize the image to the specified dimension without face deformation.\n",
				"\tRemove the face detection option in case you still wish to resize the image.")
		}
		if face.Q > 5.0 {
			scale := int(float64(face.Scale) / 1.7)
			rect := image.Rect(
				face.Col-scale,
				face.Row-scale,
				face.Col+scale,
				face.Row+scale,
			)
			p.DebugMask = image.NewNRGBA(img.Bounds())
			draw.Draw(sobel, rect, &image.Uniform{color.White}, image.Point{}, draw.Src)
			draw.Draw(p.DebugMask, rect, &image.Uniform{color.White}, image.Point{}, draw.Src)
		}
	}

	// Increase the energy value for each of the selected seam from the seams table
	// in order to avoid picking the same seam over and over again.
	// We expand the energy level of the selected seams to have a better redistribution.
	if len(energySeams) > 0 {
		for i := 0; i < len(energySeams); i++ {
			for _, seam := range energySeams[i] {
				sobel.Set(seam.X, seam.Y, &image.Uniform{color.White})
			}
		}
	}

	var srcImg *image.NRGBA
	if p.BlurRadius > 0 {
		srcImg = image.NewNRGBA(img.Bounds())
		err := Stackblur(srcImg, sobel, uint32(p.BlurRadius))
		if err != nil {
			return nil, fmt.Errorf("error bluring the image: %w", err)
		}
	} else {
		srcImg = sobel
	}

	for x := 0; x < c.Width; x++ {
		for y := 0; y < c.Height; y++ {
			r, _, _, a := srcImg.At(x, y).RGBA()
			c.set(x, y, float64(r)/float64(a))
		}
	}

	var left, middle, right float64

	// 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.
	for y := 1; y < c.Height; y++ {
		for x := 1; x < c.Width-1; x++ {
			left = c.get(x-1, y-1)
			middle = c.get(x, y-1)
			right = c.get(x+1, y-1)
			min := math.Min(math.Min(left, middle), right)
			// Set the minimum energy level.
			c.set(x, y, c.get(x, y)+min)
		}
		// Special cases: pixels are far left or far right
		left := c.get(0, y) + math.Min(c.get(0, y-1), c.get(1, y-1))
		c.set(0, y, left)
		right := c.get(0, y) + math.Min(c.get(c.Width-1, y-1), c.get(c.Width-2, y-1))
		c.set(c.Width-1, y, right)
	}
	return srcImg, nil
}

// FindLowestEnergySeams find the lowest vertical energy seam.
func (c *Carver) FindLowestEnergySeams(p *Processor) []Seam {
	// Find the lowest cost seam from the energy matrix starting from the last row.
	var (
		min = math.MaxFloat64
		px  int
	)
	seams := make([]Seam, 0)

	// Find the pixel on the last row with the minimum cumulative energy and use this as the starting pixel
	for x := 0; x < c.Width; x++ {
		seam := c.get(x, c.Height-1)
		if seam < min {
			min = seam
			px = x
		}
	}

	seams = append(seams, Seam{X: px, Y: c.Height - 1})
	var left, middle, right float64

	// Walk up in the matrix table, check the immediate three top pixels seam level
	// and add that one which has the lowest cumulative energy.
	for y := c.Height - 2; y >= 0; y-- {
		middle = c.get(px, y)
		// Leftmost seam, no child to the left
		if px == 0 {
			right = c.get(px+1, y)
			if right < middle {
				px++
			}
			// Rightmost seam, no child to the right
		} else if px == c.Width-1 {
			left = c.get(px-1, y)
			if left < middle {
				px--
			}
		} else {
			left = c.get(px-1, y)
			right = c.get(px+1, y)
			min := math.Min(math.Min(left, middle), right)

			if min == left {
				px--
			} else if min == right {
				px++
			}
		}
		seams = append(seams, Seam{X: px, Y: y})
	}

	// compare against c.Width and NOT c.Height, because the image is rotated.
	if p.NewWidth > c.Width || (p.NewHeight > 0 && p.NewHeight > c.Width) {
		// Include the currently processed energy seam into the seams table,
		// but only when an image enlargement operation is commenced.
		// We need to take this approach in order to avoid picking the same seam each time.
		energySeams = append(energySeams, seams)
	}
	return seams
}

// RemoveSeam remove the least important columns based on the stored energy (seams) level.
func (c *Carver) RemoveSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGBA {
	bounds := img.Bounds()
	// Reduce the image width with one pixel on each iteration.
	dst := image.NewNRGBA(image.Rect(0, 0, bounds.Dx()-1, bounds.Dy()))

	for _, seam := range seams {
		y := seam.Y
		for x := 0; x < bounds.Max.X; x++ {
			if seam.X == x {
				if debug {
					c.Seams = append(c.Seams, Seam{X: x, Y: y})
				}
			} else if seam.X < x {
				dst.Set(x-1, y, img.At(x, y))
			} else {
				dst.Set(x, y, img.At(x, y))
			}
		}
	}
	return dst
}

// AddSeam add a new seam.
func (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *image.NRGBA {
	var (
		lr, lg, lb uint32
		rr, rg, rb uint32
	)

	bounds := img.Bounds()
	dst := image.NewNRGBA(image.Rect(0, 0, bounds.Dx()+1, bounds.Dy()))

	for _, seam := range seams {
		y := seam.Y
		for x := 0; x < bounds.Max.X; x++ {
			if seam.X == x {
				if debug {
					c.Seams = append(c.Seams, Seam{X: x, Y: y})
				}
				if x > 0 && x != bounds.Max.X {
					lr, lg, lb, _ = img.At(x-1, y).RGBA()
				} else {
					lr, lg, lb, _ = img.At(x, y).RGBA()
				}

				if x < bounds.Max.X-1 {
					rr, rg, rb, _ = img.At(x+1, y).RGBA()
				} else if x == bounds.Max.X {
					rr, rg, rb, _ = img.At(x, y).RGBA()
				}

				// calculate the average color of the neighboring pixels
				avr, avg, avb := (lr+rr)>>1, (lg+rg)>>1, (lb+rb)>>1
				dst.Set(x, y, color.RGBA{uint8(avr >> 8), uint8(avg >> 8), uint8(avb >> 8), 0xff})
				dst.Set(x+1, y, img.At(x, y))
			} else if seam.X < x {
				dst.Set(x, y, img.At(x-1, y))
				dst.Set(x+1, y, img.At(x, y))
			} else {
				dst.Set(x, y, img.At(x, y))
			}
		}
	}

	return dst
}


================================================
FILE: carver_benchmark_test.go
================================================
package caire

import (
	"image"
	"os"
	"path/filepath"
	"testing"
)

func Benchmark_Carver(b *testing.B) {
	sampleImg := filepath.Join("./testdata", "sample.jpg")
	f, err := os.Open(sampleImg)
	if err != nil {
		b.Fatalf("could not load sample image: %v", err)
	}
	defer f.Close()

	src, _, err := image.Decode(f)
	if err != nil {
		b.Fatalf("error decoding image: %v", err)
	}
	b.ResetTimer()

	img := imgToNRGBA(src)

	width, height := img.Bounds().Max.X, img.Bounds().Max.Y
	c := NewCarver(width, height)

	for i := 0; i < b.N; i++ {
		_, err := c.ComputeSeams(p, img)
		if err != nil {
			b.FailNow()
		}

		seams := c.FindLowestEnergySeams(p)
		img = c.RemoveSeam(img, seams, p.Debug)
	}

}


================================================
FILE: carver_test.go
================================================
package caire

import (
	"image"
	"image/color"
	"image/draw"
	"os"
	"path/filepath"
	"testing"

	"github.com/esimov/caire/utils"
	pigo "github.com/esimov/pigo/core"
	"github.com/stretchr/testify/assert"
)

const (
	imgWidth  = 10
	imgHeight = 10
)

var p *Processor

func init() {
	p = &Processor{
		NewWidth:       imgWidth,
		NewHeight:      imgHeight,
		BlurRadius:     1,
		SobelThreshold: 4,
		Percentage:     false,
		Square:         false,
		Debug:          false,
	}
}

func TestCarver_EnergySeamShouldNotBeDetected(t *testing.T) {
	assert := assert.New(t)

	var seams [][]Seam
	var totalEnergySeams int

	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	dx, dy := img.Bounds().Dx(), img.Bounds().Dy()

	var c = NewCarver(dx, dy)
	for range imgWidth {
		width, height := img.Bounds().Max.X, img.Bounds().Max.Y
		c = NewCarver(width, height)

		_, err := c.ComputeSeams(p, img)
		assert.NoError(err)

		les := c.FindLowestEnergySeams(p)
		seams = append(seams, les)
	}

	for i := range seams {
		for s := range seams[i] {
			totalEnergySeams += seams[i][s].X
		}
	}
	assert.Equal(0, totalEnergySeams)
}

func TestCarver_DetectHorizontalEnergySeam(t *testing.T) {
	var seams [][]Seam
	var totalEnergySeams int

	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	draw.Draw(img, img.Bounds(), &image.Uniform{image.White}, image.Point{}, draw.Src)

	// Replace the pixel colors in a single row from 0xff to 0xdd. 5 is an arbitrary value.
	// The seam detector should recognize that line as being of low energy density
	// and should perform the seam computation process.
	// This way we'll make sure, that the seam detector correctly detects one and only one line.
	dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
	for x := 0; x < dx; x++ {
		img.Pix[(5*dx+x)*4+0] = 0xdd
		img.Pix[(5*dx+x)*4+1] = 0xdd
		img.Pix[(5*dx+x)*4+2] = 0xdd
		img.Pix[(5*dx+x)*4+3] = 0xdd
	}

	var c = NewCarver(dx, dy)
	for x := 0; x < imgWidth; x++ {
		width, height := img.Bounds().Max.X, img.Bounds().Max.Y

		c = NewCarver(width, height)
		_, err := c.ComputeSeams(p, img)
		assert.NoError(t, err)

		les := c.FindLowestEnergySeams(p)
		seams = append(seams, les)
	}

	for i := range seams {
		for s := range seams[i] {
			totalEnergySeams += seams[i][s].X
		}
	}
	assert.Greater(t, totalEnergySeams, 0)
}

func TestCarver_DetectVerticalEnergySeam(t *testing.T) {
	var seams [][]Seam
	var totalEnergySeams int

	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	draw.Draw(img, img.Bounds(), &image.Uniform{image.White}, image.Point{}, draw.Src)

	// Replace the pixel colors in a single column from 0xff to 0xdd. 5 is an arbitrary value.
	// The seam detector should recognize that line as being of low energy density
	// and should perform the seam computation process.
	// This way we'll make sure, that the seam detector correctly detects one and only one line.
	dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
	for y := 0; y < dy; y++ {
		img.Pix[5*4+(dx*y)*4+0] = 0xdd
		img.Pix[5*4+(dx*y)*4+1] = 0xdd
		img.Pix[5*4+(dx*y)*4+2] = 0xdd
		img.Pix[5*4+(dx*y)*4+3] = 0xff
	}

	var c = NewCarver(dx, dy)
	img = rotateImage90(img)
	for x := 0; x < imgHeight; x++ {
		width, height := img.Bounds().Max.X, img.Bounds().Max.Y

		c = NewCarver(width, height)
		_, err := c.ComputeSeams(p, img)
		assert.NoError(t, err)

		les := c.FindLowestEnergySeams(p)
		seams = append(seams, les)
	}

	for i := range seams {
		for s := range seams[i] {
			totalEnergySeams += seams[i][s].X
		}
	}
	assert.Greater(t, totalEnergySeams, 0)
}

func TestCarver_RemoveSeam(t *testing.T) {
	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	bounds := img.Bounds()

	// We choose to fill up the background with an uniform white color
	// and afterwards we replace the colors in a single row with lower intensity ones.
	draw.Draw(img, bounds, &image.Uniform{image.White}, image.Point{}, draw.Src)
	origImg := img

	dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
	// Replace the pixels in row 5 with lower intensity colors.
	for x := 0; x < dx; x++ {
		img.Set(x, 5, color.RGBA{R: 0xdd, G: 0xdd, B: 0xdd, A: 0xff})
	}

	c := NewCarver(dx, dy)
	_, err := c.ComputeSeams(p, img)
	assert.NoError(t, err)

	seams := c.FindLowestEnergySeams(p)
	img = c.RemoveSeam(img, seams, false)

	isEq := true
	// The test should pass if the detector correctly finds the row which pixel values are of lower intensity.
	for x := 0; x < dx; x++ {
		for y := 0; y < dy; y++ {
			// In case the seam detector correctly recognize the modified line as of low importance
			// it should remove it, which means the new image width should be 1px less then the original image.
			r0, g0, b0, _ := origImg.At(x, y).RGBA()
			r1, g1, b1, _ := img.At(x, y).RGBA()

			if r0>>8 != r1>>8 && g0>>8 != g1>>8 && b0>>8 != b1>>8 {
				isEq = false
			}
		}
	}
	assert.False(t, isEq)
}

func TestCarver_AddSeam(t *testing.T) {
	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	bounds := img.Bounds()

	// We choose to fill up the background with an uniform white color
	// Afterwards we'll replace the colors in a single row with lower intensity ones.
	draw.Draw(img, bounds, &image.Uniform{image.White}, image.Point{}, draw.Src)
	origImg := img

	dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
	// Replace the pixels in row 5 with lower intensity colors.
	for x := 0; x < dx; x++ {
		img.Set(x, 5, color.RGBA{R: 0xdd, G: 0xdd, B: 0xdd, A: 0xff})
	}

	c := NewCarver(dx, dy)
	_, err := c.ComputeSeams(p, img)
	assert.NoError(t, err)

	seams := c.FindLowestEnergySeams(p)
	img = c.AddSeam(img, seams, false)

	dx, dy = img.Bounds().Dx(), img.Bounds().Dy()

	isEq := true
	// The test should pass if the detector correctly finds the row which has lower intensity colors.
	for x := 0; x < dx; x++ {
		for y := 0; y < dy; y++ {
			r0, g0, b0, _ := origImg.At(x, y).RGBA()
			r1, g1, b1, _ := img.At(x, y).RGBA()

			if r0>>8 != r1>>8 && g0>>8 != g1>>8 && b0>>8 != b1>>8 {
				isEq = false
			}
		}
	}
	assert.False(t, isEq)
}

func TestCarver_ComputeSeams(t *testing.T) {
	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))

	// We choose to fill up the background with an uniform white color
	// Afterwards we'll replace the colors in a single row with lower intensity ones.
	dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
	// Replace the pixels in row 5 with lower intensity colors.
	for x := 0; x < dx; x++ {
		img.Pix[(5*dx+x)*4+0] = 0xdd
		img.Pix[(5*dx+x)*4+1] = 0xdd
		img.Pix[(5*dx+x)*4+2] = 0xdd
		img.Pix[(5*dx+x)*4+3] = 0xdd
	}

	c := NewCarver(dx, dy)
	_, err := c.ComputeSeams(p, img)
	assert.NoError(t, err)

	otherThenZero := findNonZeroValue(c.Points)

	assert.True(t, otherThenZero)
}

func TestCarver_ShouldDetectFace(t *testing.T) {
	p.FaceDetect = true

	sampleImg := filepath.Join("./testdata", "sample.jpg")
	f, err := os.Open(sampleImg)
	if err != nil {
		t.Fatalf("could not load sample image: %v", err)
	}
	defer f.Close()

	p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
	if err != nil {
		t.Fatalf("error unpacking the cascade file: %v", err)
	}

	src, _, err := image.Decode(f)
	if err != nil {
		t.Fatalf("error decoding image: %v", err)
	}
	img := imgToNRGBA(src)
	dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y

	// Transform the image to a pixel array.
	pixels := rgbToGrayscale(img)

	cParams := pigo.CascadeParams{
		MinSize:     100,
		MaxSize:     utils.Max(dx, dy),
		ShiftFactor: 0.1,
		ScaleFactor: 1.1,

		ImageParams: pigo.ImageParams{
			Pixels: pixels,
			Rows:   dy,
			Cols:   dx,
			Dim:    dx,
		},
	}

	// Run the classifier over the obtained leaf nodes and return the detection results.
	// The result contains quadruplets representing the row, column, scale and detection score.
	faces := p.FaceDetector.RunCascade(cParams, p.FaceAngle)

	// Calculate the intersection over union (IoU) of two clusters.
	faces = p.FaceDetector.ClusterDetections(faces, 0.2)

	assert.Equal(t, 1, len(faces))
}

func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) {
	p.FaceDetect = true
	p.BlurRadius = 10

	sampleImg := filepath.Join("./testdata", "sample.jpg")
	f, err := os.Open(sampleImg)
	if err != nil {
		t.Fatalf("could not load sample image: %v", err)
	}
	defer f.Close()

	p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
	if err != nil {
		t.Fatalf("error unpacking the cascade file: %v", err)
	}

	src, _, err := image.Decode(f)
	if err != nil {
		t.Fatalf("error decoding image: %v", err)
	}
	img := imgToNRGBA(src)
	dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y

	c := NewCarver(dx, dy)
	// Transform the image to a pixel array.
	pixels := rgbToGrayscale(img)

	sobel := c.SobelDetector(img, float64(p.SobelThreshold))

	err = Stackblur(img, sobel, uint32(p.BlurRadius))
	assert.NoError(t, err)

	cParams := pigo.CascadeParams{
		MinSize:     100,
		MaxSize:     utils.Max(dx, dy),
		ShiftFactor: 0.1,
		ScaleFactor: 1.1,

		ImageParams: pigo.ImageParams{
			Pixels: pixels,
			Rows:   dy,
			Cols:   dx,
			Dim:    dx,
		},
	}

	// Run the classifier over the obtained leaf nodes and return the detection results.
	// The result contains quadruplets representing the row, column, scale and detection score.
	faces := p.FaceDetector.RunCascade(cParams, p.FaceAngle)

	// Calculate the intersection over union (IoU) of two clusters.
	faces = p.FaceDetector.ClusterDetections(faces, 0.2)

	// Range over all the detected faces and draw a white rectangle mask over each of them.
	// We need to trick the sobel detector to consider them as important image parts.
	var rect image.Rectangle
	for _, face := range faces {
		if face.Q > 5.0 {
			rect = image.Rect(
				face.Col-face.Scale/2,
				face.Row-face.Scale/2,
				face.Col+face.Scale/2,
				face.Row+face.Scale/2,
			)
			draw.Draw(sobel, rect, &image.Uniform{image.White}, image.Point{}, draw.Src)
		}
	}
	_, err = c.ComputeSeams(p, img)
	assert.Error(t, err)

	seams := c.FindLowestEnergySeams(p)

	for _, seam := range seams {
		if seam.X >= rect.Min.X && seam.X <= rect.Max.X {
			t.Errorf("Carver shouldn't remove seams from face zone")
			break
		}
	}
}

func TestCarver_ShouldNotResizeWithFaceDistorsion(t *testing.T) {
	p.FaceDetect = true
	p.BlurRadius = 10
	p.NewHeight = 200

	sampleImg := filepath.Join("./testdata", "sample.jpg")
	f, err := os.Open(sampleImg)
	if err != nil {
		t.Fatalf("could not load sample image: %v", err)
	}
	defer f.Close()

	p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
	if err != nil {
		t.Fatalf("error unpacking the cascade file: %v", err)
	}

	src, _, err := image.Decode(f)
	if err != nil {
		t.Fatalf("error decoding image: %v", err)
	}
	img := imgToNRGBA(src)
	dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y

	// Transform the image to a pixel array.
	pixels := rgbToGrayscale(img)
	cParams := pigo.CascadeParams{
		MinSize:     100,
		MaxSize:     utils.Max(dx, dy),
		ShiftFactor: 0.1,
		ScaleFactor: 1.1,

		ImageParams: pigo.ImageParams{
			Pixels: pixels,
			Rows:   dy,
			Cols:   dx,
			Dim:    dx,
		},
	}

	// Run the classifier over the obtained leaf nodes and return the detection results.
	// The result contains quadruplets representing the row, column, scale and detection score.
	faces := p.FaceDetector.RunCascade(cParams, p.FaceAngle)

	// Calculate the intersection over union (IoU) of two clusters.
	faces = p.FaceDetector.ClusterDetections(faces, 0.2)

	for _, face := range faces {
		if p.NewHeight < face.Scale {
			t.Errorf("Should not resize image without face deformation.")
		}
	}
}

// findNonZeroValue utility function to check if the slice contains values other then zeros.
func findNonZeroValue(points []float64) bool {
	var found = false
	for i := 0; i < len(points); i++ {
		if points[i] != 0 {
			found = true
		}
	}
	return found
}


================================================
FILE: cmd/caire/main.go
================================================
package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"runtime"

	"gioui.org/app"
	"github.com/esimov/caire"
	"github.com/esimov/caire/utils"
)

const HelpBanner = `
┌─┐┌─┐┬┬─┐┌─┐
│  ├─┤│├┬┘├┤
└─┘┴ ┴┴┴└─└─┘

Content aware image resize library.
    Version: %s

`

// pipeName indicates that stdin/stdout is being used as file names.
const pipeName = "-"

// Version indicates the current build version.
var Version string

var (
	// Flags
	source         = flag.String("in", pipeName, "Source")
	destination    = flag.String("out", pipeName, "Destination")
	blurRadius     = flag.Int("blur", 4, "Blur radius")
	sobelThreshold = flag.Int("sobel", 2, "Sobel filter threshold")
	newWidth       = flag.Int("width", 0, "New width")
	newHeight      = flag.Int("height", 0, "New height")
	percentage     = flag.Bool("perc", false, "Reduce image by percentage")
	square         = flag.Bool("square", false, "Reduce image to square dimensions")
	debug          = flag.Bool("debug", false, "Show the seams")
	shapeType      = flag.String("shape", "circle", "Shape type used for debugging: circle|line")
	seamColor      = flag.String("color", "#ff0000", "Seam color")
	preview        = flag.Bool("preview", true, "Show GUI window")
	maskPath       = flag.String("mask", "", "Mask file path for retaining area")
	rMaskPath      = flag.String("rmask", "", "Mask file path for removing area")
	faceDetect     = flag.Bool("face", false, "Use face detection")
	faceAngle      = flag.Float64("angle", 0.0, "Face rotation angle")
	workers        = flag.Int("conc", runtime.NumCPU(), "Number of files to process concurrently")
)

func main() {
	log.SetFlags(0)

	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, fmt.Sprintf(HelpBanner, Version))
		flag.PrintDefaults()
	}
	flag.Parse()

	proc := &caire.Processor{
		BlurRadius:     *blurRadius,
		SobelThreshold: *sobelThreshold,
		NewWidth:       *newWidth,
		NewHeight:      *newHeight,
		Percentage:     *percentage,
		Square:         *square,
		Debug:          *debug,
		Preview:        *preview,
		FaceDetect:     *faceDetect,
		FaceAngle:      *faceAngle,
		MaskPath:       *maskPath,
		RMaskPath:      *rMaskPath,
		SeamColor:      *seamColor,
		ShapeType:      caire.ShapeType(*shapeType),
	}

	if !(*newWidth > 0 || *newHeight > 0 || *percentage || *square) {
		flag.Usage()
		log.Fatalf("%s%s",
			utils.DecorateText("\nPlease provide a width, height or percentage for image rescaling!", utils.ErrorMessage),
			utils.DefaultColor,
		)
	} else {
		op := &caire.Image{
			Src:      *source,
			Dst:      *destination,
			Workers:  *workers,
			PipeName: pipeName,
		}

		if *preview {
			// When the preview mode is activated we have to execute the resizing process
			// in a separate goroutine in order to not block the Gio thread,
			// which have to run on the main OS thread of the operating systems like MacOS.
			go proc.Execute(op)
			app.Main()
		} else {
			proc.Execute(op)
		}
	}
}


================================================
FILE: doc.go
================================================
/*
Package caire is a content aware image resize library, which can rescale the source image seamlessly
both vertically and horizontally by eliminating the less important parts of the image.

The package provides a command line interface, supporting various flags for different types of rescaling operations.
To check the supported commands type:

	$ caire --help

In case you wish to integrate the API in a self constructed environment here is a simple example:

	package main

	import (
		"fmt"
		"github.com/esimov/caire"
	)

	func main() {
		p := &caire.Processor{
			// Initialize struct variables
		}

		if err := p.Process(in, out); err != nil {
			fmt.Printf("Error rescaling image: %s", err.Error())
		}
	}
 */
package caire


================================================
FILE: draw.go
================================================
package caire

import (
	"image/color"
	"math"

	"gioui.org/f32"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"github.com/esimov/caire/utils"
)

type ShapeType string

const (
	Circle ShapeType = "circle"
	Line   ShapeType = "line"
)

// DrawSeam visualizes the seam carver in action when the preview mode is activated.
// It receives as parameters the shape type, the seam (x,y) coordinates and it's thickness.
func (g *Gui) DrawSeam(shape ShapeType, x, y, thickness float32) {
	r := getRatio(g.cfg.window.width, g.cfg.window.height)

	switch shape {
	case Circle:
		g.drawCircle(x*r, y*r, thickness)
	case Line:
		g.drawLine(x*r, y*r, thickness)
	}
}

// drawCircle draws a circle at the seam (x,y) coordinate with the provided size.
func (g *Gui) drawCircle(x, y, radius float32) {
	var (
		sq   float64
		p1   f32.Point
		p2   f32.Point
		orig = g.point(x-radius, y)
	)

	sq = math.Sqrt(float64(radius*radius) - float64(radius*radius))
	p1 = g.point(x+float32(sq), y).Sub(orig)
	p2 = g.point(x-float32(sq), y).Sub(orig)

	col := utils.HexToRGBA(g.proc.SeamColor)
	g.setFillColor(col)

	var path clip.Path
	path.Begin(g.ctx.Ops)
	path.Move(orig)
	path.Arc(p1, p2, 2*math.Pi)
	path.Close()

	defer clip.Outline{Path: path.End()}.Op().Push(g.ctx.Ops).Pop()
	paint.ColorOp{Color: g.setColor(g.getFillColor())}.Add(g.ctx.Ops)
	paint.PaintOp{}.Add(g.ctx.Ops)
}

// drawLine draws a line at the seam (x,y) coordinate with the provided line thickness.
func (g *Gui) drawLine(x, y, thickness float32) {
	var (
		p1   = g.point(x, y)
		p2   = g.point(x, y+1)
		path clip.Path
	)

	path.Begin(g.ctx.Ops)
	path.Move(p1)
	path.Line(p2.Sub(path.Pos()))
	path.Close()

	col := utils.HexToRGBA(g.proc.SeamColor)
	g.setFillColor(col)

	defer clip.Stroke{Path: path.End(), Width: float32(thickness)}.Op().Push(g.ctx.Ops).Pop()
	paint.ColorOp{Color: g.setColor(g.getFillColor())}.Add(g.ctx.Ops)
	paint.PaintOp{}.Add(g.ctx.Ops)
}

// point converts the seam (x,y) coordinate to Gio f32.Point.
func (g *Gui) point(x, y float32) f32.Point {
	return f32.Point{
		X: x,
		Y: y,
	}
}

// setColor sets the seam color.
func (g *Gui) setColor(c color.Color) color.NRGBA {
	rc, gc, bc, ac := c.RGBA()
	return color.NRGBA{
		R: uint8(rc >> 8),
		G: uint8(gc >> 8),
		B: uint8(bc >> 8),
		A: uint8(ac >> 8),
	}
}

// setFillColor sets the paint fill color.
func (g *Gui) setFillColor(c color.Color) {
	g.cfg.color.fill = c
}

// getFillColor retrieve the paint fill color.
func (g *Gui) getFillColor() color.Color {
	return g.cfg.color.fill
}

// getRatio returns the image aspect ratio.
func getRatio(w, h float32) float32 {
	var r float32 = 1
	if w > maxScreenX && h > maxScreenY {
		wr := maxScreenX / float32(w) // width ratio
		hr := maxScreenY / float32(h) // height ratio

		r = utils.Max(wr, hr)
	}
	return r
}


================================================
FILE: exec.go
================================================
package caire

import (
	"errors"
	"fmt"
	"image"
	"io"
	"log"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"sync"
	"syscall"
	"time"

	"slices"

	"github.com/esimov/caire/utils"
	"golang.org/x/term"
)

var (
	// imgFile holds the file being accessed, be it normal file or pipe name.
	imgFile *os.File

	// Common file related variable
	fs os.FileInfo
)

type Image struct {
	Src, Dst, PipeName string
	Workers            int
}

// result holds the relevant information about the resizing process and the generated image.
type result struct {
	path string
	err  error
}

func Resize(s SeamCarver, img *image.NRGBA) (image.Image, error) {
	return s.Resize(img)
}

// Execute executes the image resizing process.
// In case the preview mode is activated it will be invoked in a separate goroutine
// in order to avoid blocking the main OS thread. Otherwise it will be called normally.
func (p *Processor) Execute(img *Image) {
	var err error
	defaultMsg := fmt.Sprintf("%s %s",
		utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
		utils.DecorateText("⇢ resizing image (be patient, it may take a while)...", utils.DefaultMessage),
	)
	p.Spinner = utils.NewSpinner(defaultMsg, time.Millisecond*80)

	// Supported files
	validExtensions := []string{".jpg", ".png", ".jpeg", ".bmp", ".gif"}

	// Check if source path is a local image or URL.
	if utils.IsValidUrl(img.Src) {
		src, err := utils.DownloadImage(img.Src)
		if src != nil {
			defer os.Remove(src.Name())
		}

		if err != nil {
			log.Fatalf(
				utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage),
				utils.DecorateText(err.Error(), utils.DefaultMessage),
			)
		}
		fs, err = src.Stat()
		if err != nil {
			log.Fatalf(
				utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage),
				utils.DecorateText(err.Error(), utils.DefaultMessage),
			)
		}
		img, err := os.Open(src.Name())
		if err != nil {
			log.Fatalf(
				utils.DecorateText("Unable to open the temporary image file: %v", utils.ErrorMessage),
				utils.DecorateText(err.Error(), utils.DefaultMessage),
			)
		}

		imgFile = img
	} else {
		// Check if the source is a pipe name or a regular file.
		if img.Src == img.PipeName {
			fs, err = os.Stdin.Stat()
		} else {
			fs, err = os.Stat(img.Src)
		}
		if err != nil {
			log.Fatalf(
				utils.DecorateText("Failed to load the source image: %v", utils.ErrorMessage),
				utils.DecorateText(err.Error(), utils.DefaultMessage),
			)
		}
	}

	now := time.Now()

	switch mode := fs.Mode(); {
	case mode.IsDir():
		var wg sync.WaitGroup
		// Read destination file or directory.
		_, err := os.Stat(img.Dst)
		if err != nil {
			err = os.Mkdir(img.Dst, 0755)
			if err != nil {
				log.Fatalf(
					utils.DecorateText("Unable to get dir stats: %v\n", utils.ErrorMessage),
					utils.DecorateText(err.Error(), utils.DefaultMessage),
				)
			}
		}
		p.Preview = false

		// Limit the concurrently running workers to maxWorkers.
		if img.Workers <= 0 || img.Workers > runtime.NumCPU() {
			img.Workers = runtime.NumCPU()
		}

		// Process recursively the image files from the specified directory concurrently.
		ch := make(chan result)
		done := make(chan any)
		defer close(done)

		paths, errc := walkDir(done, img.Src, validExtensions)

		wg.Add(img.Workers)
		for range img.Workers {
			go func() {
				defer wg.Done()
				img.consumer(p, img.Dst, ch, done, paths)
			}()
		}

		// Close the channel after the values are consumed.
		go func() {
			defer close(ch)
			wg.Wait()
		}()

		// Consume the channel values.
		for res := range ch {
			if res.err != nil {
				err = res.err
			}
			img.printOpStatus(res.path, err)
		}

		if err = <-errc; err != nil {
			fmt.Fprintf(os.Stderr, utils.DecorateText(err.Error(), utils.ErrorMessage))
		}

	case mode.IsRegular() || mode&os.ModeNamedPipe != 0: // check for regular files or pipe names
		ext := filepath.Ext(img.Dst)
		if !slices.Contains(validExtensions, ext) && img.Dst != img.PipeName {
			log.Fatalf(utils.DecorateText(fmt.Sprintf("%v file type not supported", ext), utils.ErrorMessage))
		}

		err = img.process(p, img.Src, img.Dst)
		img.printOpStatus(img.Dst, err)
	}
	if err == nil {
		fmt.Fprintf(os.Stderr, "\nExecution time: %s\n", utils.DecorateText(
			utils.FormatTime(time.Since(now)), utils.SuccessMessage),
		)
	}
}

// consumer reads the path names from the paths channel and calls the resizing processor against the source image.
func (img *Image) consumer(
	p *Processor,
	dest string,
	res chan<- result,
	done <-chan any,
	paths <-chan string,
) {
	for src := range paths {
		dst := filepath.Join(dest, filepath.Base(src))
		err := img.process(p, src, dst)

		select {
		case <-done:
			return
		case res <- result{
			path: src,
			err:  err,
		}:
		}
	}
}

// processor calls the resizer method over the source image and returns the error in case exists.
func (img *Image) process(p *Processor, in, out string) error {
	var (
		successMsg string
		errorMsg   string
	)
	// Start the progress indicator.
	p.Spinner.Start()

	successMsg = fmt.Sprintf("%s %s %s",
		utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
		utils.DecorateText("⇢", utils.DefaultMessage),
		utils.DecorateText("the image has been resized successfully ✔", utils.SuccessMessage),
	)

	errorMsg = fmt.Sprintf("%s %s %s",
		utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
		utils.DecorateText("resizing image failed...", utils.DefaultMessage),
		utils.DecorateText("✘", utils.ErrorMessage),
	)

	src, dst, err := img.pathToFile(in, out)
	if err != nil {
		p.Spinner.StopMsg = errorMsg
		return err
	}

	// Capture CTRL-C signal and restores back the cursor visibility.
	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
	go func() {
		<-signalChan
		func() {
			p.Spinner.RestoreCursor()
			os.Remove(dst.(*os.File).Name())
			os.Exit(1)
		}()
	}()

	defer func() {
		if img, ok := src.(*os.File); ok {
			if err := img.Close(); err != nil {
				log.Printf("could not close the opened file: %v", err)
			}
		}
	}()

	defer func() {
		if img, ok := dst.(*os.File); ok {
			if err := img.Close(); err != nil {
				log.Printf("could not close the opened file: %v", err)
			}
		}
	}()

	if len(p.MaskPath) > 0 {
		mask, err := decodeImg(p.MaskPath)
		if err != nil {
			return fmt.Errorf("cannot decode image: %w", err)
		}
		p.Mask = dither(imgToNRGBA(mask))
		p.DebugMask = p.Mask
	}

	if len(p.RMaskPath) > 0 {
		rmask, err := decodeImg(p.RMaskPath)
		if err != nil {
			return fmt.Errorf("cannot decode image: %w", err)
		}
		p.RMask = dither(imgToNRGBA(rmask))
		p.DebugMask = p.RMask
	}

	err = p.Process(src, dst)
	if err != nil {
		// remove the generated image file in case of an error
		os.Remove(dst.(*os.File).Name())

		p.Spinner.StopMsg = errorMsg
		// Stop the progress indicator.
		p.Spinner.Stop()

		return err
	} else {
		p.Spinner.StopMsg = successMsg
		// Stop the progress indicator.
		p.Spinner.Stop()
	}

	return nil
}

// pathToFile converts the source and destination paths to readable and writable files.
func (img *Image) pathToFile(in, out string) (io.Reader, io.Writer, error) {
	var (
		src io.Reader
		dst io.Writer
		err error
	)
	// Check if the source path is a local image or URL.
	if utils.IsValidUrl(in) {
		src = imgFile
	} else {
		// Check if the source is a pipe name or a regular file.
		if in == img.PipeName {
			if term.IsTerminal(int(os.Stdin.Fd())) {
				return nil, nil, errors.New("`-` should be used with a pipe for stdin")
			}
			src = os.Stdin
		} else {
			src, err = os.Open(in)
			if err != nil {
				return nil, nil, fmt.Errorf("unable to open the source file: %v", err)
			}
		}
	}

	// Check if the destination is a pipe name or a regular file.
	if out == img.PipeName {
		if term.IsTerminal(int(os.Stdout.Fd())) {
			return nil, nil, errors.New("`-` should be used with a pipe for stdout")
		}
		dst = os.Stdout
	} else {
		dst, err = os.OpenFile(out, os.O_CREATE|os.O_WRONLY, 0755)
		if err != nil {
			return nil, nil, fmt.Errorf("unable to create the destination file: %v", err)
		}
	}

	return src, dst, nil
}

// printOpStatus displays the relevant information about the image resizing process.
func (img *Image) printOpStatus(fname string, err error) {
	if err != nil {
		log.Fatalf(
			utils.DecorateText("\nError resizing the image: %s", utils.ErrorMessage),
			utils.DecorateText(fmt.Sprintf("\n\tReason: %v\n", err.Error()), utils.DefaultMessage),
		)
	} else {
		if fname != img.PipeName {
			fmt.Fprintf(os.Stderr, "\nThe image has been saved as: %s %s\n\n",
				utils.DecorateText(filepath.Base(fname), utils.SuccessMessage),
				utils.DefaultColor,
			)
		}
	}
}

// walkDir starts a new goroutine to walk the specified directory tree
// in recursive manner and sends the path of each regular file to a new channel.
// It finishes in case the done channel is getting closed.
func walkDir(
	done <-chan any,
	src string,
	srcExts []string,
) (<-chan string, <-chan error) {
	pathChan := make(chan string)
	errChan := make(chan error, 1)

	go func() {
		// Close the paths channel after Walk returns.
		defer close(pathChan)

		errChan <- filepath.Walk(src, func(path string, f os.FileInfo, err error) error {
			isFileSupported := false
			if err != nil {
				return err
			}
			if !f.Mode().IsRegular() {
				return nil
			}

			// Get the file base name.
			fx := filepath.Ext(f.Name())
			if slices.Contains(srcExts, fx) {
				isFileSupported = true
			}

			if isFileSupported {
				select {
				case <-done:
					return errors.New("directory walk cancelled")
				case pathChan <- path:
				}
			}
			return nil
		})
	}()
	return pathChan, errChan
}


================================================
FILE: go.mod
================================================
module github.com/esimov/caire

go 1.22

require (
	gioui.org v0.8.0
	github.com/disintegration/imaging v1.6.2
	github.com/esimov/pigo v1.4.5
	github.com/stretchr/testify v1.10.0
	golang.org/x/exp v0.0.0-20240707233637-46b078467d37
	golang.org/x/image v0.23.0
	golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
)

require (
	gioui.org/shader v1.0.8 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/go-text/typesetting v0.2.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect
	golang.org/x/sys v0.29.0 // indirect
	golang.org/x/text v0.21.0 // indirect
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
gioui.org v0.8.0 h1:QV5p5JvsmSmGiIXVYOKn6d9YDliTfjtLlVf5J+BZ9Pg=
gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/esimov/pigo v1.4.5 h1:ySG0QqMh02VNALvHnx04L1ScRu66N6XA5vLLga8GiLg=
github.com/esimov/pigo v1.4.5/go.mod h1:SGkOUpm4wlEmQQJKlaymAkThY8/8iP+XE0gFo7g8G6w=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: gui.go
================================================
package caire

import (
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"math"
	"math/rand"
	"time"

	"gioui.org/app"
	"gioui.org/f32"
	"gioui.org/font/gofont"
	"gioui.org/io/key"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
	"github.com/esimov/caire/imop"
	"github.com/esimov/caire/utils"
)

type hudControlType int

const (
	hudShowSeams hudControlType = iota
	hudShowDebugMask
)

const (
	// The starting colors for the linear gradient, used when the image is resized both horizontally and vertically.
	// In this case the preview mode is deactivated and a dynamic gradient overlay is shown.
	redStart   = 137
	greenStart = 47
	blueStart  = 54

	// The ending colors for the linear gradient. The starting colors and ending colors are lerped.
	redEnd   = 255
	greenEnd = 112
	blueEnd  = 105
)

var (
	maxScreenX float32 = 1280
	maxScreenY float32 = 720

	defaultBkgColor  = color.Transparent
	defaultFillColor = color.Black
)

type interval struct {
	min, max float64
}

// Gui is the basic struct containing all of the information needed for the UI operation.
// It receives the resized image transferred through a channel which is called in a separate goroutine.
type Gui struct {
	cfg struct {
		x      interval
		y      interval
		chrot  bool
		angle  float32
		window struct {
			width  float32
			height float32
			title  string
		}
		color struct {
			randR uint8
			randG uint8
			randB uint8

			background color.Color
			fill       color.Color
		}
		timeStamp time.Time
	}
	process struct {
		isDone bool
		img    image.Image
		seams  []Seam

		worker <-chan worker
		err    chan<- error
	}
	proc    *Processor
	compOp  *imop.Composite
	blendOp *imop.Blend
	theme   *material.Theme
	ctx     layout.Context
	huds    map[hudControlType]*hudCtrl
	view    struct {
		huds layout.List
	}
}

type hudCtrl struct {
	enabled widget.Bool
	hudType hudControlType
	title   string
}

// NewGUI initializes the Gio interface.
func NewGUI(width, height int) *Gui {
	defaultColor := color.NRGBA{R: 0x2d, G: 0x23, B: 0x2e, A: 0xff}

	gui := &Gui{
		ctx: layout.Context{
			Ops: new(op.Ops),
			Constraints: layout.Constraints{
				Max: image.Pt(width, height),
			},
		},
		compOp:  imop.InitOp(),
		blendOp: imop.NewBlend(),
		theme:   material.NewTheme(),
		huds:    make(map[hudControlType]*hudCtrl),
	}

	gui.theme.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
	gui.theme.TextSize = unit.Sp(16)
	gui.theme.Palette.ContrastBg = defaultColor
	gui.theme.FingerSize = 10

	gui.initWindow(width, height)

	return gui
}

// AddHudControl adds a new hud control for debugging.
func (g *Gui) AddHudControl(hudControlType hudControlType, title string, enabled bool) {
	control := &hudCtrl{
		hudType: hudControlType,
		title:   title,
		enabled: widget.Bool{},
	}
	control.enabled.Value = enabled
	g.huds[hudControlType] = control
}

// initWindow creates and initializes the GUI window.
func (g *Gui) initWindow(width, height int) {
	rand.NewSource(time.Now().UnixNano())

	g.cfg.angle = 45
	g.cfg.color.randR = uint8(random(1, 2))
	g.cfg.color.randG = uint8(random(1, 2))
	g.cfg.color.randB = uint8(random(1, 2))

	g.cfg.window.width, g.cfg.window.height = float32(width), float32(height)
	g.cfg.x = interval{min: 0, max: float64(width)}
	g.cfg.y = interval{min: 0, max: float64(height)}

	g.cfg.color.background = defaultBkgColor
	g.cfg.color.fill = defaultFillColor

	if !resizeXY {
		g.cfg.window.width, g.cfg.window.height = g.getWindowSize()
	}
	g.cfg.window.title = "Preview process..."
}

// getWindowSize returns the resized image dimension.
func (g *Gui) getWindowSize() (float32, float32) {
	w, h := g.cfg.window.width, g.cfg.window.height
	// Maintain the image aspect ratio in case the image width and height is greater than the predefined window.
	r := getRatio(w, h)
	if w > maxScreenX && h > maxScreenY {
		w = w * r
		h = h * r
	}
	return w, h
}

// Run is the core method of the Gio GUI application.
// This updates the window with the resized image received from a channel
// and terminates when the image resizing operation completes.
func (g *Gui) Run() error {
	var (
		rc uint8 = redStart
		gc uint8 = greenStart
		bc uint8 = blueStart

		descRed, descGreen, descBlue bool
	)

	width := unit.Dp(g.cfg.window.width)
	height := unit.Dp(g.cfg.window.height)

	w := new(app.Window)
	w.Option(
		app.Title(g.cfg.window.title),
		app.Size(width, height),
		app.MinSize(width, height),
		app.MaxSize(width, height),
	)

	// Center the window.
	w.Perform(system.ActionCenter)

	g.cfg.timeStamp = time.Now()

	if g.proc.Debug {
		g.AddHudControl(hudShowSeams, "Show seams", true)
		if len(g.proc.MaskPath) > 0 || len(g.proc.RMaskPath) > 0 || g.proc.FaceDetect {
			g.AddHudControl(hudShowDebugMask, "Debug mode", false)
		}
	}

	abortFn := func() {
		var dx, dy int

		if g.process.img != nil {
			bounds := g.process.img.Bounds()
			dx, dy = bounds.Max.X, bounds.Max.Y
		}

		if !g.process.isDone {
			if (g.proc.NewWidth > 0 && g.proc.NewWidth != dx) ||
				(g.proc.NewHeight > 0 && g.proc.NewHeight != dy) {

				errorMsg := fmt.Sprintf("%s %s %s",
					utils.DecorateText("⚡ CAIRE", utils.StatusMessage),
					utils.DecorateText("⇢ process aborted by the user...", utils.DefaultMessage),
					utils.DecorateText("✘\n", utils.ErrorMessage),
				)
				g.proc.Spinner.StopMsg = errorMsg
				g.proc.Spinner.Stop()
			}
		}
		g.proc.Spinner.RestoreCursor()
	}

	for {
		select {
		case res := <-g.process.worker:
			if res.done {
				w.Option(app.Title("Done!"))
				g.process.isDone = true
				break
			}
			if resizeXY {
				continue
			}

			g.process.img = res.img
			g.process.seams = res.seams

			if mask, ok := g.huds[hudShowDebugMask]; ok {
				if mask.enabled.Value && res.mask != nil {
					bounds := res.img.Bounds()
					srcBitmap := imop.NewBitmap(bounds)
					dstBitmap := imop.NewBitmap(bounds)

					uniformCol := image.NewNRGBA(bounds)

					col := color.RGBA{R: 0x2f, G: 0xf3, B: 0xe0, A: 0xff}
					draw.Draw(uniformCol, uniformCol.Bounds(), &image.Uniform{col}, image.Point{}, draw.Src)

					_ = g.compOp.Set(imop.DstIn)
					g.compOp.Draw(srcBitmap, res.mask, uniformCol, nil)

					_ = g.blendOp.Set(imop.Screen)
					_ = g.compOp.Set(imop.SrcAtop)
					g.compOp.Draw(dstBitmap, res.img, srcBitmap.Img, g.blendOp)

					g.process.img = dstBitmap.Img
				}
			}

			if g.proc.vRes {
				g.process.img = rotateImage270(g.process.img.(*image.NRGBA))
			}
			w.Invalidate()
		default:
			switch e := w.Event().(type) {
			case app.FrameEvent:
				g.ctx = app.NewContext(g.ctx.Ops, e)

				for {
					event, ok := g.ctx.Event(key.Filter{
						Name: key.NameEscape,
					})
					if !ok {
						break
					}
					switch event := event.(type) {
					case key.Event:
						switch event.Name {
						case key.NameEscape:
							w.Perform(system.ActionClose)
							abortFn()
							return nil
						}
					}
				}

				{ // red
					if descRed {
						rc--
					} else {
						rc++
					}
					if rc >= redEnd {
						descRed = !descRed
					}
					if rc == redStart {
						descRed = !descRed
					}
				}
				{ // green
					if descGreen {
						gc--
					} else {
						gc++
					}
					if gc >= greenEnd {
						descGreen = !descGreen
					}
					if gc == greenStart {
						descGreen = !descGreen
					}
				}
				{ // blue
					if descBlue {
						bc--
					} else {
						bc++
					}
					if bc >= blueEnd {
						descBlue = !descBlue
					}
					if bc == blueStart {
						descBlue = !descBlue
					}
				}
				g.draw(color.NRGBA{R: rc, G: gc, B: bc})
				e.Frame(g.ctx.Ops)
			case app.DestroyEvent:
				abortFn()
				return e.Err
			}
		}
	}
}

type (
	C = layout.Context
	D = layout.Dimensions
)

// draw draws the resized image in the GUI window (obtained from a channel)
// and in case the debug mode is activated it prints out the seams.
func (g *Gui) draw(bgColor color.NRGBA) {
	g.ctx.Execute(op.InvalidateCmd{})

	c := g.setColor(g.cfg.color.background)
	paint.Fill(g.ctx.Ops, c)

	if g.process.img != nil {
		src := paint.NewImageOp(g.process.img)
		src.Add(g.ctx.Ops)

		layout.Stack{}.Layout(g.ctx,
			layout.Stacked(func(gtx C) D {
				paint.FillShape(gtx.Ops, c,
					clip.Rect{Max: g.ctx.Constraints.Max}.Op(),
				)
				return layout.UniformInset(unit.Dp(0)).Layout(gtx,
					func(gtx C) D {
						widget.Image{
							Src:   src,
							Scale: 1 / float32(unit.Dp(1)),
							Fit:   widget.Contain,
						}.Layout(gtx)

						if seam, ok := g.huds[hudShowSeams]; ok {
							if seam.enabled.Value {
								tr := f32.Affine2D{}
								screen := layout.FPt(g.ctx.Constraints.Max)
								width, height := float32(g.process.img.Bounds().Dx()), float32(g.process.img.Bounds().Dy())
								sw, sh := float32(screen.X), float32(screen.Y)

								if sw > width {
									ratio := sw / width
									tr = tr.Scale(f32.Pt(sw/2, sh/2), f32.Pt(1, ratio))
								} else if sh > height {
									ratio := sh / height
									tr = tr.Scale(f32.Pt(sw/2, sh/2), f32.Pt(ratio, 1))
								}

								if g.proc.vRes {
									angle := float32(270 * math.Pi / 180)
									half := float32(math.Round(float64(sh*0.5-height*0.5) * 0.5))

									ox := math.Abs(float64(sw - (sw - (sw/2 - sh/2))))
									oy := math.Abs(float64(sh - (sh - (sw/2 - height/2 + half))))
									tr = tr.Rotate(f32.Pt(sw/2, sh/2), -angle)

									if screen.X > screen.Y {
										tr = tr.Offset(f32.Pt(float32(ox), float32(oy)))
									} else {
										tr = tr.Offset(f32.Pt(float32(-ox), float32(-oy)))
									}
								}
								op.Affine(tr).Add(gtx.Ops)

								for _, s := range g.process.seams {
									dpx := gtx.Dp(unit.Dp(s.X))
									dpy := gtx.Dp(unit.Dp(s.Y))
									g.DrawSeam(g.proc.ShapeType, float32(dpx), float32(dpy), 1.0)
								}
							}
						}
						return layout.Dimensions{Size: gtx.Constraints.Max}
					})
			}),
		)
	}
	if g.proc.Debug {
		layout.Stack{}.Layout(g.ctx,
			layout.Stacked(func(gtx C) D {
				hudHeight := 30
				r := image.Rectangle{
					Max: image.Point{
						X: gtx.Constraints.Max.X,
						Y: hudHeight,
					},
				}
				defer op.Offset(image.Pt(0, gtx.Constraints.Max.Y-hudHeight)).Push(gtx.Ops).Pop()
				return layout.Stack{}.Layout(gtx,
					layout.Expanded(func(gtx C) D {
						paint.FillShape(gtx.Ops, color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xcc}, clip.Rect(r).Op())
						return layout.Dimensions{Size: r.Max}
					}),
					layout.Stacked(func(gtx C) D {
						border := image.Rectangle{
							Max: image.Point{
								X: gtx.Constraints.Max.X,
								Y: gtx.Dp(unit.Dp(0.5)),
							},
						}
						paint.FillShape(gtx.Ops, color.NRGBA{R: 0xd0, G: 0xcd, B: 0xd7, A: 0xaa}, clip.Rect(border).Op())
						return layout.Dimensions{Size: r.Max}
					}),
					layout.Stacked(func(gtx C) D {
						return g.view.huds.Layout(gtx, len(g.huds),
							func(gtx layout.Context, index int) D {
								if hud, ok := g.huds[hudControlType(index)]; ok {
									checkbox := material.CheckBox(g.theme, &hud.enabled, fmt.Sprintf("%v", hud.title))
									checkbox.Size = 20
									return checkbox.Layout(gtx)
								}
								return D{}
							})
					}),
				)
			}),
		)
	}

	// Disable the preview mode and warn the user in case the image is resized both horizontally and vertically.
	if resizeXY {
		var msg string

		if !g.process.isDone {
			msg = "Preview is not available while the image is resized both horizontally and vertically!"
		} else {
			msg = "Done, you may close this window!"
			bgColor = color.NRGBA{R: 45, G: 45, B: 42, A: 0xff}
		}
		g.displayMessage(g.ctx, bgColor, msg)
	}
}

// displayMessage show a static message when the image is resized both horizontally and vertically.
func (g *Gui) displayMessage(ctx layout.Context, bgCol color.NRGBA, msg string) {
	g.theme.Palette.Fg = color.NRGBA{R: 251, G: 254, B: 249, A: 0xff}
	paint.ColorOp{Color: bgCol}.Add(ctx.Ops)

	rect := image.Rectangle{
		Max: ctx.Constraints.Max,
	}

	defer clip.Rect(rect).Push(ctx.Ops).Pop()
	paint.PaintOp{}.Add(ctx.Ops)

	layout.Stack{}.Layout(ctx,
		layout.Stacked(func(gtx C) D {
			return layout.UniformInset(unit.Dp(4)).Layout(ctx, func(gtx C) D {
				if !g.process.isDone {
					gtx.Constraints.Min.Y = 0
					tr := f32.Affine2D{}
					dr := image.Rectangle{Max: gtx.Constraints.Min}

					tr = tr.Rotate(f32.Pt(float32(ctx.Constraints.Max.X/2), float32(ctx.Constraints.Max.Y/2)), 0.005*-g.cfg.angle)
					op.Affine(tr).Add(gtx.Ops)

					since := time.Since(g.cfg.timeStamp)

					if since.Seconds() > 5 {
						g.cfg.timeStamp = time.Now()
						g.cfg.color.randR = uint8(random(1, 2))
						g.cfg.color.randG = uint8(random(1, 2))
						g.cfg.color.randB = uint8(random(1, 2))
					}

					paint.LinearGradientOp{
						Stop1:  layout.FPt(dr.Min.Div(2)),
						Stop2:  layout.FPt(dr.Max.Mul(2)),
						Color1: color.NRGBA{R: 41, G: bgCol.G * g.cfg.color.randG, B: bgCol.B * g.cfg.color.randB, A: 0xFF},
						Color2: color.NRGBA{R: bgCol.R * g.cfg.color.randR, G: 29, B: 54, A: 0xFF},
					}.Add(gtx.Ops)
					paint.PaintOp{}.Add(gtx.Ops)

					if g.cfg.chrot {
						g.cfg.angle--
					} else {
						g.cfg.angle++
					}
					if g.cfg.angle == -90 || g.cfg.angle == 90 {
						g.cfg.chrot = !g.cfg.chrot
					}
				}

				return layout.Dimensions{
					Size: gtx.Constraints.Max,
				}
			})
		}),
		layout.Stacked(func(gtx C) D {
			return layout.UniformInset(unit.Dp(4)).Layout(ctx, func(gtx C) D {
				return layout.Center.Layout(ctx, func(gtx C) D {
					m := material.Label(g.theme, unit.Sp(40), msg)
					m.Alignment = text.Middle

					return m.Layout(gtx)
				})
			})
		}),
		layout.Stacked(func(gtx C) D {
			info := "(You will be notified once the process is finished.)"
			if g.process.isDone {
				return layout.Dimensions{}
			}

			return layout.Inset{Top: 70}.Layout(ctx, func(gtx C) D {
				return layout.Center.Layout(ctx, func(gtx C) D {
					return material.Label(g.theme, unit.Sp(13), info).Layout(gtx)
				})
			})
		}),
	)
}

// random generates a random number between two numbers.
func random(min, max float32) float32 {
	return rand.Float32()*(max-min) + min
}


================================================
FILE: image.go
================================================
package caire

import (
	"errors"
	"fmt"
	"image"
	"image/color"
	"image/jpeg"
	"image/png"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/esimov/caire/utils"
	"golang.org/x/image/bmp"
)

// decodeImg decodes an image file to type image.Image
func decodeImg(src string) (image.Image, error) {
	file, err := os.Open(src)
	if err != nil {
		return nil, fmt.Errorf("could not open the mask file: %v", err)
	}

	ctype, err := utils.DetectContentType(file.Name())
	if err != nil {
		return nil, err
	}

	if !strings.Contains(ctype.(string), "image") {
		return nil, fmt.Errorf("the mask should be an image file")
	}

	img, _, err := image.Decode(file)
	if err != nil {
		return nil, fmt.Errorf("could not decode the mask file: %v", err)
	}

	return img, nil
}

// encodeImg encodes an image to a destination of type io.Writer.
func encodeImg(p *Processor, w io.Writer, img *image.NRGBA) error {
	switch w := w.(type) {
	case *os.File:
		ext := filepath.Ext(w.Name())
		switch ext {
		case "", ".jpg", ".jpeg":
			res, err := Resize(p, img)
			if err != nil {
				return err
			}
			return jpeg.Encode(w, res, &jpeg.Options{Quality: 100})
		case ".png":
			res, err := Resize(p, img)
			if err != nil {
				return err
			}
			return png.Encode(w, res)
		case ".bmp":
			res, err := Resize(p, img)
			if err != nil {
				return err
			}
			return bmp.Encode(w, res)
		default:
			return errors.New("unsupported image format")
		}
	default:
		res, err := Resize(p, img)
		if err != nil {
			return err
		}
		return jpeg.Encode(w, res, &jpeg.Options{Quality: 100})
	}
}

// rotateImage90 rotate the image by 90 degree counter clockwise.
func rotateImage90(src *image.NRGBA) *image.NRGBA {
	b := src.Bounds()
	dst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X))
	for dstY := 0; dstY < b.Max.X; dstY++ {
		for dstX := 0; dstX < b.Max.Y; dstX++ {
			srcX := b.Max.X - dstY - 1
			srcY := dstX

			srcOff := srcY*src.Stride + srcX*4
			dstOff := dstY*dst.Stride + dstX*4
			copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
		}
	}
	return dst
}

// rotateImage270 rotate the image by 270 degree counter clockwise.
func rotateImage270(src *image.NRGBA) *image.NRGBA {
	b := src.Bounds()
	dst := image.NewNRGBA(image.Rect(0, 0, b.Max.Y, b.Max.X))
	for dstY := 0; dstY < b.Max.X; dstY++ {
		for dstX := 0; dstX < b.Max.Y; dstX++ {
			srcX := dstY
			srcY := b.Max.Y - dstX - 1

			srcOff := srcY*src.Stride + srcX*4
			dstOff := dstY*dst.Stride + dstX*4
			copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
		}
	}

	return dst
}

// imgToNRGBA converts any image type to *image.NRGBA with min-point at (0, 0).
func imgToNRGBA(img image.Image) *image.NRGBA {
	srcBounds := img.Bounds()
	if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 {
		if src0, ok := img.(*image.NRGBA); ok {
			return src0
		}
	}
	srcMinX := srcBounds.Min.X
	srcMinY := srcBounds.Min.Y

	dstBounds := srcBounds.Sub(srcBounds.Min)
	dstW := dstBounds.Dx()
	dstH := dstBounds.Dy()
	dst := image.NewNRGBA(dstBounds)

	switch src := img.(type) {
	case *image.NRGBA:
		rowSize := srcBounds.Dx() * 4
		for dstY := 0; dstY < dstH; dstY++ {
			di := dst.PixOffset(0, dstY)
			si := src.PixOffset(srcMinX, srcMinY+dstY)
			for dstX := 0; dstX < dstW; dstX++ {
				copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize])
			}
		}
	case *image.YCbCr:
		for dstY := 0; dstY < dstH; dstY++ {
			di := dst.PixOffset(0, dstY)
			for dstX := 0; dstX < dstW; dstX++ {
				srcX := srcMinX + dstX
				srcY := srcMinY + dstY
				siy := src.YOffset(srcX, srcY)
				sic := src.COffset(srcX, srcY)
				r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic])
				dst.Pix[di+0] = r
				dst.Pix[di+1] = g
				dst.Pix[di+2] = b
				dst.Pix[di+3] = 0xff
				di += 4
			}
		}
	default:
		for dstY := 0; dstY < dstH; dstY++ {
			di := dst.PixOffset(0, dstY)
			for dstX := 0; dstX < dstW; dstX++ {
				c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
				dst.Pix[di+0] = c.R
				dst.Pix[di+1] = c.G
				dst.Pix[di+2] = c.B
				dst.Pix[di+3] = c.A
				di += 4
			}
		}
	}

	return dst
}

// imgToPix converts an image to a pixel array.
func imgToPix(src *image.NRGBA) []uint8 {
	bounds := src.Bounds()
	pixels := make([]uint8, 0, bounds.Max.X*bounds.Max.Y*4)

	for x := bounds.Min.X; x < bounds.Max.X; x++ {
		for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
			r, g, b, _ := src.At(y, x).RGBA()
			pixels = append(pixels, uint8(r>>8), uint8(g>>8), uint8(b>>8), 255)
		}
	}

	return pixels
}

// pixToImage converts an array buffer to an image.
func pixToImage(pixels []uint8, width, height int) image.Image {
	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
	bounds := dst.Bounds()
	dx, dy := bounds.Max.X, bounds.Max.Y
	col := color.NRGBA{
		R: uint8(0),
		G: uint8(0),
		B: uint8(0),
		A: uint8(255),
	}

	for x := bounds.Min.X; x < dx; x++ {
		for y := bounds.Min.Y; y < dy*4; y += 4 {
			col.R = uint8(pixels[y+x*dy*4])
			col.G = uint8(pixels[y+x*dy*4+1])
			col.B = uint8(pixels[y+x*dy*4+2])
			col.A = uint8(pixels[y+x*dy*4+3])

			dst.SetNRGBA(x, int(y/4), col)
		}
	}

	return dst
}

// rgbToGrayscale converts an image to grayscale mode and
// returns the pixel values as an one dimensional array.
func rgbToGrayscale(src *image.NRGBA) []uint8 {
	width, height := src.Bounds().Dx(), src.Bounds().Dy()
	gray := make([]uint8, width*height)

	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			r, g, b, _ := src.At(x, y).RGBA()
			gray[y*width+x] = uint8(
				(0.299*float64(r) +
					0.587*float64(g) +
					0.114*float64(b)) / 256,
			)
		}
	}

	return gray
}

// dither converts an image to black and white image, where the white is fully transparent.
func dither(src *image.NRGBA) *image.NRGBA {
	var (
		bounds   = src.Bounds()
		dithered = image.NewNRGBA(bounds)
		dx       = bounds.Dx()
		dy       = bounds.Dy()
	)

	for x := 0; x < dx; x++ {
		for y := 0; y < dy; y++ {
			r, g, b, _ := src.At(x, y).RGBA()
			threshold := func() color.Color {
				if r > 127 && g > 127 && b > 127 {
					return color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
				}
				return color.NRGBA{A: 0x00}
			}
			dithered.Set(x, y, threshold())
		}
	}

	return dithered
}


================================================
FILE: image_test.go
================================================
package caire

import (
	"image"
	"image/color"
	"image/color/palette"
	"image/draw"
	"os"
	"path/filepath"
	"testing"

	"github.com/esimov/caire/utils"
)

func TestImage_ShouldGetSampleImage(t *testing.T) {
	path := filepath.Join("./testdata", "sample.jpg")
	_, err := os.ReadFile(path)
	if err != nil {
		t.Errorf("Should get the sample image")
	}
}

func TestImage_ImgToNRGBA(t *testing.T) {
	rect := image.Rect(-1, -1, 15, 15)
	colors := palette.Plan9
	testCases := []struct {
		name string
		img  image.Image
	}{
		{
			name: "NRGBA",
			img:  makeNRGBAImage(rect, colors),
		},
		{
			name: "YCbCr-444",
			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio444),
		},
		{
			name: "YCbCr-422",
			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio422),
		},
		{
			name: "YCbCr-420",
			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio420),
		},
		{
			name: "YCbCr-440",
			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio440),
		},
		{
			name: "YCbCr-410",
			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio410),
		},
		{
			name: "YCbCr-411",
			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio411),
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			r := tc.img.Bounds()
			for y := r.Min.Y; y < r.Max.Y; y++ {
				buf := make([]byte, r.Dx()*4)
				scan(tc.img, 0, y-r.Min.Y, r.Dx(), y+1-r.Min.Y, buf)
				wantBuf := readRow(tc.img, y)
				if !compareBytes(buf, wantBuf, 1) {
					t.Errorf("scan horizontal line (y=%d): got %v want %v", y, buf, wantBuf)
				}
			}
			for x := r.Min.X; x < r.Max.X; x++ {
				buf := make([]byte, r.Dy()*4)
				scan(tc.img, x-r.Min.X, 0, x+1-r.Min.X, r.Dy(), buf)
				wantBuf := readColumn(tc.img, x)
				if !compareBytes(buf, wantBuf, 1) {
					t.Errorf("scan vertical line (x=%d): got %v want %v", x, buf, wantBuf)
				}
			}
		})
	}
}

func scan(img image.Image, x1, y1, x2, y2 int, dst []uint8) {
	switch img := img.(type) {
	case *image.NRGBA:
		size := (x2 - x1) * 4
		j := 0
		i := y1*img.Stride + x1*4
		for y := y1; y < y2; y++ {
			copy(dst[j:j+size], img.Pix[i:i+size])
			j += size
			i += img.Stride
		}
	case *image.YCbCr:
		j := 0
		x1 += img.Rect.Min.X
		x2 += img.Rect.Min.X
		y1 += img.Rect.Min.Y
		y2 += img.Rect.Min.Y
		for y := y1; y < y2; y++ {
			iy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X)
			for x := x1; x < x2; x++ {
				var ic int
				switch img.SubsampleRatio {
				case image.YCbCrSubsampleRatio444:
					ic = (y-img.Rect.Min.Y)*img.CStride + (x - img.Rect.Min.X)
				case image.YCbCrSubsampleRatio422:
					ic = (y-img.Rect.Min.Y)*img.CStride + (x/2 - img.Rect.Min.X/2)
				case image.YCbCrSubsampleRatio420:
					ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x/2 - img.Rect.Min.X/2)
				case image.YCbCrSubsampleRatio440:
					ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x - img.Rect.Min.X)
				default:
					ic = img.COffset(x, y)
				}

				yy := int(img.Y[iy])
				cb := int(img.Cb[ic]) - 128
				cr := int(img.Cr[ic]) - 128

				r := (yy<<16 + 91881*cr + 1<<15) >> 16
				if r > 0xff {
					r = 0xff
				} else if r < 0 {
					r = 0
				}

				g := (yy<<16 - 22554*cb - 46802*cr + 1<<15) >> 16
				if g > 0xff {
					g = 0xff
				} else if g < 0 {
					g = 0
				}

				b := (yy<<16 + 116130*cb + 1<<15) >> 16
				if b > 0xff {
					b = 0xff
				} else if b < 0 {
					b = 0
				}

				dst[j+0] = uint8(r)
				dst[j+1] = uint8(g)
				dst[j+2] = uint8(b)
				dst[j+3] = 0xff

				iy++
				j += 4
			}
		}
	}
}

func makeYCbCrImage(rect image.Rectangle, colors []color.Color, sr image.YCbCrSubsampleRatio) *image.YCbCr {
	img := image.NewYCbCr(rect, sr)
	j := 0
	for y := rect.Min.Y; y < rect.Max.Y; y++ {
		for x := rect.Min.X; x < rect.Max.X; x++ {
			iy := img.YOffset(x, y)
			ic := img.COffset(x, y)
			c := color.NRGBAModel.Convert(colors[j]).(color.NRGBA)
			img.Y[iy], img.Cb[ic], img.Cr[ic] = color.RGBToYCbCr(c.R, c.G, c.B)
			j++
		}
	}
	return img
}

func makeNRGBAImage(rect image.Rectangle, colors []color.Color) *image.NRGBA {
	img := image.NewNRGBA(rect)
	fillDrawImage(img, colors)
	return img
}

func fillDrawImage(img draw.Image, colors []color.Color) {
	colorsNRGBA := make([]color.NRGBA, len(colors))
	for i, c := range colors {
		nrgba := color.NRGBAModel.Convert(c).(color.NRGBA)
		nrgba.A = uint8(i % 256)
		colorsNRGBA[i] = nrgba
	}
	rect := img.Bounds()
	i := 0
	for y := rect.Min.Y; y < rect.Max.Y; y++ {
		for x := rect.Min.X; x < rect.Max.X; x++ {
			img.Set(x, y, colorsNRGBA[i])
			i++
		}
	}
}

func readRow(img image.Image, y int) []uint8 {
	row := make([]byte, img.Bounds().Dx()*4)
	i := 0
	for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
		c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
		row[i+0] = c.R
		row[i+1] = c.G
		row[i+2] = c.B
		row[i+3] = c.A
		i += 4
	}
	return row
}

func readColumn(img image.Image, x int) []uint8 {
	column := make([]byte, img.Bounds().Dy()*4)
	i := 0
	for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
		c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
		column[i+0] = c.R
		column[i+1] = c.G
		column[i+2] = c.B
		column[i+3] = c.A
		i += 4
	}
	return column
}

func compareBytes(a, b []uint8, delta int) bool {
	if len(a) != len(b) {
		return false
	}
	for i := 0; i < len(a); i++ {
		if utils.Abs(int(a[i])-int(b[i])) > delta {
			return false
		}
	}
	return true
}


================================================
FILE: imop/blend.go
================================================
// Package imop implements the Porter-Duff composition operations
// used for mixing a graphic element with its backdrop.
// Porter and Duff presented in their paper 12 different composition operation,
// but the image/draw core package implements only the source-over-destination and source.
// This package is aimed to overcome the missing composite operations.

// It is mainly used to debug the seam carving operation correctness
// with face detection and image mask enabled.
// When the GUI mode and the debugging option is activated it will show
// the image mask and the detected faces rectangles in a distinct color.
package imop

import (
	"fmt"
	"math"
	"sort"

	"github.com/esimov/caire/utils"
)

type BlendType int

const (
	Normal BlendType = iota
	Darken
	Lighten
	Multiply
	Screen
	Overlay
	SoftLight
	HardLight
	ColorDodge
	ColorBurn
	Difference
	Exclusion

	// Non-separable blend modes
	Hue
	Saturation
	ColorMode
	Luminosity
)

// Blend struct contains the currently active blend mode and all the supported blend modes.
type Blend struct {
	CurrentOp BlendType
	Modes     []BlendType
}

// Color represents the RGB channel of a specific color.
type Color struct {
	R, G, B float64
}

// NewBlend intantiates a new Blend.
func NewBlend() *Blend {
	return &Blend{
		Modes: []BlendType{
			Normal, Darken, Lighten, Multiply,
			Screen, Overlay, SoftLight, HardLight,
			ColorDodge, ColorBurn, Difference, Exclusion,
			Hue, Saturation, ColorMode, Luminosity,
		},
	}
}

// Set activate one of the supported blend modes.
func (bl *Blend) Set(blendType BlendType) error {
	if utils.Contains(bl.Modes, blendType) {
		bl.CurrentOp = blendType
		return nil
	}

	return fmt.Errorf("unsupported blend mode")
}

// Get returns the active blend mode.
func (bl *Blend) Get() BlendType {
	return bl.CurrentOp
}

// Lum gets the luminosity of a color.
func (bl *Blend) Lum(rgb Color) float64 {
	return 0.3*rgb.R + 0.59*rgb.G + 0.11*rgb.B
}

// SetLum set the luminosity on a color.
func (bl *Blend) SetLum(rgb Color, l float64) Color {
	delta := l - bl.Lum(rgb)
	return bl.clip(Color{
		rgb.R + delta,
		rgb.G + delta,
		rgb.B + delta,
	})
}

// clip clips the channels of a color between certain min and max values.
func (bl *Blend) clip(rgb Color) Color {
	r, g, b := rgb.R, rgb.G, rgb.B

	l := bl.Lum(rgb)
	min := utils.Min(r, g, b)
	max := utils.Max(r, g, b)

	if min < 0 {
		r = l + (((r - l) * l) / (l - min))
		g = l + (((g - l) * l) / (l - min))
		b = l + (((b - l) * l) / (l - min))
	}
	if max > 1 {
		r = l + (((r - l) * (1 - l)) / (max - l))
		g = l + (((g - l) * (1 - l)) / (max - l))
		b = l + (((b - l) * (1 - l)) / (max - l))
	}

	return Color{R: r, G: g, B: b}
}

// Sat gets the saturation of a color.
func (bl *Blend) Sat(rgb Color) float64 {
	return utils.Max(rgb.R, rgb.G, rgb.B) - utils.Min(rgb.R, rgb.G, rgb.B)
}

// channel is a key/value struct pair used for sorting the color channels
// based on the color components having the minimum, middle, and maximum
// values upon entry to the function.
// The key component holds the channel name and val is the value it has.
type channel struct {
	key string
	val float64
}

func (bl *Blend) SetSat(rgb Color, s float64) Color {
	color := map[string]float64{
		"R": rgb.R,
		"G": rgb.G,
		"B": rgb.B,
	}
	channels := make([]channel, 0, 3)
	for k, v := range color {
		channels = append(channels, channel{k, v})
	}
	// Sort the color channels based on their values.
	sort.Slice(channels, func(i, j int) bool { return channels[i].val < channels[j].val })
	minChan, midChan, maxChan := channels[0].key, channels[1].key, channels[2].key

	if color[maxChan] > color[minChan] {
		color[midChan] = (((color[midChan] - color[minChan]) * s) / (color[maxChan] - color[minChan]))
		color[maxChan] = s
	} else {
		color[midChan], color[maxChan] = 0, 0
	}
	color[minChan] = 0

	return Color{
		R: color["R"],
		G: color["G"],
		B: color["B"],
	}
}

// Applies the alpha blending formula for a blend operation.
// See: https://www.w3.org/TR/compositing-1/#blending
func (bl *Blend) AlphaCompose(
	backdropAlpha,
	sourceAlpha,
	compositeAlpha,
	backdropColor,
	sourceColor,
	compositeColor float64,
) float64 {
	return ((1 - sourceAlpha/compositeAlpha) * backdropColor) +
		(sourceAlpha / compositeAlpha *
			math.Round((1-backdropAlpha)*sourceColor+backdropAlpha*compositeColor))
}


================================================
FILE: imop/blend_test.go
================================================
package imop

import (
	"image"
	"image/color"
	"image/draw"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestBlend_Basic(t *testing.T) {
	assert := assert.New(t)

	op := NewBlend()
	assert.Empty(op.Get())
	op.Set(Darken)
	assert.Equal(Darken, op.Get())
	op.Set(Lighten)
	assert.Equal(Lighten, op.Get())

	rgb := Color{R: 0xff, G: 0xff, B: 0xff}
	lum := op.Lum(rgb)
	assert.Equal(255.0, lum)

	rgb = Color{R: 0, G: 0, B: 0}
	lum = op.Lum(rgb)
	assert.Equal(0.0, lum)

	rgb = Color{R: 127, G: 127, B: 127}
	lum = op.Lum(rgb)
	assert.Equal(127.0, lum)

	foreground := Color{R: 0xff, G: 0xff, B: 0xff}
	background := Color{R: 0, G: 0, B: 0}

	assert.Equal(0.0, op.Sat(foreground))
	sat := op.SetSat(background, op.Sat(foreground))
	assert.Equal(Color{R: 0, G: 0, B: 0}, sat)
}

func TestBlend_Modes(t *testing.T) {
	// Note: all the expected values are taken by using as reference the results
	// obtained in Photoshop by overlapping two layers and applying the blend mode.
	assert := assert.New(t)

	op := InitOp()
	blend := NewBlend()

	pinkFront := color.RGBA{R: 214, G: 20, B: 65, A: 255}
	orangeBack := color.RGBA{R: 250, G: 121, B: 17, A: 255}

	rect := image.Rect(0, 0, 1, 1)
	bmp := NewBitmap(rect)
	source := image.NewNRGBA(rect)
	backdrop := image.NewNRGBA(rect)

	op.Set(SrcOver)

	// Darken
	blend.Set(Darken)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected := []uint8{214, 20, 17, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// Multiply
	blend.Set(Multiply)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{209, 9, 4, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// Screen
	blend.Set(Screen)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{254, 131, 77, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// Overlay
	blend.Set(Overlay)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{253, 18, 8, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// SoftLight
	blend.Set(SoftLight)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{232, 19, 23, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// HardLight
	blend.Set(HardLight)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{251, 67, 9, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// ColorDodge
	blend.Set(ColorDodge)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{255, 131, 22, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// ColorBurn
	blend.Set(ColorBurn)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{249, 0, 0, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// Difference
	blend.Set(Difference)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{35, 101, 48, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// Exclusion
	blend.Set(Exclusion)
	draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{44, 122, 73, 255}
	assert.EqualValues(expected, bmp.Img.Pix)
}

func TestBlend_NonSeparableModes(t *testing.T) {
	assert := assert.New(t)

	op := InitOp()
	blend := NewBlend()

	frontColor := color.RGBA{R: 250, G: 121, B: 17, A: 255}
	backColor := color.RGBA{R: 214, G: 20, B: 65, A: 255}

	rect := image.Rect(0, 0, 1, 1)
	bmp := NewBitmap(rect)
	source := image.NewNRGBA(rect)
	backdrop := image.NewNRGBA(rect)

	op.Set(SrcOver)

	// Hue
	blend.Set(Hue)
	draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected := []uint8{255, 97, 133, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// Saturation
	blend.Set(Saturation)
	draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{233, 126, 39, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// Color
	blend.Set(ColorMode)
	draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{255, 97, 133, 255}
	assert.EqualValues(expected, bmp.Img.Pix)

	// Luminosity
	blend.Set(Luminosity)
	draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src)
	draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, blend)

	expected = []uint8{148, 66, 0, 255}
	assert.EqualValues(expected, bmp.Img.Pix)
}


================================================
FILE: imop/comp.go
================================================
// Package imop implements the Porter-Duff composition operations
// used for mixing a graphic element with its backdrop.
// Porter and Duff presented in their paper 12 different composition operation, but the
// core image/draw core package implements only the source-over-destination and source.
// This package implements all of the 12 composite operation together with some blending modes.
package imop

import (
	"fmt"
	"image"
	"image/color"
	"math"

	"github.com/esimov/caire/utils"
)

type CompType int

const (
	Clear CompType = iota
	Copy
	Dst
	SrcOver
	DstOver
	SrcIn
	DstIn
	SrcOut
	DstOut
	SrcAtop
	DstAtop
	Xor
)

// Bitmap holds an image type as a placeholder for the Porter-Duff composition
// operations which can be used as a source or destination image.
type Bitmap struct {
	Img *image.NRGBA
}

// Composite struct contains the currently active composition operation and all the supported operations.
type Composite struct {
	CurrentOp CompType
	Ops       []CompType
}

// NewBitmap initializes a new Bitmap.
func NewBitmap(rect image.Rectangle) *Bitmap {
	return &Bitmap{
		Img: image.NewNRGBA(rect),
	}
}

// InitOp initializes a new composition operation.
func InitOp() *Composite {
	return &Composite{
		CurrentOp: SrcOver,
		Ops: []CompType{
			Clear,
			Copy,
			Dst,
			SrcOver,
			DstOver,
			SrcIn,
			DstIn,
			SrcOut,
			DstOut,
			SrcAtop,
			DstAtop,
			Xor,
		},
	}
}

// Set changes the current composition operation.
func (op *Composite) Set(compType CompType) error {
	if utils.Contains(op.Ops, compType) {
		op.CurrentOp = compType
		return nil
	}

	return fmt.Errorf("unsupported composition operation")
}

// Set changes the current composition operation.
func (op *Composite) Get() CompType {
	return op.CurrentOp
}

// Draw applies the currently active Ported-Duff composition operation formula,
// taking as parameter the source and the destination image and draws the result into the bitmap.
// If a blend mode is activated it will plug in the alpha blending formula also into the equation.
func (op *Composite) Draw(bitmap *Bitmap, src, dst *image.NRGBA, blend *Blend) {
	dx, dy := src.Bounds().Dx(), src.Bounds().Dy()

	var (
		r, g, b, a     uint32
		rn, gn, bn, an float64
	)

	for x := 0; x < dx; x++ {
		for y := 0; y < dy; y++ {
			r1, g1, b1, a1 := src.At(x, y).RGBA()
			r2, g2, b2, a2 := dst.At(x, y).RGBA()

			rs, gs, bs, as := r1>>8, g1>>8, b1>>8, a1>>8
			rb, gb, bb, ab := r2>>8, g2>>8, b2>>8, a2>>8

			// normalize the values.
			rsn := float64(rs) / 255
			gsn := float64(gs) / 255
			bsn := float64(bs) / 255
			asn := float64(as) / 255

			rbn := float64(rb) / 255
			gbn := float64(gb) / 255
			bbn := float64(bb) / 255
			abn := float64(ab) / 255

			// applying the alpha composition formula
			switch op.CurrentOp {
			case Clear:
				rn, gn, bn, an = 0, 0, 0, 0
			case Copy:
				rn = asn * rsn
				gn = asn * gsn
				bn = asn * bsn
				an = asn * asn
			case Dst:
				rn = abn * rbn
				gn = abn * gbn
				bn = abn * bbn
				an = abn * abn
			case SrcOver:
				rn = asn*rsn + abn*rbn*(1-asn)
				gn = asn*gsn + abn*gbn*(1-asn)
				bn = asn*bsn + abn*bbn*(1-asn)
				an = asn + abn*(1-asn)
			case DstOver:
				rn = asn*rsn*(1-abn) + abn*rbn
				gn = asn*gsn*(1-abn) + abn*gbn
				bn = asn*bsn*(1-abn) + abn*bbn
				an = asn*(1-abn) + abn
			case SrcIn:
				rn = asn * rsn * abn
				gn = asn * gsn * abn
				bn = asn * bsn * abn
				an = asn * abn
			case DstIn:
				rn = abn * rbn * asn
				gn = abn * gbn * asn
				bn = abn * bbn * asn
				an = abn * asn
			case SrcOut:
				rn = asn * rsn * (1 - abn)
				gn = asn * gsn * (1 - abn)
				bn = asn * bsn * (1 - abn)
				an = asn * (1 - abn)
			case DstOut:
				rn = abn * rbn * (1 - asn)
				gn = abn * gbn * (1 - asn)
				bn = abn * bbn * (1 - asn)
				an = abn * (1 - asn)
			case SrcAtop:
				rn = asn*rsn*abn + (1-asn)*abn*rbn
				gn = asn*gsn*abn + (1-asn)*abn*gbn
				bn = asn*bsn*abn + (1-asn)*abn*bbn
				an = asn*abn + abn*(1-asn)
			case DstAtop:
				rn = asn*rsn*(1-abn) + abn*rbn*asn
				gn = asn*gsn*(1-abn) + abn*gbn*asn
				bn = asn*bsn*(1-abn) + abn*bbn*asn
				an = asn*(1-abn) + abn*asn
			case Xor:
				rn = asn*rsn*(1-abn) + abn*rbn*(1-asn)
				gn = asn*gsn*(1-abn) + abn*gbn*(1-asn)
				bn = asn*bsn*(1-abn) + abn*bbn*(1-asn)
				an = asn*(1-abn) + abn*(1-asn)
			}

			r = uint32(rn * 255)
			g = uint32(gn * 255)
			b = uint32(bn * 255)
			a = uint32(an * 255)

			bitmap.Img.Set(x, y, color.NRGBA{
				R: uint8(r),
				G: uint8(g),
				B: uint8(b),
				A: uint8(a),
			})

			// applying the blending mode
			if blend != nil {
				rn, gn, bn, an = 0, 0, 0, 0 // reset the colors
				r1, g1, b1, a1 = src.At(x, y).RGBA()
				r2, g2, b2, a2 = dst.At(x, y).RGBA()

				rs, gs, bs, as = r1>>8, g1>>8, b1>>8, a1>>8
				rb, gb, bb, ab = r2>>8, g2>>8, b2>>8, a2>>8

				rsn = float64(rs) / 255
				gsn = float64(gs) / 255
				bsn = float64(bs) / 255
				asn = float64(as) / 255

				rbn = float64(rb) / 255
				gbn = float64(gb) / 255
				bbn = float64(bb) / 255
				abn = float64(ab) / 255

				foreground := Color{R: rsn, G: gsn, B: bsn}
				background := Color{R: rbn, G: gbn, B: bbn}

				switch blend.CurrentOp {
				case Normal:
					rn, gn, bn, an = rsn, gsn, bsn, asn
				case Darken:
					rn = utils.Min(rsn, rbn)
					gn = utils.Min(gsn, gbn)
					bn = utils.Min(bsn, bbn)
					an = utils.Min(asn, abn)
				case Lighten:
					rn = utils.Max(rsn, rbn)
					gn = utils.Max(gsn, gbn)
					bn = utils.Max(bsn, bbn)
					an = utils.Max(asn, abn)
				case Screen:
					rn = 1 - (1-rsn)*(1-rbn)
					gn = 1 - (1-gsn)*(1-gbn)
					bn = 1 - (1-bsn)*(1-bbn)
					an = 1 - (1-asn)*(1-abn)
				case Multiply:
					rn = rsn * rbn
					gn = gsn * gbn
					bn = bsn * bbn
					an = asn * abn
				case Overlay:
					if rsn <= 0.5 {
						rn = 2 * rsn * rbn
					} else {
						rn = 1 - 2*(1-rsn)*(1-rbn)
					}

					if gsn <= 0.5 {
						gn = 2 * gsn * gbn
					} else {
						gn = 1 - 2*(1-gsn)*(1-gbn)
					}

					if bsn <= 0.5 {
						bn = 2 * bsn * bbn
					} else {
						bn = 1 - 2*(1-bsn)*(1-bbn)
					}

					if asn <= 0.5 {
						an = 2 * asn * abn
					} else {
						an = 1 - 2*(1-asn)*(1-abn)
					}
				case SoftLight:
					if rbn < 0.5 {
						rn = rsn - (1-2*rbn)*rsn*(1-rsn)
					} else {
						var w3r float64
						if rsn < 0.25 {
							w3r = ((16*rsn-12)*rsn + 4) * rsn
						} else {
							w3r = math.Sqrt(rsn)
						}
						rn = rsn + (2*rbn-1)*(w3r-rsn)
					}

					if gbn < 0.5 {
						gn = gsn - (1-2*gbn)*gsn*(1-gsn)
					} else {
						var w3g float64
						if gsn < 0.25 {
							w3g = ((16*gsn-12)*gsn + 4) * gsn
						} else {
							w3g = math.Sqrt(gsn)
						}
						gn = gsn + (2*gbn-1)*(w3g-gsn)
					}

					if bbn < 0.5 {
						bn = bsn - (1-2*bbn)*bsn*(1-bsn)
					} else {
						var w3b float64
						if bsn < 0.25 {
							w3b = ((16*bsn-12)*bsn + 4) * bsn
						} else {
							w3b = math.Sqrt(bsn)
						}
						bn = bsn + (2*bbn-1)*(w3b-bsn)
					}

					if abn < 0.5 {
						an = asn - (1-2*abn)*asn*(1-asn)
					} else {
						var w3a float64
						if asn < 0.25 {
							w3a = ((16*asn-12)*asn + 4) * asn
						} else {
							w3a = math.Sqrt(asn)
						}
						an = asn + (2*abn-1)*(w3a-asn)
					}
				case HardLight:
					if rbn < 0.5 {
						rn = rbn - (1-2*rsn)*rbn*(1-rbn)
					} else {
						var w3r float64
						if rbn < 0.25 {
							w3r = ((16*rbn-12)*rbn + 4) * rbn
						} else {
							w3r = math.Sqrt(rbn)
						}
						rn = rbn + (2*rsn-1)*(w3r-rbn)
					}

					if gbn < 0.5 {
						gn = gbn - (1-2*gsn)*gbn*(1-gbn)
					} else {
						var w3g float64
						if gbn < 0.25 {
							w3g = ((16*gbn-12)*gbn + 4) * gbn
						} else {
							w3g = math.Sqrt(gbn)
						}
						gn = gbn + (2*gsn-1)*(w3g-gbn)
					}

					if bbn < 0.5 {
						bn = bbn - (1-2*bsn)*bbn*(1-bbn)
					} else {
						var w3b float64
						if bbn < 0.25 {
							w3b = ((16*bbn-12)*bbn + 4) * bbn
						} else {
							w3b = math.Sqrt(bbn)
						}
						bn = bbn + (2*bsn-1)*(w3b-bbn)
					}

					if abn < 0.5 {
						an = abn - (1-2*asn)*abn*(1-abn)
					} else {
						var w3a float64
						if abn < 0.25 {
							w3a = ((16*abn-12)*abn + 4) * abn
						} else {
							w3a = math.Sqrt(abn)
						}
						an = abn + (2*asn-1)*(w3a-abn)
					}
				case ColorDodge:
					if rsn < 1 {
						rn = utils.Min(1, rbn/(1-rsn))
					} else if rsn == 1 {
						rn = 1
					}

					if gsn < 1 {
						gn = utils.Min(1, gbn/(1-gsn))
					} else if gsn == 1 {
						gn = 1
					}

					if bsn < 1 {
						bn = utils.Min(1, bbn/(1-bsn))
					} else if bsn == 1 {
						bn = 1
					}

					if asn < 1 {
						an = utils.Min(1, abn/(1-asn))
					} else if asn == 1 {
						an = 1
					}
				case ColorBurn:
					if rsn > 0 {
						rn = 1 - utils.Min(1, (1-rbn)/rsn)
					} else if rsn == 0 {
						rn = 0
					}

					if gsn > 0 {
						gn = 1 - utils.Min(1, (1-gbn)/gsn)
					} else if gsn == 0 {
						gn = 0
					}

					if bsn > 0 {
						bn = 1 - utils.Min(1, (1-bbn)/bsn)
					} else if bsn == 0 {
						bn = 0
					}

					if asn > 0 {
						an = 1 - utils.Min(1, (1-abn)/asn)
					} else if asn == 0 {
						an = 0
					}
				case Difference:
					rn = utils.Abs(rbn - rsn)
					gn = utils.Abs(gbn - gsn)
					bn = utils.Abs(bbn - bsn)
					an = 1
				case Exclusion:
					rn = rsn + rbn - 2*rsn*rbn
					gn = gsn + gbn - 2*gsn*gbn
					bn = bsn + bbn - 2*bsn*bbn
					an = 1

				// Non-separable blend modes
				// https://www.w3.org/TR/compositing-1/#blendingnonseparable
				case Hue:
					sat := blend.SetSat(background, blend.Sat(foreground))
					rgb := blend.SetLum(sat, blend.Lum(foreground))

					a := asn + abn - asn*abn
					rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
					gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
					bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
					rn, gn, bn = rn/255, gn/255, bn/255
					an = a
				case Saturation:
					sat := blend.SetSat(foreground, blend.Sat(background))
					rgb := blend.SetLum(sat, blend.Lum(foreground))

					a := asn + abn - asn*abn
					rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
					gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
					bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
					rn, gn, bn = rn/255, gn/255, bn/255
					an = a
				case ColorMode:
					rgb := blend.SetLum(background, blend.Lum(foreground))

					a := asn + abn - asn*abn
					rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
					gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
					bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
					rn, gn, bn = rn/255, gn/255, bn/255
					an = a
				case Luminosity:
					rgb := blend.SetLum(foreground, blend.Lum(background))

					a := asn + abn - asn*abn
					rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
					gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
					bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
					rn, gn, bn = rn/255, gn/255, bn/255
					an = a
				}

				r = uint32(rn * 255)
				g = uint32(gn * 255)
				b = uint32(bn * 255)
				a = uint32(an * 255)

				bitmap.Img.Set(x, y, color.NRGBA{
					R: uint8(r),
					G: uint8(g),
					B: uint8(b),
					A: uint8(a),
				})
			}
		}
	}
}


================================================
FILE: imop/comp_test.go
================================================
package imop

import (
	"image"
	"image/color"
	"image/draw"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestComp_Basic(t *testing.T) {
	assert := assert.New(t)

	op := InitOp()

	op.Set(Clear)
	assert.Equal(Clear, op.Get())
	assert.NotEqual("unsupported_composite_operation", op.Get())

	op.Set(Dst)
	assert.Equal(Dst, op.Get())
}

func TestComp_Ops(t *testing.T) {
	assert := assert.New(t)
	op := InitOp()

	transparent := color.NRGBA{R: 0, G: 0, B: 0, A: 0}
	cyan := color.NRGBA{R: 33, G: 150, B: 243, A: 255}
	magenta := color.NRGBA{R: 233, G: 30, B: 99, A: 255}

	rect := image.Rect(0, 0, 10, 10)
	bmp := NewBitmap(rect)
	source := image.NewNRGBA(rect)
	backdrop := image.NewNRGBA(rect)

	// No composition operation applied. The SrcOver is the default one.
	draw.Draw(source, image.Rect(0, 4, 6, 10), &image.Uniform{cyan}, image.Point{}, draw.Src)
	draw.Draw(backdrop, image.Rect(4, 0, 10, 6), &image.Uniform{magenta}, image.Point{}, draw.Src)
	op.Draw(bmp, source, backdrop, nil)

	// Pick three representative points/pixels from the generated image output.
	// Depending on the applied composition operation the colors of the
	// selected pixels should be the source color, the destination color or transparent.
	topRight := bmp.Img.At(9, 0)
	bottomLeft := bmp.Img.At(0, 9)
	center := bmp.Img.At(5, 5)

	assert.EqualValues(topRight, magenta)
	assert.EqualValues(bottomLeft, cyan)
	assert.EqualValues(center, cyan)

	// Clear
	op.Set(Clear)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, transparent)
	assert.EqualValues(bottomLeft, transparent)
	assert.EqualValues(center, transparent)

	// Copy
	op.Set(Copy)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, transparent)
	assert.EqualValues(bottomLeft, cyan)
	assert.EqualValues(center, cyan)

	// Dst
	op.Set(Dst)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, magenta)
	assert.EqualValues(bottomLeft, transparent)
	assert.EqualValues(center, magenta)

	// SrcOver
	op.Set(SrcOver)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, magenta)
	assert.EqualValues(bottomLeft, cyan)
	assert.EqualValues(center, cyan)

	// DstOver
	op.Set(DstOver)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, magenta)
	assert.EqualValues(bottomLeft, cyan)
	assert.EqualValues(center, magenta)

	// SrcIn
	op.Set(SrcIn)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, transparent)
	assert.EqualValues(bottomLeft, transparent)
	assert.EqualValues(center, cyan)

	// DstIn
	op.Set(DstIn)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, transparent)
	assert.EqualValues(bottomLeft, transparent)
	assert.EqualValues(center, magenta)

	// SrcOut
	op.Set(SrcOut)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, transparent)
	assert.EqualValues(bottomLeft, cyan)
	assert.EqualValues(center, transparent)

	// DstOut
	op.Set(DstOut)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, magenta)
	assert.EqualValues(bottomLeft, transparent)
	assert.EqualValues(center, transparent)

	// SrcAtop
	op.Set(SrcAtop)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, magenta)
	assert.EqualValues(bottomLeft, transparent)
	assert.EqualValues(center, cyan)

	// DstAtop
	op.Set(DstAtop)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, transparent)
	assert.EqualValues(bottomLeft, cyan)
	assert.EqualValues(center, magenta)

	// Xor
	op.Set(Xor)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, magenta)
	assert.EqualValues(bottomLeft, cyan)
	assert.EqualValues(center, transparent)
	// DstAtop
	op.Set(DstAtop)
	op.Draw(bmp, source, backdrop, nil)

	topRight = bmp.Img.At(9, 0)
	bottomLeft = bmp.Img.At(0, 9)
	center = bmp.Img.At(5, 5)

	assert.EqualValues(topRight, transparent)
	assert.EqualValues(bottomLeft, cyan)
	assert.EqualValues(center, magenta)
}


================================================
FILE: preview.go
================================================
package caire

import (
	"os"
)

// showPreview spawns a new Gio GUI window and updates its content with the resized image received from a channel.
func (p *Processor) showPreview(
	imgWorker <-chan worker,
	errChan chan<- error,
	guiWindow struct {
		width  int
		height int
	},
) {
	var gui = NewGUI(guiWindow.width, guiWindow.height)
	gui.proc = p
	gui.process.worker = imgWorker

	// Run the Gio GUI app in a separate goroutine
	go func() {
		if err := gui.Run(); err != nil {
			errChan <- err
		}
		// It's important to call os.Exit(0) in order to terminate
		// the execution of the GUI app when pressing ESC key.
		os.Exit(0)
	}()
}


================================================
FILE: processor.go
================================================
package caire

import (
	_ "embed"
	"errors"
	"fmt"
	"image"
	"image/draw"
	"io"
	"math"

	"github.com/disintegration/imaging"
	"github.com/esimov/caire/utils"
	pigo "github.com/esimov/pigo/core"
)

//go:embed data/facefinder
var cascadeFile []byte

var (
	resizeXY = false // the image is resized both vertically and horizontally

	imgWorker = make(chan worker) // channel used to transfer the image to the GUI
	errs      = make(chan error)
)

var _ SeamCarver = (*Processor)(nil)

// worker struct contains all the information needed for transferring the resized image to the Gio GUI.
type worker struct {
	img   *image.NRGBA
	mask  *image.NRGBA
	seams []Seam
	done  bool
}

// shrinkFn is a generic function used to shrink an image.
type shrinkFn func(*image.NRGBA) (*image.NRGBA, error)

// enlargeFn is a generic function used to enlarge an image.
type enlargeFn func(*image.NRGBA) (*image.NRGBA, error)

// Processor options
type Processor struct {
	FaceAngle      float64
	SeamColor      string
	MaskPath       string
	RMaskPath      string
	ShapeType      ShapeType
	SobelThreshold int
	BlurRadius     int
	NewWidth       int
	NewHeight      int
	FaceDetector   *pigo.Pigo
	Spinner        *utils.Spinner
	Mask           *image.NRGBA
	RMask          *image.NRGBA
	DebugMask      *image.NRGBA
	Percentage     bool
	Square         bool
	Debug          bool
	Preview        bool
	FaceDetect     bool
	vRes           bool
}

var (
	shrinkHorizFn  shrinkFn
	shrinkVertFn   shrinkFn
	enlargeHorizFn enlargeFn
	enlargeVertFn  enlargeFn
)

// Carve is the main entry point for the image resize operation.
// The new image can be resized either horizontally or vertically (or both).
// Depending on the provided options the image can be either reduced or enlarged.
func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
	var (
		newImg    image.Image
		newWidth  int
		newHeight int
		pw, ph    int
		err       error
	)
	c := NewCarver(img.Bounds().Dx(), img.Bounds().Dy())

	if p.NewWidth > c.Width {
		newWidth = p.NewWidth - (p.NewWidth - (p.NewWidth - c.Width))
	} else {
		newWidth = c.Width - (c.Width - (c.Width - p.NewWidth))
	}

	if p.NewHeight > c.Height {
		newHeight = p.NewHeight - (p.NewHeight - (p.NewHeight - c.Height))
	} else {
		newHeight = c.Height - (c.Height - (c.Height - p.NewHeight))
	}

	if p.NewWidth == 0 {
		newWidth = p.NewWidth
	}
	if p.NewHeight == 0 {
		newHeight = p.NewHeight
	}

	// shrinkHorizFn calls itself recursively to shrink the image horizontally.
	// If the image is resized on both X and Y axis it calls the shrink and enlarge
	// function intermittently up until the desired dimension is reached.
	// I opted for this solution instead of resizing the image sequentially,
	// to avoid resulting visual artefacts, since this way
	// the horizontal and vertical seams are merged together seamlessly.
	shrinkHorizFn = func(img *image.NRGBA) (*image.NRGBA, error) {
		p.vRes = false
		dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
		if dx > p.NewWidth {
			img, err = p.shrink(img)
			if err != nil {
				return nil, err
			}
			if p.NewHeight > 0 && p.NewHeight != dy {
				if p.NewHeight <= dy {
					img, err = shrinkVertFn(img)
					if err != nil {
						return nil, err
					}
				} else {
					img, err = enlargeVertFn(img)
					if err != nil {
						return nil, err
					}
				}
			} else {
				img, err = shrinkHorizFn(img)
				if err != nil {
					return nil, err
				}
			}
		}
		return img, nil
	}

	// enlargeHorizFn calls itself recursively to enlarge the image horizontally.
	enlargeHorizFn = func(img *image.NRGBA) (*image.NRGBA, error) {
		p.vRes = false
		dx, dy := img.Bounds().Dx(), img.Bounds().Dy()
		if dx < p.NewWidth {
			img, err = p.enlarge(img)
			if err != nil {
				return nil, err
			}
			if p.NewHeight > 0 && p.NewHeight != dy {
				if p.NewHeight <= dy {
					img, err = shrinkVertFn(img)
					if err != nil {
						return nil, err
					}
				} else {
					img, err = enlargeVertFn(img)
					if err != nil {
						return nil, err
					}
				}
			} else {
				img, err = enlargeHorizFn(img)
				if err != nil {
					return nil, err
				}
			}
		}
		return img, nil
	}

	// shrinkVertFn calls itself recursively to shrink the image vertically.
	shrinkVertFn = func(img *image.NRGBA) (*image.NRGBA, error) {
		p.vRes = true
		dx, dy := img.Bounds().Dx(), img.Bounds().Dy()

		// If the image is resized both horizontally and vertically we need
		// to rotate the image each time when invoking the shrink function.
		// Otherwise we rotate the image only once, right before calling this function.
		if resizeXY {
			dx, dy = img.Bounds().Dy(), img.Bounds().Dx()
			img = rotateImage90(img)
		}
		if dx > p.NewHeight {
			img, err = p.shrink(img)
			if err != nil {
				return nil, err
			}
			if resizeXY {
				img = rotateImage270(img)
			}
			if p.NewWidth > 0 && p.NewWidth != dy {
				if p.NewWidth <= dy {
					img, err = shrinkHorizFn(img)
					if err != nil {
						return nil, err
					}
				} else {
					img, err = enlargeHorizFn(img)
					if err != nil {
						return nil, err
					}
				}
			} else {
				img, err = shrinkVertFn(img)
				if err != nil {
					return nil, err
				}
			}
		} else {
			if resizeXY {
				img = rotateImage270(img)
			}
		}
		return img, nil
	}

	// enlargeVertFn calls itself recursively to enlarge the image vertically.
	enlargeVertFn = func(img *image.NRGBA) (*image.NRGBA, error) {
		p.vRes = true
		dx, dy := img.Bounds().Dx(), img.Bounds().Dy()

		if resizeXY {
			dx, dy = img.Bounds().Dy(), img.Bounds().Dx()
			img = rotateImage90(img)
		}
		if dx < p.NewHeight {
			img, err = p.enlarge(img)
			if err != nil {
				return nil, err
			}
			if resizeXY {
				img = rotateImage270(img)
			}
			if p.NewWidth > 0 && p.NewWidth != dy {
				if p.NewWidth <= dy {
					img, err = shrinkHorizFn(img)
					if err != nil {
						return nil, err
					}
				} else {
					img, err = enlargeHorizFn(img)
					if err != nil {
						return nil, err
					}
				}
			} else {
				img, err = enlargeVertFn(img)
				if err != nil {
					return nil, err
				}
			}
		} else {
			if resizeXY {
				img = rotateImage270(img)
			}
		}
		return img, nil
	}

	if p.Percentage || p.Square {
		pw = c.Width - c.Height
		ph = c.Height - c.Width

		// In case pw and ph is zero, it means that the target image is square.
		// In this case we can simply resize the image without running the carving operation.
		if p.Percentage && pw == 0 && ph == 0 {
			pw = c.Width - int(float64(c.Width)-(float64(p.NewWidth)/100*float64(c.Width)))
			ph = c.Height - int(float64(c.Height)-(float64(p.NewHeight)/100*float64(c.Height)))

			p.NewWidth = utils.Abs(c.Width - pw)
			p.NewHeight = utils.Abs(c.Height - ph)

			resImgSize := utils.Min(p.NewWidth, p.NewHeight)
			return imaging.Resize(img, resImgSize, 0, imaging.Lanczos), nil
		}

		// When the square option is used the image will be resized to a square based on the shortest edge.
		if p.Square {
			// Calling the image rescale method only when both a new width and height is provided.
			if p.NewWidth != 0 && p.NewHeight != 0 {
				p.NewWidth = utils.Min(p.NewWidth, p.NewHeight)
				p.NewHeight = p.NewWidth

				newImg = p.calculateFitness(img, c)
				dst := image.NewNRGBA(newImg.Bounds())
				draw.Draw(dst, newImg.Bounds(), newImg, image.Point{}, draw.Src)
				img = dst

				nw, nh := img.Bounds().Dx(), img.Bounds().Dy()

				p.NewWidth = utils.Min(nw, nh)
				p.NewHeight = p.NewWidth
			} else {
				return nil, errors.New("please provide a new WIDTH and HEIGHT when using the square option")
			}
		}

		// Use the Percentage flag only for shrinking the image.
		if p.Percentage {
			// Calculate the new image size based on the provided percentage.
			pw = c.Width - int(float64(c.Width)-(float64(p.NewWidth)/100*float64(c.Width)))
			ph = c.Height - int(float64(c.Height)-(float64(p.NewHeight)/100*float64(c.Height)))

			if p.NewWidth != 0 {
				p.NewWidth = utils.Abs(c.Width - pw)
			}
			if p.NewHeight != 0 {
				p.NewHeight = utils.Abs(c.Height - ph)
			}
			if pw >= c.Width || ph >= c.Height {
				return nil, errors.New("cannot use the percentage flag for image enlargement")
			}
		}
	}

	// Rescale the image when it is resized both horizontally and vertically.
	// First the image is scaled down or up by preserving the image aspect ratio,
	// then the seam carving algorithm is applied only to the remaining pixels.

	// Scale the width and height by the smaller factor (i.e Min(wScaleFactor, hScaleFactor))
	// Example: input: 5000x2500, scale: 2160x1080, final target: 1920x1080
	if (c.Width > p.NewWidth && c.Height > p.NewHeight) &&
		(p.NewWidth != 0 && p.NewHeight != 0) {

		newImg = p.calculateFitness(img, c)

		dx0, dy0 := img.Bounds().Max.X, img.Bounds().Max.Y
		dx1, dy1 := newImg.Bounds().Max.X, newImg.Bounds().Max.Y

		// Rescale the image when the new image width or height are preserved, otherwise
		// it might happen, that the generated image size does not match with the requested image size.
		if !((p.NewWidth == 0 && dx0 == dx1) || (p.NewHeight == 0 && dy0 == dy1)) {
			dst := image.NewNRGBA(newImg.Bounds())
			draw.Draw(dst, newImg.Bounds(), newImg, image.Point{}, draw.Src)
			img = dst
		}
	}

	// Run the carver function if the desired image width is not identical with the rescaled image width.
	if newWidth > 0 && p.NewWidth != c.Width {
		if p.NewWidth > c.Width {
			img, err = enlargeHorizFn(img)
			if err != nil {
				return nil, err
			}
		} else {
			img, err = shrinkHorizFn(img)
			if err != nil {
				return nil, err
			}
		}
	}

	// Run the carver function if the desired image height is not identical with the rescaled image height.
	if newHeight > 0 && p.NewHeight != c.Height {
		if !resizeXY {
			img = rotateImage90(img)

			if p.Mask != nil {
				p.Mask = rotateImage90(p.Mask)
			}
			if p.RMask != nil {
				p.RMask = rotateImage90(p.RMask)
			}
		}
		if p.NewHeight > c.Height {
			img, err = enlargeVertFn(img)
			if err != nil {
				return nil, err
			}
		} else {
			img, err = shrinkVertFn(img)
			if err != nil {
				return nil, err
			}
		}
		if !resizeXY {
			img = rotateImage270(img)

			if p.Mask != nil {
				p.Mask = rotateImage270(p.Mask)
			}
			if p.RMask != nil {
				p.RMask = rotateImage270(p.RMask)
			}
		}
	}

	// Signal that the process is done and no more data is sent through the channel.
	go func() {
		imgWorker <- worker{
			img:   nil,
			mask:  nil,
			seams: nil,
			done:  true,
		}
	}()

	return img, nil
}

// calculateFitness iteratively try to find the best image aspect ratio for the rescale.
func (p *Processor) calculateFitness(img *image.NRGBA, c *Carver) *image.NRGBA {
	var (
		w      = float64(c.Width)
		h      = float64(c.Height)
		nw     = float64(p.NewWidth)
		nh     = float64(p.NewHeight)
		newImg *image.NRGBA
	)
	wsf := w / nw
	hsf := h / nh
	sw := math.Round(w / math.Min(wsf, hsf))
	sh := math.Round(h / math.Min(wsf, hsf))

	if sw <= sh {
		newImg = imaging.Resize(img, 0, int(sw), imaging.Lanczos)
		if p.Mask != nil {
			p.Mask = imaging.Resize(p.Mask, 0, int(sw), imaging.Lanczos)
		}
		if p.RMask != nil {
			p.RMask = imaging.Resize(p.RMask, 0, int(sw), imaging.Lanczos)
		}
	} else {
		newImg = imaging.Resize(img, 0, int(sh), imaging.Lanczos)
		if p.Mask != nil {
			p.Mask = imaging.Resize(p.Mask, 0, int(sh), imaging.Lanczos)
		}
		if p.RMask != nil {
			p.RMask = imaging.Resize(p.RMask, 0, int(sh), imaging.Lanczos)
		}
	}
	dx, dy := newImg.Bounds().Max.X, newImg.Bounds().Max.Y
	c.Width = dx
	c.Height = dy

	if int(sw) < p.NewWidth || int(sh) < p.NewHeight {
		newImg = p.calculateFitness(newImg, c)
	}
	return newImg
}

// Process encodes the resized image into an io.Writer interface.
// We are using the io package, since we can provide different input and output types,
// as long as they implement the io.Reader and io.Writer interface.
func (p *Processor) Process(r io.Reader, w io.Writer) error {
	var err error

	if p.FaceDetect {
		// Instantiate a new Pigo object in case the face detection option is used.
		p.FaceDetector = pigo.NewPigo()

		// Unpack the binary file. This will return the number of cascade trees,
		// the tree depth, the threshold and the prediction from tree's leaf nodes.
		p.FaceDetector, err = p.FaceDetector.Unpack(cascadeFile)
		if err != nil {
			return fmt.Errorf("error unpacking the cascade file: %v", err)
		}
	}

	if p.NewWidth != 0 && p.NewHeight != 0 {
		resizeXY = true
	}

	src, _, err := image.Decode(r)
	if err != nil {
		return err
	}

	img := imgToNRGBA(src)

	if p.Preview {
		guiWidth := img.Bounds().Max.X
		guiHeight := img.Bounds().Max.Y

		if p.NewWidth > guiWidth {
			guiWidth = p.NewWidth
		}
		if p.NewHeight > guiHeight {
			guiHeight = p.NewHeight
		}
		if resizeXY {
			guiWidth = int(maxScreenX)
			guiHeight = int(maxScreenY)
		}

		guiWindow := struct {
			width  int
			height int
		}{
			width:  guiWidth,
			height: guiHeight,
		}
		// Lunch Gio GUI thread.
		go p.showPreview(imgWorker, errs, guiWindow)
	}

	return encodeImg(p, w, img)
}

// shrink reduces the image dimension either horizontally or vertically.
func (p *Processor) shrink(img *image.NRGBA) (*image.NRGBA, error) {
	width, height := img.Bounds().Max.X, img.Bounds().Max.Y
	c := NewCarver(width, height)

	if _, err := c.ComputeSeams(p, img); err != nil {
		return nil, err
	}
	seams := c.FindLowestEnergySeams(p)
	img = c.RemoveSeam(img, seams, p.Debug)

	if p.Mask != nil {
		p.Mask = c.RemoveSeam(p.Mask, seams, false)
		draw.Draw(p.DebugMask, img.Bounds(), p.Mask, image.Point{}, draw.Over)
	}

	if p.RMask != nil {
		p.RMask = c.RemoveSeam(p.RMask, seams, false)
		draw.Draw(p.DebugMask, img.Bounds(), p.RMask, image.Point{}, draw.Over)
	}

	go func() {
		select {
		case imgWorker <- worker{
			img:   img,
			mask:  p.DebugMask,
			seams: c.Seams,
			done:  false,
		}:
		case <-errs:
			return
		}
	}()
	return img, nil
}

// enlarge increases the image dimension either horizontally or vertically.
func (p *Processor) enlarge(img *image.NRGBA) (*image.NRGBA, error) {
	width, height := img.Bounds().Max.X, img.Bounds().Max.Y
	c := NewCarver(width, height)

	if _, err := c.ComputeSeams(p, img); err != nil {
		return nil, err
	}
	seams := c.FindLowestEnergySeams(p)
	img = c.AddSeam(img, seams, p.Debug)

	if p.Mask != nil {
		p.Mask = c.AddSeam(p.Mask, seams, false)
		p.DebugMask = p.Mask
	}

	if p.RMask != nil {
		p.RMask = c.AddSeam(p.RMask, seams, false)
		p.DebugMask = p.RMask
	}

	go func() {
		select {
		case imgWorker <- worker{
			img:   img,
			mask:  p.DebugMask,
			seams: c.Seams,
			done:  false,
		}:
		case <-errs:
			return
		}
	}()
	return img, nil
}


================================================
FILE: processor_test.go
================================================
package caire

import (
	"image"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestResize_ShrinkImageWidth(t *testing.T) {
	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	var c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
	newWidth := imgWidth / 2

	p.NewWidth = newWidth
	p.NewHeight = imgHeight

	for x := 0; x < newWidth; x++ {
		width, height := img.Bounds().Max.X, img.Bounds().Max.Y
		c = NewCarver(width, height)

		_, err := c.ComputeSeams(p, img)
		assert.NoError(t, err)

		seams := c.FindLowestEnergySeams(p)
		img = c.RemoveSeam(img, seams, p.Debug)
	}
	imgWidth := img.Bounds().Max.X

	assert.Equal(t, imgWidth, newWidth)
}

func TestResize_ShrinkImageHeight(t *testing.T) {
	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	var c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
	newHeight := imgHeight / 2

	p.NewWidth = imgWidth
	p.NewHeight = newHeight

	img = rotateImage90(img)
	for x := 0; x < newHeight; x++ {
		width, height := img.Bounds().Max.X, img.Bounds().Max.Y
		c = NewCarver(width, height)

		_, err := c.ComputeSeams(p, img)
		assert.NoError(t, err)

		seams := c.FindLowestEnergySeams(p)
		img = c.RemoveSeam(img, seams, p.Debug)
	}
	img = rotateImage270(img)
	imgHeight := img.Bounds().Max.Y

	assert.Equal(t, imgHeight, newHeight)
}

func TestResize_EnlargeImageWidth(t *testing.T) {
	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	var c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
	newWidth := imgWidth * 2

	p.NewWidth = newWidth
	p.NewHeight = imgHeight

	for x := 0; x < newWidth; x++ {
		width, height := img.Bounds().Max.X, img.Bounds().Max.Y
		c = NewCarver(width, height)

		_, err := c.ComputeSeams(p, img)
		assert.NoError(t, err)

		seams := c.FindLowestEnergySeams(p)
		img = c.AddSeam(img, seams, p.Debug)
	}
	imgWidth := img.Bounds().Max.X - img.Bounds().Dx()

	assert.NotEqual(t, imgWidth, newWidth)
}

func TestResize_EnlargeImageHeight(t *testing.T) {
	img := image.NewNRGBA(image.Rect(0, 0, imgWidth, imgHeight))
	var c = NewCarver(img.Bounds().Dx(), img.Bounds().Dy())
	newHeight := imgHeight * 2

	p.NewWidth = imgWidth
	p.NewHeight = newHeight

	img = rotateImage90(img)
	for x := 0; x < newHeight; x++ {
		width, height := img.Bounds().Max.X, img.Bounds().Max.Y
		c = NewCarver(width, height)

		_, err := c.ComputeSeams(p, img)
		assert.NoError(t, err)

		seams := c.FindLowestEnergySeams(p)
		img = c.AddSeam(img, seams, p.Debug)
	}
	img = rotateImage270(img)
	imgHeight := img.Bounds().Max.Y - img.Bounds().Dy()

	assert.NotEqual(t, imgHeight, newHeight)
}


================================================
FILE: snapcraft.yaml
================================================
name: caire
version: '1.4.4'
summary: Content aware image resize library
description: |
  Content aware image resize library
grade: stable
confinement: strict
base: core18
parts:
  caire:
    plugin: go
    source: https://github.com/esimov/caire.git
    go-importpath: github.com/esimov/caire
    stage-packages:
      - libegl1
      - libglvnd0
      - libwayland-client0
      - libwayland-cursor0
      - libwayland-egl1
      - libx11-6
      - libx11-xcb1
      - libxau6
      - libxcb-xkb1
      - libxcb1
      - libxcursor1
      - libxdmcp6
      - libxfixes3
      - libxkbcommon-x11-0
      - libxkbcommon0
      - libxrender1

    build-packages:
      - 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

apps:
  caire:
    command: bin/caire
    plugs:
      - home
      - x11

================================================
FILE: sobel.go
================================================
package caire

import (
	"image"
	"math"
)

type kernel [][]int32

var (
	kernelX = kernel{
		{-1, 0, 1},
		{-2, 0, 2},
		{-1, 0, 1},
	}

	kernelY = kernel{
		{-1, -2, -1},
		{0, 0, 0},
		{1, 2, 1},
	}
)

// SobelDetector uses the sobel filter operator for detecting image edges.
// See https://en.wikipedia.org/wiki/Sobel_operator
func (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *image.NRGBA {
	var sumX, sumY int32
	dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y
	dst := image.NewNRGBA(img.Bounds())

	// Get 3x3 window of pixels because image data given is just a 1D array of pixels
	maxPixelOffset := dx*2 + len(kernelX) - 1

	data := c.getImageData(img)
	length := len(data)*4 - maxPixelOffset
	magnitudes := make([]uint8, length)

	for i := 0; i < length; i++ {
		// Sum each pixel with the kernel value
		sumX, sumY = 0, 0
		for x := 0; x < len(kernelX); x++ {
			for y := 0; y < len(kernelY); y++ {
				if idx := i + (dx * y) + x; idx < len(data) {
					r := data[i+(dx*y)+x]
					sumX += int32(r) * kernelX[y][x]
					sumY += int32(r) * kernelY[y][x]
				}
			}
		}
		magnitude := math.Sqrt(float64(sumX*sumX) + float64(sumY*sumY))
		// Check for pixel color boundaries
		if magnitude < 0 {
			magnitude = 0
		} else if magnitude > 255 {
			magnitude = 255
		}

		// Set magnitude to 0 if doesn't exceed threshold, else set to magnitude
		if magnitude > threshold {
			magnitudes[i] = uint8(magnitude)
		} else {
			magnitudes[i] = 0
		}
	}

	dataLength := dx * dy * 4
	edges := make([]int32, dataLength)

	// Apply the kernel values
	for i := 0; i < dataLength; i++ {
		edges[i] = 0
		if i%4 != 0 {
			m := magnitudes[i/4]
			if m != 0 {
				edges[i-1] = int32(m)
			}
		}
	}

	// Generate the new image with the sobel filter applied
	for idx := 0; idx < len(edges); idx += 4 {
		dst.Pix[idx] = uint8(edges[idx])
		dst.Pix[idx+1] = uint8(edges[idx+1])
		dst.Pix[idx+2] = uint8(edges[idx+2])
		dst.Pix[idx+3] = 0xff
	}
	return dst
}

// getImageData gets the red component of an image and returns an array of pixel brightness values.
func (c *Carver) getImageData(img *image.NRGBA) []uint8 {
	dx, dy := img.Bounds().Max.X, img.Bounds().Max.Y
	pixels := make([]uint8, dx*dy)

	for i := range pixels {
		pixels[i] = img.Pix[i*4]
	}
	return pixels
}


================================================
FILE: stackblur.go
================================================
// Go implementation of the StackBlur algorithm
// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php

package caire

import (
	"errors"
	"image"
	"image/color"
)

// blurStack is a linked list containing the color value and a pointer to the next struct.
type blurStack struct {
	r, g, b, a uint32
	next       *blurStack
}

var mulTable = []uint32{
	512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512,
	454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512,
	482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456,
	437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512,
	497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328,
	320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456,
	446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335,
	329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512,
	505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405,
	399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328,
	324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271,
	268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456,
	451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388,
	385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335,
	332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292,
	289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259,
}

var shgTable = []uint32{
	9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
	17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
	19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
	20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
	21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
	21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
	22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
	22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
	23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
	23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
	23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
	23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
	24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
	24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
	24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
	24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
}

// 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.
func Stackblur(dst, src image.Image, radius uint32) error {
	// Limit the maximum blur radius to 255 to avoid overflowing the multable.
	if int(radius) >= len(mulTable) {
		radius = uint32(len(mulTable) - 1)
	}

	if radius < 1 {
		return errors.New("blur radius must be greater than 0")
	}

	img, ok := dst.(*image.NRGBA)
	if !ok {
		return errors.New("the destination image must be image.NRGBA")
	}

	process(img, src, radius)
	return nil
}

func process(dst *image.NRGBA, src image.Image, radius uint32) {
	srcBounds := src.Bounds()
	srcMinX := srcBounds.Min.X
	srcMinY := srcBounds.Min.Y

	dstBounds := srcBounds.Sub(srcBounds.Min)
	dstW := dstBounds.Dx()
	dstH := dstBounds.Dy()

	switch src0 := src.(type) {
	case *image.NRGBA:
		rowSize := srcBounds.Dx() * 4
		for dstY := 0; dstY < dstH; dstY++ {
			di := src0.PixOffset(0, dstY)
			si := src0.PixOffset(srcMinX, srcMinY+dstY)
			for dstX := 0; dstX < dstW; dstX++ {
				copy(dst.Pix[di:di+rowSize], src0.Pix[si:si+rowSize])
			}
		}
	case *image.YCbCr:
		for dstY := 0; dstY < dstH; dstY++ {
			di := dst.PixOffset(0, dstY)
			for dstX := 0; dstX < dstW; dstX++ {
				srcX := srcMinX + dstX
				srcY := srcMinY + dstY
				siy := src0.YOffset(srcX, srcY)
				sic := src0.COffset(srcX, srcY)
				r, g, b := color.YCbCrToRGB(src0.Y[siy], src0.Cb[sic], src0.Cr[sic])
				dst.Pix[di+0] = r
				dst.Pix[di+1] = g
				dst.Pix[di+2] = b
				dst.Pix[di+3] = 0xff
				di += 4
			}
		}
	default:
		for dstY := 0; dstY < dstH; dstY++ {
			di := dst.PixOffset(0, dstY)
			for dstX := 0; dstX < dstW; dstX++ {
				c := color.NRGBAModel.Convert(src.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
				dst.Pix[di+0] = c.R
				dst.Pix[di+1] = c.G
				dst.Pix[di+2] = c.B
				dst.Pix[di+3] = c.A
				di += 4
			}
		}
	}

	blurImage(dst, radius)
}

func blurImage(src *image.NRGBA, radius uint32) {
	var (
		stackEnd *blurStack
		stackIn  *blurStack
		stackOut *blurStack
	)

	var width, height = uint32(src.Bounds().Dx()), uint32(src.Bounds().Dy())
	var (
		div, widthMinus1, heightMinus1, radiusPlus1, sumFactor uint32
		x, y, i, p, yp, yi, yw,
		rSum, gSum, bSum, aSum,
		rOutSum, gOutSum, bOutSum, aOutSum,
		rInSum, gInSum, bInSum, aInSum,
		pr, pg, pb, pa uint32
	)

	div = radius + radius + 1
	widthMinus1 = width - 1
	heightMinus1 = height - 1
	radiusPlus1 = radius + 1
	sumFactor = radiusPlus1 * (radiusPlus1 + 1) / 2

	stackStart := new(blurStack)
	stack := stackStart

	for i = 1; i < div; i++ {
		stack.next = new(blurStack)
		stack = stack.next
		if i == radiusPlus1 {
			stackEnd = stack
		}
	}
	stack.next = stackStart

	mulSum := mulTable[radius]
	shgSum := shgTable[radius]

	for y = 0; y < height; y++ {
		rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0

		pr = uint32(src.Pix[yi])
		pg = uint32(src.Pix[yi+1])
		pb = uint32(src.Pix[yi+2])
		pa = uint32(src.Pix[yi+3])

		rOutSum = radiusPlus1 * pr
		gOutSum = radiusPlus1 * pg
		bOutSum = radiusPlus1 * pb
		aOutSum = radiusPlus1 * pa

		rSum += sumFactor * pr
		gSum += sumFactor * pg
		bSum += sumFactor * pb
		aSum += sumFactor * pa

		stack = stackStart

		for i = 0; i < radiusPlus1; i++ {
			stack.r = pr
			stack.g = pg
			stack.b = pb
			stack.a = pa
			stack = stack.next
		}

		for i = 1; i < radiusPlus1; i++ {
			var diff uint32
			if widthMinus1 < i {
				diff = widthMinus1
			} else {
				diff = i
			}
			p = yi + (diff << 2)
			pr = uint32(src.Pix[p])
			pg = uint32(src.Pix[p+1])
			pb = uint32(src.Pix[p+2])
			pa = uint32(src.Pix[p+3])

			stack.r = pr
			stack.g = pg
			stack.b = pb
			stack.a = pa

			rSum += stack.r * (radiusPlus1 - i)
			gSum += stack.g * (radiusPlus1 - i)
			bSum += stack.b * (radiusPlus1 - i)
			aSum += stack.a * (radiusPlus1 - i)

			rInSum += pr
			gInSum += pg
			bInSum += pb
			aInSum += pa

			stack = stack.next
		}
		stackIn = stackStart
		stackOut = stackEnd

		for x = 0; x < width; x++ {
			pa = (aSum * mulSum) >> shgSum
			src.Pix[yi+3] = uint8(pa)

			if pa != 0 {
				src.Pix[yi] = uint8((rSum * mulSum) >> shgSum)
				src.Pix[yi+1] = uint8((gSum * mulSum) >> shgSum)
				src.Pix[yi+2] = uint8((bSum * mulSum) >> shgSum)
			} else {
				src.Pix[yi] = 0
				src.Pix[yi+1] = 0
				src.Pix[yi+2] = 0
			}

			rSum -= rOutSum
			gSum -= gOutSum
			bSum -= bOutSum
			aSum -= aOutSum

			rOutSum -= stackIn.r
			gOutSum -= stackIn.g
			bOutSum -= stackIn.b
			aOutSum -= stackIn.a

			p = x + radius + 1

			if p > widthMinus1 {
				p = widthMinus1
			}
			p = (yw + p) << 2

			stackIn.r = uint32(src.Pix[p])
			stackIn.g = uint32(src.Pix[p+1])
			stackIn.b = uint32(src.Pix[p+2])
			stackIn.a = uint32(src.Pix[p+3])

			rInSum += stackIn.r
			gInSum += stackIn.g
			bInSum += stackIn.b
			aInSum += stackIn.a

			rSum += rInSum
			gSum += gInSum
			bSum += bInSum
			aSum += aInSum

			stackIn = stackIn.next

			pr = stackOut.r
			pg = stackOut.g
			pb = stackOut.b
			pa = stackOut.a

			rOutSum += pr
			gOutSum += pg
			bOutSum += pb
			aOutSum += pa

			rInSum -= pr
			gInSum -= pg
			bInSum -= pb
			aInSum -= pa

			stackOut = stackOut.next

			yi += 4
		}
		yw += width
	}

	for x = 0; x < width; x++ {
		rInSum, gInSum, bInSum, aInSum, rSum, gSum, bSum, aSum = 0, 0, 0, 0, 0, 0, 0, 0

		yi = x << 2
		pr = uint32(src.Pix[yi])
		pg = uint32(src.Pix[yi+1])
		pb = uint32(src.Pix[yi+2])
		pa = uint32(src.Pix[yi+3])

		rOutSum = radiusPlus1 * pr
		gOutSum = radiusPlus1 * pg
		bOutSum = radiusPlus1 * pb
		aOutSum = radiusPlus1 * pa

		rSum += sumFactor * pr
		gSum += sumFactor * pg
		bSum += sumFactor * pb
		aSum += sumFactor * pa

		stack = stackStart

		for i = 0; i < radiusPlus1; i++ {
			stack.r = pr
			stack.g = pg
			stack.b = pb
			stack.a = pa
			stack = stack.next
		}

		yp = width

		for i = 1; i <= radius; i++ {
			yi = (yp + x) << 2
			pr = uint32(src.Pix[yi])
			pg = uint32(src.Pix[yi+1])
			pb = uint32(src.Pix[yi+2])
			pa = uint32(src.Pix[yi+3])

			stack.r = pr
			stack.g = pg
			stack.b = pb
			stack.a = pa

			rSum += stack.r * (radiusPlus1 - i)
			gSum += stack.g * (radiusPlus1 - i)
			bSum += stack.b * (radiusPlus1 - i)
			aSum += stack.a * (radiusPlus1 - i)

			rInSum += pr
			gInSum += pg
			bInSum += pb
			aInSum += pa

			stack = stack.next

			if i < heightMinus1 {
				yp += width
			}
		}

		yi = x
		stackIn = stackStart
		stackOut = stackEnd

		for y = 0; y < height; y++ {
			p = yi << 2
			pa = (aSum * mulSum) >> shgSum
			src.Pix[p+3] = uint8(pa)

			if pa > 0 {
				src.Pix[p] = uint8((rSum * mulSum) >> shgSum)
				src.Pix[p+1] = uint8((gSum * mulSum) >> shgSum)
				src.Pix[p+2] = uint8((bSum * mulSum) >> shgSum)
			} else {
				src.Pix[p] = 0
				src.Pix[p+1] = 0
				src.Pix[p+2] = 0
			}

			rSum -= rOutSum
			gSum -= gOutSum
			bSum -= bOutSum
			aSum -= aOutSum

			rOutSum -= stackIn.r
			gOutSum -= stackIn.g
			bOutSum -= stackIn.b
			aOutSum -= stackIn.a

			p = y + radiusPlus1

			if p > heightMinus1 {
				p = heightMinus1
			}
			p = (x + (p * width)) << 2

			stackIn.r = uint32(src.Pix[p])
			stackIn.g = uint32(src.Pix[p+1])
			stackIn.b = uint32(src.Pix[p+2])
			stackIn.a = uint32(src.Pix[p+3])

			rInSum += stackIn.r
			gInSum += stackIn.g
			bInSum += stackIn.b
			aInSum += stackIn.a

			rSum += rInSum
			gSum += gInSum
			bSum += bInSum
			aSum += aInSum

			stackIn = stackIn.next

			pr = stackOut.r
			pg = stackOut.g
			pb = stackOut.b
			pa = stackOut.a

			rOutSum += pr
			gOutSum += pg
			bOutSum += pb
			aOutSum += pa

			rInSum -= pr
			gInSum -= pg
			bInSum -= pb
			aInSum -= pa

			stackOut = stackOut.next

			yi += width
		}
	}
}


================================================
FILE: utils/download.go
================================================
package utils

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"strings"
)

// DownloadImage downloads the image from the internet and saves it into a temporary file.
func DownloadImage(url string) (*os.File, error) {
	// Retrieve the url and decode the response body.
	res, err := http.Get(url)
	if err != nil {
		return nil, fmt.Errorf("unable to download image file from URI: %s, status %v", url, res.Status)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, fmt.Errorf("unable to read response body: %w", err)
	}

	tmpfile, err := os.CreateTemp("/tmp", "image")
	if err != nil {
		return nil, fmt.Errorf("unable to create temporary file: %w", err)
	}

	// Copy the image binary data into the temporary file.
	_, err = io.Copy(tmpfile, bytes.NewBuffer(data))
	if err != nil {
		return nil, fmt.Errorf("unable to copy the source URI into the destination file")
	}

	ctype, err := DetectContentType(tmpfile.Name())
	if err != nil {
		return nil, err
	}

	if !strings.Contains(ctype.(string), "image") {
		return nil, fmt.Errorf("the downloaded file is not a valid image type")
	}

	return tmpfile, nil
}

// IsValidUrl tests a string to determine if it is a well-structured url or not.
func IsValidUrl(uri string) bool {
	_, err := url.ParseRequestURI(uri)
	if err != nil {
		return false
	}

	u, err := url.Parse(uri)
	if err != nil || u.Scheme == "" || u.Host == "" {
		return false
	}

	return true
}

// DetectContentType detects the file type by reading MIME type information of the file content.
func DetectContentType(fname string) (any, error) {
	file, err := os.Open(fname)
	if err != nil {
		return nil, err
	}
	defer func() {
		if err := file.Close(); err != nil {
			log.Printf("could not close the opened file: %v", err)
		}
	}()

	// Only the first 512 bytes are used to sniff the content type.
	buffer := make([]byte, 512)
	_, err = file.Read(buffer)
	if err != nil {
		return nil, err
	}

	// Reset the read pointer if necessary.
	if _, err := file.Seek(0, 0); err != nil {
		return nil, err
	}

	// Always returns a valid content-type and "application/octet-stream" if no others seemed to match.
	contentType := http.DetectContentType(buffer)

	return string(contentType), nil
}


================================================
FILE: utils/download_test.go
================================================
package utils

import (
	"path/filepath"
	"strings"
	"testing"
)

func TestUtils_ShouldDownloadImage(t *testing.T) {
	f, err := DownloadImage("https://raw.githubusercontent.com/esimov/caire/master/testdata/sample.jpg")
	if err != nil {
		t.Fatalf("could't download test file: %v", err)
	}

	if !strings.Contains(f.Name(), "tmp") {
		t.Errorf("The downloaded image should have been saved in a temporary folder")
	}
}

func TestUtils_ShouldBeValidUrl(t *testing.T) {
	ok := IsValidUrl("https://github.com/esimov/caire/")
	if !ok {
		t.Errorf("A valid URL should have been provided")
	}
}

func TestUtils_ShouldDetectValidFileType(t *testing.T) {
	sampleImg := filepath.Join("../testdata", "sample.jpg")

	ftype, err := DetectContentType(sampleImg)
	if err != nil {
		t.Fatalf("could not detect content type: %v", err)
	}

	if !strings.Contains(ftype.(string), "image") {
		t.Errorf("Content type expected to be of type image, got: %v", ftype)
	}
}


================================================
FILE: utils/format.go
================================================
package utils

import (
	"fmt"
	"image/color"
	"math"
	"strings"
	"time"
)

// MessageType is a custom type used as a placeholder for various message types.
type MessageType int

// The message types used across the CLI application.
const (
	DefaultMessage MessageType = iota
	SuccessMessage
	ErrorMessage
	StatusMessage
)

// Colors used across the CLI application.
const (
	DefaultColor = "\x1b[0m"
	StatusColor  = "\x1b[36m"
	SuccessColor = "\x1b[32m"
	ErrorColor   = "\x1b[31m"
)

// DecorateText shows the message types in different colors.
func DecorateText(s string, msgType MessageType) string {
	switch msgType {
	case DefaultMessage:
		s = DefaultColor + s
	case StatusMessage:
		s = StatusColor + s
	case SuccessMessage:
		s = SuccessColor + s
	case ErrorMessage:
		s = ErrorColor + s
	default:
		return s
	}
	return s + DefaultColor
}

// FormatTime formats time.Duration output to a human readable value.
func FormatTime(d time.Duration) string {
	if d.Seconds() < 60.0 {
		return fmt.Sprintf("%.2fs", d.Seconds())
	}
	if d.Minutes() < 60.0 {
		remainingSeconds := math.Mod(d.Seconds(), 60)
		return fmt.Sprintf("%dm %.2fs", int64(d.Minutes()), remainingSeconds)
	}
	if d.Hours() < 24.0 {
		remainingMinutes := math.Mod(d.Minutes(), 60)
		remainingSeconds := math.Mod(d.Seconds(), 60)
		return fmt.Sprintf("%dh %dm %.2fs",
			int64(d.Hours()), int64(remainingMinutes), remainingSeconds)
	}
	remainingHours := math.Mod(d.Hours(), 24)
	remainingMinutes := math.Mod(d.Minutes(), 60)
	remainingSeconds := math.Mod(d.Seconds(), 60)
	return fmt.Sprintf("%dd %dh %dm %.2fs",
		int64(d.Hours()/24), int64(remainingHours),
		int64(remainingMinutes), remainingSeconds)
}

// HexToRGBA converts a color expressed as hexadecimal string to RGBA color.
func HexToRGBA(x string) color.NRGBA {
	var r, g, b, a uint8

	x = strings.TrimPrefix(x, "#")
	a = 255
	if len(x) == 2 {
		format := "%03x"
		fmt.Sscanf(x, format, &r, &g, &b)
	}
	if len(x) == 3 {
		format := "%1x%1x%1x"
		fmt.Sscanf(x, format, &r, &g, &b)
		r |= r << 4
		g |= g << 4
		b |= b << 4
	}
	if len(x) == 6 {
		format := "%02x%02x%02x"
		fmt.Sscanf(x, format, &r, &g, &b)
	}
	if len(x) == 8 {
		format := "%02x%02x%02x%02x"
		fmt.Sscanf(x, format, &r, &g, &b, &a)
	}
	return color.NRGBA{R: r, G: g, B: b, A: a}
}

// RGB returns color based on RGB in range 0..1
func RGB(r, g, b float32) color.NRGBA {
	return color.NRGBA{R: sat8(r), G: sat8(g), B: sat8(b), A: 0xFF}
}

// RGBA returns color based on RGBA in range 0..1
func RGBA(r, g, b, a float32) color.NRGBA {
	return color.NRGBA{R: sat8(r), G: sat8(g), B: sat8(b), A: sat8(a)}
}

// HSLA returns color based on HSLA in range 0..1
func HSLA(h, s, l, a float32) color.NRGBA { return RGBA(hsla(h, s, l, a)) }

// HSL returns color based on HSL in range 0..1
func HSL(h, s, l float32) color.NRGBA { return HSLA(h, s, l, 1) }

func hue(v1, v2, h float32) float32 {
	if h < 0 {
		h += 1
	}
	if h > 1 {
		h -= 1
	}
	if 6*h < 1 {
		return v1 + (v2-v1)*6*h
	} else if 2*h < 1 {
		return v2
	} else if 3*h < 2 {
		return v1 + (v2-v1)*(2.0/3.0-h)*6
	}

	return v1
}

func hsla(h, s, l, a float32) (r, g, b, ra float32) {
	if s == 0 {
		return l, l, l, a
	}

	h = mod32(h, 1)

	var v2 float32
	if l < 0.5 {
		v2 = l * (1 + s)
	} else {
		v2 = (l + s) - s*l
	}

	v1 := 2*l - v2
	r = hue(v1, v2, h+1.0/3.0)
	g = hue(v1, v2, h)
	b = hue(v1, v2, h-1.0/3.0)
	ra = a

	return
}

func sat8(v float32) uint8 {
	v *= 255.0
	if v >= 255 {
		return 255
	} else if v <= 0 {
		return 0
	}
	return uint8(v)
}

func mod32(x, y float32) float32 { return float32(math.Mod(float64(x), float64(y))) }


================================================
FILE: utils/spinner.go
================================================
package utils

import (
	"fmt"
	"io"
	"os"
	"runtime"
	"strings"
	"sync"
	"time"
	"unicode/utf8"
)

// Spinner initializes the progress indicator.
type Spinner struct {
	mu         *sync.RWMutex
	delay      time.Duration
	writer     io.Writer
	message    string
	lastOutput string
	StopMsg    string
	hideCursor bool
	stopChan   chan struct{}
}

// NewSpinner instantiates a new progress indicator.
func NewSpinner(msg string, d time.Duration) *Spinner {
	return &Spinner{
		mu:         &sync.RWMutex{},
		delay:      d,
		writer:     os.Stderr,
		message:    msg,
		hideCursor: true,
		stopChan:   make(chan struct{}, 1),
	}
}

// Start starts the progress indicator.
func (s *Spinner) Start() {
	if s.hideCursor && runtime.GOOS != "windows" {
		// hides the cursor
		fmt.Fprintf(s.writer, "\033[?25l")
	}

	charSet := []string{"⠈⠁", "⠈⠑", "⠈⠱", "⠈⡱", "⢀⡱", "⢄⡱", "⢄⡱", "⢆⡱", "⢎⡱", "⢎⡰", "⢎⡠", "⢎⡀", "⢎⠁", "⠎⠁", "⠊⠁"}
	go func() {
		for {
			for _, r := range charSet {
				select {
				case <-s.stopChan:
					return
				default:
					s.mu.Lock()

					output := fmt.Sprintf("\r%s%s %s%s", s.message, StatusColor, r, DefaultColor)
					fmt.Fprintf(s.writer, output)
					s.lastOutput = output

					s.mu.Unlock()
					time.Sleep(s.delay)
				}
			}
		}
	}()
}

// Stop stops the progress indicator.
func (s *Spinner) Stop() {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.clear()
	s.RestoreCursor()
	if len(s.StopMsg) > 0 {
		fmt.Fprintf(s.writer, s.StopMsg)
	}
	s.stopChan <- struct{}{}
}

// RestoreCursor restores back the cursor visibility.
func (s *Spinner) RestoreCursor() {
	if s.hideCursor && runtime.GOOS != "windows" {
		// makes the cursor visible
		fmt.Fprint(s.writer, "\033[?25h")
	}
}

// clear deletes the last line. Caller must hold the the locker.
func (s *Spinner) clear() {
	n := utf8.RuneCountInString(s.lastOutput)
	if runtime.GOOS == "windows" {
		clearString := "\r" + strings.Repeat(" ", n) + "\r"
		fmt.Fprint(s.writer, clearString)
		s.lastOutput = ""
		return
	}
	for _, c := range []string{"\b", "\127", "\b", "\033[K"} { // "\033[K" for macOS Terminal
		fmt.Fprint(s.writer, strings.Repeat(c, n))
	}
	fmt.Fprintf(s.writer, "\r\033[K") // clear line
	s.lastOutput = ""
}


================================================
FILE: utils/utils.go
================================================
package utils

import (
	"slices"

	"golang.org/x/exp/constraints"
)

// Min returns the slowest value of the provided parameters.
func Min[T constraints.Ordered](values ...T) T {
	var acc T = values[0]

	for _, v := range values {
		if v < acc {
			acc = v
		}
	}
	return acc
}

// Max returns the biggest value of the provided parameters.
func Max[T constraints.Ordered](values ...T) T {
	var acc T = values[0]

	for _, v := range values {
		if v > acc {
			acc = v
		}
	}
	return acc
}

// Abs returns the absolut value of x.
func Abs[T constraints.Signed | constraints.Float](x T) T {
	if x < 0 {
		return -x
	}
	return x
}

// Contains returns true if a value is available in the collection.
func Contains[T comparable](slice []T, value T) bool {
	return slices.Contains(slice, value)
}
Download .txt
gitextract_jflphtww/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── build.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── build.sh
├── carver.go
├── carver_benchmark_test.go
├── carver_test.go
├── cmd/
│   └── caire/
│       └── main.go
├── data/
│   └── facefinder
├── doc.go
├── draw.go
├── exec.go
├── go.mod
├── go.sum
├── gui.go
├── image.go
├── image_test.go
├── imop/
│   ├── blend.go
│   ├── blend_test.go
│   ├── comp.go
│   └── comp_test.go
├── preview.go
├── processor.go
├── processor_test.go
├── snapcraft.yaml
├── sobel.go
├── stackblur.go
└── utils/
    ├── download.go
    ├── download_test.go
    ├── format.go
    ├── spinner.go
    └── utils.go
Download .txt
SYMBOL INDEX (197 symbols across 23 files)

FILE: carver.go
  type SeamCarver (line 16) | type SeamCarver interface
  constant maxFaceDetAttempts (line 21) | maxFaceDetAttempts = 20
  type Carver (line 34) | type Carver struct
    method get (line 58) | func (c *Carver) get(x, y int) float64 {
    method set (line 64) | func (c *Carver) set(x, y int, px float64) {
    method ComputeSeams (line 77) | func (c *Carver) ComputeSeams(p *Processor, img *image.NRGBA) (*image....
    method FindLowestEnergySeams (line 258) | func (c *Carver) FindLowestEnergySeams(p *Processor) []Seam {
    method RemoveSeam (line 319) | func (c *Carver) RemoveSeam(img *image.NRGBA, seams []Seam, debug bool...
    method AddSeam (line 342) | func (c *Carver) AddSeam(img *image.NRGBA, seams []Seam, debug bool) *...
  type Seam (line 42) | type Seam struct
  function NewCarver (line 48) | func NewCarver(width, height int) *Carver {

FILE: carver_benchmark_test.go
  function Benchmark_Carver (line 10) | func Benchmark_Carver(b *testing.B) {

FILE: carver_test.go
  constant imgWidth (line 17) | imgWidth  = 10
  constant imgHeight (line 18) | imgHeight = 10
  function init (line 23) | func init() {
  function TestCarver_EnergySeamShouldNotBeDetected (line 35) | func TestCarver_EnergySeamShouldNotBeDetected(t *testing.T) {
  function TestCarver_DetectHorizontalEnergySeam (line 64) | func TestCarver_DetectHorizontalEnergySeam(t *testing.T) {
  function TestCarver_DetectVerticalEnergySeam (line 103) | func TestCarver_DetectVerticalEnergySeam(t *testing.T) {
  function TestCarver_RemoveSeam (line 143) | func TestCarver_RemoveSeam(t *testing.T) {
  function TestCarver_AddSeam (line 182) | func TestCarver_AddSeam(t *testing.T) {
  function TestCarver_ComputeSeams (line 221) | func TestCarver_ComputeSeams(t *testing.T) {
  function TestCarver_ShouldDetectFace (line 244) | func TestCarver_ShouldDetectFace(t *testing.T) {
  function TestCarver_ShouldNotRemoveFaceZone (line 293) | func TestCarver_ShouldNotRemoveFaceZone(t *testing.T) {
  function TestCarver_ShouldNotResizeWithFaceDistorsion (line 373) | func TestCarver_ShouldNotResizeWithFaceDistorsion(t *testing.T) {
  function findNonZeroValue (line 428) | func findNonZeroValue(points []float64) bool {

FILE: cmd/caire/main.go
  constant HelpBanner (line 15) | HelpBanner = `
  constant pipeName (line 26) | pipeName = "-"
  function main (line 52) | func main() {

FILE: draw.go
  type ShapeType (line 13) | type ShapeType
  constant Circle (line 16) | Circle ShapeType = "circle"
  constant Line (line 17) | Line   ShapeType = "line"
  method DrawSeam (line 22) | func (g *Gui) DrawSeam(shape ShapeType, x, y, thickness float32) {
  method drawCircle (line 34) | func (g *Gui) drawCircle(x, y, radius float32) {
  method drawLine (line 61) | func (g *Gui) drawLine(x, y, thickness float32) {
  method point (line 82) | func (g *Gui) point(x, y float32) f32.Point {
  method setColor (line 90) | func (g *Gui) setColor(c color.Color) color.NRGBA {
  method setFillColor (line 101) | func (g *Gui) setFillColor(c color.Color) {
  method getFillColor (line 106) | func (g *Gui) getFillColor() color.Color {
  function getRatio (line 111) | func getRatio(w, h float32) float32 {

FILE: exec.go
  type Image (line 31) | type Image struct
    method consumer (line 177) | func (img *Image) consumer(
    method process (line 200) | func (img *Image) process(p *Processor, in, out string) error {
    method pathToFile (line 292) | func (img *Image) pathToFile(in, out string) (io.Reader, io.Writer, er...
    method printOpStatus (line 333) | func (img *Image) printOpStatus(fname string, err error) {
  type result (line 37) | type result struct
  function Resize (line 42) | func Resize(s SeamCarver, img *image.NRGBA) (image.Image, error) {
  method Execute (line 49) | func (p *Processor) Execute(img *Image) {
  function walkDir (line 352) | func walkDir(

FILE: gui.go
  type hudControlType (line 29) | type hudControlType
  constant hudShowSeams (line 32) | hudShowSeams hudControlType = iota
  constant hudShowDebugMask (line 33) | hudShowDebugMask
  constant redStart (line 39) | redStart   = 137
  constant greenStart (line 40) | greenStart = 47
  constant blueStart (line 41) | blueStart  = 54
  constant redEnd (line 44) | redEnd   = 255
  constant greenEnd (line 45) | greenEnd = 112
  constant blueEnd (line 46) | blueEnd  = 105
  type interval (line 57) | type interval struct
  type Gui (line 63) | type Gui struct
    method AddHudControl (line 137) | func (g *Gui) AddHudControl(hudControlType hudControlType, title strin...
    method initWindow (line 148) | func (g *Gui) initWindow(width, height int) {
    method getWindowSize (line 170) | func (g *Gui) getWindowSize() (float32, float32) {
    method Run (line 184) | func (g *Gui) Run() error {
    method draw (line 360) | func (g *Gui) draw(bgColor color.NRGBA) {
    method displayMessage (line 483) | func (g *Gui) displayMessage(ctx layout.Context, bgCol color.NRGBA, ms...
  type hudCtrl (line 103) | type hudCtrl struct
  function NewGUI (line 110) | func NewGUI(width, height int) *Gui {
  function random (line 563) | func random(min, max float32) float32 {

FILE: image.go
  function decodeImg (line 20) | func decodeImg(src string) (image.Image, error) {
  function encodeImg (line 44) | func encodeImg(p *Processor, w io.Writer, img *image.NRGBA) error {
  function rotateImage90 (line 80) | func rotateImage90(src *image.NRGBA) *image.NRGBA {
  function rotateImage270 (line 97) | func rotateImage270(src *image.NRGBA) *image.NRGBA {
  function imgToNRGBA (line 115) | func imgToNRGBA(img image.Image) *image.NRGBA {
  function imgToPix (line 174) | func imgToPix(src *image.NRGBA) []uint8 {
  function pixToImage (line 189) | func pixToImage(pixels []uint8, width, height int) image.Image {
  function rgbToGrayscale (line 216) | func rgbToGrayscale(src *image.NRGBA) []uint8 {
  function dither (line 235) | func dither(src *image.NRGBA) *image.NRGBA {

FILE: image_test.go
  function TestImage_ShouldGetSampleImage (line 15) | func TestImage_ShouldGetSampleImage(t *testing.T) {
  function TestImage_ImgToNRGBA (line 23) | func TestImage_ImgToNRGBA(t *testing.T) {
  function scan (line 83) | func scan(img image.Image, x1, y1, x2, y2 int, dst []uint8) {
  function makeYCbCrImage (line 154) | func makeYCbCrImage(rect image.Rectangle, colors []color.Color, sr image...
  function makeNRGBAImage (line 169) | func makeNRGBAImage(rect image.Rectangle, colors []color.Color) *image.N...
  function fillDrawImage (line 175) | func fillDrawImage(img draw.Image, colors []color.Color) {
  function readRow (line 192) | func readRow(img image.Image, y int) []uint8 {
  function readColumn (line 206) | func readColumn(img image.Image, x int) []uint8 {
  function compareBytes (line 220) | func compareBytes(a, b []uint8, delta int) bool {

FILE: imop/blend.go
  type BlendType (line 21) | type BlendType
  constant Normal (line 24) | Normal BlendType = iota
  constant Darken (line 25) | Darken
  constant Lighten (line 26) | Lighten
  constant Multiply (line 27) | Multiply
  constant Screen (line 28) | Screen
  constant Overlay (line 29) | Overlay
  constant SoftLight (line 30) | SoftLight
  constant HardLight (line 31) | HardLight
  constant ColorDodge (line 32) | ColorDodge
  constant ColorBurn (line 33) | ColorBurn
  constant Difference (line 34) | Difference
  constant Exclusion (line 35) | Exclusion
  constant Hue (line 38) | Hue
  constant Saturation (line 39) | Saturation
  constant ColorMode (line 40) | ColorMode
  constant Luminosity (line 41) | Luminosity
  type Blend (line 45) | type Blend struct
    method Set (line 68) | func (bl *Blend) Set(blendType BlendType) error {
    method Get (line 78) | func (bl *Blend) Get() BlendType {
    method Lum (line 83) | func (bl *Blend) Lum(rgb Color) float64 {
    method SetLum (line 88) | func (bl *Blend) SetLum(rgb Color, l float64) Color {
    method clip (line 98) | func (bl *Blend) clip(rgb Color) Color {
    method Sat (line 120) | func (bl *Blend) Sat(rgb Color) float64 {
    method SetSat (line 133) | func (bl *Blend) SetSat(rgb Color, s float64) Color {
    method AlphaCompose (line 164) | func (bl *Blend) AlphaCompose(
  type Color (line 51) | type Color struct
  function NewBlend (line 56) | func NewBlend() *Blend {
  type channel (line 128) | type channel struct

FILE: imop/blend_test.go
  function TestBlend_Basic (line 12) | func TestBlend_Basic(t *testing.T) {
  function TestBlend_Modes (line 42) | func TestBlend_Modes(t *testing.T) {
  function TestBlend_NonSeparableModes (line 151) | func TestBlend_NonSeparableModes(t *testing.T) {

FILE: imop/comp.go
  type CompType (line 17) | type CompType
  constant Clear (line 20) | Clear CompType = iota
  constant Copy (line 21) | Copy
  constant Dst (line 22) | Dst
  constant SrcOver (line 23) | SrcOver
  constant DstOver (line 24) | DstOver
  constant SrcIn (line 25) | SrcIn
  constant DstIn (line 26) | DstIn
  constant SrcOut (line 27) | SrcOut
  constant DstOut (line 28) | DstOut
  constant SrcAtop (line 29) | SrcAtop
  constant DstAtop (line 30) | DstAtop
  constant Xor (line 31) | Xor
  type Bitmap (line 36) | type Bitmap struct
  type Composite (line 41) | type Composite struct
    method Set (line 75) | func (op *Composite) Set(compType CompType) error {
    method Get (line 85) | func (op *Composite) Get() CompType {
    method Draw (line 92) | func (op *Composite) Draw(bitmap *Bitmap, src, dst *image.NRGBA, blend...
  function NewBitmap (line 47) | func NewBitmap(rect image.Rectangle) *Bitmap {
  function InitOp (line 54) | func InitOp() *Composite {

FILE: imop/comp_test.go
  function TestComp_Basic (line 12) | func TestComp_Basic(t *testing.T) {
  function TestComp_Ops (line 25) | func TestComp_Ops(t *testing.T) {

FILE: preview.go
  method showPreview (line 8) | func (p *Processor) showPreview(

FILE: processor.go
  type worker (line 30) | type worker struct
  type shrinkFn (line 38) | type shrinkFn
  type enlargeFn (line 41) | type enlargeFn
  type Processor (line 44) | type Processor struct
    method Resize (line 77) | func (p *Processor) Resize(img *image.NRGBA) (image.Image, error) {
    method calculateFitness (line 405) | func (p *Processor) calculateFitness(img *image.NRGBA, c *Carver) *ima...
    method Process (line 448) | func (p *Processor) Process(r io.Reader, w io.Writer) error {
    method shrink (line 504) | func (p *Processor) shrink(img *image.NRGBA) (*image.NRGBA, error) {
    method enlarge (line 540) | func (p *Processor) enlarge(img *image.NRGBA) (*image.NRGBA, error) {

FILE: processor_test.go
  function TestResize_ShrinkImageWidth (line 10) | func TestResize_ShrinkImageWidth(t *testing.T) {
  function TestResize_ShrinkImageHeight (line 33) | func TestResize_ShrinkImageHeight(t *testing.T) {
  function TestResize_EnlargeImageWidth (line 58) | func TestResize_EnlargeImageWidth(t *testing.T) {
  function TestResize_EnlargeImageHeight (line 81) | func TestResize_EnlargeImageHeight(t *testing.T) {

FILE: sobel.go
  type kernel (line 8) | type kernel
  method SobelDetector (line 26) | func (c *Carver) SobelDetector(img *image.NRGBA, threshold float64) *ima...
  method getImageData (line 91) | func (c *Carver) getImageData(img *image.NRGBA) []uint8 {

FILE: stackblur.go
  type blurStack (line 13) | type blurStack struct
  function Stackblur (line 57) | func Stackblur(dst, src image.Image, radius uint32) error {
  function process (line 76) | func process(dst *image.NRGBA, src image.Image, radius uint32) {
  function blurImage (line 128) | func blurImage(src *image.NRGBA, radius uint32) {

FILE: utils/download.go
  function DownloadImage (line 15) | func DownloadImage(url string) (*os.File, error) {
  function IsValidUrl (line 52) | func IsValidUrl(uri string) bool {
  function DetectContentType (line 67) | func DetectContentType(fname string) (any, error) {

FILE: utils/download_test.go
  function TestUtils_ShouldDownloadImage (line 9) | func TestUtils_ShouldDownloadImage(t *testing.T) {
  function TestUtils_ShouldBeValidUrl (line 20) | func TestUtils_ShouldBeValidUrl(t *testing.T) {
  function TestUtils_ShouldDetectValidFileType (line 27) | func TestUtils_ShouldDetectValidFileType(t *testing.T) {

FILE: utils/format.go
  type MessageType (line 12) | type MessageType
  constant DefaultMessage (line 16) | DefaultMessage MessageType = iota
  constant SuccessMessage (line 17) | SuccessMessage
  constant ErrorMessage (line 18) | ErrorMessage
  constant StatusMessage (line 19) | StatusMessage
  constant DefaultColor (line 24) | DefaultColor = "\x1b[0m"
  constant StatusColor (line 25) | StatusColor  = "\x1b[36m"
  constant SuccessColor (line 26) | SuccessColor = "\x1b[32m"
  constant ErrorColor (line 27) | ErrorColor   = "\x1b[31m"
  function DecorateText (line 31) | func DecorateText(s string, msgType MessageType) string {
  function FormatTime (line 48) | func FormatTime(d time.Duration) string {
  function HexToRGBA (line 71) | func HexToRGBA(x string) color.NRGBA {
  function RGB (line 99) | func RGB(r, g, b float32) color.NRGBA {
  function RGBA (line 104) | func RGBA(r, g, b, a float32) color.NRGBA {
  function HSLA (line 109) | func HSLA(h, s, l, a float32) color.NRGBA { return RGBA(hsla(h, s, l, a)) }
  function HSL (line 112) | func HSL(h, s, l float32) color.NRGBA { return HSLA(h, s, l, 1) }
  function hue (line 114) | func hue(v1, v2, h float32) float32 {
  function hsla (line 132) | func hsla(h, s, l, a float32) (r, g, b, ra float32) {
  function sat8 (line 155) | func sat8(v float32) uint8 {
  function mod32 (line 165) | func mod32(x, y float32) float32 { return float32(math.Mod(float64(x), f...

FILE: utils/spinner.go
  type Spinner (line 15) | type Spinner struct
    method Start (line 39) | func (s *Spinner) Start() {
    method Stop (line 68) | func (s *Spinner) Stop() {
    method RestoreCursor (line 81) | func (s *Spinner) RestoreCursor() {
    method clear (line 89) | func (s *Spinner) clear() {
  function NewSpinner (line 27) | func NewSpinner(msg string, d time.Duration) *Spinner {

FILE: utils/utils.go
  function Min (line 10) | func Min[T constraints.Ordered](values ...T) T {
  function Max (line 22) | func Max[T constraints.Ordered](values ...T) T {
  function Abs (line 34) | func Abs[T constraints.Signed | constraints.Float](x T) T {
  function Contains (line 42) | func Contains[T comparable](slice []T, value T) bool {
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (176K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 15,
    "preview": "github: esimov\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 1036,
    "preview": "---\nname: Bug report\nabout: Create a report to help me improve the library\nlabels: ''\n\n---\n\n### Describe the bug\n<!--A c"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 599,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\nlabels: ''\n\n---\n\n### Is your feature request related t"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1383,
    "preview": "name: build\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    branches: [master]\n\njobs:\n  build:\n    name: Build\n "
  },
  {
    "path": ".gitignore",
    "chars": 113,
    "preview": "*.jpg\n*.png\n*.jpeg\ncoverage.out\ntest-report.json\n/packages\n!/testdata/*.png\n!/testdata/*.jpg\n!/examples/**/*.png\n"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2018 Endre Simo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "Makefile",
    "chars": 243,
    "preview": "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\n"
  },
  {
    "path": "README.md",
    "chars": 12211,
    "preview": "<h1 align=\"center\"><img alt=\"Caire Logo\" src=\"https://user-images.githubusercontent.com/883386/51555990-a1762600-1e81-11"
  },
  {
    "path": "build.sh",
    "chars": 1451,
    "preview": "#!/bin/bash\nset -e\n\nVERSION=\"1.5.0\"\nPROTECTED_MODE=\"no\"\n\nexport GO15VENDOREXPERIMENT=1\n\ncd $(dirname \"${BASH_SOURCE[0]}\""
  },
  {
    "path": "carver.go",
    "chars": 10978,
    "preview": "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 \"gi"
  },
  {
    "path": "carver_benchmark_test.go",
    "chars": 697,
    "preview": "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 :"
  },
  {
    "path": "carver_test.go",
    "chars": 11732,
    "preview": "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/cai"
  },
  {
    "path": "cmd/caire/main.go",
    "chars": 2930,
    "preview": "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/"
  },
  {
    "path": "doc.go",
    "chars": 734,
    "preview": "/*\nPackage caire is a content aware image resize library, which can rescale the source image seamlessly\nboth vertically "
  },
  {
    "path": "draw.go",
    "chars": 2792,
    "preview": "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"
  },
  {
    "path": "exec.go",
    "chars": 9646,
    "preview": "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\""
  },
  {
    "path": "go.mod",
    "chars": 765,
    "preview": "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.c"
  },
  {
    "path": "go.sum",
    "chars": 4684,
    "preview": "eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=\neliasnaur.com/font"
  },
  {
    "path": "gui.go",
    "chars": 14152,
    "preview": "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\"gi"
  },
  {
    "path": "image.go",
    "chars": 6426,
    "preview": "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\"pat"
  },
  {
    "path": "image_test.go",
    "chars": 5368,
    "preview": "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"
  },
  {
    "path": "imop/blend.go",
    "chars": 4517,
    "preview": "// Package imop implements the Porter-Duff composition operations\r\n// used for mixing a graphic element with its backdro"
  },
  {
    "path": "imop/blend_test.go",
    "chars": 6258,
    "preview": "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"
  },
  {
    "path": "imop/comp.go",
    "chars": 11287,
    "preview": "// Package imop implements the Porter-Duff composition operations\n// used for mixing a graphic element with its backdrop"
  },
  {
    "path": "imop/comp_test.go",
    "chars": 4983,
    "preview": "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 T"
  },
  {
    "path": "preview.go",
    "chars": 641,
    "preview": "package caire\n\nimport (\n\t\"os\"\n)\n\n// showPreview spawns a new Gio GUI window and updates its content with the resized ima"
  },
  {
    "path": "processor.go",
    "chars": 14619,
    "preview": "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/im"
  },
  {
    "path": "processor_test.go",
    "chars": 2583,
    "preview": "package caire\n\nimport (\n\t\"image\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestResize_ShrinkImageWidth(t"
  },
  {
    "path": "snapcraft.yaml",
    "chars": 976,
    "preview": "name: caire\nversion: '1.4.4'\nsummary: Content aware image resize library\ndescription: |\n  Content aware image resize lib"
  },
  {
    "path": "sobel.go",
    "chars": 2278,
    "preview": "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"
  },
  {
    "path": "stackblur.go",
    "chars": 10411,
    "preview": "// Go implementation of the StackBlur algorithm\n// http://incubator.quasimondo.com/processing/fast_blur_deluxe.php\n\npack"
  },
  {
    "path": "utils/download.go",
    "chars": 2261,
    "preview": "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 downloa"
  },
  {
    "path": "utils/download_test.go",
    "chars": 946,
    "preview": "package utils\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestUtils_ShouldDownloadImage(t *testing.T) {\n\tf,"
  },
  {
    "path": "utils/format.go",
    "chars": 3588,
    "preview": "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 pl"
  },
  {
    "path": "utils/spinner.go",
    "chars": 2194,
    "preview": "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 initializ"
  },
  {
    "path": "utils/utils.go",
    "chars": 792,
    "preview": "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 p"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the esimov/caire GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (153.7 KB), approximately 52.7k tokens, and a symbol index with 197 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!