Skip to content

Commit

Permalink
stbi: fix stbi.Image's nr_channels property, after a forced convers…
Browse files Browse the repository at this point in the history
…ion on load (vlang#22528)
  • Loading branch information
spytheman authored Oct 15, 2024
1 parent 1fb9fec commit 04bd585
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 31 deletions.
61 changes: 40 additions & 21 deletions vlib/stbi/stbi.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -113,34 +116,49 @@ 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}"')
}
return res
}

// 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')
}
Expand All @@ -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))
Expand Down
40 changes: 30 additions & 10 deletions vlib/stbi/stbi_test.v
Original file line number Diff line number Diff line change
@@ -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 {}
Expand All @@ -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')
Expand All @@ -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 {
Expand All @@ -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')
Expand All @@ -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
}

0 comments on commit 04bd585

Please sign in to comment.