blob: 53dea4928a12687aec6ef8194a5e655ec5129e67 [file]
/*
* Copyright 2026 Google LLC.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "experimental/rust_jpeg/encoder/SkJpegRustEncoder.h"
#include "experimental/rust_jpeg/ffi/FFI.rs.h"
#include "include/core/SkColorType.h"
#include "include/core/SkData.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkPixmap.h"
#include "include/core/SkStream.h"
#include "include/encode/SkJpegEncoder.h"
#include "include/private/SkTPin.h"
#include "include/private/SkTo.h"
#include "src/core/SkImageInfoPriv.h"
#include "src/core/SkSafeMath.h"
#include "third_party/rust/cxx/v1/cxx.h"
namespace SkJpegRustEncoder {
bool Encode(SkWStream* dst, const SkPixmap& src,
const SkJpegEncoder::Options& options) {
if (!dst) {
return false;
}
const SkImageInfo& info = src.info();
if (!SkImageInfoIsValid(info) || !src.addr() || src.rowBytes() < info.minRowBytes()) {
return false;
}
SkSafeMath safe;
const uint32_t width = safe.castTo<uint32_t>(info.width());
const uint32_t height = safe.castTo<uint32_t>(info.height());
const uint32_t srcRowBytes = safe.castTo<uint32_t>(src.rowBytes());
const size_t totalBytes = safe.mul(static_cast<size_t>(height), src.rowBytes());
if (!safe.ok()) {
return false;
}
rust_jpeg::JpegEncodeColor colorType;
switch (info.colorType()) {
case kRGB_888x_SkColorType:
// kRGB_888x is 4 bytes/pixel (R, G, B, X) — same layout as RGBA_8888
// but the 4th byte is ignored. Map to RGBA with Ignore alpha so the
// Rust encoder uses the correct 4-byte row stride.
colorType = rust_jpeg::JpegEncodeColor::RGBA;
break;
case kRGBA_8888_SkColorType:
colorType = rust_jpeg::JpegEncodeColor::RGBA;
break;
case kBGRA_8888_SkColorType:
colorType = rust_jpeg::JpegEncodeColor::BGRA;
break;
case kGray_8_SkColorType:
colorType = rust_jpeg::JpegEncodeColor::Grayscale;
break;
default:
return false;
}
const bool hasUnpremulAlpha = (info.colorType() == kRGBA_8888_SkColorType ||
info.colorType() == kBGRA_8888_SkColorType) &&
info.alphaType() == kUnpremul_SkAlphaType;
const rust_jpeg::JpegEncodeAlpha alphaOption =
options.fAlphaOption == SkJpegEncoder::AlphaOption::kBlendOnBlack && hasUnpremulAlpha
? rust_jpeg::JpegEncodeAlpha::BlendOnBlack
: rust_jpeg::JpegEncodeAlpha::Ignore;
const uint32_t quality = SkToU32(SkTPin(options.fQuality, 0, 100));
const uint8_t* pixels = static_cast<const uint8_t*>(src.addr());
rust::Slice<const uint8_t> pixelSlice(pixels, totalBytes);
rust::Vec<uint8_t> output;
rust_jpeg::EncodingResult result = rust_jpeg::encode_jpeg(
pixelSlice, width, height, srcRowBytes, colorType, alphaOption,
quality, output);
if (result != rust_jpeg::EncodingResult::Success) {
return false;
}
// Write encoded JPEG data, then insert XMP if provided.
// The jpeg-encoder crate writes a complete JPEG. For XMP, we insert an
// APP1 marker segment right after the SOI + APP0 (JFIF) markers.
if (options.xmpMetadata && options.xmpMetadata->size() > 0) {
// JPEG structure: SOI (2 bytes), then APP0/JFIF segment, then rest.
// We find the end of the first APP marker and inject APP1 XMP there.
const uint8_t* data = output.data();
const size_t size = output.size();
// Find insertion point after SOI + first APP marker.
size_t insertPos = 2; // After SOI (FF D8)
if (size > 5 && data[2] == 0xFF && (data[3] & 0xF0) == 0xE0) {
// The JPEG length field at data[4..5] includes its own 2 bytes.
// Total bytes consumed by the APP segment: 2 (marker) + segLen.
uint16_t segLen = (static_cast<uint16_t>(data[4]) << 8) | data[5];
size_t candidate = static_cast<size_t>(2) + 2 + segLen;
if (candidate <= size) {
insertPos = candidate;
}
}
// Build APP1 XMP segment:
// Marker: FF E1
// Length: 2 + namespace.size() + 1 + xmp.size()
static const char kXmpNamespace[] = "http://ns.adobe.com/xap/1.0/";
const size_t nsLen = sizeof(kXmpNamespace); // includes null terminator
const size_t xmpSize = options.xmpMetadata->size();
SkSafeMath app1Safe;
const size_t app1PayloadSize = app1Safe.add(app1Safe.add(2, nsLen), xmpSize);
if (!app1Safe.ok() || app1PayloadSize > 0xFFFF) {
// APP1 segment length field is 16-bit; extended XMP is not
// supported yet.
return false;
}
const uint16_t app1Len = static_cast<uint16_t>(app1PayloadSize);
// Write: before insertion, APP1 marker, rest
if (!dst->write(data, insertPos)) {
return false;
}
uint8_t app1Header[4] = {
0xFF, 0xE1,
static_cast<uint8_t>(app1Len >> 8),
static_cast<uint8_t>(app1Len & 0xFF)
};
if (!dst->write(app1Header, 4) || !dst->write(kXmpNamespace, nsLen) ||
!dst->write(options.xmpMetadata->data(), xmpSize) ||
!dst->write(data + insertPos, size - insertPos)) {
return false;
}
} else {
if (!dst->write(output.data(), output.size())) {
return false;
}
}
return true;
}
} // namespace SkJpegRustEncoder