blob: 80709def8c06747277803c057dd580294e6e0669 [file] [log] [blame]
/*
* Copyright 2023 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/private/SkJpegGainmapEncoder.h"
#ifdef SK_ENCODE_JPEG
#include "include/core/SkBitmap.h"
#include "include/core/SkPixmap.h"
#include "include/core/SkStream.h"
#include "include/encode/SkJpegEncoder.h"
#include "include/private/SkGainmapInfo.h"
#include "src/codec/SkCodecPriv.h"
#include "src/codec/SkJpegConstants.h"
#include "src/codec/SkJpegMultiPicture.h"
#include "src/codec/SkJpegPriv.h"
#include "src/codec/SkJpegSegmentScan.h"
#include <vector>
////////////////////////////////////////////////////////////////////////////////////////////////////
// XMP helpers
void xmp_write_prefix(SkDynamicMemoryWStream& s, const std::string& ns, const std::string& attrib) {
s.writeText(ns.c_str());
s.writeText(":");
s.writeText(attrib.c_str());
s.writeText("=\"");
}
void xmp_write_suffix(SkDynamicMemoryWStream& s, bool newLine) {
s.writeText("\"");
if (newLine) {
s.writeText("\n");
}
}
void xmp_write_per_channel_attr(SkDynamicMemoryWStream& s,
const std::string& ns,
const std::string& attrib,
SkScalar r,
SkScalar g,
SkScalar b,
bool newLine = true) {
xmp_write_prefix(s, ns, attrib);
if (r == g && r == b) {
s.writeScalarAsText(r);
} else {
s.writeScalarAsText(r);
s.writeText(",");
s.writeScalarAsText(g);
s.writeText(",");
s.writeScalarAsText(b);
}
xmp_write_suffix(s, newLine);
}
void xmp_write_scalar_attr(SkDynamicMemoryWStream& s,
const std::string& ns,
const std::string& attrib,
SkScalar value,
bool newLine = true) {
xmp_write_prefix(s, ns, attrib);
s.writeScalarAsText(value);
xmp_write_suffix(s, newLine);
}
void xmp_write_decimal_attr(SkDynamicMemoryWStream& s,
const std::string& ns,
const std::string& attrib,
int32_t value,
bool newLine = true) {
xmp_write_prefix(s, ns, attrib);
s.writeDecAsText(value);
xmp_write_suffix(s, newLine);
}
void xmp_write_string_attr(SkDynamicMemoryWStream& s,
const std::string& ns,
const std::string& attrib,
const std::string& value,
bool newLine = true) {
xmp_write_prefix(s, ns, attrib);
s.writeText(value.c_str());
xmp_write_suffix(s, newLine);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// JpegR encoding
bool SkJpegGainmapEncoder::EncodeJpegR(SkWStream* dst,
const SkPixmap& base,
const SkJpegEncoder::Options& baseOptions,
const SkPixmap& gainmap,
const SkJpegEncoder::Options& gainmapOptions,
const SkGainmapInfo& gainmapInfo) {
return EncodeHDRGM(dst, base, baseOptions, gainmap, gainmapOptions, gainmapInfo);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// HDRGM encoding
// Generate the XMP metadata for an HDRGM file.
sk_sp<SkData> get_hdrgm_xmp_data(const SkGainmapInfo& gainmapInfo) {
const float kLog2 = sk_float_log(2.f);
SkDynamicMemoryWStream s;
s.writeText(
"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 5.5.0\">\n"
" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n"
" <rdf:Description rdf:about=\"\"\n"
" xmlns:hdrgm=\"http://ns.adobe.com/hdr-gain-map/1.0/\"\n");
const std::string hdrgmPrefix = " hdrgm";
xmp_write_string_attr(s, hdrgmPrefix, "Version", "1.0");
xmp_write_per_channel_attr(s,
hdrgmPrefix,
"GainMapMin",
sk_float_log(gainmapInfo.fGainmapRatioMin.fR) / kLog2,
sk_float_log(gainmapInfo.fGainmapRatioMin.fG) / kLog2,
sk_float_log(gainmapInfo.fGainmapRatioMin.fB) / kLog2);
xmp_write_per_channel_attr(s,
hdrgmPrefix,
"GainMapMax",
sk_float_log(gainmapInfo.fGainmapRatioMax.fR) / kLog2,
sk_float_log(gainmapInfo.fGainmapRatioMax.fG) / kLog2,
sk_float_log(gainmapInfo.fGainmapRatioMax.fB) / kLog2);
xmp_write_per_channel_attr(s,
hdrgmPrefix,
"Gamma",
gainmapInfo.fGainmapGamma.fR,
gainmapInfo.fGainmapGamma.fG,
gainmapInfo.fGainmapGamma.fB);
xmp_write_per_channel_attr(s,
hdrgmPrefix,
"OffsetSDR",
gainmapInfo.fEpsilonSdr.fR,
gainmapInfo.fEpsilonSdr.fG,
gainmapInfo.fEpsilonSdr.fB);
xmp_write_per_channel_attr(s,
hdrgmPrefix,
"OffsetHDR",
gainmapInfo.fEpsilonHdr.fR,
gainmapInfo.fEpsilonHdr.fG,
gainmapInfo.fEpsilonHdr.fB);
xmp_write_scalar_attr(
s, hdrgmPrefix, "HDRCapacityMin", sk_float_log(gainmapInfo.fDisplayRatioSdr) / kLog2);
xmp_write_scalar_attr(
s, hdrgmPrefix, "HDRCapacityMax", sk_float_log(gainmapInfo.fDisplayRatioHdr) / kLog2);
switch (gainmapInfo.fBaseImageType) {
case SkGainmapInfo::BaseImageType::kSDR:
xmp_write_string_attr(s, hdrgmPrefix, "BaseRendition", "SDR", /*newLine=*/false);
break;
case SkGainmapInfo::BaseImageType::kHDR:
xmp_write_string_attr(s, hdrgmPrefix, "BaseRendition", "HDR", /*newLine=*/false);
break;
}
s.writeText(
"/>\n"
" </rdf:RDF>\n"
"</x:xmpmeta>");
return s.detachAsData();
}
// Generate the GContainer metadata for an image with a JPEG gainmap.
static sk_sp<SkData> get_gcontainer_xmp_data(size_t gainmapItemLength) {
SkDynamicMemoryWStream s;
s.writeText(
"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"Adobe XMP Core 5.1.2\">\n"
" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n"
" <rdf:Description\n"
" xmlns:Container=\"http://ns.google.com/photos/1.0/container/\"\n"
" xmlns:Item=\"http://ns.google.com/photos/1.0/container/item/\">\n"
" <Container:Directory>\n"
" <rdf:Seq>\n"
" <rdf:li>\n"
" <Container:Item\n"
" Item:Semantic=\"Primary\"\n"
" Item:Mime=\"image/jpeg\"/>\n"
" </rdf:li>\n"
" <rdf:li>\n"
" <Container:Item\n"
" Item:Semantic=\"RecoveryMap\"\n"
" Item:Mime=\"image/jpeg\"\n"
" ");
xmp_write_decimal_attr(s, "Item", "Length", gainmapItemLength, /*newLine=*/false);
s.writeText(
"/>\n"
" </rdf:li>\n"
" </rdf:Seq>\n"
" </Container:Directory>\n"
" </rdf:Description>\n"
" </rdf:RDF>\n"
"</x:xmpmeta>\n");
return s.detachAsData();
}
// Split an SkData into segments.
std::vector<sk_sp<SkData>> get_hdrgm_image_segments(sk_sp<SkData> image,
size_t segmentMaxDataSize) {
// Compute the total size of the header to a gainmap image segment (not including the 2 bytes
// for the segment size, which the encoder is responsible for writing).
constexpr size_t kGainmapHeaderSize = sizeof(kGainmapSig) + 2 * kGainmapMarkerIndexSize;
// Compute the payload size for each segment.
const size_t kGainmapPayloadSize = segmentMaxDataSize - kGainmapHeaderSize;
// Compute the number of segments we'll need.
const size_t segmentCount = (image->size() + kGainmapPayloadSize - 1) / kGainmapPayloadSize;
std::vector<sk_sp<SkData>> result;
result.reserve(segmentCount);
// Move |imageData| through |image| until it hits |imageDataEnd|.
const uint8_t* imageData = image->bytes();
const uint8_t* imageDataEnd = image->bytes() + image->size();
while (imageData < imageDataEnd) {
SkDynamicMemoryWStream segmentStream;
// Write the signature.
segmentStream.write(kGainmapSig, sizeof(kGainmapSig));
// Write the segment index as big-endian.
size_t segmentIndex = result.size() + 1;
uint8_t segmentIndexBytes[2] = {
static_cast<uint8_t>(segmentIndex / 256u),
static_cast<uint8_t>(segmentIndex % 256u),
};
segmentStream.write(segmentIndexBytes, sizeof(segmentIndexBytes));
// Write the segment count as big-endian.
uint8_t segmentCountBytes[2] = {
static_cast<uint8_t>(segmentCount / 256u),
static_cast<uint8_t>(segmentCount % 256u),
};
segmentStream.write(segmentCountBytes, sizeof(segmentCountBytes));
// Verify that our header size math is correct.
SkASSERT(segmentStream.bytesWritten() == kGainmapHeaderSize);
// Write the rest of the segment.
size_t bytesToWrite =
std::min(imageDataEnd - imageData, static_cast<intptr_t>(kGainmapPayloadSize));
segmentStream.write(imageData, bytesToWrite);
imageData += bytesToWrite;
// Verify that our data size math is correct.
if (segmentIndex == segmentCount) {
SkASSERT(segmentStream.bytesWritten() <= segmentMaxDataSize);
} else {
SkASSERT(segmentStream.bytesWritten() == segmentMaxDataSize);
}
result.push_back(segmentStream.detachAsData());
}
// Verify that our segment count math was correct.
SkASSERT(imageData == imageDataEnd);
SkASSERT(result.size() == segmentCount);
return result;
}
static sk_sp<SkData> encode_to_data(const SkPixmap& pm,
const SkJpegEncoder::Options& options,
SkData* xmpMetadata) {
SkJpegEncoder::Options optionsWithXmp = options;
optionsWithXmp.xmpMetadata = xmpMetadata;
SkDynamicMemoryWStream encodeStream;
auto encoder = SkJpegEncoder::Make(&encodeStream, pm, optionsWithXmp);
if (!encoder || !encoder->encodeRows(pm.height())) {
return nullptr;
}
return encodeStream.detachAsData();
}
static sk_sp<SkData> get_mpf_segment(const SkJpegMultiPictureParameters& mpParams) {
SkDynamicMemoryWStream s;
auto segmentParameters = mpParams.serialize();
const size_t mpParameterLength = kJpegSegmentParameterLengthSize + segmentParameters->size();
s.write8(0xFF);
s.write8(kMpfMarker);
s.write8(mpParameterLength / 256);
s.write8(mpParameterLength % 256);
s.write(segmentParameters->data(), segmentParameters->size());
return s.detachAsData();
}
bool SkJpegGainmapEncoder::EncodeHDRGM(SkWStream* dst,
const SkPixmap& base,
const SkJpegEncoder::Options& baseOptions,
const SkPixmap& gainmap,
const SkJpegEncoder::Options& gainmapOptions,
const SkGainmapInfo& gainmapInfo) {
// Encode the gainmap image with the HDRGM XMP metadata.
sk_sp<SkData> gainmapData;
{
// We will include the HDRGM XMP metadata in the gainmap image.
auto hdrgmXmp = get_hdrgm_xmp_data(gainmapInfo);
gainmapData = encode_to_data(gainmap, gainmapOptions, hdrgmXmp.get());
if (!gainmapData) {
SkCodecPrintf("Failed to encode gainmap image.\n");
return false;
}
}
// Encode the base image with the Container XMP metadata.
sk_sp<SkData> baseData;
{
auto containerXmp = get_gcontainer_xmp_data(static_cast<int32_t>(gainmapData->size()));
baseData = encode_to_data(base, baseOptions, containerXmp.get());
if (!baseData) {
SkCodecPrintf("Failed to encode base image.\n");
return false;
}
}
// Combine them into an MPF.
const SkData* images[] = {
baseData.get(),
gainmapData.get(),
};
return MakeMPF(dst, images, 2);
}
bool SkJpegGainmapEncoder::MakeMPF(SkWStream* dst, const SkData** images, size_t imageCount) {
if (imageCount < 1) {
return true;
}
// Create a scan of the primary image.
SkJpegSegmentScanner primaryScan;
primaryScan.onBytes(images[0]->data(), images[0]->size());
if (!primaryScan.isDone()) {
SkCodecPrintf("Failed to scan encoded primary image header.\n");
return false;
}
// Copy the primary image up to its StartOfScan, then insert the MPF segment, then copy the rest
// of the primary image, and all other images.
size_t bytesRead = 0;
size_t bytesWritten = 0;
for (const auto& segment : primaryScan.getSegments()) {
// Write all ECD before this segment.
{
size_t ecdBytesToWrite = segment.offset - bytesRead;
if (!dst->write(images[0]->bytes() + bytesRead, ecdBytesToWrite)) {
SkCodecPrintf("Failed to write entropy coded data.\n");
return false;
}
bytesWritten += ecdBytesToWrite;
bytesRead = segment.offset;
}
// If this isn't a StartOfScan, write just the segment.
if (segment.marker != kJpegMarkerStartOfScan) {
const size_t bytesToWrite = kJpegMarkerCodeSize + segment.parameterLength;
if (!dst->write(images[0]->bytes() + bytesRead, bytesToWrite)) {
SkCodecPrintf("Failed to copy segment.\n");
return false;
}
bytesWritten += bytesToWrite;
bytesRead += bytesToWrite;
continue;
}
// We're now at the StartOfScan.
const size_t bytesRemaining = images[0]->size() - bytesRead;
// Compute the MPF offsets for the images.
SkJpegMultiPictureParameters mpParams;
{
mpParams.images.resize(imageCount);
const size_t mpSegmentSize = kJpegMarkerCodeSize + kJpegSegmentParameterLengthSize +
mpParams.serialize()->size();
mpParams.images[0].size =
static_cast<uint32_t>(bytesWritten + mpSegmentSize + bytesRemaining);
uint32_t offset =
static_cast<uint32_t>(bytesRemaining + mpSegmentSize - kJpegMarkerCodeSize -
kJpegSegmentParameterLengthSize - sizeof(kMpfSig));
for (size_t i = 0; i < imageCount; ++i) {
mpParams.images[i].dataOffset = offset;
mpParams.images[i].size = static_cast<uint32_t>(images[i]->size());
offset += mpParams.images[i].size;
}
}
// Write the MPF segment.
auto mpfSegment = get_mpf_segment(mpParams);
if (!dst->write(mpfSegment->data(), mpfSegment->size())) {
SkCodecPrintf("Failed to write MPF segment.\n");
return false;
}
// Write the rest of the primary file.
if (!dst->write(images[0]->bytes() + bytesRead, bytesRemaining)) {
SkCodecPrintf("Failed to write remainder of primary image.\n");
return false;
}
bytesRead += bytesRemaining;
SkASSERT(bytesRead == images[0]->size());
break;
}
// Write the remaining files.
for (size_t i = 1; i < imageCount; ++i) {
if (!dst->write(images[i]->data(), images[i]->size())) {
SkCodecPrintf("Failed to write auxiliary image.\n");
}
}
return true;
}
#endif // SK_ENCODE_JPEG