/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 "nsIURI.h" #include "nsIDocument.h" #include "nsContentUtils.h" #include "nsPresContext.h" #include "nsNetUtil.h" #include "nsCSSParser.h" #include "nsCSSProps.h" #include "nsIMediaList.h" #include "nsRuleNode.h" #include "nsRuleData.h" using namespace mozilla; using namespace mozilla::dom; namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTION(ResponsiveImageSelector, mContent) NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ResponsiveImageSelector, AddRef) NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ResponsiveImageSelector, Release) 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_IsPercent )); } ResponsiveImageSelector::ResponsiveImageSelector(nsIContent *aContent) : mContent(aContent), mBestCandidateIndex(-1) { } ResponsiveImageSelector::~ResponsiveImageSelector() {} // http://www.whatwg.org/specs/web-apps/current-work/#processing-the-image-candidates bool ResponsiveImageSelector::SetCandidatesFromSourceSet(const nsAString & aSrcSet) { nsIDocument* doc = mContent ? mContent->OwnerDoc() : nullptr; nsCOMPtr docBaseURI = mContent ? mContent->GetBaseURI() : nullptr; if (!mContent || !doc || !docBaseURI) { MOZ_ASSERT(false, "Should not be parsing SourceSet without a content and document"); return false; } // Preserve the default source if we have one, it has a separate setter. uint32_t prevNumCandidates = mCandidates.Length(); nsCOMPtr defaultURL; if (prevNumCandidates && (mCandidates[prevNumCandidates - 1].Type() == ResponsiveImageCandidate::eCandidateType_Default)) { defaultURL = mCandidates[prevNumCandidates - 1].URL(); } mCandidates.Clear(); nsAString::const_iterator iter, end; aSrcSet.BeginReading(iter); aSrcSet.EndReading(end); // Read URL / descriptor pairs while (iter != end) { nsAString::const_iterator url, desc; // Skip whitespace for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter); if (iter == end) { break; } url = iter; // Find end of url for (;iter != end && !nsContentUtils::IsHTMLWhitespace(*iter); ++iter); desc = iter; // Find end of descriptor for (; iter != end && *iter != char16_t(','); ++iter); const nsDependentSubstring &descriptor = Substring(desc, iter); nsresult rv; nsCOMPtr candidateURL; rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL), Substring(url, desc), doc, docBaseURI); if (NS_SUCCEEDED(rv) && candidateURL) { NS_TryToSetImmutable(candidateURL); ResponsiveImageCandidate candidate; if (candidate.SetParamaterFromDescriptor(descriptor)) { candidate.SetURL(candidateURL); AppendCandidateIfUnique(candidate); } } // Advance past comma if (iter != end) { ++iter; } } bool parsedCandidates = mCandidates.Length() > 0; // Re-add default to end of list if (defaultURL) { AppendDefaultCandidate(defaultURL); } return parsedCandidates; } nsresult ResponsiveImageSelector::SetDefaultSource(const nsAString & aSpec) { nsIDocument* doc = mContent ? mContent->OwnerDoc() : nullptr; nsCOMPtr docBaseURI = mContent ? mContent->GetBaseURI() : nullptr; if (!mContent || !doc || !docBaseURI) { MOZ_ASSERT(false, "Should not be calling this without a content and document"); return NS_ERROR_UNEXPECTED; } if (aSpec.IsEmpty()) { SetDefaultSource(nullptr); return NS_OK; } nsresult rv; nsCOMPtr candidateURL; rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL), aSpec, doc, docBaseURI); NS_ENSURE_SUCCESS(rv, rv); SetDefaultSource(candidateURL); return NS_OK; } uint32_t ResponsiveImageSelector::NumCandidates(bool aIncludeDefault) { uint32_t candidates = mCandidates.Length(); // If present, the default candidate is the last item if (!aIncludeDefault && candidates && (mCandidates[candidates - 1].Type() == ResponsiveImageCandidate::eCandidateType_Default)) { candidates--; } return candidates; } void ResponsiveImageSelector::SetDefaultSource(nsIURI *aURL) { // Check if the last element of our candidates is a default int32_t candidates = mCandidates.Length(); if (candidates && (mCandidates[candidates - 1].Type() == ResponsiveImageCandidate::eCandidateType_Default)) { mCandidates.RemoveElementAt(candidates - 1); if (mBestCandidateIndex == candidates - 1) { mBestCandidateIndex = -1; } } // Add new default if set if (aURL) { AppendDefaultCandidate(aURL); } } bool ResponsiveImageSelector::SetSizesFromDescriptor(const nsAString & aSizes) { mSizeQueries.Clear(); mSizeValues.Clear(); mBestCandidateIndex = -1; nsCSSParser cssParser; if (!cssParser.ParseSourceSizeList(aSizes, nullptr, 0, mSizeQueries, mSizeValues, true)) { return false; } return mSizeQueries.Length() > 0; } void ResponsiveImageSelector::AppendCandidateIfUnique(const ResponsiveImageCandidate & aCandidate) { int numCandidates = mCandidates.Length(); // With the exception of Default, which should not be added until we are done // building the list, the spec forbids mixing width and explicit density // selectors in the same set. if (numCandidates && mCandidates[0].Type() != aCandidate.Type()) { return; } // Discard candidates with identical parameters, they will never match for (int i = 0; i < numCandidates; i++) { if (mCandidates[i].HasSameParameter(aCandidate)) { return; } } mBestCandidateIndex = -1; mCandidates.AppendElement(aCandidate); } void ResponsiveImageSelector::AppendDefaultCandidate(nsIURI *aURL) { NS_ENSURE_TRUE(aURL, /* void */); ResponsiveImageCandidate defaultCandidate; defaultCandidate.SetParameterDefault(); defaultCandidate.SetURL(aURL); // 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. mBestCandidateIndex = -1; mCandidates.AppendElement(defaultCandidate); } already_AddRefed ResponsiveImageSelector::GetSelectedImageURL() { int bestIndex = GetBestCandidateIndex(); if (bestIndex < 0) { return nullptr; } nsCOMPtr bestURL = mCandidates[bestIndex].URL(); MOZ_ASSERT(bestURL, "Shouldn't have candidates with no URL in the array"); return bestURL.forget(); } double ResponsiveImageSelector::GetSelectedImageDensity() { int bestIndex = GetBestCandidateIndex(); if (bestIndex < 0) { return 1.0; } return mCandidates[bestIndex].Density(this); } bool ResponsiveImageSelector::SelectImage(bool aReselect) { if (!aReselect && mBestCandidateIndex != -1) { // Already have selection return false; } int oldBest = mBestCandidateIndex; mBestCandidateIndex = -1; return GetBestCandidateIndex() != oldBest; } int ResponsiveImageSelector::GetBestCandidateIndex() { if (mBestCandidateIndex != -1) { return mBestCandidateIndex; } int numCandidates = mCandidates.Length(); if (!numCandidates) { return -1; } nsIDocument* doc = mContent ? mContent->OwnerDoc() : nullptr; nsIPresShell *shell = doc ? doc->GetShell() : nullptr; nsPresContext *pctx = shell ? shell->GetPresContext() : nullptr; if (!pctx) { MOZ_ASSERT(false, "Unable to find document prescontext"); return -1; } double displayDensity = pctx->CSSPixelsToDevPixels(1.0f); // 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. Note that we currently disallow both computed and // static density candidates in the same selector, so checking the first // candidate is sufficient. int32_t computedWidth = -1; if (numCandidates && mCandidates[0].IsComputedFromWidth()) { DebugOnly computeResult = \ ComputeFinalWidthForCurrentViewport(&computedWidth); MOZ_ASSERT(computeResult, "Computed candidates not allowed without sizes data"); // If we have a default candidate in the list, don't consider it when using // computed widths. (It has a static 1.0 density that is inapplicable to a // sized-image) if (numCandidates > 1 && mCandidates[numCandidates - 1].Type() == ResponsiveImageCandidate::eCandidateType_Default) { numCandidates--; } } 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); mBestCandidateIndex = bestIndex; return bestIndex; } bool ResponsiveImageSelector::ComputeFinalWidthForCurrentViewport(int32_t *aWidth) { unsigned int numSizes = mSizeQueries.Length(); nsIDocument* doc = mContent ? mContent->OwnerDoc() : nullptr; nsIPresShell *presShell = doc ? doc->GetShell() : nullptr; nsPresContext *pctx = presShell ? presShell->GetPresContext() : nullptr; if (!pctx) { MOZ_ASSERT(false, "Unable to find presContext for this content"); return false; } MOZ_ASSERT(numSizes == mSizeValues.Length(), "mSizeValues length differs from mSizeQueries"); unsigned int i; for (i = 0; i < numSizes; i++) { if (mSizeQueries[i]->Matches(pctx, nullptr)) { break; } } nscoord effectiveWidth; if (i == numSizes) { // No match defaults to 100% viewport nsCSSValue defaultWidth(100.0f, eCSSUnit_ViewportWidth); effectiveWidth = nsRuleNode::CalcLengthWithInitialFont(pctx, defaultWidth); } else { effectiveWidth = nsRuleNode::CalcLengthWithInitialFont(pctx, mSizeValues[i]); } MOZ_ASSERT(effectiveWidth >= 0); *aWidth = nsPresContext::AppUnitsToIntCSSPixels(std::max(effectiveWidth, 0)); return true; } ResponsiveImageCandidate::ResponsiveImageCandidate() { mType = eCandidateType_Invalid; mValue.mDensity = 1.0; } ResponsiveImageCandidate::ResponsiveImageCandidate(nsIURI *aURL, double aDensity) : mURL(aURL) { mType = eCandidateType_Density; mValue.mDensity = aDensity; } void ResponsiveImageCandidate::SetURL(nsIURI *aURL) { mURL = aURL; } void ResponsiveImageCandidate::SetParameterAsComputedWidth(int32_t aWidth) { mType = eCandidateType_ComputedFromWidth; mValue.mWidth = aWidth; } void ResponsiveImageCandidate::SetParameterDefault() { MOZ_ASSERT(mType == eCandidateType_Invalid, "double setting candidate type"); mType = eCandidateType_Default; // 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(mType == eCandidateType_Invalid, "double setting candidate type"); mType = eCandidateType_Density; mValue.mDensity = aDensity; } bool ResponsiveImageCandidate::SetParamaterFromDescriptor(const nsAString & aDescriptor) { // Valid input values must be positive, using -1 for not-set double density = -1.0; int32_t width = -1; nsAString::const_iterator iter, end; aDescriptor.BeginReading(iter); aDescriptor.EndReading(end); // Parse descriptor list // We currently only support a single density descriptor of the form: // x // Silently ignore other descriptors in the list for forward-compat while (iter != end) { // Skip initial whitespace for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter); if (iter == end) { break; } // Find end of type nsAString::const_iterator start = iter; for (; iter != end && !nsContentUtils::IsHTMLWhitespace(*iter); ++iter); if (start == iter) { // Empty descriptor break; } // Iter is at end of descriptor, type is single character previous to that. // Safe because we just verified that iter > start --iter; nsAString::const_iterator type(iter); ++iter; const nsDependentSubstring& valStr = Substring(start, type); if (*type == char16_t('w')) { int32_t possibleWidth; if (width == -1 && density == -1.0) { if (ParseInteger(valStr, possibleWidth) && possibleWidth > 0) { width = possibleWidth; } } else { return false; } } else if (*type == char16_t('x')) { if (width == -1 && density == -1.0) { nsresult rv; double possibleDensity = PromiseFlatString(valStr).ToDouble(&rv); if (NS_SUCCEEDED(rv) && possibleDensity > 0.0) { density = possibleDensity; } } else { return false; } } } if (width != -1) { SetParameterAsComputedWidth(width); } else if (density != -1.0) { SetParameterAsDensity(density); } else { // No valid descriptors -> 1.0 density SetParameterAsDensity(1.0); } return true; } bool ResponsiveImageCandidate::HasSameParameter(const ResponsiveImageCandidate & aOther) const { if (aOther.mType != mType) { return false; } if (mType == eCandidateType_Default) { return true; } if (mType == eCandidateType_Density) { return aOther.mValue.mDensity == mValue.mDensity; } if (mType == eCandidateType_Invalid) { MOZ_ASSERT(false, "Comparing invalid candidates?"); return true; } else if (mType == eCandidateType_ComputedFromWidth) { return aOther.mValue.mWidth == mValue.mWidth; } MOZ_ASSERT(false, "Somebody forgot to check for all uses of this enum"); return false; } already_AddRefed ResponsiveImageCandidate::URL() const { nsCOMPtr url = mURL; return url.forget(); } double ResponsiveImageCandidate::Density(ResponsiveImageSelector *aSelector) const { if (mType == eCandidateType_ComputedFromWidth) { int32_t width; if (!aSelector->ComputeFinalWidthForCurrentViewport(&width)) { return 1.0; } return Density(width); } // Other types don't need matching width MOZ_ASSERT(mType == eCandidateType_Default || mType == eCandidateType_Density, "unhandled candidate type"); return Density(-1); } double ResponsiveImageCandidate::Density(int32_t aMatchingWidth) const { if (mType == eCandidateType_Invalid) { MOZ_ASSERT(false, "Getting density for uninitialized candidate"); return 1.0; } if (mType == eCandidateType_Default) { return 1.0; } if (mType == eCandidateType_Density) { return mValue.mDensity; } else if (mType == eCandidateType_ComputedFromWidth) { if (aMatchingWidth <= 0) { MOZ_ASSERT(false, "0 or negative matching width is invalid per spec"); return 1.0; } double density = double(mValue.mWidth) / double(aMatchingWidth); MOZ_ASSERT(density > 0.0); return density; } MOZ_ASSERT(false, "Unknown candidate type"); return 1.0; } bool ResponsiveImageCandidate::IsComputedFromWidth() const { if (mType == eCandidateType_ComputedFromWidth) { return true; } MOZ_ASSERT(mType == eCandidateType_Default || mType == eCandidateType_Density, "Unknown candidate type"); return false; } } // namespace dom } // namespace mozilla