Compare commits

...
39 Commits
Author SHA1 Message Date
Moonchild f720a9105c Merge pull request 'Optimize Array.prototype.slice species lookup' (#3220) from array-slice-sm into master
Reviewed-on: https://repo.palemoon.org/MoonchildProductions/UXP/pulls/3220
2026-11-12 06:44:47 +00:00
Moonchild 6e8ae471d4 Merge pull request 'Narrow :has() sibling invalidation' (#3221) from Basilisk-Dev/UXP-contrib:issue-3176-has-invalidation into master
Reviewed-on: https://repo.palemoon.org/MoonchildProductions/UXP/pulls/3221
2026-11-12 06:44:32 +00:00
Moonchild b2e56a1c3d Merge pull request 'Upgrade our libjxl to 0.13.0, brotli to 1.2.1, and fix highway build bustage in PPC8+' (#3223) from jobbautista9/UXP:3222-libjxl-0.13.0 into master
Reviewed-on: https://repo.palemoon.org/MoonchildProductions/UXP/pulls/3223
2026-11-12 06:44:09 +00:00
Job Bautista ac578bb1dc Issue #3224 - Update in-tree Brotli lib to 1.2.1.
Our local patch for commenting out the static void check has also
been separated out cleanly for this upgrade, so future updates via
the shell script should automatically apply it now.
2026-11-12 13:50:22 +08:00
Job Bautista fb681d9ff8 Issue #3201 Part 2 Follow-up: Export ops for ppc_vsx 2026-11-12 13:05:37 +08:00
Job Bautista b033b1e37f Issue #3222 - Part 2: Document libjxl 0.13.0 upgrade. 2026-11-12 13:05:12 +08:00
Job Bautista c95cc9e27a Issue #3222 - Part 1: Import libjxl 0.13.0. 2026-11-12 13:04:33 +08:00
Basilisk-Dev 8622d16d32 Issue #3176 - Narrow :has() sibling invalidation
Track the containing child for deep mutations and avoid restyling sibling anchors that occur after the changed subtree. Apply the ordering check to sibling-relative selectors that continue into descendants as well as local compounds.
2026-11-10 16:47:00 -05:00
Brian Smith 47608bd7fd Issue #2501 - Part 1: Minimal support for parsing CSS container queries.
This is not really functional, it just supports parsing and enough support to fix Google results.
2026-11-10 22:28:12 +01:00
Brian Smith 37f10da2f8 Issue #2404 - Follow-up: Aspect ratio and the related @media query for it only supported integer values.
This adds support for float ratios.
2026-11-10 22:25:38 +01:00
Brian Smith 0b8dad69d2 Issue #3156 - Fix bottom padding being ignored for scrollable boxes.
https://bugzilla.mozilla.org/show_bug.cgi?id=1527949
2026-11-10 22:25:28 +01:00
Brian Smith e5b9f4771f Issue #3128 - Missing TYPE_FLAG_BIGINT may cause GC marking to skip BigInt objects. 2026-11-10 22:25:11 +01:00
Brian Smith 2941f11082 Issue #3155 - Follow-up: Fix some similar axis related calculation problems. 2026-11-10 22:24:48 +01:00
Brian Smith db931e0bac Issue #3155 - Fix CSS aspect ratio picking the wrong axis for the size with eShrinkWrap. 2026-11-10 22:24:22 +01:00
Moonchild 9a445f541f Merge pull request 'Issue #3214 - Implement Element.checkVisibility()' (#3218) from ninthrelay/UXP:checkvisibility into master
Reviewed-on: https://repo.palemoon.org/MoonchildProductions/UXP/pulls/3218
2026-11-08 16:49:19 +00:00
ninthrelay 500f467a6d Issue #3214 - Add checkVisibility() to Element.webidl 2026-11-08 13:25:12 +00:00
ninthrelay 197400b5fd Issue #3214 - Add checkVisibility() mochitest 2026-11-08 13:24:43 +00:00
ninthrelay 4dfc3313d1 Issue #3214 - Implement Element.checkVisibility() 2026-11-08 13:23:49 +00:00
Brian Smith 57b87edf6e Issue #3206 - Implement cross zone WeakRef tracing. 2026-11-08 14:16:15 +01:00
Brian Smith 10af280359 No Issue - Fix assert and incorrect result if calc() is passed a number instead of degrees. 2026-11-08 14:15:58 +01:00
Brian Smith 199288d27c No Issue - Similar to an earlier commit, move to ParseNonNegativeVariant() to avoid asserts.
This also has the effect of avoiding silent calculation errors in release mode.
Also add a guard against negative values.
2026-11-08 14:15:36 +01:00
Brian Smith 6ab372c02a No Issue - Avoid incorrect asserts when the constructor document is NULL. 2026-11-08 14:15:20 +01:00
Brian Smith 6ea2e766ae No Issue - Avoid a transition assert and protect it from invalid destruction. 2026-11-08 14:15:10 +01:00
Brian Smith 976f435ecf No Issue - The CSS frame constructor was leaving orphaned undisplayed elements.
This was a page based memory leak, they would only go away when a page was closed.
2026-11-08 14:14:48 +01:00
Brian Smith b28af61a16 No Issue - Fix asserts in the JS parser and actually handle the isSuper case. 2026-11-08 14:14:38 +01:00
Brian Smith 4569c62cdb No Issue - Fix an off by one causing invalid range asserts. 2026-11-08 14:14:28 +01:00
Brian Smith 3474188b22 No Issue - Make sure shadow root sheets have a valid document. 2026-11-08 14:14:13 +01:00
Brian Smith 2b513d7640 No Issue - Mark result black when using a getter.
This may be related to WeakRefs deref and the Callbacks commits.
2026-11-08 14:14:04 +01:00
Brian Smith d735420923 No Issue - Fix a crash in Typed array in debug mode.
The checks were done in the wrong order potentially leading to crashes.
2026-11-08 14:13:51 +01:00
Brian Smith 77fb133f1d No Issue - Mark callback endpoints black.
Potentially related to the WeakRefs deref change, was getting gray objects
during callbacks on chat.google.com.
2026-11-08 14:13:44 +01:00
Brian Smith f15463c675 No Issue - WeakRefs deref should not return white or gray objects.
Also fix an assert due to missing finalize flag with a finalize method.
2026-11-08 14:13:37 +01:00
Brian Smith 88b1057bca Issue #3208 - Fix crash on pixiv site.
FinalizationRegistry was tracing more than it should have.
2026-11-08 14:13:29 +01:00
Moonchild 3cce03d12a Issue #3206 Back out stub pref for WeakRef/FinalizationRegistry API>
While this should work with some tracing fixes, there are subtleties that
continue to cause issues with the stub variant of the API.

Resolves #3206
2026-11-08 14:13:19 +01:00
Moonchild 5630548c86 Merge pull request 'Fix OS.File on 32-bit glibc builds with 64-bit off_t and time_t' (#3205) from valentineus/UXP:master into master
Reviewed-on: https://repo.palemoon.org/MoonchildProductions/UXP/pulls/3205
Reviewed in [Topic 33924](https://forum.palemoon.org/viewtopic.php?f=62&t=33924) on the forum.
2026-11-07 11:32:03 +00:00
Moonchild bed3920d75 Merge pull request 'Reduce repeated :has() matching and text restyles' (#3217) from Basilisk-Dev/UXP-contrib:css-has-msfn into master
Reviewed-on: https://repo.palemoon.org/MoonchildProductions/UXP/pulls/3217
2026-11-07 07:49:02 +00:00
Moonchild bc4a702f80 Merge pull request 'Don't reject when webm codec delay and opus pre-skip do not match.' (#3216) from jobbautista9/UXP:3215 into master
Reviewed-on: https://repo.palemoon.org/MoonchildProductions/UXP/pulls/3216
2026-11-07 07:32:16 +00:00
Basilisk-Dev 575aa0ebaa Issue #3176 - Reduce repeated :has() matching and text restyles
Cache relative-selector results within a styling traversal, preserving
invalidation registration on hits and bypassing scoped/shadow matching.
Keep the common single result inline and bound the fallback cache.

Limit child-combinator chains to the required depth. Avoid invalidating
local :has() compounds for text-only mutations while retaining structural
pseudo-class dependencies such as :empty.

Add regression coverage for cache lifetime, distinct anchors, child-chain
matching, and text mutation restyle counts.
2026-11-06 15:21:26 -05:00
Job Bautista 518ef1cb10 Issue #3215 - Don't reject when webm codec delay and opus pre-skip do not match. 2026-11-06 23:50:05 +08:00
Valentin Popov 73da09c35e Issue #3204 - Fix OS.File on 32-bit glibc builds with 64-bit off_t and time_t
js-ctypes looks up libc functions by name, so OS.File does not get the
redirections from the glibc headers. With _FILE_OFFSET_BITS=64 (and
_TIME_BITS=64 on 32-bit platforms) the structures in OS.Constants.libc
have the 64-bit layout, but OS.File called the old readdir, stat, lseek,
utimes and other functions. For example, DirectoryIterator returned "ml"
for "google.xml", and the search service failed to load engines.

- Export __USE_FILE_OFFSET64 and __USE_TIME64_REDIRECTS for glibc
  builds, and the size of timeval.tv_usec, from OSFileConstants.cpp.
- Use the same libc functions as the C headers in osfile_unix_back.jsm.
- Use time_t and the real field size for struct timeval.
- Do not cut the seconds to signed 32 bits in datesToTimevals(), so
  dates after January 2038 work with 64-bit time_t.
2026-10-30 11:42:46 +04:00
186 changed files with 5478 additions and 1010 deletions

No files matched your search

+32
View File
@@ -995,6 +995,38 @@ Element::GetBoundingClientRect()
return rect.forget();
}
bool
Element::CheckVisibility(const CheckVisibilityOptions& aOptions)
{
// https://drafts.csswg.org/cssom-view/#dom-element-checkvisibility
nsIFrame* frame = GetPrimaryFrame(Flush_Frames);
if (!frame) {
// No associated box, e.g. display:none on this element or an ancestor,
// display:contents, or not in a document.
return false;
}
// We don't support content-visibility, so aOptions.mContentVisibilityAuto
// has nothing to check.
if (aOptions.mCheckOpacity || aOptions.mOpacityProperty) {
for (nsIFrame* f = frame; f; f = f->GetParent()) {
if (f->StyleEffects()->mOpacity == 0.0f) {
return false;
}
}
}
if (aOptions.mCheckVisibilityCSS || aOptions.mVisibilityProperty) {
// visibility is inherited, so the element's own computed value is enough.
if (!frame->StyleVisibility()->IsVisible()) {
return false;
}
}
return true;
}
already_AddRefed<DOMRectList>
Element::GetClientRects()
{
+2
View File
@@ -909,6 +909,8 @@ public:
already_AddRefed<DOMRectList> GetClientRects();
already_AddRefed<DOMRect> GetBoundingClientRect();
bool CheckVisibility(const CheckVisibilityOptions& aOptions);
// Shadow DOM v1
already_AddRefed<ShadowRoot> AttachShadow(const ShadowRootInit& aInit,
ErrorResult& aError);
+18
View File
@@ -371,6 +371,24 @@ ShadowRoot::AdoptedStyleSheetsChanged(
OwnerDoc()->EndUpdate(UPDATE_STYLE);
}
void
ShadowRoot::OwnerDocumentChanged(nsIDocument* aOldDocument)
{
if (aOldDocument == OwnerDoc()) {
return;
}
// Deliberately use the nsIDocument overloads for both directions: the shadow
// root itself stays registered in the sheets' mAdopterShadowRoots exactly
// once, only the document the adoption count is accounted against changes.
// When the count passes through zero the sheet drops its associated document
// and re-associates with ours.
for (CSSStyleSheet* sheet : mAdoptedStyleSheets) {
sheet->RemoveAdopter(aOldDocument);
sheet->AddAdopter(OwnerDoc());
}
}
void
ShadowRoot::AddToIdTable(Element* aElement, nsIAtom* aId)
{
+1
View File
@@ -63,6 +63,7 @@ public:
void AdoptedStyleSheetsChanged(
const nsTArray<RefPtr<mozilla::CSSStyleSheet>>& aOldSheets,
const nsTArray<RefPtr<mozilla::CSSStyleSheet>>& aNewSheets);
void OwnerDocumentChanged(nsIDocument* aOldDocument);
StyleSheetList* StyleSheets()
{
return &DocumentOrShadowRoot::EnsureDOMStyleSheets();
+6
View File
@@ -535,6 +535,12 @@ nsNodeUtils::CloneAndAdopt(nsINode *aNode, bool aClone, bool aDeep,
if (elem) {
elem->NodeInfoChanged();
}
// A ShadowRoot is not an element, so NodeInfoChanged() isn't reached for
// it even though we just swapped its node document. Let it fix up the
// constructed sheets it has adopted.
if (ShadowRoot* shadow = ShadowRoot::FromNode(aNode)) {
shadow->OwnerDocumentChanged(oldDoc);
}
nsIDocument* newDoc = aNode->OwnerDoc();
if (newDoc) {
+1 -1
View File
@@ -495,7 +495,7 @@ nsRange::CharacterDataChanged(nsIDocument* aDocument,
#ifdef DEBUG
if (mStartOffsetWasIncremented || mEndOffsetWasIncremented) {
mAssertNextInsertOrAppendIndex =
(mStartOffsetWasIncremented ? mStartOffset : mEndOffset) - 1;
mStartOffsetWasIncremented ? mStartOffset : mEndOffset;
mAssertNextInsertOrAppendNode = aInfo->mDetails->mNextSibling;
}
#endif
+1
View File
@@ -611,6 +611,7 @@ skip-if = toolkit == 'android'
[test_bug1314032.html]
[test_bug1375050.html]
[test_bug1453693.html]
[test_checkVisibility.html]
skip-if = os == "mac" # Different tab focus behavior on mac
[test_caretPositionFromPoint.html]
[test_change_policy.html]
+48
View File
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test for Element.checkVisibility()</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<div id="visible">visible</div>
<div id="dn" style="display:none"><span id="dnchild">x</span></div>
<div id="dc" style="display:contents"><span id="dcchild">x</span></div>
<div id="op0" style="opacity:0"><span id="op0child">x</span></div>
<div id="ophalf" style="opacity:0.5">x</div>
<div id="vh" style="visibility:hidden"><span id="vhchild">x</span>
<span id="vhvisible" style="visibility:visible">x</span></div>
<div id="vc" style="visibility:collapse">x</div>
<pre id="test">
<script>
function $(id) { return document.getElementById(id); }
ok("checkVisibility" in Element.prototype, "checkVisibility exists");
ok($("visible").checkVisibility(), "plain element is visible");
ok(!$("dn").checkVisibility(), "display:none");
ok(!$("dnchild").checkVisibility(), "child of display:none");
ok(!$("dc").checkVisibility(), "display:contents has no box");
ok($("dcchild").checkVisibility(), "child of display:contents");
ok(!document.createElement("div").checkVisibility(), "disconnected element");
ok($("op0").checkVisibility(), "opacity:0 ignored by default");
ok(!$("op0").checkVisibility({checkOpacity: true}), "opacity:0 checkOpacity");
ok(!$("op0").checkVisibility({opacityProperty: true}), "opacity:0 opacityProperty");
ok(!$("op0child").checkVisibility({checkOpacity: true}), "child of opacity:0");
ok($("ophalf").checkVisibility({checkOpacity: true}), "opacity:0.5 visible");
ok($("vh").checkVisibility(), "visibility:hidden ignored by default");
ok(!$("vh").checkVisibility({checkVisibilityCSS: true}), "visibility:hidden checkVisibilityCSS");
ok(!$("vh").checkVisibility({visibilityProperty: true}), "visibility:hidden visibilityProperty");
ok(!$("vhchild").checkVisibility({checkVisibilityCSS: true}), "inherits visibility:hidden");
ok($("vhvisible").checkVisibility({checkVisibilityCSS: true}), "visibility:visible inside hidden");
ok(!$("vc").checkVisibility({checkVisibilityCSS: true}), "visibility:collapse");
ok($("visible").checkVisibility({contentVisibilityAuto: true}), "contentVisibilityAuto accepted");
</script>
</pre>
</body>
</html>
+7
View File
@@ -59,6 +59,13 @@ CallbackObject::HoldJSObjectsIfMoreThanOneOwner()
{
MOZ_ASSERT(mRefCnt.get() > 0);
if (mRefCnt.get() > 1) {
mCallback.exposeToActiveJS();
if (mCreationStack) {
mCreationStack.exposeToActiveJS();
}
if (mIncumbentJSGlobal) {
mIncumbentJSGlobal.exposeToActiveJS();
}
mozilla::HoldJSObjects(this);
} else {
// We can just forget all our stuff.
+13
View File
@@ -179,6 +179,19 @@ private:
mIncumbentGlobal = aIncumbentGlobal;
mIncumbentJSGlobal = aIncumbentGlobal->GetGlobalJSObject();
}
// The post-barrier in Heap::operator= only handles nursery edges. If we are
// created during an incremental marking slice and our owner is already
// black, the CC is entitled to skip the owner and TraceBlackJS has already
// run, so nothing will ever trace these members this cycle and they get
// swept. Blacken them here instead.
mCallback.exposeToActiveJS();
if (mCreationStack) {
mCreationStack.exposeToActiveJS();
}
if (mIncumbentJSGlobal) {
mIncumbentJSGlobal.exposeToActiveJS();
}
}
inline void Init(JSObject* aCallback, JSObject* aCreationStack,
@@ -88,7 +88,6 @@ OpusDataDecoder::Init()
if (codecDelay != FramesToUsecs(mOpusParser->mPreSkip,
mOpusParser->mRate).value()) {
NS_WARNING("Invalid Opus header: CodecDelay and pre-skip do not match!");
return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
}
if (mInfo.mRate != (uint32_t)mOpusParser->mRate) {
+21
View File
@@ -648,6 +648,9 @@ static const dom::ConstantSpec gLibcProperties[] =
{ "OSFILE_SIZEOF_TIMEVAL", JS::Int32Value(sizeof (struct timeval)) },
{ "OSFILE_OFFSETOF_TIMEVAL_TV_SEC", JS::Int32Value(offsetof (struct timeval, tv_sec)) },
{ "OSFILE_OFFSETOF_TIMEVAL_TV_USEC", JS::Int32Value(offsetof (struct timeval, tv_usec)) },
// The size of field |tv_usec|. It is not always a |long|, e.g. glibc uses
// a 64-bit field on 32-bit platforms when building with |_TIME_BITS=64|.
{ "OSFILE_SIZEOF_TIMEVAL_TV_USEC", JS::Int32Value(sizeof (timeval::tv_usec)) },
#if defined(DT_UNKNOWN)
// Position of field |d_type| in |dirent|
@@ -716,6 +719,24 @@ static const dom::ConstantSpec gLibcProperties[] =
INT_CONSTANT(_STAT_VER),
#endif // defined(_STAT_VER)
// With glibc, the C headers redirect some functions to other variants that
// match the data structures defined above:
// - with |_FILE_OFFSET_BITS=64|, functions that use file offsets are
// redirected to their large file variants (e.g. |readdir| to |readdir64|);
// - with |_TIME_BITS=64| on 32-bit platforms, functions that use |time_t|
// are redirected to their 64-bit time variants (e.g. |stat| to
// |__stat64_time64|). Before glibc 2.39, the macro for this case was
// |__USE_TIME_BITS64|.
// OS.File needs to use the same functions.
#if defined(__GLIBC__)
#if defined(__USE_FILE_OFFSET64)
{ "__USE_FILE_OFFSET64", JS::Int32Value(1) },
#endif // defined(__USE_FILE_OFFSET64)
#if defined(__USE_TIME64_REDIRECTS) || (defined(__USE_TIME_BITS64) && __TIMESIZE == 32)
{ "__USE_TIME64_REDIRECTS", JS::Int32Value(1) },
#endif // defined(__USE_TIME64_REDIRECTS) || (defined(__USE_TIME_BITS64) && __TIMESIZE == 32)
#endif // defined(__GLIBC__)
PROP_END
};
+11
View File
@@ -192,11 +192,22 @@ dictionary ScrollIntoViewOptions : ScrollOptions {
ScrollLogicalPosition inline = "nearest";
};
// https://drafts.csswg.org/cssom-view/#dictdef-checkvisibilityoptions
dictionary CheckVisibilityOptions {
boolean checkOpacity = false;
boolean checkVisibilityCSS = false;
boolean contentVisibilityAuto = false;
boolean opacityProperty = false;
boolean visibilityProperty = false;
};
// http://dev.w3.org/csswg/cssom-view/#extensions-to-the-element-interface
partial interface Element {
DOMRectList getClientRects();
DOMRect getBoundingClientRect();
boolean checkVisibility(optional CheckVisibilityOptions options);
// scrolling
void scrollIntoView(optional (boolean or ScrollIntoViewOptions) arg);
// None of the CSSOM attributes are [Pure], because they flush
-1
View File
@@ -301,7 +301,6 @@ LoadContextOptions(const char* aPrefName, void* /* aClosure */)
.setAsyncStack(GetWorkerPref<bool>(NS_LITERAL_CSTRING("asyncstack")))
.setWerror(GetWorkerPref<bool>(NS_LITERAL_CSTRING("werror")))
.setStreams(GetWorkerPref<bool>(NS_LITERAL_CSTRING("streams")))
.setWeakRefs(GetWorkerPref<bool>(NS_LITERAL_CSTRING("weakrefs")))
.setExtraWarnings(GetWorkerPref<bool>(NS_LITERAL_CSTRING("strict")))
.setArrayProtoValues(GetWorkerPref<bool>(
NS_LITERAL_CSTRING("array_prototype_values")));
-1
View File
@@ -35,7 +35,6 @@ WORKER_SIMPLE_PREF("dom.serviceWorkers.openWindow.enabled", OpenWindowEnabled, O
WORKER_SIMPLE_PREF("dom.storageManager.enabled", StorageManagerEnabled, STORAGEMANAGER_ENABLED)
WORKER_SIMPLE_PREF("dom.push.enabled", PushEnabled, PUSH_ENABLED)
WORKER_SIMPLE_PREF("dom.streams.enabled", StreamsEnabled, STREAMS_ENABLED)
WORKER_SIMPLE_PREF("javascript.options.weakrefs", WeakRefsEnabled, WEAKREFS_ENABLED)
WORKER_SIMPLE_PREF("dom.requestcontext.enabled", RequestContextEnabled, REQUESTCONTEXT_ENABLED)
WORKER_SIMPLE_PREF("gfx.offscreencanvas.enabled", OffscreenCanvasEnabled, OFFSCREENCANVAS_ENABLED)
WORKER_SIMPLE_PREF("dom.webkitBlink.dirPicker.enabled", WebkitBlinkDirectoryPickerEnabled, DOM_WEBKITBLINK_DIRPICKER_WEBKITBLINK)
+41 -53
View File
@@ -66,28 +66,10 @@ struct WeakCell {
WeakCellKind kind;
WeakRef<JSObject*> object;
WeakRef<JS::Symbol*> symbol;
bool enabled;
WeakCell()
: kind(WeakCellKind::Empty)
, object(nullptr)
, symbol(nullptr)
, enabled(true)
{}
explicit WeakCell(JSObject* obj, bool enabled)
: kind(WeakCellKind::Object)
, object(obj)
, symbol(nullptr)
, enabled(enabled)
{}
explicit WeakCell(JS::Symbol* sym, bool enabled)
: kind(WeakCellKind::Symbol)
, object(nullptr)
, symbol(sym)
, enabled(enabled)
{}
WeakCell() : kind(WeakCellKind::Empty), object(nullptr), symbol(nullptr) {}
explicit WeakCell(JSObject* obj) : kind(WeakCellKind::Object), object(obj), symbol(nullptr) {}
explicit WeakCell(JS::Symbol* sym) : kind(WeakCellKind::Symbol), object(nullptr), symbol(sym) {}
void clear() {
kind = WeakCellKind::Empty;
@@ -119,19 +101,20 @@ struct WeakCell {
if (!target)
return;
if (!enabled) {
// Do nothing
} else if (IsInsideNursery(target)) {
if (IsInsideNursery(target)) {
TraceManuallyBarrieredEdge(trc, object.unsafeGet(), name);
} else {
if (trc->isMarkingTracer() && !target->asTenured().zone()->isCollecting())
// A gray marker may only mark into a zone that is in the gray
// marking state; a zone still in Mark (black) is in a different
// zone group and gets its own pass.
if (trc->isMarkingTracer() && !target->asTenured().zone()->isGCMarkingGray())
return;
TraceWeakEdge(trc, &object, name);
}
} else if (kind == WeakCellKind::Symbol) {
JS::Symbol* target = symbol.unbarrieredGet();
if (enabled && target) {
if (trc->isMarkingTracer() && !target->asTenured().zone()->isCollecting())
if (target) {
if (trc->isMarkingTracer() && !target->asTenured().zone()->isGCMarkingGray())
return;
TraceWeakEdge(trc, &symbol, name);
}
@@ -144,16 +127,14 @@ struct WeakCell {
if (!target)
return;
if (!enabled) {
// Do nothing
} else if (IsInsideNursery(target)) {
if (IsInsideNursery(target)) {
TraceManuallyBarrieredEdge(trc, object.unsafeGet(), name);
} else if (target->asTenured().zone()->isCollecting()) {
} else if (target->asTenured().zone()->isGCMarkingGray()) {
TraceWeakEdge(trc, &object, name);
}
} else if (kind == WeakCellKind::Symbol) {
JS::Symbol* target = symbol.unbarrieredGet();
if (enabled && target && target->asTenured().zone()->isCollecting())
if (target && target->asTenured().zone()->isGCMarkingGray())
TraceWeakEdge(trc, &symbol, name);
}
}
@@ -165,24 +146,22 @@ struct FinalizationRecord {
WeakCell unregisterToken;
bool active;
bool queued;
bool enabled;
FinalizationRecord(HandleValue targetValue, HandleValue heldValue,
HandleValue unregisterTokenValue, bool enabled)
HandleValue unregisterTokenValue)
: heldValue(heldValue),
active(true),
queued(false),
enabled(enabled)
queued(false)
{
if (targetValue.isObject())
target = WeakCell(NormalizeWeakObject(&targetValue.toObject()), enabled);
target = WeakCell(NormalizeWeakObject(&targetValue.toObject()));
else
target = WeakCell(targetValue.toSymbol(), enabled);
target = WeakCell(targetValue.toSymbol());
if (unregisterTokenValue.isObject())
unregisterToken = WeakCell(NormalizeWeakObject(&unregisterTokenValue.toObject()), enabled);
unregisterToken = WeakCell(NormalizeWeakObject(&unregisterTokenValue.toObject()));
else if (unregisterTokenValue.isSymbol())
unregisterToken = WeakCell(unregisterTokenValue.toSymbol(), enabled);
unregisterToken = WeakCell(unregisterTokenValue.toSymbol());
}
void trace(JSTracer* trc) {
@@ -241,15 +220,13 @@ struct FinalizationRegistryObject::Data {
FinalizationRecordVector records;
FinalizationRecordVector cleanupQueue;
bool queuedForCleanup;
bool enabled;
explicit Data(JSObject* cleanupCallback, bool enabled)
explicit Data(JSObject* cleanupCallback)
: cleanupCallback(cleanupCallback),
cleanupJob(nullptr),
records(SystemAllocPolicy()),
cleanupQueue(SystemAllocPolicy()),
queuedForCleanup(false),
enabled(enabled)
queuedForCleanup(false)
{}
~Data() {
@@ -258,13 +235,11 @@ struct FinalizationRegistryObject::Data {
}
void trace(JSTracer* trc) {
if (enabled) {
JS::TraceEdge(trc, &cleanupCallback, "FinalizationRegistry cleanup callback");
if (cleanupJob.unbarrieredGet())
JS::TraceEdge(trc, &cleanupJob, "FinalizationRegistry cleanup job");
for (size_t i = 0; i < records.length(); i++)
records[i]->trace(trc);
}
JS::TraceEdge(trc, &cleanupCallback, "FinalizationRegistry cleanup callback");
if (cleanupJob.unbarrieredGet())
JS::TraceEdge(trc, &cleanupJob, "FinalizationRegistry cleanup job");
for (size_t i = 0; i < records.length(); i++)
records[i]->trace(trc);
}
bool appendRecord(FinalizationRecord* record) {
@@ -345,7 +320,7 @@ FinalizationRegistry_register_impl(JSContext* cx, const CallArgs& args)
unregisterToken.setUndefined();
}
FinalizationRecord* record = cx->new_<FinalizationRecord>(target, heldValue, unregisterToken, cx->options().weakRefs());
FinalizationRecord* record = cx->new_<FinalizationRecord>(target, heldValue, unregisterToken);
if (!record)
return false;
@@ -429,7 +404,9 @@ FinalizationRegistryObject::create(JSContext* cx, HandleObject cleanupCallback,
if (!obj)
return nullptr;
Data* data = cx->new_<Data>(cleanupCallback, cx->options().weakRefs());
MOZ_ASSERT(!IsInsideNursery(obj.get()));
Data* data = cx->new_<Data>(cleanupCallback);
if (!data)
return nullptr;
@@ -538,6 +515,17 @@ FinalizationRegistryObject::cleanupJob(JSContext* cx, unsigned argc, Value* vp)
/* static */ void
FinalizationRegistryObject::trace(JSTracer* trc, JSObject* obj)
{
// Registered by raw pointer in our own zone's finalizationRegistries, which
// nothing relocates. traceFinalizationRegistryWeakRefs() and
// Zone::~Zone both dereference these entries, so repair ours if we moved.
if (trc->runtime()->gc.isHeapCompacting()) {
for (auto& registry : obj->zone()->finalizationRegistries) {
JSObject** p = registry.unsafeGet();
if (*p && js::gc::IsForwarded(*p) && js::gc::Forwarded(*p) == obj)
*p = obj;
}
}
if (Data* data = GetData(obj))
data->trace(trc);
}
+60 -8
View File
@@ -75,6 +75,10 @@ WeakRef_deref_impl(JSContext* cx, const CallArgs& args)
// Ensure pointer is not null.
// During WeakRef deref, if the target was collected, it will be nullptr.
if (target) {
// We are handing this object to running JS, and the persistent root
// below will not be scanned until the next GC's root marking, so
// expose it here or a mid-slice deref can hand out a white object.
JS::ExposeObjectToActiveJS(target);
RootedValue kept(cx, ObjectValue(*target));
if (!cx->runtime()->addWeakRefKeptObject(cx, kept))
return false;
@@ -88,6 +92,7 @@ WeakRef_deref_impl(JSContext* cx, const CallArgs& args)
// Ensure pointer is not null.
if (target) {
js::gc::ExposeGCThingToActiveJS(JS::GCCellPtr(target));
RootedValue kept(cx, SymbolValue(target));
if (!cx->runtime()->addWeakRefKeptObject(cx, kept))
return false;
@@ -144,16 +149,30 @@ WeakRefObject::create(JSContext* cx, HandleValue target, HandleObject proto /* =
if (!obj)
return nullptr;
// targetZone->weakRefs stores this as a raw pointer, so it must not move
// on minor GC. JSCLASS_FOREGROUND_FINALIZE without
// JSCLASS_SKIP_NURSERY_FINALIZE gives us that via GetInitialHeap().
MOZ_ASSERT(!IsInsideNursery(obj.get()));
Referent* data;
if (target.isObject())
data = cx->new_<Referent>(&target.toObject(), cx->options().weakRefs());
data = cx->new_<Referent>(&target.toObject());
else
data = cx->new_<Referent>(target.toSymbol(), cx->options().weakRefs());
data = cx->new_<Referent>(target.toSymbol());
if (!data)
return nullptr;
obj->setPrivate(data);
Zone* targetZone = data->isObject()
? data->objectTarget.unbarrieredGet()->zone()
: data->symbolTarget.unbarrieredGet()->zone();
if (!targetZone->weakRefs.append(obj.get())) {
ReportOutOfMemory(cx);
return nullptr;
}
obj->targetZone = targetZone;
return obj;
}
@@ -210,15 +229,28 @@ WeakRefObject::deref(JSContext* cx, unsigned argc, Value* vp)
/* static */ void
WeakRefObject::trace(JSTracer* trc, JSObject* obj)
{
// This object is registered by raw pointer in the *referent's* zone's
// weakRefs list. That zone may never be compacted itself, so its
// fixupAfterMovingGC() cannot be relied on to fix the entry, and the
// relocated arenas are not released until the end of this slice, so the
// old location is still readable here. Only relocated weakrefs can be
// stale, and each one is traced exactly once with its new address.
if (trc->runtime()->gc.isHeapCompacting()) {
if (Zone* targetZone = obj->as<WeakRefObject>().targetZone) {
for (JSObject*& ref : targetZone->weakRefs) {
if (js::gc::IsForwarded(ref) && js::gc::Forwarded(ref) == obj)
ref = obj;
}
}
}
if (Referent* data = GetReferent(obj)) {
if (data->isObject()) {
JSObject* target = data->objectTarget.unbarrieredGet();
if (!target)
return;
if (!data->enabled) {
// Pref disabled, don't do anything.
} else if (IsInsideNursery(target)) {
if (IsInsideNursery(target)) {
// Weak edges must be tenured; trace strongly while referent is in the nursery.
TraceManuallyBarrieredEdge(trc, data->objectTarget.unsafeGet(),
"WeakRef nursery referent");
@@ -228,7 +260,7 @@ WeakRefObject::trace(JSTracer* trc, JSObject* obj)
} else {
MOZ_ASSERT(data->isSymbol());
JS::Symbol* target = data->symbolTarget.unbarrieredGet();
if (data->enabled && target)
if (target)
TraceWeakEdge(trc, &data->symbolTarget, "WeakRef symbol referent");
}
}
@@ -237,8 +269,27 @@ WeakRefObject::trace(JSTracer* trc, JSObject* obj)
/* static */ void
WeakRefObject::finalize(FreeOp* fop, JSObject* obj)
{
if (Referent* data = GetReferent(obj))
WeakRefObject* weakRef = &obj->as<WeakRefObject>();
if (Zone* zone = weakRef->targetZone) {
bool found = false;
for (JSObject** p = zone->weakRefs.begin(); p != zone->weakRefs.end(); p++) {
if (*p == obj) {
zone->weakRefs.erase(p);
found = true;
break;
}
}
// If this fires, an entry went stale again and the object is about to
// be freed while still reachable from the zone's list.
MOZ_ASSERT(found);
weakRef->clearTargetZone();
}
if (Referent* data = GetReferent(obj)) {
fop->delete_(data);
obj->as<WeakRefObject>().setPrivate(nullptr);
}
}
static const ClassOps WeakRefObjectClassOps = {
@@ -259,7 +310,8 @@ static const ClassOps WeakRefObjectClassOps = {
const Class WeakRefObject::class_ = {
"WeakRef",
JSCLASS_HAS_PRIVATE |
JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef),
JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef) |
JSCLASS_FOREGROUND_FINALIZE,
&WeakRefObjectClassOps
};
+9 -15
View File
@@ -22,27 +22,17 @@ class WeakRefObject : public NativeObject
Symbol
};
explicit Referent(JSObject* obj, bool enabled)
: kind(Kind::Object)
, objectTarget(obj)
, symbolTarget(nullptr)
, enabled(enabled)
{}
explicit Referent(JS::Symbol* sym, bool enabled)
: kind(Kind::Symbol)
, objectTarget(nullptr)
, symbolTarget(sym)
, enabled(enabled)
{}
explicit Referent(JSObject* obj)
: kind(Kind::Object), objectTarget(obj), symbolTarget(nullptr) {}
explicit Referent(JS::Symbol* sym)
: kind(Kind::Symbol), objectTarget(nullptr), symbolTarget(sym) {}
bool isObject() const { return kind == Kind::Object; }
bool isSymbol() const { return kind == Kind::Symbol; }
Kind kind;
WeakRef<JSObject*> objectTarget;
WeakRef<JS::Symbol*> symbolTarget;
bool enabled;
};
static const Class class_;
@@ -68,6 +58,10 @@ class WeakRefObject : public NativeObject
static const JSPropertySpec properties[];
static const JSFunctionSpec methods[];
Zone* targetZone = nullptr;
void clearTargetZone() { targetZone = nullptr; }
private:
};
+37 -16
View File
@@ -7084,9 +7084,16 @@ BytecodeEmitter::emitDeletePropertyInOptChain(
MOZ_ASSERT_IF(propExpr->is<PropertyAccess>(),
!propExpr->as<PropertyAccess>().isSuper());
if (!emitOptionalTree(&propExpr->expression(), oe)) {
// [stack] OBJ
return false;
if (propExpr->isSuper()) {
if (!emitGetThisForSuperBase(&propExpr->expression().as<UnaryNode>())) {
// [stack] THIS
return false;
}
} else {
if (!emitOptionalTree(&propExpr->expression(), oe)) {
// [stack] OBJ
return false;
}
}
if (propExpr->isKind(PNK_OPTDOT)) {
if (!oe.emitJumpShortCircuit()) {
@@ -7381,7 +7388,7 @@ BytecodeEmitter::emitOptionalCalleeAndThis(
case PNK_OPTDOT: {
MOZ_ASSERT(emitterMode != BytecodeEmitter::SelfHosting);
OptionalPropertyAccess* prop = &calleeNode->as<OptionalPropertyAccess>();
bool isSuper = false;
bool isSuper = prop->isSuper();
PropOpEmitter& poe = cone.prepareForPropCallee(isSuper);
if (!emitOptionalDotExpression(prop, poe, isSuper, oe)) {
return false;
@@ -7401,7 +7408,7 @@ BytecodeEmitter::emitOptionalCalleeAndThis(
case PNK_OPTELEM: {
MOZ_ASSERT(emitterMode != BytecodeEmitter::SelfHosting);
OptionalPropertyByValue* elem = &calleeNode->as<OptionalPropertyByValue>();
bool isSuper = false;
bool isSuper = elem->isSuper();
ElemOpEmitter& eoe = cone.prepareForElemCallee(isSuper);
if (!emitOptionalElemExpression(elem, eoe, isSuper, oe)) {
return false;
@@ -7431,8 +7438,15 @@ BytecodeEmitter::emitOptionalCalleeAndThis(
case PNK_OPTCHAIN: {
return emitCalleeAndThisForOptionalChain(&calleeNode->as<UnaryNode>(), callNode, cone);
}
case PNK_SUPERBASE: {
MOZ_ASSERT(callNode->isKind(PNK_OPTCALL));
MOZ_ASSERT(parser->handler.isSuperBase(calleeNode));
if (!cone.emitSuperCallee()) {
return false;
}
break;
}
default: {
MOZ_RELEASE_ASSERT(calleeNode->getKind() != PNK_SUPERBASE);
if (!cone.prepareForOtherCallee()) {
return false;
}
@@ -9725,9 +9739,11 @@ BytecodeEmitter::emitOptionalTree(
switch (kind) {
case PNK_OPTDOT: {
OptionalPropertyAccess* prop = &pn->as<OptionalPropertyAccess>();
bool isSuper = false;
bool isSuper = prop->isSuper();
PropOpEmitter poe(this, PropOpEmitter::Kind::Get,
PropOpEmitter::ObjKind::Other);
isSuper ? PropOpEmitter::ObjKind::Super
: PropOpEmitter::ObjKind::Other);
if (!emitOptionalDotExpression(prop, poe, isSuper, oe))
return false;
break;
@@ -9744,9 +9760,10 @@ BytecodeEmitter::emitOptionalTree(
}
case PNK_OPTELEM: {
OptionalPropertyByValue* elem = &pn->as<OptionalPropertyByValue>();
bool isSuper = false;
bool isSuper = elem->isSuper();
ElemOpEmitter eoe(this, ElemOpEmitter::Kind::Get,
ElemOpEmitter::ObjKind::Other);
isSuper ? ElemOpEmitter::ObjKind::Super
: ElemOpEmitter::ObjKind::Other);
if (!emitOptionalElemExpression(elem, eoe, isSuper, oe))
return false;
break;
@@ -9789,18 +9806,24 @@ BytecodeEmitter::emitOptionalTree(
kind == PNK_CLASS ||
kind == PNK_REGEXP ||
kind == PNK_TEMPLATE_STRING ||
kind == PNK_TEMPLATE_STRING_LIST ||
kind == PNK_RAW_UNDEFINED ||
kind == PNK_GENEXP ||
kind == PNK_ARRAYCOMP ||
pn->isInParens();
// https://tc39.es/ecma262/#sec-left-hand-side-expressions
bool isMemberExpression = isPrimaryExpression ||
kind == PNK_TAGGED_TEMPLATE ||
kind == PNK_NEW ||
kind == PNK_NEWTARGET;
//kind == ParseNodeKind::ImportMetaExpr;
kind == PNK_NEWTARGET ||
kind == PNK_IMPORT_META ||
kind == PNK_OPTCHAIN;
bool isCallExpression = kind == PNK_SETTHIS;
//kind == ParseNodeKind::CallImportExpr;
// https://tc39.es/ecma262/#sec-call-expression
bool isCallExpression = kind == PNK_SETTHIS ||
kind == PNK_SUPERCALL ||
kind == PNK_CALL_IMPORT;
MOZ_ASSERT(isMemberExpression || isCallExpression,
"Unknown ParseNodeKind for OptionalChain");
@@ -9897,7 +9920,6 @@ BytecodeEmitter::emitOptionalDotExpression(
}
if (prop->isKind(PNK_OPTDOT)) {
MOZ_ASSERT(!isSuper);
if (!oe.emitJumpShortCircuit()) {
// [stack] # if Jump
// [stack] UNDEFINED-OR-NULL
@@ -9940,7 +9962,6 @@ BytecodeEmitter::emitOptionalElemExpression(
}
if (elem->isKind(PNK_OPTELEM)) {
MOZ_ASSERT(!isSuper);
if (!oe.emitJumpShortCircuit()) {
// [stack] # if Jump
// [stack] UNDEFINED-OR-NULL
-2
View File
@@ -10274,7 +10274,6 @@ Parser<ParseHandler>::memberPropertyAccess(
if (!name)
return null();
if (optionalKind == OptionalKind::Optional) {
MOZ_ASSERT(!handler.isSuperBase(lhs));
return handler.newOptionalPropertyAccess(lhs, name);
}
return handler.newPropertyAccess(lhs, name);
@@ -10302,7 +10301,6 @@ Parser<ParseHandler>::memberElemAccess(
}
if (optionalKind == OptionalKind::Optional) {
MOZ_ASSERT(!handler.isSuperBase(lhs));
return handler.newOptionalPropertyByValue(lhs, propExpr, pos().end);
}
return handler.newPropertyByValue(lhs, propExpr, pos().end);
+2 -2
View File
@@ -1609,8 +1609,8 @@ ObjectDenseElementsMayBeMarkable(NativeObject* nobj)
if (!typeSet)
return true;
static const uint32_t flagMask =
TYPE_FLAG_STRING | TYPE_FLAG_SYMBOL | TYPE_FLAG_LAZYARGS | TYPE_FLAG_ANYOBJECT;
static const uint32_t flagMask = TYPE_FLAG_STRING | TYPE_FLAG_SYMBOL | TYPE_FLAG_LAZYARGS |
TYPE_FLAG_ANYOBJECT | TYPE_FLAG_BIGINT;
bool mayBeMarkable = typeSet->hasAnyFlag(flagMask) || typeSet->getObjectCount() != 0;
#ifdef DEBUG
+14 -1
View File
@@ -13,6 +13,8 @@
#include "jit/JitCompartment.h"
#include "vm/Debugger.h"
#include "vm/Runtime.h"
#include "builtin/WeakRefObject.h"
#include "gc/Tracer.h"
#include "jscompartmentinlines.h"
#include "jsgcinlines.h"
@@ -64,6 +66,10 @@ JS::Zone::Zone(JSRuntime* rt)
Zone::~Zone()
{
JSRuntime* rt = runtimeFromMainThread();
for (JSObject* ref : weakRefs)
ref->as<WeakRefObject>().clearTargetZone();
if (this == rt->gc.systemZone)
rt->gc.systemZone = nullptr;
@@ -385,7 +391,14 @@ Zone::addTypeDescrObject(JSContext* cx, HandleObject obj)
return true;
}
void
Zone::traceWeakRefs(JSTracer* trc)
{
for (JSObject* ref : weakRefs)
WeakRefObject::trace(trc, ref);
}
ZoneList::ZoneList()
: head(nullptr), tail(nullptr)
{}
+8
View File
@@ -326,6 +326,14 @@ struct Zone : public JS::shadow::Zone,
using WeakEdges = js::Vector<js::gc::TenuredCell**, 0, js::SystemAllocPolicy>;
WeakEdges gcWeakRefs;
// WeakRef objects whose referent lives in this zone. Referents are fixed at
// construction, so this list is target-side: a GC of this zone finds the
// weak refs that need tracing without knowing which zone owns them.
using WeakRefVector = js::Vector<JSObject*, 0, js::SystemAllocPolicy>;
WeakRefVector weakRefs;
void traceWeakRefs(JSTracer* trc);
// FinalizationRegistry objects with weakly-held target cells in this zone.
using FinalizationRegistryVector = js::Vector<js::WeakRef<JSObject*>, 0, js::SystemAllocPolicy>;
FinalizationRegistryVector finalizationRegistries;
+3 -6
View File
@@ -999,8 +999,7 @@ class JS_PUBLIC_API(ContextOptions) {
strictMode_(false),
extraWarnings_(false),
arrayProtoValues_(true),
streams_(false),
weakRefs_(false)
streams_(false)
{
}
@@ -1140,13 +1139,12 @@ class JS_PUBLIC_API(ContextOptions) {
return *this;
}
bool weakRefs() const { return weakRefs_; }
bool weakRefs() const { return true; }
ContextOptions& setWeakRefs(bool flag) {
weakRefs_ = flag;
(void) flag;
return *this;
}
ContextOptions& toggleWeakRefs() {
weakRefs_ = !weakRefs_;
return *this;
}
@@ -1167,7 +1165,6 @@ class JS_PUBLIC_API(ContextOptions) {
bool extraWarnings_ : 1;
bool arrayProtoValues_ : 1;
bool streams_ : 1;
bool weakRefs_ : 1;
};
JS_PUBLIC_API(ContextOptions&)
+3 -1
View File
@@ -3889,6 +3889,9 @@ template <class ZoneIterT>
void
GCRuntime::traceFinalizationRegistryWeakRefs()
{
for (ZoneIterT zone(rt); !zone.done(); zone.next())
zone->traceWeakRefs(&marker);
for (ZoneIterT zone(rt); !zone.done(); zone.next()) {
for (size_t i = 0; i < zone->finalizationRegistries.length(); i++) {
WeakRef<JSObject*>& registry = zone->finalizationRegistries[i];
@@ -4776,7 +4779,6 @@ GCRuntime::sweepFinalizationRegistries()
JSObject* obj = zone->finalizationRegistries[i].unbarrieredGet();
obj->as<FinalizationRegistryObject>().sweepAfterGC(rt);
}
zone->finalizationRegistries.clear();
}
}
+8
View File
@@ -1858,6 +1858,14 @@ CallGetter(JSContext* cx, HandleObject obj, HandleValue receiver, HandleShape sh
if (shape->hasGetterValue()) {
RootedValue getter(cx, shape->getterValue());
// The receiver can still be gray: locals are read with getlocal, which
// copies a Value without a read barrier, so a gray object can ride in
// here and trip assertSameCompartment at the CallJSNative boundary.
// This is the last point where the receiver is a HandleValue before it
// becomes |this|, so blacken it here.
if (receiver.isObject()) {
JS::ExposeObjectToActiveJS(&receiver.toObject());
}
return js::CallGetter(cx, receiver, getter, vp);
}
+13 -7
View File
@@ -168,10 +168,16 @@ TypedArrayObject::ensureHasBuffer(JSContext* cx, Handle<TypedArrayObject*> tarra
void
TypedArrayObject::assertZeroLengthArrayData() const
{
if (length() == 0 && !hasBuffer()) {
uint8_t* end = fixedData(TypedArrayObject::FIXED_DATA_START);
MOZ_ASSERT(end[0] == ZeroLengthArrayData);
// Check hasBuffer() before length(): length() walks through isOutOfBounds()
// and bufferUnshared() into the ArrayBufferObject, and this is called from
// finalize(), where the buffer may already have been finalized and its cell
// poisoned by the same sweep. The buffer slot is only a tag check.
if (hasBuffer() || length() != 0) {
return;
}
uint8_t* end = fixedData(TypedArrayObject::FIXED_DATA_START);
MOZ_ASSERT(end[0] == ZeroLengthArrayData);
}
#endif
@@ -182,7 +188,7 @@ TypedArrayObject::trace(JSTracer* trc, JSObject* objArg)
ArrayBufferViewObject::trace(trc, objArg);
}
void
/* static */ void
TypedArrayObject::finalize(FreeOp* fop, JSObject* obj)
{
MOZ_ASSERT(!IsInsideNursery(obj));
@@ -193,12 +199,12 @@ TypedArrayObject::finalize(FreeOp* fop, JSObject* obj)
if (!curObj->elementsRaw())
return;
curObj->assertZeroLengthArrayData();
// Typed arrays with a buffer object do not need to be free'd
// Typed arrays with a buffer object do not need to be free'd.
if (curObj->hasBuffer())
return;
curObj->assertZeroLengthArrayData();
// Free the data slot pointer if it does not point into the old JSObject.
if (!curObj->hasInlineElements())
js_free(curObj->elements());
+1 -3
View File
@@ -1442,7 +1442,6 @@ ReloadPrefsCallback(const char* pref, void* data)
bool extraWarnings = Preferences::GetBool(JS_OPTIONS_DOT_STR "strict");
bool streams = Preferences::GetBool(JS_OPTIONS_DOT_STR "streams");
bool weakRefs = Preferences::GetBool(JS_OPTIONS_DOT_STR "weakrefs");
bool unboxedObjects = Preferences::GetBool(JS_OPTIONS_DOT_STR "unboxed_objects");
@@ -1469,8 +1468,7 @@ ReloadPrefsCallback(const char* pref, void* data)
.setWerror(werror)
.setExtraWarnings(extraWarnings)
.setArrayProtoValues(arrayProtoValues)
.setStreams(streams)
.setWeakRefs(weakRefs);
.setStreams(streams);
JS_SetParallelParsingEnabled(cx, parallelParsing);
JS_SetOffthreadIonCompilationEnabled(cx, offthreadIonCompilation);
+33 -8
View File
@@ -389,7 +389,8 @@ RestyleManager::RestyleForHasPseudoClassChange(nsINode* aNode,
EventStates aStateMask,
nsIAtom* aAttribute,
const nsAttrValue* aNewClasses,
bool aCompareClasses)
bool aCompareClasses,
bool aIsTextChange)
{
MOZ_ASSERT(!aAttribute || aNode->IsElement());
if (!aNode->OwnerDoc()->GetProperty(nsGkAtoms::hasSelectorDependency)) {
@@ -405,19 +406,26 @@ RestyleManager::RestyleForHasPseudoClassChange(nsINode* aNode,
PostRestyleEvent(aAnchor, hint, nsChangeHint(0));
};
// Keep track of the child below each ancestor that contains the mutation.
// This matters for sibling :has() selectors: a class/state change deep
// inside a sibling can only affect anchors before that containing child.
nsINode* mutationChild = nullptr;
for (nsINode* node = aNode; node; node = node->GetParentNode()) {
auto* dependency = static_cast<Dependency*>(
node->GetProperty(nsGkAtoms::hasSelectorDependency));
if (!dependency) {
mutationChild = node;
continue;
}
if (node->IsElement() && dependency->MightDependOnChange(
node->AsElement(), aNode, aStateMask, aAttribute, aNewClasses,
aCompareClasses, false)) {
aCompareClasses, false, false, aIsTextChange)) {
restyleAnchor(node->AsElement(), dependency);
}
if (!dependency->MightAffectSiblingAnchor(
node, aNode, aStateMask, aAttribute, aNewClasses, aCompareClasses)) {
node, aNode, aStateMask, aAttribute, aNewClasses, aCompareClasses,
aIsTextChange)) {
mutationChild = node;
continue;
}
// The parent is an index of sibling anchors, not itself an anchor. Keep
@@ -426,7 +434,7 @@ RestyleManager::RestyleForHasPseudoClassChange(nsINode* aNode,
bool nodeIsFollowingSibling = true;
for (nsIContent* child = node->GetFirstChild(); child;
child = child->GetNextSibling()) {
if (child == aNode) {
if (child == mutationChild) {
nodeIsFollowingSibling = false;
}
if (!child->IsElement()) {
@@ -436,10 +444,12 @@ RestyleManager::RestyleForHasPseudoClassChange(nsINode* aNode,
child->GetProperty(nsGkAtoms::hasSelectorDependency));
if (siblingDependency && siblingDependency->MightDependOnChange(
child->AsElement(), aNode, aStateMask, aAttribute, aNewClasses,
aCompareClasses, true, nodeIsFollowingSibling)) {
aCompareClasses, true, nodeIsFollowingSibling, aIsTextChange)) {
restyleAnchor(child->AsElement(), siblingDependency);
}
}
mutationChild = node;
}
}
@@ -469,7 +479,16 @@ RestyleManager::RestyleForAppend(nsIContent* aContainer,
nsIContent* aFirstNewContent)
{
// The container cannot be a document, but might be a ShadowRoot.
RestyleForHasPseudoClassChange(aContainer);
bool onlyText = true;
for (nsIContent* child = aFirstNewContent; child;
child = child->GetNextSibling()) {
if (!child->IsNodeOfType(nsINode::eTEXT)) {
onlyText = false;
break;
}
}
RestyleForHasPseudoClassChange(aContainer, EventStates(), nullptr, nullptr,
false, onlyText);
if (!aContainer->IsElement()) {
return;
@@ -560,7 +579,10 @@ RestyleManager::RestyleForInsertOrChange(nsINode* aContainer,
nsIContent* aChild)
{
// The container might be a document or a ShadowRoot.
RestyleForHasPseudoClassChange(aContainer);
// Local compounds cannot observe text, but structural pseudo-classes
// such as :empty must still be invalidated.
RestyleForHasPseudoClassChange(aContainer, EventStates(), nullptr, nullptr,
false, aChild->IsNodeOfType(nsINode::eTEXT));
if (!aContainer->IsElement()) {
return;
@@ -652,7 +674,10 @@ RestyleManager::ContentRemoved(nsINode* aContainer,
nsIContent* aFollowingSibling)
{
// The container might be a document or a ShadowRoot.
RestyleForHasPseudoClassChange(aContainer);
// Local compounds cannot observe text, but structural pseudo-classes
// such as :empty must still be invalidated.
RestyleForHasPseudoClassChange(aContainer, EventStates(), nullptr, nullptr,
false, aOldChild->IsNodeOfType(nsINode::eTEXT));
if (!aContainer->IsElement()) {
return;
+2 -1
View File
@@ -321,7 +321,8 @@ public:
EventStates aStateMask = EventStates(),
nsIAtom* aAttribute = nullptr,
const nsAttrValue* aNewClasses = nullptr,
bool aCompareClasses = false);
bool aCompareClasses = false,
bool aIsTextChange = false);
// Process any pending restyles. This should be called after
// CreateNeededFrames.
+8 -1
View File
@@ -817,7 +817,14 @@ private:
mUndisplayedItems[0].mStyleContext->PresContext()->FrameManager();
for (uint32_t i = 0; i < mUndisplayedItems.Length(); ++i) {
UndisplayedItem& item = mUndisplayedItems[i];
mgr->SetUndisplayedContent(item.mContent, item.mStyleContext);
nsStyleContext* old = mgr->GetUndisplayedContent(item.mContent);
if (old) {
if (old != item.mStyleContext) {
mgr->ChangeUndisplayedContent(item.mContent, item.mStyleContext);
}
} else {
mgr->SetUndisplayedContent(item.mContent, item.mStyleContext);
}
}
}
}
+42 -35
View File
@@ -262,60 +262,67 @@ nsFrameManager::ClearUndisplayedContentIn(nsIContent* aContent,
static int i = 0;
printf("ClearUndisplayedContent(%d): content=%p parent=%p --> ", i++, (void *)aContent, (void*)aParentContent);
#endif
if (!mUndisplayedMap) {
return;
}
// This function is an entry point into UndisplayedMap handling code, so we
// must call GetApplicableParent so the parent we pass around is correct.
aParentContent = UndisplayedMap::GetApplicableParent(aParentContent);
// GetApplicableParent only normalizes <xbl:children>. The lookup in
// GetStyleContextInMap goes through ParentForUndisplayedMap, which crosses
// shadow roots, and SetStyleContextInMap sets the bit on that same node. A
// child of a shadow root therefore has its entry keyed on the host element
// while aParentContent is the ShadowRoot itself - not an element, never the
// bit's owner, never the bucket key. Derive the key the way the lookup does.
aParentContent = ParentForUndisplayedMap(aContent);
if (aParentContent &&
if (aParentContent &&
!aParentContent->MayHaveChildrenWithLayoutBoxesDisabled()) {
MOZ_ASSERT(!mUndisplayedMap->GetFirstNode(aParentContent),
"MayHaveChildrenWithLayoutBoxesDisabled bit out of sync - "
"may fail to removenode from mUndisplayedMap");
"may fail to remove node from mUndisplayedMap");
return;
}
// The bucket can hold more than one entry for aContent (duplicate
// registration), so drain them all instead of stopping at the first.
bool removed = false;
bool haveOtherUndisplayedChild = false;
UndisplayedNode* node = mUndisplayedMap->GetFirstNode(aParentContent);
const bool haveOneDisplayNoneChild = node && !node->getNext();
for (; node; node = node->getNext()) {
while (node) {
UndisplayedNode* next = node->getNext();
if (node->mContent == aContent) {
mUndisplayedMap->RemoveNodeFor(aParentContent, node);
#ifdef DEBUG_UNDISPLAYED_MAP
printf( "REMOVED!\n");
#endif
// make sure that there are no more entries for the same content
MOZ_ASSERT(!GetUndisplayedContent(aContent),
"Found more undisplayed content data after removal");
if (haveOneDisplayNoneChild) {
// There are no more children of aParentContent in mUndisplayedMap.
MOZ_ASSERT(!mUndisplayedMap->GetFirstNode(aParentContent),
"Bad UnsetMayHaveChildrenWithLayoutBoxesDisabled call");
// If we also know that none of its children are in mDisplayContentsMap
// then we can call UnsetMayHaveChildrenWithLayoutBoxesDisabled. We
// don't want to check mDisplayContentsMap though, since that involves
// a hash table lookup in relatively hot code. Still, we know there are
// no chlildren in mDisplayContentsMap if the map is empty, so we do
// check for that.
if (aParentContent && !mDisplayContentsMap) {
aParentContent->UnsetMayHaveChildrenWithLayoutBoxesDisabled();
}
}
return;
removed = true;
} else {
haveOtherUndisplayedChild = true;
}
node = next;
}
if (!removed) {
#ifdef DEBUG_UNDISPLAYED_MAP
printf("not found.\n");
#endif
return;
}
#ifdef DEBUG_UNDISPLAYED_MAP
printf( "not found.\n");
printf("REMOVED!\n");
#endif
MOZ_ASSERT(!GetUndisplayedContent(aContent),
"Found more undisplayed content data after removal");
if (aParentContent && !haveOtherUndisplayedChild) {
// No children of aParentContent remain in mUndisplayedMap. The bit can
// only come off if none are in mDisplayContentsMap either, and an empty
// map implies that without a lookup.
MOZ_ASSERT(!mUndisplayedMap->GetFirstNode(aParentContent),
"Bad UnsetMayHaveChildrenWithLayoutBoxesDisabled call");
if (!mDisplayContentsMap) {
aParentContent->UnsetMayHaveChildrenWithLayoutBoxesDisabled();
}
}
}
void
+17 -7
View File
@@ -1713,35 +1713,41 @@ nsBlockFrame::ComputeFinalSize(const ReflowInput& aReflowInput,
static void
ConsiderBlockEndEdgeOfChildren(const WritingMode aWritingMode,
nscoord aBEndEdgeOfChildren,
const nscoord aBEndPadding,
nsOverflowAreas& aOverflowAreas)
{
// Factor in the block-end edge of the children. Child frames will be added
// to the overflow area as we iterate through the lines, but their margins
// won't, so we need to account for block-end margins here.
// Also factor in our own block-end padding: the padding-box end edge is part
// of the scrollable overflow area of a scroll container, so that the padding
// can be scrolled into view. This matches Blink and WebKit (block axis only).
// See Mozilla bugs 748518 and 1527949.
// REVIEW: For now, we do this for both visual and scrollable area,
// although when we make scrollable overflow area not be a subset of
// visual, we can change this.
// XXX Currently, overflow areas are stored as physical rects, so we have
// to handle writing modes explicitly here. If we change overflow rects
// to be stored logically, this can be simplified again.
const nscoord bEndEdge = aBEndEdgeOfChildren + aBEndPadding;
if (aWritingMode.IsVertical()) {
if (aWritingMode.IsVerticalLR()) {
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
nsRect& o = aOverflowAreas.Overflow(otype);
o.width = std::max(o.XMost(), aBEndEdgeOfChildren) - o.x;
o.width = std::max(o.XMost(), bEndEdge) - o.x;
}
} else {
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
nsRect& o = aOverflowAreas.Overflow(otype);
nscoord xmost = o.XMost();
o.x = std::min(o.x, xmost - aBEndEdgeOfChildren);
o.x = std::min(o.x, xmost - bEndEdge);
o.width = xmost - o.x;
}
}
} else {
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
nsRect& o = aOverflowAreas.Overflow(otype);
o.height = std::max(o.YMost(), aBEndEdgeOfChildren) - o.y;
o.height = std::max(o.YMost(), bEndEdge) - o.y;
}
}
}
@@ -1773,8 +1779,10 @@ nsBlockFrame::ComputeOverflowAreas(const nsRect& aBounds,
areas.UnionAllWith(outsideBullet->GetRect());
}
ConsiderBlockEndEdgeOfChildren(GetWritingMode(),
aBEndEdgeOfChildren, areas);
const WritingMode wm = GetWritingMode();
ConsiderBlockEndEdgeOfChildren(
wm, aBEndEdgeOfChildren,
LogicalMargin(wm, GetUsedPadding()).BEnd(wm), areas);
}
#ifdef NOISY_COMBINED_AREA
@@ -1831,8 +1839,10 @@ nsBlockFrame::ComputeCustomOverflow(nsOverflowAreas& aOverflowAreas)
nscoord blockEndEdgeOfChildren =
GetProperty(BlockEndEdgeOfChildrenProperty(), &found);
if (found) {
ConsiderBlockEndEdgeOfChildren(GetWritingMode(),
blockEndEdgeOfChildren, aOverflowAreas);
const WritingMode wm = GetWritingMode();
ConsiderBlockEndEdgeOfChildren(
wm, blockEndEdgeOfChildren,
LogicalMargin(wm, GetUsedPadding()).BEnd(wm), aOverflowAreas);
}
// Line cursor invariants depend on the overflow areas of the lines, so
+198 -11
View File
@@ -5082,8 +5082,11 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
nscoord boxSizingToMarginEdgeISize =
aMargin.ISize(aWM) + aBorder.ISize(aWM) + aPadding.ISize(aWM) -
boxSizingAdjust.ISize(aWM);
nscoord boxSizingToMarginEdgeBSize =
aMargin.BSize(aWM) + aBorder.BSize(aWM) + aPadding.BSize(aWM) -
boxSizingAdjust.BSize(aWM);
nscoord iSize, minISize, maxISize, bSize, minBSize, maxBSize;
nscoord iSize, minISize, maxISize, minBSize, maxBSize, bSize = NS_UNCONSTRAINEDSIZE;
enum class Stretch {
// stretch to fill the CB (preserving intrinsic ratio) in the relevant axis
eStretchPreservingRatio,
@@ -5260,6 +5263,98 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
// Now calculate the used values for iSize and bSize:
// A button reports eReplaced, but it is really a container: its block size
// comes from its anonymous content, which is only known at reflow, so it is
// not a replaced box in the sizing sense. Treat it as content-sized
// everywhere below instead of special-casing it at each site.
const bool contentSizedButton =
GetType() == nsGkAtoms::HTMLButtonControlFrame;
const bool replacedWithIntrinsicSize =
IsFrameOfType(nsIFrame::eReplaced) && !contentSizedButton;
// A ratio only determines an axis that is neither definite nor stretched.
// Cross-axis stretch on a flex item wins over the ratio (css-flexbox §9.4),
// so mark the stretch and seed the size from the containing block; the
// existing 'stretch' handling below then skips the ratio transfer.
if (MOZ_UNLIKELY(isFlexItem) && !replacedWithIntrinsicSize) {
uint8_t alignSelf = stylePos->mAlignSelf;
if (alignSelf == NS_STYLE_ALIGN_AUTO) {
alignSelf = GetParent()->StylePosition()->mAlignItems;
}
if (alignSelf == NS_STYLE_ALIGN_NORMAL ||
alignSelf == NS_STYLE_ALIGN_STRETCH) {
if (isInlineFlexItem) {
// Row container: the cross axis is the block axis.
if (isAutoBSize && aCBSize.BSize(aWM) != NS_UNCONSTRAINEDSIZE) {
stretchB = eStretch;
bSize = aCBSize.BSize(aWM) - boxSizingToMarginEdgeBSize;
if (bSize < 0) {
bSize = 0;
}
}
} else if (isAutoISize && aCBSize.ISize(aWM) != NS_UNCONSTRAINEDSIZE) {
// Column container: the cross axis is the inline axis.
stretchI = eStretch;
iSize = aCBSize.ISize(aWM) - boxSizingToMarginEdgeISize;
if (iSize < 0) {
iSize = 0;
}
}
}
}
// An inline size is shrink-wrapped when the caller says so (flex items,
// non-stretched grid items, floats), when the box is replaced, or when it is
// inline-level -- an inline-block's auto width is shrink-to-fit whatever the
// caller passed. This function had no shrink-to-fit path at all, so an
// aspect-ratio box fell through to the containing block width instead.
const bool shrinkWrapInline = !replacedWithIntrinsicSize &&
((aFlags & ComputeSizeFlags::eShrinkWrap) ||
StyleDisplay()->IsInlineOutside(this));
// The block axis is content-sized as well. The base ComputeAutoSize reports
// NS_UNCONSTRAINEDSIZE for it and the frames that can be sized this way
// shrinkwrap their content during reflow, so the sentinel has to be allowed
// through instead of being turned into a ratio-derived value here. The clamp
// below is skipped whenever this is set, since it would bake the sentinel in.
const bool shrinkWrapBlock = shrinkWrapInline;
// Content-based block size with a ratio fallback. Only the column item below
// needs the fallback, because there the block size is the main axis and feeds
// iSizeFromBSize, which cannot digest the sentinel.
auto contentBSize = [&](nscoord aTentISize) -> nscoord {
nscoord bSize =
ComputeAutoSize(aRenderingContext, aWM, aCBSize, aCBSize.ISize(aWM),
aMargin, aBorder, aPadding, aFlags).BSize(aWM);
if (bSize != NS_UNCONSTRAINEDSIZE) {
return bSize;
}
nsIFrame* buttonContent = PrincipalChildList().FirstChild();
if (contentSizedButton && buttonContent) {
// A button's block-size is its anonymous content. Measure it.
nsPresContext* pc = PresContext();
nsContainerFrame* self = static_cast<nsContainerFrame*>(this);
LogicalSize availSize(aWM, aCBSize.ISize(aWM), NS_UNCONSTRAINEDSIZE);
ReflowInput childRI(pc, buttonContent, aRenderingContext, availSize,
ReflowInput::DUMMY_PARENT_REFLOW_STATE);
childRI.mFlags.mIsFlexContainerMeasuringHeight = true;
ReflowOutput desired(childRI);
nsReflowStatus status;
self->ReflowChild(buttonContent, pc, desired, childRI, 0, 0,
ReflowChildFlags::NoMoveFrame, status);
nsContainerFrame::FinishReflowChild(buttonContent, pc, desired, &childRI,
0, 0, ReflowChildFlags::NoMoveFrame);
nscoord measured =
desired.BSize(aWM) - childRI.ComputedLogicalBorderPadding().BStartEnd(aWM);
if (measured > 0) {
return measured;
}
}
return bSizeFromISize(aTentISize);
};
if (isAutoISize) {
if (isAutoBSize) {
@@ -5267,6 +5362,11 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
// Get tentative values - CSS 2.1 sections 10.3.2 and 10.6.2:
// Set when tentBSize was derived from the ratio, so that the min/max
// clamp further down stays enabled for the transferred value even when
// the block axis is being shrink-wrapped.
bool ratioTransferred = false;
nscoord tentISize, tentBSize;
if (hasIntrinsicISize) {
@@ -5274,7 +5374,18 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
} else if (hasIntrinsicBSize && aIntrinsicRatio) {
tentISize = iSizeFromBSize(intrinsicBSize);
} else if (aIntrinsicRatio) {
tentISize = aCBSize.ISize(aWM) - boxSizingToMarginEdgeISize; // XXX scrollbar?
// A preferred aspect ratio but no intrinsic size in either axis: the
// inline size comes from the normal inline-size algorithm for this box
// and the block size is transferred from it below.
if (shrinkWrapInline) {
// Shrink-to-fit (CSS 2.1 §10.3.9): min(max(min-content, available),
// max-content), via the frame's own auto-size logic.
tentISize =
ComputeAutoSize(aRenderingContext, aWM, aCBSize, aCBSize.ISize(aWM),
aMargin, aBorder, aPadding, aFlags).ISize(aWM);
} else {
tentISize = aCBSize.ISize(aWM) - boxSizingToMarginEdgeISize; // XXX scrollbar?
}
if (tentISize < 0) tentISize = 0;
} else {
tentISize = nsPresContext::CSSPixelsToAppUnits(300);
@@ -5290,8 +5401,53 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
if (hasIntrinsicBSize) {
tentBSize = intrinsicBSize;
} else if (aIntrinsicRatio) {
} else if (aIntrinsicRatio && !shrinkWrapBlock) {
tentBSize = bSizeFromISize(tentISize);
ratioTransferred = true;
} else if (aIntrinsicRatio) {
// Shrink-wrapping with a ratio but no intrinsic size: transfer the
// ratio only from an inline size that is actually definite. A
// shrink-to-fit inline size is content-dependent, so the block size
// has to stay content-based there. Never leave NS_UNCONSTRAINEDSIZE
// behind either way.
const bool columnItem = isFlexItem && !isInlineFlexItem;
bool stretchesCrossAxis = false;
if (columnItem && aCBSize.ISize(aWM) != NS_UNCONSTRAINEDSIZE) {
auto alignSelf = stylePos->UsedAlignSelf(GetParent()->StyleContext());
stretchesCrossAxis =
alignSelf == NS_STYLE_ALIGN_NORMAL ||
alignSelf == NS_STYLE_ALIGN_STRETCH;
}
if (stretchesCrossAxis) {
// Cross axis is definite, so the ratio may drive the main axis.
tentBSize = bSizeFromISize(aCBSize.ISize(aWM));
ratioTransferred = true;
} else if (columnItem) {
// Column item, cross axis not stretched: the block axis is the
// content-sized main axis, so transfer the ratio the other way.
nscoord cbSize = contentBSize(tentISize);
tentBSize = cbSize;
tentISize = iSizeFromBSize(cbSize);
ratioTransferred = true;
} else {
// Content-sized box. A button's block size is the business of
// ReflowButtonContents, which shrinkwraps the anonymous content and
// clamps to min/max, so it wants the sentinel rather than a value
// guessed here. That only works where the consumer hands the sentinel
// on to reflow as 'auto': a float is placed from the returned size
// directly, and a stretched cross axis overwrites it after the fact
// (and skips the measuring reflow that would otherwise resolve it),
// so both need a real number. Everything else -- non-buttons, whose
// sentinel would escape into float placement and line overflow --
// takes the ratio fallback.
// tentISize > 0 is not a usable proxy for "definite": empty content
// is 0, not missing, and used to fall back to the CB width.
if (contentSizedButton && stretchB != eStretch && !IsFloating()) {
tentBSize = NS_UNCONSTRAINEDSIZE;
} else {
tentBSize = contentBSize(tentISize);
}
}
} else {
tentBSize = nsPresContext::CSSPixelsToAppUnits(150);
}
@@ -5334,7 +5490,7 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
// ComputeAutoSizeWithIntrinsicDimensions preserves the ratio when applying
// the min/max-size. We don't want that when we have 'stretch' in either
// axis because tentISize/tentBSize is likely not according to ratio now.
if (aIntrinsicRatio && stretchI != eStretch && stretchB != eStretch) {
if (aIntrinsicRatio && (!shrinkWrapBlock || ratioTransferred)) {
nsSize autoSize = nsLayoutUtils::
ComputeAutoSizeWithIntrinsicDimensions(minISize, minBSize,
maxISize, maxBSize,
@@ -5375,8 +5531,18 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
// non-'auto' iSize, 'auto' bSize
iSize = NS_CSS_MINMAX(iSize, minISize, maxISize);
if (stretchB != eStretch) {
if (aIntrinsicRatio) {
// The ratio may only be transferred from a flex-resolved main size when
// the block size is really ratio-derived. A content-sized box gets its
// block size from reflow instead, so the transfer has to be skipped.
const bool contentMainSize =
isFlexItem && isInlineFlexItem &&
imposedMainSizeStyleCoord.isSome();
if (aIntrinsicRatio && !contentMainSize) {
bSize = bSizeFromISize(iSize);
} else if (contentMainSize) {
bSize =
ComputeAutoSize(aRenderingContext, aWM, aCBSize, aCBSize.ISize(aWM),
aMargin, aBorder, aPadding, aFlags).BSize(aWM);
} else if (hasIntrinsicBSize) {
if (!((aFlags & ComputeSizeFlags::eBClampMarginBoxMinSize) &&
intrinsicBSize > bSize)) {
@@ -5397,6 +5563,14 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
}
}
// A button handed its unconstrained block-size on for reflow, which treats it
// as 'auto'. Flex, grid and floats can't wait for that, so substitute the
// content-based size here as a backstop.
if (bSize == NS_UNCONSTRAINEDSIZE &&
(isFlexItem || isGridItem || IsFloating())) {
bSize = contentBSize(iSize);
}
return LogicalSize(aWM, iSize, bSize);
}
@@ -8527,7 +8701,9 @@ UnionBorderBoxes(nsIFrame* aFrame, bool aApplyTransform,
}
const nsStyleDisplay* disp = aFrame->StyleDisplay();
nsIAtom* fType = aFrame->GetType();
if (nsFrame::ShouldApplyOverflowClipping(aFrame, disp) ||
const bool clipsBoth = disp->mOverflowX != NS_STYLE_OVERFLOW_VISIBLE &&
disp->mOverflowY != NS_STYLE_OVERFLOW_VISIBLE;
if (clipsBoth ||
fType == nsGkAtoms::scrollFrame ||
fType == nsGkAtoms::listControlFrame ||
fType == nsGkAtoms::svgOuterSVGFrame) {
@@ -8756,12 +8932,23 @@ nsIFrame::FinishAndStoreOverflow(nsOverflowAreas& aOverflowAreas,
// overflow area should include the entire border-box, just set it to
// the border-box here.
const nsStyleDisplay* disp = StyleDisplay();
NS_ASSERTION((disp->mOverflowY == NS_STYLE_OVERFLOW_CLIP) ==
(disp->mOverflowX == NS_STYLE_OVERFLOW_CLIP),
"If one overflow is clip, the other should be too");
if (nsFrame::ShouldApplyOverflowClipping(this, disp)) {
// The contents are actually clipped to the padding area
aOverflowAreas.SetAllTo(bounds);
// The contents are actually clipped to the padding area. Clipping is
// per-axis (css-overflow-3 §3.1 keeps 'visible clip' as authored), so only
// collapse the axis that clips; the other keeps contributing overflow.
const bool clipsX = disp->mOverflowX != NS_STYLE_OVERFLOW_VISIBLE;
const bool clipsY = disp->mOverflowY != NS_STYLE_OVERFLOW_VISIBLE;
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
nsRect& o = aOverflowAreas.Overflow(otype);
if (clipsX) {
o.x = bounds.x;
o.width = bounds.width;
}
if (clipsY) {
o.y = bounds.y;
o.height = bounds.height;
}
}
}
// Overflow area must always include the frame's top-left and bottom-right,
+40 -30
View File
@@ -132,6 +132,30 @@ int32_t DoCompare(Numeric a, Numeric b)
return 1;
}
static bool
IsRatioComponent(const nsCSSValue& aValue)
{
return aValue.GetUnit() == eCSSUnit_Number ||
aValue.GetUnit() == eCSSUnit_Integer;
}
static bool
IsRatioValue(const nsCSSValue& aValue)
{
return aValue.GetUnit() == eCSSUnit_Array &&
aValue.GetArrayValue()->Count() == 2 &&
IsRatioComponent(aValue.GetArrayValue()->Item(0)) &&
IsRatioComponent(aValue.GetArrayValue()->Item(1));
}
static double
RatioComponentValue(const nsCSSValue& aValue, uint32_t aIndex)
{
const nsCSSValue& v = aValue.GetArrayValue()->Item(aIndex);
return v.GetUnit() == eCSSUnit_Integer ? double(v.GetIntValue())
: double(v.GetFloatValue());
}
struct MediaQueryTypedCalcLengthResult
{
double mValue;
@@ -348,31 +372,19 @@ nsMediaExpression::Matches(nsPresContext *aPresContext,
}
break;
case nsMediaFeature::eIntRatio:
case nsMediaFeature::eFloatRatio:
{
NS_ASSERTION(actual.GetUnit() == eCSSUnit_Array &&
actual.GetArrayValue()->Count() == 2 &&
actual.GetArrayValue()->Item(0).GetUnit() ==
eCSSUnit_Integer &&
actual.GetArrayValue()->Item(1).GetUnit() ==
eCSSUnit_Integer,
"bad actual value");
NS_ASSERTION(required.GetUnit() == eCSSUnit_Array &&
required.GetArrayValue()->Count() == 2 &&
required.GetArrayValue()->Item(0).GetUnit() ==
eCSSUnit_Integer &&
required.GetArrayValue()->Item(1).GetUnit() ==
eCSSUnit_Integer,
"bad required value");
// Convert to int64_t so we can multiply without worry. Note
NS_ASSERTION(IsRatioValue(actual), "bad actual value");
NS_ASSERTION(IsRatioValue(required), "bad required value");
// Convert to double so we can multiply without worry. Note
// that while the spec requires that both halves of |required|
// be positive, the numerator or denominator of |actual| might
// be zero (e.g., when testing 'aspect-ratio' on a 0-width or
// 0-height iframe).
int64_t actualNum = actual.GetArrayValue()->Item(0).GetIntValue(),
actualDen = actual.GetArrayValue()->Item(1).GetIntValue(),
requiredNum = required.GetArrayValue()->Item(0).GetIntValue(),
requiredDen = required.GetArrayValue()->Item(1).GetIntValue();
cmp = DoCompare(actualNum * requiredDen, requiredNum * actualDen);
cmp = DoCompare(RatioComponentValue(actual, 0) *
RatioComponentValue(required, 1),
RatioComponentValue(required, 0) *
RatioComponentValue(actual, 1));
}
break;
case nsMediaFeature::eResolution:
@@ -680,20 +692,18 @@ nsMediaQuery::AppendToString(nsAString& aString) const
}
break;
case nsMediaFeature::eIntRatio:
case nsMediaFeature::eFloatRatio:
{
NS_ASSERTION(expr.mValue.GetUnit() == eCSSUnit_Array,
"bad unit");
nsCSSValue::Array *array = expr.mValue.GetArrayValue();
NS_ASSERTION(IsRatioValue(expr.mValue), "bad unit");
nsCSSValue::Array* array = expr.mValue.GetArrayValue();
NS_ASSERTION(array->Count() == 2, "unexpected length");
NS_ASSERTION(array->Item(0).GetUnit() == eCSSUnit_Integer,
"bad unit");
NS_ASSERTION(array->Item(1).GetUnit() == eCSSUnit_Integer,
"bad unit");
array->Item(0).AppendToString(eCSSProperty_z_index, aString,
nsCSSValue::eNormalized);
aString.Append('/');
array->Item(1).AppendToString(eCSSProperty_z_index, aString,
nsCSSValue::eNormalized);
if (RatioComponentValue(expr.mValue, 1) != 1.0) {
aString.Append('/');
array->Item(1).AppendToString(eCSSProperty_z_index, aString,
nsCSSValue::eNormalized);
}
}
break;
case nsMediaFeature::eResolution:
+2 -2
View File
@@ -271,7 +271,7 @@ StyleSheet::AddAdopter(nsIDocument* aDocument)
{
MOZ_ASSERT(IsConstructed());
MOZ_ASSERT(aDocument);
MOZ_ASSERT(ConstructorDocument() == aDocument);
MOZ_ASSERT_IF(ConstructorDocument(), ConstructorDocument() == aDocument);
if (mAdoptionCount++ == 0) {
SetAssociatedDocument(aDocument, NotOwnedByDocument);
@@ -292,7 +292,7 @@ StyleSheet::RemoveAdopter(nsIDocument* aDocument)
{
MOZ_ASSERT(IsConstructed());
MOZ_ASSERT(aDocument);
MOZ_ASSERT(ConstructorDocument() == aDocument);
MOZ_ASSERT_IF(ConstructorDocument(), ConstructorDocument() == aDocument);
MOZ_ASSERT(mAdoptionCount > 0);
if (mAdoptionCount == 0) {
+217 -31
View File
@@ -74,6 +74,7 @@ static bool sMozGradientsEnabled;
static bool sControlCharVisibility;
static bool sLegacyNegationPseudoClassEnabled;
static bool sCascadeLayersEnabled;
static bool sContainerQueriesEnabled;
static bool sNestingEnabled;
const uint32_t
@@ -1037,6 +1038,7 @@ protected:
bool ParseMediaQuery(eMediaQueryType aMode, nsMediaQuery **aQuery,
bool *aHitStop);
bool ParseMediaQueryExpression(nsMediaQuery* aQuery);
bool ParseRatioComponent(nsCSSValue& aValue);
void ProcessImport(const nsString& aURLSpec,
nsMediaList* aMedia,
const nsString& aLayerName,
@@ -1047,6 +1049,7 @@ protected:
bool ParseGroupRule(css::GroupRule* aRule, RuleAppendFunc aAppendFunc,
void* aProcessData);
bool ParseMediaRule(RuleAppendFunc aAppendFunc, void* aProcessData);
bool ParseContainerRule(RuleAppendFunc aAppendFunc, void* aProcessData);
bool ParseMozDocumentRule(RuleAppendFunc aAppendFunc, void* aProcessData);
bool ParseNameSpaceRule(RuleAppendFunc aAppendFunc, void* aProcessData);
void ProcessNameSpace(const nsString& aPrefix,
@@ -3846,6 +3849,11 @@ CSSParserImpl::ParseAtRule(RuleAppendFunc aAppendFunc,
parseFunc = &CSSParserImpl::ParseSupportsRule;
newSection = eCSSSection_General;
} else if (mToken.mIdent.LowerCaseEqualsLiteral("container") &&
sContainerQueriesEnabled) {
parseFunc = &CSSParserImpl::ParseContainerRule;
newSection = eCSSSection_General;
} else if (mToken.mIdent.LowerCaseEqualsLiteral("layer") &&
sCascadeLayersEnabled) {
parseFunc = &CSSParserImpl::ParseLayerRule;
@@ -4100,6 +4108,30 @@ CSSParserImpl::GatherMedia(nsMediaList* aMedia,
return true;
}
bool
CSSParserImpl::ParseRatioComponent(nsCSSValue& aValue)
{
if (!GetToken(true)) {
return false;
}
if (mToken.mType != eCSSToken_Number) {
UngetToken();
return false;
}
// <ratio> components are non-negative (Media Queries 4). Note that
// negative values tokenize as a Number with a sign, so this also
// catches "-1".
if (mToken.mNumber < 0.0f) {
UngetToken();
return false;
}
aValue.SetFloatValue(mToken.mNumber, eCSSUnit_Number);
return true;
}
bool
CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery)
{
@@ -4233,15 +4265,33 @@ CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery)
break;
case nsMediaFeature::eInteger:
case nsMediaFeature::eBoolInteger:
rv = ParseNonNegativeInteger(expr->mValue);
// Enforce extra restrictions for eBoolInteger
if (rv &&
feature->mValueType == nsMediaFeature::eBoolInteger &&
expr->mValue.GetIntValue() > 1)
rv = false;
{
CSSParseResult result =
ParseNonNegativeVariant(expr->mValue, VARIANT_INTEGER, nullptr);
if (result == CSSParseResult::Error) {
// The failing calc() parse already consumed the closing paren, so the
// shared SkipUntil(')') below would skip past it.
REPORT_UNEXPECTED(PEMQExpectedFeatureValue);
return false;
}
rv = result == CSSParseResult::Ok;
// Enforce extra restrictions for eBoolInteger
if (rv &&
feature->mValueType == nsMediaFeature::eBoolInteger &&
expr->mValue.GetIntValue() > 1)
rv = false;
}
break;
case nsMediaFeature::eFloat:
rv = ParseNonNegativeNumber(expr->mValue);
{
CSSParseResult result =
ParseNonNegativeVariant(expr->mValue, VARIANT_NUMBER, nullptr);
if (result == CSSParseResult::Error) {
REPORT_UNEXPECTED(PEMQExpectedFeatureValue);
return false;
}
rv = result == CSSParseResult::Ok;
}
break;
case nsMediaFeature::eIntRatio:
{
@@ -4259,6 +4309,22 @@ CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery)
a->Item(1).GetIntValue() > 0;
}
break;
case nsMediaFeature::eFloatRatio:
{
// <ratio> = <number [0,inf]> [ / <number [0,inf]> ]?
// The denominator defaults to 1 when the '/' is absent.
RefPtr<nsCSSValue::Array> a = nsCSSValue::Array::Create(2);
expr->mValue.SetArrayValue(a, eCSSUnit_Array);
rv = ParseRatioComponent(a->Item(0));
if (rv) {
if (ExpectSymbol('/', true)) {
rv = ParseRatioComponent(a->Item(1));
} else {
a->Item(1).SetFloatValue(1.0f, eCSSUnit_Number);
}
}
}
break;
case nsMediaFeature::eResolution:
rv = GetToken(true);
if (!rv)
@@ -4431,6 +4497,56 @@ CSSParserImpl::ParseMediaRule(RuleAppendFunc aAppendFunc, void* aData)
return false;
}
// Parse a container rule: "@container [<container-name>] <container-condition>
// { ... }"
//
// Stopgap: the condition is parsed as a media query list and the rule is kept
// as an ordinary css::MediaRule. Containers are not resolved and containment
// is not established, so the condition is evaluated against the viewport.
// That is close enough for page-level containers, which is what sites actually
// gate layout on; per-element containers will be wrong.
bool
CSSParserImpl::ParseContainerRule(RuleAppendFunc aAppendFunc, void* aData)
{
if (!GetToken(true)) {
return false;
}
// Optional <container-name>. A condition can start with "not"/"and"/"or" or
// with a "(" or "style(" token, so only a plain ident that is none of those
// keywords can be a name.
if (eCSSToken_Ident == mToken.mType &&
!mToken.mIdent.LowerCaseEqualsLiteral("not") &&
!mToken.mIdent.LowerCaseEqualsLiteral("and") &&
!mToken.mIdent.LowerCaseEqualsLiteral("or")) {
if (!GetToken(true)) {
return false;
}
}
UngetToken();
uint32_t linenum, colnum;
if (!GetNextTokenLocation(true, &linenum, &colnum)) {
return false;
}
RefPtr<nsMediaList> media = new nsMediaList();
if (!GatherMedia(media, true)) {
// Not expressible as a media query list (style() queries, range syntax).
// Don't lose the block over it - make the rule unconditional.
media = new nsMediaList();
SkipUntil('{');
UngetToken();
}
RefPtr<css::MediaRule> rule = new css::MediaRule(linenum, colnum);
if (ParseGroupRule(rule, aAppendFunc, aData)) {
rule->SetMedia(media);
return true;
}
return false;
}
// Parse a @-moz-document rule. This is like an @media rule, but instead
// of a medium it has a nonempty list of items where each item is either
// url(), url-prefix(), or domain().
@@ -4856,12 +4972,23 @@ CSSParserImpl::ParseFontFeatureValueSet(nsCSSFontFeatureValuesRule
AutoTArray<uint32_t,4> featureSelectors;
nsCSSValue intValue;
while (ParseNonNegativeInteger(intValue)) {
CSSParseResult valueResult;
while ((valueResult = ParseNonNegativeVariant(intValue, VARIANT_INTEGER, nullptr)) ==
CSSParseResult::Ok) {
featureSelectors.AppendElement(uint32_t(intValue.GetIntValue()));
}
int32_t numValues = featureSelectors.Length();
if (valueResult == CSSParseResult::Error) {
REPORT_UNEXPECTED_TOKEN(PEFFVExpectedValue);
OUTPUT_ERROR();
if (!SkipDeclaration(true)) {
break;
}
continue;
}
if (numValues == 0) {
REPORT_UNEXPECTED_TOKEN(PEFFVExpectedValue);
OUTPUT_ERROR();
@@ -5986,9 +6113,15 @@ CSSParserImpl::ParseCounterDescriptorValue(nsCSSCounterDesc aDescID,
case eCSSCounterDesc_Pad: {
nsCSSValue width, symbol;
bool hasWidth = ParseNonNegativeInteger(width);
CSSParseResult widthResult =
ParseNonNegativeVariant(width, VARIANT_INTEGER, nullptr);
if (widthResult == CSSParseResult::Error) {
return false;
}
bool hasWidth = widthResult == CSSParseResult::Ok;
if (!ParseSingleTokenVariant(symbol, VARIANT_COUNTER_SYMBOL, nullptr) ||
(!hasWidth && !ParseNonNegativeInteger(width))) {
(!hasWidth && ParseNonNegativeVariant(width, VARIANT_INTEGER, nullptr) !=
CSSParseResult::Ok)) {
return false;
}
aValue.SetPairValue(width, symbol);
@@ -6021,9 +6154,15 @@ CSSParserImpl::ParseCounterDescriptorValue(nsCSSCounterDesc aDescID,
int32_t lastWeight = -1;
for (;;) {
nsCSSValue weight, symbol;
bool hasWeight = ParseNonNegativeInteger(weight);
CSSParseResult weightResult =
ParseNonNegativeVariant(weight, VARIANT_INTEGER, nullptr);
if (weightResult == CSSParseResult::Error) {
return false;
}
bool hasWeight = weightResult == CSSParseResult::Ok;
if (!ParseSingleTokenVariant(symbol, VARIANT_COUNTER_SYMBOL, nullptr) ||
(!hasWeight && !ParseNonNegativeInteger(weight))) {
(!hasWeight && ParseNonNegativeVariant(weight, VARIANT_INTEGER, nullptr) !=
CSSParseResult::Ok)) {
return false;
}
if (lastWeight != -1 && weight.GetIntValue() >= lastWeight) {
@@ -8240,12 +8379,18 @@ CSSParserImpl::ParseHue(float& aAngle)
}
UngetToken();
// <angle>
// <angle>, or a calc() that may reduce to a plain <number>. A hue accepts
// unitless numbers as degrees, so VARIANT_NUMBER has to be in the calc mask
// or calc(<number>) dies in ParseCalc and surfaces as CSSParseResult::Error.
nsCSSValue angleValue;
// The '0' value is handled by <number> parsing, so use VARIANT_ANGLE flag
// instead of VARIANT_ANGLE_OR_ZERO.
if (ParseSingleTokenVariant(angleValue, VARIANT_ANGLE, nullptr)) {
aAngle = angleValue.GetAngleValueInDegrees();
CSSParseResult result =
ParseVariant(angleValue, VARIANT_ANGLE | VARIANT_NUMBER, nullptr);
if (result == CSSParseResult::Ok) {
if (angleValue.GetUnit() == eCSSUnit_Number) {
aAngle = angleValue.GetFloatValue();
} else {
aAngle = angleValue.GetAngleValueInDegrees();
}
return true;
}
@@ -9732,13 +9877,13 @@ CSSParserImpl::ParseImageRect(nsCSSValue& aImage)
}
static const int32_t VARIANT_SIDE = VARIANT_NUMBER | VARIANT_PERCENT;
if (!ParseSingleTokenNonNegativeVariant(top, VARIANT_SIDE, nullptr) ||
if (ParseNonNegativeVariant(top, VARIANT_SIDE, nullptr) != CSSParseResult::Ok ||
!ExpectSymbol(',', true) ||
!ParseSingleTokenNonNegativeVariant(right, VARIANT_SIDE, nullptr) ||
ParseNonNegativeVariant(right, VARIANT_SIDE, nullptr) != CSSParseResult::Ok ||
!ExpectSymbol(',', true) ||
!ParseSingleTokenNonNegativeVariant(bottom, VARIANT_SIDE, nullptr) ||
ParseNonNegativeVariant(bottom, VARIANT_SIDE, nullptr) != CSSParseResult::Ok ||
!ExpectSymbol(',', true) ||
!ParseSingleTokenNonNegativeVariant(left, VARIANT_SIDE, nullptr) ||
ParseNonNegativeVariant(left, VARIANT_SIDE, nullptr) != CSSParseResult::Ok ||
!ExpectSymbol(')', true))
break;
@@ -9842,12 +9987,30 @@ CSSParserImpl::ParseFlex()
// Record what we just parsed as either flex-basis or flex-grow:
bool wasFirstComponentFlexBasis = (tmpVal.GetUnit() != eCSSUnit_Number);
if (wasFirstComponentFlexBasis && tmpVal.IsCalcUnit()) {
int32_t exponent;
if (GetCalcLengthTypedArithmeticExponent(tmpVal, exponent) &&
exponent == 0) {
// A calc() that reduces to a plain <number> is a flex factor, not a
// <flex-basis>; the basis mask preserved the calc unit.
ReduceNumberCalcOps ops;
float value = mozilla::css::ComputeCalc(tmpVal, ops);
// ParseNonNegativeVariant cannot sign-check a calc: the unit is
// eCSSUnit_Calc, so none of its arms match and it returns Ok.
if (value < 0.0f) {
return false;
}
tmpVal.SetFloatValue(value, eCSSUnit_Number);
wasFirstComponentFlexBasis = false;
}
}
(wasFirstComponentFlexBasis ? flexBasis : flexGrow) = tmpVal;
// (b) If we didn't get flex-grow yet, parse _next_ component as flex-grow.
bool doneParsing = false;
if (wasFirstComponentFlexBasis) {
if (ParseNonNegativeNumber(tmpVal)) {
if (ParseNonNegativeVariant(tmpVal, VARIANT_NUMBER, nullptr) ==
CSSParseResult::Ok) {
flexGrow = tmpVal;
} else {
// Failed to parse anything after our flex-basis -- that's fine. We can
@@ -9859,8 +10022,23 @@ CSSParserImpl::ParseFlex()
if (!doneParsing) {
// (c) OK -- the last thing we parsed was flex-grow, so look for a
// flex-shrink in the next position.
if (ParseNonNegativeNumber(tmpVal)) {
CSSParserInputState stateBeforeShrink;
SaveInputState(stateBeforeShrink);
CSSParseResult shrinkResult =
ParseNonNegativeVariant(tmpVal, VARIANT_NUMBER, nullptr);
if (shrinkResult == CSSParseResult::Ok) {
// Same gap: a negative calc passed the wrapper. It is already folded to
// a number here, since VARIANT_NUMBER is not a preserving mask. Reject
// outright rather than rewinding, or the basis arm below would take the
// dimensionless calc as a <flex-basis> and accept it.
if (tmpVal.GetFloatValue() < 0.0f) {
return false;
}
flexShrink = tmpVal;
} else if (shrinkResult == CSSParseResult::Error) {
// A calc() whose contents aren't a <number>; the failed parse consumed
// it. Rewind so the <flex-basis> step below can try it instead.
RestoreSavedInputState(stateBeforeShrink);
}
// d) Finally: If we didn't get flex-basis at the beginning, try to parse
@@ -9885,10 +10063,10 @@ CSSParserImpl::ParseFlex()
CSSParseResult result =
ParseNonNegativeVariant(tmpVal, flexBasisVariantMask,
nsCSSProps::kFlexBasisKTable);
if (result == CSSParseResult::Error) {
return false;
} else if (result == CSSParseResult::Ok) {
flexBasis = tmpVal;
if (result == CSSParseResult::Error) {
return false;
} else if (result == CSSParseResult::Ok) {
flexBasis = tmpVal;
}
}
}
@@ -11310,11 +11488,17 @@ CSSParserImpl::ParseInitialLetter()
if (!ParseSingleTokenVariant(value, VARIANT_INHERIT | VARIANT_NORMAL,
nullptr)) {
nsCSSValue first, second;
if (!ParseOneOrLargerNumber(first)) {
if (ParseOneOrLargerVariant(first, VARIANT_NUMBER, nullptr) !=
CSSParseResult::Ok) {
return false;
}
if (!ParseOneOrLargerInteger(second)) {
CSSParseResult secondResult =
ParseOneOrLargerVariant(second, VARIANT_INTEGER, nullptr);
if (secondResult == CSSParseResult::Error) {
return false;
}
if (secondResult != CSSParseResult::Ok) {
AppendValue(eCSSProperty_initial_letter, first);
return true;
} else {
@@ -18725,8 +18909,8 @@ CSSParserImpl::ParseTransitionStepTimingFunctionValues(nsCSSValue& aValue)
RefPtr<nsCSSValue::Array> val = nsCSSValue::Array::Create(2);
if (!ParseSingleTokenOneOrLargerVariant(val->Item(0), VARIANT_INTEGER,
nullptr)) {
if (ParseOneOrLargerVariant(val->Item(0), VARIANT_INTEGER, nullptr) !=
CSSParseResult::Ok) {
return false;
}
@@ -19797,6 +19981,8 @@ nsCSSParser::Startup()
"layout.css.legacy-negation-pseudo.enabled");
Preferences::AddBoolVarCache(&sCascadeLayersEnabled,
"layout.css.cascade-layers.enabled");
Preferences::AddBoolVarCache(&sContainerQueriesEnabled,
"layout.css.container-queries.enabled");
Preferences::AddBoolVarCache(&sNestingEnabled,
"layout.css.nesting.enabled");
}
+131 -28
View File
@@ -10,6 +10,7 @@
#include "mozilla/dom/ShadowRoot.h"
#include "nsIMozBrowserFrame.h"
#include "nsGkAtoms.h"
#include "mozilla/HashFunctions.h"
#include "nsRuleWalker.h"
#include "nsStyleUtil.h"
#include "StyleRule.h"
@@ -1924,11 +1925,17 @@ bool
HasSelectorDependency::MightDependOnChange(
Element* aAnchor, nsINode* aNode, EventStates aStateMask,
nsIAtom* aAttribute, const nsAttrValue* aNewClasses,
bool aCompareClasses, bool aSibling, bool aNodeIsFollowingSibling) const
bool aCompareClasses, bool aSibling, bool aNodeIsFollowingSibling,
bool aIsTextChange) const
{
bool contentChange = !aAttribute && aStateMask.IsEmpty();
for (const auto& branch : mBranches) {
if (branch.IsSibling() != aSibling ||
if ((aIsTextChange && branch.mIsLocal) ||
branch.IsSibling() != aSibling ||
// A sibling-relative selector can only observe siblings before the
// changed subtree. This also applies when the selector continues
// into that sibling's descendants, not just to a local compound.
(aSibling && branch.IsSibling() && !aNodeIsFollowingSibling) ||
(!aStateMask.IsEmpty() &&
!branch.mStates.HasAtLeastOneOfStates(aStateMask)) ||
(aAttribute && !branch.MightDependOnAttribute(
@@ -1971,11 +1978,12 @@ bool
HasSelectorDependency::MightAffectSiblingAnchor(
nsINode* aParent, nsINode* aNode, EventStates aStateMask,
nsIAtom* aAttribute, const nsAttrValue* aNewClasses,
bool aCompareClasses) const
bool aCompareClasses, bool aIsTextChange) const
{
bool contentChange = !aAttribute && aStateMask.IsEmpty();
for (const auto& branch : mSiblingBranches) {
if (!branch.IsSibling() ||
if ((aIsTextChange && branch.mIsLocal) ||
!branch.IsSibling() ||
(branch.mIsLocal && (contentChange ? aNode != aParent
: aNode->GetParentNode() != aParent)) ||
(!aStateMask.IsEmpty() &&
@@ -2011,35 +2019,41 @@ EnsureHasSelectorDependency(nsINode* aNode)
return dependency;
}
/* static */ bool
nsCSSRuleUtils::RelativeSelectorListMatches(
Element* aAnchor,
nsCSSSelectorList* aList,
TreeMatchContext& aTreeMatchContext)
// For a chain of child combinators, only the final depth can match. Do not
// descend into deeper subtrees, even when the rightmost compound is absent.
static bool
MatchesAtDepth(Element* aAnchor, uint32_t aDepth,
nsCSSSelectorList* aRelative, TreeMatchContext& aContext)
{
MOZ_ASSERT(aAnchor);
MOZ_ASSERT(aList);
if (aTreeMatchContext.mForStyling) {
if (!aList->mHasSelectorData) {
aList->mHasSelectorData = new nsCSSHasSelectorData(aList);
}
const auto& data = aList->mHasSelectorData;
// The document marker makes mutations in documents without styled :has()
// selectors a constant-time no-op. All markers use the same property type.
EnsureHasSelectorDependency(aAnchor->OwnerDoc());
if (auto* dependency = EnsureHasSelectorDependency(aAnchor)) {
dependency->AddSelectorData(data, false);
dependency->mRestyleLaterSiblings |=
aTreeMatchContext.mHasSelectorLaterSiblings;
}
if (data->mHasSibling && aAnchor->GetParent()) {
if (auto* dependency = EnsureHasSelectorDependency(aAnchor->GetParent())) {
dependency->AddSelectorData(data, true);
Element* candidate = aAnchor->GetFirstElementChild();
uint32_t depth = 1;
while (candidate) {
if (depth == aDepth) {
if (matchesCandidate(candidate, aRelative, aContext)) {
return true;
}
} else if (Element* child = candidate->GetFirstElementChild()) {
candidate = child;
++depth;
continue;
}
while (!candidate->GetNextElementSibling()) {
candidate = candidate->GetParentElement();
if (candidate == aAnchor) {
return false;
}
--depth;
}
candidate = candidate->GetNextElementSibling();
}
return false;
}
static bool
RelativeSelectorListMatchesUncached(Element* aAnchor,
nsCSSSelectorList* aList,
TreeMatchContext& aTreeMatchContext)
{
AutoRestore<Element*> relativeAnchorRestorer(
aTreeMatchContext.mRelativeSelectorAnchor);
aTreeMatchContext.mRelativeSelectorAnchor = aAnchor;
@@ -2059,6 +2073,23 @@ nsCSSRuleUtils::RelativeSelectorListMatches(
}
MOZ_ASSERT(leftmost->mNext);
uint32_t childDepth = 0;
bool onlyChildren = true;
for (nsCSSSelector* selector = relative->mSelectors->mNext;
selector; selector = selector->mNext) {
if (selector->mOperator != '>') {
onlyChildren = false;
break;
}
++childDepth;
}
if (onlyChildren) {
if (MatchesAtDepth(aAnchor, childDepth, relative, aTreeMatchContext)) {
return true;
}
continue;
}
// A single compound cannot move beyond the leading relationship.
// Avoid scanning descendants for >, and all following subtrees for +/~.
if (leftmost == relative->mSelectors) {
@@ -2125,6 +2156,78 @@ nsCSSRuleUtils::RelativeSelectorListMatches(
return false;
}
/* static */ bool
nsCSSRuleUtils::RelativeSelectorListMatches(
Element* aAnchor,
nsCSSSelectorList* aList,
TreeMatchContext& aTreeMatchContext)
{
MOZ_ASSERT(aAnchor);
MOZ_ASSERT(aList);
if (aTreeMatchContext.mForStyling) {
if (!aList->mHasSelectorData) {
aList->mHasSelectorData = new nsCSSHasSelectorData(aList);
}
const auto& data = aList->mHasSelectorData;
// The document marker makes mutations in documents without styled :has()
// selectors a constant-time no-op. All markers use the same property type.
EnsureHasSelectorDependency(aAnchor->OwnerDoc());
if (auto* dependency = EnsureHasSelectorDependency(aAnchor)) {
dependency->AddSelectorData(data, false);
dependency->mRestyleLaterSiblings |=
aTreeMatchContext.mHasSelectorLaterSiblings;
}
if (data->mHasSibling && aAnchor->GetParent()) {
if (auto* dependency = EnsureHasSelectorDependency(aAnchor->GetParent())) {
dependency->AddSelectorData(data, true);
}
}
}
// Register dependencies above even on a hit: the same anchor can be
// reached through an outer sibling combinator later in this traversal.
bool canCache = aTreeMatchContext.mForStyling &&
!aTreeMatchContext.mForScopedStyle &&
!aTreeMatchContext.HasSpecifiedScope() &&
!aTreeMatchContext.mScopedRoot &&
!aTreeMatchContext.mForAssignedSlot &&
!aTreeMatchContext.mRestrictToSlottedPseudo &&
!aTreeMatchContext.mOnlyMatchHostPseudo &&
aTreeMatchContext.mStyleScopes.IsEmpty() &&
aTreeMatchContext.mShadowHosts.IsEmpty() &&
!aAnchor->IsInShadowTree();
if (!canCache) {
return RelativeSelectorListMatchesUncached(aAnchor, aList, aTreeMatchContext);
}
// Most traversals only encounter one anchor/argument pair. Keep that
// result inline so short traversals do not allocate a table on every call.
auto& last = aTreeMatchContext.mLastHasMatch;
if (last.mAnchor == aAnchor && last.mSelector == aList) {
return last.mMatches;
}
auto* entry = &last;
if (last.mAnchor) {
auto& cache = aTreeMatchContext.mHasMatchCache;
const uint32_t cacheSize = 256;
if (cache.IsEmpty()) {
cache.SetLength(cacheSize);
cache[HashGeneric(last.mAnchor, last.mSelector) % cacheSize] = last;
}
entry = &cache[HashGeneric(aAnchor, aList) % cacheSize];
}
if (entry->mAnchor != aAnchor || entry->mSelector != aList) {
bool matches = RelativeSelectorListMatchesUncached(aAnchor, aList,
aTreeMatchContext);
entry->mAnchor = aAnchor;
entry->mSelector = aList;
entry->mMatches = matches;
}
last = *entry;
return last.mMatches;
}
// TreeMatchContext and AncestorFilter out of line methods
void
TreeMatchContext::InitAncestors(Element* aElement)
+4 -2
View File
@@ -71,12 +71,14 @@ struct nsCSSRuleUtils
const nsAttrValue* aNewClasses,
bool aCompareClasses,
bool aSibling,
bool aNodeIsFollowingSibling = false) const;
bool aNodeIsFollowingSibling = false,
bool aIsTextChange = false) const;
bool MightAffectSiblingAnchor(nsINode* aParent, nsINode* aNode,
mozilla::EventStates aStateMask,
nsIAtom* aAttribute,
const nsAttrValue* aNewClasses,
bool aCompareClasses) const;
bool aCompareClasses,
bool aIsTextChange = false) const;
};
static void Startup();
+15 -2
View File
@@ -244,11 +244,24 @@ MakeArray(const nsSize& aSize, nsCSSValue& aResult)
return NS_OK;
}
// Helper for aspect-ratio, which needs fractional ratios.
static nsresult
MakeFloatArray(const nsSize& aSize, nsCSSValue& aResult)
{
RefPtr<nsCSSValue::Array> a = nsCSSValue::Array::Create(2);
a->Item(0).SetFloatValue(aSize.width, eCSSUnit_Number);
a->Item(1).SetFloatValue(aSize.height, eCSSUnit_Number);
aResult.SetArrayValue(a, eCSSUnit_Array);
return NS_OK;
}
static nsresult
GetAspectRatio(nsPresContext* aPresContext, const nsMediaFeature*,
nsCSSValue& aResult)
{
return MakeArray(GetSize(aPresContext), aResult);
return MakeFloatArray(GetSize(aPresContext), aResult);
}
static nsresult
@@ -712,7 +725,7 @@ nsMediaFeatures::features[] = {
{
&nsGkAtoms::aspectRatio,
nsMediaFeature::eMinMaxAllowed,
nsMediaFeature::eIntRatio,
nsMediaFeature::eFloatRatio,
nsMediaFeature::eNoRequirements,
{ nullptr },
GetAspectRatio
+1
View File
@@ -35,6 +35,7 @@ struct nsMediaFeature
eFloat, // values are eCSSUnit_Number
eBoolInteger,// values are eCSSUnit_Integer (0, -0, or 1 only)
eIntRatio, // values are eCSSUnit_Array of two eCSSUnit_Integer
eFloatRatio, // values are eCSSUnit_Array of two eCSSUnit_Number
eResolution, // values are in eCSSUnit_Inch (for dpi),
// eCSSUnit_Pixel (for dppx), or
// eCSSUnit_Centimeter (for dpcm)
+13
View File
@@ -29,6 +29,7 @@ class nsIAtom;
class nsIContent;
class nsICSSPseudoComparator;
struct TreeMatchContext;
struct nsCSSSelectorList;
/**
* An AncestorFilter is used to keep track of ancestors so that we can
@@ -37,6 +38,7 @@ struct TreeMatchContext;
*/
class MOZ_STACK_CLASS AncestorFilter {
friend struct TreeMatchContext;
struct nsCSSSelectorList;
public:
/* Maintenance of our ancestor state */
void PushAncestor(mozilla::dom::Element *aElement);
@@ -475,6 +477,17 @@ struct MOZ_STACK_CLASS TreeMatchContext {
mozilla::dom::Element* mRelativeSelectorAnchor;
// Propagate an outer sibling combinator through :is(), :not(), etc.
bool mHasSelectorLaterSiblings;
// Bounded, lazily allocated cache for a single styling traversal. As with
// mNthIndexCache, no entries survive into the next mutation/style flush.
// Scoped/shadow matching and selector APIs do not use this cache.
struct HasMatchCacheEntry {
mozilla::dom::Element* mAnchor = nullptr;
nsCSSSelectorList* mSelector = nullptr;
bool mMatches = false;
};
HasMatchCacheEntry mLastHasMatch;
nsTArray<HasMatchCacheEntry> mHasMatchCache;
};
struct MOZ_STACK_CLASS RuleProcessorData {
+10
View File
@@ -679,6 +679,16 @@ nsTransitionManager::UpdateTransitions(
do {
--i;
CSSTransition* anim = animations[i];
// A transition we started in the loop above is by definition current,
// so it can never be pruned here. Skipping it keeps the collection we
// may have just created from being destroyed before the caller can use
// it, which is what the !startedAny || collection assertion in
// StyleContextChanged is guarding.
if (whichStarted.HasProperty(anim->TransitionProperty())) {
continue;
}
// properties no longer in 'transition-property'
if ((checkProperties &&
!allTransitionProperties.HasProperty(anim->TransitionProperty())) ||
@@ -160,6 +160,101 @@ addLoadEvent(function() {
document.getElementById("empty-child").appendChild(document.createTextNode("text"));
is(color("empty-anchor"), blue, "text insertion invalidates :empty inside :has()");
// Child chains must search all branches at exactly the required depth.
let depthAnchor = document.createElement("div");
depthAnchor.innerHTML = '<div></div><div class="level"><span></span>' +
'<span class="leaf"><i class="too-deep"></i></span></div>';
document.body.appendChild(depthAnchor);
ok(depthAnchor.matches(":has(> .level > .leaf)"),
"child-chain search reaches later children and grandchildren");
ok(!depthAnchor.matches(":has(> .level > .too-deep)"),
"child-chain search does not accept deeper descendants");
ok(depthAnchor.matches(":has(> .level > .leaf > .too-deep)"),
"longer child chains reach the requested depth");
ok(depthAnchor.matches(":has(> .missing > .leaf, > .level > .leaf)"),
"a failed bounded search still tries later selector-list branches");
ok(depthAnchor.matches(":has(> .level .too-deep)"),
"mixed child/descendant chains retain the general search");
depthAnchor.remove();
// Repeated descendant styling shares the same anchor and :has() argument.
// Neither positive nor negative results may survive a subsequent flush.
let cacheStyle = document.createElement("style");
cacheStyle.textContent =
".cache-anchor > .item { color: blue; }" +
".cache-anchor:has(.needle) > .item { color: red; }" +
".cache-anchor:has(.needle) + .cache-sibling { color: red; }" +
".cache-sibling { color: blue; }";
document.head.appendChild(cacheStyle);
let cacheAnchor = document.createElement("div");
cacheAnchor.className = "cache-anchor";
for (let i = 0; i < 64; ++i) {
let item = document.createElement("span");
item.className = "item";
item.textContent = "text";
cacheAnchor.appendChild(item);
}
let cacheSibling = document.createElement("div");
cacheSibling.className = "cache-sibling";
document.body.appendChild(cacheAnchor);
document.body.appendChild(cacheSibling);
let otherCacheAnchor = document.createElement("div");
otherCacheAnchor.className = "cache-anchor";
otherCacheAnchor.innerHTML = '<span class="item">other anchor</span>';
document.body.appendChild(otherCacheAnchor);
function checkCachedColors(expected, message) {
for (let item of cacheAnchor.children) {
is(getComputedStyle(item).color, expected, message);
}
is(getComputedStyle(otherCacheAnchor.firstElementChild).color, blue,
"cached results are specific to an anchor: " + message);
is(getComputedStyle(cacheSibling).color, expected,
"outer sibling dependency: " + message);
}
checkCachedColors(blue, "initial negative result");
cacheAnchor.lastElementChild.classList.add("needle");
checkCachedColors(red, "negative cache is discarded after class mutation");
cacheAnchor.lastElementChild.classList.remove("needle");
checkCachedColors(blue, "positive cache is discarded after class mutation");
document.body.offsetHeight;
let utils = SpecialPowers.getDOMWindowUtils(window);
let beforeTextChange = utils.elementsRestyled;
cacheAnchor.firstElementChild.firstChild.data = "changed text";
document.body.offsetHeight;
ok(utils.elementsRestyled - beforeTextChange < 32,
"text changes do not restyle the whole plain-compound :has() anchor");
let extraText = document.createTextNode("appended");
beforeTextChange = utils.elementsRestyled;
cacheAnchor.firstElementChild.appendChild(extraText);
document.body.offsetHeight;
ok(utils.elementsRestyled - beforeTextChange < 32,
"text append does not restyle the whole plain-compound :has() anchor");
beforeTextChange = utils.elementsRestyled;
extraText.remove();
document.body.offsetHeight;
ok(utils.elementsRestyled - beforeTextChange < 32,
"text removal does not restyle the whole plain-compound :has() anchor");
beforeTextChange = utils.elementsRestyled;
cacheAnchor.firstElementChild.insertBefore(extraText,
cacheAnchor.firstElementChild.firstChild);
document.body.offsetHeight;
ok(utils.elementsRestyled - beforeTextChange < 32,
"text insertion does not restyle the whole plain-compound :has() anchor");
// Text-sensitive arguments must continue to invalidate after caching.
document.getElementById("empty-child").firstChild.data = "";
is(color("empty-anchor"), red, "empty text restores :has(> :empty)");
document.getElementById("empty-child").firstChild.data = "again";
is(color("empty-anchor"), blue, "text changes invalidate cached :empty");
document.getElementById("empty-child").firstChild.remove();
is(color("empty-anchor"), red, "text removal invalidates cached :empty");
cacheAnchor.remove();
otherCacheAnchor.remove();
cacheSibling.remove();
cacheStyle.remove();
SimpleTest.finish();
});
</script>
+1
View File
@@ -36,6 +36,7 @@ EXPORTS.hwy.ops += [
"/media/highway/src/hwy/ops/inside-inl.h",
"/media/highway/src/hwy/ops/loongarch_lasx-inl.h",
"/media/highway/src/hwy/ops/loongarch_lsx-inl.h",
"/media/highway/src/hwy/ops/ppc_vsx-inl.h",
"/media/highway/src/hwy/ops/rvv-inl.h",
"/media/highway/src/hwy/ops/scalar-inl.h",
"/media/highway/src/hwy/ops/set_macros-inl.h",
+1 -1
View File
@@ -9,4 +9,4 @@ The upstream jxl git repository is:
https://github.com/libjxl/libjxl
The version used was tagged release version 0.12.0.
The version used was tagged release version 0.13.0.
+1 -1
View File
@@ -14,7 +14,7 @@
#define JXL_VERSION_H_
#define JPEGXL_MAJOR_VERSION 0 ///< JPEG XL Major version
#define JPEGXL_MINOR_VERSION 12 ///< JPEG XL Minor version
#define JPEGXL_MINOR_VERSION 13 ///< JPEG XL Minor version
#define JPEGXL_PATCH_VERSION 0 ///< JPEG XL Patch version
/** Can be used to conditionally compile code for a specific JXL version
+1 -1
View File
@@ -18,7 +18,7 @@
url = https://github.com/google/highway
[submodule "third_party/libpng"]
path = third_party/libpng
url = https://github.com/glennrp/libpng.git
url = https://github.com/pnggroup/libpng.git
[submodule "third_party/zlib"]
path = third_party/zlib
url = https://github.com/madler/zlib.git
+1 -1
View File
@@ -12,7 +12,7 @@ sphinx:
configuration: doc/sphinx/conf.py
build:
os: ubuntu-22.04
os: ubuntu-26.04
tools:
python: "3"
apt_packages:
+6
View File
@@ -38,6 +38,7 @@ Alexander Sago <cagelight@gmail.com>
Alifian Caesar Khalid <alifiancaesar@gmail.com>
Alistair Barrow
Amiralgaby
Anantha Narayanan R <evertrustai@gmail.com>
Andrew Kaster <andrew@ladybird.org>
Andrius Lukas Narbutas <andrius4669@gmail.com>
Aous Naman <aous@unsw.edu.au>
@@ -81,11 +82,14 @@ Kleis Auke Wolthuizen <github@kleisauke.nl>
L. E. Segovia
ledoge
Leo Izen <leo.izen@gmail.com>
Linus VandeVondele <linus.vandevondele@gmail.com>
LongYinan <lynweklm@gmail.com>
Lovell Fuller
Lucas Chollet <lucas.chollet@serenityos.org>
Maarten DB <anonymous.maarten@gmail.com>
Marcin Konicki <ahwayakchih@gmail.com>
Martin Strunz
Máté Makai <mmakai2@gmail.com>
Mathieu Malaterre <mathieu.malaterre@gmail.com>
Mikk Leini <mikk.leini@krakul.eu>
Misaki Kasumi <misakikasumi@outlook.com>
@@ -95,8 +99,10 @@ Nigel Tao <nigeltao@golang.org>
oupson <oupson1er@gmail.com>
Petr Diblík
Pieter Wuille
Pepper Gray <hello@peppergray.xyz>
roland-rollo
Samuel Leong <wvvwvvvvwvvw@gmail.com>
Samuele Benassai <samuelebenassai.pptx@gmail.com>
Sandro <sandro.jaeckel@gmail.com>
sandstrom
Sergey Fedorov <vital.had@gmail.com>
+102 -1
View File
@@ -5,7 +5,108 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## [0.13.0] - 2026-11-11
### Security
- bumped brotli to v1.2.1 (#5062) to mitigate CVE-2026-111497
- sized low-memory render pipeline ring buffers by simulating the actual stage
schedule, fixing use-of-uninitialized-memory bugs with vertically subsampled
stages (#5022)
- reset JPEG reconstruction state, fixing potential use-after-free writes across
decoder resets (#5029)
- security and hardening fixes across the core decoder/encoder,
JPEG reconstruction, splines, and codec plugins (EXR, APNG, PGX, PNM); this
includes fixes for integer overflows in JPEG coefficient buffer allocations
and spline area budgets, out-of-bounds read/write guards in chunked frame
adapters and truncated EXR scanline streams, bounds-checked `Span::subspan`
slicing, and null pointer guards (#4936, #4938, #5009, #5022, #5023,
#5026, #5029, #5030, #5076, #5091, #5096, #5100)
- decoder: more consistent image_out cleanup;
reported-by: Alejandro Pacheco (@alepacheco) and Eunji Ham
- harden patch rect calculations;
reported-by: Robert McConnell and freezagent
- make another decoding path aware of overflows (see #4808);
reported-by: Robert McConnell
- decoder: guard against using wrong out buffer;
reported-by: Lakshit Verma
- ensure CmsStage when converting CMYK has access to actual black channel;
reported-by: Marinko Ružić
- fix using wrong subsampling when reconstructing gray JPEGs;
reported-by: synawk dsx, ptrstr and Lakshit Verma
### Added
- encoder API: added `JXL_ENC_FRAME_SETTING_STRIP_ALPHA` frame setting and
`--strip_alpha` CLI flag to control stripping of alpha channels on
single-frame images with a single extra channel (#4981, #4984)
- encoder API: added `JXL_ENC_FRAME_SETTING_JPEG_RECON_LFS` frame setting and
`--jpeg_reconstruction_lfs` CLI flag to control LF (adaptive DC) smoothing
during lossless JPEG recompression; allows non-444 transcoded JPEGs with
adaptive DC/LF smoothing to decode (#4932, #5031)
- release & CI: added native Linux AArch64 static release artifacts (#5039),
Windows ARM64 CI build and release support (#4997), and GitHub artifact
attestation for published release assets (#4982)
### Changed
- rewrote LZ77 implementation for lossless image compression (#4928)
- compression density & encoder improvements:
- enabled LF smoothing by default for 4:4:4 lossless JPEG transcoding (#4932)
- re-enabled squeezed delta palette (#4930)
- signaled the default 39-entry block context map, saving 22-23 bytes per
frame when using the default block context map (#5001)
- improved histogram clustering with forced prefix codes (force_huffman) by
accounting for merges that turn a zero-cost single-symbol histogram into a
multi-symbol histogram (#5002)
- modular encoder: skipped `EstimateWPCost` weighted-predictor mode search
when a custom fixed tree is provided (#5003)
- performance & memory:
- optimized `EstimateWPCost` (#5093, #5120)
- allocated `allowed_counts` on the heap during initialization in to reduce
stack usage (#4918)
- tools: `jxl_from_tree` now automatically uses 16-bit buffers when bit depth is
12 or less (#4976)
### Fixed
- disabled adaptive DC smoothing during lossless JPEG reconstruction (#4931)
- cleared all patch dictionary state, preventing patches from being blended
using a previous frame's blend modes (#4989)
- forgot extra channel output buffers in `JxlDecoderSkipCurrentFrame()` (#5026)
- reset `prog_detail` on `JxlDecoderReset()` and rewind (#5121)
- copied ANS lookup data from the containing entry in `InitAliasDistribution`
and added compile-time size verification (#4952, #5069)
- guarded PerformBlending against empty segments (#4992, #5068)
- fixed `Forward`/`Meta` and `Inverse` squeeze transform agreement on
hshift/vshift updates (#5116)
- fixed luminance calculation and multiplicative factor in photon noise
synthesis (`enc_photon_noise`) (#5053)
- scaled `center_x` and `center_y` coordinates by `dc_scale` for LF frames when
encoding with center-first group ordering (#4969)
- handled legacy non-conforming Display P3 v4 ICC profiles (#4965)
- fixed possible stream inconsistency in streaming encoding mode (#5115)
- fixed formal data races in `enc_cache.cc` and modular `FillSigma`
initialization (#5102, #5109)
- validated packed frame extra-channel counts before encoding (#5075)
- PNM/PAM decoder: rejected sample values exceeding `MaxVal` (#5064)
- PNG/APNG decoder: zeroed rows left undecoded by truncated PNGs (#5083)
- Gain Map API: avoided calling memcpy with a nullptr source (#5089)
- NumPy encoder: properly escaped frame names per RFC 8259 in `--metadata_out`
JSON output (#5088)
- extras: derived file extensions from the final path component only in
`CodecFromPath`. (#5081)
- build & platform fixes:
- fixed build with Highway 1.3+ (#4906)
- dropped `-lm` from `libjxl_cms.pc` and `libjxl_threads.pc` pkg-config files
on MSVC (#5061)
- fixed MSVC intrinsics include order so `_sub_overflow_i32` is visible on
x86/x64 (#5071)
- enabled `JXL_UNUSED` macro under clang-cl (#5122)
- checked whether `Qt` has feature xcb before enabling xcb in tools/ CMake
build (#5019)
- fixed mingw32 build (#4940) and sequentialized CMake test discovery to avoid
build races on macOS/multi-core machines (#4939)
## [0.12.0] - 2026-07-01
+1
View File
@@ -18,6 +18,7 @@ https://www.bestpractices.dev/projects/7845)
https://securityscorecards.dev/viewer/?uri=github.com/libjxl/libjxl)
[![codecov](https://codecov.io/gh/libjxl/libjxl/branch/main/graph/badge.svg)](
https://codecov.io/gh/libjxl/libjxl)
[![Discord](https://img.shields.io/discord/794206087879852103?label=Discord&logo=discord)](https://discord.gg/DqkQgDRTFu)
<img src="doc/jxl.svg" width="100" align="right" alt="JXL logo">
+12 -5
View File
@@ -256,11 +256,16 @@ merge_request_commits() {
MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD)
else
# We are in a local branch, not a pull request workflow.
MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q HEAD@{upstream} || true)
# using "--verify" to avoid fatal git error when there is no upstream branch for the local branch
MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q --verify HEAD@{upstream} 2>/dev/null || true)
fi
if [[ -z "${MR_ANCESTOR_SHA}" ]]; then
echo "Warning, not tracking any branch, using the last commit in HEAD.">&2
echo "Warning: current branch has no upstream tracking branch configured" \
"(git rev-parse HEAD@{upstream} failed). Falling back to comparing" \
"against the parent of HEAD (HEAD^), so only the last commit will be" \
"checked. Run 'git branch --set-upstream-to=<remote>/<branch>' to" \
"compare against a real base branch instead.">&2
# This prints the return value with just HEAD.
MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q "${MR_HEAD_SHA}^")
else
@@ -439,7 +444,9 @@ cmake_build_and_test() {
fi
# gtest_discover_tests() runs the test binaries to discover the list of tests
# at build time, which fails under qemu.
ASAN_OPTIONS=detect_leaks=0 cmake --build "${BUILD_DIR}" -- $TARGETS
export ASAN_OPTIONS=detect_leaks=0
cmake --build "${BUILD_DIR}" -- $TARGETS
ctest -N -Q --test-dir "${BUILD_DIR}"
# Pack test binaries if requested.
if [[ "${PACK_TEST:-}" == "1" ]]; then
(cd "${BUILD_DIR}"
@@ -1258,10 +1265,10 @@ cmd_lint() {
fi
# It is ok, if zizmor is not installed.
if which typos >/dev/null; then
if which zizmor >/dev/null; then
zizmor "${MYDIR}/.github/workflows/"
else
echo -e "${TEXT_BOLD_PURPLE}SKIPPED:${TEXT_RESET} typos not installed; try: cargo install typos-cli"
echo -e "${TEXT_BOLD_PURPLE}SKIPPED:${TEXT_RESET} zizmor (not installed)"
fi
local installed=()
+8 -2
View File
@@ -1,8 +1,14 @@
jpeg-xl (0.12.0) UNRELEASED; urgency=medium
jpeg-xl (0.13.0) UNRELEASED; urgency=medium
* Bump JPEG XL version to 0.13.0.
-- JPEG XL Maintainers <jpegxl@google.com> Thu, 02 Jul 2026 08:32:47 +0000
jpeg-xl (0.12.0) unstable; urgency=medium
* Bump JPEG XL version to 0.12.0.
-- JPEG XL Maintainers <jpegxl@google.com> Fri, 13 Sep 2024 09:19:23 +0200
-- JPEG XL Maintainers <jpegxl@google.com> Thu, 01 Jul 2026 11:16:31 +0200
jpeg-xl (0.11.0) unstable; urgency=medium
+1 -1
View File
@@ -8,7 +8,7 @@ Build-Depends:
cmake,
debhelper (>= 9),
libbrotli-dev,
libgdk-pixbuf-2.0-dev | libgdk-pixbuf2.0-dev,
libgdk-pixbuf-2.0-dev | libgdk-pixbuf2.0-dev | libgdk-pixbuf-xlib-2.0-dev,
libgif-dev,
libgoogle-perftools-dev,
libgtest-dev,
+12 -13
View File
@@ -15,13 +15,13 @@ MYDIR=$(dirname "${SELF}")
# Git revisions we use for the given submodules. Update these whenever you
# update a git submodule.
TESTDATA="873045a9c42ed60721756e26e2a6b32e17415205"
THIRD_PARTY_BROTLI="028fb5a23661f123017c060daa546b55cf4bde29" # v1.2.0
THIRD_PARTY_GOOGLETEST="6910c9d9165801d8827d628cb72eb7ea9dd538c5" # v1.16.0
THIRD_PARTY_HIGHWAY="457c891775a7397bdb0376bb1031e6e027af1c48" # v1.2.0
THIRD_PARTY_SKCMS="96d9171c94b937a1b5f0293de7309ac16311b722" # 2025_09_16
THIRD_PARTY_SJPEG="94e0df6d0f8b44228de5be0ff35efb9f946a13c9" # Wed Apr 2 15:42:02 2025 -0700
THIRD_PARTY_ZLIB="51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf" # v1.3.1
THIRD_PARTY_LIBPNG="872555f4ba910252783af1507f9e7fe1653be252" # v1.6.47
THIRD_PARTY_BROTLI="db24a98aed38a16de78cc05ca15cdd8dc2a0d92d" # v1.2.1
THIRD_PARTY_GOOGLETEST="6910c9d9165801d8827d628cb72eb7ea9dd538c5" # v1.16.0
THIRD_PARTY_HIGHWAY="457c891775a7397bdb0376bb1031e6e027af1c48" # v1.2.0
THIRD_PARTY_SKCMS="96d9171c94b937a1b5f0293de7309ac16311b722" # 2025_09_16
THIRD_PARTY_SJPEG="dd4d9857c58a25713711a46d37385ad23ca2177e" # Sun Nov 8 22:48:20 2026 +0100
THIRD_PARTY_ZLIB="51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf" # v1.3.1
THIRD_PARTY_LIBPNG="872555f4ba910252783af1507f9e7fe1653be252" # v1.6.47
THIRD_PARTY_LIBJPEG_TURBO="8ecba3647edb6dd940463fedf38ca33a8e2a73d1" # 2.1.5.1
# Download the target revision from GitHub.
@@ -29,7 +29,7 @@ download_github() {
local path="$1"
local project="$2"
local varname=`echo "$path" | tr '[:lower:]' '[:upper:]'`
local varname=$(echo "$path" | tr '[:lower:]' '[:upper:]')
varname="${varname//[\/-]/_}"
local sha
eval "sha=\${${varname}}"
@@ -55,13 +55,12 @@ download_github() {
}
is_git_repository() {
local dir="$1"
local toplevel=$(git rev-parse --show-toplevel)
local dir="$1"
local toplevel=$(git rev-parse --show-toplevel)
[[ "${dir}" == "${toplevel}" ]]
[[ "${dir}" == "${toplevel}" ]]
}
main() {
if is_git_repository "${MYDIR}"; then
cat >&2 <<EOF
@@ -82,7 +81,7 @@ EOF
download_github third_party/sjpeg webmproject/sjpeg
download_github third_party/skcms google/skcms
download_github third_party/zlib madler/zlib
download_github third_party/libpng glennrp/libpng
download_github third_party/libpng pnggroup/libpng
download_github third_party/libjpeg-turbo libjpeg-turbo/libjpeg-turbo
echo "Done."
}
+1 -1
View File
@@ -29,7 +29,7 @@ locally. Here is a snippet that reflects how setup / compilation are (currently)
done in the workflow:
```
docker run -it -v`pwd`:/libjxl debian:bookworm bash
docker run -it -v`pwd`:/libjxl debian:trixie bash
cd /libjxl
+1 -1
View File
@@ -310,7 +310,7 @@ git push gitlab v0.7-snapshot
### How to build downstream projects
```bash
docker run -it debian:bookworm /bin/bash
docker run -it debian:trixie /bin/bash
apt update
apt install -y clang cmake git libbrotli-dev libopenexr-dev libpng-dev nasm pkg-config ninja-build
+9 -2
View File
@@ -4,7 +4,7 @@
# license that can be found in the LICENSE file.
set(JPEGXL_MAJOR_VERSION 0)
set(JPEGXL_MINOR_VERSION 12)
set(JPEGXL_MINOR_VERSION 13)
set(JPEGXL_PATCH_VERSION 0)
set(JPEGXL_LIBRARY_VERSION
"${JPEGXL_MAJOR_VERSION}.${JPEGXL_MINOR_VERSION}.${JPEGXL_PATCH_VERSION}")
@@ -15,7 +15,7 @@ set(JPEGXL_LIBRARY_VERSION
# It is important to update this value when making incompatible API/ABI changes
# so that programs that depend on libjxl can update their dependencies. Semantic
# versioning allows 0.y.z to have incompatible changes in minor versions.
set(JPEGXL_SO_MINOR_VERSION 12)
set(JPEGXL_SO_MINOR_VERSION 13)
if (JPEGXL_MAJOR_VERSION EQUAL 0)
set(JPEGXL_LIBRARY_SOVERSION
"${JPEGXL_MAJOR_VERSION}.${JPEGXL_SO_MINOR_VERSION}")
@@ -178,6 +178,13 @@ elseif(LIBCXX)
set(PKGCONFIG_CXX_LIB "-lc++")
endif()
# MSVCRT bundles math functions so no explicit libm dependency is required.
if(MSVC)
set(PKGCONFIG_MATH_LIB "")
else()
set(PKGCONFIG_MATH_LIB "-lm")
endif()
# The jxl_cms library definition.
include(jxl_cms.cmake)
# The jxl library definition.
+244
View File
@@ -47,6 +47,73 @@ Status PnmParseUnsigned(Bytes str, size_t* v);
namespace {
TEST(CodecTest, EncodersRejectSurplusFrameExtraChannel) {
PackedPixelFile ppf;
ppf.info.xsize = 1;
ppf.info.ysize = 1;
ppf.info.num_color_channels = 1;
ppf.info.bits_per_sample = 8;
const JxlPixelFormat gray{1, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0};
JXL_TEST_ASSIGN_OR_DIE(PackedFrame frame, PackedFrame::Create(1, 1, gray));
JXL_TEST_ASSIGN_OR_DIE(PackedImage ec, PackedImage::Create(1, 1, gray));
memset(frame.color.pixels(), 0x7f, frame.color.pixels_size);
memset(ec.pixels(), 0x3f, ec.pixels_size);
frame.extra_channels.emplace_back(std::move(ec));
ppf.frames.emplace_back(std::move(frame));
for (const char* extension : {".pgm", ".png"}) {
auto encoder = Encoder::FromExtension(extension);
if (!encoder) continue;
EncodedImage encoded;
EXPECT_FALSE(encoder->Encode(ppf, &encoded, nullptr)) << extension;
}
}
TEST(CodecTest, PackedPixelFileValidExtraChannelCounts) {
PackedPixelFile ppf;
ppf.info.xsize = 1;
ppf.info.ysize = 1;
ppf.info.num_color_channels = 1;
ppf.info.bits_per_sample = 8;
PackedExtraChannel metadata{};
metadata.ec_info.bits_per_sample = 8;
ppf.extra_channels_info.push_back(metadata);
const JxlPixelFormat gray{1, JXL_TYPE_UINT8, JXL_BIG_ENDIAN, 0};
for (int i = 0; i < 2; ++i) {
JXL_TEST_ASSIGN_OR_DIE(PackedFrame frame, PackedFrame::Create(1, 1, gray));
JXL_TEST_ASSIGN_OR_DIE(PackedImage ec, PackedImage::Create(1, 1, gray));
memset(frame.color.pixels(), 0x7f, frame.color.pixels_size);
memset(ec.pixels(), 0x3f, ec.pixels_size);
frame.extra_channels.emplace_back(std::move(ec));
ppf.frames.emplace_back(std::move(frame));
}
ASSERT_TRUE(ppf.Validate());
for (const char* extension : {".pgm", ".png"}) {
auto encoder = Encoder::FromExtension(extension);
if (!encoder) continue;
EncodedImage encoded;
ASSERT_TRUE(encoder->Encode(ppf, &encoded, nullptr)) << extension;
ASSERT_EQ(encoded.bitstreams.size(), 2u);
ASSERT_EQ(encoded.extra_channel_bitstreams.size(), 1u);
ASSERT_EQ(encoded.extra_channel_bitstreams[0].size(), 2u);
for (const auto& bytes : encoded.extra_channel_bitstreams[0]) {
PackedPixelFile decoded;
ASSERT_TRUE(DecodeBytes(Bytes(bytes), ColorHints(), &decoded));
ASSERT_EQ(decoded.frames.size(), 1u);
EXPECT_EQ(*static_cast<const uint8_t*>(decoded.frames[0].color.pixels()),
0x3f);
}
}
ppf.frames[1].extra_channels.clear();
EXPECT_FALSE(ppf.Validate());
for (const char* extension : {".pgm", ".png"}) {
auto encoder = Encoder::FromExtension(extension);
if (!encoder) continue;
EncodedImage encoded;
EXPECT_FALSE(encoder->Encode(ppf, &encoded, nullptr)) << extension;
}
}
Span<const uint8_t> MakeSpan(const char* str) {
return Bytes(reinterpret_cast<const uint8_t*>(str), strlen(str));
}
@@ -435,6 +502,49 @@ TEST(CodecTest, TestPNM) {
ASSERT_TRUE(std::abs(d - -3.141592) < 1E-15);
}
TEST(CodecTest, PNMSampleAboveMaxVal) {
const auto decode = [](const std::string& header,
const std::vector<uint8_t>& samples) -> bool {
std::vector<uint8_t> pnm(header.begin(), header.end());
pnm.insert(pnm.end(), samples.begin(), samples.end());
PackedPixelFile ppf;
return static_cast<bool>(DecodeBytes(Bytes(pnm), ColorHints(), &ppf));
};
const std::string kPam12 =
"P7\nWIDTH 1\nHEIGHT 1\nDEPTH 4\nMAXVAL 4095\n"
"TUPLTYPE RGB_ALPHA\nENDHDR\n";
// Samples up to MaxVal are valid.
EXPECT_TRUE(decode("P5\n2 1\n15\n", {0, 15}));
EXPECT_TRUE(decode("P5\n2 1\n255\n", {0, 255}));
EXPECT_TRUE(decode("P6\n1 1\n4095\n", {0x0F, 0xFF, 0, 0, 0x0F, 0xFF}));
EXPECT_TRUE(decode("P5\n1 1\n65535\n", {0xFF, 0xFF}));
EXPECT_TRUE(decode(kPam12, {0, 0, 0, 0, 0, 0, 0x0F, 0xFF}));
#if (!JXL_CRASH_ON_ERROR)
// Samples above MaxVal are rejected.
EXPECT_FALSE(decode("P5\n2 1\n15\n", {0, 16}));
EXPECT_FALSE(decode("P6\n1 1\n4095\n", {0x0F, 0xFF, 0x10, 0x00, 0, 0}));
EXPECT_FALSE(decode("P6\n1 1\n4095\n", {0, 0, 0, 0, 0xFF, 0xFF}));
EXPECT_FALSE(decode(kPam12, {0, 0, 0, 0, 0, 0, 0x10, 0x00}));
// Check every sample position of images larger than 8 bytes.
const std::vector<uint8_t> max4(19, 15);
std::vector<uint8_t> max12;
for (size_t i = 0; i < 19; ++i) max12.insert(max12.end(), {0x0F, 0xFF});
EXPECT_TRUE(decode("P5\n19 1\n15\n", max4));
EXPECT_TRUE(decode("P5\n19 1\n4095\n", max12));
for (size_t i = 0; i < 19; ++i) {
std::vector<uint8_t> samples4 = max4;
samples4[i] = 16;
EXPECT_FALSE(decode("P5\n19 1\n15\n", samples4)) << i;
std::vector<uint8_t> samples12 = max12;
samples12[2 * i] = 0x10;
samples12[2 * i + 1] = 0x00;
EXPECT_FALSE(decode("P5\n19 1\n4095\n", samples12)) << i;
}
#endif
}
TEST(CodecTest, FormatNegotiation) {
const std::vector<JxlPixelFormat> accepted_formats = {
{/*num_channels=*/4,
@@ -570,6 +680,140 @@ TEST(CodecTest, EncodeToPNG) {
decoded_ppf.info.bits_per_sample);
}
TEST(CodecTest, ExtensionFromPath) {
// The extension is derived from the final path component only: dots in
// directory names must not leak into the result, the returned extension
// never contains a path separator, and a trailing dot that carries no
// extension is reported as none.
std::string extension;
size_t bits_per_sample = 0;
extension.clear();
ASSERT_EQ(CodecFromPath("a.png/../../b", &bits_per_sample, &extension),
Codec::kUnknown);
EXPECT_EQ(extension, "");
// A valid codec extension in the final component is still detected even
// with a dotted directory part ahead of it.
extension.clear();
ASSERT_EQ(CodecFromPath("a.jpg/../../b.jpg", &bits_per_sample, &extension),
Codec::kJPG);
EXPECT_EQ(extension, ".jpg");
extension.clear();
ASSERT_EQ(CodecFromPath("dir.name/file", &bits_per_sample, &extension),
Codec::kUnknown);
EXPECT_EQ(extension, "");
extension.clear();
ASSERT_EQ(CodecFromPath("out.png.", &bits_per_sample, &extension),
Codec::kUnknown);
EXPECT_EQ(extension, "");
// Backslash separators are stripped as well.
extension.clear();
ASSERT_EQ(CodecFromPath("C:\\x\\y.png", &bits_per_sample, &extension),
Codec::kPNG);
EXPECT_EQ(extension, ".png");
extension.clear();
ASSERT_EQ(CodecFromPath("/dev/stdout", &bits_per_sample, &extension),
Codec::kUnknown);
EXPECT_EQ(extension, "");
// Bare extension strings (as passed by the benchmark tooling) keep
// working, including the .pfm bits_per_sample side effect.
extension.clear();
ASSERT_EQ(CodecFromPath(".pfm", &bits_per_sample, &extension), Codec::kPNM);
EXPECT_EQ(extension, ".pfm");
EXPECT_EQ(bits_per_sample, 32u);
}
TEST(CodecTest, DecodeTruncatedPNGLeavesMissingRowsZero) {
std::unique_ptr<Encoder> png_encoder = Encoder::FromExtension(".png");
if (!png_encoder || !CanDecode(Codec::kPNG)) {
fprintf(stderr, "Skipping test because of missing codec support.\n");
return;
}
constexpr size_t kXSize = 64;
constexpr size_t kYSize = 64;
TestImageParams params = {Codec::kPNG,
kXSize,
kYSize,
/*bits_per_sample=*/8,
/*is_gray=*/false,
/*add_alpha=*/false,
/*big_endian=*/true,
/*add_extra_channels=*/false};
PackedPixelFile ppf_in;
CreateTestImage(params, &ppf_in);
EncodedImage encoded;
ASSERT_TRUE(png_encoder->Encode(ppf_in, &encoded, nullptr));
ASSERT_EQ(encoded.bitstreams.size(), 1u);
std::vector<uint8_t> png = encoded.bitstreams[0];
// Declare twice the height in IHDR (the chunk CRC is not checked), so the
// pixel data ends after half of the rows.
ASSERT_EQ(memcmp(&png[12], "IHDR", 4), 0);
ASSERT_EQ(LoadBE32(&png[20]), kYSize);
StoreBE32(2 * kYSize, &png[20]);
// Fill freed heap blocks of the size of the pixel buffer, so that a buffer
// that is not cleared holds stale bytes instead of fresh zero pages.
for (size_t i = 0; i < 4; ++i) {
std::vector<uint8_t> dirty(kXSize * 2 * kYSize * 3, 0xA5);
EXPECT_EQ(dirty.back(), 0xA5);
}
PackedPixelFile ppf_out;
ASSERT_TRUE(DecodeBytes(Bytes(png), ColorHints(), &ppf_out));
ASSERT_EQ(ppf_out.frames.size(), 1u);
const PackedImage& in = ppf_in.frames[0].color;
const PackedImage& out = ppf_out.frames[0].color;
ASSERT_EQ(out.xsize, kXSize);
ASSERT_EQ(out.ysize, 2 * kYSize);
ASSERT_EQ(out.pixels_size, 2 * in.pixels_size);
// The rows in the file are decoded, the rows missing from it are zero.
const auto* in_pixels = static_cast<const uint8_t*>(in.pixels());
const auto* out_pixels = static_cast<const uint8_t*>(out.pixels());
EXPECT_EQ(memcmp(in_pixels, out_pixels, in.pixels_size), 0);
for (size_t i = in.pixels_size; i < out.pixels_size; ++i) {
ASSERT_EQ(out_pixels[i], 0) << "byte " << i;
}
}
// Frame names are arbitrary bytes from the codestream. The NumPy metadata
// writer must produce valid JSON for them: a raw '"' in a name used to
// terminate the JSON string early, which both broke parsing and allowed a
// crafted name to inject additional fields into the frame dictionary.
TEST(CodecTest, NumPyMetadataEscapesFrameName) {
std::unique_ptr<Encoder> npy_encoder = Encoder::FromExtension(".npy");
ASSERT_TRUE(npy_encoder);
const JxlPixelFormat format{/*num_channels=*/1, JXL_TYPE_FLOAT,
JXL_LITTLE_ENDIAN, /*align=*/0};
PackedPixelFile ppf;
ppf.info.xsize = 2;
ppf.info.ysize = 2;
ppf.info.num_color_channels = 1;
ppf.info.bits_per_sample = 32;
ppf.info.exponent_bits_per_sample = 8;
JXL_TEST_ASSIGN_OR_DIE(PackedFrame frame, PackedFrame::Create(2, 2, format));
memset(frame.color.pixels(), 0, frame.color.pixels_size);
frame.name = std::string("q\"b\\n\ne\x01", 8);
ppf.frames.emplace_back(std::move(frame));
EncodedImage encoded;
ASSERT_TRUE(npy_encoder->Encode(ppf, &encoded, nullptr));
const std::string metadata(encoded.metadata.begin(), encoded.metadata.end());
EXPECT_NE(metadata.find("\"name\": \"q\\\"b\\\\n\\ne\\u0001\""),
std::string::npos)
<< metadata;
// A quote inside the name must never end the JSON string.
EXPECT_EQ(metadata.find("\"q\"b"), std::string::npos) << metadata;
}
} // namespace
} // namespace extras
} // namespace jxl
+6 -6
View File
@@ -458,9 +458,9 @@ constexpr uint32_t MakeTag(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
/** Reusable image data container. */
struct Pixels {
// Use array instead of vector to avoid memory initialization.
uninitialized_vector<uint8_t> pixels_storage =
jxl::make_uninitialized_vector<uint8_t>(0);
// Zero-initialized: rows that libpng never delivers (truncated or corrupt
// data) are copied to the output as well.
std::vector<uint8_t> pixels_storage;
size_t pixels_size = 0;
std::vector<uint8_t*> rows;
std::atomic<uint32_t> has_error{0};
@@ -904,10 +904,10 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
if (chunk.empty()) {
return JXL_FAILURE("Malformed chunk");
}
Bytes type(chunk.data() + 4, 4);
JXL_ASSIGN_OR_RETURN(Bytes type, chunk.subspan(4, 4));
id = LoadLE32(type.data());
// Cut 'size' and 'type' at front and 'CRC' at the end.
Bytes payload(chunk.data() + 8, chunk.size() - 12);
JXL_ASSIGN_OR_RETURN(Bytes payload, chunk.subspan(8, chunk.size() - 12));
if (!isAbc(type[0]) || !isAbc(type[1]) || !isAbc(type[2]) ||
!isAbc(type[3])) {
@@ -1063,7 +1063,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
png_save_uint_32(preamble.data(), payload.size() - 4);
memcpy(preamble.data() + 4, "IDAT", 4);
// Cut-off 'size', 'type' and 'sequence_number'
Bytes chunk_tail(chunk.data() + 12, chunk.size() - 12);
JXL_ASSIGN_OR_RETURN(Bytes chunk_tail, chunk.subspan(12));
if (!ctx.FeedChunks(Bytes(preamble), chunk_tail)) {
return JXL_FAILURE("Decoding fdAT failed");
}
+12 -7
View File
@@ -38,14 +38,19 @@ namespace {
constexpr size_t kMinBytes = 9;
std::string GetExtension(const std::string& path) {
// Pattern: "name.png"
size_t pos = path.find_last_of('.');
if (pos != std::string::npos) {
return path.substr(pos);
// Pattern: "name.png". Only the final path component can carry an
// extension: strip any directory part (both '/' and '\' separators)
// first, so that dots inside directory names (e.g. "a.png/../../b")
// cannot leak into the returned extension, and a trailing dot that
// carries no extension (e.g. "out.png.") is reported as none.
const size_t sep = path.find_last_of("/\\");
const std::string base =
(sep == std::string::npos) ? path : path.substr(sep + 1);
const size_t pos = base.find_last_of('.');
if (pos == std::string::npos || pos == base.size() - 1) {
return "";
}
// Extension not found
return "";
return base.substr(pos);
}
} // namespace
+11 -1
View File
@@ -82,7 +82,17 @@ class InMemoryIStream : public OpenEXR::IStream {
explicit InMemoryIStream(const Span<const uint8_t> bytes)
: IStream(/*fileName=*/""), bytes_(bytes) {}
bool isMemoryMapped() const override { return true; }
// Do not advertise memory mapping. When an OpenEXR::IStream reports itself
// as memory-mapped, OpenEXR reads a scanline's pixel data straight from the
// pointer returned by readMemoryMapped(), using the size derived from the
// image header rather than the size of the on-disk chunk. A file whose chunk
// declares fewer bytes than a full scanline then makes OpenEXR read past the
// end of our input buffer (a heap out-of-bounds read reachable from cjxl on
// an untrusted .exr file). The copying read() path below hands OpenEXR a
// buffer that it sizes itself, so an over-read stays within OpenEXR's own
// allocation and is caught by its chunk-size validation instead. The data is
// already in memory, so this costs one extra copy per scanline and no I/O.
bool isMemoryMapped() const override { return false; }
char* readMemoryMapped(const int n) override {
if (pos_ + n < pos_) {
JXL_EXR_THROW_LENGTH_ERROR("Overflow");
+12 -4
View File
@@ -454,15 +454,23 @@ bool DecodeImageJXL(const uint8_t* bytes, size_t bytes_size,
fprintf(stderr, "Image too big\n");
return false;
}
total_pixel_count += static_cast<uint64_t>(ppf->xsize()) * ppf->ysize();
if (constraints && (total_pixel_count > constraints->dec_max_pixels)) {
return JXL_FAILURE("Image too big");
}
uint64_t file_px = static_cast<uint64_t>(ppf->xsize()) * ppf->ysize();
JxlFrameHeader fh;
if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec, &fh)) {
fprintf(stderr, "JxlDecoderGetFrameHeader failed\n");
return false;
}
if (!VerifyDimensions(constraints, fh.layer_info.xsize,
fh.layer_info.ysize)) {
fprintf(stderr, "Image too big\n");
return false;
}
uint64_t frame_px =
static_cast<uint64_t>(fh.layer_info.xsize) * fh.layer_info.ysize;
total_pixel_count += std::max(file_px, frame_px);
if (constraints && (total_pixel_count > constraints->dec_max_pixels)) {
return JXL_FAILURE("Image too big");
}
JXL_ASSIGN_OR_QUIT(jxl::extras::PackedFrame frame,
jxl::extras::PackedFrame::Create(
fh.layer_info.xsize, fh.layer_info.ysize, format),
+1 -1
View File
@@ -40,7 +40,7 @@ class Parser {
// Sets "pos" to the first non-header byte/pixel on success.
Status ParseHeader(HeaderPGX* header, const uint8_t** pos) {
// codec.cc ensures we have at least two bytes => no range check here.
if (static_cast<size_t>(end_ - pos_) < 2) return false;
if (pos_[0] != 'P' || pos_[1] != 'G') return false;
pos_ += 2;
return ParseHeaderPGX(header, pos);
@@ -8,9 +8,11 @@
#include <jxl/memory_manager.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "lib/extras/packed_image.h"
#include "lib/extras/packed_image_convert.h"
@@ -112,6 +114,26 @@ TEST(CodecPGXTest, RejectsDimensionProductOverflow) {
EXPECT_FALSE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), &ppf));
}
// The two-byte "PG" signature was read before any range check, relying on a
// minimum input size enforced in a different translation unit
// (extras/dec/decode.cc). DecodeImagePGX does not enforce that itself, so a
// caller reaching it directly - as these tests do - read past the end of the
// buffer. ParseHeader must enforce its own precondition.
//
// The buffers are heap-allocated at exactly the tested length so sanitizer
// builds observe any overread; a string literal would hide it inside the
// terminating NUL. Length 1 is checked first because it is the case a
// sanitizer reports precisely - for length 0 an empty vector may yield a null
// data() pointer, whose behaviour is not guaranteed across standard libraries.
TEST(CodecPGXTest, RejectsInputShorterThanSignature) {
PackedPixelFile ppf;
for (const size_t len : {static_cast<size_t>(1), static_cast<size_t>(0)}) {
const std::vector<uint8_t> bytes(len, static_cast<uint8_t>('P'));
EXPECT_FALSE(
DecodeImagePGX(Bytes(bytes.data(), bytes.size()), ColorHints(), &ppf));
}
}
} // namespace
} // namespace extras
} // namespace jxl
+37 -1
View File
@@ -39,7 +39,7 @@ class Parser {
// Sets "pos" to the first non-header byte/pixel on success.
Status ParseHeader(HeaderPNM* header, const uint8_t** pos) {
// codec.cc ensures we have at least two bytes => no range check here.
if (static_cast<size_t>(end_ - pos_) < 2) return false;
if (pos_[0] != 'P') return false;
const uint8_t type = pos_[1];
pos_ += 2;
@@ -340,6 +340,35 @@ class Parser {
const uint8_t* const end_;
};
// Netpbm requires every sample to be in [0, MaxVal]. MaxVal is 2^bits - 1, so
// a sample is out of range iff it has a bit set at or above `bits_per_sample`.
// Samples are big-endian, so for 16-bit storage only the first byte of each
// sample can hold such bits. `data` holds `size` bytes of integer samples.
Status VerifySampleRange(const uint8_t* data, size_t size,
size_t bits_per_sample) {
// Every value of the storage type is a valid sample.
if (bits_per_sample == 8 || bits_per_sample == 16) return true;
// OR the input 8 bytes at a time: byte k of `acc` is then the OR of all
// bytes at offsets equal to k modulo 8.
uint64_t acc = 0;
size_t i = 0;
for (; i + 8 <= size; i += 8) {
uint64_t v;
memcpy(&v, data + i, 8);
acc |= v;
}
uint8_t acc_bytes[8];
memcpy(acc_bytes, &acc, 8);
for (; i < size; ++i) acc_bytes[i % 8] |= data[i];
const size_t step = bits_per_sample > 8 ? 2 : 1;
const size_t shift =
bits_per_sample > 8 ? bits_per_sample - 8 : bits_per_sample;
uint8_t bits = 0;
for (size_t k = 0; k < 8; k += step) bits |= acc_bytes[k];
if (bits >> shift) return JXL_FAILURE("PNM: sample value exceeds MaxVal");
return true;
}
} // namespace
struct PNMChunkedInputFrame {
@@ -432,6 +461,9 @@ StatusOr<ChunkedPNMDecoder> ChunkedPNMDecoder::Init(
if (size < required_size) {
return JXL_FAILURE("PNM file too small");
}
JXL_RETURN_IF_ERROR(VerifySampleRange(dec.pnm_.data() + dec.data_start_,
required_size - dec.data_start_,
header.bits_per_sample));
return dec;
}
@@ -555,6 +587,10 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes,
if (pnm_remaining_size < required_pnm_size) {
return JXL_FAILURE("PNM file too small");
}
if (!header.floating_point) {
JXL_RETURN_IF_ERROR(
VerifySampleRange(pos, required_pnm_size, header.bits_per_sample));
}
ppf->frames.clear();
{
@@ -0,0 +1,58 @@
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "lib/extras/dec/pnm.h"
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "lib/extras/dec/color_hints.h"
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/testing.h"
namespace jxl {
namespace extras {
namespace {
Span<const uint8_t> MakeSpan(const char* str) {
return Bytes(reinterpret_cast<const uint8_t*>(str), strlen(str));
}
// The two-byte "P<type>" signature was read before any range check, relying on
// a minimum input size enforced in a different translation unit
// (extras/dec/decode.cc). DecodeImagePNM does not enforce that itself, so a
// caller reaching it directly - as these tests do - read past the end of the
// buffer. The out-of-bounds byte then selected the header variant in the switch
// that follows. ParseHeader must enforce its own precondition.
//
// The buffers are heap-allocated at exactly the tested length so sanitizer
// builds observe any overread; a string literal would hide it inside the
// terminating NUL. Length 1 is checked first because it is the case a
// sanitizer reports precisely - for length 0 an empty vector may yield a null
// data() pointer, whose behaviour is not guaranteed across standard libraries.
TEST(CodecPNMTest, RejectsInputShorterThanSignature) {
PackedPixelFile ppf;
for (const size_t len : {static_cast<size_t>(1), static_cast<size_t>(0)}) {
const std::vector<uint8_t> bytes(len, static_cast<uint8_t>('P'));
EXPECT_FALSE(
DecodeImagePNM(Bytes(bytes.data(), bytes.size()), ColorHints(), &ppf));
}
}
// A complete signature with nothing after it must still be rejected, and must
// not read beyond the two bytes it consumed.
TEST(CodecPNMTest, RejectsSignatureOnly) {
PackedPixelFile ppf;
const std::string pnm = "P6";
EXPECT_FALSE(DecodeImagePNM(MakeSpan(pnm.c_str()), ColorHints(), &ppf));
}
} // namespace
} // namespace extras
} // namespace jxl
+1
View File
@@ -98,6 +98,7 @@ class APNGEncoder : public Encoder {
}
Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
ThreadPool* pool) const override {
JXL_RETURN_IF_ERROR(ppf.Validate());
// Encode main image frames
JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
encoded_image->icc.clear();
+8
View File
@@ -58,6 +58,14 @@ bool SetupFrame(JxlEncoder* enc, JxlEncoderFrameSettings* settings,
if (!SetFrameOptions(params.options, frame_index, &option_idx, settings)) {
return false;
}
if (params.strip_alpha != -1) {
if (JXL_ENC_SUCCESS !=
JxlEncoderFrameSettingsSetOption(
settings, JXL_ENC_FRAME_SETTING_STRIP_ALPHA, params.strip_alpha)) {
fprintf(stderr, "Setting strip_alpha failed.\n");
return false;
}
}
if (frame_index < ppf.frames.size()) {
const auto& frame_name = ppf.frames[frame_index].name;
if (!frame_name.empty()) {
+7
View File
@@ -66,6 +66,13 @@ struct JXLCompressParams {
int32_t codestream_level = -1;
int32_t premultiply = -1;
// Alpha channel stripping:
// -1 = encoder chooses (default: strip if empty for lossy, keep for lossless)
// 0 = always keep alpha
// 1 = always strip
// 2 = strip if empty (opaque)
int32_t strip_alpha = -1;
// If runner_opaque is set, the encoder uses this parallel runner.
JxlParallelRunner runner = JxlThreadParallelRunner;
void* runner_opaque = nullptr;
+49 -1
View File
@@ -10,6 +10,7 @@
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <ostream>
@@ -31,6 +32,52 @@ namespace {
// JSON value writing
// Returns `value` as a quoted JSON string literal. Frame names are arbitrary
// bytes taken from the codestream, so '"', '\' and control characters have to
// be escaped (RFC 8259, section 7); otherwise the metadata is not valid JSON
// and a quote inside a name can inject arbitrary fields.
std::string JSONStringLiteral(const std::string& value) {
std::string out;
out.reserve(value.size() + 2);
out.push_back('"');
for (const char ch : value) {
const uint8_t c = static_cast<uint8_t>(ch);
switch (c) {
case '"':
out += "\\\"";
break;
case '\\':
out += "\\\\";
break;
case '\b':
out += "\\b";
break;
case '\f':
out += "\\f";
break;
case '\n':
out += "\\n";
break;
case '\r':
out += "\\r";
break;
case '\t':
out += "\\t";
break;
default:
if (c < 0x20) {
char buf[8];
snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned>(c));
out += buf;
} else {
out.push_back(ch);
}
}
}
out.push_back('"');
return out;
}
class JSONField {
public:
virtual ~JSONField() = default;
@@ -45,7 +92,8 @@ class JSONValue : public JSONField {
template <typename T>
explicit JSONValue(const T& value) : value_(std::to_string(value)) {}
explicit JSONValue(const std::string& value) : value_("\"" + value + "\"") {}
explicit JSONValue(const std::string& value)
: value_(JSONStringLiteral(value)) {}
explicit JSONValue(bool value) : value_(value ? "true" : "false") {}
+1
View File
@@ -33,6 +33,7 @@ class BasePNMEncoder : public Encoder {
public:
Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
ThreadPool* pool) const override {
JXL_RETURN_IF_ERROR(ppf.Validate());
JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
if (!ppf.metadata.exif.empty() || !ppf.metadata.iptc.empty() ||
!ppf.metadata.jumbf.empty() || !ppf.metadata.xmp.empty()) {
+12 -6
View File
@@ -115,6 +115,13 @@ JXL_BOOL JxlGainMapWriteBundle(const JxlGainMapBundle* map_bundle,
} \
} while (false)
// Optional parts of the bundle may be absent (`nullptr` with size 0);
// passing a null pointer to memcpy is undefined behavior even then.
#define SAFE_COPY(src, n) \
do { \
if ((n) != 0) memcpy(output_buffer + cursor, src, n); \
} while (false)
SAFE_CURSOR_UPDATE(1);
memcpy(output_buffer + cursor, &jhgm_version, 1);
@@ -122,8 +129,7 @@ JXL_BOOL JxlGainMapWriteBundle(const JxlGainMapBundle* map_bundle,
StoreBE16(map_bundle->gain_map_metadata_size, output_buffer + cursor);
SAFE_CURSOR_UPDATE(map_bundle->gain_map_metadata_size);
memcpy(output_buffer + cursor, map_bundle->gain_map_metadata,
map_bundle->gain_map_metadata_size);
SAFE_COPY(map_bundle->gain_map_metadata, map_bundle->gain_map_metadata_size);
jxl::Bytes bytes = color_encoding_writer.GetSpan();
uint8_t color_enc_size = static_cast<uint8_t>(bytes.size());
@@ -132,18 +138,18 @@ JXL_BOOL JxlGainMapWriteBundle(const JxlGainMapBundle* map_bundle,
memcpy(output_buffer + cursor, &color_enc_size, 1);
SAFE_CURSOR_UPDATE(color_enc_size);
memcpy(output_buffer + cursor, bytes.data(), color_enc_size);
SAFE_COPY(bytes.data(), color_enc_size);
SAFE_CURSOR_UPDATE(4);
StoreBE32(map_bundle->alt_icc_size, output_buffer + cursor);
SAFE_CURSOR_UPDATE(map_bundle->alt_icc_size);
memcpy(output_buffer + cursor, map_bundle->alt_icc, map_bundle->alt_icc_size);
SAFE_COPY(map_bundle->alt_icc, map_bundle->alt_icc_size);
SAFE_CURSOR_UPDATE(map_bundle->gain_map_size);
memcpy(output_buffer + cursor, map_bundle->gain_map,
map_bundle->gain_map_size);
SAFE_COPY(map_bundle->gain_map, map_bundle->gain_map_size);
#undef SAFE_COPY
#undef SAFE_CURSOR_UPDATE
cursor = next_cursor;
@@ -180,6 +180,15 @@ ChunkedPackedFrame::ChunkedPackedFrame(
PackedPixelFile::PackedPixelFile() { JxlEncoderInitBasicInfo(&info); };
Status PackedPixelFile::Validate() const {
for (const auto& frame : frames) {
if (frame.extra_channels.size() != extra_channels_info.size()) {
return JXL_FAILURE("Extra channel count mismatch");
}
}
return true;
}
Status PackedPixelFile::ShrinkTo(size_t new_xsize, size_t new_ysize) {
for (auto& frame : frames) {
JXL_RETURN_IF_ERROR(frame.ShrinkTo(new_xsize, new_ysize));
@@ -240,6 +240,9 @@ class PackedPixelFile {
size_t xsize() const { return info.xsize; }
size_t ysize() const { return info.ysize; }
// Each frame must supply exactly the declared extra channels.
Status Validate() const;
// Logical resize; storage is not reallocated; stride is unchanged.
Status ShrinkTo(size_t new_xsize, size_t new_ysize);
};
@@ -160,11 +160,10 @@ typedef struct {
* can always convert to linear or non-linear sRGB or to XYB). If the original
* profile is used, the decoder outputs pixel data in the color space matching
* that profile, but doesn't convert it to any other color space. If the
* original profile is not used, the decoder only outputs the data as sRGB
* (linear if outputting to floating point, nonlinear with standard sRGB
* transfer function if outputting to unsigned integers) but will not convert
* it to the original color profile. The decoder also does not convert to
* the target display color profile. To convert the pixel data produced by
* original profile is not used and @ref JxlDecoderSetOutputColorProfile is
* not called, the decoder converts the data to the original profile if it's
* encoded as a @ref JxlColorEncoding rather than an ICC color profile, and
* falls back to linear sRGB otherwise. To convert the pixel data produced by
* the decoder to the original color profile, one of the JxlDecoderGetColor*
* functions needs to be called with
* ::JXL_COLOR_PROFILE_TARGET_DATA to get the color profile of the decoder
+25 -4
View File
@@ -340,12 +340,13 @@ typedef enum {
*/
JXL_ENC_FRAME_SETTING_JPEG_COMPRESS_BOXES = 33,
/** Control what kind of input buffering is used, when using chunked image frames.
* When using streaming input the encoder minimizes memory usage, potentially at
* a cost in compression density (though not necessarily).
/** Control what kind of input buffering is used, when using chunked image
* frames.
* When using streaming input the encoder minimizes memory usage, potentially
* at a cost in compression density (though not necessarily).
* -1 = default (let the encoder decide)
* 0 = buffers everything, basically the same as non-streamed code path
(mainly for testing)
* (mainly for testing)
* 1 = buffers everything for images that are 2048 x 2048 or smaller, and
* uses streaming input and buffered output for larger images
* 2 = same as 1, but the threshold to use streaming input is lower
@@ -417,6 +418,26 @@ typedef enum {
*/
JXL_ENC_FRAME_SETTING_OUTPUT_MODE = 40,
/** Strips the alpha channel from the frame/image.
* -1 = default (let the encoder decide: strip if fully opaque in lossy mode,
* keep in lossless).
* 0 = never strip (always keep alpha).
* 1 = always strip alpha (force strip).
* 2 = strip alpha if fully opaque.
*
* Note: Only applies when the image has exactly one extra channel (alpha).
* For single-frame auto-detection, the encoder must know there are no further
* frames (e.g. via @ref JxlEncoderCloseFrames or @ref JxlEncoderCloseInput)
* before output processing begins. The setting on the first frame dictates
* codestream behavior.
*/
JXL_ENC_FRAME_SETTING_STRIP_ALPHA = 41,
/** Enable or disable LF Smoothing for lossless JPEG recompression.
* -1 = default, 0 = disable smoothing, 1 = enable smoothing.
*/
JXL_ENC_FRAME_SETTING_JPEG_RECON_LFS = 42,
/** Enum value not to be used as an option. This value is added to force the
* C compiler to have the enum to take a known size.
*/
+3 -1
View File
@@ -107,7 +107,9 @@ struct AliasTable {
#if JXL_BYTE_ORDER_LITTLE
uint64_t entry;
memcpy(&entry, &table[i].cutoff, sizeof(entry));
static_assert(sizeof(table[i]) == sizeof(entry),
"ANS table entries must match the lookup word size");
memcpy(&entry, &table[i], sizeof(entry));
const size_t cutoff = entry & 0xFF; // = MOVZX
const size_t right_value = (entry >> 8) & 0xFF; // = MOVZX
const size_t freq0 = (entry >> 16) & 0xFFFF;
+1 -1
View File
@@ -223,7 +223,7 @@ void TestCheckpointing(bool ans, bool lz77) {
EntropyEncodingData codes;
HistogramParams params;
params.lz77_method = lz77 ? HistogramParams::LZ77Method::kLZ77
params.lz77_method = lz77 ? HistogramParams::LZ77Method::kLZ77b3w3f
: HistogramParams::LZ77Method::kNone;
params.force_huffman = !ans;
+2 -2
View File
@@ -18,12 +18,12 @@
#include <type_traits>
#include <vector>
#include "lib/jxl/base/compiler_specific.h"
#if JXL_COMPILER_MSVC
#include <intrin.h>
#endif
#include "lib/jxl/base/compiler_specific.h"
namespace jxl {
// Some enums and typedefs used by more than one header file.
@@ -68,7 +68,7 @@
#define JXL_NORETURN
#endif
#if JXL_COMPILER_MSVC
#if JXL_COMPILER_MSVC && !defined(__clang__)
#define JXL_MAYBE_UNUSED
#else
// Encountered "attribute list cannot appear here" when using the C++17
+24
View File
@@ -55,6 +55,7 @@ class Span {
constexpr T* end() const noexcept { return data() + size(); }
constexpr T& operator[](size_t i) const noexcept {
JXL_DASSERT(i < len_);
// MSVC 2015 accepts this as constexpr, but not ptr_[i]
return *(data() + i);
}
@@ -66,6 +67,29 @@ class Span {
return true;
}
// Bounds-checked replacement for `Span<T>(span.data() + offset, count)`.
//
// Returns an error rather than aborting: callers derive `offset` and `count`
// from untrusted input, so an out-of-range request is ordinary rejection of a
// malformed file, not a programmer error. (JXL_ENSURE would be wrong here --
// it aborts in debug and fuzzer builds.)
//
// `len_ - offset` is only evaluated once `offset <= len_` is known to hold,
// so a caller passing an underflowed `count` is rejected rather than
// wrapping.
StatusOr<Span<T>> subspan(size_t offset, size_t count) const {
if (offset > len_ || count > len_ - offset) {
return JXL_FAILURE("Span::subspan out of range");
}
return Span<T>(ptr_ + offset, count);
}
// Bounds-checked suffix starting at `offset`.
StatusOr<Span<T>> subspan(size_t offset) const {
if (offset > len_) return JXL_FAILURE("Span::subspan out of range");
return Span<T>(ptr_ + offset, len_ - offset);
}
void AppendTo(std::vector<NCT>& dst) const {
dst.insert(dst.end(), begin(), end());
}
+112
View File
@@ -0,0 +1,112 @@
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
#include "lib/jxl/base/span.h"
#include <cstddef>
#include <cstdint>
#include <utility>
#include <vector>
#include "lib/jxl/test_utils.h"
#include "lib/jxl/testing.h"
namespace jxl {
namespace {
std::vector<uint8_t> Iota(size_t n) {
std::vector<uint8_t> v(n);
for (size_t i = 0; i < n; ++i) v[i] = static_cast<uint8_t>(i);
return v;
}
TEST(SpanTest, SubspanReturnsRequestedRange) {
const std::vector<uint8_t> data = Iota(10);
const Bytes all(data.data(), data.size());
JXL_TEST_ASSIGN_OR_DIE(Bytes middle, all.subspan(2, 3));
EXPECT_EQ(3u, middle.size());
EXPECT_EQ(2u, middle[0]);
EXPECT_EQ(4u, middle[2]);
JXL_TEST_ASSIGN_OR_DIE(Bytes whole, all.subspan(0, 10));
EXPECT_EQ(10u, whole.size());
EXPECT_EQ(all.data(), whole.data());
}
TEST(SpanTest, SubspanSuffixRunsToEnd) {
const std::vector<uint8_t> data = Iota(10);
const Bytes all(data.data(), data.size());
JXL_TEST_ASSIGN_OR_DIE(Bytes tail, all.subspan(7));
EXPECT_EQ(3u, tail.size());
EXPECT_EQ(7u, tail[0]);
// A suffix starting exactly at the end is empty, not an error.
JXL_TEST_ASSIGN_OR_DIE(Bytes end, all.subspan(10));
EXPECT_TRUE(end.empty());
}
// An empty range at the one-past-the-end offset is well defined and must be
// accepted; only offsets *beyond* that are rejected.
TEST(SpanTest, SubspanAcceptsEmptyRangeAtEnd) {
const std::vector<uint8_t> data = Iota(4);
const Bytes all(data.data(), data.size());
JXL_TEST_ASSIGN_OR_DIE(Bytes empty, all.subspan(4, 0));
EXPECT_TRUE(empty.empty());
}
TEST(SpanTest, SubspanRejectsOutOfRange) {
const std::vector<uint8_t> data = Iota(10);
const Bytes all(data.data(), data.size());
EXPECT_FALSE(all.subspan(11, 0).ok()); // offset past the end
EXPECT_FALSE(all.subspan(11).ok()); // suffix past the end
EXPECT_FALSE(all.subspan(0, 11).ok()); // count longer than the span
EXPECT_FALSE(all.subspan(5, 6).ok()); // offset + count overruns
EXPECT_FALSE(all.subspan(10, 1).ok()); // one byte past the end
}
// Callers commonly compute a count by subtracting a fixed header/trailer size,
// e.g. `chunk.subspan(8, chunk.size() - 12)`. When the span is shorter than
// that constant the subtraction wraps to a huge size_t. subspan must reject
// the wrapped value rather than construct a span over unrelated memory, so the
// bound is checked as `count <= len_ - offset` only after `offset <= len_`.
TEST(SpanTest, SubspanRejectsUnderflowedCount) {
const std::vector<uint8_t> data = Iota(8);
const Bytes chunk(data.data(), data.size());
const size_t wrapped = chunk.size() - 12; // wraps: 8 - 12
EXPECT_GT(wrapped, chunk.size());
EXPECT_FALSE(chunk.subspan(8, wrapped).ok());
}
TEST(SpanTest, SubspanOfEmptySpan) {
const Bytes empty;
EXPECT_EQ(0u, empty.size());
JXL_TEST_ASSIGN_OR_DIE(Bytes same, empty.subspan(0, 0));
EXPECT_TRUE(same.empty());
EXPECT_FALSE(empty.subspan(1, 0).ok());
EXPECT_FALSE(empty.subspan(0, 1).ok());
}
TEST(SpanTest, SubspanComposes) {
const std::vector<uint8_t> data = Iota(16);
const Bytes all(data.data(), data.size());
JXL_TEST_ASSIGN_OR_DIE(Bytes body, all.subspan(4, 8));
JXL_TEST_ASSIGN_OR_DIE(Bytes inner, body.subspan(2, 4));
EXPECT_EQ(4u, inner.size());
EXPECT_EQ(6u, inner[0]);
// The narrowed span carries its own bound.
EXPECT_FALSE(body.subspan(0, 9).ok());
}
} // namespace
} // namespace jxl
+11 -3
View File
@@ -52,6 +52,12 @@ Status PerformBlending(
break;
}
}
if (JXL_UNLIKELY(xsize == 0)) {
// An empty segment is reachable (e.g. a patch whose right edge lands
// exactly on the segment start). Blending nothing also avoids creating
// zero-width (unallocated) ImageF planes and null memcpy destinations.
return true;
}
JXL_ASSIGN_OR_RETURN(ImageF tmp,
ImageF::Create(memory_manager, xsize, 3 + num_ec));
// Blend extra channels first so that we use the pre-blending alpha.
@@ -103,11 +109,11 @@ Status PerformBlending(
continue;
case PatchBlendMode::kReplace:
if (xsize) memcpy(tmp.Row(3 + i), fg[3 + i] + x0, xsize * sizeof(**fg));
memcpy(tmp.Row(3 + i), fg[3 + i] + x0, xsize * sizeof(**fg));
continue;
case PatchBlendMode::kNone:
if (xsize) memcpy(tmp.Row(3 + i), bg[3 + i] + x0, xsize * sizeof(**fg));
memcpy(tmp.Row(3 + i), bg[3 + i] + x0, xsize * sizeof(**fg));
continue;
}
}
@@ -142,6 +148,8 @@ Status PerformBlending(
};
const auto copy = [&](const float* const* src) {
// Reaching here implies xsize > 0: empty segments return before the
// zero-width (unallocated) ImageF would hand memcpy a null destination.
for (size_t p = 0; p < 3; p++) {
memcpy(tmp.Row(p), src[p] + x0, xsize * sizeof(**src));
}
@@ -184,7 +192,7 @@ Status PerformBlending(
}
for (size_t i = 0; i < 3 + num_ec; i++) {
if (xsize != 0) memcpy(out[i] + x0, tmp.Row(i), xsize * sizeof(**out));
memcpy(out[i] + x0, tmp.Row(i), xsize * sizeof(**out));
}
return true;
}
+70
View File
@@ -15,6 +15,11 @@
#include "lib/extras/dec/jxl.h"
#include "lib/extras/packed_image.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/blending.h"
#include "lib/jxl/dec_patch_dictionary.h"
#include "lib/jxl/image.h"
#include "lib/jxl/image_metadata.h"
#include "lib/jxl/test_memory_manager.h"
#include "lib/jxl/test_utils.h"
#include "lib/jxl/testing.h"
@@ -54,5 +59,70 @@ TEST(BlendingTest, Crops) {
}
}
// ---------------------------------------------------------------------------
// Zero-width blend regions.
//
// A zero-width region is legal: PlaneBase::Allocate returns without allocating
// when xsize_ == 0, so every Row() of the temporary in PerformBlending is null.
// The `copy` lambda used to hand that null straight to memcpy as the
// destination -- undefined behaviour even with a zero copy size, and an abort
// under UBSan. Every other blend path in the function already guards with
// `if (xsize)`; these cases pin the one that did not.
// ---------------------------------------------------------------------------
struct BlendFixture {
std::vector<float> bg_storage;
std::vector<float> fg_storage;
std::vector<float> out_storage;
std::vector<float*> bg;
std::vector<float*> fg;
std::vector<float*> out;
};
BlendFixture MakeFixture(size_t x0, size_t xsize) {
BlendFixture f;
const size_t width = x0 + xsize + 1;
f.bg_storage.assign(3 * width, 0.25f);
f.fg_storage.assign(3 * width, 0.75f);
f.out_storage.assign(3 * width, 0.0f);
for (size_t p = 0; p < 3; p++) {
f.bg.push_back(f.bg_storage.data() + p * width);
f.fg.push_back(f.fg_storage.data() + p * width);
f.out.push_back(f.out_storage.data() + p * width);
}
return f;
}
Status Blend(size_t x0, size_t xsize, PatchBlendMode mode) {
JxlMemoryManager* mm = jxl::test::MemoryManager();
BlendFixture f = MakeFixture(x0, xsize);
PatchBlending color_blending{mode, /*alpha_channel=*/0, /*clamp=*/false};
// No extra channels, so `has_alpha` is false and the color switch takes the
// `copy(...)` arm for kBlendAbove / kBlendBelow.
std::vector<ExtraChannelInfo> extra_channel_info;
return PerformBlending(mm, f.bg.data(), f.fg.data(), f.out.data(), x0, xsize,
color_blending, /*ec_blending=*/nullptr,
extra_channel_info);
}
TEST(BlendingTest, ZeroWidthBlendAboveIsSafe) {
EXPECT_TRUE(Blend(/*x0=*/0, /*xsize=*/0, PatchBlendMode::kBlendAbove));
}
TEST(BlendingTest, ZeroWidthBlendBelowIsSafe) {
EXPECT_TRUE(Blend(/*x0=*/0, /*xsize=*/0, PatchBlendMode::kBlendBelow));
}
TEST(BlendingTest, ZeroWidthNonZeroOffsetIsSafe) {
// x0 != 0 exercises the `src[p] + x0` arithmetic as well as the null dest.
EXPECT_TRUE(Blend(/*x0=*/4, /*xsize=*/0, PatchBlendMode::kBlendAbove));
}
TEST(BlendingTest, NonZeroWidthStillBlends) {
// Control: a real blend region must keep working exactly as before.
EXPECT_TRUE(Blend(/*x0=*/0, /*xsize=*/8, PatchBlendMode::kBlendAbove));
EXPECT_TRUE(Blend(/*x0=*/2, /*xsize=*/4, PatchBlendMode::kBlendBelow));
}
} // namespace
} // namespace jxl
@@ -19,23 +19,26 @@ namespace jxl {
JxlBoxContentDecoder::JxlBoxContentDecoder() = default;
JxlBoxContentDecoder::~JxlBoxContentDecoder() {
if (brotli_dec) {
BrotliDecoderDestroyInstance(brotli_dec);
}
}
JxlBoxContentDecoder::~JxlBoxContentDecoder() { Reset(); }
void JxlBoxContentDecoder::StartBox(bool brob_decode, bool box_until_eof,
size_t contents_size) {
void JxlBoxContentDecoder::Reset() {
if (brotli_dec) {
BrotliDecoderDestroyInstance(brotli_dec);
brotli_dec = nullptr;
}
header_done_ = false;
brob_decode_ = false;
box_until_eof_ = false;
remaining_ = 0;
pos_ = 0;
}
void JxlBoxContentDecoder::StartBox(bool brob_decode, bool box_until_eof,
size_t contents_size) {
Reset();
brob_decode_ = brob_decode;
box_until_eof_ = box_until_eof;
remaining_ = box_until_eof ? 0 : contents_size;
pos_ = 0;
}
JxlDecoderStatus JxlBoxContentDecoder::Process(const uint8_t* next_in,
@@ -22,6 +22,7 @@ class JxlBoxContentDecoder {
JxlBoxContentDecoder();
~JxlBoxContentDecoder();
void Reset();
void StartBox(bool brob_decode, bool box_until_eof, size_t contents_size);
// Outputs decoded bytes from the box, decoding with brotli if needed.
+286 -17
View File
@@ -444,6 +444,105 @@ void MatrixProduct(const skcms_Matrix3x3& matrix, const Color& vector_in,
}
}
bool IsAcceptableCHAD(const skcms_Matrix3x3& chad) {
Matrix3x3d chad_double;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
chad_double[i][j] = chad.vals[i][j];
}
}
return Inv3x3Matrix(chad_double);
}
bool HasICCVersion4DisplayClass(const skcms_ICCProfile& profile) {
return profile.buffer != nullptr && profile.size >= 128 &&
profile.buffer[8] == 4 && profile.buffer[12] == 'm' &&
profile.buffer[13] == 'n' && profile.buffer[14] == 't' &&
profile.buffer[15] == 'r' && profile.buffer[36] == 'a' &&
profile.buffer[37] == 'c' && profile.buffer[38] == 's' &&
profile.buffer[39] == 'p';
}
bool HasTag(const skcms_ICCProfile& profile, const char* signature) {
constexpr size_t kTagCountOffset = 128;
constexpr size_t kTagTableOffset = 132;
constexpr size_t kTagRecordSize = 12;
if (profile.buffer == nullptr || profile.size < kTagCountOffset + 4) {
return false;
}
const uint32_t tag_count =
(static_cast<uint32_t>(profile.buffer[kTagCountOffset]) << 24) |
(static_cast<uint32_t>(profile.buffer[kTagCountOffset + 1]) << 16) |
(static_cast<uint32_t>(profile.buffer[kTagCountOffset + 2]) << 8) |
static_cast<uint32_t>(profile.buffer[kTagCountOffset + 3]);
if (tag_count > (profile.size - kTagTableOffset) / kTagRecordSize) {
return false;
}
for (uint32_t i = 0; i < tag_count; ++i) {
const size_t offset = kTagTableOffset + kTagRecordSize * i;
if (std::memcmp(profile.buffer + offset, signature, 4) == 0) {
return true;
}
}
return false;
}
bool HasLUTTags(const skcms_ICCProfile& profile) {
for (const char* signature :
{"A2B0", "A2B1", "A2B2", "B2A0", "B2A1", "B2A2", "D2B0", "D2B1", "D2B2",
"D2B3", "B2D0", "B2D1", "B2D2", "B2D3"}) {
if (HasTag(profile, signature)) return true;
}
return false;
}
bool HasParametricTRCs(const skcms_ICCProfile& profile) {
return profile.trc[0].table_entries == 0 &&
profile.trc[1].table_entries == 0 && profile.trc[2].table_entries == 0;
}
bool IsApproximatelyD50(const Color& XYZ) {
static constexpr Color kD50XYZ{0.96420288f, 1.0f, 0.82490540f};
constexpr float kTolerance = 1e-3f;
return std::abs(XYZ[0] - kD50XYZ[0]) < kTolerance &&
std::abs(XYZ[1] - kD50XYZ[1]) < kTolerance &&
std::abs(XYZ[2] - kD50XYZ[2]) < kTolerance;
}
bool GetNonconformingMatrixProfileWhitePoint(const skcms_ICCProfile& profile,
const Color& media_white_point_XYZ,
Color& adapted_white_point_XYZ) {
// Some v4 display profiles contain a CHAD but retain the pre-v4 media
// white point. For a matrix/TRC profile, the colorant column sum is the
// adapted neutral and is a more reliable source for undoing that CHAD.
if (!HasICCVersion4DisplayClass(profile) ||
profile.data_color_space != skcms_Signature_RGB ||
profile.pcs != skcms_Signature_XYZ || !profile.has_trc ||
!profile.has_toXYZD50 || !HasTag(profile, "chad") ||
HasLUTTags(profile) || IsApproximatelyD50(media_white_point_XYZ)) {
return false;
}
Color matrix_white_point_XYZ;
for (int i = 0; i < 3; ++i) {
matrix_white_point_XYZ[i] = profile.toXYZD50.vals[i][0] +
profile.toXYZD50.vals[i][1] +
profile.toXYZD50.vals[i][2];
}
if (!IsApproximatelyD50(matrix_white_point_XYZ)) return false;
adapted_white_point_XYZ = matrix_white_point_XYZ;
return true;
}
bool RecoverMatrixProfileWhitePoint(const skcms_ICCProfile& profile,
const Color& media_white_point_XYZ,
Color& adapted_white_point_XYZ) {
skcms_Matrix3x3 CHAD;
return HasParametricTRCs(profile) &&
GetNonconformingMatrixProfileWhitePoint(profile, media_white_point_XYZ,
adapted_white_point_XYZ) &&
skcms_GetCHAD(&profile, &CHAD) && IsAcceptableCHAD(CHAD);
}
// Returns white point that was specified when creating the profile.
JXL_MUST_USE_RESULT Status UnadaptedWhitePoint(const skcms_ICCProfile& profile,
CIExy* out) {
@@ -458,8 +557,15 @@ JXL_MUST_USE_RESULT Status UnadaptedWhitePoint(const skcms_ICCProfile& profile,
*out = CIExyFromXYZ(media_white_point_XYZ);
return true;
}
// Otherwise, it has been adapted to the PCS white point using said matrix,
// and the adaptation needs to be undone.
Color adapted_white_point_XYZ;
if (RecoverMatrixProfileWhitePoint(profile, media_white_point_XYZ,
adapted_white_point_XYZ)) {
media_white_point_XYZ = adapted_white_point_XYZ;
}
// The media white point is expressed in the PCS after the chromatic
// adaptation in the profile. Undo that adaptation to recover the source
// white point. The compatibility path above replaces the inconsistent media
// white point with the adapted neutral before applying the same operation.
skcms_Matrix3x3 inverse_CHAD;
if (!skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)) {
return JXL_FAILURE("Non-invertible ChromaticAdaptation matrix");
@@ -541,6 +647,16 @@ Status DetectTransferFunction(const skcms_ICCProfile& profile,
ColorEncoding* JXL_RESTRICT c) {
JXL_ENSURE(c->color_space != ColorSpace::kXYB);
Color media_white_point_XYZ;
Color adapted_white_point_XYZ;
if (!HasParametricTRCs(profile) &&
skcms_GetWTPT(&profile, media_white_point_XYZ.data()) &&
GetNonconformingMatrixProfileWhitePoint(profile, media_white_point_XYZ,
adapted_white_point_XYZ)) {
c->tf.SetTransferFunction(TransferFunction::kUnknown);
return true;
}
float gamma[3] = {};
if (profile.has_trc) {
const auto IsGamma = [](const skcms_TransferFunction& tf) {
@@ -608,8 +724,12 @@ ColorSpace ColorSpaceFromProfile(const Profile& profile) {
}
// "profile1" is pre-decoded to save time in DetectTransferFunction.
constexpr double kIccEquivalenceTolerance = 2E-4;
constexpr double kLegacyMatrixProfileEquivalenceTolerance = 3E-4;
Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
const IccBytes& icc, const ColorEncoding& c) {
const IccBytes& icc, const ColorEncoding& c,
const double tolerance) {
const uint32_t type_src = Type64(c);
Profile profile2;
@@ -644,7 +764,7 @@ Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
for (in[0] = init; in[0] < 1.0; in[0] += step / 8) {
cmsDoTransform(xform1.get(), in, out1, 1);
cmsDoTransform(xform2.get(), in, out2, 1);
if (!cms::ApproxEq(out1[0], out2[0], 2E-4)) {
if (!cms::ApproxEq(out1[0], out2[0], tolerance)) {
return false;
}
}
@@ -655,7 +775,7 @@ Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
cmsDoTransform(xform1.get(), in, out1, 1);
cmsDoTransform(xform2.get(), in, out2, 1);
for (size_t i = 0; i < 3; ++i) {
if (!cms::ApproxEq(out1[i], out2[i], 2E-4)) {
if (!cms::ApproxEq(out1[i], out2[i], tolerance)) {
return false;
}
}
@@ -667,6 +787,116 @@ Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
return true;
}
bool HasLUTTags(const Profile& profile) {
for (const auto tag :
{cmsSigAToB0Tag, cmsSigAToB1Tag, cmsSigAToB2Tag, cmsSigBToA0Tag,
cmsSigBToA1Tag, cmsSigBToA2Tag, cmsSigDToB0Tag, cmsSigDToB1Tag,
cmsSigDToB2Tag, cmsSigDToB3Tag, cmsSigBToD0Tag, cmsSigBToD1Tag,
cmsSigBToD2Tag, cmsSigBToD3Tag}) {
if (cmsIsTag(profile.get(), tag)) return true;
}
return false;
}
bool HasParametricTRCs(const Profile& profile) {
for (const auto tag :
{cmsSigRedTRCTag, cmsSigGreenTRCTag, cmsSigBlueTRCTag}) {
const auto* trc =
static_cast<const cmsToneCurve*>(cmsReadTag(profile.get(), tag));
if (trc == nullptr || cmsGetToneCurveParametricType(trc) <= 0) return false;
}
return true;
}
bool IsApproximatelyD50(const cmsCIEXYZ& XYZ) {
const cmsCIEXYZ d50 = D50_XYZ();
constexpr cmsFloat64Number kTolerance = 1e-3;
return std::abs(XYZ.X - d50.X) < kTolerance &&
std::abs(XYZ.Y - d50.Y) < kTolerance &&
std::abs(XYZ.Z - d50.Z) < kTolerance;
}
bool GetInverseCHAD(const Profile& profile, Matrix3x3d* inverse_chad) {
const cmsMAT3* chad = static_cast<const cmsMAT3*>(
cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag));
if (chad == nullptr) return false;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
(*inverse_chad)[i][j] = chad->v[i].n[j];
}
}
return Inv3x3Matrix(*inverse_chad);
}
bool GetNonconformingMatrixProfileWhitePoint(const Profile& profile,
const cmsCIEXYZ& media_white_point,
cmsCIEXYZ* matrix_white_point) {
const double version = cmsGetProfileVersion(profile.get());
if (version < 4.0 || version >= 5.0 ||
cmsGetDeviceClass(profile.get()) != cmsSigDisplayClass ||
cmsGetColorSpace(profile.get()) != cmsSigRgbData ||
cmsGetPCS(profile.get()) != cmsSigXYZData ||
cmsReadTag(profile.get(), cmsSigRedColorantTag) == nullptr ||
cmsReadTag(profile.get(), cmsSigGreenColorantTag) == nullptr ||
cmsReadTag(profile.get(), cmsSigBlueColorantTag) == nullptr ||
cmsReadTag(profile.get(), cmsSigRedTRCTag) == nullptr ||
cmsReadTag(profile.get(), cmsSigGreenTRCTag) == nullptr ||
cmsReadTag(profile.get(), cmsSigBlueTRCTag) == nullptr ||
HasLUTTags(profile) || IsApproximatelyD50(media_white_point)) {
return false;
}
const cmsCIEXYZ* adapted_r = static_cast<const cmsCIEXYZ*>(
cmsReadTag(profile.get(), cmsSigRedColorantTag));
const cmsCIEXYZ* adapted_g = static_cast<const cmsCIEXYZ*>(
cmsReadTag(profile.get(), cmsSigGreenColorantTag));
const cmsCIEXYZ* adapted_b = static_cast<const cmsCIEXYZ*>(
cmsReadTag(profile.get(), cmsSigBlueColorantTag));
*matrix_white_point = {adapted_r->X + adapted_g->X + adapted_b->X,
adapted_r->Y + adapted_g->Y + adapted_b->Y,
adapted_r->Z + adapted_g->Z + adapted_b->Z};
return IsApproximatelyD50(*matrix_white_point);
}
bool RecoverMatrixProfileWhitePoint(const Profile& profile,
const cmsCIEXYZ& media_white_point,
cmsCIEXYZ* unadapted_white_point) {
cmsCIEXYZ matrix_white_point;
if (!HasParametricTRCs(profile) ||
!GetNonconformingMatrixProfileWhitePoint(profile, media_white_point,
&matrix_white_point)) {
return false;
}
Matrix3x3d inverse_chad = {};
if (!GetInverseCHAD(profile, &inverse_chad)) return false;
const Vector3d adapted = {matrix_white_point.X, matrix_white_point.Y,
matrix_white_point.Z};
Vector3d unadapted = {};
Mul3x3Vector(inverse_chad, adapted, unadapted);
unadapted_white_point->X = unadapted[0];
unadapted_white_point->Y = unadapted[1];
unadapted_white_point->Z = unadapted[2];
return true;
}
bool HasNonconformingMatrixProfileShape(const Profile& profile) {
const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>(
cmsReadTag(profile.get(), cmsSigMediaWhitePointTag));
const cmsMAT3* chad = static_cast<const cmsMAT3*>(
cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag));
cmsCIEXYZ matrix_white_point;
return white_point != nullptr && chad != nullptr &&
GetNonconformingMatrixProfileWhitePoint(profile, *white_point,
&matrix_white_point);
}
bool IsMatrixProfileWithNonconformingWhitePoint(const Profile& profile) {
Matrix3x3d inverse_chad = {};
return HasNonconformingMatrixProfileShape(profile) &&
GetInverseCHAD(profile, &inverse_chad);
}
// Returns white point that was specified when creating the profile.
// NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation
// differs between ICC versions.
@@ -675,11 +905,19 @@ JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context,
const ColorEncoding& c) {
const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>(
cmsReadTag(profile.get(), cmsSigMediaWhitePointTag));
if (white_point != nullptr &&
cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag) == nullptr) {
const cmsMAT3* chad = static_cast<const cmsMAT3*>(
cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag));
if (white_point != nullptr && chad == nullptr) {
// No chromatic adaptation matrix: the white point is already unadapted.
return *white_point;
}
if (white_point != nullptr && chad != nullptr) {
cmsCIEXYZ unadapted_white_point;
if (RecoverMatrixProfileWhitePoint(profile, *white_point,
&unadapted_white_point)) {
return unadapted_white_point;
}
}
cmsCIEXYZ XYZ = {1.0, 1.0, 1.0};
Profile profile_xyz;
@@ -746,14 +984,26 @@ Status IdentifyPrimaries(const cmsContext context, const Profile& profile,
adapted_b = &converted_rgb[2];
}
// TODO(janwas): no longer assume Bradford and D50.
// Undo the chromatic adaptation.
const cmsCIEXYZ d50 = D50_XYZ();
cmsCIEXYZ r, g, b;
cmsAdaptToIlluminant(&r, &d50, &wp_unadapted, adapted_r);
cmsAdaptToIlluminant(&g, &d50, &wp_unadapted, adapted_g);
cmsAdaptToIlluminant(&b, &d50, &wp_unadapted, adapted_b);
Matrix3x3d inverse_chad = {};
if (HasParametricTRCs(profile) &&
IsMatrixProfileWithNonconformingWhitePoint(profile) &&
GetInverseCHAD(profile, &inverse_chad)) {
const cmsCIEXYZ* adapted[] = {adapted_r, adapted_g, adapted_b};
cmsCIEXYZ* unadapted[] = {&r, &g, &b};
for (size_t i = 0; i < 3; ++i) {
const Vector3d in = {adapted[i]->X, adapted[i]->Y, adapted[i]->Z};
Vector3d out = {};
Mul3x3Vector(inverse_chad, in, out);
*unadapted[i] = {out[0], out[1], out[2]};
}
} else {
// TODO(janwas): no longer assume Bradford and D50 for other profiles.
const cmsCIEXYZ d50 = D50_XYZ();
cmsAdaptToIlluminant(&r, &d50, &wp_unadapted, adapted_r);
cmsAdaptToIlluminant(&g, &d50, &wp_unadapted, adapted_g);
cmsAdaptToIlluminant(&b, &d50, &wp_unadapted, adapted_b);
}
const PrimariesCIExy rgb = {CIExyFromXYZ(r), CIExyFromXYZ(g),
CIExyFromXYZ(b)};
@@ -764,6 +1014,22 @@ Status DetectTransferFunction(const cmsContext context, const Profile& profile,
ColorEncoding* JXL_RESTRICT c) {
JXL_ENSURE(c->color_space != ColorSpace::kXYB);
const bool nonconforming_matrix_profile =
HasNonconformingMatrixProfileShape(profile);
const bool compatibility_profile =
IsMatrixProfileWithNonconformingWhitePoint(profile);
if (nonconforming_matrix_profile &&
(!compatibility_profile || !HasParametricTRCs(profile))) {
c->tf.SetTransferFunction(TransferFunction::kUnknown);
return true;
}
// Matrix/TRC coefficients and the canonical D50 white point are quantized
// independently in ICC. Allow that small difference for this compatibility
// case while retaining the functional equivalence check.
const double equivalence_tolerance =
compatibility_profile ? kLegacyMatrixProfileEquivalenceTolerance
: kIccEquivalenceTolerance;
float gamma = 0;
if (const auto* gray_trc = reinterpret_cast<const cmsToneCurve*>(
cmsReadTag(profile.get(), cmsSigGrayTRCTag))) {
@@ -796,7 +1062,8 @@ Status DetectTransferFunction(const cmsContext context, const Profile& profile,
if (gamma != 0 && c->tf.SetGamma(gamma)) {
IccBytes icc_test;
if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
ProfileEquivalentToICC(context, profile, icc_test, *c)) {
ProfileEquivalentToICC(context, profile, icc_test, *c,
equivalence_tolerance)) {
return true;
}
}
@@ -809,7 +1076,8 @@ Status DetectTransferFunction(const cmsContext context, const Profile& profile,
IccBytes icc_test;
if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
ProfileEquivalentToICC(context, profile, icc_test, *c)) {
ProfileEquivalentToICC(context, profile, icc_test, *c,
equivalence_tolerance)) {
return true;
}
}
@@ -829,7 +1097,8 @@ cmsContext GetContext() {
context_.reset(cmsCreateContext(nullptr, nullptr));
JXL_DASSERT(context_ != nullptr);
cmsSetLogErrorHandlerTHR(static_cast<cmsContext>(context_.get()), &ErrorHandler);
cmsSetLogErrorHandlerTHR(static_cast<cmsContext>(context_.get()),
&ErrorHandler);
}
return static_cast<cmsContext>(context_.get());
}
@@ -8,16 +8,20 @@
#include <jxl/memory_manager.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ostream>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "lib/jxl/base/byte_order.h"
#include "lib/jxl/base/common.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/span.h"
@@ -28,6 +32,7 @@
#include "lib/jxl/image.h"
#include "lib/jxl/image_ops.h"
#include "lib/jxl/image_test_utils.h"
#include "lib/jxl/test_image.h"
#include "lib/jxl/test_memory_manager.h"
#include "lib/jxl/test_utils.h"
#include "lib/jxl/testing.h"
@@ -50,6 +55,223 @@ constexpr size_t kWidth = 16;
constexpr size_t kNumThreads = 1; // only have a single row.
size_t CountDistinctColors(const extras::PackedImage& image, size_t limit) {
std::set<std::array<uint16_t, 3>> colors;
for (size_t y = 0; y < image.ysize; ++y) {
for (size_t x = 0; x < image.xsize; ++x) {
std::array<uint16_t, 3> color;
for (size_t c = 0; c < color.size(); ++c) {
color[c] = static_cast<uint16_t>(std::lround(
Clamp1(image.GetPixelValue(y, x, c), 0.0f, 1.0f) * 65535.0f));
}
colors.insert(color);
if (colors.size() > limit) return colors.size();
}
}
return colors.size();
}
size_t FindIccTagRecord(const IccBytes& icc, const char* signature) {
if (icc.size() < 132U) return 0;
const uint32_t tag_count = LoadBE32(icc.data() + 128);
if (tag_count > (icc.size() - 132U) / 12U) return 0;
for (uint32_t i = 0; i < tag_count; ++i) {
const size_t record = 132U + 12U * i;
if (std::memcmp(icc.data() + record, signature, 4) == 0) return record;
}
return 0;
}
bool HasIccTag(const IccBytes& icc, const char* signature) {
return FindIccTagRecord(icc, signature) != 0;
}
StatusOr<IccBytes> CreateDisplayP3MatrixProfile() {
ColorEncoding encoding;
encoding.SetColorSpace(ColorSpace::kRGB);
JXL_RETURN_IF_ERROR(encoding.SetWhitePointType(WhitePoint::kD65));
JXL_RETURN_IF_ERROR(encoding.SetPrimariesType(Primaries::kP3));
encoding.Tf().SetTransferFunction(TransferFunction::kSRGB);
encoding.SetRenderingIntent(RenderingIntent::kRelative);
JXL_RETURN_IF_ERROR(encoding.CreateICC());
IccBytes icc = encoding.ICC();
const size_t cicp_record = FindIccTagRecord(icc, "cicp");
JXL_ENSURE(cicp_record != 0);
std::memcpy(icc.data() + cicp_record, "tst0", 4);
// A zero profile ID is valid and avoids retaining the checksum after the
// intentional test mutation.
std::fill(icc.begin() + 84, icc.begin() + 100, 0);
return icc;
}
StatusOr<IccBytes> CreateNonconformingDisplayP3Profile() {
JXL_ASSIGN_OR_RETURN(IccBytes icc, CreateDisplayP3MatrixProfile());
const size_t wtpt_record = FindIccTagRecord(icc, "wtpt");
JXL_ENSURE(wtpt_record != 0);
const size_t wtpt_offset = LoadBE32(icc.data() + wtpt_record + 4);
const size_t wtpt_size = LoadBE32(icc.data() + wtpt_record + 8);
JXL_ENSURE(wtpt_offset <= icc.size());
JXL_ENSURE(wtpt_size >= 20U && wtpt_size <= icc.size() - wtpt_offset);
JXL_ENSURE(std::memcmp(icc.data() + wtpt_offset, "XYZ ", 4) == 0);
// ICC v4 display profiles normally store D50 here. This deliberately stores
// D65 while retaining CHAD and D50-adapted matrix columns, matching the
// malformed profile shape that triggered issue #4911.
constexpr std::array<double, 3> kD65XYZ = {0.3127 / 0.3290, 1.0,
(1.0 - 0.3127 - 0.3290) / 0.3290};
for (size_t i = 0; i < kD65XYZ.size(); ++i) {
StoreBE32(static_cast<uint32_t>(
static_cast<int32_t>(std::lround(kD65XYZ[i] * 65536.0))),
icc.data() + wtpt_offset + 8U + 4U * i);
}
return icc;
}
StatusOr<test::TestImage> CreateNonconformingDisplayP3TestImage(
const IccBytes& icc, size_t size, size_t bits_per_sample,
size_t alpha_pattern) {
test::TestImage image;
JXL_RETURN_IF_ERROR(image.SetDimensions(size, size));
JXL_RETURN_IF_ERROR(image.SetChannels(4));
image.SetAllBitDepths(bits_per_sample);
JXL_RETURN_IF_ERROR(image.SetColorEncoding("DisplayP3"));
JXL_ASSIGN_OR_RETURN(auto frame, image.AddFrame());
for (size_t y = 0; y < size; ++y) {
for (size_t x = 0; x < size; ++x) {
const double ramp = static_cast<double>(x + y * size) /
static_cast<double>(size * size - 1);
JXL_RETURN_IF_ERROR(frame.SetValue(y, x, 0, 0.945 + 0.055 * ramp));
JXL_RETURN_IF_ERROR(frame.SetValue(y, x, 1, 2.0 * ramp / 65535.0));
JXL_RETURN_IF_ERROR(frame.SetValue(y, x, 2, 8.0 * ramp / 65535.0));
const double alpha = alpha_pattern == 0 ? 1.0
: alpha_pattern == 1 ? (x + y) % 2
: 0.25 + 0.75 * ramp;
JXL_RETURN_IF_ERROR(frame.SetValue(y, x, 3, alpha));
}
}
image.ppf().icc.assign(icc.begin(), icc.end());
image.ppf().orig_icc.assign(icc.begin(), icc.end());
image.ppf().primary_color_representation =
extras::PackedPixelFile::kIccIsPrimary;
return image;
}
bool ReplaceIccTag(IccBytes* target, const char* target_signature,
const char* new_signature, const IccBytes& source,
const char* source_signature) {
const size_t target_record = FindIccTagRecord(*target, target_signature);
const size_t source_record = FindIccTagRecord(source, source_signature);
if (target_record == 0 || source_record == 0) return false;
const size_t source_offset = LoadBE32(source.data() + source_record + 4);
const size_t source_size = LoadBE32(source.data() + source_record + 8);
if (source_offset > source.size() ||
source_size > source.size() - source_offset) {
return false;
}
const size_t target_offset = (target->size() + 3U) & ~size_t{3};
target->resize(target_offset + source_size);
std::copy(source.begin() + source_offset,
source.begin() + source_offset + source_size,
target->begin() + target_offset);
std::memcpy(target->data() + target_record, new_signature, 4);
StoreBE32(static_cast<uint32_t>(target_offset),
target->data() + target_record + 4);
StoreBE32(static_cast<uint32_t>(source_size),
target->data() + target_record + 8);
StoreBE32(static_cast<uint32_t>(target->size()), target->data());
return true;
}
bool ReplaceTrcsWithSampledCurve(IccBytes* icc,
bool make_non_equivalent = true) {
constexpr size_t kEntries = 256;
IccBytes curve(12U + 2U * kEntries);
std::memcpy(curve.data(), "curv", 4);
StoreBE32(kEntries, curve.data() + 8);
for (size_t i = 0; i < kEntries; ++i) {
const double x = static_cast<double>(i) / (kEntries - 1);
double y = x <= 0.04045 ? x / 12.92 : std::pow((x + 0.055) / 1.055, 2.4);
if (make_non_equivalent && i == 25) y += 0.25;
StoreBE16(static_cast<uint16_t>(std::lround(Clamp1(y, 0.0, 1.0) * 65535.0)),
curve.data() + 12U + 2U * i);
}
const size_t offset = (icc->size() + 3U) & ~size_t{3};
icc->resize(offset + curve.size());
std::copy(curve.begin(), curve.end(), icc->begin() + offset);
for (const char* signature : {"rTRC", "gTRC", "bTRC"}) {
const size_t record = FindIccTagRecord(*icc, signature);
if (record == 0) return false;
StoreBE32(static_cast<uint32_t>(offset), icc->data() + record + 4);
StoreBE32(static_cast<uint32_t>(curve.size()), icc->data() + record + 8);
}
StoreBE32(static_cast<uint32_t>(icc->size()), icc->data());
return true;
}
bool ModifiedTrcRemainsICC(const IccBytes& icc, size_t trc_offset,
uint32_t delta) {
IccBytes modified_icc = icc;
StoreBE32(LoadBE32(modified_icc.data() + trc_offset + 16) + delta,
modified_icc.data() + trc_offset + 16);
ColorEncoding modified;
if (!modified.SetICC(std::move(modified_icc), JxlGetDefaultCms())) {
return false;
}
modified.DecideIfWantICC(*JxlGetDefaultCms());
return modified.WantICC();
}
void CheckNonconformingDisplayP3FullSizeLossy(
const extras::PackedPixelFile& input, size_t distinct_color_limit) {
extras::JXLCompressParams params;
params.distance = 1.0f;
// Keep RGBA output for the opaque-alpha case as well as the alpha variants.
params.strip_alpha = 0;
extras::JXLDecompressParams decode_params;
decode_params.accepted_formats.push_back(
{4, input.frames[0].color.format.data_type,
input.frames[0].color.format.endianness, /*align=*/0});
extras::PackedPixelFile output;
ASSERT_GT(test::Roundtrip(input, params, decode_params, nullptr, &output),
0U);
const uint32_t output_bits =
input.frames[0].color.format.data_type == JXL_TYPE_UINT8 ? 8U : 16U;
EXPECT_EQ(output.info.bits_per_sample, output_bits);
EXPECT_EQ(output.primary_color_representation,
extras::PackedPixelFile::kColorEncodingIsPrimary);
EXPECT_GT(CountDistinctColors(output.frames[0].color, distinct_color_limit),
distinct_color_limit);
// Butteraugli converts both inputs through their declared color encodings,
// so this also checks color fidelity in a common viewing space.
EXPECT_LE(test::ButteraugliDistance(input, output), 1.5f);
}
void CheckNonconformingDisplayP3Tolerance(
const extras::PackedPixelFile& input) {
ASSERT_GE(input.icc.size(), 132U);
const uint32_t tag_count = LoadBE32(input.icc.data() + 128);
ASSERT_LE(tag_count, (input.icc.size() - 132U) / 12U);
const size_t trc_record = FindIccTagRecord(input.icc, "rTRC");
ASSERT_GT(trc_record, 0U);
const size_t trc_offset = LoadBE32(input.icc.data() + trc_record + 4);
const size_t trc_size = LoadBE32(input.icc.data() + trc_record + 8);
ASSERT_GT(trc_offset, 0U);
ASSERT_LE(trc_offset + trc_size, input.icc.size());
ASSERT_GE(trc_size, 20U);
ASSERT_EQ(std::memcmp(input.icc.data() + trc_offset, "para", 4), 0);
// Keep the v4 matrix/CHAD/white-point shape but make the shared TRC clearly
// non-equivalent. Both CMS implementations must retain the ICC profile.
EXPECT_TRUE(ModifiedTrcRemainsICC(input.icc, trc_offset, 4096));
#if !JPEGXL_ENABLE_SKCMS
// In the lcms path, adding 16 fixed-point LSBs is a small mutation rejected
// by the relaxed 3E-4 functional-equivalence comparison.
EXPECT_TRUE(ModifiedTrcRemainsICC(input.icc, trc_offset, 16));
#endif
}
struct Globals {
// TODO(deymo): Make this a const.
static Globals* GetInstance() {
@@ -270,6 +492,257 @@ TEST_F(ColorManagementTest, D2700ToSRGB) {
EXPECT_ARRAY_NEAR(sRGB_values, sRGB_expected, 1e-3);
}
TEST_F(ColorManagementTest, NonconformingDisplayP3DoesNotCollapseWithLossyXYB) {
JXL_TEST_ASSIGN_OR_DIE(const IccBytes expected_icc,
CreateNonconformingDisplayP3Profile());
JXL_TEST_ASSIGN_OR_DIE(test::TestImage test_image,
CreateNonconformingDisplayP3TestImage(
expected_icc, 256, 16, /*alpha_pattern=*/0));
extras::PackedPixelFile& input = test_image.ppf();
ASSERT_EQ(input.primary_color_representation,
extras::PackedPixelFile::kIccIsPrimary);
EXPECT_EQ(input.icc, expected_icc);
EXPECT_EQ(input.info.bits_per_sample, 16U);
EXPECT_EQ(input.info.alpha_bits, 16U);
{
ColorEncoding parsed;
ASSERT_TRUE(parsed.SetICC(IccBytes(input.icc), JxlGetDefaultCms()));
ASSERT_TRUE(parsed.HasPrimaries());
EXPECT_EQ(parsed.GetWhitePointType(), WhitePoint::kD65);
EXPECT_EQ(parsed.GetPrimariesType(), Primaries::kP3);
EXPECT_TRUE(parsed.Tf().IsSRGB());
}
ASSERT_NO_FATAL_FAILURE(CheckNonconformingDisplayP3FullSizeLossy(input, 100));
ASSERT_NO_FATAL_FAILURE(CheckNonconformingDisplayP3Tolerance(input));
}
TEST_F(ColorManagementTest, NonconformingDisplayP3InputVariants) {
JXL_TEST_ASSIGN_OR_DIE(const IccBytes icc,
CreateNonconformingDisplayP3Profile());
struct Variant {
size_t bits_per_sample;
size_t alpha_pattern;
};
for (const Variant& variant :
{Variant{8, 0}, Variant{10, 1}, Variant{12, 2}}) {
SCOPED_TRACE(testing::Message()
<< "bits=" << variant.bits_per_sample
<< " alpha_pattern=" << variant.alpha_pattern);
JXL_TEST_ASSIGN_OR_DIE(
test::TestImage test_image,
CreateNonconformingDisplayP3TestImage(icc, 64, variant.bits_per_sample,
variant.alpha_pattern));
ASSERT_NO_FATAL_FAILURE(
CheckNonconformingDisplayP3FullSizeLossy(test_image.ppf(), 4));
}
JXL_TEST_ASSIGN_OR_DIE(
test::TestImage lossless_image,
CreateNonconformingDisplayP3TestImage(icc, 64, 16, /*alpha_pattern=*/2));
extras::JXLCompressParams params;
params.distance = 0.0f;
extras::JXLDecompressParams decode_params;
decode_params.accepted_formats.push_back(
{4, lossless_image.ppf().frames[0].color.format.data_type,
lossless_image.ppf().frames[0].color.format.endianness, /*align=*/0});
extras::PackedPixelFile output;
ASSERT_GT(test::Roundtrip(lossless_image.ppf(), params, decode_params,
nullptr, &output),
0U);
ASSERT_EQ(output.frames.size(), 1U);
EXPECT_TRUE(test::SamePixels(lossless_image.ppf().frames[0].color,
output.frames[0].color));
EXPECT_EQ(output.primary_color_representation,
extras::PackedPixelFile::kIccIsPrimary);
}
TEST_F(ColorManagementTest, ConformingDisplayP3ProfileUsesStructuredFields) {
JXL_TEST_ASSIGN_OR_DIE(IccBytes conforming_icc,
CreateDisplayP3MatrixProfile());
ASSERT_GE(conforming_icc.size(), 128U);
EXPECT_EQ(conforming_icc[8], 4U);
EXPECT_EQ(std::memcmp(conforming_icc.data() + 12, "mntr", 4), 0);
EXPECT_EQ(std::memcmp(conforming_icc.data() + 16, "RGB ", 4), 0);
EXPECT_EQ(std::memcmp(conforming_icc.data() + 20, "XYZ ", 4), 0);
for (const char* tag :
{"wtpt", "chad", "rXYZ", "gXYZ", "bXYZ", "rTRC", "gTRC", "bTRC"}) {
EXPECT_TRUE(HasIccTag(conforming_icc, tag));
}
EXPECT_FALSE(HasIccTag(conforming_icc, "A2B0"));
ColorEncoding actual;
ASSERT_TRUE(actual.SetICC(std::move(conforming_icc), JxlGetDefaultCms()));
actual.DecideIfWantICC(*JxlGetDefaultCms());
EXPECT_FALSE(actual.WantICC());
EXPECT_EQ(actual.GetWhitePointType(), WhitePoint::kD65);
EXPECT_EQ(actual.GetPrimariesType(), Primaries::kP3);
EXPECT_TRUE(actual.Tf().IsSRGB());
}
TEST_F(ColorManagementTest, CustomChadMatrixProfileRemainsICC) {
JXL_TEST_ASSIGN_OR_DIE(IccBytes icc, CreateNonconformingDisplayP3Profile());
const size_t chad_record = FindIccTagRecord(icc, "chad");
ASSERT_GT(chad_record, 0U);
const size_t chad_offset = LoadBE32(icc.data() + chad_record + 4);
const size_t chad_size = LoadBE32(icc.data() + chad_record + 8);
ASSERT_GE(chad_size, 44U);
ASSERT_LE(chad_offset + chad_size, icc.size());
ASSERT_EQ(std::memcmp(icc.data() + chad_offset, "sf32", 4), 0);
// Structured color fields can only regenerate Bradford adaptation. A valid
// profile with a different CHAD must therefore retain its ICC profile.
StoreBE32(LoadBE32(icc.data() + chad_offset + 8) + 4096,
icc.data() + chad_offset + 8);
ColorEncoding actual;
ASSERT_TRUE(actual.SetICC(std::move(icc), JxlGetDefaultCms()));
actual.DecideIfWantICC(*JxlGetDefaultCms());
EXPECT_TRUE(actual.WantICC());
}
TEST_F(ColorManagementTest, SingularChadIsNotSimplified) {
JXL_TEST_ASSIGN_OR_DIE(IccBytes icc, CreateNonconformingDisplayP3Profile());
const size_t chad_record = FindIccTagRecord(icc, "chad");
ASSERT_GT(chad_record, 0U);
const size_t chad_offset = LoadBE32(icc.data() + chad_record + 4);
const size_t chad_size = LoadBE32(icc.data() + chad_record + 8);
ASSERT_GE(chad_size, 44U);
ASSERT_LE(chad_offset + chad_size, icc.size());
for (size_t i = 0; i < 9; ++i) {
StoreBE32(0, icc.data() + chad_offset + 8U + 4U * i);
}
ColorEncoding actual;
// Rejecting a singular CHAD is also safe. If the CMS accepts the malformed
// profile, it must retain the ICC rather than enter the compatibility path.
if (actual.SetICC(std::move(icc), JxlGetDefaultCms())) {
actual.DecideIfWantICC(*JxlGetDefaultCms());
EXPECT_TRUE(actual.WantICC());
}
}
TEST_F(ColorManagementTest, NearSingularChadIsNotSimplified) {
JXL_TEST_ASSIGN_OR_DIE(IccBytes icc, CreateNonconformingDisplayP3Profile());
const size_t chad_record = FindIccTagRecord(icc, "chad");
ASSERT_GT(chad_record, 0U);
const size_t chad_offset = LoadBE32(icc.data() + chad_record + 4);
const size_t chad_size = LoadBE32(icc.data() + chad_record + 8);
ASSERT_GE(chad_size, 44U);
ASSERT_LE(chad_offset + chad_size, icc.size());
ASSERT_EQ(std::memcmp(icc.data() + chad_offset, "sf32", 4), 0);
// Each diagonal value is 1/65536 in ICC s15Fixed16 notation. This is a
// nonzero, ICC-representable determinant below Inv3x3Matrix's threshold.
constexpr uint32_t kOneFixedPointLsb = 1;
constexpr double kDeterminant = 1.0 / (65536.0 * 65536.0 * 65536.0);
EXPECT_GT(kDeterminant, 0.0);
EXPECT_LT(kDeterminant, 1e-10);
for (size_t i = 0; i < 9; ++i) {
StoreBE32(i % 4 == 0 ? kOneFixedPointLsb : 0,
icc.data() + chad_offset + 8U + 4U * i);
}
ColorEncoding actual;
// Rejecting an unsafe CHAD is also safe. If the CMS accepts the malformed
// profile, it must retain the ICC rather than enter the compatibility path.
if (actual.SetICC(std::move(icc), JxlGetDefaultCms())) {
actual.DecideIfWantICC(*JxlGetDefaultCms());
EXPECT_TRUE(actual.WantICC());
}
}
TEST_F(ColorManagementTest, Version2DisplayP3ProfileUsesStructuredFields) {
JXL_TEST_ASSIGN_OR_DIE(IccBytes icc, CreateDisplayP3MatrixProfile());
ASSERT_GE(icc.size(), 12U);
// Change the header version to ICC v2.1. The malformed-profile compatibility
// rule is v4-only, and valid v2 interpretation must remain unchanged.
icc[8] = 2;
icc[9] = 0x10;
icc[10] = 0;
icc[11] = 0;
ColorEncoding actual;
ASSERT_TRUE(actual.SetICC(std::move(icc), JxlGetDefaultCms()));
actual.DecideIfWantICC(*JxlGetDefaultCms());
EXPECT_FALSE(actual.WantICC());
EXPECT_EQ(actual.GetWhitePointType(), WhitePoint::kD65);
EXPECT_EQ(actual.GetPrimariesType(), Primaries::kP3);
EXPECT_TRUE(actual.Tf().IsSRGB());
}
TEST_F(ColorManagementTest, LUTProfileRemainsICC) {
ColorEncoding lut_source;
lut_source.SetColorSpace(ColorSpace::kXYB);
lut_source.SetRenderingIntent(RenderingIntent::kPerceptual);
ASSERT_TRUE(lut_source.CreateICC());
ASSERT_TRUE(HasIccTag(lut_source.ICC(), "A2B0"));
JXL_TEST_ASSIGN_OR_DIE(IccBytes hybrid,
CreateNonconformingDisplayP3Profile());
ASSERT_TRUE(ReplaceIccTag(&hybrid, "desc", "A2B0", lut_source.ICC(), "A2B0"));
ASSERT_TRUE(HasIccTag(hybrid, "rXYZ"));
ASSERT_TRUE(HasIccTag(hybrid, "rTRC"));
ASSERT_TRUE(HasIccTag(hybrid, "A2B0"));
ColorEncoding actual;
ASSERT_TRUE(actual.SetICC(std::move(hybrid), JxlGetDefaultCms()));
actual.DecideIfWantICC(*JxlGetDefaultCms());
EXPECT_TRUE(actual.WantICC());
EXPECT_NE(actual.GetWhitePointType(), WhitePoint::kD65);
}
TEST_F(ColorManagementTest, SampledTrcProfileSkipsWhitePointRecovery) {
JXL_TEST_ASSIGN_OR_DIE(IccBytes icc, CreateNonconformingDisplayP3Profile());
ASSERT_TRUE(ReplaceTrcsWithSampledCurve(&icc));
ColorEncoding actual;
ASSERT_TRUE(actual.SetICC(std::move(icc), JxlGetDefaultCms()));
actual.DecideIfWantICC(*JxlGetDefaultCms());
EXPECT_TRUE(actual.WantICC());
EXPECT_NE(actual.GetWhitePointType(), WhitePoint::kD65);
}
TEST_F(ColorManagementTest, NoChadProfileSkipsCompatibilityPath) {
JXL_TEST_ASSIGN_OR_DIE(IccBytes icc, CreateNonconformingDisplayP3Profile());
const size_t chad_record = FindIccTagRecord(icc, "chad");
ASSERT_GT(chad_record, 0U);
std::memcpy(icc.data() + chad_record, "tst1", 4);
ASSERT_TRUE(ReplaceTrcsWithSampledCurve(&icc, /*make_non_equivalent=*/false));
ColorEncoding actual;
ASSERT_TRUE(actual.SetICC(std::move(icc), JxlGetDefaultCms()));
actual.DecideIfWantICC(*JxlGetDefaultCms());
EXPECT_FALSE(actual.WantICC());
EXPECT_EQ(actual.GetWhitePointType(), WhitePoint::kD65);
EXPECT_EQ(actual.GetPrimariesType(), Primaries::kP3);
EXPECT_TRUE(actual.Tf().IsSRGB());
}
TEST_F(ColorManagementTest, UnequalTrcMatrixProfileRemainsICC) {
JXL_TEST_ASSIGN_OR_DIE(IccBytes icc, CreateNonconformingDisplayP3Profile());
ASSERT_GE(icc.size(), 132U);
const uint32_t tag_count = LoadBE32(icc.data() + 128);
ASSERT_LE(tag_count, (icc.size() - 132U) / 12U);
const size_t green_trc_record = FindIccTagRecord(icc, "gTRC");
ASSERT_GT(green_trc_record, 0U);
const size_t trc_offset = LoadBE32(icc.data() + green_trc_record + 4);
const size_t trc_size = LoadBE32(icc.data() + green_trc_record + 8);
ASSERT_GE(trc_size, 20U);
ASSERT_LE(trc_offset + trc_size, icc.size());
ASSERT_TRUE(ReplaceIccTag(&icc, "gTRC", "gTRC", icc, "gTRC"));
const size_t new_trc_offset = LoadBE32(icc.data() + green_trc_record + 4);
StoreBE32(LoadBE32(icc.data() + new_trc_offset + 16) + 4096,
icc.data() + new_trc_offset + 16);
ColorEncoding actual;
ASSERT_TRUE(actual.SetICC(std::move(icc), JxlGetDefaultCms()));
actual.DecideIfWantICC(*JxlGetDefaultCms());
EXPECT_TRUE(actual.WantICC());
}
TEST_F(ColorManagementTest, P3HlgTo2020Hlg) {
ColorEncoding p3_hlg;
p3_hlg.SetColorSpace(ColorSpace::kRGB);
+121 -55
View File
@@ -125,8 +125,29 @@ JXL_INLINE void ComputePixel(
Store(out, d, out_rows[2] + x);
}
template <typename D>
JXL_INLINE void ComputePixelSingleChannel(const float dc_factor,
const float* JXL_RESTRICT row_top,
const float* JXL_RESTRICT row,
const float* JXL_RESTRICT row_bottom,
float* JXL_RESTRICT out_row,
size_t x) {
const D d;
auto mc = Undefined(d);
auto sm = Undefined(d);
auto gap = Set(d, 0.5f);
ComputePixelChannel(d, dc_factor, row_top, row, row_bottom, &mc, &sm, &gap,
x);
auto factor = MulAdd(Set(d, -4.0f), gap, Set(d, 3.0f));
factor = ZeroIfNegative(factor);
auto out = MulAdd(Sub(sm, mc), factor, mc);
Store(out, d, out_row + x);
}
Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
const float* dc_factors, Image3F* dc,
const YCbCrChromaSubsampling& chroma_subsampling,
ThreadPool* pool) {
const size_t xsize = dc->xsize();
const size_t ysize = dc->ysize();
@@ -137,63 +158,107 @@ Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
// the x and b channels through color correlation.
JXL_ENSURE(w1 + w2 < 0.25f);
JXL_ASSIGN_OR_RETURN(Image3F smoothed,
Image3F::Create(memory_manager, xsize, ysize));
// Fill in borders that the loop below will not. First and last are unused.
for (size_t c = 0; c < 3; c++) {
for (size_t y : {static_cast<size_t>(0), ysize - 1}) {
memcpy(smoothed.PlaneRow(c, y), dc->PlaneRow(c, y),
xsize * sizeof(float));
}
}
auto process_row = [&](const uint32_t y, size_t /*thread*/) -> Status {
const float* JXL_RESTRICT rows_top[3]{
dc->ConstPlaneRow(0, y - 1),
dc->ConstPlaneRow(1, y - 1),
dc->ConstPlaneRow(2, y - 1),
};
const float* JXL_RESTRICT rows[3] = {
dc->ConstPlaneRow(0, y),
dc->ConstPlaneRow(1, y),
dc->ConstPlaneRow(2, y),
};
const float* JXL_RESTRICT rows_bottom[3] = {
dc->ConstPlaneRow(0, y + 1),
dc->ConstPlaneRow(1, y + 1),
dc->ConstPlaneRow(2, y + 1),
};
float* JXL_RESTRICT rows_out[3] = {
smoothed.PlaneRow(0, y),
smoothed.PlaneRow(1, y),
smoothed.PlaneRow(2, y),
};
for (size_t x : {static_cast<size_t>(0), xsize - 1}) {
for (size_t c = 0; c < 3; c++) {
rows_out[c][x] = rows[c][x];
if (chroma_subsampling.Is444()) {
JXL_ASSIGN_OR_RETURN(Image3F smoothed,
Image3F::Create(memory_manager, xsize, ysize));
// Fill in borders that the loop below will not. First and last are unused.
for (size_t c = 0; c < 3; c++) {
for (size_t y : {static_cast<size_t>(0), ysize - 1}) {
memcpy(smoothed.PlaneRow(c, y), dc->PlaneRow(c, y),
xsize * sizeof(float));
}
}
auto process_row = [&](const uint32_t y, size_t /*thread*/) -> Status {
const float* JXL_RESTRICT rows_top[3]{
dc->ConstPlaneRow(0, y - 1),
dc->ConstPlaneRow(1, y - 1),
dc->ConstPlaneRow(2, y - 1),
};
const float* JXL_RESTRICT rows[3] = {
dc->ConstPlaneRow(0, y),
dc->ConstPlaneRow(1, y),
dc->ConstPlaneRow(2, y),
};
const float* JXL_RESTRICT rows_bottom[3] = {
dc->ConstPlaneRow(0, y + 1),
dc->ConstPlaneRow(1, y + 1),
dc->ConstPlaneRow(2, y + 1),
};
float* JXL_RESTRICT rows_out[3] = {
smoothed.PlaneRow(0, y),
smoothed.PlaneRow(1, y),
smoothed.PlaneRow(2, y),
};
for (size_t x : {static_cast<size_t>(0), xsize - 1}) {
for (size_t c = 0; c < 3; c++) {
rows_out[c][x] = rows[c][x];
}
}
size_t x = 1;
// First pixels
const size_t N = Lanes(D());
for (; x < std::min(N, xsize - 1); x++) {
ComputePixel<DScalar>(dc_factors, rows_top, rows, rows_bottom, rows_out,
x);
size_t x = 1;
// First pixels
const size_t N = Lanes(D());
for (; x < std::min(N, xsize - 1); x++) {
ComputePixel<DScalar>(dc_factors, rows_top, rows, rows_bottom, rows_out,
x);
}
// Full vectors.
for (; x + N <= xsize - 1; x += N) {
ComputePixel<D>(dc_factors, rows_top, rows, rows_bottom, rows_out, x);
}
// Last pixels.
for (; x < xsize - 1; x++) {
ComputePixel<DScalar>(dc_factors, rows_top, rows, rows_bottom, rows_out,
x);
}
return true;
};
JXL_RETURN_IF_ERROR(RunOnPool(pool, 1, ysize - 1, ThreadPool::NoInit,
process_row, "DCSmoothingRow"));
dc->Swap(smoothed);
} else {
JXL_ASSIGN_OR_RETURN(ImageF smoothed,
ImageF::Create(memory_manager, xsize, ysize));
for (size_t c = 0; c < 3; c++) {
const size_t xsize_c = xsize >> chroma_subsampling.HShift(c);
const size_t ysize_c = ysize >> chroma_subsampling.VShift(c);
if (ysize_c <= 2 || xsize_c <= 2) continue;
for (size_t y : {static_cast<size_t>(0), ysize_c - 1}) {
memcpy(smoothed.Row(y), dc->PlaneRow(c, y), xsize_c * sizeof(float));
}
auto process_row = [&](const uint32_t y, size_t /*thread*/) -> Status {
const float* JXL_RESTRICT row_top = dc->ConstPlaneRow(c, y - 1);
const float* JXL_RESTRICT row = dc->ConstPlaneRow(c, y);
const float* JXL_RESTRICT row_bottom = dc->ConstPlaneRow(c, y + 1);
float* JXL_RESTRICT row_out = smoothed.Row(y);
for (size_t x : {static_cast<size_t>(0), xsize_c - 1}) {
row_out[x] = row[x];
}
size_t x = 1;
// First pixels
const size_t N = Lanes(D());
for (; x < std::min(N, xsize_c - 1); x++) {
ComputePixelSingleChannel<DScalar>(dc_factors[c], row_top, row,
row_bottom, row_out, x);
}
// Full vectors.
for (; x + N <= xsize_c - 1; x += N) {
ComputePixelSingleChannel<D>(dc_factors[c], row_top, row, row_bottom,
row_out, x);
}
// Last pixels.
for (; x < xsize_c - 1; x++) {
ComputePixelSingleChannel<DScalar>(dc_factors[c], row_top, row,
row_bottom, row_out, x);
}
return true;
};
JXL_RETURN_IF_ERROR(RunOnPool(pool, 1, ysize_c - 1, ThreadPool::NoInit,
process_row, "DCSmoothingRow"));
dc->Plane(c).Swap(smoothed);
}
// Full vectors.
for (; x + N <= xsize - 1; x += N) {
ComputePixel<D>(dc_factors, rows_top, rows, rows_bottom, rows_out, x);
}
// Last pixels.
for (; x < xsize - 1; x++) {
ComputePixel<DScalar>(dc_factors, rows_top, rows, rows_bottom, rows_out,
x);
}
return true;
};
JXL_RETURN_IF_ERROR(RunOnPool(pool, 1, ysize - 1, ThreadPool::NoInit,
process_row, "DCSmoothingRow"));
dc->Swap(smoothed);
}
return true;
}
@@ -307,9 +372,10 @@ HWY_EXPORT(DequantDC);
HWY_EXPORT(AdaptiveDCSmoothing);
Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
const float* dc_factors, Image3F* dc,
const YCbCrChromaSubsampling& chroma_subsampling,
ThreadPool* pool) {
return HWY_DYNAMIC_DISPATCH(AdaptiveDCSmoothing)(memory_manager, dc_factors,
dc, pool);
return HWY_DYNAMIC_DISPATCH(AdaptiveDCSmoothing)(
memory_manager, dc_factors, dc, chroma_subsampling, pool);
}
void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in,
+1
View File
@@ -24,6 +24,7 @@ namespace jxl {
// Smooth DC in already-smooth areas, to counteract banding.
Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
const float* dc_factors, Image3F* dc,
const YCbCrChromaSubsampling& chroma_subsampling,
ThreadPool* pool);
void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in,
+8 -3
View File
@@ -265,9 +265,14 @@ class ANSSymbolReader {
br->Refill(); // covers ReadSymbolWithoutRefill + PeekBits
size_t token = ReadSymbolHuffWithoutRefill(ctx, br);
if (JXL_UNLIKELY(token >= lz77_threshold_)) {
*run =
ReadHybridUintConfig(lz77_length_uint_, token - lz77_threshold_, br) +
lz77_min_length_ - 1;
uint32_t addend =
ReadHybridUintConfig(lz77_length_uint_, token - lz77_threshold_, br);
uint32_t result = addend + lz77_min_length_ - 1;
*run = result;
if (JXL_UNLIKELY(result < addend)) { // overflow
*run = 0;
br->MarkUnhealthy();
}
return;
}
*value = ReadHybridUintConfig(configs[ctx], token, br);
+30 -13
View File
@@ -124,6 +124,20 @@ Status PassesDecoderState::PreparePipeline(const FrameHeader& frame_header,
(options.render_noise && (frame_header.flags & FrameHeader::kNoise) != 0);
size_t num_tmp_c = render_noise ? 3 : 0;
const auto find_ec = [&metadata](ExtraChannel type) -> int {
for (size_t i = 0; i < metadata->extra_channel_info.size(); i++) {
if (metadata->extra_channel_info[i].type == type) {
return 3 + i;
}
}
return -1;
};
int alpha_c = find_ec(ExtraChannel::kAlpha);
bool has_alpha = alpha_c > 0;
int black_c = find_ec(ExtraChannel::kBlack);
bool has_black = black_c > 0;
if (frame_header.CanBeReferenced()) {
// Necessary so that SetInputSizes() can allocate output buffers as needed.
frame_storage_for_referencing = ImageBundle(memory_manager, metadata);
@@ -229,16 +243,6 @@ Status PassesDecoderState::PreparePipeline(const FrameHeader& frame_header,
&frame_storage_for_referencing, &metadata->color_encoding)));
}
bool has_alpha = false;
size_t alpha_c = 0;
for (size_t i = 0; i < metadata->extra_channel_info.size(); i++) {
if (metadata->extra_channel_info[i].type == ExtraChannel::kAlpha) {
has_alpha = true;
alpha_c = 3 + i;
break;
}
}
if (fast_xyb_srgb8_conversion) {
#if !JXL_HIGH_PRECISION
JXL_ENSURE(!NeedsBlending(frame_header));
@@ -305,7 +309,12 @@ Status PassesDecoderState::PreparePipeline(const FrameHeader& frame_header,
if (!output_encoding_info.cms_set) {
return JXL_FAILURE("Cannot tonemap this colorspace without a CMS");
}
auto cms_stage = GetCmsStage(output_encoding_info);
if (output_encoding_info.linear_color_encoding.IsCMYK() &&
!has_black) {
return JXL_FAILURE("Cannot convert CMYK without black channel");
}
auto cms_stage =
GetCmsStage(output_encoding_info, black_c, /*linear=*/true);
if (cms_stage) {
JXL_RETURN_IF_ERROR(builder.AddStage(std::move(cms_stage)));
}
@@ -342,14 +351,22 @@ Status PassesDecoderState::PreparePipeline(const FrameHeader& frame_header,
if (!output_encoding_info.linear_color_encoding.CreateICC()) {
return JXL_FAILURE("Failed to create ICC");
}
auto cms_stage = GetCmsStage(output_encoding_info);
if (output_encoding_info.linear_color_encoding.IsCMYK() && !has_black) {
return JXL_FAILURE("Cannot convert CMYK without black channel");
}
auto cms_stage =
GetCmsStage(output_encoding_info, black_c, /*linear=*/true);
if (cms_stage) {
JXL_RETURN_IF_ERROR(builder.AddStage(std::move(cms_stage)));
}
}
linear = false;
} else {
auto cms_stage = GetCmsStage(output_encoding_info, false);
if (output_encoding_info.orig_color_encoding.IsCMYK() && !has_black) {
return JXL_FAILURE("Cannot convert CMYK without black channel");
}
auto cms_stage =
GetCmsStage(output_encoding_info, black_c, /*linear=*/false);
if (cms_stage) {
JXL_RETURN_IF_ERROR(builder.AddStage(std::move(cms_stage)));
}
+31 -20
View File
@@ -202,14 +202,6 @@ Status FrameDecoder::InitFrame(BitReader* JXL_RESTRICT br, ImageBundle* decoded,
if (group_codes_begin + section_sizes_sum_ < group_codes_begin) {
return JXL_FAILURE("Invalid group codes");
}
if (!frame_header_.chroma_subsampling.Is444() &&
!(frame_header_.flags & FrameHeader::kSkipAdaptiveDCSmoothing) &&
frame_header_.encoding == FrameEncoding::kVarDCT) {
return JXL_FAILURE(
"Non-444 chroma subsampling is not allowed when adaptive DC "
"smoothing is enabled");
}
return true;
}
@@ -231,10 +223,16 @@ Status FrameDecoder::InitFrameOutput() {
if (frame_header_.nonserialized_metadata->m.xyb_encoded) {
return JXL_FAILURE("Cannot decode to JPEG an XYB image");
}
auto jpeg_c_map = JpegOrder(ColorTransform::kYCbCr, num_components == 1);
decoded_->jpeg_data->width = frame_dim_.xsize;
decoded_->jpeg_data->height = frame_dim_.ysize;
for (size_t c = 0; c < num_components; c++) {
bool is_gray = (num_components == 1);
auto jpeg_c_map = JpegOrder(ColorTransform::kYCbCr, is_gray);
if (frame_dim_.xsize > static_cast<size_t>(jpeg::kMaxDimPixels) ||
frame_dim_.ysize > static_cast<size_t>(jpeg::kMaxDimPixels)) {
return JXL_FAILURE("Frame dimensions too large for JPEG reconstruction");
}
decoded_->jpeg_data->width = static_cast<int>(frame_dim_.xsize);
decoded_->jpeg_data->height = static_cast<int>(frame_dim_.ysize);
for (size_t c = 0; c < 3; ++c) {
if (is_gray && c != 1) continue;
auto& component = jpeg_data->components[jpeg_c_map[c]];
component.width_in_blocks =
frame_dim_.xsize_blocks >> frame_header_.chroma_subsampling.HShift(c);
@@ -244,9 +242,17 @@ Status FrameDecoder::InitFrameOutput() {
1 << frame_header_.chroma_subsampling.RawHShift(c);
component.v_samp_factor =
1 << frame_header_.chroma_subsampling.RawVShift(c);
size_t num_blocks = static_cast<size_t>(component.width_in_blocks) *
component.height_in_blocks;
component.coeffs.resize(num_blocks * jxl::kDCTBlockSize);
size_t num_blocks;
if (!SafeMul(static_cast<size_t>(component.width_in_blocks),
static_cast<size_t>(component.height_in_blocks),
num_blocks)) {
return JXL_FAILURE("JPEG component block count overflow");
}
size_t num_coeffs;
if (!SafeMul(num_blocks, jxl::kDCTBlockSize, num_coeffs)) {
return JXL_FAILURE("JPEG component coefficient count overflow");
}
component.coeffs.resize(num_coeffs);
}
}
@@ -318,7 +324,6 @@ Status FrameDecoder::ProcessDCGlobal(BitReader* br) {
Status FrameDecoder::ProcessDCGroup(size_t dc_group_id, BitReader* br) {
const size_t gx = dc_group_id % frame_dim_.xsize_dc_groups;
const size_t gy = dc_group_id / frame_dim_.xsize_dc_groups;
const LoopFilter& lf = frame_header_.loop_filter;
if (frame_header_.encoding == FrameEncoding::kVarDCT &&
!(frame_header_.flags & FrameHeader::kUseDcFrame)) {
JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeVarDCTDC(
@@ -334,8 +339,6 @@ Status FrameDecoder::ProcessDCGroup(size_t dc_group_id, BitReader* br) {
if (frame_header_.encoding == FrameEncoding::kVarDCT) {
JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeAcMetadata(
frame_header_, dc_group_id, br, dec_state_));
} else if (lf.epf_iters > 0) {
FillImage(kInvSigmaNum / lf.epf_sigma_for_modular, &dec_state_->sigma);
}
decoded_dc_groups_[dc_group_id] = JXL_TRUE;
return true;
@@ -347,10 +350,18 @@ Status FrameDecoder::FinalizeDC() {
JxlMemoryManager* memory_manager = dec_state_->memory_manager();
if (frame_header_.encoding == FrameEncoding::kVarDCT &&
!(frame_header_.flags & FrameHeader::kSkipAdaptiveDCSmoothing) &&
!(frame_header_.flags & FrameHeader::kUseDcFrame)) {
!(frame_header_.flags & FrameHeader::kUseDcFrame) &&
// Skip smoothing when reconstructing to JPEG.
!decoded_->IsJPEG()) {
JXL_RETURN_IF_ERROR(AdaptiveDCSmoothing(
memory_manager, dec_state_->shared->quantizer.MulDC(),
&dec_state_->shared_storage.dc_storage, pool_));
&dec_state_->shared_storage.dc_storage,
frame_header_.chroma_subsampling, pool_));
}
if (frame_header_.encoding == FrameEncoding::kModular &&
frame_header_.loop_filter.epf_iters > 0) {
FillImage(kInvSigmaNum / frame_header_.loop_filter.epf_sigma_for_modular,
&dec_state_->sigma);
}
finalized_dc_ = true;
@@ -32,7 +32,11 @@ Status PatchDictionary::Decode(JxlMemoryManager* memory_manager, BitReader* br,
size_t xsize, size_t ysize,
size_t num_extra_channels,
bool* uses_extra_channels) {
// The dictionary is reused across frames: blendings_ is indexed by position
// index, so stale entries from a previous frame must not remain.
positions_.clear();
ref_positions_.clear();
blendings_.clear();
blendings_stride_ = num_extra_channels + 1;
std::vector<uint8_t> context_map;
ANSCode code;
@@ -76,10 +80,14 @@ Status PatchDictionary::Decode(JxlMemoryManager* memory_manager, BitReader* br,
ref_pos.y0 = read_num(kPatchReferencePositionContext);
ref_pos.xsize = read_num(kPatchSizeContext) + 1;
ref_pos.ysize = read_num(kPatchSizeContext) + 1;
if (ref_pos.x0 + ref_pos.xsize > ib.xsize()) {
if ((ref_pos.xsize == 0) || (ref_pos.ysize == 0)) { // overflow
return JXL_FAILURE("Invalid position specified in reference frame");
}
if (ref_pos.y0 + ref_pos.ysize > ib.ysize()) {
if (ref_pos.xsize > ib.xsize() || ref_pos.ysize > ib.ysize()) {
return JXL_FAILURE("Invalid position specified in reference frame");
}
if ((ref_pos.x0 > ib.xsize() - ref_pos.xsize) ||
(ref_pos.y0 > ib.ysize() - ref_pos.ysize)) {
return JXL_FAILURE("Invalid position specified in reference frame");
}
size_t id_count = read_num(kPatchCountContext);
@@ -123,15 +131,11 @@ Status PatchDictionary::Decode(JxlMemoryManager* memory_manager, BitReader* br,
}
pos.y = positions_.back().y + deltay;
}
if (pos.x + ref_pos.xsize > xsize) {
return JXL_FAILURE("Invalid patch x: at %" PRIuS " + %" PRIuS
" > %" PRIuS,
pos.x, ref_pos.xsize, xsize);
if ((ref_pos.xsize > xsize) || (ref_pos.ysize > ysize)) {
return JXL_FAILURE("Invalid patch position");
}
if (pos.y + ref_pos.ysize > ysize) {
return JXL_FAILURE("Invalid patch y: at %" PRIuS " + %" PRIuS
" > %" PRIuS,
pos.y, ref_pos.ysize, ysize);
if ((pos.x > xsize - ref_pos.xsize) || (pos.y > ysize - ref_pos.ysize)) {
return JXL_FAILURE("Invalid patch position");
}
for (size_t j = 0; j < blendings_stride_; j++) {
uint32_t blend_mode = read_num(kPatchBlendModeContext);
@@ -118,6 +118,8 @@ class PatchDictionary {
void Clear() {
positions_.clear();
ref_positions_.clear();
blendings_.clear();
ComputePatchTree();
}
+58 -64
View File
@@ -315,50 +315,6 @@ struct ExtraChannelOutput {
} // namespace
namespace jxl {
struct JxlDecoderFrameIndexBoxEntry {
// OFFi: offset of start byte of this frame compared to start
// byte of previous frame from this index in the JPEG XL codestream. For the
// first frame, this is the offset from the first byte of the JPEG XL
// codestream.
uint64_t OFFi;
// Ti: duration in ticks between the start of this frame and
// the start of the next frame in the index. If this is the last frame in the
// index, this is the duration in ticks between the start of this frame and
// the end of the stream. A tick lasts TNUM / TDEN seconds.
uint32_t Ti;
// Fi: amount of frames the next frame in the index occurs
// after this frame. If this is the last frame in the index, this is the
// amount of frames after this frame in the remainder of the stream. Only
// frames that are presented by the decoder are counted for this purpose, this
// excludes frames that are not intended for display but for compositing with
// other frames, such as frames that aren't the last frame with a duration of
// 0 ticks.
uint32_t Fi;
};
struct JxlDecoderFrameIndexBox {
int64_t NF() const { return entries.size(); }
int32_t TNUM = 1;
int32_t TDEN = 1000;
std::vector<JxlDecoderFrameIndexBoxEntry> entries;
// That way we can ensure that every index box will have the first frame.
// If the API user decides to mark it as an indexed frame, we call
// the AddFrame again, this time with requested.
void AddFrame(uint64_t OFFi, uint32_t Ti, uint32_t Fi) {
JxlDecoderFrameIndexBoxEntry e;
e.OFFi = OFFi;
e.Ti = Ti;
e.Fi = Fi;
entries.push_back(e);
}
};
} // namespace jxl
// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding)
struct JxlDecoder {
JxlDecoder() = default;
@@ -387,7 +343,6 @@ struct JxlDecoder {
bool got_all_headers; // Codestream metadata headers.
bool post_headers; // Already decoding pixels.
std::unique_ptr<jxl::ICCReader> icc_reader;
jxl::JxlDecoderFrameIndexBox frame_index_box;
// This means either we actually got the preview image, or determined we
// cannot get it or there is none.
bool got_preview_image;
@@ -446,7 +401,7 @@ struct JxlDecoder {
size_t box_count;
// The level of progressive detail in frame decoding.
JxlProgressiveDetail prog_detail = kDC;
JxlProgressiveDetail prog_detail;
// The progressive detail of the current frame.
JxlProgressiveDetail frame_prog_detail;
// The intended downsampling ratio for the current progression step.
@@ -454,6 +409,7 @@ struct JxlDecoder {
// Set to true if either an image out buffer or an image out callback was set.
bool image_out_buffer_set;
bool image_out_buffer_is_for_preview;
// Owned by the caller, buffer for preview or full resolution image.
void* image_out_buffer;
@@ -731,10 +687,25 @@ bool CheckSizeLimit(JxlDecoder* dec, size_t xsize, size_t ysize) {
return true;
}
} // namespace
void ResetImageOut(JxlDecoder* dec, bool clear_ec = true) {
dec->image_out_bit_depth.type = JXL_BIT_DEPTH_FROM_PIXEL_FORMAT;
dec->image_out_buffer = nullptr;
dec->image_out_buffer_is_for_preview = false;
dec->image_out_buffer_set = false;
dec->image_out_destroy_callback = nullptr;
dec->image_out_format = {};
dec->image_out_init_callback = nullptr;
dec->image_out_init_opaque = nullptr;
dec->image_out_run_callback = nullptr;
dec->image_out_size = 0;
if (clear_ec) {
dec->extra_channel_output.clear();
dec->extra_channel_output.shrink_to_fit();
}
}
// Resets the state that must be reset for both Rewind and Reset
void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
void RewindDecodingState(JxlDecoder* dec) {
dec->stage = DecoderStage::kInited;
dec->got_signature = false;
dec->last_codestream_seen = false;
@@ -743,7 +714,7 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
dec->got_transform_data = false;
dec->got_all_headers = false;
dec->post_headers = false;
if (dec->icc_reader) dec->icc_reader->Reset();
if (dec->icc_reader) dec->icc_reader.reset();
dec->got_preview_image = false;
dec->preview_frame = false;
dec->file_pos = 0;
@@ -770,15 +741,23 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
dec->box_out_buffer_begin = 0;
dec->box_out_buffer_pos = 0;
#if JPEGXL_ENABLE_BOXES
dec->box_content_decoder.Reset();
#endif
#if JPEGXL_ENABLE_TRANSCODE_JPEG
dec->metadata_decoder.Reset();
dec->exif_metadata.clear();
dec->exif_metadata.shrink_to_fit();
dec->xmp_metadata.clear();
dec->xmp_metadata.shrink_to_fit();
dec->store_exif = 0;
dec->store_xmp = 0;
dec->recon_out_buffer_pos = 0;
dec->recon_exif_size = 0;
dec->recon_xmp_size = 0;
dec->recon_output_jpeg = JpegReconStage::kNone;
dec->jpeg_decoder.Reset();
#endif
dec->events_wanted = dec->orig_events_wanted;
@@ -786,15 +765,9 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
dec->have_container = false;
dec->box_count = 0;
dec->downsampling_target = 8;
dec->image_out_buffer_set = false;
dec->image_out_buffer = nullptr;
dec->image_out_init_callback = nullptr;
dec->image_out_run_callback = nullptr;
dec->image_out_destroy_callback = nullptr;
dec->image_out_init_opaque = nullptr;
dec->image_out_size = 0;
dec->image_out_bit_depth.type = JXL_BIT_DEPTH_FROM_PIXEL_FORMAT;
dec->extra_channel_output.clear();
ResetImageOut(dec);
dec->simple_image_out_callback.callback = nullptr;
dec->simple_image_out_callback.opaque = nullptr;
dec->next_in = nullptr;
dec->avail_in = 0;
dec->input_closed = false;
@@ -803,13 +776,14 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
dec->frame_dec.reset();
dec->next_section = 0;
dec->section_processed.clear();
dec->section_processed.shrink_to_fit();
dec->ib.reset();
dec->metadata = jxl::CodecMetadata();
dec->image_metadata = dec->metadata.m;
dec->frame_header = jxl::make_unique<jxl::FrameHeader>(&dec->metadata);
dec->codestream_copy.clear();
dec->codestream_copy = jxl::PaddedBytes(&dec->memory_manager);
dec->codestream_unconsumed = 0;
dec->codestream_pos = 0;
dec->codestream_bits_ahead = 0;
@@ -824,8 +798,10 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
dec->external_frames = 0;
}
} // namespace
void JxlDecoderReset(JxlDecoder* dec) {
JxlDecoderRewindDecodingState(dec);
RewindDecodingState(dec);
dec->thread_pool.reset();
dec->keep_orientation = false;
@@ -835,9 +811,13 @@ void JxlDecoderReset(JxlDecoder* dec) {
dec->desired_intensity_target = 0;
dec->orig_events_wanted = 0;
dec->events_wanted = 0;
dec->prog_detail = kDC;
dec->frame_refs.clear();
dec->frame_refs.shrink_to_fit();
dec->frame_external_to_internal.clear();
dec->frame_external_to_internal.shrink_to_fit();
dec->frame_required.clear();
dec->frame_required.shrink_to_fit();
dec->decompress_boxes = false;
}
@@ -867,7 +847,7 @@ void JxlDecoderDestroy(JxlDecoder* dec) {
}
}
void JxlDecoderRewind(JxlDecoder* dec) { JxlDecoderRewindDecodingState(dec); }
void JxlDecoderRewind(JxlDecoder* dec) { RewindDecodingState(dec); }
void JxlDecoderSkipFrames(JxlDecoder* dec, size_t amount) {
// Increment amount, rather than set it: making the amount smaller is
@@ -909,7 +889,8 @@ JxlDecoderStatus JxlDecoderSkipCurrentFrame(JxlDecoder* dec) {
dec->frame_stage = FrameStage::kHeader;
dec->AdvanceCodestream(dec->remaining_frame_size);
if (dec->is_last_of_still) {
dec->image_out_buffer_set = false;
// The output buffers only apply to the frame that was skipped.
ResetImageOut(dec);
}
return JXL_DEC_SUCCESS;
}
@@ -1462,6 +1443,11 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec) {
}
if (dec->image_out_buffer_set) {
if (!!dec->image_out_buffer &&
(dec->preview_frame != dec->image_out_buffer_is_for_preview)) {
return JXL_API_ERROR("API misuse: wrong out buffer is set %d",
(int)dec->preview_frame);
}
size_t xsize;
size_t ysize;
GetCurrentDimensions(dec, xsize, ysize);
@@ -1541,8 +1527,7 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec) {
}
#endif
if (dec->preview_frame || dec->is_last_of_still) {
dec->image_out_buffer_set = false;
dec->extra_channel_output.clear();
ResetImageOut(dec);
}
}
@@ -2464,6 +2449,10 @@ size_t JxlDecoderGetIntendedDownsamplingRatio(JxlDecoder* dec) {
JxlDecoderStatus JxlDecoderFlushImage(JxlDecoder* dec) {
if (!dec->image_out_buffer_set) return JXL_DEC_ERROR;
if (!!dec->image_out_buffer &&
(dec->preview_frame != dec->image_out_buffer_is_for_preview)) {
return JXL_API_ERROR("API misuse: wrong out buffer is set");
}
if (dec->frame_stage != FrameStage::kFull) {
return JXL_DEC_ERROR;
}
@@ -2559,7 +2548,9 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderSetPreviewOutBuffer(
if (size < min_size) return JXL_DEC_ERROR;
ResetImageOut(dec, /*clear_ec=*/false);
dec->image_out_buffer_set = true;
dec->image_out_buffer_is_for_preview = true;
dec->image_out_buffer = buffer;
dec->image_out_size = size;
dec->image_out_format = *format;
@@ -2601,7 +2592,9 @@ JxlDecoderStatus JxlDecoderSetImageOutBuffer(JxlDecoder* dec,
if (size < min_size) return JXL_DEC_ERROR;
ResetImageOut(dec, /*clear_ec=*/false);
dec->image_out_buffer_set = true;
dec->image_out_buffer_is_for_preview = false;
dec->image_out_buffer = buffer;
dec->image_out_size = size;
dec->image_out_format = *format;
@@ -2696,6 +2689,7 @@ JxlDecoderStatus JxlDecoderSetMultithreadedImageOutCallback(
JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits_sink);
if (status != JXL_DEC_SUCCESS) return status;
ResetImageOut(dec, /*clear_ec=*/false);
dec->image_out_buffer_set = true;
dec->image_out_init_callback = init_callback;
dec->image_out_run_callback = run_callback;
+209
View File
@@ -3072,6 +3072,88 @@ TEST(DecodeTest, SkipCurrentFrameTest) {
JxlDecoderDestroy(dec);
}
// An extra channel buffer set for a frame that is then skipped with
// JxlDecoderSkipCurrentFrame must not be written while decoding a later frame.
TEST(DecodeTest, SkipCurrentFrameForgetsExtraChannelBuffers) {
JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
size_t xsize = 90;
size_t ysize = 120;
constexpr size_t num_frames = 2;
JxlPixelFormat format = {4, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0};
auto io = jxl::make_unique<jxl::CodecInOut>(memory_manager);
ASSERT_TRUE(io->SetSize(xsize, ysize));
io->metadata.m.SetUintSamples(16);
io->metadata.m.SetAlphaBits(16);
io->metadata.m.color_encoding = jxl::ColorEncoding::SRGB(false);
io->metadata.m.have_animation = true;
io->frames.clear();
for (size_t i = 0; i < num_frames; ++i) {
std::vector<uint8_t> pixels =
jxl::test::GetSomeTestImage(xsize, ysize, 4, i);
jxl::ImageBundle bundle(memory_manager, &io->metadata.m);
EXPECT_TRUE(
ConvertFromExternal(jxl::Bytes(pixels.data(), pixels.size()), xsize,
ysize, jxl::ColorEncoding::SRGB(/*is_gray=*/false),
/*bits_per_sample=*/16, format,
/*pool=*/nullptr, &bundle, /*set_alpha=*/true));
bundle.duration = 1;
io->frames.push_back(std::move(bundle));
}
jxl::CompressParams cparams;
cparams.speed_tier = jxl::SpeedTier::kThunder;
std::vector<uint8_t> compressed;
EXPECT_TRUE(jxl::test::EncodeFile(cparams, io.get(), &compressed));
JxlDecoderPtr dec = JxlDecoderMake(nullptr);
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSubscribeEvents(
dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FRAME |
JXL_DEC_FULL_IMAGE));
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec.get(), compressed.data(),
compressed.size()));
JxlDecoderCloseInput(dec.get());
EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec.get()));
JxlBasicInfo info;
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderGetBasicInfo(dec.get(), &info));
EXPECT_EQ(1u, info.num_extra_channels);
JxlPixelFormat rgb_format = {3, JXL_TYPE_UINT16, JXL_BIG_ENDIAN, 0};
size_t buffer_size;
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderImageOutBufferSize(dec.get(), &rgb_format, &buffer_size));
size_t extra_size;
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderExtraChannelBufferSize(
dec.get(), &rgb_format, &extra_size, 0));
std::vector<uint8_t> pixels(buffer_size);
constexpr uint8_t kUntouched = 0x5A;
std::vector<uint8_t> skipped_extra(extra_size, kUntouched);
// First frame: set color and extra channel output, then skip the frame.
EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec.get()));
EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec.get()));
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSetImageOutBuffer(dec.get(), &rgb_format, pixels.data(),
pixels.size()));
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetExtraChannelBuffer(
dec.get(), &rgb_format, skipped_extra.data(),
skipped_extra.size(), 0));
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSkipCurrentFrame(dec.get()));
// Second frame: only color output is requested.
EXPECT_EQ(JXL_DEC_FRAME, JxlDecoderProcessInput(dec.get()));
EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec.get()));
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSetImageOutBuffer(dec.get(), &rgb_format, pixels.data(),
pixels.size()));
EXPECT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec.get()));
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderProcessInput(dec.get()));
// The buffer belonged to the skipped frame only.
EXPECT_EQ(extra_size,
static_cast<size_t>(std::count(skipped_extra.begin(),
skipped_extra.end(), kUntouched)));
}
TEST(DecodeTest, SkipFrameTest) {
JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
size_t xsize = 90;
@@ -5036,6 +5118,100 @@ JXL_TRANSCODE_JPEG_TEST(DecodeTest, JPEGReconstructTestCodestream) {
VerifyJPEGReconstruction(jxl::Bytes(compressed), jxl::Bytes(jpeg_codestream));
}
// A JPEG output buffer belongs to one decode: after JxlDecoderReset the caller
// may free it, and the next decode must neither write to it nor refuse a new
// buffer.
JXL_TRANSCODE_JPEG_TEST(DecodeTest,
JPEGReconstructionBufferNotKeptAcrossReset) {
TEST_LIBJPEG_SUPPORT();
size_t xsize = 123;
size_t ysize = 77;
size_t channels = 3;
std::vector<uint8_t> pixels =
jxl::test::GetSomeTestImage(xsize, ysize, channels, /*seed=*/0);
std::vector<uint8_t> jpeg_codestream;
jxl::TestCodestreamParams params;
params.cparams.color_transform = jxl::ColorTransform::kNone;
params.box_format = kCSBF_Single;
params.jpeg_codestream = &jpeg_codestream;
std::vector<uint8_t> compressed = jxl::CreateTestJXLCodestream(
jxl::Bytes(pixels.data(), pixels.size()), xsize, ysize, channels, params);
JxlDecoderPtr dec = JxlDecoderMake(nullptr);
constexpr uint8_t kUntouched = 0x5A;
std::vector<uint8_t> first_buffer(jpeg_codestream.size());
for (int pass = 0; pass < 2; ++pass) {
std::vector<uint8_t> buffer(jpeg_codestream.size());
std::vector<uint8_t>& out = pass == 0 ? first_buffer : buffer;
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSubscribeEvents(
dec.get(), JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE));
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec.get(), compressed.data(),
compressed.size()));
JxlDecoderCloseInput(dec.get());
ASSERT_EQ(JXL_DEC_JPEG_RECONSTRUCTION, JxlDecoderProcessInput(dec.get()));
ASSERT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSetJPEGBuffer(dec.get(), out.data(), out.size()));
ASSERT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec.get()));
EXPECT_EQ(jpeg_codestream, out);
// The buffer is not released explicitly: resetting ends its use.
if (pass == 0) {
std::fill(first_buffer.begin(), first_buffer.end(), kUntouched);
}
JxlDecoderReset(dec.get());
}
// The second decode wrote only to its own buffer.
EXPECT_EQ(first_buffer.size(),
static_cast<size_t>(std::count(first_buffer.begin(),
first_buffer.end(), kUntouched)));
}
// Rewinding while a JPEG reconstruction box is partially read must not leave
// its bytes behind for the next pass over the file.
JXL_TRANSCODE_JPEG_TEST(DecodeTest, JPEGReconstructionRewindInsideBox) {
TEST_LIBJPEG_SUPPORT();
size_t xsize = 123;
size_t ysize = 77;
size_t channels = 3;
std::vector<uint8_t> pixels =
jxl::test::GetSomeTestImage(xsize, ysize, channels, /*seed=*/0);
std::vector<uint8_t> jpeg_codestream;
jxl::TestCodestreamParams params;
params.cparams.color_transform = jxl::ColorTransform::kNone;
params.box_format = kCSBF_Single;
params.jpeg_codestream = &jpeg_codestream;
std::vector<uint8_t> compressed = jxl::CreateTestJXLCodestream(
jxl::Bytes(pixels.data(), pixels.size()), xsize, ysize, channels, params);
const uint8_t kJbrd[4] = {'j', 'b', 'r', 'd'};
auto jbrd = std::search(compressed.begin(), compressed.end(), kJbrd,
kJbrd + sizeof(kJbrd));
ASSERT_NE(compressed.end(), jbrd);
// Stop a few bytes into the contents of the jbrd box.
size_t partial_size = (jbrd - compressed.begin()) + sizeof(kJbrd) + 8;
JxlDecoderPtr dec = JxlDecoderMake(nullptr);
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSubscribeEvents(
dec.get(), JXL_DEC_JPEG_RECONSTRUCTION | JXL_DEC_FULL_IMAGE));
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSetInput(dec.get(), compressed.data(), partial_size));
EXPECT_EQ(JXL_DEC_NEED_MORE_INPUT, JxlDecoderProcessInput(dec.get()));
JxlDecoderReleaseInput(dec.get());
JxlDecoderRewind(dec.get());
EXPECT_EQ(JXL_DEC_SUCCESS, JxlDecoderSetInput(dec.get(), compressed.data(),
compressed.size()));
JxlDecoderCloseInput(dec.get());
ASSERT_EQ(JXL_DEC_JPEG_RECONSTRUCTION, JxlDecoderProcessInput(dec.get()));
std::vector<uint8_t> reconstructed(jpeg_codestream.size());
ASSERT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSetJPEGBuffer(dec.get(), reconstructed.data(),
reconstructed.size()));
ASSERT_EQ(JXL_DEC_FULL_IMAGE, JxlDecoderProcessInput(dec.get()));
EXPECT_EQ(0u, JxlDecoderReleaseJPEGBuffer(dec.get()));
EXPECT_EQ(jpeg_codestream, reconstructed);
}
JXL_TRANSCODE_JPEG_TEST(DecodeTest, JPEGReconstructionTest) {
JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
const std::string jpeg_path = "jxl/flower/flower.png.im_q85_420.jpg";
@@ -5779,3 +5955,36 @@ TEST(DecodeTest, CloseInput) {
JxlDecoderCloseInput(dec.get());
EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderProcessInput(dec.get()));
}
TEST(DecodeTest, Non444AdaptiveDCSmoothing) {
// Testcase with non-444 chroma subsampling and adaptive DC smoothing enabled
// (#5005). Must not read uninitialized memory.
const std::vector<uint8_t> data = {
0xff, 0x0a, 0x20, 0x71, 0x00, 0xff, 0x0a, 0x00, 0x34, 0x58, 0xc6,
0x20, 0x10, 0x00, 0xb8, 0x00, 0x2d, 0x20, 0xfa, 0xff, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0x32, 0x35,
0x31, 0x31, 0xb2, 0x20, 0x20, 0xff, 0xff, 0x20, 0x20, 0xbf, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0xd3, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
};
JxlDecoderPtr dec = JxlDecoderMake(nullptr);
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSubscribeEvents(dec.get(),
JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE));
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSetInput(dec.get(), data.data(), data.size()));
JxlDecoderCloseInput(dec.get());
EXPECT_EQ(JXL_DEC_BASIC_INFO, JxlDecoderProcessInput(dec.get()));
EXPECT_EQ(JXL_DEC_NEED_IMAGE_OUT_BUFFER, JxlDecoderProcessInput(dec.get()));
JxlPixelFormat format = {3, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0};
size_t buffer_size;
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderImageOutBufferSize(dec.get(), &format, &buffer_size));
std::vector<uint8_t> pixels(buffer_size);
EXPECT_EQ(JXL_DEC_SUCCESS,
JxlDecoderSetImageOutBuffer(dec.get(), &format, pixels.data(),
pixels.size()));
EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderProcessInput(dec.get()));
}
+24 -5
View File
@@ -34,6 +34,8 @@ namespace jxl {
class JxlToJpegDecoder {
public:
JxlToJpegDecoder() { Reset(); }
// Returns whether an output buffer is set.
bool IsOutputSet() const { return next_out_ != nullptr; }
@@ -56,6 +58,21 @@ class JxlToJpegDecoder {
return result;
}
// Sets all per-decode state to its initial value, forgetting the
// caller-owned output buffer. Used by the constructor and when the decoder is
// rewound or reset, after which the buffer the caller passed to
// SetOutputBuffer() may no longer be valid.
void Reset() {
buffer_.clear();
buffer_.shrink_to_fit();
jpeg_data_.reset();
inside_box_ = false;
box_until_eof_ = false;
box_size_ = 0;
next_out_ = nullptr;
avail_size_ = 0;
}
void StartBox(bool box_until_eof, size_t contents_size) {
// A new box implies that we clear the buffer.
buffer_.clear();
@@ -151,18 +168,18 @@ class JxlToJpegDecoder {
// True if the decoder is currently reading bytes inside a JPEG reconstruction
// box.
bool inside_box_ = false;
bool inside_box_;
// True if the JPEG reconstruction box had undefined size (all remaining
// bytes).
bool box_until_eof_ = false;
bool box_until_eof_;
// Size of most recently parsed JPEG reconstruction box contents.
size_t box_size_ = 0;
size_t box_size_;
// Next bytes to write JPEG reconstruction to.
uint8_t* next_out_ = nullptr;
uint8_t* next_out_;
// Available bytes to write JPEG reconstruction to.
size_t avail_size_ = 0;
size_t avail_size_;
};
#else
@@ -178,6 +195,8 @@ class JxlToJpegDecoder {
}
size_t ReleaseOutputBuffer() { return 0; }
void Reset() {}
void StartBox(bool /* box_until_eof */, size_t /* contents_size */) {}
JxlDecoderStatus Process(const uint8_t** next_in, size_t* avail_in) {
Loaded 100 of 186 files, more files were not shown because too many files have changed in this diff. Show more