mirror of
https://github.com/Basilisk-Development-Team/UXP-mirror.git
synced 2026-11-12 20:32:03 -05:00
Compare commits
39
Commits
array-slice-sm
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f720a9105c | ||
|
|
6e8ae471d4 | ||
|
|
b2e56a1c3d | ||
|
|
ac578bb1dc | ||
|
|
fb681d9ff8 | ||
|
|
b033b1e37f | ||
|
|
c95cc9e27a | ||
|
|
8622d16d32 | ||
|
|
47608bd7fd | ||
|
|
37f10da2f8 | ||
|
|
0b8dad69d2 | ||
|
|
e5b9f4771f | ||
|
|
2941f11082 | ||
|
|
db931e0bac | ||
|
|
9a445f541f | ||
|
|
500f467a6d | ||
|
|
197400b5fd | ||
|
|
4dfc3313d1 | ||
|
|
57b87edf6e | ||
|
|
10af280359 | ||
|
|
199288d27c | ||
|
|
6ab372c02a | ||
|
|
6ea2e766ae | ||
|
|
976f435ecf | ||
|
|
b28af61a16 | ||
|
|
4569c62cdb | ||
|
|
3474188b22 | ||
|
|
2b513d7640 | ||
|
|
d735420923 | ||
|
|
77fb133f1d | ||
|
|
f15463c675 | ||
|
|
88b1057bca | ||
|
|
3cce03d12a | ||
|
|
5630548c86 | ||
|
|
bed3920d75 | ||
|
|
bc4a702f80 | ||
|
|
575aa0ebaa | ||
|
|
518ef1cb10 | ||
|
|
73da09c35e |
No files matched your search
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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:
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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)
|
||||
{}
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ sphinx:
|
||||
configuration: doc/sphinx/conf.py
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-26.04
|
||||
tools:
|
||||
python: "3"
|
||||
apt_packages:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ https://www.bestpractices.dev/projects/7845)
|
||||
https://securityscorecards.dev/viewer/?uri=github.com/libjxl/libjxl)
|
||||
[](
|
||||
https://codecov.io/gh/libjxl/libjxl)
|
||||
[](https://discord.gg/DqkQgDRTFu)
|
||||
|
||||
<img src="doc/jxl.svg" width="100" align="right" alt="JXL logo">
|
||||
|
||||
|
||||
+12
-5
@@ -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=()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") {}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user