Skip to content

Commit d851fcf

Browse files
committed
add hdr valgrind test via go
1 parent 0f0379c commit d851fcf

File tree

10 files changed

+89
-21
lines changed

10 files changed

+89
-21
lines changed

.github/workflows/ci.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,26 @@ jobs:
8484
wc -l | grep -q "2" || \
8585
(echo "Valgrind output:" && echo "$VALGRIND_OUT" && false)
8686
done
87+
88+
- name: Run HDR Tone Mapping Valgrind Tests
89+
run: |
90+
cd examples
91+
hdr_test_cases=(
92+
"100 100 ../data/hdr-ohmama.png out-hdr-1.webp"
93+
"200 200 ../data/hdr-ohmama.png out-hdr-2.png"
94+
"150 150 ../data/hdr-ohmama.png out-hdr-3.webp"
95+
)
96+
97+
for test_case in "${hdr_test_cases[@]}"; do
98+
read -r height width input output <<< "$test_case"
99+
echo "Testing HDR tone mapping with height=$height width=$width input=$input output=$output"
100+
VALGRIND_OUT=$(valgrind --leak-check=full ./example \
101+
-height "$height" \
102+
-width "$width" \
103+
-input "$input" \
104+
-output "$output" \
105+
-force-sdr 2>&1) && \
106+
echo "$VALGRIND_OUT" | grep -E "definitely lost: 0 bytes in 0 blocks|indirectly lost: 0 bytes in 0 blocks" | \
107+
wc -l | grep -q "2" || \
108+
(echo "Valgrind output:" && echo "$VALGRIND_OUT" && false)
109+
done

data/hdr-ohmama.png

62 KB
Loading

examples/example

-48.6 MB
Binary file not shown.

examples/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ func main() {
2525
var outputFilename string
2626
var stretch bool
2727
var encodeTimeoutOption string
28+
var forceSdr bool
2829

2930
flag.StringVar(&inputFilename, "input", "", "name of input file to resize/transcode")
3031
flag.StringVar(&outputFilename, "output", "", "name of output file, also determines output type")
3132
flag.IntVar(&outputWidth, "width", 0, "width of output file")
3233
flag.IntVar(&outputHeight, "height", 0, "height of output file")
3334
flag.BoolVar(&stretch, "stretch", false, "perform stretching resize instead of cropping")
3435
flag.StringVar(&encodeTimeoutOption, "timeout", "60s", "encode timeout for videos")
36+
flag.BoolVar(&forceSdr, "force-sdr", false, "enable HDR to SDR tone mapping for images with PQ profiles")
3537
flag.Parse()
3638

3739
if inputFilename == "" {
@@ -120,6 +122,7 @@ func main() {
120122
NormalizeOrientation: true,
121123
EncodeOptions: EncodeOptions[outputType],
122124
EncodeTimeout: encodeTimeout,
125+
ForceSdr: forceSdr,
123126
}
124127

125128
transformStartTime := time.Now()

lilliput.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,13 @@ func NewDecoderWithOptionalToneMapping(buf []byte, toneMappingEnabled bool) (Dec
170170
// into compressed image data. ext should be a string like ".jpeg" or
171171
// ".png". decodedBy is optional and can be the Decoder used to make
172172
// the Framebuffer. dst is where an encoded image will be written.
173-
func NewEncoder(ext string, decodedBy Decoder, dst []byte) (Encoder, error) {
173+
func NewEncoder(ext string, decodedBy Decoder, dst []byte, forceSdr bool) (Encoder, error) {
174174
if strings.ToLower(ext) == ".gif" {
175175
return newGifEncoder(decodedBy, dst)
176176
}
177177

178178
if strings.ToLower(ext) == ".webp" {
179-
return newWebpEncoder(decodedBy, dst)
179+
return newWebpEncoder(decodedBy, dst, forceSdr)
180180
}
181181

182182
if strings.ToLower(ext) == ".avif" {
@@ -191,5 +191,5 @@ func NewEncoder(ext string, decodedBy Decoder, dst []byte) (Encoder, error) {
191191
return newThumbhashEncoder(decodedBy, dst)
192192
}
193193

194-
return newOpenCVEncoder(ext, decodedBy, dst)
194+
return newOpenCVEncoder(ext, decodedBy, dst, forceSdr)
195195
}

opencv.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,11 @@ type openCVDecoder struct {
138138

139139
// openCVEncoder implements the Encoder interface for images supported by OpenCV.
140140
type openCVEncoder struct {
141-
encoder C.opencv_encoder // Native OpenCV encoder
142-
dst C.opencv_mat // Destination OpenCV matrix
143-
dstBuf []byte // Destination buffer for encoded data
141+
encoder C.opencv_encoder // Native OpenCV encoder
142+
dst C.opencv_mat // Destination OpenCV matrix
143+
dstBuf []byte // Destination buffer for encoded data
144+
icc []byte // ICC color profile from source image
145+
forceSdr bool // Enable HDR to SDR tone mapping
144146
}
145147

146148
// Depth returns the number of bits in the PixelType.
@@ -762,7 +764,7 @@ func (d *openCVDecoder) SkipFrame() error {
762764
return ErrSkipNotSupported
763765
}
764766

765-
func newOpenCVEncoder(ext string, decodedBy Decoder, dstBuf []byte) (*openCVEncoder, error) {
767+
func newOpenCVEncoder(ext string, decodedBy Decoder, dstBuf []byte, forceSdr bool) (*openCVEncoder, error) {
766768
dstBuf = dstBuf[:1]
767769
dst := C.opencv_mat_create_empty_from_data(C.int(cap(dstBuf)), unsafe.Pointer(&dstBuf[0]))
768770

@@ -777,10 +779,14 @@ func newOpenCVEncoder(ext string, decodedBy Decoder, dstBuf []byte) (*openCVEnco
777779
return nil, ErrInvalidImage
778780
}
779781

782+
icc := decodedBy.ICC()
783+
780784
return &openCVEncoder{
781-
encoder: enc,
782-
dst: dst,
783-
dstBuf: dstBuf,
785+
encoder: enc,
786+
dst: dst,
787+
dstBuf: dstBuf,
788+
icc: icc,
789+
forceSdr: forceSdr,
784790
}, nil
785791
}
786792

@@ -797,7 +803,21 @@ func (e *openCVEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, error)
797803
if len(optList) > 0 {
798804
firstOpt = (*C.int)(unsafe.Pointer(&optList[0]))
799805
}
800-
if !C.opencv_encoder_write(e.encoder, f.mat, firstOpt, C.size_t(len(optList))) {
806+
807+
var success bool
808+
if e.forceSdr && len(e.icc) > 0 {
809+
var iccPtr unsafe.Pointer
810+
if len(e.icc) > 0 {
811+
iccPtr = unsafe.Pointer(&e.icc[0])
812+
}
813+
success = bool(C.opencv_encoder_write_with_tone_mapping(
814+
e.encoder, f.mat, firstOpt, C.size_t(len(optList)),
815+
(*C.uint8_t)(iccPtr), C.size_t(len(e.icc)), C.bool(true)))
816+
} else {
817+
success = bool(C.opencv_encoder_write(e.encoder, f.mat, firstOpt, C.size_t(len(optList))))
818+
}
819+
820+
if !success {
801821
return nil, ErrInvalidImage
802822
}
803823

opencv_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ func TestICC(t *testing.T) {
269269

270270
// try encoding a WebP image, including ICC profile data when available
271271
dstBuf := make([]byte, destinationBufferSize)
272-
encoder, err := newWebpEncoder(decoder, dstBuf)
272+
encoder, err := newWebpEncoder(decoder, dstBuf, false)
273273
if err != nil {
274274
t.Fatalf("Failed to create a new webp encoder: %v", err)
275275
}

ops.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ type ImageOptions struct {
5757
// If set to 0, only the first frame will be extracted (default behavior).
5858
// This option only applies to video formats (MP4, MOV, WEBM).
5959
VideoFrameSampleIntervalMs int
60+
61+
// ForceSdr enables HDR to SDR tone mapping for images with PQ (Perceptual Quantizer) profiles.
62+
// When enabled, images with HDR color profiles will be tone-mapped to SDR for better compatibility.
63+
// Only applies to WebP and PNG output formats.
64+
ForceSdr bool
6065
}
6166

6267
// ImageOps is a reusable object that can resize and encode images.
@@ -418,7 +423,7 @@ func (o *ImageOps) initializeTransform(d Decoder, opt *ImageOptions, dst []byte)
418423
return nil, nil, err
419424
}
420425

421-
enc, err := NewEncoder(opt.FileType, d, dst)
426+
enc, err := NewEncoder(opt.FileType, d, dst, opt.ForceSdr)
422427
if err != nil {
423428
return nil, nil, err
424429
}

webp.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type webpEncoder struct {
2121
encoder C.webp_encoder
2222
dstBuf []byte
2323
icc []byte
24+
forceSdr bool // Enable HDR to SDR tone mapping
2425
frameIndex int
2526
hasFlushed bool
2627
}
@@ -173,7 +174,7 @@ func (d *webpDecoder) SkipFrame() error {
173174

174175
// newWebpEncoder creates a new WebP encoder using the provided decoder for metadata
175176
// and destination buffer for the encoded output.
176-
func newWebpEncoder(decodedBy Decoder, dstBuf []byte) (*webpEncoder, error) {
177+
func newWebpEncoder(decodedBy Decoder, dstBuf []byte, forceSdr bool) (*webpEncoder, error) {
177178
dstBuf = dstBuf[:1]
178179
icc := decodedBy.ICC()
179180
bgColor := decodedBy.BackgroundColor()
@@ -190,9 +191,10 @@ func newWebpEncoder(decodedBy Decoder, dstBuf []byte) (*webpEncoder, error) {
190191
}
191192

192193
return &webpEncoder{
193-
encoder: enc,
194-
dstBuf: dstBuf,
195-
icc: icc,
194+
encoder: enc,
195+
dstBuf: dstBuf,
196+
icc: icc,
197+
forceSdr: forceSdr,
196198
}, nil
197199
}
198200

@@ -228,7 +230,22 @@ func (e *webpEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, error) {
228230

229231
// Encode the current frame
230232
frameDelay := int(f.duration.Milliseconds())
231-
length := C.webp_encoder_write(e.encoder, f.mat, firstOpt, C.size_t(len(optList)), C.int(frameDelay), C.int(f.blend), C.int(f.dispose), 0, 0)
233+
234+
var length C.size_t
235+
if e.forceSdr && len(e.icc) > 0 {
236+
var iccPtr unsafe.Pointer
237+
if len(e.icc) > 0 {
238+
iccPtr = unsafe.Pointer(&e.icc[0])
239+
}
240+
length = C.webp_encoder_write_with_tone_mapping(
241+
e.encoder, f.mat, firstOpt, C.size_t(len(optList)),
242+
C.int(frameDelay), C.int(f.blend), C.int(f.dispose), 0, 0,
243+
(*C.uint8_t)(iccPtr), C.size_t(len(e.icc)), C.bool(true))
244+
} else {
245+
length = C.webp_encoder_write(e.encoder, f.mat, firstOpt, C.size_t(len(optList)),
246+
C.int(frameDelay), C.int(f.blend), C.int(f.dispose), 0, 0)
247+
}
248+
232249
if length == 0 {
233250
return nil, ErrInvalidImage
234251
}

webp_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func testNewWebpEncoder(t *testing.T) {
7575
defer decoder.Close()
7676

7777
dstBuf := make([]byte, destinationBufferSize)
78-
encoder, err := newWebpEncoder(decoder, dstBuf)
78+
encoder, err := newWebpEncoder(decoder, dstBuf, false)
7979
if err != nil {
8080
t.Fatalf("Unexpected error: %v", err)
8181
}
@@ -161,7 +161,7 @@ func testWebpEncoderEncode(t *testing.T) {
161161
}
162162

163163
dstBuf := make([]byte, destinationBufferSize)
164-
encoder, err := newWebpEncoder(decoder, dstBuf)
164+
encoder, err := newWebpEncoder(decoder, dstBuf, false)
165165
if err != nil {
166166
t.Fatalf("Failed to create a new webp encoder: %v", err)
167167
}
@@ -195,7 +195,7 @@ func testWebpEncoderEncode(t *testing.T) {
195195
defer decoder.Close()
196196

197197
dstBuf := make([]byte, destinationBufferSize)
198-
encoder, err := newWebpEncoder(decoder, dstBuf)
198+
encoder, err := newWebpEncoder(decoder, dstBuf, false)
199199
if err != nil {
200200
t.Fatalf("Failed to create a new webp encoder: %v", err)
201201
}

0 commit comments

Comments
 (0)