diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ca88f0ce..c288343d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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" diff --git a/.github/workflows/libjpegturbo.yml b/.github/workflows/libjpegturbo.yml new file mode 100644 index 00000000..82ca74a9 --- /dev/null +++ b/.github/workflows/libjpegturbo.yml @@ -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 diff --git a/internal/image/image_jpeg/image_jpeg_go.jpeg.go b/internal/image/image_jpeg/image_jpeg_go.jpeg.go index 018e42ff..789e7a35 100644 --- a/internal/image/image_jpeg/image_jpeg_go.jpeg.go +++ b/internal/image/image_jpeg/image_jpeg_go.jpeg.go @@ -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) } diff --git a/internal/image/image_jpeg/image_jpeg_go_jpeg_test.go b/internal/image/image_jpeg/image_jpeg_go_jpeg_test.go new file mode 100644 index 00000000..f634f74b --- /dev/null +++ b/internal/image/image_jpeg/image_jpeg_go_jpeg_test.go @@ -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) + } +} diff --git a/internal/image/image_jpeg/image_jpeg_turbojpeg.go b/internal/image/image_jpeg/image_jpeg_turbojpeg.go index 60091cba..62d5653d 100644 --- a/internal/image/image_jpeg/image_jpeg_turbojpeg.go +++ b/internal/image/image_jpeg/image_jpeg_turbojpeg.go @@ -7,17 +7,118 @@ import ( "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 - "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 { @@ -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 } diff --git a/internal/image/image_jpeg/image_jpeg_turbojpeg_test.go b/internal/image/image_jpeg/image_jpeg_turbojpeg_test.go new file mode 100644 index 00000000..3c96e126 --- /dev/null +++ b/internal/image/image_jpeg/image_jpeg_turbojpeg_test.go @@ -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) + } +} diff --git a/internal/image/image_jpeg/options.go b/internal/image/image_jpeg/options.go new file mode 100644 index 00000000..85f43b35 --- /dev/null +++ b/internal/image/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/internal/implementation_cgo/render.go b/internal/implementation_cgo/render.go index 3492b62d..06faded9 100644 --- a/internal/implementation_cgo/render.go +++ b/internal/implementation_cgo/render.go @@ -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 @@ -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 } diff --git a/internal/implementation_webassembly/render.go b/internal/implementation_webassembly/render.go index 28bbb196..d469c79b 100644 --- a/internal/implementation_webassembly/render.go +++ b/internal/implementation_webassembly/render.go @@ -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 @@ -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 } diff --git a/requests/render.go b/requests/render.go index e20b0e4c..37e00d90 100644 --- a/requests/render.go +++ b/requests/render.go @@ -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 }