-
Notifications
You must be signed in to change notification settings - Fork 0
03 Image
The data for an image is a flat stream of bytes, but it semantically represents a 2D grid, which is evident via the index computation in row * image.extent.x() + col
. This transformation would also be useful when wanting to modify the colours of specific pixels in an image. Let's upgrade the ad-hoc ImageData
type to accomodate both these features.
First let's move Rgb
into its own header: create a new file src/tray/rgb.hpp
(src/tray/
will henceforth be implicit), add it to the target's sources in the CMake script, and move struct Rgb : Vec<std::uint8_t, 3> {};
in there. We will upgrade this type further, but later.
Create new files image.hpp
and image.cpp
, and add them to the target's sources in the CMake script.
image.hpp
:
struct Index2D {
std::uint32_t row{};
std::uint32_t col{};
constexpr std::size_t index(uvec2 const extent) { return row * extent.x() + col; }
};
class Image {
public:
Image() = default;
Image(uvec2 extent) { resize(extent); }
void resize(uvec2 extent);
Rgb const& operator[](Index2D i) const;
Rgb& operator[](Index2D i);
uvec2 extent() const { return m_extent; }
private:
std::vector<Rgb> m_data{};
uvec2 m_extent{};
};
image.cpp
:
void Image::resize(uvec2 extent) {
m_data.resize(extent.x() * extent.y());
m_extent = extent;
}
Rgb const& Image::operator[](Index2D i) const {
auto const index = i.index(m_extent);
assert(index < m_data.size());
return m_data[index];
}
Rgb& Image::operator[](Index2D i) { return const_cast<Rgb&>(std::as_const(*this)[i]); }
In order to not bring ostream
into this header, let's move the serialization code into its own header; create io.hpp
and io.cpp
.
io.hpp
:
namespace tray {
std::ostream& operator<<(std::ostream& out, Image const& image);
namespace io {
bool write(Image const& image, char const* path);
}
} // namespace tray
io.cpp
:
namespace tray {
std::ostream& operator<<(std::ostream& out, Image const& image) {
// write header
out << "P3\n" << image.extent().x() << ' ' << image.extent().y() << "\n255\n";
// write each row
for (std::uint32_t row = 0; row < image.extent().y(); ++row) {
// write each column
for (std::uint32_t col = 0; col < image.extent().x(); ++col) {
// obtain corresponding Rgb
auto const& rgb = image[{row, col}];
// write out each channel
for (auto const channel : rgb.values) { out << static_cast<int>(channel) << ' '; }
}
out << '\n';
}
return out;
}
bool io::write(Image const& image, char const* path) {
if (auto file = std::ofstream(path)) { return static_cast<bool>(file << image); }
return false;
}
} // namespace tray
With all this out of the way, main.cpp
shrinks down to almost nothing. We spice it up by writing a 10x larger image, incorporating a red-yellow vertical gradient, and writing it directly to a file.
main.cpp
:
using namespace tray;
int main() {
static constexpr auto extent = uvec2{400U, 300U};
auto image = Image{extent};
for (std::uint32_t row = 0; row < image.extent().y(); ++row) {
auto const ratio = static_cast<float>(row) / static_cast<float>(image.extent().y());
auto const tint = ratio * static_cast<float>(0xff);
for (std::uint32_t col = 0; col < image.extent().x(); ++col) {
auto& rgb = image[{row, col}];
rgb.x() = 0xff;
rgb.y() = static_cast<std::uint8_t>(tint);
}
}
io::write(image, "test.ppm");
}
Check out the new flame! 🔥