1/* 2 * Copyright (C) 2013 Apple Inc. All rights reserved. 3 * Copyright (C) 2013 Google Inc. All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * * Redistributions in binary form must reproduce the above 12 * copyright notice, this list of conditions and the following disclaimer 13 * in the documentation and/or other materials provided with the 14 * distribution. 15 * * Neither the name of Google Inc. nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32#include "config.h" 33#include "HTMLSrcsetParser.h" 34 35#include "HTMLParserIdioms.h" 36#include "ParsingUtilities.h" 37 38namespace WebCore { 39 40static inline bool compareByDensity(const ImageCandidate& first, const ImageCandidate& second) 41{ 42 return first.density < second.density; 43} 44 45enum DescriptorTokenizerState { 46 Start, 47 InParenthesis, 48 AfterToken, 49}; 50 51template<typename CharType> 52static void appendDescriptorAndReset(const CharType*& descriptorStart, const CharType* position, Vector<StringView>& descriptors) 53{ 54 if (position > descriptorStart) 55 descriptors.append(StringView(descriptorStart, position - descriptorStart)); 56 descriptorStart = nullptr; 57} 58 59// The following is called appendCharacter to match the spec's terminology. 60template<typename CharType> 61static void appendCharacter(const CharType* descriptorStart, const CharType* position) 62{ 63 // Since we don't copy the tokens, this just set the point where the descriptor tokens start. 64 if (!descriptorStart) 65 descriptorStart = position; 66} 67 68template<typename CharType> 69static bool isEOF(const CharType* position, const CharType* end) 70{ 71 return position >= end; 72} 73 74template<typename CharType> 75static void tokenizeDescriptors(const CharType*& position, const CharType* attributeEnd, Vector<StringView>& descriptors) 76{ 77 DescriptorTokenizerState state = Start; 78 const CharType* descriptorsStart = position; 79 const CharType* currentDescriptorStart = descriptorsStart; 80 for (; ; ++position) { 81 switch (state) { 82 case Start: 83 if (isEOF(position, attributeEnd)) { 84 appendDescriptorAndReset(currentDescriptorStart, attributeEnd, descriptors); 85 return; 86 } 87 if (isComma(*position)) { 88 appendDescriptorAndReset(currentDescriptorStart, position, descriptors); 89 ++position; 90 return; 91 } 92 if (isHTMLSpace(*position)) { 93 appendDescriptorAndReset(currentDescriptorStart, position, descriptors); 94 currentDescriptorStart = position + 1; 95 state = AfterToken; 96 } else if (*position == '(') { 97 appendCharacter(currentDescriptorStart, position); 98 state = InParenthesis; 99 } else 100 appendCharacter(currentDescriptorStart, position); 101 break; 102 case InParenthesis: 103 if (isEOF(position, attributeEnd)) { 104 appendDescriptorAndReset(currentDescriptorStart, attributeEnd, descriptors); 105 return; 106 } 107 if (*position == ')') { 108 appendCharacter(currentDescriptorStart, position); 109 state = Start; 110 } else 111 appendCharacter(currentDescriptorStart, position); 112 break; 113 case AfterToken: 114 if (isEOF(position, attributeEnd)) 115 return; 116 if (!isHTMLSpace(*position)) { 117 state = Start; 118 currentDescriptorStart = position; 119 --position; 120 } 121 break; 122 } 123 } 124} 125 126static bool parseDescriptors(Vector<StringView>& descriptors, DescriptorParsingResult& result) 127{ 128 for (auto& descriptor : descriptors) { 129 if (descriptor.isEmpty()) 130 continue; 131 unsigned descriptorCharPosition = descriptor.length() - 1; 132 UChar descriptorChar = descriptor[descriptorCharPosition]; 133 descriptor = descriptor.substring(0, descriptorCharPosition); 134 bool isValid = false; 135 if (descriptorChar == 'x') { 136 if (result.hasDensity() || result.hasHeight() || result.hasWidth()) 137 return false; 138 float density = descriptor.toFloat(isValid); 139 if (!isValid || density < 0) 140 return false; 141 result.setDensity(density); 142 } else if (descriptorChar == 'w') { 143#if ENABLE(PICTURE_SIZES) 144 if (result.hasDensity() || result.hasWidth()) 145 return false; 146 int resourceWidth = descriptor.toInt(isValid); 147 if (!isValid || resourceWidth <= 0) 148 return false; 149 result.setResourceWidth(resourceWidth); 150#else 151 return false; 152#endif 153 } else if (descriptorChar == 'h') { 154#if ENABLE(PICTURE_SIZES) 155 // This is here only for future compat purposes. 156 // The value of the 'h' descriptor is not used. 157 if (result.hasDensity() || result.hasHeight()) 158 return false; 159 int resourceHeight = descriptor.toInt(isValid); 160 if (!isValid || resourceHeight <= 0) 161 return false; 162 result.setResourceHeight(resourceHeight); 163#else 164 return false; 165#endif 166 } 167 } 168 return true; 169} 170 171// http://picture.responsiveimages.org/#parse-srcset-attr 172template<typename CharType> 173static void parseImageCandidatesFromSrcsetAttribute(const CharType* attributeStart, unsigned length, Vector<ImageCandidate>& imageCandidates) 174{ 175 const CharType* attributeEnd = attributeStart + length; 176 177 for (const CharType* position = attributeStart; position < attributeEnd;) { 178 // 4. Splitting loop: Collect a sequence of characters that are space characters or U+002C COMMA characters. 179 skipWhile<CharType, isHTMLSpaceOrComma<CharType> >(position, attributeEnd); 180 if (position == attributeEnd) { 181 // Contrary to spec language - descriptor parsing happens on each candidate, so when we reach the attributeEnd, we can exit. 182 break; 183 } 184 const CharType* imageURLStart = position; 185 // 6. Collect a sequence of characters that are not space characters, and let that be url. 186 187 skipUntil<CharType, isHTMLSpace<CharType> >(position, attributeEnd); 188 const CharType* imageURLEnd = position; 189 190 DescriptorParsingResult result; 191 192 // 8. If url ends with a U+002C COMMA character (,) 193 if (isComma(*(position - 1))) { 194 // Remove all trailing U+002C COMMA characters from url. 195 imageURLEnd = position - 1; 196 reverseSkipWhile<CharType, isComma>(imageURLEnd, imageURLStart); 197 ++imageURLEnd; 198 // If url is empty, then jump to the step labeled splitting loop. 199 if (imageURLStart == imageURLEnd) 200 continue; 201 } else { 202 // Advancing position here (contrary to spec) to avoid an useless extra state machine step. 203 // Filed a spec bug: https://github.com/ResponsiveImagesCG/picture-element/issues/189 204 ++position; 205 Vector<StringView> descriptorTokens; 206 tokenizeDescriptors(position, attributeEnd, descriptorTokens); 207 // Contrary to spec language - descriptor parsing happens on each candidate. 208 // This is a black-box equivalent, to avoid storing descriptor lists for each candidate. 209 if (!parseDescriptors(descriptorTokens, result)) 210 continue; 211 } 212 213 ASSERT(imageURLEnd > imageURLStart); 214 unsigned imageURLLength = imageURLEnd - imageURLStart; 215 imageCandidates.append(ImageCandidate(StringView(imageURLStart, imageURLLength), result, ImageCandidate::SrcsetOrigin)); 216 // 11. Return to the step labeled splitting loop. 217 } 218} 219 220static void parseImageCandidatesFromSrcsetAttribute(StringView attribute, Vector<ImageCandidate>& imageCandidates) 221{ 222 // FIXME: We should consider replacing the direct pointers in the parsing process with StringView and positions. 223 if (attribute.is8Bit()) 224 parseImageCandidatesFromSrcsetAttribute<LChar>(attribute.characters8(), attribute.length(), imageCandidates); 225 else 226 parseImageCandidatesFromSrcsetAttribute<UChar>(attribute.characters16(), attribute.length(), imageCandidates); 227} 228 229static ImageCandidate pickBestImageCandidate(float deviceScaleFactor, Vector<ImageCandidate>& imageCandidates 230#if ENABLE(PICTURE_SIZES) 231 , unsigned sourceSize 232#endif 233 ) 234{ 235 bool ignoreSrc = false; 236 if (imageCandidates.isEmpty()) 237 return ImageCandidate(); 238 239 // http://picture.responsiveimages.org/#normalize-source-densities 240 for (auto& candidate : imageCandidates) { 241#if ENABLE(PICTURE_SIZES) 242 if (candidate.resourceWidth > 0) { 243 candidate.density = static_cast<float>(candidate.resourceWidth) / static_cast<float>(sourceSize); 244 ignoreSrc = true; 245 } else 246#endif 247 if (candidate.density < 0) 248 candidate.density = DefaultDensityValue; 249 } 250 251 std::stable_sort(imageCandidates.begin(), imageCandidates.end(), compareByDensity); 252 253 unsigned i; 254 for (i = 0; i < imageCandidates.size() - 1; ++i) { 255 if ((imageCandidates[i].density >= deviceScaleFactor) && (!ignoreSrc || !imageCandidates[i].srcOrigin())) 256 break; 257 } 258 259 if (imageCandidates[i].srcOrigin() && ignoreSrc) { 260 ASSERT(i > 0); 261 --i; 262 } 263 float winningDensity = imageCandidates[i].density; 264 265 unsigned winner = i; 266 // 16. If an entry b in candidates has the same associated ... pixel density as an earlier entry a in candidates, 267 // then remove entry b 268 while ((i > 0) && (imageCandidates[--i].density == winningDensity)) 269 winner = i; 270 271 return imageCandidates[winner]; 272} 273 274ImageCandidate bestFitSourceForImageAttributes(float deviceScaleFactor, const AtomicString& srcAttribute, const AtomicString& srcsetAttribute 275#if ENABLE(PICTURE_SIZES) 276 , unsigned sourceSize 277#endif 278 ) 279{ 280 if (srcsetAttribute.isNull()) { 281 if (srcAttribute.isNull()) 282 return ImageCandidate(); 283 return ImageCandidate(StringView(srcAttribute), DescriptorParsingResult(), ImageCandidate::SrcOrigin); 284 } 285 286 Vector<ImageCandidate> imageCandidates; 287 288 parseImageCandidatesFromSrcsetAttribute(StringView(srcsetAttribute), imageCandidates); 289 290 if (!srcAttribute.isEmpty()) 291 imageCandidates.append(ImageCandidate(StringView(srcAttribute), DescriptorParsingResult(), ImageCandidate::SrcOrigin)); 292 293 return pickBestImageCandidate(deviceScaleFactor, imageCandidates 294#if ENABLE(PICTURE_SIZES) 295 , sourceSize 296#endif 297 ); 298} 299 300} // namespace WebCore 301