mirror of
https://github.com/Basilisk-Development-Team/UXP-mirror.git
synced 2026-11-13 12:52:02 -05:00
Compare commits
39
Commits
| 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();
|
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>
|
already_AddRefed<DOMRectList>
|
||||||
Element::GetClientRects()
|
Element::GetClientRects()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -909,6 +909,8 @@ public:
|
|||||||
already_AddRefed<DOMRectList> GetClientRects();
|
already_AddRefed<DOMRectList> GetClientRects();
|
||||||
already_AddRefed<DOMRect> GetBoundingClientRect();
|
already_AddRefed<DOMRect> GetBoundingClientRect();
|
||||||
|
|
||||||
|
bool CheckVisibility(const CheckVisibilityOptions& aOptions);
|
||||||
|
|
||||||
// Shadow DOM v1
|
// Shadow DOM v1
|
||||||
already_AddRefed<ShadowRoot> AttachShadow(const ShadowRootInit& aInit,
|
already_AddRefed<ShadowRoot> AttachShadow(const ShadowRootInit& aInit,
|
||||||
ErrorResult& aError);
|
ErrorResult& aError);
|
||||||
|
|||||||
@@ -371,6 +371,24 @@ ShadowRoot::AdoptedStyleSheetsChanged(
|
|||||||
OwnerDoc()->EndUpdate(UPDATE_STYLE);
|
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
|
void
|
||||||
ShadowRoot::AddToIdTable(Element* aElement, nsIAtom* aId)
|
ShadowRoot::AddToIdTable(Element* aElement, nsIAtom* aId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ public:
|
|||||||
void AdoptedStyleSheetsChanged(
|
void AdoptedStyleSheetsChanged(
|
||||||
const nsTArray<RefPtr<mozilla::CSSStyleSheet>>& aOldSheets,
|
const nsTArray<RefPtr<mozilla::CSSStyleSheet>>& aOldSheets,
|
||||||
const nsTArray<RefPtr<mozilla::CSSStyleSheet>>& aNewSheets);
|
const nsTArray<RefPtr<mozilla::CSSStyleSheet>>& aNewSheets);
|
||||||
|
void OwnerDocumentChanged(nsIDocument* aOldDocument);
|
||||||
StyleSheetList* StyleSheets()
|
StyleSheetList* StyleSheets()
|
||||||
{
|
{
|
||||||
return &DocumentOrShadowRoot::EnsureDOMStyleSheets();
|
return &DocumentOrShadowRoot::EnsureDOMStyleSheets();
|
||||||
|
|||||||
@@ -535,6 +535,12 @@ nsNodeUtils::CloneAndAdopt(nsINode *aNode, bool aClone, bool aDeep,
|
|||||||
if (elem) {
|
if (elem) {
|
||||||
elem->NodeInfoChanged();
|
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();
|
nsIDocument* newDoc = aNode->OwnerDoc();
|
||||||
if (newDoc) {
|
if (newDoc) {
|
||||||
|
|||||||
@@ -495,7 +495,7 @@ nsRange::CharacterDataChanged(nsIDocument* aDocument,
|
|||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
if (mStartOffsetWasIncremented || mEndOffsetWasIncremented) {
|
if (mStartOffsetWasIncremented || mEndOffsetWasIncremented) {
|
||||||
mAssertNextInsertOrAppendIndex =
|
mAssertNextInsertOrAppendIndex =
|
||||||
(mStartOffsetWasIncremented ? mStartOffset : mEndOffset) - 1;
|
mStartOffsetWasIncremented ? mStartOffset : mEndOffset;
|
||||||
mAssertNextInsertOrAppendNode = aInfo->mDetails->mNextSibling;
|
mAssertNextInsertOrAppendNode = aInfo->mDetails->mNextSibling;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -611,6 +611,7 @@ skip-if = toolkit == 'android'
|
|||||||
[test_bug1314032.html]
|
[test_bug1314032.html]
|
||||||
[test_bug1375050.html]
|
[test_bug1375050.html]
|
||||||
[test_bug1453693.html]
|
[test_bug1453693.html]
|
||||||
|
[test_checkVisibility.html]
|
||||||
skip-if = os == "mac" # Different tab focus behavior on mac
|
skip-if = os == "mac" # Different tab focus behavior on mac
|
||||||
[test_caretPositionFromPoint.html]
|
[test_caretPositionFromPoint.html]
|
||||||
[test_change_policy.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);
|
MOZ_ASSERT(mRefCnt.get() > 0);
|
||||||
if (mRefCnt.get() > 1) {
|
if (mRefCnt.get() > 1) {
|
||||||
|
mCallback.exposeToActiveJS();
|
||||||
|
if (mCreationStack) {
|
||||||
|
mCreationStack.exposeToActiveJS();
|
||||||
|
}
|
||||||
|
if (mIncumbentJSGlobal) {
|
||||||
|
mIncumbentJSGlobal.exposeToActiveJS();
|
||||||
|
}
|
||||||
mozilla::HoldJSObjects(this);
|
mozilla::HoldJSObjects(this);
|
||||||
} else {
|
} else {
|
||||||
// We can just forget all our stuff.
|
// We can just forget all our stuff.
|
||||||
|
|||||||
@@ -179,6 +179,19 @@ private:
|
|||||||
mIncumbentGlobal = aIncumbentGlobal;
|
mIncumbentGlobal = aIncumbentGlobal;
|
||||||
mIncumbentJSGlobal = aIncumbentGlobal->GetGlobalJSObject();
|
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,
|
inline void Init(JSObject* aCallback, JSObject* aCreationStack,
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ OpusDataDecoder::Init()
|
|||||||
if (codecDelay != FramesToUsecs(mOpusParser->mPreSkip,
|
if (codecDelay != FramesToUsecs(mOpusParser->mPreSkip,
|
||||||
mOpusParser->mRate).value()) {
|
mOpusParser->mRate).value()) {
|
||||||
NS_WARNING("Invalid Opus header: CodecDelay and pre-skip do not match!");
|
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) {
|
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_SIZEOF_TIMEVAL", JS::Int32Value(sizeof (struct timeval)) },
|
||||||
{ "OSFILE_OFFSETOF_TIMEVAL_TV_SEC", JS::Int32Value(offsetof (struct timeval, tv_sec)) },
|
{ "OSFILE_OFFSETOF_TIMEVAL_TV_SEC", JS::Int32Value(offsetof (struct timeval, tv_sec)) },
|
||||||
{ "OSFILE_OFFSETOF_TIMEVAL_TV_USEC", JS::Int32Value(offsetof (struct timeval, tv_usec)) },
|
{ "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)
|
#if defined(DT_UNKNOWN)
|
||||||
// Position of field |d_type| in |dirent|
|
// Position of field |d_type| in |dirent|
|
||||||
@@ -716,6 +719,24 @@ static const dom::ConstantSpec gLibcProperties[] =
|
|||||||
INT_CONSTANT(_STAT_VER),
|
INT_CONSTANT(_STAT_VER),
|
||||||
#endif // defined(_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
|
PROP_END
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -192,11 +192,22 @@ dictionary ScrollIntoViewOptions : ScrollOptions {
|
|||||||
ScrollLogicalPosition inline = "nearest";
|
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
|
// http://dev.w3.org/csswg/cssom-view/#extensions-to-the-element-interface
|
||||||
partial interface Element {
|
partial interface Element {
|
||||||
DOMRectList getClientRects();
|
DOMRectList getClientRects();
|
||||||
DOMRect getBoundingClientRect();
|
DOMRect getBoundingClientRect();
|
||||||
|
|
||||||
|
boolean checkVisibility(optional CheckVisibilityOptions options);
|
||||||
|
|
||||||
// scrolling
|
// scrolling
|
||||||
void scrollIntoView(optional (boolean or ScrollIntoViewOptions) arg);
|
void scrollIntoView(optional (boolean or ScrollIntoViewOptions) arg);
|
||||||
// None of the CSSOM attributes are [Pure], because they flush
|
// 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")))
|
.setAsyncStack(GetWorkerPref<bool>(NS_LITERAL_CSTRING("asyncstack")))
|
||||||
.setWerror(GetWorkerPref<bool>(NS_LITERAL_CSTRING("werror")))
|
.setWerror(GetWorkerPref<bool>(NS_LITERAL_CSTRING("werror")))
|
||||||
.setStreams(GetWorkerPref<bool>(NS_LITERAL_CSTRING("streams")))
|
.setStreams(GetWorkerPref<bool>(NS_LITERAL_CSTRING("streams")))
|
||||||
.setWeakRefs(GetWorkerPref<bool>(NS_LITERAL_CSTRING("weakrefs")))
|
|
||||||
.setExtraWarnings(GetWorkerPref<bool>(NS_LITERAL_CSTRING("strict")))
|
.setExtraWarnings(GetWorkerPref<bool>(NS_LITERAL_CSTRING("strict")))
|
||||||
.setArrayProtoValues(GetWorkerPref<bool>(
|
.setArrayProtoValues(GetWorkerPref<bool>(
|
||||||
NS_LITERAL_CSTRING("array_prototype_values")));
|
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.storageManager.enabled", StorageManagerEnabled, STORAGEMANAGER_ENABLED)
|
||||||
WORKER_SIMPLE_PREF("dom.push.enabled", PushEnabled, PUSH_ENABLED)
|
WORKER_SIMPLE_PREF("dom.push.enabled", PushEnabled, PUSH_ENABLED)
|
||||||
WORKER_SIMPLE_PREF("dom.streams.enabled", StreamsEnabled, STREAMS_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("dom.requestcontext.enabled", RequestContextEnabled, REQUESTCONTEXT_ENABLED)
|
||||||
WORKER_SIMPLE_PREF("gfx.offscreencanvas.enabled", OffscreenCanvasEnabled, OFFSCREENCANVAS_ENABLED)
|
WORKER_SIMPLE_PREF("gfx.offscreencanvas.enabled", OffscreenCanvasEnabled, OFFSCREENCANVAS_ENABLED)
|
||||||
WORKER_SIMPLE_PREF("dom.webkitBlink.dirPicker.enabled", WebkitBlinkDirectoryPickerEnabled, DOM_WEBKITBLINK_DIRPICKER_WEBKITBLINK)
|
WORKER_SIMPLE_PREF("dom.webkitBlink.dirPicker.enabled", WebkitBlinkDirectoryPickerEnabled, DOM_WEBKITBLINK_DIRPICKER_WEBKITBLINK)
|
||||||
|
|||||||
@@ -66,28 +66,10 @@ struct WeakCell {
|
|||||||
WeakCellKind kind;
|
WeakCellKind kind;
|
||||||
WeakRef<JSObject*> object;
|
WeakRef<JSObject*> object;
|
||||||
WeakRef<JS::Symbol*> symbol;
|
WeakRef<JS::Symbol*> symbol;
|
||||||
bool enabled;
|
|
||||||
|
|
||||||
WeakCell()
|
WeakCell() : kind(WeakCellKind::Empty), object(nullptr), symbol(nullptr) {}
|
||||||
: kind(WeakCellKind::Empty)
|
explicit WeakCell(JSObject* obj) : kind(WeakCellKind::Object), object(obj), symbol(nullptr) {}
|
||||||
, object(nullptr)
|
explicit WeakCell(JS::Symbol* sym) : kind(WeakCellKind::Symbol), object(nullptr), symbol(sym) {}
|
||||||
, 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)
|
|
||||||
{}
|
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
kind = WeakCellKind::Empty;
|
kind = WeakCellKind::Empty;
|
||||||
@@ -119,19 +101,20 @@ struct WeakCell {
|
|||||||
if (!target)
|
if (!target)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!enabled) {
|
if (IsInsideNursery(target)) {
|
||||||
// Do nothing
|
|
||||||
} else if (IsInsideNursery(target)) {
|
|
||||||
TraceManuallyBarrieredEdge(trc, object.unsafeGet(), name);
|
TraceManuallyBarrieredEdge(trc, object.unsafeGet(), name);
|
||||||
} else {
|
} 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;
|
return;
|
||||||
TraceWeakEdge(trc, &object, name);
|
TraceWeakEdge(trc, &object, name);
|
||||||
}
|
}
|
||||||
} else if (kind == WeakCellKind::Symbol) {
|
} else if (kind == WeakCellKind::Symbol) {
|
||||||
JS::Symbol* target = symbol.unbarrieredGet();
|
JS::Symbol* target = symbol.unbarrieredGet();
|
||||||
if (enabled && target) {
|
if (target) {
|
||||||
if (trc->isMarkingTracer() && !target->asTenured().zone()->isCollecting())
|
if (trc->isMarkingTracer() && !target->asTenured().zone()->isGCMarkingGray())
|
||||||
return;
|
return;
|
||||||
TraceWeakEdge(trc, &symbol, name);
|
TraceWeakEdge(trc, &symbol, name);
|
||||||
}
|
}
|
||||||
@@ -144,16 +127,14 @@ struct WeakCell {
|
|||||||
if (!target)
|
if (!target)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!enabled) {
|
if (IsInsideNursery(target)) {
|
||||||
// Do nothing
|
|
||||||
} else if (IsInsideNursery(target)) {
|
|
||||||
TraceManuallyBarrieredEdge(trc, object.unsafeGet(), name);
|
TraceManuallyBarrieredEdge(trc, object.unsafeGet(), name);
|
||||||
} else if (target->asTenured().zone()->isCollecting()) {
|
} else if (target->asTenured().zone()->isGCMarkingGray()) {
|
||||||
TraceWeakEdge(trc, &object, name);
|
TraceWeakEdge(trc, &object, name);
|
||||||
}
|
}
|
||||||
} else if (kind == WeakCellKind::Symbol) {
|
} else if (kind == WeakCellKind::Symbol) {
|
||||||
JS::Symbol* target = symbol.unbarrieredGet();
|
JS::Symbol* target = symbol.unbarrieredGet();
|
||||||
if (enabled && target && target->asTenured().zone()->isCollecting())
|
if (target && target->asTenured().zone()->isGCMarkingGray())
|
||||||
TraceWeakEdge(trc, &symbol, name);
|
TraceWeakEdge(trc, &symbol, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,24 +146,22 @@ struct FinalizationRecord {
|
|||||||
WeakCell unregisterToken;
|
WeakCell unregisterToken;
|
||||||
bool active;
|
bool active;
|
||||||
bool queued;
|
bool queued;
|
||||||
bool enabled;
|
|
||||||
|
|
||||||
FinalizationRecord(HandleValue targetValue, HandleValue heldValue,
|
FinalizationRecord(HandleValue targetValue, HandleValue heldValue,
|
||||||
HandleValue unregisterTokenValue, bool enabled)
|
HandleValue unregisterTokenValue)
|
||||||
: heldValue(heldValue),
|
: heldValue(heldValue),
|
||||||
active(true),
|
active(true),
|
||||||
queued(false),
|
queued(false)
|
||||||
enabled(enabled)
|
|
||||||
{
|
{
|
||||||
if (targetValue.isObject())
|
if (targetValue.isObject())
|
||||||
target = WeakCell(NormalizeWeakObject(&targetValue.toObject()), enabled);
|
target = WeakCell(NormalizeWeakObject(&targetValue.toObject()));
|
||||||
else
|
else
|
||||||
target = WeakCell(targetValue.toSymbol(), enabled);
|
target = WeakCell(targetValue.toSymbol());
|
||||||
|
|
||||||
if (unregisterTokenValue.isObject())
|
if (unregisterTokenValue.isObject())
|
||||||
unregisterToken = WeakCell(NormalizeWeakObject(&unregisterTokenValue.toObject()), enabled);
|
unregisterToken = WeakCell(NormalizeWeakObject(&unregisterTokenValue.toObject()));
|
||||||
else if (unregisterTokenValue.isSymbol())
|
else if (unregisterTokenValue.isSymbol())
|
||||||
unregisterToken = WeakCell(unregisterTokenValue.toSymbol(), enabled);
|
unregisterToken = WeakCell(unregisterTokenValue.toSymbol());
|
||||||
}
|
}
|
||||||
|
|
||||||
void trace(JSTracer* trc) {
|
void trace(JSTracer* trc) {
|
||||||
@@ -241,15 +220,13 @@ struct FinalizationRegistryObject::Data {
|
|||||||
FinalizationRecordVector records;
|
FinalizationRecordVector records;
|
||||||
FinalizationRecordVector cleanupQueue;
|
FinalizationRecordVector cleanupQueue;
|
||||||
bool queuedForCleanup;
|
bool queuedForCleanup;
|
||||||
bool enabled;
|
|
||||||
|
|
||||||
explicit Data(JSObject* cleanupCallback, bool enabled)
|
explicit Data(JSObject* cleanupCallback)
|
||||||
: cleanupCallback(cleanupCallback),
|
: cleanupCallback(cleanupCallback),
|
||||||
cleanupJob(nullptr),
|
cleanupJob(nullptr),
|
||||||
records(SystemAllocPolicy()),
|
records(SystemAllocPolicy()),
|
||||||
cleanupQueue(SystemAllocPolicy()),
|
cleanupQueue(SystemAllocPolicy()),
|
||||||
queuedForCleanup(false),
|
queuedForCleanup(false)
|
||||||
enabled(enabled)
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
~Data() {
|
~Data() {
|
||||||
@@ -258,13 +235,11 @@ struct FinalizationRegistryObject::Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void trace(JSTracer* trc) {
|
void trace(JSTracer* trc) {
|
||||||
if (enabled) {
|
JS::TraceEdge(trc, &cleanupCallback, "FinalizationRegistry cleanup callback");
|
||||||
JS::TraceEdge(trc, &cleanupCallback, "FinalizationRegistry cleanup callback");
|
if (cleanupJob.unbarrieredGet())
|
||||||
if (cleanupJob.unbarrieredGet())
|
JS::TraceEdge(trc, &cleanupJob, "FinalizationRegistry cleanup job");
|
||||||
JS::TraceEdge(trc, &cleanupJob, "FinalizationRegistry cleanup job");
|
for (size_t i = 0; i < records.length(); i++)
|
||||||
for (size_t i = 0; i < records.length(); i++)
|
records[i]->trace(trc);
|
||||||
records[i]->trace(trc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool appendRecord(FinalizationRecord* record) {
|
bool appendRecord(FinalizationRecord* record) {
|
||||||
@@ -345,7 +320,7 @@ FinalizationRegistry_register_impl(JSContext* cx, const CallArgs& args)
|
|||||||
unregisterToken.setUndefined();
|
unregisterToken.setUndefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
FinalizationRecord* record = cx->new_<FinalizationRecord>(target, heldValue, unregisterToken, cx->options().weakRefs());
|
FinalizationRecord* record = cx->new_<FinalizationRecord>(target, heldValue, unregisterToken);
|
||||||
if (!record)
|
if (!record)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -429,7 +404,9 @@ FinalizationRegistryObject::create(JSContext* cx, HandleObject cleanupCallback,
|
|||||||
if (!obj)
|
if (!obj)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
Data* data = cx->new_<Data>(cleanupCallback, cx->options().weakRefs());
|
MOZ_ASSERT(!IsInsideNursery(obj.get()));
|
||||||
|
|
||||||
|
Data* data = cx->new_<Data>(cleanupCallback);
|
||||||
if (!data)
|
if (!data)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
@@ -538,6 +515,17 @@ FinalizationRegistryObject::cleanupJob(JSContext* cx, unsigned argc, Value* vp)
|
|||||||
/* static */ void
|
/* static */ void
|
||||||
FinalizationRegistryObject::trace(JSTracer* trc, JSObject* obj)
|
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))
|
if (Data* data = GetData(obj))
|
||||||
data->trace(trc);
|
data->trace(trc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ WeakRef_deref_impl(JSContext* cx, const CallArgs& args)
|
|||||||
// Ensure pointer is not null.
|
// Ensure pointer is not null.
|
||||||
// During WeakRef deref, if the target was collected, it will be nullptr.
|
// During WeakRef deref, if the target was collected, it will be nullptr.
|
||||||
if (target) {
|
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));
|
RootedValue kept(cx, ObjectValue(*target));
|
||||||
if (!cx->runtime()->addWeakRefKeptObject(cx, kept))
|
if (!cx->runtime()->addWeakRefKeptObject(cx, kept))
|
||||||
return false;
|
return false;
|
||||||
@@ -88,6 +92,7 @@ WeakRef_deref_impl(JSContext* cx, const CallArgs& args)
|
|||||||
|
|
||||||
// Ensure pointer is not null.
|
// Ensure pointer is not null.
|
||||||
if (target) {
|
if (target) {
|
||||||
|
js::gc::ExposeGCThingToActiveJS(JS::GCCellPtr(target));
|
||||||
RootedValue kept(cx, SymbolValue(target));
|
RootedValue kept(cx, SymbolValue(target));
|
||||||
if (!cx->runtime()->addWeakRefKeptObject(cx, kept))
|
if (!cx->runtime()->addWeakRefKeptObject(cx, kept))
|
||||||
return false;
|
return false;
|
||||||
@@ -144,16 +149,30 @@ WeakRefObject::create(JSContext* cx, HandleValue target, HandleObject proto /* =
|
|||||||
if (!obj)
|
if (!obj)
|
||||||
return nullptr;
|
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;
|
Referent* data;
|
||||||
if (target.isObject())
|
if (target.isObject())
|
||||||
data = cx->new_<Referent>(&target.toObject(), cx->options().weakRefs());
|
data = cx->new_<Referent>(&target.toObject());
|
||||||
else
|
else
|
||||||
data = cx->new_<Referent>(target.toSymbol(), cx->options().weakRefs());
|
data = cx->new_<Referent>(target.toSymbol());
|
||||||
|
|
||||||
if (!data)
|
if (!data)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
obj->setPrivate(data);
|
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;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,15 +229,28 @@ WeakRefObject::deref(JSContext* cx, unsigned argc, Value* vp)
|
|||||||
/* static */ void
|
/* static */ void
|
||||||
WeakRefObject::trace(JSTracer* trc, JSObject* obj)
|
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 (Referent* data = GetReferent(obj)) {
|
||||||
if (data->isObject()) {
|
if (data->isObject()) {
|
||||||
JSObject* target = data->objectTarget.unbarrieredGet();
|
JSObject* target = data->objectTarget.unbarrieredGet();
|
||||||
if (!target)
|
if (!target)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!data->enabled) {
|
if (IsInsideNursery(target)) {
|
||||||
// Pref disabled, don't do anything.
|
|
||||||
} else if (IsInsideNursery(target)) {
|
|
||||||
// Weak edges must be tenured; trace strongly while referent is in the nursery.
|
// Weak edges must be tenured; trace strongly while referent is in the nursery.
|
||||||
TraceManuallyBarrieredEdge(trc, data->objectTarget.unsafeGet(),
|
TraceManuallyBarrieredEdge(trc, data->objectTarget.unsafeGet(),
|
||||||
"WeakRef nursery referent");
|
"WeakRef nursery referent");
|
||||||
@@ -228,7 +260,7 @@ WeakRefObject::trace(JSTracer* trc, JSObject* obj)
|
|||||||
} else {
|
} else {
|
||||||
MOZ_ASSERT(data->isSymbol());
|
MOZ_ASSERT(data->isSymbol());
|
||||||
JS::Symbol* target = data->symbolTarget.unbarrieredGet();
|
JS::Symbol* target = data->symbolTarget.unbarrieredGet();
|
||||||
if (data->enabled && target)
|
if (target)
|
||||||
TraceWeakEdge(trc, &data->symbolTarget, "WeakRef symbol referent");
|
TraceWeakEdge(trc, &data->symbolTarget, "WeakRef symbol referent");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,8 +269,27 @@ WeakRefObject::trace(JSTracer* trc, JSObject* obj)
|
|||||||
/* static */ void
|
/* static */ void
|
||||||
WeakRefObject::finalize(FreeOp* fop, JSObject* obj)
|
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);
|
fop->delete_(data);
|
||||||
|
obj->as<WeakRefObject>().setPrivate(nullptr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const ClassOps WeakRefObjectClassOps = {
|
static const ClassOps WeakRefObjectClassOps = {
|
||||||
@@ -259,7 +310,8 @@ static const ClassOps WeakRefObjectClassOps = {
|
|||||||
const Class WeakRefObject::class_ = {
|
const Class WeakRefObject::class_ = {
|
||||||
"WeakRef",
|
"WeakRef",
|
||||||
JSCLASS_HAS_PRIVATE |
|
JSCLASS_HAS_PRIVATE |
|
||||||
JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef),
|
JSCLASS_HAS_CACHED_PROTO(JSProto_WeakRef) |
|
||||||
|
JSCLASS_FOREGROUND_FINALIZE,
|
||||||
&WeakRefObjectClassOps
|
&WeakRefObjectClassOps
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,27 +22,17 @@ class WeakRefObject : public NativeObject
|
|||||||
Symbol
|
Symbol
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit Referent(JSObject* obj, bool enabled)
|
explicit Referent(JSObject* obj)
|
||||||
: kind(Kind::Object)
|
: kind(Kind::Object), objectTarget(obj), symbolTarget(nullptr) {}
|
||||||
, objectTarget(obj)
|
explicit Referent(JS::Symbol* sym)
|
||||||
, symbolTarget(nullptr)
|
: kind(Kind::Symbol), objectTarget(nullptr), symbolTarget(sym) {}
|
||||||
, enabled(enabled)
|
|
||||||
{}
|
|
||||||
|
|
||||||
explicit Referent(JS::Symbol* sym, bool enabled)
|
|
||||||
: kind(Kind::Symbol)
|
|
||||||
, objectTarget(nullptr)
|
|
||||||
, symbolTarget(sym)
|
|
||||||
, enabled(enabled)
|
|
||||||
{}
|
|
||||||
|
|
||||||
bool isObject() const { return kind == Kind::Object; }
|
bool isObject() const { return kind == Kind::Object; }
|
||||||
bool isSymbol() const { return kind == Kind::Symbol; }
|
bool isSymbol() const { return kind == Kind::Symbol; }
|
||||||
|
|
||||||
Kind kind;
|
Kind kind;
|
||||||
WeakRef<JSObject*> objectTarget;
|
WeakRef<JSObject*> objectTarget;
|
||||||
WeakRef<JS::Symbol*> symbolTarget;
|
WeakRef<JS::Symbol*> symbolTarget;
|
||||||
bool enabled;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const Class class_;
|
static const Class class_;
|
||||||
@@ -68,6 +58,10 @@ class WeakRefObject : public NativeObject
|
|||||||
static const JSPropertySpec properties[];
|
static const JSPropertySpec properties[];
|
||||||
static const JSFunctionSpec methods[];
|
static const JSFunctionSpec methods[];
|
||||||
|
|
||||||
|
Zone* targetZone = nullptr;
|
||||||
|
|
||||||
|
void clearTargetZone() { targetZone = nullptr; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7084,9 +7084,16 @@ BytecodeEmitter::emitDeletePropertyInOptChain(
|
|||||||
MOZ_ASSERT_IF(propExpr->is<PropertyAccess>(),
|
MOZ_ASSERT_IF(propExpr->is<PropertyAccess>(),
|
||||||
!propExpr->as<PropertyAccess>().isSuper());
|
!propExpr->as<PropertyAccess>().isSuper());
|
||||||
|
|
||||||
if (!emitOptionalTree(&propExpr->expression(), oe)) {
|
if (propExpr->isSuper()) {
|
||||||
// [stack] OBJ
|
if (!emitGetThisForSuperBase(&propExpr->expression().as<UnaryNode>())) {
|
||||||
return false;
|
// [stack] THIS
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!emitOptionalTree(&propExpr->expression(), oe)) {
|
||||||
|
// [stack] OBJ
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (propExpr->isKind(PNK_OPTDOT)) {
|
if (propExpr->isKind(PNK_OPTDOT)) {
|
||||||
if (!oe.emitJumpShortCircuit()) {
|
if (!oe.emitJumpShortCircuit()) {
|
||||||
@@ -7381,7 +7388,7 @@ BytecodeEmitter::emitOptionalCalleeAndThis(
|
|||||||
case PNK_OPTDOT: {
|
case PNK_OPTDOT: {
|
||||||
MOZ_ASSERT(emitterMode != BytecodeEmitter::SelfHosting);
|
MOZ_ASSERT(emitterMode != BytecodeEmitter::SelfHosting);
|
||||||
OptionalPropertyAccess* prop = &calleeNode->as<OptionalPropertyAccess>();
|
OptionalPropertyAccess* prop = &calleeNode->as<OptionalPropertyAccess>();
|
||||||
bool isSuper = false;
|
bool isSuper = prop->isSuper();
|
||||||
PropOpEmitter& poe = cone.prepareForPropCallee(isSuper);
|
PropOpEmitter& poe = cone.prepareForPropCallee(isSuper);
|
||||||
if (!emitOptionalDotExpression(prop, poe, isSuper, oe)) {
|
if (!emitOptionalDotExpression(prop, poe, isSuper, oe)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -7401,7 +7408,7 @@ BytecodeEmitter::emitOptionalCalleeAndThis(
|
|||||||
case PNK_OPTELEM: {
|
case PNK_OPTELEM: {
|
||||||
MOZ_ASSERT(emitterMode != BytecodeEmitter::SelfHosting);
|
MOZ_ASSERT(emitterMode != BytecodeEmitter::SelfHosting);
|
||||||
OptionalPropertyByValue* elem = &calleeNode->as<OptionalPropertyByValue>();
|
OptionalPropertyByValue* elem = &calleeNode->as<OptionalPropertyByValue>();
|
||||||
bool isSuper = false;
|
bool isSuper = elem->isSuper();
|
||||||
ElemOpEmitter& eoe = cone.prepareForElemCallee(isSuper);
|
ElemOpEmitter& eoe = cone.prepareForElemCallee(isSuper);
|
||||||
if (!emitOptionalElemExpression(elem, eoe, isSuper, oe)) {
|
if (!emitOptionalElemExpression(elem, eoe, isSuper, oe)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -7431,8 +7438,15 @@ BytecodeEmitter::emitOptionalCalleeAndThis(
|
|||||||
case PNK_OPTCHAIN: {
|
case PNK_OPTCHAIN: {
|
||||||
return emitCalleeAndThisForOptionalChain(&calleeNode->as<UnaryNode>(), callNode, cone);
|
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: {
|
default: {
|
||||||
MOZ_RELEASE_ASSERT(calleeNode->getKind() != PNK_SUPERBASE);
|
|
||||||
if (!cone.prepareForOtherCallee()) {
|
if (!cone.prepareForOtherCallee()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -9725,9 +9739,11 @@ BytecodeEmitter::emitOptionalTree(
|
|||||||
switch (kind) {
|
switch (kind) {
|
||||||
case PNK_OPTDOT: {
|
case PNK_OPTDOT: {
|
||||||
OptionalPropertyAccess* prop = &pn->as<OptionalPropertyAccess>();
|
OptionalPropertyAccess* prop = &pn->as<OptionalPropertyAccess>();
|
||||||
bool isSuper = false;
|
bool isSuper = prop->isSuper();
|
||||||
PropOpEmitter poe(this, PropOpEmitter::Kind::Get,
|
PropOpEmitter poe(this, PropOpEmitter::Kind::Get,
|
||||||
PropOpEmitter::ObjKind::Other);
|
isSuper ? PropOpEmitter::ObjKind::Super
|
||||||
|
: PropOpEmitter::ObjKind::Other);
|
||||||
|
|
||||||
if (!emitOptionalDotExpression(prop, poe, isSuper, oe))
|
if (!emitOptionalDotExpression(prop, poe, isSuper, oe))
|
||||||
return false;
|
return false;
|
||||||
break;
|
break;
|
||||||
@@ -9744,9 +9760,10 @@ BytecodeEmitter::emitOptionalTree(
|
|||||||
}
|
}
|
||||||
case PNK_OPTELEM: {
|
case PNK_OPTELEM: {
|
||||||
OptionalPropertyByValue* elem = &pn->as<OptionalPropertyByValue>();
|
OptionalPropertyByValue* elem = &pn->as<OptionalPropertyByValue>();
|
||||||
bool isSuper = false;
|
bool isSuper = elem->isSuper();
|
||||||
ElemOpEmitter eoe(this, ElemOpEmitter::Kind::Get,
|
ElemOpEmitter eoe(this, ElemOpEmitter::Kind::Get,
|
||||||
ElemOpEmitter::ObjKind::Other);
|
isSuper ? ElemOpEmitter::ObjKind::Super
|
||||||
|
: ElemOpEmitter::ObjKind::Other);
|
||||||
if (!emitOptionalElemExpression(elem, eoe, isSuper, oe))
|
if (!emitOptionalElemExpression(elem, eoe, isSuper, oe))
|
||||||
return false;
|
return false;
|
||||||
break;
|
break;
|
||||||
@@ -9789,18 +9806,24 @@ BytecodeEmitter::emitOptionalTree(
|
|||||||
kind == PNK_CLASS ||
|
kind == PNK_CLASS ||
|
||||||
kind == PNK_REGEXP ||
|
kind == PNK_REGEXP ||
|
||||||
kind == PNK_TEMPLATE_STRING ||
|
kind == PNK_TEMPLATE_STRING ||
|
||||||
|
kind == PNK_TEMPLATE_STRING_LIST ||
|
||||||
kind == PNK_RAW_UNDEFINED ||
|
kind == PNK_RAW_UNDEFINED ||
|
||||||
|
kind == PNK_GENEXP ||
|
||||||
|
kind == PNK_ARRAYCOMP ||
|
||||||
pn->isInParens();
|
pn->isInParens();
|
||||||
|
|
||||||
// https://tc39.es/ecma262/#sec-left-hand-side-expressions
|
// https://tc39.es/ecma262/#sec-left-hand-side-expressions
|
||||||
bool isMemberExpression = isPrimaryExpression ||
|
bool isMemberExpression = isPrimaryExpression ||
|
||||||
kind == PNK_TAGGED_TEMPLATE ||
|
kind == PNK_TAGGED_TEMPLATE ||
|
||||||
kind == PNK_NEW ||
|
kind == PNK_NEW ||
|
||||||
kind == PNK_NEWTARGET;
|
kind == PNK_NEWTARGET ||
|
||||||
//kind == ParseNodeKind::ImportMetaExpr;
|
kind == PNK_IMPORT_META ||
|
||||||
|
kind == PNK_OPTCHAIN;
|
||||||
|
|
||||||
bool isCallExpression = kind == PNK_SETTHIS;
|
// https://tc39.es/ecma262/#sec-call-expression
|
||||||
//kind == ParseNodeKind::CallImportExpr;
|
bool isCallExpression = kind == PNK_SETTHIS ||
|
||||||
|
kind == PNK_SUPERCALL ||
|
||||||
|
kind == PNK_CALL_IMPORT;
|
||||||
|
|
||||||
MOZ_ASSERT(isMemberExpression || isCallExpression,
|
MOZ_ASSERT(isMemberExpression || isCallExpression,
|
||||||
"Unknown ParseNodeKind for OptionalChain");
|
"Unknown ParseNodeKind for OptionalChain");
|
||||||
@@ -9897,7 +9920,6 @@ BytecodeEmitter::emitOptionalDotExpression(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prop->isKind(PNK_OPTDOT)) {
|
if (prop->isKind(PNK_OPTDOT)) {
|
||||||
MOZ_ASSERT(!isSuper);
|
|
||||||
if (!oe.emitJumpShortCircuit()) {
|
if (!oe.emitJumpShortCircuit()) {
|
||||||
// [stack] # if Jump
|
// [stack] # if Jump
|
||||||
// [stack] UNDEFINED-OR-NULL
|
// [stack] UNDEFINED-OR-NULL
|
||||||
@@ -9940,7 +9962,6 @@ BytecodeEmitter::emitOptionalElemExpression(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (elem->isKind(PNK_OPTELEM)) {
|
if (elem->isKind(PNK_OPTELEM)) {
|
||||||
MOZ_ASSERT(!isSuper);
|
|
||||||
if (!oe.emitJumpShortCircuit()) {
|
if (!oe.emitJumpShortCircuit()) {
|
||||||
// [stack] # if Jump
|
// [stack] # if Jump
|
||||||
// [stack] UNDEFINED-OR-NULL
|
// [stack] UNDEFINED-OR-NULL
|
||||||
|
|||||||
@@ -10274,7 +10274,6 @@ Parser<ParseHandler>::memberPropertyAccess(
|
|||||||
if (!name)
|
if (!name)
|
||||||
return null();
|
return null();
|
||||||
if (optionalKind == OptionalKind::Optional) {
|
if (optionalKind == OptionalKind::Optional) {
|
||||||
MOZ_ASSERT(!handler.isSuperBase(lhs));
|
|
||||||
return handler.newOptionalPropertyAccess(lhs, name);
|
return handler.newOptionalPropertyAccess(lhs, name);
|
||||||
}
|
}
|
||||||
return handler.newPropertyAccess(lhs, name);
|
return handler.newPropertyAccess(lhs, name);
|
||||||
@@ -10302,7 +10301,6 @@ Parser<ParseHandler>::memberElemAccess(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (optionalKind == OptionalKind::Optional) {
|
if (optionalKind == OptionalKind::Optional) {
|
||||||
MOZ_ASSERT(!handler.isSuperBase(lhs));
|
|
||||||
return handler.newOptionalPropertyByValue(lhs, propExpr, pos().end);
|
return handler.newOptionalPropertyByValue(lhs, propExpr, pos().end);
|
||||||
}
|
}
|
||||||
return handler.newPropertyByValue(lhs, propExpr, pos().end);
|
return handler.newPropertyByValue(lhs, propExpr, pos().end);
|
||||||
|
|||||||
@@ -1609,8 +1609,8 @@ ObjectDenseElementsMayBeMarkable(NativeObject* nobj)
|
|||||||
if (!typeSet)
|
if (!typeSet)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
static const uint32_t flagMask =
|
static const uint32_t flagMask = TYPE_FLAG_STRING | TYPE_FLAG_SYMBOL | TYPE_FLAG_LAZYARGS |
|
||||||
TYPE_FLAG_STRING | TYPE_FLAG_SYMBOL | TYPE_FLAG_LAZYARGS | TYPE_FLAG_ANYOBJECT;
|
TYPE_FLAG_ANYOBJECT | TYPE_FLAG_BIGINT;
|
||||||
bool mayBeMarkable = typeSet->hasAnyFlag(flagMask) || typeSet->getObjectCount() != 0;
|
bool mayBeMarkable = typeSet->hasAnyFlag(flagMask) || typeSet->getObjectCount() != 0;
|
||||||
|
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
|
|||||||
+14
-1
@@ -13,6 +13,8 @@
|
|||||||
#include "jit/JitCompartment.h"
|
#include "jit/JitCompartment.h"
|
||||||
#include "vm/Debugger.h"
|
#include "vm/Debugger.h"
|
||||||
#include "vm/Runtime.h"
|
#include "vm/Runtime.h"
|
||||||
|
#include "builtin/WeakRefObject.h"
|
||||||
|
#include "gc/Tracer.h"
|
||||||
|
|
||||||
#include "jscompartmentinlines.h"
|
#include "jscompartmentinlines.h"
|
||||||
#include "jsgcinlines.h"
|
#include "jsgcinlines.h"
|
||||||
@@ -64,6 +66,10 @@ JS::Zone::Zone(JSRuntime* rt)
|
|||||||
Zone::~Zone()
|
Zone::~Zone()
|
||||||
{
|
{
|
||||||
JSRuntime* rt = runtimeFromMainThread();
|
JSRuntime* rt = runtimeFromMainThread();
|
||||||
|
|
||||||
|
for (JSObject* ref : weakRefs)
|
||||||
|
ref->as<WeakRefObject>().clearTargetZone();
|
||||||
|
|
||||||
if (this == rt->gc.systemZone)
|
if (this == rt->gc.systemZone)
|
||||||
rt->gc.systemZone = nullptr;
|
rt->gc.systemZone = nullptr;
|
||||||
|
|
||||||
@@ -385,7 +391,14 @@ Zone::addTypeDescrObject(JSContext* cx, HandleObject obj)
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
Zone::traceWeakRefs(JSTracer* trc)
|
||||||
|
{
|
||||||
|
for (JSObject* ref : weakRefs)
|
||||||
|
WeakRefObject::trace(trc, ref);
|
||||||
|
}
|
||||||
|
|
||||||
ZoneList::ZoneList()
|
ZoneList::ZoneList()
|
||||||
: head(nullptr), tail(nullptr)
|
: head(nullptr), tail(nullptr)
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -326,6 +326,14 @@ struct Zone : public JS::shadow::Zone,
|
|||||||
using WeakEdges = js::Vector<js::gc::TenuredCell**, 0, js::SystemAllocPolicy>;
|
using WeakEdges = js::Vector<js::gc::TenuredCell**, 0, js::SystemAllocPolicy>;
|
||||||
WeakEdges gcWeakRefs;
|
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.
|
// FinalizationRegistry objects with weakly-held target cells in this zone.
|
||||||
using FinalizationRegistryVector = js::Vector<js::WeakRef<JSObject*>, 0, js::SystemAllocPolicy>;
|
using FinalizationRegistryVector = js::Vector<js::WeakRef<JSObject*>, 0, js::SystemAllocPolicy>;
|
||||||
FinalizationRegistryVector finalizationRegistries;
|
FinalizationRegistryVector finalizationRegistries;
|
||||||
|
|||||||
+3
-6
@@ -999,8 +999,7 @@ class JS_PUBLIC_API(ContextOptions) {
|
|||||||
strictMode_(false),
|
strictMode_(false),
|
||||||
extraWarnings_(false),
|
extraWarnings_(false),
|
||||||
arrayProtoValues_(true),
|
arrayProtoValues_(true),
|
||||||
streams_(false),
|
streams_(false)
|
||||||
weakRefs_(false)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1140,13 +1139,12 @@ class JS_PUBLIC_API(ContextOptions) {
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool weakRefs() const { return weakRefs_; }
|
bool weakRefs() const { return true; }
|
||||||
ContextOptions& setWeakRefs(bool flag) {
|
ContextOptions& setWeakRefs(bool flag) {
|
||||||
weakRefs_ = flag;
|
(void) flag;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
ContextOptions& toggleWeakRefs() {
|
ContextOptions& toggleWeakRefs() {
|
||||||
weakRefs_ = !weakRefs_;
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1167,7 +1165,6 @@ class JS_PUBLIC_API(ContextOptions) {
|
|||||||
bool extraWarnings_ : 1;
|
bool extraWarnings_ : 1;
|
||||||
bool arrayProtoValues_ : 1;
|
bool arrayProtoValues_ : 1;
|
||||||
bool streams_ : 1;
|
bool streams_ : 1;
|
||||||
bool weakRefs_ : 1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
JS_PUBLIC_API(ContextOptions&)
|
JS_PUBLIC_API(ContextOptions&)
|
||||||
|
|||||||
+3
-1
@@ -3889,6 +3889,9 @@ template <class ZoneIterT>
|
|||||||
void
|
void
|
||||||
GCRuntime::traceFinalizationRegistryWeakRefs()
|
GCRuntime::traceFinalizationRegistryWeakRefs()
|
||||||
{
|
{
|
||||||
|
for (ZoneIterT zone(rt); !zone.done(); zone.next())
|
||||||
|
zone->traceWeakRefs(&marker);
|
||||||
|
|
||||||
for (ZoneIterT zone(rt); !zone.done(); zone.next()) {
|
for (ZoneIterT zone(rt); !zone.done(); zone.next()) {
|
||||||
for (size_t i = 0; i < zone->finalizationRegistries.length(); i++) {
|
for (size_t i = 0; i < zone->finalizationRegistries.length(); i++) {
|
||||||
WeakRef<JSObject*>& registry = zone->finalizationRegistries[i];
|
WeakRef<JSObject*>& registry = zone->finalizationRegistries[i];
|
||||||
@@ -4776,7 +4779,6 @@ GCRuntime::sweepFinalizationRegistries()
|
|||||||
JSObject* obj = zone->finalizationRegistries[i].unbarrieredGet();
|
JSObject* obj = zone->finalizationRegistries[i].unbarrieredGet();
|
||||||
obj->as<FinalizationRegistryObject>().sweepAfterGC(rt);
|
obj->as<FinalizationRegistryObject>().sweepAfterGC(rt);
|
||||||
}
|
}
|
||||||
zone->finalizationRegistries.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1858,6 +1858,14 @@ CallGetter(JSContext* cx, HandleObject obj, HandleValue receiver, HandleShape sh
|
|||||||
|
|
||||||
if (shape->hasGetterValue()) {
|
if (shape->hasGetterValue()) {
|
||||||
RootedValue getter(cx, shape->getterValue());
|
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);
|
return js::CallGetter(cx, receiver, getter, vp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,10 +168,16 @@ TypedArrayObject::ensureHasBuffer(JSContext* cx, Handle<TypedArrayObject*> tarra
|
|||||||
void
|
void
|
||||||
TypedArrayObject::assertZeroLengthArrayData() const
|
TypedArrayObject::assertZeroLengthArrayData() const
|
||||||
{
|
{
|
||||||
if (length() == 0 && !hasBuffer()) {
|
// Check hasBuffer() before length(): length() walks through isOutOfBounds()
|
||||||
uint8_t* end = fixedData(TypedArrayObject::FIXED_DATA_START);
|
// and bufferUnshared() into the ArrayBufferObject, and this is called from
|
||||||
MOZ_ASSERT(end[0] == ZeroLengthArrayData);
|
// 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
|
#endif
|
||||||
|
|
||||||
@@ -182,7 +188,7 @@ TypedArrayObject::trace(JSTracer* trc, JSObject* objArg)
|
|||||||
ArrayBufferViewObject::trace(trc, objArg);
|
ArrayBufferViewObject::trace(trc, objArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
/* static */ void
|
||||||
TypedArrayObject::finalize(FreeOp* fop, JSObject* obj)
|
TypedArrayObject::finalize(FreeOp* fop, JSObject* obj)
|
||||||
{
|
{
|
||||||
MOZ_ASSERT(!IsInsideNursery(obj));
|
MOZ_ASSERT(!IsInsideNursery(obj));
|
||||||
@@ -193,12 +199,12 @@ TypedArrayObject::finalize(FreeOp* fop, JSObject* obj)
|
|||||||
if (!curObj->elementsRaw())
|
if (!curObj->elementsRaw())
|
||||||
return;
|
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())
|
if (curObj->hasBuffer())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
curObj->assertZeroLengthArrayData();
|
||||||
|
|
||||||
// Free the data slot pointer if it does not point into the old JSObject.
|
// Free the data slot pointer if it does not point into the old JSObject.
|
||||||
if (!curObj->hasInlineElements())
|
if (!curObj->hasInlineElements())
|
||||||
js_free(curObj->elements());
|
js_free(curObj->elements());
|
||||||
|
|||||||
@@ -1442,7 +1442,6 @@ ReloadPrefsCallback(const char* pref, void* data)
|
|||||||
bool extraWarnings = Preferences::GetBool(JS_OPTIONS_DOT_STR "strict");
|
bool extraWarnings = Preferences::GetBool(JS_OPTIONS_DOT_STR "strict");
|
||||||
|
|
||||||
bool streams = Preferences::GetBool(JS_OPTIONS_DOT_STR "streams");
|
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");
|
bool unboxedObjects = Preferences::GetBool(JS_OPTIONS_DOT_STR "unboxed_objects");
|
||||||
|
|
||||||
@@ -1469,8 +1468,7 @@ ReloadPrefsCallback(const char* pref, void* data)
|
|||||||
.setWerror(werror)
|
.setWerror(werror)
|
||||||
.setExtraWarnings(extraWarnings)
|
.setExtraWarnings(extraWarnings)
|
||||||
.setArrayProtoValues(arrayProtoValues)
|
.setArrayProtoValues(arrayProtoValues)
|
||||||
.setStreams(streams)
|
.setStreams(streams);
|
||||||
.setWeakRefs(weakRefs);
|
|
||||||
|
|
||||||
JS_SetParallelParsingEnabled(cx, parallelParsing);
|
JS_SetParallelParsingEnabled(cx, parallelParsing);
|
||||||
JS_SetOffthreadIonCompilationEnabled(cx, offthreadIonCompilation);
|
JS_SetOffthreadIonCompilationEnabled(cx, offthreadIonCompilation);
|
||||||
|
|||||||
@@ -389,7 +389,8 @@ RestyleManager::RestyleForHasPseudoClassChange(nsINode* aNode,
|
|||||||
EventStates aStateMask,
|
EventStates aStateMask,
|
||||||
nsIAtom* aAttribute,
|
nsIAtom* aAttribute,
|
||||||
const nsAttrValue* aNewClasses,
|
const nsAttrValue* aNewClasses,
|
||||||
bool aCompareClasses)
|
bool aCompareClasses,
|
||||||
|
bool aIsTextChange)
|
||||||
{
|
{
|
||||||
MOZ_ASSERT(!aAttribute || aNode->IsElement());
|
MOZ_ASSERT(!aAttribute || aNode->IsElement());
|
||||||
if (!aNode->OwnerDoc()->GetProperty(nsGkAtoms::hasSelectorDependency)) {
|
if (!aNode->OwnerDoc()->GetProperty(nsGkAtoms::hasSelectorDependency)) {
|
||||||
@@ -405,19 +406,26 @@ RestyleManager::RestyleForHasPseudoClassChange(nsINode* aNode,
|
|||||||
PostRestyleEvent(aAnchor, hint, nsChangeHint(0));
|
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()) {
|
for (nsINode* node = aNode; node; node = node->GetParentNode()) {
|
||||||
auto* dependency = static_cast<Dependency*>(
|
auto* dependency = static_cast<Dependency*>(
|
||||||
node->GetProperty(nsGkAtoms::hasSelectorDependency));
|
node->GetProperty(nsGkAtoms::hasSelectorDependency));
|
||||||
if (!dependency) {
|
if (!dependency) {
|
||||||
|
mutationChild = node;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (node->IsElement() && dependency->MightDependOnChange(
|
if (node->IsElement() && dependency->MightDependOnChange(
|
||||||
node->AsElement(), aNode, aStateMask, aAttribute, aNewClasses,
|
node->AsElement(), aNode, aStateMask, aAttribute, aNewClasses,
|
||||||
aCompareClasses, false)) {
|
aCompareClasses, false, false, aIsTextChange)) {
|
||||||
restyleAnchor(node->AsElement(), dependency);
|
restyleAnchor(node->AsElement(), dependency);
|
||||||
}
|
}
|
||||||
if (!dependency->MightAffectSiblingAnchor(
|
if (!dependency->MightAffectSiblingAnchor(
|
||||||
node, aNode, aStateMask, aAttribute, aNewClasses, aCompareClasses)) {
|
node, aNode, aStateMask, aAttribute, aNewClasses, aCompareClasses,
|
||||||
|
aIsTextChange)) {
|
||||||
|
mutationChild = node;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// The parent is an index of sibling anchors, not itself an anchor. Keep
|
// The parent is an index of sibling anchors, not itself an anchor. Keep
|
||||||
@@ -426,7 +434,7 @@ RestyleManager::RestyleForHasPseudoClassChange(nsINode* aNode,
|
|||||||
bool nodeIsFollowingSibling = true;
|
bool nodeIsFollowingSibling = true;
|
||||||
for (nsIContent* child = node->GetFirstChild(); child;
|
for (nsIContent* child = node->GetFirstChild(); child;
|
||||||
child = child->GetNextSibling()) {
|
child = child->GetNextSibling()) {
|
||||||
if (child == aNode) {
|
if (child == mutationChild) {
|
||||||
nodeIsFollowingSibling = false;
|
nodeIsFollowingSibling = false;
|
||||||
}
|
}
|
||||||
if (!child->IsElement()) {
|
if (!child->IsElement()) {
|
||||||
@@ -436,10 +444,12 @@ RestyleManager::RestyleForHasPseudoClassChange(nsINode* aNode,
|
|||||||
child->GetProperty(nsGkAtoms::hasSelectorDependency));
|
child->GetProperty(nsGkAtoms::hasSelectorDependency));
|
||||||
if (siblingDependency && siblingDependency->MightDependOnChange(
|
if (siblingDependency && siblingDependency->MightDependOnChange(
|
||||||
child->AsElement(), aNode, aStateMask, aAttribute, aNewClasses,
|
child->AsElement(), aNode, aStateMask, aAttribute, aNewClasses,
|
||||||
aCompareClasses, true, nodeIsFollowingSibling)) {
|
aCompareClasses, true, nodeIsFollowingSibling, aIsTextChange)) {
|
||||||
restyleAnchor(child->AsElement(), siblingDependency);
|
restyleAnchor(child->AsElement(), siblingDependency);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutationChild = node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,7 +479,16 @@ RestyleManager::RestyleForAppend(nsIContent* aContainer,
|
|||||||
nsIContent* aFirstNewContent)
|
nsIContent* aFirstNewContent)
|
||||||
{
|
{
|
||||||
// The container cannot be a document, but might be a ShadowRoot.
|
// 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()) {
|
if (!aContainer->IsElement()) {
|
||||||
return;
|
return;
|
||||||
@@ -560,7 +579,10 @@ RestyleManager::RestyleForInsertOrChange(nsINode* aContainer,
|
|||||||
nsIContent* aChild)
|
nsIContent* aChild)
|
||||||
{
|
{
|
||||||
// The container might be a document or a ShadowRoot.
|
// 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()) {
|
if (!aContainer->IsElement()) {
|
||||||
return;
|
return;
|
||||||
@@ -652,7 +674,10 @@ RestyleManager::ContentRemoved(nsINode* aContainer,
|
|||||||
nsIContent* aFollowingSibling)
|
nsIContent* aFollowingSibling)
|
||||||
{
|
{
|
||||||
// The container might be a document or a ShadowRoot.
|
// 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()) {
|
if (!aContainer->IsElement()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -321,7 +321,8 @@ public:
|
|||||||
EventStates aStateMask = EventStates(),
|
EventStates aStateMask = EventStates(),
|
||||||
nsIAtom* aAttribute = nullptr,
|
nsIAtom* aAttribute = nullptr,
|
||||||
const nsAttrValue* aNewClasses = nullptr,
|
const nsAttrValue* aNewClasses = nullptr,
|
||||||
bool aCompareClasses = false);
|
bool aCompareClasses = false,
|
||||||
|
bool aIsTextChange = false);
|
||||||
|
|
||||||
// Process any pending restyles. This should be called after
|
// Process any pending restyles. This should be called after
|
||||||
// CreateNeededFrames.
|
// CreateNeededFrames.
|
||||||
|
|||||||
@@ -817,7 +817,14 @@ private:
|
|||||||
mUndisplayedItems[0].mStyleContext->PresContext()->FrameManager();
|
mUndisplayedItems[0].mStyleContext->PresContext()->FrameManager();
|
||||||
for (uint32_t i = 0; i < mUndisplayedItems.Length(); ++i) {
|
for (uint32_t i = 0; i < mUndisplayedItems.Length(); ++i) {
|
||||||
UndisplayedItem& item = mUndisplayedItems[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;
|
static int i = 0;
|
||||||
printf("ClearUndisplayedContent(%d): content=%p parent=%p --> ", i++, (void *)aContent, (void*)aParentContent);
|
printf("ClearUndisplayedContent(%d): content=%p parent=%p --> ", i++, (void *)aContent, (void*)aParentContent);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!mUndisplayedMap) {
|
if (!mUndisplayedMap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is an entry point into UndisplayedMap handling code, so we
|
// GetApplicableParent only normalizes <xbl:children>. The lookup in
|
||||||
// must call GetApplicableParent so the parent we pass around is correct.
|
// GetStyleContextInMap goes through ParentForUndisplayedMap, which crosses
|
||||||
aParentContent = UndisplayedMap::GetApplicableParent(aParentContent);
|
// 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()) {
|
!aParentContent->MayHaveChildrenWithLayoutBoxesDisabled()) {
|
||||||
MOZ_ASSERT(!mUndisplayedMap->GetFirstNode(aParentContent),
|
MOZ_ASSERT(!mUndisplayedMap->GetFirstNode(aParentContent),
|
||||||
"MayHaveChildrenWithLayoutBoxesDisabled bit out of sync - "
|
"MayHaveChildrenWithLayoutBoxesDisabled bit out of sync - "
|
||||||
"may fail to removenode from mUndisplayedMap");
|
"may fail to remove node from mUndisplayedMap");
|
||||||
return;
|
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);
|
UndisplayedNode* node = mUndisplayedMap->GetFirstNode(aParentContent);
|
||||||
|
while (node) {
|
||||||
const bool haveOneDisplayNoneChild = node && !node->getNext();
|
UndisplayedNode* next = node->getNext();
|
||||||
|
|
||||||
for (; node; node = node->getNext()) {
|
|
||||||
if (node->mContent == aContent) {
|
if (node->mContent == aContent) {
|
||||||
mUndisplayedMap->RemoveNodeFor(aParentContent, node);
|
mUndisplayedMap->RemoveNodeFor(aParentContent, node);
|
||||||
|
removed = true;
|
||||||
#ifdef DEBUG_UNDISPLAYED_MAP
|
} else {
|
||||||
printf( "REMOVED!\n");
|
haveOtherUndisplayedChild = true;
|
||||||
#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;
|
|
||||||
}
|
}
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!removed) {
|
||||||
|
#ifdef DEBUG_UNDISPLAYED_MAP
|
||||||
|
printf("not found.\n");
|
||||||
|
#endif
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef DEBUG_UNDISPLAYED_MAP
|
#ifdef DEBUG_UNDISPLAYED_MAP
|
||||||
printf( "not found.\n");
|
printf("REMOVED!\n");
|
||||||
#endif
|
#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
|
void
|
||||||
|
|||||||
@@ -1713,35 +1713,41 @@ nsBlockFrame::ComputeFinalSize(const ReflowInput& aReflowInput,
|
|||||||
static void
|
static void
|
||||||
ConsiderBlockEndEdgeOfChildren(const WritingMode aWritingMode,
|
ConsiderBlockEndEdgeOfChildren(const WritingMode aWritingMode,
|
||||||
nscoord aBEndEdgeOfChildren,
|
nscoord aBEndEdgeOfChildren,
|
||||||
|
const nscoord aBEndPadding,
|
||||||
nsOverflowAreas& aOverflowAreas)
|
nsOverflowAreas& aOverflowAreas)
|
||||||
{
|
{
|
||||||
// Factor in the block-end edge of the children. Child frames will be added
|
// 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
|
// 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.
|
// 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,
|
// REVIEW: For now, we do this for both visual and scrollable area,
|
||||||
// although when we make scrollable overflow area not be a subset of
|
// although when we make scrollable overflow area not be a subset of
|
||||||
// visual, we can change this.
|
// visual, we can change this.
|
||||||
// XXX Currently, overflow areas are stored as physical rects, so we have
|
// XXX Currently, overflow areas are stored as physical rects, so we have
|
||||||
// to handle writing modes explicitly here. If we change overflow rects
|
// to handle writing modes explicitly here. If we change overflow rects
|
||||||
// to be stored logically, this can be simplified again.
|
// to be stored logically, this can be simplified again.
|
||||||
|
const nscoord bEndEdge = aBEndEdgeOfChildren + aBEndPadding;
|
||||||
if (aWritingMode.IsVertical()) {
|
if (aWritingMode.IsVertical()) {
|
||||||
if (aWritingMode.IsVerticalLR()) {
|
if (aWritingMode.IsVerticalLR()) {
|
||||||
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
|
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
|
||||||
nsRect& o = aOverflowAreas.Overflow(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 {
|
} else {
|
||||||
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
|
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
|
||||||
nsRect& o = aOverflowAreas.Overflow(otype);
|
nsRect& o = aOverflowAreas.Overflow(otype);
|
||||||
nscoord xmost = o.XMost();
|
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;
|
o.width = xmost - o.x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
|
NS_FOR_FRAME_OVERFLOW_TYPES(otype) {
|
||||||
nsRect& o = aOverflowAreas.Overflow(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());
|
areas.UnionAllWith(outsideBullet->GetRect());
|
||||||
}
|
}
|
||||||
|
|
||||||
ConsiderBlockEndEdgeOfChildren(GetWritingMode(),
|
const WritingMode wm = GetWritingMode();
|
||||||
aBEndEdgeOfChildren, areas);
|
ConsiderBlockEndEdgeOfChildren(
|
||||||
|
wm, aBEndEdgeOfChildren,
|
||||||
|
LogicalMargin(wm, GetUsedPadding()).BEnd(wm), areas);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef NOISY_COMBINED_AREA
|
#ifdef NOISY_COMBINED_AREA
|
||||||
@@ -1831,8 +1839,10 @@ nsBlockFrame::ComputeCustomOverflow(nsOverflowAreas& aOverflowAreas)
|
|||||||
nscoord blockEndEdgeOfChildren =
|
nscoord blockEndEdgeOfChildren =
|
||||||
GetProperty(BlockEndEdgeOfChildrenProperty(), &found);
|
GetProperty(BlockEndEdgeOfChildrenProperty(), &found);
|
||||||
if (found) {
|
if (found) {
|
||||||
ConsiderBlockEndEdgeOfChildren(GetWritingMode(),
|
const WritingMode wm = GetWritingMode();
|
||||||
blockEndEdgeOfChildren, aOverflowAreas);
|
ConsiderBlockEndEdgeOfChildren(
|
||||||
|
wm, blockEndEdgeOfChildren,
|
||||||
|
LogicalMargin(wm, GetUsedPadding()).BEnd(wm), aOverflowAreas);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Line cursor invariants depend on the overflow areas of the lines, so
|
// Line cursor invariants depend on the overflow areas of the lines, so
|
||||||
|
|||||||
+198
-11
@@ -5082,8 +5082,11 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
|
|||||||
nscoord boxSizingToMarginEdgeISize =
|
nscoord boxSizingToMarginEdgeISize =
|
||||||
aMargin.ISize(aWM) + aBorder.ISize(aWM) + aPadding.ISize(aWM) -
|
aMargin.ISize(aWM) + aBorder.ISize(aWM) + aPadding.ISize(aWM) -
|
||||||
boxSizingAdjust.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 {
|
enum class Stretch {
|
||||||
// stretch to fill the CB (preserving intrinsic ratio) in the relevant axis
|
// stretch to fill the CB (preserving intrinsic ratio) in the relevant axis
|
||||||
eStretchPreservingRatio,
|
eStretchPreservingRatio,
|
||||||
@@ -5260,6 +5263,98 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
|
|||||||
|
|
||||||
// Now calculate the used values for iSize and bSize:
|
// 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 (isAutoISize) {
|
||||||
if (isAutoBSize) {
|
if (isAutoBSize) {
|
||||||
|
|
||||||
@@ -5267,6 +5362,11 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
|
|||||||
|
|
||||||
// Get tentative values - CSS 2.1 sections 10.3.2 and 10.6.2:
|
// 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;
|
nscoord tentISize, tentBSize;
|
||||||
|
|
||||||
if (hasIntrinsicISize) {
|
if (hasIntrinsicISize) {
|
||||||
@@ -5274,7 +5374,18 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
|
|||||||
} else if (hasIntrinsicBSize && aIntrinsicRatio) {
|
} else if (hasIntrinsicBSize && aIntrinsicRatio) {
|
||||||
tentISize = iSizeFromBSize(intrinsicBSize);
|
tentISize = iSizeFromBSize(intrinsicBSize);
|
||||||
} else if (aIntrinsicRatio) {
|
} 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;
|
if (tentISize < 0) tentISize = 0;
|
||||||
} else {
|
} else {
|
||||||
tentISize = nsPresContext::CSSPixelsToAppUnits(300);
|
tentISize = nsPresContext::CSSPixelsToAppUnits(300);
|
||||||
@@ -5290,8 +5401,53 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
|
|||||||
|
|
||||||
if (hasIntrinsicBSize) {
|
if (hasIntrinsicBSize) {
|
||||||
tentBSize = intrinsicBSize;
|
tentBSize = intrinsicBSize;
|
||||||
} else if (aIntrinsicRatio) {
|
} else if (aIntrinsicRatio && !shrinkWrapBlock) {
|
||||||
tentBSize = bSizeFromISize(tentISize);
|
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 {
|
} else {
|
||||||
tentBSize = nsPresContext::CSSPixelsToAppUnits(150);
|
tentBSize = nsPresContext::CSSPixelsToAppUnits(150);
|
||||||
}
|
}
|
||||||
@@ -5334,7 +5490,7 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
|
|||||||
// ComputeAutoSizeWithIntrinsicDimensions preserves the ratio when applying
|
// ComputeAutoSizeWithIntrinsicDimensions preserves the ratio when applying
|
||||||
// the min/max-size. We don't want that when we have 'stretch' in either
|
// 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.
|
// axis because tentISize/tentBSize is likely not according to ratio now.
|
||||||
if (aIntrinsicRatio && stretchI != eStretch && stretchB != eStretch) {
|
if (aIntrinsicRatio && (!shrinkWrapBlock || ratioTransferred)) {
|
||||||
nsSize autoSize = nsLayoutUtils::
|
nsSize autoSize = nsLayoutUtils::
|
||||||
ComputeAutoSizeWithIntrinsicDimensions(minISize, minBSize,
|
ComputeAutoSizeWithIntrinsicDimensions(minISize, minBSize,
|
||||||
maxISize, maxBSize,
|
maxISize, maxBSize,
|
||||||
@@ -5375,8 +5531,18 @@ nsFrame::ComputeSizeWithIntrinsicDimensions(nsRenderingContext* aRenderingConte
|
|||||||
// non-'auto' iSize, 'auto' bSize
|
// non-'auto' iSize, 'auto' bSize
|
||||||
iSize = NS_CSS_MINMAX(iSize, minISize, maxISize);
|
iSize = NS_CSS_MINMAX(iSize, minISize, maxISize);
|
||||||
if (stretchB != eStretch) {
|
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);
|
bSize = bSizeFromISize(iSize);
|
||||||
|
} else if (contentMainSize) {
|
||||||
|
bSize =
|
||||||
|
ComputeAutoSize(aRenderingContext, aWM, aCBSize, aCBSize.ISize(aWM),
|
||||||
|
aMargin, aBorder, aPadding, aFlags).BSize(aWM);
|
||||||
} else if (hasIntrinsicBSize) {
|
} else if (hasIntrinsicBSize) {
|
||||||
if (!((aFlags & ComputeSizeFlags::eBClampMarginBoxMinSize) &&
|
if (!((aFlags & ComputeSizeFlags::eBClampMarginBoxMinSize) &&
|
||||||
intrinsicBSize > bSize)) {
|
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);
|
return LogicalSize(aWM, iSize, bSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8527,7 +8701,9 @@ UnionBorderBoxes(nsIFrame* aFrame, bool aApplyTransform,
|
|||||||
}
|
}
|
||||||
const nsStyleDisplay* disp = aFrame->StyleDisplay();
|
const nsStyleDisplay* disp = aFrame->StyleDisplay();
|
||||||
nsIAtom* fType = aFrame->GetType();
|
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::scrollFrame ||
|
||||||
fType == nsGkAtoms::listControlFrame ||
|
fType == nsGkAtoms::listControlFrame ||
|
||||||
fType == nsGkAtoms::svgOuterSVGFrame) {
|
fType == nsGkAtoms::svgOuterSVGFrame) {
|
||||||
@@ -8756,12 +8932,23 @@ nsIFrame::FinishAndStoreOverflow(nsOverflowAreas& aOverflowAreas,
|
|||||||
// overflow area should include the entire border-box, just set it to
|
// overflow area should include the entire border-box, just set it to
|
||||||
// the border-box here.
|
// the border-box here.
|
||||||
const nsStyleDisplay* disp = StyleDisplay();
|
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)) {
|
if (nsFrame::ShouldApplyOverflowClipping(this, disp)) {
|
||||||
// The contents are actually clipped to the padding area
|
// The contents are actually clipped to the padding area. Clipping is
|
||||||
aOverflowAreas.SetAllTo(bounds);
|
// 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,
|
// 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;
|
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
|
struct MediaQueryTypedCalcLengthResult
|
||||||
{
|
{
|
||||||
double mValue;
|
double mValue;
|
||||||
@@ -348,31 +372,19 @@ nsMediaExpression::Matches(nsPresContext *aPresContext,
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case nsMediaFeature::eIntRatio:
|
case nsMediaFeature::eIntRatio:
|
||||||
|
case nsMediaFeature::eFloatRatio:
|
||||||
{
|
{
|
||||||
NS_ASSERTION(actual.GetUnit() == eCSSUnit_Array &&
|
NS_ASSERTION(IsRatioValue(actual), "bad actual value");
|
||||||
actual.GetArrayValue()->Count() == 2 &&
|
NS_ASSERTION(IsRatioValue(required), "bad required value");
|
||||||
actual.GetArrayValue()->Item(0).GetUnit() ==
|
// Convert to double so we can multiply without worry. Note
|
||||||
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
|
|
||||||
// that while the spec requires that both halves of |required|
|
// that while the spec requires that both halves of |required|
|
||||||
// be positive, the numerator or denominator of |actual| might
|
// be positive, the numerator or denominator of |actual| might
|
||||||
// be zero (e.g., when testing 'aspect-ratio' on a 0-width or
|
// be zero (e.g., when testing 'aspect-ratio' on a 0-width or
|
||||||
// 0-height iframe).
|
// 0-height iframe).
|
||||||
int64_t actualNum = actual.GetArrayValue()->Item(0).GetIntValue(),
|
cmp = DoCompare(RatioComponentValue(actual, 0) *
|
||||||
actualDen = actual.GetArrayValue()->Item(1).GetIntValue(),
|
RatioComponentValue(required, 1),
|
||||||
requiredNum = required.GetArrayValue()->Item(0).GetIntValue(),
|
RatioComponentValue(required, 0) *
|
||||||
requiredDen = required.GetArrayValue()->Item(1).GetIntValue();
|
RatioComponentValue(actual, 1));
|
||||||
cmp = DoCompare(actualNum * requiredDen, requiredNum * actualDen);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case nsMediaFeature::eResolution:
|
case nsMediaFeature::eResolution:
|
||||||
@@ -680,20 +692,18 @@ nsMediaQuery::AppendToString(nsAString& aString) const
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case nsMediaFeature::eIntRatio:
|
case nsMediaFeature::eIntRatio:
|
||||||
|
case nsMediaFeature::eFloatRatio:
|
||||||
{
|
{
|
||||||
NS_ASSERTION(expr.mValue.GetUnit() == eCSSUnit_Array,
|
NS_ASSERTION(IsRatioValue(expr.mValue), "bad unit");
|
||||||
"bad unit");
|
nsCSSValue::Array* array = expr.mValue.GetArrayValue();
|
||||||
nsCSSValue::Array *array = expr.mValue.GetArrayValue();
|
|
||||||
NS_ASSERTION(array->Count() == 2, "unexpected length");
|
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,
|
array->Item(0).AppendToString(eCSSProperty_z_index, aString,
|
||||||
nsCSSValue::eNormalized);
|
nsCSSValue::eNormalized);
|
||||||
aString.Append('/');
|
if (RatioComponentValue(expr.mValue, 1) != 1.0) {
|
||||||
array->Item(1).AppendToString(eCSSProperty_z_index, aString,
|
aString.Append('/');
|
||||||
nsCSSValue::eNormalized);
|
array->Item(1).AppendToString(eCSSProperty_z_index, aString,
|
||||||
|
nsCSSValue::eNormalized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case nsMediaFeature::eResolution:
|
case nsMediaFeature::eResolution:
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ StyleSheet::AddAdopter(nsIDocument* aDocument)
|
|||||||
{
|
{
|
||||||
MOZ_ASSERT(IsConstructed());
|
MOZ_ASSERT(IsConstructed());
|
||||||
MOZ_ASSERT(aDocument);
|
MOZ_ASSERT(aDocument);
|
||||||
MOZ_ASSERT(ConstructorDocument() == aDocument);
|
MOZ_ASSERT_IF(ConstructorDocument(), ConstructorDocument() == aDocument);
|
||||||
|
|
||||||
if (mAdoptionCount++ == 0) {
|
if (mAdoptionCount++ == 0) {
|
||||||
SetAssociatedDocument(aDocument, NotOwnedByDocument);
|
SetAssociatedDocument(aDocument, NotOwnedByDocument);
|
||||||
@@ -292,7 +292,7 @@ StyleSheet::RemoveAdopter(nsIDocument* aDocument)
|
|||||||
{
|
{
|
||||||
MOZ_ASSERT(IsConstructed());
|
MOZ_ASSERT(IsConstructed());
|
||||||
MOZ_ASSERT(aDocument);
|
MOZ_ASSERT(aDocument);
|
||||||
MOZ_ASSERT(ConstructorDocument() == aDocument);
|
MOZ_ASSERT_IF(ConstructorDocument(), ConstructorDocument() == aDocument);
|
||||||
MOZ_ASSERT(mAdoptionCount > 0);
|
MOZ_ASSERT(mAdoptionCount > 0);
|
||||||
|
|
||||||
if (mAdoptionCount == 0) {
|
if (mAdoptionCount == 0) {
|
||||||
|
|||||||
+217
-31
@@ -74,6 +74,7 @@ static bool sMozGradientsEnabled;
|
|||||||
static bool sControlCharVisibility;
|
static bool sControlCharVisibility;
|
||||||
static bool sLegacyNegationPseudoClassEnabled;
|
static bool sLegacyNegationPseudoClassEnabled;
|
||||||
static bool sCascadeLayersEnabled;
|
static bool sCascadeLayersEnabled;
|
||||||
|
static bool sContainerQueriesEnabled;
|
||||||
static bool sNestingEnabled;
|
static bool sNestingEnabled;
|
||||||
|
|
||||||
const uint32_t
|
const uint32_t
|
||||||
@@ -1037,6 +1038,7 @@ protected:
|
|||||||
bool ParseMediaQuery(eMediaQueryType aMode, nsMediaQuery **aQuery,
|
bool ParseMediaQuery(eMediaQueryType aMode, nsMediaQuery **aQuery,
|
||||||
bool *aHitStop);
|
bool *aHitStop);
|
||||||
bool ParseMediaQueryExpression(nsMediaQuery* aQuery);
|
bool ParseMediaQueryExpression(nsMediaQuery* aQuery);
|
||||||
|
bool ParseRatioComponent(nsCSSValue& aValue);
|
||||||
void ProcessImport(const nsString& aURLSpec,
|
void ProcessImport(const nsString& aURLSpec,
|
||||||
nsMediaList* aMedia,
|
nsMediaList* aMedia,
|
||||||
const nsString& aLayerName,
|
const nsString& aLayerName,
|
||||||
@@ -1047,6 +1049,7 @@ protected:
|
|||||||
bool ParseGroupRule(css::GroupRule* aRule, RuleAppendFunc aAppendFunc,
|
bool ParseGroupRule(css::GroupRule* aRule, RuleAppendFunc aAppendFunc,
|
||||||
void* aProcessData);
|
void* aProcessData);
|
||||||
bool ParseMediaRule(RuleAppendFunc aAppendFunc, void* aProcessData);
|
bool ParseMediaRule(RuleAppendFunc aAppendFunc, void* aProcessData);
|
||||||
|
bool ParseContainerRule(RuleAppendFunc aAppendFunc, void* aProcessData);
|
||||||
bool ParseMozDocumentRule(RuleAppendFunc aAppendFunc, void* aProcessData);
|
bool ParseMozDocumentRule(RuleAppendFunc aAppendFunc, void* aProcessData);
|
||||||
bool ParseNameSpaceRule(RuleAppendFunc aAppendFunc, void* aProcessData);
|
bool ParseNameSpaceRule(RuleAppendFunc aAppendFunc, void* aProcessData);
|
||||||
void ProcessNameSpace(const nsString& aPrefix,
|
void ProcessNameSpace(const nsString& aPrefix,
|
||||||
@@ -3846,6 +3849,11 @@ CSSParserImpl::ParseAtRule(RuleAppendFunc aAppendFunc,
|
|||||||
parseFunc = &CSSParserImpl::ParseSupportsRule;
|
parseFunc = &CSSParserImpl::ParseSupportsRule;
|
||||||
newSection = eCSSSection_General;
|
newSection = eCSSSection_General;
|
||||||
|
|
||||||
|
} else if (mToken.mIdent.LowerCaseEqualsLiteral("container") &&
|
||||||
|
sContainerQueriesEnabled) {
|
||||||
|
parseFunc = &CSSParserImpl::ParseContainerRule;
|
||||||
|
newSection = eCSSSection_General;
|
||||||
|
|
||||||
} else if (mToken.mIdent.LowerCaseEqualsLiteral("layer") &&
|
} else if (mToken.mIdent.LowerCaseEqualsLiteral("layer") &&
|
||||||
sCascadeLayersEnabled) {
|
sCascadeLayersEnabled) {
|
||||||
parseFunc = &CSSParserImpl::ParseLayerRule;
|
parseFunc = &CSSParserImpl::ParseLayerRule;
|
||||||
@@ -4100,6 +4108,30 @@ CSSParserImpl::GatherMedia(nsMediaList* aMedia,
|
|||||||
return true;
|
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
|
bool
|
||||||
CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery)
|
CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery)
|
||||||
{
|
{
|
||||||
@@ -4233,15 +4265,33 @@ CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery)
|
|||||||
break;
|
break;
|
||||||
case nsMediaFeature::eInteger:
|
case nsMediaFeature::eInteger:
|
||||||
case nsMediaFeature::eBoolInteger:
|
case nsMediaFeature::eBoolInteger:
|
||||||
rv = ParseNonNegativeInteger(expr->mValue);
|
{
|
||||||
// Enforce extra restrictions for eBoolInteger
|
CSSParseResult result =
|
||||||
if (rv &&
|
ParseNonNegativeVariant(expr->mValue, VARIANT_INTEGER, nullptr);
|
||||||
feature->mValueType == nsMediaFeature::eBoolInteger &&
|
if (result == CSSParseResult::Error) {
|
||||||
expr->mValue.GetIntValue() > 1)
|
// The failing calc() parse already consumed the closing paren, so the
|
||||||
rv = false;
|
// 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;
|
break;
|
||||||
case nsMediaFeature::eFloat:
|
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;
|
break;
|
||||||
case nsMediaFeature::eIntRatio:
|
case nsMediaFeature::eIntRatio:
|
||||||
{
|
{
|
||||||
@@ -4259,6 +4309,22 @@ CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery)
|
|||||||
a->Item(1).GetIntValue() > 0;
|
a->Item(1).GetIntValue() > 0;
|
||||||
}
|
}
|
||||||
break;
|
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:
|
case nsMediaFeature::eResolution:
|
||||||
rv = GetToken(true);
|
rv = GetToken(true);
|
||||||
if (!rv)
|
if (!rv)
|
||||||
@@ -4431,6 +4497,56 @@ CSSParserImpl::ParseMediaRule(RuleAppendFunc aAppendFunc, void* aData)
|
|||||||
return false;
|
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
|
// 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
|
// of a medium it has a nonempty list of items where each item is either
|
||||||
// url(), url-prefix(), or domain().
|
// url(), url-prefix(), or domain().
|
||||||
@@ -4856,12 +4972,23 @@ CSSParserImpl::ParseFontFeatureValueSet(nsCSSFontFeatureValuesRule
|
|||||||
AutoTArray<uint32_t,4> featureSelectors;
|
AutoTArray<uint32_t,4> featureSelectors;
|
||||||
|
|
||||||
nsCSSValue intValue;
|
nsCSSValue intValue;
|
||||||
while (ParseNonNegativeInteger(intValue)) {
|
CSSParseResult valueResult;
|
||||||
|
while ((valueResult = ParseNonNegativeVariant(intValue, VARIANT_INTEGER, nullptr)) ==
|
||||||
|
CSSParseResult::Ok) {
|
||||||
featureSelectors.AppendElement(uint32_t(intValue.GetIntValue()));
|
featureSelectors.AppendElement(uint32_t(intValue.GetIntValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t numValues = featureSelectors.Length();
|
int32_t numValues = featureSelectors.Length();
|
||||||
|
|
||||||
|
if (valueResult == CSSParseResult::Error) {
|
||||||
|
REPORT_UNEXPECTED_TOKEN(PEFFVExpectedValue);
|
||||||
|
OUTPUT_ERROR();
|
||||||
|
if (!SkipDeclaration(true)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (numValues == 0) {
|
if (numValues == 0) {
|
||||||
REPORT_UNEXPECTED_TOKEN(PEFFVExpectedValue);
|
REPORT_UNEXPECTED_TOKEN(PEFFVExpectedValue);
|
||||||
OUTPUT_ERROR();
|
OUTPUT_ERROR();
|
||||||
@@ -5986,9 +6113,15 @@ CSSParserImpl::ParseCounterDescriptorValue(nsCSSCounterDesc aDescID,
|
|||||||
|
|
||||||
case eCSSCounterDesc_Pad: {
|
case eCSSCounterDesc_Pad: {
|
||||||
nsCSSValue width, symbol;
|
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) ||
|
if (!ParseSingleTokenVariant(symbol, VARIANT_COUNTER_SYMBOL, nullptr) ||
|
||||||
(!hasWidth && !ParseNonNegativeInteger(width))) {
|
(!hasWidth && ParseNonNegativeVariant(width, VARIANT_INTEGER, nullptr) !=
|
||||||
|
CSSParseResult::Ok)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
aValue.SetPairValue(width, symbol);
|
aValue.SetPairValue(width, symbol);
|
||||||
@@ -6021,9 +6154,15 @@ CSSParserImpl::ParseCounterDescriptorValue(nsCSSCounterDesc aDescID,
|
|||||||
int32_t lastWeight = -1;
|
int32_t lastWeight = -1;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
nsCSSValue weight, symbol;
|
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) ||
|
if (!ParseSingleTokenVariant(symbol, VARIANT_COUNTER_SYMBOL, nullptr) ||
|
||||||
(!hasWeight && !ParseNonNegativeInteger(weight))) {
|
(!hasWeight && ParseNonNegativeVariant(weight, VARIANT_INTEGER, nullptr) !=
|
||||||
|
CSSParseResult::Ok)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (lastWeight != -1 && weight.GetIntValue() >= lastWeight) {
|
if (lastWeight != -1 && weight.GetIntValue() >= lastWeight) {
|
||||||
@@ -8240,12 +8379,18 @@ CSSParserImpl::ParseHue(float& aAngle)
|
|||||||
}
|
}
|
||||||
UngetToken();
|
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;
|
nsCSSValue angleValue;
|
||||||
// The '0' value is handled by <number> parsing, so use VARIANT_ANGLE flag
|
CSSParseResult result =
|
||||||
// instead of VARIANT_ANGLE_OR_ZERO.
|
ParseVariant(angleValue, VARIANT_ANGLE | VARIANT_NUMBER, nullptr);
|
||||||
if (ParseSingleTokenVariant(angleValue, VARIANT_ANGLE, nullptr)) {
|
if (result == CSSParseResult::Ok) {
|
||||||
aAngle = angleValue.GetAngleValueInDegrees();
|
if (angleValue.GetUnit() == eCSSUnit_Number) {
|
||||||
|
aAngle = angleValue.GetFloatValue();
|
||||||
|
} else {
|
||||||
|
aAngle = angleValue.GetAngleValueInDegrees();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9732,13 +9877,13 @@ CSSParserImpl::ParseImageRect(nsCSSValue& aImage)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const int32_t VARIANT_SIDE = VARIANT_NUMBER | VARIANT_PERCENT;
|
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) ||
|
!ExpectSymbol(',', true) ||
|
||||||
!ParseSingleTokenNonNegativeVariant(right, VARIANT_SIDE, nullptr) ||
|
ParseNonNegativeVariant(right, VARIANT_SIDE, nullptr) != CSSParseResult::Ok ||
|
||||||
!ExpectSymbol(',', true) ||
|
!ExpectSymbol(',', true) ||
|
||||||
!ParseSingleTokenNonNegativeVariant(bottom, VARIANT_SIDE, nullptr) ||
|
ParseNonNegativeVariant(bottom, VARIANT_SIDE, nullptr) != CSSParseResult::Ok ||
|
||||||
!ExpectSymbol(',', true) ||
|
!ExpectSymbol(',', true) ||
|
||||||
!ParseSingleTokenNonNegativeVariant(left, VARIANT_SIDE, nullptr) ||
|
ParseNonNegativeVariant(left, VARIANT_SIDE, nullptr) != CSSParseResult::Ok ||
|
||||||
!ExpectSymbol(')', true))
|
!ExpectSymbol(')', true))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -9842,12 +9987,30 @@ CSSParserImpl::ParseFlex()
|
|||||||
|
|
||||||
// Record what we just parsed as either flex-basis or flex-grow:
|
// Record what we just parsed as either flex-basis or flex-grow:
|
||||||
bool wasFirstComponentFlexBasis = (tmpVal.GetUnit() != eCSSUnit_Number);
|
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;
|
(wasFirstComponentFlexBasis ? flexBasis : flexGrow) = tmpVal;
|
||||||
|
|
||||||
// (b) If we didn't get flex-grow yet, parse _next_ component as flex-grow.
|
// (b) If we didn't get flex-grow yet, parse _next_ component as flex-grow.
|
||||||
bool doneParsing = false;
|
bool doneParsing = false;
|
||||||
if (wasFirstComponentFlexBasis) {
|
if (wasFirstComponentFlexBasis) {
|
||||||
if (ParseNonNegativeNumber(tmpVal)) {
|
if (ParseNonNegativeVariant(tmpVal, VARIANT_NUMBER, nullptr) ==
|
||||||
|
CSSParseResult::Ok) {
|
||||||
flexGrow = tmpVal;
|
flexGrow = tmpVal;
|
||||||
} else {
|
} else {
|
||||||
// Failed to parse anything after our flex-basis -- that's fine. We can
|
// Failed to parse anything after our flex-basis -- that's fine. We can
|
||||||
@@ -9859,8 +10022,23 @@ CSSParserImpl::ParseFlex()
|
|||||||
if (!doneParsing) {
|
if (!doneParsing) {
|
||||||
// (c) OK -- the last thing we parsed was flex-grow, so look for a
|
// (c) OK -- the last thing we parsed was flex-grow, so look for a
|
||||||
// flex-shrink in the next position.
|
// 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;
|
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
|
// d) Finally: If we didn't get flex-basis at the beginning, try to parse
|
||||||
@@ -9885,10 +10063,10 @@ CSSParserImpl::ParseFlex()
|
|||||||
CSSParseResult result =
|
CSSParseResult result =
|
||||||
ParseNonNegativeVariant(tmpVal, flexBasisVariantMask,
|
ParseNonNegativeVariant(tmpVal, flexBasisVariantMask,
|
||||||
nsCSSProps::kFlexBasisKTable);
|
nsCSSProps::kFlexBasisKTable);
|
||||||
if (result == CSSParseResult::Error) {
|
if (result == CSSParseResult::Error) {
|
||||||
return false;
|
return false;
|
||||||
} else if (result == CSSParseResult::Ok) {
|
} else if (result == CSSParseResult::Ok) {
|
||||||
flexBasis = tmpVal;
|
flexBasis = tmpVal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11310,11 +11488,17 @@ CSSParserImpl::ParseInitialLetter()
|
|||||||
if (!ParseSingleTokenVariant(value, VARIANT_INHERIT | VARIANT_NORMAL,
|
if (!ParseSingleTokenVariant(value, VARIANT_INHERIT | VARIANT_NORMAL,
|
||||||
nullptr)) {
|
nullptr)) {
|
||||||
nsCSSValue first, second;
|
nsCSSValue first, second;
|
||||||
if (!ParseOneOrLargerNumber(first)) {
|
if (ParseOneOrLargerVariant(first, VARIANT_NUMBER, nullptr) !=
|
||||||
|
CSSParseResult::Ok) {
|
||||||
return false;
|
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);
|
AppendValue(eCSSProperty_initial_letter, first);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -18725,8 +18909,8 @@ CSSParserImpl::ParseTransitionStepTimingFunctionValues(nsCSSValue& aValue)
|
|||||||
|
|
||||||
RefPtr<nsCSSValue::Array> val = nsCSSValue::Array::Create(2);
|
RefPtr<nsCSSValue::Array> val = nsCSSValue::Array::Create(2);
|
||||||
|
|
||||||
if (!ParseSingleTokenOneOrLargerVariant(val->Item(0), VARIANT_INTEGER,
|
if (ParseOneOrLargerVariant(val->Item(0), VARIANT_INTEGER, nullptr) !=
|
||||||
nullptr)) {
|
CSSParseResult::Ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19797,6 +19981,8 @@ nsCSSParser::Startup()
|
|||||||
"layout.css.legacy-negation-pseudo.enabled");
|
"layout.css.legacy-negation-pseudo.enabled");
|
||||||
Preferences::AddBoolVarCache(&sCascadeLayersEnabled,
|
Preferences::AddBoolVarCache(&sCascadeLayersEnabled,
|
||||||
"layout.css.cascade-layers.enabled");
|
"layout.css.cascade-layers.enabled");
|
||||||
|
Preferences::AddBoolVarCache(&sContainerQueriesEnabled,
|
||||||
|
"layout.css.container-queries.enabled");
|
||||||
Preferences::AddBoolVarCache(&sNestingEnabled,
|
Preferences::AddBoolVarCache(&sNestingEnabled,
|
||||||
"layout.css.nesting.enabled");
|
"layout.css.nesting.enabled");
|
||||||
}
|
}
|
||||||
|
|||||||
+131
-28
@@ -10,6 +10,7 @@
|
|||||||
#include "mozilla/dom/ShadowRoot.h"
|
#include "mozilla/dom/ShadowRoot.h"
|
||||||
#include "nsIMozBrowserFrame.h"
|
#include "nsIMozBrowserFrame.h"
|
||||||
#include "nsGkAtoms.h"
|
#include "nsGkAtoms.h"
|
||||||
|
#include "mozilla/HashFunctions.h"
|
||||||
#include "nsRuleWalker.h"
|
#include "nsRuleWalker.h"
|
||||||
#include "nsStyleUtil.h"
|
#include "nsStyleUtil.h"
|
||||||
#include "StyleRule.h"
|
#include "StyleRule.h"
|
||||||
@@ -1924,11 +1925,17 @@ bool
|
|||||||
HasSelectorDependency::MightDependOnChange(
|
HasSelectorDependency::MightDependOnChange(
|
||||||
Element* aAnchor, nsINode* aNode, EventStates aStateMask,
|
Element* aAnchor, nsINode* aNode, EventStates aStateMask,
|
||||||
nsIAtom* aAttribute, const nsAttrValue* aNewClasses,
|
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();
|
bool contentChange = !aAttribute && aStateMask.IsEmpty();
|
||||||
for (const auto& branch : mBranches) {
|
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() &&
|
(!aStateMask.IsEmpty() &&
|
||||||
!branch.mStates.HasAtLeastOneOfStates(aStateMask)) ||
|
!branch.mStates.HasAtLeastOneOfStates(aStateMask)) ||
|
||||||
(aAttribute && !branch.MightDependOnAttribute(
|
(aAttribute && !branch.MightDependOnAttribute(
|
||||||
@@ -1971,11 +1978,12 @@ bool
|
|||||||
HasSelectorDependency::MightAffectSiblingAnchor(
|
HasSelectorDependency::MightAffectSiblingAnchor(
|
||||||
nsINode* aParent, nsINode* aNode, EventStates aStateMask,
|
nsINode* aParent, nsINode* aNode, EventStates aStateMask,
|
||||||
nsIAtom* aAttribute, const nsAttrValue* aNewClasses,
|
nsIAtom* aAttribute, const nsAttrValue* aNewClasses,
|
||||||
bool aCompareClasses) const
|
bool aCompareClasses, bool aIsTextChange) const
|
||||||
{
|
{
|
||||||
bool contentChange = !aAttribute && aStateMask.IsEmpty();
|
bool contentChange = !aAttribute && aStateMask.IsEmpty();
|
||||||
for (const auto& branch : mSiblingBranches) {
|
for (const auto& branch : mSiblingBranches) {
|
||||||
if (!branch.IsSibling() ||
|
if ((aIsTextChange && branch.mIsLocal) ||
|
||||||
|
!branch.IsSibling() ||
|
||||||
(branch.mIsLocal && (contentChange ? aNode != aParent
|
(branch.mIsLocal && (contentChange ? aNode != aParent
|
||||||
: aNode->GetParentNode() != aParent)) ||
|
: aNode->GetParentNode() != aParent)) ||
|
||||||
(!aStateMask.IsEmpty() &&
|
(!aStateMask.IsEmpty() &&
|
||||||
@@ -2011,35 +2019,41 @@ EnsureHasSelectorDependency(nsINode* aNode)
|
|||||||
return dependency;
|
return dependency;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* static */ bool
|
// For a chain of child combinators, only the final depth can match. Do not
|
||||||
nsCSSRuleUtils::RelativeSelectorListMatches(
|
// descend into deeper subtrees, even when the rightmost compound is absent.
|
||||||
Element* aAnchor,
|
static bool
|
||||||
nsCSSSelectorList* aList,
|
MatchesAtDepth(Element* aAnchor, uint32_t aDepth,
|
||||||
TreeMatchContext& aTreeMatchContext)
|
nsCSSSelectorList* aRelative, TreeMatchContext& aContext)
|
||||||
{
|
{
|
||||||
MOZ_ASSERT(aAnchor);
|
Element* candidate = aAnchor->GetFirstElementChild();
|
||||||
MOZ_ASSERT(aList);
|
uint32_t depth = 1;
|
||||||
|
while (candidate) {
|
||||||
if (aTreeMatchContext.mForStyling) {
|
if (depth == aDepth) {
|
||||||
if (!aList->mHasSelectorData) {
|
if (matchesCandidate(candidate, aRelative, aContext)) {
|
||||||
aList->mHasSelectorData = new nsCSSHasSelectorData(aList);
|
return true;
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
} 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(
|
AutoRestore<Element*> relativeAnchorRestorer(
|
||||||
aTreeMatchContext.mRelativeSelectorAnchor);
|
aTreeMatchContext.mRelativeSelectorAnchor);
|
||||||
aTreeMatchContext.mRelativeSelectorAnchor = aAnchor;
|
aTreeMatchContext.mRelativeSelectorAnchor = aAnchor;
|
||||||
@@ -2059,6 +2073,23 @@ nsCSSRuleUtils::RelativeSelectorListMatches(
|
|||||||
}
|
}
|
||||||
MOZ_ASSERT(leftmost->mNext);
|
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.
|
// A single compound cannot move beyond the leading relationship.
|
||||||
// Avoid scanning descendants for >, and all following subtrees for +/~.
|
// Avoid scanning descendants for >, and all following subtrees for +/~.
|
||||||
if (leftmost == relative->mSelectors) {
|
if (leftmost == relative->mSelectors) {
|
||||||
@@ -2125,6 +2156,78 @@ nsCSSRuleUtils::RelativeSelectorListMatches(
|
|||||||
return false;
|
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
|
// TreeMatchContext and AncestorFilter out of line methods
|
||||||
void
|
void
|
||||||
TreeMatchContext::InitAncestors(Element* aElement)
|
TreeMatchContext::InitAncestors(Element* aElement)
|
||||||
|
|||||||
@@ -71,12 +71,14 @@ struct nsCSSRuleUtils
|
|||||||
const nsAttrValue* aNewClasses,
|
const nsAttrValue* aNewClasses,
|
||||||
bool aCompareClasses,
|
bool aCompareClasses,
|
||||||
bool aSibling,
|
bool aSibling,
|
||||||
bool aNodeIsFollowingSibling = false) const;
|
bool aNodeIsFollowingSibling = false,
|
||||||
|
bool aIsTextChange = false) const;
|
||||||
bool MightAffectSiblingAnchor(nsINode* aParent, nsINode* aNode,
|
bool MightAffectSiblingAnchor(nsINode* aParent, nsINode* aNode,
|
||||||
mozilla::EventStates aStateMask,
|
mozilla::EventStates aStateMask,
|
||||||
nsIAtom* aAttribute,
|
nsIAtom* aAttribute,
|
||||||
const nsAttrValue* aNewClasses,
|
const nsAttrValue* aNewClasses,
|
||||||
bool aCompareClasses) const;
|
bool aCompareClasses,
|
||||||
|
bool aIsTextChange = false) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
static void Startup();
|
static void Startup();
|
||||||
|
|||||||
@@ -244,11 +244,24 @@ MakeArray(const nsSize& aSize, nsCSSValue& aResult)
|
|||||||
return NS_OK;
|
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
|
static nsresult
|
||||||
GetAspectRatio(nsPresContext* aPresContext, const nsMediaFeature*,
|
GetAspectRatio(nsPresContext* aPresContext, const nsMediaFeature*,
|
||||||
nsCSSValue& aResult)
|
nsCSSValue& aResult)
|
||||||
{
|
{
|
||||||
return MakeArray(GetSize(aPresContext), aResult);
|
return MakeFloatArray(GetSize(aPresContext), aResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
static nsresult
|
static nsresult
|
||||||
@@ -712,7 +725,7 @@ nsMediaFeatures::features[] = {
|
|||||||
{
|
{
|
||||||
&nsGkAtoms::aspectRatio,
|
&nsGkAtoms::aspectRatio,
|
||||||
nsMediaFeature::eMinMaxAllowed,
|
nsMediaFeature::eMinMaxAllowed,
|
||||||
nsMediaFeature::eIntRatio,
|
nsMediaFeature::eFloatRatio,
|
||||||
nsMediaFeature::eNoRequirements,
|
nsMediaFeature::eNoRequirements,
|
||||||
{ nullptr },
|
{ nullptr },
|
||||||
GetAspectRatio
|
GetAspectRatio
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ struct nsMediaFeature
|
|||||||
eFloat, // values are eCSSUnit_Number
|
eFloat, // values are eCSSUnit_Number
|
||||||
eBoolInteger,// values are eCSSUnit_Integer (0, -0, or 1 only)
|
eBoolInteger,// values are eCSSUnit_Integer (0, -0, or 1 only)
|
||||||
eIntRatio, // values are eCSSUnit_Array of two eCSSUnit_Integer
|
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),
|
eResolution, // values are in eCSSUnit_Inch (for dpi),
|
||||||
// eCSSUnit_Pixel (for dppx), or
|
// eCSSUnit_Pixel (for dppx), or
|
||||||
// eCSSUnit_Centimeter (for dpcm)
|
// eCSSUnit_Centimeter (for dpcm)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class nsIAtom;
|
|||||||
class nsIContent;
|
class nsIContent;
|
||||||
class nsICSSPseudoComparator;
|
class nsICSSPseudoComparator;
|
||||||
struct TreeMatchContext;
|
struct TreeMatchContext;
|
||||||
|
struct nsCSSSelectorList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An AncestorFilter is used to keep track of ancestors so that we can
|
* An AncestorFilter is used to keep track of ancestors so that we can
|
||||||
@@ -37,6 +38,7 @@ struct TreeMatchContext;
|
|||||||
*/
|
*/
|
||||||
class MOZ_STACK_CLASS AncestorFilter {
|
class MOZ_STACK_CLASS AncestorFilter {
|
||||||
friend struct TreeMatchContext;
|
friend struct TreeMatchContext;
|
||||||
|
struct nsCSSSelectorList;
|
||||||
public:
|
public:
|
||||||
/* Maintenance of our ancestor state */
|
/* Maintenance of our ancestor state */
|
||||||
void PushAncestor(mozilla::dom::Element *aElement);
|
void PushAncestor(mozilla::dom::Element *aElement);
|
||||||
@@ -475,6 +477,17 @@ struct MOZ_STACK_CLASS TreeMatchContext {
|
|||||||
mozilla::dom::Element* mRelativeSelectorAnchor;
|
mozilla::dom::Element* mRelativeSelectorAnchor;
|
||||||
// Propagate an outer sibling combinator through :is(), :not(), etc.
|
// Propagate an outer sibling combinator through :is(), :not(), etc.
|
||||||
bool mHasSelectorLaterSiblings;
|
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 {
|
struct MOZ_STACK_CLASS RuleProcessorData {
|
||||||
|
|||||||
@@ -679,6 +679,16 @@ nsTransitionManager::UpdateTransitions(
|
|||||||
do {
|
do {
|
||||||
--i;
|
--i;
|
||||||
CSSTransition* anim = animations[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'
|
// properties no longer in 'transition-property'
|
||||||
if ((checkProperties &&
|
if ((checkProperties &&
|
||||||
!allTransitionProperties.HasProperty(anim->TransitionProperty())) ||
|
!allTransitionProperties.HasProperty(anim->TransitionProperty())) ||
|
||||||
|
|||||||
@@ -160,6 +160,101 @@ addLoadEvent(function() {
|
|||||||
document.getElementById("empty-child").appendChild(document.createTextNode("text"));
|
document.getElementById("empty-child").appendChild(document.createTextNode("text"));
|
||||||
is(color("empty-anchor"), blue, "text insertion invalidates :empty inside :has()");
|
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();
|
SimpleTest.finish();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ EXPORTS.hwy.ops += [
|
|||||||
"/media/highway/src/hwy/ops/inside-inl.h",
|
"/media/highway/src/hwy/ops/inside-inl.h",
|
||||||
"/media/highway/src/hwy/ops/loongarch_lasx-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/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/rvv-inl.h",
|
||||||
"/media/highway/src/hwy/ops/scalar-inl.h",
|
"/media/highway/src/hwy/ops/scalar-inl.h",
|
||||||
"/media/highway/src/hwy/ops/set_macros-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
|
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 JXL_VERSION_H_
|
||||||
|
|
||||||
#define JPEGXL_MAJOR_VERSION 0 ///< JPEG XL Major version
|
#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
|
#define JPEGXL_PATCH_VERSION 0 ///< JPEG XL Patch version
|
||||||
|
|
||||||
/** Can be used to conditionally compile code for a specific JXL version
|
/** Can be used to conditionally compile code for a specific JXL version
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
url = https://github.com/google/highway
|
url = https://github.com/google/highway
|
||||||
[submodule "third_party/libpng"]
|
[submodule "third_party/libpng"]
|
||||||
path = 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"]
|
[submodule "third_party/zlib"]
|
||||||
path = third_party/zlib
|
path = third_party/zlib
|
||||||
url = https://github.com/madler/zlib.git
|
url = https://github.com/madler/zlib.git
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ sphinx:
|
|||||||
configuration: doc/sphinx/conf.py
|
configuration: doc/sphinx/conf.py
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-22.04
|
os: ubuntu-26.04
|
||||||
tools:
|
tools:
|
||||||
python: "3"
|
python: "3"
|
||||||
apt_packages:
|
apt_packages:
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ Alexander Sago <cagelight@gmail.com>
|
|||||||
Alifian Caesar Khalid <alifiancaesar@gmail.com>
|
Alifian Caesar Khalid <alifiancaesar@gmail.com>
|
||||||
Alistair Barrow
|
Alistair Barrow
|
||||||
Amiralgaby
|
Amiralgaby
|
||||||
|
Anantha Narayanan R <evertrustai@gmail.com>
|
||||||
Andrew Kaster <andrew@ladybird.org>
|
Andrew Kaster <andrew@ladybird.org>
|
||||||
Andrius Lukas Narbutas <andrius4669@gmail.com>
|
Andrius Lukas Narbutas <andrius4669@gmail.com>
|
||||||
Aous Naman <aous@unsw.edu.au>
|
Aous Naman <aous@unsw.edu.au>
|
||||||
@@ -81,11 +82,14 @@ Kleis Auke Wolthuizen <github@kleisauke.nl>
|
|||||||
L. E. Segovia
|
L. E. Segovia
|
||||||
ledoge
|
ledoge
|
||||||
Leo Izen <leo.izen@gmail.com>
|
Leo Izen <leo.izen@gmail.com>
|
||||||
|
Linus VandeVondele <linus.vandevondele@gmail.com>
|
||||||
|
LongYinan <lynweklm@gmail.com>
|
||||||
Lovell Fuller
|
Lovell Fuller
|
||||||
Lucas Chollet <lucas.chollet@serenityos.org>
|
Lucas Chollet <lucas.chollet@serenityos.org>
|
||||||
Maarten DB <anonymous.maarten@gmail.com>
|
Maarten DB <anonymous.maarten@gmail.com>
|
||||||
Marcin Konicki <ahwayakchih@gmail.com>
|
Marcin Konicki <ahwayakchih@gmail.com>
|
||||||
Martin Strunz
|
Martin Strunz
|
||||||
|
Máté Makai <mmakai2@gmail.com>
|
||||||
Mathieu Malaterre <mathieu.malaterre@gmail.com>
|
Mathieu Malaterre <mathieu.malaterre@gmail.com>
|
||||||
Mikk Leini <mikk.leini@krakul.eu>
|
Mikk Leini <mikk.leini@krakul.eu>
|
||||||
Misaki Kasumi <misakikasumi@outlook.com>
|
Misaki Kasumi <misakikasumi@outlook.com>
|
||||||
@@ -95,8 +99,10 @@ Nigel Tao <nigeltao@golang.org>
|
|||||||
oupson <oupson1er@gmail.com>
|
oupson <oupson1er@gmail.com>
|
||||||
Petr Diblík
|
Petr Diblík
|
||||||
Pieter Wuille
|
Pieter Wuille
|
||||||
|
Pepper Gray <hello@peppergray.xyz>
|
||||||
roland-rollo
|
roland-rollo
|
||||||
Samuel Leong <wvvwvvvvwvvw@gmail.com>
|
Samuel Leong <wvvwvvvvwvvw@gmail.com>
|
||||||
|
Samuele Benassai <samuelebenassai.pptx@gmail.com>
|
||||||
Sandro <sandro.jaeckel@gmail.com>
|
Sandro <sandro.jaeckel@gmail.com>
|
||||||
sandstrom
|
sandstrom
|
||||||
Sergey Fedorov <vital.had@gmail.com>
|
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/),
|
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).
|
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
|
## [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://securityscorecards.dev/viewer/?uri=github.com/libjxl/libjxl)
|
||||||
[](
|
[](
|
||||||
https://codecov.io/gh/libjxl/libjxl)
|
https://codecov.io/gh/libjxl/libjxl)
|
||||||
|
[](https://discord.gg/DqkQgDRTFu)
|
||||||
|
|
||||||
<img src="doc/jxl.svg" width="100" align="right" alt="JXL logo">
|
<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)
|
MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q FETCH_HEAD)
|
||||||
else
|
else
|
||||||
# We are in a local branch, not a pull request workflow.
|
# 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
|
fi
|
||||||
|
|
||||||
if [[ -z "${MR_ANCESTOR_SHA}" ]]; then
|
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.
|
# This prints the return value with just HEAD.
|
||||||
MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q "${MR_HEAD_SHA}^")
|
MR_ANCESTOR_SHA=$(git -C "${MYDIR}" rev-parse -q "${MR_HEAD_SHA}^")
|
||||||
else
|
else
|
||||||
@@ -439,7 +444,9 @@ cmake_build_and_test() {
|
|||||||
fi
|
fi
|
||||||
# gtest_discover_tests() runs the test binaries to discover the list of tests
|
# gtest_discover_tests() runs the test binaries to discover the list of tests
|
||||||
# at build time, which fails under qemu.
|
# 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.
|
# Pack test binaries if requested.
|
||||||
if [[ "${PACK_TEST:-}" == "1" ]]; then
|
if [[ "${PACK_TEST:-}" == "1" ]]; then
|
||||||
(cd "${BUILD_DIR}"
|
(cd "${BUILD_DIR}"
|
||||||
@@ -1258,10 +1265,10 @@ cmd_lint() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# It is ok, if zizmor is not installed.
|
# It is ok, if zizmor is not installed.
|
||||||
if which typos >/dev/null; then
|
if which zizmor >/dev/null; then
|
||||||
zizmor "${MYDIR}/.github/workflows/"
|
zizmor "${MYDIR}/.github/workflows/"
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
local installed=()
|
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.
|
* 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
|
jpeg-xl (0.11.0) unstable; urgency=medium
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Build-Depends:
|
|||||||
cmake,
|
cmake,
|
||||||
debhelper (>= 9),
|
debhelper (>= 9),
|
||||||
libbrotli-dev,
|
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,
|
libgif-dev,
|
||||||
libgoogle-perftools-dev,
|
libgoogle-perftools-dev,
|
||||||
libgtest-dev,
|
libgtest-dev,
|
||||||
|
|||||||
+12
-13
@@ -15,13 +15,13 @@ MYDIR=$(dirname "${SELF}")
|
|||||||
# Git revisions we use for the given submodules. Update these whenever you
|
# Git revisions we use for the given submodules. Update these whenever you
|
||||||
# update a git submodule.
|
# update a git submodule.
|
||||||
TESTDATA="873045a9c42ed60721756e26e2a6b32e17415205"
|
TESTDATA="873045a9c42ed60721756e26e2a6b32e17415205"
|
||||||
THIRD_PARTY_BROTLI="028fb5a23661f123017c060daa546b55cf4bde29" # v1.2.0
|
THIRD_PARTY_BROTLI="db24a98aed38a16de78cc05ca15cdd8dc2a0d92d" # v1.2.1
|
||||||
THIRD_PARTY_GOOGLETEST="6910c9d9165801d8827d628cb72eb7ea9dd538c5" # v1.16.0
|
THIRD_PARTY_GOOGLETEST="6910c9d9165801d8827d628cb72eb7ea9dd538c5" # v1.16.0
|
||||||
THIRD_PARTY_HIGHWAY="457c891775a7397bdb0376bb1031e6e027af1c48" # v1.2.0
|
THIRD_PARTY_HIGHWAY="457c891775a7397bdb0376bb1031e6e027af1c48" # v1.2.0
|
||||||
THIRD_PARTY_SKCMS="96d9171c94b937a1b5f0293de7309ac16311b722" # 2025_09_16
|
THIRD_PARTY_SKCMS="96d9171c94b937a1b5f0293de7309ac16311b722" # 2025_09_16
|
||||||
THIRD_PARTY_SJPEG="94e0df6d0f8b44228de5be0ff35efb9f946a13c9" # Wed Apr 2 15:42:02 2025 -0700
|
THIRD_PARTY_SJPEG="dd4d9857c58a25713711a46d37385ad23ca2177e" # Sun Nov 8 22:48:20 2026 +0100
|
||||||
THIRD_PARTY_ZLIB="51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf" # v1.3.1
|
THIRD_PARTY_ZLIB="51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf" # v1.3.1
|
||||||
THIRD_PARTY_LIBPNG="872555f4ba910252783af1507f9e7fe1653be252" # v1.6.47
|
THIRD_PARTY_LIBPNG="872555f4ba910252783af1507f9e7fe1653be252" # v1.6.47
|
||||||
THIRD_PARTY_LIBJPEG_TURBO="8ecba3647edb6dd940463fedf38ca33a8e2a73d1" # 2.1.5.1
|
THIRD_PARTY_LIBJPEG_TURBO="8ecba3647edb6dd940463fedf38ca33a8e2a73d1" # 2.1.5.1
|
||||||
|
|
||||||
# Download the target revision from GitHub.
|
# Download the target revision from GitHub.
|
||||||
@@ -29,7 +29,7 @@ download_github() {
|
|||||||
local path="$1"
|
local path="$1"
|
||||||
local project="$2"
|
local project="$2"
|
||||||
|
|
||||||
local varname=`echo "$path" | tr '[:lower:]' '[:upper:]'`
|
local varname=$(echo "$path" | tr '[:lower:]' '[:upper:]')
|
||||||
varname="${varname//[\/-]/_}"
|
varname="${varname//[\/-]/_}"
|
||||||
local sha
|
local sha
|
||||||
eval "sha=\${${varname}}"
|
eval "sha=\${${varname}}"
|
||||||
@@ -55,13 +55,12 @@ download_github() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is_git_repository() {
|
is_git_repository() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
local toplevel=$(git rev-parse --show-toplevel)
|
local toplevel=$(git rev-parse --show-toplevel)
|
||||||
|
|
||||||
[[ "${dir}" == "${toplevel}" ]]
|
[[ "${dir}" == "${toplevel}" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
if is_git_repository "${MYDIR}"; then
|
if is_git_repository "${MYDIR}"; then
|
||||||
cat >&2 <<EOF
|
cat >&2 <<EOF
|
||||||
@@ -82,7 +81,7 @@ EOF
|
|||||||
download_github third_party/sjpeg webmproject/sjpeg
|
download_github third_party/sjpeg webmproject/sjpeg
|
||||||
download_github third_party/skcms google/skcms
|
download_github third_party/skcms google/skcms
|
||||||
download_github third_party/zlib madler/zlib
|
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
|
download_github third_party/libjpeg-turbo libjpeg-turbo/libjpeg-turbo
|
||||||
echo "Done."
|
echo "Done."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ locally. Here is a snippet that reflects how setup / compilation are (currently)
|
|||||||
done in the workflow:
|
done in the workflow:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -it -v`pwd`:/libjxl debian:bookworm bash
|
docker run -it -v`pwd`:/libjxl debian:trixie bash
|
||||||
|
|
||||||
cd /libjxl
|
cd /libjxl
|
||||||
|
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ git push gitlab v0.7-snapshot
|
|||||||
### How to build downstream projects
|
### How to build downstream projects
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it debian:bookworm /bin/bash
|
docker run -it debian:trixie /bin/bash
|
||||||
|
|
||||||
apt update
|
apt update
|
||||||
apt install -y clang cmake git libbrotli-dev libopenexr-dev libpng-dev nasm pkg-config ninja-build
|
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.
|
# license that can be found in the LICENSE file.
|
||||||
|
|
||||||
set(JPEGXL_MAJOR_VERSION 0)
|
set(JPEGXL_MAJOR_VERSION 0)
|
||||||
set(JPEGXL_MINOR_VERSION 12)
|
set(JPEGXL_MINOR_VERSION 13)
|
||||||
set(JPEGXL_PATCH_VERSION 0)
|
set(JPEGXL_PATCH_VERSION 0)
|
||||||
set(JPEGXL_LIBRARY_VERSION
|
set(JPEGXL_LIBRARY_VERSION
|
||||||
"${JPEGXL_MAJOR_VERSION}.${JPEGXL_MINOR_VERSION}.${JPEGXL_PATCH_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
|
# 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
|
# so that programs that depend on libjxl can update their dependencies. Semantic
|
||||||
# versioning allows 0.y.z to have incompatible changes in minor versions.
|
# 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)
|
if (JPEGXL_MAJOR_VERSION EQUAL 0)
|
||||||
set(JPEGXL_LIBRARY_SOVERSION
|
set(JPEGXL_LIBRARY_SOVERSION
|
||||||
"${JPEGXL_MAJOR_VERSION}.${JPEGXL_SO_MINOR_VERSION}")
|
"${JPEGXL_MAJOR_VERSION}.${JPEGXL_SO_MINOR_VERSION}")
|
||||||
@@ -178,6 +178,13 @@ elseif(LIBCXX)
|
|||||||
set(PKGCONFIG_CXX_LIB "-lc++")
|
set(PKGCONFIG_CXX_LIB "-lc++")
|
||||||
endif()
|
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.
|
# The jxl_cms library definition.
|
||||||
include(jxl_cms.cmake)
|
include(jxl_cms.cmake)
|
||||||
# The jxl library definition.
|
# The jxl library definition.
|
||||||
|
|||||||
@@ -47,6 +47,73 @@ Status PnmParseUnsigned(Bytes str, size_t* v);
|
|||||||
|
|
||||||
namespace {
|
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) {
|
Span<const uint8_t> MakeSpan(const char* str) {
|
||||||
return Bytes(reinterpret_cast<const uint8_t*>(str), strlen(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);
|
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) {
|
TEST(CodecTest, FormatNegotiation) {
|
||||||
const std::vector<JxlPixelFormat> accepted_formats = {
|
const std::vector<JxlPixelFormat> accepted_formats = {
|
||||||
{/*num_channels=*/4,
|
{/*num_channels=*/4,
|
||||||
@@ -570,6 +680,140 @@ TEST(CodecTest, EncodeToPNG) {
|
|||||||
decoded_ppf.info.bits_per_sample);
|
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
|
||||||
} // namespace extras
|
} // namespace extras
|
||||||
} // namespace jxl
|
} // 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. */
|
/** Reusable image data container. */
|
||||||
struct Pixels {
|
struct Pixels {
|
||||||
// Use array instead of vector to avoid memory initialization.
|
// Zero-initialized: rows that libpng never delivers (truncated or corrupt
|
||||||
uninitialized_vector<uint8_t> pixels_storage =
|
// data) are copied to the output as well.
|
||||||
jxl::make_uninitialized_vector<uint8_t>(0);
|
std::vector<uint8_t> pixels_storage;
|
||||||
size_t pixels_size = 0;
|
size_t pixels_size = 0;
|
||||||
std::vector<uint8_t*> rows;
|
std::vector<uint8_t*> rows;
|
||||||
std::atomic<uint32_t> has_error{0};
|
std::atomic<uint32_t> has_error{0};
|
||||||
@@ -904,10 +904,10 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
|
|||||||
if (chunk.empty()) {
|
if (chunk.empty()) {
|
||||||
return JXL_FAILURE("Malformed chunk");
|
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());
|
id = LoadLE32(type.data());
|
||||||
// Cut 'size' and 'type' at front and 'CRC' at the end.
|
// 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]) ||
|
if (!isAbc(type[0]) || !isAbc(type[1]) || !isAbc(type[2]) ||
|
||||||
!isAbc(type[3])) {
|
!isAbc(type[3])) {
|
||||||
@@ -1063,7 +1063,7 @@ Status DecodeImageAPNG(const Span<const uint8_t> bytes,
|
|||||||
png_save_uint_32(preamble.data(), payload.size() - 4);
|
png_save_uint_32(preamble.data(), payload.size() - 4);
|
||||||
memcpy(preamble.data() + 4, "IDAT", 4);
|
memcpy(preamble.data() + 4, "IDAT", 4);
|
||||||
// Cut-off 'size', 'type' and 'sequence_number'
|
// 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)) {
|
if (!ctx.FeedChunks(Bytes(preamble), chunk_tail)) {
|
||||||
return JXL_FAILURE("Decoding fdAT failed");
|
return JXL_FAILURE("Decoding fdAT failed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,19 @@ namespace {
|
|||||||
constexpr size_t kMinBytes = 9;
|
constexpr size_t kMinBytes = 9;
|
||||||
|
|
||||||
std::string GetExtension(const std::string& path) {
|
std::string GetExtension(const std::string& path) {
|
||||||
// Pattern: "name.png"
|
// Pattern: "name.png". Only the final path component can carry an
|
||||||
size_t pos = path.find_last_of('.');
|
// extension: strip any directory part (both '/' and '\' separators)
|
||||||
if (pos != std::string::npos) {
|
// first, so that dots inside directory names (e.g. "a.png/../../b")
|
||||||
return path.substr(pos);
|
// 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 "";
|
||||||
}
|
}
|
||||||
|
return base.substr(pos);
|
||||||
// Extension not found
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -82,7 +82,17 @@ class InMemoryIStream : public OpenEXR::IStream {
|
|||||||
explicit InMemoryIStream(const Span<const uint8_t> bytes)
|
explicit InMemoryIStream(const Span<const uint8_t> bytes)
|
||||||
: IStream(/*fileName=*/""), bytes_(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 {
|
char* readMemoryMapped(const int n) override {
|
||||||
if (pos_ + n < pos_) {
|
if (pos_ + n < pos_) {
|
||||||
JXL_EXR_THROW_LENGTH_ERROR("Overflow");
|
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");
|
fprintf(stderr, "Image too big\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
total_pixel_count += static_cast<uint64_t>(ppf->xsize()) * ppf->ysize();
|
uint64_t file_px = static_cast<uint64_t>(ppf->xsize()) * ppf->ysize();
|
||||||
if (constraints && (total_pixel_count > constraints->dec_max_pixels)) {
|
|
||||||
return JXL_FAILURE("Image too big");
|
|
||||||
}
|
|
||||||
JxlFrameHeader fh;
|
JxlFrameHeader fh;
|
||||||
if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec, &fh)) {
|
if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec, &fh)) {
|
||||||
fprintf(stderr, "JxlDecoderGetFrameHeader failed\n");
|
fprintf(stderr, "JxlDecoderGetFrameHeader failed\n");
|
||||||
return false;
|
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_ASSIGN_OR_QUIT(jxl::extras::PackedFrame frame,
|
||||||
jxl::extras::PackedFrame::Create(
|
jxl::extras::PackedFrame::Create(
|
||||||
fh.layer_info.xsize, fh.layer_info.ysize, format),
|
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.
|
// Sets "pos" to the first non-header byte/pixel on success.
|
||||||
Status ParseHeader(HeaderPGX* header, const uint8_t** pos) {
|
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;
|
if (pos_[0] != 'P' || pos_[1] != 'G') return false;
|
||||||
pos_ += 2;
|
pos_ += 2;
|
||||||
return ParseHeaderPGX(header, pos);
|
return ParseHeaderPGX(header, pos);
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
#include <jxl/memory_manager.h>
|
#include <jxl/memory_manager.h>
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "lib/extras/packed_image.h"
|
#include "lib/extras/packed_image.h"
|
||||||
#include "lib/extras/packed_image_convert.h"
|
#include "lib/extras/packed_image_convert.h"
|
||||||
@@ -112,6 +114,26 @@ TEST(CodecPGXTest, RejectsDimensionProductOverflow) {
|
|||||||
EXPECT_FALSE(DecodeImagePGX(MakeSpan(pgx.c_str()), ColorHints(), &ppf));
|
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
|
||||||
} // namespace extras
|
} // namespace extras
|
||||||
} // namespace jxl
|
} // namespace jxl
|
||||||
@@ -39,7 +39,7 @@ class Parser {
|
|||||||
|
|
||||||
// Sets "pos" to the first non-header byte/pixel on success.
|
// Sets "pos" to the first non-header byte/pixel on success.
|
||||||
Status ParseHeader(HeaderPNM* header, const uint8_t** pos) {
|
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;
|
if (pos_[0] != 'P') return false;
|
||||||
const uint8_t type = pos_[1];
|
const uint8_t type = pos_[1];
|
||||||
pos_ += 2;
|
pos_ += 2;
|
||||||
@@ -340,6 +340,35 @@ class Parser {
|
|||||||
const uint8_t* const end_;
|
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
|
} // namespace
|
||||||
|
|
||||||
struct PNMChunkedInputFrame {
|
struct PNMChunkedInputFrame {
|
||||||
@@ -432,6 +461,9 @@ StatusOr<ChunkedPNMDecoder> ChunkedPNMDecoder::Init(
|
|||||||
if (size < required_size) {
|
if (size < required_size) {
|
||||||
return JXL_FAILURE("PNM file too small");
|
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;
|
return dec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,6 +587,10 @@ Status DecodeImagePNM(const Span<const uint8_t> bytes,
|
|||||||
if (pnm_remaining_size < required_pnm_size) {
|
if (pnm_remaining_size < required_pnm_size) {
|
||||||
return JXL_FAILURE("PNM file too small");
|
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();
|
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,
|
Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
|
||||||
ThreadPool* pool) const override {
|
ThreadPool* pool) const override {
|
||||||
|
JXL_RETURN_IF_ERROR(ppf.Validate());
|
||||||
// Encode main image frames
|
// Encode main image frames
|
||||||
JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
|
JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
|
||||||
encoded_image->icc.clear();
|
encoded_image->icc.clear();
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ bool SetupFrame(JxlEncoder* enc, JxlEncoderFrameSettings* settings,
|
|||||||
if (!SetFrameOptions(params.options, frame_index, &option_idx, settings)) {
|
if (!SetFrameOptions(params.options, frame_index, &option_idx, settings)) {
|
||||||
return false;
|
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()) {
|
if (frame_index < ppf.frames.size()) {
|
||||||
const auto& frame_name = ppf.frames[frame_index].name;
|
const auto& frame_name = ppf.frames[frame_index].name;
|
||||||
if (!frame_name.empty()) {
|
if (!frame_name.empty()) {
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ struct JXLCompressParams {
|
|||||||
int32_t codestream_level = -1;
|
int32_t codestream_level = -1;
|
||||||
int32_t premultiply = -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.
|
// If runner_opaque is set, the encoder uses this parallel runner.
|
||||||
JxlParallelRunner runner = JxlThreadParallelRunner;
|
JxlParallelRunner runner = JxlThreadParallelRunner;
|
||||||
void* runner_opaque = nullptr;
|
void* runner_opaque = nullptr;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
@@ -31,6 +32,52 @@ namespace {
|
|||||||
|
|
||||||
// JSON value writing
|
// 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 {
|
class JSONField {
|
||||||
public:
|
public:
|
||||||
virtual ~JSONField() = default;
|
virtual ~JSONField() = default;
|
||||||
@@ -45,7 +92,8 @@ class JSONValue : public JSONField {
|
|||||||
template <typename T>
|
template <typename T>
|
||||||
explicit JSONValue(const T& value) : value_(std::to_string(value)) {}
|
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") {}
|
explicit JSONValue(bool value) : value_(value ? "true" : "false") {}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class BasePNMEncoder : public Encoder {
|
|||||||
public:
|
public:
|
||||||
Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
|
Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
|
||||||
ThreadPool* pool) const override {
|
ThreadPool* pool) const override {
|
||||||
|
JXL_RETURN_IF_ERROR(ppf.Validate());
|
||||||
JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
|
JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
|
||||||
if (!ppf.metadata.exif.empty() || !ppf.metadata.iptc.empty() ||
|
if (!ppf.metadata.exif.empty() || !ppf.metadata.iptc.empty() ||
|
||||||
!ppf.metadata.jumbf.empty() || !ppf.metadata.xmp.empty()) {
|
!ppf.metadata.jumbf.empty() || !ppf.metadata.xmp.empty()) {
|
||||||
|
|||||||
@@ -115,6 +115,13 @@ JXL_BOOL JxlGainMapWriteBundle(const JxlGainMapBundle* map_bundle,
|
|||||||
} \
|
} \
|
||||||
} while (false)
|
} 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);
|
SAFE_CURSOR_UPDATE(1);
|
||||||
memcpy(output_buffer + cursor, &jhgm_version, 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);
|
StoreBE16(map_bundle->gain_map_metadata_size, output_buffer + cursor);
|
||||||
|
|
||||||
SAFE_CURSOR_UPDATE(map_bundle->gain_map_metadata_size);
|
SAFE_CURSOR_UPDATE(map_bundle->gain_map_metadata_size);
|
||||||
memcpy(output_buffer + cursor, map_bundle->gain_map_metadata,
|
SAFE_COPY(map_bundle->gain_map_metadata, map_bundle->gain_map_metadata_size);
|
||||||
map_bundle->gain_map_metadata_size);
|
|
||||||
|
|
||||||
jxl::Bytes bytes = color_encoding_writer.GetSpan();
|
jxl::Bytes bytes = color_encoding_writer.GetSpan();
|
||||||
uint8_t color_enc_size = static_cast<uint8_t>(bytes.size());
|
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);
|
memcpy(output_buffer + cursor, &color_enc_size, 1);
|
||||||
|
|
||||||
SAFE_CURSOR_UPDATE(color_enc_size);
|
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);
|
SAFE_CURSOR_UPDATE(4);
|
||||||
StoreBE32(map_bundle->alt_icc_size, output_buffer + cursor);
|
StoreBE32(map_bundle->alt_icc_size, output_buffer + cursor);
|
||||||
|
|
||||||
SAFE_CURSOR_UPDATE(map_bundle->alt_icc_size);
|
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);
|
SAFE_CURSOR_UPDATE(map_bundle->gain_map_size);
|
||||||
memcpy(output_buffer + cursor, map_bundle->gain_map,
|
SAFE_COPY(map_bundle->gain_map, map_bundle->gain_map_size);
|
||||||
map_bundle->gain_map_size);
|
|
||||||
|
|
||||||
|
#undef SAFE_COPY
|
||||||
#undef SAFE_CURSOR_UPDATE
|
#undef SAFE_CURSOR_UPDATE
|
||||||
|
|
||||||
cursor = next_cursor;
|
cursor = next_cursor;
|
||||||
|
|||||||
@@ -180,6 +180,15 @@ ChunkedPackedFrame::ChunkedPackedFrame(
|
|||||||
|
|
||||||
PackedPixelFile::PackedPixelFile() { JxlEncoderInitBasicInfo(&info); };
|
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) {
|
Status PackedPixelFile::ShrinkTo(size_t new_xsize, size_t new_ysize) {
|
||||||
for (auto& frame : frames) {
|
for (auto& frame : frames) {
|
||||||
JXL_RETURN_IF_ERROR(frame.ShrinkTo(new_xsize, new_ysize));
|
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 xsize() const { return info.xsize; }
|
||||||
size_t ysize() const { return info.ysize; }
|
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.
|
// Logical resize; storage is not reallocated; stride is unchanged.
|
||||||
Status ShrinkTo(size_t new_xsize, size_t new_ysize);
|
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
|
* 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
|
* 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
|
* 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
|
* original profile is not used and @ref JxlDecoderSetOutputColorProfile is
|
||||||
* (linear if outputting to floating point, nonlinear with standard sRGB
|
* not called, the decoder converts the data to the original profile if it's
|
||||||
* transfer function if outputting to unsigned integers) but will not convert
|
* encoded as a @ref JxlColorEncoding rather than an ICC color profile, and
|
||||||
* it to the original color profile. The decoder also does not convert to
|
* falls back to linear sRGB otherwise. To convert the pixel data produced by
|
||||||
* the target display color profile. To convert the pixel data produced by
|
|
||||||
* the decoder to the original color profile, one of the JxlDecoderGetColor*
|
* the decoder to the original color profile, one of the JxlDecoderGetColor*
|
||||||
* functions needs to be called with
|
* functions needs to be called with
|
||||||
* ::JXL_COLOR_PROFILE_TARGET_DATA to get the color profile of the decoder
|
* ::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,
|
JXL_ENC_FRAME_SETTING_JPEG_COMPRESS_BOXES = 33,
|
||||||
|
|
||||||
/** Control what kind of input buffering is used, when using chunked image frames.
|
/** Control what kind of input buffering is used, when using chunked image
|
||||||
* When using streaming input the encoder minimizes memory usage, potentially at
|
* frames.
|
||||||
* a cost in compression density (though not necessarily).
|
* 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)
|
* -1 = default (let the encoder decide)
|
||||||
* 0 = buffers everything, basically the same as non-streamed code path
|
* 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
|
* 1 = buffers everything for images that are 2048 x 2048 or smaller, and
|
||||||
* uses streaming input and buffered output for larger images
|
* uses streaming input and buffered output for larger images
|
||||||
* 2 = same as 1, but the threshold to use streaming input is lower
|
* 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,
|
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
|
/** 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.
|
* C compiler to have the enum to take a known size.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -107,7 +107,9 @@ struct AliasTable {
|
|||||||
|
|
||||||
#if JXL_BYTE_ORDER_LITTLE
|
#if JXL_BYTE_ORDER_LITTLE
|
||||||
uint64_t entry;
|
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 cutoff = entry & 0xFF; // = MOVZX
|
||||||
const size_t right_value = (entry >> 8) & 0xFF; // = MOVZX
|
const size_t right_value = (entry >> 8) & 0xFF; // = MOVZX
|
||||||
const size_t freq0 = (entry >> 16) & 0xFFFF;
|
const size_t freq0 = (entry >> 16) & 0xFFFF;
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ void TestCheckpointing(bool ans, bool lz77) {
|
|||||||
|
|
||||||
EntropyEncodingData codes;
|
EntropyEncodingData codes;
|
||||||
HistogramParams params;
|
HistogramParams params;
|
||||||
params.lz77_method = lz77 ? HistogramParams::LZ77Method::kLZ77
|
params.lz77_method = lz77 ? HistogramParams::LZ77Method::kLZ77b3w3f
|
||||||
: HistogramParams::LZ77Method::kNone;
|
: HistogramParams::LZ77Method::kNone;
|
||||||
params.force_huffman = !ans;
|
params.force_huffman = !ans;
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "lib/jxl/base/compiler_specific.h"
|
||||||
|
|
||||||
#if JXL_COMPILER_MSVC
|
#if JXL_COMPILER_MSVC
|
||||||
#include <intrin.h>
|
#include <intrin.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "lib/jxl/base/compiler_specific.h"
|
|
||||||
|
|
||||||
namespace jxl {
|
namespace jxl {
|
||||||
// Some enums and typedefs used by more than one header file.
|
// Some enums and typedefs used by more than one header file.
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
#define JXL_NORETURN
|
#define JXL_NORETURN
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if JXL_COMPILER_MSVC
|
#if JXL_COMPILER_MSVC && !defined(__clang__)
|
||||||
#define JXL_MAYBE_UNUSED
|
#define JXL_MAYBE_UNUSED
|
||||||
#else
|
#else
|
||||||
// Encountered "attribute list cannot appear here" when using the C++17
|
// 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* end() const noexcept { return data() + size(); }
|
||||||
|
|
||||||
constexpr T& operator[](size_t i) const noexcept {
|
constexpr T& operator[](size_t i) const noexcept {
|
||||||
|
JXL_DASSERT(i < len_);
|
||||||
// MSVC 2015 accepts this as constexpr, but not ptr_[i]
|
// MSVC 2015 accepts this as constexpr, but not ptr_[i]
|
||||||
return *(data() + i);
|
return *(data() + i);
|
||||||
}
|
}
|
||||||
@@ -66,6 +67,29 @@ class Span {
|
|||||||
return true;
|
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 {
|
void AppendTo(std::vector<NCT>& dst) const {
|
||||||
dst.insert(dst.end(), begin(), end());
|
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;
|
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,
|
JXL_ASSIGN_OR_RETURN(ImageF tmp,
|
||||||
ImageF::Create(memory_manager, xsize, 3 + num_ec));
|
ImageF::Create(memory_manager, xsize, 3 + num_ec));
|
||||||
// Blend extra channels first so that we use the pre-blending alpha.
|
// Blend extra channels first so that we use the pre-blending alpha.
|
||||||
@@ -103,11 +109,11 @@ Status PerformBlending(
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
case PatchBlendMode::kReplace:
|
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;
|
continue;
|
||||||
|
|
||||||
case PatchBlendMode::kNone:
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,6 +148,8 @@ Status PerformBlending(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const auto copy = [&](const float* const* src) {
|
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++) {
|
for (size_t p = 0; p < 3; p++) {
|
||||||
memcpy(tmp.Row(p), src[p] + x0, xsize * sizeof(**src));
|
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++) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
#include "lib/extras/dec/jxl.h"
|
#include "lib/extras/dec/jxl.h"
|
||||||
#include "lib/extras/packed_image.h"
|
#include "lib/extras/packed_image.h"
|
||||||
#include "lib/jxl/base/span.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/test_utils.h"
|
||||||
#include "lib/jxl/testing.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
|
||||||
} // namespace jxl
|
} // namespace jxl
|
||||||
@@ -19,23 +19,26 @@ namespace jxl {
|
|||||||
|
|
||||||
JxlBoxContentDecoder::JxlBoxContentDecoder() = default;
|
JxlBoxContentDecoder::JxlBoxContentDecoder() = default;
|
||||||
|
|
||||||
JxlBoxContentDecoder::~JxlBoxContentDecoder() {
|
JxlBoxContentDecoder::~JxlBoxContentDecoder() { Reset(); }
|
||||||
if (brotli_dec) {
|
|
||||||
BrotliDecoderDestroyInstance(brotli_dec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void JxlBoxContentDecoder::StartBox(bool brob_decode, bool box_until_eof,
|
void JxlBoxContentDecoder::Reset() {
|
||||||
size_t contents_size) {
|
|
||||||
if (brotli_dec) {
|
if (brotli_dec) {
|
||||||
BrotliDecoderDestroyInstance(brotli_dec);
|
BrotliDecoderDestroyInstance(brotli_dec);
|
||||||
brotli_dec = nullptr;
|
brotli_dec = nullptr;
|
||||||
}
|
}
|
||||||
header_done_ = false;
|
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;
|
brob_decode_ = brob_decode;
|
||||||
box_until_eof_ = box_until_eof;
|
box_until_eof_ = box_until_eof;
|
||||||
remaining_ = box_until_eof ? 0 : contents_size;
|
remaining_ = box_until_eof ? 0 : contents_size;
|
||||||
pos_ = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JxlDecoderStatus JxlBoxContentDecoder::Process(const uint8_t* next_in,
|
JxlDecoderStatus JxlBoxContentDecoder::Process(const uint8_t* next_in,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class JxlBoxContentDecoder {
|
|||||||
JxlBoxContentDecoder();
|
JxlBoxContentDecoder();
|
||||||
~JxlBoxContentDecoder();
|
~JxlBoxContentDecoder();
|
||||||
|
|
||||||
|
void Reset();
|
||||||
void StartBox(bool brob_decode, bool box_until_eof, size_t contents_size);
|
void StartBox(bool brob_decode, bool box_until_eof, size_t contents_size);
|
||||||
|
|
||||||
// Outputs decoded bytes from the box, decoding with brotli if needed.
|
// 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.
|
// Returns white point that was specified when creating the profile.
|
||||||
JXL_MUST_USE_RESULT Status UnadaptedWhitePoint(const skcms_ICCProfile& profile,
|
JXL_MUST_USE_RESULT Status UnadaptedWhitePoint(const skcms_ICCProfile& profile,
|
||||||
CIExy* out) {
|
CIExy* out) {
|
||||||
@@ -458,8 +557,15 @@ JXL_MUST_USE_RESULT Status UnadaptedWhitePoint(const skcms_ICCProfile& profile,
|
|||||||
*out = CIExyFromXYZ(media_white_point_XYZ);
|
*out = CIExyFromXYZ(media_white_point_XYZ);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Otherwise, it has been adapted to the PCS white point using said matrix,
|
Color adapted_white_point_XYZ;
|
||||||
// and the adaptation needs to be undone.
|
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;
|
skcms_Matrix3x3 inverse_CHAD;
|
||||||
if (!skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)) {
|
if (!skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)) {
|
||||||
return JXL_FAILURE("Non-invertible ChromaticAdaptation matrix");
|
return JXL_FAILURE("Non-invertible ChromaticAdaptation matrix");
|
||||||
@@ -541,6 +647,16 @@ Status DetectTransferFunction(const skcms_ICCProfile& profile,
|
|||||||
ColorEncoding* JXL_RESTRICT c) {
|
ColorEncoding* JXL_RESTRICT c) {
|
||||||
JXL_ENSURE(c->color_space != ColorSpace::kXYB);
|
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] = {};
|
float gamma[3] = {};
|
||||||
if (profile.has_trc) {
|
if (profile.has_trc) {
|
||||||
const auto IsGamma = [](const skcms_TransferFunction& tf) {
|
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.
|
// "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,
|
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);
|
const uint32_t type_src = Type64(c);
|
||||||
|
|
||||||
Profile profile2;
|
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) {
|
for (in[0] = init; in[0] < 1.0; in[0] += step / 8) {
|
||||||
cmsDoTransform(xform1.get(), in, out1, 1);
|
cmsDoTransform(xform1.get(), in, out1, 1);
|
||||||
cmsDoTransform(xform2.get(), in, out2, 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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,7 +775,7 @@ Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
|
|||||||
cmsDoTransform(xform1.get(), in, out1, 1);
|
cmsDoTransform(xform1.get(), in, out1, 1);
|
||||||
cmsDoTransform(xform2.get(), in, out2, 1);
|
cmsDoTransform(xform2.get(), in, out2, 1);
|
||||||
for (size_t i = 0; i < 3; ++i) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -667,6 +787,116 @@ Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
|
|||||||
return true;
|
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.
|
// Returns white point that was specified when creating the profile.
|
||||||
// NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation
|
// NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation
|
||||||
// differs between ICC versions.
|
// differs between ICC versions.
|
||||||
@@ -675,11 +905,19 @@ JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context,
|
|||||||
const ColorEncoding& c) {
|
const ColorEncoding& c) {
|
||||||
const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>(
|
const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>(
|
||||||
cmsReadTag(profile.get(), cmsSigMediaWhitePointTag));
|
cmsReadTag(profile.get(), cmsSigMediaWhitePointTag));
|
||||||
if (white_point != nullptr &&
|
const cmsMAT3* chad = static_cast<const cmsMAT3*>(
|
||||||
cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag) == nullptr) {
|
cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag));
|
||||||
|
if (white_point != nullptr && chad == nullptr) {
|
||||||
// No chromatic adaptation matrix: the white point is already unadapted.
|
// No chromatic adaptation matrix: the white point is already unadapted.
|
||||||
return *white_point;
|
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};
|
cmsCIEXYZ XYZ = {1.0, 1.0, 1.0};
|
||||||
Profile profile_xyz;
|
Profile profile_xyz;
|
||||||
@@ -746,14 +984,26 @@ Status IdentifyPrimaries(const cmsContext context, const Profile& profile,
|
|||||||
adapted_b = &converted_rgb[2];
|
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;
|
cmsCIEXYZ r, g, b;
|
||||||
cmsAdaptToIlluminant(&r, &d50, &wp_unadapted, adapted_r);
|
Matrix3x3d inverse_chad = {};
|
||||||
cmsAdaptToIlluminant(&g, &d50, &wp_unadapted, adapted_g);
|
if (HasParametricTRCs(profile) &&
|
||||||
cmsAdaptToIlluminant(&b, &d50, &wp_unadapted, adapted_b);
|
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),
|
const PrimariesCIExy rgb = {CIExyFromXYZ(r), CIExyFromXYZ(g),
|
||||||
CIExyFromXYZ(b)};
|
CIExyFromXYZ(b)};
|
||||||
@@ -764,6 +1014,22 @@ Status DetectTransferFunction(const cmsContext context, const Profile& profile,
|
|||||||
ColorEncoding* JXL_RESTRICT c) {
|
ColorEncoding* JXL_RESTRICT c) {
|
||||||
JXL_ENSURE(c->color_space != ColorSpace::kXYB);
|
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;
|
float gamma = 0;
|
||||||
if (const auto* gray_trc = reinterpret_cast<const cmsToneCurve*>(
|
if (const auto* gray_trc = reinterpret_cast<const cmsToneCurve*>(
|
||||||
cmsReadTag(profile.get(), cmsSigGrayTRCTag))) {
|
cmsReadTag(profile.get(), cmsSigGrayTRCTag))) {
|
||||||
@@ -796,7 +1062,8 @@ Status DetectTransferFunction(const cmsContext context, const Profile& profile,
|
|||||||
if (gamma != 0 && c->tf.SetGamma(gamma)) {
|
if (gamma != 0 && c->tf.SetGamma(gamma)) {
|
||||||
IccBytes icc_test;
|
IccBytes icc_test;
|
||||||
if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
|
if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
|
||||||
ProfileEquivalentToICC(context, profile, icc_test, *c)) {
|
ProfileEquivalentToICC(context, profile, icc_test, *c,
|
||||||
|
equivalence_tolerance)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -809,7 +1076,8 @@ Status DetectTransferFunction(const cmsContext context, const Profile& profile,
|
|||||||
|
|
||||||
IccBytes icc_test;
|
IccBytes icc_test;
|
||||||
if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
|
if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
|
||||||
ProfileEquivalentToICC(context, profile, icc_test, *c)) {
|
ProfileEquivalentToICC(context, profile, icc_test, *c,
|
||||||
|
equivalence_tolerance)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -829,7 +1097,8 @@ cmsContext GetContext() {
|
|||||||
context_.reset(cmsCreateContext(nullptr, nullptr));
|
context_.reset(cmsCreateContext(nullptr, nullptr));
|
||||||
JXL_DASSERT(context_ != 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());
|
return static_cast<cmsContext>(context_.get());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,20 @@
|
|||||||
#include <jxl/memory_manager.h>
|
#include <jxl/memory_manager.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "lib/jxl/base/byte_order.h"
|
||||||
#include "lib/jxl/base/common.h"
|
#include "lib/jxl/base/common.h"
|
||||||
#include "lib/jxl/base/compiler_specific.h"
|
#include "lib/jxl/base/compiler_specific.h"
|
||||||
#include "lib/jxl/base/span.h"
|
#include "lib/jxl/base/span.h"
|
||||||
@@ -28,6 +32,7 @@
|
|||||||
#include "lib/jxl/image.h"
|
#include "lib/jxl/image.h"
|
||||||
#include "lib/jxl/image_ops.h"
|
#include "lib/jxl/image_ops.h"
|
||||||
#include "lib/jxl/image_test_utils.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_memory_manager.h"
|
||||||
#include "lib/jxl/test_utils.h"
|
#include "lib/jxl/test_utils.h"
|
||||||
#include "lib/jxl/testing.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.
|
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 {
|
struct Globals {
|
||||||
// TODO(deymo): Make this a const.
|
// TODO(deymo): Make this a const.
|
||||||
static Globals* GetInstance() {
|
static Globals* GetInstance() {
|
||||||
@@ -270,6 +492,257 @@ TEST_F(ColorManagementTest, D2700ToSRGB) {
|
|||||||
EXPECT_ARRAY_NEAR(sRGB_values, sRGB_expected, 1e-3);
|
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) {
|
TEST_F(ColorManagementTest, P3HlgTo2020Hlg) {
|
||||||
ColorEncoding p3_hlg;
|
ColorEncoding p3_hlg;
|
||||||
p3_hlg.SetColorSpace(ColorSpace::kRGB);
|
p3_hlg.SetColorSpace(ColorSpace::kRGB);
|
||||||
|
|||||||
@@ -125,8 +125,29 @@ JXL_INLINE void ComputePixel(
|
|||||||
Store(out, d, out_rows[2] + x);
|
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,
|
Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
|
||||||
const float* dc_factors, Image3F* dc,
|
const float* dc_factors, Image3F* dc,
|
||||||
|
const YCbCrChromaSubsampling& chroma_subsampling,
|
||||||
ThreadPool* pool) {
|
ThreadPool* pool) {
|
||||||
const size_t xsize = dc->xsize();
|
const size_t xsize = dc->xsize();
|
||||||
const size_t ysize = dc->ysize();
|
const size_t ysize = dc->ysize();
|
||||||
@@ -137,63 +158,107 @@ Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
|
|||||||
// the x and b channels through color correlation.
|
// the x and b channels through color correlation.
|
||||||
JXL_ENSURE(w1 + w2 < 0.25f);
|
JXL_ENSURE(w1 + w2 < 0.25f);
|
||||||
|
|
||||||
JXL_ASSIGN_OR_RETURN(Image3F smoothed,
|
if (chroma_subsampling.Is444()) {
|
||||||
Image3F::Create(memory_manager, xsize, ysize));
|
JXL_ASSIGN_OR_RETURN(Image3F smoothed,
|
||||||
// Fill in borders that the loop below will not. First and last are unused.
|
Image3F::Create(memory_manager, xsize, ysize));
|
||||||
for (size_t c = 0; c < 3; c++) {
|
// Fill in borders that the loop below will not. First and last are unused.
|
||||||
for (size_t y : {static_cast<size_t>(0), ysize - 1}) {
|
for (size_t c = 0; c < 3; c++) {
|
||||||
memcpy(smoothed.PlaneRow(c, y), dc->PlaneRow(c, y),
|
for (size_t y : {static_cast<size_t>(0), ysize - 1}) {
|
||||||
xsize * sizeof(float));
|
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];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
size_t x = 1;
|
||||||
// First pixels
|
// First pixels
|
||||||
const size_t N = Lanes(D());
|
const size_t N = Lanes(D());
|
||||||
for (; x < std::min(N, xsize - 1); x++) {
|
for (; x < std::min(N, xsize - 1); x++) {
|
||||||
ComputePixel<DScalar>(dc_factors, rows_top, rows, rows_bottom, rows_out,
|
ComputePixel<DScalar>(dc_factors, rows_top, rows, rows_bottom, rows_out,
|
||||||
x);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,9 +372,10 @@ HWY_EXPORT(DequantDC);
|
|||||||
HWY_EXPORT(AdaptiveDCSmoothing);
|
HWY_EXPORT(AdaptiveDCSmoothing);
|
||||||
Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
|
Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
|
||||||
const float* dc_factors, Image3F* dc,
|
const float* dc_factors, Image3F* dc,
|
||||||
|
const YCbCrChromaSubsampling& chroma_subsampling,
|
||||||
ThreadPool* pool) {
|
ThreadPool* pool) {
|
||||||
return HWY_DYNAMIC_DISPATCH(AdaptiveDCSmoothing)(memory_manager, dc_factors,
|
return HWY_DYNAMIC_DISPATCH(AdaptiveDCSmoothing)(
|
||||||
dc, pool);
|
memory_manager, dc_factors, dc, chroma_subsampling, pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in,
|
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.
|
// Smooth DC in already-smooth areas, to counteract banding.
|
||||||
Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
|
Status AdaptiveDCSmoothing(JxlMemoryManager* memory_manager,
|
||||||
const float* dc_factors, Image3F* dc,
|
const float* dc_factors, Image3F* dc,
|
||||||
|
const YCbCrChromaSubsampling& chroma_subsampling,
|
||||||
ThreadPool* pool);
|
ThreadPool* pool);
|
||||||
|
|
||||||
void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in,
|
void DequantDC(const Rect& r, Image3F* dc, ImageB* quant_dc, const Image& in,
|
||||||
|
|||||||
@@ -265,9 +265,14 @@ class ANSSymbolReader {
|
|||||||
br->Refill(); // covers ReadSymbolWithoutRefill + PeekBits
|
br->Refill(); // covers ReadSymbolWithoutRefill + PeekBits
|
||||||
size_t token = ReadSymbolHuffWithoutRefill(ctx, br);
|
size_t token = ReadSymbolHuffWithoutRefill(ctx, br);
|
||||||
if (JXL_UNLIKELY(token >= lz77_threshold_)) {
|
if (JXL_UNLIKELY(token >= lz77_threshold_)) {
|
||||||
*run =
|
uint32_t addend =
|
||||||
ReadHybridUintConfig(lz77_length_uint_, token - lz77_threshold_, br) +
|
ReadHybridUintConfig(lz77_length_uint_, token - lz77_threshold_, br);
|
||||||
lz77_min_length_ - 1;
|
uint32_t result = addend + lz77_min_length_ - 1;
|
||||||
|
*run = result;
|
||||||
|
if (JXL_UNLIKELY(result < addend)) { // overflow
|
||||||
|
*run = 0;
|
||||||
|
br->MarkUnhealthy();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*value = ReadHybridUintConfig(configs[ctx], token, br);
|
*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);
|
(options.render_noise && (frame_header.flags & FrameHeader::kNoise) != 0);
|
||||||
size_t num_tmp_c = render_noise ? 3 : 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()) {
|
if (frame_header.CanBeReferenced()) {
|
||||||
// Necessary so that SetInputSizes() can allocate output buffers as needed.
|
// Necessary so that SetInputSizes() can allocate output buffers as needed.
|
||||||
frame_storage_for_referencing = ImageBundle(memory_manager, metadata);
|
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)));
|
&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 (fast_xyb_srgb8_conversion) {
|
||||||
#if !JXL_HIGH_PRECISION
|
#if !JXL_HIGH_PRECISION
|
||||||
JXL_ENSURE(!NeedsBlending(frame_header));
|
JXL_ENSURE(!NeedsBlending(frame_header));
|
||||||
@@ -305,7 +309,12 @@ Status PassesDecoderState::PreparePipeline(const FrameHeader& frame_header,
|
|||||||
if (!output_encoding_info.cms_set) {
|
if (!output_encoding_info.cms_set) {
|
||||||
return JXL_FAILURE("Cannot tonemap this colorspace without a CMS");
|
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) {
|
if (cms_stage) {
|
||||||
JXL_RETURN_IF_ERROR(builder.AddStage(std::move(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()) {
|
if (!output_encoding_info.linear_color_encoding.CreateICC()) {
|
||||||
return JXL_FAILURE("Failed to create ICC");
|
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) {
|
if (cms_stage) {
|
||||||
JXL_RETURN_IF_ERROR(builder.AddStage(std::move(cms_stage)));
|
JXL_RETURN_IF_ERROR(builder.AddStage(std::move(cms_stage)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
linear = false;
|
linear = false;
|
||||||
} else {
|
} 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) {
|
if (cms_stage) {
|
||||||
JXL_RETURN_IF_ERROR(builder.AddStage(std::move(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) {
|
if (group_codes_begin + section_sizes_sum_ < group_codes_begin) {
|
||||||
return JXL_FAILURE("Invalid group codes");
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,10 +223,16 @@ Status FrameDecoder::InitFrameOutput() {
|
|||||||
if (frame_header_.nonserialized_metadata->m.xyb_encoded) {
|
if (frame_header_.nonserialized_metadata->m.xyb_encoded) {
|
||||||
return JXL_FAILURE("Cannot decode to JPEG an XYB image");
|
return JXL_FAILURE("Cannot decode to JPEG an XYB image");
|
||||||
}
|
}
|
||||||
auto jpeg_c_map = JpegOrder(ColorTransform::kYCbCr, num_components == 1);
|
bool is_gray = (num_components == 1);
|
||||||
decoded_->jpeg_data->width = frame_dim_.xsize;
|
auto jpeg_c_map = JpegOrder(ColorTransform::kYCbCr, is_gray);
|
||||||
decoded_->jpeg_data->height = frame_dim_.ysize;
|
if (frame_dim_.xsize > static_cast<size_t>(jpeg::kMaxDimPixels) ||
|
||||||
for (size_t c = 0; c < num_components; c++) {
|
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]];
|
auto& component = jpeg_data->components[jpeg_c_map[c]];
|
||||||
component.width_in_blocks =
|
component.width_in_blocks =
|
||||||
frame_dim_.xsize_blocks >> frame_header_.chroma_subsampling.HShift(c);
|
frame_dim_.xsize_blocks >> frame_header_.chroma_subsampling.HShift(c);
|
||||||
@@ -244,9 +242,17 @@ Status FrameDecoder::InitFrameOutput() {
|
|||||||
1 << frame_header_.chroma_subsampling.RawHShift(c);
|
1 << frame_header_.chroma_subsampling.RawHShift(c);
|
||||||
component.v_samp_factor =
|
component.v_samp_factor =
|
||||||
1 << frame_header_.chroma_subsampling.RawVShift(c);
|
1 << frame_header_.chroma_subsampling.RawVShift(c);
|
||||||
size_t num_blocks = static_cast<size_t>(component.width_in_blocks) *
|
size_t num_blocks;
|
||||||
component.height_in_blocks;
|
if (!SafeMul(static_cast<size_t>(component.width_in_blocks),
|
||||||
component.coeffs.resize(num_blocks * jxl::kDCTBlockSize);
|
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) {
|
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 gx = dc_group_id % frame_dim_.xsize_dc_groups;
|
||||||
const size_t gy = 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 &&
|
if (frame_header_.encoding == FrameEncoding::kVarDCT &&
|
||||||
!(frame_header_.flags & FrameHeader::kUseDcFrame)) {
|
!(frame_header_.flags & FrameHeader::kUseDcFrame)) {
|
||||||
JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeVarDCTDC(
|
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) {
|
if (frame_header_.encoding == FrameEncoding::kVarDCT) {
|
||||||
JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeAcMetadata(
|
JXL_RETURN_IF_ERROR(modular_frame_decoder_.DecodeAcMetadata(
|
||||||
frame_header_, dc_group_id, br, dec_state_));
|
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;
|
decoded_dc_groups_[dc_group_id] = JXL_TRUE;
|
||||||
return true;
|
return true;
|
||||||
@@ -347,10 +350,18 @@ Status FrameDecoder::FinalizeDC() {
|
|||||||
JxlMemoryManager* memory_manager = dec_state_->memory_manager();
|
JxlMemoryManager* memory_manager = dec_state_->memory_manager();
|
||||||
if (frame_header_.encoding == FrameEncoding::kVarDCT &&
|
if (frame_header_.encoding == FrameEncoding::kVarDCT &&
|
||||||
!(frame_header_.flags & FrameHeader::kSkipAdaptiveDCSmoothing) &&
|
!(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(
|
JXL_RETURN_IF_ERROR(AdaptiveDCSmoothing(
|
||||||
memory_manager, dec_state_->shared->quantizer.MulDC(),
|
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;
|
finalized_dc_ = true;
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ Status PatchDictionary::Decode(JxlMemoryManager* memory_manager, BitReader* br,
|
|||||||
size_t xsize, size_t ysize,
|
size_t xsize, size_t ysize,
|
||||||
size_t num_extra_channels,
|
size_t num_extra_channels,
|
||||||
bool* uses_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();
|
positions_.clear();
|
||||||
|
ref_positions_.clear();
|
||||||
|
blendings_.clear();
|
||||||
blendings_stride_ = num_extra_channels + 1;
|
blendings_stride_ = num_extra_channels + 1;
|
||||||
std::vector<uint8_t> context_map;
|
std::vector<uint8_t> context_map;
|
||||||
ANSCode code;
|
ANSCode code;
|
||||||
@@ -76,10 +80,14 @@ Status PatchDictionary::Decode(JxlMemoryManager* memory_manager, BitReader* br,
|
|||||||
ref_pos.y0 = read_num(kPatchReferencePositionContext);
|
ref_pos.y0 = read_num(kPatchReferencePositionContext);
|
||||||
ref_pos.xsize = read_num(kPatchSizeContext) + 1;
|
ref_pos.xsize = read_num(kPatchSizeContext) + 1;
|
||||||
ref_pos.ysize = 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");
|
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");
|
return JXL_FAILURE("Invalid position specified in reference frame");
|
||||||
}
|
}
|
||||||
size_t id_count = read_num(kPatchCountContext);
|
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;
|
pos.y = positions_.back().y + deltay;
|
||||||
}
|
}
|
||||||
if (pos.x + ref_pos.xsize > xsize) {
|
if ((ref_pos.xsize > xsize) || (ref_pos.ysize > ysize)) {
|
||||||
return JXL_FAILURE("Invalid patch x: at %" PRIuS " + %" PRIuS
|
return JXL_FAILURE("Invalid patch position");
|
||||||
" > %" PRIuS,
|
|
||||||
pos.x, ref_pos.xsize, xsize);
|
|
||||||
}
|
}
|
||||||
if (pos.y + ref_pos.ysize > ysize) {
|
if ((pos.x > xsize - ref_pos.xsize) || (pos.y > ysize - ref_pos.ysize)) {
|
||||||
return JXL_FAILURE("Invalid patch y: at %" PRIuS " + %" PRIuS
|
return JXL_FAILURE("Invalid patch position");
|
||||||
" > %" PRIuS,
|
|
||||||
pos.y, ref_pos.ysize, ysize);
|
|
||||||
}
|
}
|
||||||
for (size_t j = 0; j < blendings_stride_; j++) {
|
for (size_t j = 0; j < blendings_stride_; j++) {
|
||||||
uint32_t blend_mode = read_num(kPatchBlendModeContext);
|
uint32_t blend_mode = read_num(kPatchBlendModeContext);
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ class PatchDictionary {
|
|||||||
|
|
||||||
void Clear() {
|
void Clear() {
|
||||||
positions_.clear();
|
positions_.clear();
|
||||||
|
ref_positions_.clear();
|
||||||
|
blendings_.clear();
|
||||||
ComputePatchTree();
|
ComputePatchTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -315,50 +315,6 @@ struct ExtraChannelOutput {
|
|||||||
|
|
||||||
} // namespace
|
} // 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)
|
// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding)
|
||||||
struct JxlDecoder {
|
struct JxlDecoder {
|
||||||
JxlDecoder() = default;
|
JxlDecoder() = default;
|
||||||
@@ -387,7 +343,6 @@ struct JxlDecoder {
|
|||||||
bool got_all_headers; // Codestream metadata headers.
|
bool got_all_headers; // Codestream metadata headers.
|
||||||
bool post_headers; // Already decoding pixels.
|
bool post_headers; // Already decoding pixels.
|
||||||
std::unique_ptr<jxl::ICCReader> icc_reader;
|
std::unique_ptr<jxl::ICCReader> icc_reader;
|
||||||
jxl::JxlDecoderFrameIndexBox frame_index_box;
|
|
||||||
// This means either we actually got the preview image, or determined we
|
// This means either we actually got the preview image, or determined we
|
||||||
// cannot get it or there is none.
|
// cannot get it or there is none.
|
||||||
bool got_preview_image;
|
bool got_preview_image;
|
||||||
@@ -446,7 +401,7 @@ struct JxlDecoder {
|
|||||||
size_t box_count;
|
size_t box_count;
|
||||||
|
|
||||||
// The level of progressive detail in frame decoding.
|
// The level of progressive detail in frame decoding.
|
||||||
JxlProgressiveDetail prog_detail = kDC;
|
JxlProgressiveDetail prog_detail;
|
||||||
// The progressive detail of the current frame.
|
// The progressive detail of the current frame.
|
||||||
JxlProgressiveDetail frame_prog_detail;
|
JxlProgressiveDetail frame_prog_detail;
|
||||||
// The intended downsampling ratio for the current progression step.
|
// 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.
|
// 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_set;
|
||||||
|
bool image_out_buffer_is_for_preview;
|
||||||
|
|
||||||
// Owned by the caller, buffer for preview or full resolution image.
|
// Owned by the caller, buffer for preview or full resolution image.
|
||||||
void* image_out_buffer;
|
void* image_out_buffer;
|
||||||
@@ -731,10 +687,25 @@ bool CheckSizeLimit(JxlDecoder* dec, size_t xsize, size_t ysize) {
|
|||||||
return true;
|
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
|
// 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->stage = DecoderStage::kInited;
|
||||||
dec->got_signature = false;
|
dec->got_signature = false;
|
||||||
dec->last_codestream_seen = false;
|
dec->last_codestream_seen = false;
|
||||||
@@ -743,7 +714,7 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
|
|||||||
dec->got_transform_data = false;
|
dec->got_transform_data = false;
|
||||||
dec->got_all_headers = false;
|
dec->got_all_headers = false;
|
||||||
dec->post_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->got_preview_image = false;
|
||||||
dec->preview_frame = false;
|
dec->preview_frame = false;
|
||||||
dec->file_pos = 0;
|
dec->file_pos = 0;
|
||||||
@@ -770,15 +741,23 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
|
|||||||
dec->box_out_buffer_begin = 0;
|
dec->box_out_buffer_begin = 0;
|
||||||
dec->box_out_buffer_pos = 0;
|
dec->box_out_buffer_pos = 0;
|
||||||
|
|
||||||
|
#if JPEGXL_ENABLE_BOXES
|
||||||
|
dec->box_content_decoder.Reset();
|
||||||
|
#endif
|
||||||
|
|
||||||
#if JPEGXL_ENABLE_TRANSCODE_JPEG
|
#if JPEGXL_ENABLE_TRANSCODE_JPEG
|
||||||
|
dec->metadata_decoder.Reset();
|
||||||
dec->exif_metadata.clear();
|
dec->exif_metadata.clear();
|
||||||
|
dec->exif_metadata.shrink_to_fit();
|
||||||
dec->xmp_metadata.clear();
|
dec->xmp_metadata.clear();
|
||||||
|
dec->xmp_metadata.shrink_to_fit();
|
||||||
dec->store_exif = 0;
|
dec->store_exif = 0;
|
||||||
dec->store_xmp = 0;
|
dec->store_xmp = 0;
|
||||||
dec->recon_out_buffer_pos = 0;
|
dec->recon_out_buffer_pos = 0;
|
||||||
dec->recon_exif_size = 0;
|
dec->recon_exif_size = 0;
|
||||||
dec->recon_xmp_size = 0;
|
dec->recon_xmp_size = 0;
|
||||||
dec->recon_output_jpeg = JpegReconStage::kNone;
|
dec->recon_output_jpeg = JpegReconStage::kNone;
|
||||||
|
dec->jpeg_decoder.Reset();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
dec->events_wanted = dec->orig_events_wanted;
|
dec->events_wanted = dec->orig_events_wanted;
|
||||||
@@ -786,15 +765,9 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
|
|||||||
dec->have_container = false;
|
dec->have_container = false;
|
||||||
dec->box_count = 0;
|
dec->box_count = 0;
|
||||||
dec->downsampling_target = 8;
|
dec->downsampling_target = 8;
|
||||||
dec->image_out_buffer_set = false;
|
ResetImageOut(dec);
|
||||||
dec->image_out_buffer = nullptr;
|
dec->simple_image_out_callback.callback = nullptr;
|
||||||
dec->image_out_init_callback = nullptr;
|
dec->simple_image_out_callback.opaque = 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();
|
|
||||||
dec->next_in = nullptr;
|
dec->next_in = nullptr;
|
||||||
dec->avail_in = 0;
|
dec->avail_in = 0;
|
||||||
dec->input_closed = false;
|
dec->input_closed = false;
|
||||||
@@ -803,13 +776,14 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
|
|||||||
dec->frame_dec.reset();
|
dec->frame_dec.reset();
|
||||||
dec->next_section = 0;
|
dec->next_section = 0;
|
||||||
dec->section_processed.clear();
|
dec->section_processed.clear();
|
||||||
|
dec->section_processed.shrink_to_fit();
|
||||||
|
|
||||||
dec->ib.reset();
|
dec->ib.reset();
|
||||||
dec->metadata = jxl::CodecMetadata();
|
dec->metadata = jxl::CodecMetadata();
|
||||||
dec->image_metadata = dec->metadata.m;
|
dec->image_metadata = dec->metadata.m;
|
||||||
dec->frame_header = jxl::make_unique<jxl::FrameHeader>(&dec->metadata);
|
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_unconsumed = 0;
|
||||||
dec->codestream_pos = 0;
|
dec->codestream_pos = 0;
|
||||||
dec->codestream_bits_ahead = 0;
|
dec->codestream_bits_ahead = 0;
|
||||||
@@ -824,8 +798,10 @@ void JxlDecoderRewindDecodingState(JxlDecoder* dec) {
|
|||||||
dec->external_frames = 0;
|
dec->external_frames = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void JxlDecoderReset(JxlDecoder* dec) {
|
void JxlDecoderReset(JxlDecoder* dec) {
|
||||||
JxlDecoderRewindDecodingState(dec);
|
RewindDecodingState(dec);
|
||||||
|
|
||||||
dec->thread_pool.reset();
|
dec->thread_pool.reset();
|
||||||
dec->keep_orientation = false;
|
dec->keep_orientation = false;
|
||||||
@@ -835,9 +811,13 @@ void JxlDecoderReset(JxlDecoder* dec) {
|
|||||||
dec->desired_intensity_target = 0;
|
dec->desired_intensity_target = 0;
|
||||||
dec->orig_events_wanted = 0;
|
dec->orig_events_wanted = 0;
|
||||||
dec->events_wanted = 0;
|
dec->events_wanted = 0;
|
||||||
|
dec->prog_detail = kDC;
|
||||||
dec->frame_refs.clear();
|
dec->frame_refs.clear();
|
||||||
|
dec->frame_refs.shrink_to_fit();
|
||||||
dec->frame_external_to_internal.clear();
|
dec->frame_external_to_internal.clear();
|
||||||
|
dec->frame_external_to_internal.shrink_to_fit();
|
||||||
dec->frame_required.clear();
|
dec->frame_required.clear();
|
||||||
|
dec->frame_required.shrink_to_fit();
|
||||||
dec->decompress_boxes = false;
|
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) {
|
void JxlDecoderSkipFrames(JxlDecoder* dec, size_t amount) {
|
||||||
// Increment amount, rather than set it: making the amount smaller is
|
// 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->frame_stage = FrameStage::kHeader;
|
||||||
dec->AdvanceCodestream(dec->remaining_frame_size);
|
dec->AdvanceCodestream(dec->remaining_frame_size);
|
||||||
if (dec->is_last_of_still) {
|
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;
|
return JXL_DEC_SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -1462,6 +1443,11 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dec->image_out_buffer_set) {
|
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 xsize;
|
||||||
size_t ysize;
|
size_t ysize;
|
||||||
GetCurrentDimensions(dec, xsize, ysize);
|
GetCurrentDimensions(dec, xsize, ysize);
|
||||||
@@ -1541,8 +1527,7 @@ JxlDecoderStatus JxlDecoderProcessCodestream(JxlDecoder* dec) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (dec->preview_frame || dec->is_last_of_still) {
|
if (dec->preview_frame || dec->is_last_of_still) {
|
||||||
dec->image_out_buffer_set = false;
|
ResetImageOut(dec);
|
||||||
dec->extra_channel_output.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2464,6 +2449,10 @@ size_t JxlDecoderGetIntendedDownsamplingRatio(JxlDecoder* dec) {
|
|||||||
|
|
||||||
JxlDecoderStatus JxlDecoderFlushImage(JxlDecoder* dec) {
|
JxlDecoderStatus JxlDecoderFlushImage(JxlDecoder* dec) {
|
||||||
if (!dec->image_out_buffer_set) return JXL_DEC_ERROR;
|
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) {
|
if (dec->frame_stage != FrameStage::kFull) {
|
||||||
return JXL_DEC_ERROR;
|
return JXL_DEC_ERROR;
|
||||||
}
|
}
|
||||||
@@ -2559,7 +2548,9 @@ JXL_EXPORT JxlDecoderStatus JxlDecoderSetPreviewOutBuffer(
|
|||||||
|
|
||||||
if (size < min_size) return JXL_DEC_ERROR;
|
if (size < min_size) return JXL_DEC_ERROR;
|
||||||
|
|
||||||
|
ResetImageOut(dec, /*clear_ec=*/false);
|
||||||
dec->image_out_buffer_set = true;
|
dec->image_out_buffer_set = true;
|
||||||
|
dec->image_out_buffer_is_for_preview = true;
|
||||||
dec->image_out_buffer = buffer;
|
dec->image_out_buffer = buffer;
|
||||||
dec->image_out_size = size;
|
dec->image_out_size = size;
|
||||||
dec->image_out_format = *format;
|
dec->image_out_format = *format;
|
||||||
@@ -2601,7 +2592,9 @@ JxlDecoderStatus JxlDecoderSetImageOutBuffer(JxlDecoder* dec,
|
|||||||
|
|
||||||
if (size < min_size) return JXL_DEC_ERROR;
|
if (size < min_size) return JXL_DEC_ERROR;
|
||||||
|
|
||||||
|
ResetImageOut(dec, /*clear_ec=*/false);
|
||||||
dec->image_out_buffer_set = true;
|
dec->image_out_buffer_set = true;
|
||||||
|
dec->image_out_buffer_is_for_preview = false;
|
||||||
dec->image_out_buffer = buffer;
|
dec->image_out_buffer = buffer;
|
||||||
dec->image_out_size = size;
|
dec->image_out_size = size;
|
||||||
dec->image_out_format = *format;
|
dec->image_out_format = *format;
|
||||||
@@ -2696,6 +2689,7 @@ JxlDecoderStatus JxlDecoderSetMultithreadedImageOutCallback(
|
|||||||
JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits_sink);
|
JxlDecoderStatus status = PrepareSizeCheck(dec, format, &bits_sink);
|
||||||
if (status != JXL_DEC_SUCCESS) return status;
|
if (status != JXL_DEC_SUCCESS) return status;
|
||||||
|
|
||||||
|
ResetImageOut(dec, /*clear_ec=*/false);
|
||||||
dec->image_out_buffer_set = true;
|
dec->image_out_buffer_set = true;
|
||||||
dec->image_out_init_callback = init_callback;
|
dec->image_out_init_callback = init_callback;
|
||||||
dec->image_out_run_callback = run_callback;
|
dec->image_out_run_callback = run_callback;
|
||||||
|
|||||||
@@ -3072,6 +3072,88 @@ TEST(DecodeTest, SkipCurrentFrameTest) {
|
|||||||
JxlDecoderDestroy(dec);
|
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) {
|
TEST(DecodeTest, SkipFrameTest) {
|
||||||
JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
|
JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
|
||||||
size_t xsize = 90;
|
size_t xsize = 90;
|
||||||
@@ -5036,6 +5118,100 @@ JXL_TRANSCODE_JPEG_TEST(DecodeTest, JPEGReconstructTestCodestream) {
|
|||||||
VerifyJPEGReconstruction(jxl::Bytes(compressed), jxl::Bytes(jpeg_codestream));
|
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) {
|
JXL_TRANSCODE_JPEG_TEST(DecodeTest, JPEGReconstructionTest) {
|
||||||
JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
|
JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
|
||||||
const std::string jpeg_path = "jxl/flower/flower.png.im_q85_420.jpg";
|
const std::string jpeg_path = "jxl/flower/flower.png.im_q85_420.jpg";
|
||||||
@@ -5779,3 +5955,36 @@ TEST(DecodeTest, CloseInput) {
|
|||||||
JxlDecoderCloseInput(dec.get());
|
JxlDecoderCloseInput(dec.get());
|
||||||
EXPECT_EQ(JXL_DEC_ERROR, JxlDecoderProcessInput(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 {
|
class JxlToJpegDecoder {
|
||||||
public:
|
public:
|
||||||
|
JxlToJpegDecoder() { Reset(); }
|
||||||
|
|
||||||
// Returns whether an output buffer is set.
|
// Returns whether an output buffer is set.
|
||||||
bool IsOutputSet() const { return next_out_ != nullptr; }
|
bool IsOutputSet() const { return next_out_ != nullptr; }
|
||||||
|
|
||||||
@@ -56,6 +58,21 @@ class JxlToJpegDecoder {
|
|||||||
return result;
|
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) {
|
void StartBox(bool box_until_eof, size_t contents_size) {
|
||||||
// A new box implies that we clear the buffer.
|
// A new box implies that we clear the buffer.
|
||||||
buffer_.clear();
|
buffer_.clear();
|
||||||
@@ -151,18 +168,18 @@ class JxlToJpegDecoder {
|
|||||||
|
|
||||||
// True if the decoder is currently reading bytes inside a JPEG reconstruction
|
// True if the decoder is currently reading bytes inside a JPEG reconstruction
|
||||||
// box.
|
// box.
|
||||||
bool inside_box_ = false;
|
bool inside_box_;
|
||||||
|
|
||||||
// True if the JPEG reconstruction box had undefined size (all remaining
|
// True if the JPEG reconstruction box had undefined size (all remaining
|
||||||
// bytes).
|
// bytes).
|
||||||
bool box_until_eof_ = false;
|
bool box_until_eof_;
|
||||||
// Size of most recently parsed JPEG reconstruction box contents.
|
// 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.
|
// Next bytes to write JPEG reconstruction to.
|
||||||
uint8_t* next_out_ = nullptr;
|
uint8_t* next_out_;
|
||||||
// Available bytes to write JPEG reconstruction to.
|
// Available bytes to write JPEG reconstruction to.
|
||||||
size_t avail_size_ = 0;
|
size_t avail_size_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#else
|
#else
|
||||||
@@ -178,6 +195,8 @@ class JxlToJpegDecoder {
|
|||||||
}
|
}
|
||||||
size_t ReleaseOutputBuffer() { return 0; }
|
size_t ReleaseOutputBuffer() { return 0; }
|
||||||
|
|
||||||
|
void Reset() {}
|
||||||
|
|
||||||
void StartBox(bool /* box_until_eof */, size_t /* contents_size */) {}
|
void StartBox(bool /* box_until_eof */, size_t /* contents_size */) {}
|
||||||
|
|
||||||
JxlDecoderStatus Process(const uint8_t** next_in, size_t* avail_in) {
|
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