Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add metadata to f3d::image #1273

Merged
merged 7 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/actions/static-analysis-ci/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ runs:
--project=../build/compile_commands.json
--enable=all
--suppressions-list=.cppcheck.supp
--inline-suppr
--error-exitcode=1
16 changes: 16 additions & 0 deletions library/public/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,22 @@ class F3D_EXPORT image
*/
std::string toTerminalText() const;

/**
* Set the value for a metadata key. Setting an empty value (`""`) removes the key.
*/
f3d::image& setMetadata(const std::string& key, const std::string& value);

/**
* Get the value for a metadata key.
* Throw `std::invalid_argument` exception if key does not exist.
*/
std::string getMetadata(const std::string& key) const;

/**
* List all the metadata keys which have a value set.
*/
std::vector<std::string> allMetadata() const;

/**
* An exception that can be thrown by the image when there.
* is an error on write.
Expand Down
102 changes: 95 additions & 7 deletions library/src/image.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,35 @@
#include <vtkImageReader2Collection.h>
#include <vtkImageReader2Factory.h>
#include <vtkJPEGWriter.h>
#include <vtkPNGReader.h>
#include <vtkPNGWriter.h>
#include <vtkPointData.h>
#include <vtkSmartPointer.h>
#include <vtkStringArray.h>
#include <vtkTIFFWriter.h>
#include <vtkUnsignedCharArray.h>
#include <vtksys/SystemTools.hxx>

#include <algorithm>
#include <cassert>
#include <regex>
#include <sstream>
#include <string>
#include <unordered_map>

namespace f3d
{
class image::internals
{
inline static const std::string metadataKeyPrefix = "f3d:";

public:
vtkSmartPointer<vtkImageData> Image;
std::unordered_map<std::string, std::string> Metadata;

template<typename WriterType>
std::vector<unsigned char> SaveBuffer()
std::vector<unsigned char> SaveBuffer(vtkSmartPointer<WriterType> writer)
{
vtkNew<WriterType> writer;
writer->WriteToMemoryOn();
writer->SetInputData(this->Image);
writer->Write();
Expand All @@ -45,6 +51,41 @@

return result;
}

void WritePngMetadata(vtkPNGWriter* pngWriter)
{
// cppcheck-suppress unassignedVariable
// (false positive, fixed in cppcheck 2.8)
for (const auto& [key, value] : this->Metadata)
{
if (!value.empty())
{
pngWriter->AddText((metadataKeyPrefix + key).c_str(), value.c_str());
}
}
}

void ReadPngMetadata(vtkPNGReader* pngReader)
{
int beginEndIndex[2];
for (size_t i = 0; i < pngReader->GetNumberOfTextChunks(); ++i)
{
const vtkStdString key = pngReader->GetTextKey(static_cast<int>(i));
if (key.rfind(metadataKeyPrefix, 0) == 0)
{
pngReader->GetTextChunks(key.c_str(), beginEndIndex);
const int index = beginEndIndex[1] - 1; // only read the last key
if (index > -1)
{
const std::string value(pngReader->GetTextValue(index));
if (!value.empty())
{
this->Metadata[key.substr(metadataKeyPrefix.length())] = value;
}
}
}
}
}
};

//----------------------------------------------------------------------------
Expand Down Expand Up @@ -95,6 +136,12 @@
reader->SetFileName(fullPath.c_str());
reader->Update();
this->Internals->Image = reader->GetOutput();

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

if (!this->Internals->Image)
Expand Down Expand Up @@ -314,8 +361,12 @@
switch (format)
{
case SaveFormat::PNG:
writer = vtkSmartPointer<vtkPNGWriter>::New();
break;
{
vtkNew<vtkPNGWriter> pngWriter;
this->Internals->WritePngMetadata(pngWriter);
writer = pngWriter;
}
break;
case SaveFormat::JPG:
writer = vtkSmartPointer<vtkJPEGWriter>::New();
break;
Expand Down Expand Up @@ -343,11 +394,15 @@
switch (format)
{
case SaveFormat::PNG:
return this->Internals->SaveBuffer<vtkPNGWriter>();
{
vtkSmartPointer<vtkPNGWriter> writer = vtkSmartPointer<vtkPNGWriter>::New();
this->Internals->WritePngMetadata(writer);
return this->Internals->SaveBuffer(writer);
}
case SaveFormat::JPG:
return this->Internals->SaveBuffer<vtkJPEGWriter>();
return this->Internals->SaveBuffer(vtkSmartPointer<vtkJPEGWriter>::New());
case SaveFormat::BMP:
return this->Internals->SaveBuffer<vtkBMPWriter>();
return this->Internals->SaveBuffer(vtkSmartPointer<vtkBMPWriter>::New());
default:
throw write_exception("Cannot save to buffer in the specified format");
}
Expand Down Expand Up @@ -496,6 +551,39 @@
return ss.str();
}

//----------------------------------------------------------------------------
f3d::image& image::setMetadata(const std::string& key, const std::string& value)
{
if (value.empty())
{
this->Internals->Metadata.erase(key);
}
else
{
this->Internals->Metadata[key] = value;
}
return *this;
}

//----------------------------------------------------------------------------
std::string image::getMetadata(const std::string& key) const
{
if (this->Internals->Metadata.count(key))
{
return this->Internals->Metadata[key];
}
throw std::out_of_range(key);
}

//----------------------------------------------------------------------------
std::vector<std::string> image::allMetadata() const
{
std::vector<std::string> keys;
std::transform(this->Internals->Metadata.begin(), this->Internals->Metadata.end(),
std::back_inserter(keys), [](const auto& kv) { return kv.first; });
return keys;
}

Check warning on line 585 in library/src/image.cxx

View check run for this annotation

Codecov / codecov/patch

library/src/image.cxx#L585

Added line #L585 was not covered by tests

//----------------------------------------------------------------------------
image::write_exception::write_exception(const std::string& what)
: exception(what)
Expand Down
112 changes: 99 additions & 13 deletions library/testing/TestSDKImage.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
#include <functional>
#include <iostream>
#include <random>
#include <set>
#include <sstream>

int TestSDKImage(int argc, char* argv[])
{
const std::string testingDir(argv[1]);
const std::string tmpDir(argv[2]);

// check supported formats
std::vector<std::string> formats = f3d::image::getSupportedFormats();

Expand Down Expand Up @@ -42,10 +46,10 @@ int TestSDKImage(int argc, char* argv[])
generated.setContent(pixels.data());

// test save in different formats
generated.save(std::string(argv[2]) + "TestSDKImage.png");
generated.save(std::string(argv[2]) + "TestSDKImage.jpg", f3d::image::SaveFormat::JPG);
generated.save(std::string(argv[2]) + "TestSDKImage.tif", f3d::image::SaveFormat::TIF);
generated.save(std::string(argv[2]) + "TestSDKImage.bmp", f3d::image::SaveFormat::BMP);
generated.save(tmpDir + "/TestSDKImage.png");
generated.save(tmpDir + "/TestSDKImage.jpg", f3d::image::SaveFormat::JPG);
generated.save(tmpDir + "/TestSDKImage.tif", f3d::image::SaveFormat::TIF);
generated.save(tmpDir + "/TestSDKImage.bmp", f3d::image::SaveFormat::BMP);

// test saveBuffer in different formats
std::vector<unsigned char> bufferPNG = generated.saveBuffer();
Expand Down Expand Up @@ -107,7 +111,7 @@ int TestSDKImage(int argc, char* argv[])
}

// check reading a 16-bits image
f3d::image shortImg(std::string(argv[1]) + "/data/16bit.png");
f3d::image shortImg(testingDir + "/data/16bit.png");

if (shortImg.getChannelType() != f3d::image::ChannelType::SHORT)
{
Expand All @@ -122,7 +126,7 @@ int TestSDKImage(int argc, char* argv[])
}

// check reading a 32-bits image
f3d::image hdrImg(std::string(argv[1]) + "/data/palermo_park_1k.hdr");
f3d::image hdrImg(testingDir + "/data/palermo_park_1k.hdr");

if (hdrImg.getChannelType() != f3d::image::ChannelType::FLOAT)
{
Expand All @@ -138,7 +142,7 @@ int TestSDKImage(int argc, char* argv[])

#if F3D_MODULE_EXR
// check reading EXR
f3d::image exrImg(std::string(argv[1]) + "/data/kloofendal_43d_clear_1k.exr");
f3d::image exrImg(testingDir + "/data/kloofendal_43d_clear_1k.exr");

if (exrImg.getChannelType() != f3d::image::ChannelType::FLOAT)
{
Expand All @@ -150,7 +154,7 @@ int TestSDKImage(int argc, char* argv[])
// check reading invalid image
try
{
f3d::image invalidImg(std::string(argv[1]) + "/data/invalid.png");
f3d::image invalidImg(testingDir + "/data/invalid.png");

std::cerr << "An exception has not been thrown when reading an invalid file" << std::endl;
return EXIT_FAILURE;
Expand All @@ -166,7 +170,7 @@ int TestSDKImage(int argc, char* argv[])
}

// check generated image with baseline
f3d::image baseline(std::string(argv[1]) + "/baselines/TestSDKImage.png");
f3d::image baseline(testingDir + "/baselines/TestSDKImage.png");

if (generated.getWidth() != width || generated.getHeight() != height)
{
Expand Down Expand Up @@ -258,20 +262,102 @@ int TestSDKImage(int argc, char* argv[])
return ss.str();
};

if (f3d::image(std::string(argv[1]) + "/data/toTerminalText-rgb.png").toTerminalText() !=
fileToString(std::string(argv[1]) + "/data/toTerminalText-rgb.txt"))
if (f3d::image(testingDir + "/data/toTerminalText-rgb.png").toTerminalText() !=
fileToString(testingDir + "/data/toTerminalText-rgb.txt"))
{
std::cerr << "toTerminalText() (RGB image) failed" << std::endl;
return EXIT_FAILURE;
}

if (f3d::image(std::string(argv[1]) + "/data/toTerminalText-rgba.png").toTerminalText() !=
fileToString(std::string(argv[1]) + "/data/toTerminalText-rgba.txt"))
if (f3d::image(testingDir + "/data/toTerminalText-rgba.png").toTerminalText() !=
fileToString(testingDir + "/data/toTerminalText-rgba.txt"))
{
std::cerr << "toTerminalText() (RGBA image) failed" << std::endl;
return EXIT_FAILURE;
}
}

{
f3d::image img(4, 2, 3);
img.setMetadata("foo", "bar");
img.setMetadata("hello", "world");
if (img.getMetadata("foo") != "bar" || img.getMetadata("hello") != "world")
{
std::cerr << "setMetadata() or getMetadata() failed" << std::endl;
return EXIT_FAILURE;
}

const std::vector<std::string> keys = img.allMetadata();
if (std::set<std::string>(keys.begin(), keys.end()) !=
Meakk marked this conversation as resolved.
Show resolved Hide resolved
std::set<std::string>({ "foo", "hello" }))
{
std::cerr << "allMetadata() failed" << std::endl;
return EXIT_FAILURE;
}

try
{
img.getMetadata("baz"); // expected to throw
std::cerr << "getMetadata() failed to throw" << std::endl;
return EXIT_FAILURE;
}
catch (std::out_of_range& e)
{
/* expected, key doesn't exist */
}

try
{
img.setMetadata("foo", ""); // empty value, should remove key
img.getMetadata("foo"); // expected to throw
std::cerr << "setMetadata() with empty value failed" << std::endl;
return EXIT_FAILURE;
}
catch (std::out_of_range& e)
{
/* expected, key has been removed */
}

if (img.allMetadata() != std::vector<std::string>({ "hello" }))
{
std::cerr << "allMetadata() failed" << std::endl;
return EXIT_FAILURE;
}

img.setMetadata("foo", ""); // make sure removing twice is ok
}

{
f3d::image img1(4, 2, 3);
img1.setMetadata("foo", "bar");
img1.setMetadata("hello", "world");
img1.save(tmpDir + "/metadata.png");

f3d::image img2(tmpDir + "/metadata.png");
if (img2.getMetadata("foo") != "bar" || img2.getMetadata("hello") != "world")
{
std::cerr << "saving or loading file metadata failed" << std::endl;
return EXIT_FAILURE;
}
}

{
f3d::image img1(4, 2, 3);
img1.setMetadata("foo", "bar");
img1.setMetadata("hello", "world");
{
std::vector<unsigned char> buffer = img1.saveBuffer();
std::ofstream outfile(tmpDir + "/metadata-buffer.png", std::ios::out | std::ios::binary);
outfile.write((const char*)&buffer[0], buffer.size());
}

f3d::image img2(tmpDir + "/metadata-buffer.png");
if (img2.getMetadata("foo") != "bar" || img2.getMetadata("hello") != "world")
{
std::cerr << "saving or loading buffer metadata failed" << std::endl;
return EXIT_FAILURE;
}
}

return EXIT_SUCCESS;
}
Loading
Loading