Skip to content
Open
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
8723e62
Implementation, testing, c, java, and python bindings
RealEdwardS Mar 20, 2026
02c9216
Removed config.cxx file
RealEdwardS Mar 23, 2026
6cd7274
Removed unnecessary iostream
RealEdwardS Mar 23, 2026
e955178
Removed double comment
RealEdwardS Mar 26, 2026
2e8017d
Formatting
RealEdwardS Mar 26, 2026
b4e7a44
Fixed java and python bindings
RealEdwardS Mar 27, 2026
44e510d
Added doc, removed file filesystem_error try/catch
RealEdwardS Mar 27, 2026
3ada711
Update python/F3DPythonBindings.cxx
RealEdwardS Mar 27, 2026
6d33273
Updated docs/removed webassembly for now
RealEdwardS Apr 6, 2026
c9dbfa9
Updated docs
RealEdwardS Apr 6, 2026
d9148ee
Updated api
RealEdwardS Apr 6, 2026
8407e37
Removed comments in testing
RealEdwardS Apr 6, 2026
2e80b88
Added more testing
RealEdwardS Apr 7, 2026
9434128
Fixed best reader iteration
RealEdwardS Apr 25, 2026
c851819
Fixed doc on available supported formats
RealEdwardS Apr 25, 2026
44bbd60
Added WebP to doc
RealEdwardS Apr 25, 2026
a7c5fe0
Updated docs
RealEdwardS May 5, 2026
01cd40b
Formatting and fixed best reader
RealEdwardS May 6, 2026
a9736ec
Formatting
RealEdwardS May 28, 2026
6a5d325
Formatting
RealEdwardS May 29, 2026
d9d537a
Formatting again
RealEdwardS May 29, 2026
563dbec
Formatting again again
RealEdwardS May 29, 2026
2a09647
Added vtk version check
RealEdwardS May 30, 2026
9d1f812
Removed duplicate header include
RealEdwardS May 31, 2026
bbab7b2
Fixed variable redeclaration in testing, and added version check in t…
RealEdwardS May 31, 2026
4f7d762
Fixed unreferenced parameter warning/error
RealEdwardS May 31, 2026
4ad5f97
Formatting in testing
RealEdwardS May 31, 2026
b897187
Revert "Fixed variable redeclaration in testing, and added version ch…
RealEdwardS May 31, 2026
2d87f8b
Fix redeclaration error
RealEdwardS May 31, 2026
7863b0f
Removed assigning variables to itself
RealEdwardS May 31, 2026
12b17c8
Modified SDKImage Test to see if everything else works - Binding test…
RealEdwardS Jun 8, 2026
33cfb62
Changed version build
RealEdwardS Jun 11, 2026
2a3f4c7
Added std::byte library in testing
RealEdwardS Jun 11, 2026
91d1801
Renamed bestReader to reader
RealEdwardS Jun 14, 2026
cde32df
Removed unnecessary cstddef library
RealEdwardS Jun 14, 2026
21d2a44
Created new python test
RealEdwardS Jun 14, 2026
066bb7f
Created dedicated test for java
RealEdwardS Jun 14, 2026
3d84a9c
Created dedicated c test
RealEdwardS Jun 14, 2026
fc48c8d
Changed version
RealEdwardS Jun 14, 2026
6aeb46d
Formatting
RealEdwardS Jun 14, 2026
edea228
Created dedicated cpp test
RealEdwardS Jun 14, 2026
85c6e11
Fixed memory leak in c binding test
RealEdwardS Jun 14, 2026
38fcd2b
Removed duplicate test case py
RealEdwardS Jun 15, 2026
68c4bf4
Formatting
RealEdwardS Jun 15, 2026
cb04623
Rerun
RealEdwardS Jun 15, 2026
f26cff5
Rerun pt2
RealEdwardS Jun 15, 2026
0f16cb2
Renamed file and removed unncessary comment for TestSDKImageStream
RealEdwardS Jun 16, 2026
2839274
Renamed TestImageStream to TestSDKImageStream
RealEdwardS Jun 16, 2026
2047c90
Added version requirement to docs
RealEdwardS Jun 16, 2026
599337f
Added eol to c binding test case
RealEdwardS Jun 16, 2026
2aec1ff
Renamed compare variable in c binding test
RealEdwardS Jun 16, 2026
4a0fc8f
Freed memory in c binding
RealEdwardS Jun 16, 2026
3686aa1
Formatting
RealEdwardS Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions c/image_c_api.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ f3d_image_t* f3d_image_new_path(const char* path)
return reinterpret_cast<f3d_image_t*>(img);
}

//----------------------------------------------------------------------------
f3d_image_t* f3d_image_new_stream(unsigned char* byte, unsigned int size)
{

f3d::image* img = nullptr;

try
{
img = new f3d::image(reinterpret_cast<std::byte*>(byte), static_cast<size_t>(size));
}
catch (const f3d::image::read_exception& e)
{
std::cerr << "Error loading image: " << e.what() << "\n";
return nullptr;
}

return reinterpret_cast<f3d_image_t*>(img);
}

//----------------------------------------------------------------------------
void f3d_image_delete(f3d_image_t* img)
{
Expand Down
9 changes: 9 additions & 0 deletions c/image_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ extern "C"
*/
F3D_EXPORT f3d_image_t* f3d_image_new_path(const char* path);

/**
* @brief Create a new image object from a stream
*
* The returned image must be deleted with f3d_image_delete().
*
* @return Pointer to the newly created image object, NULL on failure
*/
F3D_EXPORT f3d_image_t* f3d_image_new_stream(unsigned char* buffer, unsigned int size);

/**
* @brief Delete an image object
* @param img Pointer to the image object to be deleted
Expand Down
6 changes: 6 additions & 0 deletions c/testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250501)
)
endif()

if(VTK_VERSION VERSION_GREATER_EQUAL 9.6.20260128)
list(APPEND f3d_c_api_tests_list
test_image_stream.c
)
endif()

set(CMAKE_TESTDRIVER_EXTRA_INCLUDES "")
set(CMAKE_TESTDRIVER_ARGVC_FUNCTION "")
set(CMAKE_TESTDRIVER_BEFORE_TESTMAIN "")
Expand Down
56 changes: 56 additions & 0 deletions c/testing/test_image_stream.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#include <image_c_api.h>

#include <stdio.h>

int test_image_stream()
{
f3d_image_t* img = f3d_image_new_params(800, 600, 3, BYTE);
if (!img)
{
puts("[ERROR] Failed to create image");
return 1;
}

unsigned int count;
unsigned char* tempBuffer = f3d_image_save_buffer(NULL, PNG, &count); // this shouldn't crash
if (tempBuffer != NULL)
{
f3d_image_delete(img);
return 1;
}

f3d_image_t* temp_image_stream = f3d_image_new_stream(tempBuffer, 0);
if (temp_image_stream != NULL)
{
f3d_image_delete(img);
return 1;
}

unsigned int buffer_size;
unsigned char* buffer = f3d_image_save_buffer(img, PNG, &buffer_size);
if (buffer)
{
f3d_image_t* image_stream = f3d_image_new_stream(buffer, buffer_size);

if (image_stream)
{
double stream_error = f3d_image_compare(img, image_stream);
Comment thread
RealEdwardS marked this conversation as resolved.
Outdated
f3d_image_free_buffer(buffer);
f3d_image_delete(image_stream);

if (stream_error != 0)
{
f3d_image_delete(img);
return 1;
}
}
else
{
f3d_image_delete(img);
Comment thread
RealEdwardS marked this conversation as resolved.
return 1;
}
}

f3d_image_delete(img);
return 0;
}
Comment thread
RealEdwardS marked this conversation as resolved.
Outdated
9 changes: 9 additions & 0 deletions java/F3DImageBindings.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ extern "C"
return reinterpret_cast<jlong>(img);
}

JNIEXPORT jlong JAVA_BIND(Image, nativeCreateFromStream)(JNIEnv* env, jclass, jbyteArray buffer)
{
jbyte* bufferData = env->GetByteArrayElements(buffer, nullptr);
size_t size = env->GetArrayLength(buffer);

f3d::image* img = new f3d::image(reinterpret_cast<std::byte*>(bufferData), size);
return reinterpret_cast<jlong>(img);
}

JNIEXPORT jlong JAVA_BIND(Image, nativeCreate)(
JNIEnv* env, jclass, jint width, jint height, jint channelCount, jint type)
{
Expand Down
10 changes: 10 additions & 0 deletions java/Image.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ public Image(int width, int height, int channelCount) {
this(width, height, channelCount, ChannelType.BYTE);
}

/**
* Create an image from given stream buffer
*
* @param buffer stream buffer
*/
public Image(byte[] buffer) {
mNativeAddress = nativeCreateFromStream(buffer);
}

Image(long nativeAddress) {
mNativeAddress = nativeAddress;
}
Expand Down Expand Up @@ -233,5 +242,6 @@ public void delete() {

private static native long nativeCreateFromFile(String filePath);
private static native long nativeCreate(int width, int height, int channelCount, int type);
private static native long nativeCreateFromStream(byte[] buffer);
private static native void nativeDestroy(long nativeAddress);
}
6 changes: 6 additions & 0 deletions java/testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ if(VTK_VERSION VERSION_GREATER_EQUAL 9.4.20250501)
)
endif()

if(VTK_VERSION VERSION_GREATER_EQUAL 9.6.20260128)
list(APPEND javaf3dTests_list
${CMAKE_CURRENT_SOURCE_DIR}/TestImageStream.java
)
endif()

set(java_test_args "-ea") # enable assertions
if (WIN32)
list(APPEND java_test_args "--enable-native-access=ALL-UNNAMED")
Expand Down
16 changes: 16 additions & 0 deletions java/testing/TestImageStream.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import app.f3d.F3D.*;

public class TestImageStream {
public static void main(String[] args) {
Image img1 = new Image(300, 200, 3);

byte[] img1Buffer = img1.saveBuffer(Image.SaveFormat.PNG);

Image img2 = new Image(img1Buffer);

img1.equals(img2);

img1.delete();
img2.delete();
}
}
10 changes: 10 additions & 0 deletions library/public/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "export.h"

/// @cond
#include <cstddef>
#include <filesystem>
#include <string>
#include <vector>
Expand Down Expand Up @@ -56,6 +57,15 @@ class F3D_EXPORT image
*/
explicit image(const std::filesystem::path& filePath);

/**
* Read provided buffer into a new image instance, the following formats are
* supported: PNG, PNM , BMP, HDR, JPEG, TGA, WebP. EXR files are also
* supported if the associated module is built.
*
* Throws an image::read_exception in case of failure.
Comment thread
RealEdwardS marked this conversation as resolved.
*/
image(std::byte* byte, std::size_t size);

/**
* Create an image from a given width, height, and channel count.
* A channel type can also be given. Default is BYTE.
Expand Down
62 changes: 62 additions & 0 deletions library/src/image.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <vtkImageReader2Factory.h>
#include <vtkImageSSIM.h>
#include <vtkJPEGWriter.h>
#include <vtkMemoryResourceStream.h>
#include <vtkPNGReader.h>
#include <vtkPNGWriter.h>
#include <vtkPointData.h>
Expand Down Expand Up @@ -214,6 +215,67 @@ image::image(const fs::path& filePath)
}
}

//----------------------------------------------------------------------------
image::image([[maybe_unused]] std::byte* buffer, [[maybe_unused]] std::size_t size)
: Internals(new image::internals())
{
#if VTK_VERSION_NUMBER >= VTK_VERSION_CHECK(9, 6, 20260128)
detail::init::initialize();

if (buffer == nullptr)
{
delete this->Internals;
throw read_exception("Cannot read image from buffer: Buffer is empty");
}

vtkNew<vtkMemoryResourceStream> stream;
stream->SetBuffer(buffer, size);

vtkNew<vtkImageReader2Collection> availableReaders;
vtkImageReader2Factory::GetRegisteredReaders(availableReaders);

Comment thread
RealEdwardS marked this conversation as resolved.
vtkCollectionSimpleIterator iterator;
vtkImageReader2* currentReader;
vtkImageReader2* reader = nullptr;

for (availableReaders->InitTraversal(iterator);
(currentReader = availableReaders->GetNextImageReader2(iterator));)
{
if (currentReader->CanReadFile(stream) > 0)
{
reader = currentReader;
break;
}
}
Comment thread
mwestphal marked this conversation as resolved.

if (reader == nullptr)
{
delete this->Internals;
throw read_exception("Cannot read image from buffer: No image reader supports this stream.");
}

reader->SetStream(stream);
reader->Update();
this->Internals->Image = reader->GetOutput();

vtkPNGReader* pngReader = vtkPNGReader::SafeDownCast(reader);
if (pngReader != nullptr)
{
this->Internals->ReadPngMetadata(pngReader);
}

if (!this->Internals->Image)
{
delete this->Internals;
throw read_exception("Cannot read image from buffer");
}

#else
delete this->Internals;
throw read_exception("VTK >= v9.6.20260128 is required for streaming images");
#endif
}

//----------------------------------------------------------------------------
image::~image()
{
Expand Down
1 change: 1 addition & 0 deletions library/testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ endif()
if(VTK_VERSION VERSION_GREATER_EQUAL 9.6.20260128)
list(APPEND libf3dSDKTests_list
TestSDKSceneInvalidHeader.cxx
TestImageStream.cxx
)
endif()

Expand Down
43 changes: 43 additions & 0 deletions library/testing/TestImageStream.cxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#include "PseudoUnitTest.h"

#include <image.h>

#include <algorithm>
#include <random>

int TestImageStream([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
{
PseudoUnitTest test;

constexpr unsigned int width = 64;
constexpr unsigned int height = 64;
constexpr unsigned int channels = 3;

// fill with deterministic random values
// do not use std::uniform_int_distribution, it's not giving the same result on different
// platforms
Comment thread
RealEdwardS marked this conversation as resolved.
Outdated
std::mt19937 randGenerator;

f3d::image generated(width, height, channels);
std::vector<uint8_t> pixels(width * height * channels);
std::ranges::generate(pixels, [&]() { return static_cast<uint8_t>(randGenerator() % 256); });
generated.setContent(pixels.data());

// check reading stream
std::vector<unsigned char> generatedBuffer = generated.saveBuffer();
std::byte* bufferData = reinterpret_cast<std::byte*>(generatedBuffer.data());
f3d::image bufferImage(bufferData, generatedBuffer.size());
test("check loading stream from image reader", generated.compare(bufferImage), 0.0);

// check reading inexistent/null stream
test.expect<f3d::image::read_exception>(
"read image from invalid/null stream", [&]() { f3d::image nullImgStream(nullptr, 10); });

// check reading invalid stream
std::vector<unsigned char> invalidBuffer = { 0, 1, 2, 3, 4, 5 };
std::byte* invalidBufferData = reinterpret_cast<std::byte*>(invalidBuffer.data());
test.expect<f3d::image::read_exception>("read image from invalid stream",
[&]() { f3d::image invalidImgStream(invalidBufferData, 10); });

return test.result();
}
5 changes: 5 additions & 0 deletions library/testing/TestSDKImage.cxx
Comment thread
mwestphal marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,21 @@ int TestSDKImage([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
// test exceptions
test.expect<f3d::image::write_exception>("save incompatible buffer to BMP format",
[&]() { std::ignore = img16.saveBuffer(f3d::image::SaveFormat::BMP); });

test.expect<f3d::image::write_exception>("save incompatible buffer to PNG format",
[&]() { std::ignore = img32.saveBuffer(f3d::image::SaveFormat::PNG); });

f3d::image img2Ch(4, 4, 2);
f3d::image img5Ch(4, 4, 5);
test.expect<f3d::image::write_exception>("save incompatible channel count to BMP format",
[&]() { std::ignore = img5Ch.saveBuffer(f3d::image::SaveFormat::BMP); });

test.expect<f3d::image::write_exception>("save incompatible channel count to JPG format",
[&]() { std::ignore = img2Ch.saveBuffer(f3d::image::SaveFormat::JPG); });

test.expect<f3d::image::write_exception>("save image to invalid path",
[&]() { img2Ch.save("/" + std::string(257, 'x') + "/file.ext"); });

test.expect<f3d::image::write_exception>("save image to invalid filename",
[&]() { img2Ch.save(testingDir + std::string(257, 'x') + ".ext"); });

Expand Down Expand Up @@ -174,6 +178,7 @@ int TestSDKImage([[maybe_unused]] int argc, [[maybe_unused]] char* argv[])
{
test.expect<f3d::image::write_exception>("invalid toTerminalText with BYTE",
[&]() { std::ignore = f3d::image(3, 3, 1, f3d::image::ChannelType::BYTE).toTerminalText(); });

test.expect<f3d::image::write_exception>("invalid toTerminalText with SHORT", [&]() {
std::ignore = f3d::image(3, 3, 4, f3d::image::ChannelType::SHORT).toTerminalText();
});
Expand Down
6 changes: 6 additions & 0 deletions python/F3DPythonBindings.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ PYBIND11_MODULE(pyf3d, module)
.def(py::init<>())
.def(py::init<const std::filesystem::path&>())
.def(py::init<unsigned int, unsigned int, unsigned int, f3d::image::ChannelType>())
.def(py::init(
[](const py::bytes& buffer)
{
const py::buffer_info info(py::buffer(buffer).request());
return f3d::image(reinterpret_cast<std::byte*>(info.ptr), info.size);
}))
Comment thread
RealEdwardS marked this conversation as resolved.
.def(py::self == py::self)
.def(py::self != py::self)
.def_static("supported_formats", &f3d::image::getSupportedFormats)
Expand Down
6 changes: 6 additions & 0 deletions python/testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ list(APPEND pyf3dTestsNoRender_list
test_utils.py
)

if(VTK_VERSION VERSION_GREATER_EQUAL 9.6.20260128)
list(APPEND pyf3dTests_list
test_image_stream.py
)
endif()

# Add all the ADD_TEST for each test
foreach(test ${pyf3dTests_list})
get_filename_component (TName ${test} NAME_WE)
Expand Down
Loading
Loading