// -*- tab-width: 4; -*- // vi: set sw=2 ts=4 expandtab: // Copyright 2022 The Khronos Group Inc. // SPDX-License-Identifier: Apache-2.0 #include "imageio.h" #include #include #include #include /** @internal * @~English * @file * * @brief ImageInput from netpbm format files (.pam, .pbm, .pgm or .ppm). * * Plain formats (magic numbers 'P1', 'P2' & 'P3') are not supported. * * PPM and PGM specify that sample values are encoded with the BT.709 OETF. * They do not indicate that bt.709 only applies when maxval <= 255 so this * class always reports OETF as bt.709 for color and grayscale. The * specifications also say that both sRGB and linear encoding are often used. * Since there is no metadata to indicate a differing OETF this loader always * assumes bt.709. * * Documentation on the netpbm formats can be found at: * http://netpbm.sourceforge.net/doc/ * * @author Mark Callow */ class NpbmInput final : public ImageInput { public: NpbmInput() : ImageInput("npbm") { } virtual void open(ImageSpec& newspec) override; virtual void close() override; //virtual bool read_native_scanline(uint32_t subimage, uint32_t miplevel, int y, int z, // void* data) override; virtual void readImage(void* buffer, size_t bufferByteCount, uint32_t subimage, uint32_t miplevel, const FormatDescriptor& targetFormat) override; /// Read a single scanline (all channels) of native data into contiguous /// memory. virtual void readNativeScanline(void* buffer, size_t bufferByteCount, uint32_t y, uint32_t z, uint32_t subimage, uint32_t miplevel) override; virtual uint32_t subimageCount(void) const override { return static_cast(images.size()); } virtual bool seekSubimage(uint32_t subimage, uint32_t miplevel = 0) override; virtual const ImageSpec& spec(void) const override { return images[curSubimage].spec; } using ImageInput::spec; private: std::string currentLine; std::istream::iostate exceptionsIn; size_t pos; unsigned int curImageScanline; using ImageInput::rescale; void readImageHeaders(); void parseAHeader(); enum class filetype {PGM, PPM}; void parseGPHeader(filetype ftype); void nextLine(); void nextToken(); void skipComments(char comment = '#'); void swap(void* pBuffer, size_t nvals); }; ImageInput* npbmInputCreate() { return new NpbmInput; } const char* npbmInputExtensions[] = { "pam", "pbm", "pgm", "ppm", nullptr }; void NpbmInput::open(ImageSpec& newspec) { assert(isp != nullptr && "istream not initialized"); currentLine = ""; pos = 0; isp->exceptions(std::istream::failbit | std::istream::badbit | std::istream::eofbit ); readImageHeaders(); seekSubimage(0); newspec = spec(); } void NpbmInput::close() { isp->exceptions(exceptionsIn); ImageInput::close(); } static int tupleComponentCount(const char* tupleType) { if (strcmp(tupleType, "BLACKANDWHITE") == 0) return 1; else if (strcmp(tupleType, "GRAYSCALE") == 0) return 1; else if (strcmp(tupleType, "GRAYSCALE_ALPHA") == 0) return 2; else if (strcmp(tupleType, "RGB") == 0) return 3; else if (strcmp(tupleType, "RGB_ALPHA") == 0) return 4; else return -1; } inline void NpbmInput::nextLine() { std::getline(*isp, currentLine); pos = 0; } inline void NpbmInput::nextToken() { while (1) { while (isspace(currentLine[pos])) pos++; if (pos != currentLine.size()) break; else nextLine(); } } inline void NpbmInput::skipComments(char comment) { while (1) { nextToken(); if (currentLine[pos] == comment) nextLine(); else break; } } void NpbmInput::readImageHeaders() { for(;;) { try { // MagicNumber // If not an NPBM file, there may not be a line terminator. currentLine.resize(3); isp->read(¤tLine[0], 3); if (!currentLine.compare("P7\n")) { parseAHeader(); } else if (!currentLine.compare("P5\n")) { parseGPHeader(filetype::PGM); //images.back().spec.colortype = colortype_e::Luminance; } else if (!currentLine.compare("P6\n")) { parseGPHeader(filetype::PPM); } else if (!currentLine.compare("P1\n") || !currentLine.compare("P2\n") || !currentLine.compare("P3\n")) { throw std::runtime_error("Plain netpbm formats are not supported."); } else if (!currentLine.compare("P4\n")) { throw std::runtime_error(".pbm files are not supported."); } else { throw different_format(); } images.back().filepos = isp->tellg(); // Save image data start pos. // We've only read the header. Seek to the expected end // of the image. isp->seekg(images.back().spec.imageByteCount(), isp->cur); } catch (const std::istream::failure&) { throwOnReadFailure(); } // Check if there is any more data in the file. try { isp->peek(); } catch (const std::istream::failure&) { if (isp->eof()) { return; } else { throwOnReadFailure(); } } } } /* * SwapEndian16: Swaps endianness in an array of 16-bit values */ static void swapEndian16(uint16_t* pData16, size_t nvals) { for (size_t i = 0; i < nvals; ++i) { uint16_t x = *pData16; *pData16++ = (x << 8) | (x >> 8); } } static const union foo { uint16_t x; uint8_t c; } bar{1}; #define IS_LITTLE_ENDIAN (bar.c) bool littleendian(void) noexcept { return bar.c; } void NpbmInput::swap(void* pBuffer, size_t nvals) { if (spec().format().channelBitLength(KHR_DF_CHANNEL_RGBSDA_R) == 16) { // If 2 bytes, MSB is first. if (littleendian()) { swapEndian16((uint16_t*)pBuffer, nvals); } } } void NpbmInput::readNativeScanline(void* bufferOut, size_t bufferByteCount, uint32_t y, uint32_t z, uint32_t subimage, uint32_t miplevel) { if (isp == nullptr) throw std::runtime_error("istream not initialized"); if (z > 1) throw std::runtime_error("npbm does not support 3d images."); if (bufferByteCount < spec().scanlineByteCount()) throw buffer_too_small(); seekSubimage(subimage, miplevel); if (y != curImageScanline) isp->seekg(images[currentSubimage()].filepos + (spec().scanlineByteCount() * y), isp->beg); isp->read((char*)bufferOut, spec().scanlineByteCount()); swap(bufferOut, spec().scanlineByteCount()); } bool NpbmInput::seekSubimage(uint32_t subimage, uint32_t miplevel) { if (subimage == currentSubimage() && miplevel == currentMiplevel()) return true; if (subimage >= images.size() || miplevel > 0) return false; isp->seekg(images[subimage].filepos, isp->beg); curSubimage = subimage; curMiplevel = miplevel; curImageScanline = 0; return true; } /// /// @internal /// @~English /// @brief parse the header of a PGM or PPM file. /// /// @param [in] src pointer to FILE stream to read /// @param [out] width reference to a var in which to write the image width. /// @param [out] height reference to a var in which to write the image height /// @param [out] maxval reference to a var in which to write the maxval. /// /// @exception invalid_file if there is no width or height, if maxval is not /// an integer or if maxval is out of range. /// void NpbmInput::parseAHeader() { #define MAX_TUPLETYPE_SIZE 20 #define xtupletype_sscanf_fmt(ms) tupletype_sscanf_fmt(ms) #define tupletype_sscanf_fmt(ms) "TUPLTYPE %"#ms"s" char tupleType[MAX_TUPLETYPE_SIZE+1]; // +1 for terminating NUL. uint32_t width = 0, height = 0; unsigned int numFieldsFound = 0; uint32_t componentCount = 0; uint32_t tCompCount = 0; uint32_t maxVal = 0; for (;;) { nextLine(); skipComments(); if (currentLine.compare("ENDHDR") == 0) break; const char* cur_line_cs = currentLine.c_str(); if (sscanf(cur_line_cs, "HEIGHT %u", &height)) numFieldsFound++; else if (sscanf(cur_line_cs, "WIDTH %u", &width)) numFieldsFound++; else if (sscanf(cur_line_cs, "DEPTH %u", &componentCount)) numFieldsFound++; else if (sscanf(cur_line_cs, "MAXVAL %u", &maxVal)) numFieldsFound++; else if (sscanf(cur_line_cs, xtupletype_sscanf_fmt(MAX_TUPLETYPE_SIZE), tupleType)) numFieldsFound++; }; if (numFieldsFound < 5) { throw invalid_file("Missing fields in pam header."); } if ((tCompCount = tupleComponentCount(tupleType)) < 1) { throw invalid_file( fmt::format("Invalid TUPLTYPE: {}.", tupleType) ); } if (componentCount < tCompCount) { throw invalid_file( fmt::format("Mismatched TUPLTYPE, {}, and DEPTH, {}.", tupleType, componentCount) ); } if (maxVal <= 0 || maxVal >= (1<<16)) { throw std::runtime_error( fmt::format("Max color component value must be > 0 && < 65536. " "It is {}", maxVal) ); } images.emplace_back(ImageSpec(width, height, 1, componentCount, maxVal > 255 ? 16 : 8, 0U, maxVal, static_cast(0), KHR_DF_TRANSFER_ITU, KHR_DF_PRIMARIES_BT709, tCompCount < 3 ? KHR_DF_MODEL_YUVSDA : KHR_DF_MODEL_RGBSDA), ImageInputFormatType::npbm); } /// /// @internal /// @~English /// @brief parse the header of a PGM or PPM file. /// /// @param [in] src pointer to FILE stream to read /// @param [out] width reference to a var in which to write the image width. /// @param [out] height reference to a var in which to write the image height /// @param [out] maxval reference to a var in which to write the maxval. /// /// @exception invalid_file if there is no width or height, if maxval is not /// an integer or if maxval is out of range. /// void NpbmInput::parseGPHeader(filetype ftype) { nextLine(); skipComments(); uint32_t numvals, width, height, maxVal; numvals = sscanf(currentLine.c_str(), "%u %u", &width, &height); if (numvals != 2) { throw invalid_file("width or height is missing."); } if (width <= 0 || height <= 0) { throw invalid_file("width or height is negative."); } nextLine(); skipComments(); numvals = sscanf(currentLine.c_str(), "%d", &maxVal); if (numvals == 0) { throw invalid_file("maxval must be an integer."); } if (maxVal <= 0 || maxVal >= (1<<16)) { throw invalid_file("Max color component value must be > 0 && < 65536."); } images.emplace_back(ImageSpec(width, height, 1, ImageSpec::Origin(ImageSpec::Origin::eLeft, ImageSpec::Origin::eTop), ftype == filetype::PPM ? 3 : 1, 8, 0, maxVal, static_cast(0), KHR_DF_TRANSFER_ITU, KHR_DF_PRIMARIES_BT709, ftype == filetype::PPM ? KHR_DF_MODEL_RGBSDA : KHR_DF_MODEL_YUVSDA), ImageInputFormatType::npbm); } /// @brief Read an entire image into contiguous memory performing conversions /// to @a requestFormat. /// /// @sa ImageInput::readScanline() for supported conversions. void NpbmInput::readImage(void* pBuffer, size_t bufferByteCount, uint32_t subimage, uint32_t miplevel, const FormatDescriptor& format) { const FormatDescriptor* targetFormat; if (isp == nullptr) throw std::runtime_error("No open input stream"); if (bufferByteCount < spec().imageByteCount()) throw buffer_too_small(); if (format.isUnknown()) targetFormat = &spec().format(); else targetFormat = &format; if (*targetFormat != spec().format()) { // Use default function which reads a scanline at a time to avoid // having to buffer entire image for conversion. ImageInput::readImage(pBuffer, bufferByteCount, subimage, miplevel, format); return; } try { seekSubimage(subimage, miplevel); isp->read((char*)pBuffer, spec().imageByteCount()); curImageScanline = spec().height(); swap(pBuffer, spec().imageChannelCount()); } catch (const std::istream::failure&) { throwOnReadFailure(); } }