stbi: fix stbi.Image's nr_channels property, after a forced conversion on load (#22528)

This commit is contained in:
Delyan Angelov 2024-10-15 13:27:34 +03:00 committed by GitHub
parent 1fb9fec7fa
commit 04bd58596f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 70 additions and 31 deletions

View File

@ -36,14 +36,17 @@ fn cb_free(p voidptr) {
#include "stb_v_header.h" #include "stb_v_header.h"
#flag @VEXEROOT/thirdparty/stb_image/stbi.o #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 struct Image {
pub mut: pub mut:
width int width int // the width in pixels in the .data
height int height int // the height in pixels in the .data
nr_channels int nr_channels int // the number of color channels in the .data
ok bool ok bool // if the image was loaded successfully
data &u8 = unsafe { nil } data &u8 = unsafe { nil } // the actual data/pixels in the image, after reading and potentially converting the image
ext string 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] @[params]
pub struct LoadParams { pub struct LoadParams {
pub: pub:
// the number of channels you expect the image to have. 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.
// If set to 0 stbi will figure out the correct number of channels
desired_channels int = C.STBI_rgb_alpha
} }
// 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 { pub fn load(path string, params LoadParams) !Image {
ext := path.all_after_last('.') ext := path.all_after_last('.')
mut res := Image{ mut res := Image{
ok: true ok: true
ext: ext 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) params.desired_channels)
if params.desired_channels == 0 {
res.nr_channels = res.original_nr_channels
}
if isnil(res.data) { if isnil(res.data) {
return error('stbi_image failed to load from "${path}"') 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 // 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 { pub fn load_from_memory(buf &u8, bufsize int, params LoadParams) !Image {
mut res := 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) params.desired_channels)
if params.desired_channels == 0 {
res.nr_channels = res.original_nr_channels
}
if isnil(res.data) { if isnil(res.data) {
return error('stbi_image failed to load from memory') 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` // 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 { pub fn resize_uint8(img &Image, output_w int, output_h int) !Image {
mut res := Image{ mut res := Image{
ok: true ok: true
ext: img.ext ext: img.ext
width: output_w width: output_w
height: output_h height: output_h
nr_channels: img.nr_channels 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)) res.data = cb_malloc(usize(output_w * output_h * img.nr_channels))

View File

@ -1,7 +1,10 @@
import os import os
import stbi import stbi
const vroot = @VEXEROOT
const tfolder = os.join_path(os.vtmp_dir(), 'stbi') 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() { fn testsuite_begin() {
os.mkdir_all(tfolder) or {} os.mkdir_all(tfolder) or {}
@ -12,10 +15,8 @@ fn testsuite_end() {
} }
fn test_stbi_read_write() { fn test_stbi_read_write() {
vroot := @VEXEROOT println('Source path: ${logo_path}')
path := os.join_path(vroot, 'examples', 'assets', 'logo.png') d_s := stbi.load(logo_path) or { panic(err) }
println('Source path: ${path}')
d_s := stbi.load(path) or { panic(err) }
println('Image source data:\n ${d_s}') println('Image source data:\n ${d_s}')
out_path := os.join_path(tfolder, 'test.png') 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.width == d_d.width
assert d_s.height == d_d.height assert d_s.height == d_d.height
assert d_s.nr_channels == d_d.nr_channels 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_s := unsafe { &u32(d_s.data) }
mut v_d := &u32(d_d.data) mut v_d := unsafe { &u32(d_d.data) }
mut delta := i64(0) mut delta := i64(0)
for index in 0 .. (d_d.width * d_d.width) { for index in 0 .. (d_d.width * d_d.width) {
unsafe { unsafe {
@ -44,15 +46,14 @@ fn test_stbi_read_write() {
} }
fn test_stbi_resize() { fn test_stbi_resize() {
vroot := @VEXEROOT println('Source path: ${logo_path}')
path := os.join_path(vroot, 'examples', 'assets', 'logo.png') d_s := stbi.load(logo_path) or { panic(err) }
println('Source path: ${path}')
d_s := stbi.load(path) or { panic(err) }
println('Image source data:\n ${d_s}') println('Image source data:\n ${d_s}')
new_width, new_height := 100, 100 new_width, new_height := 100, 100
d_r := stbi.resize_uint8(d_s, new_width, new_height) or { panic(err) } 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}') println('Resized Image source data:\n ${d_s}')
out_path := os.join_path(tfolder, 'test.png') 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.width == new_width
assert d_d.height == new_height assert d_d.height == new_height
assert d_d.nr_channels == d_r.nr_channels assert d_d.nr_channels == d_r.nr_channels
assert d_d.original_nr_channels == 4
os.rm(out_path) or {} 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
}