diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..81c33fa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..20acf4a --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,41 @@ +name: Test Coverage +run-name: Test Coverage + +on: + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Go 1.21 + uses: actions/setup-go@v2 + with: + go-version: 1.21 + + - name: Generate Test Coverage + run: go test ./... -coverprofile=./cover.out + + - name: Check Test Coverage + id: coverage + uses: vladopajic/go-test-coverage@v2 + with: + config: ./.testcoverage.yml + + - name: Make Coverage Badge + uses: action-badges/core@0.2.2 + if: contains(github.ref, 'master') + with: + label: coverage + message: ${{ steps.coverage.outputs.badge-text }} + message-color: ${{ steps.coverage.outputs.badge-color }} + file-name: coverage.svg + badge-branch: badges ## orphan branch where badge will be committed + github-token: "${{ secrets.GH_PAT }}" \ No newline at end of file diff --git a/.github/workflows/security-check.yml b/.github/workflows/security-check.yml new file mode 100644 index 0000000..c1589b3 --- /dev/null +++ b/.github/workflows/security-check.yml @@ -0,0 +1,18 @@ +name: "Security Check" + +on: + pull_request: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + env: + GO111MODULE: on + steps: + - name: Checkout Source + uses: actions/checkout@v3 + - name: Run Gosec Security Scanner + uses: securego/gosec@master + with: + args: './...' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfe2dab --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Go workspace file +go.work + +# Build output +build/ +dist/ + +# IDE files +.idea/ +.vscode/ \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..e69de29 diff --git a/.testcoverage.yml b/.testcoverage.yml new file mode 100644 index 0000000..2a6ae5b --- /dev/null +++ b/.testcoverage.yml @@ -0,0 +1,9 @@ +profile: cover.out +local-prefix: "github.com/ryanbekhen/go-webp" +threshold: + file: 60 + package: 80 + total: 80 +exclude: + paths: + - \.pb\.go$ # excludes all protobuf generated files \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..126c22a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Achmad Irianto Eka Putra + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..69b049f --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# GO WebP + +This is a simple library Go to convert images to WebP format using the [libwebp](https://developers.google.com/speed/webp/docs/api) library. + +## Development Requirements + +- Go 1.22 +- libwebp-dev + +Before running the project, you need to install the libwebp-dev package. You can install it by running the following command: + +```bash +# Ubuntu +sudo apt-get install libwebp-dev +``` + +## Installation + +To install the library, you need to run the following command: + +```bash +go get github.com/ryanbekhen/go-webp +``` + +## Usage + +To use the library, you need to run the following command: + +```go +package main + +import ( + "github.com/ryanbekhen/go-webp" + "image" + _ "image/jpeg" + _ "image/png" + "os" +) + +func main() { + input := "img.png" + output := "img.webp" + + // Open the input image file. + imgFile, err := os.Open(input) + if err != nil { + panic(err) + } + defer imgFile.Close() + + // Decode the input image file. + img, _, err := image.Decode(imgFile) + if err != nil { + panic(err) + } + + // Create the output image file. + outFile, err := os.Create(output) + if err != nil { + panic(err) + } + defer outFile.Close() + + // Encode the input image file. + err = webp.Encode(img, 75, outFile) + if err != nil { + panic(err) + } +} +``` + +> Note: The quality parameter is a number between 0 and 100. +> import _ "image/jpeg" and import _ "image/png" are required to decode the input image file. + +## Contributing + +Contributions are welcome! For feature requests and bug reports please [submit an issue](https://github.com/ryanbekhen/go-webp/issues). + +## License + +This project is licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..205478e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/ryanbekhen/go-webp + +go 1.22 diff --git a/webp.go b/webp.go new file mode 100644 index 0000000..81cf630 --- /dev/null +++ b/webp.go @@ -0,0 +1,81 @@ +package webp + +/* +#cgo LDFLAGS: -lwebp +#include +#include + +int encodeWebP(uint8_t* img, int width, int height, int stride, float quality_factor, uint8_t** output, size_t* output_size) { + WebPConfig config; + if (!WebPConfigInit(&config)) { + return 0; + } + config.quality = quality_factor; + if (!WebPValidateConfig(&config)) { + return 0; + } + *output_size = WebPEncodeRGBA(img, width, height, stride, quality_factor, output); + return 1; +} +*/ +import "C" +import ( + "errors" + "image" + "io" + "unsafe" +) + +// Encode encodes the input image in the WebP format with the specified quality and writes it to the given destination writer. +// The function converts the image into a byte slice and calls a C function to perform the encoding. +// The resulting encoded data is written to the destination writer. +// If there are any errors during the encoding or writing process, an error is returned. +// Note: The C function is not included in this code snippet, it must be implemented separately. +// Example usage: +// +// err := webp.Encode(img, 75, w) +// if err != nil { +// log.Fatal(err) +// } +func Encode(img image.Image, quality float32, dest io.Writer) error { + if img == nil { + return errors.New("img must not be nil") + } + + bounds := img.Bounds() + width, height := bounds.Dx(), bounds.Dy() + + output := make([]byte, 4*width*height) + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + r, g, b, a := img.At(x, y).RGBA() + index := y*width*4 + x*4 + output[index+0] = byte(r >> 8) + output[index+1] = byte(g >> 8) + output[index+2] = byte(b >> 8) + output[index+3] = byte(a >> 8) + } + } + + // Convert the go byte slice to c array. + cArray := (*C.uint8_t)(C.CBytes(output)) + defer C.free(unsafe.Pointer(cArray)) + + var outputSize C.size_t + result := C.encodeWebP(cArray, C.int(width), C.int(height), C.int(width*4), C.float(quality), &cArray, &outputSize) + + if result == 0 { + return errors.New("failed to encode image to webp") + } + + // Get the bytes, don't forget to free it after getting the bytes. + encodedData := C.GoBytes(unsafe.Pointer(cArray), C.int(outputSize)) + C.free(unsafe.Pointer(cArray)) + + _, err := dest.Write(encodedData) + if err != nil { + return errors.New("failed to write encoded data") + } + + return nil +} diff --git a/webp_test.go b/webp_test.go new file mode 100644 index 0000000..947ade8 --- /dev/null +++ b/webp_test.go @@ -0,0 +1,87 @@ +package webp + +import ( + "bytes" + "image" + "image/color" + "testing" +) + +func TestEncode(t *testing.T) { + tests := []struct { + name string + img image.Image + quality float32 + }{ + { + name: "empty image", + img: image.NewRGBA(image.Rect(0, 0, 0, 0)), + quality: float32(75.0), + }, + { + name: "small image", + img: image.NewRGBA(image.Rect(0, 0, 2, 2)), + quality: float32(75.0), + }, + { + name: "large image", + img: randomImage(500, 500), + quality: float32(75.0), + }, + { + name: "high quality", + img: randomImage(100, 100), + quality: float32(100.0), + }, + { + name: "low quality", + img: randomImage(100, 100), + quality: float32(10.0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := &bytes.Buffer{} + err := Encode(tt.img, tt.quality, buf) + if err != nil { + t.Fatalf("Encode() error = %v, wantErr = %v", err, false) + } + }) + } +} + +func TestEncode_EmptyImage(t *testing.T) { + tests := []struct { + name string + img image.Image + quality float32 + }{ + { + name: "nil image", + img: nil, + quality: float32(75.0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := &bytes.Buffer{} + err := Encode(tt.img, tt.quality, buf) + if err == nil { + t.Fatalf("Encode() error = %v, wantErr = %v", err, true) + } + }) + } +} + +// This function generates an image with random colors +func randomImage(width, height int) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, width, height)) + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + img.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), B: uint8((x + y) % 256), A: 255}) + } + } + return img +}