Skip to content

Commit

Permalink
Merge pull request #107 from klippa-app/feature/improve-libjpeg-turbo
Browse files Browse the repository at this point in the history
Improve libjpegturbo
  • Loading branch information
jerbob92 authored Sep 28, 2023
2 parents 80ba3f0 + d422617 commit b688478
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go: [ "1.19", "1.20", "1.21" ]
go: [ "1.20", "1.21" ]
pdfium: [ "4849", "6029" ]
env:
PDFIUM_EXPERIMENTAL_VERSION: "6029"
Expand Down
59 changes: 59 additions & 0 deletions .github/workflows/libjpegturbo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: lib-jpeg-turbo

on:
push:
branches:
- main
- development
pull_request:
branches:
- main
- development

jobs:
test-libjpegturbo:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go: [ "1.20", "1.21" ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go }}

- name: Set up libturbojpeg library (Linux)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt install libturbojpeg libturbojpeg-dev
- name: Set up jpeg-turbo library (MacOS)
if: matrix.os == 'macos-latest'
run: |
brew install jpeg-turbo
- name: Set up jpeg-turbo library (Windows)
if: matrix.os == 'windows-latest'
run: |
curl -L https://master.dl.sourceforge.net/project/libjpeg-turbo/3.0.0/libjpeg-turbo-3.0.0-gcc64.exe -o libjpeg-turbo-3.0.0-gcc64.exe
./libjpeg-turbo-3.0.0-gcc64.exe /S
$Folder = 'C:\libjpeg-turbo-gcc64\lib\pkgconfig'
while (!(Test-Path -Path $Folder)) {
"libjpeg-turbo does not exist yet!"
Start-Sleep -s 5
}
- name: Test package (non-Windows)
if: matrix.os != 'windows-latest'
run: |
go test ./internal/image/image_jpeg -tags pdfium_use_turbojpeg -v
- name: Test package (Windows)
if: matrix.os == 'windows-latest'
run: |
$env:PKG_CONFIG_PATH = 'C:\libjpeg-turbo-gcc64\lib\pkgconfig'
go test ./internal/image/image_jpeg -tags pdfium_use_turbojpeg -v
4 changes: 2 additions & 2 deletions internal/image/image_jpeg/image_jpeg_go.jpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ import (
"io"
)

func Encode(w io.Writer, m *image.RGBA, o *jpeg.Options) error {
return jpeg.Encode(w, m, o)
func Encode(w io.Writer, m *image.RGBA, o Options) error {
return jpeg.Encode(w, m, o.Options)
}
38 changes: 38 additions & 0 deletions internal/image/image_jpeg/image_jpeg_go_jpeg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build !pdfium_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)
}
}
120 changes: 113 additions & 7 deletions internal/image/image_jpeg/image_jpeg_turbojpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,118 @@ import (
"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

"github.com/bmharper/turbo"
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
)

func Encode(w io.Writer, m *image.RGBA, o *jpeg.Options) error {
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.RGBA, o Options) error {
imageWriter := bufio.NewWriter(w)

// Clip quality to [1, 100].
quality := jpeg.DefaultQuality
if o != nil {
quality = o.Quality
if o.Options != nil {
quality = o.Options.Quality
if quality < 1 {
quality = 1
} else if quality > 100 {
Expand All @@ -27,15 +128,20 @@ func Encode(w io.Writer, m *image.RGBA, o *jpeg.Options) error {

dimensions := m.Bounds().Size()

raw := turbo.Image{
raw := Image{
Width: dimensions.X,
Height: dimensions.Y,
Stride: m.Stride,
Pixels: m.Pix,
}

params := turbo.MakeCompressParams(turbo.PixelFormatRGBA, turbo.Sampling420, quality, 0)
jpg, err := turbo.Compress(&raw, params)
flags := Flags(0)
if o.Progressive {
flags |= FlagProgressive
}

params := MakeCompressParams(PixelFormatRGBA, Sampling420, quality, flags)
jpg, err := Compress(&raw, params)
if err != nil {
return err
}
Expand Down
55 changes: 55 additions & 0 deletions internal/image/image_jpeg/image_jpeg_turbojpeg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//go:build pdfium_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)
}
}
8 changes: 8 additions & 0 deletions internal/image/image_jpeg/options.go
Original file line number Diff line number Diff line change
@@ -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.
}
12 changes: 8 additions & 4 deletions internal/implementation_cgo/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,11 +481,15 @@ func (p *PdfiumImplementation) RenderToFile(request *requests.RenderToFile) (*re
var imgBuf bytes.Buffer

if request.OutputFormat == requests.RenderToFileOutputFormatJPG {
var opt jpeg.Options
opt.Quality = 95
opt := image_jpeg.Options{
Options: &jpeg.Options{
Quality: 95,
},
Progressive: request.Progressive,
}

if request.OutputQuality > 0 {
opt.Quality = request.OutputQuality
opt.Options.Quality = request.OutputQuality
}

// If any of the pages have transparency, place a white background under
Expand All @@ -501,7 +505,7 @@ func (p *PdfiumImplementation) RenderToFile(request *requests.RenderToFile) (*re
}

for {
err := image_jpeg.Encode(&imgBuf, renderedImage, &opt)
err := image_jpeg.Encode(&imgBuf, renderedImage, opt)
if err != nil {
return nil, err
}
Expand Down
11 changes: 7 additions & 4 deletions internal/implementation_webassembly/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,11 +536,14 @@ func (p *PdfiumImplementation) RenderToFile(request *requests.RenderToFile) (*re
var imgBuf bytes.Buffer

if request.OutputFormat == requests.RenderToFileOutputFormatJPG {
var opt jpeg.Options
opt.Quality = 95
opt := image_jpeg.Options{
Options: &jpeg.Options{
Quality: 95,
},
}

if request.OutputQuality > 0 {
opt.Quality = request.OutputQuality
opt.Options.Quality = request.OutputQuality
}

// If any of the pages have transparency, place a white background under
Expand All @@ -556,7 +559,7 @@ func (p *PdfiumImplementation) RenderToFile(request *requests.RenderToFile) (*re
}

for {
err := image_jpeg.Encode(&imgBuf, renderedImage, &opt)
err := image_jpeg.Encode(&imgBuf, renderedImage, opt)
if err != nil {
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions requests/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type RenderToFile struct {
OutputFormat RenderToFileOutputFormat // The format to output the image as
OutputTarget RenderToFileOutputTarget // Where to output the image
OutputQuality int // Only used when OutputFormat RenderToFileOutputFormatJPG. Ranges from 1 to 100 inclusive, higher is better. The default is 95.
Progressive bool // Only used when OutputFormat RenderToFileOutputFormatJPG and with build tag pdfium_use_turbojpeg. Will render a progressive jpeg.
MaxFileSize int64 // The maximum file size, when OutputFormat RenderToFileOutputFormatJPG, it will try to lower the quality it until it fits.
TargetFilePath string // When OutputTarget is file, the path to write it to, if not given, a temp file is created
}

0 comments on commit b688478

Please sign in to comment.