From 04bd58596f8275c8ce9ebc8579d3339c142a2df8 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Tue, 15 Oct 2024 13:27:34 +0300 Subject: [PATCH] stbi: fix stbi.Image's `nr_channels` property, after a forced conversion on load (#22528) --- vlib/stbi/stbi.c.v | 61 ++++++++++++++++++++++++++++--------------- vlib/stbi/stbi_test.v | 40 +++++++++++++++++++++------- 2 files changed, 70 insertions(+), 31 deletions(-) diff --git a/vlib/stbi/stbi.c.v b/vlib/stbi/stbi.c.v index d859c2585e..01674da9d3 100644 --- a/vlib/stbi/stbi.c.v +++ b/vlib/stbi/stbi.c.v @@ -36,14 +36,17 @@ fn cb_free(p voidptr) { #include "stb_v_header.h" #flag @VEXEROOT/thirdparty/stb_image/stbi.o +// Image represents an image loaded from file or memory, or an image, produced after resizing pub struct Image { pub mut: - width int - height int - nr_channels int - ok bool - data &u8 = unsafe { nil } - ext string + width int // the width in pixels in the .data + height int // the height in pixels in the .data + nr_channels int // the number of color channels in the .data + ok bool // if the image was loaded successfully + data &u8 = unsafe { nil } // the actual data/pixels in the image, after reading and potentially converting the image + ext string // the extension of the file, from which the image was loaded + // + original_nr_channels int // when loaded from memory/disk, this field will contain the original number of channels, based on the data, prior to any conversions. Use only as metadata, not for further conversions. } //----------------------------------------------------------------------------- @@ -113,21 +116,27 @@ fn C.stbi_load_from_memory(buffer &u8, len int, x &int, y &int, channels_in_file @[params] pub struct LoadParams { pub: - // the number of channels you expect the image to have. - // If set to 0 stbi will figure out the correct number of channels - desired_channels int = C.STBI_rgb_alpha + desired_channels int = C.STBI_rgb_alpha // 4 by default (RGBA); desired_channels is the number of color channels, that will be used for representing the image in memory. If set to 0, stbi will figure out the number of channels, based on the original image data. } -// load load an image from a path +// load loads an image from `path` +// If you do not pass desired_channels: explicitly, it will default to 4 (RGBA), +// The image, will get converted into that internal format, no matter what it was on disk. +// Use desired_channels:0, if you need to keep the channels of the image on disk. +// Note that displaying such an image, with gg/sokol later, can be a problem. +// Converting/resizing it, should work fine though. pub fn load(path string, params LoadParams) !Image { ext := path.all_after_last('.') mut res := Image{ - ok: true - ext: ext + ok: true + ext: ext + nr_channels: params.desired_channels } - res.data = C.stbi_load(&char(path.str), &res.width, &res.height, &res.nr_channels, + res.data = C.stbi_load(&char(path.str), &res.width, &res.height, &res.original_nr_channels, params.desired_channels) - + if params.desired_channels == 0 { + res.nr_channels = res.original_nr_channels + } if isnil(res.data) { return error('stbi_image failed to load from "${path}"') } @@ -135,12 +144,21 @@ pub fn load(path string, params LoadParams) !Image { } // load_from_memory load an image from a memory buffer +// If you do not pass desired_channels: explicitly, it will default to 4 (RGBA), +// and the image will get converted into that internal format, no matter what it was originally. +// Use desired_channels:0, if you need to keep the channels of the image as they were. +// Note that displaying such an image, with gg/sokol later, can be a problem. +// Converting/resizing it, should work fine though. pub fn load_from_memory(buf &u8, bufsize int, params LoadParams) !Image { mut res := Image{ - ok: true + ok: true + nr_channels: params.desired_channels } - res.data = C.stbi_load_from_memory(buf, bufsize, &res.width, &res.height, &res.nr_channels, + res.data = C.stbi_load_from_memory(buf, bufsize, &res.width, &res.height, &res.original_nr_channels, params.desired_channels) + if params.desired_channels == 0 { + res.nr_channels = res.original_nr_channels + } if isnil(res.data) { return error('stbi_image failed to load from memory') } @@ -158,11 +176,12 @@ fn C.stbir_resize_uint8_linear(input_pixels &u8, input_w int, input_h int, input // resize_uint8 resizes `img` to dimensions of `output_w` and `output_h` pub fn resize_uint8(img &Image, output_w int, output_h int) !Image { mut res := Image{ - ok: true - ext: img.ext - width: output_w - height: output_h - nr_channels: img.nr_channels + ok: true + ext: img.ext + width: output_w + height: output_h + nr_channels: img.nr_channels + original_nr_channels: img.original_nr_channels // preserve the metadata of the original, during resizes } res.data = cb_malloc(usize(output_w * output_h * img.nr_channels)) diff --git a/vlib/stbi/stbi_test.v b/vlib/stbi/stbi_test.v index 48d8665b5c..cf7566fc7b 100644 --- a/vlib/stbi/stbi_test.v +++ b/vlib/stbi/stbi_test.v @@ -1,7 +1,10 @@ import os import stbi +const vroot = @VEXEROOT const tfolder = os.join_path(os.vtmp_dir(), 'stbi') +const logo_path = os.join_path(vroot, 'examples/assets/logo.png') +const background_path = os.join_path(vroot, 'examples/flappylearning/assets/img/background.png') fn testsuite_begin() { os.mkdir_all(tfolder) or {} @@ -12,10 +15,8 @@ fn testsuite_end() { } fn test_stbi_read_write() { - vroot := @VEXEROOT - path := os.join_path(vroot, 'examples', 'assets', 'logo.png') - println('Source path: ${path}') - d_s := stbi.load(path) or { panic(err) } + println('Source path: ${logo_path}') + d_s := stbi.load(logo_path) or { panic(err) } println('Image source data:\n ${d_s}') out_path := os.join_path(tfolder, 'test.png') @@ -30,9 +31,10 @@ fn test_stbi_read_write() { assert d_s.width == d_d.width assert d_s.height == d_d.height assert d_s.nr_channels == d_d.nr_channels + assert d_s.original_nr_channels == 4 - mut v_s := &u32(d_s.data) - mut v_d := &u32(d_d.data) + mut v_s := unsafe { &u32(d_s.data) } + mut v_d := unsafe { &u32(d_d.data) } mut delta := i64(0) for index in 0 .. (d_d.width * d_d.width) { unsafe { @@ -44,15 +46,14 @@ fn test_stbi_read_write() { } fn test_stbi_resize() { - vroot := @VEXEROOT - path := os.join_path(vroot, 'examples', 'assets', 'logo.png') - println('Source path: ${path}') - d_s := stbi.load(path) or { panic(err) } + println('Source path: ${logo_path}') + d_s := stbi.load(logo_path) or { panic(err) } println('Image source data:\n ${d_s}') new_width, new_height := 100, 100 d_r := stbi.resize_uint8(d_s, new_width, new_height) or { panic(err) } + assert d_r.original_nr_channels == 4 println('Resized Image source data:\n ${d_s}') out_path := os.join_path(tfolder, 'test.png') @@ -67,5 +68,24 @@ fn test_stbi_resize() { assert d_d.width == new_width assert d_d.height == new_height assert d_d.nr_channels == d_r.nr_channels + assert d_d.original_nr_channels == 4 os.rm(out_path) or {} } + +fn test_load_image_with_channels_different_than_4() { + img := stbi.load(background_path)! + assert img.nr_channels == 4, 'by default, stbi.load should convert images to 4 channels' + assert img.original_nr_channels == 3, 'the default, should not affect the img.original_nr_channels field; it should be based solely on the original image data on disk' + + img_resized := stbi.resize_uint8(img, 128, 128)! + assert img_resized.nr_channels == 4 + assert img_resized.original_nr_channels == 3 + + img3 := stbi.load(background_path, desired_channels: 0)! + assert img3.nr_channels == 3, 'stbi.load, with desired_channels: 0, should return an image, without any conversion. the nr_channels should be determined by the image data' + assert img.original_nr_channels == 3, 'the default, should not affect the img.original_nr_channels field; it should be based solely on the original image data on disk' + + img3_resized := stbi.resize_uint8(img3, 128, 128)! + assert img3_resized.nr_channels == 3 + assert img3_resized.original_nr_channels == 3 +}