Skip to content

Commit

Permalink
Add libjpeg turbo
Browse files Browse the repository at this point in the history
  • Loading branch information
jerbob92 committed Oct 13, 2023
1 parent d57ba7e commit d72ce03
Show file tree
Hide file tree
Showing 12 changed files with 405 additions and 25 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ jobs:
sudo apt-get update && sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:strukturag/libde265
sudo add-apt-repository -y ppa:strukturag/libheif
sudo apt-get update && sudo apt-get install -y libheif-dev
sudo apt-get update && sudo apt-get install -y libheif-dev libturbojpeg libturbojpeg-dev
- name: Test
run: |
go test .
go test ./...
go test -tags go_libheif_use_turbojpeg ./...
22 changes: 22 additions & 0 deletions libheif_jpeg_turbo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//go:build go_libheif_use_turbojpeg

package libheif

import (
"github.com/klippa-app/go-libheif/library"
_ "image/jpeg"
_ "image/png"
)

func initLib() error {
err := Init(Config{LibraryConfig: library.Config{
Command: library.Command{
BinPath: "go",
Args: []string{"run", "-tags", "go_libheif_use_turbojpeg", "library/worker_example/main.go"},
},
}})
if err != nil {
return err
}
return nil
}
22 changes: 22 additions & 0 deletions libheif_no_jpeg_turbo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//go:build !go_libheif_use_turbojpeg

package libheif

import (
"github.com/klippa-app/go-libheif/library"
_ "image/jpeg"
_ "image/png"
)

func initLib() error {
err := Init(Config{LibraryConfig: library.Config{
Command: library.Command{
BinPath: "go",
Args: []string{"run", "library/worker_example/main.go"},
},
}})
if err != nil {
return err
}
return nil
}
38 changes: 24 additions & 14 deletions libheif_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,6 @@ import (
"github.com/klippa-app/go-libheif/library"
)

func initLib() error {
err := Init(Config{LibraryConfig: library.Config{
Command: library.Command{
BinPath: "go",
Args: []string{"run", "library/worker_example/main.go"},
},
}})
if err != nil {
return err
}
return nil
}

func TestFormatRegistered(t *testing.T) {
err := initLib()
if err != nil {
Expand Down Expand Up @@ -162,7 +149,7 @@ func TestRenderPNG(t *testing.T) {
}
}

func Benchmark(b *testing.B) {
func BenchmarkDecode(b *testing.B) {
err := initLib()
if err != nil {
b.Fatal(err)
Expand All @@ -185,3 +172,26 @@ func Benchmark(b *testing.B) {
r.Seek(0, io.SeekStart)
}
}

func BenchmarkRender(b *testing.B) {
err := initLib()
if err != nil {
b.Fatal(err)
}

data, err := os.ReadFile("testdata/camel.heic")
if err != nil {
b.Fatal(err)
}

b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err = library.RenderFile(&data, library.RenderOptions{
OutputFormat: library.RenderFileOutputFormatJPG,
})
if err != nil {
b.Fatal(err)
}
}
}
8 changes: 5 additions & 3 deletions library/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,10 @@ const (
)

type RenderOptions struct {
OutputFormat RenderFileOutputFormat // The format to output the image as
MaxFileSize int64 // The maximum filesize, if jpg is chosen as output format, it will try to compress it until it fits
OutputFormat RenderFileOutputFormat // The format to output the image as
MaxFileSize int64 // Only used when OutputFormat RenderFileOutputFormatJPG. The maximum filesize, if jpg is chosen as output format, it will try to lower the quality it until it fits.
OutputQuality int // Only used when OutputFormat RenderFileOutputFormatJPG. Ranges from 1 to 100 inclusive, higher is better. The default is 95.
Progressive bool // Only used when OutputFormat RenderFileOutputFormatJPG and with build tag go_libheif_use_turbojpeg. Will render a progressive jpeg.
}

func RenderFile(data *[]byte, options RenderOptions) (*responses.RenderFile, error) {
Expand All @@ -162,7 +164,7 @@ func RenderFile(data *[]byte, options RenderOptions) (*responses.RenderFile, err
return nil, errors.New("could not check or start plugin")
}

resp, err := libheifplugin.RenderFile(&requests.RenderFile{Data: data, OutputFormat: requests.RenderFileOutputFormat(options.OutputFormat), MaxFileSize: options.MaxFileSize})
resp, err := libheifplugin.RenderFile(&requests.RenderFile{Data: data, OutputFormat: requests.RenderFileOutputFormat(options.OutputFormat), MaxFileSize: options.MaxFileSize, OutputQuality: options.OutputQuality, Progressive: options.Progressive})
if err != nil {
return nil, err
}
Expand Down
13 changes: 13 additions & 0 deletions library/plugin/image_jpeg/image_jpeg_go.jpeg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build !go_libheif_use_turbojpeg

package image_jpeg

import (
"image"
"image/jpeg"
"io"
)

func Encode(w io.Writer, m image.Image, o Options) error {
return jpeg.Encode(w, m, o.Options)
}
38 changes: 38 additions & 0 deletions library/plugin/image_jpeg/image_jpeg_go_jpeg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build !go_libheif_use_turbojpeg

package image_jpeg

import (
"bytes"
"image"
"image/jpeg"
"testing"
)

func TestEncode(t *testing.T) {
img := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{100, 100}})
testWriter := bytes.NewBuffer(nil)
err := Encode(testWriter, img, Options{})
if err != nil {
t.Fatalf("Encode resulted in error: %s", err.Error())
}
if testWriter.Len() != 789 {
t.Fatalf("Encode resulted in wrong byte result, got %d, want %d", testWriter.Len(), 789)
}
}

func TestEncodeQuality(t *testing.T) {
img := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{100, 100}})
testWriter := bytes.NewBuffer(nil)
err := Encode(testWriter, img, Options{
Options: &jpeg.Options{
Quality: 100,
},
})
if err != nil {
t.Fatalf("Encode resulted in error: %s", err.Error())
}
if testWriter.Len() != 791 {
t.Fatalf("Encode resulted in wrong byte result, got %d, want %d", testWriter.Len(), 791)
}
}
198 changes: 198 additions & 0 deletions library/plugin/image_jpeg/image_jpeg_turbojpeg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//go:build go_libheif_use_turbojpeg

package image_jpeg

import (
"bufio"
"image"
"image/jpeg"
"io"
"unsafe"
)

/*
#cgo pkg-config: libturbojpeg
#include <turbojpeg.h>
*/
import "C"
import "fmt"

type Sampling C.int

const (
Sampling444 Sampling = C.TJSAMP_444
Sampling422 Sampling = C.TJSAMP_422
Sampling420 Sampling = C.TJSAMP_420
SamplingGray Sampling = C.TJSAMP_GRAY
)

type PixelFormat C.int

const (
PixelFormatRGB PixelFormat = C.TJPF_RGB
PixelFormatBGR PixelFormat = C.TJPF_BGR
PixelFormatRGBX PixelFormat = C.TJPF_RGBX
PixelFormatBGRX PixelFormat = C.TJPF_BGRX
PixelFormatXBGR PixelFormat = C.TJPF_XBGR
PixelFormatXRGB PixelFormat = C.TJPF_XRGB
PixelFormatGRAY PixelFormat = C.TJPF_GRAY
PixelFormatRGBA PixelFormat = C.TJPF_RGBA
PixelFormatBGRA PixelFormat = C.TJPF_BGRA
PixelFormatABGR PixelFormat = C.TJPF_ABGR
PixelFormatARGB PixelFormat = C.TJPF_ARGB
PixelFormatCMYK PixelFormat = C.TJPF_CMYK
PixelFormatUNKNOWN PixelFormat = C.TJPF_UNKNOWN
)

type Flags C.int

const (
FlagAccurateDCT Flags = C.TJFLAG_ACCURATEDCT
FlagBottomUp Flags = C.TJFLAG_BOTTOMUP
FlagFastDCT Flags = C.TJFLAG_FASTDCT
FlagFastUpsample Flags = C.TJFLAG_FASTUPSAMPLE
FlagNoRealloc Flags = C.TJFLAG_NOREALLOC
FlagProgressive Flags = C.TJFLAG_PROGRESSIVE
FlagStopOnWarning Flags = C.TJFLAG_STOPONWARNING
)

func makeError(handler C.tjhandle, returnVal C.int) error {
if returnVal == 0 {
return nil
}
str := C.GoString(C.tjGetErrorStr2(handler))
return fmt.Errorf("turbojpeg error: %v", str)
}

type Image struct {
Width int
Height int
Stride int
Pixels []byte
}

type CompressParams struct {
PixelFormat PixelFormat
Sampling Sampling
Quality int // 1 .. 100
Flags Flags
}

func MakeCompressParams(pixelFormat PixelFormat, sampling Sampling, quality int, flags Flags) CompressParams {
return CompressParams{
PixelFormat: pixelFormat,
Sampling: sampling,
Quality: quality,
Flags: flags,
}
}

func Compress(img *Image, params CompressParams) ([]byte, error) {
encoder := C.tjInitCompress()
defer C.tjDestroy(encoder)

var outBuf *C.uchar
var outBufSize C.ulong

// int tjCompress2(tjhandle handle, const unsigned char *srcBuf, int width, int pitch, int height, int pixelFormat,
// unsigned char **jpegBuf, unsigned long *jpegSize, int jpegSubsamp, int jpegQual, int flags);
res := C.tjCompress2(encoder, (*C.uchar)(&img.Pixels[0]), C.int(img.Width), C.int(img.Stride), C.int(img.Height), C.int(params.PixelFormat),
&outBuf, &outBufSize, C.int(params.Sampling), C.int(params.Quality), C.int(params.Flags))

var enc []byte
err := makeError(encoder, res)
if outBuf != nil {
enc = C.GoBytes(unsafe.Pointer(outBuf), C.int(outBufSize))
C.tjFree(outBuf)
}

if err != nil {
return nil, err
}
return enc, nil
}

func Encode(w io.Writer, m image.Image, o Options) error {
imageWriter := bufio.NewWriter(w)

// Clip quality to [1, 100].
quality := jpeg.DefaultQuality
if o.Options != nil {
quality = o.Options.Quality
if quality < 1 {
quality = 1
} else if quality > 100 {
quality = 100
}
}

raw := FromImage(m, true)

flags := Flags(0)
if o.Progressive {
flags |= FlagProgressive
}

params := MakeCompressParams(PixelFormatRGBA, Sampling420, quality, flags)
jpg, err := Compress(raw, params)
if err != nil {
return err
}

_, err = imageWriter.Write(jpg)
if err != nil {
return err
}

err = imageWriter.Flush()
if err != nil {
return err
}

return nil
}

// Convert a Go image.Image into a turbo.Image
// If allowDeepClone is true, and the source image is type NRGBA or RGBA,
// then the resulting Image points directly to the pixel buffer of the source image.
func FromImage(src image.Image, allowDeepClone bool) *Image {
dst := &Image{
Width: src.Bounds().Dx(),
Height: src.Bounds().Dy(),
Stride: src.Bounds().Dx() * 4,
}
switch v := src.(type) {
case *image.RGBA:
if allowDeepClone {
dst.Pixels = v.Pix
} else {
dst.Pixels = make([]byte, dst.Stride*dst.Height)
copy(dst.Pixels, v.Pix)
}
return dst
case *image.NRGBA:
if allowDeepClone {
dst.Pixels = v.Pix
} else {
dst.Pixels = make([]byte, dst.Stride*dst.Height)
copy(dst.Pixels, v.Pix)
}
return dst
}

// This must be super slow - I haven't tested
dst.Pixels = make([]byte, dst.Stride*dst.Height)
p := 0
for y := 0; y < dst.Height; y++ {
for x := 0; x < dst.Width; x++ {
r, g, b, a := src.At(x, y).RGBA()
dst.Pixels[p] = byte(r >> 8)
dst.Pixels[p+1] = byte(g >> 8)
dst.Pixels[p+2] = byte(b >> 8)
dst.Pixels[p+3] = byte(a >> 8)
p += 4
}
}

return dst
}
Loading

0 comments on commit d72ce03

Please sign in to comment.