1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "mozilla/dom/ResponsiveImageSelector.h"
8 #include "nsIDocument.h"
9 #include "nsContentUtils.h"
10 #include "nsPresContext.h"
11 #include "nsNetUtil.h"
13 #include "nsCSSParser.h"
14 #include "nsCSSProps.h"
15 #include "nsIMediaList.h"
16 #include "nsRuleNode.h"
17 #include "nsRuleData.h"
19 using namespace mozilla
;
20 using namespace mozilla::dom
;
25 NS_IMPL_CYCLE_COLLECTION(ResponsiveImageSelector
, mContent
)
27 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ResponsiveImageSelector
, AddRef
)
28 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ResponsiveImageSelector
, Release
)
31 ParseInteger(const nsAString
& aString
, int32_t& aInt
)
33 nsContentUtils::ParseHTMLIntegerResultFlags parseResult
;
34 aInt
= nsContentUtils::ParseHTMLInteger(aString
, &parseResult
);
35 return !(parseResult
&
36 ( nsContentUtils::eParseHTMLInteger_Error
|
37 nsContentUtils::eParseHTMLInteger_DidNotConsumeAllInput
|
38 nsContentUtils::eParseHTMLInteger_IsPercent
));
41 ResponsiveImageSelector::ResponsiveImageSelector(nsIContent
*aContent
)
43 mBestCandidateIndex(-1)
47 ResponsiveImageSelector::~ResponsiveImageSelector()
50 // http://www.whatwg.org/specs/web-apps/current-work/#processing-the-image-candidates
52 ResponsiveImageSelector::SetCandidatesFromSourceSet(const nsAString
& aSrcSet
)
54 nsIDocument
* doc
= mContent
? mContent
->OwnerDoc() : nullptr;
55 nsCOMPtr
<nsIURI
> docBaseURI
= mContent
? mContent
->GetBaseURI() : nullptr;
57 if (!mContent
|| !doc
|| !docBaseURI
) {
59 "Should not be parsing SourceSet without a content and document");
63 // Preserve the default source if we have one, it has a separate setter.
64 uint32_t prevNumCandidates
= mCandidates
.Length();
65 nsCOMPtr
<nsIURI
> defaultURL
;
66 if (prevNumCandidates
&& (mCandidates
[prevNumCandidates
- 1].Type() ==
67 ResponsiveImageCandidate::eCandidateType_Default
)) {
68 defaultURL
= mCandidates
[prevNumCandidates
- 1].URL();
73 nsAString::const_iterator iter
, end
;
74 aSrcSet
.BeginReading(iter
);
75 aSrcSet
.EndReading(end
);
77 // Read URL / descriptor pairs
79 nsAString::const_iterator url
, desc
;
82 for (; iter
!= end
&& nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
);
91 for (;iter
!= end
&& !nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
);
95 // Find end of descriptor
96 for (; iter
!= end
&& *iter
!= char16_t(','); ++iter
);
97 const nsDependentSubstring
&descriptor
= Substring(desc
, iter
);
100 nsCOMPtr
<nsIURI
> candidateURL
;
101 rv
= nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL
),
102 Substring(url
, desc
),
105 if (NS_SUCCEEDED(rv
) && candidateURL
) {
106 NS_TryToSetImmutable(candidateURL
);
107 ResponsiveImageCandidate candidate
;
108 if (candidate
.SetParamaterFromDescriptor(descriptor
)) {
109 candidate
.SetURL(candidateURL
);
110 AppendCandidateIfUnique(candidate
);
114 // Advance past comma
120 bool parsedCandidates
= mCandidates
.Length() > 0;
122 // Re-add default to end of list
124 AppendDefaultCandidate(defaultURL
);
127 return parsedCandidates
;
131 ResponsiveImageSelector::SetDefaultSource(const nsAString
& aSpec
)
133 nsIDocument
* doc
= mContent
? mContent
->OwnerDoc() : nullptr;
134 nsCOMPtr
<nsIURI
> docBaseURI
= mContent
? mContent
->GetBaseURI() : nullptr;
136 if (!mContent
|| !doc
|| !docBaseURI
) {
138 "Should not be calling this without a content and document");
139 return NS_ERROR_UNEXPECTED
;
142 if (aSpec
.IsEmpty()) {
143 SetDefaultSource(nullptr);
148 nsCOMPtr
<nsIURI
> candidateURL
;
149 rv
= nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL
),
150 aSpec
, doc
, docBaseURI
);
151 NS_ENSURE_SUCCESS(rv
, rv
);
153 SetDefaultSource(candidateURL
);
158 ResponsiveImageSelector::NumCandidates(bool aIncludeDefault
)
160 uint32_t candidates
= mCandidates
.Length();
162 // If present, the default candidate is the last item
163 if (!aIncludeDefault
&& candidates
&&
164 (mCandidates
[candidates
- 1].Type() ==
165 ResponsiveImageCandidate::eCandidateType_Default
)) {
173 ResponsiveImageSelector::SetDefaultSource(nsIURI
*aURL
)
175 // Check if the last element of our candidates is a default
176 int32_t candidates
= mCandidates
.Length();
177 if (candidates
&& (mCandidates
[candidates
- 1].Type() ==
178 ResponsiveImageCandidate::eCandidateType_Default
)) {
179 mCandidates
.RemoveElementAt(candidates
- 1);
180 if (mBestCandidateIndex
== candidates
- 1) {
181 mBestCandidateIndex
= -1;
185 // Add new default if set
187 AppendDefaultCandidate(aURL
);
192 ResponsiveImageSelector::SetSizesFromDescriptor(const nsAString
& aSizes
)
194 mSizeQueries
.Clear();
196 mBestCandidateIndex
= -1;
198 nsCSSParser cssParser
;
200 if (!cssParser
.ParseSourceSizeList(aSizes
, nullptr, 0,
201 mSizeQueries
, mSizeValues
, true)) {
205 return mSizeQueries
.Length() > 0;
209 ResponsiveImageSelector::AppendCandidateIfUnique(const ResponsiveImageCandidate
& aCandidate
)
211 int numCandidates
= mCandidates
.Length();
213 // With the exception of Default, which should not be added until we are done
214 // building the list, the spec forbids mixing width and explicit density
215 // selectors in the same set.
216 if (numCandidates
&& mCandidates
[0].Type() != aCandidate
.Type()) {
220 // Discard candidates with identical parameters, they will never match
221 for (int i
= 0; i
< numCandidates
; i
++) {
222 if (mCandidates
[i
].HasSameParameter(aCandidate
)) {
227 mBestCandidateIndex
= -1;
228 mCandidates
.AppendElement(aCandidate
);
232 ResponsiveImageSelector::AppendDefaultCandidate(nsIURI
*aURL
)
234 NS_ENSURE_TRUE(aURL
, /* void */);
236 ResponsiveImageCandidate defaultCandidate
;
237 defaultCandidate
.SetParameterDefault();
238 defaultCandidate
.SetURL(aURL
);
239 // We don't use MaybeAppend since we want to keep this even if it can never
240 // match, as it may if the source set changes.
241 mBestCandidateIndex
= -1;
242 mCandidates
.AppendElement(defaultCandidate
);
245 already_AddRefed
<nsIURI
>
246 ResponsiveImageSelector::GetSelectedImageURL()
248 int bestIndex
= GetBestCandidateIndex();
253 nsCOMPtr
<nsIURI
> bestURL
= mCandidates
[bestIndex
].URL();
254 MOZ_ASSERT(bestURL
, "Shouldn't have candidates with no URL in the array");
255 return bestURL
.forget();
259 ResponsiveImageSelector::GetSelectedImageDensity()
261 int bestIndex
= GetBestCandidateIndex();
266 return mCandidates
[bestIndex
].Density(this);
270 ResponsiveImageSelector::SelectImage(bool aReselect
)
272 if (!aReselect
&& mBestCandidateIndex
!= -1) {
273 // Already have selection
277 int oldBest
= mBestCandidateIndex
;
278 mBestCandidateIndex
= -1;
279 return GetBestCandidateIndex() != oldBest
;
283 ResponsiveImageSelector::GetBestCandidateIndex()
285 if (mBestCandidateIndex
!= -1) {
286 return mBestCandidateIndex
;
289 int numCandidates
= mCandidates
.Length();
290 if (!numCandidates
) {
294 nsIDocument
* doc
= mContent
? mContent
->OwnerDoc() : nullptr;
295 nsIPresShell
*shell
= doc
? doc
->GetShell() : nullptr;
296 nsPresContext
*pctx
= shell
? shell
->GetPresContext() : nullptr;
299 MOZ_ASSERT(false, "Unable to find document prescontext");
303 double displayDensity
= pctx
->CSSPixelsToDevPixels(1.0f
);
305 // Per spec, "In a UA-specific manner, choose one image source"
306 // - For now, select the lowest density greater than displayDensity, otherwise
307 // the greatest density available
309 // If the list contains computed width candidates, compute the current
310 // effective image width. Note that we currently disallow both computed and
311 // static density candidates in the same selector, so checking the first
312 // candidate is sufficient.
313 int32_t computedWidth
= -1;
314 if (numCandidates
&& mCandidates
[0].IsComputedFromWidth()) {
315 DebugOnly
<bool> computeResult
= \
316 ComputeFinalWidthForCurrentViewport(&computedWidth
);
317 MOZ_ASSERT(computeResult
,
318 "Computed candidates not allowed without sizes data");
320 // If we have a default candidate in the list, don't consider it when using
321 // computed widths. (It has a static 1.0 density that is inapplicable to a
323 if (numCandidates
> 1 && mCandidates
[numCandidates
- 1].Type() ==
324 ResponsiveImageCandidate::eCandidateType_Default
) {
330 double bestDensity
= -1.0;
331 for (int i
= 0; i
< numCandidates
; i
++) {
332 double candidateDensity
= \
333 (computedWidth
== -1) ? mCandidates
[i
].Density(this)
334 : mCandidates
[i
].Density(computedWidth
);
335 // - If bestIndex is below display density, pick anything larger.
336 // - Otherwise, prefer if less dense than bestDensity but still above
338 if (bestIndex
== -1 ||
339 (bestDensity
< displayDensity
&& candidateDensity
> bestDensity
) ||
340 (candidateDensity
>= displayDensity
&& candidateDensity
< bestDensity
)) {
342 bestDensity
= candidateDensity
;
346 MOZ_ASSERT(bestIndex
>= 0 && bestIndex
< numCandidates
);
347 mBestCandidateIndex
= bestIndex
;
352 ResponsiveImageSelector::ComputeFinalWidthForCurrentViewport(int32_t *aWidth
)
354 unsigned int numSizes
= mSizeQueries
.Length();
355 nsIDocument
* doc
= mContent
? mContent
->OwnerDoc() : nullptr;
356 nsIPresShell
*presShell
= doc
? doc
->GetShell() : nullptr;
357 nsPresContext
*pctx
= presShell
? presShell
->GetPresContext() : nullptr;
360 MOZ_ASSERT(false, "Unable to find presContext for this content");
364 MOZ_ASSERT(numSizes
== mSizeValues
.Length(),
365 "mSizeValues length differs from mSizeQueries");
368 for (i
= 0; i
< numSizes
; i
++) {
369 if (mSizeQueries
[i
]->Matches(pctx
, nullptr)) {
374 nscoord effectiveWidth
;
376 // No match defaults to 100% viewport
377 nsCSSValue
defaultWidth(100.0f
, eCSSUnit_ViewportWidth
);
378 effectiveWidth
= nsRuleNode::CalcLengthWithInitialFont(pctx
,
381 effectiveWidth
= nsRuleNode::CalcLengthWithInitialFont(pctx
,
385 MOZ_ASSERT(effectiveWidth
>= 0);
386 *aWidth
= nsPresContext::AppUnitsToIntCSSPixels(std::max(effectiveWidth
, 0));
390 ResponsiveImageCandidate::ResponsiveImageCandidate()
392 mType
= eCandidateType_Invalid
;
393 mValue
.mDensity
= 1.0;
396 ResponsiveImageCandidate::ResponsiveImageCandidate(nsIURI
*aURL
,
400 mType
= eCandidateType_Density
;
401 mValue
.mDensity
= aDensity
;
406 ResponsiveImageCandidate::SetURL(nsIURI
*aURL
)
412 ResponsiveImageCandidate::SetParameterAsComputedWidth(int32_t aWidth
)
414 mType
= eCandidateType_ComputedFromWidth
;
415 mValue
.mWidth
= aWidth
;
419 ResponsiveImageCandidate::SetParameterDefault()
421 MOZ_ASSERT(mType
== eCandidateType_Invalid
, "double setting candidate type");
423 mType
= eCandidateType_Default
;
424 // mValue shouldn't actually be used for this type, but set it to default
426 mValue
.mDensity
= 1.0;
430 ResponsiveImageCandidate::SetParameterAsDensity(double aDensity
)
432 MOZ_ASSERT(mType
== eCandidateType_Invalid
, "double setting candidate type");
434 mType
= eCandidateType_Density
;
435 mValue
.mDensity
= aDensity
;
439 ResponsiveImageCandidate::SetParamaterFromDescriptor(const nsAString
& aDescriptor
)
441 // Valid input values must be positive, using -1 for not-set
442 double density
= -1.0;
445 nsAString::const_iterator iter
, end
;
446 aDescriptor
.BeginReading(iter
);
447 aDescriptor
.EndReading(end
);
449 // Parse descriptor list
450 // We currently only support a single density descriptor of the form:
451 // <floating-point number>x
452 // Silently ignore other descriptors in the list for forward-compat
453 while (iter
!= end
) {
454 // Skip initial whitespace
455 for (; iter
!= end
&& nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
);
461 nsAString::const_iterator start
= iter
;
462 for (; iter
!= end
&& !nsContentUtils::IsHTMLWhitespace(*iter
); ++iter
);
469 // Iter is at end of descriptor, type is single character previous to that.
470 // Safe because we just verified that iter > start
472 nsAString::const_iterator
type(iter
);
475 const nsDependentSubstring
& valStr
= Substring(start
, type
);
476 if (*type
== char16_t('w')) {
477 int32_t possibleWidth
;
478 if (width
== -1 && density
== -1.0) {
479 if (ParseInteger(valStr
, possibleWidth
) && possibleWidth
> 0) {
480 width
= possibleWidth
;
485 } else if (*type
== char16_t('x')) {
486 if (width
== -1 && density
== -1.0) {
488 double possibleDensity
= PromiseFlatString(valStr
).ToDouble(&rv
);
489 if (NS_SUCCEEDED(rv
) && possibleDensity
> 0.0) {
490 density
= possibleDensity
;
499 SetParameterAsComputedWidth(width
);
500 } else if (density
!= -1.0) {
501 SetParameterAsDensity(density
);
503 // No valid descriptors -> 1.0 density
504 SetParameterAsDensity(1.0);
511 ResponsiveImageCandidate::HasSameParameter(const ResponsiveImageCandidate
& aOther
) const
513 if (aOther
.mType
!= mType
) {
517 if (mType
== eCandidateType_Default
) {
521 if (mType
== eCandidateType_Density
) {
522 return aOther
.mValue
.mDensity
== mValue
.mDensity
;
525 if (mType
== eCandidateType_Invalid
) {
526 MOZ_ASSERT(false, "Comparing invalid candidates?");
528 } else if (mType
== eCandidateType_ComputedFromWidth
) {
529 return aOther
.mValue
.mWidth
== mValue
.mWidth
;
532 MOZ_ASSERT(false, "Somebody forgot to check for all uses of this enum");
536 already_AddRefed
<nsIURI
>
537 ResponsiveImageCandidate::URL() const
539 nsCOMPtr
<nsIURI
> url
= mURL
;
544 ResponsiveImageCandidate::Density(ResponsiveImageSelector
*aSelector
) const
546 if (mType
== eCandidateType_ComputedFromWidth
) {
548 if (!aSelector
->ComputeFinalWidthForCurrentViewport(&width
)) {
551 return Density(width
);
554 // Other types don't need matching width
555 MOZ_ASSERT(mType
== eCandidateType_Default
|| mType
== eCandidateType_Density
,
556 "unhandled candidate type");
561 ResponsiveImageCandidate::Density(int32_t aMatchingWidth
) const
563 if (mType
== eCandidateType_Invalid
) {
564 MOZ_ASSERT(false, "Getting density for uninitialized candidate");
568 if (mType
== eCandidateType_Default
) {
572 if (mType
== eCandidateType_Density
) {
573 return mValue
.mDensity
;
574 } else if (mType
== eCandidateType_ComputedFromWidth
) {
575 if (aMatchingWidth
<= 0) {
576 MOZ_ASSERT(false, "0 or negative matching width is invalid per spec");
579 double density
= double(mValue
.mWidth
) / double(aMatchingWidth
);
580 MOZ_ASSERT(density
> 0.0);
584 MOZ_ASSERT(false, "Unknown candidate type");
589 ResponsiveImageCandidate::IsComputedFromWidth() const
591 if (mType
== eCandidateType_ComputedFromWidth
) {
595 MOZ_ASSERT(mType
== eCandidateType_Default
|| mType
== eCandidateType_Density
,
596 "Unknown candidate type");
601 } // namespace mozilla