When decoding jxl-rs first gives us the basic info from the file header, this is enough to allocate the buffers, get the size, and pixel format. And then jxl-rs will later make available the frame header; it seems as though jxl-rs will only make it available relatively late, after parsing a decent chunk of the frame data. Animated images do need to wait for the frame header to get the frame duration before they can decode. But non-animated images can get useful pixel data as soon as we have basic info. We were waiting until we had a frame header (jxl_decoder_is_frame_ready which returns true once image/rust/jxl/src/decoder.rs observes the frame_header) for all jxl images, but we can get useful pixel data via flush_pixels much sooner than that. The page https://random-stuff.jakearchibald.com/apps/partial-img-decode/?demo=fox-progressive.jxl&density=2 probably shows the biggest improvement this change can make. Before this change we would show the first image at around 55k. After this change we show an image around 2.5k. We split BeginFrame into AllocateFrameBuffers and EnsureSurfacePipe so that we can call AllocateFrameBuffers as soon as we observe basic info, but not pass that data to the surface pipe until a flush_pixels call returns true, at which point we can create the surface pipe and start making that pixel data user visible. These functions now return nsresult instead of FrameOutputResult (which seemed more confusing to me). ProcessAvailableData gets a refactor that I think makes it a bit easier to follow. (I'd like to do more refactoring: these processing loops seem harder to follow than necessary.) This also includes a refactoring of the main loop in process_data in decoder.rs. Most of the code we were executing for ProcessingResult::Complete should also happen for ProcessingResult::NeedsMoreInput: just because we need more data does not mean that we can't look for the basic info. As part of this change I re-did all of our progressive image tests (one mochitest, one wpt reftest, some gtests) to better test this feature. The gtest changes are a significant improvement and I'm still working them into landable state, but the wpt and mochitest should be good coverage to land this. Differential Revision: https://phabricator.services.mozilla.com/D298797
159 lines
4.8 KiB
C++
159 lines
4.8 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/. */
|
|
|
|
#ifndef mozilla_image_decoders_nsJXLDecoder_h
|
|
#define mozilla_image_decoders_nsJXLDecoder_h
|
|
|
|
#include "Decoder.h"
|
|
#include "SurfacePipe.h"
|
|
#include "mozilla/Vector.h"
|
|
#include "mozilla/image/jxl_decoder_ffi.h"
|
|
|
|
namespace mozilla::image {
|
|
|
|
struct JxlDecoderDeleter {
|
|
void operator()(JxlApiDecoder* ptr) { jxl_decoder_destroy(ptr); }
|
|
};
|
|
|
|
class nsJXLDecoder final : public Decoder {
|
|
public:
|
|
~nsJXLDecoder() override;
|
|
|
|
DecoderType GetType() const override { return DecoderType::JXL; }
|
|
|
|
#ifdef DEBUG
|
|
// Debug-only (for tests): number of times WritePixelRowsToPipe has run on
|
|
// this decoder. Counts both partial flushes and the final frame write.
|
|
uint32_t GetWritePixelRowsCount() const { return mWritePixelRowsCount; }
|
|
#endif
|
|
|
|
protected:
|
|
nsresult InitInternal() override;
|
|
LexerResult DoDecode(SourceBufferIterator& aIterator,
|
|
IResumable* aOnResume) override;
|
|
|
|
private:
|
|
friend class DecoderFactory;
|
|
|
|
explicit nsJXLDecoder(RasterImage* aImage);
|
|
|
|
enum class DecoderState { Initial, HaveBasicInfo };
|
|
|
|
// jxl-rs output pixel format. Determines buffer stride, the conversion path
|
|
// in WritePixelRowsToPipe, and the SurfacePipe input format.
|
|
enum class PixelFormat {
|
|
Rgba8, // u8 R,G,B,A (A=255 when no alpha channel)
|
|
Gray8, // u8 G
|
|
GrayAlpha8, // u8 G,A
|
|
Cmyk8, // u8 C,M,Y,_ (4th byte unused); u8 K in mKBuffer
|
|
Rgba16f, // f16 R,G,B,A, native-endian; HDR images when CMS was
|
|
// requested (mTransform may still be null if transform
|
|
// creation failed)
|
|
};
|
|
|
|
enum class FrameOutputResult {
|
|
BufferAllocated,
|
|
FrameAdvanced,
|
|
DecodeComplete,
|
|
NoOutput,
|
|
Error
|
|
};
|
|
|
|
enum class ProcessResult { NeedMoreData, YieldOutput, Complete, Error };
|
|
|
|
JxlDecoderStatus ProcessInput(const uint8_t** aData, size_t* aLength);
|
|
FrameOutputResult HandleFrameOutput();
|
|
/// @aData and @aLength are in/out: on return they point to and describe the
|
|
/// unconsumed remainder of the input.
|
|
ProcessResult ProcessAvailableData(const uint8_t** aData, size_t* aLength);
|
|
|
|
LexerResult ScanForFrameCount(SourceBufferIterator& aIterator,
|
|
IResumable* aOnResume);
|
|
|
|
static PixelFormat DetectPixelFormat(JxlApiDecoder* aDecoder,
|
|
const JxlBasicInfo& aBasicInfo);
|
|
// Allocate the pixel / K / scratch buffers, detect the output pixel
|
|
// format, and perform any CMS setup needed for this image. The SurfacePipe
|
|
// (and the surface it exposes) is deferred to EnsureSurfacePipe until we
|
|
// actually have pixels to output.
|
|
nsresult AllocateFrameBuffers();
|
|
// Create mCurrentPipe if it doesn't already exist. Idempotent.
|
|
// Preconditions: AllocateFrameBuffers has already run (mPixelFormat /
|
|
// mTransform are read here); for animated images the caller must also
|
|
// have observed frame_ready so that AnimationParams is available.
|
|
nsresult EnsureSurfacePipe();
|
|
void BuildCMSTransform();
|
|
nsresult FinishFrame();
|
|
void FlushPartialFrame();
|
|
bool WritePixelRowsToPipe();
|
|
|
|
LexerResult DrainFrames();
|
|
|
|
size_t BytesPerPixel() const {
|
|
switch (mPixelFormat.value()) {
|
|
case PixelFormat::Rgba8:
|
|
return 4;
|
|
case PixelFormat::Gray8:
|
|
return 1;
|
|
case PixelFormat::GrayAlpha8:
|
|
return 2;
|
|
case PixelFormat::Cmyk8:
|
|
return 4;
|
|
case PixelFormat::Rgba16f:
|
|
return 8;
|
|
}
|
|
MOZ_ASSERT_UNREACHABLE("unhandled PixelFormat");
|
|
return 4;
|
|
}
|
|
|
|
std::unique_ptr<JxlApiDecoder, JxlDecoderDeleter> mDecoder;
|
|
std::unique_ptr<JxlApiDecoder, JxlDecoderDeleter> mScanner;
|
|
|
|
DecoderState mDecoderState = DecoderState::Initial;
|
|
|
|
uint32_t mFrameIndex = 0;
|
|
|
|
// Field wrapper that asserts on read before first write and asserts on any
|
|
// write after the first.
|
|
template <typename T>
|
|
class WriteOnce {
|
|
public:
|
|
T value() const {
|
|
MOZ_ASSERT(mIsSet);
|
|
return mValue;
|
|
}
|
|
void set(T aVal) {
|
|
MOZ_ASSERT(!mIsSet);
|
|
mIsSet = true;
|
|
mValue = aVal;
|
|
}
|
|
|
|
private:
|
|
bool mIsSet = false;
|
|
T mValue{};
|
|
};
|
|
|
|
WriteOnce<PixelFormat> mPixelFormat;
|
|
|
|
// Per-row u8 output buffer for manual CMS paths (HDR, gray, CMYK).
|
|
Vector<uint8_t> mU8RowBuf;
|
|
|
|
// Full-frame decoded pixel buffer; allocated in AllocateFrameBuffers, sized
|
|
// width * height * BytesPerPixel(). Passed to jxl-rs as the output buffer.
|
|
Vector<uint8_t> mPixelBuffer;
|
|
Vector<uint8_t> mKBuffer; // K (Black) channel, 1 byte/pixel, for CMYK images
|
|
Maybe<SurfacePipe> mCurrentPipe;
|
|
|
|
bool mIteratorComplete = false;
|
|
|
|
#ifdef DEBUG
|
|
uint32_t mWritePixelRowsCount = 0;
|
|
#endif
|
|
};
|
|
|
|
} // namespace mozilla::image
|
|
|
|
#endif // mozilla_image_decoders_nsJXLDecoder_h
|