1 /* 2 * Copyright (C) 2013 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 #include "config.h" 32 #include "core/frame/SmartClip.h" 33 34 #include "core/dom/ContainerNode.h" 35 #include "core/dom/Document.h" 36 #include "core/dom/NodeTraversal.h" 37 #include "core/frame/DOMWindow.h" 38 #include "core/frame/FrameView.h" 39 #include "core/html/HTMLFrameOwnerElement.h" 40 #include "core/page/Page.h" 41 #include "wtf/text/StringBuilder.h" 42 43 namespace WebCore { 44 45 static IntRect applyScaleWithoutCollapsingToZero(const IntRect& rect, float scale) 46 { 47 IntRect result = rect; 48 result.scale(scale); 49 if (rect.width() > 0 && !result.width()) 50 result.setWidth(1); 51 if (rect.height() > 0 && !result.height()) 52 result.setHeight(1); 53 return result; 54 } 55 56 static Node* nodeInsideFrame(Node* node) 57 { 58 if (node->isFrameOwnerElement()) 59 return toHTMLFrameOwnerElement(node)->contentDocument(); 60 return 0; 61 } 62 63 // FIXME: SmartClipData is eventually returned via 64 // SLookSmartClip.DataExtractionListener: 65 // http://img-developer.samsung.com/onlinedocs/sms/com/samsung/android/sdk/look/... 66 // however the original author of this change chose to use a string-serialization 67 // format (presumably to make IPC easy?). 68 // If we're going to use this as a Pickle format, we should at least have the 69 // read/write code in one place! 70 String SmartClipData::toString() 71 { 72 if (!m_node) 73 return emptyString(); 74 75 const UChar fieldSeparator = 0xFFFE; 76 const UChar rowSeparator = 0xFFFF; 77 78 StringBuilder result; 79 result.append(String::number(m_rect.x())); 80 result.append(fieldSeparator); 81 result.append(String::number(m_rect.y())); 82 result.append(fieldSeparator); 83 result.append(String::number(m_rect.width())); 84 result.append(fieldSeparator); 85 result.append(String::number(m_rect.height())); 86 result.append(fieldSeparator); 87 result.append(m_string); 88 result.append(rowSeparator); 89 return result.toString(); 90 } 91 92 SmartClip::SmartClip(PassRefPtr<Frame> frame) 93 : m_frame(frame) 94 { 95 } 96 97 SmartClipData SmartClip::dataForRect(const IntRect& cropRect) 98 { 99 IntRect resizedCropRect = applyScaleWithoutCollapsingToZero(cropRect, 1 / pageScaleFactor()); 100 101 Node* bestNode = findBestOverlappingNode(m_frame->document(), resizedCropRect); 102 if (!bestNode) 103 return SmartClipData(); 104 105 if (Node* nodeFromFrame = nodeInsideFrame(bestNode)) { 106 // FIXME: This code only hit-tests a single iframe. It seems like we ought support nested frames. 107 if (Node* bestNodeInFrame = findBestOverlappingNode(nodeFromFrame, resizedCropRect)) 108 bestNode = bestNodeInFrame; 109 } 110 111 Vector<Node*> hitNodes; 112 collectOverlappingChildNodes(bestNode, resizedCropRect, hitNodes); 113 114 if (hitNodes.isEmpty() || hitNodes.size() == bestNode->childNodeCount()) { 115 hitNodes.clear(); 116 hitNodes.append(bestNode); 117 } 118 119 // Unite won't work with the empty rect, so we initialize to the first rect. 120 IntRect unitedRects = hitNodes[0]->pixelSnappedBoundingBox(); 121 StringBuilder collectedText; 122 for (size_t i = 0; i < hitNodes.size(); ++i) { 123 collectedText.append(extractTextFromNode(hitNodes[i])); 124 unitedRects.unite(hitNodes[i]->pixelSnappedBoundingBox()); 125 } 126 127 return SmartClipData(bestNode, convertRectToWindow(unitedRects), collectedText.toString()); 128 } 129 130 float SmartClip::pageScaleFactor() 131 { 132 return m_frame->page()->pageScaleFactor(); 133 } 134 135 // This function is a bit of a mystery. If you understand what it does, please 136 // consider adding a more descriptive name. 137 Node* SmartClip::minNodeContainsNodes(Node* minNode, Node* newNode) 138 { 139 if (!newNode) 140 return minNode; 141 if (!minNode) 142 return newNode; 143 144 IntRect minNodeRect = minNode->pixelSnappedBoundingBox(); 145 IntRect newNodeRect = newNode->pixelSnappedBoundingBox(); 146 147 Node* parentMinNode = minNode->parentNode(); 148 Node* parentNewNode = newNode->parentNode(); 149 150 if (minNodeRect.contains(newNodeRect)) { 151 if (parentMinNode && parentNewNode && parentNewNode->parentNode() == parentMinNode) 152 return parentMinNode; 153 return minNode; 154 } 155 156 if (newNodeRect.contains(minNodeRect)) { 157 if (parentMinNode && parentNewNode && parentMinNode->parentNode() == parentNewNode) 158 return parentNewNode; 159 return newNode; 160 } 161 162 // This loop appears to find the nearest ancestor of minNode (in DOM order) 163 // that contains the newNodeRect. It's very unclear to me why that's an 164 // interesting node to find. Presumably this loop will often just return 165 // the documentElement. 166 Node* node = minNode; 167 while (node) { 168 if (node->renderer()) { 169 IntRect nodeRect = node->pixelSnappedBoundingBox(); 170 if (nodeRect.contains(newNodeRect)) { 171 return node; 172 } 173 } 174 node = node->parentNode(); 175 } 176 177 return 0; 178 } 179 180 Node* SmartClip::findBestOverlappingNode(Node* rootNode, const IntRect& cropRect) 181 { 182 if (!rootNode) 183 return 0; 184 185 IntRect resizedCropRect = rootNode->document().view()->windowToContents(cropRect); 186 187 Node* node = rootNode; 188 Node* minNode = 0; 189 190 while (node) { 191 IntRect nodeRect = node->pixelSnappedBoundingBox(); 192 193 if (node->isElementNode() && equalIgnoringCase(toElement(node)->fastGetAttribute(HTMLNames::aria_hiddenAttr), "true")) { 194 node = NodeTraversal::nextSkippingChildren(*node, rootNode); 195 continue; 196 } 197 198 RenderObject* renderer = node->renderer(); 199 if (renderer && !nodeRect.isEmpty()) { 200 if (renderer->isText() 201 || renderer->isRenderImage() 202 || node->isFrameOwnerElement() 203 || (renderer->style()->hasBackgroundImage() && !shouldSkipBackgroundImage(node))) { 204 if (resizedCropRect.intersects(nodeRect)) { 205 minNode = minNodeContainsNodes(minNode, node); 206 } else { 207 node = NodeTraversal::nextSkippingChildren(*node, rootNode); 208 continue; 209 } 210 } 211 } 212 node = NodeTraversal::next(*node, rootNode); 213 } 214 215 return minNode; 216 } 217 218 // This function appears to heuristically guess whether to include a background 219 // image in the smart clip. It seems to want to include sprites created from 220 // CSS background images but to skip actual backgrounds. 221 bool SmartClip::shouldSkipBackgroundImage(Node* node) 222 { 223 // Apparently we're only interested in background images on spans and divs. 224 if (!node->hasTagName(HTMLNames::spanTag) && !node->hasTagName(HTMLNames::divTag)) 225 return true; 226 227 // This check actually makes a bit of sense. If you're going to sprite an 228 // image out of a CSS background, you're probably going to specify a height 229 // or a width. On the other hand, if we've got a legit background image, 230 // it's very likely the height or the width will be set to auto. 231 RenderObject* renderer = node->renderer(); 232 if (renderer && (renderer->style()->logicalHeight().isAuto() || renderer->style()->logicalWidth().isAuto())) 233 return true; 234 235 return false; 236 } 237 238 void SmartClip::collectOverlappingChildNodes(Node* parentNode, const IntRect& cropRect, Vector<Node*>& hitNodes) 239 { 240 if (!parentNode) 241 return; 242 IntRect resizedCropRect = parentNode->document().view()->windowToContents(cropRect); 243 for (Node* child = parentNode->firstChild(); child; child = child->nextSibling()) { 244 IntRect childRect = child->pixelSnappedBoundingBox(); 245 if (resizedCropRect.intersects(childRect)) 246 hitNodes.append(child); 247 } 248 } 249 250 IntRect SmartClip::convertRectToWindow(const IntRect& nodeRect) 251 { 252 IntRect result = m_frame->document()->view()->contentsToWindow(nodeRect); 253 result.scale(pageScaleFactor()); 254 return result; 255 } 256 257 String SmartClip::extractTextFromNode(Node* node) 258 { 259 // Science has proven that no text nodes are ever positioned at y == -99999. 260 int prevYPos = -99999; 261 262 StringBuilder result; 263 for (Node* currentNode = node; currentNode; currentNode = NodeTraversal::next(*currentNode, node)) { 264 RenderStyle* style = currentNode->computedStyle(); 265 if (style && style->userSelect() == SELECT_NONE) 266 continue; 267 268 if (Node* nodeFromFrame = nodeInsideFrame(currentNode)) 269 result.append(extractTextFromNode(nodeFromFrame)); 270 271 IntRect nodeRect = currentNode->pixelSnappedBoundingBox(); 272 if (currentNode->renderer() && !nodeRect.isEmpty()) { 273 if (currentNode->isTextNode()) { 274 String nodeValue = currentNode->nodeValue(); 275 276 // It's unclear why we blacklist solitary "\n" node values. 277 // Maybe we're trying to ignore <br> tags somehow? 278 if (nodeValue == "\n") 279 nodeValue = ""; 280 281 if (nodeRect.y() != prevYPos) { 282 prevYPos = nodeRect.y(); 283 result.append('\n'); 284 } 285 286 result.append(nodeValue); 287 } 288 } 289 } 290 291 return result.toString(); 292 } 293 294 } // namespace WebCore 295