diff options
Diffstat (limited to 'dom/base/ResponsiveImageSelector.cpp')
-rw-r--r-- | dom/base/ResponsiveImageSelector.cpp | 730 |
1 files changed, 730 insertions, 0 deletions
diff --git a/dom/base/ResponsiveImageSelector.cpp b/dom/base/ResponsiveImageSelector.cpp new file mode 100644 index 0000000000..29483e0e55 --- /dev/null +++ b/dom/base/ResponsiveImageSelector.cpp @@ -0,0 +1,730 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ResponsiveImageSelector.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresShellInlines.h" +#include "mozilla/ServoStyleSetInlines.h" +#include "mozilla/TextUtils.h" +#include "nsIURI.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "nsContentUtils.h" +#include "nsPresContext.h" + +#include "nsCSSProps.h" + +using namespace mozilla; +using namespace mozilla::dom; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION(ResponsiveImageSelector, mOwnerNode) + +static bool ParseInteger(const nsAString& aString, int32_t& aInt) { + nsContentUtils::ParseHTMLIntegerResultFlags parseResult; + aInt = nsContentUtils::ParseHTMLInteger(aString, &parseResult); + return !(parseResult & + (nsContentUtils::eParseHTMLInteger_Error | + nsContentUtils::eParseHTMLInteger_DidNotConsumeAllInput | + nsContentUtils::eParseHTMLInteger_NonStandard)); +} + +static bool ParseFloat(const nsAString& aString, double& aDouble) { + // Check if it is a valid floating-point number first since the result of + // nsString.ToDouble() is more lenient than the spec, + // https://html.spec.whatwg.org/#valid-floating-point-number + nsAString::const_iterator iter, end; + aString.BeginReading(iter); + aString.EndReading(end); + + if (iter == end) { + return false; + } + + if (*iter == char16_t('-') && ++iter == end) { + return false; + } + + if (IsAsciiDigit(*iter)) { + for (; iter != end && IsAsciiDigit(*iter); ++iter) + ; + } else if (*iter == char16_t('.')) { + // Do nothing, jumps to fraction part + } else { + return false; + } + + // Fraction + if (*iter == char16_t('.')) { + ++iter; + if (iter == end || !IsAsciiDigit(*iter)) { + // U+002E FULL STOP character (.) must be followed by one or more ASCII + // digits + return false; + } + + for (; iter != end && IsAsciiDigit(*iter); ++iter) + ; + } + + if (iter != end && (*iter == char16_t('e') || *iter == char16_t('E'))) { + ++iter; + if (*iter == char16_t('-') || *iter == char16_t('+')) { + ++iter; + } + + if (iter == end || !IsAsciiDigit(*iter)) { + // Should have one or more ASCII digits + return false; + } + + for (; iter != end && IsAsciiDigit(*iter); ++iter) + ; + } + + if (iter != end) { + return false; + } + + nsresult rv; + aDouble = PromiseFlatString(aString).ToDouble(&rv); + return NS_SUCCEEDED(rv); +} + +ResponsiveImageSelector::ResponsiveImageSelector(nsIContent* aContent) + : mOwnerNode(aContent), mSelectedCandidateIndex(-1) {} + +ResponsiveImageSelector::ResponsiveImageSelector(dom::Document* aDocument) + : mOwnerNode(aDocument), mSelectedCandidateIndex(-1) {} + +ResponsiveImageSelector::~ResponsiveImageSelector() = default; + +void ResponsiveImageSelector::ParseSourceSet( + const nsAString& aSrcSet, + FunctionRef<void(ResponsiveImageCandidate&&)> aCallback) { + nsAString::const_iterator iter, end; + aSrcSet.BeginReading(iter); + aSrcSet.EndReading(end); + + // Read URL / descriptor pairs + while (iter != end) { + nsAString::const_iterator url, urlEnd, descriptor; + + // Skip whitespace and commas. + // Extra commas at this point are a non-fatal syntax error. + for (; iter != end && + (nsContentUtils::IsHTMLWhitespace(*iter) || *iter == char16_t(',')); + ++iter) + ; + + if (iter == end) { + break; + } + + url = iter; + + // Find end of url + for (; iter != end && !nsContentUtils::IsHTMLWhitespace(*iter); ++iter) + ; + + // Omit trailing commas from URL. + // Multiple commas are a non-fatal error. + while (iter != url) { + if (*(--iter) != char16_t(',')) { + iter++; + break; + } + } + + const nsDependentSubstring& urlStr = Substring(url, iter); + + MOZ_ASSERT(url != iter, "Shouldn't have empty URL at this point"); + + ResponsiveImageCandidate candidate; + if (candidate.ConsumeDescriptors(iter, end)) { + candidate.SetURLSpec(urlStr); + aCallback(std::move(candidate)); + } + } +} + +// http://www.whatwg.org/specs/web-apps/current-work/#processing-the-image-candidates +bool ResponsiveImageSelector::SetCandidatesFromSourceSet( + const nsAString& aSrcSet, nsIPrincipal* aTriggeringPrincipal) { + ClearSelectedCandidate(); + + if (!mOwnerNode || !mOwnerNode->GetBaseURI()) { + MOZ_ASSERT(false, "Should not be parsing SourceSet without a document"); + return false; + } + + mCandidates.Clear(); + + auto eachCandidate = [&](ResponsiveImageCandidate&& aCandidate) { + aCandidate.SetTriggeringPrincipal( + nsContentUtils::GetAttrTriggeringPrincipal( + Content(), aCandidate.URLString(), aTriggeringPrincipal)); + AppendCandidateIfUnique(std::move(aCandidate)); + }; + + ParseSourceSet(aSrcSet, eachCandidate); + + bool parsedCandidates = !mCandidates.IsEmpty(); + + // Re-add default to end of list + MaybeAppendDefaultCandidate(); + + return parsedCandidates; +} + +uint32_t ResponsiveImageSelector::NumCandidates(bool aIncludeDefault) { + uint32_t candidates = mCandidates.Length(); + + // If present, the default candidate is the last item + if (!aIncludeDefault && candidates && mCandidates.LastElement().IsDefault()) { + candidates--; + } + + return candidates; +} + +nsIContent* ResponsiveImageSelector::Content() { + return mOwnerNode->IsContent() ? mOwnerNode->AsContent() : nullptr; +} + +dom::Document* ResponsiveImageSelector::Document() { + return mOwnerNode->OwnerDoc(); +} + +void ResponsiveImageSelector::SetDefaultSource(const nsAString& aURLString, + nsIPrincipal* aPrincipal) { + ClearSelectedCandidate(); + + // Check if the last element of our candidates is a default + if (!mCandidates.IsEmpty() && mCandidates.LastElement().IsDefault()) { + mCandidates.RemoveLastElement(); + } + + mDefaultSourceURL = aURLString; + mDefaultSourceTriggeringPrincipal = aPrincipal; + + // Add new default to end of list + MaybeAppendDefaultCandidate(); +} + +void ResponsiveImageSelector::ClearSelectedCandidate() { + mSelectedCandidateIndex = -1; + mSelectedCandidateURL = nullptr; +} + +bool ResponsiveImageSelector::SetSizesFromDescriptor(const nsAString& aSizes) { + ClearSelectedCandidate(); + + NS_ConvertUTF16toUTF8 sizes(aSizes); + mServoSourceSizeList.reset(Servo_SourceSizeList_Parse(&sizes)); + return !!mServoSourceSizeList; +} + +void ResponsiveImageSelector::AppendCandidateIfUnique( + ResponsiveImageCandidate&& aCandidate) { + int numCandidates = mCandidates.Length(); + + // With the exception of Default, which should not be added until we are done + // building the list. + if (aCandidate.IsDefault()) { + return; + } + + // Discard candidates with identical parameters, they will never match + for (int i = 0; i < numCandidates; i++) { + if (mCandidates[i].HasSameParameter(aCandidate)) { + return; + } + } + + mCandidates.AppendElement(std::move(aCandidate)); +} + +void ResponsiveImageSelector::MaybeAppendDefaultCandidate() { + if (mDefaultSourceURL.IsEmpty()) { + return; + } + + int numCandidates = mCandidates.Length(); + + // https://html.spec.whatwg.org/multipage/embedded-content.html#update-the-source-set + // step 4.1.3: + // If child has a src attribute whose value is not the empty string and source + // set does not contain an image source with a density descriptor value of 1, + // and no image source with a width descriptor, append child's src attribute + // value to source set. + for (int i = 0; i < numCandidates; i++) { + if (mCandidates[i].IsComputedFromWidth()) { + return; + } else if (mCandidates[i].Density(this) == 1.0) { + return; + } + } + + ResponsiveImageCandidate defaultCandidate; + defaultCandidate.SetParameterDefault(); + defaultCandidate.SetURLSpec(mDefaultSourceURL); + defaultCandidate.SetTriggeringPrincipal(mDefaultSourceTriggeringPrincipal); + // We don't use MaybeAppend since we want to keep this even if it can never + // match, as it may if the source set changes. + mCandidates.AppendElement(std::move(defaultCandidate)); +} + +already_AddRefed<nsIURI> ResponsiveImageSelector::GetSelectedImageURL() { + SelectImage(); + + nsCOMPtr<nsIURI> url = mSelectedCandidateURL; + return url.forget(); +} + +bool ResponsiveImageSelector::GetSelectedImageURLSpec(nsAString& aResult) { + SelectImage(); + + if (mSelectedCandidateIndex == -1) { + return false; + } + + aResult.Assign(mCandidates[mSelectedCandidateIndex].URLString()); + return true; +} + +double ResponsiveImageSelector::GetSelectedImageDensity() { + int bestIndex = GetSelectedCandidateIndex(); + if (bestIndex < 0) { + return 1.0; + } + + return mCandidates[bestIndex].Density(this); +} + +nsIPrincipal* ResponsiveImageSelector::GetSelectedImageTriggeringPrincipal() { + int bestIndex = GetSelectedCandidateIndex(); + if (bestIndex < 0) { + return nullptr; + } + + return mCandidates[bestIndex].TriggeringPrincipal(); +} + +bool ResponsiveImageSelector::SelectImage(bool aReselect) { + if (!aReselect && mSelectedCandidateIndex != -1) { + // Already have selection + return false; + } + + int oldBest = mSelectedCandidateIndex; + ClearSelectedCandidate(); + + int numCandidates = mCandidates.Length(); + if (!numCandidates) { + return oldBest != -1; + } + + dom::Document* doc = Document(); + nsPresContext* pctx = doc->GetPresContext(); + nsCOMPtr<nsIURI> baseURI = mOwnerNode->GetBaseURI(); + + if (!pctx || !baseURI) { + return oldBest != -1; + } + + double displayDensity = pctx->CSSPixelsToDevPixels(1.0f); + double overrideDPPX = pctx->GetOverrideDPPX(); + + if (overrideDPPX > 0) { + displayDensity = overrideDPPX; + } + + // Per spec, "In a UA-specific manner, choose one image source" + // - For now, select the lowest density greater than displayDensity, otherwise + // the greatest density available + + // If the list contains computed width candidates, compute the current + // effective image width. + double computedWidth = -1; + for (int i = 0; i < numCandidates; i++) { + if (mCandidates[i].IsComputedFromWidth()) { + DebugOnly<bool> computeResult = + ComputeFinalWidthForCurrentViewport(&computedWidth); + MOZ_ASSERT(computeResult, + "Computed candidates not allowed without sizes data"); + break; + } + } + + int bestIndex = -1; + double bestDensity = -1.0; + for (int i = 0; i < numCandidates; i++) { + double candidateDensity = (computedWidth == -1) + ? mCandidates[i].Density(this) + : mCandidates[i].Density(computedWidth); + // - If bestIndex is below display density, pick anything larger. + // - Otherwise, prefer if less dense than bestDensity but still above + // displayDensity. + if (bestIndex == -1 || + (bestDensity < displayDensity && candidateDensity > bestDensity) || + (candidateDensity >= displayDensity && + candidateDensity < bestDensity)) { + bestIndex = i; + bestDensity = candidateDensity; + } + } + + MOZ_ASSERT(bestIndex >= 0 && bestIndex < numCandidates); + + // Resolve URL + nsresult rv; + const nsAString& urlStr = mCandidates[bestIndex].URLString(); + nsCOMPtr<nsIURI> candidateURL; + rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL), + urlStr, doc, baseURI); + + mSelectedCandidateURL = NS_SUCCEEDED(rv) ? candidateURL : nullptr; + mSelectedCandidateIndex = bestIndex; + + return mSelectedCandidateIndex != oldBest; +} + +int ResponsiveImageSelector::GetSelectedCandidateIndex() { + SelectImage(); + + return mSelectedCandidateIndex; +} + +bool ResponsiveImageSelector::ComputeFinalWidthForCurrentViewport( + double* aWidth) { + dom::Document* doc = Document(); + PresShell* presShell = doc->GetPresShell(); + nsPresContext* pctx = presShell ? presShell->GetPresContext() : nullptr; + + if (!pctx) { + return false; + } + nscoord effectiveWidth = + presShell->StyleSet()->EvaluateSourceSizeList(mServoSourceSizeList.get()); + + *aWidth = + nsPresContext::AppUnitsToDoubleCSSPixels(std::max(effectiveWidth, 0)); + return true; +} + +ResponsiveImageCandidate::ResponsiveImageCandidate() { + mType = CandidateType::Invalid; + mValue.mDensity = 1.0; +} + +void ResponsiveImageCandidate::SetURLSpec(const nsAString& aURLString) { + mURLString = aURLString; +} + +void ResponsiveImageCandidate::SetTriggeringPrincipal( + nsIPrincipal* aPrincipal) { + mTriggeringPrincipal = aPrincipal; +} + +void ResponsiveImageCandidate::SetParameterAsComputedWidth(int32_t aWidth) { + mType = CandidateType::ComputedFromWidth; + mValue.mWidth = aWidth; +} + +void ResponsiveImageCandidate::SetParameterDefault() { + MOZ_ASSERT(!IsValid(), "double setting candidate type"); + + mType = CandidateType::Default; + // mValue shouldn't actually be used for this type, but set it to default + // anyway + mValue.mDensity = 1.0; +} + +void ResponsiveImageCandidate::SetParameterInvalid() { + mType = CandidateType::Invalid; + // mValue shouldn't actually be used for this type, but set it to default + // anyway + mValue.mDensity = 1.0; +} + +void ResponsiveImageCandidate::SetParameterAsDensity(double aDensity) { + MOZ_ASSERT(!IsValid(), "double setting candidate type"); + + mType = CandidateType::Density; + mValue.mDensity = aDensity; +} + +// Represents all supported descriptors for a ResponsiveImageCandidate, though +// there is no candidate type that uses all of these. This should generally +// match the mValue union of ResponsiveImageCandidate. +struct ResponsiveImageDescriptors { + ResponsiveImageDescriptors() : mInvalid(false){}; + + Maybe<double> mDensity; + Maybe<int32_t> mWidth; + // We don't support "h" descriptors yet and they are not spec'd, but the + // current spec does specify that they can be silently ignored (whereas + // entirely unknown descriptors cause us to invalidate the candidate) + // + // If we ever start honoring them we should serialize them in + // AppendDescriptors. + Maybe<int32_t> mFutureCompatHeight; + // If this descriptor set is bogus, e.g. a value was added twice (and thus + // dropped) or an unknown descriptor was added. + bool mInvalid; + + void AddDescriptor(const nsAString& aDescriptor); + bool Valid(); + // Use the current set of descriptors to configure a candidate + void FillCandidate(ResponsiveImageCandidate& aCandidate); +}; + +// Try to parse a single descriptor from a string. If value already set or +// unknown, sets invalid flag. +// This corresponds to the descriptor "Descriptor parser" step in: +// https://html.spec.whatwg.org/#parse-a-srcset-attribute +void ResponsiveImageDescriptors::AddDescriptor(const nsAString& aDescriptor) { + if (aDescriptor.IsEmpty()) { + return; + } + + // All currently supported descriptors end with an identifying character. + nsAString::const_iterator descStart, descType; + aDescriptor.BeginReading(descStart); + aDescriptor.EndReading(descType); + descType--; + const nsDependentSubstring& valueStr = Substring(descStart, descType); + if (*descType == char16_t('w')) { + int32_t possibleWidth; + // If the value is not a valid non-negative integer, it doesn't match this + // descriptor, fall through. + if (ParseInteger(valueStr, possibleWidth) && possibleWidth >= 0) { + if (possibleWidth != 0 && mWidth.isNothing() && mDensity.isNothing()) { + mWidth.emplace(possibleWidth); + } else { + // Valid width descriptor, but width or density were already seen, sizes + // support isn't enabled, or it parsed to 0, which is an error per spec + mInvalid = true; + } + + return; + } + } else if (*descType == char16_t('h')) { + int32_t possibleHeight; + // If the value is not a valid non-negative integer, it doesn't match this + // descriptor, fall through. + if (ParseInteger(valueStr, possibleHeight) && possibleHeight >= 0) { + if (possibleHeight != 0 && mFutureCompatHeight.isNothing() && + mDensity.isNothing()) { + mFutureCompatHeight.emplace(possibleHeight); + } else { + // Valid height descriptor, but height or density were already seen, or + // it parsed to zero, which is an error per spec + mInvalid = true; + } + + return; + } + } else if (*descType == char16_t('x')) { + // If the value is not a valid floating point number, it doesn't match this + // descriptor, fall through. + double possibleDensity = 0.0; + if (ParseFloat(valueStr, possibleDensity)) { + if (possibleDensity >= 0.0 && mWidth.isNothing() && + mDensity.isNothing() && mFutureCompatHeight.isNothing()) { + mDensity.emplace(possibleDensity); + } else { + // Valid density descriptor, but height or width or density were already + // seen, or it parsed to less than zero, which is an error per spec + mInvalid = true; + } + + return; + } + } + + // Matched no known descriptor, mark this descriptor set invalid + mInvalid = true; +} + +bool ResponsiveImageDescriptors::Valid() { + return !mInvalid && !(mFutureCompatHeight.isSome() && mWidth.isNothing()); +} + +void ResponsiveImageDescriptors::FillCandidate( + ResponsiveImageCandidate& aCandidate) { + if (!Valid()) { + aCandidate.SetParameterInvalid(); + } else if (mWidth.isSome()) { + MOZ_ASSERT(mDensity.isNothing()); // Shouldn't be valid + + aCandidate.SetParameterAsComputedWidth(*mWidth); + } else if (mDensity.isSome()) { + MOZ_ASSERT(mWidth.isNothing()); // Shouldn't be valid + + aCandidate.SetParameterAsDensity(*mDensity); + } else { + // A valid set of descriptors with no density nor width (e.g. an empty set) + // becomes 1.0 density, per spec + aCandidate.SetParameterAsDensity(1.0); + } +} + +bool ResponsiveImageCandidate::ConsumeDescriptors( + nsAString::const_iterator& aIter, + const nsAString::const_iterator& aIterEnd) { + nsAString::const_iterator& iter = aIter; + const nsAString::const_iterator& end = aIterEnd; + + bool inParens = false; + + ResponsiveImageDescriptors descriptors; + + // Parse descriptor list. + // This corresponds to the descriptor parsing loop from: + // https://html.spec.whatwg.org/#parse-a-srcset-attribute + + // Skip initial whitespace + for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter) + ; + + nsAString::const_iterator currentDescriptor = iter; + + for (;; iter++) { + if (iter == end) { + descriptors.AddDescriptor(Substring(currentDescriptor, iter)); + break; + } else if (inParens) { + if (*iter == char16_t(')')) { + inParens = false; + } + } else { + if (*iter == char16_t(',')) { + // End of descriptors, flush current descriptor and advance past comma + // before breaking + descriptors.AddDescriptor(Substring(currentDescriptor, iter)); + iter++; + break; + } + if (nsContentUtils::IsHTMLWhitespace(*iter)) { + // End of current descriptor, consume it, skip spaces + // ("After descriptor" state in spec) before continuing + descriptors.AddDescriptor(Substring(currentDescriptor, iter)); + for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter) + ; + if (iter == end) { + break; + } + currentDescriptor = iter; + // Leave one whitespace so the loop advances to this position next + // iteration + iter--; + } else if (*iter == char16_t('(')) { + inParens = true; + } + } + } + + descriptors.FillCandidate(*this); + + return IsValid(); +} + +bool ResponsiveImageCandidate::HasSameParameter( + const ResponsiveImageCandidate& aOther) const { + if (aOther.mType != mType) { + return false; + } + + if (mType == CandidateType::Default) { + return true; + } + + if (mType == CandidateType::Density) { + return aOther.mValue.mDensity == mValue.mDensity; + } + + if (mType == CandidateType::Invalid) { + MOZ_ASSERT_UNREACHABLE("Comparing invalid candidates?"); + return true; + } + + if (mType == CandidateType::ComputedFromWidth) { + return aOther.mValue.mWidth == mValue.mWidth; + } + + MOZ_ASSERT(false, "Somebody forgot to check for all uses of this enum"); + return false; +} + +double ResponsiveImageCandidate::Density( + ResponsiveImageSelector* aSelector) const { + if (mType == CandidateType::ComputedFromWidth) { + double width; + if (!aSelector->ComputeFinalWidthForCurrentViewport(&width)) { + return 1.0; + } + return Density(width); + } + + // Other types don't need matching width + MOZ_ASSERT(mType == CandidateType::Default || mType == CandidateType::Density, + "unhandled candidate type"); + return Density(-1); +} + +void ResponsiveImageCandidate::AppendDescriptors( + nsAString& aDescriptors) const { + MOZ_ASSERT(IsValid()); + switch (mType) { + case CandidateType::Default: + case CandidateType::Invalid: + return; + case CandidateType::ComputedFromWidth: + aDescriptors.Append(' '); + aDescriptors.AppendInt(mValue.mWidth); + aDescriptors.Append('w'); + return; + case CandidateType::Density: + aDescriptors.Append(' '); + aDescriptors.AppendFloat(mValue.mDensity); + aDescriptors.Append('x'); + return; + } +} + +double ResponsiveImageCandidate::Density(double aMatchingWidth) const { + if (mType == CandidateType::Invalid) { + MOZ_ASSERT(false, "Getting density for uninitialized candidate"); + return 1.0; + } + + if (mType == CandidateType::Default) { + return 1.0; + } + + if (mType == CandidateType::Density) { + return mValue.mDensity; + } + if (mType == CandidateType::ComputedFromWidth) { + if (aMatchingWidth < 0) { + MOZ_ASSERT( + false, + "Don't expect to have a negative matching width at this point"); + return 1.0; + } + double density = double(mValue.mWidth) / aMatchingWidth; + MOZ_ASSERT(density > 0.0); + return density; + } + + MOZ_ASSERT(false, "Unknown candidate type"); + return 1.0; +} + +} // namespace mozilla::dom |