Files
sousa-gecko/image/decoders/nsJXLDecoder.cpp
T
Emilio Cobos Álvarez fdebe04602 Bug 2054311 - Sort includes in image/. r=tnikkel
Autogenerated by:

    cp dom/.clang-format image/ && mach format image/**.{cpp,h,mm}

No manual changes required.

Differential Revision: https://phabricator.services.mozilla.com/D311693
2026-07-12 05:32:50 +00:00

728 lines
25 KiB
C++

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsJXLDecoder.h"
#include "AnimationParams.h"
#include "ImageLogging.h" // Must appear first
#include "RasterImage.h"
#include "SurfacePipeFactory.h"
#include "gfxPlatform.h"
#include "mozilla/CheckedInt.h"
using namespace mozilla::gfx;
namespace mozilla::image {
static LazyLogModule sJXLLog("JXLDecoder");
nsJXLDecoder::nsJXLDecoder(RasterImage* aImage) : Decoder(aImage) {
MOZ_LOG(sJXLLog, LogLevel::Debug,
("[this=%p] nsJXLDecoder::nsJXLDecoder", this));
}
nsresult nsJXLDecoder::InitInternal() {
bool hasCMS = GetCMSOutputProfile() && mCMSMode != CMSMode::Off;
mDecoder.reset(jxl_decoder_new(IsMetadataDecode(), hasCMS));
if (WantsFrameCount()) {
mScanner.reset(jxl_scanner_new());
}
return NS_OK;
}
nsJXLDecoder::~nsJXLDecoder() {
MOZ_LOG(sJXLLog, LogLevel::Debug,
("[this=%p] nsJXLDecoder::~nsJXLDecoder", this));
}
LexerResult nsJXLDecoder::DoDecode(SourceBufferIterator& aIterator,
IResumable* aOnResume) {
MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");
if (WantsFrameCount()) {
return ScanForFrameCount(aIterator, aOnResume);
}
while (true) {
if (mIteratorComplete) {
return DrainFrames();
}
if (!aIterator.IsReady() || aIterator.Length() == 0) {
SourceBufferIterator::State state =
aIterator.AdvanceOrScheduleResume(SIZE_MAX, aOnResume);
if (state == SourceBufferIterator::WAITING ||
state == SourceBufferIterator::COMPLETE) {
// We are about to suspend until more bytes arrive, so this is the
// point where flushing a partial frame is actually useful for the
// user. NeedMoreData inside ProcessAvailableData does not imply this:
// the iterator may still have buffered bytes that we will consume on
// the next loop iteration in ProcessAvailableData. This is also
// potentially the only and last chance to actually push the pixels
// through the surface pipe so they get to the user.
if (!HasAnimation() && !mPixelBuffer.empty()) {
FlushPartialFrame();
}
}
if (state == SourceBufferIterator::WAITING) {
return LexerResult(Yield::NEED_MORE_DATA);
}
if (state == SourceBufferIterator::COMPLETE) {
mIteratorComplete = true;
continue;
}
}
const uint8_t* data = reinterpret_cast<const uint8_t*>(aIterator.Data());
size_t length = aIterator.Length();
MOZ_ASSERT(length > 0);
const uint8_t* const chunkStart = data;
// data and length are in/out: updated to reflect the unconsumed remainder.
ProcessResult result = ProcessAvailableData(&data, &length);
aIterator.MarkConsumed(static_cast<size_t>(data - chunkStart));
switch (result) {
case ProcessResult::Error:
return LexerResult(TerminalState::FAILURE);
case ProcessResult::Complete:
return LexerResult(TerminalState::SUCCESS);
case ProcessResult::NeedMoreData:
break;
case ProcessResult::YieldOutput:
return LexerResult(Yield::OUTPUT_AVAILABLE);
}
}
}
LexerResult nsJXLDecoder::ScanForFrameCount(SourceBufferIterator& aIterator,
IResumable* aOnResume) {
MOZ_ASSERT(mScanner);
const uint8_t* currentData = nullptr;
size_t currentLength = 0;
bool iteratorComplete = false;
while (true) {
// Only fetch a new chunk when all buffered bytes have been consumed.
if (currentLength == 0 && !iteratorComplete) {
SourceBufferIterator::State state =
aIterator.AdvanceOrScheduleResume(SIZE_MAX, aOnResume);
if (state == SourceBufferIterator::WAITING) {
return LexerResult(Yield::NEED_MORE_DATA);
}
if (state == SourceBufferIterator::READY) {
currentData = reinterpret_cast<const uint8_t*>(aIterator.Data());
currentLength = aIterator.Length();
}
if (state == SourceBufferIterator::COMPLETE) {
iteratorComplete = true;
}
}
JxlDecoderStatus scanStatus = jxl_decoder_process_data(
mScanner.get(), &currentData, &currentLength,
/*pixel_buffer=*/nullptr, 0, /*k_buffer=*/nullptr, 0);
if (scanStatus == JxlDecoderStatus::Error) {
return LexerResult(TerminalState::FAILURE);
}
if (!HasSize()) {
JxlBasicInfo info = jxl_decoder_get_basic_info(mScanner.get());
if (info.valid) {
if (info.width > INT32_MAX || info.height > INT32_MAX) {
return LexerResult(TerminalState::FAILURE);
}
PostSize(info.width, info.height);
if (info.has_alpha) {
PostHasTransparency();
}
if (!info.is_animated) {
PostFrameCount(1);
return LexerResult(TerminalState::SUCCESS);
}
}
}
// Post animation info once we have at least one scanned frame.
if (HasSize() && !HasAnimation()) {
// If we get here we know that we are animated but we haven't called
// PostIsAnimated yet because we return above if we are not animated.
uint32_t scannedCount =
jxl_decoder_get_scanned_frame_count(mScanner.get());
if (scannedCount > 0) {
JxlBasicInfo info = jxl_decoder_get_basic_info(mScanner.get());
JxlFrameInfo frameInfo =
jxl_decoder_get_scanned_frame_info(mScanner.get(), 0);
PostIsAnimated(
FrameTimeout::FromRawMilliseconds(frameInfo.duration_ms));
// num_loops == 0 in jxl means infinite loop, whereas 1 means one total
// play through.
PostLoopCount((info.num_loops == 0 || info.num_loops > INT32_MAX)
? -1
: static_cast<int32_t>(info.num_loops - 1));
}
}
if (HasSize()) {
uint32_t count = jxl_decoder_get_scanned_frame_count(mScanner.get());
if (count >= 1) {
PostFrameCount(count);
}
}
if (HasSize() && !jxl_decoder_has_more_frames(mScanner.get())) {
return LexerResult(TerminalState::SUCCESS);
}
// If the scanner needs more data and the iterator is exhausted with no
// buffered bytes remaining, finalize with what we have.
if (scanStatus == JxlDecoderStatus::NeedMoreData && currentLength == 0 &&
iteratorComplete) {
if (!HasSize()) {
return LexerResult(TerminalState::FAILURE);
}
uint32_t count = jxl_decoder_get_scanned_frame_count(mScanner.get());
PostFrameCount(count > 0 ? count : 1);
return LexerResult(TerminalState::SUCCESS);
}
}
}
// aData and aLength are in/out: on return they point to and describe the
// unconsumed remainder of the input.
nsJXLDecoder::ProcessResult nsJXLDecoder::ProcessAvailableData(
const uint8_t** aData, size_t* aLength) {
while (true) {
JxlDecoderStatus status = ProcessInput(aData, aLength);
if (status == JxlDecoderStatus::Error) {
return ProcessResult::Error;
}
// Basic info may have just become available, regardless of whether
// ProcessInput returned Ok or NeedMoreData (jxl-rs sets it during its
// internal loop and continues). Posting the size and pre-allocating
// the pixel buffer and SurfacePipe before the visible frame header
// arrives is what lets flush_pixels render an LF-frame preview into
// the buffer while we are still waiting on bytes.
if (mDecoderState == DecoderState::Initial) {
JxlBasicInfo basicInfo = jxl_decoder_get_basic_info(mDecoder.get());
if (basicInfo.valid) {
if (basicInfo.width > INT32_MAX || basicInfo.height > INT32_MAX) {
return ProcessResult::Error;
}
PostSize(basicInfo.width, basicInfo.height);
if (basicInfo.has_alpha) {
PostHasTransparency();
}
if (!basicInfo.is_animated) {
PostFrameCount(1);
if (IsMetadataDecode()) {
return ProcessResult::Complete;
}
}
mDecoderState = DecoderState::HaveBasicInfo;
// Allocate the pixel buffer for non-animated images here so that
// FlushPartialFrame can render an LF preview during the LF-frame
// phase. The SurfacePipe (and the surface it exposes) is created
// lazily in FlushPartialFrame / FinishFrame so that no opaque-black
// surface is visible while we wait for the first rendered pixels.
// Animated images don't take the FlushPartialFrame path, so eager
// allocation buys them nothing; each frame's buffer is allocated
// per-frame in HandleFrameOutput.
if (!basicInfo.is_animated) {
if (NS_FAILED(AllocateFrameBuffers())) {
return ProcessResult::Error;
}
}
} else if (status == JxlDecoderStatus::Ok) {
// Ok without basic info shouldn't happen, but if it does, retry
// with the remaining bytes.
if (*aLength == 0) {
return ProcessResult::NeedMoreData;
}
continue;
}
}
if (status == JxlDecoderStatus::NeedMoreData) {
return ProcessResult::NeedMoreData;
}
MOZ_ASSERT(status == JxlDecoderStatus::Ok);
if (mDecoderState == DecoderState::HaveBasicInfo) {
if (jxl_decoder_is_frame_ready(mDecoder.get()) && !HasAnimation()) {
JxlBasicInfo basicInfo = jxl_decoder_get_basic_info(mDecoder.get());
if (basicInfo.is_animated) {
JxlFrameInfo frameInfo = jxl_decoder_get_frame_info(mDecoder.get());
PostIsAnimated(
FrameTimeout::FromRawMilliseconds(frameInfo.duration_ms));
// num_loops == 0 in jxl means infinite loop, whereas 1 means one
// total play through.
PostLoopCount(
(basicInfo.num_loops == 0 || basicInfo.num_loops > INT32_MAX)
? -1
: static_cast<int32_t>(basicInfo.num_loops - 1));
if (IsMetadataDecode()) {
return ProcessResult::Complete;
}
}
}
}
switch (HandleFrameOutput()) {
case FrameOutputResult::BufferAllocated:
case FrameOutputResult::NoOutput:
continue;
case FrameOutputResult::FrameAdvanced:
return ProcessResult::YieldOutput;
case FrameOutputResult::DecodeComplete:
return ProcessResult::Complete;
case FrameOutputResult::Error:
return ProcessResult::Error;
}
MOZ_CRASH("Unhandled FrameOutputResult");
}
}
LexerResult nsJXLDecoder::DrainFrames() {
while (true) {
const uint8_t* noData = nullptr;
size_t noLength = 0;
JxlDecoderStatus status = ProcessInput(&noData, &noLength);
switch (status) {
case JxlDecoderStatus::Ok: {
if (!HasSize()) {
return LexerResult(TerminalState::FAILURE);
}
switch (HandleFrameOutput()) {
case FrameOutputResult::BufferAllocated:
break;
case FrameOutputResult::FrameAdvanced:
return LexerResult(Yield::OUTPUT_AVAILABLE);
case FrameOutputResult::DecodeComplete:
case FrameOutputResult::NoOutput:
return LexerResult(TerminalState::SUCCESS);
case FrameOutputResult::Error:
return LexerResult(TerminalState::FAILURE);
}
break;
}
case JxlDecoderStatus::NeedMoreData:
if (!HasSize()) {
return LexerResult(TerminalState::FAILURE);
}
return LexerResult(TerminalState::SUCCESS);
case JxlDecoderStatus::Error:
return LexerResult(TerminalState::FAILURE);
}
}
}
JxlDecoderStatus nsJXLDecoder::ProcessInput(const uint8_t** aData,
size_t* aLength) {
uint8_t* bufferPtr = mPixelBuffer.empty() ? nullptr : mPixelBuffer.begin();
size_t bufferLen = mPixelBuffer.length();
uint8_t* kPtr = mKBuffer.empty() ? nullptr : mKBuffer.begin();
size_t kLen = mKBuffer.length();
return jxl_decoder_process_data(mDecoder.get(), aData, aLength, bufferPtr,
bufferLen, kPtr, kLen);
}
nsJXLDecoder::FrameOutputResult nsJXLDecoder::HandleFrameOutput() {
bool frameNeedsBuffer = jxl_decoder_is_frame_ready(mDecoder.get());
if (frameNeedsBuffer && mPixelBuffer.empty()) {
if (NS_FAILED(AllocateFrameBuffers())) {
return FrameOutputResult::Error;
}
return FrameOutputResult::BufferAllocated;
}
if (!frameNeedsBuffer && !mPixelBuffer.empty()) {
nsresult rv = FinishFrame();
if (NS_FAILED(rv)) {
return FrameOutputResult::Error;
}
bool hasMoreFrames = jxl_decoder_has_more_frames(mDecoder.get());
if (IsFirstFrameDecode() || !HasAnimation() || !hasMoreFrames) {
PostFrameCount(mFrameIndex + 1);
PostDecodeDone();
return FrameOutputResult::DecodeComplete;
}
mFrameIndex++;
mPixelBuffer.clear();
mKBuffer.clear();
return FrameOutputResult::FrameAdvanced;
}
return FrameOutputResult::NoOutput;
}
nsJXLDecoder::PixelFormat nsJXLDecoder::DetectPixelFormat(
JxlApiDecoder* aDecoder, const JxlBasicInfo& aBasicInfo) {
if (jxl_decoder_use_f16(aDecoder)) {
return PixelFormat::Rgba16f;
}
if (jxl_decoder_is_gray(aDecoder)) {
return aBasicInfo.has_alpha ? PixelFormat::GrayAlpha8 : PixelFormat::Gray8;
}
// Cmyk8 is set when a Black extra channel is present, regardless of CMS,
// so the no-CMS fallback works too.
return jxl_decoder_has_black_channel(aDecoder) ? PixelFormat::Cmyk8
: PixelFormat::Rgba8;
}
nsresult nsJXLDecoder::AllocateFrameBuffers() {
MOZ_ASSERT(HasSize());
OrientedIntSize size = Size();
JxlBasicInfo basicInfo = jxl_decoder_get_basic_info(mDecoder.get());
MOZ_ASSERT(basicInfo.valid);
// Format is constant across all frames; detect once on the first frame.
if (mFrameIndex == 0) {
mPixelFormat.set(DetectPixelFormat(mDecoder.get(), basicInfo));
}
// These buffers are cleared in HandleFrameOutput after each frame is consumed
// and resized here for the next frame. After the first frame the capacity is
// already sufficient, so clear() + resize() is just two integer field updates
// with no memory operations.
CheckedInt<size_t> bufferSize =
CheckedInt<size_t>(size.width) * size.height * BytesPerPixel();
if (!bufferSize.isValid() || !mPixelBuffer.resize(bufferSize.value())) {
MOZ_LOG(sJXLLog, LogLevel::Error,
("[this=%p] nsJXLDecoder::AllocateFrameBuffers -- "
"failed to allocate pixel buffer\n",
this));
return NS_ERROR_FAILURE;
}
if (mPixelFormat.value() == PixelFormat::Cmyk8 &&
!mKBuffer.resize(size_t(size.width) * size.height)) {
return NS_ERROR_FAILURE;
}
// Per-row u8 scratch for all non-passthrough paths (gray, CMYK, HDR).
// Rgba8 passes directly through the pipe; all other formats need conversion.
// Cmyk8 qcms output is RGB8 (3 bytes/pixel) but we allocate 4 for uniformity.
if (mPixelFormat.value() != PixelFormat::Rgba8) {
CheckedInt<size_t> rowBufSize = CheckedInt<size_t>(size.width) * 4;
if (!rowBufSize.isValid() || !mU8RowBuf.resize(rowBufSize.value())) {
return NS_ERROR_FAILURE;
}
}
// Build the qcms transform once on the first frame. The ICC profile and
// color space are constant across all frames of a JXL image.
if (mFrameIndex == 0 && GetCMSOutputProfile() && mCMSMode != CMSMode::Off) {
BuildCMSTransform();
}
return NS_OK;
}
nsresult nsJXLDecoder::EnsureSurfacePipe() {
if (mCurrentPipe) {
return NS_OK;
}
MOZ_ASSERT(HasSize());
OrientedIntSize size = Size();
JxlBasicInfo basicInfo = jxl_decoder_get_basic_info(mDecoder.get());
MOZ_ASSERT(basicInfo.valid);
Maybe<AnimationParams> animParams;
if (HasAnimation()) {
JxlFrameInfo frameInfo = jxl_decoder_get_frame_info(mDecoder.get());
MOZ_ASSERT(frameInfo.frame_duration_valid);
if (!frameInfo.frame_duration_valid) {
return NS_ERROR_FAILURE;
}
animParams.emplace(FullFrame().ToUnknownRect(),
FrameTimeout::FromRawMilliseconds(frameInfo.duration_ms),
mFrameIndex, BlendMethod::SOURCE, DisposalMethod::KEEP);
}
// Cmyk8 with CMS: qcms outputs RGB8 (3 bytes/pixel), pipe handles R8G8B8.
// All other cases: pipe input is R8G8B8A8.
SurfaceFormat inFormat;
SurfaceFormat outFormat;
if (mPixelFormat.value() == PixelFormat::Cmyk8 && mTransform) {
inFormat = SurfaceFormat::R8G8B8;
outFormat = SurfaceFormat::OS_RGBX;
} else {
inFormat = SurfaceFormat::R8G8B8A8;
outFormat =
basicInfo.has_alpha && mPixelFormat.value() != PixelFormat::Cmyk8
? SurfaceFormat::OS_RGBA
: SurfaceFormat::OS_RGBX;
}
// mTransform usage: for Rgba8 it is passed to SurfacePipe for inline CMS;
// for all other formats it is applied per-row in WritePixelRowsToPipe
// (Rgba16f via qcms_transform_data_rgba_f16_to_rgba_u8, gray/CMYK via
// qcms_transform_data). Null when CMS is off, the ICC profile is
// missing/invalid, or the profile color space doesn't match mPixelFormat.
bool usePipeTransform = mPixelFormat.value() == PixelFormat::Rgba8;
qcms_transform* pipeTransform = usePipeTransform ? mTransform : nullptr;
// jxl-rs always outputs straight alpha; the pipe handles premultiplication
// for all formats. CMYK is excluded as its pipe input has no alpha channel.
const bool wantPremultiply =
!(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA);
SurfacePipeFlags pipeFlags = SurfacePipeFlags();
if (wantPremultiply && mPixelFormat.value() != PixelFormat::Cmyk8) {
pipeFlags |= SurfacePipeFlags::PREMULTIPLY_ALPHA;
}
mCurrentPipe = SurfacePipeFactory::CreateSurfacePipe(
this, size, OutputSize(), FullFrame(), inFormat, outFormat, animParams,
pipeTransform, pipeFlags);
if (!mCurrentPipe) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
void nsJXLDecoder::BuildCMSTransform() {
size_t iccLen = 0;
const uint8_t* iccData = jxl_decoder_get_icc_profile(mDecoder.get(), &iccLen);
if (iccData && iccLen) {
mInProfile = qcms_profile_from_memory(
reinterpret_cast<const char*>(iccData), iccLen);
if (mInProfile) {
auto intent = static_cast<qcms_intent>(gfxPlatform::GetRenderingIntent());
if (intent < QCMS_INTENT_MIN || intent > QCMS_INTENT_MAX) {
intent = qcms_profile_get_rendering_intent(mInProfile);
}
uint32_t profileSpace = qcms_profile_get_color_space(mInProfile);
qcms_data_type inType;
qcms_data_type outType;
bool compatible = true;
if (profileSpace == icSigGrayData) {
if (mPixelFormat.value() != PixelFormat::Gray8 &&
mPixelFormat.value() != PixelFormat::GrayAlpha8) {
compatible = false;
}
// jxl-rs outputs Gray8 or GrayAlpha8; qcms produces Rgba8 output.
inType = mPixelFormat.value() == PixelFormat::GrayAlpha8
? QCMS_DATA_GRAYA_8
: QCMS_DATA_GRAY_8;
outType = QCMS_DATA_RGBA_8;
} else if (profileSpace == icSigCmykData) {
if (mPixelFormat.value() != PixelFormat::Cmyk8) {
compatible = false;
}
// jxl-rs outputs C,M,Y,_ in Rgba8 positions; K in mKBuffer.
// qcms expects CMYK (0=no ink) and produces RGB8 output.
inType = QCMS_DATA_CMYK;
outType = QCMS_DATA_RGB_8;
} else {
if (mPixelFormat.value() != PixelFormat::Rgba8 &&
mPixelFormat.value() != PixelFormat::Rgba16f) {
compatible = false;
}
inType = QCMS_DATA_RGBA_8;
outType = QCMS_DATA_RGBA_8;
}
if (compatible) {
mTransform = qcms_transform_create(
mInProfile, inType, GetCMSOutputProfile(), outType, intent);
}
}
}
}
// IEEE 754 half-float to float, used for HDR fallback when no CMS transform.
static float F16ToF32(uint16_t h) {
uint32_t sign = (h >> 15) & 1u;
uint32_t exp = (h >> 10) & 0x1fu;
uint32_t mantissa = h & 0x3ffu;
uint32_t f;
if (exp == 0) {
f = sign << 31; // zero (subnormals treated as zero)
} else if (exp == 31) {
f = (sign << 31) | (0xffu << 23) | (mantissa << 13); // inf / nan
} else {
f = (sign << 31) | ((exp + 127u - 15u) << 23) | (mantissa << 13);
}
float result;
memcpy(&result, &f, sizeof(result));
return result;
}
bool nsJXLDecoder::WritePixelRowsToPipe() {
MOZ_ASSERT(mCurrentPipe);
#ifdef DEBUG
++mWritePixelRowsCount;
#endif
OrientedIntSize size = Size();
uint8_t* currentRow = mPixelBuffer.begin();
for (int32_t y = 0; y < size.height; ++y) {
uint8_t* pipeInput;
if (mPixelFormat.value() == PixelFormat::Rgba16f) {
if (mTransform) {
qcms_transform_data_rgba_f16_to_rgba_u8(
mTransform, reinterpret_cast<const uint16_t*>(currentRow),
mU8RowBuf.begin(), size.width);
} else {
// No CMS: clip f16 to [0,1].
const uint16_t* src = reinterpret_cast<const uint16_t*>(currentRow);
for (int32_t i = 0; i < size.width * 4; ++i) {
float v = F16ToF32(src[i]);
mU8RowBuf[i] =
v <= 0.0f ? 0 : (v >= 1.0f ? 255 : uint8_t(v * 255.0f + 0.5f));
}
}
pipeInput = mU8RowBuf.begin();
} else if (mPixelFormat.value() == PixelFormat::Gray8 ||
mPixelFormat.value() == PixelFormat::GrayAlpha8) {
if (mTransform) {
// qcms reads the packed Gray8/GrayAlpha8 and produces Rgba8 output.
qcms_transform_data(mTransform, currentRow, mU8RowBuf.begin(),
size.width);
} else {
// No CMS: expand gray → Rgba8 without color management.
uint8_t* out = mU8RowBuf.begin();
for (int32_t x = 0; x < size.width; ++x) {
uint8_t g = currentRow[x * BytesPerPixel()];
uint8_t a = mPixelFormat.value() == PixelFormat::GrayAlpha8
? currentRow[x * BytesPerPixel() + 1]
: 255;
out[x * 4] = g;
out[x * 4 + 1] = g;
out[x * 4 + 2] = g;
out[x * 4 + 3] = a;
}
}
pipeInput = mU8RowBuf.begin();
} else if (mPixelFormat.value() == PixelFormat::Cmyk8) {
uint8_t* out = mU8RowBuf.begin();
const uint8_t* kRow =
mKBuffer.empty() ? nullptr : mKBuffer.begin() + y * size.width;
if (mTransform) {
// JXL CMYK: all channels use 0=max-ink, 255=no-ink; qcms uses 0=no-ink,
// so invert all. qcms produces RGB8 (3 bytes/pixel); pipe was
// configured with R8G8B8 inFormat.
for (int32_t x = 0; x < size.width; ++x) {
out[x * 4] = 255 - currentRow[x * 4];
out[x * 4 + 1] = 255 - currentRow[x * 4 + 1];
out[x * 4 + 2] = 255 - currentRow[x * 4 + 2];
// Alpha from the pixel buffer is unused; K fills the 4th CMYK slot.
out[x * 4 + 3] = kRow ? (255 - kRow[x]) : 0;
}
qcms_transform_data(mTransform, out, out, size.width);
} else {
// No CMS: naive CMY+K → RGB without color management.
// JXL encodes 0=max-ink, 255=no-ink, so R = C*K/255 gives correct
// luminance.
for (int32_t x = 0; x < size.width; ++x) {
uint8_t k = kRow ? kRow[x] : 255;
out[x * 4] = (uint16_t)currentRow[x * 4] * k / 255;
out[x * 4 + 1] = (uint16_t)currentRow[x * 4 + 1] * k / 255;
out[x * 4 + 2] = (uint16_t)currentRow[x * 4 + 2] * k / 255;
out[x * 4 + 3] = 255;
}
}
pipeInput = out;
} else {
pipeInput = currentRow;
}
if (mCurrentPipe->WriteBuffer(reinterpret_cast<uint32_t*>(pipeInput)) ==
WriteState::FAILURE) {
return false;
}
currentRow += size.width * BytesPerPixel();
}
return true;
}
nsresult nsJXLDecoder::FinishFrame() {
MOZ_ASSERT(HasSize());
MOZ_ASSERT(mDecoder);
nsresult rv = EnsureSurfacePipe();
if (NS_FAILED(rv)) {
return rv;
}
JxlBasicInfo basicInfo = jxl_decoder_get_basic_info(mDecoder.get());
mCurrentPipe->ResetToFirstRow();
if (!WritePixelRowsToPipe()) {
mCurrentPipe.reset();
return NS_ERROR_FAILURE;
}
if (Maybe<SurfaceInvalidRect> invalidRect = mCurrentPipe->TakeInvalidRect()) {
PostInvalidation(invalidRect->mInputSpaceRect,
Some(invalidRect->mOutputSpaceRect));
}
// Cmyk8 images are always opaque: alpha is not decoded (see
// WritePixelRowsToPipe).
bool hasTransparency =
basicInfo.has_alpha && mPixelFormat.value() != PixelFormat::Cmyk8;
PostFrameStop(hasTransparency ? Opacity::SOME_TRANSPARENCY
: Opacity::FULLY_OPAQUE);
mCurrentPipe.reset();
return NS_OK;
}
void nsJXLDecoder::FlushPartialFrame() {
MOZ_ASSERT(!mPixelBuffer.empty());
JxlDecoderStatus status = jxl_decoder_flush_pixels(
mDecoder.get(), mPixelBuffer.begin(), mPixelBuffer.length(),
mKBuffer.empty() ? nullptr : mKBuffer.begin(), mKBuffer.length());
if (status != JxlDecoderStatus::Ok) {
// Nothing new was rendered.
return;
}
// Lazily create the SurfacePipe now that we have content for it. Doing
// this before any pixels are ready would expose an opaque-black surface
// for images without an alpha channel.
if (NS_FAILED(EnsureSurfacePipe())) {
MOZ_LOG(sJXLLog, LogLevel::Error,
("[this=%p] nsJXLDecoder::FlushPartialFrame -- "
"EnsureSurfacePipe failed\n",
this));
return;
}
mCurrentPipe->ResetToFirstRow();
WritePixelRowsToPipe();
if (Maybe<SurfaceInvalidRect> invalidRect = mCurrentPipe->TakeInvalidRect()) {
PostInvalidation(invalidRect->mInputSpaceRect,
Some(invalidRect->mOutputSpaceRect));
}
}
} // namespace mozilla::image