Skip to content

03 Image

Karn Kaul edited this page Oct 1, 2022 · 1 revision

Introduction

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.

Code

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! 🔥

test.ppm

Clone this wiki locally