1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/dom/ResponsiveImageSelector.h"
8 #include "mozilla/PresShell.h"
9 #include "mozilla/PresShellInlines.h"
10 #include "mozilla/ServoStyleSetInlines.h"
11 #include "mozilla/TextUtils.h"
13 #include "mozilla/dom/Document.h"
14 #include "mozilla/dom/DocumentInlines.h"
15 #include "nsContentUtils.h"
16 #include "nsPresContext.h"
18 #include "nsCSSProps.h"
20 using namespace mozilla
;
21 using namespace mozilla::dom
;
23 namespace mozilla::dom
{
25 NS_IMPL_CYCLE_COLLECTION(ResponsiveImageSelector
, mOwnerNode
)
27 static bool ParseInteger(const nsAString
& aString
, int32_t& aInt
) {
28 nsContentUtils::ParseHTMLIntegerResultFlags parseResult
;
29 aInt
= nsContentUtils::ParseHTMLInteger(aString
, &parseResult
);
30 return !(parseResult
&
31 (nsContentUtils::eParseHTMLInteger_Error
|
32 nsContentUtils::eParseHTMLInteger_DidNotConsumeAllInput
|
33 nsContentUtils::eParseHTMLInteger_NonStandard
));
36 static bool ParseFloat(const nsAString
& aString
, double& aDouble
) {
37 // Check if it is a valid floating-point number first since the result of
38 // nsString.ToDouble() is more lenient than the spec,
39 // https://html.spec.whatwg.org/#valid-floating-point-number
40 nsAString::const_iterator iter
, end
;
41 aString
.BeginReading(iter
);
42 aString
.EndReading(end
);
48 if (*iter
== char16_t('-') && ++iter
== end
) {
52 if (IsAsciiDigit(*iter
)) {
53 for (; iter
!= end
&& IsAsciiDigit(*iter
); ++iter
);
54 } else if (*iter
== char16_t('.')) {
55 // Do nothing, jumps to fraction part
61 if (*iter
== char16_t('.')) {
63 if (iter
== end
|| !IsAsciiDigit(*iter
)) {
64 // U+002E FULL STOP character (.) must be followed by one or more ASCII
69 for (; iter
!= end
&& IsAsciiDigit(*iter
); ++iter
);
72 if (iter
!= end
&& (*iter
== char16_t('e') || *iter
== char16_t('E'))) {
74 if (*iter
== char16_t('-') || *iter
== char16_t('+')) {
78 if (iter
== end
|| !IsAsciiDigit(*iter
)) {
79 // Should have one or more ASCII digits
83 for (; iter
!= end
&& IsAsciiDigit(*iter
); ++iter
);
91 aDouble
= PromiseFlatString(aString
).ToDouble(&rv
);
92 return NS_SUCCEEDED(rv
);
95 ResponsiveImageSelector::ResponsiveImageSelector(nsIContent
* aContent
)
96 : mOwnerNode(aContent
), mSelectedCandidateIndex(-1) {}
98 ResponsiveImageSelector::ResponsiveImageSelector(dom::Document
* aDocument
)
99 : mOwnerNode(aDocument
), mSelectedCandidateIndex(-1) {}
101 ResponsiveImageSelector::~ResponsiveImageSelector() = default;
103 void ResponsiveImageSelector::ParseSourceSet(
104 const nsAString
& aSrcSet
,
105 FunctionRef
<void(ResponsiveImageCandidate
&&)> aCallback
) {
106 nsAString::const_iterator iter
, end
;
107 aSrcSet
.BeginReading(iter
);
108 aSrcSet
.EndReading(end
);
110 // Read URL / descriptor pairs
111 while (iter
!= end
) {
112 nsAString::const_iterator url
, urlEnd
, descriptor
;
114 // Skip whitespace and commas.
115 // Extra commas at this point are a non-fatal syntax error.
116 for (; iter
!= end
&&
117 (nsContentUtils::IsHTMLWhitespace(*iter
) || *iter
== char16_t(','));
127 for (; iter
!= end
&& !nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
);
129 // Omit trailing commas from URL.
130 // Multiple commas are a non-fatal error.
131 while (iter
!= url
) {
132 if (*(--iter
) != char16_t(',')) {
138 const nsDependentSubstring
& urlStr
= Substring(url
, iter
);
140 MOZ_ASSERT(url
!= iter
, "Shouldn't have empty URL at this point");
142 ResponsiveImageCandidate candidate
;
143 if (candidate
.ConsumeDescriptors(iter
, end
)) {
144 candidate
.SetURLSpec(urlStr
);
145 aCallback(std::move(candidate
));
150 // http://www.whatwg.org/specs/web-apps/current-work/#processing-the-image-candidates
151 bool ResponsiveImageSelector::SetCandidatesFromSourceSet(
152 const nsAString
& aSrcSet
, nsIPrincipal
* aTriggeringPrincipal
) {
153 ClearSelectedCandidate();
155 if (!mOwnerNode
|| !mOwnerNode
->GetBaseURI()) {
156 MOZ_ASSERT(false, "Should not be parsing SourceSet without a document");
162 auto eachCandidate
= [&](ResponsiveImageCandidate
&& aCandidate
) {
163 aCandidate
.SetTriggeringPrincipal(
164 nsContentUtils::GetAttrTriggeringPrincipal(
165 Content(), aCandidate
.URLString(), aTriggeringPrincipal
));
166 AppendCandidateIfUnique(std::move(aCandidate
));
169 ParseSourceSet(aSrcSet
, eachCandidate
);
171 bool parsedCandidates
= !mCandidates
.IsEmpty();
173 // Re-add default to end of list
174 MaybeAppendDefaultCandidate();
176 return parsedCandidates
;
179 uint32_t ResponsiveImageSelector::NumCandidates(bool aIncludeDefault
) {
180 uint32_t candidates
= mCandidates
.Length();
182 // If present, the default candidate is the last item
183 if (!aIncludeDefault
&& candidates
&& mCandidates
.LastElement().IsDefault()) {
190 nsIContent
* ResponsiveImageSelector::Content() {
191 return mOwnerNode
->IsContent() ? mOwnerNode
->AsContent() : nullptr;
194 dom::Document
* ResponsiveImageSelector::Document() {
195 return mOwnerNode
->OwnerDoc();
198 void ResponsiveImageSelector::ClearDefaultSource() {
199 ClearSelectedCandidate();
200 // Check if the last element of our candidates is a default
201 if (!mCandidates
.IsEmpty() && mCandidates
.LastElement().IsDefault()) {
202 mCandidates
.RemoveLastElement();
206 void ResponsiveImageSelector::SetDefaultSource(nsIURI
* aURI
,
207 nsIPrincipal
* aPrincipal
) {
208 ClearDefaultSource();
209 mDefaultSourceTriggeringPrincipal
= aPrincipal
;
210 mDefaultSourceURL
= VoidString();
214 CopyUTF8toUTF16(spec
, mDefaultSourceURL
);
216 MaybeAppendDefaultCandidate();
219 void ResponsiveImageSelector::SetDefaultSource(const nsAString
& aURLString
,
220 nsIPrincipal
* aPrincipal
) {
221 ClearDefaultSource();
222 mDefaultSourceTriggeringPrincipal
= aPrincipal
;
223 mDefaultSourceURL
= aURLString
;
224 MaybeAppendDefaultCandidate();
227 void ResponsiveImageSelector::ClearSelectedCandidate() {
228 mSelectedCandidateIndex
= -1;
229 mSelectedCandidateURL
= nullptr;
232 bool ResponsiveImageSelector::SetSizesFromDescriptor(const nsAString
& aSizes
) {
233 ClearSelectedCandidate();
235 NS_ConvertUTF16toUTF8
sizes(aSizes
);
236 mServoSourceSizeList
.reset(Servo_SourceSizeList_Parse(&sizes
));
237 return !!mServoSourceSizeList
;
240 void ResponsiveImageSelector::AppendCandidateIfUnique(
241 ResponsiveImageCandidate
&& aCandidate
) {
242 int numCandidates
= mCandidates
.Length();
244 // With the exception of Default, which should not be added until we are done
245 // building the list.
246 if (aCandidate
.IsDefault()) {
250 // Discard candidates with identical parameters, they will never match
251 for (int i
= 0; i
< numCandidates
; i
++) {
252 if (mCandidates
[i
].HasSameParameter(aCandidate
)) {
257 mCandidates
.AppendElement(std::move(aCandidate
));
260 void ResponsiveImageSelector::MaybeAppendDefaultCandidate() {
261 if (mDefaultSourceURL
.IsEmpty()) {
265 int numCandidates
= mCandidates
.Length();
267 // https://html.spec.whatwg.org/multipage/embedded-content.html#update-the-source-set
269 // If child has a src attribute whose value is not the empty string and source
270 // set does not contain an image source with a density descriptor value of 1,
271 // and no image source with a width descriptor, append child's src attribute
272 // value to source set.
273 for (int i
= 0; i
< numCandidates
; i
++) {
274 if (mCandidates
[i
].IsComputedFromWidth()) {
276 } else if (mCandidates
[i
].Density(this) == 1.0) {
281 ResponsiveImageCandidate defaultCandidate
;
282 defaultCandidate
.SetParameterDefault();
283 defaultCandidate
.SetURLSpec(mDefaultSourceURL
);
284 defaultCandidate
.SetTriggeringPrincipal(mDefaultSourceTriggeringPrincipal
);
285 // We don't use MaybeAppend since we want to keep this even if it can never
286 // match, as it may if the source set changes.
287 mCandidates
.AppendElement(std::move(defaultCandidate
));
290 already_AddRefed
<nsIURI
> ResponsiveImageSelector::GetSelectedImageURL() {
293 nsCOMPtr
<nsIURI
> url
= mSelectedCandidateURL
;
297 bool ResponsiveImageSelector::GetSelectedImageURLSpec(nsAString
& aResult
) {
300 if (mSelectedCandidateIndex
== -1) {
304 aResult
.Assign(mCandidates
[mSelectedCandidateIndex
].URLString());
308 double ResponsiveImageSelector::GetSelectedImageDensity() {
309 int bestIndex
= GetSelectedCandidateIndex();
314 return mCandidates
[bestIndex
].Density(this);
317 nsIPrincipal
* ResponsiveImageSelector::GetSelectedImageTriggeringPrincipal() {
318 int bestIndex
= GetSelectedCandidateIndex();
323 return mCandidates
[bestIndex
].TriggeringPrincipal();
326 bool ResponsiveImageSelector::SelectImage(bool aReselect
) {
327 if (!aReselect
&& mSelectedCandidateIndex
!= -1) {
328 // Already have selection
332 int oldBest
= mSelectedCandidateIndex
;
333 ClearSelectedCandidate();
335 int numCandidates
= mCandidates
.Length();
336 if (!numCandidates
) {
337 return oldBest
!= -1;
340 dom::Document
* doc
= Document();
341 nsPresContext
* pctx
= doc
->GetPresContext();
342 nsCOMPtr
<nsIURI
> baseURI
= mOwnerNode
->GetBaseURI();
344 if (!pctx
|| !baseURI
) {
345 return oldBest
!= -1;
348 double displayDensity
= pctx
->CSSPixelsToDevPixels(1.0f
);
349 double overrideDPPX
= pctx
->GetOverrideDPPX();
351 if (overrideDPPX
> 0) {
352 displayDensity
= overrideDPPX
;
354 if (doc
->ShouldResistFingerprinting(RFPTarget::WindowDevicePixelRatio
)) {
355 displayDensity
= nsRFPService::GetDevicePixelRatioAtZoom(1);
358 // Per spec, "In a UA-specific manner, choose one image source"
359 // - For now, select the lowest density greater than displayDensity, otherwise
360 // the greatest density available
362 // If the list contains computed width candidates, compute the current
363 // effective image width.
364 double computedWidth
= -1;
365 for (int i
= 0; i
< numCandidates
; i
++) {
366 if (mCandidates
[i
].IsComputedFromWidth()) {
367 DebugOnly
<bool> computeResult
=
368 ComputeFinalWidthForCurrentViewport(&computedWidth
);
369 MOZ_ASSERT(computeResult
,
370 "Computed candidates not allowed without sizes data");
376 double bestDensity
= -1.0;
377 for (int i
= 0; i
< numCandidates
; i
++) {
378 double candidateDensity
= (computedWidth
== -1)
379 ? mCandidates
[i
].Density(this)
380 : mCandidates
[i
].Density(computedWidth
);
381 // - If bestIndex is below display density, pick anything larger.
382 // - Otherwise, prefer if less dense than bestDensity but still above
384 if (bestIndex
== -1 ||
385 (bestDensity
< displayDensity
&& candidateDensity
> bestDensity
) ||
386 (candidateDensity
>= displayDensity
&&
387 candidateDensity
< bestDensity
)) {
389 bestDensity
= candidateDensity
;
393 MOZ_ASSERT(bestIndex
>= 0 && bestIndex
< numCandidates
);
397 const nsAString
& urlStr
= mCandidates
[bestIndex
].URLString();
398 nsCOMPtr
<nsIURI
> candidateURL
;
399 rv
= nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL
),
400 urlStr
, doc
, baseURI
);
402 mSelectedCandidateURL
= NS_SUCCEEDED(rv
) ? candidateURL
: nullptr;
403 mSelectedCandidateIndex
= bestIndex
;
405 return mSelectedCandidateIndex
!= oldBest
;
408 int ResponsiveImageSelector::GetSelectedCandidateIndex() {
411 return mSelectedCandidateIndex
;
414 bool ResponsiveImageSelector::ComputeFinalWidthForCurrentViewport(
416 dom::Document
* doc
= Document();
417 PresShell
* presShell
= doc
->GetPresShell();
418 nsPresContext
* pctx
= presShell
? presShell
->GetPresContext() : nullptr;
423 nscoord effectiveWidth
=
424 presShell
->StyleSet()->EvaluateSourceSizeList(mServoSourceSizeList
.get());
427 nsPresContext::AppUnitsToDoubleCSSPixels(std::max(effectiveWidth
, 0));
431 ResponsiveImageCandidate::ResponsiveImageCandidate() {
432 mType
= CandidateType::Invalid
;
433 mValue
.mDensity
= 1.0;
436 void ResponsiveImageCandidate::SetURLSpec(const nsAString
& aURLString
) {
437 mURLString
= aURLString
;
440 void ResponsiveImageCandidate::SetTriggeringPrincipal(
441 nsIPrincipal
* aPrincipal
) {
442 mTriggeringPrincipal
= aPrincipal
;
445 void ResponsiveImageCandidate::SetParameterAsComputedWidth(int32_t aWidth
) {
446 mType
= CandidateType::ComputedFromWidth
;
447 mValue
.mWidth
= aWidth
;
450 void ResponsiveImageCandidate::SetParameterDefault() {
451 MOZ_ASSERT(!IsValid(), "double setting candidate type");
453 mType
= CandidateType::Default
;
454 // mValue shouldn't actually be used for this type, but set it to default
456 mValue
.mDensity
= 1.0;
459 void ResponsiveImageCandidate::SetParameterInvalid() {
460 mType
= CandidateType::Invalid
;
461 // mValue shouldn't actually be used for this type, but set it to default
463 mValue
.mDensity
= 1.0;
466 void ResponsiveImageCandidate::SetParameterAsDensity(double aDensity
) {
467 MOZ_ASSERT(!IsValid(), "double setting candidate type");
469 mType
= CandidateType::Density
;
470 mValue
.mDensity
= aDensity
;
473 // Represents all supported descriptors for a ResponsiveImageCandidate, though
474 // there is no candidate type that uses all of these. This should generally
475 // match the mValue union of ResponsiveImageCandidate.
476 struct ResponsiveImageDescriptors
{
477 ResponsiveImageDescriptors() : mInvalid(false) {};
479 Maybe
<double> mDensity
;
480 Maybe
<int32_t> mWidth
;
481 // We don't support "h" descriptors yet and they are not spec'd, but the
482 // current spec does specify that they can be silently ignored (whereas
483 // entirely unknown descriptors cause us to invalidate the candidate)
485 // If we ever start honoring them we should serialize them in
486 // AppendDescriptors.
487 Maybe
<int32_t> mFutureCompatHeight
;
488 // If this descriptor set is bogus, e.g. a value was added twice (and thus
489 // dropped) or an unknown descriptor was added.
492 void AddDescriptor(const nsAString
& aDescriptor
);
494 // Use the current set of descriptors to configure a candidate
495 void FillCandidate(ResponsiveImageCandidate
& aCandidate
);
498 // Try to parse a single descriptor from a string. If value already set or
499 // unknown, sets invalid flag.
500 // This corresponds to the descriptor "Descriptor parser" step in:
501 // https://html.spec.whatwg.org/#parse-a-srcset-attribute
502 void ResponsiveImageDescriptors::AddDescriptor(const nsAString
& aDescriptor
) {
503 if (aDescriptor
.IsEmpty()) {
507 // All currently supported descriptors end with an identifying character.
508 nsAString::const_iterator descStart
, descType
;
509 aDescriptor
.BeginReading(descStart
);
510 aDescriptor
.EndReading(descType
);
512 const nsDependentSubstring
& valueStr
= Substring(descStart
, descType
);
513 if (*descType
== char16_t('w')) {
514 int32_t possibleWidth
;
515 // If the value is not a valid non-negative integer, it doesn't match this
516 // descriptor, fall through.
517 if (ParseInteger(valueStr
, possibleWidth
) && possibleWidth
>= 0) {
518 if (possibleWidth
!= 0 && mWidth
.isNothing() && mDensity
.isNothing()) {
519 mWidth
.emplace(possibleWidth
);
521 // Valid width descriptor, but width or density were already seen, sizes
522 // support isn't enabled, or it parsed to 0, which is an error per spec
528 } else if (*descType
== char16_t('h')) {
529 int32_t possibleHeight
;
530 // If the value is not a valid non-negative integer, it doesn't match this
531 // descriptor, fall through.
532 if (ParseInteger(valueStr
, possibleHeight
) && possibleHeight
>= 0) {
533 if (possibleHeight
!= 0 && mFutureCompatHeight
.isNothing() &&
534 mDensity
.isNothing()) {
535 mFutureCompatHeight
.emplace(possibleHeight
);
537 // Valid height descriptor, but height or density were already seen, or
538 // it parsed to zero, which is an error per spec
544 } else if (*descType
== char16_t('x')) {
545 // If the value is not a valid floating point number, it doesn't match this
546 // descriptor, fall through.
547 double possibleDensity
= 0.0;
548 if (ParseFloat(valueStr
, possibleDensity
)) {
549 if (possibleDensity
>= 0.0 && mWidth
.isNothing() &&
550 mDensity
.isNothing() && mFutureCompatHeight
.isNothing()) {
551 mDensity
.emplace(possibleDensity
);
553 // Valid density descriptor, but height or width or density were already
554 // seen, or it parsed to less than zero, which is an error per spec
562 // Matched no known descriptor, mark this descriptor set invalid
566 bool ResponsiveImageDescriptors::Valid() {
567 return !mInvalid
&& !(mFutureCompatHeight
.isSome() && mWidth
.isNothing());
570 void ResponsiveImageDescriptors::FillCandidate(
571 ResponsiveImageCandidate
& aCandidate
) {
573 aCandidate
.SetParameterInvalid();
574 } else if (mWidth
.isSome()) {
575 MOZ_ASSERT(mDensity
.isNothing()); // Shouldn't be valid
577 aCandidate
.SetParameterAsComputedWidth(*mWidth
);
578 } else if (mDensity
.isSome()) {
579 MOZ_ASSERT(mWidth
.isNothing()); // Shouldn't be valid
581 aCandidate
.SetParameterAsDensity(*mDensity
);
583 // A valid set of descriptors with no density nor width (e.g. an empty set)
584 // becomes 1.0 density, per spec
585 aCandidate
.SetParameterAsDensity(1.0);
589 bool ResponsiveImageCandidate::ConsumeDescriptors(
590 nsAString::const_iterator
& aIter
,
591 const nsAString::const_iterator
& aIterEnd
) {
592 nsAString::const_iterator
& iter
= aIter
;
593 const nsAString::const_iterator
& end
= aIterEnd
;
595 bool inParens
= false;
597 ResponsiveImageDescriptors descriptors
;
599 // Parse descriptor list.
600 // This corresponds to the descriptor parsing loop from:
601 // https://html.spec.whatwg.org/#parse-a-srcset-attribute
603 // Skip initial whitespace
604 for (; iter
!= end
&& nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
);
606 nsAString::const_iterator currentDescriptor
= iter
;
610 descriptors
.AddDescriptor(Substring(currentDescriptor
, iter
));
612 } else if (inParens
) {
613 if (*iter
== char16_t(')')) {
617 if (*iter
== char16_t(',')) {
618 // End of descriptors, flush current descriptor and advance past comma
620 descriptors
.AddDescriptor(Substring(currentDescriptor
, iter
));
624 if (nsContentUtils::IsHTMLWhitespace(*iter
)) {
625 // End of current descriptor, consume it, skip spaces
626 // ("After descriptor" state in spec) before continuing
627 descriptors
.AddDescriptor(Substring(currentDescriptor
, iter
));
628 for (; iter
!= end
&& nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
);
632 currentDescriptor
= iter
;
633 // Leave one whitespace so the loop advances to this position next
636 } else if (*iter
== char16_t('(')) {
642 descriptors
.FillCandidate(*this);
647 bool ResponsiveImageCandidate::HasSameParameter(
648 const ResponsiveImageCandidate
& aOther
) const {
649 if (aOther
.mType
!= mType
) {
653 if (mType
== CandidateType::Default
) {
657 if (mType
== CandidateType::Density
) {
658 return aOther
.mValue
.mDensity
== mValue
.mDensity
;
661 if (mType
== CandidateType::Invalid
) {
662 MOZ_ASSERT_UNREACHABLE("Comparing invalid candidates?");
666 if (mType
== CandidateType::ComputedFromWidth
) {
667 return aOther
.mValue
.mWidth
== mValue
.mWidth
;
670 MOZ_ASSERT(false, "Somebody forgot to check for all uses of this enum");
674 double ResponsiveImageCandidate::Density(
675 ResponsiveImageSelector
* aSelector
) const {
676 if (mType
== CandidateType::ComputedFromWidth
) {
678 if (!aSelector
->ComputeFinalWidthForCurrentViewport(&width
)) {
681 return Density(width
);
684 // Other types don't need matching width
685 MOZ_ASSERT(mType
== CandidateType::Default
|| mType
== CandidateType::Density
,
686 "unhandled candidate type");
690 void ResponsiveImageCandidate::AppendDescriptors(
691 nsAString
& aDescriptors
) const {
692 MOZ_ASSERT(IsValid());
694 case CandidateType::Default
:
695 case CandidateType::Invalid
:
697 case CandidateType::ComputedFromWidth
:
698 aDescriptors
.Append(' ');
699 aDescriptors
.AppendInt(mValue
.mWidth
);
700 aDescriptors
.Append('w');
702 case CandidateType::Density
:
703 aDescriptors
.Append(' ');
704 aDescriptors
.AppendFloat(mValue
.mDensity
);
705 aDescriptors
.Append('x');
710 double ResponsiveImageCandidate::Density(double aMatchingWidth
) const {
711 if (mType
== CandidateType::Invalid
) {
712 MOZ_ASSERT(false, "Getting density for uninitialized candidate");
716 if (mType
== CandidateType::Default
) {
720 if (mType
== CandidateType::Density
) {
721 return mValue
.mDensity
;
723 if (mType
== CandidateType::ComputedFromWidth
) {
724 if (aMatchingWidth
< 0) {
727 "Don't expect to have a negative matching width at this point");
730 double density
= double(mValue
.mWidth
) / aMatchingWidth
;
731 MOZ_ASSERT(density
> 0.0);
735 MOZ_ASSERT(false, "Unknown candidate type");
739 } // namespace mozilla::dom