Skip to content

Commit

Permalink
add metadata to f3d::image (#1273)
Browse files Browse the repository at this point in the history
  • Loading branch information
snoyer authored Feb 12, 2024
1 parent ab6656b commit b87ba39
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 21 deletions.
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 @@ class image::internals

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 @@ image::image(const std::string& path)
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 @@ void image::save(const std::string& path, SaveFormat format) const
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 @@ std::vector<unsigned char> image::saveBuffer(SaveFormat format) const
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 @@ std::string image::toTerminalText() const
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;
}

//----------------------------------------------------------------------------
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()) !=
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

0 comments on commit b87ba39

Please sign in to comment.