1/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2/* Copyright 2012 Mozilla Foundation 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16/* globals CustomStyle, PDFFindController, scrollIntoView, PDFJS */ 17 18'use strict'; 19 20var FIND_SCROLL_OFFSET_TOP = -50; 21var FIND_SCROLL_OFFSET_LEFT = -400; 22 23/** 24 * TextLayerBuilder provides text-selection 25 * functionality for the PDF. It does this 26 * by creating overlay divs over the PDF 27 * text. This divs contain text that matches 28 * the PDF text they are overlaying. This 29 * object also provides for a way to highlight 30 * text that is being searched for. 31 */ 32var TextLayerBuilder = function textLayerBuilder(options) { 33 var textLayerFrag = document.createDocumentFragment(); 34 35 this.textLayerDiv = options.textLayerDiv; 36 this.layoutDone = false; 37 this.divContentDone = false; 38 this.pageIdx = options.pageIndex; 39 this.matches = []; 40 this.lastScrollSource = options.lastScrollSource; 41 this.viewport = options.viewport; 42 this.isViewerInPresentationMode = options.isViewerInPresentationMode; 43 this.textDivs = []; 44 45 if (typeof PDFFindController === 'undefined') { 46 window.PDFFindController = null; 47 } 48 49 if (typeof this.lastScrollSource === 'undefined') { 50 this.lastScrollSource = null; 51 } 52 53 this.renderLayer = function textLayerBuilderRenderLayer() { 54 var textDivs = this.textDivs; 55 var canvas = document.createElement('canvas'); 56 var ctx = canvas.getContext('2d'); 57 58 // No point in rendering so many divs as it'd make the browser unusable 59 // even after the divs are rendered 60 var MAX_TEXT_DIVS_TO_RENDER = 100000; 61 if (textDivs.length > MAX_TEXT_DIVS_TO_RENDER) { 62 return; 63 } 64 65 for (var i = 0, ii = textDivs.length; i < ii; i++) { 66 var textDiv = textDivs[i]; 67 if ('isWhitespace' in textDiv.dataset) { 68 continue; 69 } 70 71 ctx.font = textDiv.style.fontSize + ' ' + textDiv.style.fontFamily; 72 var width = ctx.measureText(textDiv.textContent).width; 73 74 if (width > 0) { 75 textLayerFrag.appendChild(textDiv); 76 var textScale = textDiv.dataset.canvasWidth / width; 77 var rotation = textDiv.dataset.angle; 78 var transform = 'scale(' + textScale + ', 1)'; 79 transform = 'rotate(' + rotation + 'deg) ' + transform; 80 CustomStyle.setProp('transform' , textDiv, transform); 81 CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%'); 82 } 83 } 84 85 this.textLayerDiv.appendChild(textLayerFrag); 86 this.renderingDone = true; 87 this.updateMatches(); 88 }; 89 90 this.setupRenderLayoutTimer = function textLayerSetupRenderLayoutTimer() { 91 // Schedule renderLayout() if user has been scrolling, otherwise 92 // run it right away 93 var RENDER_DELAY = 200; // in ms 94 var self = this; 95 var lastScroll = (this.lastScrollSource === null ? 96 0 : this.lastScrollSource.lastScroll); 97 98 if (Date.now() - lastScroll > RENDER_DELAY) { 99 // Render right away 100 this.renderLayer(); 101 } else { 102 // Schedule 103 if (this.renderTimer) { 104 clearTimeout(this.renderTimer); 105 } 106 this.renderTimer = setTimeout(function() { 107 self.setupRenderLayoutTimer(); 108 }, RENDER_DELAY); 109 } 110 }; 111 112 this.appendText = function textLayerBuilderAppendText(geom, styles) { 113 var style = styles[geom.fontName]; 114 var textDiv = document.createElement('div'); 115 this.textDivs.push(textDiv); 116 if (!/\S/.test(geom.str)) { 117 textDiv.dataset.isWhitespace = true; 118 return; 119 } 120 var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform); 121 var angle = Math.atan2(tx[1], tx[0]); 122 if (style.vertical) { 123 angle += Math.PI / 2; 124 } 125 var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])); 126 var fontAscent = (style.ascent ? style.ascent * fontHeight :
127 (style.descent ? (1 + style.descent) * fontHeight : fontHeight)); 128 129 textDiv.style.position = 'absolute'; 130 textDiv.style.left = (tx[4] + (fontAscent * Math.sin(angle))) + 'px'; 131 textDiv.style.top = (tx[5] - (fontAscent * Math.cos(angle))) + 'px'; 132 textDiv.style.fontSize = fontHeight + 'px'; 133 textDiv.style.fontFamily = style.fontFamily; 134 135 textDiv.textContent = geom.str; 136 textDiv.dataset.fontName = geom.fontName; 137 textDiv.dataset.angle = angle * (180 / Math.PI); 138 if (style.vertical) { 139 textDiv.dataset.canvasWidth = geom.height * this.viewport.scale; 140 } else { 141 textDiv.dataset.canvasWidth = geom.width * this.viewport.scale; 142 } 143 144 }; 145 146 this.setTextContent = function textLayerBuilderSetTextContent(textContent) { 147 this.textContent = textContent; 148 149 var textItems = textContent.items; 150 for (var i = 0; i < textItems.length; i++) { 151 this.appendText(textItems[i], textContent.styles); 152 } 153 this.divContentDone = true; 154 155 this.setupRenderLayoutTimer(); 156 }; 157 158 this.convertMatches = function textLayerBuilderConvertMatches(matches) { 159 var i = 0; 160 var iIndex = 0; 161 var bidiTexts = this.textContent.items; 162 var end = bidiTexts.length - 1; 163 var queryLen = (PDFFindController === null ? 164 0 : PDFFindController.state.query.length); 165 166 var lastDivIdx = -1; 167 var pos; 168 169 var ret = []; 170 171 // Loop over all the matches. 172 for (var m = 0; m < matches.length; m++) { 173 var matchIdx = matches[m]; 174 // # Calculate the begin position. 175 176 // Loop over the divIdxs. 177 while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { 178 iIndex += bidiTexts[i].str.length; 179 i++; 180 } 181 182 // TODO: Do proper handling here if something goes wrong. 183 if (i == bidiTexts.length) { 184 console.error('Could not find matching mapping'); 185 } 186 187 var match = { 188 begin: { 189 divIdx: i,
190 offset: matchIdx - iIndex 191 } 192 }; 193 194 // # Calculate the end position. 195 matchIdx += queryLen; 196 197 // Somewhat same array as above, but use a > instead of >= to get the end 198 // position right. 199 while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { 200 iIndex += bidiTexts[i].str.length; 201 i++; 202 } 203 204 match.end = { 205 divIdx: i, 206 offset: matchIdx - iIndex 207 }; 208 ret.push(match); 209 } 210 211 return ret; 212 }; 213 214 this.renderMatches = function textLayerBuilder_renderMatches(matches) { 215 // Early exit if there is nothing to render. 216 if (matches.length === 0) { 217 return; 218 } 219 220 var bidiTexts = this.textContent.items; 221 var textDivs = this.textDivs; 222 var prevEnd = null; 223 var isSelectedPage = (PDFFindController === null ? 224 false : (this.pageIdx === PDFFindController.selected.pageIdx)); 225 226 var selectedMatchIdx = (PDFFindController === null ? 227 -1 : PDFFindController.selected.matchIdx); 228 229 var highlightAll = (PDFFindController === null ? 230 false : PDFFindController.state.highlightAll); 231 232 var infty = { 233 divIdx: -1, 234 offset: undefined 235 }; 236 237 function beginText(begin, className) { 238 var divIdx = begin.divIdx; 239 var div = textDivs[divIdx]; 240 div.textContent = ''; 241 appendTextToDiv(divIdx, 0, begin.offset, className); 242 } 243 244 function appendText(from, to, className) { 245 appendTextToDiv(from.divIdx, from.offset, to.offset, className); 246 } 247 248 function appendTextToDiv(divIdx, fromOffset, toOffset, className) { 249 var div = textDivs[divIdx]; 250 251 var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset); 252 var node = document.createTextNode(content); 253 if (className) { 254 var span = document.createElement('span'); 255 span.className = className; 256 span.appendChild(node); 257 div.appendChild(span); 258 return; 259 } 260 div.appendChild(node); 261 } 262 263 function highlightDiv(divIdx, className) { 264 textDivs[divIdx].className = className; 265 } 266 267 var i0 = selectedMatchIdx, i1 = i0 + 1, i; 268 269 if (highlightAll) { 270 i0 = 0; 271 i1 = matches.length; 272 } else if (!isSelectedPage) { 273 // Not highlighting all and this isn't the selected page, so do nothing. 274 return; 275 } 276 277 for (i = i0; i < i1; i++) { 278 var match = matches[i]; 279 var begin = match.begin; 280 var end = match.end; 281 282 var isSelected = isSelectedPage && i === selectedMatchIdx; 283 var highlightSuffix = (isSelected ? ' selected' : ''); 284 if (isSelected && !this.isViewerInPresentationMode) { 285 scrollIntoView(textDivs[begin.divIdx], { top: FIND_SCROLL_OFFSET_TOP, 286 left: FIND_SCROLL_OFFSET_LEFT }); 287 } 288 289 // Match inside new div. 290 if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { 291 // If there was a previous div, then add the text at the end 292 if (prevEnd !== null) { 293 appendText(prevEnd, infty); 294 } 295 // clears the divs and set the content until the begin point. 296 beginText(begin); 297 } else { 298 appendText(prevEnd, begin); 299 } 300 301 if (begin.divIdx === end.divIdx) { 302 appendText(begin, end, 'highlight' + highlightSuffix); 303 } else { 304 appendText(begin, infty, 'highlight begin' + highlightSuffix); 305 for (var n = begin.divIdx + 1; n < end.divIdx; n++) { 306 highlightDiv(n, 'highlight middle' + highlightSuffix); 307 } 308 beginText(end, 'highlight end' + highlightSuffix); 309 } 310 prevEnd = end; 311 } 312 313 if (prevEnd) { 314 appendText(prevEnd, infty); 315 } 316 }; 317 318 this.updateMatches = function textLayerUpdateMatches() { 319 // Only show matches, once all rendering is done. 320 if (!this.renderingDone) { 321 return; 322 } 323 324 // Clear out all matches. 325 var matches = this.matches; 326 var textDivs = this.textDivs; 327 var bidiTexts = this.textContent.items; 328 var clearedUntilDivIdx = -1; 329 330 // Clear out all current matches. 331 for (var i = 0; i < matches.length; i++) { 332 var match = matches[i]; 333 var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); 334 for (var n = begin; n <= match.end.divIdx; n++) { 335 var div = textDivs[n]; 336 div.textContent = bidiTexts[n].str; 337 div.className = ''; 338 } 339 clearedUntilDivIdx = match.end.divIdx + 1; 340 } 341 342 if (PDFFindController === null || !PDFFindController.active) { 343 return; 344 } 345 346 // Convert the matches on the page controller into the match format used 347 // for the textLayer. 348 this.matches = matches = (this.convertMatches(PDFFindController === null ? 349 [] : (PDFFindController.pageMatches[this.pageIdx] || []))); 350 351 this.renderMatches(this.matches); 352 }; 353}; 354
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.