#include "config.h"
#include "core/html/parser/HTMLSrcsetParser.h"
+#include "core/dom/Document.h"
+#include "core/fetch/MemoryCache.h"
+#include "core/fetch/ResourceFetcher.h"
+#include "core/frame/FrameConsole.h"
+#include "core/frame/LocalFrame.h"
+#include "core/frame/UseCounter.h"
#include "core/html/parser/HTMLParserIdioms.h"
+#include "core/inspector/ConsoleMessage.h"
#include "platform/ParsingUtilities.h"
#include "platform/RuntimeEnabledFeatures.h"
}
enum DescriptorTokenizerState {
- Start,
+ TokenStart,
InParenthesis,
AfterToken,
};
template<typename CharType>
int toInt(const CharType* attribute, bool& isValid)
{
- return charactersToIntStrict(attribute + start, length - 1, &isValid);
+ unsigned position = 0;
+ // Make sure the integer is a valid non-negative integer
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-non-negative-integer
+ unsigned lengthExcludingDescriptor = length - 1;
+ while (position < lengthExcludingDescriptor) {
+ if (!isASCIIDigit(*(attribute + start + position))) {
+ isValid = false;
+ return 0;
+ }
+ ++position;
+ }
+ return charactersToIntStrict(attribute + start, lengthExcludingDescriptor, &isValid);
}
template<typename CharType>
float toFloat(const CharType* attribute, bool& isValid)
{
- return charactersToFloat(attribute + start, length - 1, &isValid);
+ // Make sure the is a valid floating point number
+ // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
+ unsigned lengthExcludingDescriptor = length - 1;
+ if (lengthExcludingDescriptor > 0 && *(attribute + start) == '+') {
+ isValid = false;
+ return 0;
+ }
+ return charactersToFloat(attribute + start, lengthExcludingDescriptor, &isValid);
}
};
const CharType* attributeEnd,
Vector<DescriptorToken>& descriptors)
{
- DescriptorTokenizerState state = Start;
+ DescriptorTokenizerState state = TokenStart;
const CharType* descriptorsStart = position;
const CharType* currentDescriptorStart = descriptorsStart;
while (true) {
switch (state) {
- case Start:
+ case TokenStart:
if (isEOF(position, attributeEnd)) {
appendDescriptorAndReset(attributeStart, currentDescriptorStart, attributeEnd, descriptors);
return;
}
if (*position == ')') {
appendCharacter(currentDescriptorStart, position);
- state = Start;
+ state = TokenStart;
} else {
appendCharacter(currentDescriptorStart, position);
}
if (isEOF(position, attributeEnd))
return;
if (!isHTMLSpace(*position)) {
- state = Start;
+ state = TokenStart;
currentDescriptorStart = position;
--position;
}
}
}
+static void srcsetError(Document* document, String message)
+{
+ if (document && document->frame()) {
+ StringBuilder errorMessage;
+ errorMessage.append("Failed parsing 'srcset' attribute value since ");
+ errorMessage.append(message);
+ document->frame()->console().addMessage(ConsoleMessage::create(OtherMessageSource, ErrorMessageLevel, errorMessage.toString()));
+ }
+}
+
template<typename CharType>
-static bool parseDescriptors(const CharType* attribute, Vector<DescriptorToken>& descriptors, DescriptorParsingResult& result)
+static bool parseDescriptors(const CharType* attribute, Vector<DescriptorToken>& descriptors, DescriptorParsingResult& result, Document* document)
{
- for (Vector<DescriptorToken>::iterator it = descriptors.begin(); it != descriptors.end(); ++it) {
- if (it->length == 0)
+ for (DescriptorToken& descriptor : descriptors) {
+ if (descriptor.length == 0)
continue;
- CharType c = attribute[it->lastIndex()];
+ CharType c = attribute[descriptor.lastIndex()];
bool isValid = false;
if (RuntimeEnabledFeatures::pictureSizesEnabled() && c == 'w') {
- if (result.hasDensity() || result.hasWidth())
+ if (result.hasDensity() || result.hasWidth()) {
+ srcsetError(document, "it has multiple 'w' descriptors or a mix of 'x' and 'w' descriptors.");
return false;
- int resourceWidth = it->toInt(attribute, isValid);
- if (!isValid || resourceWidth <= 0)
+ }
+ int resourceWidth = descriptor.toInt(attribute, isValid);
+ if (!isValid || resourceWidth <= 0) {
+ srcsetError(document, "its 'w' descriptor is invalid.");
return false;
+ }
result.setResourceWidth(resourceWidth);
} else if (RuntimeEnabledFeatures::pictureSizesEnabled() && c == 'h') {
// This is here only for future compat purposes.
// The value of the 'h' descriptor is not used.
- if (result.hasDensity() || result.hasHeight())
+ if (result.hasDensity() || result.hasHeight()) {
+ srcsetError(document, "it has multiple 'h' descriptors or a mix of 'x' and 'h' descriptors.");
return false;
- int resourceHeight = it->toInt(attribute, isValid);
- if (!isValid || resourceHeight <= 0)
+ }
+ int resourceHeight = descriptor.toInt(attribute, isValid);
+ if (!isValid || resourceHeight <= 0) {
+ srcsetError(document, "its 'h' descriptor is invalid.");
return false;
+ }
result.setResourceHeight(resourceHeight);
} else if (c == 'x') {
- if (result.hasDensity() || result.hasHeight() || result.hasWidth())
+ if (result.hasDensity() || result.hasHeight() || result.hasWidth()) {
+ srcsetError(document, "it has multiple 'x' descriptors or a mix of 'x' and 'w'/'h' descriptors.");
return false;
- float density = it->toFloat(attribute, isValid);
- if (!isValid || density < 0)
+ }
+ float density = descriptor.toFloat(attribute, isValid);
+ if (!isValid || density < 0) {
+ srcsetError(document, "its 'x' descriptor is invalid.");
return false;
+ }
result.setDensity(density);
+ } else {
+ srcsetError(document, "it has an unknown descriptor.");
+ return false;
}
}
- return true;
+ bool res = !result.hasHeight() || result.hasWidth();
+ if (!res)
+ srcsetError(document, "it has an 'h' descriptor and no 'w' descriptor.");
+ return res;
}
-static bool parseDescriptors(const String& attribute, Vector<DescriptorToken>& descriptors, DescriptorParsingResult& result)
+static bool parseDescriptors(const String& attribute, Vector<DescriptorToken>& descriptors, DescriptorParsingResult& result, Document* document)
{
// FIXME: See if StringView can't be extended to replace DescriptorToken here.
if (attribute.is8Bit()) {
- return parseDescriptors(attribute.characters8(), descriptors, result);
+ return parseDescriptors(attribute.characters8(), descriptors, result, document);
}
- return parseDescriptors(attribute.characters16(), descriptors, result);
+ return parseDescriptors(attribute.characters16(), descriptors, result, document);
}
// http://picture.responsiveimages.org/#parse-srcset-attr
template<typename CharType>
-static void parseImageCandidatesFromSrcsetAttribute(const String& attribute, const CharType* attributeStart, unsigned length, Vector<ImageCandidate>& imageCandidates)
+static void parseImageCandidatesFromSrcsetAttribute(const String& attribute, const CharType* attributeStart, unsigned length, Vector<ImageCandidate>& imageCandidates, Document* document)
{
const CharType* position = attributeStart;
const CharType* attributeEnd = position + length;
while (position < attributeEnd) {
// 4. Splitting loop: Collect a sequence of characters that are space characters or U+002C COMMA characters.
- skipWhile<CharType, isHTMLSpaceOrComma<CharType> >(position, attributeEnd);
+ skipWhile<CharType, isHTMLSpaceOrComma<CharType>>(position, attributeEnd);
if (position == attributeEnd) {
// Contrary to spec language - descriptor parsing happens on each candidate, so when we reach the attributeEnd, we can exit.
break;
}
const CharType* imageURLStart = position;
- // 6. Collect a sequence of characters that are not space characters, and let that be url.
- skipUntil<CharType, isHTMLSpace<CharType> >(position, attributeEnd);
+ // 6. Collect a sequence of characters that are not space characters, and let that be url.
+ skipUntil<CharType, isHTMLSpace<CharType>>(position, attributeEnd);
const CharType* imageURLEnd = position;
DescriptorParsingResult result;
if (imageURLStart == imageURLEnd)
continue;
} else {
- // Advancing position here (contrary to spec) to avoid an useless extra state machine step.
- // Filed a spec bug: https://github.com/ResponsiveImagesCG/picture-element/issues/189
- ++position;
+ skipWhile<CharType, isHTMLSpace<CharType> >(position, attributeEnd);
Vector<DescriptorToken> descriptorTokens;
tokenizeDescriptors(attributeStart, position, attributeEnd, descriptorTokens);
// Contrary to spec language - descriptor parsing happens on each candidate.
// This is a black-box equivalent, to avoid storing descriptor lists for each candidate.
- if (!parseDescriptors(attribute, descriptorTokens, result))
+ if (!parseDescriptors(attribute, descriptorTokens, result, document)) {
+ if (document) {
+ UseCounter::count(document, UseCounter::SrcsetDroppedCandidate);
+ if (document->frame())
+ document->frame()->console().addMessage(ConsoleMessage::create(OtherMessageSource, ErrorMessageLevel, String("Dropped srcset candidate ") + String(imageURLStart, imageURLEnd - imageURLStart)));
+ }
continue;
+ }
}
ASSERT(imageURLEnd > attributeStart);
}
}
-static void parseImageCandidatesFromSrcsetAttribute(const String& attribute, Vector<ImageCandidate>& imageCandidates)
+static void parseImageCandidatesFromSrcsetAttribute(const String& attribute, Vector<ImageCandidate>& imageCandidates, Document* document)
{
if (attribute.isNull())
return;
if (attribute.is8Bit())
- parseImageCandidatesFromSrcsetAttribute<LChar>(attribute, attribute.characters8(), attribute.length(), imageCandidates);
+ parseImageCandidatesFromSrcsetAttribute<LChar>(attribute, attribute.characters8(), attribute.length(), imageCandidates, document);
else
- parseImageCandidatesFromSrcsetAttribute<UChar>(attribute, attribute.characters16(), attribute.length(), imageCandidates);
+ parseImageCandidatesFromSrcsetAttribute<UChar>(attribute, attribute.characters16(), attribute.length(), imageCandidates, document);
}
-static ImageCandidate pickBestImageCandidate(float deviceScaleFactor, unsigned sourceSize, Vector<ImageCandidate>& imageCandidates)
+static int selectionLogic(Vector<ImageCandidate>& imageCandidates, float deviceScaleFactor, bool ignoreSrc)
+{
+ unsigned i = 0;
+
+ for (; i < imageCandidates.size() - 1; ++i) {
+ unsigned next = i + 1;
+ float nextDensity;
+ float currentDensity;
+ float geometricMean;
+ if (ignoreSrc) {
+ if (imageCandidates[i].srcOrigin())
+ continue;
+ if (imageCandidates[next].srcOrigin()) {
+ ++next;
+ if (next >= imageCandidates.size())
+ break;
+ ASSERT(!imageCandidates[next].srcOrigin());
+ }
+ }
+
+ nextDensity = imageCandidates[next].density();
+ if (nextDensity < deviceScaleFactor)
+ continue;
+
+ currentDensity = imageCandidates[i].density();
+ geometricMean = sqrt(currentDensity * nextDensity);
+ if (deviceScaleFactor >= geometricMean)
+ return next;
+ break;
+ }
+ return i;
+}
+
+static unsigned avoidDownloadIfHigherDensityResourceIsInCache(Vector<ImageCandidate>& imageCandidates, unsigned winner, Document* document)
+{
+ if (!document)
+ return winner;
+ for (unsigned i = imageCandidates.size() - 1; i > winner; --i) {
+ KURL url = document->completeURL(stripLeadingAndTrailingHTMLSpaces(imageCandidates[i].url()));
+ if (memoryCache()->resourceForURL(url, document->fetcher()->getCacheIdentifier()))
+ return i;
+ }
+ return winner;
+}
+
+static ImageCandidate pickBestImageCandidate(float deviceScaleFactor, float sourceSize, Vector<ImageCandidate>& imageCandidates, Document* document = nullptr)
{
const float defaultDensityValue = 1.0;
bool ignoreSrc = false;
return ImageCandidate();
// http://picture.responsiveimages.org/#normalize-source-densities
- for (Vector<ImageCandidate>::iterator it = imageCandidates.begin(); it != imageCandidates.end(); ++it) {
- if (it->resourceWidth() > 0) {
- it->setDensity((float)it->resourceWidth() / (float)sourceSize);
+ for (ImageCandidate& image : imageCandidates) {
+ if (image.resourceWidth() > 0) {
+ image.setDensity((float)image.resourceWidth() / sourceSize);
ignoreSrc = true;
- } else if (it->density() < 0) {
- it->setDensity(defaultDensityValue);
+ } else if (image.density() < 0) {
+ image.setDensity(defaultDensityValue);
}
}
std::stable_sort(imageCandidates.begin(), imageCandidates.end(), compareByDensity);
- unsigned i;
- for (i = 0; i < imageCandidates.size() - 1; ++i) {
- if ((imageCandidates[i].density() >= deviceScaleFactor) && (!ignoreSrc || !imageCandidates[i].srcOrigin()))
- break;
- }
-
- if (imageCandidates[i].srcOrigin() && ignoreSrc) {
- ASSERT(i > 0);
- --i;
- }
- float winningDensity = imageCandidates[i].density();
+ unsigned winner = selectionLogic(imageCandidates, deviceScaleFactor, ignoreSrc);
+ ASSERT(winner < imageCandidates.size());
+ winner = avoidDownloadIfHigherDensityResourceIsInCache(imageCandidates, winner, document);
- unsigned winner = i;
+ float winningDensity = imageCandidates[winner].density();
// 16. If an entry b in candidates has the same associated ... pixel density as an earlier entry a in candidates,
// then remove entry b
- while ((i > 0) && (imageCandidates[--i].density() == winningDensity))
- winner = i;
+ while ((winner > 0) && (imageCandidates[winner - 1].density() == winningDensity))
+ --winner;
return imageCandidates[winner];
}
-ImageCandidate bestFitSourceForSrcsetAttribute(float deviceScaleFactor, unsigned sourceSize, const String& srcsetAttribute)
+ImageCandidate bestFitSourceForSrcsetAttribute(float deviceScaleFactor, float sourceSize, const String& srcsetAttribute, Document* document)
{
Vector<ImageCandidate> imageCandidates;
- parseImageCandidatesFromSrcsetAttribute(srcsetAttribute, imageCandidates);
+ parseImageCandidatesFromSrcsetAttribute(srcsetAttribute, imageCandidates, document);
- return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates);
+ return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates, document);
}
-ImageCandidate bestFitSourceForImageAttributes(float deviceScaleFactor, unsigned sourceSize, const String& srcAttribute, const String& srcsetAttribute)
+ImageCandidate bestFitSourceForImageAttributes(float deviceScaleFactor, float sourceSize, const String& srcAttribute, const String& srcsetAttribute, Document* document)
{
if (srcsetAttribute.isNull()) {
if (srcAttribute.isNull())
Vector<ImageCandidate> imageCandidates;
- parseImageCandidatesFromSrcsetAttribute(srcsetAttribute, imageCandidates);
+ parseImageCandidatesFromSrcsetAttribute(srcsetAttribute, imageCandidates, document);
if (!srcAttribute.isEmpty())
imageCandidates.append(ImageCandidate(srcAttribute, 0, srcAttribute.length(), DescriptorParsingResult(), ImageCandidate::SrcOrigin));
- return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates);
+ return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates, document);
}
-String bestFitSourceForImageAttributes(float deviceScaleFactor, unsigned sourceSize, const String& srcAttribute, ImageCandidate& srcsetImageCandidate)
+String bestFitSourceForImageAttributes(float deviceScaleFactor, float sourceSize, const String& srcAttribute, ImageCandidate& srcsetImageCandidate)
{
if (srcsetImageCandidate.isEmpty())
return srcAttribute;