]> git.seodisparate.com - EN605.617.81.FA21_StephenSeo_DitheringProject/commitdiff
Move image.cc fns to same order as in image.h
authorStephen Seo <seo.disparate@gmail.com>
Sat, 27 Nov 2021 04:22:23 +0000 (13:22 +0900)
committerStephen Seo <seo.disparate@gmail.com>
Sat, 27 Nov 2021 04:22:23 +0000 (13:22 +0900)
src/image.cc

index 5ebffc1bc061e33119e23b5bdf6ae40a954a11fc..3d4eef8f9fe0d8af3c4533470b0833adf0d38226 100644 (file)
@@ -211,539 +211,269 @@ bool Image::SaveAsPPM(const char *filename, bool overwrite, bool packed) {
   return SaveAsPPM(std::string(filename), overwrite, packed);
 }
 
-void Image::DecodePNG(const std::string &filename) {
-  FILE *file = std::fopen(filename.c_str(), "rb");
-
-  // Check header of file to check if it is actually a png file.
-  {
-    std::array<unsigned char, 8> buf;
-    if (std::fread(buf.data(), 1, 8, file) != 8) {
-      std::fclose(file);
-      std::cout << "ERROR: File \"" << filename << "\" is smaller than 8 bytes"
-                << std::endl;
-      return;
-    } else if (png_sig_cmp(reinterpret_cast<png_const_bytep>(buf.data()), 0,
-                           8) != 0) {
-      // not png file, do nothing
-      std::fclose(file);
-      std::cout << "ERROR: File \"" << filename << "\" is not a png file"
-                << std::endl;
-      return;
-    }
-  }
-
-  // seek to head of file
-  std::rewind(file);
+uint8_t Image::ColorToGray(uint8_t red, uint8_t green, uint8_t blue) {
+  // values taken from Wikipedia article about conversion of color to grayscale
+  double y_linear = 0.2126 * (red / 255.0) + 0.7152 * (green / 255.0) +
+                    0.0722 * (blue / 255.0);
 
-  // init required structs for png decoding
-  png_structp png_ptr =
-      png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
-  if (!png_ptr) {
-    std::cout << "ERROR: Failed to initialize libpng (png_ptr) for decoding "
-                 "PNG file \""
-              << filename << '"' << std::endl;
-    std::fclose(file);
-    return;
+  if (y_linear <= 0.0031308) {
+    return std::round((12.92 * y_linear) * 255.0);
+  } else {
+    return std::round((1.055 * std::pow(y_linear, 1 / 2.4) - 0.055) * 255.0);
   }
+}
 
-  png_infop png_info_ptr = png_create_info_struct(png_ptr);
-  if (!png_info_ptr) {
-    std::cout << "ERROR: Failed to initialize libpng (png_infop) for decoding "
-                 "PNG file \""
-              << filename << '"' << std::endl;
-    png_destroy_read_struct(&png_ptr, nullptr, nullptr);
-    std::fclose(file);
-    return;
+std::unique_ptr<Image> Image::ToGrayscale() const {
+  if (IsGrayscale()) {
+    return std::unique_ptr<Image>(new Image(*this));
   }
 
-  png_infop png_end_info_ptr = png_create_info_struct(png_ptr);
-  if (!png_end_info_ptr) {
-    std::cout << "ERROR: Failed to initialize libpng (end png_infop) for "
-                 "decoding PNG file \""
-              << filename << '"' << std::endl;
-    png_destroy_read_struct(&png_ptr, &png_info_ptr, nullptr);
-    std::fclose(file);
-    return;
-  }
+  std::unique_ptr<Image> grayscale_image = std::unique_ptr<Image>(new Image{});
+  grayscale_image->width_ = this->width_;
+  grayscale_image->height_ = this->height_;
+  grayscale_image->data_.resize(width_ * height_);
 
-  // required to handle libpng errors
-  if (setjmp(png_jmpbuf(png_ptr))) {
-    png_destroy_read_struct(&png_ptr, &png_info_ptr, &png_end_info_ptr);
-    std::fclose(file);
-    return;
+  for (unsigned int i = 0; i < width_ * height_; ++i) {
+    grayscale_image->data_.at(i) =
+        ColorToGray(this->data_.at(i * 4), this->data_.at(i * 4 + 1),
+                    this->data_.at(i * 4 + 2));
   }
 
-  // pass the FILE pointer to libpng
-  png_init_io(png_ptr, file);
-
-  // have libpng process the png data
-  png_read_png(png_ptr, png_info_ptr, PNG_TRANSFORM_IDENTITY, nullptr);
-
-  // get image width (in pixels)
-  width_ = png_get_image_width(png_ptr, png_info_ptr);
-
-  // get image height (in pixels)
-  height_ = png_get_image_height(png_ptr, png_info_ptr);
-
-  // get channel count of image
-  unsigned int channels = png_get_channels(png_ptr, png_info_ptr);
-  if (channels == 1) {
-    is_grayscale_ = true;
-  } else {
-    is_grayscale_ = false;
-  }
+  return grayscale_image;
+}
 
-  if (height_ > PNG_UINT_32_MAX) {
-    png_error(png_ptr, "Image is too tall to process in memory");
-  } else if (width_ > PNG_UINT_32_MAX) {
-    png_error(png_ptr, "Image is too wide to process in memory");
+std::unique_ptr<Image> Image::ToGrayscaleDitheredWithBlueNoise(
+    Image *blue_noise) {
+  if (!blue_noise->IsGrayscale()) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: blue_noise is not grayscale"
+        << std::endl;
+    return {};
   }
 
-  png_byte **row_pointers = png_get_rows(png_ptr, png_info_ptr);
-
-  data_.clear();
-  if (channels == 3 || channels == 4) {
-    data_.reserve(width_ * 4 * height_);
-  } else if (channels == 1) {
-    data_.reserve(width_ * height_);
-  } else {
-    std::cout << "ERROR: PNG has invalid channel count == " << channels
+  auto grayscale_image = ToGrayscale();
+  if (!grayscale_image) {
+    std::cout << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to get "
+                 "grayscale Image"
               << std::endl;
-    png_destroy_read_struct(&png_ptr, &png_info_ptr, &png_end_info_ptr);
-    return;
+    return {};
   }
-  for (unsigned int y = 0; y < height_; ++y) {
-    for (unsigned int x = 0; x < width_; ++x) {
-      if (is_grayscale_) {
-        data_.push_back(row_pointers[y][x]);
-      } else if (channels == 3) {
-        for (unsigned int c = 0; c < channels; ++c) {
-          data_.push_back(row_pointers[y][x * channels + c]);
-        }
-        data_.push_back(255);
-      } else /* if (channels == 4) */ {
-        for (unsigned int c = 0; c < channels; ++c) {
-          data_.push_back(row_pointers[y][x * channels + c]);
-        }
-      }
-    }
+  auto opencl_handle = GetOpenCLHandle();
+  if (!opencl_handle) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to get OpenCLHandle"
+        << std::endl;
+    return {};
   }
 
-  // cleanup
-  png_destroy_read_struct(&png_ptr, &png_info_ptr, &png_end_info_ptr);
-  fclose(file);
+  // set up kernel and buffers
+  auto kid = opencl_handle->CreateKernelFromSource(
+      GetGrayscaleDitheringKernel(), "Dither");
+  if (kid == 0) {
+    std::cout << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to create "
+                 "OpenCL Kernel"
+              << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
 
-  // verify
-  if (is_grayscale_) {
-    if (data_.size() != width_ * height_) {
-      std::cout << "WARNING: data_.size() doesn't match width_ * height_"
-                << std::endl;
-    }
-  } else {
-    if (data_.size() != 4 * width_ * height_) {
-      std::cout << "WARNING: data_.size() doesn't match 4 * width_ * height_"
-                << std::endl;
-    }
+  auto input_buffer_id = opencl_handle->CreateKernelBuffer(
+      kid, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
+      grayscale_image->data_.size(), grayscale_image->data_.data());
+  if (input_buffer_id == 0) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set input buffer"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
   }
-}
 
-void Image::DecodePGM(const std::string &filename) {
-  is_grayscale_ = true;
+  auto output_buffer_id = opencl_handle->CreateKernelBuffer(
+      kid, CL_MEM_WRITE_ONLY, grayscale_image->data_.size(), nullptr);
+  if (output_buffer_id == 0) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set output buffer"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
 
-  std::ifstream ifs(filename);
-  if (!ifs.is_open()) {
-    std::cout << "ERROR: Failed to open file \"" << filename << '"'
+  auto blue_noise_buffer_id = opencl_handle->CreateKernelBuffer(
+      kid, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, blue_noise->data_.size(),
+      blue_noise->data_.data());
+  if (blue_noise_buffer_id == 0) {
+    std::cout << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set "
+                 "blue-noise buffer"
               << std::endl;
-    return;
+    opencl_handle->CleanupAllKernels();
+    return {};
   }
 
-  std::string str_input;
-  int int_input;
-  ifs >> str_input;
-  if (!ifs.good()) {
-    std::cout << "ERROR: Failed to parse file (PGM first identifier) \""
-              << filename << '"' << std::endl;
-    return;
+  // assign buffers/data to kernel parameters
+  if (!opencl_handle->AssignKernelBuffer(kid, 0, input_buffer_id)) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 0"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
+  if (!opencl_handle->AssignKernelBuffer(kid, 1, blue_noise_buffer_id)) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 1"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
+  if (!opencl_handle->AssignKernelBuffer(kid, 2, output_buffer_id)) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 2"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
+  unsigned int width = grayscale_image->GetWidth();
+  if (!opencl_handle->AssignKernelArgument(kid, 3, sizeof(unsigned int),
+                                           &width)) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 3"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
+  unsigned int height = grayscale_image->GetHeight();
+  if (!opencl_handle->AssignKernelArgument(kid, 4, sizeof(unsigned int),
+                                           &height)) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 4"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
+  unsigned int blue_noise_width = blue_noise->GetWidth();
+  if (!opencl_handle->AssignKernelArgument(kid, 5, sizeof(unsigned int),
+                                           &blue_noise_width)) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 5"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
+  unsigned int blue_noise_height = blue_noise->GetHeight();
+  if (!opencl_handle->AssignKernelArgument(kid, 6, sizeof(unsigned int),
+                                           &blue_noise_height)) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 6"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
+  std::srand(std::time(nullptr));
+  unsigned int blue_noise_offset =
+      std::rand() % (blue_noise_width * blue_noise_height);
+  if (!opencl_handle->AssignKernelArgument(kid, 7, sizeof(unsigned int),
+                                           &blue_noise_offset)) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 7"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
   }
 
-  if (str_input.compare("P2") == 0) {
-    // data stored in ascii format
-
-    // get width
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PGM width) \"" << filename
-                << '"' << std::endl;
-      return;
-    }
-    width_ = int_input;
-
-    // get height
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PGM height) \"" << filename
-                << '"' << std::endl;
-      return;
-    }
-    height_ = int_input;
-
-    // get max_value
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PGM max) \"" << filename << '"'
-                << std::endl;
-      return;
-    }
-    float max_value = int_input;
-
-    // parse data
-    data_.clear();
-    data_.reserve(width_ * height_);
-    float value;
-    for (unsigned int i = 0; i < width_ * height_; ++i) {
-      ifs >> int_input;
-      if (!ifs.good()) {
-        std::cout << "ERROR: Failed to parse file (PGM data) \"" << filename
-                  << '"' << std::endl;
-        return;
-      }
-      value = static_cast<float>(int_input) / max_value;
-      data_.push_back(std::round(value * 255.0F));
-    }
-  } else if (str_input.compare("P5") == 0) {
-    // data stored in raw format
-
-    // get width
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PGM width) \"" << filename
-                << '"' << std::endl;
-      return;
-    }
-    width_ = int_input;
-
-    // get height
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PGM height) \"" << filename
-                << '"' << std::endl;
-      return;
-    }
-    height_ = int_input;
+  // auto global_work_sizes = opencl_handle->GetGlobalWorkSize(kid);
+  auto work_group_size = opencl_handle->GetWorkGroupSize(kid);
+  std::cout << "Got work_group_size == " << work_group_size << std::endl;
 
-    // get max_value
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PGM max) \"" << filename << '"'
-                << std::endl;
-      return;
-    }
-    int max_value_int = int_input;
-    float max_value = int_input;
+  // auto max_work_group_size = opencl_handle->GetDeviceMaxWorkGroupSize();
+  // std::cout << "Got max_work_group_size == " << max_work_group_size
+  //          << std::endl;
 
-    // validate max_value
-    if (max_value_int != 255 && max_value_int != 65535) {
-      std::cout << "ERROR: Invalid max value for PGM (should be 255 or 65535) "
-                   "(filename \""
-                << filename << "\")" << std::endl;
-      return;
-    }
+  std::size_t work_group_size_0 = std::sqrt(work_group_size);
+  std::size_t work_group_size_1 = work_group_size_0;
 
-    // extract whitespace before data
-    {
-      int c = ifs.get();
-      if (c != '\n' && c != ' ') {
-        std::cout << "WARNING: File data after PGM max is not whitespace "
-                     "(filename \""
-                  << filename << "\")"
-                  << " value is " << c << std::endl;
-      }
+  while (work_group_size_0 > 1 && width % work_group_size_0 != 0) {
+    --work_group_size_0;
+  }
+  while (work_group_size_1 > 1 && height % work_group_size_1 != 0) {
+    --work_group_size_1;
+  }
 
-      if (!ifs.good()) {
-        std::cout << "ERROR: Failed to parse file (PGM after whitespace) \""
-                  << filename << '"' << std::endl;
-        return;
-      }
-    }
+  std::cout << "Using WIDTHxHEIGHT: " << width << "x" << height
+            << " with work_group_sizes: " << work_group_size_0 << "x"
+            << work_group_size_1 << std::endl;
 
-    // parse raw data
-    data_.clear();
-    data_.reserve(width_ * height_);
-    float value;
-    for (unsigned int i = 0; i < width_ * height_; ++i) {
-      if (max_value_int == 255) {
-        value = ifs.get() / max_value;
-        data_.push_back(std::round(value * 255.0F));
-        if (!ifs.good()) {
-          std::cout << "ERROR: Failed to parse file (PGM data) \"" << filename
-                    << '"' << std::endl;
-          return;
-        }
-      } else /* if (max_value_int == 65535) */ {
-        value = (ifs.get() & 0xFF) | ((ifs.get() << 8) & 0xFF00);
-        value /= max_value;
-        data_.push_back(std::round(value * 255.0F));
-        if (!ifs.good()) {
-          std::cout << "ERROR: Failed to parse file (PGM data 16-bit) \""
-                    << filename << '"' << std::endl;
-          return;
-        }
-      }
-    }
+  if (!opencl_handle->ExecuteKernel2D(kid, width, height, work_group_size_0,
+                                      work_group_size_1, true)) {
+    std::cout
+        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to execute Kernel"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
 
-    if (ifs.get() != decltype(ifs)::traits_type::eof()) {
-      std::cout << "WARNING: Trailing data in PGM file \"" << filename << '"'
-                << std::endl;
-    }
-  } else {
-    std::cout << "ERROR: Invalid \"magic number\" in header of file \""
-              << filename << '"' << std::endl;
+  if (!opencl_handle->GetBufferData(kid, output_buffer_id,
+                                    grayscale_image->GetSize(),
+                                    grayscale_image->data_.data())) {
+    std::cout << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to get output "
+                 "buffer data"
+              << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
   }
+
+  opencl_handle->CleanupAllKernels();
+  return grayscale_image;
 }
 
-void Image::DecodePPM(const std::string &filename) {
-  is_grayscale_ = false;
-  std::ifstream ifs(filename);
-  if (!ifs.is_open()) {
-    std::cout << "ERROR: Failed to open file \"" << filename << '"'
-              << std::endl;
-    return;
+std::unique_ptr<Image> Image::ToColorDitheredWithBlueNoise(Image *blue_noise) {
+  if (!blue_noise->IsGrayscale()) {
+    std::cout
+        << "ERROR ToColorDitheredWithBlueNoise: blue_noise is not grayscale"
+        << std::endl;
+    return {};
   }
 
-  std::string str_input;
-  int int_input;
-  ifs >> str_input;
-  if (!ifs.good()) {
-    std::cout << "ERROR: Failed to parse file (PPM first identifier) \""
-              << filename << '"' << std::endl;
-    return;
+  if (this->IsGrayscale()) {
+    std::cout << "ERROR ToColorDitheredWithBlueNoise: current Image is not "
+                 "non-grayscale"
+              << std::endl;
+    return {};
   }
 
-  if (str_input.compare("P3") == 0) {
-    // data stored in ascii format
+  auto opencl_handle = GetOpenCLHandle();
+  if (!opencl_handle) {
+    std::cout
+        << "ERROR ToColorDitheredWithBlueNoise: Failed to get OpenCLHandle"
+        << std::endl;
+    return {};
+  }
 
-    // get width
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PPM width) \"" << filename
-                << '"' << std::endl;
-      return;
-    }
-    width_ = int_input;
+  // set up kernel and buffers
+  auto kid = opencl_handle->CreateKernelFromSource(GetColorDitheringKernel(),
+                                                   "ColorDither");
+  if (kid == 0) {
+    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to create "
+                 "OpenCL Kernel"
+              << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
 
-    // get height
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PPM height) \"" << filename
-                << '"' << std::endl;
-      return;
-    }
-    height_ = int_input;
-
-    // get max_value
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PPM max) \"" << filename << '"'
-                << std::endl;
-      return;
-    }
-    float max_value = int_input;
-
-    // parse data
-    data_.clear();
-    data_.reserve(width_ * height_ * 4);
-    float value;
-    for (unsigned int i = 0; i < width_ * height_ * 3; ++i) {
-      ifs >> int_input;
-      if (!ifs.good()) {
-        std::cout << "ERROR: Failed to parse file (PPM data) \"" << filename
-                  << '"' << std::endl;
-        return;
-      }
-      value = static_cast<float>(int_input) / max_value;
-      data_.push_back(std::round(value * 255.0F));
-      if (i % 3 == 2) {
-        // PPM is RGB but Image stores as RGBA
-        data_.push_back(255);
-      }
-    }
-  } else if (str_input.compare("P6") == 0) {
-    // data stored in raw format
-
-    // get width
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PPM width) \"" << filename
-                << '"' << std::endl;
-      return;
-    }
-    width_ = int_input;
-
-    // get height
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PPM height) \"" << filename
-                << '"' << std::endl;
-      return;
-    }
-    height_ = int_input;
-
-    // get max_value
-    ifs >> int_input;
-    if (!ifs.good() || int_input <= 0) {
-      std::cout << "ERROR: Failed to parse file (PPM max) \"" << filename << '"'
-                << std::endl;
-      return;
-    }
-    int max_value_int = int_input;
-    float max_value = int_input;
-
-    // validate max_value
-    if (max_value_int != 255 && max_value_int != 65535) {
-      std::cout << "ERROR: Invalid max value for PPM (should be 255 or 65535) "
-                   "(filename \""
-                << filename << "\")" << std::endl;
-      return;
-    }
-
-    // extract whitespace before data
-    {
-      int c = ifs.get();
-      if (c != '\n' && c != ' ') {
-        std::cout
-            << "WARNING: File data after PPM max is not whitespace (filename \""
-            << filename << "\") value is " << c << std::endl;
-      }
-
-      if (!ifs.good()) {
-        std::cout << "ERROR: Failed to parse file (PPM after whitespace) \""
-                  << filename << '"' << std::endl;
-        return;
-      }
-    }
-
-    // parse raw data
-    data_.clear();
-    data_.reserve(width_ * height_ * 4);
-    float value;
-    for (unsigned int i = 0; i < width_ * height_ * 3; ++i) {
-      if (max_value_int == 255) {
-        value = ifs.get() / max_value;
-        data_.push_back(std::round(value * 255.0F));
-        if (!ifs.good()) {
-          std::cout << "ERROR: Failed to parse file (PPM data) \"" << filename
-                    << '"' << std::endl;
-          return;
-        }
-      } else /* if (max_value_int == 65535) */ {
-        value = (ifs.get() & 0xFF) | ((ifs.get() << 8) & 0xFF00);
-        value /= max_value;
-        data_.push_back(std::round(value * 255.0F));
-        if (!ifs.good()) {
-          std::cout << "ERROR: Failed to parse file (PPM data 16-bit) \""
-                    << filename << '"' << std::endl;
-          return;
-        }
-      }
-
-      if (i % 3 == 2) {
-        // PPM is RGB but Image stores as RGBA
-        data_.push_back(255);
-      }
-    }
-
-    if (ifs.get() != decltype(ifs)::traits_type::eof()) {
-      std::cout << "WARNING: Trailing data in PPM file \"" << filename << '"'
-                << std::endl;
-    }
-  } else {
-    std::cout << "ERROR: Invalid \"magic number\" in header of file \""
-              << filename << '"' << std::endl;
-  }
-}
-
-uint8_t Image::ColorToGray(uint8_t red, uint8_t green, uint8_t blue) {
-  // values taken from Wikipedia article about conversion of color to grayscale
-  double y_linear = 0.2126 * (red / 255.0) + 0.7152 * (green / 255.0) +
-                    0.0722 * (blue / 255.0);
-
-  if (y_linear <= 0.0031308) {
-    return std::round((12.92 * y_linear) * 255.0);
-  } else {
-    return std::round((1.055 * std::pow(y_linear, 1 / 2.4) - 0.055) * 255.0);
-  }
-}
-
-std::unique_ptr<Image> Image::ToGrayscale() const {
-  if (IsGrayscale()) {
-    return std::unique_ptr<Image>(new Image(*this));
-  }
-
-  std::unique_ptr<Image> grayscale_image = std::unique_ptr<Image>(new Image{});
-  grayscale_image->width_ = this->width_;
-  grayscale_image->height_ = this->height_;
-  grayscale_image->data_.resize(width_ * height_);
-
-  for (unsigned int i = 0; i < width_ * height_; ++i) {
-    grayscale_image->data_.at(i) =
-        ColorToGray(this->data_.at(i * 4), this->data_.at(i * 4 + 1),
-                    this->data_.at(i * 4 + 2));
-  }
-
-  return grayscale_image;
-}
-
-std::unique_ptr<Image> Image::ToGrayscaleDitheredWithBlueNoise(
-    Image *blue_noise) {
-  if (!blue_noise->IsGrayscale()) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: blue_noise is not grayscale"
-        << std::endl;
-    return {};
-  }
-
-  auto grayscale_image = ToGrayscale();
-  if (!grayscale_image) {
-    std::cout << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to get "
-                 "grayscale Image"
-              << std::endl;
-    return {};
-  }
-  auto opencl_handle = GetOpenCLHandle();
-  if (!opencl_handle) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to get OpenCLHandle"
-        << std::endl;
-    return {};
-  }
-
-  // set up kernel and buffers
-  auto kid = opencl_handle->CreateKernelFromSource(
-      GetGrayscaleDitheringKernel(), "Dither");
-  if (kid == 0) {
-    std::cout << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to create "
-                 "OpenCL Kernel"
-              << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-
-  auto input_buffer_id = opencl_handle->CreateKernelBuffer(
-      kid, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
-      grayscale_image->data_.size(), grayscale_image->data_.data());
-  if (input_buffer_id == 0) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set input buffer"
-        << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
+  auto input_buffer_id = opencl_handle->CreateKernelBuffer(
+      kid, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, this->data_.size(),
+      this->data_.data());
+  if (input_buffer_id == 0) {
+    std::cout
+        << "ERROR ToColorDitheredWithBlueNoise: Failed to set input buffer"
+        << std::endl;
+    opencl_handle->CleanupAllKernels();
+    return {};
+  }
 
   auto output_buffer_id = opencl_handle->CreateKernelBuffer(
-      kid, CL_MEM_WRITE_ONLY, grayscale_image->data_.size(), nullptr);
+      kid, CL_MEM_WRITE_ONLY, this->data_.size(), nullptr);
   if (output_buffer_id == 0) {
     std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set output buffer"
+        << "ERROR ToColorDitheredWithBlueNoise: Failed to set output buffer"
         << std::endl;
     opencl_handle->CleanupAllKernels();
     return {};
@@ -753,251 +483,70 @@ std::unique_ptr<Image> Image::ToGrayscaleDitheredWithBlueNoise(
       kid, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, blue_noise->data_.size(),
       blue_noise->data_.data());
   if (blue_noise_buffer_id == 0) {
-    std::cout << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set "
+    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set "
                  "blue-noise buffer"
               << std::endl;
     opencl_handle->CleanupAllKernels();
     return {};
   }
 
+  std::srand(std::time(nullptr));
+  std::array<unsigned int, 3> blue_noise_offsets = {
+      std::rand() % blue_noise->GetSize(), std::rand() % blue_noise->GetSize(),
+      std::rand() % blue_noise->GetSize()};
+
+  while (blue_noise_offsets[0] == blue_noise_offsets[1] ||
+         blue_noise_offsets[1] == blue_noise_offsets[2] ||
+         blue_noise_offsets[0] == blue_noise_offsets[2]) {
+    blue_noise_offsets = {std::rand() % blue_noise->GetSize(),
+                          std::rand() % blue_noise->GetSize(),
+                          std::rand() % blue_noise->GetSize()};
+  }
+
+  auto blue_noise_offsets_buffer_id = opencl_handle->CreateKernelBuffer(
+      kid, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(unsigned int) * 3,
+      blue_noise_offsets.data());
+
   // assign buffers/data to kernel parameters
   if (!opencl_handle->AssignKernelBuffer(kid, 0, input_buffer_id)) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 0"
-        << std::endl;
+    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 0"
+              << std::endl;
     opencl_handle->CleanupAllKernels();
     return {};
   }
   if (!opencl_handle->AssignKernelBuffer(kid, 1, blue_noise_buffer_id)) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 1"
-        << std::endl;
+    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 1"
+              << std::endl;
     opencl_handle->CleanupAllKernels();
     return {};
   }
   if (!opencl_handle->AssignKernelBuffer(kid, 2, output_buffer_id)) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 2"
-        << std::endl;
+    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 2"
+              << std::endl;
     opencl_handle->CleanupAllKernels();
     return {};
   }
-  unsigned int width = grayscale_image->GetWidth();
+  unsigned int input_width = this->GetWidth();
   if (!opencl_handle->AssignKernelArgument(kid, 3, sizeof(unsigned int),
-                                           &width)) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 3"
-        << std::endl;
+                                           &input_width)) {
+    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 3"
+              << std::endl;
     opencl_handle->CleanupAllKernels();
     return {};
   }
-  unsigned int height = grayscale_image->GetHeight();
+  unsigned int input_height = this->GetHeight();
   if (!opencl_handle->AssignKernelArgument(kid, 4, sizeof(unsigned int),
-                                           &height)) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 4"
-        << std::endl;
+                                           &input_height)) {
+    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 4"
+              << std::endl;
     opencl_handle->CleanupAllKernels();
     return {};
   }
   unsigned int blue_noise_width = blue_noise->GetWidth();
   if (!opencl_handle->AssignKernelArgument(kid, 5, sizeof(unsigned int),
                                            &blue_noise_width)) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 5"
-        << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-  unsigned int blue_noise_height = blue_noise->GetHeight();
-  if (!opencl_handle->AssignKernelArgument(kid, 6, sizeof(unsigned int),
-                                           &blue_noise_height)) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 6"
-        << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-  std::srand(std::time(nullptr));
-  unsigned int blue_noise_offset =
-      std::rand() % (blue_noise_width * blue_noise_height);
-  if (!opencl_handle->AssignKernelArgument(kid, 7, sizeof(unsigned int),
-                                           &blue_noise_offset)) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to set parameter 7"
-        << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-
-  // auto global_work_sizes = opencl_handle->GetGlobalWorkSize(kid);
-  auto work_group_size = opencl_handle->GetWorkGroupSize(kid);
-  std::cout << "Got work_group_size == " << work_group_size << std::endl;
-
-  // auto max_work_group_size = opencl_handle->GetDeviceMaxWorkGroupSize();
-  // std::cout << "Got max_work_group_size == " << max_work_group_size
-  //          << std::endl;
-
-  std::size_t work_group_size_0 = std::sqrt(work_group_size);
-  std::size_t work_group_size_1 = work_group_size_0;
-
-  while (work_group_size_0 > 1 && width % work_group_size_0 != 0) {
-    --work_group_size_0;
-  }
-  while (work_group_size_1 > 1 && height % work_group_size_1 != 0) {
-    --work_group_size_1;
-  }
-
-  std::cout << "Using WIDTHxHEIGHT: " << width << "x" << height
-            << " with work_group_sizes: " << work_group_size_0 << "x"
-            << work_group_size_1 << std::endl;
-
-  if (!opencl_handle->ExecuteKernel2D(kid, width, height, work_group_size_0,
-                                      work_group_size_1, true)) {
-    std::cout
-        << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to execute Kernel"
-        << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-
-  if (!opencl_handle->GetBufferData(kid, output_buffer_id,
-                                    grayscale_image->GetSize(),
-                                    grayscale_image->data_.data())) {
-    std::cout << "ERROR ToGrayscaleDitheredWithBlueNoise: Failed to get output "
-                 "buffer data"
-              << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-
-  opencl_handle->CleanupAllKernels();
-  return grayscale_image;
-}
-
-std::unique_ptr<Image> Image::ToColorDitheredWithBlueNoise(Image *blue_noise) {
-  if (!blue_noise->IsGrayscale()) {
-    std::cout
-        << "ERROR ToColorDitheredWithBlueNoise: blue_noise is not grayscale"
-        << std::endl;
-    return {};
-  }
-
-  if (this->IsGrayscale()) {
-    std::cout << "ERROR ToColorDitheredWithBlueNoise: current Image is not "
-                 "non-grayscale"
-              << std::endl;
-    return {};
-  }
-
-  auto opencl_handle = GetOpenCLHandle();
-  if (!opencl_handle) {
-    std::cout
-        << "ERROR ToColorDitheredWithBlueNoise: Failed to get OpenCLHandle"
-        << std::endl;
-    return {};
-  }
-
-  // set up kernel and buffers
-  auto kid = opencl_handle->CreateKernelFromSource(GetColorDitheringKernel(),
-                                                   "ColorDither");
-  if (kid == 0) {
-    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to create "
-                 "OpenCL Kernel"
-              << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-
-  auto input_buffer_id = opencl_handle->CreateKernelBuffer(
-      kid, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, this->data_.size(),
-      this->data_.data());
-  if (input_buffer_id == 0) {
-    std::cout
-        << "ERROR ToColorDitheredWithBlueNoise: Failed to set input buffer"
-        << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-
-  auto output_buffer_id = opencl_handle->CreateKernelBuffer(
-      kid, CL_MEM_WRITE_ONLY, this->data_.size(), nullptr);
-  if (output_buffer_id == 0) {
-    std::cout
-        << "ERROR ToColorDitheredWithBlueNoise: Failed to set output buffer"
-        << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-
-  auto blue_noise_buffer_id = opencl_handle->CreateKernelBuffer(
-      kid, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, blue_noise->data_.size(),
-      blue_noise->data_.data());
-  if (blue_noise_buffer_id == 0) {
-    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set "
-                 "blue-noise buffer"
-              << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-
-  std::srand(std::time(nullptr));
-  std::array<unsigned int, 3> blue_noise_offsets = {
-      std::rand() % blue_noise->GetSize(), std::rand() % blue_noise->GetSize(),
-      std::rand() % blue_noise->GetSize()};
-
-  while (blue_noise_offsets[0] == blue_noise_offsets[1] ||
-         blue_noise_offsets[1] == blue_noise_offsets[2] ||
-         blue_noise_offsets[0] == blue_noise_offsets[2]) {
-    blue_noise_offsets = {std::rand() % blue_noise->GetSize(),
-                          std::rand() % blue_noise->GetSize(),
-                          std::rand() % blue_noise->GetSize()};
-  }
-
-  auto blue_noise_offsets_buffer_id = opencl_handle->CreateKernelBuffer(
-      kid, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(unsigned int) * 3,
-      blue_noise_offsets.data());
-
-  // assign buffers/data to kernel parameters
-  if (!opencl_handle->AssignKernelBuffer(kid, 0, input_buffer_id)) {
-    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 0"
-              << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-  if (!opencl_handle->AssignKernelBuffer(kid, 1, blue_noise_buffer_id)) {
-    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 1"
-              << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-  if (!opencl_handle->AssignKernelBuffer(kid, 2, output_buffer_id)) {
-    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 2"
-              << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-  unsigned int input_width = this->GetWidth();
-  if (!opencl_handle->AssignKernelArgument(kid, 3, sizeof(unsigned int),
-                                           &input_width)) {
-    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 3"
-              << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-  unsigned int input_height = this->GetHeight();
-  if (!opencl_handle->AssignKernelArgument(kid, 4, sizeof(unsigned int),
-                                           &input_height)) {
-    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 4"
-              << std::endl;
-    opencl_handle->CleanupAllKernels();
-    return {};
-  }
-  unsigned int blue_noise_width = blue_noise->GetWidth();
-  if (!opencl_handle->AssignKernelArgument(kid, 5, sizeof(unsigned int),
-                                           &blue_noise_width)) {
-    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 5"
-              << std::endl;
+    std::cout << "ERROR ToColorDitheredWithBlueNoise: Failed to set parameter 5"
+              << std::endl;
     opencl_handle->CleanupAllKernels();
     return {};
   }
@@ -1152,3 +701,454 @@ OpenCLHandle::Ptr Image::GetOpenCLHandle() {
 
   return opencl_handle_;
 }
+
+void Image::DecodePNG(const std::string &filename) {
+  FILE *file = std::fopen(filename.c_str(), "rb");
+
+  // Check header of file to check if it is actually a png file.
+  {
+    std::array<unsigned char, 8> buf;
+    if (std::fread(buf.data(), 1, 8, file) != 8) {
+      std::fclose(file);
+      std::cout << "ERROR: File \"" << filename << "\" is smaller than 8 bytes"
+                << std::endl;
+      return;
+    } else if (png_sig_cmp(reinterpret_cast<png_const_bytep>(buf.data()), 0,
+                           8) != 0) {
+      // not png file, do nothing
+      std::fclose(file);
+      std::cout << "ERROR: File \"" << filename << "\" is not a png file"
+                << std::endl;
+      return;
+    }
+  }
+
+  // seek to head of file
+  std::rewind(file);
+
+  // init required structs for png decoding
+  png_structp png_ptr =
+      png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+  if (!png_ptr) {
+    std::cout << "ERROR: Failed to initialize libpng (png_ptr) for decoding "
+                 "PNG file \""
+              << filename << '"' << std::endl;
+    std::fclose(file);
+    return;
+  }
+
+  png_infop png_info_ptr = png_create_info_struct(png_ptr);
+  if (!png_info_ptr) {
+    std::cout << "ERROR: Failed to initialize libpng (png_infop) for decoding "
+                 "PNG file \""
+              << filename << '"' << std::endl;
+    png_destroy_read_struct(&png_ptr, nullptr, nullptr);
+    std::fclose(file);
+    return;
+  }
+
+  png_infop png_end_info_ptr = png_create_info_struct(png_ptr);
+  if (!png_end_info_ptr) {
+    std::cout << "ERROR: Failed to initialize libpng (end png_infop) for "
+                 "decoding PNG file \""
+              << filename << '"' << std::endl;
+    png_destroy_read_struct(&png_ptr, &png_info_ptr, nullptr);
+    std::fclose(file);
+    return;
+  }
+
+  // required to handle libpng errors
+  if (setjmp(png_jmpbuf(png_ptr))) {
+    png_destroy_read_struct(&png_ptr, &png_info_ptr, &png_end_info_ptr);
+    std::fclose(file);
+    return;
+  }
+
+  // pass the FILE pointer to libpng
+  png_init_io(png_ptr, file);
+
+  // have libpng process the png data
+  png_read_png(png_ptr, png_info_ptr, PNG_TRANSFORM_IDENTITY, nullptr);
+
+  // get image width (in pixels)
+  width_ = png_get_image_width(png_ptr, png_info_ptr);
+
+  // get image height (in pixels)
+  height_ = png_get_image_height(png_ptr, png_info_ptr);
+
+  // get channel count of image
+  unsigned int channels = png_get_channels(png_ptr, png_info_ptr);
+  if (channels == 1) {
+    is_grayscale_ = true;
+  } else {
+    is_grayscale_ = false;
+  }
+
+  if (height_ > PNG_UINT_32_MAX) {
+    png_error(png_ptr, "Image is too tall to process in memory");
+  } else if (width_ > PNG_UINT_32_MAX) {
+    png_error(png_ptr, "Image is too wide to process in memory");
+  }
+
+  png_byte **row_pointers = png_get_rows(png_ptr, png_info_ptr);
+
+  data_.clear();
+  if (channels == 3 || channels == 4) {
+    data_.reserve(width_ * 4 * height_);
+  } else if (channels == 1) {
+    data_.reserve(width_ * height_);
+  } else {
+    std::cout << "ERROR: PNG has invalid channel count == " << channels
+              << std::endl;
+    png_destroy_read_struct(&png_ptr, &png_info_ptr, &png_end_info_ptr);
+    return;
+  }
+  for (unsigned int y = 0; y < height_; ++y) {
+    for (unsigned int x = 0; x < width_; ++x) {
+      if (is_grayscale_) {
+        data_.push_back(row_pointers[y][x]);
+      } else if (channels == 3) {
+        for (unsigned int c = 0; c < channels; ++c) {
+          data_.push_back(row_pointers[y][x * channels + c]);
+        }
+        data_.push_back(255);
+      } else /* if (channels == 4) */ {
+        for (unsigned int c = 0; c < channels; ++c) {
+          data_.push_back(row_pointers[y][x * channels + c]);
+        }
+      }
+    }
+  }
+
+  // cleanup
+  png_destroy_read_struct(&png_ptr, &png_info_ptr, &png_end_info_ptr);
+  fclose(file);
+
+  // verify
+  if (is_grayscale_) {
+    if (data_.size() != width_ * height_) {
+      std::cout << "WARNING: data_.size() doesn't match width_ * height_"
+                << std::endl;
+    }
+  } else {
+    if (data_.size() != 4 * width_ * height_) {
+      std::cout << "WARNING: data_.size() doesn't match 4 * width_ * height_"
+                << std::endl;
+    }
+  }
+}
+
+void Image::DecodePGM(const std::string &filename) {
+  is_grayscale_ = true;
+
+  std::ifstream ifs(filename);
+  if (!ifs.is_open()) {
+    std::cout << "ERROR: Failed to open file \"" << filename << '"'
+              << std::endl;
+    return;
+  }
+
+  std::string str_input;
+  int int_input;
+  ifs >> str_input;
+  if (!ifs.good()) {
+    std::cout << "ERROR: Failed to parse file (PGM first identifier) \""
+              << filename << '"' << std::endl;
+    return;
+  }
+
+  if (str_input.compare("P2") == 0) {
+    // data stored in ascii format
+
+    // get width
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PGM width) \"" << filename
+                << '"' << std::endl;
+      return;
+    }
+    width_ = int_input;
+
+    // get height
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PGM height) \"" << filename
+                << '"' << std::endl;
+      return;
+    }
+    height_ = int_input;
+
+    // get max_value
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PGM max) \"" << filename << '"'
+                << std::endl;
+      return;
+    }
+    float max_value = int_input;
+
+    // parse data
+    data_.clear();
+    data_.reserve(width_ * height_);
+    float value;
+    for (unsigned int i = 0; i < width_ * height_; ++i) {
+      ifs >> int_input;
+      if (!ifs.good()) {
+        std::cout << "ERROR: Failed to parse file (PGM data) \"" << filename
+                  << '"' << std::endl;
+        return;
+      }
+      value = static_cast<float>(int_input) / max_value;
+      data_.push_back(std::round(value * 255.0F));
+    }
+  } else if (str_input.compare("P5") == 0) {
+    // data stored in raw format
+
+    // get width
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PGM width) \"" << filename
+                << '"' << std::endl;
+      return;
+    }
+    width_ = int_input;
+
+    // get height
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PGM height) \"" << filename
+                << '"' << std::endl;
+      return;
+    }
+    height_ = int_input;
+
+    // get max_value
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PGM max) \"" << filename << '"'
+                << std::endl;
+      return;
+    }
+    int max_value_int = int_input;
+    float max_value = int_input;
+
+    // validate max_value
+    if (max_value_int != 255 && max_value_int != 65535) {
+      std::cout << "ERROR: Invalid max value for PGM (should be 255 or 65535) "
+                   "(filename \""
+                << filename << "\")" << std::endl;
+      return;
+    }
+
+    // extract whitespace before data
+    {
+      int c = ifs.get();
+      if (c != '\n' && c != ' ') {
+        std::cout << "WARNING: File data after PGM max is not whitespace "
+                     "(filename \""
+                  << filename << "\")"
+                  << " value is " << c << std::endl;
+      }
+
+      if (!ifs.good()) {
+        std::cout << "ERROR: Failed to parse file (PGM after whitespace) \""
+                  << filename << '"' << std::endl;
+        return;
+      }
+    }
+
+    // parse raw data
+    data_.clear();
+    data_.reserve(width_ * height_);
+    float value;
+    for (unsigned int i = 0; i < width_ * height_; ++i) {
+      if (max_value_int == 255) {
+        value = ifs.get() / max_value;
+        data_.push_back(std::round(value * 255.0F));
+        if (!ifs.good()) {
+          std::cout << "ERROR: Failed to parse file (PGM data) \"" << filename
+                    << '"' << std::endl;
+          return;
+        }
+      } else /* if (max_value_int == 65535) */ {
+        value = (ifs.get() & 0xFF) | ((ifs.get() << 8) & 0xFF00);
+        value /= max_value;
+        data_.push_back(std::round(value * 255.0F));
+        if (!ifs.good()) {
+          std::cout << "ERROR: Failed to parse file (PGM data 16-bit) \""
+                    << filename << '"' << std::endl;
+          return;
+        }
+      }
+    }
+
+    if (ifs.get() != decltype(ifs)::traits_type::eof()) {
+      std::cout << "WARNING: Trailing data in PGM file \"" << filename << '"'
+                << std::endl;
+    }
+  } else {
+    std::cout << "ERROR: Invalid \"magic number\" in header of file \""
+              << filename << '"' << std::endl;
+  }
+}
+
+void Image::DecodePPM(const std::string &filename) {
+  is_grayscale_ = false;
+  std::ifstream ifs(filename);
+  if (!ifs.is_open()) {
+    std::cout << "ERROR: Failed to open file \"" << filename << '"'
+              << std::endl;
+    return;
+  }
+
+  std::string str_input;
+  int int_input;
+  ifs >> str_input;
+  if (!ifs.good()) {
+    std::cout << "ERROR: Failed to parse file (PPM first identifier) \""
+              << filename << '"' << std::endl;
+    return;
+  }
+
+  if (str_input.compare("P3") == 0) {
+    // data stored in ascii format
+
+    // get width
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PPM width) \"" << filename
+                << '"' << std::endl;
+      return;
+    }
+    width_ = int_input;
+
+    // get height
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PPM height) \"" << filename
+                << '"' << std::endl;
+      return;
+    }
+    height_ = int_input;
+
+    // get max_value
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PPM max) \"" << filename << '"'
+                << std::endl;
+      return;
+    }
+    float max_value = int_input;
+
+    // parse data
+    data_.clear();
+    data_.reserve(width_ * height_ * 4);
+    float value;
+    for (unsigned int i = 0; i < width_ * height_ * 3; ++i) {
+      ifs >> int_input;
+      if (!ifs.good()) {
+        std::cout << "ERROR: Failed to parse file (PPM data) \"" << filename
+                  << '"' << std::endl;
+        return;
+      }
+      value = static_cast<float>(int_input) / max_value;
+      data_.push_back(std::round(value * 255.0F));
+      if (i % 3 == 2) {
+        // PPM is RGB but Image stores as RGBA
+        data_.push_back(255);
+      }
+    }
+  } else if (str_input.compare("P6") == 0) {
+    // data stored in raw format
+
+    // get width
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PPM width) \"" << filename
+                << '"' << std::endl;
+      return;
+    }
+    width_ = int_input;
+
+    // get height
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PPM height) \"" << filename
+                << '"' << std::endl;
+      return;
+    }
+    height_ = int_input;
+
+    // get max_value
+    ifs >> int_input;
+    if (!ifs.good() || int_input <= 0) {
+      std::cout << "ERROR: Failed to parse file (PPM max) \"" << filename << '"'
+                << std::endl;
+      return;
+    }
+    int max_value_int = int_input;
+    float max_value = int_input;
+
+    // validate max_value
+    if (max_value_int != 255 && max_value_int != 65535) {
+      std::cout << "ERROR: Invalid max value for PPM (should be 255 or 65535) "
+                   "(filename \""
+                << filename << "\")" << std::endl;
+      return;
+    }
+
+    // extract whitespace before data
+    {
+      int c = ifs.get();
+      if (c != '\n' && c != ' ') {
+        std::cout
+            << "WARNING: File data after PPM max is not whitespace (filename \""
+            << filename << "\") value is " << c << std::endl;
+      }
+
+      if (!ifs.good()) {
+        std::cout << "ERROR: Failed to parse file (PPM after whitespace) \""
+                  << filename << '"' << std::endl;
+        return;
+      }
+    }
+
+    // parse raw data
+    data_.clear();
+    data_.reserve(width_ * height_ * 4);
+    float value;
+    for (unsigned int i = 0; i < width_ * height_ * 3; ++i) {
+      if (max_value_int == 255) {
+        value = ifs.get() / max_value;
+        data_.push_back(std::round(value * 255.0F));
+        if (!ifs.good()) {
+          std::cout << "ERROR: Failed to parse file (PPM data) \"" << filename
+                    << '"' << std::endl;
+          return;
+        }
+      } else /* if (max_value_int == 65535) */ {
+        value = (ifs.get() & 0xFF) | ((ifs.get() << 8) & 0xFF00);
+        value /= max_value;
+        data_.push_back(std::round(value * 255.0F));
+        if (!ifs.good()) {
+          std::cout << "ERROR: Failed to parse file (PPM data 16-bit) \""
+                    << filename << '"' << std::endl;
+          return;
+        }
+      }
+
+      if (i % 3 == 2) {
+        // PPM is RGB but Image stores as RGBA
+        data_.push_back(255);
+      }
+    }
+
+    if (ifs.get() != decltype(ifs)::traits_type::eof()) {
+      std::cout << "WARNING: Trailing data in PPM file \"" << filename << '"'
+                << std::endl;
+    }
+  } else {
+    std::cout << "ERROR: Invalid \"magic number\" in header of file \""
+              << filename << '"' << std::endl;
+  }
+}