Skip to content

Commit

Permalink
webp-near-lossless (#31)
Browse files Browse the repository at this point in the history
* Remember target profile in OptimizeICCProfile, set that profile later when saving webp.

* Move ICC optimization logic from C to go, save optimized profile as go variable

* Add golden file

* Add "near_lossless" parameter for saving webp
  • Loading branch information
alon-ne authored Nov 9, 2020
1 parent 8f11fc3 commit 466b65a
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 79 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ build/
*.failed.*
.vscode
.idea
/vips/vips.test
Binary file not shown.
Binary file not shown.
Binary file added resources/has-icc-profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 12 additions & 25 deletions vips/color.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#include "icc_profiles.h"
#include <unistd.h>

#define SRGB_PROFILE_PATH SRGB_V2_MICRO_ICC_PATH
#define GRAY_PROFILE_PATH SGRAY_V2_MICRO_ICC_PATH

int is_colorspace_supported(VipsImage *in) {
return vips_colourspace_issupported(in) ? 1 : 0;
}
Expand All @@ -11,29 +14,13 @@ int to_colorspace(VipsImage *in, VipsImage **out, VipsInterpretation space) {
}

// https://libvips.github.io/libvips/API/8.6/libvips-colour.html#vips-icc-transform
int optimize_icc_profile(VipsImage *in, VipsImage **out, int isCmyk) {
// todo: check current embedded profile, and skip if already set

char *srgb_profile_path = SRGB_V2_MICRO_ICC_PATH;
char *gray_profile_path = SGRAY_V2_MICRO_ICC_PATH;

int channels = vips_image_get_bands(in);
int result;

if (channels > 2) {
if (isCmyk == 1) {
result = vips_icc_transform(in, out, srgb_profile_path, "input_profile", "cmyk", "intent", VIPS_INTENT_PERCEPTUAL, NULL);
} else {
result = vips_icc_transform(in, out, srgb_profile_path, "embedded", TRUE, "intent", VIPS_INTENT_PERCEPTUAL, NULL);
// ignore embedded errors
if (result != 0) {
result = 0;
*out = in;
}
}
} else {
result = vips_icc_transform(in, out, gray_profile_path, "input_profile", gray_profile_path, "embedded", TRUE, "intent", VIPS_INTENT_PERCEPTUAL, NULL);
}

return result;
int icc_transform(VipsImage *in, VipsImage **out, const char *output_profile, const char *input_profile, VipsIntent intent,
int depth, gboolean embedded) {
return vips_icc_transform(
in, out, output_profile,
"input_profile", input_profile ? input_profile : "none",
"intent", intent,
"depth", depth ? depth : 8,
"embedded", embedded,
NULL);
}
36 changes: 31 additions & 5 deletions vips/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package vips
// #cgo pkg-config: vips
// #include "color.h"
import "C"
import "unsafe"

// Color represents an RGB
type Color struct {
Expand Down Expand Up @@ -37,6 +38,18 @@ const (
InterpretationHSV Interpretation = C.VIPS_INTERPRETATION_HSV
)

// Interpretation represents VIPS_INTENT type
type Intent int

//Intent enum
const (
IntentPerceptual Intent = C.VIPS_INTENT_PERCEPTUAL
IntentRelative Intent = C.VIPS_INTENT_RELATIVE
IntentSaturation Intent = C.VIPS_INTENT_SATURATION
IntentAbsolute Intent = C.VIPS_INTENT_ABSOLUTE
IntentLast Intent = C.VIPS_INTENT_LAST
)

func vipsIsColorSpaceSupported(in *C.VipsImage) bool {
return C.is_colorspace_supported(in) == 1
}
Expand All @@ -46,19 +59,32 @@ func vipsToColorSpace(in *C.VipsImage, interpretation Interpretation) (*C.VipsIm
incOpCounter("to_colorspace")
var out *C.VipsImage

inter := C.VipsInterpretation(interpretation)

if err := C.to_colorspace(in, &out, inter); err != 0 {
if res := C.to_colorspace(in, &out, C.VipsInterpretation(interpretation)); res != 0 {
return nil, handleImageError(out)
}

return out, nil
}

func vipsOptimizeICCProfile(in *C.VipsImage, isCmyk int) (*C.VipsImage, error) {
func vipsICCTransform(in *C.VipsImage, outputProfile string, inputProfile string, intent Intent, depth int,
embedded bool) (*C.VipsImage, error) {
var out *C.VipsImage
var cInputProfile *C.char
var cEmbedded C.gboolean

cOutputProfile := C.CString(outputProfile)
defer C.free(unsafe.Pointer(cOutputProfile))

if inputProfile != "" {
cInputProfile = C.CString(inputProfile)
defer C.free(unsafe.Pointer(cInputProfile))
}

if embedded {
cEmbedded = C.TRUE
}

if res := int(C.optimize_icc_profile(in, &out, C.int(isCmyk))); res != 0 {
if res := C.icc_transform(in, &out, cOutputProfile, cInputProfile, C.VipsIntent(intent), C.int(depth), cEmbedded); res != 0 {
return nil, handleImageError(out)
}

Expand Down
3 changes: 2 additions & 1 deletion vips/color.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
int is_colorspace_supported(VipsImage *in);
int to_colorspace(VipsImage *in, VipsImage **out, VipsInterpretation space);

int optimize_icc_profile(VipsImage *in, VipsImage **out, int isCmyk);
int icc_transform(VipsImage *in, VipsImage **out, const char *output_profile, const char *input_profile,
VipsIntent intent, int depth, gboolean embedded);
6 changes: 5 additions & 1 deletion vips/foreign.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "lang.h"
#include "foreign.h"
#include "icc_profiles.h"

int load_image_buffer(void *buf, size_t len, int imageType, VipsImage **out) {
int code = 1;
Expand Down Expand Up @@ -55,12 +56,15 @@ int save_png_buffer(VipsImage *in, void **buf, size_t *len, int strip, int compr
// todo: support additional params
// https://github.com/libvips/libvips/blob/master/libvips/foreign/webpsave.c#L524
// https://libvips.github.io/libvips/API/current/VipsForeignSave.html#vips-webpsave-buffer
int save_webp_buffer(VipsImage *in, void **buf, size_t *len, int strip, int quality, int lossless, int effort) {
int save_webp_buffer(VipsImage *in, void **buf, size_t *len, int strip, int quality, int lossless, int near_lossless,
int effort, const char *profile) {
return vips_webpsave_buffer(in, buf, len,
"strip", INT_TO_GBOOLEAN(strip),
"Q", quality,
"lossless", INT_TO_GBOOLEAN(lossless),
"near_lossless", INT_TO_GBOOLEAN(near_lossless),
"reduction_effort", effort,
"profile", profile ? profile : "none",
NULL
);
}
Expand Down
18 changes: 11 additions & 7 deletions vips/foreign.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,17 +237,21 @@ func vipsSavePNGToBuffer(in *C.VipsImage, stripMetadata bool, compression int, i
return toBuff(ptr, cLen), nil
}

func vipsSaveWebPToBuffer(in *C.VipsImage, stripMetadata bool, quality int, lossless bool, effort int) ([]byte, error) {
func vipsSaveWebPToBuffer(in *C.VipsImage, stripMetadata bool, quality int, lossless bool, nearLossless bool,
effort int, profile string) ([]byte, error) {
incOpCounter("save_webp_buffer")
var ptr unsafe.Pointer
cLen := C.size_t(0)
var cProfile *C.char
var cLen C.size_t

strip := C.int(boolToInt(stripMetadata))
qual := C.int(quality)
loss := C.int(boolToInt(lossless))
eff := C.int(effort)
if profile != "" {
cProfile = C.CString(profile)
defer C.free(unsafe.Pointer(cProfile))
}

if err := C.save_webp_buffer(in, &ptr, &cLen, strip, qual, loss, eff); err != 0 {
if err := C.save_webp_buffer(
in, &ptr, &cLen, C.int(boolToInt(stripMetadata)), C.int(quality), C.int(boolToInt(lossless)),
C.int(boolToInt(nearLossless)), C.int(effort), cProfile); err != 0 {
return nil, handleSaveBufferError(ptr)
}

Expand Down
3 changes: 2 additions & 1 deletion vips/foreign.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ int load_image_buffer(void *buf, size_t len, int imageType, VipsImage **out);

int save_jpeg_buffer(VipsImage* image, void **buf, size_t *len, int strip, int quality, int interlace);
int save_png_buffer(VipsImage *in, void **buf, size_t *len, int strip, int compression, int interlace);
int save_webp_buffer(VipsImage *in, void **buf, size_t *len, int strip, int quality, int lossless, int effort);
int save_webp_buffer(VipsImage *in, void **buf, size_t *len, int strip, int quality, int lossless, int near_lossless,
int effort, const char *profile);
int save_heif_buffer(VipsImage *in, void **buf, size_t *len, int quality, int lossless);
int save_tiff_buffer(VipsImage *in, void **buf, size_t *len);
102 changes: 64 additions & 38 deletions vips/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package vips

// #cgo pkg-config: vips
// #include "image.h"
// #include "icc_profiles.h"
import "C"

import (
Expand All @@ -23,11 +24,12 @@ type ImageRef struct {
// NOTE: We keep a reference to this so that the input buffer is
// never garbage collected during processing. Some image loaders use random
// access transcoding and therefore need the original buffer to be in memory.
buf []byte
image *C.VipsImage
format ImageType
lock sync.Mutex
preMultiplication *PreMultiplicationState
buf []byte
image *C.VipsImage
format ImageType
lock sync.Mutex
preMultiplication *PreMultiplicationState
optimizedIccProfile string
}

type ImageMetadata struct {
Expand All @@ -40,12 +42,13 @@ type ImageMetadata struct {

// ExportParams are options when exporting an image to file or buffer
type ExportParams struct {
Format ImageType
Quality int
Compression int
Interlaced bool
Lossless bool
Effort int
Format ImageType
Quality int
Compression int
Interlaced bool
Lossless bool
NearLossless bool
Effort int
}

func NewDefaultExportParams() *ExportParams {
Expand Down Expand Up @@ -77,10 +80,11 @@ func NewDefaultPNGExportParams() *ExportParams {

func NewDefaultWEBPExportParams() *ExportParams {
return &ExportParams{
Format: ImageTypeWEBP,
Quality: 75,
Lossless: false,
Effort: 4,
Format: ImageTypeWEBP,
Quality: 75,
Lossless: false,
NearLossless: false,
Effort: 4,
}
}

Expand Down Expand Up @@ -275,26 +279,10 @@ func (r *ImageRef) IsColorSpaceSupported() bool {

// Export exports the image
func (r *ImageRef) Export(params *ExportParams) ([]byte, *ImageMetadata, error) {
p := params
if p == nil {
switch r.format {
case ImageTypeJPEG:
p = NewDefaultJPEGExportParams()
case ImageTypePNG:
p = NewDefaultPNGExportParams()
case ImageTypeWEBP:
p = NewDefaultWEBPExportParams()
default:
p = NewDefaultExportParams()
}
}

if p.Format == ImageTypeUnknown {
p.Format = r.format
}
params = r.resolveExportParams(params)

// the exported buf is not necessarily in same format as the original buf, might default to JPEG as well.
buf, format, err := r.exportBuffer(p)
buf, format, err := r.exportBuffer(params)
if err != nil {
return nil, nil, err
}
Expand All @@ -310,6 +298,27 @@ func (r *ImageRef) Export(params *ExportParams) ([]byte, *ImageMetadata, error)
return buf, metadata, nil
}

func (r *ImageRef) resolveExportParams(params *ExportParams) *ExportParams {
if params == nil {
switch r.format {
case ImageTypeJPEG:
params = NewDefaultJPEGExportParams()
case ImageTypePNG:
params = NewDefaultPNGExportParams()
case ImageTypeWEBP:
params = NewDefaultWEBPExportParams()
default:
params = NewDefaultExportParams()
}
}

if params.Format == ImageTypeUnknown {
params.Format = r.format
}

return params
}

func (r *ImageRef) Composite(overlay *ImageRef, mode BlendMode, x, y int) error {
out, err := vipsComposite2(r.image, overlay.image, mode, x, y)
if err != nil {
Expand Down Expand Up @@ -464,16 +473,25 @@ func (r *ImageRef) RemoveICCProfile() error {
}

func (r *ImageRef) OptimizeICCProfile() error {
isCMYK := 0
if r.Interpretation() == InterpretationCMYK {
isCMYK = 1
inputProfile := r.determineInputICCProfile()
if !r.HasICCProfile() && (inputProfile == "") {
//No embedded ICC profile in the input image and no input profile determined, nothing to do.
return nil
}

out, err := vipsOptimizeICCProfile(r.image, isCMYK)
r.optimizedIccProfile = C.GoString(C.SRGB_V2_MICRO_ICC_PATH)
if r.Bands() <= 2 {
r.optimizedIccProfile = C.GoString(C.SGRAY_V2_MICRO_ICC_PATH)
}

embedded := r.HasICCProfile() && (inputProfile == "")

out, err := vipsICCTransform(r.image, r.optimizedIccProfile, inputProfile, IntentPerceptual, 0, embedded)
if err != nil {
info(err.Error())
return err
}

r.setImage(out)
return nil
}
Expand Down Expand Up @@ -702,6 +720,13 @@ func (r *ImageRef) ToBytes() ([]byte, error) {
return bytes, nil
}

func (r *ImageRef) determineInputICCProfile() (inputProfile string) {
if r.Interpretation() == InterpretationCMYK {
inputProfile = "cmyk"
}
return
}

// setImage resets the image for this image and frees the previous one
func (r *ImageRef) setImage(image *C.VipsImage) {
r.lock.Lock()
Expand Down Expand Up @@ -729,7 +754,8 @@ func (r *ImageRef) exportBuffer(params *ExportParams) ([]byte, ImageType, error)

switch format {
case ImageTypeWEBP:
buf, err = vipsSaveWebPToBuffer(r.image, false, params.Quality, params.Lossless, params.Effort)
buf, err = vipsSaveWebPToBuffer(r.image, false, params.Quality, params.Lossless, params.NearLossless,
params.Effort, r.optimizedIccProfile)
case ImageTypePNG:
buf, err = vipsSavePNGToBuffer(r.image, false, params.Compression, params.Interlaced)
case ImageTypeTIFF:
Expand Down
Loading

0 comments on commit 466b65a

Please sign in to comment.