Add support for OpenImageIO.

This commit is contained in:
Alberto Torres 2025-03-18 17:05:13 +01:00
parent 5b2a22b2c0
commit a38138582e
5 changed files with 450 additions and 36 deletions

308
libs/oiio/oiio.cpp Normal file
View file

@ -0,0 +1,308 @@
enum ImageFormat {
FORMAT_UNKNOWN = 0,
FORMAT_BYTE, // 8-bit unsigned
FORMAT_SHORT, // 16-bit unsigned
FORMAT_HALF, // 16-bit float
FORMAT_FLOAT, // 32-bit float
MAX_ENUM = 0x7FFFFFFF
};
// image_decoder.cpp
#include <OpenImageIO/imageio.h>
#include <OpenImageIO/filesystem.h>
#include <OpenImageIO/imagebufalgo.h>
#include <cstring>
#include <exception>
#include <array>
#include <string>
using namespace OIIO;
static ImageFormat type_to_format(TypeDesc type) {
switch (type.basetype) {
case TypeDesc::UINT8: return FORMAT_BYTE;
case TypeDesc::UINT16: return FORMAT_SHORT;
case TypeDesc::HALF: return FORMAT_HALF;
case TypeDesc::FLOAT: return FORMAT_FLOAT;
default: return FORMAT_UNKNOWN;
}
}
static const char* type_to_string(TypeDesc type) {
switch (type.basetype) {
case TypeDesc::UINT8: return "FORMAT_BYTE";
case TypeDesc::UINT16: return "FORMAT_SHORT";
case TypeDesc::HALF: return "FORMAT_HALF";
case TypeDesc::FLOAT: return "FORMAT_FLOAT";
default: return "FORMAT_UNKNOWN";
}
}
static TypeDesc format_to_type(enum ImageFormat format) {
switch (format) {
case FORMAT_BYTE: return TypeDesc::UINT8; // 8-bit unsigned
case FORMAT_SHORT: return TypeDesc::UINT16; // 16-bit unsigned
case FORMAT_HALF: return TypeDesc::HALF; // 16-bit float
case FORMAT_FLOAT: return TypeDesc::FLOAT; // 32-bit float
default: return TypeDesc::UNKNOWN; // Unknown format
}
}
extern "C" {
int oiio_image_decode(const uint8_t* input, size_t input_len,
uint8_t* output, size_t output_len,
int32_t desired_channels, ImageFormat format, char* is_BGR, const char* file_name) {
if (!input || !output || desired_channels < 1 || desired_channels > 4) {
fprintf(stderr, "Invalid input parameters\n");
return 0;
}
uint8_t* decoded = output;
bool free_decoded = false;
try {
auto mem_reader = Filesystem::IOMemReader(input, input_len);
auto in = ImageInput::open(file_name, nullptr, &mem_reader);
if (!in) {
fprintf(stderr, "Open failed: %s\n", geterror().c_str());
return 0;
}
const ImageSpec& spec = in->spec();
*is_BGR = spec.channelnames[0] == "B"? 1:0;
const size_t required = spec.width * spec.height * desired_channels;
const TypeDesc ftype = format_to_type(format);
if (output_len != required * ftype.size()) {
fprintf(stderr, "Buffer size mismatch: Needed %zu, got %u\n",
required * ftype.size(), output_len);
in->close();
return 0;
}
if(spec.nchannels != desired_channels){
// have a separate buffer for the decoded image, free later
decoded = (uint8_t*)malloc(spec.image_bytes());
free_decoded = true;
}
if (!in->read_image(0, 0, 0, spec.nchannels, ftype, decoded)) {
fprintf(stderr, "Read error: %s\n", in->geterror().c_str());
in->close();
if(free_decoded) free(decoded);
return 0;
}
if(spec.nchannels < desired_channels){
// expand channels
ImageSpec src_spec(spec.width, spec.height, spec.nchannels, ftype);
ImageBuf src_buf(src_spec, decoded);
ImageSpec dst_spec(spec.width, spec.height, desired_channels, ftype);
ImageBuf dst_buf(dst_spec, output);
const float channel_values[] = {0.0f, 0.0f, 0.0f, 1.0f};
// -1 below fills with the values above
std::array<int, 4> channel_order = {-1, -1, -1, -1};
for(int i=0; i<spec.nchannels; i++){
channel_order[i] = i;
}
if(spec.nchannels == 1 && desired_channels >= 3){
// single channel exception: make it greyscale RGB/RGBA
channel_order = {0,0,0,-1};
}
if (!ImageBufAlgo::channels(dst_buf, src_buf,
desired_channels,
channel_order, channel_values)) {
fprintf(stderr, "Channel conversion failed: %s\n", dst_buf.geterror().c_str());
in->close();
if(free_decoded) free(decoded);
return 0;
}
uint8_t* pixels = (uint8_t*)dst_buf.localpixels();
if(pixels != output){
// it turns out channels() always resets the buffer
// so it's no longer wrapping output
memcpy(output, pixels, dst_spec.image_bytes());
}
}
in->close();
if(free_decoded) free(decoded);
return 1;
}
catch (const std::exception& e) {
fprintf(stderr, "Exception: %s\n", e.what());
if(free_decoded) free(decoded);
return 0;
}
catch (...) {
fprintf(stderr, "Unknown exception occurred\n");
if(free_decoded) free(decoded);
return 0;
}
}
int oiio_image_get_attributes(const uint8_t* input, size_t input_len,
int32_t* width, int32_t* height, int32_t* channels,
enum ImageFormat* format, const char* file_name) {
if (!input) {
fprintf(stderr, "Null input buffer\n");
return 0;
}
try {
auto mem_reader = Filesystem::IOMemReader(input, input_len);
auto in = ImageInput::open(file_name, nullptr, &mem_reader);
if (!in) {
fprintf(stderr, "Open failed: %s\n", geterror().c_str());
return 0;
}
const ImageSpec& spec = in->spec();
if (width) *width = spec.width;
if (height) *height = spec.height;
if (channels) *channels = spec.nchannels;
if (format) *format = type_to_format(spec.format);
in->close();
return 1;
}
catch (const std::exception& e) {
fprintf(stderr, "Exception: %s\n", e.what());
return 0;
}
catch (...) {
fprintf(stderr, "Unknown exception occurred\n");
return 0;
}
}
} // extern "C"
// // image_to_ppm.c
// #include <stdio.h>
// #include <stdlib.h>
// int main(int argc, char** argv) {
// if (argc != 2) {
// fprintf(stderr, "Usage: %s <image-file>\n", argv[0]);
// return 1;
// }
// // Read input file
// FILE* file = fopen(argv[1], "rb");
// if (!file) {
// perror("Failed to open file");
// return 1;
// }
// fseek(file, 0, SEEK_END);
// long file_size = ftell(file);
// fseek(file, 0, SEEK_SET);
// uint8_t* input = (uint8_t*)malloc(file_size);
// if (!input) {
// fclose(file);
// fprintf(stderr, "Memory allocation failed\n");
// return 1;
// }
// if (fread(input, 1, file_size, file) != file_size) {
// fclose(file);
// free(input);
// fprintf(stderr, "File read error\n");
// return 1;
// }
// fclose(file);
// // Get image attributes
// int width, height, channels;
// enum ImageFormat format;
// if (!image_get_attributes(input, file_size, &width, &height, &channels, &format)) {
// free(input);
// fprintf(stderr, "Unsupported image format\n");
// return 1;
// }
// // Allocate output buffer for 3-channel RGB
// size_t output_size = width * height * 3;
// uint8_t* pixels = (uint8_t*)malloc(output_size);
// if (!pixels) {
// free(input);
// fprintf(stderr, "Output buffer allocation failed\n");
// return 1;
// }
// // Decode to 3 channels (RGB)
// if (!image_decode(input, file_size, pixels, output_size, 3, format)) {
// free(input);
// free(pixels);
// fprintf(stderr, "Image decoding failed\n");
// return 1;
// }
// free(input);
// // Output PPM header
// printf("P6\n%d %d\n255\n", width, height);
// // Output raw pixel data
// fwrite(pixels, 1, output_size, stdout);
// free(pixels);
// return 0;
// }
bool convertRGBToRGBA(const std::string& filename,
void* output_buffer,
size_t buffer_length,
std::string& error_msg) {
// Load source image
ImageBuf src_buf(filename);
if (!src_buf.read()) {
error_msg = "Failed to load image: " + src_buf.geterror();
return false;
}
const ImageSpec& src_spec = src_buf.spec();
// Verify source is RGB
if (src_spec.nchannels != 3) {
error_msg = "Image is not RGB (has " + std::to_string(src_spec.nchannels) + " channels)";
return false;
}
// Calculate required buffer size
TypeDesc data_type = src_spec.format;
const size_t required_size = src_spec.width * src_spec.height * 4 * data_type.size();
if (buffer_length < required_size) {
error_msg = "Buffer too small. Required: " + std::to_string(required_size) +
", provided: " + std::to_string(buffer_length);
return false;
}
// Prepare destination buffer wrapped in ImageBuf
ImageSpec dst_spec(src_spec.width, src_spec.height, 4, data_type);
ImageBuf dst_buf(dst_spec, output_buffer);
// Set up channel remapping with alpha=1.0
const int channel_order[] = {0, 1, 2, -1}; // Source RGB, new alpha
const float channel_values[] = {0.0f, 0.0f, 0.0f, 1.0f};
// Perform channel conversion
if (!ImageBufAlgo::channels(dst_buf, src_buf,
/* channel count */ 4,
channel_order, channel_values)) {
error_msg = "Channel conversion failed: " + dst_buf.geterror();
return false;
}
return true;
}

75
libs/oiio/oiio.nim Normal file
View file

@ -0,0 +1,75 @@
import std/strformat
{.compile("oiio.cpp","-std=c++14").}
{.passL: "-lOpenImageIO -lOpenImageIO_Util".}
type OiioImageFormat* {.size: 4.} = enum
FORMAT_UNKNOWN = 0
FORMAT_BYTE # 8-bit unsigned
FORMAT_SHORT # 16-bit unsigned
FORMAT_HALF # 16-bit float
FORMAT_FLOAT # 32-bit float
template size*(format: OiioImageFormat): int =
case format:
of FORMAT_BYTE: 1
of FORMAT_SHORT: 2
of FORMAT_HALF: 2
of FORMAT_FLOAT: 4
else: 0
proc decode(input: pointer, input_len: int,
output: pointer, output_len: int,
desired_channels: int32, format: OiioImageFormat, is_BGR: var byte,
file_name: cstring): int32 {.importc:"oiio_image_decode", cdecl.}
proc get_attributes(input: pointer, input_len: int,
width: var int32, height: var int32,
channels: var int32, format: var OiioImageFormat,
file_name: cstring): int32 {.importc:"oiio_image_get_attributes", cdecl.}
template has_bytes(p: pointer, bytes: string): bool =
cmpMem(p, bytes.cstring, bytes.len) == 0
proc file_name_from_magic(input: openArray[byte]): string =
if input.len < 10:
return "image"
var p = input[0].addr
# OpenImageIO usually has no trouble detecting the most common formats,
# however it seems we need to give it a file name for some, like EXR.
if p.has_bytes "\x89PNG": return "image.png"
if p.has_bytes "\xFF\xD8\xFF": return "image.jpg"
if p.has_bytes "v/1\x01": return "image.exr"
if p.has_bytes "#?RADIANCE": return "image.hdr"
return "image"
proc oiioImageGetAttributes*(input: openArray[byte]): (int, int, int, OiioImageFormat) =
# returns: width, height, channels, format
doAssert input.len != 0, "input is empty"
let file_name = file_name_from_magic(input)
var w,h,c: int32
var f: OiioImageFormat
var res = get_attributes(input[0].addr, input.len, w,h,c,f, file_name.cstring)
doAssert res != 0, "error getting image attibutes"
doAssert f != FORMAT_UNKNOWN, "unknown image format"
return (w.int, h.int, c.int, f)
proc oiioImageDecode*[T](input: openArray[byte], min_channels = 0): seq[T] =
doAssert input.len != 0, "input is empty"
let file_name = file_name_from_magic(input)
var w,h,c: int32
var f: OiioImageFormat
var res = get_attributes(input[0].addr, input.len, w,h,c,f, file_name.cstring)
doAssert res != 0, "error getting image attibutes"
doAssert w != 0 and h != 0 and c != 0, "invalid image"
let channels = max(min_channels.int32, c)
# let size_in = w * h * c
let size_out = w * h * channels
result.setLen size_out * f.size
var is_BGR: byte
res = decode(input[0].addr, input.len,
result[0].addr, size_out * f.size,
channels, f, is_BGR, file_name.cstring)
doAssert res != 0, "error decoding image"

2
libs/packages/oiio.nim Normal file
View file

@ -0,0 +1,2 @@
import ../oiio/oiio
export oiio

View file

@ -41,10 +41,13 @@ import vmath except Quat, quat
import arr_ref
import float16
import stb_image/read as stbi
const myouConvertHdrToFloat16 {.booldefine.} = true
const myouConvertHdrToFloat16 {.booldefine.} = not defined(myouUseOpenImageIO)
when not defined(nimdoc):
when defined(myouUseOpenImageIO):
import oiio
else:
import stb_image/read as stbi
when not defined(nimdoc):
import tinyexr
proc stride*(format: TextureFormat): int =
@ -106,7 +109,19 @@ proc f32_to_f16(source: ptr UncheckedArray[float32], dest: ptr UncheckedArray[Fl
dest[i] = source[i].tofloat16(clamp=true)
proc getDimensionsFormat*(p: pointer, len: int, min_channels=0): (int, int, TextureFormat) =
when not defined(nimdoc):
when defined(myouUseOpenImageIO):
var (width, height, channels, fmt) = oiioImageGetAttributes(p.toOpenArrayByte(0, len-1))
channels = max(min_channels, channels)
when myouConvertHdrToFloat16:
if fmt == FORMAT_FLOAT: fmt = FORMAT_HALF
let format = case fmt:
of FORMAT_BYTE: (R_u8.int - 1 + channels).TextureFormat
of FORMAT_SHORT: (R_u16.int - 1 + channels).TextureFormat
of FORMAT_HALF: (R_f16.int - 1 + channels).TextureFormat
of FORMAT_FLOAT: (R_f32.int - 1 + channels).TextureFormat
else: R_u8
return (width, height, format)
elif not defined(nimdoc):
if isEXR(p, len):
let dims = getEXRDimensions(p, len)
return (dims[0], dims[1], RGBA_f16)
@ -144,13 +159,25 @@ proc loadFileFromSlices*(tex: Texture, slices: seq[SliceMem[byte]],
var out_buffer = newSliceMem[byte](layer_stride * tex.depth)
var pos = 0
for slice in slices:
var pixels_ptr: pointer
var pixels_len: int
when defined(myouUseOpenImageIO):
let image = oiioImageDecode[byte](slice.toOpenArrayByte, min_channels)
pixels_ptr = image[0].addr
pixels_len = image.len
when myouConvertHdrToFloat16:
if pixels_len == layer_stride * 2:
f32_to_f16(
cast[ptr UncheckedArray[float32]](pixels_ptr),
cast[ptr UncheckedArray[Float16]](pixels_ptr),
pixels_len div 4)
pixels_len = pixels_len div 2
# var flip = not flip
else:
var image: imagePixelData[byte]
var image_16: imagePixelData[uint16]
var image_f: imagePixelData[float32]
var buffer: ArrRef[byte]
# a reference to this pointer is kept with one of the vars above
var pixels_ptr: pointer
var pixels_len: int
var flip = flip
if isEXR(slice.data, slice.byte_len):
let (width, height, pixels) = decodeEXR(slice.data, slice.byte_len)

View file

@ -430,8 +430,10 @@ proc newTexture*(engine: MyouEngine, name: string,
self.engine.renderer.enqueue proc()=
self.ensure_storage(existing_resource)
if pixels != nil:
doAssert pixels.byte_len == width * height * depth * format.stride,
"Texture pixels have wrong size for the given dimensions and format"
self.loadFromPixelsPointer(pixels.toPointer)
else:
elif existing_resource == nil:
self.preallocate()
when defined(myouUseRenderdoc):
# Note: when we switch to arrays we'll have to store resolutions
@ -530,7 +532,7 @@ proc newTexture*(engine: MyouEngine, name: string, file_name: string, is_sRGB: b
echo getCurrentExceptionMsg()
echo "Error loading texture " & file_name
else:
var (width, height, format) = getDimensionsFormat(res.orig_data.data, res.orig_data.byte_len, 3)
var (width, height, format) = getDimensionsFormat(res.orig_data.toPointer, res.orig_data.byte_len, 3)
if is_sRGB:
if format in [RGB_u8, RGBA_u8]:
format = format.to_sRGB