Some WebP files advertise ICC metadata in their VP8X feature flags even
though the ICCP chunk is absent or malformed. We accepted the bitstream
header and could decode the pixels, but the follow-up mux metadata
lookup failed during header decoding and made sniffing reject the image
entirely.
Treat mux and ICC metadata extraction as best-effort after libwebp
accepts the header. Images without readable optional ICC metadata now
decode normally and simply report no ICC profile.
Previously, font selection ignored the Unicode emoji presentation of a
code point. Emoji-capable code points were always resolved through
pre-baked color emoji and symbol fonts. Text-default code points which
lacked the `Emoji_Presentation` property, were therefore rendered as
color emoji rather than text. We now classify each code point's default
presentation from its `Emoji_Presentation` property and any trailing
variation selector.
Problem: A borked ImageDecoder could send a BitmapSequence over IPC with
metadata for a (large) bitmap while shipping a too-small backing buffer.
Decoding it produced a Gfx::Bitmap that reported the (large) geometry
but pointed at the too-small buffer — making the first write go OOB.
Cause: BitmapSequence decode reads size_in_bytes and the bitmap geometry
as independent fields, and only checked if size_in_bytes matched the
transferred buffer size — never that either is consistent with the
geometry. The single-frame fast path then handed the buffer to
Bitmap::create_with_anonymous_buffer with no verification.
Fix: Make the two bitmap factories that take externally-provided storage
enforce that it covers the geometry. create_with_anonymous_buffer now
fails with buffers smaller than the minimum expected size_in_bytes — and
create_with_raw_data similarly rejects data too small for the geometry.
Fixes https://github.com/LadybirdBrowser/ladybird/issues/10036
Problem: A borked process sending a Gfx::Bitmap (inside BitmapSequence)
or a Gfx::ShareableBitmap over IPC could set BitmapFormat::Invalid as
the format field. The receiving process then aborted while decoding the
message — an IPC-reachable crash.
Cause: The helper that both decoders use for validating the format read
off the wire accepts BitmapFormat::Invalid. The decoders go on to build
a bitmap with that format. But that triggers an assert — because
minimum_pitch only knows the four real pixel formats.
Fix: Drop BitmapFormat::Invalid from is_valid_bitmap_format. It’s an
indicator of an absent/unknown format, never one a real bitmap can have.
And any real bitmap is never encoded with it. So, both BitmapSequence
and ShareableBitmap decode now return a clean decode error for it.
2D canvas contexts started publishing partial frames after canvas
rasterization moved into the Compositor. WebContent still splits large
recorded command lists after 64 commands, but every split batch was sent
through the same compositor path as the end-of-frame flush. The
compositor replayed each batch into the DrawCanvas source surface,
so a pending present could sample a canvas after clear and before the
rest of the next frame had been drawn. Canvas-heavy pages such as
slither.com then flickered between partial and complete frames.
Carry an explicit commit bit with 2D canvas command updates. Non-commit
batches now update a hidden working canvas in the Compositor, while the
display-list-visible surface keeps the last committed canvas contents.
The end-of-frame canvas preparation sends the commit boundary, including
the empty-commit case needed when the auto-flush consumed all recorded
commands before prepare_for_compositing() runs.
Selecting text without custom ::selection styling changed the
foreground color of the selected content. This was especially visible
for links, where the text changed color but the underline did not.
The default selection style supplied both a selection background and a
foreground color from the palette or HighlightText system color. That
made ordinary selections behave as if the page had explicitly styled
::selection color.
Only provide a default selection background, so selected content keeps
its own foreground color unless CSS overrides it. Remove the now-unused
SelectionText palette role.
DisplayListPlayerSkia kept a separate DecodedImageFrameSkiaImageCache
that was pruned during flushes. That made Skia image lifetime
independent of display list resource lifetime, even though resource
storage is what knows when image frames and compositor surfaces are no
longer needed.
Make DisplayListResourceStorage own an opaque stored image-frame
resource that holds the decoded frame and its lazily-created SkImage.
Removing image frames or compositor surfaces now drops the decoded frame
and Skia image together, while transactions still carry only Skia-free
decoded frames.
2D canvas rendering now lives in the compositor, but drawing one canvas
into another still converted the source HTMLCanvasElement into a
DecodedImageFrame in WebContent. That forced a compositor readback for
every drawImage(canvas, ...) call before sending the destination canvas
commands back to the compositor.
Teach the canvas command stream to carry a DrawCanvas command that names
the source canvas surface. The destination 2D context now flushes the
source canvas, records that command, and immediately flushes the
destination command list so the compositor copies the source surface at
the drawImage call boundary. Bitmap sources continue to use DrawBitmap,
and true readback APIs still read pixels explicitly.
The decoded-frame Skia image cache is useful for display-list
rasterization because decoded image resources can be replayed over many
frames. The cache lets that path reuse SkImage wrappers and GPU-backed
copies instead of rebuilding them whenever the same resource is painted.
For canvas, commands are consumed into one backing surface and decoded
frames are already held by the command or paint style for the draw. A
per-painter cache does not match that usage model, and can keep decoded
frames and Skia images alive after the draw has consumed them.
This removes the cache from PainterSkia and drops the now-unused pruning
hook from CanvasCommandPlayer. With the cache gone, PainterSkia can hold
its painting surface directly instead of allocating a private Impl.
DisplayListPlayerSkia keeps owning the cache, so display-list
rasterization keeps the SkImage reuse behavior.
Problem: A BMP V5 image whose embedded ICC profile offset points past
the end of the file triggers an OOB read.
Cause: The bounds check summed the profile offset, the file-header size,
and the profile size in 32-bit arithmetic. So, a large offset (e.g.
0xfffffff0) wraps the sum back into range and passes the check. The
decoder then returns a span pointing far past the end of the file.
Fix: Compute the sum in 64 bits — so an out-of-bounds offset can no
longer wrap, and the profile’s rejected.
Fixes: https://github.com/LadybirdBrowser/ladybird/issues/9967
Problem: Decoding a BMP whose height is INT_MIN triggered a UBSan error.
A top-down BMP legitimately uses a negative height — and unlike width,
it’s not rejected, so it can be INT_MIN.
Cause: decode_bmp_pixel_data() took the magnitude of the i32 width and
height with abs(). Negating INT_MIN is undefined behavior.
Fix: Widen to i64 before taking the absolute value — so the magnitude of
INT_MIN is representable. The resulting out-of-range dimension is still
rejected by Bitmap::create — so only the undefined behavior changes.
Fixes: https://github.com/LadybirdBrowser/ladybird/issues/9994
Problem: Decoding a non-square image whose Exif orientation is 5
(transpose) writes one pixel past the end of the destination bitmap —
an ASan heap-buffer-overflow in ExifOrientedBitmap::set_pixel.
Cause: oriented_position() mapped orientation 5 by composing the
“flip-horizontally” and “rotate-90-clockwise” helpers. Each helper
mirrors using the source width. But after the rotate, the point is
already in the transposed coordinate space — where the relevant
dimension is the source height. For a non-square image, that composition
produces x-coordinates past the destination width. Only square images
happened to stay in bounds. The destination bitmap is the transposed
size — so the out-of-range column wrote past its allocation.
Fix: Map orientation 5 directly as a transpose across the main diagonal:
source (x, y) to destination (y, x).
Fixes https://github.com/LadybirdBrowser/ladybird/issues/10102
With Windows backend selection fixed to Direct3D, LibGfx needs a native
GPU context that can feed Skia directly. Add a Direct3DContext helper
that owns the DXGI adapter, D3D12 device, and direct command queue, then
pass that state to Skia through GrDirectContext::MakeDirect3D..
After 2D canvas stops using Gfx::Painter as its drawing backend, the
base Painter interface only needs to describe bitmap compositing work
that still happens outside PainterSkia. Narrow the interface to
clear_rect, fill_rect and draw_bitmap, which covers GIF/APNG frame
compositing, CSS cursor bitmap painting, canvas readback and the Android
UI blit.
Move CanvasRenderingContext2D onto the same record-and-replay model that
the compositor-process path will use, but keep playback local for now.
Draw calls append CanvasCommandList entries and flush/readback paths
replay the commands into a local CanvasCommandPlayer-owned surface.
Once canvas commands can be sent to another process, the receiver needs
an endpoint that owns the persistent surface and validates command data
before it reaches Skia. Add CanvasCommandPlayer for that role.
The player replays CanvasCommandList deltas through concrete PainterSkia
APIs and keeps painter state across play() calls, matching the way a
compositor-hosted canvas surface will accumulate mutations over time.
Initialize ops allocate or resize the backing surface so creation,
resize and repaint all flow through the same command stream.
Moving 2D canvas rasterization into the Compositor process needs a wire
format for canvas mutations that does not depend on LibWeb state. Add
CanvasCommandList as an apply-once log of drawing operations whose
operands are Gfx value types and whose IPC encoders can be shared by
WebContent and the compositor side.
Keep the Linux DMABUF handle alongside the bitmap wrapper when imported
shared images reach the UI process. This lets consumers import the same
GPU backing store directly instead of only reading it through the mapped
bitmap.
Also require exported Vulkan shared images to be sampleable, since the
Qt Vulkan presentation path needs to sample compositor backing stores.
Replace the homegrown GIF parser and LZW decompressor with the wuffs
GIF decoder, which is memory-safe by construction and already used in
other engines via Skia.
One behavior change is that `loop_count()` now reports the correct
value, since the raw value stored in the file does not include the
first frane and should be incremented by 1 to be compatible with what
callers expect.
Move the image loader sources and decoder-only dependencies from LibGfx
into a new LibImageDecoders library. This keeps the APNG-enabled PNG
loader out of processes that only need core graphics and image writers.
Link the ImageDecoder service, direct decoder tests, fuzzers, test-web,
and the image utility against LibImageDecoders where they still decode
images in-process.
The shaping cache previously stored HarfBuzz buffers keyed by string,
and `shape_text()` rebuilt a fresh `GlyphRun` on every call. Cache the
font-independent shape data instead, so repeated shaping of the same
input skips both the HarfBuzz call and the glyph-vector build.
We're going to implement the `contrast-color()` CSS function in the next
commit, and the spec advises to use the contrast ratio definition as
described in WCAG 2. So let's replace our `Color::contrast_ratio()`
implementation by that.
Co-authored-by: InvalidUsernameException
<InvalidUsernameException@users.noreply.github.com>
Problem: Decoding a BI_RLE24 BMP binds a u32 reference to a misaligned
address, gets flagged by UBSan while decompressing the run-length data.
Cause: The decompressed RLE24 buffer holds 24-bit pixels at a 3-byte
stride (decode_bmp_pixel_data reads it back with LE read_u24) — but each
pixel was getting stored with a 4-byte write at 3-byte-strided offsets.
Fix: Store the 24-bit value as three LE bytes — so the write
matches the stride and is always aligned. Size the buffer to the
real 3-bytes-per-pixel total, and bound-check the 3-byte write.
Fixes https://github.com/LadybirdBrowser/ladybird/issues/9958
Clamp the CSS pixel size used for HarfBuzz font scales before passing
it to the int-sized HarfBuzz API. Also accumulate shaped advances
in double precision while measuring text. Very large web-provided font
sizes can otherwise overflow before layout clamps dimensions.
Add a reduced crash test for laying out text with a huge font size.
Enable -Wexit-time-destructors for all in-tree library targets and
update process-lifetime library statics so they no longer register
exit-time destructors. Long-lived caches, lookup tables, singleton
registries, and generated constants now use NeverDestroyed or leaked
references where the data is intended to live until process exit.
Update LibWeb, LibLine, and the binding generators so regenerated
sources follow the same rule instead of reintroducing destructed
statics.
Remove the TinyVG decoder now that the Qt chrome no longer depends on
TVG resources. Drop the decoder registration, MIME and supported image
type entries, fuzzer target, decoder tests, and TinyVG test inputs.
By default, `rustfmt` persists the import granularity. In practice, most
Rust code has import granularity "Module" due to LSP's actions.
"Item" gets rid of import groupings and achieves cleaner diffs and
better conflict resolution. Better greppability is a positive side
effect.
Note: it's an unstable rustfmt feature. `cargo +nightly fmt` must be
used instead of `cargo fmt`.
When choosing the best image from an ICO file, prefer the entry with the
largest pixel area first, and only use bits-per-pixel as a tie-breaker.
This had regressed in commit b10fe7c136.
This fixes ICO files like Discord's favicon, where 16x16, 32x32, 48x48,
and 256x256 entries all advertise the same bpp. We were previously just
choosing whichever we saw first, which happened to be the 16x16 icon.
DecodedImageFrameSkiaImageCache used to treat each display-list
flush as a cache generation. That worked poorly for the compositor,
where all contexts share one DisplayListPlayerSkia and therefore one
decoded-frame cache. A busy context could advance the generation
counter enough to evict images from another context even when the
shared cache was still comfortably under its entry and byte limits.
Track recency from actual image-cache hits and inserts instead, and
evict the least-recently used entries only when the existing global
entry or byte caps are exceeded. This keeps cross-context reuse while
making eviction respond to memory pressure rather than unrelated
presentation churn.
Prune the decoded-image cache before handing the surface to the shared
SkiaBackendContext flush cleanup, so any SkImage references released
by the cache are visible to Skia's resource cleanup in the same flush
path.
Keep CoreText system UI fonts backed by their CoreGraphics font instead
of opening the Skia typeface stream during font matching. Opening that
stream copies the full macOS system font data into SkData, which is very
expensive when CSS font computation asks for several system UI faces.
Let Typeface subclasses provide the HarfBuzz face creation path, and use
HarfBuzz's CoreText integration for these macOS system fonts. Clone Skia
font variations through SkTypeface::makeClone() so variation handling no
longer depends on rebuilding a typeface from the raw font stream.
The compositor present path submitted Skia work with a synchronous GPU
wait, so each present_frame call stayed blocked until the backend
finished the submitted work. That serialized compositor processing
behind GPU completion even though the presented-bitmap acknowledgement
already controls backing-store reuse.
Add an async Skia flush-and-submit entrypoint and have the compositor
track pending presents until the Skia finished callback runs back on the
compositor event loop. did_present_frame now fires from that completion
path, while the existing presented_bitmap_ready_to_paint acknowledgement
remains the reuse gate for client-presented bitmaps.
Ganesh does not reliably run the finished proc without explicit async
work polling on this backend, so pending presents keep a completion
timer alive to call checkAsyncWorkCompletion().
Problem: Runs of the Linux Sanitizers intermittently fail with
LeakSanitizer leaks of ~45 KB across 21 allocations from
PNGLoadingContext::read_frames() — triggered when the test suite
decodes malformed PNGs.
Cause: When libpng hits a corrupted IDAT chunk inside png_read_image(),
it longjmps back to the setjmp landing pad in
PNGLoadingContext::read_all_frames(). longjmp unwinds the stack without
running C++ destructors — so the stack-locals in read_frames
(Vector<u8*> row_pointers, the in-flight Bitmap inside decode_frame, and
the APNG branch’s output buffer and Painter) leak their heap storage.
Fix: Promote those stack-locals to members of PNGLoadingContext (which
is heap-allocated and outlives the setjmp scope) — so their storage is
reachable by RAII when the context is destroyed. Clear them in the
setjmp error handler too — so memory is released promptly on the error
path, rather than waiting until ~PNGLoadingContext().
BitmapSequence IPC decodes every frame from the collated transport
buffer into a new malloc-backed Bitmap. That keeps animated sequences
compact, but it also breaks the shared-memory chain for the common
single-frame image path. WebContent receives anonymous buffer data from
ImageDecoder, then has to allocate another anonymous buffer when the
image is sent to the Compositor.
Preserve the backing for single-frame sequences by wrapping the received
AnonymousBuffer directly in the decoded Bitmap after validating that it
exactly matches the frame metadata. That lets the decoded image keep its
shared-memory backing all the way through ImageDecoder -> WebContent ->
Compositor without another allocation and copy.
The compositor thread had local Skia resource cache cleanup in its
surface flush helper, but the compositor process uses the same backend
through its own paint and present path. That left the new process path
without the deferred and high-watermark purging policy.
Move the cleanup into SkiaBackendContext::flush_and_submit(), keeping
the public flush API shared while letting the Vulkan and Metal backends
provide only the backend-specific submit implementation. This keeps the
existing compositor thread behavior and applies the same cache pressure
handling to compositor process flushes.
Display-list resource transactions need to carry fonts across an IPC
boundary without making LibWeb know how each typeface stores its bytes.
Add a LibGfx-owned Typeface IPC representation so callers can encode
and decode typefaces directly.
Anonymous-buffer and Core::Resource-backed typefaces serialize through
their retained backing. System typefaces serialize as family and style
data, with macOS system UI typefaces carrying their SystemUIFontKind
from creation so the receiver can rematch them through CoreText.
This is preparatory work required to add IPC between the main and
compositor threads.
Compositor video frame transport should not flatten frames into a
shareable bitmap before crossing an IPC boundary. That conversion loses
the native YUV representation and makes Web-side transport code own the
frame wire format.
Teach VideoFrame to encode its YUV planes into a shared anonymous buffer
with the color space, timing, subsampling, bit depth, and CICP metadata
needed to rebuild the frame on decode. Add YUVData helpers for checked
plane sizing and construction from validated plane bytes so malformed
buffers are rejected before a frame is created.
This is preparatory work required to add IPC between the main and
compositor threads.
Font transport needs shape feature tags and values to cross the
compositor IPC boundary with their owning LibGfx representation. Add a
ShapeFeature specialization in its own source file and encode the fixed
tag bytes together with the feature value.
This is preparatory work required to add IPC between the main and
compositor threads.
The compositor IPC path needs to move vector paths without teaching Web
code about the path wire format. Declare the Path specialization on the
owning LibGfx type, then reuse the existing path byte serialization for
IPC encode and decode.
This is preparatory work required to add IPC between the main and
compositor threads.
The compositor IPC path transports transform matrices as LibGfx values,
so the matrix type should own its byte representation. Add the
FloatMatrix4x4 specialization in a new LibGfx source file and serialize
the fixed-size float array directly through the IPC stream.
This is preparatory work required to add IPC between the main and
compositor threads.
The compositor IPC path needs variable-font axes to survive transport
with the font data that owns them. Add LibGfx-local specializations for
FontVariationSettings, compile the new implementation into LibGfx, and
expose the stored settings from Font so later font serializers can read
them without reaching into the class internals.
This is preparatory work required to add IPC between the main and
compositor threads.