@ -208,6 +208,345 @@ var Settings = (function SettingsClosure() {
@@ -208,6 +208,345 @@ var Settings = (function SettingsClosure() {
var cache = new Cache ( kCacheSize ) ;
var currentPageNumber = 1 ;
var PDFFindController = {
startedTextExtraction : false ,
// If active, search resulsts will be highlighted.
active : false ,
// Stores the text for each page.
pageContents : [ ] ,
pageMatches : [ ] ,
selected : {
pageIdx : 0 ,
matchIdx : 0
} ,
dirtyMatch : false ,
findTimeout : null ,
initialize : function ( ) {
var events = [
'find' ,
'findagain' ,
'findhighlightallchange' ,
'findcasesensitivitychange'
] ;
this . handelEvent = this . handelEvent . bind ( this ) ;
for ( var i = 0 ; i < events . length ; i ++ ) {
window . addEventListener ( events [ i ] , this . handelEvent ) ;
}
} ,
calcFindMatch : function ( pageContent ) {
// TODO: Handle the other search options here as well.
var query = this . state . query ;
var queryLen = query . length ;
if ( queryLen === 0 )
return [ ] ;
var matches = [ ] ;
var matchIdx = - queryLen ;
while ( true ) {
matchIdx = pageContent . indexOf ( query , matchIdx + queryLen ) ;
if ( matchIdx === - 1 ) {
break ;
}
matches . push ( matchIdx ) ;
}
return matches ;
} ,
extractText : function ( ) {
if ( this . startedTextExtraction )
return ;
this . startedTextExtraction = true ;
var self = this ;
function extractPageText ( pageIndex ) {
PDFView . pages [ pageIndex ] . getTextContent ( ) . then (
function textContentResolved ( data ) {
// Bulid the search string.
var bidiTexts = data . bidiTexts ;
var str = '' ;
for ( var i = 0 ; i < bidiTexts . length ; i ++ ) {
str += bidiTexts [ i ] . str ;
}
// Store the pageContent as a string.
self . pageContents . push ( str ) ;
// Ensure there is a empty array of matches.
self . pageMatches . push ( [ ] ) ;
if ( ( pageIndex + 1 ) < PDFView . pages . length )
extractPageText ( pageIndex + 1 ) ;
}
) ;
}
extractPageText ( 0 ) ;
} ,
handelEvent : function ( e ) {
this . state = e . detail ;
if ( e . detail . findPrevious === undefined ) {
this . dirtyMatch = true ;
}
clearTimeout ( this . findTimeout ) ;
if ( e . type === 'find' ) {
// Only trigger the find action after 250ms of silence.
this . findTimeout = setTimeout ( this . performFind . bind ( this ) , 250 ) ;
} else {
this . performFind ( ) ;
}
} ,
updatePage : function ( idx ) {
var page = PDFView . pages [ idx ] ;
if ( page . textLayer ) {
page . textLayer . updateMatches ( ) ;
} else if ( this . selected . pageIdx === idx ) {
// If the page is selected, scroll the page into view, which triggers
// rendering the page, which adds the textLayer. Once the textLayer is
// build, it will scroll onto the selected match.
page . scrollIntoView ( ) ;
}
} ,
performFind : function ( ) {
// Recalculate all the matches.
// TODO: Make one match show up as the current match
var pages = PDFView . pages ;
var pageContents = this . pageContents ;
var pageMatches = this . pageMatches ;
this . active = true ;
if ( this . dirtyMatch ) {
// Need to recalculate the matches.
this . dirtyMatch = false ;
this . selected = {
pageIdx : - 1 ,
matchIdx : - 1
} ;
// TODO: Make this way more lasily (aka. efficient) - e.g. calculate only
// the matches for the current visible pages.
var firstMatch = true ;
for ( var i = 0 ; i < pageContents . length ; i ++ ) {
var matches = pageMatches [ i ] = this . calcFindMatch ( pageContents [ i ] ) ;
if ( firstMatch && matches . length !== 0 ) {
firstMatch = false ;
this . selected = {
pageIdx : i ,
matchIdx : 0
} ;
}
this . updatePage ( i , true ) ;
}
} else {
// If there is NO selection, then there is no match at all -> no sense to
// handel previous/next action.
if ( this . selected . pageIdx === - 1 )
return ;
// Handle findAgain case.
var previous = this . state . findPrevious ;
var sPageIdx = this . selected . pageIdx ;
var sMatchIdx = this . selected . matchIdx ;
if ( previous ) {
// Select previous match.
if ( sMatchIdx !== 0 ) {
this . selected . matchIdx -= 1 ;
} else {
var len = pageMatches . length ;
for ( var i = sPageIdx - 1 ; i != sPageIdx ; i -- ) {
if ( i < 0 )
i += len ;
if ( pageMatches [ i ] . length !== 0 ) {
this . selected = {
pageIdx : i ,
matchIdx : pageMatches [ i ] . length - 1
} ;
break ;
}
}
// If pageIdx stayed the same, select last match on the page.
if ( this . selected . pageIdx === sPageIdx ) {
this . selected . matchIdx = pageMatches [ sPageIdx ] . length - 1 ;
}
}
} else {
// Select next match.
if ( pageMatches [ sPageIdx ] . length !== sMatchIdx + 1 ) {
this . selected . matchIdx += 1 ;
} else {
var len = pageMatches . length ;
for ( var i = sPageIdx + 1 ; i < len + sPageIdx ; i ++ ) {
if ( pageMatches [ i % len ] . length !== 0 ) {
this . selected = {
pageIdx : i % len ,
matchIdx : 0
} ;
break ;
}
}
// If pageIdx stayed the same, select last match on the page.
if ( this . selected . pageIdx === sPageIdx ) {
this . selected . matchIdx = 0 ;
}
}
}
this . updatePage ( sPageIdx , sPageIdx === this . selected . pageIdx ) ;
if ( sPageIdx !== this . selected . pageIdx ) {
this . updatePage ( this . selected . pageIdx , true ) ;
}
}
}
} ;
var PDFFindBar = {
// TODO: Enable the FindBar *AFTER* the pagesPromise in the load function
// got resolved
opened : false ,
FIND _FOUND : 0 , // Successful find
FIND _NOTFOUND : 1 , // Unsuccessful find
FIND _WRAPPED : 2 , // Successful find, but wrapped around
initialize : function ( ) {
this . bar = document . getElementById ( 'findbar' ) ;
this . toggleButton = document . getElementById ( 'viewSearch' ) ;
this . findField = document . getElementById ( 'findInput' ) ;
this . highlightAll = document . getElementById ( 'findHighlightAll' ) ;
this . caseSensitive = document . getElementById ( 'findMatchCase' ) ;
this . findMsgWrap = document . getElementById ( 'findMsgWrap' ) ;
this . findMsgNotFound = document . getElementById ( 'findMsgNotFound' ) ;
var self = this ;
this . toggleButton . addEventListener ( 'click' ,
function ( ) {
self . toggle ( ) ;
} ) ;
this . findField . addEventListener ( 'input' , function ( ) {
self . dispatchEvent ( '' ) ;
} ) ;
// TODO: Add keybindings like enter, shift-enter, CMD-G etc. to go to prev/
// next match when the findField is selected.
document . getElementById ( 'findPrevious' ) . addEventListener ( 'click' ,
function ( ) {
self . dispatchEvent ( 'again' , true ) ;
} ) ;
document . getElementById ( 'findNext' ) . addEventListener ( 'click' ,
function ( ) {
self . dispatchEvent ( 'again' , false ) ;
} ) ;
this . highlightAll . addEventListener ( 'click' ,
function ( ) {
self . dispatchEvent ( 'highlightallchange' ) ;
} ) ;
this . caseSensitive . addEventListener ( 'click' ,
function ( ) {
self . dispatchEvent ( 'casesensitivitychange' ) ;
} ) ;
} ,
dispatchEvent : function ( aType , aFindPrevious ) {
var event = document . createEvent ( 'CustomEvent' ) ;
event . initCustomEvent ( 'find' + aType , true , true , {
query : this . findField . value ,
caseSensitive : this . caseSensitive . checked ,
highlightAll : this . highlightAll . checked ,
findPrevious : aFindPrevious
} ) ;
return window . dispatchEvent ( event ) ;
} ,
updateUIState : function ( aState ) {
var notFound = false ;
var wrapped = false ;
switch ( aState ) {
case this . FIND _FOUND :
break ;
case this . FIND _NOTFOUND :
notFound = true ;
break ;
case this . FIND _WRAPPED :
wrapped = true ;
break ;
}
if ( notFound ) {
this . findField . classList . add ( 'notFound' ) ;
this . findMsgNotFound . classList . remove ( 'hidden' ) ;
} else {
this . findField . classList . remove ( 'notFound' ) ;
this . findMsgNotFound . classList . add ( 'hidden' ) ;
}
if ( wrapped ) {
this . findMsgWrap . classList . remove ( 'hidden' ) ;
} else {
this . findMsgWrap . classList . add ( 'hidden' ) ;
}
} ,
open : function ( ) {
if ( this . opened ) return ;
this . opened = true ;
this . toggleButton . classList . add ( 'toggled' ) ;
this . bar . classList . remove ( 'hidden' ) ;
this . findField . select ( ) ;
this . findField . focus ( ) ;
} ,
close : function ( ) {
if ( ! this . opened ) return ;
this . opened = false ;
this . toggleButton . classList . remove ( 'toggled' ) ;
this . bar . classList . add ( 'hidden' ) ;
PDFFindController . active = false ;
} ,
toggle : function ( ) {
if ( this . opened ) {
this . close ( ) ;
} else {
this . open ( ) ;
}
}
} ;
var PDFView = {
pages : [ ] ,
thumbnails : [ ] ,
@ -242,6 +581,9 @@ var PDFView = {
@@ -242,6 +581,9 @@ var PDFView = {
this . watchScroll ( thumbnailContainer , this . thumbnailViewScroll ,
this . renderHighestPriority . bind ( this ) ) ;
PDFFindBar . initialize ( ) ;
PDFFindController . initialize ( ) ;
this . initialized = true ;
container . addEventListener ( 'scroll' , function ( ) {
self . lastScroll = Date . now ( ) ;
@ -736,6 +1078,9 @@ var PDFView = {
@@ -736,6 +1078,9 @@ var PDFView = {
thumbnails . push ( thumbnailView ) ;
var pageRef = page . ref ;
pagesRefMap [ pageRef . num + ' ' + pageRef . gen + ' R' ] = i ;
// Trigger text extraction. TODO: Make this happen lasyliy if needed.
PDFFindController . extractText ( ) ;
}
self . pagesRefMap = pagesRefMap ;
@ -1229,6 +1574,7 @@ var PageView = function pageView(container, pdfPage, id, scale,
@@ -1229,6 +1574,7 @@ var PageView = function pageView(container, pdfPage, id, scale,
this . resume = null ;
this . textContent = null ;
this . textLayer = null ;
var anchor = document . createElement ( 'a' ) ;
anchor . name = '' + this . id ;
@ -1475,7 +1821,8 @@ var PageView = function pageView(container, pdfPage, id, scale,
@@ -1475,7 +1821,8 @@ var PageView = function pageView(container, pdfPage, id, scale,
textLayerDiv . className = 'textLayer' ;
div . appendChild ( textLayerDiv ) ;
}
var textLayer = textLayerDiv ? new TextLayerBuilder ( textLayerDiv ) : null ;
var textLayer = this . textLayer =
textLayerDiv ? new TextLayerBuilder ( textLayerDiv , this . id - 1 ) : null ;
var scale = this . scale , viewport = this . viewport ;
canvas . width = viewport . width ;
@ -1834,21 +2181,25 @@ var CustomStyle = (function CustomStyleClosure() {
@@ -1834,21 +2181,25 @@ var CustomStyle = (function CustomStyleClosure() {
return CustomStyle ;
} ) ( ) ;
var TextLayerBuilder = function textLayerBuilder ( textLayerDiv ) {
var TextLayerBuilder = function textLayerBuilder ( textLayerDiv , pageIdx ) {
var textLayerFrag = document . createDocumentFragment ( ) ;
this . textLayerDiv = textLayerDiv ;
this . layoutDone = false ;
this . divContentDone = false ;
this . pageIdx = pageIdx ;
this . matches = [ ] ;
this . beginLayout = function textLayerBuilderBeginLayout ( ) {
this . textDivs = [ ] ;
this . textLayerQueue = [ ] ;
this . renderingDone = false ;
} ;
this . endLayout = function textLayerBuilderEndLayout ( ) {
this . layoutDone = true ;
this . insertDivContent ( ) ;
} ,
} ;
this . renderLayer = function textLayerBuilderRenderLayer ( ) {
var self = this ;
@ -1862,8 +2213,11 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv) {
@@ -1862,8 +2213,11 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv) {
if ( textDivs . length > 100000 )
return ;
while ( textDivs . length > 0 ) {
var textDiv = textDivs . shift ( ) ;
var i = textDivs . length ;
while ( i !== 0 ) {
i -- ;
var textDiv = textDivs [ i ] ;
textLayerFrag . appendChild ( textDiv ) ;
ctx . font = textDiv . style . fontSize + ' ' + textDiv . style . fontFamily ;
@ -1875,9 +2229,14 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv) {
@@ -1875,9 +2229,14 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv) {
CustomStyle . setProp ( 'transform' , textDiv ,
'scale(' + textScale + ', 1)' ) ;
CustomStyle . setProp ( 'transformOrigin' , textDiv , '0% 0%' ) ;
textLayerDiv . appendChild ( textDiv ) ;
}
}
this . renderingDone = true ;
this . updateMatches ( ) ;
textLayerDiv . appendChild ( textLayerFrag ) ;
} ;
@ -1944,6 +2303,184 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv) {
@@ -1944,6 +2303,184 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv) {
this . textContent = textContent ;
this . insertDivContent ( ) ;
} ;
this . convertMatches = function textLayerBuilderConvertMatches ( matches ) {
var i = 0 ;
var iIndex = 0 ;
var bidiTexts = this . textContent . bidiTexts ;
var end = bidiTexts . length - 1 ;
var queryLen = PDFFindController . state . query . length ;
var lastDivIdx = - 1 ;
var pos ;
var ret = [ ] ;
// Loop over all the matches.
for ( var m = 0 ; m < matches . length ; m ++ ) {
var matchIdx = matches [ m ] ;
// # Calculate the begin position.
// Loop over the divIdxs.
while ( i !== end && matchIdx >= ( iIndex + bidiTexts [ i ] . str . length ) ) {
iIndex += bidiTexts [ i ] . str . length ;
i ++ ;
}
// TODO: Do proper handling here if something goes wrong.
if ( i == bidiTexts . length ) {
console . error ( 'Could not find matching mapping' ) ;
}
var match = {
begin : {
divIdx : i ,
offset : matchIdx - iIndex
}
} ;
// # Calculate the end position.
matchIdx += queryLen ;
// Somewhat same array as above, but use a > instead of >= to get the end
// position right.
while ( i !== end && matchIdx > ( iIndex + bidiTexts [ i ] . str . length ) ) {
iIndex += bidiTexts [ i ] . str . length ;
i ++ ;
}
match . end = {
divIdx : i ,
offset : matchIdx - iIndex
} ;
ret . push ( match ) ;
}
return ret ;
} ;
this . renderMatches = function textLayerBuilder _renderMatches ( matches ) {
var bidiTexts = this . textContent . bidiTexts ;
var textDivs = this . textDivs ;
var prevEnd = null ;
var isSelectedPage = this . pageIdx === PDFFindController . selected . pageIdx ;
var selectedMatchIdx = PDFFindController . selected . matchIdx ;
var infty = {
divIdx : - 1 ,
offset : undefined
} ;
function beginText ( begin , className ) {
var divIdx = begin . divIdx ;
var div = textDivs [ divIdx ] ;
div . innerHTML = '' ;
var content = bidiTexts [ divIdx ] . str . substring ( 0 , begin . offset ) ;
var node = document . createTextNode ( content ) ;
if ( className ) {
var isSelected = isSelectedPage &&
divIdx === selectedMatchIdx ;
var span = document . createElement ( 'span' ) ;
span . className = className + ( isSelected ? ' selected' : '' ) ;
span . appendChild ( node ) ;
div . appendChild ( span ) ;
return ;
}
div . appendChild ( node ) ;
}
function appendText ( from , to , className ) {
var divIdx = from . divIdx ;
var div = textDivs [ divIdx ] ;
var content = bidiTexts [ divIdx ] . str . substring ( from . offset , to . offset ) ;
var node = document . createTextNode ( content ) ;
if ( className ) {
var span = document . createElement ( 'span' ) ;
span . className = className ;
span . appendChild ( node ) ;
div . appendChild ( span ) ;
return ;
}
div . appendChild ( node ) ;
}
function highlightDiv ( divIdx , className ) {
textDivs [ divIdx ] . className = className ;
}
for ( var i = 0 ; i < matches . length ; i ++ ) {
var match = matches [ i ] ;
var begin = match . begin ;
var end = match . end ;
var isSelected = isSelectedPage && i === selectedMatchIdx ;
var highlightSuffix = ( isSelected ? ' selected' : '' ) ;
if ( isSelected )
scrollIntoView ( textDivs [ begin . divIdx ] , { top : - 50 } ) ;
// Match inside new div.
if ( ! prevEnd || begin . divIdx !== prevEnd . divIdx ) {
// If there was a previous div, then add the text at the end
if ( prevEnd !== null ) {
appendText ( prevEnd , infty ) ;
}
// clears the divs and set the content until the begin point.
beginText ( begin ) ;
} else {
appendText ( prevEnd , begin ) ;
}
if ( begin . divIdx === end . divIdx ) {
appendText ( begin , end , 'highlight' + highlightSuffix ) ;
} else {
appendText ( begin , infty , 'highlight begin' + highlightSuffix ) ;
for ( var n = begin . divIdx + 1 ; n < end . divIdx ; n ++ ) {
highlightDiv ( n , 'highlight middle' + highlightSuffix ) ;
}
beginText ( end , 'highlight end' + highlightSuffix ) ;
}
prevEnd = end ;
}
if ( prevEnd ) {
appendText ( prevEnd , infty ) ;
}
} ;
this . updateMatches = function textLayerUpdateMatches ( ) {
// Only show matches, once all rendering is done.
if ( ! this . renderingDone )
return ;
// Clear out all matches.
var matches = this . matches ;
var textDivs = this . textDivs ;
var bidiTexts = this . textContent . bidiTexts ;
var clearedUntilDivIdx = - 1 ;
for ( var i = 0 ; i < matches . length ; i ++ ) {
var match = matches [ i ] ;
var begin = Math . max ( clearedUntilDivIdx , match . begin . divIdx ) ;
for ( var n = begin ; n <= match . end . divIdx ; n ++ ) {
var div = bidiTexts [ n ] . str ;
div . textContent = div . textContent ;
div . className = '' ;
}
clearedUntilDivIdx = match . end . divIdx + 1 ;
}
if ( ! PDFFindController . active )
return ;
// Convert the matches on the page controller into the match format used
// for the textLayer.
this . matches = matches =
this . convertMatches ( PDFFindController . pageMatches [ this . pageIdx ] || [ ] ) ;
this . renderMatches ( this . matches ) ;
} ;
} ;
document . addEventListener ( 'DOMContentLoaded' , function webViewerLoad ( evt ) {
@ -2059,16 +2596,6 @@ document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) {
@@ -2059,16 +2596,6 @@ document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) {
PDFView . switchSidebarView ( 'outline' ) ;
} ) ;
document . getElementById ( 'viewSearch' ) . addEventListener ( 'click' ,
function ( ) {
PDFView . switchSidebarView ( 'search' ) ;
} ) ;
document . getElementById ( 'searchButton' ) . addEventListener ( 'click' ,
function ( ) {
PDFView . search ( ) ;
} ) ;
document . getElementById ( 'previous' ) . addEventListener ( 'click' ,
function ( ) {
PDFView . page -- ;
@ -2331,6 +2858,12 @@ window.addEventListener('keydown', function keydown(evt) {
@@ -2331,6 +2858,12 @@ window.addEventListener('keydown', function keydown(evt) {
// control is selected or not.
if ( cmd == 1 || cmd == 8 ) { // either CTRL or META key.
switch ( evt . keyCode ) {
//#if !(FIREFOX || MOZCENTRAL)
case 70 :
PDFFindBar . toggle ( ) ;
handled = true ;
break ;
//#endif
case 61 : // FF/Mac '='
case 107 : // FF '+' and '='
case 187 : // Chrome '+'