You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
706 lines
26 KiB
C++
706 lines
26 KiB
C++
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
// included header files
|
|
#include "config.h"
|
|
|
|
#ifdef EXV_HAVE_LIBZ
|
|
#include <zlib.h> // To uncompress IccProfiles
|
|
|
|
#include "basicio.hpp"
|
|
#include "enforce.hpp"
|
|
#include "error.hpp"
|
|
#include "futils.hpp"
|
|
#include "image.hpp"
|
|
#include "image_int.hpp"
|
|
#include "jpgimage.hpp"
|
|
#include "photoshop.hpp"
|
|
#include "pngchunk_int.hpp"
|
|
#include "pngimage.hpp"
|
|
#include "tiffimage.hpp"
|
|
#include "types.hpp"
|
|
#include "utils.hpp"
|
|
|
|
#include <array>
|
|
#include <iostream>
|
|
|
|
namespace {
|
|
// Signature from front of PNG file
|
|
constexpr unsigned char pngSignature[] = {
|
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
|
};
|
|
|
|
constexpr unsigned char pngBlank[] = {
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00,
|
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00,
|
|
0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73,
|
|
0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b, 0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00, 0x00, 0x0c, 0x49,
|
|
0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff, 0x3f, 0x00, 0x05, 0xfe, 0x02, 0xfe, 0xdc, 0xcc, 0x59,
|
|
0xe7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
|
|
};
|
|
|
|
const auto nullComp = reinterpret_cast<const Exiv2::byte*>("\0\0");
|
|
const auto typeICCP = reinterpret_cast<const Exiv2::byte*>("iCCP");
|
|
inline bool compare(std::string_view str, const Exiv2::DataBuf& buf) {
|
|
const auto minlen = std::min(str.size(), buf.size());
|
|
return buf.cmpBytes(0, str.data(), minlen) == 0;
|
|
}
|
|
} // namespace
|
|
|
|
// *****************************************************************************
|
|
// class member definitions
|
|
namespace Exiv2 {
|
|
using namespace Internal;
|
|
|
|
PngImage::PngImage(BasicIo::UniquePtr io, bool create) :
|
|
Image(ImageType::png, mdExif | mdIptc | mdXmp | mdComment, std::move(io)) {
|
|
if (create) {
|
|
if (io_->open() == 0) {
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cerr << "Exiv2::PngImage:: Creating PNG image to memory\n";
|
|
#endif
|
|
IoCloser closer(*io_);
|
|
if (io_->write(pngBlank, sizeof(pngBlank)) != sizeof(pngBlank)) {
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cerr << "Exiv2::PngImage:: Failed to create PNG image on memory\n";
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string PngImage::mimeType() const {
|
|
return "image/png";
|
|
}
|
|
|
|
static bool zlibToDataBuf(const byte* bytes, uLongf length, DataBuf& result) {
|
|
uLongf uncompressedLen = length; // just a starting point
|
|
int zlibResult = Z_BUF_ERROR;
|
|
|
|
do {
|
|
result.alloc(uncompressedLen);
|
|
zlibResult = uncompress(result.data(), &uncompressedLen, bytes, length);
|
|
// if result buffer is large than necessary, redo to fit perfectly.
|
|
if (zlibResult == Z_OK && uncompressedLen < result.size()) {
|
|
result.reset();
|
|
|
|
result.alloc(uncompressedLen);
|
|
zlibResult = uncompress(result.data(), &uncompressedLen, bytes, length);
|
|
}
|
|
if (zlibResult == Z_BUF_ERROR) {
|
|
// the uncompressed buffer needs to be larger
|
|
result.reset();
|
|
|
|
// Sanity - never bigger than 16mb
|
|
if (uncompressedLen > 16 * 1024 * 1024)
|
|
zlibResult = Z_DATA_ERROR;
|
|
else
|
|
uncompressedLen *= 2;
|
|
}
|
|
} while (zlibResult == Z_BUF_ERROR);
|
|
|
|
return zlibResult == Z_OK;
|
|
}
|
|
|
|
static bool zlibToCompressed(const byte* bytes, uLongf length, DataBuf& result) {
|
|
uLongf compressedLen = length; // just a starting point
|
|
int zlibResult = Z_BUF_ERROR;
|
|
|
|
do {
|
|
result.alloc(compressedLen);
|
|
zlibResult = compress(result.data(), &compressedLen, bytes, length);
|
|
if (zlibResult == Z_BUF_ERROR) {
|
|
// the compressedArray needs to be larger
|
|
result.reset();
|
|
compressedLen *= 2;
|
|
} else {
|
|
result.reset();
|
|
result.alloc(compressedLen);
|
|
zlibResult = compress(result.data(), &compressedLen, bytes, length);
|
|
}
|
|
} while (zlibResult == Z_BUF_ERROR);
|
|
|
|
return zlibResult == Z_OK;
|
|
}
|
|
|
|
static bool tEXtToDataBuf(const byte* bytes, size_t length, DataBuf& result) {
|
|
static std::array<int, 256> value;
|
|
static bool bFirst = true;
|
|
if (bFirst) {
|
|
value.fill(0);
|
|
for (int i = 0; i < 10; i++) {
|
|
value['0' + i] = i + 1;
|
|
}
|
|
for (int i = 0; i < 6; i++) {
|
|
value['a' + i] = i + 10 + 1;
|
|
value['A' + i] = i + 10 + 1;
|
|
}
|
|
bFirst = false;
|
|
}
|
|
|
|
// calculate length and allocate result;
|
|
// count: number of \n in the header
|
|
size_t count = 0;
|
|
// p points to the current position in the array bytes
|
|
const byte* p = bytes;
|
|
|
|
// header is '\nsomething\n number\n hex'
|
|
// => increment p until it points to the byte after the last \n
|
|
// p must stay within bounds of the bytes array!
|
|
while (count < 3 && 0 < length) {
|
|
// length is later used for range checks of p => decrement it for each increment of p
|
|
--length;
|
|
if (*p++ == '\n') {
|
|
count++;
|
|
}
|
|
}
|
|
for (size_t i = 0; i < length; i++)
|
|
if (value[p[i]])
|
|
++count;
|
|
result.alloc((count + 1) / 2);
|
|
|
|
// hex to binary
|
|
count = 0;
|
|
byte* r = result.data();
|
|
int n = 0; // nibble
|
|
for (size_t i = 0; i < length; i++) {
|
|
if (value[p[i]]) {
|
|
int v = value[p[i]] - 1;
|
|
if (++count % 2)
|
|
n = v * 16; // leading digit
|
|
else
|
|
*r++ = n + v; // trailing
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::string::size_type findi(const std::string& str, const std::string& substr) {
|
|
return str.find(substr);
|
|
}
|
|
|
|
void PngImage::printStructure(std::ostream& out, PrintStructureOption option, size_t depth) {
|
|
if (io_->open() != 0) {
|
|
throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError());
|
|
}
|
|
if (!isPngType(*io_, true)) {
|
|
throw Error(ErrorCode::kerNotAnImage, "PNG");
|
|
}
|
|
|
|
char chType[5];
|
|
chType[0] = 0;
|
|
chType[4] = 0;
|
|
|
|
if (option == kpsBasic || option == kpsXMP || option == kpsIccProfile || option == kpsRecursive) {
|
|
const auto xmpKey = upper("XML:com.adobe.xmp");
|
|
const auto exifKey = upper("Raw profile type exif");
|
|
const auto app1Key = upper("Raw profile type APP1");
|
|
const auto iptcKey = upper("Raw profile type iptc");
|
|
const auto softKey = upper("Software");
|
|
const auto commKey = upper("Comment");
|
|
const auto descKey = upper("Description");
|
|
|
|
bool bPrint = option == kpsBasic || option == kpsRecursive;
|
|
if (bPrint) {
|
|
out << "STRUCTURE OF PNG FILE: " << io_->path() << std::endl;
|
|
out << " address | chunk | length | data | checksum" << std::endl;
|
|
}
|
|
|
|
const size_t imgSize = io_->size();
|
|
DataBuf cheaderBuf(8);
|
|
|
|
while (!io_->eof() && ::strcmp(chType, "IEND") != 0) {
|
|
const size_t address = io_->tell();
|
|
|
|
size_t bufRead = io_->read(cheaderBuf.data(), cheaderBuf.size());
|
|
if (io_->error())
|
|
throw Error(ErrorCode::kerFailedToReadImageData);
|
|
if (bufRead != cheaderBuf.size())
|
|
throw Error(ErrorCode::kerInputDataReadFailed);
|
|
|
|
// Decode chunk data length.
|
|
const uint32_t dataOffset = cheaderBuf.read_uint32(0, Exiv2::bigEndian);
|
|
for (int i = 4; i < 8; i++) {
|
|
chType[i - 4] = cheaderBuf.read_uint8(i);
|
|
}
|
|
|
|
// test that we haven't hit EOF, or wanting to read excessive data
|
|
const size_t restore = io_->tell();
|
|
if (dataOffset > imgSize - restore) {
|
|
throw Exiv2::Error(ErrorCode::kerFailedToReadImageData);
|
|
}
|
|
|
|
DataBuf buff(dataOffset);
|
|
if (dataOffset > 0) {
|
|
bufRead = io_->read(buff.data(), dataOffset);
|
|
enforce(bufRead == dataOffset, ErrorCode::kerFailedToReadImageData);
|
|
}
|
|
io_->seek(restore, BasicIo::beg);
|
|
|
|
// format output
|
|
const int iMax = 30;
|
|
const uint32_t blen = dataOffset > iMax ? iMax : dataOffset;
|
|
std::string dataString;
|
|
// if blen == 0 => slice construction fails
|
|
if (blen > 0) {
|
|
std::stringstream ss;
|
|
ss << Internal::binaryToString(makeSlice(buff, 0, blen));
|
|
dataString = ss.str();
|
|
}
|
|
while (dataString.size() < iMax)
|
|
dataString += ' ';
|
|
dataString = dataString.substr(0, iMax);
|
|
|
|
if (bPrint) {
|
|
io_->seek(dataOffset, BasicIo::cur); // jump to checksum
|
|
byte checksum[4];
|
|
bufRead = io_->read(checksum, 4);
|
|
enforce(bufRead == 4, ErrorCode::kerFailedToReadImageData);
|
|
io_->seek(restore, BasicIo::beg); // restore file pointer
|
|
|
|
out << Internal::stringFormat("%8d | %-5s |%8d | ", static_cast<uint32_t>(address), chType, dataOffset)
|
|
<< dataString
|
|
<< Internal::stringFormat(" | 0x%02x%02x%02x%02x", checksum[0], checksum[1], checksum[2], checksum[3])
|
|
<< std::endl;
|
|
}
|
|
|
|
// chunk type
|
|
bool tEXt = std::strcmp(chType, "tEXt") == 0;
|
|
bool zTXt = std::strcmp(chType, "zTXt") == 0;
|
|
bool iCCP = std::strcmp(chType, "iCCP") == 0;
|
|
bool iTXt = std::strcmp(chType, "iTXt") == 0;
|
|
bool eXIf = std::strcmp(chType, "eXIf") == 0;
|
|
|
|
// for XMP, ICC etc: read and format data
|
|
const auto dataStringU = upper(dataString);
|
|
bool bXMP = option == kpsXMP && findi(dataStringU, xmpKey) == 0;
|
|
bool bExif = option == kpsRecursive && (findi(dataStringU, exifKey) == 0 || findi(dataStringU, app1Key) == 0);
|
|
bool bIptc = option == kpsRecursive && findi(dataStringU, iptcKey) == 0;
|
|
bool bSoft = option == kpsRecursive && findi(dataStringU, softKey) == 0;
|
|
bool bComm = option == kpsRecursive && findi(dataStringU, commKey) == 0;
|
|
bool bDesc = option == kpsRecursive && findi(dataStringU, descKey) == 0;
|
|
bool bDump = bXMP || bExif || bIptc || bSoft || bComm || bDesc || iCCP || eXIf;
|
|
|
|
if (bDump) {
|
|
DataBuf dataBuf;
|
|
enforce(dataOffset < std::numeric_limits<uint32_t>::max(), ErrorCode::kerFailedToReadImageData);
|
|
DataBuf data(dataOffset + 1ul);
|
|
data.write_uint8(dataOffset, 0);
|
|
bufRead = io_->read(data.data(), dataOffset);
|
|
enforce(bufRead == dataOffset, ErrorCode::kerFailedToReadImageData);
|
|
io_->seek(restore, BasicIo::beg);
|
|
size_t name_l = std::strlen(data.c_str()) + 1; // leading string length
|
|
enforce(name_l < dataOffset, ErrorCode::kerCorruptedMetadata);
|
|
|
|
auto start = static_cast<uint32_t>(name_l);
|
|
bool bLF = false;
|
|
|
|
// decode the chunk
|
|
bool bGood = false;
|
|
if (tEXt) {
|
|
bGood = tEXtToDataBuf(data.c_data(name_l), dataOffset - name_l, dataBuf);
|
|
}
|
|
if (zTXt || iCCP) {
|
|
enforce(dataOffset - name_l - 1 <= std::numeric_limits<uLongf>::max(), ErrorCode::kerCorruptedMetadata);
|
|
bGood = zlibToDataBuf(data.c_data(name_l + 1), static_cast<uLongf>(dataOffset - name_l - 1),
|
|
dataBuf); // +1 = 'compressed' flag
|
|
}
|
|
if (iTXt) {
|
|
bGood = (3 <= dataOffset) && (start < dataOffset - 3); // good if not a nul chunk
|
|
}
|
|
if (eXIf) {
|
|
bGood = true; // eXIf requires no pre-processing)
|
|
}
|
|
|
|
// format is content dependent
|
|
if (bGood) {
|
|
if (bXMP) {
|
|
while (start < dataOffset && !data.read_uint8(start))
|
|
start++; // skip leading nul bytes
|
|
out << data.c_data(start); // output the xmp
|
|
}
|
|
|
|
if (bExif || bIptc) {
|
|
DataBuf parsedBuf = PngChunk::readRawProfile(dataBuf, tEXt);
|
|
#if EXIV2_DEBUG_MESSAGES
|
|
std::cerr << Exiv2::Internal::binaryToString(
|
|
makeSlice(parsedBuf.c_data(), parsedBuf.size() > 50 ? 50 : parsedBuf.size(), 0))
|
|
<< std::endl;
|
|
#endif
|
|
if (!parsedBuf.empty()) {
|
|
if (bExif) {
|
|
// create memio object with the data, then print the structure
|
|
MemIo p(parsedBuf.c_data(6), parsedBuf.size() - 6);
|
|
printTiffStructure(p, out, option, depth + 1);
|
|
}
|
|
if (bIptc) {
|
|
IptcData::printStructure(out, makeSlice(parsedBuf, 0, parsedBuf.size()), depth);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bSoft && !dataBuf.empty()) {
|
|
DataBuf s(dataBuf.size() + 1); // allocate buffer with an extra byte
|
|
std::copy(dataBuf.begin(), dataBuf.end(), s.begin()); // copy in the dataBuf
|
|
s.write_uint8(dataBuf.size(), 0); // nul terminate it
|
|
const auto str = s.c_str(); // give it name
|
|
out << Internal::indent(depth) << buff.c_str() << ": " << str;
|
|
bLF = true;
|
|
}
|
|
|
|
if ((iCCP && option == kpsIccProfile) || bComm) {
|
|
out.write(dataBuf.c_str(), dataBuf.size());
|
|
bLF = bComm;
|
|
}
|
|
|
|
if (bDesc && iTXt) {
|
|
DataBuf decoded = PngChunk::decodeTXTChunk(buff, PngChunk::iTXt_Chunk);
|
|
out.write(decoded.c_str(), decoded.size());
|
|
bLF = true;
|
|
}
|
|
|
|
if (eXIf && option == kpsRecursive) {
|
|
// create memio object with the data, then print the structure
|
|
MemIo p(data.c_data(), dataOffset);
|
|
printTiffStructure(p, out, option, depth + 1);
|
|
}
|
|
|
|
if (bLF)
|
|
out << std::endl;
|
|
}
|
|
}
|
|
io_->seek(dataOffset + 4, BasicIo::cur); // jump past checksum
|
|
if (io_->error())
|
|
throw Error(ErrorCode::kerFailedToReadImageData);
|
|
}
|
|
}
|
|
}
|
|
|
|
void readChunk(DataBuf& buffer, BasicIo& io) {
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::readMetadata: Position: " << io.tell() << std::endl;
|
|
#endif
|
|
const size_t bufRead = io.read(buffer.data(), buffer.size());
|
|
if (io.error()) {
|
|
throw Error(ErrorCode::kerFailedToReadImageData);
|
|
}
|
|
if (bufRead != buffer.size()) {
|
|
throw Error(ErrorCode::kerInputDataReadFailed);
|
|
}
|
|
}
|
|
|
|
void PngImage::readMetadata() {
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cerr << "Exiv2::PngImage::readMetadata: Reading PNG file " << io_->path() << std::endl;
|
|
#endif
|
|
if (io_->open() != 0) {
|
|
throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError());
|
|
}
|
|
IoCloser closer(*io_);
|
|
if (!isPngType(*io_, true)) {
|
|
throw Error(ErrorCode::kerNotAnImage, "PNG");
|
|
}
|
|
clearMetadata();
|
|
|
|
const size_t imgSize = io_->size();
|
|
DataBuf cheaderBuf(8); // Chunk header: 4 bytes (data size) + 4 bytes (chunk type).
|
|
|
|
while (!io_->eof()) {
|
|
readChunk(cheaderBuf, *io_); // Read chunk header.
|
|
|
|
// Decode chunk data length.
|
|
uint32_t chunkLength = cheaderBuf.read_uint32(0, Exiv2::bigEndian);
|
|
const size_t pos = io_->tell();
|
|
if (chunkLength > imgSize - pos) {
|
|
throw Exiv2::Error(ErrorCode::kerFailedToReadImageData);
|
|
}
|
|
|
|
std::string chunkType(cheaderBuf.c_str(4), 4);
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::readMetadata: chunk type: " << chunkType << " length: " << chunkLength << std::endl;
|
|
#endif
|
|
|
|
/// \todo analyse remaining chunks of the standard
|
|
// Perform a chunk triage for item that we need.
|
|
if (chunkType == "IEND" || chunkType == "IHDR" || chunkType == "tEXt" || chunkType == "zTXt" ||
|
|
chunkType == "eXIf" || chunkType == "iTXt" || chunkType == "iCCP") {
|
|
DataBuf chunkData(chunkLength);
|
|
if (chunkLength > 0) {
|
|
readChunk(chunkData, *io_); // Extract chunk data.
|
|
}
|
|
|
|
if (chunkType == "IEND") {
|
|
return; // Last chunk found: we stop parsing.
|
|
}
|
|
if (chunkType == "IHDR" && chunkData.size() >= 8) {
|
|
PngChunk::decodeIHDRChunk(chunkData, &pixelWidth_, &pixelHeight_);
|
|
} else if (chunkType == "tEXt") {
|
|
PngChunk::decodeTXTChunk(this, chunkData, PngChunk::tEXt_Chunk);
|
|
} else if (chunkType == "zTXt") {
|
|
PngChunk::decodeTXTChunk(this, chunkData, PngChunk::zTXt_Chunk);
|
|
} else if (chunkType == "iTXt") {
|
|
PngChunk::decodeTXTChunk(this, chunkData, PngChunk::iTXt_Chunk);
|
|
} else if (chunkType == "eXIf") {
|
|
ByteOrder bo = TiffParser::decode(exifData(), iptcData(), xmpData(), chunkData.c_data(), chunkData.size());
|
|
setByteOrder(bo);
|
|
} else if (chunkType == "iCCP") {
|
|
// The ICC profile name can vary from 1-79 characters.
|
|
uint32_t iccOffset = 0;
|
|
do {
|
|
enforce(iccOffset < 80 && iccOffset < chunkLength, Exiv2::ErrorCode::kerCorruptedMetadata);
|
|
} while (chunkData.read_uint8(iccOffset++) != 0x00);
|
|
|
|
profileName_ = std::string(chunkData.c_str(), iccOffset - 1);
|
|
++iccOffset; // +1 = 'compressed' flag
|
|
enforce(iccOffset <= chunkLength, Exiv2::ErrorCode::kerCorruptedMetadata);
|
|
|
|
zlibToDataBuf(chunkData.c_data(iccOffset), static_cast<uLongf>(chunkLength - iccOffset), iccProfile_);
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::readMetadata: profile name: " << profileName_ << std::endl;
|
|
std::cout << "Exiv2::PngImage::readMetadata: iccProfile.size_ (uncompressed) : " << iccProfile_.size()
|
|
<< std::endl;
|
|
#endif
|
|
}
|
|
|
|
// Set chunkLength to 0 in case we have read a supported chunk type. Otherwise, we need to seek the
|
|
// file to the next chunk position.
|
|
chunkLength = 0;
|
|
}
|
|
|
|
// Move to the next chunk: chunk data size + 4 CRC bytes.
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::readMetadata: Seek to offset: " << chunkLength + 4 << std::endl;
|
|
#endif
|
|
io_->seek(chunkLength + 4, BasicIo::cur);
|
|
if (io_->error() || io_->eof()) {
|
|
throw Error(ErrorCode::kerFailedToReadImageData);
|
|
}
|
|
}
|
|
} // PngImage::readMetadata
|
|
|
|
void PngImage::writeMetadata() {
|
|
if (io_->open() != 0) {
|
|
throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError());
|
|
}
|
|
IoCloser closer(*io_);
|
|
MemIo tempIo;
|
|
|
|
doWriteMetadata(tempIo); // may throw
|
|
io_->close();
|
|
io_->transfer(tempIo); // may throw
|
|
|
|
} // PngImage::writeMetadata
|
|
|
|
void PngImage::doWriteMetadata(BasicIo& outIo) {
|
|
if (!io_->isopen())
|
|
throw Error(ErrorCode::kerInputDataReadFailed);
|
|
if (!outIo.isopen())
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::doWriteMetadata: Writing PNG file " << io_->path() << "\n";
|
|
std::cout << "Exiv2::PngImage::doWriteMetadata: tmp file created " << outIo.path() << "\n";
|
|
#endif
|
|
|
|
if (!isPngType(*io_, true)) {
|
|
throw Error(ErrorCode::kerNoImageInInputData);
|
|
}
|
|
|
|
// Write PNG Signature.
|
|
if (outIo.write(pngSignature, 8) != 8)
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
|
|
DataBuf cheaderBuf(8); // Chunk header : 4 bytes (data size) + 4 bytes (chunk type).
|
|
|
|
while (!io_->eof()) {
|
|
// Read chunk header.
|
|
size_t bufRead = io_->read(cheaderBuf.data(), 8);
|
|
if (io_->error())
|
|
throw Error(ErrorCode::kerFailedToReadImageData);
|
|
if (bufRead != 8)
|
|
throw Error(ErrorCode::kerInputDataReadFailed);
|
|
|
|
// Decode chunk data length.
|
|
|
|
uint32_t dataOffset = cheaderBuf.read_uint32(0, Exiv2::bigEndian);
|
|
if (dataOffset > 0x7FFFFFFF)
|
|
throw Exiv2::Error(ErrorCode::kerFailedToReadImageData);
|
|
|
|
// Read whole chunk : Chunk header + Chunk data (not fixed size - can be null) + CRC (4 bytes).
|
|
|
|
DataBuf chunkBuf(8 + dataOffset + 4); // Chunk header (8 bytes) + Chunk data + CRC (4 bytes).
|
|
std::copy_n(cheaderBuf.begin(), 8, chunkBuf.begin()); // Copy header.
|
|
bufRead = io_->read(chunkBuf.data(8), dataOffset + 4); // Extract chunk data + CRC
|
|
if (io_->error())
|
|
throw Error(ErrorCode::kerFailedToReadImageData);
|
|
if (bufRead != dataOffset + 4)
|
|
throw Error(ErrorCode::kerInputDataReadFailed);
|
|
|
|
char szChunk[5];
|
|
memcpy(szChunk, cheaderBuf.c_data(4), 4);
|
|
szChunk[4] = 0;
|
|
|
|
if (!strcmp(szChunk, "IEND")) {
|
|
// Last chunk found: we write it and done.
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::doWriteMetadata: Write IEND chunk (length: " << dataOffset << ")\n";
|
|
#endif
|
|
if (outIo.write(chunkBuf.data(), chunkBuf.size()) != chunkBuf.size())
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
return;
|
|
}
|
|
if (!strcmp(szChunk, "eXIf") || !strcmp(szChunk, "iCCP")) {
|
|
// do nothing (strip): Exif metadata is written following IHDR
|
|
// as zTXt chunk with signature "Raw profile type exif",
|
|
// together with the ICC profile as a fresh iCCP chunk
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::doWriteMetadata: strip " << szChunk << " chunk (length: " << dataOffset << ")"
|
|
<< std::endl;
|
|
#endif
|
|
} else if (!strcmp(szChunk, "IHDR")) {
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::doWriteMetadata: Write IHDR chunk (length: " << dataOffset << ")\n";
|
|
#endif
|
|
if (outIo.write(chunkBuf.data(), chunkBuf.size()) != chunkBuf.size())
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
|
|
// Write all updated metadata here, just after IHDR.
|
|
if (!comment_.empty()) {
|
|
// Update Comment data to a new PNG chunk
|
|
std::string chunk = PngChunk::makeMetadataChunk(comment_, mdComment);
|
|
if (outIo.write(reinterpret_cast<const byte*>(chunk.data()), chunk.size()) != chunk.size()) {
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
}
|
|
}
|
|
|
|
if (exifData_.count() > 0) {
|
|
// Update Exif data to a new PNG chunk
|
|
Blob blob;
|
|
ExifParser::encode(blob, littleEndian, exifData_);
|
|
if (!blob.empty()) {
|
|
static const char exifHeader[] = {0x45, 0x78, 0x69, 0x66, 0x00, 0x00};
|
|
std::string rawExif =
|
|
std::string(exifHeader, 6) + std::string(reinterpret_cast<const char*>(blob.data()), blob.size());
|
|
std::string chunk = PngChunk::makeMetadataChunk(rawExif, mdExif);
|
|
if (outIo.write(reinterpret_cast<const byte*>(chunk.data()), chunk.size()) != chunk.size()) {
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (iptcData_.count() > 0) {
|
|
// Update IPTC data to a new PNG chunk
|
|
DataBuf newPsData = Photoshop::setIptcIrb(nullptr, 0, iptcData_);
|
|
if (!newPsData.empty()) {
|
|
std::string rawIptc(newPsData.c_str(), newPsData.size());
|
|
std::string chunk = PngChunk::makeMetadataChunk(rawIptc, mdIptc);
|
|
if (outIo.write(reinterpret_cast<const byte*>(chunk.data()), chunk.size()) != chunk.size()) {
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (iccProfileDefined()) {
|
|
DataBuf compressed;
|
|
enforce(iccProfile_.size() <= std::numeric_limits<uLongf>::max(), ErrorCode::kerCorruptedMetadata);
|
|
if (zlibToCompressed(iccProfile_.c_data(), static_cast<uLongf>(iccProfile_.size()), compressed)) {
|
|
const auto nameLength = static_cast<uint32_t>(profileName_.size());
|
|
const uint32_t chunkLength = nameLength + 2 + static_cast<uint32_t>(compressed.size());
|
|
byte length[4];
|
|
ul2Data(length, chunkLength, bigEndian);
|
|
|
|
// calculate CRC
|
|
uLong tmp = crc32(0L, Z_NULL, 0);
|
|
tmp = crc32(tmp, typeICCP, 4);
|
|
tmp = crc32(tmp, reinterpret_cast<const Bytef*>(profileName_.data()), nameLength);
|
|
tmp = crc32(tmp, nullComp, 2);
|
|
tmp = crc32(tmp, compressed.c_data(), static_cast<uint32_t>(compressed.size()));
|
|
byte crc[4];
|
|
ul2Data(crc, tmp, bigEndian);
|
|
|
|
if (outIo.write(length, 4) != 4 || outIo.write(typeICCP, 4) != 4 ||
|
|
outIo.write(reinterpret_cast<const byte*>(profileName_.data()), nameLength) != nameLength ||
|
|
outIo.write(nullComp, 2) != 2 ||
|
|
outIo.write(compressed.c_data(), compressed.size()) != compressed.size() || outIo.write(crc, 4) != 4) {
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
}
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::doWriteMetadata: build iCCP"
|
|
<< " chunk (length: " << compressed.size() + chunkLength << ")" << std::endl;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
if (!writeXmpFromPacket()) {
|
|
if (XmpParser::encode(xmpPacket_, xmpData_) > 1) {
|
|
#ifndef SUPPRESS_WARNINGS
|
|
EXV_ERROR << "Failed to encode XMP metadata.\n";
|
|
#endif
|
|
}
|
|
}
|
|
if (!xmpPacket_.empty()) {
|
|
// Update XMP data to a new PNG chunk
|
|
std::string chunk = PngChunk::makeMetadataChunk(xmpPacket_, mdXmp);
|
|
if (outIo.write(reinterpret_cast<const byte*>(chunk.data()), chunk.size()) != chunk.size()) {
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
}
|
|
}
|
|
} else if (!strcmp(szChunk, "tEXt") || !strcmp(szChunk, "zTXt") || !strcmp(szChunk, "iTXt")) {
|
|
DataBuf key = PngChunk::keyTXTChunk(chunkBuf, true);
|
|
if (!key.empty() && (compare("Raw profile type exif", key) || compare("Raw profile type APP1", key) ||
|
|
compare("Raw profile type iptc", key) || compare("Raw profile type xmp", key) ||
|
|
compare("XML:com.adobe.xmp", key) || compare("Description", key))) {
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::doWriteMetadata: strip " << szChunk << " chunk (length: " << dataOffset << ")"
|
|
<< std::endl;
|
|
#endif
|
|
} else {
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::doWriteMetadata: write " << szChunk << " chunk (length: " << dataOffset << ")"
|
|
<< std::endl;
|
|
#endif
|
|
if (outIo.write(chunkBuf.c_data(), chunkBuf.size()) != chunkBuf.size())
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
}
|
|
} else {
|
|
// Write all others chunk as well.
|
|
#ifdef EXIV2_DEBUG_MESSAGES
|
|
std::cout << "Exiv2::PngImage::doWriteMetadata: copy " << szChunk << " chunk (length: " << dataOffset << ")"
|
|
<< std::endl;
|
|
#endif
|
|
if (outIo.write(chunkBuf.c_data(), chunkBuf.size()) != chunkBuf.size())
|
|
throw Error(ErrorCode::kerImageWriteFailed);
|
|
}
|
|
}
|
|
|
|
} // PngImage::doWriteMetadata
|
|
|
|
// *************************************************************************
|
|
// free functions
|
|
Image::UniquePtr newPngInstance(BasicIo::UniquePtr io, bool create) {
|
|
auto image = std::make_unique<PngImage>(std::move(io), create);
|
|
if (!image->good()) {
|
|
image.reset();
|
|
}
|
|
return image;
|
|
}
|
|
|
|
bool isPngType(BasicIo& iIo, bool advance) {
|
|
if (iIo.error() || iIo.eof()) {
|
|
throw Error(ErrorCode::kerInputDataReadFailed);
|
|
}
|
|
const int32_t len = 8;
|
|
byte buf[len];
|
|
iIo.read(buf, len);
|
|
if (iIo.error() || iIo.eof()) {
|
|
return false;
|
|
}
|
|
int rc = memcmp(buf, pngSignature, 8);
|
|
if (!advance || rc != 0) {
|
|
iIo.seek(-len, BasicIo::cur);
|
|
}
|
|
|
|
return rc == 0;
|
|
}
|
|
} // namespace Exiv2
|
|
#endif
|