From d72ce031a7c4b7d8d7587f1d288dfb3bc5cd3097 Mon Sep 17 00:00:00 2001 From: Jeroen Bobbeldijk Date: Fri, 13 Oct 2023 10:57:13 +0200 Subject: [PATCH] Add libjpeg turbo --- .github/workflows/go.yml | 5 +- libheif_jpeg_turbo_test.go | 22 ++ libheif_no_jpeg_turbo_test.go | 22 ++ libheif_test.go | 38 ++-- library/library.go | 8 +- .../plugin/image_jpeg/image_jpeg_go.jpeg.go | 13 ++ .../image_jpeg/image_jpeg_go_jpeg_test.go | 38 ++++ .../plugin/image_jpeg/image_jpeg_turbojpeg.go | 198 ++++++++++++++++++ .../image_jpeg/image_jpeg_turbojpeg_test.go | 55 +++++ library/plugin/image_jpeg/options.go | 8 + library/plugin/plugin.go | 15 +- library/requests/requests.go | 8 +- 12 files changed, 405 insertions(+), 25 deletions(-) create mode 100644 libheif_jpeg_turbo_test.go create mode 100644 libheif_no_jpeg_turbo_test.go create mode 100644 library/plugin/image_jpeg/image_jpeg_go.jpeg.go create mode 100644 library/plugin/image_jpeg/image_jpeg_go_jpeg_test.go create mode 100644 library/plugin/image_jpeg/image_jpeg_turbojpeg.go create mode 100644 library/plugin/image_jpeg/image_jpeg_turbojpeg_test.go create mode 100644 library/plugin/image_jpeg/options.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 571bd48..c676dcb 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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 . \ No newline at end of file + go test ./... + go test -tags go_libheif_use_turbojpeg ./... \ No newline at end of file diff --git a/libheif_jpeg_turbo_test.go b/libheif_jpeg_turbo_test.go new file mode 100644 index 0000000..6de7325 --- /dev/null +++ b/libheif_jpeg_turbo_test.go @@ -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 +} diff --git a/libheif_no_jpeg_turbo_test.go b/libheif_no_jpeg_turbo_test.go new file mode 100644 index 0000000..142e4c4 --- /dev/null +++ b/libheif_no_jpeg_turbo_test.go @@ -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 +} diff --git a/libheif_test.go b/libheif_test.go index cbf996a..28152ee 100644 --- a/libheif_test.go +++ b/libheif_test.go @@ -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 { @@ -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) @@ -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) + } + } +} diff --git a/library/library.go b/library/library.go index 8489363..38d61c1 100644 --- a/library/library.go +++ b/library/library.go @@ -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) { @@ -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 } diff --git a/library/plugin/image_jpeg/image_jpeg_go.jpeg.go b/library/plugin/image_jpeg/image_jpeg_go.jpeg.go new file mode 100644 index 0000000..5c31c89 --- /dev/null +++ b/library/plugin/image_jpeg/image_jpeg_go.jpeg.go @@ -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) +} diff --git a/library/plugin/image_jpeg/image_jpeg_go_jpeg_test.go b/library/plugin/image_jpeg/image_jpeg_go_jpeg_test.go new file mode 100644 index 0000000..3591a94 --- /dev/null +++ b/library/plugin/image_jpeg/image_jpeg_go_jpeg_test.go @@ -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) + } +} diff --git a/library/plugin/image_jpeg/image_jpeg_turbojpeg.go b/library/plugin/image_jpeg/image_jpeg_turbojpeg.go new file mode 100644 index 0000000..4890900 --- /dev/null +++ b/library/plugin/image_jpeg/image_jpeg_turbojpeg.go @@ -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 +*/ +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 +} diff --git a/library/plugin/image_jpeg/image_jpeg_turbojpeg_test.go b/library/plugin/image_jpeg/image_jpeg_turbojpeg_test.go new file mode 100644 index 0000000..fc72b8a --- /dev/null +++ b/library/plugin/image_jpeg/image_jpeg_turbojpeg_test.go @@ -0,0 +1,55 @@ +//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() != 823 { + t.Fatalf("Encode resulted in wrong byte result, got %d, want %d", testWriter.Len(), 823) + } +} + +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() != 825 { + t.Fatalf("Encode resulted in wrong byte result, got %d, want %d", testWriter.Len(), 825) + } +} + +func TestEncodeProgressive(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, + }, + Progressive: true, + }) + if err != nil { + t.Fatalf("Encode resulted in error: %s", err.Error()) + } + if testWriter.Len() != 592 { + t.Fatalf("Encode resulted in wrong byte result, got %d, want %d", testWriter.Len(), 592) + } +} diff --git a/library/plugin/image_jpeg/options.go b/library/plugin/image_jpeg/options.go new file mode 100644 index 0000000..85f43b3 --- /dev/null +++ b/library/plugin/image_jpeg/options.go @@ -0,0 +1,8 @@ +package image_jpeg + +import "image/jpeg" + +type Options struct { + *jpeg.Options + Progressive bool // Render in progressive mode, only available with libturbojpeg. +} diff --git a/library/plugin/plugin.go b/library/plugin/plugin.go index 10d7d82..e4b58e0 100644 --- a/library/plugin/plugin.go +++ b/library/plugin/plugin.go @@ -14,6 +14,7 @@ import ( "image/jpeg" "image/png" + "github.com/klippa-app/go-libheif/library/plugin/image_jpeg" "github.com/klippa-app/go-libheif/library/requests" "github.com/klippa-app/go-libheif/library/responses" "github.com/klippa-app/go-libheif/library/shared" @@ -88,11 +89,19 @@ func (l *libHeifImplementation) RenderFile(request *requests.RenderFile) (*respo var imgBuf bytes.Buffer if request.OutputFormat == requests.RenderFileOutputFormatJPG { newFormat = "jpeg" - var opt jpeg.Options - opt.Quality = 95 + opt := image_jpeg.Options{ + Options: &jpeg.Options{ + Quality: 95, + }, + Progressive: request.Progressive, + } + + if request.OutputQuality > 0 { + opt.Options.Quality = request.OutputQuality + } for { - err := jpeg.Encode(&imgBuf, decodedImage, &opt) + err := image_jpeg.Encode(&imgBuf, decodedImage, opt) if err != nil { return nil, err } diff --git a/library/requests/requests.go b/library/requests/requests.go index f721ca7..0fe7b5e 100644 --- a/library/requests/requests.go +++ b/library/requests/requests.go @@ -16,7 +16,9 @@ const ( ) type RenderFile struct { - Data *[]byte // The file data. - 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 + Data *[]byte // The file data. + 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. }